@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.
Files changed (101) hide show
  1. package/bin/cli.js +604 -364
  2. package/content/definitions/block-kama-forms.yaml +5 -0
  3. package/content/definitions/blocks/button.yaml +15 -5
  4. package/content/definitions/blocks/image-gallery.yaml +34 -0
  5. package/content/definitions/blocks/node-list.yaml +75 -0
  6. package/content/definitions/blocks/youtube.yaml +8 -0
  7. package/content/definitions/field-sets/access.yaml +10 -0
  8. package/content/definitions/field-sets/channel-base.yaml +7 -0
  9. package/content/definitions/field-sets/page-base.yaml +16 -1
  10. package/content/definitions/nodes/article.yaml +17 -1
  11. package/content/definitions/taxonomies/categories.yaml +5 -2
  12. package/content/definitions/taxonomies/media-tag.yaml +15 -0
  13. package/content/definitions/taxonomies/tags.yaml +5 -2
  14. package/content/forms/contact.yaml +44 -0
  15. package/content/forms/subscription.yaml +32 -0
  16. package/definitions/editor/default.ts +15 -1
  17. package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
  18. package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
  19. package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
  20. package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
  21. package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
  22. package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
  23. package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
  24. package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
  25. package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
  26. package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
  27. package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
  28. package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
  29. package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
  30. package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
  31. package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
  32. package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
  33. package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
  34. package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
  35. package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
  36. package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
  37. package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
  38. package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
  39. package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
  40. package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
  41. package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
  42. package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
  43. package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
  44. package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
  45. package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
  46. package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
  47. package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
  48. package/dist/assets/circle-check-A-QeFqt3.js +1 -0
  49. package/dist/assets/database-hg5SqKLG.js +1 -0
  50. package/dist/assets/folder-tree-BWYot72H.js +1 -0
  51. package/dist/assets/globe-BraoljaN.js +1 -0
  52. package/dist/assets/history-pqIbKK9u.js +1 -0
  53. package/dist/assets/index-CY0VYSyt.css +1 -0
  54. package/dist/assets/index-DdG9Pozx.js +14 -0
  55. package/dist/assets/layers-B6RgfpPR.js +1 -0
  56. package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
  57. package/dist/assets/monitor-CrpvInqG.js +1 -0
  58. package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
  59. package/dist/assets/plus-CX_GHzXs.js +1 -0
  60. package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
  61. package/dist/assets/save-Ctv8vW9H.js +1 -0
  62. package/dist/assets/schema-helpers-DvficKko.js +1 -0
  63. package/dist/assets/send-F-mJeL54.js +1 -0
  64. package/dist/assets/smartphone-EPce6VdP.js +1 -0
  65. package/dist/assets/trash-2-CAUF1KtI.js +1 -0
  66. package/dist/assets/useLocalStorage-C805uplI.js +1 -0
  67. package/dist/assets/useNodes-CqFlc72m.js +1 -0
  68. package/dist/assets/user-IFlT6mYq.js +1 -0
  69. package/dist/build-seed.js +495 -0
  70. package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
  71. package/dist/index.html +34 -2
  72. package/dist/index.js +3116 -1315
  73. package/dist/schema-compiler.js +5 -5
  74. package/documentation/EXTENSIONS.md +204 -0
  75. package/extension-registry.js +5 -0
  76. package/package.json +13 -6
  77. package/schema.sql +44 -1
  78. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
  79. package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
  80. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
  81. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
  82. package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
  83. package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
  84. package/templates/astro-starter/src/components/global/Footer.astro +7 -46
  85. package/templates/astro-starter/src/components/global/Header.astro +10 -16
  86. package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
  87. package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
  88. package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
  89. package/templates/astro-starter/src/lib/cms.ts +33 -11
  90. package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
  91. package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
  92. package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
  93. package/templates/astro-starter/src/middleware.ts +158 -100
  94. package/templates/astro-starter/src/pages/[...slug].astro +77 -3
  95. package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
  96. package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
  97. package/templates/kama-worker/index.js +38 -15
  98. package/templates/kama-worker/server.js +13 -5
  99. package/templates/kama-worker/setup-local-db.mjs +17 -0
  100. package/dist/assets/index-F2rfUymo.css +0 -1
  101. 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, slug, isHomePage } = Astro.props;
10
+ const { title, isHomePage } = Astro.props;
11
11
 
12
- // 🎯 Detect home page via URL pathname or explicit flag
13
- const isHome = isHomePage || Astro.url.pathname === "/" || !slug || slug === "";
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
- <li class="breadcrumb-item active" aria-current="page">
24
- <span class="separator">/</span>
25
- <span>{title}</span>
26
- </li>
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
- // 🎯 Fetch data natively during the server request
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
- }
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
- footerBlocks && footerBlocks.length > 0 ? (
17
+ blocks && blocks.length > 0 ? (
57
18
  <BlockRenderer
58
- blocks={footerBlocks}
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
- // 🎯 Fetch Global Channel Metadata and Nav Tree in Parallel
45
- const channelId = getChannelId() || "website";
46
- const treeId = getTreeId() || "main-nav";
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
- const homeNode = tree.find(
16
- item =>
17
- item.slug === homeSlug ||
18
- item.slug === "" ||
19
- item.path === "/" ||
20
- item.path === "",
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
- const topLevelNavItems =
24
- homeNode?.children && homeNode.children.length > 0
25
- ? homeNode.children
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.slug !== homeSlug &&
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
- const hasChildren = item.children && item.children.length > 0;
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
- {item.children?.map(child => (
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
- // 🎯 Clean, standard event listeners that run when the HTML is parsed
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
- if (toggleBtn && navOverlay) {
136
- toggleBtn.setAttribute("aria-expanded", "false");
137
- toggleBtn.classList.remove("is-open");
138
- navOverlay.classList.remove("is-open");
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
- if (toggleBtn && navOverlay) {
144
- toggleBtn.addEventListener("click", () => {
145
- const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
146
- toggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
147
- toggleBtn.classList.toggle("is-open");
148
- navOverlay.classList.toggle("is-open");
149
- document.body.classList.toggle("menu-lock-scroll");
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
- const navLinks = navOverlay.querySelectorAll("a");
153
- navLinks.forEach(link => {
154
- link.addEventListener("click", () => {
155
- const targetUrl = new URL(link.href);
156
- const currentUrl = new window.URL(window.location.href);
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;