@brightsideoy/kama 0.1.0-alpha.98 → 1.0.0-alpha.10

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 (119) hide show
  1. package/README.md +169 -0
  2. package/bin/cli.js +726 -261
  3. package/content/definitions/blocks/button.yaml +34 -0
  4. package/content/definitions/blocks/header.yaml +13 -0
  5. package/content/definitions/blocks/header_image.yaml +21 -0
  6. package/content/definitions/blocks/hero.yaml +28 -0
  7. package/content/definitions/blocks/html.yaml +13 -0
  8. package/content/definitions/blocks/image_card.yaml +56 -0
  9. package/content/definitions/blocks/node-list.yaml +61 -0
  10. package/content/definitions/blocks/single_image.yaml +27 -0
  11. package/content/definitions/blocks/text.yaml +15 -0
  12. package/content/definitions/channels/ecommerce.yaml +28 -0
  13. package/content/definitions/channels/website.yaml +41 -0
  14. package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
  15. package/content/definitions/field-sets/channel-base.yaml +57 -0
  16. package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
  17. package/content/definitions/field-sets/content-block.yaml +41 -0
  18. package/content/definitions/field-sets/page-base.yaml +44 -0
  19. package/content/definitions/field-sets/page-seo.yaml +34 -0
  20. package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
  21. package/content/definitions/field-sets/schema-type.yaml +43 -0
  22. package/content/definitions/field-sets/tree-base.yaml +31 -0
  23. package/content/definitions/node-layouts/content.yml +12 -0
  24. package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
  25. package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
  26. package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
  27. package/content/definitions/nodes/address.yaml +40 -0
  28. package/content/definitions/nodes/article.yaml +65 -0
  29. package/content/definitions/nodes/event.yaml +55 -0
  30. package/content/definitions/nodes/external-link.yaml +29 -0
  31. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  32. package/{defaults → content}/definitions/nodes/page.yaml +9 -20
  33. package/content/definitions/nodes/person.yaml +37 -0
  34. package/content/definitions/nodes/product.yaml +89 -0
  35. package/content/definitions/taxonomies/categories.yaml +15 -0
  36. package/content/definitions/taxonomies/tags.yaml +12 -0
  37. package/content/definitions/trees/navigation.yaml +24 -0
  38. package/definitions/editor/default.ts +1 -0
  39. package/dist/assets/index-Bzbme3Ob.css +1 -0
  40. package/dist/assets/index-DX98CiK-.js +164 -0
  41. package/dist/dist-CErcWOwp.js +27136 -0
  42. package/dist/index.html +4 -2
  43. package/dist/index.js +26159 -8575
  44. package/dist/schema-compiler.js +48 -0
  45. package/documentation/SCHEMAS.md +182 -0
  46. package/package.json +23 -25
  47. package/schema.sql +67 -23
  48. package/templates/astro-starter/astro.config.mjs +27 -0
  49. package/templates/astro-starter/package.json.template +20 -0
  50. package/templates/astro-starter/public/robots.txt +23 -0
  51. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
  52. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
  53. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
  54. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  55. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
  56. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
  57. package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
  58. package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
  59. package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
  60. package/templates/astro-starter/src/components/global/Footer.astro +50 -0
  61. package/templates/astro-starter/src/components/global/Header.astro +171 -0
  62. package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
  63. package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
  64. package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
  65. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
  66. package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
  67. package/templates/astro-starter/src/lib/cms.ts +219 -0
  68. package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
  69. package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
  70. package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
  71. package/templates/astro-starter/src/middleware.ts +508 -0
  72. package/templates/astro-starter/src/pages/404.astro +46 -0
  73. package/templates/astro-starter/src/pages/[...slug].astro +318 -0
  74. package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
  75. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  76. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  77. package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
  78. package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
  79. package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
  80. package/templates/astro-starter/src/styles/standard-theme.css +157 -0
  81. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  82. package/templates/astro-starter/tsconfig.json +0 -0
  83. package/templates/kama-worker/index.js +19 -0
  84. package/templates/kama-worker/server.js +104 -0
  85. package/wrangler.json +22 -0
  86. package/defaults/definitions/blocks/button.yaml +0 -15
  87. package/defaults/definitions/blocks/header.yaml +0 -15
  88. package/defaults/definitions/blocks/header_image.yaml +0 -22
  89. package/defaults/definitions/blocks/hero.yaml +0 -27
  90. package/defaults/definitions/blocks/image_card.yaml +0 -57
  91. package/defaults/definitions/blocks/single_image.yaml +0 -28
  92. package/defaults/definitions/blocks/text.yaml +0 -16
  93. package/defaults/definitions/channels/ecommerce.yaml +0 -24
  94. package/defaults/definitions/channels/website.yaml +0 -32
  95. package/defaults/definitions/field-sets/channel-base.yaml +0 -31
  96. package/defaults/definitions/field-sets/content-block.yaml +0 -46
  97. package/defaults/definitions/field-sets/page-base.yaml +0 -49
  98. package/defaults/definitions/field-sets/tree-base.yaml +0 -31
  99. package/defaults/definitions/node-layouts/three-column.yml +0 -41
  100. package/defaults/definitions/nodes/address.yaml +0 -32
  101. package/defaults/definitions/nodes/article.yaml +0 -43
  102. package/defaults/definitions/nodes/event.yaml +0 -49
  103. package/defaults/definitions/nodes/external-link.yaml +0 -26
  104. package/defaults/definitions/nodes/person.yaml +0 -37
  105. package/defaults/definitions/nodes/product.yaml +0 -86
  106. package/defaults/definitions/trees/navigation.yaml +0 -29
  107. package/defaults/definitions/trees/social-links.yaml +0 -9
  108. package/definitions/_page.yaml +0 -52
  109. package/dist/assets/index-TdW5F0pX.css +0 -1
  110. package/dist/assets/index-u9OkBauM.js +0 -1392
  111. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  112. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  113. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  114. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
  115. package/scripts/bundle-definitions.js +0 -176
  116. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  117. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  118. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  119. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
@@ -0,0 +1,143 @@
1
+ ---
2
+ // src/components/blocks/BlockRenderer.astro
3
+
4
+ import ImageBlock from "@/components/blocks/ImageBlock.astro";
5
+ import HeaderImage from "@/components/blocks/HeaderImage.astro";
6
+ import ColumnLayout from "@/components/blocks/ColumnLayout.astro";
7
+ import HtmlBlock from "@/components/blocks/HtmlBlock.astro";
8
+ import NodeList from "@/components/blocks/NodeList.astro"; // 🎯 NEW IMPORT
9
+ import { extractHtml } from "@/utils/tiptap";
10
+
11
+ interface Block {
12
+ instanceId: number;
13
+ blockId?: string;
14
+ componentId: string;
15
+ data: Record<string, any>;
16
+ }
17
+
18
+ interface Props {
19
+ blocks?: Block[];
20
+ contentMap?: Record<string, any[]>;
21
+ region?: string;
22
+ }
23
+
24
+ const { blocks = [], contentMap = {}, region = "main" } = Astro.props;
25
+ const safeBlocks = Array.isArray(blocks) ? blocks : [];
26
+
27
+ const visibleBlocks = safeBlocks.filter(b => {
28
+ if (!b || (!b.data && !b.componentId?.includes("column"))) return false;
29
+
30
+ const blockData = b.data || {};
31
+ if (
32
+ blockData.hidden === true ||
33
+ blockData.hidden === 1 ||
34
+ String(blockData.hidden) === "true"
35
+ ) {
36
+ return false;
37
+ }
38
+
39
+ const now = new Date();
40
+ if (blockData.startShowingAt) {
41
+ const start = new Date(blockData.startShowingAt);
42
+ if (!isNaN(start.getTime()) && now < start) return false;
43
+ }
44
+ if (blockData.stopShowingAt) {
45
+ const stop = new Date(blockData.stopShowingAt);
46
+ if (!isNaN(stop.getTime()) && now > stop) return false;
47
+ }
48
+
49
+ return true;
50
+ });
51
+ ---
52
+
53
+ {
54
+ visibleBlocks.map((block, index) => {
55
+ const data = block.data || {};
56
+ const blockId = block.blockId || data.blockId || "";
57
+ const componentId = (block.componentId || "").toLowerCase();
58
+
59
+ // Check for any column layout ending in column or two-column
60
+ if (componentId.includes("column")) {
61
+ return <ColumnLayout blockId={blockId} contentMap={contentMap} />;
62
+ }
63
+
64
+ switch (componentId) {
65
+ // 🎯 ADDED NODE LIST CASES
66
+ case "node-list":
67
+ case "nodelist":
68
+ case "article-list":
69
+ case "content-feed": {
70
+ return <NodeList data={data} />;
71
+ }
72
+
73
+ case "header":
74
+ case "headline":
75
+ case "header-block":
76
+ case "header_block": {
77
+ const headline = extractHtml(data.headline || data.title);
78
+ const subhead = extractHtml(data.subhead || data.body);
79
+ return (
80
+ <header class="cms-block cms-block-header">
81
+ {headline && <Fragment set:html={headline} />}
82
+ {subhead && <Fragment set:html={subhead} />}
83
+ </header>
84
+ );
85
+ }
86
+
87
+ case "text":
88
+ case "text-block":
89
+ case "text_block": {
90
+ const bodyText = extractHtml(data.body || data.text || data.content);
91
+ return (
92
+ <div class="cms-block cms-block-text">
93
+ <Fragment set:html={bodyText} />
94
+ </div>
95
+ );
96
+ }
97
+
98
+ case "html":
99
+ case "html-block":
100
+ case "html_block":
101
+ case "raw-html": {
102
+ return <HtmlBlock data={data} />;
103
+ }
104
+
105
+ case "header-image":
106
+ case "header_image": {
107
+ return (
108
+ <HeaderImage data={data} region={region} isFirst={index === 0} />
109
+ );
110
+ }
111
+
112
+ case "image":
113
+ case "image-block":
114
+ case "image_block":
115
+ case "single_image":
116
+ case "single-image": {
117
+ return <ImageBlock data={data} region={region} isFirst={index === 0} />;
118
+ }
119
+
120
+ default:
121
+ return import.meta.env.DEV ? (
122
+ <div class="unhandled-block">
123
+ <p>Unhandled component: {block.componentId}</p>
124
+ </div>
125
+ ) : null;
126
+ }
127
+ })
128
+ }
129
+
130
+ <style>
131
+ .cms-block {
132
+ margin-bottom: 1.5rem;
133
+ }
134
+
135
+ .unhandled-block {
136
+ padding: 12px 16px;
137
+ border: 1px dashed #cbd5e1;
138
+ background: #f8fafc;
139
+ color: #64748b;
140
+ font-size: 13px;
141
+ margin-bottom: 1.5rem;
142
+ }
143
+ </style>
@@ -0,0 +1,67 @@
1
+ ---
2
+ // src/components/blocks/ColumnLayout.astro
3
+
4
+ import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
5
+
6
+ interface Props {
7
+ blockId: string;
8
+ contentMap: Record<string, any[]>;
9
+ }
10
+
11
+ const { blockId, contentMap } = Astro.props;
12
+
13
+ const order = ["left", "middle", "right", "col1", "col2", "col3", "col4"];
14
+
15
+ // 1. Find all sub-regions belonging to this specific layout instance
16
+ const regionKeys = Object.keys(contentMap).filter(
17
+ key => blockId && key.startsWith(`${blockId}_`),
18
+ );
19
+
20
+ // 2. Sort keys logically (left -> middle -> right or col1 -> col2 -> col3)
21
+ const sortedKeys = regionKeys.sort((a, b) => {
22
+ const suffixA = a.replace(`${blockId}_`, "");
23
+ const suffixB = b.replace(`${blockId}_`, "");
24
+ const idxA = order.indexOf(suffixA);
25
+ const idxB = order.indexOf(suffixB);
26
+
27
+ if (idxA === -1 && idxB === -1) return a.localeCompare(b);
28
+ if (idxA === -1) return 1;
29
+ if (idxB === -1) return -1;
30
+ return idxA - idxB;
31
+ });
32
+
33
+ const columnCount = Math.max(sortedKeys.length, 1);
34
+ ---
35
+
36
+ <div class="cms-column-layout" style={`--col-count: ${columnCount}`}>
37
+ {
38
+ sortedKeys.map(key => {
39
+ const columnBlocks = contentMap[key] || [];
40
+ return (
41
+ <div class="column">
42
+ <!-- 🎯 Passes region={key} down to BlockRenderer -->
43
+ <BlockRenderer
44
+ blocks={columnBlocks}
45
+ contentMap={contentMap}
46
+ region={key}
47
+ />
48
+ </div>
49
+ );
50
+ })
51
+ }
52
+ </div>
53
+
54
+ <style>
55
+ .cms-column-layout {
56
+ display: grid;
57
+ gap: 2rem;
58
+ margin-bottom: 1.5rem;
59
+ grid-template-columns: 1fr;
60
+ }
61
+
62
+ @media (min-width: 768px) {
63
+ .cms-column-layout {
64
+ grid-template-columns: repeat(var(--col-count, 1), 1fr);
65
+ }
66
+ }
67
+ </style>
@@ -0,0 +1,126 @@
1
+ ---
2
+ // src/components/blocks/HeaderImage.astro
3
+
4
+ import KamaImage from "@/components/ui/KamaImage.astro";
5
+
6
+ interface Props {
7
+ data: {
8
+ image?: any;
9
+ image_meta?: any;
10
+ width?: number | string;
11
+ height?: number | string;
12
+ };
13
+ region?: string;
14
+ isFirst?: boolean;
15
+ }
16
+
17
+ const { data, region = "main", isFirst = false } = Astro.props;
18
+
19
+ const img = data?.image;
20
+
21
+ if (!img) {
22
+ return null;
23
+ }
24
+
25
+ /**
26
+ * Determine if this image is likely to be the Largest Contentful Paint.
27
+ */
28
+ const isLcp = region === "header" || (region === "main" && isFirst);
29
+
30
+ /**
31
+ * Determine grid column count from the region name.
32
+ */
33
+ let columnCount = 1;
34
+
35
+ if (region.includes("_")) {
36
+ columnCount = 2;
37
+
38
+ if (region.includes("_middle") || region.includes("_col3")) {
39
+ columnCount = 3;
40
+ }
41
+
42
+ if (region.includes("_col4")) {
43
+ columnCount = 4;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Default single-column layout (aligned to backend CDN buckets).
49
+ */
50
+ let maxWidth = "1352";
51
+ let targetColumnWidth = 1352;
52
+
53
+ // 1-col: 1x/2x desktop and retina buckets
54
+ let optimizedWidths = [768, 1352, 1920, 2560];
55
+ let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
56
+
57
+ /**
58
+ * Two-column layout.
59
+ */
60
+ if (columnCount === 2) {
61
+ maxWidth = "660";
62
+ targetColumnWidth = 660;
63
+
64
+ optimizedWidths = [640, 768, 1024, 1352];
65
+ sizes =
66
+ "(max-width: 767px) calc(100vw - 48px), " +
67
+ "(max-width: 1399px) calc((100vw - 80px) / 2), " +
68
+ "660px";
69
+ }
70
+
71
+ /**
72
+ * Three-column layout.
73
+ */
74
+ else if (columnCount === 3) {
75
+ maxWidth = "429";
76
+ targetColumnWidth = 429;
77
+
78
+ optimizedWidths = [320, 640, 768, 1024];
79
+ sizes =
80
+ "(max-width: 767px) calc(100vw - 48px), " +
81
+ "(max-width: 1399px) calc((100vw - 112px) / 3), " +
82
+ "429px";
83
+ }
84
+
85
+ /**
86
+ * Four-column layout.
87
+ */
88
+ else if (columnCount === 4) {
89
+ maxWidth = "314";
90
+ targetColumnWidth = 314;
91
+
92
+ optimizedWidths = [320, 640, 768];
93
+ sizes =
94
+ "(max-width: 767px) calc(100vw - 48px), " +
95
+ "(max-width: 1399px) calc((100vw - 144px) / 4), " +
96
+ "314px";
97
+ }
98
+ ---
99
+
100
+ <div class="cms-block cms-block-header-image" style={`--max-w: ${maxWidth}px;`}>
101
+ <KamaImage
102
+ image={img}
103
+ sizes={sizes}
104
+ desktopWidths={optimizedWidths}
105
+ idealWidth={targetColumnWidth}
106
+ class="responsive-header-image"
107
+ loading={isLcp ? "eager" : "lazy"}
108
+ fetchpriority={isLcp ? "high" : "auto"}
109
+ />
110
+ </div>
111
+
112
+ <style>
113
+ .cms-block-header-image {
114
+ width: 100%;
115
+ max-width: var(--max-w, 1352px);
116
+ margin: 0 auto 1.5rem auto;
117
+ }
118
+
119
+ :global(.responsive-header-image) {
120
+ width: 100%;
121
+ height: auto;
122
+ display: block;
123
+ border-radius: var(--cms-radius-md, 0.5rem);
124
+ max-width: 100%;
125
+ }
126
+ </style>
@@ -0,0 +1,218 @@
1
+ ---
2
+ // src/components/blocks/Hero.astro
3
+ ---
4
+
5
+ <section class="hero-container">
6
+ <div class="hero-content">
7
+ <!-- Sub-badge -->
8
+ <div class="hero-badge">
9
+ <span class="badge-dot"></span>
10
+ Freeware for Education & NGOs
11
+ </div>
12
+
13
+ <!-- H1 Title with Slogan -->
14
+ <h1 class="hero-title">KAMA – Headless CMS on Edge</h1>
15
+
16
+ <!-- Subtitle -->
17
+ <p class="hero-subtitle">
18
+ Deploy sub-millisecond corporate web experiences with zero infrastructure
19
+ overhead. Built natively on Astro, Hono, and Cloudflare Workers (D1/R2).
20
+ </p>
21
+
22
+ <!-- Call to Action Buttons -->
23
+ <div class="hero-actions">
24
+ <a href="/docs/quick-start" class="btn btn-primary">
25
+ Get Started
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="16"
29
+ height="16"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ stroke-width="2"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ class="btn-icon"
37
+ ><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
38
+ >
39
+ </a>
40
+ <a
41
+ href="https://github.com/brightside-ou/kama"
42
+ target="_blank"
43
+ rel="noopener noreferrer"
44
+ class="btn btn-secondary"
45
+ >
46
+ View on GitHub
47
+ </a>
48
+ </div>
49
+
50
+ <!-- Tech Stack Highlights -->
51
+ <div class="hero-stack">
52
+ <span class="stack-label">Powered by edge infrastructure:</span>
53
+ <div class="stack-tags">
54
+ <span class="tag">Cloudflare Workers</span>
55
+ <span class="tag">D1 SQL</span>
56
+ <span class="tag">R2 Storage</span>
57
+ <span class="tag">Astro</span>
58
+ <span class="tag">Hono</span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </section>
63
+
64
+ <style>
65
+ .hero-container {
66
+ padding: 64px 0 48px;
67
+ background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
68
+ border-bottom: 1px solid #e2e8f0;
69
+ }
70
+
71
+ .hero-content {
72
+ max-width: 860px;
73
+ margin: 0 auto;
74
+ text-align: center;
75
+ }
76
+
77
+ .hero-badge {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ padding: 6px 14px;
82
+ background-color: #f0f9ff;
83
+ border: 1px solid #bae6fd;
84
+ color: #0369a1;
85
+ font-size: 0.875rem;
86
+ font-weight: 600;
87
+ margin-bottom: 24px;
88
+ }
89
+
90
+ .badge-dot {
91
+ width: 8px;
92
+ height: 8px;
93
+ background-color: #0284c7;
94
+ border-radius: 50% !important;
95
+ }
96
+
97
+ .hero-title {
98
+ font-size: 3rem;
99
+ font-weight: 800;
100
+ line-height: 1.15;
101
+ color: #0f172a;
102
+ letter-spacing: -0.03em;
103
+ margin-top: 0;
104
+ margin-bottom: 20px;
105
+ }
106
+
107
+ .hero-subtitle {
108
+ font-size: 1.25rem;
109
+ line-height: 1.6;
110
+ color: #475569;
111
+ max-width: 720px;
112
+ margin: 0 auto 36px;
113
+ }
114
+
115
+ .hero-actions {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: 16px;
120
+ margin-bottom: 48px;
121
+ }
122
+
123
+ .btn {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ gap: 8px;
128
+ padding: 12px 28px;
129
+ font-size: 1rem;
130
+ font-weight: 600;
131
+ text-decoration: none;
132
+ transition: all 0.15s ease-in-out;
133
+ }
134
+
135
+ .btn-primary {
136
+ background-color: #0284c7;
137
+ color: #ffffff;
138
+ }
139
+
140
+ .btn-primary:hover {
141
+ background-color: #0369a1;
142
+ color: #ffffff;
143
+ }
144
+
145
+ .btn-secondary {
146
+ background-color: #ffffff;
147
+ color: #334155;
148
+ border: 1px solid #cbd5e1;
149
+ }
150
+
151
+ .btn-secondary:hover {
152
+ background-color: #f8fafc;
153
+ color: #0f172a;
154
+ border-color: #94a3b8;
155
+ }
156
+
157
+ .btn-icon {
158
+ transition: transform 0.15s ease;
159
+ }
160
+
161
+ .btn-primary:hover .btn-icon {
162
+ transform: translateX(4px);
163
+ }
164
+
165
+ .hero-stack {
166
+ display: flex;
167
+ flex-direction: column;
168
+ align-items: center;
169
+ gap: 12px;
170
+ }
171
+
172
+ .stack-label {
173
+ font-size: 0.8125rem;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.05em;
176
+ color: #64748b;
177
+ font-weight: 600;
178
+ }
179
+
180
+ .stack-tags {
181
+ display: flex;
182
+ flex-wrap: wrap;
183
+ justify-content: center;
184
+ gap: 8px;
185
+ }
186
+
187
+ .tag {
188
+ padding: 4px 10px;
189
+ background-color: #f1f5f9;
190
+ color: #334155;
191
+ font-size: 0.8125rem;
192
+ font-weight: 500;
193
+ border: 1px solid #e2e8f0;
194
+ }
195
+
196
+ @media (max-width: 768px) {
197
+ .hero-container {
198
+ padding: 40px 0 32px;
199
+ }
200
+
201
+ .hero-title {
202
+ font-size: 2.125rem;
203
+ }
204
+
205
+ .hero-subtitle {
206
+ font-size: 1.05rem;
207
+ }
208
+
209
+ .hero-actions {
210
+ flex-direction: column;
211
+ width: 100%;
212
+ }
213
+
214
+ .btn {
215
+ width: 100%;
216
+ }
217
+ }
218
+ </style>
@@ -0,0 +1,25 @@
1
+ ---
2
+ // src/components/blocks/HtmlBlock.astro
3
+
4
+ interface Props {
5
+ data: {
6
+ body?: string;
7
+ html?: string;
8
+ content?: string;
9
+ text?: string;
10
+ };
11
+ }
12
+
13
+ const { data } = Astro.props;
14
+
15
+ // Fallback chain matching your schema's HTML field key ('body') and legacy aliases
16
+ const rawHtml = data?.body || data?.html || data?.content || data?.text || "";
17
+ ---
18
+
19
+ {
20
+ rawHtml && (
21
+ <div class="cms-block cms-block-html">
22
+ <Fragment set:html={rawHtml} />
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,91 @@
1
+ ---
2
+ // src/components/blocks/ImageBlock.astro
3
+
4
+ import KamaImage from "@/components/ui/KamaImage.astro";
5
+
6
+ interface Props {
7
+ data: {
8
+ image?: any;
9
+ width?: number | string;
10
+ height?: number | string;
11
+ };
12
+ region?: string;
13
+ isFirst?: boolean;
14
+ }
15
+
16
+ const { data, region = "main", isFirst = false } = Astro.props;
17
+
18
+ const img = data?.image;
19
+ const isLcp = region === "header" || (region === "main" && isFirst);
20
+
21
+ // 🎯 Detect column layout based on CMS region names
22
+ let columnCount = 1;
23
+ if (region.includes("_")) {
24
+ columnCount = 2;
25
+ if (region.includes("_middle") || region.includes("_col3")) columnCount = 3;
26
+ if (region.includes("_col4")) columnCount = 4;
27
+ }
28
+
29
+ // 🎯 Column Config Lookup (Target width, aligned CDN breakpoint widths, sizes)
30
+ const COLUMN_CONFIGS: Record<
31
+ number,
32
+ { targetWidth: number; widths: number[]; sizes: string }
33
+ > = {
34
+ 1: {
35
+ targetWidth: 1352,
36
+ widths: [768, 1352, 1920, 2560],
37
+ sizes: "(max-width: 1399px) calc(100vw - 48px), 1352px",
38
+ },
39
+ 2: {
40
+ targetWidth: 660,
41
+ widths: [640, 768, 1024, 1352],
42
+ sizes:
43
+ "(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 80px) / 2), 660px",
44
+ },
45
+ 3: {
46
+ targetWidth: 429,
47
+ widths: [320, 640, 768, 1024],
48
+ sizes:
49
+ "(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 112px) / 3), 429px",
50
+ },
51
+ 4: {
52
+ targetWidth: 314,
53
+ widths: [320, 640, 768],
54
+ sizes:
55
+ "(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 144px) / 4), 314px",
56
+ },
57
+ };
58
+
59
+ const layout = COLUMN_CONFIGS[columnCount] || COLUMN_CONFIGS[1];
60
+ ---
61
+
62
+ {
63
+ img && (
64
+ <div class="cms-block cms-block-image">
65
+ <KamaImage
66
+ image={img}
67
+ sizes={layout.sizes}
68
+ desktopWidths={layout.widths}
69
+ idealWidth={layout.targetWidth}
70
+ class="free-crop-image"
71
+ loading={isLcp ? "eager" : "lazy"}
72
+ fetchpriority={isLcp ? "high" : "auto"}
73
+ />
74
+ </div>
75
+ )
76
+ }
77
+
78
+ <style>
79
+ .cms-block-image {
80
+ width: 100%;
81
+ margin: 0 auto 1.5rem auto;
82
+ }
83
+
84
+ :global(.free-crop-image) {
85
+ width: 100%;
86
+ height: auto;
87
+ object-fit: contain;
88
+ display: block;
89
+ border-radius: var(--cms-radius-md, 0.5rem);
90
+ }
91
+ </style>