@xosen/site-sdk 0.0.11 → 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
+ };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { usePageData } from './composables/usePageData.js';
7
7
  export type { UsePageDataReturn } from './composables/usePageData.js';
8
8
  export { useSiteComponents } from './composables/useSiteComponents.js';
9
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';
10
12
  export type { PreviewMessage } from './composables/useLivePreview.js';
11
13
  export { createSiteWorker } from './worker/index.js';
12
14
  export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
- import { ref as y, onMounted as T, watch as _, computed as u, onUnmounted as C, provide as L } from "vue";
1
+ import { ref as w, onMounted as T, watch as _, computed as u, onUnmounted as N, provide as I } from "vue";
2
2
  import { useI18n as $ } from "vue-i18n";
3
- function k(o, n) {
4
- const t = y(null), { locale: e } = $(), s = window.__SITE_DATA__, a = s?.locale;
5
- function l() {
6
- return s && s[o] && e.value === a ? (t.value = s[o], !0) : !1;
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 i() {
9
+ async function l() {
9
10
  try {
10
11
  const c = await fetch(`/api/content/${n}:${e.value}`);
11
12
  c.ok && (t.value = await c.json());
@@ -13,35 +14,35 @@ function k(o, n) {
13
14
  }
14
15
  }
15
16
  return T(() => {
16
- l() || i();
17
+ i() || l();
17
18
  }), _(e, () => {
18
- i();
19
+ l();
19
20
  }), t;
20
21
  }
21
- function M() {
22
- const o = window.__SITE_DATA__, n = u(() => o?.config || null), t = u(() => o?.components || {}), e = u(() => n.value?.navigation || []), s = u(() => n.value?.locales || []), a = u(() => n.value?.defaultLocale || "en"), l = u(() => n.value?.branding || {}), i = u(() => n.value?.footer || {}), c = u(() => n.value?.features || {});
23
- function v(w) {
24
- const g = c.value[w];
25
- return typeof g == "boolean" ? g : typeof g == "object";
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];
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: i,
32
+ locales: a,
33
+ defaultLocale: s,
34
+ branding: i,
35
+ footer: l,
35
36
  features: c,
36
- hasFeature: v
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
48
  T(() => {
@@ -57,177 +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
100
  return T(() => {
100
101
  window.addEventListener("message", t);
101
- }), C(() => {
102
+ }), N(() => {
102
103
  window.removeEventListener("message", t);
103
104
  }), {
104
105
  previewPage: o,
105
106
  isPreviewMode: n
106
107
  };
107
108
  }
108
- const x = /* @__PURE__ */ Symbol("pageContext");
109
- function K(o) {
110
- const { locale: n } = $(), { previewPage: t, isPreviewMode: e } = P(), s = window.__SITE_DATA__, a = y(null), l = y(!0), i = y(!1), c = u(() => typeof o == "string" ? o : o.value), v = u(() => `page:${c.value}`), w = u(() => {
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: h, meta: f, slug: d, locale: O, ...A } = r;
114
- return A;
114
+ const { blocks: y, meta: p, slug: m, locale: z, ...L } = r;
115
+ return L;
115
116
  });
116
- L(x, w);
117
- const g = u(() => e.value && t.value?.slug === c.value ? t.value.blocks || [] : a.value?.blocks || []);
117
+ I(D, d);
118
+ const v = u(() => e.value && t.value?.slug === c.value ? t.value.blocks || [] : s.value?.blocks || []);
118
119
  function S() {
119
- const r = s?.[v.value];
120
- return r && n.value === s?.locale ? (a.value = r, i.value = !1, m(), !0) : !1;
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, i.value = !1;
123
+ async function g() {
124
+ i.value = !0, l.value = !1;
124
125
  try {
125
- const r = await fetch(`/api/content/${v.value}:${n.value}`);
126
- r.ok ? (a.value = await r.json(), m()) : (a.value = null, i.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, i.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
- function m() {
134
- if (!a.value) return;
135
- if (a.value.title) {
136
- const h = s?.config?.branding, f = h?.siteName ? ` — ${h.siteName}` : "";
137
- document.title = `${a.value.title}${f}`;
134
+ function h() {
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 h = document.querySelector('meta[name="description"]');
142
- h && h.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 === c.value && (l.value = !1, i.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
- }, m()));
151
+ }, h()));
151
152
  }), T(() => {
152
- S() ? l.value = !1 : p();
153
- }), _(n, () => p()), _(c, () => {
154
- S() ? l.value = !1 : p();
153
+ S() ? i.value = !1 : g();
154
+ }), _(n, () => g()), _(c, () => {
155
+ S() ? i.value = !1 : g();
155
156
  }), {
156
- page: a,
157
- blocks: g,
158
- pageContext: w,
159
- loading: l,
160
- notFound: i,
157
+ page: s,
158
+ blocks: v,
159
+ pageContext: d,
160
+ loading: i,
161
+ notFound: l,
161
162
  isPreviewMode: e,
162
- reload: p
163
+ reload: g
163
164
  };
164
165
  }
165
- function W() {
166
- const { locale: o } = $(), n = window.__SITE_DATA__, t = n?.locale, e = y({ ...n?.components || {} });
167
- async function s(l) {
168
- if (l === t) {
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) {
169
170
  e.value = { ...n?.components || {} };
170
171
  return;
171
172
  }
172
173
  try {
173
- const i = await fetch(`/api/content/components:${l}`);
174
- i.ok && (e.value = await i.json());
174
+ const l = await fetch(`/api/content/components:${i}`);
175
+ l.ok && (e.value = await l.json());
175
176
  } catch {
176
177
  }
177
178
  }
178
- _(o, (l) => s(l)), T(() => {
179
- o.value !== t && s(o.value);
179
+ _(o, (i) => a(i)), T(() => {
180
+ o.value !== t && a(o.value);
180
181
  });
181
- function a(l) {
182
- return u(() => e.value[l] || null);
182
+ function s(i) {
183
+ return u(() => e.value[i] || null);
183
184
  }
184
185
  return {
185
186
  /** All components (reactive, locale-aware) */
186
187
  components: e,
187
188
  /** Get a single component by key */
188
- getComponent: a,
189
+ getComponent: s,
189
190
  /** Force refetch for current locale */
190
- reload: () => s(o.value)
191
+ reload: () => a(o.value)
191
192
  };
192
193
  }
193
- function B() {
194
- const { locale: o } = $(), n = window.__SITE_DATA__, t = u(() => n?.config?.locales || []), e = u(() => n?.config?.defaultLocale || "en"), s = u(() => t.value.length > 1);
195
- function a(i) {
196
- o.value = i, localStorage.setItem("x-site-locale", i);
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);
197
198
  }
198
- function l(i) {
199
- return typeof i == "string" ? i : i[o.value] || i[e.value] || Object.values(i)[0] || "";
199
+ function i(l) {
200
+ return typeof l == "string" ? l : l[o.value] || l[e.value] || Object.values(l)[0] || "";
200
201
  }
201
202
  return {
202
203
  locale: o,
203
204
  locales: t,
204
205
  defaultLocale: e,
205
- hasMultipleLocales: s,
206
- switchLocale: a,
207
- resolveText: l
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
208
284
  };
209
285
  }
210
- const I = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
211
- 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) {
212
288
  const e = new URL(o.url).searchParams.get("lang");
213
289
  if (e && n.supportedLocales?.includes(e)) return e;
214
290
  if (n.supportedLocales?.length) {
215
- const s = o.headers.get("Accept-Language") || "";
216
- for (const a of n.supportedLocales)
217
- 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;
218
294
  }
219
295
  return n.defaultLocale;
220
296
  }
221
- function j(o, n, t) {
297
+ function U(o, n, t) {
222
298
  if (!o.pathname.startsWith("/api/content/")) return null;
223
299
  const e = decodeURIComponent(o.pathname.replace("/api/content/", ""));
224
300
  return e ? (async () => {
225
- let s = await n.SITE_CONTENT.get(e);
226
- if (!s) {
227
- const a = e.lastIndexOf(":");
228
- 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));
229
305
  }
230
- return s ? new Response(s, {
306
+ return a ? new Response(a, {
231
307
  headers: {
232
308
  "Content-Type": "application/json",
233
309
  "Cache-Control": "public, max-age=60",
@@ -236,53 +312,53 @@ function j(o, n, t) {
236
312
  }) : Response.json({ error: "Not found" }, { status: 404 });
237
313
  })() : Promise.resolve(Response.json({ error: "Key is required" }, { status: 400 }));
238
314
  }
239
- function b(o, n, t) {
315
+ function E(o, n, t) {
240
316
  let e = o;
241
317
  if (n.title) {
242
- const s = t ? ` — ${t}` : "";
243
- 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>`);
244
320
  }
245
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;
246
322
  }
247
- function X(o) {
323
+ function ee(o) {
248
324
  return {
249
325
  async fetch(n, t) {
250
- const e = new URL(n.url), s = j(e, t, o.defaultLocale);
251
- if (s) return s;
252
- if (I.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))
253
329
  return t.ASSETS.fetch(n);
254
- const a = R(n, o), l = new URL("/index.html", n.url);
255
- let c = await (await t.ASSETS.fetch(new Request(l))).text();
256
- const v = o.defaultLocale, [w, g] = 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([
257
333
  t.SITE_CONTENT.get("config"),
258
- t.SITE_CONTENT.get(`content:${a}`)
259
- ]), S = w ? JSON.parse(w) : {};
260
- let p = g ? JSON.parse(g) : {};
261
- if (!g && a !== v) {
262
- const f = await t.SITE_CONTENT.get(`content:${v}`);
263
- f && (p = JSON.parse(f));
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));
264
340
  }
265
- const m = e.pathname.match(/^\/p\/(.+)$/);
266
- if (m) {
267
- const f = m[1];
268
- if (!p[f]) {
269
- let d = await t.SITE_CONTENT.get(`page:${f}:${a}`);
270
- !d && a !== v && (d = await t.SITE_CONTENT.get(`page:${f}:${v}`)), d && (p[f] = JSON.parse(d));
341
+ const h = e.pathname.match(/^\/p\/(.+)$/);
342
+ if (h) {
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));
271
347
  }
272
348
  }
273
349
  const r = {
274
- locale: a,
350
+ locale: s,
275
351
  config: S
276
352
  };
277
- for (const [f, d] of Object.entries(p))
278
- r[`page:${f}`] = d;
279
- if (m) {
280
- const f = m[1], d = r[`page:${f}`];
281
- d && (c = b(c, d, o.siteName));
353
+ for (const [p, m] of Object.entries(g))
354
+ r[`page:${p}`] = m;
355
+ if (h) {
356
+ const p = h[1], m = r[`page:${p}`];
357
+ m && (c = E(c, m, o.siteName));
282
358
  }
283
- e.pathname === "/" && p.home && (c = b(c, p.home, o.siteName));
284
- const h = `<script>window.__SITE_DATA__ = ${JSON.stringify(r)};<\/script>`;
285
- return c = c.replace("</head>", `${h}
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}
286
362
  </head>`), new Response(c, {
287
363
  headers: {
288
364
  "Content-Type": "text/html;charset=utf-8",
@@ -293,16 +369,18 @@ function X(o) {
293
369
  };
294
370
  }
295
371
  export {
296
- x as PAGE_CONTEXT_KEY,
297
- X as createSiteWorker,
298
- E as isInPreview,
299
- z as setupPreviewRouter,
300
- P as useLivePreview,
301
- B as useLocaleSwitcher,
302
- K as usePageData,
303
- J as useSiteApi,
304
- W as useSiteComponents,
305
- M as useSiteConfig,
306
- k as useSiteData,
307
- 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
308
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.11",
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
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,8 @@ export { usePageData } from './composables/usePageData.js';
8
8
  export type { UsePageDataReturn } from './composables/usePageData.js';
9
9
  export { useSiteComponents } from './composables/useSiteComponents.js';
10
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';
11
13
  export type { PreviewMessage } from './composables/useLivePreview.js';
12
14
 
13
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 {