@brightsideoy/kama 0.1.0-alpha.16 → 0.1.0-alpha.161
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/README.md +169 -0
- package/bin/cli.js +686 -155
- package/content/definitions/block-layouts/three-column.yaml +20 -0
- package/content/definitions/block-layouts/two-column.yaml +29 -0
- package/content/definitions/blocks/button.yaml +24 -0
- package/content/definitions/blocks/header.yaml +13 -0
- package/content/definitions/blocks/header_image.yaml +21 -0
- package/content/definitions/blocks/hero.yaml +28 -0
- package/content/definitions/blocks/html.yaml +13 -0
- package/content/definitions/blocks/image_card.yaml +56 -0
- package/content/definitions/blocks/single_image.yaml +27 -0
- package/content/definitions/blocks/text.yaml +15 -0
- package/content/definitions/channels/ecommerce.yaml +28 -0
- package/content/definitions/channels/website.yaml +41 -0
- package/content/definitions/field-sets/access.yaml +27 -0
- package/content/definitions/field-sets/channel-base.yaml +50 -0
- package/content/definitions/field-sets/channel-seo.yaml +21 -0
- package/content/definitions/field-sets/comments.yaml +11 -0
- package/content/definitions/field-sets/content-block.yaml +41 -0
- package/content/definitions/field-sets/page-base.yaml +44 -0
- package/content/definitions/field-sets/page-seo.yaml +34 -0
- package/content/definitions/field-sets/page-social.yaml +78 -0
- package/content/definitions/field-sets/schema-type.yaml +43 -0
- package/content/definitions/field-sets/tree-base.yaml +31 -0
- package/content/definitions/field-sets/visibility.yaml +8 -0
- package/content/definitions/node-layouts/content.yml +12 -0
- package/content/definitions/node-layouts/landing-page.yml +27 -0
- package/content/definitions/node-layouts/single-column.yml +43 -0
- package/content/definitions/node-layouts/two-column.yml +48 -0
- package/content/definitions/nodes/address.yaml +40 -0
- package/content/definitions/nodes/article.yaml +37 -0
- package/content/definitions/nodes/event.yaml +55 -0
- package/content/definitions/nodes/external-link.yaml +29 -0
- package/content/definitions/nodes/page-skeleton.yaml +48 -0
- package/content/definitions/nodes/page.yaml +54 -0
- package/content/definitions/nodes/person.yaml +41 -0
- package/content/definitions/nodes/product.yaml +89 -0
- package/content/definitions/trees/navigation.yaml +24 -0
- package/definitions/editor/button.ts +11 -0
- package/definitions/editor/default.ts +21 -0
- package/definitions/editor/header.ts +36 -0
- package/definitions/editor/minimal.ts +15 -0
- package/definitions/validators/iban.ts +12 -0
- package/definitions/validators/magic-word.ts +7 -0
- package/dist/assets/index-C5TmJVER.css +1 -0
- package/dist/assets/index-jpMh2BZv.js +164 -0
- package/dist/dist-BNkCx-o-.js +8912 -0
- package/dist/index.html +15 -0
- package/dist/index.js +7646 -1562
- package/dist/schema-compiler.js +48 -0
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +19 -17
- package/schema.sql +206 -0
- package/templates/astro-starter/astro.config.mjs +24 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/public/robots.txt +23 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +134 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +132 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +92 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +67 -0
- package/templates/astro-starter/src/components/global/Footer.astro +50 -0
- package/templates/astro-starter/src/components/global/Header.astro +177 -0
- package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +318 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +172 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +47 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +62 -0
- package/templates/astro-starter/src/lib/cms.ts +197 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +56 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +129 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +98 -0
- package/templates/astro-starter/src/middleware.ts +406 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +238 -0
- package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
- package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
- package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
- package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
- package/templates/astro-starter/src/styles/standard-theme.css +157 -0
- package/templates/astro-starter/src/utils/tiptap.ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/templates/kama-worker/index.js +19 -0
- package/templates/kama-worker/server.js +98 -0
- package/wrangler.json +22 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/global/NavigationMenu.astro
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<!-- 2-LINE HAMBURGER TOGGLE BUTTON -->
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
class="mobile-toggle"
|
|
9
|
+
id="menu-toggle"
|
|
10
|
+
aria-label="Toggle Navigation Menu"
|
|
11
|
+
aria-expanded="false"
|
|
12
|
+
>
|
|
13
|
+
<span class="line line-1"></span>
|
|
14
|
+
<span class="line line-2"></span>
|
|
15
|
+
</button>
|
|
16
|
+
|
|
17
|
+
<!-- NAVIGATION BLOCK & FULLSCREEN CONTAINER -->
|
|
18
|
+
<nav class="nav-block" id="nav-overlay">
|
|
19
|
+
<!-- 🎯 The partial HTML will be injected into this div -->
|
|
20
|
+
<div id="nav-injection-target"></div>
|
|
21
|
+
</nav>
|
|
22
|
+
|
|
23
|
+
<script>
|
|
24
|
+
async function initNav() {
|
|
25
|
+
const target = document.getElementById("nav-injection-target");
|
|
26
|
+
if (!target) return;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// 🎯 Fetch the partial and pass the current page URL so it highlights correctly
|
|
30
|
+
const currentPath = encodeURIComponent(window.location.pathname);
|
|
31
|
+
const res = await fetch(`/partials/nav?path=${currentPath}`);
|
|
32
|
+
|
|
33
|
+
if (res.ok) {
|
|
34
|
+
target.innerHTML = await res.text();
|
|
35
|
+
bindNavEvents();
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error("Failed to load nav partial", e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bindNavEvents() {
|
|
43
|
+
const toggleBtn = document.getElementById("menu-toggle");
|
|
44
|
+
const navOverlay = document.getElementById("nav-overlay");
|
|
45
|
+
|
|
46
|
+
const closeMenu = () => {
|
|
47
|
+
if (toggleBtn && navOverlay) {
|
|
48
|
+
toggleBtn.setAttribute("aria-expanded", "false");
|
|
49
|
+
toggleBtn.classList.remove("is-open");
|
|
50
|
+
navOverlay.classList.remove("is-open");
|
|
51
|
+
document.body.classList.remove("menu-lock-scroll");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (toggleBtn && navOverlay) {
|
|
56
|
+
toggleBtn.addEventListener("click", () => {
|
|
57
|
+
const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
|
|
58
|
+
toggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
|
|
59
|
+
toggleBtn.classList.toggle("is-open");
|
|
60
|
+
navOverlay.classList.toggle("is-open");
|
|
61
|
+
document.body.classList.toggle("menu-lock-scroll");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const navLinks = navOverlay.querySelectorAll("a");
|
|
65
|
+
navLinks.forEach(link => {
|
|
66
|
+
link.addEventListener("click", () => {
|
|
67
|
+
const targetUrl = new URL(link.href);
|
|
68
|
+
const currentUrl = new window.URL(window.location.href);
|
|
69
|
+
if (targetUrl.pathname === currentUrl.pathname) closeMenu();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
window.addEventListener("pageshow", closeMenu);
|
|
75
|
+
|
|
76
|
+
document.addEventListener("click", event => {
|
|
77
|
+
const allDetails = document.querySelectorAll("details.nav-details");
|
|
78
|
+
allDetails.forEach(details => {
|
|
79
|
+
if (
|
|
80
|
+
details.hasAttribute("open") &&
|
|
81
|
+
!details.contains(event.target as Node)
|
|
82
|
+
) {
|
|
83
|
+
details.removeAttribute("open");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Run on page load
|
|
90
|
+
initNav();
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
/* Keeps all of your exact original CSS styles intact! */
|
|
95
|
+
.mobile-toggle {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 6px;
|
|
101
|
+
width: 44px;
|
|
102
|
+
height: 44px;
|
|
103
|
+
background: transparent;
|
|
104
|
+
border: none;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
padding: 0;
|
|
107
|
+
z-index: 1001;
|
|
108
|
+
}
|
|
109
|
+
.mobile-toggle .line {
|
|
110
|
+
width: 22px;
|
|
111
|
+
height: 2px;
|
|
112
|
+
background-color: #0f172a;
|
|
113
|
+
transition:
|
|
114
|
+
transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
115
|
+
opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
116
|
+
transform-origin: center;
|
|
117
|
+
}
|
|
118
|
+
.mobile-toggle.is-open .line-1 {
|
|
119
|
+
transform: translateY(4px) rotate(45deg);
|
|
120
|
+
}
|
|
121
|
+
.mobile-toggle.is-open .line-2 {
|
|
122
|
+
transform: translateY(-4px) rotate(-45deg);
|
|
123
|
+
}
|
|
124
|
+
.nav-block {
|
|
125
|
+
position: fixed;
|
|
126
|
+
top: 0;
|
|
127
|
+
left: 0;
|
|
128
|
+
width: 100vw;
|
|
129
|
+
height: 100vh;
|
|
130
|
+
background-color: #ffffff;
|
|
131
|
+
padding: 80px 24px 40px 24px;
|
|
132
|
+
box-sizing: border-box;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
opacity: 0;
|
|
137
|
+
visibility: hidden;
|
|
138
|
+
pointer-events: none;
|
|
139
|
+
transition:
|
|
140
|
+
opacity 0.3s ease,
|
|
141
|
+
visibility 0.3s ease;
|
|
142
|
+
z-index: 1000;
|
|
143
|
+
}
|
|
144
|
+
.nav-block.is-open {
|
|
145
|
+
opacity: 1;
|
|
146
|
+
visibility: visible;
|
|
147
|
+
pointer-events: auto;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* 🎯 We target the injected .nav-list using :global so Astro doesn't scope it away */
|
|
151
|
+
:global(.nav-list) {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
list-style: none;
|
|
156
|
+
margin: 0;
|
|
157
|
+
padding: 0;
|
|
158
|
+
width: 100%;
|
|
159
|
+
}
|
|
160
|
+
:global(.nav-item) {
|
|
161
|
+
position: relative;
|
|
162
|
+
width: 100%;
|
|
163
|
+
}
|
|
164
|
+
:global(.nav-button) {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: space-between;
|
|
168
|
+
width: 100%;
|
|
169
|
+
height: 52px;
|
|
170
|
+
padding: 0 16px;
|
|
171
|
+
font-size: 18px;
|
|
172
|
+
font-weight: 500;
|
|
173
|
+
color: #334155;
|
|
174
|
+
text-decoration: none;
|
|
175
|
+
background: transparent;
|
|
176
|
+
border: none;
|
|
177
|
+
border-radius: 4px;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
}
|
|
181
|
+
:global(.nav-button:hover) {
|
|
182
|
+
color: #0f172a;
|
|
183
|
+
background: #f8fafc;
|
|
184
|
+
}
|
|
185
|
+
:global(.nav-button.active) {
|
|
186
|
+
color: #0c4a6e;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
}
|
|
189
|
+
:global(.dropdown-summary) {
|
|
190
|
+
list-style: none;
|
|
191
|
+
user-select: none;
|
|
192
|
+
}
|
|
193
|
+
:global(.dropdown-summary::-webkit-details-marker) {
|
|
194
|
+
display: none;
|
|
195
|
+
}
|
|
196
|
+
:global(.indicator) {
|
|
197
|
+
font-size: 11px;
|
|
198
|
+
color: #64748b;
|
|
199
|
+
transition: transform 0.2s ease;
|
|
200
|
+
}
|
|
201
|
+
:global(details[open] .indicator) {
|
|
202
|
+
transform: rotate(180deg);
|
|
203
|
+
}
|
|
204
|
+
:global(details.nav-details) {
|
|
205
|
+
border-radius: 6px;
|
|
206
|
+
transition: background-color 0.2s ease;
|
|
207
|
+
}
|
|
208
|
+
:global(details[open].nav-details) {
|
|
209
|
+
background: #f8fafc;
|
|
210
|
+
padding-bottom: 8px;
|
|
211
|
+
}
|
|
212
|
+
:global(.submenu-container) {
|
|
213
|
+
position: relative;
|
|
214
|
+
margin-left: 24px;
|
|
215
|
+
padding-left: 16px;
|
|
216
|
+
border-left: 2px solid #cbd5e1;
|
|
217
|
+
}
|
|
218
|
+
:global(.dropdown-panel) {
|
|
219
|
+
list-style: none;
|
|
220
|
+
margin: 4px 0 0 0;
|
|
221
|
+
padding: 0;
|
|
222
|
+
display: flex;
|
|
223
|
+
flex-direction: column;
|
|
224
|
+
gap: 4px;
|
|
225
|
+
}
|
|
226
|
+
:global(.dropdown-item) {
|
|
227
|
+
display: block;
|
|
228
|
+
padding: 10px 12px;
|
|
229
|
+
font-size: 16px;
|
|
230
|
+
font-weight: 400;
|
|
231
|
+
color: #475569;
|
|
232
|
+
text-decoration: none;
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
transition:
|
|
235
|
+
background 0.15s ease,
|
|
236
|
+
color 0.15s ease;
|
|
237
|
+
}
|
|
238
|
+
:global(.overview-item) {
|
|
239
|
+
font-weight: 500;
|
|
240
|
+
color: #334155;
|
|
241
|
+
}
|
|
242
|
+
:global(.dropdown-item:hover) {
|
|
243
|
+
background: #e2e8f0;
|
|
244
|
+
color: #0f172a;
|
|
245
|
+
}
|
|
246
|
+
:global(.dropdown-item.active) {
|
|
247
|
+
font-weight: 600;
|
|
248
|
+
color: #0c4a6e;
|
|
249
|
+
background: #e0f2fe;
|
|
250
|
+
}
|
|
251
|
+
:global(body.menu-lock-scroll) {
|
|
252
|
+
overflow: hidden !important;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@media (min-width: 768px) {
|
|
256
|
+
.mobile-toggle {
|
|
257
|
+
display: none;
|
|
258
|
+
}
|
|
259
|
+
.nav-block {
|
|
260
|
+
position: static;
|
|
261
|
+
width: 100%;
|
|
262
|
+
height: auto;
|
|
263
|
+
background: transparent;
|
|
264
|
+
padding: 0;
|
|
265
|
+
opacity: 1;
|
|
266
|
+
visibility: visible;
|
|
267
|
+
pointer-events: auto;
|
|
268
|
+
overflow-y: visible;
|
|
269
|
+
display: block;
|
|
270
|
+
}
|
|
271
|
+
:global(.nav-list) {
|
|
272
|
+
flex-direction: row;
|
|
273
|
+
gap: 4px;
|
|
274
|
+
justify-content: flex-end;
|
|
275
|
+
}
|
|
276
|
+
:global(.nav-item) {
|
|
277
|
+
width: auto;
|
|
278
|
+
}
|
|
279
|
+
:global(.nav-button) {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
justify-content: flex-start;
|
|
282
|
+
gap: 6px;
|
|
283
|
+
height: 40px;
|
|
284
|
+
padding: 0 16px;
|
|
285
|
+
font-size: 14px;
|
|
286
|
+
border-radius: 4px;
|
|
287
|
+
margin-top: 4px;
|
|
288
|
+
}
|
|
289
|
+
:global(details[open].nav-details) {
|
|
290
|
+
background: transparent;
|
|
291
|
+
padding-bottom: 0;
|
|
292
|
+
}
|
|
293
|
+
:global(.submenu-container) {
|
|
294
|
+
margin-left: 0;
|
|
295
|
+
padding-left: 0;
|
|
296
|
+
border-left: none;
|
|
297
|
+
}
|
|
298
|
+
:global(.dropdown-panel) {
|
|
299
|
+
position: absolute;
|
|
300
|
+
top: 100%;
|
|
301
|
+
left: 0;
|
|
302
|
+
min-width: 220px;
|
|
303
|
+
background: #ffffff;
|
|
304
|
+
border: 1px solid #e2e8f0;
|
|
305
|
+
margin: 0;
|
|
306
|
+
gap: 0;
|
|
307
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
308
|
+
border-radius: 4px;
|
|
309
|
+
padding: 4px;
|
|
310
|
+
}
|
|
311
|
+
:global(.dropdown-item) {
|
|
312
|
+
font-size: 14px;
|
|
313
|
+
padding: 8px 16px;
|
|
314
|
+
border-radius: 4px;
|
|
315
|
+
border-bottom: none;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
</style>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/ui/KamaImage.astro
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildAgnosticImageUrl,
|
|
6
|
+
isSvgImage,
|
|
7
|
+
getImageUri,
|
|
8
|
+
} from "@/lib/media/media-url";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
image: any;
|
|
12
|
+
sizes?: string;
|
|
13
|
+
desktopWidths?: number[];
|
|
14
|
+
idealWidth?: number;
|
|
15
|
+
class?: string;
|
|
16
|
+
loading?: "lazy" | "eager";
|
|
17
|
+
fetchpriority?: "auto" | "high" | "low";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
image = {},
|
|
22
|
+
sizes = "100vw",
|
|
23
|
+
desktopWidths = [768, 1352],
|
|
24
|
+
idealWidth = 1352,
|
|
25
|
+
class: className = "",
|
|
26
|
+
loading = "lazy",
|
|
27
|
+
fetchpriority = "auto",
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
|
|
30
|
+
// Unpack array wrappers if passed
|
|
31
|
+
const targetImage = Array.isArray(image) ? image[0] : image;
|
|
32
|
+
|
|
33
|
+
// 🎯 FIX 1: Support string paths in addition to object payloads
|
|
34
|
+
const isValidImage = Boolean(
|
|
35
|
+
targetImage &&
|
|
36
|
+
(typeof targetImage === "string" ||
|
|
37
|
+
targetImage.url ||
|
|
38
|
+
targetImage.path ||
|
|
39
|
+
targetImage.variants),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Detect if this asset is a vector SVG
|
|
43
|
+
const isSvg =
|
|
44
|
+
isSvgImage(targetImage?.mimeType || "") ||
|
|
45
|
+
isSvgImage(targetImage?.filename || "") ||
|
|
46
|
+
isSvgImage(targetImage?.url || targetImage?.path || "") ||
|
|
47
|
+
(typeof targetImage === "string" && isSvgImage(targetImage));
|
|
48
|
+
|
|
49
|
+
const variants = targetImage?.variants || {};
|
|
50
|
+
|
|
51
|
+
// Resolve base paths using getImageUri to ensure CSP compliance & relativity
|
|
52
|
+
const rawMobileUrl = getImageUri(variants.mobile?.url);
|
|
53
|
+
const rawTabletUrl = getImageUri(variants.tablet?.url);
|
|
54
|
+
const rawDesktopUrl = getImageUri(
|
|
55
|
+
variants.standard?.url ||
|
|
56
|
+
targetImage?.url ||
|
|
57
|
+
targetImage?.path ||
|
|
58
|
+
targetImage,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const mobileW = Number(variants.mobile?.cropCoordinates?.width || 768);
|
|
62
|
+
const mobileH = Number(variants.mobile?.cropCoordinates?.height || 576);
|
|
63
|
+
|
|
64
|
+
const tabletW = Number(variants.tablet?.cropCoordinates?.width || 1024);
|
|
65
|
+
const tabletH = Number(variants.tablet?.cropCoordinates?.height || 512);
|
|
66
|
+
|
|
67
|
+
// 🎯 FIX 2: Omit explicit width/height attributes on SVGs if intrinsic dimensions are missing/0
|
|
68
|
+
const rawWidth = Number(
|
|
69
|
+
variants.standard?.cropCoordinates?.width || targetImage?.width,
|
|
70
|
+
);
|
|
71
|
+
const rawHeight = Number(
|
|
72
|
+
variants.standard?.cropCoordinates?.height || targetImage?.height,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const baseWidth =
|
|
76
|
+
rawWidth && !isNaN(rawWidth) && rawWidth > 0 ? rawWidth : idealWidth;
|
|
77
|
+
const baseHeight =
|
|
78
|
+
rawHeight && !isNaN(rawHeight) && rawHeight > 0
|
|
79
|
+
? rawHeight
|
|
80
|
+
: Math.round(idealWidth * 0.5);
|
|
81
|
+
|
|
82
|
+
function buildAgnosticSrcset(
|
|
83
|
+
url: string,
|
|
84
|
+
targetWidths: number[],
|
|
85
|
+
quality = 75,
|
|
86
|
+
): string {
|
|
87
|
+
if (!url) return "";
|
|
88
|
+
return targetWidths
|
|
89
|
+
.map(width => `${buildAgnosticImageUrl(url, width, quality)} ${width}w`)
|
|
90
|
+
.join(", ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate srcsets only for raster images
|
|
94
|
+
const mobileSrcset =
|
|
95
|
+
!isSvg && rawMobileUrl ? buildAgnosticSrcset(rawMobileUrl, [400, 768]) : null;
|
|
96
|
+
|
|
97
|
+
const tabletSrcset =
|
|
98
|
+
!isSvg && rawTabletUrl
|
|
99
|
+
? buildAgnosticSrcset(rawTabletUrl, [768, 1024])
|
|
100
|
+
: null;
|
|
101
|
+
|
|
102
|
+
const desktopSrcset =
|
|
103
|
+
!isSvg && rawDesktopUrl
|
|
104
|
+
? buildAgnosticSrcset(rawDesktopUrl, desktopWidths)
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
const fallbackUrl =
|
|
108
|
+
getImageUri(rawDesktopUrl, {
|
|
109
|
+
width: isSvg ? undefined : baseWidth,
|
|
110
|
+
quality: 75,
|
|
111
|
+
}) || "";
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
isValidImage &&
|
|
116
|
+
(isSvg ? (
|
|
117
|
+
/* 🎯 Clean single <img> for SVG Vectors */
|
|
118
|
+
<img
|
|
119
|
+
src={fallbackUrl}
|
|
120
|
+
alt={targetImage.alt || targetImage.filename || ""}
|
|
121
|
+
width={rawWidth > 0 ? rawWidth : undefined}
|
|
122
|
+
height={rawHeight > 0 ? rawHeight : undefined}
|
|
123
|
+
loading={loading}
|
|
124
|
+
fetchpriority={fetchpriority}
|
|
125
|
+
decoding={loading === "eager" ? "sync" : "async"}
|
|
126
|
+
class={className}
|
|
127
|
+
/>
|
|
128
|
+
) : (
|
|
129
|
+
/* 🎯 Responsive <picture> for Raster Images (.png, .jpg, .webp) */
|
|
130
|
+
<picture>
|
|
131
|
+
{Boolean(mobileSrcset) && (
|
|
132
|
+
<source
|
|
133
|
+
media="(max-width: 767px)"
|
|
134
|
+
srcset={mobileSrcset as string}
|
|
135
|
+
sizes="(max-width: 767px) calc(100vw - 48px), 768px"
|
|
136
|
+
width={mobileW}
|
|
137
|
+
height={mobileH}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{Boolean(tabletSrcset) && (
|
|
142
|
+
<source
|
|
143
|
+
media="(min-width: 768px) and (max-width: 1024px)"
|
|
144
|
+
srcset={tabletSrcset as string}
|
|
145
|
+
sizes="(max-width: 1024px) calc(100vw - 48px), 1024px"
|
|
146
|
+
width={tabletW}
|
|
147
|
+
height={tabletH}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{Boolean(desktopSrcset) && (
|
|
152
|
+
<source
|
|
153
|
+
srcset={desktopSrcset as string}
|
|
154
|
+
sizes={sizes}
|
|
155
|
+
width={baseWidth}
|
|
156
|
+
height={baseHeight}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
<img
|
|
161
|
+
src={fallbackUrl}
|
|
162
|
+
alt={targetImage.alt || targetImage.filename || ""}
|
|
163
|
+
width={baseWidth}
|
|
164
|
+
height={baseHeight}
|
|
165
|
+
loading={loading}
|
|
166
|
+
fetchpriority={fetchpriority}
|
|
167
|
+
decoding={loading === "eager" ? "sync" : "async"}
|
|
168
|
+
class={className}
|
|
169
|
+
/>
|
|
170
|
+
</picture>
|
|
171
|
+
))
|
|
172
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/layouts/SkeletonLayout.astro
|
|
3
|
+
|
|
4
|
+
import MetaHead from "@/components/global/MetaHead.astro";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
node: Record<string, any>;
|
|
8
|
+
content: Record<string, any>;
|
|
9
|
+
normalizedSlug: string;
|
|
10
|
+
isHomePage: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { node = {}, content = {}, normalizedSlug, isHomePage } = Astro.props;
|
|
14
|
+
|
|
15
|
+
// 1. Extract raw custom code fields from the node payload
|
|
16
|
+
const customCss = node.css || "";
|
|
17
|
+
const customJsHeader = node.jsHeader || "";
|
|
18
|
+
const customJsFooter = node.jsFooter || "";
|
|
19
|
+
const pageLang = language || node?.language || "en";
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<!doctype html>
|
|
23
|
+
<html lang={pageLang}>
|
|
24
|
+
<head>
|
|
25
|
+
<!-- 🎯 1. SEO & Meta Tags -->
|
|
26
|
+
<MetaHead
|
|
27
|
+
node={node}
|
|
28
|
+
content={content}
|
|
29
|
+
normalizedSlug={normalizedSlug}
|
|
30
|
+
isHomePage={isHomePage}
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<!-- 🎯 2. Custom CSS injected into <head> -->
|
|
34
|
+
{customCss && <style set:html={customCss} />}
|
|
35
|
+
|
|
36
|
+
<!-- 🎯 3. Custom Header JS injected into <head> -->
|
|
37
|
+
{customJsHeader && <script set:html={customJsHeader} />}
|
|
38
|
+
</head>
|
|
39
|
+
|
|
40
|
+
<body>
|
|
41
|
+
<!-- 🎯 4. Renders your HTML Block natively (No wrappers, no headers) -->
|
|
42
|
+
<slot />
|
|
43
|
+
|
|
44
|
+
<!-- 🎯 5. Custom Footer JS injected before closing body -->
|
|
45
|
+
{customJsFooter && <script set:html={customJsFooter} />}
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/layouts/StandardLayout.astro
|
|
3
|
+
|
|
4
|
+
import "@/styles/standard-theme.css";
|
|
5
|
+
import Header from "@/components/global/Header.astro";
|
|
6
|
+
import Footer from "@/components/global/Footer.astro";
|
|
7
|
+
import MetaHead from "@/components/global/MetaHead.astro";
|
|
8
|
+
import type { TreeNode, ContentBlock } from "@/lib/cms";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
node: Record<string, any>;
|
|
12
|
+
content: Record<string, ContentBlock[]>;
|
|
13
|
+
normalizedSlug: string;
|
|
14
|
+
isHomePage: boolean;
|
|
15
|
+
hasSidebar?: boolean;
|
|
16
|
+
headerBlocks?: ContentBlock[];
|
|
17
|
+
tree?: TreeNode[];
|
|
18
|
+
language?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
node = {},
|
|
23
|
+
content = {},
|
|
24
|
+
normalizedSlug,
|
|
25
|
+
isHomePage,
|
|
26
|
+
headerBlocks = [],
|
|
27
|
+
tree,
|
|
28
|
+
language,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
// 🎯 Language Cascade: explicit prop -> node language -> fallback undefined
|
|
32
|
+
// (MetaHead will resolve channel.defaultLanguage if this remains undefined)
|
|
33
|
+
const pageLang = language || node?.language || undefined;
|
|
34
|
+
|
|
35
|
+
// ❌ Removed: const footerBlocks = content?.footer || [];
|
|
36
|
+
// (The new Footer component handles its own data now!)
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<!doctype html>
|
|
40
|
+
<html lang={pageLang}>
|
|
41
|
+
<head>
|
|
42
|
+
<MetaHead
|
|
43
|
+
node={node}
|
|
44
|
+
content={content}
|
|
45
|
+
normalizedSlug={normalizedSlug}
|
|
46
|
+
isHomePage={isHomePage}
|
|
47
|
+
headerBlocks={headerBlocks}
|
|
48
|
+
resolvedLanguage={pageLang}
|
|
49
|
+
/>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<Header node={node} tree={tree} />
|
|
53
|
+
|
|
54
|
+
<main class="content-block">
|
|
55
|
+
<div class="content-container">
|
|
56
|
+
<slot />
|
|
57
|
+
</div>
|
|
58
|
+
</main>
|
|
59
|
+
|
|
60
|
+
<Footer />
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|