@bookklik/senangstart-actions 0.1.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 +58 -0
- package/dist/senangstart-actions.esm.js +992 -0
- package/dist/senangstart-actions.js +997 -0
- package/dist/senangstart-actions.min.js +9 -0
- package/package.json +46 -0
- package/src/evaluator.js +118 -0
- package/src/handlers/attributes.js +168 -0
- package/src/handlers/bind.js +46 -0
- package/src/handlers/directives.js +119 -0
- package/src/handlers/events.js +66 -0
- package/src/handlers/index.js +11 -0
- package/src/index.js +106 -0
- package/src/observer.js +42 -0
- package/src/reactive.js +210 -0
- package/src/walker.js +117 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions v0.1.0
|
|
3
|
+
* Declarative UI framework for humans and AI agents
|
|
4
|
+
*
|
|
5
|
+
* @license MIT
|
|
6
|
+
* @author Bookklik
|
|
7
|
+
* @module senangstart-actions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createReactive } from './reactive.js';
|
|
11
|
+
import { walk, setReferences } from './walker.js';
|
|
12
|
+
import { setupObserver } from './observer.js';
|
|
13
|
+
|
|
14
|
+
// =========================================================================
|
|
15
|
+
// CSS Injection for ss-cloak
|
|
16
|
+
// =========================================================================
|
|
17
|
+
const style = document.createElement('style');
|
|
18
|
+
style.textContent = '[ss-cloak] { display: none !important; }';
|
|
19
|
+
document.head.appendChild(style);
|
|
20
|
+
|
|
21
|
+
// =========================================================================
|
|
22
|
+
// Internal State
|
|
23
|
+
// =========================================================================
|
|
24
|
+
const registeredDataFactories = {}; // SenangStart.data() registrations
|
|
25
|
+
const stores = {}; // SenangStart.store() registrations
|
|
26
|
+
|
|
27
|
+
// Set references in walker module
|
|
28
|
+
setReferences(registeredDataFactories, stores);
|
|
29
|
+
|
|
30
|
+
// =========================================================================
|
|
31
|
+
// Public API
|
|
32
|
+
// =========================================================================
|
|
33
|
+
|
|
34
|
+
const SenangStart = {
|
|
35
|
+
/**
|
|
36
|
+
* Register a reusable data component
|
|
37
|
+
* @param {string} name - Component name
|
|
38
|
+
* @param {Function} factory - Factory function returning data object
|
|
39
|
+
*/
|
|
40
|
+
data(name, factory) {
|
|
41
|
+
if (typeof factory !== 'function') {
|
|
42
|
+
console.error('[SenangStart] data() requires a factory function');
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
registeredDataFactories[name] = factory;
|
|
46
|
+
return this;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register a global reactive store
|
|
51
|
+
* @param {string} name - Store name
|
|
52
|
+
* @param {Object} data - Store data object
|
|
53
|
+
*/
|
|
54
|
+
store(name, data) {
|
|
55
|
+
if (typeof data !== 'object') {
|
|
56
|
+
console.error('[SenangStart] store() requires an object');
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
stores[name] = createReactive(data, () => {});
|
|
60
|
+
return this;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Manually initialize a DOM tree
|
|
65
|
+
* @param {Element} root - Root element to initialize
|
|
66
|
+
*/
|
|
67
|
+
init(root = document.body) {
|
|
68
|
+
walk(root, null);
|
|
69
|
+
return this;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start the framework
|
|
74
|
+
*/
|
|
75
|
+
start() {
|
|
76
|
+
if (document.readyState === 'loading') {
|
|
77
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
78
|
+
this.init();
|
|
79
|
+
setupObserver();
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
this.init();
|
|
83
|
+
setupObserver();
|
|
84
|
+
}
|
|
85
|
+
return this;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Version
|
|
90
|
+
*/
|
|
91
|
+
version: '0.1.0'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// =========================================================================
|
|
95
|
+
// Auto-start
|
|
96
|
+
// =========================================================================
|
|
97
|
+
|
|
98
|
+
// Expose globally
|
|
99
|
+
if (typeof window !== 'undefined') {
|
|
100
|
+
window.SenangStart = SenangStart;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Auto-start when script loads
|
|
104
|
+
SenangStart.start();
|
|
105
|
+
|
|
106
|
+
export default SenangStart;
|
package/src/observer.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - MutationObserver
|
|
3
|
+
* Watch for dynamically added elements
|
|
4
|
+
*
|
|
5
|
+
* @module observer
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { walk } from './walker.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set up observer for dynamically added elements
|
|
12
|
+
*/
|
|
13
|
+
export function setupObserver() {
|
|
14
|
+
const observer = new MutationObserver((mutations) => {
|
|
15
|
+
for (const mutation of mutations) {
|
|
16
|
+
for (const node of mutation.addedNodes) {
|
|
17
|
+
if (node.nodeType === 1) {
|
|
18
|
+
// Check if node or any ancestor already has scope
|
|
19
|
+
let current = node;
|
|
20
|
+
let parentScope = null;
|
|
21
|
+
|
|
22
|
+
while (current.parentElement) {
|
|
23
|
+
current = current.parentElement;
|
|
24
|
+
if (current.__ssScope) {
|
|
25
|
+
parentScope = current.__ssScope;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
walk(node, parentScope);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
observer.observe(document.body, {
|
|
37
|
+
childList: true,
|
|
38
|
+
subtree: true
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return observer;
|
|
42
|
+
}
|
package/src/reactive.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Reactive System
|
|
3
|
+
* Proxy-based reactivity with dependency tracking
|
|
4
|
+
*
|
|
5
|
+
* @module reactive
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Array methods that mutate the array
|
|
9
|
+
const ARRAY_MUTATING_METHODS = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin'];
|
|
10
|
+
|
|
11
|
+
// Internal state
|
|
12
|
+
let pendingUpdate = false;
|
|
13
|
+
export const pendingEffects = new Set();
|
|
14
|
+
export let currentEffect = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a reactive array that triggers updates on mutations
|
|
18
|
+
*/
|
|
19
|
+
function createReactiveArray(arr, onMutate, subscribers) {
|
|
20
|
+
const handler = {
|
|
21
|
+
get(target, prop) {
|
|
22
|
+
// Track dependency for length and numeric indices
|
|
23
|
+
if (currentEffect && (prop === 'length' || !isNaN(parseInt(prop)))) {
|
|
24
|
+
if (!subscribers.has('__array__')) {
|
|
25
|
+
subscribers.set('__array__', new Set());
|
|
26
|
+
}
|
|
27
|
+
subscribers.get('__array__').add(currentEffect);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const value = target[prop];
|
|
31
|
+
|
|
32
|
+
// Intercept mutating array methods
|
|
33
|
+
if (ARRAY_MUTATING_METHODS.includes(prop) && typeof value === 'function') {
|
|
34
|
+
return function(...args) {
|
|
35
|
+
const result = Array.prototype[prop].apply(target, args);
|
|
36
|
+
|
|
37
|
+
// Notify all array subscribers
|
|
38
|
+
if (subscribers.has('__array__')) {
|
|
39
|
+
subscribers.get('__array__').forEach(callback => {
|
|
40
|
+
pendingEffects.add(callback);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
scheduleUpdate(onMutate);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Recursively wrap nested objects/arrays
|
|
50
|
+
if (value && typeof value === 'object') {
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return createReactiveArray(value, onMutate, subscribers);
|
|
53
|
+
} else {
|
|
54
|
+
return createReactiveObject(value, onMutate, subscribers);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return value;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
set(target, prop, value) {
|
|
62
|
+
const oldValue = target[prop];
|
|
63
|
+
if (oldValue === value) return true;
|
|
64
|
+
|
|
65
|
+
target[prop] = value;
|
|
66
|
+
|
|
67
|
+
// Notify subscribers
|
|
68
|
+
if (subscribers.has('__array__')) {
|
|
69
|
+
subscribers.get('__array__').forEach(callback => {
|
|
70
|
+
pendingEffects.add(callback);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
scheduleUpdate(onMutate);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return new Proxy(arr, handler);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a reactive object that triggers updates on property changes
|
|
84
|
+
*/
|
|
85
|
+
function createReactiveObject(obj, onMutate, subscribers) {
|
|
86
|
+
const handler = {
|
|
87
|
+
get(target, prop) {
|
|
88
|
+
// Skip internal properties
|
|
89
|
+
if (prop === '__subscribers' || prop === '__isReactive') {
|
|
90
|
+
return target[prop];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Track dependency if we're in an effect context
|
|
94
|
+
if (currentEffect) {
|
|
95
|
+
if (!subscribers.has(prop)) {
|
|
96
|
+
subscribers.set(prop, new Set());
|
|
97
|
+
}
|
|
98
|
+
subscribers.get(prop).add(currentEffect);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const value = target[prop];
|
|
102
|
+
|
|
103
|
+
// If it's a function, bind it to the proxy
|
|
104
|
+
if (typeof value === 'function') {
|
|
105
|
+
return value.bind(proxy);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Recursively wrap nested objects/arrays
|
|
109
|
+
if (value && typeof value === 'object') {
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return createReactiveArray(value, onMutate, subscribers);
|
|
112
|
+
} else if (!value.__isReactive) {
|
|
113
|
+
return createReactiveObject(value, onMutate, subscribers);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return value;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
set(target, prop, value) {
|
|
121
|
+
const oldValue = target[prop];
|
|
122
|
+
if (oldValue === value) return true;
|
|
123
|
+
|
|
124
|
+
target[prop] = value;
|
|
125
|
+
|
|
126
|
+
// Notify subscribers for this property
|
|
127
|
+
if (subscribers.has(prop)) {
|
|
128
|
+
subscribers.get(prop).forEach(callback => {
|
|
129
|
+
pendingEffects.add(callback);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Schedule batched update
|
|
134
|
+
scheduleUpdate(onMutate);
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
deleteProperty(target, prop) {
|
|
140
|
+
delete target[prop];
|
|
141
|
+
|
|
142
|
+
if (subscribers.has(prop)) {
|
|
143
|
+
subscribers.get(prop).forEach(callback => {
|
|
144
|
+
pendingEffects.add(callback);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
scheduleUpdate(onMutate);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const proxy = new Proxy(obj, handler);
|
|
154
|
+
return proxy;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates a reactive proxy that tracks dependencies and triggers updates
|
|
159
|
+
*/
|
|
160
|
+
export function createReactive(data, onUpdate) {
|
|
161
|
+
const subscribers = new Map(); // property -> Set of callbacks
|
|
162
|
+
|
|
163
|
+
let proxy;
|
|
164
|
+
if (Array.isArray(data)) {
|
|
165
|
+
proxy = createReactiveArray(data, onUpdate, subscribers);
|
|
166
|
+
} else {
|
|
167
|
+
proxy = createReactiveObject(data, onUpdate, subscribers);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
proxy.__subscribers = subscribers;
|
|
171
|
+
proxy.__isReactive = true;
|
|
172
|
+
return proxy;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Run an effect function while tracking its dependencies
|
|
177
|
+
*/
|
|
178
|
+
export function runEffect(fn) {
|
|
179
|
+
currentEffect = fn;
|
|
180
|
+
try {
|
|
181
|
+
fn();
|
|
182
|
+
} finally {
|
|
183
|
+
currentEffect = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Schedule a batched DOM update
|
|
189
|
+
*/
|
|
190
|
+
export function scheduleUpdate(callback) {
|
|
191
|
+
if (pendingUpdate) return;
|
|
192
|
+
|
|
193
|
+
pendingUpdate = true;
|
|
194
|
+
queueMicrotask(() => {
|
|
195
|
+
pendingUpdate = false;
|
|
196
|
+
|
|
197
|
+
// Run all pending effects
|
|
198
|
+
const effects = [...pendingEffects];
|
|
199
|
+
pendingEffects.clear();
|
|
200
|
+
effects.forEach(effect => {
|
|
201
|
+
try {
|
|
202
|
+
runEffect(effect);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error('[SenangStart] Effect error:', e);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (callback) callback();
|
|
209
|
+
});
|
|
210
|
+
}
|
package/src/walker.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - DOM Walker
|
|
3
|
+
* Recursive DOM traversal and initialization
|
|
4
|
+
*
|
|
5
|
+
* @module walker
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createReactive } from './reactive.js';
|
|
9
|
+
import { attributeHandlers, handleBind, handleEvent, handleFor, handleIf, setWalkFunction } from './handlers/index.js';
|
|
10
|
+
|
|
11
|
+
// Store references
|
|
12
|
+
let registeredDataFactories = {};
|
|
13
|
+
let stores = {};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set external references (called from index.js)
|
|
17
|
+
*/
|
|
18
|
+
export function setReferences(dataFactories, storeRef) {
|
|
19
|
+
registeredDataFactories = dataFactories;
|
|
20
|
+
stores = storeRef;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Walk the DOM tree and initialize SenangStart attributes
|
|
25
|
+
*/
|
|
26
|
+
export function walk(el, parentScope = null) {
|
|
27
|
+
// Skip non-element nodes
|
|
28
|
+
if (el.nodeType !== 1) return;
|
|
29
|
+
|
|
30
|
+
// Skip if element has ss-ignore
|
|
31
|
+
if (el.hasAttribute('ss-ignore')) return;
|
|
32
|
+
|
|
33
|
+
let scope = parentScope;
|
|
34
|
+
|
|
35
|
+
// Check for ss-data to create new scope
|
|
36
|
+
if (el.hasAttribute('ss-data')) {
|
|
37
|
+
const dataExpr = el.getAttribute('ss-data').trim();
|
|
38
|
+
let initialData = {};
|
|
39
|
+
|
|
40
|
+
if (dataExpr) {
|
|
41
|
+
// Check if it's a registered data factory
|
|
42
|
+
if (registeredDataFactories[dataExpr]) {
|
|
43
|
+
initialData = registeredDataFactories[dataExpr]();
|
|
44
|
+
} else {
|
|
45
|
+
// Parse as object literal - use Function for safety
|
|
46
|
+
try {
|
|
47
|
+
initialData = new Function(`return (${dataExpr})`)();
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('[SenangStart] Failed to parse ss-data:', dataExpr, e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
scope = {
|
|
55
|
+
data: createReactive(initialData, () => {}),
|
|
56
|
+
$refs: {},
|
|
57
|
+
$store: stores
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Store scope on element for MutationObserver
|
|
61
|
+
el.__ssScope = scope;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If no scope, skip processing directives
|
|
65
|
+
if (!scope) {
|
|
66
|
+
// Still walk children in case they have ss-data
|
|
67
|
+
Array.from(el.children).forEach(child => walk(child, null));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle ss-for (must be on template element)
|
|
72
|
+
if (el.tagName === 'TEMPLATE' && el.hasAttribute('ss-for')) {
|
|
73
|
+
handleFor(el, el.getAttribute('ss-for'), scope);
|
|
74
|
+
return; // ss-for handles its own children
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle ss-if (must be on template element)
|
|
78
|
+
if (el.tagName === 'TEMPLATE' && el.hasAttribute('ss-if')) {
|
|
79
|
+
handleIf(el, el.getAttribute('ss-if'), scope);
|
|
80
|
+
return; // ss-if handles its own children
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Process all ss-* attributes
|
|
84
|
+
const attributes = Array.from(el.attributes);
|
|
85
|
+
|
|
86
|
+
for (const attr of attributes) {
|
|
87
|
+
const name = attr.name;
|
|
88
|
+
const value = attr.value;
|
|
89
|
+
|
|
90
|
+
// Skip ss-data (already processed) and ss-describe (metadata only)
|
|
91
|
+
if (name === 'ss-data' || name === 'ss-describe') continue;
|
|
92
|
+
|
|
93
|
+
// Handle standard attributes
|
|
94
|
+
if (attributeHandlers[name]) {
|
|
95
|
+
attributeHandlers[name](el, value, scope);
|
|
96
|
+
}
|
|
97
|
+
// Handle ss-bind:[attr]
|
|
98
|
+
else if (name.startsWith('ss-bind:')) {
|
|
99
|
+
handleBind(el, name, value, scope);
|
|
100
|
+
}
|
|
101
|
+
// Handle ss-on:[event]
|
|
102
|
+
else if (name.startsWith('ss-on:')) {
|
|
103
|
+
handleEvent(el, name, value, scope);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Remove ss-cloak after processing
|
|
108
|
+
if (el.hasAttribute('ss-cloak')) {
|
|
109
|
+
el.removeAttribute('ss-cloak');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Walk children
|
|
113
|
+
Array.from(el.children).forEach(child => walk(child, scope));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Set the walk function reference in directives module
|
|
117
|
+
setWalkFunction(walk);
|