@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.
@@ -0,0 +1,378 @@
1
+ /**
2
+ * TODO APP - Complete Example
3
+ *
4
+ * Features:
5
+ * - Add, edit, delete todos
6
+ * - Mark as complete
7
+ * - Filter (all, active, completed)
8
+ * - LocalStorage persistence
9
+ * - Form validation
10
+ */
11
+
12
+ import { app, html, each, when } from '../../src/index.js';
13
+ import { local } from '../../src/plugins/storage.js';
14
+ import { createValidator, rules, getFormData, displayErrors } from '../../src/plugins/validator.js';
15
+ import { uid } from '../../src/utils/helpers.js';
16
+
17
+ // ============================================
18
+ // COMPONENTS
19
+ // ============================================
20
+
21
+ function TodoItem(todo, onToggle, onDelete, onEdit) {
22
+ return html`
23
+ <div
24
+ class="todo-item ${todo.completed ? 'completed' : ''}"
25
+ style="
26
+ display: flex;
27
+ align-items: center;
28
+ padding: 15px;
29
+ border-bottom: 1px solid #eee;
30
+ gap: 10px;
31
+ "
32
+ >
33
+ <input
34
+ type="checkbox"
35
+ ${todo.completed ? 'checked' : ''}
36
+ data-id="${todo.id}"
37
+ class="todo-checkbox"
38
+ style="width: 20px; height: 20px; cursor: pointer;"
39
+ />
40
+ <span
41
+ style="
42
+ flex: 1;
43
+ text-decoration: ${todo.completed ? 'line-through' : 'none'};
44
+ color: ${todo.completed ? '#999' : '#333'};
45
+ "
46
+ >
47
+ ${todo.text}
48
+ </span>
49
+ <button
50
+ data-id="${todo.id}"
51
+ class="edit-btn"
52
+ style="
53
+ padding: 5px 10px;
54
+ background: #4CAF50;
55
+ color: white;
56
+ border: none;
57
+ border-radius: 4px;
58
+ cursor: pointer;
59
+ "
60
+ >
61
+ Edit
62
+ </button>
63
+ <button
64
+ data-id="${todo.id}"
65
+ class="delete-btn"
66
+ style="
67
+ padding: 5px 10px;
68
+ background: #f44336;
69
+ color: white;
70
+ border: none;
71
+ border-radius: 4px;
72
+ cursor: pointer;
73
+ "
74
+ >
75
+ Delete
76
+ </button>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ function TodoStats(todos) {
82
+ const total = todos.length;
83
+ const completed = todos.filter(t => t.completed).length;
84
+ const active = total - completed;
85
+
86
+ return html`
87
+ <div style="
88
+ display: flex;
89
+ justify-content: space-around;
90
+ padding: 20px;
91
+ background: #f5f5f5;
92
+ border-radius: 8px;
93
+ margin: 20px 0;
94
+ ">
95
+ <div style="text-align: center;">
96
+ <div style="font-size: 24px; font-weight: bold; color: #2196F3;">
97
+ ${total}
98
+ </div>
99
+ <div style="color: #666;">Total</div>
100
+ </div>
101
+ <div style="text-align: center;">
102
+ <div style="font-size: 24px; font-weight: bold; color: #FF9800;">
103
+ ${active}
104
+ </div>
105
+ <div style="color: #666;">Active</div>
106
+ </div>
107
+ <div style="text-align: center;">
108
+ <div style="font-size: 24px; font-weight: bold; color: #4CAF50;">
109
+ ${completed}
110
+ </div>
111
+ <div style="color: #666;">Completed</div>
112
+ </div>
113
+ </div>
114
+ `;
115
+ }
116
+
117
+ // ============================================
118
+ // VALIDATION
119
+ // ============================================
120
+
121
+ const todoValidator = createValidator({
122
+ todoText: [
123
+ rules.required,
124
+ rules.minLength(3),
125
+ rules.maxLength(100)
126
+ ]
127
+ });
128
+
129
+ // ============================================
130
+ // MAIN APP
131
+ // ============================================
132
+
133
+ const todoApp = app.create({
134
+ state: {
135
+ todos: local.get('todos', []),
136
+ filter: 'all', // all, active, completed
137
+ editingId: null
138
+ },
139
+
140
+ render: (state) => {
141
+ // Filter todos based on current filter
142
+ const filteredTodos = state.todos.filter(todo => {
143
+ if (state.filter === 'active') return !todo.completed;
144
+ if (state.filter === 'completed') return todo.completed;
145
+ return true;
146
+ });
147
+
148
+ const element = html`
149
+ <div style="
150
+ max-width: 600px;
151
+ margin: 50px auto;
152
+ padding: 20px;
153
+ font-family: system-ui, sans-serif;
154
+ ">
155
+ <h1 style="text-align: center; color: #2196F3;">
156
+ 📝 Todo App
157
+ </h1>
158
+
159
+ ${TodoStats(state.todos)}
160
+
161
+ <!-- Add Todo Form -->
162
+ <form id="todo-form" style="margin: 20px 0;">
163
+ <div style="display: flex; gap: 10px;">
164
+ <input
165
+ type="text"
166
+ name="todoText"
167
+ placeholder="What needs to be done?"
168
+ style="
169
+ flex: 1;
170
+ padding: 12px;
171
+ border: 2px solid #ddd;
172
+ border-radius: 4px;
173
+ font-size: 16px;
174
+ "
175
+ />
176
+ <button
177
+ type="submit"
178
+ style="
179
+ padding: 12px 24px;
180
+ background: #2196F3;
181
+ color: white;
182
+ border: none;
183
+ border-radius: 4px;
184
+ cursor: pointer;
185
+ font-size: 16px;
186
+ font-weight: bold;
187
+ "
188
+ >
189
+ Add
190
+ </button>
191
+ </div>
192
+ </form>
193
+
194
+ <!-- Filter Buttons -->
195
+ <div style="
196
+ display: flex;
197
+ gap: 10px;
198
+ margin: 20px 0;
199
+ justify-content: center;
200
+ ">
201
+ <button
202
+ class="filter-btn"
203
+ data-filter="all"
204
+ style="
205
+ padding: 8px 16px;
206
+ border: 2px solid ${state.filter === 'all' ? '#2196F3' : '#ddd'};
207
+ background: ${state.filter === 'all' ? '#2196F3' : 'white'};
208
+ color: ${state.filter === 'all' ? 'white' : '#666'};
209
+ border-radius: 4px;
210
+ cursor: pointer;
211
+ "
212
+ >
213
+ All
214
+ </button>
215
+ <button
216
+ class="filter-btn"
217
+ data-filter="active"
218
+ style="
219
+ padding: 8px 16px;
220
+ border: 2px solid ${state.filter === 'active' ? '#FF9800' : '#ddd'};
221
+ background: ${state.filter === 'active' ? '#FF9800' : 'white'};
222
+ color: ${state.filter === 'active' ? 'white' : '#666'};
223
+ border-radius: 4px;
224
+ cursor: pointer;
225
+ "
226
+ >
227
+ Active
228
+ </button>
229
+ <button
230
+ class="filter-btn"
231
+ data-filter="completed"
232
+ style="
233
+ padding: 8px 16px;
234
+ border: 2px solid ${state.filter === 'completed' ? '#4CAF50' : '#ddd'};
235
+ background: ${state.filter === 'completed' ? '#4CAF50' : 'white'};
236
+ color: ${state.filter === 'completed' ? 'white' : '#666'};
237
+ border-radius: 4px;
238
+ cursor: pointer;
239
+ "
240
+ >
241
+ Completed
242
+ </button>
243
+ </div>
244
+
245
+ <!-- Todo List -->
246
+ <div style="
247
+ border: 2px solid #ddd;
248
+ border-radius: 8px;
249
+ overflow: hidden;
250
+ ">
251
+ ${when(
252
+ filteredTodos.length > 0,
253
+ () => html`
254
+ <div>
255
+ ${each(filteredTodos, (todo) => TodoItem(todo))}
256
+ </div>
257
+ `,
258
+ () => html`
259
+ <div style="
260
+ padding: 40px;
261
+ text-align: center;
262
+ color: #999;
263
+ ">
264
+ ${state.filter === 'all' ? 'No todos yet! Add one above.' : `No ${state.filter} todos.`}
265
+ </div>
266
+ `
267
+ )}
268
+ </div>
269
+
270
+ <!-- Clear Completed -->
271
+ ${when(
272
+ state.todos.some(t => t.completed),
273
+ () => html`
274
+ <button
275
+ id="clear-completed"
276
+ style="
277
+ margin-top: 20px;
278
+ padding: 10px 20px;
279
+ background: #f44336;
280
+ color: white;
281
+ border: none;
282
+ border-radius: 4px;
283
+ cursor: pointer;
284
+ width: 100%;
285
+ "
286
+ >
287
+ Clear Completed
288
+ </button>
289
+ `
290
+ )}
291
+ </div>
292
+ `;
293
+
294
+ const events = {
295
+ '#todo-form': {
296
+ submit: (e) => {
297
+ e.preventDefault();
298
+
299
+ const formData = getFormData(e.target);
300
+ const validation = todoValidator.validate(formData);
301
+
302
+ if (!validation.isValid) {
303
+ displayErrors(e.target, validation.errors);
304
+ return;
305
+ }
306
+
307
+ // Add todo
308
+ const newTodo = {
309
+ id: uid('todo'),
310
+ text: formData.todoText,
311
+ completed: false,
312
+ createdAt: Date.now()
313
+ };
314
+
315
+ state.todos = [...state.todos, newTodo];
316
+ local.set('todos', state.todos);
317
+
318
+ e.target.reset();
319
+ }
320
+ },
321
+ '.todo-checkbox': {
322
+ change: (e) => {
323
+ const id = e.target.dataset.id;
324
+ state.todos = state.todos.map(todo =>
325
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
326
+ );
327
+ local.set('todos', state.todos);
328
+ }
329
+ },
330
+ '.delete-btn': {
331
+ click: (e) => {
332
+ const id = e.target.dataset.id;
333
+ if (confirm('Delete this todo?')) {
334
+ state.todos = state.todos.filter(todo => todo.id !== id);
335
+ local.set('todos', state.todos);
336
+ }
337
+ }
338
+ },
339
+ '.edit-btn': {
340
+ click: (e) => {
341
+ const id = e.target.dataset.id;
342
+ const todo = state.todos.find(t => t.id === id);
343
+ const newText = prompt('Edit todo:', todo.text);
344
+
345
+ if (newText && newText.trim()) {
346
+ state.todos = state.todos.map(t =>
347
+ t.id === id ? { ...t, text: newText.trim() } : t
348
+ );
349
+ local.set('todos', state.todos);
350
+ }
351
+ }
352
+ },
353
+ '.filter-btn': {
354
+ click: (e) => {
355
+ state.filter = e.target.dataset.filter;
356
+ }
357
+ },
358
+ '#clear-completed': {
359
+ click: () => {
360
+ if (confirm('Clear all completed todos?')) {
361
+ state.todos = state.todos.filter(todo => !todo.completed);
362
+ local.set('todos', state.todos);
363
+ }
364
+ }
365
+ }
366
+ };
367
+
368
+ return { element, events };
369
+ },
370
+
371
+ onMount: () => {
372
+ console.log('✅ Todo App mounted!');
373
+ },
374
+
375
+ onUpdate: (state) => {
376
+ console.log('🔄 Todos updated:', state.todos.length);
377
+ }
378
+ });
File without changes
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@kaiserofthenight/human-js",
3
+ "version": "1.0.0",
4
+ "description": "A framework built for humans, not machines. Zero dependencies, zero build tools, 100% readable.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "human-js",
8
+ "humanjs",
9
+ "framework",
10
+ "frontend",
11
+ "javascript",
12
+ "reactive",
13
+ "spa",
14
+ "human-first",
15
+ "simple",
16
+ "minimalist",
17
+ "zero-build",
18
+ "no-dependencies",
19
+ "vanilla-js",
20
+ "lightweight",
21
+ "web-framework",
22
+ "ui-framework",
23
+ "reactive-programming",
24
+ "state-management",
25
+ "router",
26
+ "components",
27
+ "template-literals",
28
+ "proxy",
29
+ "human-readable"
30
+ ],
31
+ "author": {
32
+ "name": "Abderrazzak Elouazghi",
33
+ "email": "abderrazzak.elouazghi@gmail.com",
34
+ "url": "https://github.com/kaiserofthenight"
35
+ },
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/kaiserofthenight/human-js.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/kaiserofthenight/human-js/issues",
43
+ "email": "abderrazzak.elouazghi@gmail.com"
44
+ },
45
+ "homepage": "https://github.com/kaiserofthenight/human-js#readme",
46
+ "files": [
47
+ "src/",
48
+ "examples/",
49
+ "README.md",
50
+ "LICENSE",
51
+ "CHANGELOG.md"
52
+ ],
53
+ "scripts": {
54
+ "dev": "python -m http.server 8000 || python3 -m http.server 8000 || npx serve .",
55
+ "demo": "open index.html || start index.html || xdg-open index.html",
56
+ "test": "echo 'Tests coming soon! Contributions welcome.' && exit 0",
57
+ "prepublishOnly": "echo '🚀 Publishing human-js v1.0.0...'"
58
+ },
59
+ "engines": {
60
+ "node": ">=14.0.0"
61
+ },
62
+ "funding": {
63
+ "type": "github",
64
+ "url": "https://github.com/sponsors/kaiserofthenight"
65
+ }
66
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * COMPONENT SYSTEM
3
+ *
4
+ * Create reactive components with state, lifecycle hooks, and auto re-rendering.
5
+ */
6
+
7
+ import { createState } from './state.js';
8
+ import { attachEvents } from './events.js';
9
+
10
+ /**
11
+ * Create a reactive component
12
+ * @param {HTMLElement} rootElement - Where to mount the component
13
+ * @param {Function} renderFn - Function that returns { element, events }
14
+ * @param {Object} initialState - Initial state
15
+ * @param {Object} lifecycle - { onMount, onUpdate, onDestroy }
16
+ */
17
+ export function createComponent(rootElement, renderFn, initialState = {}, initialLifecycle = {}) {
18
+ let currentElement = null;
19
+ let cleanupEvents = null;
20
+ let isDestroyed = false;
21
+
22
+ // Create reactive state FIRST
23
+ const componentState = createState(initialState);
24
+
25
+ // Render function
26
+ function render() {
27
+ if (isDestroyed) return;
28
+
29
+ // Call render function with state
30
+ const result = renderFn(componentState);
31
+
32
+ // Extract element and events
33
+ let newElement, events;
34
+ if (result && typeof result === 'object' && result.element) {
35
+ newElement = result.element;
36
+ events = result.events || {};
37
+ } else {
38
+ newElement = result;
39
+ events = {};
40
+ }
41
+
42
+ // First render - mount
43
+ if (!currentElement) {
44
+ currentElement = newElement;
45
+ rootElement.appendChild(currentElement);
46
+
47
+ // Attach events
48
+ cleanupEvents = attachEvents(currentElement, events);
49
+
50
+ // Call onMount
51
+ if (initialLifecycle.onMount) {
52
+ initialLifecycle.onMount(componentState);
53
+ }
54
+ }
55
+ // Subsequent renders - replace
56
+ else {
57
+ // Cleanup old events
58
+ if (cleanupEvents) cleanupEvents();
59
+
60
+ // Replace element
61
+ rootElement.replaceChild(newElement, currentElement);
62
+ currentElement = newElement;
63
+
64
+ // Attach new events
65
+ cleanupEvents = attachEvents(currentElement, events);
66
+
67
+ // Call onUpdate
68
+ if (initialLifecycle.onUpdate) {
69
+ initialLifecycle.onUpdate(componentState);
70
+ }
71
+ }
72
+ }
73
+
74
+ // Watch for state changes and re-render
75
+ componentState.$watch = new Proxy(componentState.$watch || (() => {}), {
76
+ apply(target, thisArg, args) {
77
+ const result = target.apply(thisArg, args);
78
+ return result;
79
+ }
80
+ });
81
+
82
+ // Override state setter to trigger re-render
83
+ const originalState = { ...componentState.$raw() };
84
+ Object.keys(originalState).forEach(key => {
85
+ let value = componentState[key];
86
+ Object.defineProperty(componentState, key, {
87
+ get() {
88
+ return value;
89
+ },
90
+ set(newValue) {
91
+ if (value !== newValue) {
92
+ value = newValue;
93
+ render();
94
+ }
95
+ },
96
+ enumerable: true,
97
+ configurable: true
98
+ });
99
+ });
100
+
101
+ // Destroy function
102
+ function destroy() {
103
+ if (isDestroyed) return;
104
+
105
+ isDestroyed = true;
106
+
107
+ // Cleanup events
108
+ if (cleanupEvents) cleanupEvents();
109
+
110
+ // Call onDestroy
111
+ if (initialLifecycle.onDestroy) {
112
+ initialLifecycle.onDestroy(componentState);
113
+ }
114
+
115
+ // Remove from DOM
116
+ if (currentElement && currentElement.parentNode) {
117
+ currentElement.parentNode.removeChild(currentElement);
118
+ }
119
+
120
+ currentElement = null;
121
+ }
122
+
123
+ // Initial render
124
+ render();
125
+
126
+ // Return component API
127
+ return {
128
+ state: componentState,
129
+ destroy,
130
+ render,
131
+ get element() {
132
+ return currentElement;
133
+ }
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Higher-level app API for cleaner component creation
139
+ */
140
+ export const app = {
141
+ /**
142
+ * Create a component with options object
143
+ */
144
+ create(options = {}) {
145
+ const {
146
+ root = document.getElementById('app'),
147
+ state: initialState = {},
148
+ render: renderFn,
149
+ onMount,
150
+ onUpdate,
151
+ onDestroy
152
+ } = options;
153
+
154
+ if (!renderFn) {
155
+ throw new Error('[HumanJS] render function is required');
156
+ }
157
+
158
+ return createComponent(
159
+ root,
160
+ renderFn,
161
+ initialState,
162
+ { onMount, onUpdate, onDestroy }
163
+ );
164
+ },
165
+
166
+ /**
167
+ * Quick mount without state management
168
+ */
169
+ mount(rootOrFn, renderFn) {
170
+ let root, render;
171
+
172
+ if (typeof rootOrFn === 'function') {
173
+ root = document.getElementById('app');
174
+ render = rootOrFn;
175
+ } else {
176
+ root = rootOrFn;
177
+ render = renderFn;
178
+ }
179
+
180
+ return createComponent(root, () => render(), {});
181
+ }
182
+ };