@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.
- package/bin/cli.js +48 -117
- package/content/definitions/node-layouts/landing-page.yml +2 -0
- package/content/definitions/node-layouts/single-column.yml +2 -0
- package/content/definitions/node-layouts/two-column.yml +2 -0
- package/dist/assets/{index-B-WxBAFU.js → index-DIxtk5i5.js} +1 -1
- package/dist/{dist-CBGESg3a.js → dist-BNkCx-o-.js} +82 -51
- package/dist/index.html +1 -1
- package/dist/index.js +82 -51
- package/dist/schema-compiler.js +23 -13
- package/package.json +1 -1
- package/templates/astro-starter/astro.config.mjs +10 -26
- package/templates/astro-starter/src/components/global/Footer.astro +24 -24
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +86 -235
- package/templates/astro-starter/src/layouts/StandardLayout.astro +3 -2
- package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
- package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
- package/templates/kama-worker/index.js +19 -0
- package/templates/kama-worker/server.js +98 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// templates/astro-starter/astro.config.mjs
|
|
2
1
|
import { defineConfig } from 'astro/config';
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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>© {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
//
|
|
184
|
-
|
|
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
|
|
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>© {new Date().getFullYear()} all rights reserved.</p>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|