@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.
- package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
2
|
+
|
|
3
|
+
export class AfriIcon extends AfriCodeComponent {
|
|
4
|
+
static get observedAttributes() { return ['name', 'size']; }
|
|
5
|
+
|
|
6
|
+
connectedCallback() {
|
|
7
|
+
this.render();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
render() {
|
|
11
|
+
const name = this.getAttribute('name') || 'circle';
|
|
12
|
+
const size = this.getAttribute('size') || '24';
|
|
13
|
+
|
|
14
|
+
// Simple SVG library (expand as needed)
|
|
15
|
+
const icons = {
|
|
16
|
+
'check': '<path d="M20 6L9 17l-5-5" />',
|
|
17
|
+
'x': '<path d="M18 6L6 18M6 6l12 12" />',
|
|
18
|
+
'arrow-right': '<path d="M5 12h14M12 5l7 7-7 7" />',
|
|
19
|
+
'menu': '<path d="M3 12h18M3 6h18M3 18h18" />',
|
|
20
|
+
'search': '<circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" />'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const path = icons[name] || '';
|
|
24
|
+
|
|
25
|
+
this.shadowRoot.innerHTML = `
|
|
26
|
+
<style>
|
|
27
|
+
:host { display: inline-flex; vertical-align: middle; }
|
|
28
|
+
svg { width: ${size}px; height: ${size}px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
29
|
+
</style>
|
|
30
|
+
<svg viewBox="0 0 24 24">${path}</svg>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
registerComponent('af-icon', AfriIcon);
|
|
36
|
+
export default AfriIcon;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Registry (Islands Map)
|
|
3
|
+
*
|
|
4
|
+
* Instead of statically importing all components (monolithic bundle),
|
|
5
|
+
* we export a map for lazy hydration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { AfriCodeComponent } from './base.js';
|
|
9
|
+
export { AfriErrorBoundary } from './error-boundary.js';
|
|
10
|
+
|
|
11
|
+
// Map of Tag Name -> Dynamic Import
|
|
12
|
+
export const componentMap = {
|
|
13
|
+
// Core UI
|
|
14
|
+
'af-button': () => import('./button.js'),
|
|
15
|
+
'af-card': () => import('./card.js'),
|
|
16
|
+
'af-navbar': () => import('./navbar.js'),
|
|
17
|
+
'af-input': () => import('./input.js'),
|
|
18
|
+
'af-alert': () => import('./alert.js'),
|
|
19
|
+
'af-grid': () => import('./grid.js'),
|
|
20
|
+
'af-section': () => import('./section.js'),
|
|
21
|
+
'af-icon': () => import('./icon.js'),
|
|
22
|
+
|
|
23
|
+
// Enterprise
|
|
24
|
+
'af-accordion': () => import('./accordion.js'),
|
|
25
|
+
'af-hero': () => import('./hero.js'),
|
|
26
|
+
'af-table': () => import('./table.js'),
|
|
27
|
+
'af-modal': () => import('./modal.js'),
|
|
28
|
+
'af-tabs': () => import('./tabs.js'),
|
|
29
|
+
'af-sidebar': () => import('./sidebar.js'),
|
|
30
|
+
'af-dropdown': () => import('./dropdown.js'),
|
|
31
|
+
'af-select': () => import('./select.js'),
|
|
32
|
+
'af-form': () => import('./form.js'),
|
|
33
|
+
'af-tooltip': () => import('./tooltip.js'),
|
|
34
|
+
'af-toast': () => import('./toast.js'),
|
|
35
|
+
|
|
36
|
+
// Feedback & Display
|
|
37
|
+
'af-badge': () => import('./badge.js'),
|
|
38
|
+
'af-avatar': () => import('./avatar.js'),
|
|
39
|
+
'af-progress': () => import('./progress.js'),
|
|
40
|
+
'af-loader': () => import('./loader.js'),
|
|
41
|
+
'af-skeleton': () => import('./skeleton.js'),
|
|
42
|
+
'af-divider': () => import('./divider.js'),
|
|
43
|
+
|
|
44
|
+
// Cultural
|
|
45
|
+
'af-kanga-card': () => import('./kanga-card.js'),
|
|
46
|
+
'af-cultural-card': () => import('./cultural-card.js'),
|
|
47
|
+
'af-pattern-showcase': () => import('./pattern-showcase.js'),
|
|
48
|
+
'af-language-switcher': () => import('./language-switcher.js'),
|
|
49
|
+
|
|
50
|
+
// Auth
|
|
51
|
+
'af-auth': () => import('./auth.js'),
|
|
52
|
+
|
|
53
|
+
// Theming
|
|
54
|
+
'af-theme-toggle': () => import('./theme-toggle.js'),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default componentMap;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Input Component
|
|
3
|
+
*
|
|
4
|
+
* Form input with validation, icons, and cultural styling.
|
|
5
|
+
* Suitable for: All sectors requiring data collection
|
|
6
|
+
*
|
|
7
|
+
* @module components/input
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
11
|
+
import { Validation, rules } from '../core/validation.js';
|
|
12
|
+
|
|
13
|
+
export class AfriInput extends AfriCodeComponent {
|
|
14
|
+
static get observedAttributes() {
|
|
15
|
+
return ['type', 'name', 'label', 'placeholder', 'error', 'helper', 'theme', 'icon', 'required', 'aria-label', 'aria-describedby', 'validation'];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this._uniqueId = Math.random().toString(36).substr(2, 9);
|
|
21
|
+
this.render();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get value() {
|
|
25
|
+
return this.shadowRoot.querySelector('input')?.value || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set value(val) {
|
|
29
|
+
const input = this.shadowRoot.querySelector('input');
|
|
30
|
+
if (input) {input.value = val;}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
this._attachInputListeners();
|
|
35
|
+
this.loadStyles();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
attributeChangedCallback() {
|
|
39
|
+
this.render();
|
|
40
|
+
this._attachInputListeners();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_attachInputListeners() {
|
|
44
|
+
const input = this.shadowRoot.querySelector('input');
|
|
45
|
+
if (!input) {return;}
|
|
46
|
+
|
|
47
|
+
input.addEventListener('input', (e) => {
|
|
48
|
+
this.emit('af-input', { value: e.target.value });
|
|
49
|
+
if (this.hasAttribute('error')) {
|
|
50
|
+
this.removeAttribute('error');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
input.addEventListener('blur', () => {
|
|
55
|
+
this.emit('af-blur', { value: this.value });
|
|
56
|
+
this._validateInput();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_setupValidation() {
|
|
61
|
+
const validationRule = this.getAttribute('validation');
|
|
62
|
+
if (!validationRule) {return;}
|
|
63
|
+
|
|
64
|
+
const input = this.shadowRoot.querySelector('input');
|
|
65
|
+
if (!input) {return;}
|
|
66
|
+
|
|
67
|
+
input.addEventListener('input', () => {
|
|
68
|
+
if (this.hasAttribute('error')) {
|
|
69
|
+
this.removeAttribute('error');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_validateInput() {
|
|
75
|
+
const input = this.shadowRoot.querySelector('input');
|
|
76
|
+
if (!input) {return;}
|
|
77
|
+
|
|
78
|
+
const validationRule = this.getAttribute('validation');
|
|
79
|
+
const value = input.value;
|
|
80
|
+
const required = this.hasAttribute('required');
|
|
81
|
+
|
|
82
|
+
// Check required first
|
|
83
|
+
if (required && !value.trim()) {
|
|
84
|
+
this.setAttribute('error', 'This field is required');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Apply validation rule
|
|
89
|
+
if (validationRule && value.trim()) {
|
|
90
|
+
const schema = this._getValidationSchema(validationRule);
|
|
91
|
+
if (schema) {
|
|
92
|
+
const result = Validation.validateField(schema, value);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
this.setAttribute('error', result.error);
|
|
95
|
+
} else {
|
|
96
|
+
this.removeAttribute('error');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_getValidationSchema(ruleName) {
|
|
103
|
+
const ruleMap = {
|
|
104
|
+
'email': rules.email(),
|
|
105
|
+
'password': rules.password(),
|
|
106
|
+
'phone': rules.phone(),
|
|
107
|
+
'url': rules.url(),
|
|
108
|
+
'required': rules.required()
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Handle parameterized rules like minLength:5
|
|
112
|
+
if (ruleName.includes(':')) {
|
|
113
|
+
const [rule, param] = ruleName.split(':');
|
|
114
|
+
const numParam = parseInt(param);
|
|
115
|
+
|
|
116
|
+
switch (rule) {
|
|
117
|
+
case 'minLength':
|
|
118
|
+
return rules.minLength(numParam);
|
|
119
|
+
case 'maxLength':
|
|
120
|
+
return rules.maxLength(numParam);
|
|
121
|
+
case 'pattern':
|
|
122
|
+
return rules.pattern(new RegExp(param), 'Invalid format');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return ruleMap[ruleName] || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
attributeChangedCallback() {
|
|
130
|
+
this.render();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
render() {
|
|
134
|
+
const type = this.getAttribute('type') || 'text';
|
|
135
|
+
const name = this.getAttribute('name') || '';
|
|
136
|
+
const label = this.getAttribute('label') || '';
|
|
137
|
+
const placeholder = this.getAttribute('placeholder') || '';
|
|
138
|
+
const error = this.getAttribute('error') || '';
|
|
139
|
+
const helper = this.getAttribute('helper') || '';
|
|
140
|
+
const theme = this.getAttribute('theme') || 'tanzania';
|
|
141
|
+
const icon = this.getAttribute('icon') || '';
|
|
142
|
+
const required = this.hasAttribute('required');
|
|
143
|
+
const ariaLabel = this.getAttribute('aria-label');
|
|
144
|
+
const ariaDescribedBy = this.getAttribute('aria-describedby');
|
|
145
|
+
|
|
146
|
+
const themes = {
|
|
147
|
+
tanzania: { focus: '#1EB53A', error: '#DC3545' },
|
|
148
|
+
maasai: { focus: '#FF0000', error: '#8B0000' },
|
|
149
|
+
ndebele: { focus: '#4169E1', error: '#DC143C' }
|
|
150
|
+
};
|
|
151
|
+
const t = themes[theme] || themes.tanzania;
|
|
152
|
+
|
|
153
|
+
const describedByList = [];
|
|
154
|
+
if (error) {describedByList.push(`error-${this._uniqueId}`);}
|
|
155
|
+
else if (helper) {describedByList.push(`helper-${this._uniqueId}`);}
|
|
156
|
+
if (ariaDescribedBy) {describedByList.push(...ariaDescribedBy.split(' '));}
|
|
157
|
+
const combinedDescribedBy = describedByList.length > 0 ? `aria-describedby="${describedByList.join(' ')}"` : '';
|
|
158
|
+
|
|
159
|
+
this.shadowRoot.innerHTML = `
|
|
160
|
+
<style>
|
|
161
|
+
:host {
|
|
162
|
+
display: block;
|
|
163
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.input-wrapper {
|
|
167
|
+
position: relative;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
label {
|
|
171
|
+
display: block;
|
|
172
|
+
font-weight: 500;
|
|
173
|
+
margin-bottom: 5px;
|
|
174
|
+
color: #333;
|
|
175
|
+
font-size: 14px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.required-star {
|
|
179
|
+
color: ${t.error};
|
|
180
|
+
margin-left: 2px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.input-container {
|
|
184
|
+
position: relative;
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
input {
|
|
190
|
+
width: 100%;
|
|
191
|
+
padding: 10px 13px;
|
|
192
|
+
${icon ? 'padding-left: 40px;' : ''}
|
|
193
|
+
font-size: 15px;
|
|
194
|
+
border: 2px solid #ddd;
|
|
195
|
+
border-radius: 5px;
|
|
196
|
+
transition: all 200ms ease;
|
|
197
|
+
font-family: inherit;
|
|
198
|
+
box-sizing: border-box;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
input:focus {
|
|
202
|
+
outline: none;
|
|
203
|
+
border-color: ${error ? t.error : t.focus};
|
|
204
|
+
box-shadow: 0 0 0 3px ${error ? t.error + '20' : t.focus + '20'};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
input.has-error {
|
|
208
|
+
border-color: ${t.error};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.icon {
|
|
212
|
+
position: absolute;
|
|
213
|
+
left: 13px;
|
|
214
|
+
color: #888;
|
|
215
|
+
font-size: 16px;
|
|
216
|
+
pointer-events: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.helper-text {
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
margin-top: 4px;
|
|
222
|
+
color: #666;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.error-text {
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
margin-top: 4px;
|
|
228
|
+
color: ${t.error};
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
231
|
+
|
|
232
|
+
<div class="input-wrapper">
|
|
233
|
+
${label ? `<label for="input-${this._uniqueId}">${label}${required ? '<span class="required-star">*</span>' : ''}</label>` : ''}
|
|
234
|
+
<div class="input-container">
|
|
235
|
+
${icon ? `<span class="icon">${icon}</span>` : ''}
|
|
236
|
+
<input
|
|
237
|
+
id="input-${this._uniqueId}"
|
|
238
|
+
name="${name}"
|
|
239
|
+
type="${type}"
|
|
240
|
+
placeholder="${placeholder}"
|
|
241
|
+
class="${error ? 'has-error' : ''}"
|
|
242
|
+
${required ? 'required' : ''}
|
|
243
|
+
${ariaLabel ? `aria-label="${ariaLabel}"` : ''}
|
|
244
|
+
${error ? 'aria-invalid="true"' : ''}
|
|
245
|
+
${combinedDescribedBy}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
${error ? `<div id="error-${this._uniqueId}" class="error-text" role="alert">${error}</div>` : ''}
|
|
249
|
+
${helper && !error ? `<div id="helper-${this._uniqueId}" class="helper-text">${helper}</div>` : ''}
|
|
250
|
+
</div>
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
registerComponent('af-input', AfriInput);
|
|
256
|
+
export default AfriInput;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Kanga Card Component
|
|
3
|
+
*
|
|
4
|
+
* A card component inspired by the traditional East African Kanga textile.
|
|
5
|
+
* Structure follows the authentic Kanga layout:
|
|
6
|
+
* - pindo: decorative border frame
|
|
7
|
+
* - mji: central content area
|
|
8
|
+
* - jina: Swahili proverb inscription slot
|
|
9
|
+
*
|
|
10
|
+
* @module components/kanga-card
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
14
|
+
import patterns from '../core/patterns.js';
|
|
15
|
+
|
|
16
|
+
export class KangaCard extends AfriCodeComponent {
|
|
17
|
+
static get observedAttributes() {
|
|
18
|
+
return ['theme', 'proverb', 'proverb-translation'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.render();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
this.loadStyles();
|
|
28
|
+
this.applyTheme();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
32
|
+
if (oldValue !== newValue) {
|
|
33
|
+
if (name === 'theme') {
|
|
34
|
+
this.applyTheme();
|
|
35
|
+
} else {
|
|
36
|
+
this.render();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get theme colors based on attribute
|
|
43
|
+
*/
|
|
44
|
+
getThemeColors() {
|
|
45
|
+
const theme = this.getAttribute('theme') || 'tanzania';
|
|
46
|
+
|
|
47
|
+
const themes = {
|
|
48
|
+
tanzania: {
|
|
49
|
+
primary: '#1EB53A',
|
|
50
|
+
accent: '#FCD116',
|
|
51
|
+
border: '#00A3DD',
|
|
52
|
+
text: '#000000'
|
|
53
|
+
},
|
|
54
|
+
maasai: {
|
|
55
|
+
primary: '#8B0000',
|
|
56
|
+
accent: '#FF0000',
|
|
57
|
+
border: '#000000',
|
|
58
|
+
text: '#FFFFFF'
|
|
59
|
+
},
|
|
60
|
+
ndebele: {
|
|
61
|
+
primary: '#4169E1',
|
|
62
|
+
accent: '#FFD700',
|
|
63
|
+
border: '#FF69B4',
|
|
64
|
+
text: '#000000'
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return themes[theme] || themes.tanzania;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Apply theme-specific patterns and colors
|
|
73
|
+
*/
|
|
74
|
+
applyTheme() {
|
|
75
|
+
const colors = this.getThemeColors();
|
|
76
|
+
const pindo = this.shadowRoot.querySelector('.pindo');
|
|
77
|
+
|
|
78
|
+
if (pindo) {
|
|
79
|
+
const pattern = generateKangaBorder({
|
|
80
|
+
primaryColor: colors.primary,
|
|
81
|
+
accentColor: colors.accent
|
|
82
|
+
});
|
|
83
|
+
pindo.style.backgroundImage = `url("${pattern}")`;
|
|
84
|
+
pindo.style.backgroundRepeat = 'repeat';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render() {
|
|
89
|
+
const proverb = this.getAttribute('proverb') || '';
|
|
90
|
+
const translation = this.getAttribute('proverb-translation') || '';
|
|
91
|
+
const colors = this.getThemeColors();
|
|
92
|
+
|
|
93
|
+
this.shadowRoot.innerHTML = `
|
|
94
|
+
<style>
|
|
95
|
+
:host {
|
|
96
|
+
display: block;
|
|
97
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.kanga-card {
|
|
101
|
+
border-radius: var(--radius-lg, 8px);
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
box-shadow: var(--shadow-lg, 0 5px 21px rgba(0, 0, 0, 0.2));
|
|
104
|
+
transition: transform var(--transition-base, 250ms ease-out),
|
|
105
|
+
box-shadow var(--transition-base, 250ms ease-out);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.kanga-card:hover {
|
|
109
|
+
transform: translateY(-3px);
|
|
110
|
+
box-shadow: 0 8px 34px rgba(0, 0, 0, 0.25);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Pindo - Decorative Border */
|
|
114
|
+
.pindo {
|
|
115
|
+
padding: var(--space-13, 13px);
|
|
116
|
+
background-color: ${colors.primary};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Mji - Central Content Area */
|
|
120
|
+
.mji {
|
|
121
|
+
background: linear-gradient(135deg, #FFFFFF 0%, #F5F5F5 100%);
|
|
122
|
+
padding: var(--space-21, 21px);
|
|
123
|
+
min-height: 120px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Jina - Proverb Inscription */
|
|
127
|
+
.jina {
|
|
128
|
+
background-color: ${colors.border};
|
|
129
|
+
color: ${colors.text};
|
|
130
|
+
padding: var(--space-8, 8px) var(--space-13, 13px);
|
|
131
|
+
text-align: center;
|
|
132
|
+
font-style: italic;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.jina .swahili {
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
font-size: 1rem;
|
|
138
|
+
margin-bottom: var(--space-3, 3px);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.jina .translation {
|
|
142
|
+
font-size: 0.85rem;
|
|
143
|
+
opacity: 0.9;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.jina:empty {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Content slot styles */
|
|
151
|
+
::slotted(*) {
|
|
152
|
+
margin: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
::slotted(h1), ::slotted(h2), ::slotted(h3) {
|
|
156
|
+
color: ${colors.primary};
|
|
157
|
+
margin-bottom: var(--space-8, 8px);
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
160
|
+
|
|
161
|
+
<article class="kanga-card">
|
|
162
|
+
<div class="pindo">
|
|
163
|
+
<div class="mji">
|
|
164
|
+
<slot></slot>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
${proverb ? `
|
|
169
|
+
<div class="jina">
|
|
170
|
+
<p class="swahili">${proverb}</p>
|
|
171
|
+
${translation ? `<p class="translation">${translation}</p>` : ''}
|
|
172
|
+
</div>
|
|
173
|
+
` : '<div class="jina"><slot name="proverb"></slot></div>'}
|
|
174
|
+
</article>
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
// Re-apply theme after render
|
|
178
|
+
this.applyTheme();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Register the component
|
|
183
|
+
registerComponent('af-kanga-card', KangaCard);
|
|
184
|
+
|
|
185
|
+
export default KangaCard;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Language Switcher
|
|
3
|
+
* A premium dropdown that uses native language names.
|
|
4
|
+
*/
|
|
5
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
6
|
+
|
|
7
|
+
export class AfriLanguageSwitcher extends AfriCodeComponent {
|
|
8
|
+
static get observedAttributes() {
|
|
9
|
+
return ['current'];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
const languages = [
|
|
14
|
+
{ code: 'en', name: 'English' },
|
|
15
|
+
{ code: 'sw', name: 'Kiswahili' },
|
|
16
|
+
{ code: 'fr', name: 'Français' },
|
|
17
|
+
{ code: 'ar', name: 'العربية', dir: 'rtl' },
|
|
18
|
+
{ code: 'am', name: 'አማርኛ' },
|
|
19
|
+
{ code: 'yo', name: 'Yorùbá' }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const current = this.getAttribute('current') || 'en';
|
|
23
|
+
const currentLang = languages.find(l => l.code === current) || languages[0];
|
|
24
|
+
|
|
25
|
+
this.shadowRoot.innerHTML = html`
|
|
26
|
+
<style>
|
|
27
|
+
:host { display: inline-block; position: relative; font-family: var(--font-body); }
|
|
28
|
+
.switcher-btn {
|
|
29
|
+
padding: 8px 16px;
|
|
30
|
+
background: var(--glass-bg);
|
|
31
|
+
border: 1px solid var(--glass-border);
|
|
32
|
+
border-radius: var(--radius-md);
|
|
33
|
+
color: var(--text-primary);
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 8px;
|
|
38
|
+
transition: all 0.3s ease;
|
|
39
|
+
}
|
|
40
|
+
.switcher-btn:hover { background: rgba(255,255,255,0.1); }
|
|
41
|
+
.dropdown {
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: calc(100% + 8px);
|
|
44
|
+
right: 0;
|
|
45
|
+
background: var(--afri-void);
|
|
46
|
+
border: 1px solid var(--glass-border);
|
|
47
|
+
border-radius: var(--radius-md);
|
|
48
|
+
padding: 8px;
|
|
49
|
+
display: none;
|
|
50
|
+
min-width: 150px;
|
|
51
|
+
z-index: 1000;
|
|
52
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
|
53
|
+
}
|
|
54
|
+
:host([open]) .dropdown { display: block; }
|
|
55
|
+
.lang-option {
|
|
56
|
+
padding: 8px 12px;
|
|
57
|
+
border-radius: var(--radius-sm);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
transition: all 0.2s ease;
|
|
60
|
+
}
|
|
61
|
+
.lang-option:hover { background: var(--glass-bg); color: var(--accent-gold); }
|
|
62
|
+
</style>
|
|
63
|
+
|
|
64
|
+
<button class="switcher-btn">
|
|
65
|
+
<span>🌍</span>
|
|
66
|
+
<span>${currentLang.name}</span>
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<div class="dropdown">
|
|
70
|
+
${languages.map(lang => html`
|
|
71
|
+
<div class="lang-option" data-code="${lang.code}" dir="${lang.dir || 'ltr'}">
|
|
72
|
+
${lang.name}
|
|
73
|
+
</div>
|
|
74
|
+
`)}
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
this.shadowRoot.querySelector('.switcher-btn').onclick = () => {
|
|
79
|
+
this.toggleAttribute('open');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.shadowRoot.querySelectorAll('.lang-option').forEach(opt => {
|
|
83
|
+
opt.onclick = () => {
|
|
84
|
+
const code = opt.dataset.code;
|
|
85
|
+
this.setAttribute('current', code);
|
|
86
|
+
this.removeAttribute('open');
|
|
87
|
+
this.emit('lang-change', { code });
|
|
88
|
+
|
|
89
|
+
// Demo logic: change document direction for Arabic
|
|
90
|
+
if (code === 'ar') {
|
|
91
|
+
document.documentElement.dir = 'rtl';
|
|
92
|
+
} else {
|
|
93
|
+
document.documentElement.dir = 'ltr';
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
connectedCallback() {
|
|
100
|
+
this.render();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
attributeChangedCallback() {
|
|
104
|
+
this.render();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
registerComponent('af-language-switcher', AfriLanguageSwitcher);
|