@xosen/site-sdk 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composables/useDynamicLayout.d.ts +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +356 -174
- package/dist/types/blocks.d.ts +3 -0
- package/dist/types/config.d.ts +18 -0
- package/dist/types/layout.d.ts +48 -0
- package/package.json +14 -14
- package/src/composables/useDynamicLayout.ts +127 -0
- package/src/index.ts +3 -0
- package/src/types/blocks.ts +3 -0
- package/src/types/config.ts +22 -0
- package/src/types/layout.ts +158 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LayoutConfig } from '../types/layout.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate responsive CSS for a layout config.
|
|
4
|
+
* Returns a CSS string with media queries for each breakpoint.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildLayoutCSS(layout: LayoutConfig, selector: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the current layout config from site data, route, and preview state.
|
|
9
|
+
*
|
|
10
|
+
* Returns:
|
|
11
|
+
* - `layoutName` — current layout name (e.g. 'default', 'sidebar')
|
|
12
|
+
* - `layoutConfig` — resolved LayoutConfig with grid + zones
|
|
13
|
+
* - `layoutCSS` — generated responsive CSS string
|
|
14
|
+
* - `layoutClass` — unique CSS class for this layout instance
|
|
15
|
+
*/
|
|
16
|
+
export declare function useDynamicLayout(): {
|
|
17
|
+
layoutName: import("vue").ComputedRef<any>;
|
|
18
|
+
layoutConfig: import("vue").ComputedRef<LayoutConfig>;
|
|
19
|
+
layoutCSS: import("vue").ComputedRef<string>;
|
|
20
|
+
layoutClass: import("vue").ComputedRef<string[]>;
|
|
21
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ 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, LayoutSchemaDefinition, LayoutField } from './types/layout.js';
|
|
12
|
+
export { layoutSchema, layoutPresets } from './types/layout.js';
|
|
10
13
|
export type { PreviewMessage } from './composables/useLivePreview.js';
|
|
11
14
|
export { createSiteWorker } from './worker/index.js';
|
|
12
15
|
export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,233 +1,411 @@
|
|
|
1
|
-
import { ref as
|
|
2
|
-
import { useI18n as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { ref as w, onMounted as _, watch as b, computed as c, onUnmounted as E, provide as N } from "vue";
|
|
2
|
+
import { useI18n as S } from "vue-i18n";
|
|
3
|
+
import { useRoute as L } from "vue-router";
|
|
4
|
+
function J(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;
|
|
7
8
|
}
|
|
8
9
|
async function i() {
|
|
9
10
|
try {
|
|
10
|
-
const
|
|
11
|
-
|
|
11
|
+
const u = await fetch(`/api/content/${a}:${e.value}`);
|
|
12
|
+
u.ok && (t.value = await u.json());
|
|
12
13
|
} catch {
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
}),
|
|
16
|
+
return _(() => {
|
|
17
|
+
r() || i();
|
|
18
|
+
}), b(e, () => {
|
|
18
19
|
i();
|
|
19
20
|
}), t;
|
|
20
21
|
}
|
|
21
|
-
function
|
|
22
|
-
const o = window.__SITE_DATA__,
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
return typeof
|
|
22
|
+
function B() {
|
|
23
|
+
const o = window.__SITE_DATA__, a = c(() => o?.config || null), t = c(() => o?.components || {}), e = c(() => a.value?.navigation || []), n = c(() => a.value?.locales || []), s = c(() => a.value?.defaultLocale || "en"), r = c(() => a.value?.branding || {}), i = c(() => a.value?.footer || {}), u = c(() => a.value?.features || {});
|
|
24
|
+
function f(d) {
|
|
25
|
+
const y = u.value[d];
|
|
26
|
+
return typeof y == "boolean" ? y : typeof y == "object";
|
|
26
27
|
}
|
|
27
28
|
return {
|
|
28
|
-
config:
|
|
29
|
+
config: a,
|
|
29
30
|
components: t,
|
|
30
31
|
navigation: e,
|
|
31
|
-
locales:
|
|
32
|
-
defaultLocale:
|
|
33
|
-
branding:
|
|
32
|
+
locales: n,
|
|
33
|
+
defaultLocale: s,
|
|
34
|
+
branding: r,
|
|
34
35
|
footer: i,
|
|
35
|
-
features:
|
|
36
|
-
hasFeature:
|
|
36
|
+
features: u,
|
|
37
|
+
hasFeature: f
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
|
-
function
|
|
40
|
-
function
|
|
40
|
+
function Y(o) {
|
|
41
|
+
function a(t) {
|
|
41
42
|
const e = document.documentElement;
|
|
42
43
|
if (t.colors)
|
|
43
|
-
for (const [
|
|
44
|
-
|
|
44
|
+
for (const [n, s] of Object.entries(t.colors))
|
|
45
|
+
s && e.style.setProperty(`--x-color-${P(n)}`, 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
|
+
_(() => {
|
|
48
49
|
if (o) {
|
|
49
|
-
|
|
50
|
+
a(o);
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const e = window.__SITE_DATA__?.skin;
|
|
53
|
-
e && (e.colors || e.typography || e.shape) &&
|
|
54
|
+
e && (e.colors || e.typography || e.shape) && a({
|
|
54
55
|
colors: e.colors || {},
|
|
55
56
|
typography: e.typography || {},
|
|
56
57
|
shape: e.shape || {}
|
|
57
58
|
});
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
|
-
function
|
|
61
|
+
function P(o) {
|
|
61
62
|
return o.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
62
63
|
}
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
async function t(
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
throw new Error(`API error: ${
|
|
69
|
-
return
|
|
64
|
+
function X() {
|
|
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();
|
|
70
71
|
}
|
|
71
72
|
async function e() {
|
|
72
73
|
return t("/v1/billing/tariffs");
|
|
73
74
|
}
|
|
74
|
-
async function
|
|
75
|
+
async function n() {
|
|
75
76
|
return t("/v1/billing/services");
|
|
76
77
|
}
|
|
77
78
|
return {
|
|
78
79
|
get: t,
|
|
79
80
|
getTariffs: e,
|
|
80
|
-
getProducts:
|
|
81
|
+
getProducts: n
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
|
-
function
|
|
84
|
+
function x() {
|
|
84
85
|
try {
|
|
85
86
|
return window.self !== window.top;
|
|
86
87
|
} catch {
|
|
87
88
|
return !0;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
91
|
+
function Z(o) {
|
|
92
|
+
x() && o.beforeEach((a, t) => !t.name);
|
|
92
93
|
}
|
|
93
|
-
function
|
|
94
|
-
const o =
|
|
94
|
+
function A() {
|
|
95
|
+
const o = w(null), a = w(x());
|
|
95
96
|
function t(e) {
|
|
96
|
-
const
|
|
97
|
-
|
|
97
|
+
const n = e.data;
|
|
98
|
+
n?.type === "xosen-preview-update" && n.page && (o.value = n.page, a.value = !0);
|
|
98
99
|
}
|
|
99
|
-
return
|
|
100
|
+
return _(() => {
|
|
100
101
|
window.addEventListener("message", t);
|
|
101
|
-
}),
|
|
102
|
+
}), E(() => {
|
|
102
103
|
window.removeEventListener("message", t);
|
|
103
104
|
}), {
|
|
104
105
|
previewPage: o,
|
|
105
|
-
isPreviewMode:
|
|
106
|
+
isPreviewMode: a
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
|
-
const
|
|
109
|
-
function
|
|
110
|
-
const { locale:
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
113
|
-
const { blocks:
|
|
114
|
-
return
|
|
109
|
+
const I = /* @__PURE__ */ Symbol("pageContext");
|
|
110
|
+
function q(o) {
|
|
111
|
+
const { locale: a } = S(), { previewPage: t, isPreviewMode: e } = A(), n = window.__SITE_DATA__, s = w(null), r = w(!0), i = w(!1), u = c(() => typeof o == "string" ? o : o.value), f = c(() => `page:${u.value}`), d = c(() => {
|
|
112
|
+
const l = e.value ? t.value : s.value;
|
|
113
|
+
if (!l) return {};
|
|
114
|
+
const { blocks: v, meta: p, slug: g, locale: M, ...C } = l;
|
|
115
|
+
return C;
|
|
115
116
|
});
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
function
|
|
119
|
-
const
|
|
120
|
-
return
|
|
117
|
+
N(I, d);
|
|
118
|
+
const y = c(() => e.value && t.value?.slug === u.value ? t.value.blocks || [] : s.value?.blocks || []);
|
|
119
|
+
function T() {
|
|
120
|
+
const l = n?.[f.value];
|
|
121
|
+
return l && a.value === n?.locale ? (s.value = l, i.value = !1, h(), !0) : !1;
|
|
121
122
|
}
|
|
122
|
-
async function
|
|
123
|
-
|
|
123
|
+
async function m() {
|
|
124
|
+
r.value = !0, i.value = !1;
|
|
124
125
|
try {
|
|
125
|
-
const
|
|
126
|
-
|
|
126
|
+
const l = await fetch(`/api/content/${f.value}:${a.value}`);
|
|
127
|
+
l.ok ? (s.value = await l.json(), h()) : (s.value = null, i.value = !0);
|
|
127
128
|
} catch {
|
|
128
|
-
|
|
129
|
+
s.value = null, i.value = !0;
|
|
129
130
|
} finally {
|
|
130
|
-
|
|
131
|
+
r.value = !1;
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
function
|
|
134
|
-
if (!
|
|
135
|
-
if (
|
|
136
|
-
const
|
|
137
|
-
document.title = `${
|
|
134
|
+
function h() {
|
|
135
|
+
if (!s.value) return;
|
|
136
|
+
if (s.value.title) {
|
|
137
|
+
const v = n?.config?.branding, p = v?.siteName ? ` — ${v.siteName}` : "";
|
|
138
|
+
document.title = `${s.value.title}${p}`;
|
|
138
139
|
}
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
const
|
|
142
|
-
|
|
140
|
+
const l = s.value.meta?.description;
|
|
141
|
+
if (l) {
|
|
142
|
+
const v = document.querySelector('meta[name="description"]');
|
|
143
|
+
v && v.setAttribute("content", l);
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
...
|
|
148
|
-
...
|
|
149
|
-
...
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
|
|
153
|
-
}),
|
|
154
|
-
|
|
146
|
+
return b(t, (l) => {
|
|
147
|
+
l?.slug === u.value && (r.value = !1, i.value = !1, (l.title || l.blocks) && (s.value = {
|
|
148
|
+
...s.value || {},
|
|
149
|
+
...l.title ? { title: l.title } : {},
|
|
150
|
+
...l.blocks ? { blocks: l.blocks } : {}
|
|
151
|
+
}, h()));
|
|
152
|
+
}), _(() => {
|
|
153
|
+
T() ? r.value = !1 : m();
|
|
154
|
+
}), b(a, () => m()), b(u, () => {
|
|
155
|
+
T() ? r.value = !1 : m();
|
|
155
156
|
}), {
|
|
156
|
-
page:
|
|
157
|
-
blocks:
|
|
158
|
-
pageContext:
|
|
159
|
-
loading:
|
|
157
|
+
page: s,
|
|
158
|
+
blocks: y,
|
|
159
|
+
pageContext: d,
|
|
160
|
+
loading: r,
|
|
160
161
|
notFound: i,
|
|
161
162
|
isPreviewMode: e,
|
|
162
|
-
reload:
|
|
163
|
+
reload: m
|
|
163
164
|
};
|
|
164
165
|
}
|
|
165
|
-
function
|
|
166
|
-
const { locale: o } =
|
|
167
|
-
async function
|
|
168
|
-
if (
|
|
169
|
-
e.value = { ...
|
|
166
|
+
function H() {
|
|
167
|
+
const { locale: o } = S(), a = window.__SITE_DATA__, t = a?.locale, e = w({ ...a?.components || {} });
|
|
168
|
+
async function n(r) {
|
|
169
|
+
if (r === t) {
|
|
170
|
+
e.value = { ...a?.components || {} };
|
|
170
171
|
return;
|
|
171
172
|
}
|
|
172
173
|
try {
|
|
173
|
-
const i = await fetch(`/api/content/components:${
|
|
174
|
+
const i = await fetch(`/api/content/components:${r}`);
|
|
174
175
|
i.ok && (e.value = await i.json());
|
|
175
176
|
} catch {
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
|
-
|
|
179
|
-
o.value !== t &&
|
|
179
|
+
b(o, (r) => n(r)), _(() => {
|
|
180
|
+
o.value !== t && n(o.value);
|
|
180
181
|
});
|
|
181
|
-
function
|
|
182
|
-
return
|
|
182
|
+
function s(r) {
|
|
183
|
+
return c(() => e.value[r] || 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:
|
|
189
|
+
getComponent: s,
|
|
189
190
|
/** Force refetch for current locale */
|
|
190
|
-
reload: () =>
|
|
191
|
+
reload: () => n(o.value)
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
|
-
function
|
|
194
|
-
const { locale: o } =
|
|
195
|
-
function
|
|
194
|
+
function Q() {
|
|
195
|
+
const { locale: o } = S(), a = window.__SITE_DATA__, t = c(() => a?.config?.locales || []), e = c(() => a?.config?.defaultLocale || "en"), n = c(() => t.value.length > 1);
|
|
196
|
+
function s(i) {
|
|
196
197
|
o.value = i, localStorage.setItem("x-site-locale", i);
|
|
197
198
|
}
|
|
198
|
-
function
|
|
199
|
+
function r(i) {
|
|
199
200
|
return typeof i == "string" ? i : i[o.value] || i[e.value] || Object.values(i)[0] || "";
|
|
200
201
|
}
|
|
201
202
|
return {
|
|
202
203
|
locale: o,
|
|
203
204
|
locales: t,
|
|
204
205
|
defaultLocale: e,
|
|
205
|
-
hasMultipleLocales:
|
|
206
|
-
switchLocale:
|
|
207
|
-
resolveText:
|
|
206
|
+
hasMultipleLocales: n,
|
|
207
|
+
switchLocale: s,
|
|
208
|
+
resolveText: r
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
|
-
const
|
|
211
|
-
|
|
211
|
+
const R = {
|
|
212
|
+
sm: 600,
|
|
213
|
+
md: 960,
|
|
214
|
+
lg: 1280,
|
|
215
|
+
xl: 1920
|
|
216
|
+
}, D = {
|
|
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
|
+
}, j = {
|
|
229
|
+
grid: {
|
|
230
|
+
default: {
|
|
231
|
+
areas: ["main"]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
zones: {
|
|
235
|
+
main: { type: "slot" }
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function $(o, a) {
|
|
239
|
+
const t = [];
|
|
240
|
+
if (o.areas?.length) {
|
|
241
|
+
const e = o.areas.map((n) => `"${n}"`).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 ? `${a} { ${t.join("; ")}; }` : "";
|
|
245
|
+
}
|
|
246
|
+
function O(o, a) {
|
|
247
|
+
const t = [`${a} { display: grid; min-height: 100vh; }`], e = o.grid;
|
|
248
|
+
if (!e) return t.join(`
|
|
249
|
+
`);
|
|
250
|
+
if (e.default) {
|
|
251
|
+
const n = $(e.default, a);
|
|
252
|
+
n && t.push(n);
|
|
253
|
+
}
|
|
254
|
+
for (const n of ["sm", "md", "lg", "xl"]) {
|
|
255
|
+
const s = e[n];
|
|
256
|
+
if (!s) continue;
|
|
257
|
+
const r = $(s, a);
|
|
258
|
+
r && t.push(`@media (min-width: ${R[n]}px) { ${r} }`);
|
|
259
|
+
}
|
|
260
|
+
return t.join(`
|
|
261
|
+
`);
|
|
262
|
+
}
|
|
263
|
+
function V() {
|
|
264
|
+
const o = L(), { previewPage: a, isPreviewMode: t } = A(), e = window.__SITE_DATA__, n = `dl-${Math.random().toString(36).slice(2, 8)}`, s = c(() => ["dynamic-layout", n]), r = c(() => {
|
|
265
|
+
if (o.path === "/_preview_")
|
|
266
|
+
return a.value?.layout || "landing";
|
|
267
|
+
if (t.value && a.value?.layout)
|
|
268
|
+
return a.value.layout;
|
|
269
|
+
const f = o.path === "/" ? "page:home" : `page:${o.params.slug || ""}`;
|
|
270
|
+
return e?.[f]?.layout || "default";
|
|
271
|
+
}), i = c(() => {
|
|
272
|
+
const f = e?.config?.layouts;
|
|
273
|
+
if (f) {
|
|
274
|
+
const d = f[r.value] || f.default;
|
|
275
|
+
if (d?.zones) return d;
|
|
276
|
+
}
|
|
277
|
+
return r.value === "landing" ? j : D;
|
|
278
|
+
}), u = c(() => O(i.value, `.${n}`));
|
|
279
|
+
return {
|
|
280
|
+
layoutName: r,
|
|
281
|
+
layoutConfig: i,
|
|
282
|
+
layoutCSS: u,
|
|
283
|
+
layoutClass: s
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const ee = {
|
|
287
|
+
type: "layout",
|
|
288
|
+
displayName: "Layout",
|
|
289
|
+
description: "CSS Grid layout with responsive breakpoints and component zones",
|
|
290
|
+
fields: [
|
|
291
|
+
// Mobile (default)
|
|
292
|
+
{ key: "grid.default.areas", type: "grid-areas", label: "Grid Areas (mobile)", breakpoint: "default", required: !0 },
|
|
293
|
+
{ key: "grid.default.columns", type: "text", label: "Columns (mobile)", breakpoint: "default" },
|
|
294
|
+
{ key: "grid.default.rows", type: "text", label: "Rows (mobile)", breakpoint: "default" },
|
|
295
|
+
{ key: "grid.default.gap", type: "text", label: "Gap (mobile)", breakpoint: "default" },
|
|
296
|
+
// Tablet (sm — 600px)
|
|
297
|
+
{ key: "grid.sm.areas", type: "grid-areas", label: "Grid Areas (sm 600px+)", breakpoint: "sm" },
|
|
298
|
+
{ key: "grid.sm.columns", type: "text", label: "Columns (sm)", breakpoint: "sm" },
|
|
299
|
+
{ key: "grid.sm.rows", type: "text", label: "Rows (sm)", breakpoint: "sm" },
|
|
300
|
+
{ key: "grid.sm.gap", type: "text", label: "Gap (sm)", breakpoint: "sm" },
|
|
301
|
+
// Desktop (md — 960px)
|
|
302
|
+
{ key: "grid.md.areas", type: "grid-areas", label: "Grid Areas (md 960px+)", breakpoint: "md" },
|
|
303
|
+
{ key: "grid.md.columns", type: "text", label: "Columns (md)", breakpoint: "md" },
|
|
304
|
+
{ key: "grid.md.rows", type: "text", label: "Rows (md)", breakpoint: "md" },
|
|
305
|
+
{ key: "grid.md.gap", type: "text", label: "Gap (md)", breakpoint: "md" },
|
|
306
|
+
// Large desktop (lg — 1280px)
|
|
307
|
+
{ key: "grid.lg.areas", type: "grid-areas", label: "Grid Areas (lg 1280px+)", breakpoint: "lg" },
|
|
308
|
+
{ key: "grid.lg.columns", type: "text", label: "Columns (lg)", breakpoint: "lg" },
|
|
309
|
+
{ key: "grid.lg.rows", type: "text", label: "Rows (lg)", breakpoint: "lg" },
|
|
310
|
+
{ key: "grid.lg.gap", type: "text", label: "Gap (lg)", breakpoint: "lg" },
|
|
311
|
+
// Zones
|
|
312
|
+
{ key: "zones", type: "zone-list", label: "Zones", required: !0 }
|
|
313
|
+
],
|
|
314
|
+
zoneFields: [
|
|
315
|
+
{ key: "type", type: "select", label: "Type", options: ["slot", "component"] },
|
|
316
|
+
{ key: "component", type: "text", label: "Component Key" },
|
|
317
|
+
{ key: "class", type: "text", label: "CSS Class" },
|
|
318
|
+
{ key: "container", type: "switch", label: "Wrap in Container" }
|
|
319
|
+
]
|
|
320
|
+
}, te = {
|
|
321
|
+
default: {
|
|
322
|
+
displayName: "Full Width",
|
|
323
|
+
config: {
|
|
324
|
+
grid: {
|
|
325
|
+
default: { areas: ["header", "main", "footer"], rows: "auto 1fr auto" }
|
|
326
|
+
},
|
|
327
|
+
zones: {
|
|
328
|
+
header: { component: "nav-menu" },
|
|
329
|
+
main: { type: "slot" },
|
|
330
|
+
footer: { component: "footer" }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
sidebar: {
|
|
335
|
+
displayName: "Sidebar",
|
|
336
|
+
config: {
|
|
337
|
+
grid: {
|
|
338
|
+
default: { areas: ["header", "main", "sidebar", "footer"], columns: "1fr", rows: "auto 1fr auto auto" },
|
|
339
|
+
md: { areas: ["header header", "sidebar main", "footer footer"], columns: "280px 1fr", rows: "auto 1fr auto" }
|
|
340
|
+
},
|
|
341
|
+
zones: {
|
|
342
|
+
header: { component: "nav-menu" },
|
|
343
|
+
sidebar: { component: "sidebar-menu" },
|
|
344
|
+
main: { type: "slot" },
|
|
345
|
+
footer: { component: "footer" }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
article: {
|
|
350
|
+
displayName: "Article",
|
|
351
|
+
config: {
|
|
352
|
+
grid: {
|
|
353
|
+
default: { areas: ["header", "main", "footer"], rows: "auto 1fr auto" }
|
|
354
|
+
},
|
|
355
|
+
zones: {
|
|
356
|
+
header: { component: "nav-menu" },
|
|
357
|
+
main: { type: "slot", container: !0, containerStyle: { "max-width": "900px" } },
|
|
358
|
+
footer: { component: "footer" }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
landing: {
|
|
363
|
+
displayName: "Landing (no nav)",
|
|
364
|
+
config: {
|
|
365
|
+
grid: {
|
|
366
|
+
default: { areas: ["main"] }
|
|
367
|
+
},
|
|
368
|
+
zones: {
|
|
369
|
+
main: { type: "slot" }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
magazine: {
|
|
374
|
+
displayName: "Magazine",
|
|
375
|
+
config: {
|
|
376
|
+
grid: {
|
|
377
|
+
default: { areas: ["header", "main", "aside", "footer"], columns: "1fr", rows: "auto 1fr auto auto" },
|
|
378
|
+
md: { areas: ["header header header", "main main aside", "footer footer footer"], columns: "1fr 1fr 300px", rows: "auto 1fr auto", gap: "24px" }
|
|
379
|
+
},
|
|
380
|
+
zones: {
|
|
381
|
+
header: { component: "nav-menu" },
|
|
382
|
+
main: { type: "slot" },
|
|
383
|
+
aside: { component: "sidebar-menu" },
|
|
384
|
+
footer: { component: "footer" }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}, z = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
|
|
389
|
+
function F(o, a) {
|
|
212
390
|
const e = new URL(o.url).searchParams.get("lang");
|
|
213
|
-
if (e &&
|
|
214
|
-
if (
|
|
215
|
-
const
|
|
216
|
-
for (const
|
|
217
|
-
if (
|
|
391
|
+
if (e && a.supportedLocales?.includes(e)) return e;
|
|
392
|
+
if (a.supportedLocales?.length) {
|
|
393
|
+
const n = o.headers.get("Accept-Language") || "";
|
|
394
|
+
for (const s of a.supportedLocales)
|
|
395
|
+
if (n.includes(s)) return s;
|
|
218
396
|
}
|
|
219
|
-
return
|
|
397
|
+
return a.defaultLocale;
|
|
220
398
|
}
|
|
221
|
-
function
|
|
399
|
+
function G(o, a, t) {
|
|
222
400
|
if (!o.pathname.startsWith("/api/content/")) return null;
|
|
223
401
|
const e = decodeURIComponent(o.pathname.replace("/api/content/", ""));
|
|
224
402
|
return e ? (async () => {
|
|
225
|
-
let
|
|
226
|
-
if (!
|
|
227
|
-
const
|
|
228
|
-
|
|
403
|
+
let n = await a.SITE_CONTENT.get(e);
|
|
404
|
+
if (!n) {
|
|
405
|
+
const s = e.lastIndexOf(":");
|
|
406
|
+
s > 0 && e.slice(s + 1) !== t && (n = await a.SITE_CONTENT.get(e.slice(0, s + 1) + t));
|
|
229
407
|
}
|
|
230
|
-
return
|
|
408
|
+
return n ? new Response(n, {
|
|
231
409
|
headers: {
|
|
232
410
|
"Content-Type": "application/json",
|
|
233
411
|
"Cache-Control": "public, max-age=60",
|
|
@@ -236,54 +414,54 @@ function j(o, n, t) {
|
|
|
236
414
|
}) : Response.json({ error: "Not found" }, { status: 404 });
|
|
237
415
|
})() : Promise.resolve(Response.json({ error: "Key is required" }, { status: 400 }));
|
|
238
416
|
}
|
|
239
|
-
function
|
|
417
|
+
function k(o, a, t) {
|
|
240
418
|
let e = o;
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
243
|
-
e = e.replace(/<title>[^<]*<\/title>/, `<title>${
|
|
419
|
+
if (a.title) {
|
|
420
|
+
const n = t ? ` — ${t}` : "";
|
|
421
|
+
e = e.replace(/<title>[^<]*<\/title>/, `<title>${a.title}${n}</title>`);
|
|
244
422
|
}
|
|
245
|
-
return
|
|
423
|
+
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;
|
|
246
424
|
}
|
|
247
|
-
function
|
|
425
|
+
function ae(o) {
|
|
248
426
|
return {
|
|
249
|
-
async fetch(
|
|
250
|
-
const e = new URL(
|
|
251
|
-
if (
|
|
252
|
-
if (
|
|
253
|
-
return t.ASSETS.fetch(
|
|
254
|
-
const
|
|
255
|
-
let
|
|
256
|
-
const
|
|
427
|
+
async fetch(a, t) {
|
|
428
|
+
const e = new URL(a.url), n = G(e, t, o.defaultLocale);
|
|
429
|
+
if (n) return n;
|
|
430
|
+
if (z.test(e.pathname))
|
|
431
|
+
return t.ASSETS.fetch(a);
|
|
432
|
+
const s = F(a, o), r = new URL("/index.html", a.url);
|
|
433
|
+
let u = await (await t.ASSETS.fetch(new Request(r))).text();
|
|
434
|
+
const f = o.defaultLocale, [d, y] = await Promise.all([
|
|
257
435
|
t.SITE_CONTENT.get("config"),
|
|
258
|
-
t.SITE_CONTENT.get(`content:${
|
|
259
|
-
]),
|
|
260
|
-
let
|
|
261
|
-
if (!
|
|
262
|
-
const
|
|
263
|
-
|
|
436
|
+
t.SITE_CONTENT.get(`content:${s}`)
|
|
437
|
+
]), T = d ? JSON.parse(d) : {};
|
|
438
|
+
let m = y ? JSON.parse(y) : {};
|
|
439
|
+
if (!y && s !== f) {
|
|
440
|
+
const p = await t.SITE_CONTENT.get(`content:${f}`);
|
|
441
|
+
p && (m = JSON.parse(p));
|
|
264
442
|
}
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
const
|
|
268
|
-
if (!p
|
|
269
|
-
let
|
|
270
|
-
!
|
|
443
|
+
const h = e.pathname.match(/^\/p\/(.+)$/);
|
|
444
|
+
if (h) {
|
|
445
|
+
const p = h[1];
|
|
446
|
+
if (!m[p]) {
|
|
447
|
+
let g = await t.SITE_CONTENT.get(`page:${p}:${s}`);
|
|
448
|
+
!g && s !== f && (g = await t.SITE_CONTENT.get(`page:${p}:${f}`)), g && (m[p] = JSON.parse(g));
|
|
271
449
|
}
|
|
272
450
|
}
|
|
273
|
-
const
|
|
274
|
-
locale:
|
|
275
|
-
config:
|
|
451
|
+
const l = {
|
|
452
|
+
locale: s,
|
|
453
|
+
config: T
|
|
276
454
|
};
|
|
277
|
-
for (const [
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
const
|
|
281
|
-
|
|
455
|
+
for (const [p, g] of Object.entries(m))
|
|
456
|
+
l[`page:${p}`] = g;
|
|
457
|
+
if (h) {
|
|
458
|
+
const p = h[1], g = l[`page:${p}`];
|
|
459
|
+
g && (u = k(u, g, o.siteName));
|
|
282
460
|
}
|
|
283
|
-
e.pathname === "/" &&
|
|
284
|
-
const
|
|
285
|
-
return
|
|
286
|
-
</head>`), new Response(
|
|
461
|
+
e.pathname === "/" && m.home && (u = k(u, m.home, o.siteName));
|
|
462
|
+
const v = `<script>window.__SITE_DATA__ = ${JSON.stringify(l)};<\/script>`;
|
|
463
|
+
return u = u.replace("</head>", `${v}
|
|
464
|
+
</head>`), new Response(u, {
|
|
287
465
|
headers: {
|
|
288
466
|
"Content-Type": "text/html;charset=utf-8",
|
|
289
467
|
"Cache-Control": "public, max-age=60"
|
|
@@ -293,16 +471,20 @@ function X(o) {
|
|
|
293
471
|
};
|
|
294
472
|
}
|
|
295
473
|
export {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
474
|
+
I as PAGE_CONTEXT_KEY,
|
|
475
|
+
O as buildLayoutCSS,
|
|
476
|
+
ae as createSiteWorker,
|
|
477
|
+
x as isInPreview,
|
|
478
|
+
te as layoutPresets,
|
|
479
|
+
ee as layoutSchema,
|
|
480
|
+
Z as setupPreviewRouter,
|
|
481
|
+
V as useDynamicLayout,
|
|
482
|
+
A as useLivePreview,
|
|
483
|
+
Q as useLocaleSwitcher,
|
|
484
|
+
q as usePageData,
|
|
485
|
+
X as useSiteApi,
|
|
486
|
+
H as useSiteComponents,
|
|
487
|
+
B as useSiteConfig,
|
|
488
|
+
J as useSiteData,
|
|
489
|
+
Y as useSkin
|
|
308
490
|
};
|
package/dist/types/blocks.d.ts
CHANGED
|
@@ -12,9 +12,12 @@ export interface Block<T = any> {
|
|
|
12
12
|
settings?: BlockSettings & Record<string, any>;
|
|
13
13
|
}
|
|
14
14
|
export interface HeroBlockData {
|
|
15
|
+
variant?: 'default' | 'minimal' | 'video';
|
|
15
16
|
title: string;
|
|
16
17
|
subtitle?: string;
|
|
18
|
+
badge?: string;
|
|
17
19
|
image?: string;
|
|
20
|
+
backgroundImage?: string;
|
|
18
21
|
overlay?: boolean;
|
|
19
22
|
buttons?: Array<{
|
|
20
23
|
text: string;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout schema definition for the editor.
|
|
3
|
+
* Describes the editable structure of a layout config.
|
|
4
|
+
*/
|
|
5
|
+
export interface GridBreakpoint {
|
|
6
|
+
areas?: string[];
|
|
7
|
+
columns?: string;
|
|
8
|
+
rows?: string;
|
|
9
|
+
gap?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface LayoutZone {
|
|
12
|
+
type?: 'slot';
|
|
13
|
+
component?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
container?: boolean;
|
|
16
|
+
containerStyle?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export interface LayoutConfig {
|
|
19
|
+
grid?: Record<string, GridBreakpoint>;
|
|
20
|
+
zones: Record<string, LayoutZone>;
|
|
21
|
+
}
|
|
22
|
+
export interface LayoutField {
|
|
23
|
+
key: string;
|
|
24
|
+
type: 'text' | 'grid-areas' | 'select' | 'switch' | 'zone-list';
|
|
25
|
+
label: string;
|
|
26
|
+
breakpoint?: string;
|
|
27
|
+
options?: string[];
|
|
28
|
+
required?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface LayoutSchemaDefinition {
|
|
31
|
+
type: string;
|
|
32
|
+
displayName: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
fields: LayoutField[];
|
|
35
|
+
zoneFields: LayoutField[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Schema describing how to edit a layout in the editor.
|
|
39
|
+
* Used to render a form for creating/editing layout configs.
|
|
40
|
+
*/
|
|
41
|
+
export declare const layoutSchema: LayoutSchemaDefinition;
|
|
42
|
+
/**
|
|
43
|
+
* Built-in layout presets for quick layout creation.
|
|
44
|
+
*/
|
|
45
|
+
export declare const layoutPresets: Record<string, {
|
|
46
|
+
displayName: string;
|
|
47
|
+
config: LayoutConfig;
|
|
48
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xosen/site-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
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": "
|
|
39
|
-
"vue-i18n": "
|
|
40
|
-
"vue-router": "
|
|
41
|
-
"vue-tsc": "
|
|
42
|
-
"typescript": "
|
|
43
|
-
"vite": "
|
|
44
|
-
"@vitejs/plugin-vue": "
|
|
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,127 @@
|
|
|
1
|
+
import { computed, type Ref } from 'vue';
|
|
2
|
+
import { useRoute } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
import type { SiteData } from '../types/config.js';
|
|
5
|
+
import type { GridBreakpoint, LayoutConfig } from '../types/layout.js';
|
|
6
|
+
import { useLivePreview } from './useLivePreview.js';
|
|
7
|
+
|
|
8
|
+
const BREAKPOINTS: Record<string, number> = {
|
|
9
|
+
sm: 600,
|
|
10
|
+
md: 960,
|
|
11
|
+
lg: 1280,
|
|
12
|
+
xl: 1920,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_LAYOUT: LayoutConfig = {
|
|
16
|
+
grid: {
|
|
17
|
+
default: {
|
|
18
|
+
areas: ['header', 'main', 'footer'],
|
|
19
|
+
rows: 'auto 1fr auto',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
zones: {
|
|
23
|
+
header: { component: 'nav-menu' },
|
|
24
|
+
main: { type: 'slot' },
|
|
25
|
+
footer: { component: 'footer' },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const LANDING_LAYOUT: LayoutConfig = {
|
|
30
|
+
grid: {
|
|
31
|
+
default: {
|
|
32
|
+
areas: ['main'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
zones: {
|
|
36
|
+
main: { type: 'slot' },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function buildGridCSS(bp: GridBreakpoint, selector: string): string {
|
|
41
|
+
const rules: string[] = [];
|
|
42
|
+
if (bp.areas?.length) {
|
|
43
|
+
const areas = bp.areas.map((row) => `"${row}"`).join(' ');
|
|
44
|
+
rules.push(`grid-template-areas: ${areas}`);
|
|
45
|
+
}
|
|
46
|
+
if (bp.columns) rules.push(`grid-template-columns: ${bp.columns}`);
|
|
47
|
+
if (bp.rows) rules.push(`grid-template-rows: ${bp.rows}`);
|
|
48
|
+
if (bp.gap) rules.push(`gap: ${bp.gap}`);
|
|
49
|
+
if (!rules.length) return '';
|
|
50
|
+
return `${selector} { ${rules.join('; ')}; }`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate responsive CSS for a layout config.
|
|
55
|
+
* Returns a CSS string with media queries for each breakpoint.
|
|
56
|
+
*/
|
|
57
|
+
export function buildLayoutCSS(layout: LayoutConfig, selector: string): string {
|
|
58
|
+
const parts: string[] = [`${selector} { display: grid; min-height: 100vh; }`];
|
|
59
|
+
const grid = layout.grid;
|
|
60
|
+
|
|
61
|
+
if (!grid) return parts.join('\n');
|
|
62
|
+
|
|
63
|
+
if (grid.default) {
|
|
64
|
+
const css = buildGridCSS(grid.default, selector);
|
|
65
|
+
if (css) parts.push(css);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const bp of ['sm', 'md', 'lg', 'xl']) {
|
|
69
|
+
const config = grid[bp];
|
|
70
|
+
if (!config) continue;
|
|
71
|
+
const css = buildGridCSS(config, selector);
|
|
72
|
+
if (css) {
|
|
73
|
+
parts.push(`@media (min-width: ${BREAKPOINTS[bp]}px) { ${css} }`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return parts.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the current layout config from site data, route, and preview state.
|
|
82
|
+
*
|
|
83
|
+
* Returns:
|
|
84
|
+
* - `layoutName` — current layout name (e.g. 'default', 'sidebar')
|
|
85
|
+
* - `layoutConfig` — resolved LayoutConfig with grid + zones
|
|
86
|
+
* - `layoutCSS` — generated responsive CSS string
|
|
87
|
+
* - `layoutClass` — unique CSS class for this layout instance
|
|
88
|
+
*/
|
|
89
|
+
export function useDynamicLayout() {
|
|
90
|
+
const route = useRoute();
|
|
91
|
+
const { previewPage, isPreviewMode } = useLivePreview();
|
|
92
|
+
const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
|
|
93
|
+
|
|
94
|
+
const layoutId = `dl-${Math.random().toString(36).slice(2, 8)}`;
|
|
95
|
+
const layoutClass = computed(() => ['dynamic-layout', layoutId]);
|
|
96
|
+
|
|
97
|
+
const layoutName = computed(() => {
|
|
98
|
+
if (route.path === '/_preview_') {
|
|
99
|
+
return previewPage.value?.layout || 'landing';
|
|
100
|
+
}
|
|
101
|
+
if (isPreviewMode.value && previewPage.value?.layout) {
|
|
102
|
+
return previewPage.value.layout;
|
|
103
|
+
}
|
|
104
|
+
const pageKey = route.path === '/' ? 'page:home' : `page:${route.params.slug || ''}`;
|
|
105
|
+
const pageData = siteData?.[pageKey];
|
|
106
|
+
return pageData?.layout || 'default';
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const layoutConfig = computed<LayoutConfig>(() => {
|
|
110
|
+
const layouts = siteData?.config?.layouts;
|
|
111
|
+
if (layouts) {
|
|
112
|
+
const config = layouts[layoutName.value] || layouts.default;
|
|
113
|
+
if (config?.zones) return config;
|
|
114
|
+
}
|
|
115
|
+
if (layoutName.value === 'landing') return LANDING_LAYOUT;
|
|
116
|
+
return DEFAULT_LAYOUT;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const layoutCSS = computed(() => buildLayoutCSS(layoutConfig.value, `.${layoutId}`));
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
layoutName,
|
|
123
|
+
layoutConfig,
|
|
124
|
+
layoutCSS,
|
|
125
|
+
layoutClass,
|
|
126
|
+
};
|
|
127
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@ 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, LayoutSchemaDefinition, LayoutField } from './types/layout.js';
|
|
13
|
+
export { layoutSchema, layoutPresets } from './types/layout.js';
|
|
11
14
|
export type { PreviewMessage } from './composables/useLivePreview.js';
|
|
12
15
|
|
|
13
16
|
// Worker
|
package/src/types/blocks.ts
CHANGED
|
@@ -14,9 +14,12 @@ export interface Block<T = any> {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface HeroBlockData {
|
|
17
|
+
variant?: 'default' | 'minimal' | 'video';
|
|
17
18
|
title: string;
|
|
18
19
|
subtitle?: string;
|
|
20
|
+
badge?: string;
|
|
19
21
|
image?: string;
|
|
22
|
+
backgroundImage?: string;
|
|
20
23
|
overlay?: boolean;
|
|
21
24
|
buttons?: Array<{ text: string; url: string; variant?: 'primary' | 'outline' | 'white' }>;
|
|
22
25
|
stats?: Array<{ value: string; label: string }>;
|
package/src/types/config.ts
CHANGED
|
@@ -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 {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout schema definition for the editor.
|
|
3
|
+
* Describes the editable structure of a layout config.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface GridBreakpoint {
|
|
7
|
+
areas?: string[];
|
|
8
|
+
columns?: string;
|
|
9
|
+
rows?: string;
|
|
10
|
+
gap?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LayoutZone {
|
|
14
|
+
type?: 'slot';
|
|
15
|
+
component?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
container?: boolean;
|
|
18
|
+
containerStyle?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LayoutConfig {
|
|
22
|
+
grid?: Record<string, GridBreakpoint>;
|
|
23
|
+
zones: Record<string, LayoutZone>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LayoutField {
|
|
27
|
+
key: string;
|
|
28
|
+
type: 'text' | 'grid-areas' | 'select' | 'switch' | 'zone-list';
|
|
29
|
+
label: string;
|
|
30
|
+
breakpoint?: string;
|
|
31
|
+
options?: string[];
|
|
32
|
+
required?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LayoutSchemaDefinition {
|
|
36
|
+
type: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
fields: LayoutField[];
|
|
40
|
+
zoneFields: LayoutField[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Schema describing how to edit a layout in the editor.
|
|
45
|
+
* Used to render a form for creating/editing layout configs.
|
|
46
|
+
*/
|
|
47
|
+
export const layoutSchema: LayoutSchemaDefinition = {
|
|
48
|
+
type: 'layout',
|
|
49
|
+
displayName: 'Layout',
|
|
50
|
+
description: 'CSS Grid layout with responsive breakpoints and component zones',
|
|
51
|
+
fields: [
|
|
52
|
+
// Mobile (default)
|
|
53
|
+
{ key: 'grid.default.areas', type: 'grid-areas', label: 'Grid Areas (mobile)', breakpoint: 'default', required: true },
|
|
54
|
+
{ key: 'grid.default.columns', type: 'text', label: 'Columns (mobile)', breakpoint: 'default' },
|
|
55
|
+
{ key: 'grid.default.rows', type: 'text', label: 'Rows (mobile)', breakpoint: 'default' },
|
|
56
|
+
{ key: 'grid.default.gap', type: 'text', label: 'Gap (mobile)', breakpoint: 'default' },
|
|
57
|
+
|
|
58
|
+
// Tablet (sm — 600px)
|
|
59
|
+
{ key: 'grid.sm.areas', type: 'grid-areas', label: 'Grid Areas (sm 600px+)', breakpoint: 'sm' },
|
|
60
|
+
{ key: 'grid.sm.columns', type: 'text', label: 'Columns (sm)', breakpoint: 'sm' },
|
|
61
|
+
{ key: 'grid.sm.rows', type: 'text', label: 'Rows (sm)', breakpoint: 'sm' },
|
|
62
|
+
{ key: 'grid.sm.gap', type: 'text', label: 'Gap (sm)', breakpoint: 'sm' },
|
|
63
|
+
|
|
64
|
+
// Desktop (md — 960px)
|
|
65
|
+
{ key: 'grid.md.areas', type: 'grid-areas', label: 'Grid Areas (md 960px+)', breakpoint: 'md' },
|
|
66
|
+
{ key: 'grid.md.columns', type: 'text', label: 'Columns (md)', breakpoint: 'md' },
|
|
67
|
+
{ key: 'grid.md.rows', type: 'text', label: 'Rows (md)', breakpoint: 'md' },
|
|
68
|
+
{ key: 'grid.md.gap', type: 'text', label: 'Gap (md)', breakpoint: 'md' },
|
|
69
|
+
|
|
70
|
+
// Large desktop (lg — 1280px)
|
|
71
|
+
{ key: 'grid.lg.areas', type: 'grid-areas', label: 'Grid Areas (lg 1280px+)', breakpoint: 'lg' },
|
|
72
|
+
{ key: 'grid.lg.columns', type: 'text', label: 'Columns (lg)', breakpoint: 'lg' },
|
|
73
|
+
{ key: 'grid.lg.rows', type: 'text', label: 'Rows (lg)', breakpoint: 'lg' },
|
|
74
|
+
{ key: 'grid.lg.gap', type: 'text', label: 'Gap (lg)', breakpoint: 'lg' },
|
|
75
|
+
|
|
76
|
+
// Zones
|
|
77
|
+
{ key: 'zones', type: 'zone-list', label: 'Zones', required: true },
|
|
78
|
+
],
|
|
79
|
+
zoneFields: [
|
|
80
|
+
{ key: 'type', type: 'select', label: 'Type', options: ['slot', 'component'] },
|
|
81
|
+
{ key: 'component', type: 'text', label: 'Component Key' },
|
|
82
|
+
{ key: 'class', type: 'text', label: 'CSS Class' },
|
|
83
|
+
{ key: 'container', type: 'switch', label: 'Wrap in Container' },
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Built-in layout presets for quick layout creation.
|
|
89
|
+
*/
|
|
90
|
+
export const layoutPresets: Record<string, { displayName: string; config: LayoutConfig }> = {
|
|
91
|
+
default: {
|
|
92
|
+
displayName: 'Full Width',
|
|
93
|
+
config: {
|
|
94
|
+
grid: {
|
|
95
|
+
default: { areas: ['header', 'main', 'footer'], rows: 'auto 1fr auto' },
|
|
96
|
+
},
|
|
97
|
+
zones: {
|
|
98
|
+
header: { component: 'nav-menu' },
|
|
99
|
+
main: { type: 'slot' },
|
|
100
|
+
footer: { component: 'footer' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
sidebar: {
|
|
105
|
+
displayName: 'Sidebar',
|
|
106
|
+
config: {
|
|
107
|
+
grid: {
|
|
108
|
+
default: { areas: ['header', 'main', 'sidebar', 'footer'], columns: '1fr', rows: 'auto 1fr auto auto' },
|
|
109
|
+
md: { areas: ['header header', 'sidebar main', 'footer footer'], columns: '280px 1fr', rows: 'auto 1fr auto' },
|
|
110
|
+
},
|
|
111
|
+
zones: {
|
|
112
|
+
header: { component: 'nav-menu' },
|
|
113
|
+
sidebar: { component: 'sidebar-menu' },
|
|
114
|
+
main: { type: 'slot' },
|
|
115
|
+
footer: { component: 'footer' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
article: {
|
|
120
|
+
displayName: 'Article',
|
|
121
|
+
config: {
|
|
122
|
+
grid: {
|
|
123
|
+
default: { areas: ['header', 'main', 'footer'], rows: 'auto 1fr auto' },
|
|
124
|
+
},
|
|
125
|
+
zones: {
|
|
126
|
+
header: { component: 'nav-menu' },
|
|
127
|
+
main: { type: 'slot', container: true, containerStyle: { 'max-width': '900px' } },
|
|
128
|
+
footer: { component: 'footer' },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
landing: {
|
|
133
|
+
displayName: 'Landing (no nav)',
|
|
134
|
+
config: {
|
|
135
|
+
grid: {
|
|
136
|
+
default: { areas: ['main'] },
|
|
137
|
+
},
|
|
138
|
+
zones: {
|
|
139
|
+
main: { type: 'slot' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
magazine: {
|
|
144
|
+
displayName: 'Magazine',
|
|
145
|
+
config: {
|
|
146
|
+
grid: {
|
|
147
|
+
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' },
|
|
149
|
+
},
|
|
150
|
+
zones: {
|
|
151
|
+
header: { component: 'nav-menu' },
|
|
152
|
+
main: { type: 'slot' },
|
|
153
|
+
aside: { component: 'sidebar-menu' },
|
|
154
|
+
footer: { component: 'footer' },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|