@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.
@@ -0,0 +1,992 @@
1
+ /**
2
+ * SenangStart Actions v0.1.0
3
+ * Declarative UI framework for humans and AI agents
4
+ * @license MIT
5
+ */
6
+ /**
7
+ * SenangStart Actions - Reactive System
8
+ * Proxy-based reactivity with dependency tracking
9
+ *
10
+ * @module reactive
11
+ */
12
+
13
+ // Array methods that mutate the array
14
+ const ARRAY_MUTATING_METHODS = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin'];
15
+
16
+ // Internal state
17
+ let pendingUpdate = false;
18
+ const pendingEffects = new Set();
19
+ let currentEffect = null;
20
+
21
+ /**
22
+ * Creates a reactive array that triggers updates on mutations
23
+ */
24
+ function createReactiveArray(arr, onMutate, subscribers) {
25
+ const handler = {
26
+ get(target, prop) {
27
+ // Track dependency for length and numeric indices
28
+ if (currentEffect && (prop === 'length' || !isNaN(parseInt(prop)))) {
29
+ if (!subscribers.has('__array__')) {
30
+ subscribers.set('__array__', new Set());
31
+ }
32
+ subscribers.get('__array__').add(currentEffect);
33
+ }
34
+
35
+ const value = target[prop];
36
+
37
+ // Intercept mutating array methods
38
+ if (ARRAY_MUTATING_METHODS.includes(prop) && typeof value === 'function') {
39
+ return function(...args) {
40
+ const result = Array.prototype[prop].apply(target, args);
41
+
42
+ // Notify all array subscribers
43
+ if (subscribers.has('__array__')) {
44
+ subscribers.get('__array__').forEach(callback => {
45
+ pendingEffects.add(callback);
46
+ });
47
+ }
48
+
49
+ scheduleUpdate(onMutate);
50
+ return result;
51
+ };
52
+ }
53
+
54
+ // Recursively wrap nested objects/arrays
55
+ if (value && typeof value === 'object') {
56
+ if (Array.isArray(value)) {
57
+ return createReactiveArray(value, onMutate, subscribers);
58
+ } else {
59
+ return createReactiveObject(value, onMutate, subscribers);
60
+ }
61
+ }
62
+
63
+ return value;
64
+ },
65
+
66
+ set(target, prop, value) {
67
+ const oldValue = target[prop];
68
+ if (oldValue === value) return true;
69
+
70
+ target[prop] = value;
71
+
72
+ // Notify subscribers
73
+ if (subscribers.has('__array__')) {
74
+ subscribers.get('__array__').forEach(callback => {
75
+ pendingEffects.add(callback);
76
+ });
77
+ }
78
+
79
+ scheduleUpdate(onMutate);
80
+ return true;
81
+ }
82
+ };
83
+
84
+ return new Proxy(arr, handler);
85
+ }
86
+
87
+ /**
88
+ * Creates a reactive object that triggers updates on property changes
89
+ */
90
+ function createReactiveObject(obj, onMutate, subscribers) {
91
+ const handler = {
92
+ get(target, prop) {
93
+ // Skip internal properties
94
+ if (prop === '__subscribers' || prop === '__isReactive') {
95
+ return target[prop];
96
+ }
97
+
98
+ // Track dependency if we're in an effect context
99
+ if (currentEffect) {
100
+ if (!subscribers.has(prop)) {
101
+ subscribers.set(prop, new Set());
102
+ }
103
+ subscribers.get(prop).add(currentEffect);
104
+ }
105
+
106
+ const value = target[prop];
107
+
108
+ // If it's a function, bind it to the proxy
109
+ if (typeof value === 'function') {
110
+ return value.bind(proxy);
111
+ }
112
+
113
+ // Recursively wrap nested objects/arrays
114
+ if (value && typeof value === 'object') {
115
+ if (Array.isArray(value)) {
116
+ return createReactiveArray(value, onMutate, subscribers);
117
+ } else if (!value.__isReactive) {
118
+ return createReactiveObject(value, onMutate, subscribers);
119
+ }
120
+ }
121
+
122
+ return value;
123
+ },
124
+
125
+ set(target, prop, value) {
126
+ const oldValue = target[prop];
127
+ if (oldValue === value) return true;
128
+
129
+ target[prop] = value;
130
+
131
+ // Notify subscribers for this property
132
+ if (subscribers.has(prop)) {
133
+ subscribers.get(prop).forEach(callback => {
134
+ pendingEffects.add(callback);
135
+ });
136
+ }
137
+
138
+ // Schedule batched update
139
+ scheduleUpdate(onMutate);
140
+
141
+ return true;
142
+ },
143
+
144
+ deleteProperty(target, prop) {
145
+ delete target[prop];
146
+
147
+ if (subscribers.has(prop)) {
148
+ subscribers.get(prop).forEach(callback => {
149
+ pendingEffects.add(callback);
150
+ });
151
+ }
152
+
153
+ scheduleUpdate(onMutate);
154
+ return true;
155
+ }
156
+ };
157
+
158
+ const proxy = new Proxy(obj, handler);
159
+ return proxy;
160
+ }
161
+
162
+ /**
163
+ * Creates a reactive proxy that tracks dependencies and triggers updates
164
+ */
165
+ function createReactive(data, onUpdate) {
166
+ const subscribers = new Map(); // property -> Set of callbacks
167
+
168
+ let proxy;
169
+ if (Array.isArray(data)) {
170
+ proxy = createReactiveArray(data, onUpdate, subscribers);
171
+ } else {
172
+ proxy = createReactiveObject(data, onUpdate, subscribers);
173
+ }
174
+
175
+ proxy.__subscribers = subscribers;
176
+ proxy.__isReactive = true;
177
+ return proxy;
178
+ }
179
+
180
+ /**
181
+ * Run an effect function while tracking its dependencies
182
+ */
183
+ function runEffect(fn) {
184
+ currentEffect = fn;
185
+ try {
186
+ fn();
187
+ } finally {
188
+ currentEffect = null;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Schedule a batched DOM update
194
+ */
195
+ function scheduleUpdate(callback) {
196
+ if (pendingUpdate) return;
197
+
198
+ pendingUpdate = true;
199
+ queueMicrotask(() => {
200
+ pendingUpdate = false;
201
+
202
+ // Run all pending effects
203
+ const effects = [...pendingEffects];
204
+ pendingEffects.clear();
205
+ effects.forEach(effect => {
206
+ try {
207
+ runEffect(effect);
208
+ } catch (e) {
209
+ console.error('[SenangStart] Effect error:', e);
210
+ }
211
+ });
212
+
213
+ if (callback) callback();
214
+ });
215
+ }
216
+
217
+ /**
218
+ * SenangStart Actions - Expression Evaluator
219
+ * Safe evaluation of expressions within component scope
220
+ *
221
+ * @module evaluator
222
+ */
223
+
224
+ /**
225
+ * Create a function to evaluate an expression within a component scope
226
+ */
227
+ function createEvaluator(expression, scope, element) {
228
+ const { data, $refs, $store } = scope;
229
+
230
+ // Magic properties
231
+ const magics = {
232
+ $data: data,
233
+ $store: $store,
234
+ $el: element,
235
+ $my: element,
236
+ $refs: $refs,
237
+ $dispatch: (name, detail = {}) => {
238
+ element.dispatchEvent(new CustomEvent(name, {
239
+ detail,
240
+ bubbles: true,
241
+ cancelable: true
242
+ }));
243
+ },
244
+ $watch: (prop, callback) => {
245
+ const watchEffect = () => {
246
+ const value = data[prop];
247
+ callback(value);
248
+ };
249
+ if (data.__subscribers) {
250
+ if (!data.__subscribers.has(prop)) {
251
+ data.__subscribers.set(prop, new Set());
252
+ }
253
+ data.__subscribers.get(prop).add(watchEffect);
254
+ }
255
+ },
256
+ $nextTick: (fn) => {
257
+ queueMicrotask(fn);
258
+ }
259
+ };
260
+
261
+ // Build the function with data properties spread as local variables
262
+ const dataKeys = Object.keys(typeof data === 'object' && data !== null ? data : {});
263
+ const magicKeys = Object.keys(magics);
264
+
265
+ try {
266
+ const fn = new Function(
267
+ ...dataKeys,
268
+ ...magicKeys,
269
+ `with(this) { return (${expression}); }`
270
+ );
271
+
272
+ return function() {
273
+ const dataValues = dataKeys.map(k => data[k]);
274
+ const magicValues = magicKeys.map(k => magics[k]);
275
+ return fn.call(data, ...dataValues, ...magicValues);
276
+ };
277
+ } catch (e) {
278
+ console.error(`[SenangStart] Failed to parse expression: ${expression}`, e);
279
+ return () => undefined;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Create a function to execute a statement (not return value)
285
+ */
286
+ function createExecutor(expression, scope, element) {
287
+ const { data, $refs, $store } = scope;
288
+
289
+ const magics = {
290
+ $data: data,
291
+ $store: $store,
292
+ $el: element,
293
+ $my: element,
294
+ $refs: $refs,
295
+ $dispatch: (name, detail = {}) => {
296
+ element.dispatchEvent(new CustomEvent(name, {
297
+ detail,
298
+ bubbles: true,
299
+ cancelable: true
300
+ }));
301
+ },
302
+ $watch: (prop, callback) => {
303
+ if (data.__subscribers) {
304
+ if (!data.__subscribers.has(prop)) {
305
+ data.__subscribers.set(prop, new Set());
306
+ }
307
+ data.__subscribers.get(prop).add(() => callback(data[prop]));
308
+ }
309
+ },
310
+ $nextTick: (fn) => {
311
+ queueMicrotask(fn);
312
+ }
313
+ };
314
+
315
+ const dataKeys = Object.keys(typeof data === 'object' && data !== null ? data : {});
316
+ const magicKeys = Object.keys(magics);
317
+
318
+ try {
319
+ const fn = new Function(
320
+ ...dataKeys,
321
+ ...magicKeys,
322
+ `with(this) { ${expression} }`
323
+ );
324
+
325
+ return function() {
326
+ const dataValues = dataKeys.map(k => data[k]);
327
+ const magicValues = magicKeys.map(k => magics[k]);
328
+ return fn.call(data, ...dataValues, ...magicValues);
329
+ };
330
+ } catch (e) {
331
+ console.error(`[SenangStart] Failed to parse expression: ${expression}`, e);
332
+ return () => {};
333
+ }
334
+ }
335
+
336
+ /**
337
+ * SenangStart Actions - Attribute Handlers
338
+ * Handlers for basic ss-* attributes
339
+ *
340
+ * @module handlers/attributes
341
+ */
342
+
343
+
344
+ /**
345
+ * Handle ss-transition animations
346
+ */
347
+ function handleTransition(el, show, originalDisplay) {
348
+ if (show) {
349
+ // Enter transition
350
+ el.classList.add('ss-enter-from');
351
+ el.classList.add('ss-enter-active');
352
+ el.style.display = originalDisplay;
353
+
354
+ requestAnimationFrame(() => {
355
+ el.classList.remove('ss-enter-from');
356
+ el.classList.add('ss-enter-to');
357
+ });
358
+
359
+ const onEnd = () => {
360
+ el.classList.remove('ss-enter-active', 'ss-enter-to');
361
+ el.removeEventListener('transitionend', onEnd);
362
+ };
363
+ el.addEventListener('transitionend', onEnd);
364
+ } else {
365
+ // Leave transition
366
+ el.classList.add('ss-leave-from');
367
+ el.classList.add('ss-leave-active');
368
+
369
+ requestAnimationFrame(() => {
370
+ el.classList.remove('ss-leave-from');
371
+ el.classList.add('ss-leave-to');
372
+ });
373
+
374
+ const onEnd = () => {
375
+ el.style.display = 'none';
376
+ el.classList.remove('ss-leave-active', 'ss-leave-to');
377
+ el.removeEventListener('transitionend', onEnd);
378
+ };
379
+ el.addEventListener('transitionend', onEnd);
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Attribute handlers map
385
+ */
386
+ const attributeHandlers = {
387
+ /**
388
+ * ss-text: Set element's innerText
389
+ */
390
+ 'ss-text': (el, expr, scope) => {
391
+ const update = () => {
392
+ const evaluator = createEvaluator(expr, scope, el);
393
+ el.innerText = evaluator() ?? '';
394
+ };
395
+ runEffect(update);
396
+ },
397
+
398
+ /**
399
+ * ss-html: Set element's innerHTML
400
+ */
401
+ 'ss-html': (el, expr, scope) => {
402
+ const update = () => {
403
+ const evaluator = createEvaluator(expr, scope, el);
404
+ el.innerHTML = evaluator() ?? '';
405
+ };
406
+ runEffect(update);
407
+ },
408
+
409
+ /**
410
+ * ss-show: Toggle visibility
411
+ */
412
+ 'ss-show': (el, expr, scope) => {
413
+ const originalDisplay = el.style.display || '';
414
+
415
+ const update = () => {
416
+ const evaluator = createEvaluator(expr, scope, el);
417
+ const show = !!evaluator();
418
+
419
+ if (el.hasAttribute('ss-transition')) {
420
+ handleTransition(el, show, originalDisplay);
421
+ } else {
422
+ el.style.display = show ? originalDisplay : 'none';
423
+ }
424
+ };
425
+ runEffect(update);
426
+ },
427
+
428
+ /**
429
+ * ss-model: Two-way binding for inputs
430
+ */
431
+ 'ss-model': (el, expr, scope) => {
432
+ const { data } = scope;
433
+
434
+ // Determine input type
435
+ const isCheckbox = el.type === 'checkbox';
436
+ const isRadio = el.type === 'radio';
437
+ const isSelect = el.tagName === 'SELECT';
438
+
439
+ // Set initial value
440
+ const setInitialValue = () => {
441
+ const evaluator = createEvaluator(expr, scope, el);
442
+ const value = evaluator();
443
+
444
+ if (isCheckbox) {
445
+ el.checked = !!value;
446
+ } else if (isRadio) {
447
+ el.checked = el.value === value;
448
+ } else if (isSelect) {
449
+ el.value = value ?? '';
450
+ } else {
451
+ el.value = value ?? '';
452
+ }
453
+ };
454
+
455
+ runEffect(setInitialValue);
456
+
457
+ // Listen for changes
458
+ const eventType = isCheckbox || isRadio ? 'change' : 'input';
459
+ el.addEventListener(eventType, () => {
460
+ let newValue;
461
+
462
+ if (isCheckbox) {
463
+ newValue = el.checked;
464
+ } else if (isRadio) {
465
+ if (el.checked) newValue = el.value;
466
+ else return; // Don't update if not checked
467
+ } else {
468
+ newValue = el.value;
469
+ }
470
+
471
+ // Set the value on the data object
472
+ data[expr] = newValue;
473
+ });
474
+ },
475
+
476
+ /**
477
+ * ss-ref: Register element reference
478
+ */
479
+ 'ss-ref': (el, name, scope) => {
480
+ scope.$refs[name] = el;
481
+ },
482
+
483
+ /**
484
+ * ss-init: Run initialization code
485
+ */
486
+ 'ss-init': (el, expr, scope) => {
487
+ const executor = createExecutor(expr, scope, el);
488
+ executor();
489
+ },
490
+
491
+ /**
492
+ * ss-effect: Run reactive effect
493
+ */
494
+ 'ss-effect': (el, expr, scope) => {
495
+ const update = () => {
496
+ const executor = createExecutor(expr, scope, el);
497
+ executor();
498
+ };
499
+ runEffect(update);
500
+ }
501
+ };
502
+
503
+ /**
504
+ * SenangStart Actions - Bind Handler
505
+ * Handler for ss-bind:[attr] dynamic attribute binding
506
+ *
507
+ * @module handlers/bind
508
+ */
509
+
510
+
511
+ /**
512
+ * Handle ss-bind:[attr] dynamically
513
+ */
514
+ function handleBind(el, attrName, expr, scope) {
515
+ const attr = attrName.replace('ss-bind:', '');
516
+
517
+ const update = () => {
518
+ const evaluator = createEvaluator(expr, scope, el);
519
+ const value = evaluator();
520
+
521
+ if (attr === 'class') {
522
+ if (typeof value === 'string') {
523
+ el.className = value;
524
+ } else if (typeof value === 'object') {
525
+ // Object syntax: { 'class-name': condition }
526
+ Object.entries(value).forEach(([className, condition]) => {
527
+ el.classList.toggle(className, !!condition);
528
+ });
529
+ }
530
+ } else if (attr === 'style') {
531
+ if (typeof value === 'string') {
532
+ el.style.cssText = value;
533
+ } else if (typeof value === 'object') {
534
+ Object.assign(el.style, value);
535
+ }
536
+ } else if (value === false || value === null || value === undefined) {
537
+ el.removeAttribute(attr);
538
+ } else if (value === true) {
539
+ el.setAttribute(attr, '');
540
+ } else {
541
+ el.setAttribute(attr, value);
542
+ }
543
+ };
544
+
545
+ runEffect(update);
546
+ }
547
+
548
+ /**
549
+ * SenangStart Actions - Event Handler
550
+ * Handler for ss-on:[event] with modifiers
551
+ *
552
+ * @module handlers/events
553
+ */
554
+
555
+
556
+ /**
557
+ * Handle ss-on:[event] dynamically
558
+ */
559
+ function handleEvent(el, attrName, expr, scope) {
560
+ const parts = attrName.replace('ss-on:', '').split('.');
561
+ const eventName = parts[0];
562
+ const modifiers = parts.slice(1);
563
+
564
+ const executor = createExecutor(expr, scope, el);
565
+
566
+ const handler = (event) => {
567
+ // Handle modifiers
568
+ if (modifiers.includes('prevent')) event.preventDefault();
569
+ if (modifiers.includes('stop')) event.stopPropagation();
570
+ if (modifiers.includes('self') && event.target !== el) return;
571
+ if (modifiers.includes('once')) {
572
+ el.removeEventListener(eventName, handler);
573
+ }
574
+
575
+ // For keyboard events, check key modifiers
576
+ if (event instanceof KeyboardEvent) {
577
+ const key = event.key.toLowerCase();
578
+ const keyModifiers = ['enter', 'escape', 'tab', 'space', 'up', 'down', 'left', 'right'];
579
+ const hasKeyModifier = modifiers.some(m => keyModifiers.includes(m));
580
+
581
+ if (hasKeyModifier) {
582
+ const keyMap = {
583
+ 'enter': 'enter',
584
+ 'escape': 'escape',
585
+ 'tab': 'tab',
586
+ 'space': ' ',
587
+ 'up': 'arrowup',
588
+ 'down': 'arrowdown',
589
+ 'left': 'arrowleft',
590
+ 'right': 'arrowright'
591
+ };
592
+
593
+ const shouldFire = modifiers.some(m => keyMap[m] === key);
594
+ if (!shouldFire) return;
595
+ }
596
+ }
597
+
598
+ // Execute the expression with $event available
599
+ scope.data.$event = event;
600
+ executor();
601
+ delete scope.data.$event;
602
+ };
603
+
604
+ // Special window/document events
605
+ if (modifiers.includes('window')) {
606
+ window.addEventListener(eventName, handler);
607
+ } else if (modifiers.includes('document')) {
608
+ document.addEventListener(eventName, handler);
609
+ } else {
610
+ el.addEventListener(eventName, handler);
611
+ }
612
+ }
613
+
614
+ /**
615
+ * SenangStart Actions - Directive Handlers
616
+ * Handlers for ss-for and ss-if template directives
617
+ *
618
+ * @module handlers/directives
619
+ */
620
+
621
+
622
+ // Forward declaration - will be set by walker.js
623
+ let walkFn = null;
624
+
625
+ /**
626
+ * Set the walk function reference (to avoid circular imports)
627
+ */
628
+ function setWalkFunction(fn) {
629
+ walkFn = fn;
630
+ }
631
+
632
+ /**
633
+ * Handle ss-for directive
634
+ */
635
+ function handleFor(templateEl, expr, scope) {
636
+ // Parse expression: "item in items" or "(item, index) in items"
637
+ const match = expr.match(/^\s*(?:\(([^,]+),\s*([^)]+)\)|([^\s]+))\s+in\s+(.+)$/);
638
+ if (!match) {
639
+ console.error('[SenangStart] Invalid ss-for expression:', expr);
640
+ return;
641
+ }
642
+
643
+ const itemName = match[1] || match[3];
644
+ const indexName = match[2] || 'index';
645
+ const arrayExpr = match[4];
646
+
647
+ const parent = templateEl.parentNode;
648
+ const anchor = document.createComment(`ss-for: ${expr}`);
649
+ parent.insertBefore(anchor, templateEl);
650
+ templateEl.remove();
651
+
652
+ let currentNodes = [];
653
+ let lastItemsJSON = '';
654
+
655
+ const update = () => {
656
+ const evaluator = createEvaluator(arrayExpr, scope, templateEl);
657
+ const items = evaluator() || [];
658
+
659
+ // Check if items actually changed (shallow comparison)
660
+ const itemsJSON = JSON.stringify(items);
661
+ if (itemsJSON === lastItemsJSON) {
662
+ return; // No change, skip re-render
663
+ }
664
+ lastItemsJSON = itemsJSON;
665
+
666
+ // Remove old nodes
667
+ currentNodes.forEach(node => node.remove());
668
+ currentNodes = [];
669
+
670
+ // Create new nodes
671
+ items.forEach((item, index) => {
672
+ const clone = templateEl.content.cloneNode(true);
673
+ const nodes = Array.from(clone.childNodes).filter(n => n.nodeType === 1);
674
+
675
+ // Create child scope with item and index - use parent scope's data for non-item properties
676
+ const itemScope = {
677
+ data: createReactive({
678
+ ...scope.data,
679
+ [itemName]: item,
680
+ [indexName]: index
681
+ }, () => {}),
682
+ $refs: scope.$refs,
683
+ $store: scope.$store,
684
+ parentData: scope.data // Keep reference to parent data
685
+ };
686
+
687
+ nodes.forEach(node => {
688
+ parent.insertBefore(node, anchor);
689
+ currentNodes.push(node);
690
+ if (walkFn) walkFn(node, itemScope);
691
+ });
692
+ });
693
+ };
694
+
695
+ runEffect(update);
696
+ }
697
+
698
+ /**
699
+ * Handle ss-if directive
700
+ */
701
+ function handleIf(templateEl, expr, scope) {
702
+ const parent = templateEl.parentNode;
703
+ const anchor = document.createComment(`ss-if: ${expr}`);
704
+ parent.insertBefore(anchor, templateEl);
705
+ templateEl.remove();
706
+
707
+ let currentNodes = [];
708
+
709
+ const update = () => {
710
+ const evaluator = createEvaluator(expr, scope, templateEl);
711
+ const condition = !!evaluator();
712
+
713
+ // Remove old nodes
714
+ currentNodes.forEach(node => node.remove());
715
+ currentNodes = [];
716
+
717
+ if (condition) {
718
+ const clone = templateEl.content.cloneNode(true);
719
+ const nodes = Array.from(clone.childNodes).filter(n => n.nodeType === 1);
720
+
721
+ nodes.forEach(node => {
722
+ parent.insertBefore(node, anchor);
723
+ currentNodes.push(node);
724
+ if (walkFn) walkFn(node, scope);
725
+ });
726
+ }
727
+ };
728
+
729
+ runEffect(update);
730
+ }
731
+
732
+ /**
733
+ * SenangStart Actions - DOM Walker
734
+ * Recursive DOM traversal and initialization
735
+ *
736
+ * @module walker
737
+ */
738
+
739
+
740
+ // Store references
741
+ let registeredDataFactories$1 = {};
742
+ let stores$1 = {};
743
+
744
+ /**
745
+ * Set external references (called from index.js)
746
+ */
747
+ function setReferences(dataFactories, storeRef) {
748
+ registeredDataFactories$1 = dataFactories;
749
+ stores$1 = storeRef;
750
+ }
751
+
752
+ /**
753
+ * Walk the DOM tree and initialize SenangStart attributes
754
+ */
755
+ function walk(el, parentScope = null) {
756
+ // Skip non-element nodes
757
+ if (el.nodeType !== 1) return;
758
+
759
+ // Skip if element has ss-ignore
760
+ if (el.hasAttribute('ss-ignore')) return;
761
+
762
+ let scope = parentScope;
763
+
764
+ // Check for ss-data to create new scope
765
+ if (el.hasAttribute('ss-data')) {
766
+ const dataExpr = el.getAttribute('ss-data').trim();
767
+ let initialData = {};
768
+
769
+ if (dataExpr) {
770
+ // Check if it's a registered data factory
771
+ if (registeredDataFactories$1[dataExpr]) {
772
+ initialData = registeredDataFactories$1[dataExpr]();
773
+ } else {
774
+ // Parse as object literal - use Function for safety
775
+ try {
776
+ initialData = new Function(`return (${dataExpr})`)();
777
+ } catch (e) {
778
+ console.error('[SenangStart] Failed to parse ss-data:', dataExpr, e);
779
+ }
780
+ }
781
+ }
782
+
783
+ scope = {
784
+ data: createReactive(initialData, () => {}),
785
+ $refs: {},
786
+ $store: stores$1
787
+ };
788
+
789
+ // Store scope on element for MutationObserver
790
+ el.__ssScope = scope;
791
+ }
792
+
793
+ // If no scope, skip processing directives
794
+ if (!scope) {
795
+ // Still walk children in case they have ss-data
796
+ Array.from(el.children).forEach(child => walk(child, null));
797
+ return;
798
+ }
799
+
800
+ // Handle ss-for (must be on template element)
801
+ if (el.tagName === 'TEMPLATE' && el.hasAttribute('ss-for')) {
802
+ handleFor(el, el.getAttribute('ss-for'), scope);
803
+ return; // ss-for handles its own children
804
+ }
805
+
806
+ // Handle ss-if (must be on template element)
807
+ if (el.tagName === 'TEMPLATE' && el.hasAttribute('ss-if')) {
808
+ handleIf(el, el.getAttribute('ss-if'), scope);
809
+ return; // ss-if handles its own children
810
+ }
811
+
812
+ // Process all ss-* attributes
813
+ const attributes = Array.from(el.attributes);
814
+
815
+ for (const attr of attributes) {
816
+ const name = attr.name;
817
+ const value = attr.value;
818
+
819
+ // Skip ss-data (already processed) and ss-describe (metadata only)
820
+ if (name === 'ss-data' || name === 'ss-describe') continue;
821
+
822
+ // Handle standard attributes
823
+ if (attributeHandlers[name]) {
824
+ attributeHandlers[name](el, value, scope);
825
+ }
826
+ // Handle ss-bind:[attr]
827
+ else if (name.startsWith('ss-bind:')) {
828
+ handleBind(el, name, value, scope);
829
+ }
830
+ // Handle ss-on:[event]
831
+ else if (name.startsWith('ss-on:')) {
832
+ handleEvent(el, name, value, scope);
833
+ }
834
+ }
835
+
836
+ // Remove ss-cloak after processing
837
+ if (el.hasAttribute('ss-cloak')) {
838
+ el.removeAttribute('ss-cloak');
839
+ }
840
+
841
+ // Walk children
842
+ Array.from(el.children).forEach(child => walk(child, scope));
843
+ }
844
+
845
+ // Set the walk function reference in directives module
846
+ setWalkFunction(walk);
847
+
848
+ /**
849
+ * SenangStart Actions - MutationObserver
850
+ * Watch for dynamically added elements
851
+ *
852
+ * @module observer
853
+ */
854
+
855
+
856
+ /**
857
+ * Set up observer for dynamically added elements
858
+ */
859
+ function setupObserver() {
860
+ const observer = new MutationObserver((mutations) => {
861
+ for (const mutation of mutations) {
862
+ for (const node of mutation.addedNodes) {
863
+ if (node.nodeType === 1) {
864
+ // Check if node or any ancestor already has scope
865
+ let current = node;
866
+ let parentScope = null;
867
+
868
+ while (current.parentElement) {
869
+ current = current.parentElement;
870
+ if (current.__ssScope) {
871
+ parentScope = current.__ssScope;
872
+ break;
873
+ }
874
+ }
875
+
876
+ walk(node, parentScope);
877
+ }
878
+ }
879
+ }
880
+ });
881
+
882
+ observer.observe(document.body, {
883
+ childList: true,
884
+ subtree: true
885
+ });
886
+
887
+ return observer;
888
+ }
889
+
890
+ /**
891
+ * SenangStart Actions v0.1.0
892
+ * Declarative UI framework for humans and AI agents
893
+ *
894
+ * @license MIT
895
+ * @author Bookklik
896
+ * @module senangstart-actions
897
+ */
898
+
899
+
900
+ // =========================================================================
901
+ // CSS Injection for ss-cloak
902
+ // =========================================================================
903
+ const style = document.createElement('style');
904
+ style.textContent = '[ss-cloak] { display: none !important; }';
905
+ document.head.appendChild(style);
906
+
907
+ // =========================================================================
908
+ // Internal State
909
+ // =========================================================================
910
+ const registeredDataFactories = {}; // SenangStart.data() registrations
911
+ const stores = {}; // SenangStart.store() registrations
912
+
913
+ // Set references in walker module
914
+ setReferences(registeredDataFactories, stores);
915
+
916
+ // =========================================================================
917
+ // Public API
918
+ // =========================================================================
919
+
920
+ const SenangStart = {
921
+ /**
922
+ * Register a reusable data component
923
+ * @param {string} name - Component name
924
+ * @param {Function} factory - Factory function returning data object
925
+ */
926
+ data(name, factory) {
927
+ if (typeof factory !== 'function') {
928
+ console.error('[SenangStart] data() requires a factory function');
929
+ return this;
930
+ }
931
+ registeredDataFactories[name] = factory;
932
+ return this;
933
+ },
934
+
935
+ /**
936
+ * Register a global reactive store
937
+ * @param {string} name - Store name
938
+ * @param {Object} data - Store data object
939
+ */
940
+ store(name, data) {
941
+ if (typeof data !== 'object') {
942
+ console.error('[SenangStart] store() requires an object');
943
+ return this;
944
+ }
945
+ stores[name] = createReactive(data, () => {});
946
+ return this;
947
+ },
948
+
949
+ /**
950
+ * Manually initialize a DOM tree
951
+ * @param {Element} root - Root element to initialize
952
+ */
953
+ init(root = document.body) {
954
+ walk(root, null);
955
+ return this;
956
+ },
957
+
958
+ /**
959
+ * Start the framework
960
+ */
961
+ start() {
962
+ if (document.readyState === 'loading') {
963
+ document.addEventListener('DOMContentLoaded', () => {
964
+ this.init();
965
+ setupObserver();
966
+ });
967
+ } else {
968
+ this.init();
969
+ setupObserver();
970
+ }
971
+ return this;
972
+ },
973
+
974
+ /**
975
+ * Version
976
+ */
977
+ version: '0.1.0'
978
+ };
979
+
980
+ // =========================================================================
981
+ // Auto-start
982
+ // =========================================================================
983
+
984
+ // Expose globally
985
+ if (typeof window !== 'undefined') {
986
+ window.SenangStart = SenangStart;
987
+ }
988
+
989
+ // Auto-start when script loads
990
+ SenangStart.start();
991
+
992
+ export { SenangStart as default };