@brightsideoy/kama 1.0.0-alpha.2 → 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/bin/cli.js +604 -364
- package/content/definitions/block-kama-forms.yaml +5 -0
- package/content/definitions/blocks/button.yaml +15 -5
- package/content/definitions/blocks/image-gallery.yaml +34 -0
- package/content/definitions/blocks/node-list.yaml +75 -0
- package/content/definitions/blocks/youtube.yaml +8 -0
- package/content/definitions/field-sets/access.yaml +10 -0
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/content/definitions/field-sets/page-base.yaml +16 -1
- package/content/definitions/nodes/article.yaml +17 -1
- package/content/definitions/taxonomies/categories.yaml +5 -2
- package/content/definitions/taxonomies/media-tag.yaml +15 -0
- package/content/definitions/taxonomies/tags.yaml +5 -2
- package/content/forms/contact.yaml +44 -0
- package/content/forms/subscription.yaml +32 -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/CmsBadge-BNsgxyyB.css +1 -0
- package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
- package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
- package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
- package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
- package/dist/assets/CmsDataRow-CYNxfVaZ.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-BO_vWABq.css +1 -0
- package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
- package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
- package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
- package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
- package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
- package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
- package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
- package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
- package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
- package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
- package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
- package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
- package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
- package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
- package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
- package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
- package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
- package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
- package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
- package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
- package/dist/assets/circle-check-A-QeFqt3.js +1 -0
- package/dist/assets/database-hg5SqKLG.js +1 -0
- package/dist/assets/folder-tree-BWYot72H.js +1 -0
- package/dist/assets/globe-BraoljaN.js +1 -0
- package/dist/assets/history-pqIbKK9u.js +1 -0
- package/dist/assets/index-CY0VYSyt.css +1 -0
- package/dist/assets/index-DdG9Pozx.js +14 -0
- package/dist/assets/layers-B6RgfpPR.js +1 -0
- package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
- package/dist/assets/monitor-CrpvInqG.js +1 -0
- package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
- package/dist/assets/plus-CX_GHzXs.js +1 -0
- package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
- package/dist/assets/save-Ctv8vW9H.js +1 -0
- package/dist/assets/schema-helpers-DvficKko.js +1 -0
- package/dist/assets/send-F-mJeL54.js +1 -0
- package/dist/assets/smartphone-EPce6VdP.js +1 -0
- package/dist/assets/trash-2-CAUF1KtI.js +1 -0
- package/dist/assets/useLocalStorage-C805uplI.js +1 -0
- package/dist/assets/useNodes-CqFlc72m.js +1 -0
- package/dist/assets/user-IFlT6mYq.js +1 -0
- package/dist/build-seed.js +495 -0
- package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
- package/dist/index.html +34 -2
- package/dist/index.js +3116 -1315
- package/dist/schema-compiler.js +5 -5
- package/documentation/EXTENSIONS.md +204 -0
- package/extension-registry.js +5 -0
- package/package.json +13 -6
- package/schema.sql +44 -1
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
- package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
- package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
- package/templates/astro-starter/src/components/global/Footer.astro +7 -46
- package/templates/astro-starter/src/components/global/Header.astro +10 -16
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
- package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
- package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
- package/templates/astro-starter/src/lib/cms.ts +33 -11
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
- package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
- package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
- package/templates/astro-starter/src/middleware.ts +158 -100
- package/templates/astro-starter/src/pages/[...slug].astro +77 -3
- package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
- package/templates/kama-worker/index.js +38 -15
- package/templates/kama-worker/server.js +13 -5
- package/templates/kama-worker/setup-local-db.mjs +17 -0
- package/dist/assets/index-F2rfUymo.css +0 -1
- package/dist/assets/index-lXxH3VFK.js +0 -164
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/NodeList.astro
|
|
3
|
+
import KamaImage from "@/components/ui/KamaImage.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
data: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { data } = Astro.props;
|
|
10
|
+
const items = data.initialItems || [];
|
|
11
|
+
const listTitle = data.listTitle || "";
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="cms-block node-list-wrapper">
|
|
15
|
+
{listTitle && <h2 class="node-list-title">{listTitle}</h2>}
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
items.length === 0 ? (
|
|
19
|
+
<p class="empty-state">No items found.</p>
|
|
20
|
+
) : (
|
|
21
|
+
<div class="node-grid" id={`grid-${data.blockId}`}>
|
|
22
|
+
{items.map((item: any) => (
|
|
23
|
+
<article class="node-card">
|
|
24
|
+
<a href={`/${item.slug}`} class="node-card-link">
|
|
25
|
+
{/* 🎯 ARCHITECTURAL FIX: Use KamaImage for initial load optimization */}
|
|
26
|
+
{item.image ? (
|
|
27
|
+
<KamaImage
|
|
28
|
+
image={item.image}
|
|
29
|
+
sizes="(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 33vw"
|
|
30
|
+
desktopWidths={[320, 640, 768, 1024]}
|
|
31
|
+
idealWidth={429}
|
|
32
|
+
loading="lazy"
|
|
33
|
+
/>
|
|
34
|
+
) : (
|
|
35
|
+
<div class="image-placeholder" />
|
|
36
|
+
)}
|
|
37
|
+
<div class="node-card-content">
|
|
38
|
+
<h3 class="node-card-title">{item.title}</h3>
|
|
39
|
+
{item.createdAt && (
|
|
40
|
+
<time datetime={item.createdAt} class="node-card-date">
|
|
41
|
+
{new Date(item.createdAt).toLocaleDateString("en-GB", {
|
|
42
|
+
day: "numeric",
|
|
43
|
+
month: "short",
|
|
44
|
+
year: "numeric",
|
|
45
|
+
})}
|
|
46
|
+
</time>
|
|
47
|
+
)}
|
|
48
|
+
{item.excerpt && (
|
|
49
|
+
<p class="node-card-excerpt">{item.excerpt}</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</a>
|
|
53
|
+
</article>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
data.loadMoreUrl && (
|
|
61
|
+
<div class="load-more-container">
|
|
62
|
+
<button
|
|
63
|
+
class="load-more-btn"
|
|
64
|
+
data-url={data.loadMoreUrl}
|
|
65
|
+
data-target={`grid-${data.blockId}`}
|
|
66
|
+
>
|
|
67
|
+
Load More
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<style is:global>
|
|
75
|
+
.node-list-wrapper {
|
|
76
|
+
margin: 2rem 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.node-list-title {
|
|
80
|
+
font-size: 1.5rem;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
margin-bottom: 1.5rem;
|
|
83
|
+
color: #1e293b;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.node-list-wrapper .empty-state {
|
|
87
|
+
color: #64748b;
|
|
88
|
+
font-style: italic;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.node-grid {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: repeat(1, 1fr);
|
|
94
|
+
gap: 1.5rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (min-width: 640px) {
|
|
98
|
+
.node-grid {
|
|
99
|
+
grid-template-columns: repeat(2, 1fr);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@media (min-width: 1024px) {
|
|
104
|
+
.node-grid {
|
|
105
|
+
grid-template-columns: repeat(3, 1fr);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.node-card {
|
|
110
|
+
border: 1px solid #e2e8f0;
|
|
111
|
+
border-radius: 8px;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
background: #fff;
|
|
114
|
+
transition:
|
|
115
|
+
transform 0.2s ease,
|
|
116
|
+
box-shadow 0.2s ease;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.node-card:hover {
|
|
120
|
+
transform: translateY(-2px);
|
|
121
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.node-card-link {
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
color: inherit;
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
height: 100%;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* 🎯 Because of is:global, this styling will automatically apply to <KamaImage> outputs! */
|
|
133
|
+
.node-card img,
|
|
134
|
+
.node-card .image-placeholder {
|
|
135
|
+
width: 100%;
|
|
136
|
+
height: auto; /* 🎯 FIX: Allows tall images to display their natural height */
|
|
137
|
+
object-fit: cover;
|
|
138
|
+
background: #f1f5f9;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.node-card .image-placeholder {
|
|
142
|
+
aspect-ratio: 16 / 9; /* Keep the placeholder a rectangle if there is no image at all */
|
|
143
|
+
}
|
|
144
|
+
.node-card-content {
|
|
145
|
+
padding: 1.25rem;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
flex-grow: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.node-card-title {
|
|
152
|
+
font-size: 1.125rem;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
margin: 0 0 0.5rem 0;
|
|
155
|
+
color: #0f172a;
|
|
156
|
+
line-height: 1.4;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.node-card-date {
|
|
160
|
+
font-size: 0.875rem;
|
|
161
|
+
color: #64748b;
|
|
162
|
+
margin-bottom: 0.75rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.node-card-excerpt {
|
|
166
|
+
font-size: 0.9375rem;
|
|
167
|
+
color: #475569;
|
|
168
|
+
margin: 0;
|
|
169
|
+
line-height: 1.5;
|
|
170
|
+
display: -webkit-box;
|
|
171
|
+
-webkit-line-clamp: 3;
|
|
172
|
+
-webkit-box-orient: vertical;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.load-more-container {
|
|
177
|
+
margin-top: 2rem;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.load-more-btn {
|
|
182
|
+
padding: 0.75rem 1.5rem;
|
|
183
|
+
background: #0f172a;
|
|
184
|
+
color: #fff;
|
|
185
|
+
border: none;
|
|
186
|
+
border-radius: 6px;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
transition: background 0.2s;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.load-more-btn:hover {
|
|
193
|
+
background: #334155;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
function initLoadMoreButtons() {
|
|
199
|
+
const buttons = document.querySelectorAll(".load-more-btn:not(.is-bound)");
|
|
200
|
+
|
|
201
|
+
buttons.forEach(btn => {
|
|
202
|
+
btn.classList.add("is-bound");
|
|
203
|
+
|
|
204
|
+
btn.addEventListener("click", async e => {
|
|
205
|
+
const targetBtn = e.currentTarget as HTMLButtonElement;
|
|
206
|
+
const urlString = targetBtn.getAttribute("data-url");
|
|
207
|
+
const targetId = targetBtn.getAttribute("data-target");
|
|
208
|
+
|
|
209
|
+
if (!urlString || !targetId) return;
|
|
210
|
+
|
|
211
|
+
const grid = document.getElementById(targetId);
|
|
212
|
+
if (!grid) return;
|
|
213
|
+
|
|
214
|
+
const urlObj = new URL(urlString, window.location.origin);
|
|
215
|
+
// 🎯 ARCHITECTURAL FIX: Route to the new HTML Partial Endpoint
|
|
216
|
+
const proxyUrl = `/api/partials/node-cards${urlObj.search}`;
|
|
217
|
+
|
|
218
|
+
targetBtn.textContent = "Loading...";
|
|
219
|
+
targetBtn.disabled = true;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(proxyUrl);
|
|
223
|
+
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 1. Read metadata from headers sent by our partial endpoint
|
|
229
|
+
const rawCountStr = res.headers.get("X-Consumed-Rows");
|
|
230
|
+
const limitStr = res.headers.get("X-Limit");
|
|
231
|
+
const consumedRows = parseInt(rawCountStr || "0", 10);
|
|
232
|
+
const limit = parseInt(limitStr || "12", 10);
|
|
233
|
+
|
|
234
|
+
// 2. Extract and render the raw HTML
|
|
235
|
+
const htmlSnippet = await res.text();
|
|
236
|
+
|
|
237
|
+
if (htmlSnippet.trim().length > 0) {
|
|
238
|
+
grid.insertAdjacentHTML("beforeend", htmlSnippet);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Handle Pagination
|
|
242
|
+
if (consumedRows >= limit && limit > 0) {
|
|
243
|
+
const currentOffset = parseInt(
|
|
244
|
+
urlObj.searchParams.get("offset") || "0",
|
|
245
|
+
10,
|
|
246
|
+
);
|
|
247
|
+
urlObj.searchParams.set(
|
|
248
|
+
"offset",
|
|
249
|
+
(currentOffset + limit).toString(),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
targetBtn.setAttribute("data-url", urlObj.pathname + urlObj.search);
|
|
253
|
+
targetBtn.textContent = "Load More";
|
|
254
|
+
targetBtn.disabled = false;
|
|
255
|
+
} else {
|
|
256
|
+
if (targetBtn.parentElement)
|
|
257
|
+
targetBtn.parentElement.style.display = "none";
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error("Failed to load more items:", err);
|
|
261
|
+
targetBtn.textContent = "Error. Try again";
|
|
262
|
+
targetBtn.disabled = false;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (document.readyState === "loading") {
|
|
269
|
+
document.addEventListener("DOMContentLoaded", initLoadMoreButtons);
|
|
270
|
+
} else {
|
|
271
|
+
initLoadMoreButtons();
|
|
272
|
+
}
|
|
273
|
+
document.addEventListener("astro:page-load", initLoadMoreButtons);
|
|
274
|
+
</script>
|
|
@@ -7,24 +7,60 @@ interface Props {
|
|
|
7
7
|
isHomePage?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const { title,
|
|
10
|
+
const { title, isHomePage } = Astro.props;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const currentPath = Astro.url.pathname;
|
|
13
|
+
|
|
14
|
+
// Extract non-empty path segments
|
|
15
|
+
const rawSegments = currentPath.split("/").filter(Boolean);
|
|
16
|
+
|
|
17
|
+
// Detect homepage
|
|
18
|
+
const isHome = isHomePage || currentPath === "/" || rawSegments.length === 0;
|
|
19
|
+
|
|
20
|
+
function formatSegmentTitle(segment: string): string {
|
|
21
|
+
return segment
|
|
22
|
+
.split("-")
|
|
23
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
24
|
+
.join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build progressive paths: /some-page, /some-page/child-page, etc.
|
|
28
|
+
let runningPath = "";
|
|
29
|
+
const breadcrumbItems = rawSegments.map((segment, index) => {
|
|
30
|
+
runningPath += `/${segment}`;
|
|
31
|
+
const isLast = index === rawSegments.length - 1;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
label: isLast ? title : formatSegmentTitle(segment),
|
|
35
|
+
href: runningPath,
|
|
36
|
+
isCurrent: isLast,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
14
39
|
---
|
|
15
40
|
|
|
16
41
|
<nav class="breadcrumb-nav" aria-label="Breadcrumb">
|
|
17
42
|
<ol class="breadcrumb-list">
|
|
43
|
+
<!-- Home Link -->
|
|
18
44
|
<li class="breadcrumb-item">
|
|
19
45
|
<a href="/">Home</a>
|
|
20
46
|
</li>
|
|
47
|
+
|
|
48
|
+
<!-- Subpage Breadcrumbs -->
|
|
21
49
|
{
|
|
22
|
-
!isHome &&
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
!isHome &&
|
|
51
|
+
breadcrumbItems.map(item => (
|
|
52
|
+
<li
|
|
53
|
+
class={`breadcrumb-item ${item.isCurrent ? "active" : ""}`}
|
|
54
|
+
aria-current={item.isCurrent ? "page" : undefined}
|
|
55
|
+
>
|
|
56
|
+
<span class="separator">/</span>
|
|
57
|
+
{item.isCurrent ? (
|
|
58
|
+
<span>{item.label}</span>
|
|
59
|
+
) : (
|
|
60
|
+
<a href={item.href}>{item.label}</a>
|
|
61
|
+
)}
|
|
62
|
+
</li>
|
|
63
|
+
))
|
|
28
64
|
}
|
|
29
65
|
</ol>
|
|
30
66
|
</nav>
|
|
@@ -45,6 +81,11 @@ const isHome = isHomePage || Astro.url.pathname === "/" || !slug || slug === "";
|
|
|
45
81
|
color: #64748b;
|
|
46
82
|
}
|
|
47
83
|
|
|
84
|
+
.breadcrumb-item {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
.breadcrumb-item a {
|
|
49
90
|
color: #334155;
|
|
50
91
|
text-decoration: none;
|
|
@@ -2,60 +2,21 @@
|
|
|
2
2
|
// src/components/global/Footer.astro
|
|
3
3
|
|
|
4
4
|
import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
|
|
5
|
-
import { getChannelId } from "@/lib/cms";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const channelId = getChannelId() || "website";
|
|
11
|
-
|
|
12
|
-
const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
13
|
-
const reqInit = { ...init };
|
|
14
|
-
delete reqInit.cache;
|
|
15
|
-
|
|
16
|
-
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
17
|
-
if (urlStr.startsWith("/")) {
|
|
18
|
-
const baseUrl =
|
|
19
|
-
import.meta.env.PUBLIC_API_URL ||
|
|
20
|
-
safeEnv.PUBLIC_API_URL ||
|
|
21
|
-
"http://127.0.0.1:8787";
|
|
22
|
-
urlStr = `${baseUrl}${urlStr}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const req = new Request(urlStr, reqInit);
|
|
26
|
-
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
27
|
-
return cmsWorker.fetch(req);
|
|
28
|
-
}
|
|
29
|
-
return fetch(req);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// 🎯 Resolve the Home Page slug
|
|
33
|
-
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
|
|
34
|
-
|
|
35
|
-
let footerBlocks: any[] = [];
|
|
36
|
-
let contentMap: Record<string, any[]> = {};
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const res = await customFetch(
|
|
40
|
-
`/api/v1/node/${homeSlug}?channelId=${channelId}`,
|
|
41
|
-
);
|
|
42
|
-
if (res.ok) {
|
|
43
|
-
const data = await res.json();
|
|
44
|
-
const allContent = data.content || {};
|
|
45
|
-
footerBlocks = allContent.footer || [];
|
|
46
|
-
contentMap = allContent;
|
|
47
|
-
}
|
|
48
|
-
} catch (e) {
|
|
49
|
-
console.error("[Footer] Failed to fetch home page blocks:", e);
|
|
6
|
+
interface Props {
|
|
7
|
+
blocks?: any[];
|
|
8
|
+
contentMap?: Record<string, any[]>;
|
|
50
9
|
}
|
|
10
|
+
|
|
11
|
+
const { blocks = [], contentMap = {} } = Astro.props;
|
|
51
12
|
---
|
|
52
13
|
|
|
53
14
|
<footer class="site-footer">
|
|
54
15
|
<div class="footer-container">
|
|
55
16
|
{
|
|
56
|
-
|
|
17
|
+
blocks && blocks.length > 0 ? (
|
|
57
18
|
<BlockRenderer
|
|
58
|
-
blocks={
|
|
19
|
+
blocks={blocks}
|
|
59
20
|
contentMap={contentMap}
|
|
60
21
|
region="footer"
|
|
61
22
|
/>
|
|
@@ -15,7 +15,6 @@ const props = Astro.props as Props;
|
|
|
15
15
|
const currentPropsNode = props.node || {};
|
|
16
16
|
let rawTree: TreeNode[] = props.tree || [];
|
|
17
17
|
|
|
18
|
-
// 🎯 Read the safely extracted environment from middleware (Fixes Astro 7 crash)
|
|
19
18
|
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
20
19
|
const cmsWorker = safeEnv.CMS_WORKER;
|
|
21
20
|
|
|
@@ -41,29 +40,27 @@ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
41
40
|
return fetch(req);
|
|
42
41
|
};
|
|
43
42
|
|
|
44
|
-
// 🎯
|
|
45
|
-
const channelId = getChannelId()
|
|
46
|
-
const treeId = getTreeId()
|
|
43
|
+
// 🎯 Resolve Channel ID dynamically from env/lib without hardcoding "website"
|
|
44
|
+
const channelId = getChannelId();
|
|
45
|
+
const treeId = getTreeId();
|
|
47
46
|
|
|
48
47
|
let channelData: Record<string, any> | null = null;
|
|
49
48
|
|
|
50
49
|
try {
|
|
51
|
-
const [fetchedChannel, fetchedTree] = await Promise.all([
|
|
52
|
-
getChannel(channelId, customFetch),
|
|
53
|
-
!rawTree || rawTree.length === 0
|
|
54
|
-
? getTree(treeId, channelId, customFetch)
|
|
55
|
-
: Promise.resolve(rawTree),
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
channelData = fetchedChannel;
|
|
59
50
|
if (!rawTree || rawTree.length === 0) {
|
|
51
|
+
const [fetchedChannel, fetchedTree] = await Promise.all([
|
|
52
|
+
getChannel(channelId, customFetch),
|
|
53
|
+
getTree(treeId, channelId, customFetch),
|
|
54
|
+
]);
|
|
55
|
+
channelData = fetchedChannel;
|
|
60
56
|
rawTree = fetchedTree || [];
|
|
57
|
+
} else {
|
|
58
|
+
channelData = await getChannel(channelId, customFetch);
|
|
61
59
|
}
|
|
62
60
|
} catch (e) {
|
|
63
61
|
console.error("[Header] Failed to load layout data:", e);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
// 🎯 1. Resolve Logo Text (Strict waterfall prioritizing explicit strings)
|
|
67
64
|
const rawLogoText = channelData?.logoText;
|
|
68
65
|
const rawSiteTitle = channelData?.siteTitle;
|
|
69
66
|
const rawLabel = channelData?.label;
|
|
@@ -82,11 +79,9 @@ if (typeof rawLogoText === "string" && rawLogoText.trim() !== "") {
|
|
|
82
79
|
logoText = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
// 🎯 2. Extract Raw Logo Asset (Unwraps array wrappers if present)
|
|
86
82
|
const rawLogoAsset = channelData?.logoImage || channelData?.logo;
|
|
87
83
|
const targetLogo = Array.isArray(rawLogoAsset) ? rawLogoAsset[0] : rawLogoAsset;
|
|
88
84
|
|
|
89
|
-
// 🎯 3. Strict Image Validation Guard (Guarantees non-empty string or populated object)
|
|
90
85
|
const hasValidLogoImage = Boolean(
|
|
91
86
|
targetLogo &&
|
|
92
87
|
((typeof targetLogo === "string" && targetLogo.trim() !== "") ||
|
|
@@ -152,7 +147,6 @@ const hasValidLogoImage = Boolean(
|
|
|
152
147
|
z-index: 1001;
|
|
153
148
|
}
|
|
154
149
|
|
|
155
|
-
/* 🎯 Strict 16px Height Constraint for KamaImage output */
|
|
156
150
|
:global(.logo-image) {
|
|
157
151
|
height: 16px !important;
|
|
158
152
|
max-height: 16px !important;
|
|
@@ -8,29 +8,37 @@ export interface Props {
|
|
|
8
8
|
|
|
9
9
|
const { tree = [] } = Astro.props;
|
|
10
10
|
|
|
11
|
-
// 🎯 Grab the current path directly from Astro's server request
|
|
12
11
|
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
13
|
-
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "index").trim();
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
item
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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];
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
// ✨ NEW: Filter out nodes where notInMenu is true
|
|
27
|
+
const rawTopLevelItems =
|
|
28
|
+
rootNode?.children && rootNode.children.length > 0
|
|
29
|
+
? rootNode.children
|
|
26
30
|
: tree.filter(
|
|
27
31
|
item =>
|
|
28
|
-
item.
|
|
29
|
-
item.slug !==
|
|
32
|
+
item.id !== rootNode?.id &&
|
|
33
|
+
item.slug !== rootNode?.slug &&
|
|
30
34
|
item.path !== "/" &&
|
|
31
35
|
item.path !== "",
|
|
32
36
|
);
|
|
33
37
|
|
|
38
|
+
const topLevelNavItems = rawTopLevelItems.filter(
|
|
39
|
+
item => !(item as any).notInMenu,
|
|
40
|
+
);
|
|
41
|
+
|
|
34
42
|
const isPathActive = (itemPath?: string) => {
|
|
35
43
|
const normItem = (itemPath || "").replace(/\/$/, "") || "/";
|
|
36
44
|
if (normItem === "/") return currentPath === "/";
|
|
@@ -59,7 +67,10 @@ const isExactActive = (itemPath?: string) => {
|
|
|
59
67
|
<ul class="nav-list">
|
|
60
68
|
{
|
|
61
69
|
topLevelNavItems.map(item => {
|
|
62
|
-
|
|
70
|
+
// ✨ NEW: Also filter children for notInMenu
|
|
71
|
+
const visibleChildren =
|
|
72
|
+
item.children?.filter(child => !(child as any).notInMenu) || [];
|
|
73
|
+
const hasChildren = visibleChildren.length > 0;
|
|
63
74
|
const isParentActive = isPathActive(item.path);
|
|
64
75
|
|
|
65
76
|
if (hasChildren) {
|
|
@@ -90,7 +101,7 @@ const isExactActive = (itemPath?: string) => {
|
|
|
90
101
|
{item.title} Overview
|
|
91
102
|
</a>
|
|
92
103
|
</li>
|
|
93
|
-
{
|
|
104
|
+
{visibleChildren.map(child => (
|
|
94
105
|
<li>
|
|
95
106
|
<a
|
|
96
107
|
href={child.path}
|
|
@@ -125,58 +136,66 @@ const isExactActive = (itemPath?: string) => {
|
|
|
125
136
|
</ul>
|
|
126
137
|
</nav>
|
|
127
138
|
|
|
128
|
-
<script>
|
|
129
|
-
|
|
130
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
139
|
+
<script is:inline>
|
|
140
|
+
const initMobileMenu = () => {
|
|
131
141
|
const toggleBtn = document.getElementById("menu-toggle");
|
|
132
142
|
const navOverlay = document.getElementById("nav-overlay");
|
|
133
143
|
|
|
144
|
+
if (!toggleBtn || !navOverlay) return;
|
|
145
|
+
|
|
146
|
+
// Remove old listeners by replacing the nodes (prevents duplicate bindings)
|
|
147
|
+
const newToggleBtn = toggleBtn.cloneNode(true);
|
|
148
|
+
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
|
|
149
|
+
|
|
134
150
|
const closeMenu = () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
document.body.classList.remove("menu-lock-scroll");
|
|
140
|
-
}
|
|
151
|
+
newToggleBtn.setAttribute("aria-expanded", "false");
|
|
152
|
+
newToggleBtn.classList.remove("is-open");
|
|
153
|
+
navOverlay.classList.remove("is-open");
|
|
154
|
+
document.body.classList.remove("menu-lock-scroll");
|
|
141
155
|
};
|
|
142
156
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
newToggleBtn.addEventListener("click", e => {
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
const isOpen = newToggleBtn.getAttribute("aria-expanded") === "true";
|
|
160
|
+
newToggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
|
|
161
|
+
newToggleBtn.classList.toggle("is-open");
|
|
162
|
+
navOverlay.classList.toggle("is-open");
|
|
163
|
+
document.body.classList.toggle("menu-lock-scroll");
|
|
164
|
+
});
|
|
151
165
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (targetUrl.pathname === currentUrl.pathname) closeMenu();
|
|
158
|
-
});
|
|
166
|
+
// Close menu when clicking nav links
|
|
167
|
+
const navLinks = navOverlay.querySelectorAll("a");
|
|
168
|
+
navLinks.forEach(link => {
|
|
169
|
+
link.addEventListener("click", () => {
|
|
170
|
+
closeMenu();
|
|
159
171
|
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
window.addEventListener("pageshow", closeMenu);
|
|
172
|
+
});
|
|
163
173
|
|
|
174
|
+
// Close details dropdowns when clicking outside
|
|
164
175
|
document.addEventListener("click", event => {
|
|
165
176
|
const allDetails = document.querySelectorAll("details.nav-details");
|
|
166
177
|
allDetails.forEach(details => {
|
|
167
|
-
if (
|
|
168
|
-
details.hasAttribute("open") &&
|
|
169
|
-
!details.contains(event.target as Node)
|
|
170
|
-
) {
|
|
178
|
+
if (details.hasAttribute("open") && !details.contains(event.target)) {
|
|
171
179
|
details.removeAttribute("open");
|
|
172
180
|
}
|
|
173
181
|
});
|
|
174
182
|
});
|
|
175
|
-
|
|
183
|
+
|
|
184
|
+
window.addEventListener("pageshow", closeMenu);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Run on first load
|
|
188
|
+
if (document.readyState === "loading") {
|
|
189
|
+
document.addEventListener("DOMContentLoaded", initMobileMenu);
|
|
190
|
+
} else {
|
|
191
|
+
initMobileMenu();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Run on every Astro View Transition page navigation
|
|
195
|
+
document.addEventListener("astro:page-load", initMobileMenu);
|
|
176
196
|
</script>
|
|
177
197
|
|
|
178
198
|
<style>
|
|
179
|
-
/* Keeps all of your exact original CSS styles intact! */
|
|
180
199
|
.mobile-toggle {
|
|
181
200
|
display: flex;
|
|
182
201
|
flex-direction: column;
|