@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,80 @@
1
+ /**
2
+ * AfriCode Loading Spinner Component
3
+ *
4
+ * Cultural loading indicators using spinning Adinkra symbols or geometric rotation.
5
+ *
6
+ * @module components/loader
7
+ */
8
+
9
+ import { AfriCodeComponent, registerComponent } from './base.js';
10
+
11
+ export class AfriLoader extends AfriCodeComponent {
12
+ static get observedAttributes() {
13
+ return ['size', 'color', 'type'];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this.render();
19
+ }
20
+
21
+ attributeChangedCallback() {
22
+ this.render();
23
+ }
24
+
25
+ render() {
26
+ const size = this.getAttribute('size') || '40';
27
+ const color = this.getAttribute('color') || 'currentColor';
28
+ const type = this.getAttribute('type') || 'spiral';
29
+
30
+ const paths = {
31
+ // African spiral (growth/life)
32
+ spiral: `M25,25 m0,-20 a20,20 0 1,1 0,40 a20,20 0 1,1 0,-40
33
+ M25,25 m0,-15 a15,15 0 1,0 0,30 a15,15 0 1,0 0,-30`,
34
+ // Geometric sun/star
35
+ sun: `M25,5 L25,45 M5,25 L45,25 M11,11 L39,39 M39,11 L11,39`,
36
+ // Diamond rotation
37
+ diamond: `M25,5 L45,25 L25,45 L5,25 Z`
38
+ };
39
+
40
+ this.shadowRoot.innerHTML = `
41
+ <style>
42
+ :host {
43
+ display: inline-block;
44
+ color: ${color};
45
+ }
46
+ .loader {
47
+ width: ${size}px;
48
+ height: ${size}px;
49
+ animation: spin 1.5s linear infinite;
50
+ }
51
+ svg {
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+ @keyframes spin {
56
+ 100% { transform: rotate(360deg); }
57
+ }
58
+ circle {
59
+ opacity: 0.25;
60
+ }
61
+ path, line, rect {
62
+ opacity: 0.75;
63
+ }
64
+ </style>
65
+ <div class="loader">
66
+ <svg viewBox="0 0 50 50">
67
+ ${type === 'spiral'
68
+ ? `<path d="${paths.spiral}" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" />`
69
+ : type === 'sun'
70
+ ? `<path d="${paths.sun}" stroke="currentColor" stroke-width="4" stroke-linecap="round" />`
71
+ : `<path d="${paths.diamond}" fill="none" stroke="currentColor" stroke-width="4" />`
72
+ }
73
+ </svg>
74
+ </div>
75
+ `;
76
+ }
77
+ }
78
+
79
+ registerComponent('af-loader', AfriLoader);
80
+ export default AfriLoader;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * AfriCode Modal/Dialog Component
3
+ *
4
+ * Accessible modal dialog with backdrop and animations.
5
+ * Suitable for: Confirmations, Forms, Details view, Alerts
6
+ *
7
+ * @module components/modal
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+
12
+ export class AfriModal extends AfriCodeComponent {
13
+ static get observedAttributes() {
14
+ return ['open', 'theme', 'size', 'aria-label', 'aria-labelledby'];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this.render();
20
+ }
21
+
22
+ connectedCallback() {
23
+ this._setupListeners();
24
+ }
25
+
26
+ _setupListeners() {
27
+ // Close on backdrop click
28
+ this.shadowRoot.querySelector('.backdrop')?.addEventListener('click', (e) => {
29
+ if (e.target.classList.contains('backdrop')) {
30
+ this.close();
31
+ }
32
+ });
33
+
34
+ // Close button
35
+ this.shadowRoot.querySelector('.close-btn')?.addEventListener('click', () => {
36
+ this.close();
37
+ });
38
+
39
+ // ESC key
40
+ document.addEventListener('keydown', (e) => {
41
+ if (e.key === 'Escape' && this.hasAttribute('open')) {
42
+ this.close();
43
+ }
44
+ });
45
+
46
+ // Focus trapping
47
+ this.shadowRoot.addEventListener('keydown', (e) => {
48
+ if (e.key === 'Tab' && this.hasAttribute('open')) {
49
+ this._handleTabKey(e);
50
+ }
51
+ });
52
+ }
53
+
54
+ _getFocusableElements() {
55
+ const modal = this.shadowRoot.querySelector('.modal');
56
+ const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
57
+
58
+ // Modal internals
59
+ const shadowFocusables = Array.from(modal?.querySelectorAll(focusableSelectors) || []);
60
+
61
+ // Slotted contents
62
+ const lightFocusables = Array.from(this.querySelectorAll(focusableSelectors));
63
+
64
+ return [...shadowFocusables, ...lightFocusables];
65
+ }
66
+
67
+ _focusFirstFocusableElement() {
68
+ const focusableElements = this._getFocusableElements();
69
+ if (focusableElements.length > 0) {
70
+ focusableElements[0].focus();
71
+ } else {
72
+ this.shadowRoot.querySelector('.close-btn')?.focus();
73
+ }
74
+ }
75
+
76
+ _handleTabKey(e) {
77
+ const focusableElements = this._getFocusableElements();
78
+ if (focusableElements.length === 0) {
79
+ e.preventDefault();
80
+ return;
81
+ }
82
+
83
+ const firstElement = focusableElements[0];
84
+ const lastElement = focusableElements[focusableElements.length - 1];
85
+
86
+ if (e.shiftKey) {
87
+ // Shift + Tab
88
+ if (document.activeElement === firstElement) {
89
+ e.preventDefault();
90
+ lastElement.focus();
91
+ }
92
+ } else {
93
+ // Tab
94
+ if (document.activeElement === lastElement) {
95
+ e.preventDefault();
96
+ firstElement.focus();
97
+ }
98
+ }
99
+ }
100
+
101
+ open() {
102
+ this.setAttribute('open', '');
103
+ document.body.style.overflow = 'hidden';
104
+
105
+ // Focus management
106
+ this._previousFocus = document.activeElement;
107
+ this._focusFirstFocusableElement();
108
+
109
+ this.emit('af-open');
110
+ }
111
+
112
+ close() {
113
+ this.removeAttribute('open');
114
+ document.body.style.overflow = '';
115
+
116
+ // Restore focus
117
+ if (this._previousFocus && this._previousFocus.focus) {
118
+ this._previousFocus.focus();
119
+ }
120
+
121
+ this.emit('af-close');
122
+ }
123
+
124
+ attributeChangedCallback(name, oldVal, newVal) {
125
+ this.render();
126
+ if (name === 'open') {
127
+ this._setupListeners();
128
+ }
129
+ }
130
+
131
+ render() {
132
+ const isOpen = this.hasAttribute('open');
133
+ const theme = this.getAttribute('theme') || 'tanzania';
134
+ const size = this.getAttribute('size') || 'md';
135
+
136
+ const themes = {
137
+ tanzania: { accent: '#1EB53A' },
138
+ maasai: { accent: '#FF0000' },
139
+ ndebele: { accent: '#4169E1' }
140
+ };
141
+ const t = themes[theme] || themes.tanzania;
142
+
143
+ const sizes = {
144
+ sm: '400px',
145
+ md: '560px',
146
+ lg: '720px',
147
+ full: '95vw'
148
+ };
149
+
150
+ const ariaLabel = this.getAttribute('aria-label');
151
+ const ariaLabelledBy = this.getAttribute('aria-labelledby') || 'modal-title';
152
+
153
+ this.shadowRoot.innerHTML = `
154
+ <style>
155
+ :host {
156
+ font-family: 'Inter', system-ui, sans-serif;
157
+ }
158
+
159
+ .backdrop {
160
+ position: fixed;
161
+ inset: 0;
162
+ background: rgba(0, 0, 0, 0.5);
163
+ display: ${isOpen ? 'flex' : 'none'};
164
+ align-items: center;
165
+ justify-content: center;
166
+ z-index: 10000;
167
+ animation: fadeIn 200ms ease-out;
168
+ padding: 21px;
169
+ }
170
+
171
+ @keyframes fadeIn {
172
+ from { opacity: 0; }
173
+ to { opacity: 1; }
174
+ }
175
+
176
+ .modal {
177
+ background: white;
178
+ border-radius: 8px;
179
+ width: 100%;
180
+ max-width: ${sizes[size] || sizes.md};
181
+ max-height: 90vh;
182
+ overflow: hidden;
183
+ display: flex;
184
+ flex-direction: column;
185
+ animation: slideUp 300ms ease-out;
186
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
187
+ }
188
+
189
+ @keyframes slideUp {
190
+ from {
191
+ opacity: 0;
192
+ transform: translateY(20px);
193
+ }
194
+ to {
195
+ opacity: 1;
196
+ transform: translateY(0);
197
+ }
198
+ }
199
+
200
+ .modal-header {
201
+ padding: 13px 21px;
202
+ border-bottom: 1px solid #eee;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: space-between;
206
+ }
207
+
208
+ .modal-header ::slotted(*) {
209
+ margin: 0;
210
+ font-size: 18px;
211
+ font-weight: 600;
212
+ }
213
+
214
+ .close-btn {
215
+ background: none;
216
+ border: none;
217
+ font-size: 24px;
218
+ cursor: pointer;
219
+ color: #666;
220
+ padding: 0;
221
+ line-height: 1;
222
+ }
223
+
224
+ .close-btn:hover {
225
+ color: ${t.accent};
226
+ }
227
+
228
+ .modal-body {
229
+ padding: 21px;
230
+ overflow-y: auto;
231
+ flex: 1;
232
+ }
233
+
234
+ .modal-footer {
235
+ padding: 13px 21px;
236
+ border-top: 1px solid #eee;
237
+ display: flex;
238
+ gap: 8px;
239
+ justify-content: flex-end;
240
+ }
241
+ </style>
242
+
243
+ <div class="backdrop">
244
+ <div class="modal" role="dialog" aria-modal="true" ${ariaLabel ? `aria-label="${ariaLabel}"` : `aria-labelledby="${ariaLabelledBy}"`}>
245
+ <div class="modal-header">
246
+ <slot name="header" id="modal-title"></slot>
247
+ <button class="close-btn" aria-label="Close modal">×</button>
248
+ </div>
249
+ <div class="modal-body">
250
+ <slot></slot>
251
+ </div>
252
+ <div class="modal-footer">
253
+ <slot name="footer"></slot>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ `;
258
+ }
259
+ }
260
+
261
+ registerComponent('af-modal', AfriModal);
262
+ export default AfriModal;
@@ -0,0 +1,84 @@
1
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
2
+ import { animate } from 'framer-motion/dom';
3
+
4
+ /**
5
+ * af-motion
6
+ *
7
+ * A wrapper component that bridges the gap between Web Components
8
+ * and Framer Motion's vanilla JS animation engine.
9
+ *
10
+ * Usage:
11
+ * <af-motion animate='{"y": [20, 0], "opacity": [0, 1]}'>
12
+ * <div class="content">...</div>
13
+ * </af-motion>
14
+ */
15
+ class MotionComponent extends AfriCodeComponent {
16
+ static get observedAttributes() {
17
+ return ['animate', 'transition'];
18
+ }
19
+
20
+ connectedCallback() {
21
+ this.loadStyles();
22
+ this.render();
23
+
24
+ // Observe slot changes to trigger animations on slotted content
25
+ const slot = this.shadowRoot.querySelector('slot');
26
+ if (slot) {
27
+ slot.addEventListener('slotchange', () => this._runAnimation());
28
+ }
29
+ }
30
+
31
+ attributeChangedCallback() {
32
+ if (this.shadowRoot) {
33
+ this._runAnimation();
34
+ }
35
+ }
36
+
37
+ _runAnimation() {
38
+ const slot = this.shadowRoot.querySelector('slot');
39
+ if (!slot) return;
40
+
41
+ const elements = slot.assignedElements();
42
+ if (!elements.length) return;
43
+
44
+ const animateAttr = this.getAttribute('animate');
45
+ const transitionAttr = this.getAttribute('transition');
46
+
47
+ if (animateAttr) {
48
+ try {
49
+ // e.g. {"x": 100, "opacity": 1}
50
+ const keyframes = JSON.parse(animateAttr);
51
+
52
+ // e.g. {"type": "spring", "stiffness": 100}
53
+ const options = transitionAttr ? JSON.parse(transitionAttr) : {
54
+ type: "spring",
55
+ stiffness: 100,
56
+ damping: 15,
57
+ mass: 1
58
+ };
59
+
60
+ // Animate all assigned children
61
+ elements.forEach(el => {
62
+ animate(el, keyframes, options);
63
+ });
64
+ } catch (e) {
65
+ console.error('[AfriCode] <af-motion> invalid JSON payload:', e);
66
+ }
67
+ }
68
+ }
69
+
70
+ render() {
71
+ this.shadowRoot.innerHTML = html`
72
+ <style>
73
+ :host {
74
+ display: block; /* Requires block context for layout stability */
75
+ width: 100%;
76
+ }
77
+ </style>
78
+ <slot></slot>
79
+ `;
80
+ }
81
+ }
82
+
83
+ registerComponent('af-motion', MotionComponent);
84
+ export default MotionComponent;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * AfriCode Navigation Bar Component
3
+ *
4
+ * Responsive navigation with logo, links, and mobile menu.
5
+ * Suitable for: All websites and web applications
6
+ *
7
+ * @module components/navbar
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+ import { store } from '../core/store.js';
12
+ import { subscribe } from '../core/state.js';
13
+
14
+ export class AfriNavbar extends AfriCodeComponent {
15
+ static get observedAttributes() {
16
+ return ['theme', 'logo', 'sticky'];
17
+ }
18
+
19
+ constructor() {
20
+ super();
21
+ this._isOpen = false;
22
+ this._unsubscribe = null;
23
+ this.render();
24
+ }
25
+
26
+ connectedCallback() {
27
+ super.connectedCallback();
28
+
29
+ // Mobile toggle
30
+ this.shadowRoot.querySelector('.menu-toggle')?.addEventListener('click', () => {
31
+ this._isOpen = !this._isOpen;
32
+ const navLinks = this.shadowRoot.querySelector('.nav-links');
33
+ const toggle = this.shadowRoot.querySelector('.menu-toggle');
34
+ if (navLinks) {navLinks.classList.toggle('open', this._isOpen);}
35
+ if (toggle) {
36
+ toggle.classList.toggle('open', this._isOpen);
37
+ toggle.setAttribute('aria-expanded', String(this._isOpen));
38
+ }
39
+ });
40
+ }
41
+
42
+ disconnectedCallback() {
43
+ if (this._unsubscribe) {this._unsubscribe();}
44
+ }
45
+
46
+ attributeChangedCallback() {
47
+ this.render();
48
+ }
49
+
50
+ render() {
51
+ const theme = this.getAttribute('theme') || 'dark';
52
+ const logo = this.getAttribute('logo') || 'AfriCode';
53
+ const sticky = this.hasAttribute('sticky');
54
+
55
+ this.shadowRoot.innerHTML = `
56
+ <style>
57
+ :host {
58
+ display: block;
59
+ --nav-height: 80px;
60
+ }
61
+
62
+ nav {
63
+ height: var(--nav-height);
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ padding-inline: 5%;
68
+ background: var(--glass-bg);
69
+ backdrop-filter: blur(12px);
70
+ -webkit-backdrop-filter: blur(12px);
71
+ border-bottom: 1px solid var(--glass-border);
72
+ position: fixed;
73
+ top: 0;
74
+ inset-inline: 0;
75
+ z-index: 1000;
76
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
77
+ }
78
+
79
+ .logo-container {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ text-decoration: none;
84
+ z-index: 1002;
85
+ }
86
+
87
+ .logo-mark {
88
+ width: 32px;
89
+ height: 32px;
90
+ background: linear-gradient(135deg, var(--afri-green, #1eb53a), var(--afri-gold, #fcd116));
91
+ border-radius: 8px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ font-weight: 900;
96
+ color: black;
97
+ font-size: 1rem;
98
+ }
99
+
100
+ .logo-text {
101
+ font-family: 'Outfit', 'Space Grotesk', sans-serif;
102
+ font-size: 1.5rem;
103
+ font-weight: 800;
104
+ color: var(--text-primary);
105
+ letter-spacing: -0.02em;
106
+ }
107
+
108
+ .nav-links {
109
+ display: flex;
110
+ gap: 32px;
111
+ align-items: center;
112
+ list-style: none;
113
+ margin: 0;
114
+ padding: 0;
115
+ }
116
+
117
+ .nav-links ::slotted(a) {
118
+ color: var(--text-muted);
119
+ text-decoration: none;
120
+ font-size: 0.95rem;
121
+ font-weight: 500;
122
+ transition: all 0.2s ease;
123
+ position: relative;
124
+ padding: 8px 0;
125
+ }
126
+
127
+ .nav-links ::slotted(a:hover),
128
+ .nav-links ::slotted(a.active) {
129
+ color: var(--text-primary);
130
+ }
131
+
132
+ .nav-links ::slotted(a)::after {
133
+ content: '';
134
+ position: absolute;
135
+ bottom: 0;
136
+ left: 50%;
137
+ width: 0;
138
+ height: 2px;
139
+ background: var(--afri-gold, #fcd116);
140
+ transition: all 0.3s ease;
141
+ transform: translateX(-50%);
142
+ }
143
+
144
+ .nav-links ::slotted(a:hover)::after,
145
+ .nav-links ::slotted(a.active)::after {
146
+ width: 100%;
147
+ }
148
+
149
+ /* Mobile Toggle */
150
+ .menu-toggle {
151
+ display: none;
152
+ flex-direction: column;
153
+ gap: 6px;
154
+ background: none;
155
+ border: none;
156
+ cursor: pointer;
157
+ z-index: 1002;
158
+ padding: 8px;
159
+ }
160
+
161
+ .menu-toggle span {
162
+ width: 24px;
163
+ height: 2px;
164
+ background: var(--text-primary);
165
+ border-radius: 2px;
166
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
167
+ }
168
+
169
+ /* Mobile State */
170
+ .menu-toggle.open span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
171
+ .menu-toggle.open span:nth-child(2) { opacity: 0; transform: translateX(-10px); }
172
+ .menu-toggle.open span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }
173
+
174
+ @media (max-width: 768px) {
175
+ .menu-toggle {
176
+ display: flex;
177
+ }
178
+
179
+ .nav-links {
180
+ position: fixed;
181
+ top: 0;
182
+ left: 0;
183
+ width: 100%;
184
+ height: 100vh;
185
+ background: var(--bg-base, #020617);
186
+ flex-direction: column;
187
+ justify-content: center;
188
+ align-items: center;
189
+ gap: 24px;
190
+ opacity: 0;
191
+ visibility: hidden;
192
+ transform: translateY(-20px);
193
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
194
+ z-index: 1001;
195
+ }
196
+
197
+ .nav-links.open {
198
+ opacity: 1;
199
+ visibility: visible;
200
+ transform: translateY(0);
201
+ }
202
+
203
+ .nav-links ::slotted(a) {
204
+ font-size: 1.5rem;
205
+ font-weight: 700;
206
+ }
207
+ }
208
+ </style>
209
+
210
+ <nav>
211
+ <a href="/" class="logo-container">
212
+ <div class="logo-mark">A</div>
213
+ <span class="logo-text">${logo}</span>
214
+ </a>
215
+
216
+ <button
217
+ class="menu-toggle"
218
+ aria-label="Toggle menu"
219
+ aria-controls="nav-links"
220
+ aria-expanded="${this._isOpen}"
221
+ >
222
+ <span></span>
223
+ <span></span>
224
+ <span></span>
225
+ </button>
226
+
227
+ <div id="nav-links" class="nav-links ${this._isOpen ? 'open' : ''}">
228
+ <slot></slot>
229
+ </div>
230
+ </nav>
231
+ `;
232
+ }
233
+ }
234
+
235
+ registerComponent('af-navbar', AfriNavbar);
236
+ export default AfriNavbar;