@brightsideoy/kama 0.1.0-alpha.113 → 0.1.0-alpha.115
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/bin/cli.js +172 -379
- package/content/definitions/blocks/button.yaml +7 -8
- package/content/definitions/blocks/header.yaml +7 -9
- package/content/definitions/blocks/header_image.yaml +14 -15
- package/content/definitions/blocks/hero.yaml +22 -21
- package/content/definitions/blocks/image_card.yaml +46 -47
- package/content/definitions/blocks/single_image.yaml +19 -20
- package/content/definitions/blocks/text.yaml +8 -9
- package/content/definitions/channels/ecommerce.yaml +14 -16
- package/content/definitions/channels/website.yaml +20 -21
- package/content/definitions/field-sets/access.yaml +3 -5
- package/content/definitions/field-sets/channel-base.yaml +25 -25
- package/content/definitions/field-sets/content-block.yaml +36 -41
- package/content/definitions/field-sets/page-base.yaml +40 -45
- package/content/definitions/field-sets/page-meta.yaml +1 -4
- package/content/definitions/field-sets/page-social.yaml +6 -8
- package/content/definitions/field-sets/tree-base.yaml +25 -25
- package/content/definitions/nodes/address.yaml +29 -21
- package/content/definitions/nodes/article.yaml +24 -19
- package/content/definitions/nodes/event.yaml +32 -26
- package/content/definitions/nodes/external-link.yaml +20 -17
- package/content/definitions/nodes/page.yaml +38 -35
- package/content/definitions/nodes/person.yaml +29 -27
- package/content/definitions/nodes/product.yaml +52 -49
- package/content/definitions/trees/navigation.yaml +26 -21
- package/content/definitions/trees/social-links.yaml +5 -2
- package/dist/assets/index-8T39DEP3.js +162 -0
- package/dist/assets/index-B9oh2_TL.css +1 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6492 -4249
- package/package.json +5 -6
- package/schema.sql +5 -13
- package/templates/astro-starter/astro.config.mjs +40 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +125 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
- package/templates/astro-starter/src/components/global/Footer.astro +48 -0
- package/templates/astro-starter/src/components/global/Header.astro +92 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +148 -0
- package/templates/astro-starter/src/kama-image-service.ts +41 -0
- package/templates/astro-starter/src/layouts/Layout.astro +280 -0
- package/templates/astro-starter/src/lib/cms.ts +149 -0
- package/templates/astro-starter/src/lib/media-url.ts +34 -0
- package/templates/astro-starter/src/middleware.ts +306 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +234 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/test-local.sh +76 -0
- package/dist/assets/index-10GEw0KY.css +0 -1
- package/dist/assets/index-THJ6BqPP.js +0 -1394
- package/kama-worker/bundled-definitions.js +0 -2394
- package/kama-worker/bundled-definitions.ts +0 -2395
- package/scripts/bundle-definitions.js +0 -224
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/global/NavigationMenu.astro
|
|
3
|
+
import type { TreeNode } from "@/lib/cms";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
tree: TreeNode[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { tree = [] } = Astro.props;
|
|
10
|
+
|
|
11
|
+
// 1. Get current path from Astro context
|
|
12
|
+
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
13
|
+
|
|
14
|
+
const homeNode = tree.find(
|
|
15
|
+
(item: TreeNode) => item.slug === "home" || item.path === "/",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const topLevelNavItems =
|
|
19
|
+
homeNode?.children && homeNode.children.length > 0
|
|
20
|
+
? homeNode.children
|
|
21
|
+
: tree.filter((item: TreeNode) => item.slug !== "home");
|
|
22
|
+
|
|
23
|
+
// Normalize path comparison helper
|
|
24
|
+
const normalizePath = (path?: string) => {
|
|
25
|
+
if (!path) return "";
|
|
26
|
+
return path.replace(/\/$/, "") || "/";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Active trail check (parent active when on a child page)
|
|
30
|
+
const isPathActive = (itemPath?: string) => {
|
|
31
|
+
const normItem = normalizePath(itemPath);
|
|
32
|
+
if (normItem === "/") return currentPath === "/";
|
|
33
|
+
return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Exact path check
|
|
37
|
+
const isExactActive = (itemPath?: string) => {
|
|
38
|
+
return currentPath === normalizePath(itemPath);
|
|
39
|
+
};
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<!-- 2-LINE HAMBURGER TOGGLE BUTTON -->
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="mobile-toggle"
|
|
46
|
+
id="menu-toggle"
|
|
47
|
+
aria-label="Toggle Navigation Menu"
|
|
48
|
+
aria-expanded="false"
|
|
49
|
+
>
|
|
50
|
+
<span class="line line-1"></span>
|
|
51
|
+
<span class="line line-2"></span>
|
|
52
|
+
</button>
|
|
53
|
+
|
|
54
|
+
<!-- NAVIGATION BLOCK & FULLSCREEN CONTAINER -->
|
|
55
|
+
<nav class="nav-block" id="nav-overlay">
|
|
56
|
+
<ul class="nav-list">
|
|
57
|
+
{
|
|
58
|
+
topLevelNavItems.map((item: TreeNode) => {
|
|
59
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
60
|
+
const isParentActive = isPathActive(item.path);
|
|
61
|
+
|
|
62
|
+
if (hasChildren) {
|
|
63
|
+
return (
|
|
64
|
+
<li class="nav-item">
|
|
65
|
+
<details class="nav-details">
|
|
66
|
+
<summary
|
|
67
|
+
class:list={[
|
|
68
|
+
"nav-button",
|
|
69
|
+
"dropdown-summary",
|
|
70
|
+
{ active: isParentActive },
|
|
71
|
+
]}
|
|
72
|
+
>
|
|
73
|
+
<span>{item.title}</span>
|
|
74
|
+
<span class="indicator">▼</span>
|
|
75
|
+
</summary>
|
|
76
|
+
<div class="submenu-container">
|
|
77
|
+
<ul class="dropdown-panel">
|
|
78
|
+
<li>
|
|
79
|
+
<a
|
|
80
|
+
href={item.path}
|
|
81
|
+
class:list={[
|
|
82
|
+
"dropdown-item",
|
|
83
|
+
"overview-item",
|
|
84
|
+
{ active: isExactActive(item.path) },
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
{item.title} Overview
|
|
88
|
+
</a>
|
|
89
|
+
</li>
|
|
90
|
+
{item.children?.map((child: TreeNode) => (
|
|
91
|
+
<li>
|
|
92
|
+
<a
|
|
93
|
+
href={child.path}
|
|
94
|
+
class:list={[
|
|
95
|
+
"dropdown-item",
|
|
96
|
+
{ active: isExactActive(child.path) },
|
|
97
|
+
]}
|
|
98
|
+
>
|
|
99
|
+
{child.title}
|
|
100
|
+
</a>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
</details>
|
|
106
|
+
</li>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<li class="nav-item">
|
|
112
|
+
<a
|
|
113
|
+
href={item.path}
|
|
114
|
+
class:list={["nav-button", { active: isExactActive(item.path) }]}
|
|
115
|
+
>
|
|
116
|
+
{item.title}
|
|
117
|
+
</a>
|
|
118
|
+
</li>
|
|
119
|
+
);
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
</ul>
|
|
123
|
+
</nav>
|
|
124
|
+
<script>
|
|
125
|
+
const toggleBtn = document.getElementById("menu-toggle");
|
|
126
|
+
const navOverlay = document.getElementById("nav-overlay");
|
|
127
|
+
|
|
128
|
+
const closeMenu = () => {
|
|
129
|
+
if (toggleBtn && navOverlay) {
|
|
130
|
+
toggleBtn.setAttribute("aria-expanded", "false");
|
|
131
|
+
toggleBtn.classList.remove("is-open");
|
|
132
|
+
navOverlay.classList.remove("is-open");
|
|
133
|
+
document.body.classList.remove("menu-lock-scroll");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (toggleBtn && navOverlay) {
|
|
138
|
+
// Toggle Menu
|
|
139
|
+
toggleBtn.addEventListener("click", () => {
|
|
140
|
+
const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
|
|
141
|
+
toggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
|
|
142
|
+
toggleBtn.classList.toggle("is-open");
|
|
143
|
+
navOverlay.classList.toggle("is-open");
|
|
144
|
+
document.body.classList.toggle("menu-lock-scroll");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 🎯 UPDATED LINK CLICK LOGIC
|
|
148
|
+
const navLinks = navOverlay.querySelectorAll("a");
|
|
149
|
+
navLinks.forEach(link => {
|
|
150
|
+
link.addEventListener("click", e => {
|
|
151
|
+
// Check where the link is going
|
|
152
|
+
const targetUrl = new URL(link.href);
|
|
153
|
+
const currentUrl = new window.URL(window.location.href);
|
|
154
|
+
|
|
155
|
+
// If it's a link to the exact page we are already on (like a #hash anchor),
|
|
156
|
+
// close the menu so the user can see the page.
|
|
157
|
+
if (targetUrl.pathname === currentUrl.pathname) {
|
|
158
|
+
closeMenu();
|
|
159
|
+
}
|
|
160
|
+
// If it's a new page, DO NOTHING. The menu stays visually open,
|
|
161
|
+
// acting as a loading screen until the browser paints the new page!
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Close menu on back/forward browser navigation (bfcache)
|
|
167
|
+
window.addEventListener("pageshow", () => {
|
|
168
|
+
closeMenu();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Close desktop dropdowns when clicking outside
|
|
172
|
+
document.addEventListener("click", event => {
|
|
173
|
+
const allDetails = document.querySelectorAll("details.nav-details");
|
|
174
|
+
allDetails.forEach(details => {
|
|
175
|
+
if (
|
|
176
|
+
details.hasAttribute("open") &&
|
|
177
|
+
!details.contains(event.target as Node)
|
|
178
|
+
) {
|
|
179
|
+
details.removeAttribute("open");
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<style>
|
|
186
|
+
/* -------------------------------------------------------------
|
|
187
|
+
MOBILE HAMBURGER BUTTON (2-LINE TRANSFORMING TO X)
|
|
188
|
+
------------------------------------------------------------- */
|
|
189
|
+
.mobile-toggle {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 6px;
|
|
195
|
+
width: 44px;
|
|
196
|
+
height: 44px;
|
|
197
|
+
background: transparent;
|
|
198
|
+
border: none;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
padding: 0;
|
|
201
|
+
z-index: 1001;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.mobile-toggle .line {
|
|
205
|
+
width: 22px;
|
|
206
|
+
height: 2px;
|
|
207
|
+
background-color: #0f172a;
|
|
208
|
+
transition:
|
|
209
|
+
transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
210
|
+
opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
211
|
+
transform-origin: center;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.mobile-toggle.is-open .line-1 {
|
|
215
|
+
transform: translateY(4px) rotate(45deg);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.mobile-toggle.is-open .line-2 {
|
|
219
|
+
transform: translateY(-4px) rotate(-45deg);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* -------------------------------------------------------------
|
|
223
|
+
MOBILE FULLSCREEN OVERLAY (DEFAULT MOBILE VIEW < 768px)
|
|
224
|
+
------------------------------------------------------------- */
|
|
225
|
+
.nav-block {
|
|
226
|
+
position: fixed;
|
|
227
|
+
top: 0;
|
|
228
|
+
left: 0;
|
|
229
|
+
width: 100vw;
|
|
230
|
+
height: 100vh;
|
|
231
|
+
background-color: #ffffff;
|
|
232
|
+
padding: 80px 24px 40px 24px;
|
|
233
|
+
box-sizing: border-box;
|
|
234
|
+
display: flex;
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
overflow-y: auto;
|
|
237
|
+
opacity: 0;
|
|
238
|
+
visibility: hidden;
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
transition:
|
|
241
|
+
opacity 0.3s ease,
|
|
242
|
+
visibility 0.3s ease;
|
|
243
|
+
z-index: 1000;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.nav-block.is-open {
|
|
247
|
+
opacity: 1;
|
|
248
|
+
visibility: visible;
|
|
249
|
+
pointer-events: auto;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.nav-list {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
gap: 8px;
|
|
256
|
+
list-style: none;
|
|
257
|
+
margin: 0;
|
|
258
|
+
padding: 0;
|
|
259
|
+
width: 100%;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.nav-item {
|
|
263
|
+
position: relative;
|
|
264
|
+
width: 100%;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.nav-button {
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
justify-content: space-between;
|
|
271
|
+
width: 100%;
|
|
272
|
+
height: 52px;
|
|
273
|
+
padding: 0 16px;
|
|
274
|
+
font-size: 18px;
|
|
275
|
+
font-weight: 500;
|
|
276
|
+
color: #334155;
|
|
277
|
+
text-decoration: none;
|
|
278
|
+
background: transparent;
|
|
279
|
+
border: none;
|
|
280
|
+
border-radius: 4px;
|
|
281
|
+
cursor: pointer;
|
|
282
|
+
box-sizing: border-box;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.nav-button:hover {
|
|
286
|
+
color: #0f172a;
|
|
287
|
+
background: #f8fafc;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.nav-button.active {
|
|
291
|
+
color: #0c4a6e;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.dropdown-summary {
|
|
296
|
+
list-style: none;
|
|
297
|
+
user-select: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.dropdown-summary::-webkit-details-marker {
|
|
301
|
+
display: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.indicator {
|
|
305
|
+
font-size: 11px;
|
|
306
|
+
color: #64748b;
|
|
307
|
+
transition: transform 0.2s ease;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
details[open] .indicator {
|
|
311
|
+
transform: rotate(180deg);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* OPEN DROPDOWN SINGLE BLOCK WITH CONNECTING LINE & INDENTATION */
|
|
315
|
+
details.nav-details {
|
|
316
|
+
border-radius: 6px;
|
|
317
|
+
transition: background-color 0.2s ease;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
details[open].nav-details {
|
|
321
|
+
background: #f8fafc;
|
|
322
|
+
padding-bottom: 8px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* Left vertical accent line connecting parent item and sub-levels */
|
|
326
|
+
.submenu-container {
|
|
327
|
+
position: relative;
|
|
328
|
+
margin-left: 24px;
|
|
329
|
+
padding-left: 16px;
|
|
330
|
+
border-left: 2px solid #cbd5e1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.dropdown-panel {
|
|
334
|
+
list-style: none;
|
|
335
|
+
margin: 4px 0 0 0;
|
|
336
|
+
padding: 0;
|
|
337
|
+
display: flex;
|
|
338
|
+
flex-direction: column;
|
|
339
|
+
gap: 4px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.dropdown-item {
|
|
343
|
+
display: block;
|
|
344
|
+
padding: 10px 12px;
|
|
345
|
+
font-size: 16px;
|
|
346
|
+
font-weight: 400;
|
|
347
|
+
color: #475569;
|
|
348
|
+
text-decoration: none;
|
|
349
|
+
border-radius: 4px;
|
|
350
|
+
transition:
|
|
351
|
+
background 0.15s ease,
|
|
352
|
+
color 0.15s ease;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.overview-item {
|
|
356
|
+
font-weight: 500;
|
|
357
|
+
color: #334155;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.dropdown-item:hover {
|
|
361
|
+
background: #e2e8f0;
|
|
362
|
+
color: #0f172a;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.dropdown-item.active {
|
|
366
|
+
font-weight: 600;
|
|
367
|
+
color: #0c4a6e;
|
|
368
|
+
background: #e0f2fe;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Prevent body scrolling when menu open */
|
|
372
|
+
:global(body.menu-lock-scroll) {
|
|
373
|
+
overflow: hidden !important;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* -------------------------------------------------------------
|
|
377
|
+
DESKTOP MEDIA QUERY (>= 768px)
|
|
378
|
+
------------------------------------------------------------- */
|
|
379
|
+
@media (min-width: 768px) {
|
|
380
|
+
.mobile-toggle {
|
|
381
|
+
display: none;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.nav-block {
|
|
385
|
+
position: static;
|
|
386
|
+
width: 100%;
|
|
387
|
+
height: auto;
|
|
388
|
+
background: transparent;
|
|
389
|
+
padding: 0;
|
|
390
|
+
opacity: 1;
|
|
391
|
+
visibility: visible;
|
|
392
|
+
pointer-events: auto;
|
|
393
|
+
overflow-y: visible;
|
|
394
|
+
display: block;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.nav-list {
|
|
398
|
+
flex-direction: row;
|
|
399
|
+
gap: 4px;
|
|
400
|
+
justify-content: flex-end;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.nav-item {
|
|
404
|
+
width: auto;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.nav-button {
|
|
408
|
+
display: inline-flex;
|
|
409
|
+
justify-content: flex-start;
|
|
410
|
+
gap: 6px;
|
|
411
|
+
height: 40px;
|
|
412
|
+
padding: 0 16px;
|
|
413
|
+
font-size: 14px;
|
|
414
|
+
border-radius: 4px;
|
|
415
|
+
margin-top: 4px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
details[open].nav-details {
|
|
419
|
+
background: transparent;
|
|
420
|
+
padding-bottom: 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.submenu-container {
|
|
424
|
+
margin-left: 0;
|
|
425
|
+
padding-left: 0;
|
|
426
|
+
border-left: none;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.dropdown-panel {
|
|
430
|
+
position: absolute;
|
|
431
|
+
top: 100%;
|
|
432
|
+
left: 0;
|
|
433
|
+
min-width: 220px;
|
|
434
|
+
background: #ffffff;
|
|
435
|
+
border: 1px solid #e2e8f0;
|
|
436
|
+
margin: 0;
|
|
437
|
+
gap: 0;
|
|
438
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
439
|
+
border-radius: 4px;
|
|
440
|
+
padding: 4px;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.dropdown-item {
|
|
444
|
+
font-size: 14px;
|
|
445
|
+
padding: 8px 16px;
|
|
446
|
+
border-radius: 4px;
|
|
447
|
+
border-bottom: none;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
</style>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/ui/KamaImage.astro
|
|
3
|
+
|
|
4
|
+
import { getMediaUrl } from "@/lib/cms";
|
|
5
|
+
import { buildAgnosticImageUrl } from "@/lib/media-url";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
image: any;
|
|
9
|
+
sizes?: string;
|
|
10
|
+
desktopWidths?: number[];
|
|
11
|
+
idealWidth?: number;
|
|
12
|
+
class?: string;
|
|
13
|
+
loading?: "lazy" | "eager";
|
|
14
|
+
fetchpriority?: "auto" | "high" | "low";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
image = {},
|
|
19
|
+
sizes = "100vw",
|
|
20
|
+
desktopWidths = [768, 1352],
|
|
21
|
+
idealWidth = 1352,
|
|
22
|
+
class: className = "",
|
|
23
|
+
loading = "lazy",
|
|
24
|
+
fetchpriority = "auto",
|
|
25
|
+
} = Astro.props;
|
|
26
|
+
|
|
27
|
+
if (!image || (!image.url && !image.path && !image.variants)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const variants = image.variants || {};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve CMS media URLs.
|
|
35
|
+
*/
|
|
36
|
+
const rawMobileUrl = getMediaUrl(variants.mobile?.url);
|
|
37
|
+
const rawTabletUrl = getMediaUrl(variants.tablet?.url);
|
|
38
|
+
const rawDesktopUrl = getMediaUrl(
|
|
39
|
+
variants.standard?.url || image.url || image.path,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve dimensions.
|
|
44
|
+
* Crop dimensions take precedence over original image dimensions.
|
|
45
|
+
*/
|
|
46
|
+
const mobileW = Number(variants.mobile?.cropCoordinates?.width || 768);
|
|
47
|
+
const mobileH = Number(variants.mobile?.cropCoordinates?.height || 576);
|
|
48
|
+
|
|
49
|
+
const tabletW = Number(variants.tablet?.cropCoordinates?.width || 1024);
|
|
50
|
+
const tabletH = Number(variants.tablet?.cropCoordinates?.height || 512);
|
|
51
|
+
|
|
52
|
+
const baseWidth = Number(
|
|
53
|
+
variants.standard?.cropCoordinates?.width || image.width || idealWidth,
|
|
54
|
+
);
|
|
55
|
+
const baseHeight = Number(
|
|
56
|
+
variants.standard?.cropCoordinates?.height ||
|
|
57
|
+
image.height ||
|
|
58
|
+
Math.round(idealWidth * 0.5),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build responsive srcset using query-based media proxy URLs (/media/foo.jpg?w=768&q=75&f=auto)
|
|
63
|
+
*/
|
|
64
|
+
function buildAgnosticSrcset(
|
|
65
|
+
url: string,
|
|
66
|
+
targetWidths: number[],
|
|
67
|
+
quality = 75,
|
|
68
|
+
): string {
|
|
69
|
+
if (!url) return "";
|
|
70
|
+
|
|
71
|
+
return targetWidths
|
|
72
|
+
.map(width => `${buildAgnosticImageUrl(url, width, quality)} ${width}w`)
|
|
73
|
+
.join(", ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build srcsets.
|
|
78
|
+
*/
|
|
79
|
+
const mobileSrcset = rawMobileUrl
|
|
80
|
+
? buildAgnosticSrcset(rawMobileUrl, [400, 768])
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
const tabletSrcset = rawTabletUrl
|
|
84
|
+
? buildAgnosticSrcset(rawTabletUrl, [768, 1024])
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
const desktopSrcset = rawDesktopUrl
|
|
88
|
+
? buildAgnosticSrcset(rawDesktopUrl, desktopWidths)
|
|
89
|
+
: null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fallback image URL: /media/foo.jpg?w=1352&q=75&f=auto
|
|
93
|
+
*/
|
|
94
|
+
const absoluteFallback = rawDesktopUrl
|
|
95
|
+
? buildAgnosticImageUrl(rawDesktopUrl, idealWidth, 75)
|
|
96
|
+
: "";
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
<picture>
|
|
100
|
+
{/* Mobile Variant (< 768px) */}
|
|
101
|
+
{
|
|
102
|
+
mobileSrcset && (
|
|
103
|
+
<source
|
|
104
|
+
media="(max-width: 767px)"
|
|
105
|
+
srcset={mobileSrcset}
|
|
106
|
+
sizes="(max-width: 767px) calc(100vw - 48px), 768px"
|
|
107
|
+
width={mobileW}
|
|
108
|
+
height={mobileH}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
{/* Tablet Variant (768px - 1024px) */}
|
|
114
|
+
{
|
|
115
|
+
tabletSrcset && (
|
|
116
|
+
<source
|
|
117
|
+
media="(min-width: 768px) and (max-width: 1024px)"
|
|
118
|
+
srcset={tabletSrcset}
|
|
119
|
+
sizes="(max-width: 1024px) calc(100vw - 48px), 1024px"
|
|
120
|
+
width={tabletW}
|
|
121
|
+
height={tabletH}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
{/* Desktop Variant (> 1024px) */}
|
|
127
|
+
{
|
|
128
|
+
desktopSrcset && (
|
|
129
|
+
<source
|
|
130
|
+
srcset={desktopSrcset}
|
|
131
|
+
sizes={sizes}
|
|
132
|
+
width={baseWidth}
|
|
133
|
+
height={baseHeight}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
<img
|
|
139
|
+
src={absoluteFallback}
|
|
140
|
+
alt={image.alt || image.filename || ""}
|
|
141
|
+
width={baseWidth}
|
|
142
|
+
height={baseHeight}
|
|
143
|
+
loading={loading}
|
|
144
|
+
fetchpriority={fetchpriority}
|
|
145
|
+
decoding={loading === "eager" ? "sync" : "async"}
|
|
146
|
+
class={className}
|
|
147
|
+
/>
|
|
148
|
+
</picture>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/kama-image-service.ts
|
|
2
|
+
import type { LocalImageService } from "astro";
|
|
3
|
+
|
|
4
|
+
const kamaImageService: LocalImageService = {
|
|
5
|
+
getURL(options) {
|
|
6
|
+
let src = options.src;
|
|
7
|
+
|
|
8
|
+
if (typeof src === 'object' && 'src' in src) {
|
|
9
|
+
src = src.src;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof src === 'string') {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(src, 'http://localhost');
|
|
15
|
+
|
|
16
|
+
if (options.width) url.searchParams.set('w', Math.round(options.width).toString());
|
|
17
|
+
if (options.height) url.searchParams.set('h', Math.round(options.height).toString());
|
|
18
|
+
if (options.quality) url.searchParams.set('q', options.quality.toString());
|
|
19
|
+
if (options.format) url.searchParams.set('f', options.format.toString());
|
|
20
|
+
|
|
21
|
+
return src.startsWith('http') ? url.toString() : (url.pathname + url.search);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return src;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return String(src);
|
|
27
|
+
},
|
|
28
|
+
parseURL(url) {
|
|
29
|
+
return { src: url.pathname + url.search };
|
|
30
|
+
},
|
|
31
|
+
getHTMLAttributes(options) {
|
|
32
|
+
return {
|
|
33
|
+
width: options.width,
|
|
34
|
+
height: options.height,
|
|
35
|
+
loading: options.loading || 'lazy',
|
|
36
|
+
decoding: options.decoding || 'async',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default kamaImageService;
|