humanjs-core 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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +655 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +141 -0
- package/examples/human-counter/app.human +27 -0
- package/examples/human-counter/app.js +77 -0
- package/examples/routing/app.js +129 -0
- package/examples/simple-js/app.js +30 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +78 -0
- package/scripts/human-compile.js +43 -0
- package/scripts/humanjs.js +700 -0
- package/src/compiler/human.js +194 -0
- package/src/core/component.js +381 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +173 -0
- package/src/core/router.js +274 -0
- package/src/core/state.js +114 -0
- package/src/index.js +61 -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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { html, component, layout, createRouter, buildUrl, getParams } from '../../src/index.js';
|
|
2
|
+
|
|
3
|
+
const root = document.getElementById('app');
|
|
4
|
+
const session = {
|
|
5
|
+
loggedIn: false
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const PillLink = component(({ path, label }) => html`
|
|
9
|
+
<a
|
|
10
|
+
href="#${path}"
|
|
11
|
+
style="padding: 10px 14px; border-radius: 999px; text-decoration: none; background: white; color: #0f172a; border: 1px solid #cbd5e1; font-weight: 600;"
|
|
12
|
+
>
|
|
13
|
+
${label}
|
|
14
|
+
</a>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
const InfoCard = component(({ title, body }) => html`
|
|
18
|
+
<section style="padding: 18px; border: 1px solid #e2e8f0; border-radius: 20px; background: white;">
|
|
19
|
+
<h2 style="margin: 0 0 10px; font-size: 20px; color: #0f172a;">${title}</h2>
|
|
20
|
+
<div style="color: #475569; line-height: 1.6;">${body}</div>
|
|
21
|
+
</section>
|
|
22
|
+
`);
|
|
23
|
+
|
|
24
|
+
const AppLayout = layout(({ title = 'HumanJS Router', children }) => html`
|
|
25
|
+
<div style="min-height: 100vh; background: linear-gradient(180deg, #f8fafc 0%, #dbeafe 100%); padding: 24px;">
|
|
26
|
+
<div style="max-width: 920px; margin: 0 auto; background: rgba(255,255,255,0.95); border-radius: 28px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.12); overflow: hidden;">
|
|
27
|
+
<header style="padding: 24px 28px; background: #0f172a; color: white;">
|
|
28
|
+
<p style="margin: 0; font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase; color: rgba(255,255,255,0.65);">Routing + Layouts</p>
|
|
29
|
+
<h1 style="margin: 10px 0 6px; font-size: 32px;">${title}</h1>
|
|
30
|
+
<p style="margin: 0; color: rgba(255,255,255,0.72);">Components, layouts, params, redirects, and protected routes.</p>
|
|
31
|
+
</header>
|
|
32
|
+
<nav style="display: flex; flex-wrap: wrap; gap: 10px; padding: 20px 28px; border-bottom: 1px solid #e2e8f0; background: #f8fafc;">
|
|
33
|
+
${PillLink({ path: '/', label: 'Home' })}
|
|
34
|
+
${PillLink({ path: '/user/42', label: 'User 42' })}
|
|
35
|
+
${PillLink({ path: '/user/7?tab=settings', label: 'User 7 Settings' })}
|
|
36
|
+
${PillLink({ path: '/dashboard', label: 'Dashboard' })}
|
|
37
|
+
${PillLink({ path: '/go-home', label: 'Redirect Route' })}
|
|
38
|
+
${PillLink({ path: '/missing-page', label: '404' })}
|
|
39
|
+
</nav>
|
|
40
|
+
<main style="padding: 28px; display: grid; gap: 16px;">
|
|
41
|
+
${children}
|
|
42
|
+
</main>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
const router = createRouter(
|
|
48
|
+
{
|
|
49
|
+
'/': {
|
|
50
|
+
layout: AppLayout,
|
|
51
|
+
render: () => html`
|
|
52
|
+
${InfoCard({ title: 'Start here', body: 'Use the links above to move around without reloading the page.' })}
|
|
53
|
+
${InfoCard({ title: 'Protected route', body: 'Open Dashboard. If you are not logged in, the router redirects you to Login first.' })}
|
|
54
|
+
${InfoCard({ title: 'Params and query', body: `Open <code>#${buildUrl('/user/7', { tab: 'settings' })}</code> to see route params and query params together.` })}
|
|
55
|
+
`
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
'/login': {
|
|
59
|
+
layout: ({ children }) => AppLayout({ title: 'Login', children }),
|
|
60
|
+
state: () => ({
|
|
61
|
+
next: getParams().next || '/dashboard'
|
|
62
|
+
}),
|
|
63
|
+
actions: {
|
|
64
|
+
login({ state }) {
|
|
65
|
+
session.loggedIn = true;
|
|
66
|
+
router.navigate(state.next, true);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
render: (params, { state }) => html`
|
|
70
|
+
${InfoCard({ title: 'Redirected to login', body: `The router sent you here because <code>${state.next}</code> is protected.` })}
|
|
71
|
+
<button data-click="login" style="padding: 16px 18px; border: none; border-radius: 18px; background: #2563eb; color: white; font-weight: 700; width: 220px;">
|
|
72
|
+
Login and continue
|
|
73
|
+
</button>
|
|
74
|
+
`
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'/dashboard': {
|
|
78
|
+
layout: ({ children }) => AppLayout({ title: 'Dashboard', children }),
|
|
79
|
+
actions: {
|
|
80
|
+
logout() {
|
|
81
|
+
session.loggedIn = false;
|
|
82
|
+
router.navigate('/', true);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
render: () => html`
|
|
86
|
+
${InfoCard({ title: 'Protected page', body: 'You are inside the dashboard because the route guard allowed you through.' })}
|
|
87
|
+
${InfoCard({ title: 'Current state', body: `loggedIn = <strong>${String(session.loggedIn)}</strong>` })}
|
|
88
|
+
<button data-click="logout" style="padding: 16px 18px; border: none; border-radius: 18px; background: #e11d48; color: white; font-weight: 700; width: 180px;">
|
|
89
|
+
Logout
|
|
90
|
+
</button>
|
|
91
|
+
`
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
'/user/:id': {
|
|
95
|
+
layout: ({ children, params }) => AppLayout({ title: `User ${params.id}`, children }),
|
|
96
|
+
render: (params, { query }) => html`
|
|
97
|
+
${InfoCard({ title: 'Route param', body: `User id from path: <strong>${params.id}</strong>` })}
|
|
98
|
+
${InfoCard({ title: 'Query params', body: `tab = <strong>${query.tab || 'overview'}</strong>` })}
|
|
99
|
+
${InfoCard({ title: 'Built URL', body: `<code>#${buildUrl(`/user/${params.id}`, { tab: 'activity' })}</code>` })}
|
|
100
|
+
`
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
'/go-home': {
|
|
104
|
+
layout: ({ children }) => AppLayout({ title: 'Redirecting', children }),
|
|
105
|
+
render: () => html`${InfoCard({ title: 'Redirect route', body: 'You should not stay here. The router sends you back home.' })}`
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
'*': {
|
|
109
|
+
layout: ({ children }) => AppLayout({ title: 'Not Found', children }),
|
|
110
|
+
render: () => html`
|
|
111
|
+
${InfoCard({ title: '404', body: 'That route does not exist.' })}
|
|
112
|
+
${InfoCard({ title: 'Try this', body: 'Go back home or open one of the predefined links in the header.' })}
|
|
113
|
+
`
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
root,
|
|
118
|
+
beforeEach(to) {
|
|
119
|
+
if (to === '/dashboard' && !session.loggedIn) {
|
|
120
|
+
return buildUrl('/login', { next: '/dashboard' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (to === '/go-home') {
|
|
124
|
+
return '/';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
window.humanRouter = router;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { app } from '../../src/index.js';
|
|
2
|
+
|
|
3
|
+
app.simple({
|
|
4
|
+
state: {
|
|
5
|
+
count: 0,
|
|
6
|
+
step: 1
|
|
7
|
+
},
|
|
8
|
+
template: `
|
|
9
|
+
<main style="min-height: 100vh; display: grid; place-items: center; padding: 24px; background: linear-gradient(180deg, #f8fafc 0%, #e0f2fe 100%);">
|
|
10
|
+
<section style="width: min(100%, 560px); background: white; border-radius: 28px; padding: 28px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.12); display: grid; gap: 18px;">
|
|
11
|
+
<p style="margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.18em; color: #64748b;">simple js app</p>
|
|
12
|
+
<h1 style="margin: 0; font-size: clamp(40px, 10vw, 72px); line-height: 1; color: #0f172a;">{count}</h1>
|
|
13
|
+
<p style="margin: 0; color: #475569;">Step: {step}</p>
|
|
14
|
+
|
|
15
|
+
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;">
|
|
16
|
+
<button @click="count -= step" style="padding: 16px; border: none; border-radius: 18px; background: #e11d48; color: white; font-weight: 700;">-</button>
|
|
17
|
+
<button @click="count = 0" style="padding: 16px; border: none; border-radius: 18px; background: #64748b; color: white; font-weight: 700;">reset</button>
|
|
18
|
+
<button @click="count += step" style="padding: 16px; border: none; border-radius: 18px; background: #2563eb; color: white; font-weight: 700;">+</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px;">
|
|
22
|
+
<button @click="step = 1" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 1 ? '#0f172a' : 'white'}; color: {step === 1 ? 'white' : '#334155'};">1</button>
|
|
23
|
+
<button @click="step = 5" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 5 ? '#0f172a' : 'white'}; color: {step === 5 ? 'white' : '#334155'};">5</button>
|
|
24
|
+
<button @click="step = 10" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 10 ? '#0f172a' : 'white'}; color: {step === 10 ? 'white' : '#334155'};">10</button>
|
|
25
|
+
<button @click="step = 100" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 100 ? '#0f172a' : 'white'}; color: {step === 100 ? 'white' : '#334155'};">100</button>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</main>
|
|
29
|
+
`
|
|
30
|
+
});
|
|
@@ -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,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "humanjs-core",
|
|
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
|
+
"main": "src/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./compiler": "./src/compiler/human.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"humanjs": "./scripts/humanjs.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"human-js",
|
|
18
|
+
"humanjs",
|
|
19
|
+
"framework",
|
|
20
|
+
"frontend",
|
|
21
|
+
"javascript",
|
|
22
|
+
"reactive",
|
|
23
|
+
"spa",
|
|
24
|
+
"human-first",
|
|
25
|
+
"simple",
|
|
26
|
+
"minimalist",
|
|
27
|
+
"zero-build",
|
|
28
|
+
"no-dependencies",
|
|
29
|
+
"vanilla-js",
|
|
30
|
+
"lightweight",
|
|
31
|
+
"web-framework",
|
|
32
|
+
"ui-framework",
|
|
33
|
+
"state-management",
|
|
34
|
+
"router",
|
|
35
|
+
"components",
|
|
36
|
+
"template-literals",
|
|
37
|
+
"human-readable"
|
|
38
|
+
],
|
|
39
|
+
"author": {
|
|
40
|
+
"name": "Abderrazzak Elouazghi",
|
|
41
|
+
"email": "abderrazzak.elouazghi@gmail.com",
|
|
42
|
+
"url": "https://github.com/kaiserofthenight"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/kaiserofthenight/human-js.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/kaiserofthenight/human-js/issues",
|
|
51
|
+
"email": "abderrazzak.elouazghi@gmail.com"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/kaiserofthenight/human-js#readme",
|
|
54
|
+
"files": [
|
|
55
|
+
"src/",
|
|
56
|
+
"scripts/",
|
|
57
|
+
"examples/",
|
|
58
|
+
"README.md",
|
|
59
|
+
"LICENSE",
|
|
60
|
+
"CHANGELOG.md"
|
|
61
|
+
],
|
|
62
|
+
"scripts": {
|
|
63
|
+
"start": "python -m http.server 8000 || python3 -m http.server 8000 || npx serve .",
|
|
64
|
+
"dev": "python -m http.server 8000 || python3 -m http.server 8000 || npx serve .",
|
|
65
|
+
"demo": "open index.html || start index.html || xdg-open index.html",
|
|
66
|
+
"build:human": "node scripts/human-compile.js",
|
|
67
|
+
"check": "node --check src/index.js && node --check src/compiler/human.js && node --check scripts/human-compile.js && node --check scripts/humanjs.js",
|
|
68
|
+
"test": "echo \"Tests coming soon! Contributions welcome.\" && exit 0",
|
|
69
|
+
"prepublishOnly": "npm run check && echo \"Publishing humanjs-core v1.0.0...\""
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=18.0.0"
|
|
73
|
+
},
|
|
74
|
+
"funding": {
|
|
75
|
+
"type": "github",
|
|
76
|
+
"url": "https://github.com/sponsors/kaiserofthenight"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { compileHuman } from '../src/compiler/human.js';
|
|
4
|
+
|
|
5
|
+
const [, , inputPath, outputPath] = process.argv;
|
|
6
|
+
|
|
7
|
+
if (!inputPath || !outputPath) {
|
|
8
|
+
console.error('Usage: node scripts/human-compile.js <input.human> <output.js>');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const absoluteInput = path.resolve(cwd, inputPath);
|
|
14
|
+
const absoluteOutput = path.resolve(cwd, outputPath);
|
|
15
|
+
const source = await readFile(absoluteInput, 'utf8');
|
|
16
|
+
const appImportPath = await resolveAppImportPath(cwd, absoluteOutput);
|
|
17
|
+
|
|
18
|
+
const compiled = compileHuman(source, {
|
|
19
|
+
appImportPath
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await writeFile(absoluteOutput, compiled, 'utf8');
|
|
23
|
+
console.log(`Compiled ${path.relative(cwd, absoluteInput)} -> ${path.relative(cwd, absoluteOutput)}`);
|
|
24
|
+
|
|
25
|
+
async function resolveAppImportPath(cwd, absoluteOutput) {
|
|
26
|
+
try {
|
|
27
|
+
const packageJson = JSON.parse(await readFile(path.resolve(cwd, 'package.json'), 'utf8'));
|
|
28
|
+
const localEntry = path.resolve(cwd, 'src/index.js');
|
|
29
|
+
|
|
30
|
+
if (packageJson.name === 'human-js') {
|
|
31
|
+
const relativeImport = path
|
|
32
|
+
.relative(path.dirname(absoluteOutput), localEntry)
|
|
33
|
+
.split(path.sep)
|
|
34
|
+
.join('/');
|
|
35
|
+
|
|
36
|
+
return relativeImport.startsWith('.') ? relativeImport : `./${relativeImport}`;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Fall back to package import when compiling from another project.
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return 'human-js';
|
|
43
|
+
}
|