@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,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;