@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,137 @@
1
+ /**
2
+ * AfriCode Simple Card Component
3
+ *
4
+ * Versatile content card for various use cases.
5
+ * Suitable for: Product cards, Blog posts, Team members, Stats
6
+ *
7
+ * @module components/card
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+
12
+ export class AfriCard extends AfriCodeComponent {
13
+ static get observedAttributes() {
14
+ return ['theme', 'variant', 'clickable', 'image'];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this.render();
20
+ }
21
+
22
+ connectedCallback() {
23
+ if (this.hasAttribute('clickable')) {
24
+ this.shadowRoot.querySelector('.card').addEventListener('click', () => {
25
+ this.emit('af-click');
26
+ });
27
+ }
28
+ }
29
+
30
+ attributeChangedCallback() {
31
+ this.render();
32
+ }
33
+
34
+ render() {
35
+ const theme = this.getAttribute('theme') || 'default';
36
+ const variant = this.getAttribute('variant') || 'elevated';
37
+ const clickable = this.hasAttribute('clickable');
38
+ const image = this.getAttribute('image');
39
+
40
+ const themes = {
41
+ default: { accent: '#333', border: '#eee' },
42
+ tanzania: { accent: '#1EB53A', border: '#1EB53A' },
43
+ maasai: { accent: '#FF0000', border: '#8B0000' },
44
+ ndebele: { accent: '#4169E1', border: '#4169E1' }
45
+ };
46
+ const t = themes[theme] || themes.default;
47
+
48
+ const variants = {
49
+ elevated: 'box-shadow: 0 2px 12px rgba(0,0,0,0.1);',
50
+ outlined: `border: 2px solid ${t.border};`,
51
+ flat: 'background: #f8f9fa;'
52
+ };
53
+
54
+ this.shadowRoot.innerHTML = html`
55
+ <style>
56
+ :host {
57
+ display: block;
58
+ font-family: var(--font-body, 'Inter', sans-serif);
59
+ }
60
+
61
+ .card {
62
+ background: var(--glass-bg, rgba(15, 23, 42, 0.7));
63
+ backdrop-filter: blur(var(--glass-blur, 12px));
64
+ border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
65
+ border-radius: var(--radius-lg, 16px);
66
+ overflow: hidden;
67
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
68
+ position: relative;
69
+ ${clickable ? 'cursor: pointer;' : ''}
70
+ }
71
+
72
+ .card::before {
73
+ content: '';
74
+ position: absolute;
75
+ inset: 0;
76
+ background: radial-gradient(circle at top left, rgba(255,255,255,0.05), transparent);
77
+ pointer-events: none;
78
+ }
79
+
80
+ ${clickable ? `
81
+ .card:hover {
82
+ transform: translateY(-8px);
83
+ border-color: rgba(252, 209, 22, 0.4);
84
+ box-shadow: 0 20px 40px rgba(0,0,0,0.4);
85
+ }
86
+ ` : ''}
87
+
88
+ .card-image {
89
+ width: 100%;
90
+ height: 200px;
91
+ object-fit: cover;
92
+ border-bottom: 1px solid var(--glass-border);
93
+ }
94
+
95
+ .card-body {
96
+ padding: 32px;
97
+ }
98
+
99
+ ::slotted(h1), ::slotted(h2), ::slotted(h3), ::slotted(h4) {
100
+ margin: 0 0 12px 0;
101
+ font-family: 'Outfit', sans-serif;
102
+ font-weight: 700;
103
+ color: var(--text-primary, #fff);
104
+ letter-spacing: -0.02em;
105
+ }
106
+
107
+ ::slotted(p) {
108
+ margin: 0;
109
+ color: var(--text-muted, #94a3b8);
110
+ line-height: 1.6;
111
+ font-size: 0.95rem;
112
+ }
113
+
114
+ .card-footer {
115
+ padding: 20px 32px;
116
+ background: rgba(0,0,0,0.2);
117
+ border-top: 1px solid var(--glass-border);
118
+ }
119
+ </style>
120
+
121
+ <article class="card">
122
+ ${image ? `<img class="card-image" src="${image}" alt="" />` : ''}
123
+ <div class="card-body">
124
+ <slot></slot>
125
+ </div>
126
+ ${this.querySelector('[slot="footer"]') ? `
127
+ <div class="card-footer">
128
+ <slot name="footer"></slot>
129
+ </div>
130
+ ` : ''}
131
+ </article>
132
+ `;
133
+ }
134
+ }
135
+
136
+ registerComponent('af-card', AfriCard);
137
+ export default AfriCard;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * AfriCode Cultural Card Component
3
+ *
4
+ * Interactive card showcasing African cultural patterns
5
+ * with region selection and animated pattern backgrounds.
6
+ *
7
+ * @module components/cultural-card
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+ import patterns from '../core/patterns.js';
12
+
13
+ export class CulturalCard extends AfriCodeComponent {
14
+ static get observedAttributes() {
15
+ return ['culture', 'interactive'];
16
+ }
17
+
18
+ constructor() {
19
+ super();
20
+ this._cultures = {
21
+ // East Africa
22
+ maasai: { name: 'Maasai', region: 'Tanzania/Kenya', pattern: 'generateShuka', colors: { primary: '#FF0000', accent: '#000000' } },
23
+ hadzabe: { name: 'Hadzabe', region: 'Tanzania', pattern: 'generateHadzabe', colors: { primary: '#8B4513', accent: '#CD853F' } },
24
+ swahili: { name: 'Swahili', region: 'East Africa', pattern: 'generateKangaBorder', colors: { primary: '#1EB53A', accent: '#FCD116' } },
25
+ kitenge: { name: 'Kitenge', region: 'East Africa', pattern: 'generateKitenge', colors: { primary: '#00A3DD', accent: '#1EB53A' } },
26
+ tibeb: { name: 'Tibeb', region: 'Ethiopia', pattern: 'generateTibeb', colors: { primary: '#009E60', accent: '#FCDD09' } },
27
+ // West Africa
28
+ ashanti: { name: 'Ashanti/Akan', region: 'Ghana', pattern: 'generateKente', colors: { primary: '#FFD700', accent: '#228B22' } },
29
+ adinkra: { name: 'Adinkra', region: 'Ghana', pattern: 'generateAdinkra', colors: { primary: '#000000', accent: '#CD853F' } },
30
+ bogolan: { name: 'Bogolan', region: 'Mali', pattern: 'generateBogolan', colors: { primary: '#3E2723', accent: '#D7CCC8' } },
31
+ yoruba: { name: 'Yoruba', region: 'Nigeria', pattern: 'generateAsoOke', colors: { primary: '#4B0082', accent: '#FFD700' } },
32
+ ankara: { name: 'Ankara', region: 'West Africa', pattern: 'generateAnkara', colors: { primary: '#FF6B00', accent: '#00A86B' } },
33
+ // Central Africa
34
+ kuba: { name: 'Kuba Shoowa', region: 'DRC', pattern: 'generateKuba', colors: { primary: '#D2B48C', accent: '#3E2723' } },
35
+ imigongo: { name: 'Imigongo', region: 'Rwanda', pattern: 'generateImigongo', colors: { primary: '#000000', accent: '#B22222' } },
36
+ // Southern Africa
37
+ ndebele: { name: 'Ndebele', region: 'South Africa', pattern: 'generateNdebele', colors: { primary: '#4169E1', accent: '#FFD700' } },
38
+ zulu: { name: 'Zulu', region: 'South Africa', pattern: 'generateZulu', colors: { primary: '#FF0000', accent: '#000000' } },
39
+ swazi: { name: 'Swazi', region: 'Eswatini', pattern: 'generateSwazi', colors: { primary: '#3E5EB9', accent: '#FECD00' } },
40
+ xhosa: { name: 'Xhosa', region: 'South Africa', pattern: 'generateXhosa', colors: { primary: '#00A86B', accent: '#1a1a2e' } }
41
+ };
42
+ this.render();
43
+ }
44
+
45
+ connectedCallback() {
46
+ this._setupInteraction();
47
+ }
48
+
49
+ _setupInteraction() {
50
+ if (!this.hasAttribute('interactive')) {return;}
51
+
52
+ const selector = this.shadowRoot.querySelector('.culture-selector');
53
+ selector?.addEventListener('change', (e) => {
54
+ this.setAttribute('culture', e.target.value);
55
+ this.emit('af-culture-change', { culture: e.target.value });
56
+ });
57
+ }
58
+
59
+ attributeChangedCallback() {
60
+ this.render();
61
+ this._setupInteraction();
62
+ }
63
+
64
+ render() {
65
+ const cultureKey = this.getAttribute('culture') || 'maasai';
66
+ const interactive = this.hasAttribute('interactive');
67
+ const culture = this._cultures[cultureKey] || this._cultures.maasai;
68
+
69
+ // Generate pattern
70
+ const patternFn = patterns[culture.pattern];
71
+ const patternUrl = patternFn ? patternFn() : '';
72
+
73
+ this.shadowRoot.innerHTML = `
74
+ <style>
75
+ :host {
76
+ display: block;
77
+ font-family: 'Inter', system-ui, sans-serif;
78
+ }
79
+
80
+ .cultural-card {
81
+ border-radius: 12px;
82
+ overflow: hidden;
83
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
84
+ transition: transform 300ms ease, box-shadow 300ms ease;
85
+ }
86
+
87
+ .cultural-card:hover {
88
+ transform: translateY(-5px);
89
+ box-shadow: 0 8px 30px rgba(0,0,0,0.2);
90
+ }
91
+
92
+ .pattern-header {
93
+ height: 120px;
94
+ background-image: url("${patternUrl}");
95
+ background-repeat: repeat;
96
+ position: relative;
97
+ animation: patternShift 20s linear infinite;
98
+ }
99
+
100
+ @keyframes patternShift {
101
+ 0% { background-position: 0 0; }
102
+ 100% { background-position: 100px 100px; }
103
+ }
104
+
105
+ .pattern-overlay {
106
+ position: absolute;
107
+ inset: 0;
108
+ background: linear-gradient(transparent 0%, rgba(0,0,0,0.3) 100%);
109
+ }
110
+
111
+ .culture-badge {
112
+ position: absolute;
113
+ bottom: 10px;
114
+ left: 10px;
115
+ background: ${culture.colors.primary};
116
+ color: white;
117
+ padding: 4px 12px;
118
+ border-radius: 20px;
119
+ font-size: 12px;
120
+ font-weight: 600;
121
+ text-shadow: 0 1px 2px rgba(0,0,0,0.3);
122
+ }
123
+
124
+ .region-badge {
125
+ position: absolute;
126
+ bottom: 10px;
127
+ right: 10px;
128
+ background: rgba(255,255,255,0.9);
129
+ color: #333;
130
+ padding: 4px 10px;
131
+ border-radius: 20px;
132
+ font-size: 11px;
133
+ }
134
+
135
+ .card-body {
136
+ background: white;
137
+ padding: 20px;
138
+ }
139
+
140
+ .card-title {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 10px;
144
+ margin-bottom: 12px;
145
+ }
146
+
147
+ .card-title h3 {
148
+ margin: 0;
149
+ font-size: 18px;
150
+ color: ${culture.colors.primary};
151
+ }
152
+
153
+ .culture-icon {
154
+ width: 40px;
155
+ height: 40px;
156
+ border-radius: 50%;
157
+ background-image: url("${patternUrl}");
158
+ background-size: cover;
159
+ border: 2px solid ${culture.colors.accent};
160
+ }
161
+
162
+ .card-content {
163
+ color: #555;
164
+ line-height: 1.6;
165
+ }
166
+
167
+ .culture-selector {
168
+ width: 100%;
169
+ padding: 10px;
170
+ margin-top: 15px;
171
+ border: 2px solid ${culture.colors.primary};
172
+ border-radius: 6px;
173
+ font-family: inherit;
174
+ font-size: 14px;
175
+ cursor: pointer;
176
+ background: white;
177
+ }
178
+
179
+ .culture-selector:focus {
180
+ outline: none;
181
+ box-shadow: 0 0 0 3px ${culture.colors.primary}30;
182
+ }
183
+
184
+ select option {
185
+ padding: 8px;
186
+ }
187
+
188
+ optgroup {
189
+ font-weight: 600;
190
+ color: #333;
191
+ }
192
+ </style>
193
+
194
+ <article class="cultural-card">
195
+ <div class="pattern-header">
196
+ <div class="pattern-overlay"></div>
197
+ <span class="culture-badge">${culture.name}</span>
198
+ <span class="region-badge">📍 ${culture.region}</span>
199
+ </div>
200
+ <div class="card-body">
201
+ <div class="card-title">
202
+ <div class="culture-icon"></div>
203
+ <h3>${culture.name} Pattern</h3>
204
+ </div>
205
+ <div class="card-content">
206
+ <slot></slot>
207
+ </div>
208
+ ${interactive ? `
209
+ <select class="culture-selector">
210
+ <optgroup label="🌍 East Africa">
211
+ <option value="maasai" ${cultureKey === 'maasai' ? 'selected' : ''}>Maasai (Tanzania/Kenya)</option>
212
+ <option value="hadzabe" ${cultureKey === 'hadzabe' ? 'selected' : ''}>Hadzabe (Tanzania)</option>
213
+ <option value="swahili" ${cultureKey === 'swahili' ? 'selected' : ''}>Swahili Kanga</option>
214
+ <option value="kitenge" ${cultureKey === 'kitenge' ? 'selected' : ''}>Kitenge</option>
215
+ <option value="tibeb" ${cultureKey === 'tibeb' ? 'selected' : ''}>Ethiopian Tibeb</option>
216
+ </optgroup>
217
+ <optgroup label="🌍 West Africa">
218
+ <option value="ashanti" ${cultureKey === 'ashanti' ? 'selected' : ''}>Ashanti Kente (Ghana)</option>
219
+ <option value="adinkra" ${cultureKey === 'adinkra' ? 'selected' : ''}>Adinkra (Ghana)</option>
220
+ <option value="bogolan" ${cultureKey === 'bogolan' ? 'selected' : ''}>Mali Bogolan (Mud Cloth)</option>
221
+ <option value="yoruba" ${cultureKey === 'yoruba' ? 'selected' : ''}>Yoruba Aso-Oke (Nigeria)</option>
222
+ <option value="ankara" ${cultureKey === 'ankara' ? 'selected' : ''}>Ankara Wax Print</option>
223
+ </optgroup>
224
+ <optgroup label="🌍 Central Africa">
225
+ <option value="kuba" ${cultureKey === 'kuba' ? 'selected' : ''}>Kuba Shoowa (DRC)</option>
226
+ <option value="imigongo" ${cultureKey === 'imigongo' ? 'selected' : ''}>Imigongo (Rwanda)</option>
227
+ </optgroup>
228
+ <optgroup label="🌍 Southern Africa">
229
+ <option value="ndebele" ${cultureKey === 'ndebele' ? 'selected' : ''}>Ndebele (South Africa)</option>
230
+ <option value="zulu" ${cultureKey === 'zulu' ? 'selected' : ''}>Zulu (South Africa)</option>
231
+ <option value="swazi" ${cultureKey === 'swazi' ? 'selected' : ''}>Swazi (Eswatini)</option>
232
+ <option value="xhosa" ${cultureKey === 'xhosa' ? 'selected' : ''}>Xhosa (South Africa)</option>
233
+ </optgroup>
234
+ </select>
235
+ ` : ''}
236
+ </div>
237
+ </article>
238
+ `;
239
+ }
240
+ }
241
+
242
+ registerComponent('af-cultural-card', CulturalCard);
243
+ export default CulturalCard;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * AfriCode Section Divider Component
3
+ *
4
+ * Patterned SVG dividers for separating content sections using African motifs.
5
+ * Supports patterns: zigzag, waves, diamonds, triangles.
6
+ *
7
+ * @module components/divider
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+
12
+ export class AfriDivider extends AfriCodeComponent {
13
+ static get observedAttributes() {
14
+ return ['pattern', 'height', 'color', 'flip'];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this.render();
20
+ }
21
+
22
+ attributeChangedCallback() {
23
+ this.render();
24
+ }
25
+
26
+ _getPatternPath(pattern, width, height) {
27
+ const p = pattern || 'zigzag';
28
+
29
+ switch (p) {
30
+ case 'zigzag':
31
+ // Continuous zigzag
32
+ return `M0,${height} L${width / 4},0 L${width / 2},${height} L${width * 0.75},0 L${width},${height} V${height + 10} H0 Z`;
33
+ case 'waves':
34
+ // Smooth sine wave
35
+ return `M0,${height / 2} Q${width / 4},0 ${width / 2},${height / 2} T${width},${height / 2} V${height} H0 Z`;
36
+ case 'triangles':
37
+ // Repeating triangles
38
+ return `M0,${height} L${width / 8},0 L${width / 4},${height} L${width * 0.375},0 L${width / 2},${height}
39
+ L${width * 0.625},0 L${width * 0.75},${height} L${width * 0.875},0 L${width},${height} V${height + 10} H0 Z`;
40
+ case 'diamonds':
41
+ // Row of diamonds on bottom
42
+ return `M0,0 H${width} V${height / 2}
43
+ L${width * 0.9},${height} L${width * 0.8},${height / 2}
44
+ L${width * 0.7},${height} L${width * 0.6},${height / 2}
45
+ L${width * 0.5},${height} L${width * 0.4},${height / 2}
46
+ L${width * 0.3},${height} L${width * 0.2},${height / 2}
47
+ L${width * 0.1},${height} L0,${height / 2} Z`;
48
+ default:
49
+ return `M0,0 H${width} V${height} H0 Z`; // Rectangle fallback
50
+ }
51
+ }
52
+
53
+ render() {
54
+ const pattern = this.getAttribute('pattern') || 'zigzag';
55
+ const height = this.getAttribute('height') || '50';
56
+ const color = this.getAttribute('color') || 'currentColor';
57
+ const flip = this.hasAttribute('flip');
58
+
59
+ this.shadowRoot.innerHTML = `
60
+ <style>
61
+ :host {
62
+ display: block;
63
+ line-height: 0;
64
+ width: 100%;
65
+ overflow: hidden;
66
+ color: ${color};
67
+ }
68
+ svg {
69
+ display: block;
70
+ width: 100%;
71
+ height: ${height}px;
72
+ ${flip ? 'transform: rotate(180deg);' : ''}
73
+ }
74
+ </style>
75
+ <svg viewBox="0 0 100 ${height}" preserveAspectRatio="none">
76
+ <path d="${this._getPatternPath(pattern, 100, parseInt(height))}" fill="currentColor" />
77
+ </svg>
78
+ `;
79
+ }
80
+ }
81
+
82
+ registerComponent('af-divider', AfriDivider);
83
+ export default AfriDivider;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * AfriCode Dropdown Component
3
+ *
4
+ * Dropdown menu with keyboard navigation.
5
+ *
6
+ * @module components/dropdown
7
+ */
8
+
9
+ import { AfriCodeComponent, registerComponent } from './base.js';
10
+
11
+ export class AfriDropdown extends AfriCodeComponent {
12
+ static get observedAttributes() {
13
+ return ['label', 'open'];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this._open = false;
19
+ this.render();
20
+ }
21
+
22
+ toggle() {
23
+ this._open = !this._open;
24
+ if (this._open) {
25
+ this.setAttribute('open', '');
26
+ } else {
27
+ this.removeAttribute('open');
28
+ }
29
+ this.render();
30
+ }
31
+
32
+ close() {
33
+ this._open = false;
34
+ this.removeAttribute('open');
35
+ this.render();
36
+ }
37
+
38
+ connectedCallback() {
39
+ // Close on outside click
40
+ document.addEventListener('click', (e) => {
41
+ if (!this.contains(e.target)) {
42
+ this.close();
43
+ }
44
+ });
45
+
46
+ // Keyboard navigation
47
+ this.addEventListener('keydown', this._handleKeydown.bind(this));
48
+ }
49
+
50
+ _handleKeydown(e) {
51
+ const trigger = this.shadowRoot.getElementById('trigger');
52
+
53
+ switch (e.key) {
54
+ case 'Enter':
55
+ case ' ':
56
+ e.preventDefault();
57
+ this.toggle();
58
+ break;
59
+ case 'Escape':
60
+ if (this._open) {
61
+ e.preventDefault();
62
+ this.close();
63
+ trigger.focus();
64
+ }
65
+ break;
66
+ case 'ArrowDown':
67
+ if (!this._open) {
68
+ e.preventDefault();
69
+ this.toggle();
70
+ }
71
+ break;
72
+ }
73
+ }
74
+
75
+ render() {
76
+ const label = this.getAttribute('label') || 'Menu';
77
+ const open = this.hasAttribute('open');
78
+
79
+ this.shadowRoot.innerHTML = `
80
+ <style>
81
+ :host {
82
+ display: inline-block;
83
+ position: relative;
84
+ font-family: 'Inter', system-ui, sans-serif;
85
+ }
86
+
87
+ .dropdown-trigger {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 8px;
91
+ padding: 10px 16px;
92
+ background: #1e1e1e;
93
+ border: 1px solid #2a2a2a;
94
+ border-radius: 8px;
95
+ color: #ffffff;
96
+ font-size: 0.9rem;
97
+ cursor: pointer;
98
+ transition: all 0.2s;
99
+ }
100
+
101
+ .dropdown-trigger:hover {
102
+ border-color: #1EB53A;
103
+ }
104
+
105
+ .dropdown-arrow {
106
+ transition: transform 0.2s;
107
+ ${open ? 'transform: rotate(180deg);' : ''}
108
+ }
109
+
110
+ .dropdown-menu {
111
+ position: absolute;
112
+ top: 100%;
113
+ left: 0;
114
+ min-width: 180px;
115
+ margin-top: 4px;
116
+ background: #121212;
117
+ border: 1px solid #1e1e1e;
118
+ border-radius: 8px;
119
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
120
+ opacity: ${open ? 1 : 0};
121
+ visibility: ${open ? 'visible' : 'hidden'};
122
+ transform: translateY(${open ? 0 : '-10px'});
123
+ transition: all 0.2s ease;
124
+ z-index: 1000;
125
+ overflow: hidden;
126
+ }
127
+
128
+ ::slotted(a), ::slotted(button), ::slotted(div) {
129
+ display: block;
130
+ padding: 12px 16px;
131
+ color: #a0a0a0;
132
+ text-decoration: none;
133
+ border: none;
134
+ background: none;
135
+ width: 100%;
136
+ text-align: left;
137
+ font-size: 0.9rem;
138
+ cursor: pointer;
139
+ transition: all 0.15s;
140
+ }
141
+
142
+ ::slotted(a:hover), ::slotted(button:hover), ::slotted(div:hover) {
143
+ background: #1e1e1e;
144
+ color: #1EB53A;
145
+ }
146
+
147
+ ::slotted(hr) {
148
+ border: none;
149
+ border-top: 1px solid #1e1e1e;
150
+ margin: 4px 0;
151
+ }
152
+ </style>
153
+
154
+ <button class="dropdown-trigger" id="trigger" aria-expanded="${open}" aria-haspopup="menu">
155
+ ${label}
156
+ <span class="dropdown-arrow" aria-hidden="true">▼</span>
157
+ </button>
158
+ <div class="dropdown-menu" role="menu" ${open ? '' : 'aria-hidden="true"'}>
159
+ <slot></slot>
160
+ </div>
161
+ `;
162
+
163
+ this.shadowRoot.getElementById('trigger').addEventListener('click', (e) => {
164
+ e.stopPropagation();
165
+ this.toggle();
166
+ });
167
+ }
168
+ }
169
+
170
+ registerComponent('af-dropdown', AfriDropdown);
171
+ export default AfriDropdown;