@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,155 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+
3
+ /**
4
+ * AfriCode Error Boundary Component
5
+ *
6
+ * Native Web Component implementation of an error boundary.
7
+ * Listens for 'af-component-error' events bubbling up from children.
8
+ * Gracefully displays a localized fallback UI.
9
+ *
10
+ * @module components/error-boundary
11
+ */
12
+ export class AfriErrorBoundary extends AfriCodeComponent {
13
+ static get observedAttributes() {
14
+ return ['theme', 'fallback-message'];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this._hasError = false;
20
+ this._errorDetail = null;
21
+ this.render();
22
+ }
23
+
24
+ connectedCallback() {
25
+ // Listen for internal errors bubbling up from custom AfriCode children
26
+ this.addEventListener('af-component-error', this._handleError.bind(this));
27
+ }
28
+
29
+ disconnectedCallback() {
30
+ this.removeEventListener('af-component-error', this._handleError.bind(this));
31
+ }
32
+
33
+ _handleError(e) {
34
+ // Prevent it from bubbling further up and crashing higher boundaries
35
+ e.stopPropagation();
36
+
37
+ this._hasError = true;
38
+ this._errorDetail = e.detail;
39
+
40
+ console.warn('[AfriCode Boundary] Caught error:', this._errorDetail);
41
+ this.render();
42
+ }
43
+
44
+ reset() {
45
+ this._hasError = false;
46
+ this._errorDetail = null;
47
+ this.render();
48
+ }
49
+
50
+ attributeChangedCallback() {
51
+ this.render();
52
+ }
53
+
54
+ render() {
55
+ const theme = this.getAttribute('theme') || 'tanzania';
56
+ const fallbackMessage = this.getAttribute('fallback-message') || 'A part of this page failed to load.';
57
+
58
+ // Use appropriate semantic warning colors depending on culture theme
59
+ const themes = {
60
+ tanzania: { bg: '#FFF9E6', border: '#FCD116', text: '#5A4A00' },
61
+ maasai: { bg: '#FFEEEE', border: '#FF0000', text: '#8B0000' },
62
+ ndebele: { bg: '#EBF0FF', border: '#4169E1', text: '#002B5C' },
63
+ ocean: { bg: '#E6FAFF', border: '#00A3DD', text: '#004A66' }
64
+ };
65
+
66
+ const t = themes[theme] || themes.tanzania;
67
+
68
+ if (this._hasError) {
69
+ this.shadowRoot.innerHTML = `
70
+ <style>
71
+ :host { display: block; width: 100%; font-family: 'Inter', system-ui, sans-serif; }
72
+ .boundary-error {
73
+ background: ${t.bg};
74
+ border-left: 4px solid ${t.border};
75
+ color: ${t.text};
76
+ padding: 16px 20px;
77
+ border-radius: 4px;
78
+ margin: 12px 0;
79
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
80
+ }
81
+ .boundary-header {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 12px;
85
+ font-weight: 600;
86
+ margin-bottom: 8px;
87
+ font-size: 16px;
88
+ }
89
+ .boundary-message {
90
+ font-size: 14px;
91
+ opacity: 0.9;
92
+ margin-bottom: 12px;
93
+ }
94
+ .boundary-dev-detail {
95
+ font-family: monospace;
96
+ font-size: 12px;
97
+ background: rgba(0,0,0,0.05);
98
+ padding: 8px;
99
+ border-radius: 4px;
100
+ overflow-x: auto;
101
+ white-space: pre-wrap;
102
+ }
103
+ .boundary-action {
104
+ margin-top: 12px;
105
+ }
106
+ button {
107
+ background: transparent;
108
+ border: 1px solid ${t.border};
109
+ color: ${t.text};
110
+ padding: 6px 12px;
111
+ border-radius: 4px;
112
+ cursor: pointer;
113
+ font-weight: 500;
114
+ transition: background 0.2s;
115
+ }
116
+ button:hover {
117
+ background: rgba(0,0,0,0.05);
118
+ }
119
+ </style>
120
+ <div class="boundary-error" role="alert">
121
+ <div class="boundary-header">
122
+ <span aria-hidden="true">🔄</span>
123
+ <span>Component Recovery</span>
124
+ </div>
125
+ <div class="boundary-message">${fallbackMessage}</div>
126
+
127
+ ${this._errorDetail ? `
128
+ <div class="boundary-dev-detail">
129
+ <code>&lt;${this._errorDetail.component}&gt;: ${this._errorDetail.message}</code>
130
+ </div>
131
+ ` : ''}
132
+
133
+ <div class="boundary-action">
134
+ <button id="retry-btn">Try Again</button>
135
+ </div>
136
+ </div>
137
+ `;
138
+
139
+ // Re-bind retry logic
140
+ this.shadowRoot.getElementById('retry-btn').addEventListener('click', () => {
141
+ this.reset();
142
+ });
143
+ return;
144
+ }
145
+
146
+ // If no error, just passthrough the light DOM via slots
147
+ this.shadowRoot.innerHTML = `
148
+ <style>:host { display: block; }</style>
149
+ <slot></slot>
150
+ `;
151
+ }
152
+ }
153
+
154
+ registerComponent('af-error-boundary', AfriErrorBoundary);
155
+ export default AfriErrorBoundary;
@@ -0,0 +1,131 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+ import { Validation, schemas } from '../core/validation.js';
3
+
4
+ /**
5
+ * Smart Form Component
6
+ * Handles submission, loading state, and validation.
7
+ *
8
+ * Usage:
9
+ * <af-form action="/api/login" method="POST">
10
+ * <af-input name="email"></af-input>
11
+ * </af-form>
12
+ */
13
+ export class AfriForm extends AfriCodeComponent {
14
+ static get observedAttributes() { return ['action', 'method', 'error', 'success', 'schema']; }
15
+
16
+ constructor() {
17
+ super();
18
+ this.handleSubmit = this.handleSubmit.bind(this);
19
+ }
20
+
21
+ connectedCallback() {
22
+ this.render();
23
+ this.shadowRoot.querySelector('form').addEventListener('submit', this.handleSubmit);
24
+ this.loadStyles();
25
+ }
26
+
27
+ async handleSubmit(e) {
28
+ e.preventDefault();
29
+ const form = e.target;
30
+ const submitBtn = this.querySelector('[type="submit"]') || this.shadowRoot.querySelector('button[type="submit"]');
31
+
32
+ // Get validation schema
33
+ const schemaName = this.getAttribute('schema');
34
+ const schema = schemaName ? this._getSchema(schemaName) : null;
35
+
36
+ // derived from attributes
37
+ const action = this.getAttribute('action');
38
+ const method = this.getAttribute('method') || 'GET';
39
+
40
+ // Collect form data from native controls
41
+ const formData = new FormData(form);
42
+ const data = Object.fromEntries(formData.entries());
43
+
44
+ // Include custom af-input values that live in shadow DOM
45
+ Array.from(this.querySelectorAll('af-input')).forEach((customInput) => {
46
+ const name = customInput.getAttribute('name');
47
+ if (name) {
48
+ data[name] = customInput.value;
49
+ }
50
+ });
51
+
52
+ // Validate if schema provided
53
+ if (schema) {
54
+ const validationResult = Validation.validate(schema, data);
55
+ if (!validationResult.success) {
56
+ this._showValidationErrors(validationResult.errors);
57
+ this.emit('af-validation-error', { errors: validationResult.errors });
58
+ return;
59
+ }
60
+ }
61
+
62
+ if (!action) {return;}
63
+
64
+ // Loading State
65
+ if (submitBtn) {
66
+ var originalText = submitBtn.textContent;
67
+ submitBtn.textContent = 'Wait...';
68
+ submitBtn.disabled = true;
69
+ }
70
+
71
+ try {
72
+ const response = await fetch(action, {
73
+ method: method,
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify(data)
76
+ });
77
+
78
+ if (response.ok) {
79
+ this.emit('success', await response.json());
80
+ this.setAttribute('success', 'Form submitted successfully!');
81
+ this.removeAttribute('error');
82
+ } else {
83
+ const errorData = await response.text();
84
+ this.emit('error', errorData);
85
+ this.setAttribute('error', errorData);
86
+ this.removeAttribute('success');
87
+ }
88
+ } catch (err) {
89
+ this.emit('error', err.message);
90
+ this.setAttribute('error', err.message);
91
+ this.removeAttribute('success');
92
+ } finally {
93
+ if (submitBtn) {
94
+ submitBtn.textContent = originalText;
95
+ submitBtn.disabled = false;
96
+ }
97
+ }
98
+ }
99
+
100
+ _getSchema(schemaName) {
101
+ return schemas[schemaName] || null;
102
+ }
103
+
104
+ _showValidationErrors(errors) {
105
+ // Clear previous errors
106
+ this.shadowRoot.querySelectorAll('.field-error').forEach(el => el.remove());
107
+
108
+ // Show errors on corresponding inputs
109
+ Object.entries(errors).forEach(([fieldName, errorMessage]) => {
110
+ const input = this.querySelector(`[name="${fieldName}"]`);
111
+ if (input) {
112
+ // For custom af-input components
113
+ if (input.tagName === 'AF-INPUT') {
114
+ input.setAttribute('error', errorMessage);
115
+ } else {
116
+ // For regular inputs, add error display
117
+ const errorDiv = document.createElement('div');
118
+ errorDiv.className = 'field-error';
119
+ errorDiv.textContent = errorMessage;
120
+ errorDiv.style.color = 'red';
121
+ errorDiv.style.fontSize = '12px';
122
+ errorDiv.style.marginTop = '4px';
123
+ input.parentNode.insertBefore(errorDiv, input.nextSibling);
124
+ }
125
+ }
126
+ });
127
+ }
128
+ }
129
+
130
+ registerComponent('af-form', AfriForm);
131
+ export default AfriForm;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * AfriCode Grid Layout — <af-grid>
3
+ *
4
+ * A fully-featured, reactive responsive grid system.
5
+ *
6
+ * Attributes:
7
+ * min-width — Minimum column width before wrapping (default: 280px)
8
+ * gap — Grid gap spacing (default: 24px)
9
+ * columns — Fixed column count (overrides auto-fit / min-width)
10
+ * rows — Fixed row count for explicit grid
11
+ * layout — Preset layouts: 'auto' | 'masonry' | 'sidebar' | 'holy-grail' | 'bento'
12
+ * align — Align items: 'start' | 'center' | 'end' | 'stretch' (default: stretch)
13
+ * justify — Justify items: 'start' | 'center' | 'end' | 'stretch' (default: stretch)
14
+ * dense — Enable grid-auto-flow: dense (boolean attribute)
15
+ * sidebar-side — 'left' | 'right' for sidebar layout (default: left)
16
+ * sidebar-width — Width of the sidebar column (default: 280px)
17
+ *
18
+ * Events:
19
+ * af-grid-resize — Fired when grid reflows due to container resize
20
+ *
21
+ * Examples:
22
+ * <af-grid min-width="300px" gap="32px">...</af-grid>
23
+ * <af-grid columns="3" gap="16px">...</af-grid>
24
+ * <af-grid layout="sidebar" sidebar-width="240px">...</af-grid>
25
+ * <af-grid layout="bento" gap="16px">...</af-grid>
26
+ */
27
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
28
+
29
+ // Preset layout generators
30
+ function getLayoutTemplate(layout, attrs) {
31
+ const {
32
+ sidebarSide = 'left',
33
+ sidebarWidth = '280px',
34
+ minWidth = '280px',
35
+ gap = '24px',
36
+ columns,
37
+ rows,
38
+ align = 'stretch',
39
+ justify = 'stretch',
40
+ dense = false,
41
+ } = attrs;
42
+
43
+ const denseVal = dense ? 'dense' : '';
44
+ const base = `
45
+ align-items: ${align};
46
+ justify-items: ${justify};
47
+ gap: ${gap};
48
+ `;
49
+
50
+ switch (layout) {
51
+ case 'sidebar':
52
+ return sidebarSide === 'right'
53
+ ? `display: grid; grid-template-columns: 1fr ${sidebarWidth}; ${base}`
54
+ : `display: grid; grid-template-columns: ${sidebarWidth} 1fr; ${base}`;
55
+
56
+ case 'holy-grail':
57
+ return `
58
+ display: grid;
59
+ grid-template-columns: ${sidebarWidth} 1fr ${sidebarWidth};
60
+ grid-template-rows: auto 1fr auto;
61
+ ${base}
62
+ `;
63
+
64
+ case 'bento':
65
+ // Bento grid: 12-column base, children use grid-column/row spans
66
+ return `
67
+ display: grid;
68
+ grid-template-columns: repeat(12, 1fr);
69
+ grid-auto-rows: minmax(120px, auto);
70
+ grid-auto-flow: row ${denseVal};
71
+ ${base}
72
+ `;
73
+
74
+ case 'masonry':
75
+ // CSS-native masonry (progressive enhancement)
76
+ return `
77
+ display: grid;
78
+ grid-template-columns: repeat(auto-fill, minmax(${minWidth}, 1fr));
79
+ grid-template-rows: masonry;
80
+ grid-auto-flow: row ${denseVal};
81
+ ${base}
82
+ `;
83
+
84
+ case 'auto':
85
+ default: {
86
+ const colValue = columns
87
+ ? `repeat(${columns}, 1fr)`
88
+ : `repeat(auto-fit, minmax(${minWidth}, 1fr))`;
89
+ const rowValue = rows ? `repeat(${rows}, 1fr)` : 'auto';
90
+ return `
91
+ display: grid;
92
+ grid-template-columns: ${colValue};
93
+ ${rows ? `grid-template-rows: ${rowValue};` : ''}
94
+ grid-auto-flow: row ${denseVal};
95
+ ${base}
96
+ `;
97
+ }
98
+ }
99
+ }
100
+
101
+ export class AfriGrid extends AfriCodeComponent {
102
+ static get observedAttributes() {
103
+ return [
104
+ 'min-width',
105
+ 'gap',
106
+ 'columns',
107
+ 'rows',
108
+ 'layout',
109
+ 'align',
110
+ 'justify',
111
+ 'dense',
112
+ 'sidebar-side',
113
+ 'sidebar-width',
114
+ ];
115
+ }
116
+
117
+ attributeChangedCallback() {
118
+ if (this.isConnected) {
119
+ this.render();
120
+ }
121
+ }
122
+
123
+ _getAttrs() {
124
+ return {
125
+ minWidth: this.getAttribute('min-width') || '280px',
126
+ gap: this.getAttribute('gap') || '24px',
127
+ columns: this.getAttribute('columns') || null,
128
+ rows: this.getAttribute('rows') || null,
129
+ layout: this.getAttribute('layout') || 'auto',
130
+ align: this.getAttribute('align') || 'stretch',
131
+ justify: this.getAttribute('justify') || 'stretch',
132
+ dense: this.hasAttribute('dense'),
133
+ sidebarSide: this.getAttribute('sidebar-side') || 'left',
134
+ sidebarWidth: this.getAttribute('sidebar-width') || '280px',
135
+ };
136
+ }
137
+
138
+ _buildResponsiveBreakpoints(attrs) {
139
+ // Only apply responsive fallback to auto/masonry layouts
140
+ if (attrs.layout !== 'auto' && attrs.layout !== 'masonry') {
141
+ return '';
142
+ }
143
+
144
+ return `
145
+ @media (max-width: 768px) {
146
+ .grid {
147
+ grid-template-columns: repeat(auto-fit, minmax(min(${attrs.minWidth}, 100%), 1fr));
148
+ }
149
+ }
150
+ @media (max-width: 480px) {
151
+ .grid {
152
+ grid-template-columns: 1fr;
153
+ }
154
+ }
155
+ `;
156
+ }
157
+
158
+ _buildSidebarResponsive(attrs) {
159
+ if (attrs.layout !== 'sidebar' && attrs.layout !== 'holy-grail') {
160
+ return '';
161
+ }
162
+ return `
163
+ @media (max-width: 768px) {
164
+ .grid {
165
+ grid-template-columns: 1fr;
166
+ }
167
+ }
168
+ `;
169
+ }
170
+
171
+ render() {
172
+ const attrs = this._getAttrs();
173
+ const gridStyles = getLayoutTemplate(attrs.layout, attrs);
174
+ const responsiveStyles =
175
+ this._buildResponsiveBreakpoints(attrs) + this._buildSidebarResponsive(attrs);
176
+
177
+ this.shadowRoot.innerHTML = html`
178
+ <style>
179
+ :host {
180
+ display: block;
181
+ width: 100%;
182
+ box-sizing: border-box;
183
+ }
184
+
185
+ .grid {
186
+ ${gridStyles}
187
+ width: 100%;
188
+ box-sizing: border-box;
189
+ }
190
+
191
+ /* Slotted children base styles */
192
+ ::slotted(*) {
193
+ min-width: 0; /* Prevent overflow in grid context */
194
+ }
195
+
196
+ ${responsiveStyles}
197
+ </style>
198
+
199
+ <div
200
+ class="grid"
201
+ role="list"
202
+ part="grid"
203
+ aria-label="${this.getAttribute('aria-label') || 'Grid layout'}"
204
+ >
205
+ <slot></slot>
206
+ </div>
207
+ `;
208
+ }
209
+
210
+ connectedCallback() {
211
+ this.render();
212
+ this._setupResizeObserver();
213
+ }
214
+
215
+ disconnectedCallback() {
216
+ if (this._resizeObserver) {
217
+ this._resizeObserver.disconnect();
218
+ }
219
+ }
220
+
221
+ _setupResizeObserver() {
222
+ if (typeof ResizeObserver === 'undefined') {
223
+ return;
224
+ }
225
+
226
+ let lastWidth = this.offsetWidth;
227
+ this._resizeObserver = new ResizeObserver((entries) => {
228
+ for (const entry of entries) {
229
+ const newWidth = entry.contentRect.width;
230
+ if (Math.abs(newWidth - lastWidth) > 10) {
231
+ lastWidth = newWidth;
232
+ this.emit('af-grid-resize', {
233
+ width: newWidth,
234
+ height: entry.contentRect.height,
235
+ layout: this.getAttribute('layout') || 'auto',
236
+ });
237
+ }
238
+ }
239
+ });
240
+
241
+ this._resizeObserver.observe(this);
242
+ }
243
+
244
+ /**
245
+ * Public API: programmatically update grid layout
246
+ * @param {string} layout - New layout preset
247
+ */
248
+ setLayout(layout) {
249
+ this.setAttribute('layout', layout);
250
+ }
251
+
252
+ /**
253
+ * Public API: programmatically set column count
254
+ * @param {number} count - Number of columns
255
+ */
256
+ setColumns(count) {
257
+ this.setAttribute('columns', String(count));
258
+ }
259
+
260
+ /**
261
+ * Public API: toggle dense packing
262
+ * @param {boolean} enabled
263
+ */
264
+ setDense(enabled) {
265
+ if (enabled) {
266
+ this.setAttribute('dense', '');
267
+ } else {
268
+ this.removeAttribute('dense');
269
+ }
270
+ }
271
+ }
272
+
273
+ registerComponent('af-grid', AfriGrid);
@@ -0,0 +1,138 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+ import { html } from '../core/html.js';
3
+ import { store } from '../core/store.js';
4
+ import { subscribe } from '../core/state.js';
5
+
6
+ /**
7
+ * AfriHero Component
8
+ *
9
+ * High-impact hero section with support for patterns and rhythmic layout.
10
+ *
11
+ * @example
12
+ * <af-hero
13
+ * title="AfriCode"
14
+ * subtitle="The Rhythmic Web Framework"
15
+ * pattern="kente">
16
+ * </af-hero>
17
+ */
18
+ export class AfriHero extends AfriCodeComponent {
19
+ static get observedAttributes() { return ['title', 'subtitle', 'pattern', 'overlay']; }
20
+
21
+ connectedCallback() {
22
+ super.connectedCallback();
23
+ this.render();
24
+ this.loadStyles();
25
+ }
26
+
27
+ render() {
28
+ const title = this.getAttribute('title') || 'AfriCode';
29
+ const subtitle = this.getAttribute('subtitle') || '';
30
+ const pattern = this.getAttribute('pattern') || '';
31
+ const variant = this.getAttribute('variant') || 'default';
32
+
33
+ this.shadowRoot.innerHTML = `
34
+ <style>
35
+ :host {
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ justify-content: center;
40
+ text-align: center;
41
+ padding: 120px 5%;
42
+ min-height: 60vh;
43
+ background: var(--bg-base, #020617);
44
+ color: var(--text-main, white);
45
+ position: relative;
46
+ overflow: hidden;
47
+ }
48
+
49
+ .content {
50
+ position: relative;
51
+ z-index: 10;
52
+ max-width: 900px;
53
+ opacity: 0;
54
+ transform: translateY(20px);
55
+ animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
56
+ }
57
+
58
+ @keyframes fadeInUp {
59
+ to { opacity: 1; transform: translateY(0); }
60
+ }
61
+
62
+ h1 {
63
+ font-family: 'Outfit', 'Space Grotesk', sans-serif;
64
+ font-size: clamp(3rem, 8vw, 5rem);
65
+ font-weight: 900;
66
+ line-height: 1.1;
67
+ margin-bottom: 24px;
68
+ letter-spacing: -0.04em;
69
+ background: linear-gradient(135deg, var(--text-main, #fff) 30%, var(--text-muted, rgba(255,255,255,0.5)) 100%);
70
+ -webkit-background-clip: text;
71
+ -webkit-text-fill-color: transparent;
72
+ }
73
+
74
+ p {
75
+ font-family: 'Inter', sans-serif;
76
+ font-size: clamp(1.1rem, 2vw, 1.5rem);
77
+ color: var(--text-secondary, #94a3b8);
78
+ line-height: 1.5;
79
+ margin-bottom: 40px;
80
+ max-width: 700px;
81
+ margin-left: auto;
82
+ margin-right: auto;
83
+ }
84
+
85
+ /* Visual Accents */
86
+ .aura {
87
+ position: absolute;
88
+ inset: 0;
89
+ background: radial-gradient(circle at 50% -20%, rgba(30,181,58,0.15), transparent 70%);
90
+ z-index: 1;
91
+ }
92
+
93
+ .mesh {
94
+ position: absolute;
95
+ inset: 0;
96
+ background-image: radial-gradient(rgba(255,255,255,0.03) 1px, transparent 1px);
97
+ background-size: 40px 40px;
98
+ z-index: 2;
99
+ }
100
+
101
+ ${pattern ? `
102
+ .pattern-bg {
103
+ position: absolute;
104
+ inset: 0;
105
+ background-image: url('${pattern}');
106
+ background-size: cover;
107
+ background-position: center;
108
+ opacity: 0.15;
109
+ z-index: 1;
110
+ }
111
+ ` : ''}
112
+
113
+ ::slotted([slot="actions"]) {
114
+ display: flex;
115
+ gap: 16px;
116
+ justify-content: center;
117
+ flex-wrap: wrap;
118
+ }
119
+ </style>
120
+
121
+ <div class="aura"></div>
122
+ <div class="mesh"></div>
123
+ ${pattern ? `<div class="pattern-bg"></div>` : ''}
124
+
125
+ <div class="content">
126
+ <h1>${title}</h1>
127
+ ${subtitle ? `<p>${subtitle}</p>` : ''}
128
+ <div class="actions">
129
+ <slot name="actions"></slot>
130
+ </div>
131
+ <slot></slot>
132
+ </div>
133
+ `;
134
+ }
135
+ }
136
+
137
+ registerComponent('af-hero', AfriHero);
138
+ export default AfriHero;