@xosen/site-sdk 0.0.14 → 0.0.16

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.
@@ -1,6 +1,12 @@
1
1
  import type { Router } from 'vue-router';
2
+ export interface PreviewSiteData {
3
+ config?: Record<string, any>;
4
+ skin?: Record<string, any>;
5
+ components?: Record<string, any>;
6
+ }
2
7
  export interface PreviewMessage {
3
8
  type: 'xosen-preview-update';
9
+ site?: PreviewSiteData;
4
10
  page: {
5
11
  slug: string;
6
12
  locale: string;
@@ -50,5 +56,14 @@ export declare function useLivePreview(): {
50
56
  meta?: Record<string, any> | undefined;
51
57
  layout?: string | undefined;
52
58
  } | null>;
59
+ previewSite: import("vue").Ref<{
60
+ config?: Record<string, any> | undefined;
61
+ skin?: Record<string, any> | undefined;
62
+ components?: Record<string, any> | undefined;
63
+ } | null, PreviewSiteData | {
64
+ config?: Record<string, any> | undefined;
65
+ skin?: Record<string, any> | undefined;
66
+ components?: Record<string, any> | undefined;
67
+ } | null>;
53
68
  isPreviewMode: import("vue").Ref<boolean, boolean>;
54
69
  };
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { useLocaleSwitcher } from './composables/useLocaleSwitcher.js';
10
10
  export { useDynamicLayout, buildLayoutCSS } from './composables/useDynamicLayout.js';
11
11
  export type { LayoutConfig, LayoutZone, GridBreakpoint, LayoutSchemaDefinition, LayoutField } from './types/layout.js';
12
12
  export { layoutSchema, layoutPresets } from './types/layout.js';
13
- export type { PreviewMessage } from './composables/useLivePreview.js';
13
+ export type { PreviewMessage, PreviewSiteData } from './composables/useLivePreview.js';
14
14
  export { createSiteWorker } from './worker/index.js';
15
15
  export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
16
16
  export type { Block, BlockSettings, HeroBlockData, HtmlBlockData, CardsBlockData, ImageTextBlockData, PricingBlockData, PricingPlan, ContactsBlockData, Office, GalleryBlockData, MapBlockData, } from './types/blocks.js';
package/dist/index.js CHANGED
@@ -1,51 +1,51 @@
1
- import { ref as w, onMounted as T, watch as _, computed as u, onUnmounted as N, reactive as L, provide as P } from "vue";
2
- import { useI18n as S } from "vue-i18n";
1
+ import { ref as v, onMounted as S, watch as _, computed as f, onUnmounted as N, reactive as L, provide as P } from "vue";
2
+ import { useI18n as T } from "vue-i18n";
3
3
  import { useRoute as I } from "vue-router";
4
4
  function Z(o, a) {
5
- const t = w(null), { locale: e } = S(), n = window.__SITE_DATA__, s = n?.locale;
6
- function r() {
7
- return n && n[o] && e.value === s ? (t.value = n[o], !0) : !1;
5
+ const t = v(null), { locale: e } = T(), s = window.__SITE_DATA__, n = s?.locale;
6
+ function i() {
7
+ return s && s[o] && e.value === n ? (t.value = s[o], !0) : !1;
8
8
  }
9
- async function i() {
9
+ async function l() {
10
10
  try {
11
- const c = await fetch(`/api/content/${a}:${e.value}`);
12
- c.ok && (t.value = await c.json());
11
+ const r = await fetch(`/api/content/${a}:${e.value}`);
12
+ r.ok && (t.value = await r.json());
13
13
  } catch {
14
14
  }
15
15
  }
16
- return T(() => {
17
- r() || i();
16
+ return S(() => {
17
+ i() || l();
18
18
  }), _(e, () => {
19
- i();
19
+ l();
20
20
  }), t;
21
21
  }
22
22
  function q() {
23
- const o = window.__SITE_DATA__, a = u(() => o?.config || null), t = u(() => o?.components || {}), e = u(() => a.value?.navigation || []), n = u(() => a.value?.locales || []), s = u(() => a.value?.defaultLocale || "en"), r = u(() => a.value?.branding || {}), i = u(() => a.value?.footer || {}), c = u(() => a.value?.features || {});
24
- function p(m) {
25
- const g = c.value[m];
23
+ const o = window.__SITE_DATA__, a = f(() => o?.config || null), t = f(() => o?.components || {}), e = f(() => a.value?.navigation || []), s = f(() => a.value?.locales || []), n = f(() => a.value?.defaultLocale || "en"), i = f(() => a.value?.branding || {}), l = f(() => a.value?.footer || {}), r = f(() => a.value?.features || {});
24
+ function u(m) {
25
+ const g = r.value[m];
26
26
  return typeof g == "boolean" ? g : typeof g == "object";
27
27
  }
28
28
  return {
29
29
  config: a,
30
30
  components: t,
31
31
  navigation: e,
32
- locales: n,
33
- defaultLocale: s,
34
- branding: r,
35
- footer: i,
36
- features: c,
37
- hasFeature: p
32
+ locales: s,
33
+ defaultLocale: n,
34
+ branding: i,
35
+ footer: l,
36
+ features: r,
37
+ hasFeature: u
38
38
  };
39
39
  }
40
40
  function H(o) {
41
41
  function a(t) {
42
42
  const e = document.documentElement;
43
43
  if (t.colors)
44
- for (const [n, s] of Object.entries(t.colors))
45
- s && e.style.setProperty(`--x-color-${D(n)}`, s);
44
+ for (const [s, n] of Object.entries(t.colors))
45
+ n && e.style.setProperty(`--x-color-${D(s)}`, n);
46
46
  t.typography?.fontFamily && e.style.setProperty("--x-font-family", t.typography.fontFamily), t.typography?.headingFont && e.style.setProperty("--x-font-heading", t.typography.headingFont), t.typography?.baseFontSize && e.style.setProperty("--x-font-size", t.typography.baseFontSize), t.shape?.borderRadius && e.style.setProperty("--x-border-radius", t.shape.borderRadius), t.shape?.maxWidth && e.style.setProperty("--x-max-width", t.shape.maxWidth);
47
47
  }
48
- T(() => {
48
+ S(() => {
49
49
  if (o) {
50
50
  a(o);
51
51
  return;
@@ -63,25 +63,25 @@ function D(o) {
63
63
  }
64
64
  function Q() {
65
65
  const a = window.__SITE_DATA__?.apiBase || "";
66
- async function t(s) {
67
- const r = await fetch(`${a}${s}`);
68
- if (!r.ok)
69
- throw new Error(`API error: ${r.status}`);
70
- return r.json();
66
+ async function t(n) {
67
+ const i = await fetch(`${a}${n}`);
68
+ if (!i.ok)
69
+ throw new Error(`API error: ${i.status}`);
70
+ return i.json();
71
71
  }
72
72
  async function e() {
73
73
  return t("/v1/billing/tariffs");
74
74
  }
75
- async function n() {
75
+ async function s() {
76
76
  return t("/v1/billing/services");
77
77
  }
78
78
  return {
79
79
  get: t,
80
80
  getTariffs: e,
81
- getProducts: n
81
+ getProducts: s
82
82
  };
83
83
  }
84
- function A() {
84
+ function $() {
85
85
  try {
86
86
  return window.self !== window.top;
87
87
  } catch {
@@ -89,133 +89,141 @@ function A() {
89
89
  }
90
90
  }
91
91
  function V(o) {
92
- A() && o.beforeEach((a, t) => !t.name);
92
+ $() && o.beforeEach((a, t) => !t.name);
93
93
  }
94
94
  function C() {
95
- const o = w(null), a = w(A());
96
- function t(e) {
97
- const n = e.data;
98
- n?.type === "xosen-preview-update" && n.page && (o.value = n.page, a.value = !0);
95
+ const o = v(null), a = v(null), t = v($());
96
+ function e(s) {
97
+ const n = s.data;
98
+ n?.type === "xosen-preview-update" && n.page && (o.value = n.page, n.site && (a.value = n.site), t.value = !0);
99
99
  }
100
- return T(() => {
101
- window.addEventListener("message", t);
100
+ return S(() => {
101
+ window.addEventListener("message", e);
102
102
  }), N(() => {
103
- window.removeEventListener("message", t);
103
+ window.removeEventListener("message", e);
104
104
  }), {
105
105
  previewPage: o,
106
- isPreviewMode: a
106
+ previewSite: a,
107
+ isPreviewMode: t
107
108
  };
108
109
  }
109
- const R = /* @__PURE__ */ Symbol("pageContext"), $ = L({});
110
+ const R = /* @__PURE__ */ Symbol("pageContext"), k = L({});
110
111
  function j() {
111
112
  const o = window.__SITE_DATA__;
112
113
  if (o)
113
114
  for (const a of Object.keys(o))
114
- a.startsWith("page:") && ($[a] = o[a]);
115
+ a.startsWith("page:") && (k[a] = o[a]);
115
116
  }
116
117
  j();
117
118
  function ee(o) {
118
- const { locale: a } = S(), { previewPage: t, isPreviewMode: e } = C(), n = window.__SITE_DATA__, s = w(null), r = w(!0), i = w(!1), c = u(() => typeof o == "string" ? o : o.value), p = u(() => `page:${c.value}`), m = u(() => {
119
- const l = e.value ? t.value : s.value;
120
- if (!l) return {};
121
- const { blocks: f, meta: d, slug: W, locale: J, ...E } = l;
119
+ const { locale: a } = T(), { previewPage: t, isPreviewMode: e } = C(), s = window.__SITE_DATA__, n = v(null), i = v(!0), l = v(!1), r = f(() => typeof o == "string" ? o : o.value), u = f(() => `page:${r.value}`), m = f(() => {
120
+ const c = e.value ? t.value : n.value;
121
+ if (!c) return {};
122
+ const { blocks: p, meta: d, slug: W, locale: J, ...E } = c;
122
123
  return E;
123
124
  });
124
125
  P(R, m);
125
- const g = u(() => e.value && t.value?.slug === c.value ? t.value.blocks || [] : s.value?.blocks || []);
126
- function b(l) {
127
- s.value = l, l && ($[p.value] = l);
126
+ const g = f(() => e.value && t.value?.slug === r.value ? t.value.blocks || [] : n.value?.blocks || []);
127
+ function b(c) {
128
+ n.value = c, c && (k[u.value] = c);
128
129
  }
129
130
  function y() {
130
- const l = n?.[p.value];
131
- return l && a.value === n?.locale ? (b(l), i.value = !1, v(), !0) : !1;
131
+ const c = s?.[u.value];
132
+ return c && a.value === s?.locale ? (b(c), l.value = !1, w(), !0) : !1;
132
133
  }
133
134
  async function h() {
134
- r.value = !0, i.value = !1;
135
+ i.value = !0, l.value = !1;
135
136
  try {
136
- const l = await fetch(`/api/content/${p.value}:${a.value}`);
137
- l.ok ? (b(await l.json()), v()) : (b(null), i.value = !0);
137
+ const c = await fetch(`/api/content/${u.value}:${a.value}`);
138
+ c.ok ? (b(await c.json()), w()) : (b(null), l.value = !0);
138
139
  } catch {
139
- b(null), i.value = !0;
140
+ b(null), l.value = !0;
140
141
  } finally {
141
- r.value = !1;
142
+ i.value = !1;
142
143
  }
143
144
  }
144
- function v() {
145
- if (!s.value) return;
146
- if (s.value.title) {
147
- const f = n?.config?.branding, d = f?.siteName ? ` — ${f.siteName}` : "";
148
- document.title = `${s.value.title}${d}`;
145
+ function w() {
146
+ if (!n.value) return;
147
+ if (n.value.title) {
148
+ const p = s?.config?.branding, d = p?.siteName ? ` — ${p.siteName}` : "";
149
+ document.title = `${n.value.title}${d}`;
149
150
  }
150
- const l = s.value.meta?.description;
151
- if (l) {
152
- const f = document.querySelector('meta[name="description"]');
153
- f && f.setAttribute("content", l);
151
+ const c = n.value.meta?.description;
152
+ if (c) {
153
+ const p = document.querySelector('meta[name="description"]');
154
+ p && p.setAttribute("content", c);
154
155
  }
155
156
  }
156
- return _(t, (l) => {
157
- l?.slug === c.value && (r.value = !1, i.value = !1, (l.title || l.blocks) && (s.value = {
158
- ...s.value || {},
159
- ...l.title ? { title: l.title } : {},
160
- ...l.blocks ? { blocks: l.blocks } : {}
161
- }, v()));
162
- }), T(() => {
163
- y() ? r.value = !1 : h();
164
- }), _(a, () => h()), _(c, () => {
165
- y() ? r.value = !1 : h();
157
+ return _(t, (c) => {
158
+ c?.slug === r.value && (i.value = !1, l.value = !1, (c.title || c.blocks) && (n.value = {
159
+ ...n.value || {},
160
+ ...c.title ? { title: c.title } : {},
161
+ ...c.blocks ? { blocks: c.blocks } : {}
162
+ }, w()));
163
+ }), S(() => {
164
+ y() ? i.value = !1 : h();
165
+ }), _(a, () => h()), _(r, () => {
166
+ y() ? i.value = !1 : h();
166
167
  }), {
167
- page: s,
168
+ page: n,
168
169
  blocks: g,
169
170
  pageContext: m,
170
- loading: r,
171
- notFound: i,
171
+ loading: i,
172
+ notFound: l,
172
173
  isPreviewMode: e,
173
174
  reload: h
174
175
  };
175
176
  }
176
177
  function te() {
177
- const { locale: o } = S(), a = window.__SITE_DATA__, t = a?.locale, e = w({ ...a?.components || {} });
178
- async function n(r) {
179
- if (r === t) {
180
- e.value = { ...a?.components || {} };
181
- return;
182
- }
183
- try {
184
- const i = await fetch(`/api/content/components:${r}`);
185
- i.ok && (e.value = await i.json());
186
- } catch {
178
+ const { locale: o } = T(), a = window.__SITE_DATA__, t = a?.locale, e = v({ ...a?.components || {} });
179
+ let s = t, n = null;
180
+ async function i(r) {
181
+ if (!$() && r !== s) {
182
+ if (r === t) {
183
+ e.value = { ...a?.components || {} }, s = r;
184
+ return;
185
+ }
186
+ n || (n = (async () => {
187
+ try {
188
+ const u = await fetch(`/api/content/components:${r}`);
189
+ u.ok && (e.value = await u.json(), s = r);
190
+ } catch {
191
+ } finally {
192
+ n = null;
193
+ }
194
+ })(), await n);
187
195
  }
188
196
  }
189
- _(o, (r) => n(r)), T(() => {
190
- o.value !== t && n(o.value);
197
+ _(o, (r) => i(r)), S(() => {
198
+ o.value !== t && i(o.value);
191
199
  });
192
- function s(r) {
193
- return u(() => e.value[r] || null);
200
+ function l(r) {
201
+ return f(() => e.value[r] || null);
194
202
  }
195
203
  return {
196
204
  /** All components (reactive, locale-aware) */
197
205
  components: e,
198
206
  /** Get a single component by key */
199
- getComponent: s,
207
+ getComponent: l,
200
208
  /** Force refetch for current locale */
201
- reload: () => n(o.value)
209
+ reload: () => i(o.value)
202
210
  };
203
211
  }
204
212
  function ae() {
205
- const { locale: o } = S(), a = window.__SITE_DATA__, t = u(() => a?.config?.locales || []), e = u(() => a?.config?.defaultLocale || "en"), n = u(() => t.value.length > 1);
206
- function s(i) {
207
- o.value = i, localStorage.setItem("x-site-locale", i);
213
+ const { locale: o } = T(), a = window.__SITE_DATA__, t = f(() => a?.config?.locales || []), e = f(() => a?.config?.defaultLocale || "en"), s = f(() => t.value.length > 1);
214
+ function n(l) {
215
+ o.value = l, localStorage.setItem("x-site-locale", l);
208
216
  }
209
- function r(i) {
210
- return typeof i == "string" ? i : i[o.value] || i[e.value] || Object.values(i)[0] || "";
217
+ function i(l) {
218
+ return typeof l == "string" ? l : l[o.value] || l[e.value] || Object.values(l)[0] || "";
211
219
  }
212
220
  return {
213
221
  locale: o,
214
222
  locales: t,
215
223
  defaultLocale: e,
216
- hasMultipleLocales: n,
217
- switchLocale: s,
218
- resolveText: r
224
+ hasMultipleLocales: s,
225
+ switchLocale: n,
226
+ resolveText: i
219
227
  };
220
228
  }
221
229
  const O = {
@@ -245,10 +253,10 @@ const O = {
245
253
  main: { type: "slot" }
246
254
  }
247
255
  };
248
- function k(o, a) {
256
+ function x(o, a) {
249
257
  const t = [];
250
258
  if (o.areas?.length) {
251
- const e = o.areas.map((n) => `"${n}"`).join(" ");
259
+ const e = o.areas.map((s) => `"${s}"`).join(" ");
252
260
  t.push(`grid-template-areas: ${e}`);
253
261
  }
254
262
  return o.columns && t.push(`grid-template-columns: ${o.columns}`), o.rows && t.push(`grid-template-rows: ${o.rows}`), o.gap && t.push(`gap: ${o.gap}`), t.length ? `${a} { ${t.join("; ")}; }` : "";
@@ -258,39 +266,39 @@ function G(o, a) {
258
266
  if (!e) return t.join(`
259
267
  `);
260
268
  if (e.default) {
261
- const n = k(e.default, a);
262
- n && t.push(n);
269
+ const s = x(e.default, a);
270
+ s && t.push(s);
263
271
  }
264
- for (const n of ["sm", "md", "lg", "xl"]) {
265
- const s = e[n];
266
- if (!s) continue;
267
- const r = k(s, a);
268
- r && t.push(`@media (min-width: ${O[n]}px) { ${r} }`);
272
+ for (const s of ["sm", "md", "lg", "xl"]) {
273
+ const n = e[s];
274
+ if (!n) continue;
275
+ const i = x(n, a);
276
+ i && t.push(`@media (min-width: ${O[s]}px) { ${i} }`);
269
277
  }
270
278
  return t.join(`
271
279
  `);
272
280
  }
273
281
  function oe() {
274
- const o = I(), { previewPage: a, isPreviewMode: t } = C(), e = window.__SITE_DATA__, n = `dl-${Math.random().toString(36).slice(2, 8)}`, s = u(() => ["dynamic-layout", n]), r = u(() => {
282
+ const o = I(), { previewPage: a, isPreviewMode: t } = C(), e = window.__SITE_DATA__, s = `dl-${Math.random().toString(36).slice(2, 8)}`, n = f(() => ["dynamic-layout", s]), i = f(() => {
275
283
  if (o.path === "/_preview_")
276
284
  return a.value?.layout || "landing";
277
285
  if (t.value && a.value?.layout)
278
286
  return a.value.layout;
279
- const p = o.path === "/" ? "page:home" : `page:${o.params.slug || ""}`;
280
- return ($[p] || e?.[p])?.layout || "default";
281
- }), i = u(() => {
282
- const p = e?.config?.layouts;
283
- if (p) {
284
- const m = p[r.value] || p.default;
287
+ const u = o.path === "/" ? "page:home" : `page:${o.params.slug || ""}`;
288
+ return (k[u] || e?.[u])?.layout || "default";
289
+ }), l = f(() => {
290
+ const u = e?.config?.layouts;
291
+ if (u) {
292
+ const m = u[i.value] || u.default;
285
293
  if (m?.zones) return m;
286
294
  }
287
- return r.value === "landing" ? F : z;
288
- }), c = u(() => G(i.value, `.${n}`));
295
+ return i.value === "landing" ? F : z;
296
+ }), r = f(() => G(l.value, `.${s}`));
289
297
  return {
290
- layoutName: r,
291
- layoutConfig: i,
292
- layoutCSS: c,
293
- layoutClass: s
298
+ layoutName: i,
299
+ layoutConfig: l,
300
+ layoutCSS: r,
301
+ layoutClass: n
294
302
  };
295
303
  }
296
304
  const ne = {
@@ -299,7 +307,13 @@ const ne = {
299
307
  description: "CSS Grid layout with responsive breakpoints and component zones",
300
308
  fields: [
301
309
  // Mobile (default)
302
- { key: "grid.default.areas", type: "grid-areas", label: "Grid Areas (mobile)", breakpoint: "default", required: !0 },
310
+ {
311
+ key: "grid.default.areas",
312
+ type: "grid-areas",
313
+ label: "Grid Areas (mobile)",
314
+ breakpoint: "default",
315
+ required: !0
316
+ },
303
317
  { key: "grid.default.columns", type: "text", label: "Columns (mobile)", breakpoint: "default" },
304
318
  { key: "grid.default.rows", type: "text", label: "Rows (mobile)", breakpoint: "default" },
305
319
  { key: "grid.default.gap", type: "text", label: "Gap (mobile)", breakpoint: "default" },
@@ -385,7 +399,12 @@ const ne = {
385
399
  config: {
386
400
  grid: {
387
401
  default: { areas: ["header", "main", "aside", "footer"], columns: "1fr", rows: "auto 1fr auto auto" },
388
- md: { areas: ["header header header", "main main aside", "footer footer footer"], columns: "1fr 1fr 300px", rows: "auto 1fr auto", gap: "24px" }
402
+ md: {
403
+ areas: ["header header header", "main main aside", "footer footer footer"],
404
+ columns: "1fr 1fr 300px",
405
+ rows: "auto 1fr auto",
406
+ gap: "24px"
407
+ }
389
408
  },
390
409
  zones: {
391
410
  header: { component: "nav-menu" },
@@ -400,9 +419,9 @@ function U(o, a) {
400
419
  const e = new URL(o.url).searchParams.get("lang");
401
420
  if (e && a.supportedLocales?.includes(e)) return e;
402
421
  if (a.supportedLocales?.length) {
403
- const n = o.headers.get("Accept-Language") || "";
404
- for (const s of a.supportedLocales)
405
- if (n.includes(s)) return s;
422
+ const s = o.headers.get("Accept-Language") || "";
423
+ for (const n of a.supportedLocales)
424
+ if (s.includes(n)) return n;
406
425
  }
407
426
  return a.defaultLocale;
408
427
  }
@@ -410,12 +429,12 @@ function K(o, a, t) {
410
429
  if (!o.pathname.startsWith("/api/content/")) return null;
411
430
  const e = decodeURIComponent(o.pathname.replace("/api/content/", ""));
412
431
  return e ? (async () => {
413
- let n = await a.SITE_CONTENT.get(e);
414
- if (!n) {
415
- const s = e.lastIndexOf(":");
416
- s > 0 && e.slice(s + 1) !== t && (n = await a.SITE_CONTENT.get(e.slice(0, s + 1) + t));
432
+ let s = await a.SITE_CONTENT.get(e);
433
+ if (!s) {
434
+ const n = e.lastIndexOf(":");
435
+ n > 0 && e.slice(n + 1) !== t && (s = await a.SITE_CONTENT.get(e.slice(0, n + 1) + t));
417
436
  }
418
- return n ? new Response(n, {
437
+ return s ? new Response(s, {
419
438
  headers: {
420
439
  "Content-Type": "application/json",
421
440
  "Cache-Control": "public, max-age=60",
@@ -424,54 +443,54 @@ function K(o, a, t) {
424
443
  }) : Response.json({ error: "Not found" }, { status: 404 });
425
444
  })() : Promise.resolve(Response.json({ error: "Key is required" }, { status: 400 }));
426
445
  }
427
- function x(o, a, t) {
446
+ function A(o, a, t) {
428
447
  let e = o;
429
448
  if (a.title) {
430
- const n = t ? ` — ${t}` : "";
431
- e = e.replace(/<title>[^<]*<\/title>/, `<title>${a.title}${n}</title>`);
449
+ const s = t ? ` — ${t}` : "";
450
+ e = e.replace(/<title>[^<]*<\/title>/, `<title>${a.title}${s}</title>`);
432
451
  }
433
452
  return a.meta?.description && (e = e.replace(/(<meta\s+name="description"\s+content=")[^"]*(")/, `$1${a.meta.description}$2`)), a.meta?.ogTitle && (e = e.replace(/(<meta\s+property="og:title"\s+content=")[^"]*(")/, `$1${a.meta.ogTitle}$2`)), a.html && (e = e.replace('<div id="app"></div>', `<div id="app">${a.html}</div>`)), e;
434
453
  }
435
454
  function re(o) {
436
455
  return {
437
456
  async fetch(a, t) {
438
- const e = new URL(a.url), n = K(e, t, o.defaultLocale);
439
- if (n) return n;
457
+ const e = new URL(a.url), s = K(e, t, o.defaultLocale);
458
+ if (s) return s;
440
459
  if (M.test(e.pathname))
441
460
  return t.ASSETS.fetch(a);
442
- const s = U(a, o), r = new URL("/index.html", a.url);
443
- let c = await (await t.ASSETS.fetch(new Request(r))).text();
444
- const p = o.defaultLocale, [m, g] = await Promise.all([
461
+ const n = U(a, o), i = new URL("/index.html", a.url);
462
+ let r = await (await t.ASSETS.fetch(new Request(i))).text();
463
+ const u = o.defaultLocale, [m, g] = await Promise.all([
445
464
  t.SITE_CONTENT.get("config"),
446
- t.SITE_CONTENT.get(`content:${s}`)
465
+ t.SITE_CONTENT.get(`content:${n}`)
447
466
  ]), b = m ? JSON.parse(m) : {};
448
467
  let y = g ? JSON.parse(g) : {};
449
- if (!g && s !== p) {
450
- const f = await t.SITE_CONTENT.get(`content:${p}`);
451
- f && (y = JSON.parse(f));
468
+ if (!g && n !== u) {
469
+ const p = await t.SITE_CONTENT.get(`content:${u}`);
470
+ p && (y = JSON.parse(p));
452
471
  }
453
472
  const h = e.pathname.match(/^\/p\/(.+)$/);
454
473
  if (h) {
455
- const f = h[1];
456
- if (!y[f]) {
457
- let d = await t.SITE_CONTENT.get(`page:${f}:${s}`);
458
- !d && s !== p && (d = await t.SITE_CONTENT.get(`page:${f}:${p}`)), d && (y[f] = JSON.parse(d));
474
+ const p = h[1];
475
+ if (!y[p]) {
476
+ let d = await t.SITE_CONTENT.get(`page:${p}:${n}`);
477
+ !d && n !== u && (d = await t.SITE_CONTENT.get(`page:${p}:${u}`)), d && (y[p] = JSON.parse(d));
459
478
  }
460
479
  }
461
- const v = {
462
- locale: s,
480
+ const w = {
481
+ locale: n,
463
482
  config: b
464
483
  };
465
- for (const [f, d] of Object.entries(y))
466
- v[`page:${f}`] = d;
484
+ for (const [p, d] of Object.entries(y))
485
+ w[`page:${p}`] = d;
467
486
  if (h) {
468
- const f = h[1], d = v[`page:${f}`];
469
- d && (c = x(c, d, o.siteName));
487
+ const p = h[1], d = w[`page:${p}`];
488
+ d && (r = A(r, d, o.siteName));
470
489
  }
471
- e.pathname === "/" && y.home && (c = x(c, y.home, o.siteName));
472
- const l = `<script>window.__SITE_DATA__ = ${JSON.stringify(v)};<\/script>`;
473
- return c = c.replace("</head>", `${l}
474
- </head>`), new Response(c, {
490
+ e.pathname === "/" && y.home && (r = A(r, y.home, o.siteName));
491
+ const c = `<script>window.__SITE_DATA__ = ${JSON.stringify(w)};<\/script>`;
492
+ return r = r.replace("</head>", `${c}
493
+ </head>`), new Response(r, {
475
494
  headers: {
476
495
  "Content-Type": "text/html;charset=utf-8",
477
496
  "Cache-Control": "public, max-age=60"
@@ -484,7 +503,7 @@ export {
484
503
  R as PAGE_CONTEXT_KEY,
485
504
  G as buildLayoutCSS,
486
505
  re as createSiteWorker,
487
- A as isInPreview,
506
+ $ as isInPreview,
488
507
  se as layoutPresets,
489
508
  ne as layoutSchema,
490
509
  V as setupPreviewRouter,
@@ -18,7 +18,8 @@ export interface HeroBlockData {
18
18
  badge?: string;
19
19
  image?: string;
20
20
  backgroundImage?: string;
21
- overlay?: boolean;
21
+ /** Overlay: false to disable, number (0-1) for opacity, or CSS string for custom gradient */
22
+ overlay?: boolean | number | string;
22
23
  buttons?: Array<{
23
24
  text: string;
24
25
  url: string;
@@ -12,6 +12,7 @@ export interface LayoutZone {
12
12
  type?: 'slot';
13
13
  component?: string;
14
14
  class?: string;
15
+ style?: Record<string, string>;
15
16
  container?: boolean;
16
17
  containerStyle?: Record<string, string>;
17
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xosen/site-sdk",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Shared Vue components and composables for Xosen site templates",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,23 +24,23 @@
24
24
  "dist",
25
25
  "src"
26
26
  ],
27
+ "scripts": {
28
+ "build": "vite build && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
29
+ "dev": "vite build --watch",
30
+ "clean": "rm -rf dist"
31
+ },
27
32
  "peerDependencies": {
28
33
  "vue": "^3.5.0",
29
34
  "vue-i18n": "^11.0.0",
30
35
  "vue-router": "^5.0.0"
31
36
  },
32
37
  "devDependencies": {
33
- "vue": "^3.5.30",
34
- "vue-i18n": "^11.3.0",
35
- "vue-router": "^5.0.3",
36
- "vue-tsc": "^3.2.5",
37
- "typescript": "^5.9.3",
38
- "vite": "^7.3.1",
39
- "@vitejs/plugin-vue": "^6.0.5"
40
- },
41
- "scripts": {
42
- "build": "vite build && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
43
- "dev": "vite build --watch",
44
- "clean": "rm -rf dist"
38
+ "vue": "catalog:",
39
+ "vue-i18n": "catalog:",
40
+ "vue-router": "catalog:",
41
+ "vue-tsc": "catalog:",
42
+ "typescript": "catalog:",
43
+ "vite": "catalog:",
44
+ "@vitejs/plugin-vue": "catalog:"
45
45
  }
46
- }
46
+ }
@@ -1,8 +1,15 @@
1
1
  import { ref, onMounted, onUnmounted } from 'vue';
2
2
  import type { Router } from 'vue-router';
3
3
 
4
+ export interface PreviewSiteData {
5
+ config?: Record<string, any>;
6
+ skin?: Record<string, any>;
7
+ components?: Record<string, any>;
8
+ }
9
+
4
10
  export interface PreviewMessage {
5
11
  type: 'xosen-preview-update';
12
+ site?: PreviewSiteData;
6
13
  page: {
7
14
  slug: string;
8
15
  locale: string;
@@ -45,12 +52,16 @@ export function setupPreviewRouter(router: Router) {
45
52
  */
46
53
  export function useLivePreview() {
47
54
  const previewPage = ref<PreviewMessage['page'] | null>(null);
55
+ const previewSite = ref<PreviewSiteData | null>(null);
48
56
  const isPreviewMode = ref(isInPreview());
49
57
 
50
58
  function handleMessage(event: MessageEvent) {
51
59
  const data = event.data;
52
60
  if (data?.type === 'xosen-preview-update' && data.page) {
53
61
  previewPage.value = data.page;
62
+ if (data.site) {
63
+ previewSite.value = data.site;
64
+ }
54
65
  isPreviewMode.value = true;
55
66
  }
56
67
  }
@@ -65,6 +76,7 @@ export function useLivePreview() {
65
76
 
66
77
  return {
67
78
  previewPage,
79
+ previewSite,
68
80
  isPreviewMode,
69
81
  };
70
82
  }
@@ -40,7 +40,7 @@ export function usePageData(slug: Ref<string> | string): UsePageDataReturn {
40
40
  const loading = ref(true);
41
41
  const notFound = ref(false);
42
42
 
43
- const resolvedSlug = computed(() => typeof slug === 'string' ? slug : slug.value);
43
+ const resolvedSlug = computed(() => (typeof slug === 'string' ? slug : slug.value));
44
44
  const pageKey = computed(() => `page:${resolvedSlug.value}`);
45
45
 
46
46
  // Page context: page metadata without blocks (for injection)
@@ -121,7 +121,7 @@ export function usePageData(slug: Ref<string> | string): UsePageDataReturn {
121
121
  notFound.value = false;
122
122
  if (p.title || p.blocks) {
123
123
  page.value = {
124
- ...(page.value || {} as PageConfig),
124
+ ...(page.value || ({} as PageConfig)),
125
125
  ...(p.title ? { title: p.title } : {}),
126
126
  ...(p.blocks ? { blocks: p.blocks as Block[] } : {}),
127
127
  };
@@ -2,6 +2,7 @@ import { ref, computed, watch, onMounted, type Ref } from 'vue';
2
2
  import { useI18n } from 'vue-i18n';
3
3
 
4
4
  import type { SiteData } from '../types/config.js';
5
+ import { isInPreview } from './useLivePreview.js';
5
6
 
6
7
  /**
7
8
  * Locale-aware access to site components (navigation, footer, sidebar, etc.).
@@ -14,22 +15,42 @@ export function useSiteComponents() {
14
15
  const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
15
16
  const initialLocale = siteData?.locale;
16
17
 
17
- const components = ref<Record<string, any>>({ ...(siteData as any)?.components || {} });
18
+ const components = ref<Record<string, any>>({ ...((siteData as any)?.components || {}) });
19
+ let lastFetchedLocale: string | undefined = initialLocale;
20
+ let fetchPromise: Promise<void> | null = null;
18
21
 
19
22
  async function fetchComponents(loc: string) {
23
+ // Skip fetching in preview mode — components come via postMessage
24
+ if (isInPreview()) return;
25
+
26
+ // Skip if already fetched/loaded for this locale
27
+ if (loc === lastFetchedLocale) return;
28
+
20
29
  // If same as initial locale, use preloaded data
21
30
  if (loc === initialLocale) {
22
- components.value = { ...(siteData as any)?.components || {} };
31
+ components.value = { ...((siteData as any)?.components || {}) };
32
+ lastFetchedLocale = loc;
23
33
  return;
24
34
  }
25
- try {
26
- const res = await fetch(`/api/content/components:${loc}`);
27
- if (res.ok) {
28
- components.value = await res.json();
35
+
36
+ // Deduplicate concurrent fetches
37
+ if (fetchPromise) return;
38
+
39
+ fetchPromise = (async () => {
40
+ try {
41
+ const res = await fetch(`/api/content/components:${loc}`);
42
+ if (res.ok) {
43
+ components.value = await res.json();
44
+ lastFetchedLocale = loc;
45
+ }
46
+ } catch {
47
+ // Keep current
48
+ } finally {
49
+ fetchPromise = null;
29
50
  }
30
- } catch {
31
- // Keep current
32
- }
51
+ })();
52
+
53
+ await fetchPromise;
33
54
  }
34
55
 
35
56
  watch(locale, (newLocale) => fetchComponents(newLocale));
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export { useLocaleSwitcher } from './composables/useLocaleSwitcher.js';
11
11
  export { useDynamicLayout, buildLayoutCSS } from './composables/useDynamicLayout.js';
12
12
  export type { LayoutConfig, LayoutZone, GridBreakpoint, LayoutSchemaDefinition, LayoutField } from './types/layout.js';
13
13
  export { layoutSchema, layoutPresets } from './types/layout.js';
14
- export type { PreviewMessage } from './composables/useLivePreview.js';
14
+ export type { PreviewMessage, PreviewSiteData } from './composables/useLivePreview.js';
15
15
 
16
16
  // Worker
17
17
  export { createSiteWorker } from './worker/index.js';
@@ -20,7 +20,8 @@ export interface HeroBlockData {
20
20
  badge?: string;
21
21
  image?: string;
22
22
  backgroundImage?: string;
23
- overlay?: boolean;
23
+ /** Overlay: false to disable, number (0-1) for opacity, or CSS string for custom gradient */
24
+ overlay?: boolean | number | string;
24
25
  buttons?: Array<{ text: string; url: string; variant?: 'primary' | 'outline' | 'white' }>;
25
26
  stats?: Array<{ value: string; label: string }>;
26
27
  }
@@ -14,6 +14,7 @@ export interface LayoutZone {
14
14
  type?: 'slot';
15
15
  component?: string;
16
16
  class?: string;
17
+ style?: Record<string, string>;
17
18
  container?: boolean;
18
19
  containerStyle?: Record<string, string>;
19
20
  }
@@ -50,7 +51,13 @@ export const layoutSchema: LayoutSchemaDefinition = {
50
51
  description: 'CSS Grid layout with responsive breakpoints and component zones',
51
52
  fields: [
52
53
  // Mobile (default)
53
- { key: 'grid.default.areas', type: 'grid-areas', label: 'Grid Areas (mobile)', breakpoint: 'default', required: true },
54
+ {
55
+ key: 'grid.default.areas',
56
+ type: 'grid-areas',
57
+ label: 'Grid Areas (mobile)',
58
+ breakpoint: 'default',
59
+ required: true,
60
+ },
54
61
  { key: 'grid.default.columns', type: 'text', label: 'Columns (mobile)', breakpoint: 'default' },
55
62
  { key: 'grid.default.rows', type: 'text', label: 'Rows (mobile)', breakpoint: 'default' },
56
63
  { key: 'grid.default.gap', type: 'text', label: 'Gap (mobile)', breakpoint: 'default' },
@@ -145,7 +152,12 @@ export const layoutPresets: Record<string, { displayName: string; config: Layout
145
152
  config: {
146
153
  grid: {
147
154
  default: { areas: ['header', 'main', 'aside', 'footer'], columns: '1fr', rows: 'auto 1fr auto auto' },
148
- md: { areas: ['header header header', 'main main aside', 'footer footer footer'], columns: '1fr 1fr 300px', rows: 'auto 1fr auto', gap: '24px' },
155
+ md: {
156
+ areas: ['header header header', 'main main aside', 'footer footer footer'],
157
+ columns: '1fr 1fr 300px',
158
+ rows: 'auto 1fr auto',
159
+ gap: '24px',
160
+ },
149
161
  },
150
162
  zones: {
151
163
  header: { component: 'nav-menu' },