@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Avatar Component
|
|
3
|
+
*
|
|
4
|
+
* User profile images and initials with status indicators.
|
|
5
|
+
* Suitable for: User profiles, Comments, Team members, Chat
|
|
6
|
+
*
|
|
7
|
+
* @module components/avatar
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
11
|
+
|
|
12
|
+
export class AfriAvatar extends AfriCodeComponent {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['src', 'name', 'size', 'status', 'theme'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
attributeChangedCallback() {
|
|
23
|
+
this.render();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_getInitials(name) {
|
|
27
|
+
if (!name) {return '?';}
|
|
28
|
+
const parts = name.trim().split(' ');
|
|
29
|
+
if (parts.length >= 2) {
|
|
30
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
return name.substring(0, 2).toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render() {
|
|
36
|
+
const src = this.getAttribute('src');
|
|
37
|
+
const name = this.getAttribute('name') || '';
|
|
38
|
+
const size = this.getAttribute('size') || 'md';
|
|
39
|
+
const status = this.getAttribute('status');
|
|
40
|
+
const theme = this.getAttribute('theme') || 'tanzania';
|
|
41
|
+
|
|
42
|
+
const themes = {
|
|
43
|
+
tanzania: '#1EB53A',
|
|
44
|
+
maasai: '#FF0000',
|
|
45
|
+
ndebele: '#4169E1',
|
|
46
|
+
gold: '#FCD116'
|
|
47
|
+
};
|
|
48
|
+
const bgColor = themes[theme] || themes.tanzania;
|
|
49
|
+
|
|
50
|
+
const sizes = {
|
|
51
|
+
sm: { size: '32px', font: '12px', status: '8px' },
|
|
52
|
+
md: { size: '44px', font: '16px', status: '10px' },
|
|
53
|
+
lg: { size: '64px', font: '22px', status: '14px' },
|
|
54
|
+
xl: { size: '96px', font: '32px', status: '18px' }
|
|
55
|
+
};
|
|
56
|
+
const s = sizes[size] || sizes.md;
|
|
57
|
+
|
|
58
|
+
const statusColors = {
|
|
59
|
+
online: '#28a745',
|
|
60
|
+
offline: '#6c757d',
|
|
61
|
+
busy: '#dc3545',
|
|
62
|
+
away: '#ffc107'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.shadowRoot.innerHTML = `
|
|
66
|
+
<style>
|
|
67
|
+
:host {
|
|
68
|
+
display: inline-block;
|
|
69
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.avatar {
|
|
73
|
+
position: relative;
|
|
74
|
+
width: ${s.size};
|
|
75
|
+
height: ${s.size};
|
|
76
|
+
border-radius: 50%;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
background: ${bgColor};
|
|
81
|
+
color: white;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
font-size: ${s.font};
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.avatar img {
|
|
88
|
+
width: 100%;
|
|
89
|
+
height: 100%;
|
|
90
|
+
object-fit: cover;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.status {
|
|
94
|
+
position: absolute;
|
|
95
|
+
bottom: 0;
|
|
96
|
+
right: 0;
|
|
97
|
+
width: ${s.status};
|
|
98
|
+
height: ${s.status};
|
|
99
|
+
border-radius: 50%;
|
|
100
|
+
border: 2px solid white;
|
|
101
|
+
background: ${status ? statusColors[status] || statusColors.offline : 'transparent'};
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
104
|
+
|
|
105
|
+
<div class="avatar" title="${name}">
|
|
106
|
+
${src
|
|
107
|
+
? `<img src="${src}" alt="${name}" />`
|
|
108
|
+
: this._getInitials(name)
|
|
109
|
+
}
|
|
110
|
+
${status ? '<span class="status"></span>' : ''}
|
|
111
|
+
</div>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
registerComponent('af-avatar', AfriAvatar);
|
|
117
|
+
export default AfriAvatar;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Badge/Tag Component
|
|
3
|
+
*
|
|
4
|
+
* Status indicators, labels, and tags.
|
|
5
|
+
* Suitable for: E-commerce (product tags), Healthcare (status), Finance (categories)
|
|
6
|
+
*
|
|
7
|
+
* @module components/badge
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
11
|
+
|
|
12
|
+
export class AfriBadge extends AfriCodeComponent {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['variant', 'size', 'removable'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connectedCallback() {
|
|
23
|
+
this.shadowRoot.querySelector('.remove-btn')?.addEventListener('click', () => {
|
|
24
|
+
this.emit('af-remove');
|
|
25
|
+
this.remove();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
attributeChangedCallback() {
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
const variant = this.getAttribute('variant') || 'default';
|
|
35
|
+
const size = this.getAttribute('size') || 'md';
|
|
36
|
+
const removable = this.hasAttribute('removable');
|
|
37
|
+
|
|
38
|
+
const variants = {
|
|
39
|
+
default: { bg: '#e9ecef', text: '#495057' },
|
|
40
|
+
success: { bg: '#d4edda', text: '#155724' },
|
|
41
|
+
warning: { bg: '#fff3cd', text: '#856404' },
|
|
42
|
+
error: { bg: '#f8d7da', text: '#721c24' },
|
|
43
|
+
info: { bg: '#d1ecf1', text: '#0c5460' },
|
|
44
|
+
tanzania: { bg: '#1EB53A', text: '#FFFFFF' },
|
|
45
|
+
maasai: { bg: '#FF0000', text: '#FFFFFF' },
|
|
46
|
+
ndebele: { bg: '#4169E1', text: '#FFFFFF' },
|
|
47
|
+
gold: { bg: '#FCD116', text: '#000000' }
|
|
48
|
+
};
|
|
49
|
+
const v = variants[variant] || variants.default;
|
|
50
|
+
|
|
51
|
+
const sizes = {
|
|
52
|
+
sm: { padding: '2px 6px', fontSize: '11px' },
|
|
53
|
+
md: { padding: '4px 10px', fontSize: '13px' },
|
|
54
|
+
lg: { padding: '6px 14px', fontSize: '15px' }
|
|
55
|
+
};
|
|
56
|
+
const s = sizes[size] || sizes.md;
|
|
57
|
+
|
|
58
|
+
this.shadowRoot.innerHTML = `
|
|
59
|
+
<style>
|
|
60
|
+
:host {
|
|
61
|
+
display: inline-block;
|
|
62
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.badge {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 5px;
|
|
69
|
+
padding: ${s.padding};
|
|
70
|
+
font-size: ${s.fontSize};
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
background: ${v.bg};
|
|
73
|
+
color: ${v.text};
|
|
74
|
+
border-radius: 100px;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.remove-btn {
|
|
79
|
+
background: none;
|
|
80
|
+
border: none;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
padding: 0;
|
|
83
|
+
font-size: 14px;
|
|
84
|
+
line-height: 1;
|
|
85
|
+
color: inherit;
|
|
86
|
+
opacity: 0.7;
|
|
87
|
+
margin-left: 2px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.remove-btn:hover {
|
|
91
|
+
opacity: 1;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
94
|
+
|
|
95
|
+
<span class="badge">
|
|
96
|
+
<slot></slot>
|
|
97
|
+
${removable ? '<button class="remove-btn" aria-label="Remove">×</button>' : ''}
|
|
98
|
+
</span>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
registerComponent('af-badge', AfriBadge);
|
|
104
|
+
export default AfriBadge;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Components - Base Class TypeScript Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component element creation options
|
|
7
|
+
*/
|
|
8
|
+
export interface ElementOptions {
|
|
9
|
+
classes?: string[];
|
|
10
|
+
attributes?: Record<string, string>;
|
|
11
|
+
text?: string;
|
|
12
|
+
html?: string;
|
|
13
|
+
children?: (HTMLElement | string)[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom event detail
|
|
18
|
+
*/
|
|
19
|
+
export interface ComponentEvent<T = any> extends CustomEvent {
|
|
20
|
+
detail: T;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* AfriCode Component Base Class
|
|
25
|
+
* All AfriCode components extend this class
|
|
26
|
+
*/
|
|
27
|
+
export class AfriCodeComponent extends HTMLElement {
|
|
28
|
+
/**
|
|
29
|
+
* Shadow DOM root
|
|
30
|
+
*/
|
|
31
|
+
shadowRoot: ShadowRoot;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Component name (e.g., 'af-button')
|
|
35
|
+
*/
|
|
36
|
+
static readonly is: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Observed attributes that trigger attributeChanged callback
|
|
40
|
+
*/
|
|
41
|
+
static readonly observedAttributes: string[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a DOM element with options
|
|
45
|
+
*/
|
|
46
|
+
createElement(
|
|
47
|
+
tag: string,
|
|
48
|
+
options?: ElementOptions
|
|
49
|
+
): HTMLElement;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emit a custom event
|
|
53
|
+
*/
|
|
54
|
+
emit<T = any>(name: string, detail?: T): void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load and apply component styles
|
|
58
|
+
*/
|
|
59
|
+
loadStyles(css?: string): void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render component to shadow DOM
|
|
63
|
+
*/
|
|
64
|
+
render(): void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mount lifecycle hook
|
|
68
|
+
*/
|
|
69
|
+
connectedCallback?(): void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Unmount lifecycle hook
|
|
73
|
+
*/
|
|
74
|
+
disconnectedCallback?(): void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Attribute change lifecycle hook
|
|
78
|
+
*/
|
|
79
|
+
attributeChangedCallback?(
|
|
80
|
+
name: string,
|
|
81
|
+
oldValue: string | null,
|
|
82
|
+
newValue: string | null
|
|
83
|
+
): void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Adopt node into shadow DOM
|
|
87
|
+
*/
|
|
88
|
+
adoptNode(node: Node): Node;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if attribute exists
|
|
92
|
+
*/
|
|
93
|
+
hasAttribute(name: string): boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get attribute value
|
|
97
|
+
*/
|
|
98
|
+
getAttribute(name: string): string | null;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set attribute value
|
|
102
|
+
*/
|
|
103
|
+
setAttribute(name: string, value: string): void;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Remove attribute
|
|
107
|
+
*/
|
|
108
|
+
removeAttribute(name: string): void;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Query shadow DOM
|
|
112
|
+
*/
|
|
113
|
+
querySelector(selector: string): Element | null;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Query all in shadow DOM
|
|
117
|
+
*/
|
|
118
|
+
querySelectorAll(selector: string): NodeListOf<Element>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Component registration
|
|
123
|
+
*/
|
|
124
|
+
export function registerComponent(
|
|
125
|
+
name: string,
|
|
126
|
+
component: typeof AfriCodeComponent
|
|
127
|
+
): void;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Component registry
|
|
131
|
+
*/
|
|
132
|
+
export const componentRegistry: Map<string, typeof AfriCodeComponent>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get registered component
|
|
136
|
+
*/
|
|
137
|
+
export function getComponent(name: string): typeof AfriCodeComponent | undefined;
|
|
138
|
+
|
|
139
|
+
export default AfriCodeComponent;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Base Component
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for all AfriCode Web Components.
|
|
5
|
+
* Provides common utilities and enforces the Shadow DOM pattern.
|
|
6
|
+
*
|
|
7
|
+
* @module components/base
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { html } from '../core/html.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base class for AfriCode components
|
|
14
|
+
* All components extend this to inherit common functionality
|
|
15
|
+
*/
|
|
16
|
+
export class AfriCodeComponent extends HTMLElement {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.attachShadow({ mode: 'open' });
|
|
20
|
+
|
|
21
|
+
// Native Web Component Error Boundary: intercept child render calls
|
|
22
|
+
const originalRender = this.render;
|
|
23
|
+
if (originalRender && typeof originalRender === 'function') {
|
|
24
|
+
this.render = () => {
|
|
25
|
+
try {
|
|
26
|
+
originalRender.call(this);
|
|
27
|
+
this.removeAttribute('has-error');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`[AfriCode] Rendering error in <${this.localName}>:`, error);
|
|
30
|
+
this.setAttribute('has-error', 'true');
|
|
31
|
+
this.shadowRoot.innerHTML = `
|
|
32
|
+
<style>
|
|
33
|
+
.af-local-error {
|
|
34
|
+
padding: 1rem;
|
|
35
|
+
border: 1px dashed rgba(220, 20, 60, 0.4);
|
|
36
|
+
border-radius: 4px;
|
|
37
|
+
color: #DC143C;
|
|
38
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
39
|
+
font-size: 0.85rem;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
background: rgba(220, 20, 60, 0.05);
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
<div class="af-local-error">
|
|
47
|
+
<span aria-hidden="true">⚠️</span>
|
|
48
|
+
<span>Failed to render</span>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
this.emit('af-component-error', {
|
|
52
|
+
component: this.localName,
|
|
53
|
+
message: error.message,
|
|
54
|
+
stack: error.stack
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Component lifecycle - connected to DOM
|
|
63
|
+
* Empty base implementation to support super calls in subclasses.
|
|
64
|
+
*/
|
|
65
|
+
connectedCallback() { }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load shared Africanity styles into Shadow DOM
|
|
69
|
+
*/
|
|
70
|
+
// Static cache for Constructable Stylesheets to prevent fetch waterfall
|
|
71
|
+
static styleCache = new Map();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Global stylesheets injected by adapters (e.g. Tailwind, custom design systems).
|
|
75
|
+
* Each entry must be a CSSStyleSheet instance.
|
|
76
|
+
* Usage: AfriCodeComponent.injectGlobalSheet(sheet)
|
|
77
|
+
* @type {CSSStyleSheet[]}
|
|
78
|
+
*/
|
|
79
|
+
static globalSheets = [];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Inject a global CSSStyleSheet into every AfriCode component's Shadow DOM.
|
|
83
|
+
* This is the primary integration point for external CSS frameworks like Tailwind.
|
|
84
|
+
* @param {CSSStyleSheet} sheet - A Constructable Stylesheet instance
|
|
85
|
+
*/
|
|
86
|
+
static injectGlobalSheet(sheet) {
|
|
87
|
+
if (!(sheet instanceof CSSStyleSheet)) {
|
|
88
|
+
console.error('[AfriCode] injectGlobalSheet requires a CSSStyleSheet instance.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!AfriCodeComponent.globalSheets.includes(sheet)) {
|
|
92
|
+
AfriCodeComponent.globalSheets.push(sheet);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Load shared Africanity styles into Shadow DOM.
|
|
98
|
+
* Optimized to fetch only once per session.
|
|
99
|
+
* Also adopts any globally injected sheets (e.g. Tailwind utilities).
|
|
100
|
+
*/
|
|
101
|
+
async loadStyles() {
|
|
102
|
+
const styleUrl = '/styles/africanity.css';
|
|
103
|
+
|
|
104
|
+
// 1. Check cache first
|
|
105
|
+
if (AfriCodeComponent.styleCache.has(styleUrl)) {
|
|
106
|
+
this.shadowRoot.adoptedStyleSheets = [
|
|
107
|
+
AfriCodeComponent.styleCache.get(styleUrl),
|
|
108
|
+
...AfriCodeComponent.globalSheets
|
|
109
|
+
];
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Fetch if not cached (Single Request)
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(styleUrl);
|
|
116
|
+
const css = await response.text();
|
|
117
|
+
|
|
118
|
+
const styleSheet = new CSSStyleSheet();
|
|
119
|
+
styleSheet.replaceSync(css);
|
|
120
|
+
|
|
121
|
+
// Cache it
|
|
122
|
+
AfriCodeComponent.styleCache.set(styleUrl, styleSheet);
|
|
123
|
+
|
|
124
|
+
// Apply core + any global sheets
|
|
125
|
+
this.shadowRoot.adoptedStyleSheets = [
|
|
126
|
+
styleSheet,
|
|
127
|
+
...AfriCodeComponent.globalSheets
|
|
128
|
+
];
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.warn('AfriCode: Failed to load core styles', err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get Fibonacci spacing value
|
|
136
|
+
* @param {number} index - Fibonacci sequence index (1,2,3,5,8,13,21,34,55)
|
|
137
|
+
* @returns {string} CSS variable reference
|
|
138
|
+
*/
|
|
139
|
+
spacing(index) {
|
|
140
|
+
return `var(--space-${index})`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create element with classes and attributes
|
|
145
|
+
* @param {string} tag - HTML tag name
|
|
146
|
+
* @param {Object} options - Element options
|
|
147
|
+
* @returns {HTMLElement}
|
|
148
|
+
*/
|
|
149
|
+
createElement(tag, { classes = [], attributes = {}, text = '' } = {}) {
|
|
150
|
+
const el = document.createElement(tag);
|
|
151
|
+
if (classes.length) {el.classList.add(...classes);}
|
|
152
|
+
Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value));
|
|
153
|
+
if (text) {el.textContent = text;}
|
|
154
|
+
return el;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Emit a custom event
|
|
160
|
+
* @param {string} name - Event name
|
|
161
|
+
* @param {*} detail - Event detail data
|
|
162
|
+
*/
|
|
163
|
+
emit(name, detail) {
|
|
164
|
+
this.dispatchEvent(new CustomEvent(name, {
|
|
165
|
+
detail,
|
|
166
|
+
bubbles: true,
|
|
167
|
+
composed: true
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Register a component with the Custom Elements registry
|
|
174
|
+
* @param {string} name - Component tag name (must include hyphen)
|
|
175
|
+
* @param {typeof HTMLElement} component - Component class
|
|
176
|
+
*/
|
|
177
|
+
export function registerComponent(name, component) {
|
|
178
|
+
if (!customElements.get(name)) {
|
|
179
|
+
customElements.define(name, component);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { html };
|
|
184
|
+
export default { AfriCodeComponent, registerComponent, html };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Button Component
|
|
3
|
+
*
|
|
4
|
+
* Versatile button with multiple variants, sizes, and cultural themes.
|
|
5
|
+
* Suitable for: E-commerce, Healthcare, Education, Finance, Government
|
|
6
|
+
*
|
|
7
|
+
* @module components/button
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
11
|
+
|
|
12
|
+
export class AfriButton extends AfriCodeComponent {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['variant', 'size', 'theme', 'disabled', 'loading', 'icon', 'aria-label'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connectedCallback() {
|
|
23
|
+
const button = this.shadowRoot.querySelector('button');
|
|
24
|
+
button?.addEventListener('click', (e) => {
|
|
25
|
+
if (this.hasAttribute('disabled') || this.hasAttribute('loading')) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.emit('af-click', { originalEvent: e });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
attributeChangedCallback() {
|
|
34
|
+
this.render();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
const variant = this.getAttribute('variant') || 'primary';
|
|
39
|
+
const size = this.getAttribute('size') || 'md';
|
|
40
|
+
const theme = this.getAttribute('theme') || 'tanzania';
|
|
41
|
+
const isDisabled = this.hasAttribute('disabled');
|
|
42
|
+
const isLoading = this.hasAttribute('loading');
|
|
43
|
+
const icon = this.getAttribute('icon');
|
|
44
|
+
const ariaLabel = this.getAttribute('aria-label');
|
|
45
|
+
|
|
46
|
+
const themes = {
|
|
47
|
+
tanzania: { primary: '#1EB53A', accent: '#FCD116', text: '#FFFFFF' },
|
|
48
|
+
maasai: { primary: '#FF0000', accent: '#8B0000', text: '#FFFFFF' },
|
|
49
|
+
ndebele: { primary: '#4169E1', accent: '#FFD700', text: '#FFFFFF' },
|
|
50
|
+
ocean: { primary: '#00A3DD', accent: '#0077BE', text: '#FFFFFF' }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const t = themes[theme] || themes.tanzania;
|
|
54
|
+
|
|
55
|
+
this.shadowRoot.innerHTML = `
|
|
56
|
+
<style>
|
|
57
|
+
:host {
|
|
58
|
+
display: inline-block;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
button {
|
|
62
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
border: none;
|
|
65
|
+
border-radius: 5px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
transition: all 200ms ease-out;
|
|
72
|
+
position: relative;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Sizes */
|
|
77
|
+
.size-sm { padding: 5px 13px; font-size: 13px; }
|
|
78
|
+
.size-md { padding: 8px 21px; font-size: 15px; }
|
|
79
|
+
.size-lg { padding: 13px 34px; font-size: 17px; }
|
|
80
|
+
|
|
81
|
+
/* Variants */
|
|
82
|
+
.variant-primary {
|
|
83
|
+
background: ${t.primary};
|
|
84
|
+
color: ${t.text};
|
|
85
|
+
}
|
|
86
|
+
.variant-primary:hover:not(:disabled) {
|
|
87
|
+
filter: brightness(1.1);
|
|
88
|
+
transform: translateY(-2px);
|
|
89
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.variant-secondary {
|
|
93
|
+
background: transparent;
|
|
94
|
+
color: ${t.primary};
|
|
95
|
+
border: 2px solid ${t.primary};
|
|
96
|
+
}
|
|
97
|
+
.variant-secondary:hover:not(:disabled) {
|
|
98
|
+
background: ${t.primary};
|
|
99
|
+
color: ${t.text};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.variant-ghost {
|
|
103
|
+
background: transparent;
|
|
104
|
+
color: ${t.primary};
|
|
105
|
+
}
|
|
106
|
+
.variant-ghost:hover:not(:disabled) {
|
|
107
|
+
background: rgba(0,0,0,0.05);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.variant-gradient {
|
|
111
|
+
background: linear-gradient(135deg, ${t.primary} 0%, ${t.accent} 100%);
|
|
112
|
+
color: ${t.text};
|
|
113
|
+
}
|
|
114
|
+
.variant-gradient:hover:not(:disabled) {
|
|
115
|
+
transform: translateY(-2px);
|
|
116
|
+
box-shadow: 0 6px 20px rgba(0,0,0,0.25);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* States */
|
|
120
|
+
button:disabled {
|
|
121
|
+
opacity: 0.5;
|
|
122
|
+
cursor: not-allowed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
button:active:not(:disabled) {
|
|
126
|
+
transform: translateY(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Loading spinner */
|
|
130
|
+
.spinner {
|
|
131
|
+
width: 16px;
|
|
132
|
+
height: 16px;
|
|
133
|
+
border: 2px solid transparent;
|
|
134
|
+
border-top-color: currentColor;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
animation: spin 0.8s linear infinite;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes spin {
|
|
140
|
+
to { transform: rotate(360deg); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Icon */
|
|
144
|
+
.icon {
|
|
145
|
+
font-size: 1.1em;
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
148
|
+
|
|
149
|
+
<button
|
|
150
|
+
class="variant-${variant} size-${size}"
|
|
151
|
+
${isDisabled ? 'disabled' : ''}
|
|
152
|
+
${ariaLabel ? `aria-label="${ariaLabel}"` : ''}
|
|
153
|
+
${isLoading ? 'aria-busy="true"' : ''}
|
|
154
|
+
>
|
|
155
|
+
${isLoading ? '<span class="spinner"></span>' : ''}
|
|
156
|
+
${icon && !isLoading ? `<span class="icon">${icon}</span>` : ''}
|
|
157
|
+
<slot></slot>
|
|
158
|
+
</button>
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
registerComponent('af-button', AfriButton);
|
|
164
|
+
export default AfriButton;
|