@kaiserofthenight/human-js 1.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/README.md +545 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +201 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +66 -0
- package/src/core/component.js +182 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +151 -0
- package/src/core/router.js +182 -0
- package/src/core/state.js +114 -0
- package/src/index.js +63 -0
- package/src/plugins/http.js +167 -0
- package/src/plugins/storage.js +181 -0
- package/src/plugins/validator.js +193 -0
- package/src/utils/dom.js +0 -0
- package/src/utils/helpers.js +209 -0
package/README.md
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# 🦉 HumanJS Framework
|
|
2
|
+
|
|
3
|
+
**A framework built for humans, not machines.**
|
|
4
|
+
|
|
5
|
+
Simple. Readable. Easy to extend. Zero magic. No build tools required.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🌟 Philosophy
|
|
10
|
+
|
|
11
|
+
HumanJS prioritizes human understanding over machine optimization.
|
|
12
|
+
|
|
13
|
+
### Core Principles
|
|
14
|
+
|
|
15
|
+
1. **Zero Magic** - Every behavior is traceable in ~200 lines of code
|
|
16
|
+
2. **Human-First API** - Read like English sentences
|
|
17
|
+
3. **Minimal Abstractions** - Use native DOM APIs when possible
|
|
18
|
+
4. **Easy to Extend** - Modify the source, don't fight it
|
|
19
|
+
5. **No Heavy Dependencies** - Pure JavaScript (ES6+), browser APIs only
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🚀 Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Clone or Download
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/kaiserofthenight/humanjs.git
|
|
29
|
+
cd humanjs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Open in Browser
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# No build step required!
|
|
36
|
+
# Just open index.html in your browser
|
|
37
|
+
open index.html
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Your First App
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { app, html } from './src/index.js';
|
|
44
|
+
|
|
45
|
+
app.create({
|
|
46
|
+
state: { count: 0 },
|
|
47
|
+
|
|
48
|
+
render: (state) => {
|
|
49
|
+
const element = html`
|
|
50
|
+
<div style="padding: 40px; text-align: center;">
|
|
51
|
+
<h1>Count: ${state.count}</h1>
|
|
52
|
+
<button id="increment">+1</button>
|
|
53
|
+
</div>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const events = {
|
|
57
|
+
'#increment': {
|
|
58
|
+
click: () => state.count += 1
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return { element, events };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**That's it!** No compilation, no bundling, no configuration.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 📚 Core Concepts
|
|
72
|
+
|
|
73
|
+
### 1. Reactive State
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
import { createState } from './src/index.js';
|
|
77
|
+
|
|
78
|
+
const state = createState({ count: 0 }, (prop, value) => {
|
|
79
|
+
console.log(`${prop} changed to ${value}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
state.count = 5; // Triggers callback
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Advanced state features:**
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// Watch specific properties
|
|
89
|
+
state.$watch('count', (newVal, oldVal) => {
|
|
90
|
+
console.log(`Count: ${oldVal} → ${newVal}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Computed properties
|
|
94
|
+
state.$computed('double', function() {
|
|
95
|
+
return this.count * 2;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Reset state
|
|
99
|
+
state.$reset({ count: 0 });
|
|
100
|
+
|
|
101
|
+
// Get raw state
|
|
102
|
+
const raw = state.$raw();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. HTML Rendering
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
import { html } from './src/index.js';
|
|
109
|
+
|
|
110
|
+
// Simple
|
|
111
|
+
const greeting = html`<h1>Hello, World!</h1>`;
|
|
112
|
+
|
|
113
|
+
// With dynamic values
|
|
114
|
+
const count = 5;
|
|
115
|
+
const counter = html`<div>Count: ${count}</div>`;
|
|
116
|
+
|
|
117
|
+
// With arrays
|
|
118
|
+
const items = ['Apple', 'Banana', 'Orange'];
|
|
119
|
+
const list = html`
|
|
120
|
+
<ul>
|
|
121
|
+
${items.map(item => html`<li>${item}</li>`)}
|
|
122
|
+
</ul>
|
|
123
|
+
`;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Conditional rendering:**
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
import { when } from './src/index.js';
|
|
130
|
+
|
|
131
|
+
when(
|
|
132
|
+
user.isLoggedIn,
|
|
133
|
+
() => html`<div>Welcome, ${user.name}!</div>`,
|
|
134
|
+
() => html`<div>Please log in</div>`
|
|
135
|
+
);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**List rendering:**
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
import { each } from './src/index.js';
|
|
142
|
+
|
|
143
|
+
each(users, (user) => html`
|
|
144
|
+
<div class="user-card">
|
|
145
|
+
<h3>${user.name}</h3>
|
|
146
|
+
<p>${user.email}</p>
|
|
147
|
+
</div>
|
|
148
|
+
`);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 3. Components
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
import { app, html } from './src/index.js';
|
|
155
|
+
|
|
156
|
+
app.create({
|
|
157
|
+
// Where to mount
|
|
158
|
+
root: document.getElementById('app'),
|
|
159
|
+
|
|
160
|
+
// Initial state
|
|
161
|
+
state: {
|
|
162
|
+
count: 0,
|
|
163
|
+
user: { name: 'John' }
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Render function
|
|
167
|
+
render: (state) => {
|
|
168
|
+
const element = html`<div>${state.count}</div>`;
|
|
169
|
+
const events = {
|
|
170
|
+
'#btn': { click: () => state.count++ }
|
|
171
|
+
};
|
|
172
|
+
return { element, events };
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// Lifecycle hooks
|
|
176
|
+
onMount: (state) => {
|
|
177
|
+
console.log('Component mounted!');
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
onUpdate: (state) => {
|
|
181
|
+
console.log('State updated!');
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
onDestroy: (state) => {
|
|
185
|
+
console.log('Component destroyed!');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 4. Event Handling
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Basic events
|
|
194
|
+
const events = {
|
|
195
|
+
'#button': {
|
|
196
|
+
click: () => console.log('Clicked!'),
|
|
197
|
+
mouseover: () => console.log('Hovered!')
|
|
198
|
+
},
|
|
199
|
+
'#input': {
|
|
200
|
+
input: (e) => state.value = e.target.value,
|
|
201
|
+
focus: () => console.log('Focused!')
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Form submission
|
|
206
|
+
'#form': {
|
|
207
|
+
submit: (e) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
const data = getFormData(e.target);
|
|
210
|
+
console.log(data);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Event helpers:**
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
import { on, debounce, throttle } from './src/index.js';
|
|
219
|
+
|
|
220
|
+
// Debounce search input
|
|
221
|
+
const handleSearch = debounce((e) => {
|
|
222
|
+
state.query = e.target.value;
|
|
223
|
+
}, 300);
|
|
224
|
+
|
|
225
|
+
// Throttle scroll events
|
|
226
|
+
const handleScroll = throttle(() => {
|
|
227
|
+
console.log('Scrolling...');
|
|
228
|
+
}, 100);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 5. Routing
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
import { createRouter, Link } from './src/index.js';
|
|
235
|
+
|
|
236
|
+
const router = createRouter({
|
|
237
|
+
'/': () => html`<h1>Home Page</h1>`,
|
|
238
|
+
'/about': () => html`<h1>About Page</h1>`,
|
|
239
|
+
'/user/:id': (params) => html`<h1>User ${params.id}</h1>`,
|
|
240
|
+
'*': () => html`<h1>404 - Not Found</h1>`
|
|
241
|
+
}, {
|
|
242
|
+
beforeEach: (to, from, params) => {
|
|
243
|
+
console.log(`Navigating from ${from} to ${to}`);
|
|
244
|
+
},
|
|
245
|
+
afterEach: (to, from, params) => {
|
|
246
|
+
console.log(`Navigated to ${to}`);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Navigate programmatically
|
|
251
|
+
router.navigate('/about');
|
|
252
|
+
|
|
253
|
+
// Create links
|
|
254
|
+
const link = Link('/about', 'About Us', 'nav-link');
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 🔌 Plugins
|
|
260
|
+
|
|
261
|
+
### Form Validation
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
import { createValidator, rules, displayErrors } from './src/plugins/validator.js';
|
|
265
|
+
|
|
266
|
+
const validator = createValidator({
|
|
267
|
+
email: [rules.required, rules.email],
|
|
268
|
+
password: [rules.required, rules.minLength(8)],
|
|
269
|
+
age: [rules.required, rules.number, rules.min(18)],
|
|
270
|
+
confirmPassword: [rules.required, rules.match('password', 'Password')]
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = validator.validate(formData);
|
|
274
|
+
|
|
275
|
+
if (!result.isValid) {
|
|
276
|
+
displayErrors(formElement, result.errors);
|
|
277
|
+
} else {
|
|
278
|
+
// Submit form
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Available rules:**
|
|
283
|
+
- `required` - Field must have a value
|
|
284
|
+
- `email` - Valid email format
|
|
285
|
+
- `minLength(n)` - Minimum length
|
|
286
|
+
- `maxLength(n)` - Maximum length
|
|
287
|
+
- `min(n)` - Minimum numeric value
|
|
288
|
+
- `max(n)` - Maximum numeric value
|
|
289
|
+
- `pattern(regex, msg)` - Custom regex
|
|
290
|
+
- `url` - Valid URL
|
|
291
|
+
- `number` - Numeric value
|
|
292
|
+
- `integer` - Integer value
|
|
293
|
+
- `match(field, name)` - Match another field
|
|
294
|
+
- `custom(fn, msg)` - Custom validation
|
|
295
|
+
|
|
296
|
+
### HTTP Client
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
import { createHttp, http } from './src/plugins/http.js';
|
|
300
|
+
|
|
301
|
+
// Use default instance
|
|
302
|
+
const { data } = await http.get('/api/users');
|
|
303
|
+
await http.post('/api/users', { name: 'John' });
|
|
304
|
+
await http.put('/api/users/1', { name: 'Jane' });
|
|
305
|
+
await http.delete('/api/users/1');
|
|
306
|
+
|
|
307
|
+
// Custom instance
|
|
308
|
+
const api = createHttp({
|
|
309
|
+
baseURL: 'https://api.example.com',
|
|
310
|
+
headers: { 'Authorization': 'Bearer token' },
|
|
311
|
+
timeout: 10000,
|
|
312
|
+
onRequest: (config) => {
|
|
313
|
+
console.log('Request:', config);
|
|
314
|
+
},
|
|
315
|
+
onResponse: (response) => {
|
|
316
|
+
console.log('Response:', response);
|
|
317
|
+
},
|
|
318
|
+
onError: (error) => {
|
|
319
|
+
console.error('Error:', error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const users = await api.get('/users');
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Storage
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
import { local, session, createNamespace } from './src/plugins/storage.js';
|
|
330
|
+
|
|
331
|
+
// LocalStorage (persists)
|
|
332
|
+
local.set('user', { name: 'John', age: 30 });
|
|
333
|
+
const user = local.get('user');
|
|
334
|
+
local.remove('user');
|
|
335
|
+
local.clear();
|
|
336
|
+
|
|
337
|
+
// SessionStorage (cleared on close)
|
|
338
|
+
session.set('token', 'abc123');
|
|
339
|
+
const token = session.get('token');
|
|
340
|
+
|
|
341
|
+
// Namespaced storage
|
|
342
|
+
const appStorage = createNamespace('myapp');
|
|
343
|
+
appStorage.local.set('settings', { theme: 'dark' });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## 🛠️ Utilities
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
import { helpers } from './src/index.js';
|
|
352
|
+
|
|
353
|
+
// Generate unique ID
|
|
354
|
+
const id = helpers.uid('todo'); // 'todo_1234567890_abc'
|
|
355
|
+
|
|
356
|
+
// Deep clone
|
|
357
|
+
const copy = helpers.clone(originalObject);
|
|
358
|
+
|
|
359
|
+
// Deep merge
|
|
360
|
+
const merged = helpers.merge(obj1, obj2, obj3);
|
|
361
|
+
|
|
362
|
+
// Format date
|
|
363
|
+
const formatted = helpers.formatDate(new Date(), 'YYYY-MM-DD HH:mm');
|
|
364
|
+
|
|
365
|
+
// Sleep/delay
|
|
366
|
+
await helpers.sleep(1000); // Wait 1 second
|
|
367
|
+
|
|
368
|
+
// String utilities
|
|
369
|
+
helpers.capitalize('hello'); // 'Hello'
|
|
370
|
+
helpers.truncate('Long text...', 10); // 'Long text...'
|
|
371
|
+
|
|
372
|
+
// Array utilities
|
|
373
|
+
helpers.shuffle([1, 2, 3, 4, 5]);
|
|
374
|
+
helpers.unique([1, 2, 2, 3, 3]); // [1, 2, 3]
|
|
375
|
+
helpers.groupBy(users, 'role');
|
|
376
|
+
|
|
377
|
+
// Object utilities
|
|
378
|
+
helpers.get(obj, 'user.address.city', 'Unknown');
|
|
379
|
+
helpers.set(obj, 'user.name', 'John');
|
|
380
|
+
helpers.pick(obj, ['name', 'email']);
|
|
381
|
+
helpers.omit(obj, ['password', 'token']);
|
|
382
|
+
|
|
383
|
+
// Async utilities
|
|
384
|
+
await helpers.waitFor(() => element.isReady, 5000);
|
|
385
|
+
await helpers.retry(() => fetchData(), 3, 1000);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## 📦 Project Structure
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
humanjs-framework/
|
|
394
|
+
├── index.html # Demo page
|
|
395
|
+
├── README.md # This file
|
|
396
|
+
│
|
|
397
|
+
├── src/ # Framework source
|
|
398
|
+
│ ├── core/ # Core modules
|
|
399
|
+
│ │ ├── state.js # Reactive state
|
|
400
|
+
│ │ ├── render.js # HTML rendering
|
|
401
|
+
│ │ ├── component.js # Components
|
|
402
|
+
│ │ ├── router.js # SPA routing
|
|
403
|
+
│ │ └── events.js # Event handling
|
|
404
|
+
│ │
|
|
405
|
+
│ ├── plugins/ # Optional plugins
|
|
406
|
+
│ │ ├── validator.js # Form validation
|
|
407
|
+
│ │ ├── http.js # HTTP client
|
|
408
|
+
│ │ └── storage.js # Storage wrapper
|
|
409
|
+
│ │
|
|
410
|
+
│ ├── utils/ # Utilities
|
|
411
|
+
│ │ └── helpers.js # Helper functions
|
|
412
|
+
│ │
|
|
413
|
+
│ └── index.js # Main entry point
|
|
414
|
+
│
|
|
415
|
+
└── examples/ # Example apps
|
|
416
|
+
├── todo-app/
|
|
417
|
+
│ └── app.js
|
|
418
|
+
└── api-example/
|
|
419
|
+
└── app.js
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 🎓 Examples
|
|
425
|
+
|
|
426
|
+
### Todo App
|
|
427
|
+
|
|
428
|
+
A complete todo application with:
|
|
429
|
+
- Add, edit, delete todos
|
|
430
|
+
- Mark as complete
|
|
431
|
+
- Filter (all, active, completed)
|
|
432
|
+
- LocalStorage persistence
|
|
433
|
+
- Form validation
|
|
434
|
+
|
|
435
|
+
[View Source](./examples/todo-app/app.js)
|
|
436
|
+
|
|
437
|
+
### API Integration
|
|
438
|
+
|
|
439
|
+
Demonstrates API usage with:
|
|
440
|
+
- Fetch users from API
|
|
441
|
+
- Loading states
|
|
442
|
+
- Error handling
|
|
443
|
+
- Search and filter
|
|
444
|
+
- Debounced input
|
|
445
|
+
|
|
446
|
+
[View Source](./examples/api-example/app.js)
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## 🆚 Comparison with Other Frameworks
|
|
451
|
+
|
|
452
|
+
### vs React
|
|
453
|
+
|
|
454
|
+
| Feature | HumanJS | React |
|
|
455
|
+
|---------|---------|-------|
|
|
456
|
+
| Learning curve | 1 day | 1-2 weeks |
|
|
457
|
+
| Setup | Open HTML file | Complex build setup |
|
|
458
|
+
| Bundle size | ~5KB | ~40KB (min) |
|
|
459
|
+
| Abstraction | Minimal | Heavy (JSX, virtual DOM) |
|
|
460
|
+
| Debugging | Trace in source | Complex stack traces |
|
|
461
|
+
|
|
462
|
+
### vs Vue
|
|
463
|
+
|
|
464
|
+
| Feature | HumanJS | Vue |
|
|
465
|
+
|---------|---------|-----|
|
|
466
|
+
| Template syntax | Native HTML | Custom directives |
|
|
467
|
+
| Reactivity | Proxy (native) | Proxy + compiler |
|
|
468
|
+
| Components | Functions | Classes/objects |
|
|
469
|
+
| Build step | None | Required for SFC |
|
|
470
|
+
|
|
471
|
+
### vs Svelte
|
|
472
|
+
|
|
473
|
+
| Feature | HumanJS | Svelte |
|
|
474
|
+
|---------|---------|--------|
|
|
475
|
+
| Compilation | None | Required |
|
|
476
|
+
| Runtime size | ~5KB | ~2KB |
|
|
477
|
+
| Learning curve | 1 day | 3-4 days |
|
|
478
|
+
| Debugging | Browser native | Compiled output |
|
|
479
|
+
|
|
480
|
+
### When to use HumanJS
|
|
481
|
+
|
|
482
|
+
✅ **Use when:**
|
|
483
|
+
- Building simple to medium apps
|
|
484
|
+
- Learning frontend concepts
|
|
485
|
+
- Prototyping quickly
|
|
486
|
+
- Want full control
|
|
487
|
+
- No build step desired
|
|
488
|
+
|
|
489
|
+
❌ **Don't use when:**
|
|
490
|
+
- Building complex enterprise apps
|
|
491
|
+
- Need large ecosystem
|
|
492
|
+
- Team requires specific framework experience
|
|
493
|
+
- Need SSR or mobile apps
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 🔮 Future Improvements
|
|
498
|
+
|
|
499
|
+
### Optional Enhancements
|
|
500
|
+
|
|
501
|
+
1. **Virtual DOM** - For better performance
|
|
502
|
+
2. **TypeScript** - Type definitions
|
|
503
|
+
3. **SSR Support** - Server-side rendering
|
|
504
|
+
4. **Dev Tools** - Browser extension
|
|
505
|
+
5. **Plugin System** - Third-party plugins
|
|
506
|
+
6. **CLI Tool** - Project scaffolding
|
|
507
|
+
|
|
508
|
+
### How to Extend
|
|
509
|
+
|
|
510
|
+
The framework is designed to be modified:
|
|
511
|
+
|
|
512
|
+
1. Fork the repository
|
|
513
|
+
2. Modify `src/` files directly
|
|
514
|
+
3. Add new plugins in `src/plugins/`
|
|
515
|
+
4. Share your improvements!
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## License
|
|
520
|
+
|
|
521
|
+
MIT License - do whatever you want with it!
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Contributing
|
|
526
|
+
|
|
527
|
+
Contributions welcome! Please:
|
|
528
|
+
|
|
529
|
+
1. Keep it simple
|
|
530
|
+
2. Maintain readability
|
|
531
|
+
3. Add tests for new features
|
|
532
|
+
4. Update documentation
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Support
|
|
537
|
+
|
|
538
|
+
- 📧 Email: abderrazzak.elouazghi@gmail.com
|
|
539
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/kaiserofthenight/humanjs/issues)
|
|
540
|
+
- 💬 Discussions: [GitHub Discussions](https://github.com/kaiserofthenight/humanjs/discussions)
|
|
541
|
+
- ⭐ Star us on GitHub!
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
**Remember: Frameworks are tools, not religions. Use what makes you productive!** 🚀
|