@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/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;
@@ -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
+ }
@@ -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);