@africode/core 5.0.4 → 5.0.5
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.
- package/COMPONENT_SCHEMA.json +990 -836
- package/README.md +30 -0
- package/components/index.js +14 -0
- package/components/ui-badge.js +61 -0
- package/components/ui-button.js +149 -0
- package/components/ui-card.js +75 -0
- package/components/ui-input.js +110 -0
- package/components/ui-switch.js +85 -0
- package/core/cli/commands/build.js +31 -1
- package/core/cli/commands/dev.js +38 -1
- package/core/lipa-namba-journey.js +28 -6
- package/dist/africode.js +672 -422
- package/dist/africode.js.map +9 -4
- package/dist/build-info.json +3 -3
- package/dist/components.js +554 -304
- package/dist/components.js.map +9 -4
- package/package.json +1 -1
- package/templates/starter-tailwind/src/pages/index.js +30 -0
- package/templates/starter-tailwind/tailwind.config.cjs +18 -0
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ Following the rollout framework, AfriCode v5.0.0 implements a phased Pan-African
|
|
|
43
43
|
- **33+ Web Components**: African-themed UI elements with cultural semiotics
|
|
44
44
|
- **Procedural SVG Engine**: Fractal geometry for traditional patterns
|
|
45
45
|
- **Mobile-First Design**: Optimized for African connectivity constraints
|
|
46
|
+
- **Native UI Kit**: Shadcn-style Web Components like `af-ui-button`, `af-ui-card`, and `af-ui-input` for consistent app UI
|
|
46
47
|
|
|
47
48
|
## 🚀 Quick Start
|
|
48
49
|
|
|
@@ -342,6 +343,35 @@ Then use them in your HTML:
|
|
|
342
343
|
<af-button onclick="handleClick()">Click Me</af-button>
|
|
343
344
|
```
|
|
344
345
|
|
|
346
|
+
### Native AfriCode UI Kit
|
|
347
|
+
|
|
348
|
+
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.
|
|
349
|
+
|
|
350
|
+
The library is optimized for consistent spacing, elevation, and interactive states across theme-aware apps.
|
|
351
|
+
|
|
352
|
+
Use the Tailwind starter template for a ready-made UI experience:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
create-africode my-app --template starter-tailwind
|
|
356
|
+
cd my-app
|
|
357
|
+
bun install
|
|
358
|
+
bun run dev
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Example usage:
|
|
362
|
+
|
|
363
|
+
```html
|
|
364
|
+
<af-ui-card variant="outlined" clickable>
|
|
365
|
+
<h3>Actionable Panel</h3>
|
|
366
|
+
<p>Build data-driven experiences with native AfriCode UI components.</p>
|
|
367
|
+
<af-ui-badge variant="success" pill>Ready</af-ui-badge>
|
|
368
|
+
</af-ui-card>
|
|
369
|
+
|
|
370
|
+
<af-ui-input label="Email" placeholder="you@example.com"></af-ui-input>
|
|
371
|
+
<af-ui-switch checked></af-ui-switch>
|
|
372
|
+
<af-ui-button variant="primary">Submit</af-ui-button>
|
|
373
|
+
```
|
|
374
|
+
|
|
345
375
|
## ⚙️ Development
|
|
346
376
|
|
|
347
377
|
### Hot Module Replacement (HMR)
|
package/components/index.js
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
export { AfriCodeComponent } from './base.js';
|
|
9
9
|
export { AfriErrorBoundary } from './error-boundary.js';
|
|
10
10
|
|
|
11
|
+
// UI Kit Components
|
|
12
|
+
export { AfriUIButton } from './ui-button.js';
|
|
13
|
+
export { AfriUICard } from './ui-card.js';
|
|
14
|
+
export { AfriUIInput } from './ui-input.js';
|
|
15
|
+
export { AfriUIBadge } from './ui-badge.js';
|
|
16
|
+
export { AfriUISwitch } from './ui-switch.js';
|
|
17
|
+
|
|
11
18
|
// Map of Tag Name -> Dynamic Import
|
|
12
19
|
export const componentMap = {
|
|
13
20
|
// Core UI
|
|
@@ -20,6 +27,13 @@ export const componentMap = {
|
|
|
20
27
|
'af-section': () => import('./section.js'),
|
|
21
28
|
'af-icon': () => import('./icon.js'),
|
|
22
29
|
|
|
30
|
+
// AfriCode UI Kit
|
|
31
|
+
'af-ui-button': () => import('./ui-button.js'),
|
|
32
|
+
'af-ui-card': () => import('./ui-card.js'),
|
|
33
|
+
'af-ui-input': () => import('./ui-input.js'),
|
|
34
|
+
'af-ui-badge': () => import('./ui-badge.js'),
|
|
35
|
+
'af-ui-switch': () => import('./ui-switch.js'),
|
|
36
|
+
|
|
23
37
|
// Enterprise
|
|
24
38
|
'af-accordion': () => import('./accordion.js'),
|
|
25
39
|
'af-hero': () => import('./hero.js'),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode UI Badge
|
|
3
|
+
*
|
|
4
|
+
* A minimal badge component for status labels and counters.
|
|
5
|
+
* @module components/ui-badge
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
9
|
+
|
|
10
|
+
export class AfriUIBadge extends AfriCodeComponent {
|
|
11
|
+
static get observedAttributes() {
|
|
12
|
+
return ['variant', 'size', 'pill'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.render();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
attributeChangedCallback() {
|
|
21
|
+
this.render();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
const variant = this.getAttribute('variant') || 'secondary';
|
|
26
|
+
const size = this.getAttribute('size') || 'md';
|
|
27
|
+
const pill = this.hasAttribute('pill');
|
|
28
|
+
|
|
29
|
+
this.shadowRoot.innerHTML = html`
|
|
30
|
+
<style>
|
|
31
|
+
:host { display: inline-block; font-family: 'Inter', system-ui, sans-serif; }
|
|
32
|
+
|
|
33
|
+
.badge {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
gap: 0.4rem;
|
|
38
|
+
border-radius: ${pill ? '999px' : '0.6rem'};
|
|
39
|
+
padding: ${size === 'sm' ? '0.25rem 0.65rem' : size === 'lg' ? '0.55rem 1rem' : '0.4rem 0.8rem'};
|
|
40
|
+
font-size: ${size === 'sm' ? '0.75rem' : size === 'lg' ? '0.95rem' : '0.85rem'};
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
color: #111827;
|
|
43
|
+
background: var(--af-ui-badge-bg, #e2e8f0);
|
|
44
|
+
border: 1px solid transparent;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.badge.primary { background: #2563eb; color: #ffffff; }
|
|
49
|
+
.badge.success { background: #16a34a; color: #ffffff; }
|
|
50
|
+
.badge.warning { background: #f59e0b; color: #0f172a; }
|
|
51
|
+
.badge.danger { background: #dc2626; color: #ffffff; }
|
|
52
|
+
.badge.secondary { background: #f8fafc; color: #0f172a; }
|
|
53
|
+
</style>
|
|
54
|
+
|
|
55
|
+
<span class="badge ${variant}"><slot></slot></span>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerComponent('af-ui-badge', AfriUIBadge);
|
|
61
|
+
export default AfriUIBadge;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode UI Button
|
|
3
|
+
*
|
|
4
|
+
* A shadcn-style button component with variant and size options.
|
|
5
|
+
* Works well with Tailwind-style theming and Shadow DOM.
|
|
6
|
+
*
|
|
7
|
+
* @module components/ui-button
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
11
|
+
|
|
12
|
+
export class AfriUIButton extends AfriCodeComponent {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['variant', 'size', 'disabled', 'loading', 'full-width', 'aria-label'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connectedCallback() {
|
|
23
|
+
const button = this.shadowRoot.querySelector('button');
|
|
24
|
+
button?.addEventListener('click', (event) => {
|
|
25
|
+
if (this.hasAttribute('disabled') || this.hasAttribute('loading')) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.emit('af-ui-button-click', { originalEvent: event });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
attributeChangedCallback() {
|
|
34
|
+
this.render();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
const variant = this.getAttribute('variant') || 'primary';
|
|
39
|
+
const size = this.getAttribute('size') || 'md';
|
|
40
|
+
const isDisabled = this.hasAttribute('disabled');
|
|
41
|
+
const isLoading = this.hasAttribute('loading');
|
|
42
|
+
const ariaLabel = this.getAttribute('aria-label') || '';
|
|
43
|
+
|
|
44
|
+
this.shadowRoot.innerHTML = html`
|
|
45
|
+
<style>
|
|
46
|
+
:host { display: inline-block; }
|
|
47
|
+
|
|
48
|
+
button {
|
|
49
|
+
all: unset;
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
min-height: 42px;
|
|
52
|
+
padding: 0 18px;
|
|
53
|
+
border-radius: 999px;
|
|
54
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
gap: 0.5rem;
|
|
61
|
+
transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
border: 1px solid transparent;
|
|
64
|
+
user-select: none;
|
|
65
|
+
white-space: nowrap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:host([full-width]) button { width: 100%; }
|
|
69
|
+
|
|
70
|
+
button.primary {
|
|
71
|
+
background: #2563eb;
|
|
72
|
+
color: #fff;
|
|
73
|
+
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.24);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
button.primary:hover:not(:disabled) {
|
|
77
|
+
transform: translateY(-1px);
|
|
78
|
+
background: #1d4ed8;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
button.secondary {
|
|
82
|
+
background: #f8fafc;
|
|
83
|
+
color: #1f2937;
|
|
84
|
+
border-color: #cbd5e1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
button.secondary:hover:not(:disabled) {
|
|
88
|
+
background: #e2e8f0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
button.ghost {
|
|
92
|
+
background: transparent;
|
|
93
|
+
color: #0f172a;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
button.ghost:hover:not(:disabled) {
|
|
97
|
+
background: rgba(15, 23, 42, 0.05);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
button.link {
|
|
101
|
+
background: transparent;
|
|
102
|
+
color: #2563eb;
|
|
103
|
+
box-shadow: none;
|
|
104
|
+
padding: 0;
|
|
105
|
+
border-radius: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
button.link:hover:not(:disabled) {
|
|
109
|
+
text-decoration: underline;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
button.size-sm { min-height: 34px; padding: 0 14px; font-size: 0.85rem; }
|
|
113
|
+
button.size-md { min-height: 42px; padding: 0 18px; font-size: 0.95rem; }
|
|
114
|
+
button.size-lg { min-height: 50px; padding: 0 22px; font-size: 1rem; }
|
|
115
|
+
|
|
116
|
+
button:disabled {
|
|
117
|
+
opacity: 0.55;
|
|
118
|
+
cursor: not-allowed;
|
|
119
|
+
transform: none;
|
|
120
|
+
box-shadow: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.spinner {
|
|
124
|
+
width: 18px;
|
|
125
|
+
height: 18px;
|
|
126
|
+
border: 2px solid currentColor;
|
|
127
|
+
border-bottom-color: transparent;
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
animation: spin 0.8s linear infinite;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
133
|
+
</style>
|
|
134
|
+
|
|
135
|
+
<button
|
|
136
|
+
class="${variant} ${size}"
|
|
137
|
+
${isDisabled ? 'disabled' : ''}
|
|
138
|
+
${ariaLabel ? `aria-label="${ariaLabel}"` : ''}
|
|
139
|
+
${isLoading ? 'aria-busy="true"' : ''}
|
|
140
|
+
>
|
|
141
|
+
${isLoading ? html`<span class="spinner"></span>` : ''}
|
|
142
|
+
<slot></slot>
|
|
143
|
+
</button>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
registerComponent('af-ui-button', AfriUIButton);
|
|
149
|
+
export default AfriUIButton;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode UI Card
|
|
3
|
+
*
|
|
4
|
+
* A lightweight card component for content grouping.
|
|
5
|
+
* Provides subtle elevation and variant support.
|
|
6
|
+
*
|
|
7
|
+
* @module components/ui-card
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
11
|
+
|
|
12
|
+
export class AfriUICard extends AfriCodeComponent {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['variant', 'bordered', 'clickable'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connectedCallback() {
|
|
23
|
+
this.loadStyles();
|
|
24
|
+
if (this.hasAttribute('clickable')) {
|
|
25
|
+
this.shadowRoot.querySelector('.card')?.addEventListener('click', () => {
|
|
26
|
+
this.emit('af-ui-card-click');
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
attributeChangedCallback() {
|
|
32
|
+
this.render();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render() {
|
|
36
|
+
const variant = this.getAttribute('variant') || 'elevated';
|
|
37
|
+
const bordered = this.hasAttribute('bordered');
|
|
38
|
+
const clickable = this.hasAttribute('clickable');
|
|
39
|
+
|
|
40
|
+
this.shadowRoot.innerHTML = html`
|
|
41
|
+
<style>
|
|
42
|
+
:host { display: block; }
|
|
43
|
+
|
|
44
|
+
.card {
|
|
45
|
+
border-radius: 1rem;
|
|
46
|
+
padding: 1.5rem;
|
|
47
|
+
background: #ffffff;
|
|
48
|
+
color: #0f172a;
|
|
49
|
+
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
|
50
|
+
border: 1px solid transparent;
|
|
51
|
+
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.08);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.card.elevated { background: #fff; }
|
|
55
|
+
.card.flat { background: #f8fafc; box-shadow: none; }
|
|
56
|
+
.card.outlined { background: #fff; border-color: #e2e8f0; box-shadow: none; }
|
|
57
|
+
|
|
58
|
+
.card:hover:not(.disabled) {
|
|
59
|
+
transform: translateY(-2px);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:host([bordered]) .card { border-color: #cbd5e1; }
|
|
63
|
+
|
|
64
|
+
:host([clickable]) .card { cursor: pointer; }
|
|
65
|
+
</style>
|
|
66
|
+
|
|
67
|
+
<article class="card ${variant}">
|
|
68
|
+
<slot></slot>
|
|
69
|
+
</article>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
registerComponent('af-ui-card', AfriUICard);
|
|
75
|
+
export default AfriUICard;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode UI Input
|
|
3
|
+
*
|
|
4
|
+
* A styled input component with label support for forms and data entry.
|
|
5
|
+
*
|
|
6
|
+
* @module components/ui-input
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
10
|
+
|
|
11
|
+
export class AfriUIInput extends AfriCodeComponent {
|
|
12
|
+
static get observedAttributes() {
|
|
13
|
+
return ['label', 'placeholder', 'type', 'value', 'disabled', 'error'];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.render();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
this._attachListeners();
|
|
23
|
+
this.loadStyles();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
attributeChangedCallback() {
|
|
27
|
+
this.render();
|
|
28
|
+
this._attachListeners();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_attachListeners() {
|
|
32
|
+
const input = this.shadowRoot.querySelector('input');
|
|
33
|
+
if (!input) return;
|
|
34
|
+
input.addEventListener('input', () => {
|
|
35
|
+
this.emit('af-ui-input', { value: input.value });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
const label = this.getAttribute('label') || '';
|
|
41
|
+
const placeholder = this.getAttribute('placeholder') || '';
|
|
42
|
+
const type = this.getAttribute('type') || 'text';
|
|
43
|
+
const value = this.getAttribute('value') || '';
|
|
44
|
+
const disabled = this.hasAttribute('disabled');
|
|
45
|
+
const error = this.getAttribute('error') || '';
|
|
46
|
+
|
|
47
|
+
this.shadowRoot.innerHTML = html`
|
|
48
|
+
<style>
|
|
49
|
+
:host { display: block; font-family: 'Inter', system-ui, sans-serif; }
|
|
50
|
+
|
|
51
|
+
label {
|
|
52
|
+
display: block;
|
|
53
|
+
font-size: 0.85rem;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
margin-bottom: 0.5rem;
|
|
56
|
+
color: #111827;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.field {
|
|
60
|
+
display: grid;
|
|
61
|
+
gap: 0.45rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input {
|
|
65
|
+
width: 100%;
|
|
66
|
+
border: 1px solid #d1d5db;
|
|
67
|
+
border-radius: 0.75rem;
|
|
68
|
+
padding: 0.95rem 1rem;
|
|
69
|
+
font-size: 0.95rem;
|
|
70
|
+
color: #111827;
|
|
71
|
+
background: #ffffff;
|
|
72
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
input:focus {
|
|
76
|
+
outline: none;
|
|
77
|
+
border-color: #2563eb;
|
|
78
|
+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
input:disabled {
|
|
82
|
+
opacity: 0.65;
|
|
83
|
+
cursor: not-allowed;
|
|
84
|
+
background: #f8fafc;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.error {
|
|
88
|
+
color: #dc2626;
|
|
89
|
+
font-size: 0.8rem;
|
|
90
|
+
min-height: 1rem;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
93
|
+
|
|
94
|
+
<div class="field">
|
|
95
|
+
${label ? html`<label>${label}</label>` : ''}
|
|
96
|
+
<input
|
|
97
|
+
type="${type}"
|
|
98
|
+
placeholder="${placeholder}"
|
|
99
|
+
value="${value}"
|
|
100
|
+
${disabled ? 'disabled' : ''}
|
|
101
|
+
aria-invalid="${Boolean(error)}"
|
|
102
|
+
/>
|
|
103
|
+
<div class="error">${error}</div>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
registerComponent('af-ui-input', AfriUIInput);
|
|
110
|
+
export default AfriUIInput;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode UI Switch
|
|
3
|
+
*
|
|
4
|
+
* A form control switch component with accessible state.
|
|
5
|
+
* @module components/ui-switch
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
9
|
+
|
|
10
|
+
export class AfriUISwitch extends AfriCodeComponent {
|
|
11
|
+
static get observedAttributes() {
|
|
12
|
+
return ['checked', 'disabled', 'size'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.render();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connectedCallback() {
|
|
21
|
+
this.shadowRoot.querySelector('button')?.addEventListener('click', (event) => {
|
|
22
|
+
if (this.hasAttribute('disabled')) return;
|
|
23
|
+
const next = !this.hasAttribute('checked');
|
|
24
|
+
if (next) {
|
|
25
|
+
this.setAttribute('checked', '');
|
|
26
|
+
} else {
|
|
27
|
+
this.removeAttribute('checked');
|
|
28
|
+
}
|
|
29
|
+
this.emit('af-ui-switch-toggle', { checked: next, originalEvent: event });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
attributeChangedCallback() {
|
|
34
|
+
this.render();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
const checked = this.hasAttribute('checked');
|
|
39
|
+
const disabled = this.hasAttribute('disabled');
|
|
40
|
+
const size = this.getAttribute('size') || 'md';
|
|
41
|
+
const dimensions = size === 'sm' ? { width: 36, height: 20, knob: 14 } : size === 'lg' ? { width: 56, height: 32, knob: 24 } : { width: 48, height: 28, knob: 20 };
|
|
42
|
+
|
|
43
|
+
const ariaChecked = checked ? 'true' : 'false';
|
|
44
|
+
|
|
45
|
+
this.shadowRoot.innerHTML = html`
|
|
46
|
+
<style>
|
|
47
|
+
:host { display: inline-block; }
|
|
48
|
+
|
|
49
|
+
button {
|
|
50
|
+
all: unset;
|
|
51
|
+
position: relative;
|
|
52
|
+
width: ${dimensions.width}px;
|
|
53
|
+
height: ${dimensions.height}px;
|
|
54
|
+
border-radius: ${dimensions.height / 2}px;
|
|
55
|
+
background: ${checked ? '#2563eb' : '#e5e7eb'};
|
|
56
|
+
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
|
57
|
+
transition: background 180ms ease;
|
|
58
|
+
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.08);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.knob {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 50%;
|
|
64
|
+
left: ${checked ? `${dimensions.width - dimensions.knob - 4}px` : '4px'};
|
|
65
|
+
transform: translateY(-50%);
|
|
66
|
+
width: ${dimensions.knob}px;
|
|
67
|
+
height: ${dimensions.knob}px;
|
|
68
|
+
border-radius: 999px;
|
|
69
|
+
background: #ffffff;
|
|
70
|
+
transition: left 180ms ease;
|
|
71
|
+
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
button:disabled { opacity: 0.65; }
|
|
75
|
+
</style>
|
|
76
|
+
|
|
77
|
+
<button role="switch" aria-checked="${ariaChecked}" ${disabled ? 'disabled' : ''}>
|
|
78
|
+
<span class="knob"></span>
|
|
79
|
+
</button>
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
registerComponent('af-ui-switch', AfriUISwitch);
|
|
85
|
+
export default AfriUISwitch;
|
|
@@ -6,6 +6,35 @@ import { rm, cp, mkdir, readdir, writeFile } from 'fs/promises';
|
|
|
6
6
|
import { existsSync } from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
+
import { RawHtml } from '../../html.js';
|
|
10
|
+
|
|
11
|
+
export async function normalizeRenderOutput(htmlContent) {
|
|
12
|
+
if (htmlContent && typeof htmlContent.text === 'function') {
|
|
13
|
+
return await htmlContent.text();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (htmlContent instanceof RawHtml) {
|
|
17
|
+
return htmlContent.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof htmlContent === 'string') {
|
|
21
|
+
return htmlContent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (htmlContent === null || htmlContent === undefined) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof htmlContent === 'object') {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(htmlContent);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return String(htmlContent);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return String(htmlContent);
|
|
37
|
+
}
|
|
9
38
|
|
|
10
39
|
export async function build() {
|
|
11
40
|
logo();
|
|
@@ -90,7 +119,8 @@ export async function build() {
|
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
if (typeof pageModule.default === 'function') {
|
|
93
|
-
|
|
122
|
+
let htmlContent = await pageModule.default({ data: loaderData });
|
|
123
|
+
htmlContent = await normalizeRenderOutput(htmlContent);
|
|
94
124
|
const outputPath = path.join(distDir, `${name}.html`);
|
|
95
125
|
await writeFile(outputPath, htmlContent);
|
|
96
126
|
const renderEnd = performance.now();
|
package/core/cli/commands/dev.js
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
import { logo, colors } from '../ui.js';
|
|
2
2
|
import * as nodePath from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { createConnection } from 'node:net';
|
|
4
5
|
import { createHMRServer } from '../../hmr.js';
|
|
5
6
|
|
|
7
|
+
async function isPortAvailable(port) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const socket = createConnection({ port, host: '127.0.0.1', timeout: 300 }, () => {
|
|
10
|
+
socket.destroy();
|
|
11
|
+
resolve(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
socket.on('error', (err) => {
|
|
15
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'EHOSTUNREACH' || err.code === 'ENETUNREACH') {
|
|
16
|
+
resolve(true);
|
|
17
|
+
} else {
|
|
18
|
+
resolve(false);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
socket.on('timeout', () => {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
resolve(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function findAvailablePort(startPort, attempts = 10) {
|
|
30
|
+
for (let offset = 0; offset < attempts; offset++) {
|
|
31
|
+
const portToCheck = startPort + offset;
|
|
32
|
+
if (await isPortAvailable(portToCheck)) {
|
|
33
|
+
return portToCheck;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`No available ports found between ${startPort} and ${startPort + attempts - 1}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
6
39
|
export async function dev() {
|
|
7
40
|
logo();
|
|
8
41
|
console.log(`${colors.green}▶ Starting development server...${colors.reset}\n`);
|
|
9
42
|
|
|
10
|
-
const
|
|
43
|
+
const desiredPort = Number(process.env.PORT) || 3000;
|
|
44
|
+
const PORT = await findAvailablePort(desiredPort, 10);
|
|
45
|
+
if (PORT !== desiredPort) {
|
|
46
|
+
console.log(`${colors.gold}⚠ Port ${desiredPort} was unavailable; using ${PORT} instead.${colors.reset}`);
|
|
47
|
+
}
|
|
11
48
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
49
|
const __dirname = nodePath.dirname(__filename);
|
|
13
50
|
const frameworkRoot = nodePath.resolve(__dirname, '../../../');
|