@brightsideoy/kama 0.1.0-alpha.113 → 0.1.0-alpha.115
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 +172 -379
- package/content/definitions/blocks/button.yaml +7 -8
- package/content/definitions/blocks/header.yaml +7 -9
- package/content/definitions/blocks/header_image.yaml +14 -15
- package/content/definitions/blocks/hero.yaml +22 -21
- package/content/definitions/blocks/image_card.yaml +46 -47
- package/content/definitions/blocks/single_image.yaml +19 -20
- package/content/definitions/blocks/text.yaml +8 -9
- package/content/definitions/channels/ecommerce.yaml +14 -16
- package/content/definitions/channels/website.yaml +20 -21
- package/content/definitions/field-sets/access.yaml +3 -5
- package/content/definitions/field-sets/channel-base.yaml +25 -25
- package/content/definitions/field-sets/content-block.yaml +36 -41
- package/content/definitions/field-sets/page-base.yaml +40 -45
- package/content/definitions/field-sets/page-meta.yaml +1 -4
- package/content/definitions/field-sets/page-social.yaml +6 -8
- package/content/definitions/field-sets/tree-base.yaml +25 -25
- package/content/definitions/nodes/address.yaml +29 -21
- package/content/definitions/nodes/article.yaml +24 -19
- package/content/definitions/nodes/event.yaml +32 -26
- package/content/definitions/nodes/external-link.yaml +20 -17
- package/content/definitions/nodes/page.yaml +38 -35
- package/content/definitions/nodes/person.yaml +29 -27
- package/content/definitions/nodes/product.yaml +52 -49
- package/content/definitions/trees/navigation.yaml +26 -21
- package/content/definitions/trees/social-links.yaml +5 -2
- package/dist/assets/index-8T39DEP3.js +162 -0
- package/dist/assets/index-B9oh2_TL.css +1 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6492 -4249
- package/package.json +5 -6
- package/schema.sql +5 -13
- package/templates/astro-starter/astro.config.mjs +40 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +125 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
- package/templates/astro-starter/src/components/global/Footer.astro +48 -0
- package/templates/astro-starter/src/components/global/Header.astro +92 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +148 -0
- package/templates/astro-starter/src/kama-image-service.ts +41 -0
- package/templates/astro-starter/src/layouts/Layout.astro +280 -0
- package/templates/astro-starter/src/lib/cms.ts +149 -0
- package/templates/astro-starter/src/lib/media-url.ts +34 -0
- package/templates/astro-starter/src/middleware.ts +306 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +234 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/test-local.sh +76 -0
- package/dist/assets/index-10GEw0KY.css +0 -1
- package/dist/assets/index-THJ6BqPP.js +0 -1394
- package/kama-worker/bundled-definitions.js +0 -2394
- package/kama-worker/bundled-definitions.ts +0 -2395
- package/scripts/bundle-definitions.js +0 -224
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightsideoy/kama",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.115",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,15 +15,14 @@
|
|
|
15
15
|
"create-kama": "bin/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"predev": "npm run build:schema && npm run bundle:definitions",
|
|
19
|
-
"dev": "concurrently \"vite\" \"tsx watch server/dev.ts\"",
|
|
20
|
-
"bundle:definitions": "node scripts/bundle-definitions.js",
|
|
21
18
|
"build:schema": "tsx src/db/generate-schema.ts",
|
|
22
|
-
"build:server": "
|
|
19
|
+
"build:server": "vite build",
|
|
23
20
|
"build:ui": "vite build --config vite.config.ui.ts",
|
|
24
21
|
"build": "npm run build:schema && npm run build:server && npm run build:ui",
|
|
25
22
|
"build:cms": "npm run build",
|
|
26
|
-
"
|
|
23
|
+
"predev": "npm run build:schema",
|
|
24
|
+
"dev": "concurrently \"vite\" \"tsx watch server/dev.ts\"",
|
|
25
|
+
"prepack": "npm run build",
|
|
27
26
|
"serve": "tsx server/dev.ts"
|
|
28
27
|
},
|
|
29
28
|
"publishConfig": {
|
package/schema.sql
CHANGED
|
@@ -205,17 +205,9 @@ CREATE INDEX IF NOT EXISTS idx_assets_deleted ON assets(deletedAt);
|
|
|
205
205
|
CREATE INDEX IF NOT EXISTS idx_assets_parent ON assets(parentAssetId);
|
|
206
206
|
|
|
207
207
|
-- System Defaults
|
|
208
|
-
INSERT OR IGNORE INTO channels (id, key, label, channelType, defaultLanguage, isHidden, dataJson) VALUES (1, '
|
|
209
|
-
INSERT OR IGNORE INTO trees (id, key, label, treeType, channelId, isHidden, dataJson) VALUES (1, '
|
|
208
|
+
INSERT OR IGNORE INTO channels (id, key, label, channelType, defaultLanguage, isHidden, dataJson) VALUES (1, 'website', 'Website', 'website', 'en', 0, '{}');
|
|
209
|
+
INSERT OR IGNORE INTO trees (id, key, label, treeType, channelId, isHidden, dataJson) VALUES (1, 'main-navigation', 'Main Navigation', 'navigation', 1, 0, '{}');
|
|
210
210
|
INSERT OR IGNORE INTO channel_trees (channelId, treeId, key) VALUES (1, 1, 'main');
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
CREATE INDEX IF NOT EXISTS idx_content_nodes_slug ON content_nodes (slug);
|
|
215
|
-
CREATE INDEX IF NOT EXISTS idx_content_nodes_contentType ON content_nodes (contentType);
|
|
216
|
-
CREATE INDEX IF NOT EXISTS idx_content_nodes_updatedAt ON content_nodes (updatedAt);
|
|
217
|
-
|
|
218
|
-
-- JSON Custom Fields
|
|
219
|
-
CREATE INDEX IF NOT EXISTS idx_content_nodes_articleDate ON content_nodes (json_extract(dataJson, '$.articleDate'));
|
|
220
|
-
CREATE INDEX IF NOT EXISTS idx_content_nodes_startTime ON content_nodes (json_extract(dataJson, '$.startTime'));
|
|
221
|
-
CREATE INDEX IF NOT EXISTS idx_content_nodes_endTime ON content_nodes (json_extract(dataJson, '$.endTime'));
|
|
211
|
+
INSERT OR IGNORE INTO content_nodes (id, contentType, title, slug, language, status, dataJson, draftDataJson, publishedDataJson, publishedAt) VALUES (1, 'page', 'Global Content Delivery at the Edge', 'home', 'en', 'published', '{"titleSuffix":"– KAMA"}', '{"titleSuffix":"– KAMA"}', '{"titleSuffix":"– KAMA"}', CURRENT_TIMESTAMP);
|
|
212
|
+
INSERT OR IGNORE INTO node_channels (nodeId, channelId) VALUES (1, 1);
|
|
213
|
+
INSERT OR IGNORE INTO tree_nodes (id, treeId, contentId, parentId, sortOrder) VALUES (1, 1, 1, NULL, 0);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// templates/astro-starter/astro.config.mjs
|
|
2
|
+
import { defineConfig } from 'astro/config';
|
|
3
|
+
import cloudflare from '@astrojs/cloudflare';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
output: 'server',
|
|
11
|
+
trailingSlash: 'ignore',
|
|
12
|
+
|
|
13
|
+
// 🎯 Use Kama Image Service
|
|
14
|
+
image: {
|
|
15
|
+
service: {
|
|
16
|
+
entrypoint: './src/kama-image-service.ts',
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// 🎯 Passthrough allows Kama proxy to take 100% control
|
|
21
|
+
adapter: cloudflare({
|
|
22
|
+
imageService: 'passthrough',
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
vite: {
|
|
26
|
+
resolve: {
|
|
27
|
+
alias: {
|
|
28
|
+
'@': path.resolve(__dirname, './src')
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
optimizeDeps: {
|
|
32
|
+
exclude: ['cloudflare:workers', '@astrojs/cloudflare', 'astro']
|
|
33
|
+
},
|
|
34
|
+
ssr: {
|
|
35
|
+
target: 'webworker',
|
|
36
|
+
noExternal: true,
|
|
37
|
+
external: ['cloudflare:workers', 'node:process']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{SITE_NAME}}",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.12.0"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "astro dev",
|
|
10
|
+
"build": "astro build",
|
|
11
|
+
"preview": "astro preview",
|
|
12
|
+
"deploy": "astro build && wrangler deploy"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@astrojs/cloudflare": "^14.2.5",
|
|
16
|
+
"@brightsideoy/kama": "{{KAMA_VERSION}}",
|
|
17
|
+
"astro": "^7.2.0",
|
|
18
|
+
"wrangler": "^4.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/BlockRenderer.astro
|
|
3
|
+
import ImageBlock from "@/components/blocks/ImageBlock.astro";
|
|
4
|
+
import HeaderImage from "@/components/blocks/HeaderImage.astro";
|
|
5
|
+
import ColumnLayout from "@/components/blocks/ColumnLayout.astro";
|
|
6
|
+
|
|
7
|
+
interface Block {
|
|
8
|
+
instanceId: number;
|
|
9
|
+
blockId?: string;
|
|
10
|
+
componentId: string;
|
|
11
|
+
data: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
blocks?: Block[];
|
|
16
|
+
contentMap?: Record<string, any[]>;
|
|
17
|
+
region?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { blocks = [], contentMap = {}, region = "main" } = Astro.props;
|
|
21
|
+
const safeBlocks = Array.isArray(blocks) ? blocks : [];
|
|
22
|
+
|
|
23
|
+
const visibleBlocks = safeBlocks.filter(b => {
|
|
24
|
+
if (!b || (!b.data && !b.componentId?.includes("column"))) return false;
|
|
25
|
+
|
|
26
|
+
const blockData = b.data || {};
|
|
27
|
+
if (
|
|
28
|
+
blockData.hidden === true ||
|
|
29
|
+
blockData.hidden === 1 ||
|
|
30
|
+
String(blockData.hidden) === "true"
|
|
31
|
+
) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const now = new Date();
|
|
36
|
+
if (blockData.startShowingAt) {
|
|
37
|
+
const start = new Date(blockData.startShowingAt);
|
|
38
|
+
if (!isNaN(start.getTime()) && now < start) return false;
|
|
39
|
+
}
|
|
40
|
+
if (blockData.stopShowingAt) {
|
|
41
|
+
const stop = new Date(blockData.stopShowingAt);
|
|
42
|
+
if (!isNaN(stop.getTime()) && now > stop) return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
visibleBlocks.map((block, index) => {
|
|
51
|
+
const data = block.data || {};
|
|
52
|
+
const blockId = block.blockId || data.blockId || "";
|
|
53
|
+
const componentId = (block.componentId || "").toLowerCase();
|
|
54
|
+
|
|
55
|
+
// Check for any column layout ending in column or two-column
|
|
56
|
+
if (componentId.includes("column")) {
|
|
57
|
+
return <ColumnLayout blockId={blockId} contentMap={contentMap} />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
switch (componentId) {
|
|
61
|
+
case "header":
|
|
62
|
+
case "headline":
|
|
63
|
+
case "header-block":
|
|
64
|
+
case "header_block": {
|
|
65
|
+
const headline = data.headline || data.title || "";
|
|
66
|
+
const subhead = data.subhead || data.body || "";
|
|
67
|
+
return (
|
|
68
|
+
<header class="cms-block cms-block-header">
|
|
69
|
+
{headline && <Fragment set:html={headline} />}
|
|
70
|
+
{subhead && <Fragment set:html={subhead} />}
|
|
71
|
+
</header>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "text":
|
|
76
|
+
case "text-block":
|
|
77
|
+
case "text_block": {
|
|
78
|
+
const bodyText = data.body || data.text || data.content || "";
|
|
79
|
+
return (
|
|
80
|
+
<div class="cms-block cms-block-text">
|
|
81
|
+
<Fragment set:html={bodyText} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "header-image":
|
|
87
|
+
case "header_image": {
|
|
88
|
+
return (
|
|
89
|
+
<HeaderImage data={data} region={region} isFirst={index === 0} />
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 🎯 Handles all image component identifiers from the payload
|
|
94
|
+
case "image":
|
|
95
|
+
case "image-block":
|
|
96
|
+
case "image_block":
|
|
97
|
+
case "single_image":
|
|
98
|
+
case "single-image": {
|
|
99
|
+
return <ImageBlock data={data} region={region} isFirst={index === 0} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
return import.meta.env.DEV ? (
|
|
104
|
+
<div class="unhandled-block">
|
|
105
|
+
<p>Unhandled component: {block.componentId}</p>
|
|
106
|
+
</div>
|
|
107
|
+
) : null;
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
.cms-block {
|
|
114
|
+
margin-bottom: 1.5rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.unhandled-block {
|
|
118
|
+
padding: 12px 16px;
|
|
119
|
+
border: 1px dashed #cbd5e1;
|
|
120
|
+
background: #f8fafc;
|
|
121
|
+
color: #64748b;
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
margin-bottom: 1.5rem;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/ColumnLayout.astro
|
|
3
|
+
import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
blockId: string;
|
|
7
|
+
contentMap: Record<string, any[]>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { blockId, contentMap } = Astro.props;
|
|
11
|
+
|
|
12
|
+
const order = ["left", "middle", "right", "col1", "col2", "col3", "col4"];
|
|
13
|
+
|
|
14
|
+
// 1. Find all sub-regions belonging to this specific layout instance
|
|
15
|
+
const regionKeys = Object.keys(contentMap).filter(
|
|
16
|
+
key => blockId && key.startsWith(`${blockId}_`),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// 2. Sort keys logically (left -> middle -> right or col1 -> col2 -> col3)
|
|
20
|
+
const sortedKeys = regionKeys.sort((a, b) => {
|
|
21
|
+
const suffixA = a.replace(`${blockId}_`, "");
|
|
22
|
+
const suffixB = b.replace(`${blockId}_`, "");
|
|
23
|
+
const idxA = order.indexOf(suffixA);
|
|
24
|
+
const idxB = order.indexOf(suffixB);
|
|
25
|
+
|
|
26
|
+
if (idxA === -1 && idxB === -1) return a.localeCompare(b);
|
|
27
|
+
if (idxA === -1) return 1;
|
|
28
|
+
if (idxB === -1) return -1;
|
|
29
|
+
return idxA - idxB;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const columnCount = Math.max(sortedKeys.length, 1);
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<div class="cms-column-layout" style={`--col-count: ${columnCount}`}>
|
|
36
|
+
{
|
|
37
|
+
sortedKeys.map(key => {
|
|
38
|
+
const columnBlocks = contentMap[key] || [];
|
|
39
|
+
return (
|
|
40
|
+
<div class="column">
|
|
41
|
+
<!-- 🎯 Passes region={key} down to BlockRenderer -->
|
|
42
|
+
<BlockRenderer
|
|
43
|
+
blocks={columnBlocks}
|
|
44
|
+
contentMap={contentMap}
|
|
45
|
+
region={key}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.cms-column-layout {
|
|
55
|
+
display: grid;
|
|
56
|
+
gap: 2rem;
|
|
57
|
+
margin-bottom: 1.5rem;
|
|
58
|
+
grid-template-columns: 1fr;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@media (min-width: 768px) {
|
|
62
|
+
.cms-column-layout {
|
|
63
|
+
grid-template-columns: repeat(var(--col-count, 1), 1fr);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/HeaderImage.astro
|
|
3
|
+
import KamaImage from "@/components/ui/KamaImage.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
data: {
|
|
7
|
+
image?: any;
|
|
8
|
+
image_meta?: 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
|
+
|
|
20
|
+
if (!img) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Determine if this image is likely to be the Largest Contentful Paint.
|
|
26
|
+
*/
|
|
27
|
+
const isLcp = region === "header" || (region === "main" && isFirst);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determine grid column count from the region name.
|
|
31
|
+
*/
|
|
32
|
+
let columnCount = 1;
|
|
33
|
+
|
|
34
|
+
if (region.includes("_")) {
|
|
35
|
+
columnCount = 2;
|
|
36
|
+
|
|
37
|
+
if (region.includes("_middle") || region.includes("_col3")) {
|
|
38
|
+
columnCount = 3;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (region.includes("_col4")) {
|
|
42
|
+
columnCount = 4;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default single-column layout.
|
|
48
|
+
*/
|
|
49
|
+
let maxWidth = "1352";
|
|
50
|
+
let targetColumnWidth = 1352;
|
|
51
|
+
|
|
52
|
+
// 1-col: Base mobile (~400), 2x mobile/tablet (~800), 1x desktop (1352), 2x desktop (2704)
|
|
53
|
+
let optimizedWidths = [400, 800, 1352, 2048, 2704];
|
|
54
|
+
let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Two-column layout.
|
|
58
|
+
*/
|
|
59
|
+
if (columnCount === 2) {
|
|
60
|
+
maxWidth = "660";
|
|
61
|
+
targetColumnWidth = 660;
|
|
62
|
+
|
|
63
|
+
// 2-col: 1x desktop (660), 2x mobile stacked (~800), 2x desktop (1320)
|
|
64
|
+
optimizedWidths = [400, 660, 800, 1320];
|
|
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
|
+
// 3-col: 1x desktop (429), 2x desktop/tablet (~858), 2x/3x mobile stacked (~1000)
|
|
79
|
+
optimizedWidths = [300, 429, 858, 1024];
|
|
80
|
+
sizes =
|
|
81
|
+
"(max-width: 767px) calc(100vw - 48px), " +
|
|
82
|
+
"(max-width: 1399px) calc((100vw - 112px) / 3), " +
|
|
83
|
+
"429px";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Four-column layout.
|
|
88
|
+
*/
|
|
89
|
+
else if (columnCount === 4) {
|
|
90
|
+
maxWidth = "314";
|
|
91
|
+
targetColumnWidth = 314;
|
|
92
|
+
|
|
93
|
+
// 4-col: 1x desktop (314), 2x desktop (628), 2x/3x mobile stacked (~1000)
|
|
94
|
+
optimizedWidths = [264, 314, 628, 1024];
|
|
95
|
+
sizes =
|
|
96
|
+
"(max-width: 767px) calc(100vw - 48px), " +
|
|
97
|
+
"(max-width: 1399px) calc((100vw - 144px) / 4), " +
|
|
98
|
+
"314px";
|
|
99
|
+
}
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
<div class="cms-block cms-block-header-image" style={`--max-w: ${maxWidth}px;`}>
|
|
103
|
+
<KamaImage
|
|
104
|
+
image={img}
|
|
105
|
+
sizes={sizes}
|
|
106
|
+
desktopWidths={optimizedWidths}
|
|
107
|
+
idealWidth={targetColumnWidth}
|
|
108
|
+
width={data.width || data.image_meta?.width}
|
|
109
|
+
height={data.height || data.image_meta?.height}
|
|
110
|
+
class="responsive-header-image"
|
|
111
|
+
loading={isLcp ? "eager" : "lazy"}
|
|
112
|
+
fetchpriority={isLcp ? "high" : "auto"}
|
|
113
|
+
decoding={isLcp ? "sync" : "async"}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<style>
|
|
118
|
+
.cms-block-header-image {
|
|
119
|
+
width: 100%;
|
|
120
|
+
max-width: var(--max-w, 1352px);
|
|
121
|
+
margin: 0 auto 1.5rem auto;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:global(.responsive-header-image) {
|
|
125
|
+
width: 100%;
|
|
126
|
+
height: auto;
|
|
127
|
+
display: block;
|
|
128
|
+
border-radius: var(--cms-radius-md, 0.5rem);
|
|
129
|
+
max-width: 100%;
|
|
130
|
+
}
|
|
131
|
+
</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>
|