@africode/core 5.0.0

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 (136) hide show
  1. package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
  2. package/LICENSE +623 -0
  3. package/README.md +442 -0
  4. package/bin/africode.js +73 -0
  5. package/bin/africode.js.1758507140 +343 -0
  6. package/bin/cli.ts +83 -0
  7. package/bin/create-africode.js +158 -0
  8. package/bin/scaffold.ts +219 -0
  9. package/components/accordion.js +183 -0
  10. package/components/alert.js +131 -0
  11. package/components/auth.js +172 -0
  12. package/components/avatar.js +117 -0
  13. package/components/badge.js +104 -0
  14. package/components/base.d.ts +139 -0
  15. package/components/base.js +184 -0
  16. package/components/button.js +164 -0
  17. package/components/card.js +137 -0
  18. package/components/cultural-card.js +243 -0
  19. package/components/divider.js +83 -0
  20. package/components/dropdown.js +171 -0
  21. package/components/error-boundary.js +155 -0
  22. package/components/form.js +131 -0
  23. package/components/grid.js +273 -0
  24. package/components/hero.js +138 -0
  25. package/components/icon.js +36 -0
  26. package/components/index.js +57 -0
  27. package/components/input.js +256 -0
  28. package/components/kanga-card.js +185 -0
  29. package/components/language-switcher.js +108 -0
  30. package/components/loader.js +80 -0
  31. package/components/modal.js +262 -0
  32. package/components/motion.js +84 -0
  33. package/components/navbar.js +236 -0
  34. package/components/pattern-showcase.js +225 -0
  35. package/components/progress.js +134 -0
  36. package/components/react.js +111 -0
  37. package/components/section.js +54 -0
  38. package/components/select.js +322 -0
  39. package/components/sidebar.js +180 -0
  40. package/components/skeleton.js +85 -0
  41. package/components/table.js +181 -0
  42. package/components/tabs.js +202 -0
  43. package/components/theme-toggle.js +82 -0
  44. package/components/toast.js +139 -0
  45. package/components/tooltip.js +167 -0
  46. package/core/a2ui-schema-manager.js +344 -0
  47. package/core/a2ui.js +431 -0
  48. package/core/bun-runtime.js +799 -0
  49. package/core/cli/commands/add.js +23 -0
  50. package/core/cli/commands/audit.js +58 -0
  51. package/core/cli/commands/build.js +137 -0
  52. package/core/cli/commands/create-plugin.js +241 -0
  53. package/core/cli/commands/dev.js +228 -0
  54. package/core/cli/commands/lint.js +23 -0
  55. package/core/cli/commands/test.js +34 -0
  56. package/core/cli/migrator.js +71 -0
  57. package/core/cli/ui.js +46 -0
  58. package/core/compliance.js +628 -0
  59. package/core/config.js +263 -0
  60. package/core/db-advanced.js +481 -0
  61. package/core/db.js +284 -0
  62. package/core/enhanced-hmr.js +404 -0
  63. package/core/errors.js +222 -0
  64. package/core/file-router.js +290 -0
  65. package/core/heartbeat.js +64 -0
  66. package/core/hmr-client.js +204 -0
  67. package/core/hmr.js +196 -0
  68. package/core/html.d.ts +116 -0
  69. package/core/html.js +160 -0
  70. package/core/hydration.js +52 -0
  71. package/core/lipa-namba-journey.js +572 -0
  72. package/core/motion.js +106 -0
  73. package/core/nida-cig-middleware.js +455 -0
  74. package/core/patterns.d.ts +124 -0
  75. package/core/patterns.js +833 -0
  76. package/core/plugins/index.js +312 -0
  77. package/core/router.js +387 -0
  78. package/core/sdk-client.js +62 -0
  79. package/core/sdk.d.ts +133 -0
  80. package/core/sdk.js +123 -0
  81. package/core/seo.js +76 -0
  82. package/core/server/auth-endpoints.js +339 -0
  83. package/core/server/auth.js +180 -0
  84. package/core/server/csrf.js +206 -0
  85. package/core/server/db.js +39 -0
  86. package/core/server/middleware.js +324 -0
  87. package/core/server/rate-limit.js +238 -0
  88. package/core/server/render.js +69 -0
  89. package/core/server/router.js +120 -0
  90. package/core/shim.js +28 -0
  91. package/core/state.d.ts +86 -0
  92. package/core/state.js +242 -0
  93. package/core/store.d.ts +122 -0
  94. package/core/store.js +61 -0
  95. package/core/validation.d.ts +233 -0
  96. package/core/validation.js +590 -0
  97. package/core/websocket.js +639 -0
  98. package/dist/africode.js +2905 -0
  99. package/dist/africode.js.map +61 -0
  100. package/dist/build-info.json +23 -0
  101. package/dist/components.js +2888 -0
  102. package/dist/components.js.map +58 -0
  103. package/dist/styles/africanity.css +322 -0
  104. package/dist/styles/typography.css +141 -0
  105. package/docs/IDE-Guide.md +50 -0
  106. package/package.json +110 -0
  107. package/src/index.ts +196 -0
  108. package/styles/africanity.css +322 -0
  109. package/styles/typography.css +141 -0
  110. package/templates/starter/.env.example +15 -0
  111. package/templates/starter/africode.config.js +40 -0
  112. package/templates/starter/package.json +14 -0
  113. package/templates/starter/src/pages/index.html +46 -0
  114. package/templates/starter/src/pages/index.js +32 -0
  115. package/templates/starter/src/styles/main.css +4 -0
  116. package/templates/starter-3d/.env.example +7 -0
  117. package/templates/starter-3d/africode.config.js +29 -0
  118. package/templates/starter-3d/components/af-model-viewer.js +125 -0
  119. package/templates/starter-3d/package.json +15 -0
  120. package/templates/starter-3d/src/pages/index.html +46 -0
  121. package/templates/starter-3d/src/pages/index.js +50 -0
  122. package/templates/starter-3d/src/styles/main.css +4 -0
  123. package/templates/starter-react/.env.example +15 -0
  124. package/templates/starter-react/africode.config.js +40 -0
  125. package/templates/starter-react/package.json +16 -0
  126. package/templates/starter-react/src/pages/index.html +46 -0
  127. package/templates/starter-react/src/pages/index.js +68 -0
  128. package/templates/starter-react/src/styles/main.css +4 -0
  129. package/templates/starter-tailwind/.env.example +15 -0
  130. package/templates/starter-tailwind/africode.config.js +40 -0
  131. package/templates/starter-tailwind/package.json +20 -0
  132. package/templates/starter-tailwind/src/pages/index.html +46 -0
  133. package/templates/starter-tailwind/src/pages/index.js +37 -0
  134. package/templates/starter-tailwind/src/styles/main.css +4 -0
  135. package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
  136. package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * AfriCode HMR Client
3
+ *
4
+ * Injected into pages during development
5
+ * Handles module reloading and page updates
6
+ */
7
+
8
+ (function hmrClient() {
9
+ const WS_URL = `ws://${window.location.hostname}:${window.location.port === '3000' ? '3000' : window.location.port}`;
10
+ let ws = null;
11
+ let reconnectAttempts = 0;
12
+ const MAX_RECONNECT = 10;
13
+ const RECONNECT_DELAY = 1000;
14
+
15
+ /**
16
+ * Connect to HMR server via WebSocket
17
+ */
18
+ function connect() {
19
+ if (ws && ws.readyState === WebSocket.OPEN) {
20
+ return;
21
+ }
22
+
23
+ try {
24
+ ws = new WebSocket(WS_URL);
25
+
26
+ ws.onopen = () => {
27
+ console.log(`%c[HMR] Connected`, 'color: green; font-weight: bold;');
28
+ reconnectAttempts = 0;
29
+ sendPing();
30
+ };
31
+
32
+ ws.onmessage = (event) => {
33
+ try {
34
+ const message = JSON.parse(event.data);
35
+ handleUpdate(message);
36
+ } catch (err) {
37
+ console.error('[HMR] Failed to parse message:', err);
38
+ }
39
+ };
40
+
41
+ ws.onerror = (error) => {
42
+ console.error('[HMR] WebSocket error:', error);
43
+ };
44
+
45
+ ws.onclose = () => {
46
+ console.log(`%c[HMR] Disconnected`, 'color: orange; font-weight: bold;');
47
+ scheduleReconnect();
48
+ };
49
+ } catch (err) {
50
+ console.error('[HMR] Connection failed:', err);
51
+ scheduleReconnect();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Schedule reconnection with exponential backoff
57
+ */
58
+ function scheduleReconnect() {
59
+ if (reconnectAttempts >= MAX_RECONNECT) {
60
+ console.warn('[HMR] Max reconnection attempts reached. Giving up.');
61
+ return;
62
+ }
63
+
64
+ reconnectAttempts++;
65
+ const delay = RECONNECT_DELAY * Math.pow(1.5, reconnectAttempts - 1);
66
+ console.log(`[HMR] Reconnecting in ${delay.toFixed(0)}ms... (attempt ${reconnectAttempts})`);
67
+
68
+ setTimeout(connect, delay);
69
+ }
70
+
71
+ /**
72
+ * Send ping to keep connection alive
73
+ */
74
+ function sendPing() {
75
+ if (ws && ws.readyState === WebSocket.OPEN) {
76
+ ws.send(JSON.stringify({ type: 'ping' }));
77
+ setTimeout(sendPing, 30000); // Ping every 30 seconds
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Handle update from HMR server
83
+ */
84
+ function handleUpdate(message) {
85
+ if (message.type !== 'update') {
86
+ return;
87
+ }
88
+
89
+ const { moduleType, modulePath, filename, timestamp } = message;
90
+
91
+ console.log(`%c[HMR] Update received: ${filename}`, 'color: #4CAF50; font-weight: bold;');
92
+
93
+ // Handle different module types
94
+ switch (moduleType) {
95
+ case 'page':
96
+ reloadPage();
97
+ break;
98
+
99
+ case 'style':
100
+ reloadStyles();
101
+ break;
102
+
103
+ case 'component':
104
+ reloadComponent(modulePath);
105
+ break;
106
+
107
+ case 'core':
108
+ // Core module changes require full reload
109
+ reloadPage();
110
+ break;
111
+
112
+ case 'markup':
113
+ reloadPage();
114
+ break;
115
+
116
+ default:
117
+ // Unknown type, do full reload to be safe
118
+ reloadPage();
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Reload all stylesheets
124
+ */
125
+ function reloadStyles() {
126
+ console.log('%c[HMR] Reloading styles...', 'color: #2196F3;');
127
+
128
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
129
+ links.forEach(link => {
130
+ const href = link.href.split('?')[0];
131
+ link.href = href + '?t=' + Date.now();
132
+ });
133
+
134
+ // Also reload inline styles if they reference external files
135
+ const styles = document.querySelectorAll('style');
136
+ styles.forEach(style => {
137
+ if (style.textContent.includes('@import')) {
138
+ style.textContent = style.textContent.replace(
139
+ /\?t=\d+/g,
140
+ `?t=${Date.now()}`
141
+ );
142
+ }
143
+ });
144
+
145
+ console.log('%c[HMR] Styles reloaded ✓', 'color: green;');
146
+ }
147
+
148
+ /**
149
+ * Reload a component by clearing its cache and refreshing
150
+ */
151
+ function reloadComponent(componentPath) {
152
+ console.log(`%c[HMR] Reloading component: ${componentPath}`, 'color: #FF9800;');
153
+
154
+ // Find all instances of the component in the DOM
155
+ const componentName = componentPath.split('-')[0];
156
+ const componentElements = document.querySelectorAll(`[data-hmr-component="${componentPath}"]`);
157
+
158
+ if (componentElements.length === 0) {
159
+ console.log('[HMR] Component not found in DOM, doing full reload');
160
+ reloadPage();
161
+ return;
162
+ }
163
+
164
+ // For Web Components, we need to reload the entire page
165
+ // because ES modules have singleton nature
166
+ reloadPage();
167
+ }
168
+
169
+ /**
170
+ * Perform full page reload
171
+ */
172
+ function reloadPage() {
173
+ console.log('%c[HMR] Performing full page reload...', 'color: #FF5722; font-weight: bold;');
174
+
175
+ // Add query param to bust cache
176
+ const url = new URL(window.location);
177
+ url.searchParams.set('hmr', Date.now());
178
+
179
+ // Use replace instead of reload to avoid adding history entries
180
+ window.location.replace(url.toString());
181
+ }
182
+
183
+ /**
184
+ * Initialize HMR client
185
+ */
186
+ function init() {
187
+ console.log('%c[AfriCode HMR Client] Initializing...', 'color: #673AB7; font-weight: bold;');
188
+ connect();
189
+ }
190
+
191
+ // Initialize when DOM is ready
192
+ if (document.readyState === 'loading') {
193
+ document.addEventListener('DOMContentLoaded', init);
194
+ } else {
195
+ init();
196
+ }
197
+
198
+ // Cleanup on page unload
199
+ window.addEventListener('beforeunload', () => {
200
+ if (ws) {
201
+ ws.close();
202
+ }
203
+ });
204
+ })();
package/core/hmr.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * HMR Server - Hot Module Replacement for AfriCode
3
+ *
4
+ * Provides:
5
+ * - WebSocket server for dev clients
6
+ * - File watcher for source changes
7
+ * - Module update broadcasting
8
+ * - Graceful reload handling
9
+ */
10
+
11
+ import * as nodePath from 'node:path';
12
+ import { watch } from 'node:fs';
13
+
14
+ export class HMRServer {
15
+ constructor(port = 3001) {
16
+ this.port = port;
17
+ this.clients = new Set();
18
+ this.watchers = new Map();
19
+ this.fileChanges = new Map();
20
+ }
21
+
22
+ /**
23
+ * Start HMR WebSocket server
24
+ */
25
+ start(devServer) {
26
+ const hmrPort = this.port;
27
+
28
+ // Upgrade HTTP connections to WebSocket
29
+ devServer.upgrade({
30
+ open(ws) {
31
+ console.log(`[HMR] Client connected from ${ws.remoteAddress}`);
32
+ this.clients.add(ws);
33
+ },
34
+ message(ws, message) {
35
+ // Handle ping/pong and client messages
36
+ try {
37
+ const data = JSON.parse(message);
38
+ if (data.type === 'ping') {
39
+ ws.send(JSON.stringify({ type: 'pong' }));
40
+ }
41
+ } catch (e) {
42
+ // Invalid message, ignore
43
+ }
44
+ },
45
+ close(ws) {
46
+ this.clients.delete(ws);
47
+ console.log(`[HMR] Client disconnected`);
48
+ }
49
+ });
50
+
51
+ // Start file watching
52
+ this.watchDirectory(process.cwd());
53
+ console.log(`[HMR] Watching for changes...`);
54
+ }
55
+
56
+ /**
57
+ * Watch directory for changes recursively
58
+ */
59
+ watchDirectory(dir, prefix = '') {
60
+ const ignoreDirs = new Set([
61
+ 'node_modules', '.git', 'dist', '.bun', 'build',
62
+ '.next', 'coverage', 'test-results'
63
+ ]);
64
+
65
+ try {
66
+ const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
67
+ if (!filename) return;
68
+
69
+ const fullPath = nodePath.join(dir, filename);
70
+ const dirName = nodePath.dirname(filename);
71
+
72
+ // Ignore node_modules, build files, etc.
73
+ if (ignoreDirs.has(dirName.split(nodePath.sep)[0])) return;
74
+ if (filename.includes('node_modules')) return;
75
+ if (filename.endsWith('.test.js')) return;
76
+
77
+ // Only watch JS, JSX, TS, TSX, CSS, HTML
78
+ const validExts = ['.js', '.jsx', '.ts', '.tsx', '.css', '.html'];
79
+ if (!validExts.some(ext => filename.endsWith(ext))) return;
80
+
81
+ // Debounce rapid changes
82
+ const changeKey = fullPath;
83
+ if (this.fileChanges.has(changeKey)) {
84
+ clearTimeout(this.fileChanges.get(changeKey));
85
+ }
86
+
87
+ const timeoutId = setTimeout(() => {
88
+ this.handleFileChange(filename, fullPath);
89
+ this.fileChanges.delete(changeKey);
90
+ }, 100);
91
+
92
+ this.fileChanges.set(changeKey, timeoutId);
93
+ });
94
+
95
+ this.watchers.set(dir, watcher);
96
+ } catch (err) {
97
+ console.error(`[HMR] Failed to watch ${dir}:`, err.message);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Handle file change - determine type and broadcast update
103
+ */
104
+ handleFileChange(filename, fullPath) {
105
+ const ext = nodePath.extname(filename);
106
+ const relativePath = nodePath.relative(process.cwd(), fullPath);
107
+ const normalizedPath = relativePath.replace(/\\/g, '/'); // Normalize Windows paths
108
+
109
+ // Filter out ignored files
110
+ if (normalizedPath.includes('node_modules')) return;
111
+ if (normalizedPath.includes('.test.')) return;
112
+ if (normalizedPath.includes('test/')) return;
113
+
114
+ console.log(`[HMR] File changed: ${normalizedPath}`);
115
+
116
+ // Determine module type and extract clean path
117
+ let moduleType = 'unknown';
118
+ let modulePath = normalizedPath;
119
+
120
+ if (normalizedPath.includes('pages/') && (ext === '.js' || ext === '.html')) {
121
+ moduleType = 'page';
122
+ // Extract just the page name from the path
123
+ const match = normalizedPath.match(/pages\/([^/]+)/);
124
+ modulePath = match ? match[1].replace(ext, '') : normalizedPath;
125
+ } else if (normalizedPath.includes('components/') && ext === '.js') {
126
+ moduleType = 'component';
127
+ // Extract just the component name
128
+ const match = normalizedPath.match(/components\/([^/]+)/);
129
+ modulePath = match ? match[1].replace(ext, '') : normalizedPath;
130
+ } else if (normalizedPath.includes('core/') && ext === '.js') {
131
+ moduleType = 'core';
132
+ // Extract just the module name
133
+ const match = normalizedPath.match(/core\/([^/]+)/);
134
+ modulePath = match ? match[1].replace(ext, '') : normalizedPath;
135
+ } else if (ext === '.css') {
136
+ moduleType = 'style';
137
+ } else if (ext === '.html') {
138
+ moduleType = 'markup';
139
+ }
140
+
141
+ // Broadcast change to all connected clients
142
+ this.broadcast({
143
+ type: 'update',
144
+ moduleType,
145
+ modulePath,
146
+ filename: normalizedPath,
147
+ timestamp: Date.now()
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Broadcast message to all connected clients
153
+ */
154
+ broadcast(message) {
155
+ const payload = JSON.stringify(message);
156
+ let successCount = 0;
157
+
158
+ for (const client of this.clients) {
159
+ try {
160
+ client.send(payload);
161
+ successCount++;
162
+ } catch (err) {
163
+ // Client disconnected, will be cleaned up on next event
164
+ }
165
+ }
166
+
167
+ if (successCount > 0) {
168
+ console.log(`[HMR] Broadcasted update to ${successCount} client(s)`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Stop all watchers and close WebSocket connections
174
+ */
175
+ stop() {
176
+ for (const watcher of this.watchers.values()) {
177
+ try {
178
+ watcher.close();
179
+ } catch (err) {
180
+ // Ignore close errors
181
+ }
182
+ }
183
+ this.watchers.clear();
184
+ this.clients.clear();
185
+ console.log(`[HMR] Server stopped`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Create HMR server instance
191
+ */
192
+ export function createHMRServer(port = 3001) {
193
+ return new HMRServer(port);
194
+ }
195
+
196
+ export default HMRServer;
package/core/html.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * AfriCode HTML Template Engine - TypeScript Definitions
3
+ * JSX-free template literal engine
4
+ */
5
+
6
+ /**
7
+ * HTML template tag function
8
+ * Safely renders template literals as HTML strings
9
+ *
10
+ * Usage:
11
+ * ```ts
12
+ * const name = 'Alice';
13
+ * const result = html`<h1>Hello ${name}</h1>`;
14
+ * ```
15
+ *
16
+ * @param strings - Template string parts
17
+ * @param values - Interpolated values
18
+ * @returns Rendered HTML string
19
+ */
20
+ export function html(
21
+ strings: TemplateStringsArray,
22
+ ...values: any[]
23
+ ): string;
24
+
25
+ /**
26
+ * Render options
27
+ */
28
+ export interface RenderOptions {
29
+ /** Escape HTML special characters */
30
+ escape?: boolean;
31
+ /** Keep falsy values as string */
32
+ keepFalsy?: boolean;
33
+ /** Custom serializer */
34
+ serializer?: (value: any) => string;
35
+ }
36
+
37
+ /**
38
+ * Safe HTML rendering with custom options
39
+ */
40
+ export function renderHtml(
41
+ strings: TemplateStringsArray,
42
+ values: any[],
43
+ options?: RenderOptions
44
+ ): string;
45
+
46
+ /**
47
+ * Layout component props
48
+ */
49
+ export interface LayoutProps {
50
+ /** Use vertical layout (default: horizontal) */
51
+ vertical?: boolean;
52
+ /** Spacing between items (px) */
53
+ gap?: number;
54
+ /** Vertical alignment */
55
+ align?: 'start' | 'center' | 'end' | 'stretch';
56
+ /** Horizontal justification */
57
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around';
58
+ /** Additional CSS classes */
59
+ class?: string;
60
+ /** Inline styles */
61
+ style?: Record<string, string>;
62
+ }
63
+
64
+ /**
65
+ * Layout component class
66
+ */
67
+ export class Layout {
68
+ constructor(props?: LayoutProps);
69
+
70
+ /**
71
+ * Add child element or text
72
+ */
73
+ addChild(child: HTMLElement | string): this;
74
+
75
+ /**
76
+ * Render to HTML element
77
+ */
78
+ render(): HTMLElement;
79
+
80
+ /**
81
+ * Get HTML string representation
82
+ */
83
+ toString(): string;
84
+ }
85
+
86
+ /**
87
+ * Flex layout shorthand
88
+ */
89
+ export function flex(
90
+ options?: LayoutProps
91
+ ): HTMLElement;
92
+
93
+ /**
94
+ * Grid layout shorthand
95
+ */
96
+ export function grid(
97
+ columnCount: number,
98
+ gap?: number
99
+ ): HTMLElement;
100
+
101
+ /**
102
+ * Create safe text node
103
+ */
104
+ export function text(content: string): string;
105
+
106
+ /**
107
+ * Escape HTML entities
108
+ */
109
+ export function escape(str: string): string;
110
+
111
+ /**
112
+ * Unescape HTML entities
113
+ */
114
+ export function unescape(str: string): string;
115
+
116
+ export default html;
package/core/html.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * AfriCode HTML Template Engine
3
+ *
4
+ * Provides the `html` tagged template literal for writing markup in JavaScript.
5
+ * This is a lightweight, zero-compile template system (JSX alternative).
6
+ *
7
+ * @module core/html
8
+ */
9
+
10
+ /**
11
+ * RawHtml wrapper class
12
+ * Marks HTML content as safe and prevents escaping when interpolated
13
+ *
14
+ * @class RawHtml
15
+ * @param {string} content - The raw HTML content
16
+ */
17
+ export class RawHtml {
18
+ constructor(content) {
19
+ this.content = content;
20
+ }
21
+
22
+ toString() {
23
+ return this.content;
24
+ }
25
+
26
+ valueOf() {
27
+ return this.content;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Escape HTML special characters to prevent XSS
33
+ *
34
+ * @param {any} str - Value to escape
35
+ * @returns {string} Escaped string
36
+ */
37
+ function escapeHtml(str) {
38
+ if (typeof str !== 'string') {return str;}
39
+
40
+ const map = {
41
+ '&': '&amp;',
42
+ '<': '&lt;',
43
+ '>': '&gt;',
44
+ '"': '&quot;',
45
+ "'": '&#039;'
46
+ };
47
+
48
+ return str.replace(/[&<>"']/g, char => map[char]);
49
+ }
50
+
51
+ /**
52
+ * Safely interpolate a value into HTML
53
+ * - If RawHtml: insert as raw markup
54
+ * - If array: recursively process and join
55
+ * - If null/undefined/false: render empty string
56
+ * - Otherwise: escape as text
57
+ *
58
+ * @param {any} value - Value to interpolate
59
+ * @returns {string}
60
+ */
61
+ function interpolateValue(value) {
62
+ // RawHtml: insert as raw markup (safe for nested templates)
63
+ if (value instanceof RawHtml) {
64
+ return value.toString();
65
+ }
66
+
67
+ // Arrays: recursively process and join
68
+ if (Array.isArray(value)) {
69
+ return value.map(v => interpolateValue(v)).join('');
70
+ }
71
+
72
+ // Falsy values: render empty string
73
+ if (value === null || value === undefined || value === false) {
74
+ return '';
75
+ }
76
+
77
+ // Functions: convert to string (for component functions, better to call them first)
78
+ if (typeof value === 'function') {
79
+ return escapeHtml(String(value));
80
+ }
81
+
82
+ // Everything else: escape as text
83
+ return escapeHtml(String(value));
84
+ }
85
+
86
+ /**
87
+ * Tagged template literal for HTML
88
+ * Safely handles nested templates, arrays, and text escaping.
89
+ *
90
+ * Returns a RawHtml wrapper so nested templates aren't double-escaped.
91
+ *
92
+ * @param {TemplateStringsArray} strings
93
+ * @param {...any} values
94
+ * @returns {RawHtml}
95
+ *
96
+ * @example
97
+ * // Nested templates (no escaping of markup)
98
+ * const button = html`<af-button>Click</af-button>`;
99
+ * const page = html`<div>${button}</div>`;
100
+ * // Output: <div><af-button>Click</af-button></div>
101
+ *
102
+ * @example
103
+ * // Plain text (escaped for safety)
104
+ * const user = html`<p>${userInput}</p>`;
105
+ * // If userInput = "<script>alert('xss')</script>"
106
+ * // Output: <p>&lt;script&gt;alert('xss')&lt;/script&gt;</p>
107
+ *
108
+ * @example
109
+ * // Arrays of templates
110
+ * const items = [html`<li>Item 1</li>`, html`<li>Item 2</li>`];
111
+ * const list = html`<ul>${items}</ul>`;
112
+ * // Output: <ul><li>Item 1</li><li>Item 2</li></ul>
113
+ */
114
+ export function html(strings, ...values) {
115
+ let result = '';
116
+
117
+ for (let i = 0; i < values.length; i++) {
118
+ result += strings[i];
119
+ result += interpolateValue(values[i]);
120
+ }
121
+
122
+ result += strings[strings.length - 1];
123
+
124
+ // Return as RawHtml so nested html() calls aren't escaped
125
+ return new RawHtml(result);
126
+ }
127
+
128
+ /**
129
+ * Standard Root Layout
130
+ * Similar to Next.js layout.js concept
131
+ *
132
+ * @param {Object} options
133
+ * @param {string} options.title - Page title
134
+ * @param {string} options.meta - Meta tags
135
+ * @param {RawHtml|string} options.children - Page content
136
+ * @returns {RawHtml}
137
+ */
138
+ export function Layout({ title = 'AfriCode App', meta = '', children }) {
139
+ return html`<!DOCTYPE html>
140
+ <html lang="en">
141
+ <head>
142
+ <meta charset="UTF-8">
143
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
+ <title>${title}</title>
145
+ ${meta}
146
+ <link rel="stylesheet" href="/styles/africanity.css">
147
+ <script type="module" src="/core/sdk.js"></script>
148
+ <script type="module">
149
+ import { init } from '/core/sdk.js';
150
+ init();
151
+ </script>
152
+ </head>
153
+ <body>
154
+ ${children}
155
+ </body>
156
+ </html>`;
157
+ }
158
+
159
+
160
+ export default { html, Layout, RawHtml };