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