@brightsideoy/kama 0.1.0-alpha.158 → 0.1.0-alpha.159

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.
@@ -1,6 +1,5 @@
1
- // templates/astro-starter/astro.config.mjs
2
1
  import { defineConfig } from 'astro/config';
3
- import cloudflare from '@astrojs/cloudflare';
2
+ import node from '@astrojs/node';
4
3
  import path from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
 
@@ -9,32 +8,17 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
8
  export default defineConfig({
10
9
  output: 'server',
11
10
  trailingSlash: 'ignore',
12
-
13
- // 🎯 FIX: Point to the new folder location!
14
- image: {
15
- service: {
16
- entrypoint: './src/lib/media/kama-image-service.ts',
17
- }
18
- },
19
-
20
- // 🎯 Passthrough allows Kama proxy to take 100% control
21
- adapter: cloudflare({
22
- imageService: 'passthrough',
23
- }),
24
-
11
+ image: { service: { entrypoint: './src/lib/media/kama-image-service.ts' } },
12
+ adapter: node({ mode: "standalone" }),
25
13
  vite: {
26
- resolve: {
27
- alias: {
28
- '@': path.resolve(__dirname, './src')
29
- }
30
- },
31
- optimizeDeps: {
32
- exclude: ['cloudflare:workers', '@astrojs/cloudflare', 'astro']
33
- },
14
+ resolve: { alias: { '@': path.resolve(__dirname, './src') } },
34
15
  ssr: {
35
- target: 'webworker',
36
- noExternal: true,
37
- external: ['cloudflare:workers', 'node:process']
16
+ external: ['cloudflare:workers', 'tinyexec', 'node:child_process', 'node:fs', 'node:url', 'node:path', 'node:stream', 'node:readline', 'node:stream/promises']
17
+ },
18
+ environments: {
19
+ ssr: {
20
+ external: ['cloudflare:workers', 'tinyexec', 'node:child_process', 'node:fs', 'node:url', 'node:path', 'node:stream', 'node:readline', 'node:stream/promises']
21
+ }
38
22
  }
39
23
  }
40
24
  });
@@ -1,34 +1,33 @@
1
1
  ---
2
2
  // src/components/global/Footer.astro
3
-
4
- import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
5
-
6
- interface Props {
7
- blocks?: any[];
8
- contentMap?: Record<string, any[]>;
9
- }
10
-
11
- const { blocks = [], contentMap = {} } = Astro.props;
3
+ // Notice: No props needed anymore! It fetches its own data independently.
12
4
  ---
13
5
 
14
6
  <footer class="site-footer">
15
- <div class="footer-container">
16
- {
17
- blocks && blocks.length > 0 ? (
18
- <BlockRenderer
19
- blocks={blocks}
20
- contentMap={contentMap}
21
- region="footer"
22
- />
23
- ) : (
24
- <div class="footer-default">
25
- <p>&copy; {new Date().getFullYear()} all rights reserved.</p>
26
- </div>
27
- )
28
- }
7
+ <div class="footer-container" id="footer-injection-target">
8
+ <!-- Lightning fast SSR placeholder before the partial loads -->
29
9
  </div>
30
10
  </footer>
31
11
 
12
+ <script>
13
+ async function initFooter() {
14
+ const target = document.getElementById("footer-injection-target");
15
+ if (!target) return;
16
+
17
+ try {
18
+ const res = await fetch(`/partials/footer`);
19
+ if (res.ok) {
20
+ target.innerHTML = await res.text();
21
+ }
22
+ } catch (e) {
23
+ console.error("Failed to load footer partial", e);
24
+ }
25
+ }
26
+
27
+ // Run immediately
28
+ initFooter();
29
+ </script>
30
+
32
31
  <style>
33
32
  .site-footer {
34
33
  background: #ffffff;
@@ -42,7 +41,8 @@ const { blocks = [], contentMap = {} } = Astro.props;
42
41
  padding: 24px 20px;
43
42
  }
44
43
 
45
- .footer-default p {
44
+ /* 🎯 Use :global() so Astro doesn't strip the CSS for the injected partial */
45
+ :global(.footer-default p) {
46
46
  margin: 0;
47
47
  font-size: 14px;
48
48
  color: #64748b;
@@ -1,59 +1,5 @@
1
1
  ---
2
2
  // src/components/global/NavigationMenu.astro
3
-
4
- import type { TreeNode } from "@/lib/cms";
5
-
6
- interface Props {
7
- tree: TreeNode[];
8
- }
9
-
10
- const { tree = [] } = Astro.props;
11
-
12
- // 1. Get current path from Astro context
13
- const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
14
-
15
- // 🎯 Extract configurable fallback home slug
16
- const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
17
-
18
- // 2. Locate Home Node (either root item, slug matching homeSlug, or path === "/")
19
- const homeNode = tree.find(
20
- (item: TreeNode) =>
21
- item.slug === homeSlug ||
22
- item.slug === "" ||
23
- item.path === "/" ||
24
- item.path === "",
25
- );
26
-
27
- // 3. Determine top-level navigation items
28
- // If home node has children (e.g. nested navigation), render its children; otherwise filter out the home node.
29
- const topLevelNavItems =
30
- homeNode?.children && homeNode.children.length > 0
31
- ? homeNode.children
32
- : tree.filter(
33
- (item: TreeNode) =>
34
- item.slug !== homeSlug &&
35
- item.slug !== "" &&
36
- item.path !== "/" &&
37
- item.path !== "",
38
- );
39
-
40
- // Normalize path comparison helper
41
- const normalizePath = (path?: string) => {
42
- if (!path) return "";
43
- return path.replace(/\/$/, "") || "/";
44
- };
45
-
46
- // Active trail check (parent active when on a child page)
47
- const isPathActive = (itemPath?: string) => {
48
- const normItem = normalizePath(itemPath);
49
- if (normItem === "/") return currentPath === "/";
50
- return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
51
- };
52
-
53
- // Exact path check
54
- const isExactActive = (itemPath?: string) => {
55
- return currentPath === normalizePath(itemPath);
56
- };
57
3
  ---
58
4
 
59
5
  <!-- 2-LINE HAMBURGER TOGGLE BUTTON -->
@@ -70,139 +16,82 @@ const isExactActive = (itemPath?: string) => {
70
16
 
71
17
  <!-- NAVIGATION BLOCK & FULLSCREEN CONTAINER -->
72
18
  <nav class="nav-block" id="nav-overlay">
73
- <ul class="nav-list">
74
- {
75
- topLevelNavItems.map((item: TreeNode) => {
76
- const hasChildren = item.children && item.children.length > 0;
77
- const isParentActive = isPathActive(item.path);
78
-
79
- if (hasChildren) {
80
- return (
81
- <li class="nav-item">
82
- <details class="nav-details">
83
- <summary
84
- class:list={[
85
- "nav-button",
86
- "dropdown-summary",
87
- { active: isParentActive },
88
- ]}
89
- >
90
- <span>{item.title}</span>
91
- <span class="indicator">▼</span>
92
- </summary>
93
- <div class="submenu-container">
94
- <ul class="dropdown-panel">
95
- <li>
96
- <a
97
- href={item.path}
98
- class:list={[
99
- "dropdown-item",
100
- "overview-item",
101
- { active: isExactActive(item.path) },
102
- ]}
103
- >
104
- {item.title} Overview
105
- </a>
106
- </li>
107
- {item.children?.map((child: TreeNode) => (
108
- <li>
109
- <a
110
- href={child.path}
111
- class:list={[
112
- "dropdown-item",
113
- { active: isExactActive(child.path) },
114
- ]}
115
- >
116
- {child.title}
117
- </a>
118
- </li>
119
- ))}
120
- </ul>
121
- </div>
122
- </details>
123
- </li>
124
- );
125
- }
126
-
127
- return (
128
- <li class="nav-item">
129
- <a
130
- href={item.path}
131
- class:list={["nav-button", { active: isExactActive(item.path) }]}
132
- >
133
- {item.title}
134
- </a>
135
- </li>
136
- );
137
- })
138
- }
139
- </ul>
19
+ <!-- 🎯 The partial HTML will be injected into this div -->
20
+ <div id="nav-injection-target"></div>
140
21
  </nav>
22
+
141
23
  <script>
142
- const toggleBtn = document.getElementById("menu-toggle");
143
- const navOverlay = document.getElementById("nav-overlay");
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
+ };
144
54
 
145
- const closeMenu = () => {
146
55
  if (toggleBtn && navOverlay) {
147
- toggleBtn.setAttribute("aria-expanded", "false");
148
- toggleBtn.classList.remove("is-open");
149
- navOverlay.classList.remove("is-open");
150
- document.body.classList.remove("menu-lock-scroll");
151
- }
152
- };
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
+ });
153
63
 
154
- if (toggleBtn && navOverlay) {
155
- // Toggle Menu
156
- toggleBtn.addEventListener("click", () => {
157
- const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
158
- toggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
159
- toggleBtn.classList.toggle("is-open");
160
- navOverlay.classList.toggle("is-open");
161
- document.body.classList.toggle("menu-lock-scroll");
162
- });
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
+ }
163
73
 
164
- // 🎯 UPDATED LINK CLICK LOGIC
165
- const navLinks = navOverlay.querySelectorAll("a");
166
- navLinks.forEach(link => {
167
- link.addEventListener("click", e => {
168
- // Check where the link is going
169
- const targetUrl = new URL(link.href);
170
- const currentUrl = new window.URL(window.location.href);
74
+ window.addEventListener("pageshow", closeMenu);
171
75
 
172
- // If it's a link to the exact page we are already on (like a #hash anchor),
173
- // close the menu so the user can see the page.
174
- if (targetUrl.pathname === currentUrl.pathname) {
175
- closeMenu();
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");
176
84
  }
177
- // If it's a new page, DO NOTHING. The menu stays visually open,
178
- // acting as a loading screen until the browser paints the new page!
179
85
  });
180
86
  });
181
87
  }
182
88
 
183
- // Close menu on back/forward browser navigation (bfcache)
184
- window.addEventListener("pageshow", () => {
185
- closeMenu();
186
- });
187
-
188
- // Close desktop dropdowns when clicking outside
189
- document.addEventListener("click", event => {
190
- const allDetails = document.querySelectorAll("details.nav-details");
191
- allDetails.forEach(details => {
192
- if (
193
- details.hasAttribute("open") &&
194
- !details.contains(event.target as Node)
195
- ) {
196
- details.removeAttribute("open");
197
- }
198
- });
199
- });
89
+ // Run on page load
90
+ initNav();
200
91
  </script>
201
92
 
202
93
  <style>
203
- /* -------------------------------------------------------------
204
- MOBILE HAMBURGER BUTTON (2-LINE TRANSFORMING TO X)
205
- ------------------------------------------------------------- */
94
+ /* Keeps all of your exact original CSS styles intact! */
206
95
  .mobile-toggle {
207
96
  display: flex;
208
97
  flex-direction: column;
@@ -217,7 +106,6 @@ const isExactActive = (itemPath?: string) => {
217
106
  padding: 0;
218
107
  z-index: 1001;
219
108
  }
220
-
221
109
  .mobile-toggle .line {
222
110
  width: 22px;
223
111
  height: 2px;
@@ -227,18 +115,12 @@ const isExactActive = (itemPath?: string) => {
227
115
  opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
228
116
  transform-origin: center;
229
117
  }
230
-
231
118
  .mobile-toggle.is-open .line-1 {
232
119
  transform: translateY(4px) rotate(45deg);
233
120
  }
234
-
235
121
  .mobile-toggle.is-open .line-2 {
236
122
  transform: translateY(-4px) rotate(-45deg);
237
123
  }
238
-
239
- /* -------------------------------------------------------------
240
- MOBILE FULLSCREEN OVERLAY (DEFAULT MOBILE VIEW < 768px)
241
- ------------------------------------------------------------- */
242
124
  .nav-block {
243
125
  position: fixed;
244
126
  top: 0;
@@ -259,14 +141,14 @@ const isExactActive = (itemPath?: string) => {
259
141
  visibility 0.3s ease;
260
142
  z-index: 1000;
261
143
  }
262
-
263
144
  .nav-block.is-open {
264
145
  opacity: 1;
265
146
  visibility: visible;
266
147
  pointer-events: auto;
267
148
  }
268
149
 
269
- .nav-list {
150
+ /* 🎯 We target the injected .nav-list using :global so Astro doesn't scope it away */
151
+ :global(.nav-list) {
270
152
  display: flex;
271
153
  flex-direction: column;
272
154
  gap: 8px;
@@ -275,13 +157,11 @@ const isExactActive = (itemPath?: string) => {
275
157
  padding: 0;
276
158
  width: 100%;
277
159
  }
278
-
279
- .nav-item {
160
+ :global(.nav-item) {
280
161
  position: relative;
281
162
  width: 100%;
282
163
  }
283
-
284
- .nav-button {
164
+ :global(.nav-button) {
285
165
  display: flex;
286
166
  align-items: center;
287
167
  justify-content: space-between;
@@ -298,56 +178,44 @@ const isExactActive = (itemPath?: string) => {
298
178
  cursor: pointer;
299
179
  box-sizing: border-box;
300
180
  }
301
-
302
- .nav-button:hover {
181
+ :global(.nav-button:hover) {
303
182
  color: #0f172a;
304
183
  background: #f8fafc;
305
184
  }
306
-
307
- .nav-button.active {
185
+ :global(.nav-button.active) {
308
186
  color: #0c4a6e;
309
187
  font-weight: 600;
310
188
  }
311
-
312
- .dropdown-summary {
189
+ :global(.dropdown-summary) {
313
190
  list-style: none;
314
191
  user-select: none;
315
192
  }
316
-
317
- .dropdown-summary::-webkit-details-marker {
193
+ :global(.dropdown-summary::-webkit-details-marker) {
318
194
  display: none;
319
195
  }
320
-
321
- .indicator {
196
+ :global(.indicator) {
322
197
  font-size: 11px;
323
198
  color: #64748b;
324
199
  transition: transform 0.2s ease;
325
200
  }
326
-
327
- details[open] .indicator {
201
+ :global(details[open] .indicator) {
328
202
  transform: rotate(180deg);
329
203
  }
330
-
331
- /* OPEN DROPDOWN SINGLE BLOCK WITH CONNECTING LINE & INDENTATION */
332
- details.nav-details {
204
+ :global(details.nav-details) {
333
205
  border-radius: 6px;
334
206
  transition: background-color 0.2s ease;
335
207
  }
336
-
337
- details[open].nav-details {
208
+ :global(details[open].nav-details) {
338
209
  background: #f8fafc;
339
210
  padding-bottom: 8px;
340
211
  }
341
-
342
- /* Left vertical accent line connecting parent item and sub-levels */
343
- .submenu-container {
212
+ :global(.submenu-container) {
344
213
  position: relative;
345
214
  margin-left: 24px;
346
215
  padding-left: 16px;
347
216
  border-left: 2px solid #cbd5e1;
348
217
  }
349
-
350
- .dropdown-panel {
218
+ :global(.dropdown-panel) {
351
219
  list-style: none;
352
220
  margin: 4px 0 0 0;
353
221
  padding: 0;
@@ -355,8 +223,7 @@ const isExactActive = (itemPath?: string) => {
355
223
  flex-direction: column;
356
224
  gap: 4px;
357
225
  }
358
-
359
- .dropdown-item {
226
+ :global(.dropdown-item) {
360
227
  display: block;
361
228
  padding: 10px 12px;
362
229
  font-size: 16px;
@@ -368,36 +235,27 @@ const isExactActive = (itemPath?: string) => {
368
235
  background 0.15s ease,
369
236
  color 0.15s ease;
370
237
  }
371
-
372
- .overview-item {
238
+ :global(.overview-item) {
373
239
  font-weight: 500;
374
240
  color: #334155;
375
241
  }
376
-
377
- .dropdown-item:hover {
242
+ :global(.dropdown-item:hover) {
378
243
  background: #e2e8f0;
379
244
  color: #0f172a;
380
245
  }
381
-
382
- .dropdown-item.active {
246
+ :global(.dropdown-item.active) {
383
247
  font-weight: 600;
384
248
  color: #0c4a6e;
385
249
  background: #e0f2fe;
386
250
  }
387
-
388
- /* Prevent body scrolling when menu open */
389
251
  :global(body.menu-lock-scroll) {
390
252
  overflow: hidden !important;
391
253
  }
392
254
 
393
- /* -------------------------------------------------------------
394
- DESKTOP MEDIA QUERY (>= 768px)
395
- ------------------------------------------------------------- */
396
255
  @media (min-width: 768px) {
397
256
  .mobile-toggle {
398
257
  display: none;
399
258
  }
400
-
401
259
  .nav-block {
402
260
  position: static;
403
261
  width: 100%;
@@ -410,18 +268,15 @@ const isExactActive = (itemPath?: string) => {
410
268
  overflow-y: visible;
411
269
  display: block;
412
270
  }
413
-
414
- .nav-list {
271
+ :global(.nav-list) {
415
272
  flex-direction: row;
416
273
  gap: 4px;
417
274
  justify-content: flex-end;
418
275
  }
419
-
420
- .nav-item {
276
+ :global(.nav-item) {
421
277
  width: auto;
422
278
  }
423
-
424
- .nav-button {
279
+ :global(.nav-button) {
425
280
  display: inline-flex;
426
281
  justify-content: flex-start;
427
282
  gap: 6px;
@@ -431,19 +286,16 @@ const isExactActive = (itemPath?: string) => {
431
286
  border-radius: 4px;
432
287
  margin-top: 4px;
433
288
  }
434
-
435
- details[open].nav-details {
289
+ :global(details[open].nav-details) {
436
290
  background: transparent;
437
291
  padding-bottom: 0;
438
292
  }
439
-
440
- .submenu-container {
293
+ :global(.submenu-container) {
441
294
  margin-left: 0;
442
295
  padding-left: 0;
443
296
  border-left: none;
444
297
  }
445
-
446
- .dropdown-panel {
298
+ :global(.dropdown-panel) {
447
299
  position: absolute;
448
300
  top: 100%;
449
301
  left: 0;
@@ -456,8 +308,7 @@ const isExactActive = (itemPath?: string) => {
456
308
  border-radius: 4px;
457
309
  padding: 4px;
458
310
  }
459
-
460
- .dropdown-item {
311
+ :global(.dropdown-item) {
461
312
  font-size: 14px;
462
313
  padding: 8px 16px;
463
314
  border-radius: 4px;
@@ -32,7 +32,8 @@ const {
32
32
  // (MetaHead will resolve channel.defaultLanguage if this remains undefined)
33
33
  const pageLang = language || node?.language || undefined;
34
34
 
35
- const footerBlocks = content?.footer || [];
35
+ // ❌ Removed: const footerBlocks = content?.footer || [];
36
+ // (The new Footer component handles its own data now!)
36
37
  ---
37
38
 
38
39
  <!doctype html>
@@ -56,6 +57,6 @@ const footerBlocks = content?.footer || [];
56
57
  </div>
57
58
  </main>
58
59
 
59
- <Footer blocks={footerBlocks} contentMap={content} />
60
+ <Footer />
60
61
  </body>
61
62
  </html>
@@ -0,0 +1,69 @@
1
+ ---
2
+ // src/pages/partials/footer.astro
3
+ export const partial = true;
4
+
5
+ import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
6
+ import { getChannelId } from "@/lib/cms";
7
+
8
+ const safeEnv = (Astro.locals as any).safeEnv || {};
9
+ const cmsWorker = safeEnv.CMS_WORKER;
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
+ return fetch(req);
29
+ };
30
+
31
+ // 🎯 Resolve the Home Page slug
32
+ const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
33
+
34
+ let footerBlocks: any[] = [];
35
+ let contentMap: Record<string, any[]> = {};
36
+
37
+ try {
38
+ // 🎯 Fetch the Home Page node from Kama CMS Public API
39
+ const res = await customFetch(
40
+ `/api/v1/node/${homeSlug}?channelId=${channelId}`,
41
+ );
42
+ if (res.ok) {
43
+ const data = await res.json();
44
+
45
+ // Kama CMS public node endpoints usually organize blocks inside a 'content' object mapped by region
46
+ const allContent = data.content || {};
47
+
48
+ // Extract specifically the footer region from the Home page
49
+ footerBlocks = allContent.footer || [];
50
+ contentMap = allContent;
51
+ }
52
+ } catch (e) {
53
+ console.error("[Partials/Footer] Failed to fetch home page blocks:", e);
54
+ }
55
+ ---
56
+
57
+ {
58
+ footerBlocks && footerBlocks.length > 0 ? (
59
+ <BlockRenderer
60
+ blocks={footerBlocks}
61
+ contentMap={contentMap}
62
+ region="footer"
63
+ />
64
+ ) : (
65
+ <div class="footer-default">
66
+ <p>&copy; {new Date().getFullYear()} all rights reserved.</p>
67
+ </div>
68
+ )
69
+ }