@brightsideoy/kama 1.0.0-alpha.17 → 1.0.0-alpha.18

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 (103) hide show
  1. package/bin/cli.js +70 -44
  2. package/content/definitions/block-kama-forms.yaml +5 -0
  3. package/content/definitions/blocks/node-list.yaml +8 -0
  4. package/content/forms/contact.yaml +44 -0
  5. package/content/forms/subscription.yaml +32 -0
  6. package/dist/assets/AssetEditor-CY9SjGaJ.js +1 -0
  7. package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
  8. package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
  9. package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
  10. package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
  11. package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
  12. package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
  13. package/dist/assets/CmsHelp-DJSdFYVH.js +1 -0
  14. package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
  15. package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
  16. package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
  17. package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
  18. package/dist/assets/CmsPublisher-DBcCz-AD.js +3 -0
  19. package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
  20. package/dist/assets/EditorSplitPreview-wCnwVnnb.js +1 -0
  21. package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
  22. package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
  23. package/dist/assets/NodeEditor-D3tAGViP.js +147 -0
  24. package/dist/assets/NodeEditor-DlfAHtiG.css +1 -0
  25. package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
  26. package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
  27. package/dist/assets/TrashWorkspace--uFsWt53.js +1 -0
  28. package/dist/assets/{_plugin-vue_export-helper-CTn6xl4w.js → _plugin-vue_export-helper-CQxWhVtr.js} +1 -1
  29. package/dist/assets/circle-check-A-QeFqt3.js +1 -0
  30. package/dist/assets/database-hg5SqKLG.js +1 -0
  31. package/dist/assets/folder-tree-BWYot72H.js +1 -0
  32. package/dist/assets/globe-BraoljaN.js +1 -0
  33. package/dist/assets/{history-XvWNE5zO.js → history-pqIbKK9u.js} +1 -1
  34. package/dist/assets/index-AQYAbg3R.js +14 -0
  35. package/dist/assets/index-DcKiaRjI.css +1 -0
  36. package/dist/assets/{layers-D0DLWjff.js → layers-B6RgfpPR.js} +1 -1
  37. package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
  38. package/dist/assets/monitor-CrpvInqG.js +1 -0
  39. package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
  40. package/dist/assets/plus-mqZr4I1D.js +1 -0
  41. package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
  42. package/dist/assets/{save-C8e-QRkP.js → save-Ctv8vW9H.js} +1 -1
  43. package/dist/assets/schema-helpers-1UXlO0Ye.js +1 -0
  44. package/dist/assets/send-F-mJeL54.js +1 -0
  45. package/dist/assets/{smartphone-Wy6xQi_i.js → smartphone-EPce6VdP.js} +1 -1
  46. package/dist/assets/trash-2-CAUF1KtI.js +1 -0
  47. package/dist/assets/useLocalStorage-C805uplI.js +1 -0
  48. package/dist/assets/useNodes-W5j_uxdQ.js +1 -0
  49. package/dist/assets/user-IFlT6mYq.js +1 -0
  50. package/dist/{dist-BGv2bzzF.js → dist-Bta0OpXl.js} +145 -30
  51. package/dist/index.html +34 -3
  52. package/dist/index.js +145 -30
  53. package/dist/schema-compiler.js +1 -1
  54. package/documentation/EXTENSIONS.md +204 -0
  55. package/extension-registry.js +5 -0
  56. package/package.json +3 -1
  57. package/templates/astro-starter/old_src/components/blocks/BlockRenderer.astro +143 -0
  58. package/templates/astro-starter/old_src/components/blocks/ColumnLayout.astro +67 -0
  59. package/templates/astro-starter/old_src/components/blocks/HeaderImage.astro +126 -0
  60. package/templates/astro-starter/old_src/components/blocks/Hero.astro +218 -0
  61. package/templates/astro-starter/old_src/components/blocks/HtmlBlock.astro +25 -0
  62. package/templates/astro-starter/old_src/components/blocks/ImageBlock.astro +91 -0
  63. package/templates/astro-starter/old_src/components/blocks/NodeList.astro +274 -0
  64. package/templates/astro-starter/old_src/components/blocks/TextBlock.astro +15 -0
  65. package/templates/astro-starter/old_src/components/global/Breadcrumb.astro +108 -0
  66. package/templates/astro-starter/old_src/components/global/Footer.astro +50 -0
  67. package/templates/astro-starter/old_src/components/global/Header.astro +171 -0
  68. package/templates/astro-starter/old_src/components/global/MetaHead.astro +266 -0
  69. package/templates/astro-starter/old_src/components/global/NavigationMenu.astro +413 -0
  70. package/templates/astro-starter/old_src/components/ui/KamaImage.astro +180 -0
  71. package/templates/astro-starter/old_src/layouts/SkeletonLayout.astro +56 -0
  72. package/templates/astro-starter/old_src/layouts/StandardLayout.astro +63 -0
  73. package/templates/astro-starter/old_src/lib/cms.ts +219 -0
  74. package/templates/astro-starter/old_src/lib/media/kama-image-service.ts +67 -0
  75. package/templates/astro-starter/old_src/lib/media/media-proxy.ts +148 -0
  76. package/templates/astro-starter/old_src/lib/media/media-url.ts +112 -0
  77. package/templates/astro-starter/old_src/middleware.ts +536 -0
  78. package/templates/astro-starter/old_src/pages/404.astro +46 -0
  79. package/templates/astro-starter/old_src/pages/[...slug].astro +311 -0
  80. package/templates/astro-starter/old_src/pages/api/load-nodes.ts +68 -0
  81. package/templates/astro-starter/old_src/pages/api/partials/node-cards.astro +101 -0
  82. package/templates/astro-starter/old_src/pages/llms-full.txt.ts +171 -0
  83. package/templates/astro-starter/old_src/pages/llms.txt.ts +113 -0
  84. package/templates/astro-starter/old_src/pages/media/[...path].ts +14 -0
  85. package/templates/astro-starter/old_src/pages/partials/footer.astro +69 -0
  86. package/templates/astro-starter/old_src/pages/partials/nav.astro +141 -0
  87. package/templates/astro-starter/old_src/styles/standard-theme.css +157 -0
  88. package/templates/astro-starter/old_src/utils/tiptap.ts +113 -0
  89. package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
  90. package/templates/kama-worker/index.js +35 -12
  91. package/templates/kama-worker/server.js +2 -0
  92. package/dist/assets/AssetEditor-CCNDGlQ_.js +0 -1
  93. package/dist/assets/CmsHelp-m9EOY_nE.js +0 -1
  94. package/dist/assets/CmsPublisher-Cs_ys68V.js +0 -3
  95. package/dist/assets/CmsUserCenter-D-T6HyWF.js +0 -1
  96. package/dist/assets/EditorSplitPreview-CnWpXZWQ.js +0 -1
  97. package/dist/assets/NodeEditor-B1TvB2eW.css +0 -1
  98. package/dist/assets/NodeEditor-CZGnkwvf.js +0 -147
  99. package/dist/assets/TrashWorkspace-CERX58Ek.js +0 -1
  100. package/dist/assets/index-BpOAL4tE.js +0 -14
  101. package/dist/assets/index-C-EkPNvq.css +0 -1
  102. package/dist/assets/link-Bs_SUS0O.js +0 -1
  103. package/dist/assets/rotate-ccw-CvD7yNQj.js +0 -1
@@ -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>
@@ -0,0 +1,15 @@
1
+ ---
2
+ // src/components/blocks/TextBlock.astro
3
+
4
+ interface Props {
5
+ data: {
6
+ text?: string;
7
+ content?: string;
8
+ };
9
+ }
10
+ const { data } = Astro.props;
11
+ ---
12
+
13
+ <div class="block-text">
14
+ <Fragment set:html={data.text || data.content || ""} />
15
+ </div>
@@ -0,0 +1,108 @@
1
+ ---
2
+ // src/components/global/Breadcrumb.astro
3
+
4
+ interface Props {
5
+ title: string;
6
+ slug?: string;
7
+ isHomePage?: boolean;
8
+ }
9
+
10
+ const { title, isHomePage } = Astro.props;
11
+
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
+ });
39
+ ---
40
+
41
+ <nav class="breadcrumb-nav" aria-label="Breadcrumb">
42
+ <ol class="breadcrumb-list">
43
+ <!-- Home Link -->
44
+ <li class="breadcrumb-item">
45
+ <a href="/">Home</a>
46
+ </li>
47
+
48
+ <!-- Subpage Breadcrumbs -->
49
+ {
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
+ ))
64
+ }
65
+ </ol>
66
+ </nav>
67
+
68
+ <style>
69
+ .breadcrumb-nav {
70
+ margin-bottom: 16px;
71
+ }
72
+
73
+ .breadcrumb-list {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ list-style: none;
78
+ margin: 0;
79
+ padding: 0;
80
+ font-size: 13px;
81
+ color: #64748b;
82
+ }
83
+
84
+ .breadcrumb-item {
85
+ display: flex;
86
+ align-items: center;
87
+ }
88
+
89
+ .breadcrumb-item a {
90
+ color: #334155;
91
+ text-decoration: none;
92
+ }
93
+
94
+ .breadcrumb-item a:hover {
95
+ color: #0f172a;
96
+ text-decoration: underline;
97
+ }
98
+
99
+ .breadcrumb-item.active {
100
+ color: #0f172a;
101
+ font-weight: 500;
102
+ }
103
+
104
+ .separator {
105
+ margin-right: 8px;
106
+ color: #94a3b8;
107
+ }
108
+ </style>
@@ -0,0 +1,50 @@
1
+ ---
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;
12
+ ---
13
+
14
+ <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
+ }
29
+ </div>
30
+ </footer>
31
+
32
+ <style>
33
+ .site-footer {
34
+ background: #ffffff;
35
+ border-top: 1px solid #e2e8f0;
36
+ margin-top: auto;
37
+ }
38
+
39
+ .footer-container {
40
+ max-width: 1400px;
41
+ margin: 0 auto;
42
+ padding: 24px 20px;
43
+ }
44
+
45
+ .footer-default p {
46
+ margin: 0;
47
+ font-size: 14px;
48
+ color: #64748b;
49
+ }
50
+ </style>
@@ -0,0 +1,171 @@
1
+ ---
2
+ // src/components/global/Header.astro
3
+
4
+ import NavigationMenu from "@/components/global/NavigationMenu.astro";
5
+ import KamaImage from "@/components/ui/KamaImage.astro";
6
+ import { getTree, getTreeId, getChannel, getChannelId } from "@/lib/cms";
7
+ import type { TreeNode } from "@/lib/cms";
8
+
9
+ interface Props {
10
+ node?: Record<string, any>;
11
+ tree?: TreeNode[];
12
+ }
13
+
14
+ const props = Astro.props as Props;
15
+ const currentPropsNode = props.node || {};
16
+ let rawTree: TreeNode[] = props.tree || [];
17
+
18
+ const safeEnv = (Astro.locals as any).safeEnv || {};
19
+ const cmsWorker = safeEnv.CMS_WORKER;
20
+
21
+ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
22
+ const reqInit = { ...init };
23
+ delete reqInit.cache;
24
+
25
+ let urlStr = input instanceof URL ? input.toString() : String(input);
26
+
27
+ if (urlStr.startsWith("/")) {
28
+ const baseUrl =
29
+ import.meta.env.PUBLIC_API_URL ||
30
+ safeEnv.PUBLIC_API_URL ||
31
+ "http://127.0.0.1:8787";
32
+ urlStr = `${baseUrl}${urlStr}`;
33
+ }
34
+
35
+ const req = new Request(urlStr, reqInit);
36
+
37
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
38
+ return cmsWorker.fetch(req);
39
+ }
40
+ return fetch(req);
41
+ };
42
+
43
+ // 🎯 Resolve Channel ID dynamically from env/lib without hardcoding "website"
44
+ const channelId = getChannelId();
45
+ const treeId = getTreeId();
46
+
47
+ let channelData: Record<string, any> | null = null;
48
+
49
+ try {
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;
56
+ rawTree = fetchedTree || [];
57
+ } else {
58
+ channelData = await getChannel(channelId, customFetch);
59
+ }
60
+ } catch (e) {
61
+ console.error("[Header] Failed to load layout data:", e);
62
+ }
63
+
64
+ const rawLogoText = channelData?.logoText;
65
+ const rawSiteTitle = channelData?.siteTitle;
66
+ const rawLabel = channelData?.label;
67
+
68
+ let logoText = "";
69
+
70
+ if (typeof rawLogoText === "string" && rawLogoText.trim() !== "") {
71
+ logoText = rawLogoText;
72
+ } else if (typeof rawSiteTitle === "string" && rawSiteTitle.trim() !== "") {
73
+ logoText = rawSiteTitle;
74
+ } else if (typeof rawLabel === "string" && rawLabel.trim() !== "") {
75
+ logoText = rawLabel;
76
+ } else if (currentPropsNode.logoText) {
77
+ logoText = currentPropsNode.logoText;
78
+ } else {
79
+ logoText = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
80
+ }
81
+
82
+ const rawLogoAsset = channelData?.logoImage || channelData?.logo;
83
+ const targetLogo = Array.isArray(rawLogoAsset) ? rawLogoAsset[0] : rawLogoAsset;
84
+
85
+ const hasValidLogoImage = Boolean(
86
+ targetLogo &&
87
+ ((typeof targetLogo === "string" && targetLogo.trim() !== "") ||
88
+ targetLogo.url ||
89
+ targetLogo.path ||
90
+ targetLogo.variants),
91
+ );
92
+ ---
93
+
94
+ <header class="site-header">
95
+ <div class="header-container main-header-row">
96
+ <a href="/" class="logo-block">
97
+ {
98
+ hasValidLogoImage ? (
99
+ <KamaImage
100
+ image={targetLogo}
101
+ idealWidth={300}
102
+ class="logo-image"
103
+ loading="eager"
104
+ fetchpriority="high"
105
+ />
106
+ ) : (
107
+ <span class="logo-text">{logoText}</span>
108
+ )
109
+ }
110
+ </a>
111
+ <NavigationMenu tree={rawTree} />
112
+ </div>
113
+ </header>
114
+
115
+ <style>
116
+ .site-header {
117
+ background: #ffffff;
118
+ color: #1e293b;
119
+ position: sticky;
120
+ top: 0;
121
+ z-index: 100;
122
+ border-bottom: 1px solid #e2e8f0;
123
+ }
124
+
125
+ .header-container {
126
+ max-width: 1400px;
127
+ margin: 0 auto;
128
+ padding: 0 20px;
129
+ width: 100%;
130
+ box-sizing: border-box;
131
+ }
132
+
133
+ .main-header-row {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ height: 64px;
138
+ }
139
+
140
+ .logo-block {
141
+ text-decoration: none;
142
+ color: #0f172a;
143
+ font-weight: 700;
144
+ font-size: 20px;
145
+ display: inline-flex;
146
+ align-items: center;
147
+ z-index: 1001;
148
+ }
149
+
150
+ :global(.logo-image) {
151
+ height: 16px !important;
152
+ max-height: 16px !important;
153
+ width: auto !important;
154
+ object-fit: contain;
155
+ display: block;
156
+ }
157
+
158
+ .logo-text {
159
+ white-space: nowrap;
160
+ font-size: 16px;
161
+ line-height: 16px;
162
+ }
163
+
164
+ @media (min-width: 768px) {
165
+ .main-header-row {
166
+ height: 64px;
167
+ justify-content: space-between;
168
+ gap: 32px;
169
+ }
170
+ }
171
+ </style>