@brightsideoy/kama 1.0.0-alpha.20 → 1.0.0-alpha.21
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/content/definitions/blocks/image-gallery.yaml +34 -0
- package/content/definitions/blocks/youtube.yaml +8 -0
- package/content/definitions/field-sets/access.yaml +10 -0
- package/content/definitions/field-sets/page-base.yaml +16 -1
- package/content/definitions/taxonomies/media-tag.yaml +15 -0
- package/definitions/editor/default.ts +15 -1
- package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
- package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
- package/dist/assets/{CmsHelp-DyPJLGGi.js → CmsHelp-BQkr9KyU.js} +1 -1
- package/dist/assets/{CmsPublisher-DNLwjirk.js → CmsPublisher-XSCPrp5L.js} +1 -1
- package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
- package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
- package/dist/assets/{TrashWorkspace-DgIg1v6t.js → TrashWorkspace-CATe46WF.js} +1 -1
- package/dist/assets/index-CY0VYSyt.css +1 -0
- package/dist/assets/index-DdG9Pozx.js +14 -0
- package/dist/assets/{plus-FQRsZBoJ.js → plus-CX_GHzXs.js} +1 -1
- package/dist/assets/useNodes-CqFlc72m.js +1 -0
- package/dist/{dist-D_Q1y4Kh.js → dist-3Xc2TTZw.js} +1117 -456
- package/dist/index.html +4 -4
- package/dist/index.js +1117 -456
- package/dist/schema-compiler.js +1 -1
- package/package.json +5 -5
- package/schema.sql +10 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +11 -3
- package/dist/assets/AssetEditor-D5j7pwcK.css +0 -1
- package/dist/assets/AssetEditor-Dt1zCR7_.js +0 -1
- package/dist/assets/NodeEditor-CiXQKXK1.js +0 -147
- package/dist/assets/NodeEditor-pZI3gu4a.css +0 -1
- package/dist/assets/index-DQ6_RmCI.js +0 -14
- package/dist/assets/index-DcKiaRjI.css +0 -1
- package/dist/assets/useNodes-D2-VxsxR.js +0 -1
- package/templates/astro-starter/old_src/components/blocks/BlockRenderer.astro +0 -143
- package/templates/astro-starter/old_src/components/blocks/ColumnLayout.astro +0 -67
- package/templates/astro-starter/old_src/components/blocks/HeaderImage.astro +0 -126
- package/templates/astro-starter/old_src/components/blocks/Hero.astro +0 -218
- package/templates/astro-starter/old_src/components/blocks/HtmlBlock.astro +0 -25
- package/templates/astro-starter/old_src/components/blocks/ImageBlock.astro +0 -91
- package/templates/astro-starter/old_src/components/blocks/NodeList.astro +0 -274
- package/templates/astro-starter/old_src/components/blocks/TextBlock.astro +0 -15
- package/templates/astro-starter/old_src/components/global/Breadcrumb.astro +0 -108
- package/templates/astro-starter/old_src/components/global/Footer.astro +0 -50
- package/templates/astro-starter/old_src/components/global/Header.astro +0 -171
- package/templates/astro-starter/old_src/components/global/MetaHead.astro +0 -266
- package/templates/astro-starter/old_src/components/global/NavigationMenu.astro +0 -413
- package/templates/astro-starter/old_src/components/ui/KamaImage.astro +0 -180
- package/templates/astro-starter/old_src/layouts/SkeletonLayout.astro +0 -56
- package/templates/astro-starter/old_src/layouts/StandardLayout.astro +0 -63
- package/templates/astro-starter/old_src/lib/cms.ts +0 -219
- package/templates/astro-starter/old_src/lib/media/kama-image-service.ts +0 -67
- package/templates/astro-starter/old_src/lib/media/media-proxy.ts +0 -148
- package/templates/astro-starter/old_src/lib/media/media-url.ts +0 -112
- package/templates/astro-starter/old_src/middleware.ts +0 -536
- package/templates/astro-starter/old_src/pages/404.astro +0 -46
- package/templates/astro-starter/old_src/pages/[...slug].astro +0 -311
- package/templates/astro-starter/old_src/pages/api/load-nodes.ts +0 -68
- package/templates/astro-starter/old_src/pages/api/partials/node-cards.astro +0 -101
- package/templates/astro-starter/old_src/pages/llms-full.txt.ts +0 -171
- package/templates/astro-starter/old_src/pages/llms.txt.ts +0 -113
- package/templates/astro-starter/old_src/pages/media/[...path].ts +0 -14
- package/templates/astro-starter/old_src/pages/partials/footer.astro +0 -69
- package/templates/astro-starter/old_src/pages/partials/nav.astro +0 -141
- package/templates/astro-starter/old_src/styles/standard-theme.css +0 -157
- package/templates/astro-starter/old_src/utils/tiptap.ts +0 -113
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// src/components/global/NavigationMenu.astro
|
|
3
|
-
import type { TreeNode } from "@/lib/cms";
|
|
4
|
-
|
|
5
|
-
export interface Props {
|
|
6
|
-
tree?: TreeNode[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { tree = [] } = Astro.props;
|
|
10
|
-
|
|
11
|
-
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
12
|
-
|
|
13
|
-
// 🎯 FIX: Identify the root node in the tree structure
|
|
14
|
-
const rootNode =
|
|
15
|
-
tree.find(
|
|
16
|
-
item =>
|
|
17
|
-
item.path === "/" ||
|
|
18
|
-
item.path === "" ||
|
|
19
|
-
item.slug === "index" ||
|
|
20
|
-
(item as any).isRoot === true ||
|
|
21
|
-
(item as any).isRootNode === 1,
|
|
22
|
-
) || tree[0];
|
|
23
|
-
|
|
24
|
-
// 🎯 FIX: If rootNode has children, display those children as top-level menu items.
|
|
25
|
-
// If not, filter out the root node by ID AND path.
|
|
26
|
-
const topLevelNavItems =
|
|
27
|
-
rootNode?.children && rootNode.children.length > 0
|
|
28
|
-
? rootNode.children
|
|
29
|
-
: tree.filter(
|
|
30
|
-
item =>
|
|
31
|
-
item.id !== rootNode?.id &&
|
|
32
|
-
item.slug !== rootNode?.slug &&
|
|
33
|
-
item.path !== "/" &&
|
|
34
|
-
item.path !== "",
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const isPathActive = (itemPath?: string) => {
|
|
38
|
-
const normItem = (itemPath || "").replace(/\/$/, "") || "/";
|
|
39
|
-
if (normItem === "/") return currentPath === "/";
|
|
40
|
-
return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const isExactActive = (itemPath?: string) => {
|
|
44
|
-
return currentPath === ((itemPath || "").replace(/\/$/, "") || "/");
|
|
45
|
-
};
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
<!-- 2-LINE HAMBURGER TOGGLE BUTTON -->
|
|
49
|
-
<button
|
|
50
|
-
type="button"
|
|
51
|
-
class="mobile-toggle"
|
|
52
|
-
id="menu-toggle"
|
|
53
|
-
aria-label="Toggle Navigation Menu"
|
|
54
|
-
aria-expanded="false"
|
|
55
|
-
>
|
|
56
|
-
<span class="line line-1"></span>
|
|
57
|
-
<span class="line line-2"></span>
|
|
58
|
-
</button>
|
|
59
|
-
|
|
60
|
-
<!-- NAVIGATION BLOCK & FULLSCREEN CONTAINER -->
|
|
61
|
-
<nav class="nav-block" id="nav-overlay">
|
|
62
|
-
<ul class="nav-list">
|
|
63
|
-
{
|
|
64
|
-
topLevelNavItems.map(item => {
|
|
65
|
-
const hasChildren = item.children && item.children.length > 0;
|
|
66
|
-
const isParentActive = isPathActive(item.path);
|
|
67
|
-
|
|
68
|
-
if (hasChildren) {
|
|
69
|
-
return (
|
|
70
|
-
<li class="nav-item">
|
|
71
|
-
<details class="nav-details">
|
|
72
|
-
<summary
|
|
73
|
-
class:list={[
|
|
74
|
-
"nav-button",
|
|
75
|
-
"dropdown-summary",
|
|
76
|
-
{ active: isParentActive },
|
|
77
|
-
]}
|
|
78
|
-
>
|
|
79
|
-
<span>{item.title}</span>
|
|
80
|
-
<span class="indicator">▼</span>
|
|
81
|
-
</summary>
|
|
82
|
-
<div class="submenu-container">
|
|
83
|
-
<ul class="dropdown-panel">
|
|
84
|
-
<li>
|
|
85
|
-
<a
|
|
86
|
-
href={item.path}
|
|
87
|
-
class:list={[
|
|
88
|
-
"dropdown-item",
|
|
89
|
-
"overview-item",
|
|
90
|
-
{ active: isExactActive(item.path) },
|
|
91
|
-
]}
|
|
92
|
-
>
|
|
93
|
-
{item.title} Overview
|
|
94
|
-
</a>
|
|
95
|
-
</li>
|
|
96
|
-
{item.children?.map(child => (
|
|
97
|
-
<li>
|
|
98
|
-
<a
|
|
99
|
-
href={child.path}
|
|
100
|
-
class:list={[
|
|
101
|
-
"dropdown-item",
|
|
102
|
-
{ active: isExactActive(child.path) },
|
|
103
|
-
]}
|
|
104
|
-
>
|
|
105
|
-
{child.title}
|
|
106
|
-
</a>
|
|
107
|
-
</li>
|
|
108
|
-
))}
|
|
109
|
-
</ul>
|
|
110
|
-
</div>
|
|
111
|
-
</details>
|
|
112
|
-
</li>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<li class="nav-item">
|
|
118
|
-
<a
|
|
119
|
-
href={item.path}
|
|
120
|
-
class:list={["nav-button", { active: isExactActive(item.path) }]}
|
|
121
|
-
>
|
|
122
|
-
{item.title}
|
|
123
|
-
</a>
|
|
124
|
-
</li>
|
|
125
|
-
);
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
</ul>
|
|
129
|
-
</nav>
|
|
130
|
-
|
|
131
|
-
<script is:inline>
|
|
132
|
-
const initMobileMenu = () => {
|
|
133
|
-
const toggleBtn = document.getElementById("menu-toggle");
|
|
134
|
-
const navOverlay = document.getElementById("nav-overlay");
|
|
135
|
-
|
|
136
|
-
if (!toggleBtn || !navOverlay) return;
|
|
137
|
-
|
|
138
|
-
// Remove old listeners by replacing the nodes (prevents duplicate bindings)
|
|
139
|
-
const newToggleBtn = toggleBtn.cloneNode(true);
|
|
140
|
-
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
|
|
141
|
-
|
|
142
|
-
const closeMenu = () => {
|
|
143
|
-
newToggleBtn.setAttribute("aria-expanded", "false");
|
|
144
|
-
newToggleBtn.classList.remove("is-open");
|
|
145
|
-
navOverlay.classList.remove("is-open");
|
|
146
|
-
document.body.classList.remove("menu-lock-scroll");
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
newToggleBtn.addEventListener("click", e => {
|
|
150
|
-
e.stopPropagation();
|
|
151
|
-
const isOpen = newToggleBtn.getAttribute("aria-expanded") === "true";
|
|
152
|
-
newToggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
|
|
153
|
-
newToggleBtn.classList.toggle("is-open");
|
|
154
|
-
navOverlay.classList.toggle("is-open");
|
|
155
|
-
document.body.classList.toggle("menu-lock-scroll");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Close menu when clicking nav links
|
|
159
|
-
const navLinks = navOverlay.querySelectorAll("a");
|
|
160
|
-
navLinks.forEach(link => {
|
|
161
|
-
link.addEventListener("click", () => {
|
|
162
|
-
closeMenu();
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Close details dropdowns when clicking outside
|
|
167
|
-
document.addEventListener("click", event => {
|
|
168
|
-
const allDetails = document.querySelectorAll("details.nav-details");
|
|
169
|
-
allDetails.forEach(details => {
|
|
170
|
-
if (details.hasAttribute("open") && !details.contains(event.target)) {
|
|
171
|
-
details.removeAttribute("open");
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
window.addEventListener("pageshow", closeMenu);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// Run on first load
|
|
180
|
-
if (document.readyState === "loading") {
|
|
181
|
-
document.addEventListener("DOMContentLoaded", initMobileMenu);
|
|
182
|
-
} else {
|
|
183
|
-
initMobileMenu();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Run on every Astro View Transition page navigation
|
|
187
|
-
document.addEventListener("astro:page-load", initMobileMenu);
|
|
188
|
-
</script>
|
|
189
|
-
|
|
190
|
-
<style>
|
|
191
|
-
.mobile-toggle {
|
|
192
|
-
display: flex;
|
|
193
|
-
flex-direction: column;
|
|
194
|
-
justify-content: center;
|
|
195
|
-
align-items: center;
|
|
196
|
-
gap: 6px;
|
|
197
|
-
width: 44px;
|
|
198
|
-
height: 44px;
|
|
199
|
-
background: transparent;
|
|
200
|
-
border: none;
|
|
201
|
-
cursor: pointer;
|
|
202
|
-
padding: 0;
|
|
203
|
-
z-index: 1001;
|
|
204
|
-
}
|
|
205
|
-
.mobile-toggle .line {
|
|
206
|
-
width: 22px;
|
|
207
|
-
height: 2px;
|
|
208
|
-
background-color: #0f172a;
|
|
209
|
-
transition:
|
|
210
|
-
transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
211
|
-
opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
212
|
-
transform-origin: center;
|
|
213
|
-
}
|
|
214
|
-
.mobile-toggle.is-open .line-1 {
|
|
215
|
-
transform: translateY(4px) rotate(45deg);
|
|
216
|
-
}
|
|
217
|
-
.mobile-toggle.is-open .line-2 {
|
|
218
|
-
transform: translateY(-4px) rotate(-45deg);
|
|
219
|
-
}
|
|
220
|
-
.nav-block {
|
|
221
|
-
position: fixed;
|
|
222
|
-
top: 0;
|
|
223
|
-
left: 0;
|
|
224
|
-
width: 100vw;
|
|
225
|
-
height: 100vh;
|
|
226
|
-
background-color: #ffffff;
|
|
227
|
-
padding: 80px 24px 40px 24px;
|
|
228
|
-
box-sizing: border-box;
|
|
229
|
-
display: flex;
|
|
230
|
-
flex-direction: column;
|
|
231
|
-
overflow-y: auto;
|
|
232
|
-
opacity: 0;
|
|
233
|
-
visibility: hidden;
|
|
234
|
-
pointer-events: none;
|
|
235
|
-
transition:
|
|
236
|
-
opacity 0.3s ease,
|
|
237
|
-
visibility 0.3s ease;
|
|
238
|
-
z-index: 1000;
|
|
239
|
-
}
|
|
240
|
-
.nav-block.is-open {
|
|
241
|
-
opacity: 1;
|
|
242
|
-
visibility: visible;
|
|
243
|
-
pointer-events: auto;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
.nav-list {
|
|
247
|
-
display: flex;
|
|
248
|
-
flex-direction: column;
|
|
249
|
-
gap: 8px;
|
|
250
|
-
list-style: none;
|
|
251
|
-
margin: 0;
|
|
252
|
-
padding: 0;
|
|
253
|
-
width: 100%;
|
|
254
|
-
}
|
|
255
|
-
.nav-item {
|
|
256
|
-
position: relative;
|
|
257
|
-
width: 100%;
|
|
258
|
-
}
|
|
259
|
-
.nav-button {
|
|
260
|
-
display: flex;
|
|
261
|
-
align-items: center;
|
|
262
|
-
justify-content: space-between;
|
|
263
|
-
width: 100%;
|
|
264
|
-
height: 52px;
|
|
265
|
-
padding: 0 16px;
|
|
266
|
-
font-size: 18px;
|
|
267
|
-
font-weight: 500;
|
|
268
|
-
color: #334155;
|
|
269
|
-
text-decoration: none;
|
|
270
|
-
background: transparent;
|
|
271
|
-
border: none;
|
|
272
|
-
border-radius: 4px;
|
|
273
|
-
cursor: pointer;
|
|
274
|
-
box-sizing: border-box;
|
|
275
|
-
}
|
|
276
|
-
.nav-button:hover {
|
|
277
|
-
color: #0f172a;
|
|
278
|
-
background: #f8fafc;
|
|
279
|
-
}
|
|
280
|
-
.nav-button.active {
|
|
281
|
-
color: #0c4a6e;
|
|
282
|
-
font-weight: 600;
|
|
283
|
-
}
|
|
284
|
-
.dropdown-summary {
|
|
285
|
-
list-style: none;
|
|
286
|
-
user-select: none;
|
|
287
|
-
}
|
|
288
|
-
.dropdown-summary::-webkit-details-marker {
|
|
289
|
-
display: none;
|
|
290
|
-
}
|
|
291
|
-
.indicator {
|
|
292
|
-
font-size: 11px;
|
|
293
|
-
color: #64748b;
|
|
294
|
-
transition: transform 0.2s ease;
|
|
295
|
-
}
|
|
296
|
-
details[open] .indicator {
|
|
297
|
-
transform: rotate(180deg);
|
|
298
|
-
}
|
|
299
|
-
details.nav-details {
|
|
300
|
-
border-radius: 6px;
|
|
301
|
-
transition: background-color 0.2s ease;
|
|
302
|
-
}
|
|
303
|
-
details[open].nav-details {
|
|
304
|
-
background: #f8fafc;
|
|
305
|
-
padding-bottom: 8px;
|
|
306
|
-
}
|
|
307
|
-
.submenu-container {
|
|
308
|
-
position: relative;
|
|
309
|
-
margin-left: 24px;
|
|
310
|
-
padding-left: 16px;
|
|
311
|
-
border-left: 2px solid #cbd5e1;
|
|
312
|
-
}
|
|
313
|
-
.dropdown-panel {
|
|
314
|
-
list-style: none;
|
|
315
|
-
margin: 4px 0 0 0;
|
|
316
|
-
padding: 0;
|
|
317
|
-
display: flex;
|
|
318
|
-
flex-direction: column;
|
|
319
|
-
gap: 4px;
|
|
320
|
-
}
|
|
321
|
-
.dropdown-item {
|
|
322
|
-
display: block;
|
|
323
|
-
padding: 10px 12px;
|
|
324
|
-
font-size: 16px;
|
|
325
|
-
font-weight: 400;
|
|
326
|
-
color: #475569;
|
|
327
|
-
text-decoration: none;
|
|
328
|
-
border-radius: 4px;
|
|
329
|
-
transition:
|
|
330
|
-
background 0.15s ease,
|
|
331
|
-
color 0.15s ease;
|
|
332
|
-
}
|
|
333
|
-
.overview-item {
|
|
334
|
-
font-weight: 500;
|
|
335
|
-
color: #334155;
|
|
336
|
-
}
|
|
337
|
-
.dropdown-item:hover {
|
|
338
|
-
background: #e2e8f0;
|
|
339
|
-
color: #0f172a;
|
|
340
|
-
}
|
|
341
|
-
.dropdown-item.active {
|
|
342
|
-
font-weight: 600;
|
|
343
|
-
color: #0c4a6e;
|
|
344
|
-
background: #e0f2fe;
|
|
345
|
-
}
|
|
346
|
-
body.menu-lock-scroll {
|
|
347
|
-
overflow: hidden !important;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
@media (min-width: 768px) {
|
|
351
|
-
.mobile-toggle {
|
|
352
|
-
display: none;
|
|
353
|
-
}
|
|
354
|
-
.nav-block {
|
|
355
|
-
position: static;
|
|
356
|
-
width: 100%;
|
|
357
|
-
height: auto;
|
|
358
|
-
background: transparent;
|
|
359
|
-
padding: 0;
|
|
360
|
-
opacity: 1;
|
|
361
|
-
visibility: visible;
|
|
362
|
-
pointer-events: auto;
|
|
363
|
-
overflow-y: visible;
|
|
364
|
-
display: block;
|
|
365
|
-
}
|
|
366
|
-
.nav-list {
|
|
367
|
-
flex-direction: row;
|
|
368
|
-
gap: 4px;
|
|
369
|
-
justify-content: flex-end;
|
|
370
|
-
}
|
|
371
|
-
.nav-item {
|
|
372
|
-
width: auto;
|
|
373
|
-
}
|
|
374
|
-
.nav-button {
|
|
375
|
-
display: inline-flex;
|
|
376
|
-
justify-content: flex-start;
|
|
377
|
-
gap: 6px;
|
|
378
|
-
height: 40px;
|
|
379
|
-
padding: 0 16px;
|
|
380
|
-
font-size: 14px;
|
|
381
|
-
border-radius: 4px;
|
|
382
|
-
margin-top: 4px;
|
|
383
|
-
}
|
|
384
|
-
details[open].nav-details {
|
|
385
|
-
background: transparent;
|
|
386
|
-
padding-bottom: 0;
|
|
387
|
-
}
|
|
388
|
-
.submenu-container {
|
|
389
|
-
margin-left: 0;
|
|
390
|
-
padding-left: 0;
|
|
391
|
-
border-left: none;
|
|
392
|
-
}
|
|
393
|
-
.dropdown-panel {
|
|
394
|
-
position: absolute;
|
|
395
|
-
top: 100%;
|
|
396
|
-
left: 0;
|
|
397
|
-
min-width: 220px;
|
|
398
|
-
background: #ffffff;
|
|
399
|
-
border: 1px solid #e2e8f0;
|
|
400
|
-
margin: 0;
|
|
401
|
-
gap: 0;
|
|
402
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
403
|
-
border-radius: 4px;
|
|
404
|
-
padding: 4px;
|
|
405
|
-
}
|
|
406
|
-
.dropdown-item {
|
|
407
|
-
font-size: 14px;
|
|
408
|
-
padding: 8px 16px;
|
|
409
|
-
border-radius: 4px;
|
|
410
|
-
border-bottom: none;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
</style>
|
|
@@ -1,180 +0,0 @@
|
|
|
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, 1920, 2560],
|
|
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
|
-
// 🎯 High-DPI / Retina Optimization:
|
|
94
|
-
// - Mobile: Includes 320, 640, 768 (covers 2x and 3x Retina mobile displays)
|
|
95
|
-
// - Tablet: Includes 768, 1024, 1352 (covers iPad Pro Retina density)
|
|
96
|
-
// - Desktop: Uses desktopWidths (defaults to 768, 1352, 1920, 2560 for 1440p and 4K)
|
|
97
|
-
const mobileSrcset =
|
|
98
|
-
!isSvg && rawMobileUrl
|
|
99
|
-
? buildAgnosticSrcset(rawMobileUrl, [320, 640, 768])
|
|
100
|
-
: null;
|
|
101
|
-
|
|
102
|
-
const tabletSrcset =
|
|
103
|
-
!isSvg && rawTabletUrl
|
|
104
|
-
? buildAgnosticSrcset(rawTabletUrl, [768, 1024, 1352])
|
|
105
|
-
: null;
|
|
106
|
-
|
|
107
|
-
const desktopSrcset =
|
|
108
|
-
!isSvg && rawDesktopUrl
|
|
109
|
-
? buildAgnosticSrcset(
|
|
110
|
-
rawDesktopUrl,
|
|
111
|
-
desktopWidths.length ? desktopWidths : [768, 1352, 1920, 2560],
|
|
112
|
-
)
|
|
113
|
-
: null;
|
|
114
|
-
|
|
115
|
-
const fallbackUrl =
|
|
116
|
-
getImageUri(rawDesktopUrl, {
|
|
117
|
-
width: isSvg ? undefined : baseWidth,
|
|
118
|
-
quality: 75,
|
|
119
|
-
}) || "";
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
isValidImage &&
|
|
124
|
-
(isSvg ? (
|
|
125
|
-
/* 🎯 Clean single <img> for SVG Vectors */
|
|
126
|
-
<img
|
|
127
|
-
src={fallbackUrl}
|
|
128
|
-
alt={targetImage.alt || targetImage.filename || ""}
|
|
129
|
-
width={rawWidth > 0 ? rawWidth : undefined}
|
|
130
|
-
height={rawHeight > 0 ? rawHeight : undefined}
|
|
131
|
-
loading={loading}
|
|
132
|
-
fetchpriority={fetchpriority}
|
|
133
|
-
decoding={loading === "eager" ? "sync" : "async"}
|
|
134
|
-
class={className}
|
|
135
|
-
/>
|
|
136
|
-
) : (
|
|
137
|
-
/* 🎯 Responsive <picture> for Raster Images (.png, .jpg, .webp) */
|
|
138
|
-
<picture>
|
|
139
|
-
{Boolean(mobileSrcset) && (
|
|
140
|
-
<source
|
|
141
|
-
media="(max-width: 767px)"
|
|
142
|
-
srcset={mobileSrcset as string}
|
|
143
|
-
sizes="(max-width: 767px) calc(100vw - 48px), 768px"
|
|
144
|
-
width={mobileW}
|
|
145
|
-
height={mobileH}
|
|
146
|
-
/>
|
|
147
|
-
)}
|
|
148
|
-
|
|
149
|
-
{Boolean(tabletSrcset) && (
|
|
150
|
-
<source
|
|
151
|
-
media="(min-width: 768px) and (max-width: 1024px)"
|
|
152
|
-
srcset={tabletSrcset as string}
|
|
153
|
-
sizes="(max-width: 1024px) calc(100vw - 48px), 1024px"
|
|
154
|
-
width={tabletW}
|
|
155
|
-
height={tabletH}
|
|
156
|
-
/>
|
|
157
|
-
)}
|
|
158
|
-
|
|
159
|
-
{Boolean(desktopSrcset) && (
|
|
160
|
-
<source
|
|
161
|
-
srcset={desktopSrcset as string}
|
|
162
|
-
sizes={sizes}
|
|
163
|
-
width={baseWidth}
|
|
164
|
-
height={baseHeight}
|
|
165
|
-
/>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
<img
|
|
169
|
-
src={fallbackUrl}
|
|
170
|
-
alt={targetImage.alt || targetImage.filename || ""}
|
|
171
|
-
width={baseWidth}
|
|
172
|
-
height={baseHeight}
|
|
173
|
-
loading={loading}
|
|
174
|
-
fetchpriority={fetchpriority}
|
|
175
|
-
decoding={loading === "eager" ? "sync" : "async"}
|
|
176
|
-
class={className}
|
|
177
|
-
/>
|
|
178
|
-
</picture>
|
|
179
|
-
))
|
|
180
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
// 🎯 1. Added language to Props
|
|
12
|
-
language?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// 🎯 2. Destructure language
|
|
16
|
-
const {
|
|
17
|
-
node = {},
|
|
18
|
-
content = {},
|
|
19
|
-
normalizedSlug,
|
|
20
|
-
isHomePage,
|
|
21
|
-
language,
|
|
22
|
-
} = Astro.props;
|
|
23
|
-
|
|
24
|
-
const customCss = node.css || "";
|
|
25
|
-
const customJsHeader = node.jsHeader || "";
|
|
26
|
-
const customJsFooter = node.jsFooter || "";
|
|
27
|
-
|
|
28
|
-
// Now this works without crashing!
|
|
29
|
-
const pageLang = language || node?.language || "en";
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
<!doctype html>
|
|
33
|
-
<html lang={pageLang}>
|
|
34
|
-
<head>
|
|
35
|
-
<MetaHead
|
|
36
|
-
node={node}
|
|
37
|
-
content={content}
|
|
38
|
-
normalizedSlug={normalizedSlug}
|
|
39
|
-
isHomePage={isHomePage}
|
|
40
|
-
/>
|
|
41
|
-
|
|
42
|
-
<!-- 🎯 3. Added is:inline so Astro actually renders your database CSS -->
|
|
43
|
-
{customCss && <style is:inline set:html={customCss} />}
|
|
44
|
-
|
|
45
|
-
<!-- 🎯 4. Added is:inline so Astro executes your header JS -->
|
|
46
|
-
{customJsHeader && <script is:inline set:html={customJsHeader} />}
|
|
47
|
-
</head>
|
|
48
|
-
|
|
49
|
-
<body>
|
|
50
|
-
<!-- Renders your Blocks (like the HTML Block) natively -->
|
|
51
|
-
<slot />
|
|
52
|
-
|
|
53
|
-
<!-- 🎯 5. Added is:inline to footer JS -->
|
|
54
|
-
{customJsFooter && <script is:inline set:html={customJsFooter} />}
|
|
55
|
-
</body>
|
|
56
|
-
</html>
|
|
@@ -1,63 +0,0 @@
|
|
|
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
|
-
// 🎯 FIX: Extract the footer blocks from the payload again
|
|
36
|
-
const footerBlocks = content?.footer || [];
|
|
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
|
-
{/* 🎯 FIX: Pass the blocks and contentMap into the Footer */}
|
|
61
|
-
<Footer blocks={footerBlocks} contentMap={content} />
|
|
62
|
-
</body>
|
|
63
|
-
</html>
|