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.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * HTML RENDERING SYSTEM
3
+ *
4
+ * Convert tagged template literals into real DOM elements.
5
+ * Supports dynamic values, nested elements, and arrays.
6
+ */
7
+
8
+ /**
9
+ * Main html tagged template function
10
+ * @param {Array} strings - Template string parts
11
+ * @param {...any} values - Dynamic values
12
+ * @returns {HTMLElement} DOM element
13
+ */
14
+ export function html(strings, ...values) {
15
+ // Build complete HTML string
16
+ let htmlString = '';
17
+
18
+ strings.forEach((str, i) => {
19
+ htmlString += str;
20
+
21
+ if (i < values.length) {
22
+ const value = values[i];
23
+
24
+ // Handle different value types
25
+ if (value === null || value === undefined) {
26
+ htmlString += '';
27
+ } else if (Array.isArray(value)) {
28
+ // For arrays, join DOM elements
29
+ htmlString += value.map(v => {
30
+ if (v instanceof HTMLElement) {
31
+ return v.outerHTML;
32
+ }
33
+ return String(v);
34
+ }).join('');
35
+ } else if (value instanceof HTMLElement) {
36
+ htmlString += value.outerHTML;
37
+ } else {
38
+ htmlString += String(value);
39
+ }
40
+ }
41
+ });
42
+
43
+ // Create DOM from string
44
+ return createElementFromHTML(htmlString.trim());
45
+ }
46
+
47
+ /**
48
+ * Create a DOM element from HTML string
49
+ * @param {String} htmlString - HTML markup
50
+ * @returns {HTMLElement} DOM element
51
+ */
52
+ function createElementFromHTML(htmlString) {
53
+ const template = document.createElement('template');
54
+ template.innerHTML = htmlString;
55
+
56
+ const element = template.content.firstChild;
57
+
58
+ if (!element) {
59
+ throw new Error('Invalid HTML: ' + htmlString);
60
+ }
61
+
62
+ return element;
63
+ }
64
+
65
+ /**
66
+ * Create element with props (alternative to html``)
67
+ * @param {String} tag - Element tag name
68
+ * @param {Object} props - Element properties
69
+ * @param {...any} children - Child elements
70
+ * @returns {HTMLElement}
71
+ */
72
+ export function createElement(tag, props = {}, ...children) {
73
+ const element = document.createElement(tag);
74
+
75
+ // Apply props
76
+ Object.keys(props).forEach(key => {
77
+ if (key === 'className') {
78
+ element.className = props[key];
79
+ } else if (key === 'style' && typeof props[key] === 'object') {
80
+ Object.assign(element.style, props[key]);
81
+ } else if (key.startsWith('on')) {
82
+ const eventName = key.substring(2).toLowerCase();
83
+ element.addEventListener(eventName, props[key]);
84
+ } else {
85
+ element.setAttribute(key, props[key]);
86
+ }
87
+ });
88
+
89
+ // Append children
90
+ children.forEach(child => {
91
+ if (child === null || child === undefined) return;
92
+
93
+ if (typeof child === 'string') {
94
+ element.appendChild(document.createTextNode(child));
95
+ } else if (child instanceof HTMLElement) {
96
+ element.appendChild(child);
97
+ } else if (Array.isArray(child)) {
98
+ child.forEach(c => {
99
+ if (c instanceof HTMLElement) {
100
+ element.appendChild(c);
101
+ } else {
102
+ element.appendChild(document.createTextNode(String(c)));
103
+ }
104
+ });
105
+ } else {
106
+ element.appendChild(document.createTextNode(String(child)));
107
+ }
108
+ });
109
+
110
+ return element;
111
+ }
112
+
113
+ /**
114
+ * Render multiple children
115
+ * @param {...any} children
116
+ * @returns {DocumentFragment}
117
+ */
118
+ export function fragment(...children) {
119
+ const frag = document.createDocumentFragment();
120
+
121
+ children.forEach(child => {
122
+ if (child instanceof HTMLElement) {
123
+ frag.appendChild(child);
124
+ } else if (typeof child === 'string') {
125
+ frag.appendChild(document.createTextNode(child));
126
+ }
127
+ });
128
+
129
+ return frag;
130
+ }
131
+
132
+ /**
133
+ * Conditionally render elements
134
+ * @param {Boolean} condition
135
+ * @param {Function} trueRender
136
+ * @param {Function} falseRender
137
+ * @returns {HTMLElement}
138
+ */
139
+ export function when(condition, trueRender, falseRender = () => html`<span></span>`) {
140
+ return condition ? trueRender() : falseRender();
141
+ }
142
+
143
+ /**
144
+ * Render list of items
145
+ * @param {Array} items
146
+ * @param {Function} renderItem
147
+ * @returns {Array<HTMLElement>}
148
+ */
149
+ export function each(items, renderItem) {
150
+ return items.map((item, index) => renderItem(item, index));
151
+ }
152
+
153
+ /**
154
+ * Create a reusable view component
155
+ * @param {Function} renderFn
156
+ * @returns {Function}
157
+ */
158
+ export function component(renderFn) {
159
+ return function HumanComponent(props = {}) {
160
+ return renderFn(props);
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Create a reusable layout
166
+ * @param {Function} renderFn
167
+ * @returns {Function}
168
+ */
169
+ export function layout(renderFn) {
170
+ return function HumanLayout(props = {}) {
171
+ return renderFn(props);
172
+ };
173
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * SPA ROUTING SYSTEM
3
+ *
4
+ * Hash-based routing for single page applications.
5
+ * No server configuration required.
6
+ */
7
+
8
+ import { createApp } from './component.js';
9
+
10
+ /**
11
+ * Create a router
12
+ * @param {Object} routes - Route definitions { '/path': component }
13
+ * @param {Object} options - Router options
14
+ */
15
+ export function createRouter(routes, options = {}) {
16
+ const {
17
+ root = document.getElementById('app'),
18
+ notFound = () => {
19
+ const el = document.createElement('div');
20
+ el.innerHTML = '<h1>404 - Page Not Found</h1>';
21
+ return el;
22
+ },
23
+ beforeEach = null,
24
+ afterEach = null
25
+ } = options;
26
+
27
+ let currentRoute = null;
28
+ let currentComponent = null;
29
+ let isRedirecting = false;
30
+
31
+ function applyLayoutSync(layoutFn, content, context) {
32
+ if (typeof layoutFn !== 'function') return content;
33
+ return layoutFn({ ...context, children: content });
34
+ }
35
+
36
+ async function applyLayout(layoutFn, content, context) {
37
+ if (typeof layoutFn !== 'function') return content;
38
+ return await layoutFn({ ...context, children: content });
39
+ }
40
+
41
+ function normalizeRouteEntry(route, params) {
42
+ const entry = routes[route] || notFound;
43
+
44
+ if (typeof entry === 'function') {
45
+ return {
46
+ type: 'view',
47
+ render: entry,
48
+ layout: null
49
+ };
50
+ }
51
+
52
+ if (entry && typeof entry === 'object') {
53
+ const hasAppConfig = 'state' in entry || 'actions' in entry || 'derived' in entry || 'onMount' in entry || 'onUpdate' in entry || 'onDestroy' in entry;
54
+ return {
55
+ type: hasAppConfig ? 'app' : 'view',
56
+ render: entry.render || notFound,
57
+ layout: entry.layout || null,
58
+ state: entry.state,
59
+ derived: entry.derived,
60
+ actions: entry.actions,
61
+ onMount: entry.onMount,
62
+ onUpdate: entry.onUpdate,
63
+ onDestroy: entry.onDestroy
64
+ };
65
+ }
66
+
67
+ return {
68
+ type: 'view',
69
+ render: notFound,
70
+ layout: null
71
+ };
72
+ }
73
+
74
+ function getCurrentPath() {
75
+ const hash = window.location.hash.slice(1) || '/';
76
+ const [path] = hash.split('?');
77
+ return path || '/';
78
+ }
79
+
80
+ // Parse route params (e.g., /user/:id)
81
+ function matchRoute(hash) {
82
+ const [pathOnly] = (hash.slice(1) || '/').split('?');
83
+ const path = pathOnly || '/';
84
+
85
+ // Try exact match first
86
+ if (routes[path]) {
87
+ return { route: path, params: {} };
88
+ }
89
+
90
+ // Try parameterized routes
91
+ for (const route in routes) {
92
+ const paramNames = [];
93
+ const regexPattern = route.replace(/:([^/]+)/g, (_, paramName) => {
94
+ paramNames.push(paramName);
95
+ return '([^/]+)';
96
+ });
97
+
98
+ const regex = new RegExp(`^${regexPattern}$`);
99
+ const match = path.match(regex);
100
+
101
+ if (match) {
102
+ const params = {};
103
+ paramNames.forEach((name, index) => {
104
+ params[name] = match[index + 1];
105
+ });
106
+ return { route, params };
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ // Navigate to a route
114
+ function navigate(path, replace = false) {
115
+ if (replace) {
116
+ const normalized = path.startsWith('#') ? path : `#${path}`;
117
+ window.history.replaceState(null, '', normalized);
118
+ render();
119
+ } else {
120
+ window.location.hash = path;
121
+ }
122
+ }
123
+
124
+ // Render current route
125
+ async function render() {
126
+ const hash = window.location.hash;
127
+ const match = matchRoute(hash);
128
+
129
+ let route, params;
130
+ if (match) {
131
+ route = match.route;
132
+ params = match.params;
133
+ } else {
134
+ route = '*';
135
+ params = {};
136
+ }
137
+
138
+ // Call beforeEach guard
139
+ if (beforeEach) {
140
+ const result = beforeEach(route, currentRoute, params);
141
+ if (result === false) return;
142
+ if (typeof result === 'string' && !isRedirecting) {
143
+ isRedirecting = true;
144
+ navigate(result, true);
145
+ isRedirecting = false;
146
+ return;
147
+ }
148
+ }
149
+
150
+ const routeEntry = normalizeRouteEntry(route, params);
151
+
152
+ // Destroy previous component
153
+ if (currentComponent && currentComponent.destroy) {
154
+ currentComponent.destroy();
155
+ }
156
+
157
+ // Clear root
158
+ root.innerHTML = '';
159
+
160
+ // Render new component
161
+ if (typeof routeEntry.render === 'function') {
162
+ const context = { route, params, path: getCurrentPath(), query: getParams() };
163
+ const renderToken = Symbol('route-render');
164
+ render.activeToken = renderToken;
165
+
166
+ if (routeEntry.type === 'app') {
167
+ currentComponent = createApp({
168
+ root,
169
+ state: typeof routeEntry.state === 'function' ? routeEntry.state(params, context) : (routeEntry.state || {}),
170
+ derived: routeEntry.derived || {},
171
+ actions: routeEntry.actions || {},
172
+ onMount: routeEntry.onMount,
173
+ onUpdate: routeEntry.onUpdate,
174
+ onDestroy: routeEntry.onDestroy,
175
+ render: (appContext) => {
176
+ const content = routeEntry.render(params, { ...context, ...appContext });
177
+ return applyLayoutSync(routeEntry.layout, content, { ...context, ...appContext });
178
+ }
179
+ });
180
+ return;
181
+ }
182
+
183
+ const element = await routeEntry.render(params, context);
184
+ const layouted = await applyLayout(routeEntry.layout, element, context);
185
+
186
+ if (render.activeToken !== renderToken) {
187
+ return;
188
+ }
189
+
190
+ if (layouted instanceof HTMLElement) {
191
+ root.appendChild(layouted);
192
+ } else if (layouted && layouted.element) {
193
+ currentComponent = layouted;
194
+ root.appendChild(layouted.element);
195
+ } else if (element instanceof HTMLElement) {
196
+ root.appendChild(element);
197
+ } else if (element && element.element) {
198
+ currentComponent = element;
199
+ root.appendChild(element.element);
200
+ }
201
+ }
202
+
203
+ const previousRoute = currentRoute;
204
+ currentRoute = route;
205
+
206
+ // Call afterEach hook
207
+ if (afterEach) {
208
+ afterEach(route, previousRoute, params);
209
+ }
210
+ }
211
+
212
+ // Listen for hash changes
213
+ window.addEventListener('hashchange', render);
214
+
215
+ // Initial render
216
+ render();
217
+
218
+ // Return router API
219
+ return {
220
+ navigate,
221
+ back: () => window.history.back(),
222
+ forward: () => window.history.forward(),
223
+ getCurrentRoute: () => currentRoute,
224
+ getCurrentPath,
225
+ destroy: () => {
226
+ window.removeEventListener('hashchange', render);
227
+ }
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Link component for navigation
233
+ */
234
+ export function Link(to, text, className = '') {
235
+ const link = document.createElement('a');
236
+ link.href = `#${to}`;
237
+ link.textContent = text;
238
+ link.className = className;
239
+
240
+ // Prevent default and use router navigation
241
+ link.addEventListener('click', (e) => {
242
+ e.preventDefault();
243
+ window.location.hash = to;
244
+ });
245
+
246
+ return link;
247
+ }
248
+
249
+ /**
250
+ * Get route params from URL
251
+ */
252
+ export function getParams() {
253
+ const hash = window.location.hash.slice(1);
254
+ const [path, query] = hash.split('?');
255
+
256
+ if (!query) return {};
257
+
258
+ return query.split('&').reduce((params, param) => {
259
+ const [key, value] = param.split('=');
260
+ params[decodeURIComponent(key)] = decodeURIComponent(value || '');
261
+ return params;
262
+ }, {});
263
+ }
264
+
265
+ /**
266
+ * Build URL with query params
267
+ */
268
+ export function buildUrl(path, params = {}) {
269
+ const query = Object.keys(params)
270
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
271
+ .join('&');
272
+
273
+ return query ? `${path}?${query}` : path;
274
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * REACTIVE STATE MANAGEMENT
3
+ *
4
+ * Creates observable state objects that trigger callbacks on changes.
5
+ * Uses JavaScript Proxy to intercept property access and modifications.
6
+ */
7
+
8
+ /**
9
+ * Create a reactive state object
10
+ * @param {Object} initialState - Initial state values
11
+ * @param {Function} onChange - Callback fired on any state change
12
+ * @returns {Proxy} Reactive state object
13
+ */
14
+ export function createState(initialState = {}, onChange = null) {
15
+ // Store the raw state
16
+ const state = { ...initialState };
17
+
18
+ // Store computed properties
19
+ const computed = {};
20
+
21
+ // Store watchers for specific properties
22
+ const watchers = {};
23
+
24
+ // Create reactive proxy
25
+ const proxy = new Proxy(state, {
26
+ get(target, property) {
27
+ // Return computed value if it exists
28
+ if (computed[property]) {
29
+ return computed[property]();
30
+ }
31
+ return target[property];
32
+ },
33
+
34
+ set(target, property, value) {
35
+ const oldValue = target[property];
36
+
37
+ // Only update if value actually changed
38
+ if (oldValue === value) return true;
39
+
40
+ // Update the value
41
+ target[property] = value;
42
+
43
+ // Call property-specific watchers
44
+ if (watchers[property]) {
45
+ watchers[property].forEach(watcher => {
46
+ watcher(value, oldValue);
47
+ });
48
+ }
49
+
50
+ // Call global onChange callback
51
+ if (onChange) {
52
+ onChange(property, value, oldValue);
53
+ }
54
+
55
+ return true;
56
+ }
57
+ });
58
+
59
+ // Add utility methods
60
+ proxy.$watch = function(property, callback) {
61
+ if (!watchers[property]) {
62
+ watchers[property] = [];
63
+ }
64
+ watchers[property].push(callback);
65
+
66
+ // Return unwatch function
67
+ return () => {
68
+ const index = watchers[property].indexOf(callback);
69
+ if (index > -1) {
70
+ watchers[property].splice(index, 1);
71
+ }
72
+ };
73
+ };
74
+
75
+ proxy.$computed = function(property, computeFn) {
76
+ computed[property] = computeFn.bind(proxy);
77
+ };
78
+
79
+ proxy.$reset = function(newState = initialState) {
80
+ Object.keys(state).forEach(key => delete state[key]);
81
+ Object.assign(state, newState);
82
+ if (onChange) onChange('$reset', newState);
83
+ };
84
+
85
+ proxy.$raw = function() {
86
+ return { ...state };
87
+ };
88
+
89
+ return proxy;
90
+ }
91
+
92
+ /**
93
+ * Create a global store (shared state across components)
94
+ */
95
+ export function createStore(initialState = {}) {
96
+ const listeners = [];
97
+ const state = createState(initialState, (prop, value) => {
98
+ listeners.forEach(listener => listener(prop, value, state));
99
+ });
100
+
101
+ return {
102
+ state,
103
+ subscribe(callback) {
104
+ listeners.push(callback);
105
+ return () => {
106
+ const index = listeners.indexOf(callback);
107
+ if (index > -1) listeners.splice(index, 1);
108
+ };
109
+ },
110
+ getState() {
111
+ return state.$raw();
112
+ }
113
+ };
114
+ }
package/src/index.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * HUMANJS FRAMEWORK
3
+ *
4
+ * A human-first frontend framework.
5
+ * Simple, readable, and easy to extend.
6
+ *
7
+ * @version 1.0.0
8
+ * @author HumanJS Team
9
+ */
10
+
11
+ // Core modules
12
+ export { createState, createStore } from './core/state.js';
13
+ export { html, createElement, fragment, when, each, component, layout } from './core/render.js';
14
+ export { createComponent, createApp, createSimpleApp, app } from './core/component.js';
15
+ export { createRouter, Link, getParams, buildUrl } from './core/router.js';
16
+ export { attachEvents, delegate, debounce, throttle, on, mergeEvents } from './core/events.js';
17
+ export { parseHumanFile, compileTemplate, compileHuman, createRuntimeTemplate, loadHuman } from './compiler/human.js';
18
+
19
+ // Plugins
20
+ export { createValidator, rules, getFormData, displayErrors } from './plugins/validator.js';
21
+ export { createHttp, http } from './plugins/http.js';
22
+ export { local, session, createNamespace, onStorageChange } from './plugins/storage.js';
23
+
24
+ // Utilities
25
+ export * as helpers from './utils/helpers.js';
26
+
27
+ /**
28
+ * Framework version
29
+ */
30
+ export const VERSION = '1.0.0';
31
+
32
+ /**
33
+ * Quick start helper
34
+ */
35
+ export function humanjs() {
36
+ console.log(`
37
+ 🎯 HumanJS Framework v${VERSION}
38
+
39
+ A framework built for humans, not machines.
40
+
41
+ Quick start:
42
+ import { loadHuman } from './src/index.js';
43
+
44
+ loadHuman('./examples/human-counter/app.human');
45
+
46
+ Documentation: https://github.com/kaiserofthenight/humanjs
47
+ `);
48
+ }
49
+
50
+ /**
51
+ * Global error handler (optional)
52
+ */
53
+ if (typeof window !== 'undefined') {
54
+ window.addEventListener('error', (event) => {
55
+ console.error('[HumanJS] Uncaught error:', event.error);
56
+ });
57
+
58
+ window.addEventListener('unhandledrejection', (event) => {
59
+ console.error('[HumanJS] Unhandled promise rejection:', event.reason);
60
+ });
61
+ }