@africode/core 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AfriCode Error Boundary Component
|
|
5
|
+
*
|
|
6
|
+
* Native Web Component implementation of an error boundary.
|
|
7
|
+
* Listens for 'af-component-error' events bubbling up from children.
|
|
8
|
+
* Gracefully displays a localized fallback UI.
|
|
9
|
+
*
|
|
10
|
+
* @module components/error-boundary
|
|
11
|
+
*/
|
|
12
|
+
export class AfriErrorBoundary extends AfriCodeComponent {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['theme', 'fallback-message'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this._hasError = false;
|
|
20
|
+
this._errorDetail = null;
|
|
21
|
+
this.render();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
connectedCallback() {
|
|
25
|
+
// Listen for internal errors bubbling up from custom AfriCode children
|
|
26
|
+
this.addEventListener('af-component-error', this._handleError.bind(this));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnectedCallback() {
|
|
30
|
+
this.removeEventListener('af-component-error', this._handleError.bind(this));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_handleError(e) {
|
|
34
|
+
// Prevent it from bubbling further up and crashing higher boundaries
|
|
35
|
+
e.stopPropagation();
|
|
36
|
+
|
|
37
|
+
this._hasError = true;
|
|
38
|
+
this._errorDetail = e.detail;
|
|
39
|
+
|
|
40
|
+
console.warn('[AfriCode Boundary] Caught error:', this._errorDetail);
|
|
41
|
+
this.render();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
reset() {
|
|
45
|
+
this._hasError = false;
|
|
46
|
+
this._errorDetail = null;
|
|
47
|
+
this.render();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attributeChangedCallback() {
|
|
51
|
+
this.render();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render() {
|
|
55
|
+
const theme = this.getAttribute('theme') || 'tanzania';
|
|
56
|
+
const fallbackMessage = this.getAttribute('fallback-message') || 'A part of this page failed to load.';
|
|
57
|
+
|
|
58
|
+
// Use appropriate semantic warning colors depending on culture theme
|
|
59
|
+
const themes = {
|
|
60
|
+
tanzania: { bg: '#FFF9E6', border: '#FCD116', text: '#5A4A00' },
|
|
61
|
+
maasai: { bg: '#FFEEEE', border: '#FF0000', text: '#8B0000' },
|
|
62
|
+
ndebele: { bg: '#EBF0FF', border: '#4169E1', text: '#002B5C' },
|
|
63
|
+
ocean: { bg: '#E6FAFF', border: '#00A3DD', text: '#004A66' }
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const t = themes[theme] || themes.tanzania;
|
|
67
|
+
|
|
68
|
+
if (this._hasError) {
|
|
69
|
+
this.shadowRoot.innerHTML = `
|
|
70
|
+
<style>
|
|
71
|
+
:host { display: block; width: 100%; font-family: 'Inter', system-ui, sans-serif; }
|
|
72
|
+
.boundary-error {
|
|
73
|
+
background: ${t.bg};
|
|
74
|
+
border-left: 4px solid ${t.border};
|
|
75
|
+
color: ${t.text};
|
|
76
|
+
padding: 16px 20px;
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
margin: 12px 0;
|
|
79
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
80
|
+
}
|
|
81
|
+
.boundary-header {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 12px;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
margin-bottom: 8px;
|
|
87
|
+
font-size: 16px;
|
|
88
|
+
}
|
|
89
|
+
.boundary-message {
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
opacity: 0.9;
|
|
92
|
+
margin-bottom: 12px;
|
|
93
|
+
}
|
|
94
|
+
.boundary-dev-detail {
|
|
95
|
+
font-family: monospace;
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
background: rgba(0,0,0,0.05);
|
|
98
|
+
padding: 8px;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
overflow-x: auto;
|
|
101
|
+
white-space: pre-wrap;
|
|
102
|
+
}
|
|
103
|
+
.boundary-action {
|
|
104
|
+
margin-top: 12px;
|
|
105
|
+
}
|
|
106
|
+
button {
|
|
107
|
+
background: transparent;
|
|
108
|
+
border: 1px solid ${t.border};
|
|
109
|
+
color: ${t.text};
|
|
110
|
+
padding: 6px 12px;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
transition: background 0.2s;
|
|
115
|
+
}
|
|
116
|
+
button:hover {
|
|
117
|
+
background: rgba(0,0,0,0.05);
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
120
|
+
<div class="boundary-error" role="alert">
|
|
121
|
+
<div class="boundary-header">
|
|
122
|
+
<span aria-hidden="true">🔄</span>
|
|
123
|
+
<span>Component Recovery</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="boundary-message">${fallbackMessage}</div>
|
|
126
|
+
|
|
127
|
+
${this._errorDetail ? `
|
|
128
|
+
<div class="boundary-dev-detail">
|
|
129
|
+
<code><${this._errorDetail.component}>: ${this._errorDetail.message}</code>
|
|
130
|
+
</div>
|
|
131
|
+
` : ''}
|
|
132
|
+
|
|
133
|
+
<div class="boundary-action">
|
|
134
|
+
<button id="retry-btn">Try Again</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
// Re-bind retry logic
|
|
140
|
+
this.shadowRoot.getElementById('retry-btn').addEventListener('click', () => {
|
|
141
|
+
this.reset();
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If no error, just passthrough the light DOM via slots
|
|
147
|
+
this.shadowRoot.innerHTML = `
|
|
148
|
+
<style>:host { display: block; }</style>
|
|
149
|
+
<slot></slot>
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
registerComponent('af-error-boundary', AfriErrorBoundary);
|
|
155
|
+
export default AfriErrorBoundary;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
2
|
+
import { Validation, schemas } from '../core/validation.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smart Form Component
|
|
6
|
+
* Handles submission, loading state, and validation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <af-form action="/api/login" method="POST">
|
|
10
|
+
* <af-input name="email"></af-input>
|
|
11
|
+
* </af-form>
|
|
12
|
+
*/
|
|
13
|
+
export class AfriForm extends AfriCodeComponent {
|
|
14
|
+
static get observedAttributes() { return ['action', 'method', 'error', 'success', 'schema']; }
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.handleSubmit = this.handleSubmit.bind(this);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
this.render();
|
|
23
|
+
this.shadowRoot.querySelector('form').addEventListener('submit', this.handleSubmit);
|
|
24
|
+
this.loadStyles();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async handleSubmit(e) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
const form = e.target;
|
|
30
|
+
const submitBtn = this.querySelector('[type="submit"]') || this.shadowRoot.querySelector('button[type="submit"]');
|
|
31
|
+
|
|
32
|
+
// Get validation schema
|
|
33
|
+
const schemaName = this.getAttribute('schema');
|
|
34
|
+
const schema = schemaName ? this._getSchema(schemaName) : null;
|
|
35
|
+
|
|
36
|
+
// derived from attributes
|
|
37
|
+
const action = this.getAttribute('action');
|
|
38
|
+
const method = this.getAttribute('method') || 'GET';
|
|
39
|
+
|
|
40
|
+
// Collect form data from native controls
|
|
41
|
+
const formData = new FormData(form);
|
|
42
|
+
const data = Object.fromEntries(formData.entries());
|
|
43
|
+
|
|
44
|
+
// Include custom af-input values that live in shadow DOM
|
|
45
|
+
Array.from(this.querySelectorAll('af-input')).forEach((customInput) => {
|
|
46
|
+
const name = customInput.getAttribute('name');
|
|
47
|
+
if (name) {
|
|
48
|
+
data[name] = customInput.value;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Validate if schema provided
|
|
53
|
+
if (schema) {
|
|
54
|
+
const validationResult = Validation.validate(schema, data);
|
|
55
|
+
if (!validationResult.success) {
|
|
56
|
+
this._showValidationErrors(validationResult.errors);
|
|
57
|
+
this.emit('af-validation-error', { errors: validationResult.errors });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!action) {return;}
|
|
63
|
+
|
|
64
|
+
// Loading State
|
|
65
|
+
if (submitBtn) {
|
|
66
|
+
var originalText = submitBtn.textContent;
|
|
67
|
+
submitBtn.textContent = 'Wait...';
|
|
68
|
+
submitBtn.disabled = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(action, {
|
|
73
|
+
method: method,
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(data)
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (response.ok) {
|
|
79
|
+
this.emit('success', await response.json());
|
|
80
|
+
this.setAttribute('success', 'Form submitted successfully!');
|
|
81
|
+
this.removeAttribute('error');
|
|
82
|
+
} else {
|
|
83
|
+
const errorData = await response.text();
|
|
84
|
+
this.emit('error', errorData);
|
|
85
|
+
this.setAttribute('error', errorData);
|
|
86
|
+
this.removeAttribute('success');
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
this.emit('error', err.message);
|
|
90
|
+
this.setAttribute('error', err.message);
|
|
91
|
+
this.removeAttribute('success');
|
|
92
|
+
} finally {
|
|
93
|
+
if (submitBtn) {
|
|
94
|
+
submitBtn.textContent = originalText;
|
|
95
|
+
submitBtn.disabled = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_getSchema(schemaName) {
|
|
101
|
+
return schemas[schemaName] || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_showValidationErrors(errors) {
|
|
105
|
+
// Clear previous errors
|
|
106
|
+
this.shadowRoot.querySelectorAll('.field-error').forEach(el => el.remove());
|
|
107
|
+
|
|
108
|
+
// Show errors on corresponding inputs
|
|
109
|
+
Object.entries(errors).forEach(([fieldName, errorMessage]) => {
|
|
110
|
+
const input = this.querySelector(`[name="${fieldName}"]`);
|
|
111
|
+
if (input) {
|
|
112
|
+
// For custom af-input components
|
|
113
|
+
if (input.tagName === 'AF-INPUT') {
|
|
114
|
+
input.setAttribute('error', errorMessage);
|
|
115
|
+
} else {
|
|
116
|
+
// For regular inputs, add error display
|
|
117
|
+
const errorDiv = document.createElement('div');
|
|
118
|
+
errorDiv.className = 'field-error';
|
|
119
|
+
errorDiv.textContent = errorMessage;
|
|
120
|
+
errorDiv.style.color = 'red';
|
|
121
|
+
errorDiv.style.fontSize = '12px';
|
|
122
|
+
errorDiv.style.marginTop = '4px';
|
|
123
|
+
input.parentNode.insertBefore(errorDiv, input.nextSibling);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
registerComponent('af-form', AfriForm);
|
|
131
|
+
export default AfriForm;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Grid Layout — <af-grid>
|
|
3
|
+
*
|
|
4
|
+
* A fully-featured, reactive responsive grid system.
|
|
5
|
+
*
|
|
6
|
+
* Attributes:
|
|
7
|
+
* min-width — Minimum column width before wrapping (default: 280px)
|
|
8
|
+
* gap — Grid gap spacing (default: 24px)
|
|
9
|
+
* columns — Fixed column count (overrides auto-fit / min-width)
|
|
10
|
+
* rows — Fixed row count for explicit grid
|
|
11
|
+
* layout — Preset layouts: 'auto' | 'masonry' | 'sidebar' | 'holy-grail' | 'bento'
|
|
12
|
+
* align — Align items: 'start' | 'center' | 'end' | 'stretch' (default: stretch)
|
|
13
|
+
* justify — Justify items: 'start' | 'center' | 'end' | 'stretch' (default: stretch)
|
|
14
|
+
* dense — Enable grid-auto-flow: dense (boolean attribute)
|
|
15
|
+
* sidebar-side — 'left' | 'right' for sidebar layout (default: left)
|
|
16
|
+
* sidebar-width — Width of the sidebar column (default: 280px)
|
|
17
|
+
*
|
|
18
|
+
* Events:
|
|
19
|
+
* af-grid-resize — Fired when grid reflows due to container resize
|
|
20
|
+
*
|
|
21
|
+
* Examples:
|
|
22
|
+
* <af-grid min-width="300px" gap="32px">...</af-grid>
|
|
23
|
+
* <af-grid columns="3" gap="16px">...</af-grid>
|
|
24
|
+
* <af-grid layout="sidebar" sidebar-width="240px">...</af-grid>
|
|
25
|
+
* <af-grid layout="bento" gap="16px">...</af-grid>
|
|
26
|
+
*/
|
|
27
|
+
import { AfriCodeComponent, registerComponent, html } from './base.js';
|
|
28
|
+
|
|
29
|
+
// Preset layout generators
|
|
30
|
+
function getLayoutTemplate(layout, attrs) {
|
|
31
|
+
const {
|
|
32
|
+
sidebarSide = 'left',
|
|
33
|
+
sidebarWidth = '280px',
|
|
34
|
+
minWidth = '280px',
|
|
35
|
+
gap = '24px',
|
|
36
|
+
columns,
|
|
37
|
+
rows,
|
|
38
|
+
align = 'stretch',
|
|
39
|
+
justify = 'stretch',
|
|
40
|
+
dense = false,
|
|
41
|
+
} = attrs;
|
|
42
|
+
|
|
43
|
+
const denseVal = dense ? 'dense' : '';
|
|
44
|
+
const base = `
|
|
45
|
+
align-items: ${align};
|
|
46
|
+
justify-items: ${justify};
|
|
47
|
+
gap: ${gap};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
switch (layout) {
|
|
51
|
+
case 'sidebar':
|
|
52
|
+
return sidebarSide === 'right'
|
|
53
|
+
? `display: grid; grid-template-columns: 1fr ${sidebarWidth}; ${base}`
|
|
54
|
+
: `display: grid; grid-template-columns: ${sidebarWidth} 1fr; ${base}`;
|
|
55
|
+
|
|
56
|
+
case 'holy-grail':
|
|
57
|
+
return `
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: ${sidebarWidth} 1fr ${sidebarWidth};
|
|
60
|
+
grid-template-rows: auto 1fr auto;
|
|
61
|
+
${base}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
case 'bento':
|
|
65
|
+
// Bento grid: 12-column base, children use grid-column/row spans
|
|
66
|
+
return `
|
|
67
|
+
display: grid;
|
|
68
|
+
grid-template-columns: repeat(12, 1fr);
|
|
69
|
+
grid-auto-rows: minmax(120px, auto);
|
|
70
|
+
grid-auto-flow: row ${denseVal};
|
|
71
|
+
${base}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
case 'masonry':
|
|
75
|
+
// CSS-native masonry (progressive enhancement)
|
|
76
|
+
return `
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: repeat(auto-fill, minmax(${minWidth}, 1fr));
|
|
79
|
+
grid-template-rows: masonry;
|
|
80
|
+
grid-auto-flow: row ${denseVal};
|
|
81
|
+
${base}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
case 'auto':
|
|
85
|
+
default: {
|
|
86
|
+
const colValue = columns
|
|
87
|
+
? `repeat(${columns}, 1fr)`
|
|
88
|
+
: `repeat(auto-fit, minmax(${minWidth}, 1fr))`;
|
|
89
|
+
const rowValue = rows ? `repeat(${rows}, 1fr)` : 'auto';
|
|
90
|
+
return `
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: ${colValue};
|
|
93
|
+
${rows ? `grid-template-rows: ${rowValue};` : ''}
|
|
94
|
+
grid-auto-flow: row ${denseVal};
|
|
95
|
+
${base}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class AfriGrid extends AfriCodeComponent {
|
|
102
|
+
static get observedAttributes() {
|
|
103
|
+
return [
|
|
104
|
+
'min-width',
|
|
105
|
+
'gap',
|
|
106
|
+
'columns',
|
|
107
|
+
'rows',
|
|
108
|
+
'layout',
|
|
109
|
+
'align',
|
|
110
|
+
'justify',
|
|
111
|
+
'dense',
|
|
112
|
+
'sidebar-side',
|
|
113
|
+
'sidebar-width',
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
attributeChangedCallback() {
|
|
118
|
+
if (this.isConnected) {
|
|
119
|
+
this.render();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_getAttrs() {
|
|
124
|
+
return {
|
|
125
|
+
minWidth: this.getAttribute('min-width') || '280px',
|
|
126
|
+
gap: this.getAttribute('gap') || '24px',
|
|
127
|
+
columns: this.getAttribute('columns') || null,
|
|
128
|
+
rows: this.getAttribute('rows') || null,
|
|
129
|
+
layout: this.getAttribute('layout') || 'auto',
|
|
130
|
+
align: this.getAttribute('align') || 'stretch',
|
|
131
|
+
justify: this.getAttribute('justify') || 'stretch',
|
|
132
|
+
dense: this.hasAttribute('dense'),
|
|
133
|
+
sidebarSide: this.getAttribute('sidebar-side') || 'left',
|
|
134
|
+
sidebarWidth: this.getAttribute('sidebar-width') || '280px',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_buildResponsiveBreakpoints(attrs) {
|
|
139
|
+
// Only apply responsive fallback to auto/masonry layouts
|
|
140
|
+
if (attrs.layout !== 'auto' && attrs.layout !== 'masonry') {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return `
|
|
145
|
+
@media (max-width: 768px) {
|
|
146
|
+
.grid {
|
|
147
|
+
grid-template-columns: repeat(auto-fit, minmax(min(${attrs.minWidth}, 100%), 1fr));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
@media (max-width: 480px) {
|
|
151
|
+
.grid {
|
|
152
|
+
grid-template-columns: 1fr;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_buildSidebarResponsive(attrs) {
|
|
159
|
+
if (attrs.layout !== 'sidebar' && attrs.layout !== 'holy-grail') {
|
|
160
|
+
return '';
|
|
161
|
+
}
|
|
162
|
+
return `
|
|
163
|
+
@media (max-width: 768px) {
|
|
164
|
+
.grid {
|
|
165
|
+
grid-template-columns: 1fr;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
render() {
|
|
172
|
+
const attrs = this._getAttrs();
|
|
173
|
+
const gridStyles = getLayoutTemplate(attrs.layout, attrs);
|
|
174
|
+
const responsiveStyles =
|
|
175
|
+
this._buildResponsiveBreakpoints(attrs) + this._buildSidebarResponsive(attrs);
|
|
176
|
+
|
|
177
|
+
this.shadowRoot.innerHTML = html`
|
|
178
|
+
<style>
|
|
179
|
+
:host {
|
|
180
|
+
display: block;
|
|
181
|
+
width: 100%;
|
|
182
|
+
box-sizing: border-box;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.grid {
|
|
186
|
+
${gridStyles}
|
|
187
|
+
width: 100%;
|
|
188
|
+
box-sizing: border-box;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Slotted children base styles */
|
|
192
|
+
::slotted(*) {
|
|
193
|
+
min-width: 0; /* Prevent overflow in grid context */
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
${responsiveStyles}
|
|
197
|
+
</style>
|
|
198
|
+
|
|
199
|
+
<div
|
|
200
|
+
class="grid"
|
|
201
|
+
role="list"
|
|
202
|
+
part="grid"
|
|
203
|
+
aria-label="${this.getAttribute('aria-label') || 'Grid layout'}"
|
|
204
|
+
>
|
|
205
|
+
<slot></slot>
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
connectedCallback() {
|
|
211
|
+
this.render();
|
|
212
|
+
this._setupResizeObserver();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
disconnectedCallback() {
|
|
216
|
+
if (this._resizeObserver) {
|
|
217
|
+
this._resizeObserver.disconnect();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_setupResizeObserver() {
|
|
222
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let lastWidth = this.offsetWidth;
|
|
227
|
+
this._resizeObserver = new ResizeObserver((entries) => {
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const newWidth = entry.contentRect.width;
|
|
230
|
+
if (Math.abs(newWidth - lastWidth) > 10) {
|
|
231
|
+
lastWidth = newWidth;
|
|
232
|
+
this.emit('af-grid-resize', {
|
|
233
|
+
width: newWidth,
|
|
234
|
+
height: entry.contentRect.height,
|
|
235
|
+
layout: this.getAttribute('layout') || 'auto',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
this._resizeObserver.observe(this);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Public API: programmatically update grid layout
|
|
246
|
+
* @param {string} layout - New layout preset
|
|
247
|
+
*/
|
|
248
|
+
setLayout(layout) {
|
|
249
|
+
this.setAttribute('layout', layout);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Public API: programmatically set column count
|
|
254
|
+
* @param {number} count - Number of columns
|
|
255
|
+
*/
|
|
256
|
+
setColumns(count) {
|
|
257
|
+
this.setAttribute('columns', String(count));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Public API: toggle dense packing
|
|
262
|
+
* @param {boolean} enabled
|
|
263
|
+
*/
|
|
264
|
+
setDense(enabled) {
|
|
265
|
+
if (enabled) {
|
|
266
|
+
this.setAttribute('dense', '');
|
|
267
|
+
} else {
|
|
268
|
+
this.removeAttribute('dense');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
registerComponent('af-grid', AfriGrid);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { AfriCodeComponent, registerComponent } from './base.js';
|
|
2
|
+
import { html } from '../core/html.js';
|
|
3
|
+
import { store } from '../core/store.js';
|
|
4
|
+
import { subscribe } from '../core/state.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AfriHero Component
|
|
8
|
+
*
|
|
9
|
+
* High-impact hero section with support for patterns and rhythmic layout.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <af-hero
|
|
13
|
+
* title="AfriCode"
|
|
14
|
+
* subtitle="The Rhythmic Web Framework"
|
|
15
|
+
* pattern="kente">
|
|
16
|
+
* </af-hero>
|
|
17
|
+
*/
|
|
18
|
+
export class AfriHero extends AfriCodeComponent {
|
|
19
|
+
static get observedAttributes() { return ['title', 'subtitle', 'pattern', 'overlay']; }
|
|
20
|
+
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
super.connectedCallback();
|
|
23
|
+
this.render();
|
|
24
|
+
this.loadStyles();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render() {
|
|
28
|
+
const title = this.getAttribute('title') || 'AfriCode';
|
|
29
|
+
const subtitle = this.getAttribute('subtitle') || '';
|
|
30
|
+
const pattern = this.getAttribute('pattern') || '';
|
|
31
|
+
const variant = this.getAttribute('variant') || 'default';
|
|
32
|
+
|
|
33
|
+
this.shadowRoot.innerHTML = `
|
|
34
|
+
<style>
|
|
35
|
+
:host {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
text-align: center;
|
|
41
|
+
padding: 120px 5%;
|
|
42
|
+
min-height: 60vh;
|
|
43
|
+
background: var(--bg-base, #020617);
|
|
44
|
+
color: var(--text-main, white);
|
|
45
|
+
position: relative;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.content {
|
|
50
|
+
position: relative;
|
|
51
|
+
z-index: 10;
|
|
52
|
+
max-width: 900px;
|
|
53
|
+
opacity: 0;
|
|
54
|
+
transform: translateY(20px);
|
|
55
|
+
animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes fadeInUp {
|
|
59
|
+
to { opacity: 1; transform: translateY(0); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
h1 {
|
|
63
|
+
font-family: 'Outfit', 'Space Grotesk', sans-serif;
|
|
64
|
+
font-size: clamp(3rem, 8vw, 5rem);
|
|
65
|
+
font-weight: 900;
|
|
66
|
+
line-height: 1.1;
|
|
67
|
+
margin-bottom: 24px;
|
|
68
|
+
letter-spacing: -0.04em;
|
|
69
|
+
background: linear-gradient(135deg, var(--text-main, #fff) 30%, var(--text-muted, rgba(255,255,255,0.5)) 100%);
|
|
70
|
+
-webkit-background-clip: text;
|
|
71
|
+
-webkit-text-fill-color: transparent;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
p {
|
|
75
|
+
font-family: 'Inter', sans-serif;
|
|
76
|
+
font-size: clamp(1.1rem, 2vw, 1.5rem);
|
|
77
|
+
color: var(--text-secondary, #94a3b8);
|
|
78
|
+
line-height: 1.5;
|
|
79
|
+
margin-bottom: 40px;
|
|
80
|
+
max-width: 700px;
|
|
81
|
+
margin-left: auto;
|
|
82
|
+
margin-right: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Visual Accents */
|
|
86
|
+
.aura {
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset: 0;
|
|
89
|
+
background: radial-gradient(circle at 50% -20%, rgba(30,181,58,0.15), transparent 70%);
|
|
90
|
+
z-index: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.mesh {
|
|
94
|
+
position: absolute;
|
|
95
|
+
inset: 0;
|
|
96
|
+
background-image: radial-gradient(rgba(255,255,255,0.03) 1px, transparent 1px);
|
|
97
|
+
background-size: 40px 40px;
|
|
98
|
+
z-index: 2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
${pattern ? `
|
|
102
|
+
.pattern-bg {
|
|
103
|
+
position: absolute;
|
|
104
|
+
inset: 0;
|
|
105
|
+
background-image: url('${pattern}');
|
|
106
|
+
background-size: cover;
|
|
107
|
+
background-position: center;
|
|
108
|
+
opacity: 0.15;
|
|
109
|
+
z-index: 1;
|
|
110
|
+
}
|
|
111
|
+
` : ''}
|
|
112
|
+
|
|
113
|
+
::slotted([slot="actions"]) {
|
|
114
|
+
display: flex;
|
|
115
|
+
gap: 16px;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
flex-wrap: wrap;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
120
|
+
|
|
121
|
+
<div class="aura"></div>
|
|
122
|
+
<div class="mesh"></div>
|
|
123
|
+
${pattern ? `<div class="pattern-bg"></div>` : ''}
|
|
124
|
+
|
|
125
|
+
<div class="content">
|
|
126
|
+
<h1>${title}</h1>
|
|
127
|
+
${subtitle ? `<p>${subtitle}</p>` : ''}
|
|
128
|
+
<div class="actions">
|
|
129
|
+
<slot name="actions"></slot>
|
|
130
|
+
</div>
|
|
131
|
+
<slot></slot>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
registerComponent('af-hero', AfriHero);
|
|
138
|
+
export default AfriHero;
|