@fayz-ai/app-builder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/admin/AdminAppBuilder.d.ts +11 -0
  4. package/dist/admin/AdminAppBuilder.d.ts.map +1 -0
  5. package/dist/admin/agent-surface.d.ts +18 -0
  6. package/dist/admin/agent-surface.d.ts.map +1 -0
  7. package/dist/admin/options.d.ts +22 -0
  8. package/dist/admin/options.d.ts.map +1 -0
  9. package/dist/admin/target.d.ts +20 -0
  10. package/dist/admin/target.d.ts.map +1 -0
  11. package/dist/chunk-45SFS6FV.js +721 -0
  12. package/dist/chunk-45SFS6FV.js.map +1 -0
  13. package/dist/components/AppBuilder.d.ts +12 -0
  14. package/dist/components/AppBuilder.d.ts.map +1 -0
  15. package/dist/components/BuilderCanvas.d.ts +9 -0
  16. package/dist/components/BuilderCanvas.d.ts.map +1 -0
  17. package/dist/components/BuilderChrome.d.ts +25 -0
  18. package/dist/components/BuilderChrome.d.ts.map +1 -0
  19. package/dist/components/BuilderEmpty.d.ts +7 -0
  20. package/dist/components/BuilderEmpty.d.ts.map +1 -0
  21. package/dist/components/PagePicker.d.ts +20 -0
  22. package/dist/components/PagePicker.d.ts.map +1 -0
  23. package/dist/components/PublishButton.d.ts +9 -0
  24. package/dist/components/PublishButton.d.ts.map +1 -0
  25. package/dist/hooks/useBuilderFrame.d.ts +22 -0
  26. package/dist/hooks/useBuilderFrame.d.ts.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/lib/bridge.d.ts +38 -0
  32. package/dist/lib/bridge.d.ts.map +1 -0
  33. package/dist/lib/cn.d.ts +4 -0
  34. package/dist/lib/cn.d.ts.map +1 -0
  35. package/dist/lib/pages.d.ts +29 -0
  36. package/dist/lib/pages.d.ts.map +1 -0
  37. package/dist/lib/url.d.ts +9 -0
  38. package/dist/lib/url.d.ts.map +1 -0
  39. package/dist/plugin.d.ts +12 -0
  40. package/dist/plugin.d.ts.map +1 -0
  41. package/dist/plugin.js +277 -0
  42. package/dist/plugin.js.map +1 -0
  43. package/dist/types.d.ts +107 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/package.json +71 -0
@@ -0,0 +1,721 @@
1
+ import { Loader2, Check, ChevronsUpDown, ChevronLeft, Search, Folder, ChevronRight, House, FileText, Eye, Code2, Monitor, Smartphone, ExternalLink, RotateCw, AlertTriangle, Globe } from 'lucide-react';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import * as React3 from 'react';
4
+
5
+ // src/components/PublishButton.tsx
6
+ function PublishButton({
7
+ publish,
8
+ labels
9
+ }) {
10
+ const label = labels?.publish ?? "Publicar";
11
+ const state = publish?.state ?? "idle";
12
+ const busy = state === "publishing";
13
+ const disabled = !publish || busy;
14
+ return /* @__PURE__ */ jsxs(
15
+ "button",
16
+ {
17
+ type: "button",
18
+ onClick: () => publish?.publish(),
19
+ disabled,
20
+ title: publish ? publish.error ?? label : labels?.publishUnavailable ?? "Publicar ainda n\xE3o est\xE1 ligado neste ambiente",
21
+ className: "flex h-9 items-center gap-1.5 rounded-full bg-primary px-4 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-40",
22
+ children: [
23
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : null,
24
+ state === "published" && !busy ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : null,
25
+ label
26
+ ]
27
+ }
28
+ );
29
+ }
30
+
31
+ // src/lib/pages.ts
32
+ var HOME_LABELS = { "/": "In\xEDcio" };
33
+ function normalizeRoute(route) {
34
+ const clean = route.split("?")[0].replace(/^#/, "");
35
+ return clean.startsWith("/") ? clean : `/${clean}`;
36
+ }
37
+ function isHomeRoute(route) {
38
+ return normalizeRoute(route) === "/";
39
+ }
40
+ var KNOWN = {
41
+ produtos: "Produtos",
42
+ products: "Produtos",
43
+ carrinho: "Carrinho",
44
+ cart: "Carrinho",
45
+ checkout: "Checkout",
46
+ contato: "Contato",
47
+ contact: "Contato",
48
+ sobre: "Sobre",
49
+ about: "Sobre",
50
+ blog: "Blog",
51
+ colecoes: "Cole\xE7\xF5es",
52
+ collections: "Cole\xE7\xF5es",
53
+ cardapio: "Card\xE1pio",
54
+ conta: "Conta",
55
+ account: "Conta",
56
+ pedidos: "Pedidos",
57
+ catalog: "Cat\xE1logo",
58
+ catalogo: "Cat\xE1logo",
59
+ privacy: "Privacidade",
60
+ terms: "Termos",
61
+ refunds: "Reembolsos",
62
+ receitas: "Receitas",
63
+ product: "Produtos",
64
+ collection: "Cole\xE7\xF5es"
65
+ };
66
+ function labelForRoute(route) {
67
+ const clean = normalizeRoute(route);
68
+ if (HOME_LABELS[clean]) return HOME_LABELS[clean];
69
+ const segments = clean.split("/").filter(Boolean);
70
+ if (!segments.length) return HOME_LABELS["/"];
71
+ return segments.map((segment) => {
72
+ const key = segment.toLowerCase();
73
+ if (KNOWN[key]) return KNOWN[key];
74
+ const words = segment.replace(/^:/, "").replace(/[-_]+/g, " ");
75
+ return words.charAt(0).toUpperCase() + words.slice(1);
76
+ }).join(" \u203A ");
77
+ }
78
+ var GROUP_FROM = 3;
79
+ function toPageGroups(routes) {
80
+ const bySegment = /* @__PURE__ */ new Map();
81
+ for (const route of routes) {
82
+ const segment = normalizeRoute(route).split("/").filter(Boolean)[0] ?? "";
83
+ const list = bySegment.get(segment) ?? [];
84
+ list.push({ route, label: labelForRoute(route) });
85
+ bySegment.set(segment, list);
86
+ }
87
+ const groups = [];
88
+ for (const [segment, pages] of bySegment) {
89
+ if (pages.length < GROUP_FROM || !segment) {
90
+ groups.push(...pages.map((page) => ({ key: page.route, label: page.label, route: page.route, children: [] })));
91
+ continue;
92
+ }
93
+ groups.push({
94
+ key: segment,
95
+ label: labelForRoute(`/${segment}`),
96
+ children: pages.map((page) => ({
97
+ route: page.route,
98
+ // Inside the folder the section name is the folder's, not each row's.
99
+ label: page.label.split(" \u203A ").slice(1).join(" \u203A ") || page.label
100
+ }))
101
+ });
102
+ }
103
+ const rank = (g) => g.route && isHomeRoute(g.route) ? 0 : 1;
104
+ return groups.sort((a, b) => rank(a) - rank(b) || a.label.localeCompare(b.label));
105
+ }
106
+ function toPages(routes) {
107
+ return routes.map((route) => ({ route, label: labelForRoute(route) }));
108
+ }
109
+
110
+ // src/lib/cn.ts
111
+ function cn(...parts) {
112
+ return parts.filter(Boolean).join(" ");
113
+ }
114
+ function PagePicker({
115
+ routes,
116
+ currentPath,
117
+ onNavigate,
118
+ fallback,
119
+ leading,
120
+ searchLabel = "Buscar p\xE1gina",
121
+ emptyLabel = "Nenhuma p\xE1gina com esse nome"
122
+ }) {
123
+ const [open, setOpen] = React3.useState(false);
124
+ const [query, setQuery] = React3.useState("");
125
+ const boxRef = React3.useRef(null);
126
+ const inputRef = React3.useRef(null);
127
+ const [section, setSection] = React3.useState(null);
128
+ const pages = React3.useMemo(() => toPages(routes), [routes]);
129
+ const groups = React3.useMemo(() => toPageGroups(routes), [routes]);
130
+ const openSection = React3.useMemo(() => groups.find((g) => g.key === section) ?? null, [groups, section]);
131
+ const matches = React3.useMemo(() => {
132
+ const q = query.trim().toLowerCase();
133
+ if (q) return pages.filter((p) => p.label.toLowerCase().includes(q) || p.route.toLowerCase().includes(q));
134
+ if (openSection) return openSection.children;
135
+ return null;
136
+ }, [pages, query, openSection]);
137
+ React3.useEffect(() => {
138
+ if (!open) return;
139
+ const onDown = (event) => {
140
+ if (!boxRef.current?.contains(event.target)) setOpen(false);
141
+ };
142
+ const onKey = (event) => {
143
+ if (event.key === "Escape") setOpen(false);
144
+ };
145
+ document.addEventListener("mousedown", onDown);
146
+ document.addEventListener("keydown", onKey);
147
+ inputRef.current?.focus();
148
+ return () => {
149
+ document.removeEventListener("mousedown", onDown);
150
+ document.removeEventListener("keydown", onKey);
151
+ };
152
+ }, [open]);
153
+ const label = pages.length ? labelForRoute(currentPath) : fallback;
154
+ if (!pages.length) {
155
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-9 min-w-0 flex-1 items-center gap-2 rounded-full bg-muted/60 px-2", children: [
156
+ leading,
157
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: label })
158
+ ] });
159
+ }
160
+ return /* @__PURE__ */ jsxs("div", { ref: boxRef, className: "relative flex h-9 min-w-0 flex-1 items-center gap-2 rounded-full bg-muted/60 px-2", children: [
161
+ leading,
162
+ /* @__PURE__ */ jsxs(
163
+ "button",
164
+ {
165
+ type: "button",
166
+ onClick: () => {
167
+ setQuery("");
168
+ setSection(null);
169
+ setOpen((v) => !v);
170
+ },
171
+ "aria-expanded": open,
172
+ "aria-haspopup": "listbox",
173
+ className: "flex min-w-0 flex-1 items-center gap-1.5 rounded-full py-1 text-left text-xs font-medium text-foreground",
174
+ children: [
175
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: label }),
176
+ /* @__PURE__ */ jsx(ChevronsUpDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })
177
+ ]
178
+ }
179
+ ),
180
+ open ? /* @__PURE__ */ jsxs(
181
+ "div",
182
+ {
183
+ role: "listbox",
184
+ className: "absolute left-0 top-[calc(100%+6px)] z-50 w-[min(22rem,100%)] overflow-hidden rounded-xl border border-border bg-card shadow-lg",
185
+ children: [
186
+ openSection && !query ? /* @__PURE__ */ jsxs(
187
+ "button",
188
+ {
189
+ type: "button",
190
+ onClick: () => setSection(null),
191
+ className: "flex w-full items-center gap-2 border-b border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-muted/60",
192
+ children: [
193
+ /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
194
+ openSection.label
195
+ ]
196
+ }
197
+ ) : null,
198
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3 py-2", children: [
199
+ /* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
200
+ /* @__PURE__ */ jsx(
201
+ "input",
202
+ {
203
+ ref: inputRef,
204
+ value: query,
205
+ onChange: (event) => setQuery(event.target.value),
206
+ placeholder: searchLabel,
207
+ "aria-label": searchLabel,
208
+ className: "w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
209
+ }
210
+ )
211
+ ] }),
212
+ /* @__PURE__ */ jsx("div", { className: "max-h-72 overflow-y-auto p-1", children: !matches ? groups.map((group) => {
213
+ if (!group.route) {
214
+ return /* @__PURE__ */ jsxs(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ onClick: () => setSection(group.key),
219
+ className: "flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm text-foreground transition-colors hover:bg-muted/60",
220
+ children: [
221
+ /* @__PURE__ */ jsx(Folder, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
222
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: group.label }),
223
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: group.children.length }),
224
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground" })
225
+ ]
226
+ },
227
+ group.key
228
+ );
229
+ }
230
+ const active = normalizeRoute(group.route) === normalizeRoute(currentPath);
231
+ const Icon = isHomeRoute(group.route) ? House : FileText;
232
+ return /* @__PURE__ */ jsxs(
233
+ "button",
234
+ {
235
+ type: "button",
236
+ role: "option",
237
+ "aria-selected": active,
238
+ onClick: () => {
239
+ onNavigate(group.route);
240
+ setOpen(false);
241
+ },
242
+ className: cn(
243
+ "flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm transition-colors",
244
+ active ? "bg-muted font-medium text-foreground" : "text-foreground hover:bg-muted/60"
245
+ ),
246
+ children: [
247
+ /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
248
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: group.label }),
249
+ active ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0 text-muted-foreground" }) : null
250
+ ]
251
+ },
252
+ group.key
253
+ );
254
+ }) : matches.length ? matches.map((page) => {
255
+ const active = normalizeRoute(page.route) === normalizeRoute(currentPath);
256
+ const Icon = isHomeRoute(page.route) ? House : FileText;
257
+ return /* @__PURE__ */ jsxs(
258
+ "button",
259
+ {
260
+ type: "button",
261
+ role: "option",
262
+ "aria-selected": active,
263
+ onClick: () => {
264
+ onNavigate(page.route);
265
+ setOpen(false);
266
+ },
267
+ className: cn(
268
+ "flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm transition-colors",
269
+ active ? "bg-muted font-medium text-foreground" : "text-foreground hover:bg-muted/60"
270
+ ),
271
+ children: [
272
+ /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
273
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: page.label }),
274
+ active ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0 text-muted-foreground" }) : null
275
+ ]
276
+ },
277
+ page.route
278
+ );
279
+ }) : /* @__PURE__ */ jsx("p", { className: "px-2.5 py-6 text-center text-xs text-muted-foreground", children: emptyLabel }) })
280
+ ]
281
+ }
282
+ ) : null
283
+ ] });
284
+ }
285
+ var iconButton = "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-foreground/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40";
286
+ function addressOf(url, path) {
287
+ if (!url) return path;
288
+ try {
289
+ const parsed = new URL(url);
290
+ const base = parsed.host + parsed.pathname.replace(/\/+$/, "");
291
+ return path === "/" ? base + "/" : base + path;
292
+ } catch {
293
+ return path;
294
+ }
295
+ }
296
+ function TrafficLights({
297
+ onClose,
298
+ onToggleChat,
299
+ onToggleWide,
300
+ labels
301
+ }) {
302
+ const lights = [
303
+ { id: "close", color: "bg-[#ff5f57]", label: labels?.close ?? "Sair", onClick: onClose },
304
+ { id: "minimize", color: "bg-[#febc2e]", label: labels?.toggleChat ?? "Recolher a conversa", onClick: onToggleChat },
305
+ { id: "zoom", color: "bg-[#28c840]", label: labels?.toggleWide ?? "Dar a tela toda ao app", onClick: onToggleWide }
306
+ ].filter((light) => !!light.onClick);
307
+ if (!lights.length) return null;
308
+ return /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1.5 pl-1", children: lights.map((light) => /* @__PURE__ */ jsx(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ title: light.label,
313
+ "aria-label": light.label,
314
+ onClick: light.onClick,
315
+ className: "flex h-4 w-4 items-center justify-center rounded-full transition-transform hover:scale-125 active:scale-95",
316
+ children: /* @__PURE__ */ jsx("span", { className: cn("h-3 w-3 rounded-full", light.color) })
317
+ },
318
+ light.id
319
+ )) });
320
+ }
321
+ function BuilderChrome({
322
+ target,
323
+ currentPath,
324
+ routes,
325
+ onNavigate,
326
+ viewport,
327
+ onViewportChange,
328
+ onReload,
329
+ mode,
330
+ onModeChange,
331
+ publish,
332
+ onClose,
333
+ onToggleChat,
334
+ onToggleWide,
335
+ chromeEnd,
336
+ labels
337
+ }) {
338
+ const address = addressOf(target.url, currentPath);
339
+ const openTarget = target.liveUrl ?? target.url;
340
+ const Device = viewport === "desktop" ? Monitor : Smartphone;
341
+ const deviceLabel = viewport === "desktop" ? "Ver no celular" : "Ver no computador";
342
+ return (
343
+ // Same 56px as the companion column's header (RightRail), so the two read
344
+ // as one window bar rather than two rows that nearly line up.
345
+ /* @__PURE__ */ jsxs("div", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border px-3", children: [
346
+ viewport === "desktop" ? /* @__PURE__ */ jsx(
347
+ TrafficLights,
348
+ {
349
+ onClose,
350
+ onToggleChat,
351
+ onToggleWide,
352
+ labels
353
+ }
354
+ ) : null,
355
+ onModeChange ? /* @__PURE__ */ jsx("div", { className: "ml-1 flex shrink-0 items-center gap-0.5 rounded-full bg-muted/60 p-0.5", children: [
356
+ { id: "preview", icon: Eye, title: labels?.preview ?? "Visual" },
357
+ { id: "code", icon: Code2, title: labels?.code ?? "C\xF3digo" }
358
+ ].map(({ id, icon: Icon, title }) => /* @__PURE__ */ jsxs(
359
+ "button",
360
+ {
361
+ type: "button",
362
+ title,
363
+ "aria-label": title,
364
+ "aria-pressed": mode === id,
365
+ onClick: () => onModeChange(id),
366
+ className: cn(
367
+ "flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors",
368
+ mode === id ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
369
+ ),
370
+ children: [
371
+ /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
372
+ title
373
+ ]
374
+ },
375
+ id
376
+ )) }) : null,
377
+ /* @__PURE__ */ jsx(
378
+ PagePicker,
379
+ {
380
+ routes,
381
+ currentPath,
382
+ onNavigate,
383
+ fallback: address,
384
+ searchLabel: labels?.searchPage,
385
+ emptyLabel: labels?.noPage,
386
+ leading: /* @__PURE__ */ jsx(
387
+ "button",
388
+ {
389
+ type: "button",
390
+ title: deviceLabel,
391
+ "aria-label": deviceLabel,
392
+ onClick: () => onViewportChange(viewport === "desktop" ? "mobile" : "desktop"),
393
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-foreground/10 hover:text-foreground",
394
+ children: /* @__PURE__ */ jsx(Device, { className: "h-4 w-4" })
395
+ }
396
+ )
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsx(
400
+ "button",
401
+ {
402
+ type: "button",
403
+ title: labels?.viewLive ?? "Abrir em uma aba",
404
+ "aria-label": labels?.viewLive ?? "Abrir em uma aba",
405
+ disabled: !openTarget,
406
+ onClick: () => openTarget && window.open(openTarget, "_blank", "noopener,noreferrer"),
407
+ className: iconButton,
408
+ children: /* @__PURE__ */ jsx(ExternalLink, { className: "h-4 w-4" })
409
+ }
410
+ ),
411
+ /* @__PURE__ */ jsx(
412
+ "button",
413
+ {
414
+ type: "button",
415
+ title: labels?.reload ?? "Recarregar",
416
+ "aria-label": labels?.reload ?? "Recarregar",
417
+ onClick: onReload,
418
+ className: iconButton,
419
+ children: /* @__PURE__ */ jsx(RotateCw, { className: "h-4 w-4" })
420
+ }
421
+ ),
422
+ chromeEnd,
423
+ /* @__PURE__ */ jsx(PublishButton, { publish, labels })
424
+ ] })
425
+ );
426
+ }
427
+ var FRAME = "h-full w-full";
428
+ function BuilderCanvas({
429
+ target,
430
+ frame,
431
+ labels
432
+ }) {
433
+ const [dragging, setDragging] = React3.useState(false);
434
+ React3.useEffect(() => {
435
+ const down = (event) => {
436
+ const el = event.target;
437
+ if (el?.closest('[role="separator"]')) setDragging(true);
438
+ };
439
+ const up = () => setDragging(false);
440
+ window.addEventListener("pointerdown", down, true);
441
+ window.addEventListener("pointerup", up, true);
442
+ return () => {
443
+ window.removeEventListener("pointerdown", down, true);
444
+ window.removeEventListener("pointerup", up, true);
445
+ };
446
+ }, []);
447
+ if (!frame.src) return null;
448
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1 justify-center overflow-hidden", children: [
449
+ /* @__PURE__ */ jsx(
450
+ "iframe",
451
+ {
452
+ ref: frame.frameRef,
453
+ src: frame.src,
454
+ title: target.name,
455
+ onLoad: frame.onLoad,
456
+ className: cn("border-0 bg-white", FRAME, dragging && "pointer-events-none"),
457
+ sandbox: "allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
458
+ },
459
+ `${frame.src}#${frame.nonce}`
460
+ ),
461
+ frame.loading && !frame.failed && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }),
462
+ frame.failed && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2 bg-background/95 p-6 text-center", children: [
463
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-6 w-6 text-muted-foreground" }),
464
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: labels?.loadFailed ?? "N\xE3o foi poss\xEDvel carregar o app aqui dentro." }),
465
+ /* @__PURE__ */ jsx("p", { className: "max-w-sm text-xs text-muted-foreground", children: labels?.loadFailedHint ?? "O servidor pode estar bloqueando a exibi\xE7\xE3o em outra p\xE1gina. Abra em uma nova aba enquanto isso." }),
466
+ /* @__PURE__ */ jsx(
467
+ "button",
468
+ {
469
+ type: "button",
470
+ onClick: () => frame.src && window.open(frame.src, "_blank", "noopener,noreferrer"),
471
+ className: "mt-1 rounded-full border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted",
472
+ children: labels?.openInTab ?? "Abrir em nova aba"
473
+ }
474
+ )
475
+ ] })
476
+ ] });
477
+ }
478
+ function BuilderEmpty({ labels, onAction }) {
479
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-3 p-8 text-center", children: [
480
+ /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Globe, { className: "h-6 w-6 text-muted-foreground" }) }),
481
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: labels?.emptyTitle ?? "Nenhum app vinculado ainda" }),
482
+ /* @__PURE__ */ jsx("p", { className: "max-w-sm text-xs text-muted-foreground", children: labels?.emptyBody ?? "Vincule o projeto para edit\xE1-lo aqui, conversando com o assistente." }),
483
+ onAction ? /* @__PURE__ */ jsx(
484
+ "button",
485
+ {
486
+ type: "button",
487
+ onClick: onAction,
488
+ className: "mt-1 rounded-full border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted",
489
+ children: labels?.emptyAction ?? "Vincular em Configura\xE7\xF5es"
490
+ }
491
+ ) : null
492
+ ] });
493
+ }
494
+
495
+ // src/lib/bridge.ts
496
+ var BUILDER_MESSAGES = {
497
+ activate: "ACTIVATE_ELEMENT_PICKER",
498
+ deactivate: "DEACTIVATE_ELEMENT_PICKER",
499
+ ready: "ELEMENT_PICKER_READY",
500
+ picked: "ELEMENT_PICKED",
501
+ cancelled: "ELEMENT_PICKER_CANCELLED",
502
+ navigation: "NAVIGATION_CHANGE"
503
+ };
504
+ var ROUTES_SOURCE = "fayz-routes";
505
+ var ROUTES_PARENT_SOURCE = "fayz-routes-parent";
506
+ function connectBuilderBridge(frame, origin, handlers) {
507
+ const win = typeof window === "undefined" ? void 0 : window;
508
+ if (!win) return () => {
509
+ };
510
+ const onMessage = (event) => {
511
+ if (event.origin !== origin) return;
512
+ if (event.source !== frame.contentWindow) return;
513
+ const data = event.data;
514
+ if (data?.source === ROUTES_SOURCE) {
515
+ if (data.type === "ROUTES_DISCOVERED" && Array.isArray(data.routes)) handlers.onRoutes?.(data.routes);
516
+ else if (data.type === "ROUTE_CHANGED" && typeof data.route === "string") handlers.onNavigate?.(data.route);
517
+ return;
518
+ }
519
+ switch (data?.type) {
520
+ case BUILDER_MESSAGES.navigation:
521
+ if (typeof data.path === "string") handlers.onNavigate?.(data.path);
522
+ break;
523
+ case BUILDER_MESSAGES.ready:
524
+ handlers.onReady?.();
525
+ break;
526
+ case BUILDER_MESSAGES.picked:
527
+ if (data.data) handlers.onPicked?.(data.data);
528
+ break;
529
+ case BUILDER_MESSAGES.cancelled:
530
+ handlers.onCancelled?.();
531
+ break;
532
+ }
533
+ };
534
+ win.addEventListener("message", onMessage);
535
+ return () => win.removeEventListener("message", onMessage);
536
+ }
537
+ function navigateBuilder(frame, origin, route) {
538
+ postToBuilder(frame, origin, { source: ROUTES_PARENT_SOURCE, type: "NAVIGATE", route });
539
+ }
540
+ function postToBuilder(frame, origin, message) {
541
+ if (!frame?.contentWindow || !origin) return;
542
+ frame.contentWindow.postMessage(message, origin);
543
+ }
544
+
545
+ // src/lib/url.ts
546
+ function getBaseUrl(url) {
547
+ return url.split("?")[0].split("#")[0].replace(/\/$/, "");
548
+ }
549
+ function getQueryString(url) {
550
+ const parts = url.split("?");
551
+ return parts.length > 1 ? parts[1].split("#")[0] : "";
552
+ }
553
+ function buildIframeUrl(baseUrl, route) {
554
+ const cleanBase = getBaseUrl(baseUrl);
555
+ const queryString = getQueryString(baseUrl);
556
+ if (route.startsWith("#/")) {
557
+ return `${cleanBase}${queryString ? "?" + queryString : ""}${route}`;
558
+ }
559
+ const hashIndex = route.indexOf("#");
560
+ const routePath = hashIndex === -1 ? route : route.substring(0, hashIndex);
561
+ const hashFragment = hashIndex === -1 ? "" : route.substring(hashIndex);
562
+ let result = `${cleanBase}${routePath === "/" ? "" : routePath}`;
563
+ if (queryString) result += `?${queryString}`;
564
+ if (hashFragment) result += hashFragment;
565
+ return result;
566
+ }
567
+ function addCacheBuster(url, now = Date.now()) {
568
+ return `${url}${url.includes("?") ? "&" : "?"}_cb=${now}`;
569
+ }
570
+ function extractRouteFromUrl(fullUrl, baseUrl) {
571
+ try {
572
+ const full = new URL(fullUrl);
573
+ const base = new URL(baseUrl);
574
+ if (full.host !== base.host) return "/";
575
+ let pathname = full.pathname;
576
+ if (base.pathname !== "/" && pathname.startsWith(base.pathname)) {
577
+ pathname = pathname.slice(base.pathname.length);
578
+ }
579
+ if (!pathname.startsWith("/")) pathname = "/" + pathname;
580
+ return pathname || "/";
581
+ } catch {
582
+ return "/";
583
+ }
584
+ }
585
+ function isValidPreviewUrl(url) {
586
+ if (!url) return false;
587
+ try {
588
+ const parsed = new URL(url);
589
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+ function getUrlOrigin(url) {
595
+ try {
596
+ return new URL(url).origin;
597
+ } catch {
598
+ return null;
599
+ }
600
+ }
601
+ var LOAD_TIMEOUT_MS = 8e3;
602
+ function useBuilderFrame(url, handlers) {
603
+ const frameRef = React3.useRef(null);
604
+ const [nonce, setNonce] = React3.useState(0);
605
+ const [loading, setLoading] = React3.useState(true);
606
+ const [failed, setFailed] = React3.useState(false);
607
+ const [currentPath, setCurrentPath] = React3.useState("/");
608
+ const [routes, setRoutes] = React3.useState([]);
609
+ const origin = React3.useMemo(() => url ? getUrlOrigin(url) : null, [url]);
610
+ const src = React3.useMemo(() => url ? buildIframeUrl(url, "/") : null, [url]);
611
+ const cb = React3.useRef(handlers);
612
+ cb.current = handlers;
613
+ React3.useEffect(() => {
614
+ setLoading(true);
615
+ setFailed(false);
616
+ }, [src, nonce]);
617
+ React3.useEffect(() => {
618
+ if (!loading) return;
619
+ const timer = setTimeout(() => setFailed(true), LOAD_TIMEOUT_MS);
620
+ return () => clearTimeout(timer);
621
+ }, [loading, src, nonce]);
622
+ React3.useEffect(() => {
623
+ const frame = frameRef.current;
624
+ if (!frame || !origin) return;
625
+ return connectBuilderBridge(frame, origin, {
626
+ onRoutes: setRoutes,
627
+ onNavigate: (path) => {
628
+ setCurrentPath(path);
629
+ cb.current?.onNavigate?.(path);
630
+ },
631
+ onPicked: (block) => cb.current?.onPick?.({ selector: block.path, tagName: block.type })
632
+ });
633
+ }, [origin, nonce]);
634
+ const reload = React3.useCallback(() => setNonce((n) => n + 1), []);
635
+ const navigate = React3.useCallback(
636
+ (path) => {
637
+ setCurrentPath(path);
638
+ navigateBuilder(frameRef.current, origin, path);
639
+ },
640
+ [origin]
641
+ );
642
+ const onLoad = React3.useCallback(() => {
643
+ setLoading(false);
644
+ setFailed(false);
645
+ }, []);
646
+ return { frameRef, nonce, src, origin, loading, failed, currentPath, routes, onLoad, reload, navigate };
647
+ }
648
+ function AppBuilder({
649
+ target,
650
+ mode = "preview",
651
+ onModeChange,
652
+ code,
653
+ chat,
654
+ publish,
655
+ onClose,
656
+ onToggleChat,
657
+ onToggleWide,
658
+ onNavigate,
659
+ onPick,
660
+ viewport: viewportProp,
661
+ onViewportChange,
662
+ empty,
663
+ labels,
664
+ slots,
665
+ className,
666
+ frame: frameProp
667
+ }) {
668
+ const [internalViewport, setInternalViewport] = React3.useState("desktop");
669
+ const viewport = viewportProp ?? internalViewport;
670
+ const setViewport = onViewportChange ?? setInternalViewport;
671
+ const handlePick = React3.useCallback(
672
+ (context) => onPick?.(context),
673
+ [onPick]
674
+ );
675
+ const ownFrame = useBuilderFrame(frameProp ? null : target.url, { onNavigate, onPick: handlePick });
676
+ const frame = frameProp ?? ownFrame;
677
+ const body = !target.url ? empty ?? /* @__PURE__ */ jsx(BuilderEmpty, { labels }) : mode === "code" && code ? /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: code }) : /* @__PURE__ */ jsx(BuilderCanvas, { target, frame, labels });
678
+ const mobile = viewport === "mobile" && !!target.url;
679
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 overflow-hidden", className), children: [
680
+ /* @__PURE__ */ jsxs("div", { className: cn("flex min-w-0 flex-1 flex-col overflow-hidden", mobile && "items-center bg-muted/30 py-3"), children: [
681
+ /* @__PURE__ */ jsxs(
682
+ "div",
683
+ {
684
+ className: cn(
685
+ "flex min-h-0 w-full flex-1 flex-col overflow-hidden",
686
+ mobile && "max-w-[420px] rounded-2xl border border-border bg-card shadow-xl"
687
+ ),
688
+ children: [
689
+ /* @__PURE__ */ jsx(
690
+ BuilderChrome,
691
+ {
692
+ target,
693
+ currentPath: frame.currentPath,
694
+ routes: frame.routes,
695
+ onNavigate: frame.navigate,
696
+ viewport,
697
+ onViewportChange: setViewport,
698
+ onReload: frame.reload,
699
+ mode,
700
+ onModeChange: code ? onModeChange : void 0,
701
+ publish,
702
+ onClose,
703
+ onToggleChat,
704
+ onToggleWide,
705
+ chromeEnd: slots?.chromeEnd,
706
+ labels
707
+ }
708
+ ),
709
+ body
710
+ ]
711
+ }
712
+ ),
713
+ slots?.belowCanvas
714
+ ] }),
715
+ chat ? /* @__PURE__ */ jsx("div", { className: "hidden w-[min(28rem,32%)] shrink-0 border-l border-border md:flex md:flex-col md:overflow-hidden", children: chat }) : null
716
+ ] });
717
+ }
718
+
719
+ export { AppBuilder, BUILDER_MESSAGES, BuilderCanvas, BuilderChrome, BuilderEmpty, PagePicker, PublishButton, addCacheBuster, buildIframeUrl, connectBuilderBridge, extractRouteFromUrl, getBaseUrl, getQueryString, getUrlOrigin, isHomeRoute, isValidPreviewUrl, labelForRoute, navigateBuilder, normalizeRoute, postToBuilder, toPageGroups, toPages, useBuilderFrame };
720
+ //# sourceMappingURL=chunk-45SFS6FV.js.map
721
+ //# sourceMappingURL=chunk-45SFS6FV.js.map