@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
@@ -0,0 +1,84 @@
1
+ /**
2
+ * AfriCode Dashboard Topbar
3
+ *
4
+ * Top navigation bar for dashboard control and page context.
5
+ * @module components/dashboard-topbar
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriDashboardTopbar extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['title', 'description'];
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') || 'Dashboard';
26
+ const description = this.getAttribute('description') || '';
27
+
28
+ this.shadowRoot.innerHTML = html`
29
+ <style>
30
+ :host {
31
+ display: block;
32
+ width: 100%;
33
+ font-family: 'Inter', system-ui, sans-serif;
34
+ }
35
+
36
+ .topbar {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ padding: 1rem 1.5rem;
41
+ gap: 1rem;
42
+ min-height: 72px;
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ .heading {
47
+ display: grid;
48
+ gap: 0.25rem;
49
+ }
50
+
51
+ .title {
52
+ margin: 0;
53
+ font-size: 1.1rem;
54
+ font-weight: 700;
55
+ color: #0f172a;
56
+ }
57
+
58
+ .description {
59
+ margin: 0;
60
+ color: #475569;
61
+ font-size: 0.95rem;
62
+ }
63
+
64
+ .actions {
65
+ display: flex;
66
+ gap: 0.75rem;
67
+ align-items: center;
68
+ flex-wrap: wrap;
69
+ }
70
+ </style>
71
+
72
+ <div class="topbar">
73
+ <div class="heading">
74
+ <div class="title">${title}</div>
75
+ ${description ? html`<div class="description">${description}</div>` : ''}
76
+ </div>
77
+ <div class="actions"><slot name="actions"></slot></div>
78
+ </div>
79
+ `;
80
+ }
81
+ }
82
+
83
+ registerComponent('af-dashboard-topbar', AfriDashboardTopbar);
84
+ export default AfriDashboardTopbar;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * AfriCode FAQ Accordion
3
+ *
4
+ * Simple accordion for FAQ sections.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriFAQAccordion extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ connectedCallback(){ this.shadowRoot.addEventListener('click',(e)=>{ if(e.target.matches('.q')){ const p=e.target.nextElementSibling; p.hidden=!p.hidden}})}
12
+ render(){ this.shadowRoot.innerHTML = html`
13
+ <style>
14
+ :host{display:block}
15
+ .q{cursor:pointer;padding:12px;border-radius:8px;background:#f8fafc}
16
+ .a{padding:12px}
17
+ </style>
18
+ <slot></slot>
19
+ `}
20
+ }
21
+
22
+ registerComponent('af-faq-accordion', AfriFAQAccordion);
23
+ export default AfriFAQAccordion;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AfriCode Feature Grid Component
3
+ *
4
+ * Responsive feature tiles for website marketing sections.
5
+ * @module components/feature-grid
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriFeatureGrid extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['columns'];
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+ this.render();
18
+ }
19
+
20
+ attributeChangedCallback() {
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ const columns = this.getAttribute('columns') || '3';
26
+
27
+ this.shadowRoot.innerHTML = html`
28
+ <style>
29
+ :host {
30
+ display: block;
31
+ width: 100%;
32
+ box-sizing: border-box;
33
+ font-family: 'Inter', system-ui, sans-serif;
34
+ }
35
+
36
+ .grid {
37
+ display: grid;
38
+ gap: 1.5rem;
39
+ grid-template-columns: repeat(${columns}, minmax(0, 1fr));
40
+ }
41
+
42
+ @media (max-width: 1024px) {
43
+ .grid {
44
+ grid-template-columns: repeat(2, minmax(0, 1fr));
45
+ }
46
+ }
47
+
48
+ @media (max-width: 640px) {
49
+ .grid {
50
+ grid-template-columns: 1fr;
51
+ }
52
+ }
53
+
54
+ ::slotted(.feature-card) {
55
+ display: grid;
56
+ gap: 0.75rem;
57
+ padding: 1.5rem;
58
+ border-radius: 24px;
59
+ background: #ffffff;
60
+ border: 1px solid rgba(15, 23, 42, 0.08);
61
+ box-shadow: 0 12px 32px rgba(15, 23, 42, 0.04);
62
+ }
63
+
64
+ ::slotted(.feature-card h3) {
65
+ margin: 0;
66
+ font-size: 1.1rem;
67
+ color: #0f172a;
68
+ }
69
+
70
+ ::slotted(.feature-card p) {
71
+ margin: 0;
72
+ color: #475569;
73
+ line-height: 1.7;
74
+ }
75
+ </style>
76
+
77
+ <div class="grid">
78
+ <slot></slot>
79
+ </div>
80
+ `;
81
+ }
82
+ }
83
+
84
+ registerComponent('af-feature-grid', AfriFeatureGrid);
85
+ export default AfriFeatureGrid;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * AfriCode File Uploader
3
+ *
4
+ * Simple drag-and-drop file uploader component.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriFileUploader extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ connectedCallback(){ const area=this.shadowRoot.getElementById('drop'); ['dragover','dragleave','drop'].forEach(ev=>area.addEventListener(ev,(e)=>{e.preventDefault();if(ev==='drop'){const files=Array.from(e.dataTransfer.files);this.emit('files',files)}})) }
12
+ render(){ this.shadowRoot.innerHTML = html`
13
+ <style>
14
+ :host{display:block}
15
+ .drop{padding:20px;border:2px dashed #e5e7eb;border-radius:12px;text-align:center}
16
+ </style>
17
+ <div id="drop" class="drop">Drop files here or click to select</div>
18
+ ` }
19
+ }
20
+
21
+ registerComponent('af-file-uploader', AfriFileUploader);
22
+ export default AfriFileUploader;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * AfriCode Filter Bar
3
+ *
4
+ * Simple filter controls for listing pages.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriFilterBar extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ connectedCallback(){ this.shadowRoot.addEventListener('change', (e)=>{ this.emit('filter-change', {name: e.target.name, value: e.target.value}) }) }
12
+ render(){ this.shadowRoot.innerHTML = html`
13
+ <style> .wrap{display:flex;gap:8px;flex-wrap:wrap} select,input{padding:8px;border-radius:8px;border:1px solid #e5e7eb} </style>
14
+ <div class="wrap"><slot></slot></div>
15
+ ` }
16
+ }
17
+
18
+ registerComponent('af-filter-bar', AfriFilterBar);
19
+ export default AfriFilterBar;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * AfriCode Footer Component
3
+ *
4
+ * Flexible website footer with logo, links, and legal slot.
5
+ * @module components/footer
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriFooter extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['company', 'year'];
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+ this.render();
18
+ }
19
+
20
+ attributeChangedCallback() {
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ const company = this.getAttribute('company') || 'AfriCode';
26
+ const year = this.getAttribute('year') || new Date().getFullYear();
27
+
28
+ this.shadowRoot.innerHTML = html`
29
+ <style>
30
+ :host {
31
+ display: block;
32
+ width: 100%;
33
+ box-sizing: border-box;
34
+ background: #0f172a;
35
+ color: #e2e8f0;
36
+ font-family: 'Inter', system-ui, sans-serif;
37
+ }
38
+
39
+ .footer {
40
+ padding: 48px 5%;
41
+ max-width: 1280px;
42
+ margin: 0 auto;
43
+ display: grid;
44
+ gap: 2rem;
45
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
46
+ }
47
+
48
+ .brand {
49
+ display: grid;
50
+ gap: 0.75rem;
51
+ }
52
+
53
+ .brand h3 {
54
+ margin: 0;
55
+ font-size: 1.25rem;
56
+ color: #ffffff;
57
+ }
58
+
59
+ .links {
60
+ display: grid;
61
+ gap: 0.5rem;
62
+ }
63
+
64
+ .links slot {
65
+ display: grid;
66
+ gap: 0.5rem;
67
+ }
68
+
69
+ a {
70
+ color: #94a3b8;
71
+ text-decoration: none;
72
+ }
73
+
74
+ a:hover {
75
+ color: #ffffff;
76
+ }
77
+
78
+ .bottom {
79
+ grid-column: 1 / -1;
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ gap: 1rem;
85
+ padding-top: 1.5rem;
86
+ border-top: 1px solid rgba(226, 232, 240, 0.12);
87
+ color: #94a3b8;
88
+ font-size: 0.95rem;
89
+ }
90
+ </style>
91
+
92
+ <footer class="footer">
93
+ <div class="brand">
94
+ <h3>${company}</h3>
95
+ <slot name="description"></slot>
96
+ </div>
97
+ <div class="links">
98
+ <slot name="links"></slot>
99
+ </div>
100
+ <div class="links">
101
+ <slot name="resources"></slot>
102
+ </div>
103
+ <div class="bottom">
104
+ <span>© ${year} ${company}. All rights reserved.</span>
105
+ <slot name="legal"></slot>
106
+ </div>
107
+ </footer>
108
+ `;
109
+ }
110
+ }
111
+
112
+ registerComponent('af-footer', AfriFooter);
113
+ export default AfriFooter;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * AfriCode Gallery
3
+ *
4
+ * Responsive image gallery grid with lightbox slot support.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriGallery extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ render(){ this.shadowRoot.innerHTML = html`
12
+ <style>
13
+ :host{display:block}
14
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}
15
+ ::slotted(img){width:100%;height:160px;object-fit:cover;border-radius:12px}
16
+ </style>
17
+ <div class="grid"><slot></slot></div>
18
+ `}
19
+ }
20
+
21
+ registerComponent('af-gallery', AfriGallery);
22
+ export default AfriGallery;
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Component Registry (Islands Map)
3
3
  *
4
+ * All AfriCode components, including the native UI kit (`af-ui-*`),
5
+ * dashboard widgets (`af-dashboard-*`), and website/system helpers,
6
+ * ship together from the same @africode/core package.
7
+ * This keeps theme, styles, registry, and versioning unified.
8
+ *
4
9
  * Instead of statically importing all components (monolithic bundle),
5
10
  * we export a map for lazy hydration.
6
11
  */
@@ -14,6 +19,27 @@ export { AfriUICard } from './ui-card.js';
14
19
  export { AfriUIInput } from './ui-input.js';
15
20
  export { AfriUIBadge } from './ui-badge.js';
16
21
  export { AfriUISwitch } from './ui-switch.js';
22
+ export { AfriUISelect } from './ui-select.js';
23
+ export { AfriUITextarea } from './ui-textarea.js';
24
+ export { AfriUIChip } from './ui-chip.js';
25
+ export { AfriUITag } from './ui-tag.js';
26
+ export { AfriUIAvatar } from './ui-avatar.js';
27
+ export { AfriUIProgressRing } from './ui-progress-ring.js';
28
+ export { AfriUITooltip } from './ui-tooltip.js';
29
+ export { AfriUIPopover } from './ui-popover.js';
30
+ export { AfriUITabs } from './ui-tabs.js';
31
+ export { AfriUITable } from './ui-table.js';
32
+ export { AfriUIStat } from './ui-stat.js';
33
+ export { AfriFooter } from './footer.js';
34
+ export { AfriBreadcrumb } from './breadcrumb.js';
35
+ export { AfriFeatureGrid } from './feature-grid.js';
36
+ export { AfriPricingCard } from './pricing-card.js';
37
+ export { AfriTestimonial } from './testimonial.js';
38
+ export { AfriDashboardShell } from './dashboard-shell.js';
39
+ export { AfriDashboardTopbar } from './dashboard-topbar.js';
40
+ export { AfriDashboardCard } from './dashboard-card.js';
41
+ export { AfriDashboardMetric } from './dashboard-metric.js';
42
+ export { AfriDashboardActivityList } from './dashboard-activity-list.js';
17
43
 
18
44
  // Map of Tag Name -> Dynamic Import
19
45
  export const componentMap = {
@@ -33,6 +59,27 @@ export const componentMap = {
33
59
  'af-ui-input': () => import('./ui-input.js'),
34
60
  'af-ui-badge': () => import('./ui-badge.js'),
35
61
  'af-ui-switch': () => import('./ui-switch.js'),
62
+ 'af-ui-select': () => import('./ui-select.js'),
63
+ 'af-ui-textarea': () => import('./ui-textarea.js'),
64
+ 'af-ui-chip': () => import('./ui-chip.js'),
65
+ 'af-ui-tag': () => import('./ui-tag.js'),
66
+ 'af-ui-avatar': () => import('./ui-avatar.js'),
67
+ 'af-ui-progress-ring': () => import('./ui-progress-ring.js'),
68
+ 'af-ui-tooltip': () => import('./ui-tooltip.js'),
69
+ 'af-ui-popover': () => import('./ui-popover.js'),
70
+ 'af-ui-tabs': () => import('./ui-tabs.js'),
71
+ 'af-ui-table': () => import('./ui-table.js'),
72
+ 'af-ui-stat': () => import('./ui-stat.js'),
73
+ 'af-footer': () => import('./footer.js'),
74
+ 'af-breadcrumb': () => import('./breadcrumb.js'),
75
+ 'af-feature-grid': () => import('./feature-grid.js'),
76
+ 'af-pricing-card': () => import('./pricing-card.js'),
77
+ 'af-testimonial': () => import('./testimonial.js'),
78
+ 'af-dashboard-shell': () => import('./dashboard-shell.js'),
79
+ 'af-dashboard-topbar': () => import('./dashboard-topbar.js'),
80
+ 'af-dashboard-card': () => import('./dashboard-card.js'),
81
+ 'af-dashboard-metric': () => import('./dashboard-metric.js'),
82
+ 'af-dashboard-activity-list': () => import('./dashboard-activity-list.js'),
36
83
 
37
84
  // Enterprise
38
85
  'af-accordion': () => import('./accordion.js'),
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AfriCode Newsletter Signup
3
+ *
4
+ * Simple email capture component.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriNewsletterSignup extends AfriCodeComponent {
10
+ constructor(){ super(); this.render(); }
11
+ connectedCallback(){ this.shadowRoot.querySelector('form')?.addEventListener('submit',(e)=>{e.preventDefault();const f=new FormData(e.target);this.emit('subscribe',Object.fromEntries(f.entries()))}) }
12
+ render(){ this.shadowRoot.innerHTML = html`
13
+ <style>
14
+ :host{display:block}
15
+ form{display:flex;gap:8px}
16
+ input{flex:1;padding:10px;border-radius:8px;border:1px solid #e5e7eb}
17
+ button{padding:10px;border-radius:8px;background:#1EB53A;color:white;border:none}
18
+ </style>
19
+ <form><input name="email" type="email" placeholder="Your email" required/><button>Subscribe</button></form>
20
+ `}
21
+ }
22
+
23
+ registerComponent('af-newsletter-signup', AfriNewsletterSignup);
24
+ export default AfriNewsletterSignup;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * AfriCode Pagination
3
+ *
4
+ * Simple pagination control for lists.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriPagination extends AfriCodeComponent {
10
+ static get observedAttributes(){return ['page','pages']}
11
+ constructor(){super();this.render();}
12
+ attributeChangedCallback(){this.render();}
13
+ render(){const page=Number(this.getAttribute('page')||1);const pages=Number(this.getAttribute('pages')||1);
14
+ this.shadowRoot.innerHTML=html`
15
+ <style>
16
+ :host{display:block}
17
+ .wrap{display:flex;gap:8px}
18
+ button{padding:6px 10px;border-radius:8px;border:1px solid #e5e7eb;background:white}
19
+ </style>
20
+ <div class="wrap">
21
+ ${page>1?html`<button data-action="prev">Prev</button>`:''}
22
+ <span>Page ${page} / ${pages}</span>
23
+ ${page<pages?html`<button data-action="next">Next</button>`:''}
24
+ </div>
25
+ `;
26
+ this.shadowRoot.querySelectorAll('button').forEach(b=>b.addEventListener('click',e=>{const a=e.currentTarget.getAttribute('data-action');this.emit('navigate',{action:a})}))
27
+ }
28
+ }
29
+
30
+ registerComponent('af-pagination', AfriPagination);
31
+ export default AfriPagination;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * AfriCode Pricing Card Component
3
+ *
4
+ * Pricing plan card for SaaS and business websites.
5
+ * @module components/pricing-card
6
+ */
7
+
8
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
9
+
10
+ export class AfriPricingCard extends AfriCodeComponent {
11
+ static get observedAttributes() {
12
+ return ['title', 'price', 'frequency', 'highlight'];
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') || 'Standard';
26
+ const price = this.getAttribute('price') || '$49';
27
+ const frequency = this.getAttribute('frequency') || 'per month';
28
+ const highlight = this.hasAttribute('highlight');
29
+
30
+ this.shadowRoot.innerHTML = html`
31
+ <style>
32
+ :host {
33
+ display: block;
34
+ font-family: 'Inter', system-ui, sans-serif;
35
+ }
36
+
37
+ .card {
38
+ display: grid;
39
+ gap: 1rem;
40
+ padding: 1.75rem;
41
+ border-radius: 28px;
42
+ background: ${highlight ? 'linear-gradient(180deg, #1EB53A, #0f172a)' : '#ffffff'};
43
+ color: ${highlight ? '#ffffff' : '#0f172a'};
44
+ border: 1px solid rgba(15, 23, 42, 0.08);
45
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.08);
46
+ }
47
+
48
+ .title {
49
+ margin: 0;
50
+ font-size: 1.2rem;
51
+ font-weight: 700;
52
+ }
53
+
54
+ .price {
55
+ font-size: 2.75rem;
56
+ font-weight: 800;
57
+ line-height: 1;
58
+ }
59
+
60
+ .frequency {
61
+ color: ${highlight ? 'rgba(255,255,255,0.8)' : '#64748b'};
62
+ font-size: 0.95rem;
63
+ margin-top: -0.25rem;
64
+ }
65
+
66
+ .features {
67
+ display: grid;
68
+ gap: 0.75rem;
69
+ }
70
+
71
+ ::slotted(.feature) {
72
+ color: ${highlight ? '#e2e8f0' : '#475569'};
73
+ font-size: 0.95rem;
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.5rem;
77
+ }
78
+
79
+ .footer {
80
+ margin-top: 1rem;
81
+ }
82
+ </style>
83
+
84
+ <div class="card">
85
+ <div>
86
+ <h3 class="title">${title}</h3>
87
+ <div class="price">${price}</div>
88
+ <div class="frequency">${frequency}</div>
89
+ </div>
90
+ <div class="features">
91
+ <slot></slot>
92
+ </div>
93
+ <div class="footer"><slot name="action"></slot></div>
94
+ </div>
95
+ `;
96
+ }
97
+ }
98
+
99
+ registerComponent('af-pricing-card', AfriPricingCard);
100
+ export default AfriPricingCard;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * AfriCode Product Card
3
+ *
4
+ * Product display card for e-commerce pages.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriProductCard extends AfriCodeComponent {
10
+ static get observedAttributes() { return ['title','price','image','currency']; }
11
+ constructor(){ super(); this.render(); }
12
+ attributeChangedCallback(){ this.render(); }
13
+ render(){
14
+ const title = this.getAttribute('title') || 'Product';
15
+ const price = this.getAttribute('price') || '0';
16
+ const image = this.getAttribute('image') || '';
17
+ const currency = this.getAttribute('currency') || '$';
18
+ this.shadowRoot.innerHTML = html`
19
+ <style>
20
+ :host{display:block;font-family:Inter,system-ui,sans-serif}
21
+ .card{background:#fff;border-radius:12px;padding:12px;display:grid;gap:8px}
22
+ .media{height:180px;border-radius:8px;background:#f3f4f6;overflow:hidden}
23
+ .media img{width:100%;height:100%;object-fit:cover}
24
+ .meta{display:flex;justify-content:space-between;align-items:center}
25
+ .title{font-weight:700}
26
+ .price{font-weight:800}
27
+ .actions{display:flex;gap:8px}
28
+ button{padding:8px 12px;border-radius:8px;border:none;background:#1EB53A;color:white;cursor:pointer}
29
+ </style>
30
+ <div class="card">
31
+ <div class="media">${image?html`<img src="${image}" alt="${title}"/>` : ''}</div>
32
+ <div class="meta"><div class="title">${title}</div><div class="price">${currency}${price}</div></div>
33
+ <div class="actions"><button class="add">Add to cart</button><button class="view">View</button></div>
34
+ </div>
35
+ `;
36
+ this.shadowRoot.querySelector('.add')?.addEventListener('click',()=>this.emit('add-to-cart',{title,price,currency}))
37
+ }
38
+ }
39
+
40
+ registerComponent('af-product-card', AfriProductCard);
41
+ export default AfriProductCard;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AfriCode Search Box
3
+ *
4
+ * Search input with debounced emit for live search.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriSearchBox extends AfriCodeComponent {
10
+ constructor(){ super(); this._deb=null; this.render(); }
11
+ connectedCallback(){ this.shadowRoot.getElementById('q').addEventListener('input', (e)=>{ clearTimeout(this._deb); this._deb=setTimeout(()=>this.emit('search',{q:e.target.value}),300) }) }
12
+ render(){ this.shadowRoot.innerHTML = html`
13
+ <style>
14
+ :host{display:block}
15
+ .wrap{display:flex;gap:8px}
16
+ input{padding:10px;border-radius:8px;border:1px solid #e5e7eb;flex:1}
17
+ button{padding:10px;border-radius:8px;background:#0f172a;color:white;border:none}
18
+ </style>
19
+ <div class="wrap"><input id="q" placeholder="Search..."/><button aria-hidden>🔍</button></div>
20
+ `}
21
+ }
22
+
23
+ registerComponent('af-search-box', AfriSearchBox);
24
+ export default AfriSearchBox;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * AfriCode Simple Chart
3
+ *
4
+ * Tiny chart wrapper for simple bar/line charts using Canvas.
5
+ */
6
+
7
+ import { AfriCodeComponent, registerComponent, html } from './base.js';
8
+
9
+ export class AfriSimpleChart extends AfriCodeComponent {
10
+ static get observedAttributes(){return ['type','data']}
11
+ constructor(){ super(); this.render(); }
12
+ attributeChangedCallback(){ this.render(); }
13
+ render(){ const type=this.getAttribute('type')||'bar'; const dataAttr=this.getAttribute('data')||'[]'; let data=[]; try{data=JSON.parse(dataAttr)}catch(e){}
14
+ this.shadowRoot.innerHTML = html`
15
+ <style>canvas{width:100%;height:200px}</style>
16
+ <canvas></canvas>
17
+ `; const canvas=this.shadowRoot.querySelector('canvas'); if(canvas){ const ctx=canvas.getContext('2d'); canvas.width=600; canvas.height=200; ctx.clearRect(0,0,canvas.width,canvas.height); if(type==='bar'){ const w=canvas.width/data.length; data.forEach((v,i)=>{ const h=(v/Math.max(...data))*150; ctx.fillStyle='#1EB53A'; ctx.fillRect(i*w+10,canvas.height-20-h,w-20,h) }) } }
18
+ }
19
+ }
20
+
21
+ registerComponent('af-simple-chart', AfriSimpleChart);
22
+ export default AfriSimpleChart;