@africode/core 5.0.7 → 5.0.9

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 (64) hide show
  1. package/AGENTS.md +2 -0
  2. package/AGENT_INSTRUCTIONS.md +595 -595
  3. package/COMPONENT_SCHEMA.json +1800 -990
  4. package/README.md +2 -0
  5. package/bin/create-africode.js +87 -25
  6. package/components/auth-form.js +154 -0
  7. package/components/index.js +15 -0
  8. package/components/kyc-upload.js +173 -0
  9. package/components/nav-drawer.js +217 -0
  10. package/components/transaction-ledger.js +138 -0
  11. package/components/wallet-balance.js +114 -0
  12. package/core/a2ui-schema-manager.js +1 -1
  13. package/core/a2ui.js +178 -1
  14. package/core/bun-runtime.js +122 -7
  15. package/core/cli/commands/build.js +30 -5
  16. package/core/cli/ui.js +13 -3
  17. package/core/compliance.js +201 -0
  18. package/core/middleware.js +80 -17
  19. package/core/patterns.js +168 -0
  20. package/core/request-analytics.js +254 -0
  21. package/core/request-identity.js +79 -29
  22. package/core/sdk.js +4 -1
  23. package/core/validation.js +13 -0
  24. package/dist/africode.js +858 -457
  25. package/dist/africode.js.map +14 -9
  26. package/dist/build-info.json +3 -3
  27. package/dist/components.js +784 -383
  28. package/dist/components.js.map +11 -6
  29. package/package.json +1 -1
  30. package/templates/starter/package.json +5 -5
  31. package/templates/starter/src/index.js +18 -0
  32. package/templates/starter/src/pages/index.js +1 -1
  33. package/templates/starter-3d/package.json +5 -5
  34. package/templates/starter-3d/src/pages/index.js +1 -1
  35. package/templates/starter-dashboard/.env.example +21 -0
  36. package/templates/starter-dashboard/africode.config.js +20 -0
  37. package/templates/starter-dashboard/package.json +14 -0
  38. package/templates/starter-dashboard/src/index.js +17 -0
  39. package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
  40. package/templates/starter-dashboard/src/pages/index.html +118 -0
  41. package/templates/starter-dashboard/src/pages/index.js +110 -0
  42. package/templates/starter-dashboard/src/styles/main.css +172 -0
  43. package/templates/starter-fintech/.env.example +28 -0
  44. package/templates/starter-fintech/africode.config.js +20 -0
  45. package/templates/starter-fintech/package.json +15 -0
  46. package/templates/starter-fintech/src/index.js +17 -0
  47. package/templates/starter-fintech/src/pages/api/auth.js +45 -0
  48. package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
  49. package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
  50. package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
  51. package/templates/starter-fintech/src/pages/index.html +169 -0
  52. package/templates/starter-fintech/src/pages/index.js +161 -0
  53. package/templates/starter-fintech/src/styles/main.css +246 -0
  54. package/templates/starter-react/package.json +5 -5
  55. package/templates/starter-react/src/pages/index.js +1 -1
  56. package/templates/starter-tailwind/package.json +5 -5
  57. package/templates/starter-tailwind/src/pages/index.js +1 -1
  58. package/templates/starter-website/.env.example +18 -0
  59. package/templates/starter-website/africode.config.js +20 -0
  60. package/templates/starter-website/package.json +14 -0
  61. package/templates/starter-website/src/index.js +17 -0
  62. package/templates/starter-website/src/pages/index.html +124 -0
  63. package/templates/starter-website/src/pages/index.js +116 -0
  64. package/templates/starter-website/src/styles/main.css +195 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * AfriCode Navigation Drawer Component
3
+ *
4
+ * A responsive navigation drawer that works on mobile and desktop
5
+ * with smooth animations and accessibility features.
6
+ *
7
+ * @module components/nav-drawer
8
+ */
9
+
10
+ import { AfriCodeComponent, html } from './base.js';
11
+
12
+ export class AfriNavDrawer extends AfriCodeComponent {
13
+ constructor() {
14
+ super();
15
+ this.isOpen = false;
16
+ this.position = this.getAttribute('position') || 'left'; // left or right
17
+ this.width = this.getAttribute('width') || '280px';
18
+ this.overlay = this.hasAttribute('overlay');
19
+ }
20
+
21
+ static get observedAttributes() {
22
+ return ['open', 'position', 'width', 'overlay'];
23
+ }
24
+
25
+ attributeChangedCallback(name, oldValue, newValue) {
26
+ switch (name) {
27
+ case 'open':
28
+ this.isOpen = newValue !== null;
29
+ this.render();
30
+ break;
31
+ case 'position':
32
+ this.position = newValue;
33
+ break;
34
+ case 'width':
35
+ this.width = newValue;
36
+ break;
37
+ case 'overlay':
38
+ this.overlay = newValue !== null;
39
+ break;
40
+ }
41
+ }
42
+
43
+ connectedCallback() {
44
+ super.connectedCallback();
45
+ this.addEventListener('keydown', this.handleKeyDown);
46
+ this.loadStyles();
47
+ }
48
+
49
+ disconnectedCallback() {
50
+ this.removeEventListener('keydown', this.handleKeyDown);
51
+ }
52
+
53
+ handleKeyDown = (e) => {
54
+ if (e.key === 'Escape' && this.isOpen) {
55
+ this.close();
56
+ }
57
+ }
58
+
59
+ toggle() {
60
+ this.isOpen ? this.close() : this.open();
61
+ }
62
+
63
+ open() {
64
+ this.isOpen = true;
65
+ this.setAttribute('open', '');
66
+ this.render();
67
+ this.emit('af-navdrawer-open', { drawer: this });
68
+ }
69
+
70
+ close() {
71
+ this.isOpen = false;
72
+ this.removeAttribute('open');
73
+ this.render();
74
+ this.emit('af-navdrawer-close', { drawer: this });
75
+ }
76
+
77
+ render() {
78
+ if (!this.shadowRoot) return;
79
+
80
+ // Apply styles first
81
+ this.shadowRoot.innerHTML = `
82
+ <style>
83
+ :host {
84
+ display: block;
85
+ position: fixed;
86
+ z-index: 1000;
87
+ }
88
+
89
+ .nav-drawer {
90
+ position: fixed;
91
+ top: 0;
92
+ ${this.position}: 0;
93
+ width: ${this.width};
94
+ height: 100vh;
95
+ background: var(--af-bg, #ffffff);
96
+ box-shadow: var(--af-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
97
+ transform: translateX(${this.position === 'left' ? '-100%' : '100%'});
98
+ transition: transform 0.3s ease-in-out;
99
+ overflow-y: auto;
100
+ z-index: 1001;
101
+ }
102
+
103
+ .nav-drawer.open {
104
+ transform: translateX(0);
105
+ }
106
+
107
+ .nav-drawer-overlay {
108
+ position: fixed;
109
+ top: 0;
110
+ left: 0;
111
+ width: 100vw;
112
+ height: 100vh;
113
+ background: rgba(0, 0, 0, 0.5);
114
+ opacity: 0;
115
+ visibility: hidden;
116
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
117
+ z-index: 1000;
118
+ }
119
+
120
+ .nav-drawer-overlay.open {
121
+ opacity: 1;
122
+ visibility: visible;
123
+ }
124
+
125
+ .nav-drawer-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: center;
129
+ padding: 1rem;
130
+ border-bottom: 1px solid var(--af-border-color, #e5e7eb);
131
+ }
132
+
133
+ .nav-drawer-title {
134
+ margin: 0;
135
+ font-size: 1.25rem;
136
+ font-weight: 600;
137
+ }
138
+
139
+ .close-btn {
140
+ background: none;
141
+ border: none;
142
+ font-size: 1.5rem;
143
+ cursor: pointer;
144
+ padding: 0.25rem;
145
+ border-radius: 4px;
146
+ }
147
+
148
+ .close-btn:hover {
149
+ background: var(--af-hover-bg, #f3f4f6);
150
+ }
151
+
152
+ .nav-drawer-content {
153
+ padding: 1rem;
154
+ }
155
+
156
+ .nav-item {
157
+ display: block;
158
+ padding: 0.75rem 1rem;
159
+ text-decoration: none;
160
+ color: var(--af-text, #1f2937);
161
+ border-radius: 6px;
162
+ margin-bottom: 0.25rem;
163
+ transition: background-color 0.2s;
164
+ }
165
+
166
+ .nav-item:hover {
167
+ background-color: var(--af-hover-bg, #f3f4f6);
168
+ }
169
+
170
+ .nav-item.active {
171
+ background-color: var(--af-primary, #10b981);
172
+ color: white;
173
+ }
174
+ </style>
175
+
176
+ ${this.overlay ? `<div class="nav-drawer-overlay ${this.isOpen ? 'open' : ''}" part="overlay"></div>` : ''}
177
+
178
+ <aside class="nav-drawer ${this.isOpen ? 'open' : ''}" part="drawer" role="dialog" aria-modal="true" aria-label="Navigation menu">
179
+ <header class="nav-drawer-header" part="header">
180
+ <h2 class="nav-drawer-title" part="title"><slot name="title">Menu</slot></h2>
181
+ <button class="close-btn" part="close-button" @click="${() => this.close()}" aria-label="Close menu">✕</button>
182
+ </header>
183
+ <div class="nav-drawer-content" part="content">
184
+ <slot name="content"></slot>
185
+ </div>
186
+ </aside>
187
+ `;
188
+
189
+ // Add event listener to overlay if it exists
190
+ if (this.overlay) {
191
+ const overlay = this.shadowRoot.querySelector('.nav-drawer-overlay');
192
+ if (overlay) {
193
+ overlay.addEventListener('click', () => this.close());
194
+ }
195
+ }
196
+ }
197
+
198
+ // Allow programmatic access to menu items
199
+ getItems() {
200
+ return this.querySelectorAll('[slot="content"] .nav-item');
201
+ }
202
+
203
+ setActiveItem(selector) {
204
+ const items = this.getItems();
205
+ items.forEach(item => item.classList.remove('active'));
206
+ const activeItem = this.querySelector(selector);
207
+ if (activeItem) {
208
+ activeItem.classList.add('active');
209
+ }
210
+ }
211
+ }
212
+
213
+ // Register the component
214
+ import { registerComponent } from './base.js';
215
+ registerComponent('af-nav-drawer', AfriNavDrawer);
216
+
217
+ export default AfriNavDrawer;
@@ -0,0 +1,138 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+
3
+ const styles = `
4
+ :host {
5
+ display: block;
6
+ color: var(--af-smart-text, var(--af-text, #172033));
7
+ font-family: var(--af-font-family, Inter, system-ui, sans-serif);
8
+ }
9
+
10
+ .ledger {
11
+ display: grid;
12
+ gap: 10px;
13
+ }
14
+
15
+ .row {
16
+ display: grid;
17
+ grid-template-columns: 1fr auto;
18
+ gap: 12px;
19
+ padding: 12px;
20
+ border: 1px solid var(--af-smart-border, var(--af-border, #d8dee8));
21
+ border-radius: 8px;
22
+ background: var(--af-smart-surface, var(--af-surface, #ffffff));
23
+ }
24
+
25
+ .status,
26
+ .empty {
27
+ color: var(--af-smart-muted, var(--af-muted, #647084));
28
+ font-size: 0.84rem;
29
+ }
30
+
31
+ .amount {
32
+ font-weight: 760;
33
+ }
34
+
35
+ .error {
36
+ color: var(--af-smart-danger, var(--af-danger, #b42318));
37
+ }
38
+ `;
39
+
40
+ function formatCurrency(value, currency) {
41
+ return `${currency || 'TZS'} ${Number(value || 0).toLocaleString('en-US')}`;
42
+ }
43
+
44
+ export class AfriTransactionLedger extends AfriCodeComponent {
45
+ static get observedAttributes() {
46
+ return ['endpoint'];
47
+ }
48
+
49
+ connectedCallback() {
50
+ this.renderShell('Loading transactions');
51
+ this.load();
52
+ }
53
+
54
+ attributeChangedCallback() {
55
+ if (this.isConnected) {
56
+ this.load();
57
+ }
58
+ }
59
+
60
+ get endpoint() {
61
+ return this.getAttribute('endpoint') || '/api/wallet';
62
+ }
63
+
64
+ renderShell(message = '') {
65
+ this.shadowRoot.innerHTML = `
66
+ <style>${styles}</style>
67
+ <div class="ledger" role="list"></div>
68
+ `;
69
+
70
+ if (message) {
71
+ const empty = document.createElement('div');
72
+ empty.className = 'empty';
73
+ empty.textContent = message;
74
+ this.shadowRoot.querySelector('.ledger').append(empty);
75
+ }
76
+ }
77
+
78
+ renderTransactions(transactions) {
79
+ this.renderShell();
80
+ const ledger = this.shadowRoot.querySelector('.ledger');
81
+
82
+ if (!transactions.length) {
83
+ const empty = document.createElement('div');
84
+ empty.className = 'empty';
85
+ empty.textContent = 'No transactions yet.';
86
+ ledger.append(empty);
87
+ return;
88
+ }
89
+
90
+ ledger.replaceChildren(...transactions.map((tx) => {
91
+ const row = document.createElement('article');
92
+ const details = document.createElement('div');
93
+ const counterparty = document.createElement('strong');
94
+ const status = document.createElement('div');
95
+ const amount = document.createElement('span');
96
+
97
+ row.className = 'row';
98
+ row.setAttribute('role', 'listitem');
99
+ status.className = 'status';
100
+ amount.className = 'amount';
101
+ counterparty.textContent = tx.counterparty || tx.id || 'Transaction';
102
+ status.textContent = tx.status || tx.type || 'pending';
103
+ amount.textContent = formatCurrency(tx.amount, tx.currency);
104
+
105
+ details.append(counterparty, status);
106
+ row.append(details, amount);
107
+ return row;
108
+ }));
109
+ }
110
+
111
+ renderError(message) {
112
+ this.shadowRoot.innerHTML = `
113
+ <style>${styles}</style>
114
+ <div class="empty error"></div>
115
+ `;
116
+ this.shadowRoot.querySelector('.empty').textContent = message;
117
+ }
118
+
119
+ async load() {
120
+ try {
121
+ const response = await fetch(this.endpoint);
122
+ const data = await response.json();
123
+ const transactions = Array.isArray(data.transactions) ? data.transactions : [];
124
+
125
+ if (!response.ok || data.ok === false) {
126
+ throw new Error(data.error || 'Ledger data unavailable.');
127
+ }
128
+
129
+ this.renderTransactions(transactions);
130
+ this.emit('af-ledger-load', { transactions });
131
+ } catch (error) {
132
+ this.renderError('Transaction ledger unavailable.');
133
+ this.emit('af-ledger-error', { message: error.message });
134
+ }
135
+ }
136
+ }
137
+
138
+ registerComponent('af-transaction-ledger', AfriTransactionLedger);
@@ -0,0 +1,114 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+
3
+ const styles = `
4
+ :host {
5
+ display: block;
6
+ color: var(--af-smart-text, var(--af-text, #172033));
7
+ font-family: var(--af-font-family, Inter, system-ui, sans-serif);
8
+ }
9
+
10
+ .wallet {
11
+ display: grid;
12
+ gap: 8px;
13
+ padding: 16px;
14
+ border: 1px solid var(--af-smart-border, var(--af-border, #d8dee8));
15
+ border-radius: 8px;
16
+ background: var(--af-smart-surface, var(--af-surface, #ffffff));
17
+ }
18
+
19
+ .label,
20
+ .meta {
21
+ color: var(--af-smart-muted, var(--af-muted, #647084));
22
+ font-size: 0.84rem;
23
+ }
24
+
25
+ .amount {
26
+ font-size: 1.8rem;
27
+ font-weight: 760;
28
+ }
29
+
30
+ .error {
31
+ color: var(--af-smart-danger, var(--af-danger, #b42318));
32
+ }
33
+ `;
34
+
35
+ function formatCurrency(value, currency) {
36
+ return `${currency || 'TZS'} ${Number(value || 0).toLocaleString('en-US')}`;
37
+ }
38
+
39
+ export class AfriWalletBalance extends AfriCodeComponent {
40
+ static get observedAttributes() {
41
+ return ['endpoint'];
42
+ }
43
+
44
+ connectedCallback() {
45
+ this.renderLoading();
46
+ this.load();
47
+ }
48
+
49
+ attributeChangedCallback() {
50
+ if (this.isConnected) {
51
+ this.load();
52
+ }
53
+ }
54
+
55
+ get endpoint() {
56
+ return this.getAttribute('endpoint') || '/api/wallet';
57
+ }
58
+
59
+ renderLoading() {
60
+ this.shadowRoot.innerHTML = `
61
+ <style>${styles}</style>
62
+ <article class="wallet">
63
+ <span class="label">Available balance</span>
64
+ <strong class="amount">Loading</strong>
65
+ <span class="meta">Fetching wallet</span>
66
+ </article>
67
+ `;
68
+ }
69
+
70
+ renderWallet(wallet) {
71
+ this.shadowRoot.innerHTML = `
72
+ <style>${styles}</style>
73
+ <article class="wallet">
74
+ <span class="label">Available balance</span>
75
+ <strong class="amount"></strong>
76
+ <span class="meta"></span>
77
+ </article>
78
+ `;
79
+
80
+ this.shadowRoot.querySelector('.amount').textContent = formatCurrency(wallet.availableBalance, wallet.currency);
81
+ this.shadowRoot.querySelector('.meta').textContent = `Ledger ${formatCurrency(wallet.ledgerBalance, wallet.currency)} · Daily limit ${formatCurrency(wallet.dailyLimit, wallet.currency)}`;
82
+ }
83
+
84
+ renderError(message) {
85
+ this.shadowRoot.innerHTML = `
86
+ <style>${styles}</style>
87
+ <article class="wallet">
88
+ <span class="label">Available balance</span>
89
+ <strong class="amount error">Unavailable</strong>
90
+ <span class="meta error"></span>
91
+ </article>
92
+ `;
93
+ this.shadowRoot.querySelector('.meta').textContent = message;
94
+ }
95
+
96
+ async load() {
97
+ try {
98
+ const response = await fetch(this.endpoint);
99
+ const data = await response.json();
100
+
101
+ if (!response.ok || data.ok === false || !data.wallet) {
102
+ throw new Error(data.error || 'Wallet data unavailable.');
103
+ }
104
+
105
+ this.renderWallet(data.wallet);
106
+ this.emit('af-wallet-load', data);
107
+ } catch (error) {
108
+ this.renderError('Wallet API is unavailable.');
109
+ this.emit('af-wallet-error', { message: error.message });
110
+ }
111
+ }
112
+ }
113
+
114
+ registerComponent('af-wallet-balance', AfriWalletBalance);
@@ -146,7 +146,7 @@ export class A2UISchemaManager {
146
146
  */
147
147
  extractComponentMetadata(filename, content) {
148
148
  // Extract tagName
149
- const tagMatch = content.match(/customElements\.define\(['"]([a-z0-9-]+)['"]/);
149
+ const tagMatch = content.match(/(?:customElements\.define|registerComponent)\(['"]([a-z0-9-]+)['"]/);
150
150
  if (!tagMatch) return null;
151
151
 
152
152
  const tagName = tagMatch[1];
package/core/a2ui.js CHANGED
@@ -20,7 +20,13 @@ export const A2UI_TYPES = {
20
20
  CARD: 'card',
21
21
  MODAL: 'modal',
22
22
  TOAST: 'toast',
23
- PROGRESS: 'progress'
23
+ PROGRESS: 'progress',
24
+ CHART: 'chart',
25
+ DATATABLE: 'datatable',
26
+ TABS: 'tabs',
27
+ ACCORDION: 'accordion',
28
+ STEPPER: 'stepper',
29
+ DASHBOARD_WIDGET: 'dashboard-widget'
24
30
  };
25
31
 
26
32
  /**
@@ -167,6 +173,24 @@ export class A2UIRenderer {
167
173
  case A2UI_TYPES.PROGRESS:
168
174
  element = this.createProgress(props);
169
175
  break;
176
+ case A2UI_TYPES.CHART:
177
+ element = this.createChart(props);
178
+ break;
179
+ case A2UI_TYPES.DATATABLE:
180
+ element = this.createDatatable(props);
181
+ break;
182
+ case A2UI_TYPES.TABS:
183
+ element = this.createTabs(props);
184
+ break;
185
+ case A2UI_TYPES.ACCORDION:
186
+ element = this.createAccordion(props);
187
+ break;
188
+ case A2UI_TYPES.STEPPER:
189
+ element = this.createStepper(props);
190
+ break;
191
+ case A2UI_TYPES.DASHBOARD_WIDGET:
192
+ element = this.createDashboardWidget(props);
193
+ break;
170
194
  default:
171
195
  throw new Error(`Unknown A2UI component type: ${type}`);
172
196
  }
@@ -324,6 +348,159 @@ export class A2UIRenderer {
324
348
  return progress;
325
349
  }
326
350
 
351
+ /**
352
+ * Create chart component
353
+ */
354
+ createChart({ type = 'bar', data = [], labels = [], title, options = {} }) {
355
+ const chart = document.createElement('af-chart');
356
+
357
+ chart.setAttribute('type', type);
358
+ chart.setAttribute('data', JSON.stringify(data));
359
+ chart.setAttribute('labels', JSON.stringify(labels));
360
+
361
+ if (title) {
362
+ chart.setAttribute('title', title);
363
+ }
364
+
365
+ if (options) {
366
+ chart.setAttribute('options', JSON.stringify(options));
367
+ }
368
+
369
+ return chart;
370
+ }
371
+
372
+ /**
373
+ * Create datatable component
374
+ */
375
+ createDatatable({ columns = [], rows = [], title, options = {} }) {
376
+ const table = document.createElement('af-datatable');
377
+
378
+ table.setAttribute('columns', JSON.stringify(columns));
379
+ table.setAttribute('rows', JSON.stringify(rows));
380
+
381
+ if (title) {
382
+ table.setAttribute('title', title);
383
+ }
384
+
385
+ if (options) {
386
+ table.setAttribute('options', JSON.stringify(options));
387
+ }
388
+
389
+ return table;
390
+ }
391
+
392
+ /**
393
+ * Create tabs component
394
+ */
395
+ createTabs({ tabs = [], activeTab = 0 }) {
396
+ const tabsContainer = document.createElement('af-tabs');
397
+ tabsContainer.setAttribute('active', activeTab);
398
+
399
+ for (let i = 0; i < tabs.length; i++) {
400
+ const tab = tabs[i];
401
+ const tabButton = document.createElement('af-tab-button');
402
+ tabButton.setAttribute('tab-id', i);
403
+ tabButton.textContent = tab.title;
404
+
405
+ const tabContent = document.createElement('af-tab-content');
406
+ tabContent.setAttribute('tab-id', i);
407
+ tabContent.innerHTML = tab.content || '';
408
+
409
+ tabsContainer.appendChild(tabButton);
410
+ tabsContainer.appendChild(tabContent);
411
+ }
412
+
413
+ return tabsContainer;
414
+ }
415
+
416
+ /**
417
+ * Create accordion component
418
+ */
419
+ createAccordion({ items = [] }) {
420
+ const accordion = document.createElement('af-accordion');
421
+
422
+ for (const item of items) {
423
+ const panel = document.createElement('af-accordion-panel');
424
+
425
+ const header = document.createElement('af-accordion-header');
426
+ header.textContent = item.title;
427
+
428
+ const content = document.createElement('af-accordion-content');
429
+ content.innerHTML = item.content || '';
430
+
431
+ panel.appendChild(header);
432
+ panel.appendChild(content);
433
+ accordion.appendChild(panel);
434
+ }
435
+
436
+ return accordion;
437
+ }
438
+
439
+ /**
440
+ * Create stepper component
441
+ */
442
+ createStepper({ steps = [], currentStep = 0 }) {
443
+ const stepper = document.createElement('af-stepper');
444
+ stepper.setAttribute('current-step', currentStep);
445
+
446
+ for (let i = 0; i < steps.length; i++) {
447
+ const step = document.createElement('af-step');
448
+ step.setAttribute('step-number', i + 1);
449
+ step.setAttribute('active', i <= currentStep);
450
+
451
+ if (i === currentStep) {
452
+ step.setAttribute('current', 'true');
453
+ }
454
+
455
+ step.textContent = steps[i].title || `Step ${i + 1}`;
456
+ stepper.appendChild(step);
457
+ }
458
+
459
+ return stepper;
460
+ }
461
+
462
+ /**
463
+ * Create dashboard widget component
464
+ */
465
+ createDashboardWidget({ title, content, type = 'card', size = 'medium', actions = [] }) {
466
+ const widget = document.createElement('af-dashboard-widget');
467
+ widget.setAttribute('type', type);
468
+ widget.setAttribute('size', size);
469
+
470
+ if (title) {
471
+ const header = document.createElement('h3');
472
+ header.className = 'widget-title';
473
+ header.textContent = title;
474
+ widget.appendChild(header);
475
+ }
476
+
477
+ if (content) {
478
+ const contentEl = document.createElement('div');
479
+ contentEl.className = 'widget-content';
480
+ contentEl.innerHTML = typeof content === 'string' ? content : JSON.stringify(content);
481
+ widget.appendChild(contentEl);
482
+ }
483
+
484
+ if (actions.length > 0) {
485
+ const actionsEl = document.createElement('div');
486
+ actionsEl.className = 'widget-actions';
487
+
488
+ for (const action of actions) {
489
+ const button = document.createElement('af-button');
490
+ button.textContent = action.label;
491
+ button.setAttribute('variant', action.variant || 'secondary');
492
+ if (action.onClick) {
493
+ button.addEventListener('click', action.onClick);
494
+ }
495
+ actionsEl.appendChild(button);
496
+ }
497
+
498
+ widget.appendChild(actionsEl);
499
+ }
500
+
501
+ return widget;
502
+ }
503
+
327
504
  /**
328
505
  * Update existing component
329
506
  */