@africode/core 5.0.6 → 5.0.8

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 (49) hide show
  1. package/AGENTS.md +2 -0
  2. package/COMPONENT_SCHEMA.json +1 -0
  3. package/README.md +2 -0
  4. package/components/breadcrumb.js +72 -0
  5. package/components/carousel.js +32 -0
  6. package/components/cart-drawer.js +28 -0
  7. package/components/code-block.js +20 -0
  8. package/components/contact-form.js +25 -0
  9. package/components/cookie-consent.js +23 -0
  10. package/components/cta-banner.js +38 -0
  11. package/components/dashboard-activity-list.js +103 -0
  12. package/components/dashboard-card.js +85 -0
  13. package/components/dashboard-metric.js +81 -0
  14. package/components/dashboard-shell.js +85 -0
  15. package/components/dashboard-topbar.js +84 -0
  16. package/components/faq-accordion.js +23 -0
  17. package/components/feature-grid.js +85 -0
  18. package/components/file-uploader.js +22 -0
  19. package/components/filter-bar.js +19 -0
  20. package/components/footer.js +113 -0
  21. package/components/gallery.js +22 -0
  22. package/components/index.js +47 -0
  23. package/components/newsletter-signup.js +24 -0
  24. package/components/pagination.js +31 -0
  25. package/components/pricing-card.js +100 -0
  26. package/components/product-card.js +41 -0
  27. package/components/search-box.js +24 -0
  28. package/components/simple-chart.js +22 -0
  29. package/components/stepper.js +28 -0
  30. package/components/team-grid.js +23 -0
  31. package/components/testimonial.js +94 -0
  32. package/components/ui-avatar.js +79 -0
  33. package/components/ui-chip.js +87 -0
  34. package/components/ui-popover.js +84 -0
  35. package/components/ui-progress-ring.js +60 -0
  36. package/components/ui-select.js +85 -0
  37. package/components/ui-stat.js +75 -0
  38. package/components/ui-table.js +69 -0
  39. package/components/ui-tabs.js +99 -0
  40. package/components/ui-tag.js +64 -0
  41. package/components/ui-textarea.js +84 -0
  42. package/components/ui-tooltip.js +66 -0
  43. package/components/video-player.js +22 -0
  44. package/dist/africode.js +1480 -549
  45. package/dist/africode.js.map +25 -4
  46. package/dist/build-info.json +3 -3
  47. package/dist/components.js +1506 -575
  48. package/dist/components.js.map +25 -4
  49. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -376,6 +376,8 @@ export async function screenTransaction(amount, nin) {
376
376
 
377
377
  **Purpose**: Auto-generated JSON schema that whitelists safe components for AI generation. Prevents hallucinated HTML and enforces A2UI compliance.
378
378
 
379
+ **Package note**: `af-`, `af-ui-`, and `af-dashboard-` components are all part of the same unified `@africode/core` package, sharing theme, styles, registry, and versioning.
380
+
379
381
  **Generated by**: `scripts/generate-component-schema.js`
380
382
 
381
383
  ```json
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "version": "5.0.0",
3
3
  "timestamp": "2026-06-28T10:52:30.385Z",
4
+ "packageNote": "All af-, af-ui-, and af-dashboard components ship together in @africode/core as a unified AfriCode UI system.",
4
5
  "components": [
5
6
  {
6
7
  "tagName": "af-accordion",
package/README.md CHANGED
@@ -349,6 +349,8 @@ Then use them in your HTML:
349
349
 
350
350
  AfriCode now includes a native UI kit with shadcn-inspired Web Components for rapid interface design. The Tailwind starter demonstrates the new UI kit using components like `af-ui-button`, `af-ui-card`, `af-ui-input`, `af-ui-badge`, and `af-ui-switch` in a Shadow DOM-friendly workflow.
351
351
 
352
+ All `af-`, `af-ui-`, and `af-dashboard-` components are distributed together in the same `@africode/core` package, so they share theme, styles, registry, and versioning.
353
+
352
354
  The library is optimized for consistent spacing, elevation, and interactive states across theme-aware apps.
353
355
 
354
356
  Use the Tailwind starter template for a ready-made UI experience:
@@ -0,0 +1,72 @@
1
+ /**
2
+ * AfriCode Breadcrumb Component
3
+ *
4
+ * Simple breadcrumb trail for website navigation.
5
+ * @module components/breadcrumb
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriBreadcrumb extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['separator'];
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+ this.render();
18
+ }
19
+
20
+ attributeChangedCallback() {
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ const separator = this.getAttribute('separator') || '/';
26
+
27
+ this.shadowRoot.innerHTML = html`
28
+ <style>
29
+ :host {
30
+ display: block;
31
+ font-family: 'Inter', system-ui, sans-serif;
32
+ color: #475569;
33
+ font-size: 0.9rem;
34
+ }
35
+
36
+ nav {
37
+ display: flex;
38
+ flex-wrap: wrap;
39
+ gap: 0.5rem;
40
+ align-items: center;
41
+ }
42
+
43
+ .crumb {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ gap: 0.35rem;
47
+ }
48
+
49
+ .separator {
50
+ color: #cbd5e1;
51
+ }
52
+
53
+ ::slotted(a) {
54
+ color: #0f172a;
55
+ text-decoration: none;
56
+ font-weight: 600;
57
+ }
58
+
59
+ ::slotted(a:hover) {
60
+ text-decoration: underline;
61
+ }
62
+ </style>
63
+
64
+ <nav aria-label="Breadcrumb">
65
+ <slot></slot>
66
+ </nav>
67
+ `;
68
+ }
69
+ }
70
+
71
+ registerComponent('af-breadcrumb', AfriBreadcrumb);
72
+ export default AfriBreadcrumb;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * AfriCode Carousel
3
+ *
4
+ * Minimal image/content carousel with navigation.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriCarousel extends AfriCodeComponent {
10
+ constructor(){ super(); this._idx=0; this.render(); }
11
+ connectedCallback(){ this._bind(); }
12
+ _bind(){ this.shadowRoot.addEventListener('click', (e)=>{ const t=e.target; if(t.matches('.next')){this.next();} if(t.matches('.prev')){this.prev();}}); }
13
+ next(){ const items=this.shadowRoot.querySelectorAll('.slide'); if(items.length===0) return; items[this._idx].classList.remove('active'); this._idx=(this._idx+1)%items.length; items[this._idx].classList.add('active'); }
14
+ prev(){ const items=this.shadowRoot.querySelectorAll('.slide'); if(items.length===0) return; items[this._idx].classList.remove('active'); this._idx=(this._idx-1+items.length)%items.length; items[this._idx].classList.add('active'); }
15
+ render(){ this.shadowRoot.innerHTML = html`
16
+ <style>
17
+ :host{display:block;position:relative;overflow:hidden}
18
+ .slides{display:flex}
19
+ .slide{min-width:100%;transition:transform 300ms ease;display:none}
20
+ .slide.active{display:block}
21
+ .nav{position:absolute;top:50%;transform:translateY(-50%);width:100%;display:flex;justify-content:space-between;padding:0 1rem}
22
+ .btn{background:rgba(0,0,0,0.4);border:none;color:white;padding:0.5rem 0.7rem;border-radius:8px;cursor:pointer}
23
+ </style>
24
+ <div class="viewport">
25
+ <slot></slot>
26
+ </div>
27
+ <div class="nav"><button class="btn prev">‹</button><button class="btn next">›</button></div>
28
+ `; }
29
+ }
30
+
31
+ registerComponent('af-carousel', AfriCarousel);
32
+ export default AfriCarousel;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * AfriCode Cart Drawer
3
+ *
4
+ * A slide-over cart drawer for e-commerce flows.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriCartDrawer extends AfriCodeComponent {
10
+ constructor(){ super(); this.open=false; this.render(); }
11
+ connectedCallback(){ this.shadowRoot.addEventListener('click',(e)=>{ if(e.target.matches('.close')) this.close() }) }
12
+ openDrawer(){ this.open=true; this.render(); }
13
+ close(){ this.open=false; this.render(); }
14
+ render(){ this.shadowRoot.innerHTML = html`
15
+ <style>
16
+ :host{display:block}
17
+ .backdrop{position:fixed;inset:0;background:rgba(2,6,23,0.5);display:${this.open?'block':'none'};z-index:1200}
18
+ .drawer{position:fixed;right:0;top:0;height:100vh;width:360px;background:#fff;box-shadow:-20px 0 40px rgba(2,6,23,0.2);transform:translateX(${this.open?'0':'100%'});transition:transform 300ms}
19
+ .header{padding:16px;display:flex;justify-content:space-between;align-items:center}
20
+ .items{padding:16px;display:grid;gap:12px;overflow:auto;height:calc(100vh - 160px)}
21
+ .footer{padding:16px;border-top:1px solid #eef2f7}
22
+ </style>
23
+ <div class="backdrop"><div class="drawer"><div class="header"><strong>Cart</strong><button class="close">✕</button></div><div class="items"><slot></slot></div><div class="footer"><button class="checkout">Checkout</button></div></div></div>
24
+ `; this.shadowRoot.querySelector('.checkout')?.addEventListener('click',()=>this.emit('checkout')) }
25
+ }
26
+
27
+ registerComponent('af-cart-drawer', AfriCartDrawer);
28
+ export default AfriCartDrawer;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * AfriCode Code Block
3
+ *
4
+ * Syntax-highlighted code block wrapper (lightweight).
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriCodeBlock extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ render(){ this.shadowRoot.innerHTML = html`
12
+ <style>
13
+ pre{background:#0f172a;color:#e6eef8;padding:12px;border-radius:8px;overflow:auto}
14
+ </style>
15
+ <pre><code><slot></slot></code></pre>
16
+ ` }
17
+ }
18
+
19
+ registerComponent('af-code-block', AfriCodeBlock);
20
+ export default AfriCodeBlock;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * AfriCode Contact Form
3
+ *
4
+ * Ready-to-use contact form with basic validation.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriContactForm extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ connectedCallback(){ this.shadowRoot.addEventListener('submit',(e)=>{ if(e.target.matches('form')) this._onSubmit(e) }, true); }
12
+ async _onSubmit(e){ e.preventDefault(); const data=new FormData(e.target); this.emit('submit', Object.fromEntries(data.entries())); }
13
+ render(){ this.shadowRoot.innerHTML = html`
14
+ <style>
15
+ :host{display:block}
16
+ form{display:grid;gap:8px}
17
+ input,textarea{padding:10px;border-radius:8px;border:1px solid #e5e7eb}
18
+ button{padding:10px;border-radius:8px;background:#1EB53A;color:white;border:none}
19
+ </style>
20
+ <form><input name="name" placeholder="Name" required/><input name="email" placeholder="Email" type="email" required/><textarea name="message" placeholder="Message" rows="4" required></textarea><button type="submit">Send</button></form>
21
+ `}
22
+ }
23
+
24
+ registerComponent('af-contact-form', AfriContactForm);
25
+ export default AfriContactForm;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * AfriCode Cookie Consent
3
+ *
4
+ * Simple cookie consent bar.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriCookieConsent extends AfriCodeComponent {
10
+ constructor(){ super(); this.visible=!localStorage.getItem('af_cookie'); this.render(); }
11
+ connectedCallback(){ this.shadowRoot.addEventListener('click',(e)=>{ if(e.target.matches('.accept')){ localStorage.setItem('af_cookie','1'); this.visible=false; this.render(); this.emit('consent', {accepted:true}) }}) }
12
+ render(){ this.shadowRoot.innerHTML = html`
13
+ <style>
14
+ :host{display:block}
15
+ .bar{position:fixed;left:16px;right:16px;bottom:16px;padding:12px;border-radius:12px;background:#111;color:#fff;display:${this.visible?'flex':'none'};justify-content:space-between;align-items:center;gap:12px}
16
+ button{background:#1EB53A;border:none;padding:8px 12px;border-radius:8px;color:#fff}
17
+ </style>
18
+ <div class="bar"><div>We use cookies to improve your experience.</div><div><button class="accept">Accept</button></div></div>
19
+ ` }
20
+ }
21
+
22
+ registerComponent('af-cookie-consent', AfriCookieConsent);
23
+ export default AfriCookieConsent;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * AfriCode CTA Banner
3
+ *
4
+ * Simple call-to-action banner for marketing pages.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriCTABanner extends AfriCodeComponent {
10
+ static get observedAttributes() { return ['title','subtitle','cta-text']; }
11
+ constructor(){ super(); this.render(); }
12
+ attributeChangedCallback(){ this.render(); }
13
+ render(){
14
+ const title = this.getAttribute('title') || 'Get started today';
15
+ const subtitle = this.getAttribute('subtitle') || '';
16
+ const cta = this.getAttribute('cta-text') || 'Try for free';
17
+ this.shadowRoot.innerHTML = html`
18
+ <style>
19
+ :host{display:block;font-family:Inter,system-ui,sans-serif}
20
+ .banner{display:flex;gap:1rem;align-items:center;justify-content:space-between;padding:1.25rem;border-radius:12px;background:linear-gradient(90deg,#1EB53A,#FCD116);color:#08111a}
21
+ .text{display:flex;flex-direction:column}
22
+ .title{font-weight:800;font-size:1.125rem}
23
+ .subtitle{font-size:0.95rem;color:rgba(8,17,26,0.8)}
24
+ .cta{background:rgba(255,255,255,0.12);border:none;padding:0.6rem 1rem;border-radius:8px;color:inherit;cursor:pointer}
25
+ </style>
26
+ <div class="banner">
27
+ <div class="text">
28
+ <div class="title">${title}</div>
29
+ ${subtitle?html`<div class="subtitle">${subtitle}</div>`:''}
30
+ </div>
31
+ <button class="cta">${cta}</button>
32
+ </div>
33
+ `;
34
+ }
35
+ }
36
+
37
+ registerComponent('af-cta-banner', AfriCTABanner);
38
+ export default AfriCTABanner;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * AfriCode Dashboard Activity List
3
+ *
4
+ * Displays recent activity or event history in a dashboard card.
5
+ * @module components/dashboard-activity-list
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriDashboardActivityList extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['title', 'empty-text'];
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+ this.render();
18
+ }
19
+
20
+ attributeChangedCallback() {
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ const title = this.getAttribute('title') || 'Recent Activity';
26
+ const emptyText = this.getAttribute('empty-text') || 'No recent activity available.';
27
+
28
+ this.shadowRoot.innerHTML = html`
29
+ <style>
30
+ :host {
31
+ display: block;
32
+ font-family: 'Inter', system-ui, sans-serif;
33
+ }
34
+
35
+ .activity-card {
36
+ display: grid;
37
+ gap: 1rem;
38
+ padding: 1.5rem;
39
+ border-radius: 24px;
40
+ background: #ffffff;
41
+ border: 1px solid rgba(15, 23, 42, 0.08);
42
+ }
43
+
44
+ .header {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ gap: 0.75rem;
49
+ }
50
+
51
+ .title {
52
+ margin: 0;
53
+ font-size: 1rem;
54
+ font-weight: 700;
55
+ color: #0f172a;
56
+ }
57
+
58
+ .list {
59
+ display: grid;
60
+ gap: 0.5rem;
61
+ }
62
+
63
+ .item {
64
+ padding: 0.95rem 1rem;
65
+ border-radius: 18px;
66
+ background: #f8fafc;
67
+ border: 1px solid rgba(15, 23, 42, 0.04);
68
+ }
69
+
70
+ .item-title {
71
+ font-size: 0.95rem;
72
+ font-weight: 600;
73
+ color: #0f172a;
74
+ margin: 0 0 0.25rem;
75
+ }
76
+
77
+ .item-meta {
78
+ margin: 0;
79
+ color: #6b7280;
80
+ font-size: 0.85rem;
81
+ }
82
+
83
+ .empty {
84
+ color: #6b7280;
85
+ font-size: 0.95rem;
86
+ padding: 1rem 0;
87
+ }
88
+ </style>
89
+
90
+ <div class="activity-card">
91
+ <div class="header"><h3 class="title">${title}</h3></div>
92
+ <div class="list">
93
+ <slot>
94
+ <div class="empty">${emptyText}</div>
95
+ </slot>
96
+ </div>
97
+ </div>
98
+ `;
99
+ }
100
+ }
101
+
102
+ registerComponent('af-dashboard-activity-list', AfriDashboardActivityList);
103
+ export default AfriDashboardActivityList;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AfriCode Dashboard Card
3
+ *
4
+ * A reusable dashboard surface for metrics, lists, and analytics.
5
+ * @module components/dashboard-card
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriDashboardCard extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['title', 'variant'];
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+ this.render();
18
+ }
19
+
20
+ attributeChangedCallback() {
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ const title = this.getAttribute('title') || '';
26
+ const variant = this.getAttribute('variant') || 'surface';
27
+
28
+ const styles = {
29
+ surface: {
30
+ background: '#ffffff',
31
+ border: '1px solid rgba(15, 23, 42, 0.08)',
32
+ shadow: '0 18px 36px rgba(15, 23, 42, 0.04)'
33
+ },
34
+ elevated: {
35
+ background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
36
+ border: '1px solid rgba(15, 23, 42, 0.12)',
37
+ shadow: '0 24px 48px rgba(15, 23, 42, 0.08)'
38
+ }
39
+ };
40
+
41
+ const cardStyle = styles[variant] || styles.surface;
42
+
43
+ this.shadowRoot.innerHTML = html`
44
+ <style>
45
+ :host {
46
+ display: block;
47
+ font-family: 'Inter', system-ui, sans-serif;
48
+ }
49
+
50
+ .card {
51
+ background: ${cardStyle.background};
52
+ border: ${cardStyle.border};
53
+ border-radius: 24px;
54
+ box-shadow: ${cardStyle.shadow};
55
+ padding: 1.5rem;
56
+ min-height: 100px;
57
+ overflow: hidden;
58
+ }
59
+
60
+ .header {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ gap: 0.75rem;
65
+ margin-bottom: 1rem;
66
+ }
67
+
68
+ .title {
69
+ margin: 0;
70
+ font-size: 1rem;
71
+ font-weight: 700;
72
+ color: #0f172a;
73
+ }
74
+ </style>
75
+
76
+ <div class="card">
77
+ ${title ? html`<div class="header"><h3 class="title">${title}</h3><slot name="header"></slot></div>` : ''}
78
+ <div class="body"><slot></slot></div>
79
+ </div>
80
+ `;
81
+ }
82
+ }
83
+
84
+ registerComponent('af-dashboard-card', AfriDashboardCard);
85
+ export default AfriDashboardCard;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * AfriCode Dashboard Metric
3
+ *
4
+ * Summary tile for key performance indicators.
5
+ * @module components/dashboard-metric
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriDashboardMetric extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['label', 'value', 'delta', 'trend'];
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+ this.render();
18
+ }
19
+
20
+ attributeChangedCallback() {
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ const label = this.getAttribute('label') || 'Metric';
26
+ const value = this.getAttribute('value') || '0';
27
+ const delta = this.getAttribute('delta') || '';
28
+ const trend = this.getAttribute('trend') || 'neutral';
29
+ const trendColor = trend === 'positive' ? '#16a34a' : trend === 'negative' ? '#dc2626' : '#475569';
30
+
31
+ this.shadowRoot.innerHTML = html`
32
+ <style>
33
+ :host {
34
+ display: block;
35
+ font-family: 'Inter', system-ui, sans-serif;
36
+ }
37
+
38
+ .metric {
39
+ display: grid;
40
+ gap: 0.5rem;
41
+ padding: 1.25rem;
42
+ border-radius: 24px;
43
+ background: #ffffff;
44
+ border: 1px solid rgba(15, 23, 42, 0.08);
45
+ }
46
+
47
+ .label {
48
+ color: #6b7280;
49
+ text-transform: uppercase;
50
+ letter-spacing: 0.14em;
51
+ font-size: 0.75rem;
52
+ font-weight: 700;
53
+ }
54
+
55
+ .value {
56
+ font-size: 2rem;
57
+ font-weight: 800;
58
+ color: #0f172a;
59
+ }
60
+
61
+ .delta {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ gap: 0.4rem;
65
+ font-weight: 700;
66
+ color: ${trendColor};
67
+ font-size: 0.92rem;
68
+ }
69
+ </style>
70
+
71
+ <div class="metric">
72
+ <div class="label">${label}</div>
73
+ <div class="value">${value}</div>
74
+ ${delta ? html`<div class="delta">${delta}</div>` : ''}
75
+ </div>
76
+ `;
77
+ }
78
+ }
79
+
80
+ registerComponent('af-dashboard-metric', AfriDashboardMetric);
81
+ export default AfriDashboardMetric;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AfriCode Dashboard Shell
3
+ *
4
+ * A layout container for dashboard applications.
5
+ * Use slots for sidebar, topbar, and page content.
6
+ * @module components/dashboard-shell
7
+ */
8
+
9
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
10
+
11
+ export class AfriDashboardShell extends AfriCodeComponent {
12
+ static get observedAttributes() {
13
+ return ['sidebar-width', 'topbar-height'];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this.render();
19
+ }
20
+
21
+ attributeChangedCallback() {
22
+ this.render();
23
+ }
24
+
25
+ render() {
26
+ const sidebarWidth = this.getAttribute('sidebar-width') || '280px';
27
+ const topbarHeight = this.getAttribute('topbar-height') || '72px';
28
+
29
+ this.shadowRoot.innerHTML = html`
30
+ <style>
31
+ :host {
32
+ display: block;
33
+ min-height: 100vh;
34
+ font-family: 'Inter', system-ui, sans-serif;
35
+ color: var(--af-text, #0f172a);
36
+ background: var(--af-bg, #f8fafc);
37
+ }
38
+
39
+ .dashboard-shell {
40
+ display: grid;
41
+ grid-template-columns: ${sidebarWidth} minmax(0, 1fr);
42
+ grid-template-rows: ${topbarHeight} minmax(0, 1fr);
43
+ min-height: 100vh;
44
+ gap: 1rem;
45
+ }
46
+
47
+ .sidebar {
48
+ grid-row: 1 / span 2;
49
+ background: transparent;
50
+ }
51
+
52
+ .topbar {
53
+ grid-column: 2 / 3;
54
+ position: sticky;
55
+ top: 0;
56
+ z-index: 10;
57
+ background: var(--af-surface, #ffffff);
58
+ border-bottom: 1px solid rgba(15, 23, 42, 0.08);
59
+ box-shadow: 0 4px 18px rgba(15, 23, 42, 0.04);
60
+ }
61
+
62
+ .content {
63
+ grid-column: 2 / 3;
64
+ padding: 24px;
65
+ min-height: calc(100vh - ${topbarHeight});
66
+ background: transparent;
67
+ overflow: hidden;
68
+ }
69
+
70
+ ::slotted([slot="sidebar"]) {
71
+ height: 100%;
72
+ }
73
+ </style>
74
+
75
+ <div class="dashboard-shell">
76
+ <aside class="sidebar"><slot name="sidebar"></slot></aside>
77
+ <header class="topbar"><slot name="topbar"></slot></header>
78
+ <main class="content"><slot></slot></main>
79
+ </div>
80
+ `;
81
+ }
82
+ }
83
+
84
+ registerComponent('af-dashboard-shell', AfriDashboardShell);
85
+ export default AfriDashboardShell;