@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,322 @@
1
+ /**
2
+ * AfriCode Select/Dropdown Component
3
+ *
4
+ * Custom styled select dropdown for forms.
5
+ * Suitable for: Forms, Filters, Settings, Language selection
6
+ *
7
+ * @module components/select
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+
12
+ export class AfriSelect extends AfriCodeComponent {
13
+ static get observedAttributes() {
14
+ return ['label', 'placeholder', 'theme', 'error', 'required', 'aria-label', 'aria-describedby'];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this._uniqueId = Math.random().toString(36).substr(2, 9);
20
+ this._isOpen = false;
21
+ this._selectedIndex = -1;
22
+ this._options = [];
23
+ this.render();
24
+ }
25
+
26
+ get value() {
27
+ return this._selectedIndex >= 0 ? this._options[this._selectedIndex]?.value : '';
28
+ }
29
+
30
+ connectedCallback() {
31
+ this._options = Array.from(this.querySelectorAll('option')).map(opt => ({
32
+ value: opt.value,
33
+ label: opt.textContent
34
+ }));
35
+ this.render();
36
+ this._setupListeners();
37
+ }
38
+
39
+ _setupListeners() {
40
+ const trigger = this.shadowRoot.querySelector('.select-trigger');
41
+ const dropdown = this.shadowRoot.querySelector('.select-dropdown');
42
+
43
+ trigger?.addEventListener('click', () => {
44
+ this._toggleDropdown();
45
+ });
46
+
47
+ trigger?.addEventListener('keydown', (e) => {
48
+ this._handleKeydown(e);
49
+ });
50
+
51
+ this.shadowRoot.querySelectorAll('.select-option').forEach((opt, i) => {
52
+ opt.addEventListener('click', () => {
53
+ this._selectOption(i);
54
+ });
55
+ });
56
+
57
+ document.addEventListener('click', (e) => {
58
+ if (!this.contains(e.target) && this._isOpen) {
59
+ this._closeDropdown();
60
+ }
61
+ });
62
+ }
63
+
64
+ _toggleDropdown() {
65
+ this._isOpen = !this._isOpen;
66
+ const trigger = this.shadowRoot.querySelector('.select-trigger');
67
+ const dropdown = this.shadowRoot.querySelector('.select-dropdown');
68
+
69
+ dropdown?.classList.toggle('open', this._isOpen);
70
+ trigger?.classList.toggle('open', this._isOpen);
71
+
72
+ if (this._isOpen) {
73
+ trigger?.setAttribute('aria-expanded', 'true');
74
+ dropdown?.setAttribute('aria-hidden', 'false');
75
+ } else {
76
+ trigger?.setAttribute('aria-expanded', 'false');
77
+ dropdown?.setAttribute('aria-hidden', 'true');
78
+ }
79
+ }
80
+
81
+ _closeDropdown() {
82
+ this._isOpen = false;
83
+ const trigger = this.shadowRoot.querySelector('.select-trigger');
84
+ const dropdown = this.shadowRoot.querySelector('.select-dropdown');
85
+
86
+ dropdown?.classList.remove('open');
87
+ trigger?.classList.remove('open');
88
+ trigger?.setAttribute('aria-expanded', 'false');
89
+ dropdown?.setAttribute('aria-hidden', 'true');
90
+ }
91
+
92
+ _selectOption(index) {
93
+ this._selectedIndex = index;
94
+ this._closeDropdown();
95
+ this.render();
96
+ this._setupListeners();
97
+ this.emit('af-change', { value: this.value, label: this._options[index].label });
98
+ }
99
+
100
+ _handleKeydown(e) {
101
+ const trigger = this.shadowRoot.querySelector('.select-trigger');
102
+
103
+ switch (e.key) {
104
+ case 'Enter':
105
+ case ' ':
106
+ e.preventDefault();
107
+ this._toggleDropdown();
108
+ break;
109
+ case 'Escape':
110
+ if (this._isOpen) {
111
+ e.preventDefault();
112
+ this._closeDropdown();
113
+ trigger.focus();
114
+ }
115
+ break;
116
+ case 'ArrowDown':
117
+ if (!this._isOpen) {
118
+ e.preventDefault();
119
+ this._toggleDropdown();
120
+ } else {
121
+ e.preventDefault();
122
+ this._navigateOptions(1);
123
+ }
124
+ break;
125
+ case 'ArrowUp':
126
+ if (this._isOpen) {
127
+ e.preventDefault();
128
+ this._navigateOptions(-1);
129
+ }
130
+ break;
131
+ }
132
+ }
133
+
134
+ _navigateOptions(direction) {
135
+ const options = this.shadowRoot.querySelectorAll('.select-option');
136
+ if (options.length === 0) {return;}
137
+
138
+ let newIndex = this._selectedIndex + direction;
139
+ if (newIndex < 0) {newIndex = options.length - 1;}
140
+ if (newIndex >= options.length) {newIndex = 0;}
141
+
142
+ this._selectedIndex = newIndex;
143
+ this.render();
144
+ this._setupListeners();
145
+
146
+ // Focus the selected option
147
+ const selectedOption = this.shadowRoot.querySelector('.select-option.selected');
148
+ selectedOption?.scrollIntoView({ block: 'nearest' });
149
+ }
150
+
151
+ attributeChangedCallback() {
152
+ this.render();
153
+ this._setupListeners();
154
+ }
155
+
156
+ render() {
157
+ const label = this.getAttribute('label') || '';
158
+ const placeholder = this.getAttribute('placeholder') || 'Select an option';
159
+ const theme = this.getAttribute('theme') || 'tanzania';
160
+ const error = this.getAttribute('error') || '';
161
+ const required = this.hasAttribute('required');
162
+ const ariaLabel = this.getAttribute('aria-label');
163
+ const ariaDescribedBy = this.getAttribute('aria-describedby');
164
+
165
+ const themes = {
166
+ tanzania: { focus: '#1EB53A', error: '#DC3545' },
167
+ maasai: { focus: '#FF0000', error: '#8B0000' },
168
+ ndebele: { focus: '#4169E1', error: '#DC143C' }
169
+ };
170
+ const t = themes[theme] || themes.tanzania;
171
+
172
+ const describedByList = [];
173
+ if (error) {describedByList.push(`error-${this._uniqueId}`);}
174
+ if (ariaDescribedBy) {describedByList.push(...ariaDescribedBy.split(' '));}
175
+ const combinedDescribedBy = describedByList.length > 0 ? `aria-describedby="${describedByList.join(' ')}"` : '';
176
+
177
+ const selectedLabel = this._selectedIndex >= 0
178
+ ? this._options[this._selectedIndex]?.label
179
+ : placeholder;
180
+
181
+ this.shadowRoot.innerHTML = `
182
+ <style>
183
+ :host {
184
+ display: block;
185
+ font-family: 'Inter', system-ui, sans-serif;
186
+ position: relative;
187
+ }
188
+
189
+ label {
190
+ display: block;
191
+ font-weight: 500;
192
+ margin-bottom: 5px;
193
+ color: #333;
194
+ font-size: 14px;
195
+ }
196
+
197
+ .required-star {
198
+ color: ${t.error};
199
+ margin-left: 2px;
200
+ }
201
+
202
+ .select-container {
203
+ position: relative;
204
+ }
205
+
206
+ .select-trigger {
207
+ width: 100%;
208
+ padding: 10px 40px 10px 13px;
209
+ font-size: 15px;
210
+ border: 2px solid ${error ? t.error : '#ddd'};
211
+ border-radius: 5px;
212
+ background: white;
213
+ cursor: pointer;
214
+ text-align: left;
215
+ font-family: inherit;
216
+ transition: all 200ms ease;
217
+ color: ${this._selectedIndex >= 0 ? '#333' : '#888'};
218
+ }
219
+
220
+ .select-trigger:focus,
221
+ .select-trigger.open {
222
+ outline: none;
223
+ border-color: ${error ? t.error : t.focus};
224
+ box-shadow: 0 0 0 3px ${error ? t.error + '20' : t.focus + '20'};
225
+ }
226
+
227
+ .select-arrow {
228
+ position: absolute;
229
+ right: 13px;
230
+ top: 50%;
231
+ transform: translateY(-50%);
232
+ pointer-events: none;
233
+ transition: transform 200ms ease;
234
+ }
235
+
236
+ .select-trigger.open + .select-arrow {
237
+ transform: translateY(-50%) rotate(180deg);
238
+ }
239
+
240
+ .select-dropdown {
241
+ position: absolute;
242
+ top: 100%;
243
+ left: 0;
244
+ right: 0;
245
+ background: white;
246
+ border: 2px solid #ddd;
247
+ border-radius: 5px;
248
+ margin-top: 4px;
249
+ max-height: 0;
250
+ overflow: hidden;
251
+ opacity: 0;
252
+ transition: all 200ms ease;
253
+ z-index: 1000;
254
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
255
+ }
256
+
257
+ .select-dropdown.open {
258
+ max-height: 200px;
259
+ overflow-y: auto;
260
+ opacity: 1;
261
+ }
262
+
263
+ .select-option {
264
+ padding: 10px 13px;
265
+ cursor: pointer;
266
+ transition: background 150ms ease;
267
+ }
268
+
269
+ .select-option:hover {
270
+ background: ${t.focus}15;
271
+ }
272
+
273
+ .select-option.selected {
274
+ background: ${t.focus};
275
+ color: white;
276
+ }
277
+
278
+ .error-text {
279
+ font-size: 12px;
280
+ margin-top: 4px;
281
+ color: ${t.error};
282
+ }
283
+
284
+ ::slotted(option) {
285
+ display: none;
286
+ }
287
+ </style>
288
+
289
+ ${label ? `<label for="select-${this._uniqueId}">${label}${required ? '<span class="required-star">*</span>' : ''}</label>` : ''}
290
+ <div class="select-container">
291
+ <button type="button"
292
+ id="select-${this._uniqueId}"
293
+ class="select-trigger ${this._isOpen ? 'open' : ''}"
294
+ aria-expanded="${this._isOpen}"
295
+ aria-haspopup="listbox"
296
+ ${ariaLabel ? `aria-label="${ariaLabel}"` : ''}
297
+ ${error ? 'aria-invalid="true"' : ''}
298
+ ${combinedDescribedBy}>
299
+ ${selectedLabel}
300
+ </button>
301
+ <span class="select-arrow" aria-hidden="true">▼</span>
302
+ <div class="select-dropdown ${this._isOpen ? 'open' : ''}"
303
+ role="listbox"
304
+ aria-hidden="${!this._isOpen}">
305
+ ${this._options.map((opt, i) => `
306
+ <div class="select-option ${i === this._selectedIndex ? 'selected' : ''}"
307
+ data-index="${i}"
308
+ role="option"
309
+ aria-selected="${i === this._selectedIndex}">
310
+ ${opt.label}
311
+ </div>
312
+ `).join('')}
313
+ </div>
314
+ </div>
315
+ ${error ? `<div id="error-${this._uniqueId}" class="error-text" role="alert">${error}</div>` : ''}
316
+ <slot></slot>
317
+ `;
318
+ }
319
+ }
320
+
321
+ registerComponent('af-select', AfriSelect);
322
+ export default AfriSelect;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * AfriCode Sidebar Component
3
+ *
4
+ * Collapsible side navigation with pattern header.
5
+ * Reactively binded to the Global Store.
6
+ *
7
+ * @module components/sidebar
8
+ */
9
+
10
+ import { AfriCodeComponent, registerComponent } from './base.js';
11
+ import { store } from '../core/store.js';
12
+ import { subscribe } from '../core/state.js';
13
+
14
+ export class AfriSidebar extends AfriCodeComponent {
15
+ static get observedAttributes() {
16
+ return ['position'];
17
+ }
18
+
19
+ constructor() {
20
+ super();
21
+ this._unsubscribe = null;
22
+ }
23
+
24
+ connectedCallback() {
25
+ super.connectedCallback();
26
+
27
+ // Subscribe to store updates
28
+ this._unsubscribe = subscribe(() => {
29
+ this.updateUI();
30
+ });
31
+
32
+ // Initial render
33
+ this.render();
34
+ }
35
+
36
+ disconnectedCallback() {
37
+ if (this._unsubscribe) {
38
+ this._unsubscribe();
39
+ }
40
+ }
41
+
42
+ updateUI() {
43
+ // Reactive update: minimal DOM manipulation
44
+ const collapsed = store.ui.sidebarCollapsed;
45
+ const sidebar = this.shadowRoot.querySelector('.sidebar');
46
+ const title = this.shadowRoot.querySelector('.sidebar-title');
47
+ const toggleBtn = this.shadowRoot.getElementById('toggle');
48
+ const content = this.shadowRoot.querySelector('.sidebar-content');
49
+
50
+ // Apply changes if elements exist (shadowDOM might be lazy)
51
+ if (sidebar) {sidebar.style.width = collapsed ? '60px' : '260px';}
52
+ if (title) {title.style.opacity = collapsed ? '0' : '1';}
53
+ if (content) {content.style.padding = collapsed ? '12px' : '20px';}
54
+ if (toggleBtn) {toggleBtn.innerHTML = collapsed ? '→' : '← Collapse';}
55
+ }
56
+
57
+ render() {
58
+ const position = this.getAttribute('position') || 'left';
59
+ // Read initial state from store
60
+ const collapsed = store.ui.sidebarCollapsed;
61
+
62
+ this.shadowRoot.innerHTML = `
63
+ <style>
64
+ :host {
65
+ display: block;
66
+ position: fixed;
67
+ top: 0;
68
+ ${position}: 0;
69
+ height: 100vh;
70
+ z-index: 900;
71
+ font-family: 'Inter', system-ui, sans-serif;
72
+ }
73
+
74
+ .sidebar {
75
+ width: ${collapsed ? '60px' : '260px'};
76
+ height: 100%;
77
+ background: #121212;
78
+ border-${position === 'left' ? 'right' : 'left'}: 1px solid #1e1e1e;
79
+ display: flex;
80
+ flex-direction: column;
81
+ transition: width 0.3s var(--ease-drum, ease);
82
+ overflow: hidden;
83
+ }
84
+
85
+ .sidebar-header {
86
+ padding: 20px;
87
+ border-bottom: 1px solid #1e1e1e;
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 12px;
91
+ }
92
+
93
+ .sidebar-logo {
94
+ width: 32px;
95
+ height: 32px;
96
+ background: linear-gradient(135deg, #1EB53A, #FCD116);
97
+ border-radius: 8px;
98
+ flex-shrink: 0;
99
+ }
100
+
101
+ .sidebar-title {
102
+ font-weight: 700;
103
+ font-size: 1.1rem;
104
+ color: #FCD116;
105
+ white-space: nowrap;
106
+ opacity: ${collapsed ? 0 : 1};
107
+ transition: opacity 0.2s;
108
+ }
109
+
110
+ .sidebar-content {
111
+ flex: 1;
112
+ padding: ${collapsed ? '12px' : '20px'};
113
+ overflow-y: auto;
114
+ }
115
+
116
+ .sidebar-footer {
117
+ padding: 16px;
118
+ border-top: 1px solid #1e1e1e;
119
+ }
120
+
121
+ .toggle-btn {
122
+ width: 100%;
123
+ padding: 10px;
124
+ background: #1e1e1e;
125
+ border: none;
126
+ border-radius: 8px;
127
+ color: #a0a0a0;
128
+ cursor: pointer;
129
+ font-size: 0.85rem;
130
+ transition: all 0.2s;
131
+ }
132
+
133
+ .toggle-btn:hover {
134
+ background: #2a2a2a;
135
+ color: #1EB53A;
136
+ }
137
+
138
+ /* Slotted Content Styles */
139
+ ::slotted(a), ::slotted(button) {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 12px;
143
+ padding: 12px 16px;
144
+ color: #a0a0a0;
145
+ text-decoration: none;
146
+ border-radius: 8px;
147
+ margin-bottom: 4px;
148
+ transition: all 0.2s;
149
+ }
150
+
151
+ ::slotted(a:hover), ::slotted(button:hover) {
152
+ background: #1e1e1e;
153
+ color: #1EB53A;
154
+ }
155
+ </style>
156
+
157
+ <aside class="sidebar">
158
+ <div class="sidebar-header">
159
+ <div class="sidebar-logo"></div>
160
+ <span class="sidebar-title">AfriCode</span>
161
+ </div>
162
+ <nav class="sidebar-content">
163
+ <slot></slot>
164
+ </nav>
165
+ <div class="sidebar-footer">
166
+ <button class="toggle-btn" id="toggle">
167
+ ${collapsed ? '→' : '← Collapse'}
168
+ </button>
169
+ </div>
170
+ </aside>
171
+ `;
172
+
173
+ this.shadowRoot.getElementById('toggle').addEventListener('click', () => {
174
+ actions.toggleSidebar(); // Dispatch action to store
175
+ });
176
+ }
177
+ }
178
+
179
+ registerComponent('af-sidebar', AfriSidebar);
180
+ export default AfriSidebar;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AfriCode Skeleton Component
3
+ *
4
+ * Loading placeholder with shimmer animation.
5
+ *
6
+ * @module components/skeleton
7
+ */
8
+
9
+ import { AfriCodeComponent, registerComponent } from './base.js';
10
+
11
+ export class AfriSkeleton extends AfriCodeComponent {
12
+ static get observedAttributes() {
13
+ return ['variant', 'width', 'height', 'lines'];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this.render();
19
+ }
20
+
21
+ attributeChangedCallback() {
22
+ this.render();
23
+ }
24
+
25
+ render() {
26
+ const variant = this.getAttribute('variant') || 'text';
27
+ const width = this.getAttribute('width') || '100%';
28
+ const height = this.getAttribute('height') || 'auto';
29
+ const lines = parseInt(this.getAttribute('lines')) || 1;
30
+
31
+ const variants = {
32
+ text: { height: '16px', borderRadius: '4px' },
33
+ title: { height: '28px', borderRadius: '4px' },
34
+ avatar: { width: '48px', height: '48px', borderRadius: '50%' },
35
+ card: { height: '200px', borderRadius: '12px' },
36
+ button: { height: '40px', borderRadius: '6px', width: '120px' }
37
+ };
38
+
39
+ const v = variants[variant] || variants.text;
40
+
41
+ this.shadowRoot.innerHTML = `
42
+ <style>
43
+ :host {
44
+ display: block;
45
+ }
46
+
47
+ .skeleton {
48
+ background: linear-gradient(
49
+ 90deg,
50
+ #1e1e1e 0%,
51
+ #2a2a2a 50%,
52
+ #1e1e1e 100%
53
+ );
54
+ background-size: 200% 100%;
55
+ animation: shimmer 1.5s infinite;
56
+ border-radius: ${v.borderRadius};
57
+ }
58
+
59
+ @keyframes shimmer {
60
+ 0% { background-position: 200% 0; }
61
+ 100% { background-position: -200% 0; }
62
+ }
63
+
64
+ .skeleton-line {
65
+ width: ${variant === 'text' ? width : v.width || width};
66
+ height: ${v.height};
67
+ margin-bottom: 8px;
68
+ }
69
+
70
+ .skeleton-line:last-child {
71
+ margin-bottom: 0;
72
+ width: ${variant === 'text' ? '70%' : '100%'};
73
+ }
74
+ </style>
75
+
76
+ ${variant === 'text' || variant === 'title'
77
+ ? Array(lines).fill(0).map(() => `<div class="skeleton skeleton-line"></div>`).join('')
78
+ : `<div class="skeleton" style="width: ${v.width || width}; height: ${v.height || height};"></div>`
79
+ }
80
+ `;
81
+ }
82
+ }
83
+
84
+ registerComponent('af-skeleton', AfriSkeleton);
85
+ export default AfriSkeleton;