@xosen/site-sdk 0.0.10 → 0.0.12

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.
@@ -0,0 +1,37 @@
1
+ export interface GridBreakpoint {
2
+ areas?: string[];
3
+ columns?: string;
4
+ rows?: string;
5
+ gap?: string;
6
+ }
7
+ export interface LayoutZone {
8
+ type?: 'slot';
9
+ component?: string;
10
+ class?: string;
11
+ container?: boolean;
12
+ containerStyle?: Record<string, string>;
13
+ }
14
+ export interface LayoutConfig {
15
+ grid?: Record<string, GridBreakpoint>;
16
+ zones: Record<string, LayoutZone>;
17
+ }
18
+ /**
19
+ * Generate responsive CSS for a layout config.
20
+ * Returns a CSS string with media queries for each breakpoint.
21
+ */
22
+ export declare function buildLayoutCSS(layout: LayoutConfig, selector: string): string;
23
+ /**
24
+ * Resolve the current layout config from site data, route, and preview state.
25
+ *
26
+ * Returns:
27
+ * - `layoutName` — current layout name (e.g. 'default', 'sidebar')
28
+ * - `layoutConfig` — resolved LayoutConfig with grid + zones
29
+ * - `layoutCSS` — generated responsive CSS string
30
+ * - `layoutClass` — unique CSS class for this layout instance
31
+ */
32
+ export declare function useDynamicLayout(): {
33
+ layoutName: import("vue").ComputedRef<any>;
34
+ layoutConfig: import("vue").ComputedRef<LayoutConfig>;
35
+ layoutCSS: import("vue").ComputedRef<string>;
36
+ layoutClass: import("vue").ComputedRef<string[]>;
37
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Locale switcher composable.
3
+ * Manages locale state, available locales, and persistence.
4
+ */
5
+ export declare function useLocaleSwitcher(): {
6
+ locale: import("vue").WritableComputedRef<string, string>;
7
+ locales: import("vue").ComputedRef<string[]>;
8
+ defaultLocale: import("vue").ComputedRef<string>;
9
+ hasMultipleLocales: import("vue").ComputedRef<boolean>;
10
+ switchLocale: (loc: string) => void;
11
+ resolveText: (text: string | Record<string, string>) => string;
12
+ };
@@ -0,0 +1,15 @@
1
+ import { type Ref } from 'vue';
2
+ /**
3
+ * Locale-aware access to site components (navigation, footer, sidebar, etc.).
4
+ *
5
+ * On initial load, reads from preloaded __SITE_DATA__.components.
6
+ * When locale changes, fetches updated components from the API.
7
+ */
8
+ export declare function useSiteComponents(): {
9
+ /** All components (reactive, locale-aware) */
10
+ components: Ref<Record<string, any>, Record<string, any>>;
11
+ /** Get a single component by key */
12
+ getComponent: <T = any>(key: string) => Ref<T | null>;
13
+ /** Force refetch for current locale */
14
+ reload: () => Promise<void>;
15
+ };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,10 @@ export { useSiteApi } from './composables/useSiteApi.js';
5
5
  export { useLivePreview, setupPreviewRouter, isInPreview } from './composables/useLivePreview.js';
6
6
  export { usePageData } from './composables/usePageData.js';
7
7
  export type { UsePageDataReturn } from './composables/usePageData.js';
8
+ export { useSiteComponents } from './composables/useSiteComponents.js';
9
+ export { useLocaleSwitcher } from './composables/useLocaleSwitcher.js';
10
+ export { useDynamicLayout, buildLayoutCSS } from './composables/useDynamicLayout.js';
11
+ export type { LayoutConfig, LayoutZone, GridBreakpoint } from './composables/useDynamicLayout.js';
8
12
  export type { PreviewMessage } from './composables/useLivePreview.js';
9
13
  export { createSiteWorker } from './worker/index.js';
10
14
  export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
package/dist/index.js CHANGED
@@ -1,50 +1,51 @@
1
- import { ref as y, onMounted as S, watch as _, computed as f, onUnmounted as x, provide as C } from "vue";
2
- import { useI18n as b } from "vue-i18n";
3
- function k(o, n) {
4
- const t = y(null), { locale: e } = b(), s = window.__SITE_DATA__, a = s?.locale;
5
- function l() {
6
- return s && s[o] && e.value === a ? (t.value = s[o], !0) : !1;
1
+ import { ref as w, onMounted as T, watch as _, computed as u, onUnmounted as N, provide as I } from "vue";
2
+ import { useI18n as $ } from "vue-i18n";
3
+ import { useRoute as P } from "vue-router";
4
+ function B(o, n) {
5
+ const t = w(null), { locale: e } = $(), a = window.__SITE_DATA__, s = a?.locale;
6
+ function i() {
7
+ return a && a[o] && e.value === s ? (t.value = a[o], !0) : !1;
7
8
  }
8
- async function u() {
9
+ async function l() {
9
10
  try {
10
- const i = await fetch(`/api/content/${n}:${e.value}`);
11
- i.ok && (t.value = await i.json());
11
+ const c = await fetch(`/api/content/${n}:${e.value}`);
12
+ c.ok && (t.value = await c.json());
12
13
  } catch {
13
14
  }
14
15
  }
15
- return S(() => {
16
- l() || u();
16
+ return T(() => {
17
+ i() || l();
17
18
  }), _(e, () => {
18
- u();
19
+ l();
19
20
  }), t;
20
21
  }
21
- function M() {
22
- const o = window.__SITE_DATA__, n = f(() => o?.config || null), t = f(() => o?.components || {}), e = f(() => n.value?.navigation || []), s = f(() => n.value?.locales || []), a = f(() => n.value?.defaultLocale || "en"), l = f(() => n.value?.branding || {}), u = f(() => n.value?.footer || {}), i = f(() => n.value?.features || {});
23
- function g(w) {
24
- const v = i.value[w];
22
+ function G() {
23
+ const o = window.__SITE_DATA__, n = u(() => o?.config || null), t = u(() => o?.components || {}), e = u(() => n.value?.navigation || []), a = u(() => n.value?.locales || []), s = u(() => n.value?.defaultLocale || "en"), i = u(() => n.value?.branding || {}), l = u(() => n.value?.footer || {}), c = u(() => n.value?.features || {});
24
+ function f(d) {
25
+ const v = c.value[d];
25
26
  return typeof v == "boolean" ? v : typeof v == "object";
26
27
  }
27
28
  return {
28
29
  config: n,
29
30
  components: t,
30
31
  navigation: e,
31
- locales: s,
32
- defaultLocale: a,
33
- branding: l,
34
- footer: u,
35
- features: i,
36
- hasFeature: g
32
+ locales: a,
33
+ defaultLocale: s,
34
+ branding: i,
35
+ footer: l,
36
+ features: c,
37
+ hasFeature: f
37
38
  };
38
39
  }
39
- function U(o) {
40
+ function Y(o) {
40
41
  function n(t) {
41
42
  const e = document.documentElement;
42
43
  if (t.colors)
43
- for (const [s, a] of Object.entries(t.colors))
44
- a && e.style.setProperty(`--x-color-${N(s)}`, a);
44
+ for (const [a, s] of Object.entries(t.colors))
45
+ s && e.style.setProperty(`--x-color-${x(a)}`, s);
45
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);
46
47
  }
47
- S(() => {
48
+ T(() => {
48
49
  if (o) {
49
50
  n(o);
50
51
  return;
@@ -57,132 +58,252 @@ function U(o) {
57
58
  });
58
59
  });
59
60
  }
60
- function N(o) {
61
+ function x(o) {
61
62
  return o.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
62
63
  }
63
- function J() {
64
+ function X() {
64
65
  const n = window.__SITE_DATA__?.apiBase || "";
65
- async function t(a) {
66
- const l = await fetch(`${n}${a}`);
67
- if (!l.ok)
68
- throw new Error(`API error: ${l.status}`);
69
- return l.json();
66
+ async function t(s) {
67
+ const i = await fetch(`${n}${s}`);
68
+ if (!i.ok)
69
+ throw new Error(`API error: ${i.status}`);
70
+ return i.json();
70
71
  }
71
72
  async function e() {
72
73
  return t("/v1/billing/tariffs");
73
74
  }
74
- async function s() {
75
+ async function a() {
75
76
  return t("/v1/billing/services");
76
77
  }
77
78
  return {
78
79
  get: t,
79
80
  getTariffs: e,
80
- getProducts: s
81
+ getProducts: a
81
82
  };
82
83
  }
83
- function E() {
84
+ function b() {
84
85
  try {
85
86
  return window.self !== window.top;
86
87
  } catch {
87
88
  return !0;
88
89
  }
89
90
  }
90
- function z(o) {
91
- E() && o.beforeEach((n, t) => !t.name);
91
+ function Z(o) {
92
+ b() && o.beforeEach((n, t) => !t.name);
92
93
  }
93
- function P() {
94
- const o = y(null), n = y(E());
94
+ function C() {
95
+ const o = w(null), n = w(b());
95
96
  function t(e) {
96
- const s = e.data;
97
- s?.type === "xosen-preview-update" && s.page && (o.value = s.page, n.value = !0);
97
+ const a = e.data;
98
+ a?.type === "xosen-preview-update" && a.page && (o.value = a.page, n.value = !0);
98
99
  }
99
- return S(() => {
100
+ return T(() => {
100
101
  window.addEventListener("message", t);
101
- }), x(() => {
102
+ }), N(() => {
102
103
  window.removeEventListener("message", t);
103
104
  }), {
104
105
  previewPage: o,
105
106
  isPreviewMode: n
106
107
  };
107
108
  }
108
- const I = /* @__PURE__ */ Symbol("pageContext");
109
- function K(o) {
110
- const { locale: n } = b(), { previewPage: t, isPreviewMode: e } = P(), s = window.__SITE_DATA__, a = y(null), l = y(!0), u = y(!1), i = f(() => typeof o == "string" ? o : o.value), g = f(() => `page:${i.value}`), w = f(() => {
111
- const r = e.value ? t.value : a.value;
109
+ const D = /* @__PURE__ */ Symbol("pageContext");
110
+ function H(o) {
111
+ const { locale: n } = $(), { previewPage: t, isPreviewMode: e } = C(), a = window.__SITE_DATA__, s = w(null), i = w(!0), l = w(!1), c = u(() => typeof o == "string" ? o : o.value), f = u(() => `page:${c.value}`), d = u(() => {
112
+ const r = e.value ? t.value : s.value;
112
113
  if (!r) return {};
113
- const { blocks: m, meta: c, slug: d, locale: j, ...A } = r;
114
- return A;
114
+ const { blocks: y, meta: p, slug: m, locale: z, ...L } = r;
115
+ return L;
115
116
  });
116
- C(I, w);
117
- const v = f(() => e.value && t.value?.slug === i.value ? t.value.blocks || [] : a.value?.blocks || []);
118
- function T() {
119
- const r = s?.[g.value];
120
- return r && n.value === s?.locale ? (a.value = r, u.value = !1, h(), !0) : !1;
117
+ I(D, d);
118
+ const v = u(() => e.value && t.value?.slug === c.value ? t.value.blocks || [] : s.value?.blocks || []);
119
+ function S() {
120
+ const r = a?.[f.value];
121
+ return r && n.value === a?.locale ? (s.value = r, l.value = !1, h(), !0) : !1;
121
122
  }
122
- async function p() {
123
- l.value = !0, u.value = !1;
123
+ async function g() {
124
+ i.value = !0, l.value = !1;
124
125
  try {
125
- const r = await fetch(`/api/content/${g.value}:${n.value}`);
126
- r.ok ? (a.value = await r.json(), h()) : (a.value = null, u.value = !0);
126
+ const r = await fetch(`/api/content/${f.value}:${n.value}`);
127
+ r.ok ? (s.value = await r.json(), h()) : (s.value = null, l.value = !0);
127
128
  } catch {
128
- a.value = null, u.value = !0;
129
+ s.value = null, l.value = !0;
129
130
  } finally {
130
- l.value = !1;
131
+ i.value = !1;
131
132
  }
132
133
  }
133
134
  function h() {
134
- if (!a.value) return;
135
- if (a.value.title) {
136
- const m = s?.config?.branding, c = m?.siteName ? ` — ${m.siteName}` : "";
137
- document.title = `${a.value.title}${c}`;
135
+ if (!s.value) return;
136
+ if (s.value.title) {
137
+ const y = a?.config?.branding, p = y?.siteName ? ` — ${y.siteName}` : "";
138
+ document.title = `${s.value.title}${p}`;
138
139
  }
139
- const r = a.value.meta?.description;
140
+ const r = s.value.meta?.description;
140
141
  if (r) {
141
- const m = document.querySelector('meta[name="description"]');
142
- m && m.setAttribute("content", r);
142
+ const y = document.querySelector('meta[name="description"]');
143
+ y && y.setAttribute("content", r);
143
144
  }
144
145
  }
145
146
  return _(t, (r) => {
146
- r?.slug === i.value && (l.value = !1, u.value = !1, (r.title || r.blocks) && (a.value = {
147
- ...a.value || {},
147
+ r?.slug === c.value && (i.value = !1, l.value = !1, (r.title || r.blocks) && (s.value = {
148
+ ...s.value || {},
148
149
  ...r.title ? { title: r.title } : {},
149
150
  ...r.blocks ? { blocks: r.blocks } : {}
150
151
  }, h()));
151
- }), S(() => {
152
- T() ? l.value = !1 : p();
153
- }), _(n, () => p()), _(i, () => {
154
- T() ? l.value = !1 : p();
152
+ }), T(() => {
153
+ S() ? i.value = !1 : g();
154
+ }), _(n, () => g()), _(c, () => {
155
+ S() ? i.value = !1 : g();
155
156
  }), {
156
- page: a,
157
+ page: s,
157
158
  blocks: v,
158
- pageContext: w,
159
- loading: l,
160
- notFound: u,
159
+ pageContext: d,
160
+ loading: i,
161
+ notFound: l,
161
162
  isPreviewMode: e,
162
- reload: p
163
+ reload: g
164
+ };
165
+ }
166
+ function Q() {
167
+ const { locale: o } = $(), n = window.__SITE_DATA__, t = n?.locale, e = w({ ...n?.components || {} });
168
+ async function a(i) {
169
+ if (i === t) {
170
+ e.value = { ...n?.components || {} };
171
+ return;
172
+ }
173
+ try {
174
+ const l = await fetch(`/api/content/components:${i}`);
175
+ l.ok && (e.value = await l.json());
176
+ } catch {
177
+ }
178
+ }
179
+ _(o, (i) => a(i)), T(() => {
180
+ o.value !== t && a(o.value);
181
+ });
182
+ function s(i) {
183
+ return u(() => e.value[i] || null);
184
+ }
185
+ return {
186
+ /** All components (reactive, locale-aware) */
187
+ components: e,
188
+ /** Get a single component by key */
189
+ getComponent: s,
190
+ /** Force refetch for current locale */
191
+ reload: () => a(o.value)
192
+ };
193
+ }
194
+ function V() {
195
+ const { locale: o } = $(), n = window.__SITE_DATA__, t = u(() => n?.config?.locales || []), e = u(() => n?.config?.defaultLocale || "en"), a = u(() => t.value.length > 1);
196
+ function s(l) {
197
+ o.value = l, localStorage.setItem("x-site-locale", l);
198
+ }
199
+ function i(l) {
200
+ return typeof l == "string" ? l : l[o.value] || l[e.value] || Object.values(l)[0] || "";
201
+ }
202
+ return {
203
+ locale: o,
204
+ locales: t,
205
+ defaultLocale: e,
206
+ hasMultipleLocales: a,
207
+ switchLocale: s,
208
+ resolveText: i
209
+ };
210
+ }
211
+ const j = {
212
+ sm: 600,
213
+ md: 960,
214
+ lg: 1280,
215
+ xl: 1920
216
+ }, O = {
217
+ grid: {
218
+ default: {
219
+ areas: ["header", "main", "footer"],
220
+ rows: "auto 1fr auto"
221
+ }
222
+ },
223
+ zones: {
224
+ header: { component: "nav-menu" },
225
+ main: { type: "slot" },
226
+ footer: { component: "footer" }
227
+ }
228
+ }, R = {
229
+ grid: {
230
+ default: {
231
+ areas: ["main"]
232
+ }
233
+ },
234
+ zones: {
235
+ main: { type: "slot" }
236
+ }
237
+ };
238
+ function A(o, n) {
239
+ const t = [];
240
+ if (o.areas?.length) {
241
+ const e = o.areas.map((a) => `"${a}"`).join(" ");
242
+ t.push(`grid-template-areas: ${e}`);
243
+ }
244
+ 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 ? `${n} { ${t.join("; ")}; }` : "";
245
+ }
246
+ function F(o, n) {
247
+ const t = [`${n} { display: grid; min-height: 100vh; }`], e = o.grid;
248
+ if (!e) return t.join(`
249
+ `);
250
+ if (e.default) {
251
+ const a = A(e.default, n);
252
+ a && t.push(a);
253
+ }
254
+ for (const a of ["sm", "md", "lg", "xl"]) {
255
+ const s = e[a];
256
+ if (!s) continue;
257
+ const i = A(s, n);
258
+ i && t.push(`@media (min-width: ${j[a]}px) { ${i} }`);
259
+ }
260
+ return t.join(`
261
+ `);
262
+ }
263
+ function q() {
264
+ const o = P(), { previewPage: n, isPreviewMode: t } = C(), e = window.__SITE_DATA__, a = `dl-${Math.random().toString(36).slice(2, 8)}`, s = u(() => ["dynamic-layout", a]), i = u(() => {
265
+ if (o.path === "/_preview_")
266
+ return n.value?.layout || "landing";
267
+ if (t.value && n.value?.layout)
268
+ return n.value.layout;
269
+ const f = o.path === "/" ? "page:home" : `page:${o.params.slug || ""}`;
270
+ return e?.[f]?.layout || "default";
271
+ }), l = u(() => {
272
+ const f = e?.config?.layouts;
273
+ if (f) {
274
+ const d = f[i.value] || f.default;
275
+ if (d?.zones) return d;
276
+ }
277
+ return i.value === "landing" ? R : O;
278
+ }), c = u(() => F(l.value, `.${a}`));
279
+ return {
280
+ layoutName: i,
281
+ layoutConfig: l,
282
+ layoutCSS: c,
283
+ layoutClass: s
163
284
  };
164
285
  }
165
- const L = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
166
- function R(o, n) {
286
+ const M = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
287
+ function k(o, n) {
167
288
  const e = new URL(o.url).searchParams.get("lang");
168
289
  if (e && n.supportedLocales?.includes(e)) return e;
169
290
  if (n.supportedLocales?.length) {
170
- const s = o.headers.get("Accept-Language") || "";
171
- for (const a of n.supportedLocales)
172
- if (s.includes(a)) return a;
291
+ const a = o.headers.get("Accept-Language") || "";
292
+ for (const s of n.supportedLocales)
293
+ if (a.includes(s)) return s;
173
294
  }
174
295
  return n.defaultLocale;
175
296
  }
176
- function O(o, n, t) {
297
+ function U(o, n, t) {
177
298
  if (!o.pathname.startsWith("/api/content/")) return null;
178
299
  const e = decodeURIComponent(o.pathname.replace("/api/content/", ""));
179
300
  return e ? (async () => {
180
- let s = await n.SITE_CONTENT.get(e);
181
- if (!s) {
182
- const a = e.lastIndexOf(":");
183
- a > 0 && e.slice(a + 1) !== t && (s = await n.SITE_CONTENT.get(e.slice(0, a + 1) + t));
301
+ let a = await n.SITE_CONTENT.get(e);
302
+ if (!a) {
303
+ const s = e.lastIndexOf(":");
304
+ s > 0 && e.slice(s + 1) !== t && (a = await n.SITE_CONTENT.get(e.slice(0, s + 1) + t));
184
305
  }
185
- return s ? new Response(s, {
306
+ return a ? new Response(a, {
186
307
  headers: {
187
308
  "Content-Type": "application/json",
188
309
  "Cache-Control": "public, max-age=60",
@@ -191,54 +312,54 @@ function O(o, n, t) {
191
312
  }) : Response.json({ error: "Not found" }, { status: 404 });
192
313
  })() : Promise.resolve(Response.json({ error: "Key is required" }, { status: 400 }));
193
314
  }
194
- function $(o, n, t) {
315
+ function E(o, n, t) {
195
316
  let e = o;
196
317
  if (n.title) {
197
- const s = t ? ` — ${t}` : "";
198
- e = e.replace(/<title>[^<]*<\/title>/, `<title>${n.title}${s}</title>`);
318
+ const a = t ? ` — ${t}` : "";
319
+ e = e.replace(/<title>[^<]*<\/title>/, `<title>${n.title}${a}</title>`);
199
320
  }
200
321
  return n.meta?.description && (e = e.replace(/(<meta\s+name="description"\s+content=")[^"]*(")/, `$1${n.meta.description}$2`)), n.meta?.ogTitle && (e = e.replace(/(<meta\s+property="og:title"\s+content=")[^"]*(")/, `$1${n.meta.ogTitle}$2`)), n.html && (e = e.replace('<div id="app"></div>', `<div id="app">${n.html}</div>`)), e;
201
322
  }
202
- function W(o) {
323
+ function ee(o) {
203
324
  return {
204
325
  async fetch(n, t) {
205
- const e = new URL(n.url), s = O(e, t, o.defaultLocale);
206
- if (s) return s;
207
- if (L.test(e.pathname))
326
+ const e = new URL(n.url), a = U(e, t, o.defaultLocale);
327
+ if (a) return a;
328
+ if (M.test(e.pathname))
208
329
  return t.ASSETS.fetch(n);
209
- const a = R(n, o), l = new URL("/index.html", n.url);
210
- let i = await (await t.ASSETS.fetch(new Request(l))).text();
211
- const g = o.defaultLocale, [w, v] = await Promise.all([
330
+ const s = k(n, o), i = new URL("/index.html", n.url);
331
+ let c = await (await t.ASSETS.fetch(new Request(i))).text();
332
+ const f = o.defaultLocale, [d, v] = await Promise.all([
212
333
  t.SITE_CONTENT.get("config"),
213
- t.SITE_CONTENT.get(`content:${a}`)
214
- ]), T = w ? JSON.parse(w) : {};
215
- let p = v ? JSON.parse(v) : {};
216
- if (!v && a !== g) {
217
- const c = await t.SITE_CONTENT.get(`content:${g}`);
218
- c && (p = JSON.parse(c));
334
+ t.SITE_CONTENT.get(`content:${s}`)
335
+ ]), S = d ? JSON.parse(d) : {};
336
+ let g = v ? JSON.parse(v) : {};
337
+ if (!v && s !== f) {
338
+ const p = await t.SITE_CONTENT.get(`content:${f}`);
339
+ p && (g = JSON.parse(p));
219
340
  }
220
341
  const h = e.pathname.match(/^\/p\/(.+)$/);
221
342
  if (h) {
222
- const c = h[1];
223
- if (!p[c]) {
224
- let d = await t.SITE_CONTENT.get(`page:${c}:${a}`);
225
- !d && a !== g && (d = await t.SITE_CONTENT.get(`page:${c}:${g}`)), d && (p[c] = JSON.parse(d));
343
+ const p = h[1];
344
+ if (!g[p]) {
345
+ let m = await t.SITE_CONTENT.get(`page:${p}:${s}`);
346
+ !m && s !== f && (m = await t.SITE_CONTENT.get(`page:${p}:${f}`)), m && (g[p] = JSON.parse(m));
226
347
  }
227
348
  }
228
349
  const r = {
229
- locale: a,
230
- config: T
350
+ locale: s,
351
+ config: S
231
352
  };
232
- for (const [c, d] of Object.entries(p))
233
- r[`page:${c}`] = d;
353
+ for (const [p, m] of Object.entries(g))
354
+ r[`page:${p}`] = m;
234
355
  if (h) {
235
- const c = h[1], d = r[`page:${c}`];
236
- d && (i = $(i, d, o.siteName));
356
+ const p = h[1], m = r[`page:${p}`];
357
+ m && (c = E(c, m, o.siteName));
237
358
  }
238
- e.pathname === "/" && p.home && (i = $(i, p.home, o.siteName));
239
- const m = `<script>window.__SITE_DATA__ = ${JSON.stringify(r)};<\/script>`;
240
- return i = i.replace("</head>", `${m}
241
- </head>`), new Response(i, {
359
+ e.pathname === "/" && g.home && (c = E(c, g.home, o.siteName));
360
+ const y = `<script>window.__SITE_DATA__ = ${JSON.stringify(r)};<\/script>`;
361
+ return c = c.replace("</head>", `${y}
362
+ </head>`), new Response(c, {
242
363
  headers: {
243
364
  "Content-Type": "text/html;charset=utf-8",
244
365
  "Cache-Control": "public, max-age=60"
@@ -248,14 +369,18 @@ function W(o) {
248
369
  };
249
370
  }
250
371
  export {
251
- I as PAGE_CONTEXT_KEY,
252
- W as createSiteWorker,
253
- E as isInPreview,
254
- z as setupPreviewRouter,
255
- P as useLivePreview,
256
- K as usePageData,
257
- J as useSiteApi,
258
- M as useSiteConfig,
259
- k as useSiteData,
260
- U as useSkin
372
+ D as PAGE_CONTEXT_KEY,
373
+ F as buildLayoutCSS,
374
+ ee as createSiteWorker,
375
+ b as isInPreview,
376
+ Z as setupPreviewRouter,
377
+ q as useDynamicLayout,
378
+ C as useLivePreview,
379
+ V as useLocaleSwitcher,
380
+ H as usePageData,
381
+ X as useSiteApi,
382
+ Q as useSiteComponents,
383
+ G as useSiteConfig,
384
+ B as useSiteData,
385
+ Y as useSkin
261
386
  };
@@ -33,6 +33,24 @@ export interface SiteConfig {
33
33
  }>;
34
34
  };
35
35
  features?: Record<string, boolean | Record<string, any>>;
36
+ layouts?: Record<string, LayoutConfig>;
37
+ }
38
+ export interface LayoutZone {
39
+ type?: 'slot';
40
+ component?: string;
41
+ class?: string;
42
+ container?: boolean;
43
+ containerStyle?: Record<string, string>;
44
+ }
45
+ export interface LayoutGridBreakpoint {
46
+ areas: string[];
47
+ columns?: string;
48
+ rows?: string;
49
+ gap?: string;
50
+ }
51
+ export interface LayoutConfig {
52
+ grid: Record<string, LayoutGridBreakpoint>;
53
+ zones: Record<string, LayoutZone>;
36
54
  }
37
55
  export interface NavItem {
38
56
  text: string | Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xosen/site-sdk",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
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
- },
32
27
  "peerDependencies": {
33
28
  "vue": "^3.5.0",
34
29
  "vue-i18n": "^11.0.0",
35
30
  "vue-router": "^5.0.0"
36
31
  },
37
32
  "devDependencies": {
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:"
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"
45
45
  }
46
- }
46
+ }
@@ -0,0 +1,146 @@
1
+ import { computed, type Ref } from 'vue';
2
+ import { useRoute } from 'vue-router';
3
+
4
+ import type { SiteData } from '../types/config.js';
5
+ import { useLivePreview } from './useLivePreview.js';
6
+
7
+ export interface GridBreakpoint {
8
+ areas?: string[];
9
+ columns?: string;
10
+ rows?: string;
11
+ gap?: string;
12
+ }
13
+
14
+ export interface LayoutZone {
15
+ type?: 'slot';
16
+ component?: string;
17
+ class?: string;
18
+ container?: boolean;
19
+ containerStyle?: Record<string, string>;
20
+ }
21
+
22
+ export interface LayoutConfig {
23
+ grid?: Record<string, GridBreakpoint>;
24
+ zones: Record<string, LayoutZone>;
25
+ }
26
+
27
+ const BREAKPOINTS: Record<string, number> = {
28
+ sm: 600,
29
+ md: 960,
30
+ lg: 1280,
31
+ xl: 1920,
32
+ };
33
+
34
+ const DEFAULT_LAYOUT: LayoutConfig = {
35
+ grid: {
36
+ default: {
37
+ areas: ['header', 'main', 'footer'],
38
+ rows: 'auto 1fr auto',
39
+ },
40
+ },
41
+ zones: {
42
+ header: { component: 'nav-menu' },
43
+ main: { type: 'slot' },
44
+ footer: { component: 'footer' },
45
+ },
46
+ };
47
+
48
+ const LANDING_LAYOUT: LayoutConfig = {
49
+ grid: {
50
+ default: {
51
+ areas: ['main'],
52
+ },
53
+ },
54
+ zones: {
55
+ main: { type: 'slot' },
56
+ },
57
+ };
58
+
59
+ function buildGridCSS(bp: GridBreakpoint, selector: string): string {
60
+ const rules: string[] = [];
61
+ if (bp.areas?.length) {
62
+ const areas = bp.areas.map((row) => `"${row}"`).join(' ');
63
+ rules.push(`grid-template-areas: ${areas}`);
64
+ }
65
+ if (bp.columns) rules.push(`grid-template-columns: ${bp.columns}`);
66
+ if (bp.rows) rules.push(`grid-template-rows: ${bp.rows}`);
67
+ if (bp.gap) rules.push(`gap: ${bp.gap}`);
68
+ if (!rules.length) return '';
69
+ return `${selector} { ${rules.join('; ')}; }`;
70
+ }
71
+
72
+ /**
73
+ * Generate responsive CSS for a layout config.
74
+ * Returns a CSS string with media queries for each breakpoint.
75
+ */
76
+ export function buildLayoutCSS(layout: LayoutConfig, selector: string): string {
77
+ const parts: string[] = [`${selector} { display: grid; min-height: 100vh; }`];
78
+ const grid = layout.grid;
79
+
80
+ if (!grid) return parts.join('\n');
81
+
82
+ if (grid.default) {
83
+ const css = buildGridCSS(grid.default, selector);
84
+ if (css) parts.push(css);
85
+ }
86
+
87
+ for (const bp of ['sm', 'md', 'lg', 'xl']) {
88
+ const config = grid[bp];
89
+ if (!config) continue;
90
+ const css = buildGridCSS(config, selector);
91
+ if (css) {
92
+ parts.push(`@media (min-width: ${BREAKPOINTS[bp]}px) { ${css} }`);
93
+ }
94
+ }
95
+
96
+ return parts.join('\n');
97
+ }
98
+
99
+ /**
100
+ * Resolve the current layout config from site data, route, and preview state.
101
+ *
102
+ * Returns:
103
+ * - `layoutName` — current layout name (e.g. 'default', 'sidebar')
104
+ * - `layoutConfig` — resolved LayoutConfig with grid + zones
105
+ * - `layoutCSS` — generated responsive CSS string
106
+ * - `layoutClass` — unique CSS class for this layout instance
107
+ */
108
+ export function useDynamicLayout() {
109
+ const route = useRoute();
110
+ const { previewPage, isPreviewMode } = useLivePreview();
111
+ const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
112
+
113
+ const layoutId = `dl-${Math.random().toString(36).slice(2, 8)}`;
114
+ const layoutClass = computed(() => ['dynamic-layout', layoutId]);
115
+
116
+ const layoutName = computed(() => {
117
+ if (route.path === '/_preview_') {
118
+ return previewPage.value?.layout || 'landing';
119
+ }
120
+ if (isPreviewMode.value && previewPage.value?.layout) {
121
+ return previewPage.value.layout;
122
+ }
123
+ const pageKey = route.path === '/' ? 'page:home' : `page:${route.params.slug || ''}`;
124
+ const pageData = siteData?.[pageKey];
125
+ return pageData?.layout || 'default';
126
+ });
127
+
128
+ const layoutConfig = computed<LayoutConfig>(() => {
129
+ const layouts = siteData?.config?.layouts;
130
+ if (layouts) {
131
+ const config = layouts[layoutName.value] || layouts.default;
132
+ if (config?.zones) return config;
133
+ }
134
+ if (layoutName.value === 'landing') return LANDING_LAYOUT;
135
+ return DEFAULT_LAYOUT;
136
+ });
137
+
138
+ const layoutCSS = computed(() => buildLayoutCSS(layoutConfig.value, `.${layoutId}`));
139
+
140
+ return {
141
+ layoutName,
142
+ layoutConfig,
143
+ layoutCSS,
144
+ layoutClass,
145
+ };
146
+ }
@@ -0,0 +1,40 @@
1
+ import { computed } from 'vue';
2
+ import { useI18n } from 'vue-i18n';
3
+
4
+ import type { SiteData } from '../types/config.js';
5
+
6
+ /**
7
+ * Locale switcher composable.
8
+ * Manages locale state, available locales, and persistence.
9
+ */
10
+ export function useLocaleSwitcher() {
11
+ const { locale } = useI18n();
12
+ const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
13
+
14
+ const locales = computed(() => siteData?.config?.locales || []);
15
+ const defaultLocale = computed(() => siteData?.config?.defaultLocale || 'en');
16
+ const hasMultipleLocales = computed(() => locales.value.length > 1);
17
+
18
+ function switchLocale(loc: string) {
19
+ locale.value = loc;
20
+ localStorage.setItem('x-site-locale', loc);
21
+ }
22
+
23
+ /**
24
+ * Resolve a text value that may be a string or a locale map.
25
+ * e.g. "Home" or { "en": "Home", "uk": "Головна" }
26
+ */
27
+ function resolveText(text: string | Record<string, string>): string {
28
+ if (typeof text === 'string') return text;
29
+ return text[locale.value] || text[defaultLocale.value] || Object.values(text)[0] || '';
30
+ }
31
+
32
+ return {
33
+ locale,
34
+ locales,
35
+ defaultLocale,
36
+ hasMultipleLocales,
37
+ switchLocale,
38
+ resolveText,
39
+ };
40
+ }
@@ -0,0 +1,59 @@
1
+ import { ref, computed, watch, onMounted, type Ref } from 'vue';
2
+ import { useI18n } from 'vue-i18n';
3
+
4
+ import type { SiteData } from '../types/config.js';
5
+
6
+ /**
7
+ * Locale-aware access to site components (navigation, footer, sidebar, etc.).
8
+ *
9
+ * On initial load, reads from preloaded __SITE_DATA__.components.
10
+ * When locale changes, fetches updated components from the API.
11
+ */
12
+ export function useSiteComponents() {
13
+ const { locale } = useI18n();
14
+ const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
15
+ const initialLocale = siteData?.locale;
16
+
17
+ const components = ref<Record<string, any>>({ ...(siteData as any)?.components || {} });
18
+
19
+ async function fetchComponents(loc: string) {
20
+ // If same as initial locale, use preloaded data
21
+ if (loc === initialLocale) {
22
+ components.value = { ...(siteData as any)?.components || {} };
23
+ return;
24
+ }
25
+ try {
26
+ const res = await fetch(`/api/content/components:${loc}`);
27
+ if (res.ok) {
28
+ components.value = await res.json();
29
+ }
30
+ } catch {
31
+ // Keep current
32
+ }
33
+ }
34
+
35
+ watch(locale, (newLocale) => fetchComponents(newLocale));
36
+
37
+ onMounted(() => {
38
+ if (locale.value !== initialLocale) {
39
+ fetchComponents(locale.value);
40
+ }
41
+ });
42
+
43
+ /**
44
+ * Get a specific component's data by key.
45
+ * Returns a computed ref that updates on locale change.
46
+ */
47
+ function getComponent<T = any>(key: string): Ref<T | null> {
48
+ return computed(() => components.value[key] || null) as Ref<T | null>;
49
+ }
50
+
51
+ return {
52
+ /** All components (reactive, locale-aware) */
53
+ components,
54
+ /** Get a single component by key */
55
+ getComponent,
56
+ /** Force refetch for current locale */
57
+ reload: () => fetchComponents(locale.value),
58
+ };
59
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,10 @@ export { useSiteApi } from './composables/useSiteApi.js';
6
6
  export { useLivePreview, setupPreviewRouter, isInPreview } from './composables/useLivePreview.js';
7
7
  export { usePageData } from './composables/usePageData.js';
8
8
  export type { UsePageDataReturn } from './composables/usePageData.js';
9
+ export { useSiteComponents } from './composables/useSiteComponents.js';
10
+ export { useLocaleSwitcher } from './composables/useLocaleSwitcher.js';
11
+ export { useDynamicLayout, buildLayoutCSS } from './composables/useDynamicLayout.js';
12
+ export type { LayoutConfig, LayoutZone, GridBreakpoint } from './composables/useDynamicLayout.js';
9
13
  export type { PreviewMessage } from './composables/useLivePreview.js';
10
14
 
11
15
  // Worker
@@ -38,6 +38,28 @@ export interface SiteConfig {
38
38
  };
39
39
 
40
40
  features?: Record<string, boolean | Record<string, any>>;
41
+
42
+ layouts?: Record<string, LayoutConfig>;
43
+ }
44
+
45
+ export interface LayoutZone {
46
+ type?: 'slot';
47
+ component?: string;
48
+ class?: string;
49
+ container?: boolean;
50
+ containerStyle?: Record<string, string>;
51
+ }
52
+
53
+ export interface LayoutGridBreakpoint {
54
+ areas: string[];
55
+ columns?: string;
56
+ rows?: string;
57
+ gap?: string;
58
+ }
59
+
60
+ export interface LayoutConfig {
61
+ grid: Record<string, LayoutGridBreakpoint>;
62
+ zones: Record<string, LayoutZone>;
41
63
  }
42
64
 
43
65
  export interface NavItem {