@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,9 @@
1
+ var SenangStart=function(){"use strict";const e=["push","pop","shift","unshift","splice","sort","reverse","fill","copyWithin"];let t=!1;const s=new Set;let r=null;function n(t,a,i){return new Proxy(t,{get(t,d){!r||"length"!==d&&isNaN(parseInt(d))||(i.has("__array__")||i.set("__array__",new Set),i.get("__array__").add(r));const l=t[d];return e.includes(d)&&"function"==typeof l?function(...e){const r=Array.prototype[d].apply(t,e);return i.has("__array__")&&i.get("__array__").forEach(e=>{s.add(e)}),c(a),r}:l&&"object"==typeof l?Array.isArray(l)?n(l,a,i):o(l,a,i):l},set:(e,t,r)=>(e[t]===r||(e[t]=r,i.has("__array__")&&i.get("__array__").forEach(e=>{s.add(e)}),c(a)),!0)})}function o(e,t,a){const i=new Proxy(e,{get(e,s){if("__subscribers"===s||"__isReactive"===s)return e[s];r&&(a.has(s)||a.set(s,new Set),a.get(s).add(r));const c=e[s];if("function"==typeof c)return c.bind(i);if(c&&"object"==typeof c){if(Array.isArray(c))return n(c,t,a);if(!c.__isReactive)return o(c,t,a)}return c},set:(e,r,n)=>(e[r]===n||(e[r]=n,a.has(r)&&a.get(r).forEach(e=>{s.add(e)}),c(t)),!0),deleteProperty:(e,r)=>(delete e[r],a.has(r)&&a.get(r).forEach(e=>{s.add(e)}),c(t),!0)});return i}function a(e,t){const s=new Map;let r;return r=Array.isArray(e)?n(e,t,s):o(e,t,s),r.__subscribers=s,r.__isReactive=!0,r}function i(e){r=e;try{e()}finally{r=null}}function c(e){t||(t=!0,queueMicrotask(()=>{t=!1;const r=[...s];s.clear(),r.forEach(e=>{try{i(e)}catch(e){console.error("[SenangStart] Effect error:",e)}}),e&&e()}))}function d(e,t,s){const{data:r,$refs:n,$store:o}=t,a={$data:r,$store:o,$el:s,$my:s,$refs:n,$dispatch:(e,t={})=>{s.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,cancelable:!0}))},$watch:(e,t)=>{const s=()=>{const s=r[e];t(s)};r.__subscribers&&(r.__subscribers.has(e)||r.__subscribers.set(e,new Set),r.__subscribers.get(e).add(s))},$nextTick:e=>{queueMicrotask(e)}},i=Object.keys("object"==typeof r&&null!==r?r:{}),c=Object.keys(a);try{const t=new Function(...i,...c,`with(this) { return (${e}); }`);return function(){const e=i.map(e=>r[e]),s=c.map(e=>a[e]);return t.call(r,...e,...s)}}catch(t){return console.error(`[SenangStart] Failed to parse expression: ${e}`,t),()=>{}}}function l(e,t,s){const{data:r,$refs:n,$store:o}=t,a={$data:r,$store:o,$el:s,$my:s,$refs:n,$dispatch:(e,t={})=>{s.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,cancelable:!0}))},$watch:(e,t)=>{r.__subscribers&&(r.__subscribers.has(e)||r.__subscribers.set(e,new Set),r.__subscribers.get(e).add(()=>t(r[e])))},$nextTick:e=>{queueMicrotask(e)}},i=Object.keys("object"==typeof r&&null!==r?r:{}),c=Object.keys(a);try{const t=new Function(...i,...c,`with(this) { ${e} }`);return function(){const e=i.map(e=>r[e]),s=c.map(e=>a[e]);return t.call(r,...e,...s)}}catch(t){return console.error(`[SenangStart] Failed to parse expression: ${e}`,t),()=>{}}}const u={"ss-text":(e,t,s)=>{i(()=>{const r=d(t,s,e);e.innerText=r()??""})},"ss-html":(e,t,s)=>{i(()=>{const r=d(t,s,e);e.innerHTML=r()??""})},"ss-show":(e,t,s)=>{const r=e.style.display||"";i(()=>{const n=!!d(t,s,e)();e.hasAttribute("ss-transition")?function(e,t,s){if(t){e.classList.add("ss-enter-from"),e.classList.add("ss-enter-active"),e.style.display=s,requestAnimationFrame(()=>{e.classList.remove("ss-enter-from"),e.classList.add("ss-enter-to")});const t=()=>{e.classList.remove("ss-enter-active","ss-enter-to"),e.removeEventListener("transitionend",t)};e.addEventListener("transitionend",t)}else{e.classList.add("ss-leave-from"),e.classList.add("ss-leave-active"),requestAnimationFrame(()=>{e.classList.remove("ss-leave-from"),e.classList.add("ss-leave-to")});const t=()=>{e.style.display="none",e.classList.remove("ss-leave-active","ss-leave-to"),e.removeEventListener("transitionend",t)};e.addEventListener("transitionend",t)}}(e,n,r):e.style.display=n?r:"none"})},"ss-model":(e,t,s)=>{const{data:r}=s,n="checkbox"===e.type,o="radio"===e.type;e.tagName;i(()=>{const r=d(t,s,e)();n?e.checked=!!r:o?e.checked=e.value===r:e.value=r??""});const a=n||o?"change":"input";e.addEventListener(a,()=>{let s;if(n)s=e.checked;else if(o){if(!e.checked)return;s=e.value}else s=e.value;r[t]=s})},"ss-ref":(e,t,s)=>{s.$refs[t]=e},"ss-init":(e,t,s)=>{l(t,s,e)()},"ss-effect":(e,t,s)=>{i(()=>{l(t,s,e)()})}};function f(e,t,s,r){const n=t.replace("ss-bind:","");i(()=>{const t=d(s,r,e)();"class"===n?"string"==typeof t?e.className=t:"object"==typeof t&&Object.entries(t).forEach(([t,s])=>{e.classList.toggle(t,!!s)}):"style"===n?"string"==typeof t?e.style.cssText=t:"object"==typeof t&&Object.assign(e.style,t):!1===t||null==t?e.removeAttribute(n):!0===t?e.setAttribute(n,""):e.setAttribute(n,t)})}function p(e,t,s,r){const n=t.replace("ss-on:","").split("."),o=n[0],a=n.slice(1),i=l(s,r,e),c=t=>{if(a.includes("prevent")&&t.preventDefault(),a.includes("stop")&&t.stopPropagation(),!a.includes("self")||t.target===e){if(a.includes("once")&&e.removeEventListener(o,c),t instanceof KeyboardEvent){const e=t.key.toLowerCase(),s=["enter","escape","tab","space","up","down","left","right"];if(a.some(e=>s.includes(e))){const t={enter:"enter",escape:"escape",tab:"tab",space:" ",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright"};if(!a.some(s=>t[s]===e))return}}r.data.$event=t,i(),delete r.data.$event}};a.includes("window")?window.addEventListener(o,c):a.includes("document")?document.addEventListener(o,c):e.addEventListener(o,c)}let h=null;let y={},b={};function m(e,t=null){if(1!==e.nodeType)return;if(e.hasAttribute("ss-ignore"))return;let s=t;if(e.hasAttribute("ss-data")){const t=e.getAttribute("ss-data").trim();let r={};if(t)if(y[t])r=y[t]();else try{r=new Function(`return (${t})`)()}catch(e){console.error("[SenangStart] Failed to parse ss-data:",t,e)}s={data:a(r,()=>{}),$refs:{},$store:b},e.__ssScope=s}if(!s)return void Array.from(e.children).forEach(e=>m(e,null));if("TEMPLATE"===e.tagName&&e.hasAttribute("ss-for"))return void function(e,t,s){const r=t.match(/^\s*(?:\(([^,]+),\s*([^)]+)\)|([^\s]+))\s+in\s+(.+)$/);if(!r)return void console.error("[SenangStart] Invalid ss-for expression:",t);const n=r[1]||r[3],o=r[2]||"index",c=r[4],l=e.parentNode,u=document.createComment(`ss-for: ${t}`);l.insertBefore(u,e),e.remove();let f=[],p="";i(()=>{const t=d(c,s,e)()||[],r=JSON.stringify(t);r!==p&&(p=r,f.forEach(e=>e.remove()),f=[],t.forEach((t,r)=>{const i=e.content.cloneNode(!0),c=Array.from(i.childNodes).filter(e=>1===e.nodeType),d={data:a({...s.data,[n]:t,[o]:r},()=>{}),$refs:s.$refs,$store:s.$store,parentData:s.data};c.forEach(e=>{l.insertBefore(e,u),f.push(e),h&&h(e,d)})}))})}(e,e.getAttribute("ss-for"),s);if("TEMPLATE"===e.tagName&&e.hasAttribute("ss-if"))return void function(e,t,s){const r=e.parentNode,n=document.createComment(`ss-if: ${t}`);r.insertBefore(n,e),e.remove();let o=[];i(()=>{const a=!!d(t,s,e)();if(o.forEach(e=>e.remove()),o=[],a){const t=e.content.cloneNode(!0);Array.from(t.childNodes).filter(e=>1===e.nodeType).forEach(e=>{r.insertBefore(e,n),o.push(e),h&&h(e,s)})}})}(e,e.getAttribute("ss-if"),s);const r=Array.from(e.attributes);for(const t of r){const r=t.name,n=t.value;"ss-data"!==r&&"ss-describe"!==r&&(u[r]?u[r](e,n,s):r.startsWith("ss-bind:")?f(e,r,n,s):r.startsWith("ss-on:")&&p(e,r,n,s))}e.hasAttribute("ss-cloak")&&e.removeAttribute("ss-cloak"),Array.from(e.children).forEach(e=>m(e,s))}function v(){const e=new MutationObserver(e=>{for(const t of e)for(const e of t.addedNodes)if(1===e.nodeType){let t=e,s=null;for(;t.parentElement;)if(t=t.parentElement,t.__ssScope){s=t.__ssScope;break}m(e,s)}});return e.observe(document.body,{childList:!0,subtree:!0}),e}
2
+ /**
3
+ * SenangStart Actions v0.1.0
4
+ * Declarative UI framework for humans and AI agents
5
+ *
6
+ * @license MIT
7
+ * @author Bookklik
8
+ * @module senangstart-actions
9
+ */h=m;const _=document.createElement("style");_.textContent="[ss-cloak] { display: none !important; }",document.head.appendChild(_);const g={},$={};y=g,b=$;const E={data(e,t){return"function"!=typeof t?(console.error("[SenangStart] data() requires a factory function"),this):(g[e]=t,this)},store(e,t){return"object"!=typeof t?(console.error("[SenangStart] store() requires an object"),this):($[e]=a(t,()=>{}),this)},init(e=document.body){return m(e,null),this},start(){return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{this.init(),v()}):(this.init(),v()),this},version:"0.1.0"};return"undefined"!=typeof window&&(window.SenangStart=E),E.start(),E}();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@bookklik/senangstart-actions",
3
+ "version": "0.1.0",
4
+ "description": "Declarative UI framework for humans and AI agents",
5
+ "main": "dist/senangstart-actions.js",
6
+ "module": "dist/senangstart-actions.esm.js",
7
+ "unpkg": "dist/senangstart-actions.min.js",
8
+ "jsdelivr": "dist/senangstart-actions.min.js",
9
+ "type": "module",
10
+ "files": [
11
+ "src",
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "rollup -c",
16
+ "dev": "npx -y serve .",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "test:coverage": "vitest run --coverage",
20
+ "docs:dev": "vitepress dev docs",
21
+ "docs:build": "vitepress build docs",
22
+ "docs:preview": "vitepress preview docs"
23
+ },
24
+ "keywords": [
25
+ "javascript",
26
+ "framework",
27
+ "reactive",
28
+ "declarative",
29
+ "ai-friendly",
30
+ "lightweight"
31
+ ],
32
+ "author": "a-hakim",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/bookklik-technologies/senangstart-actions"
37
+ },
38
+ "devDependencies": {
39
+ "@rollup/plugin-terser": "^0.4.4",
40
+ "@vitest/coverage-v8": "^2.1.0",
41
+ "jsdom": "^25.0.0",
42
+ "rollup": "^4.9.0",
43
+ "vitepress": "^1.5.0",
44
+ "vitest": "^2.1.0"
45
+ }
46
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * SenangStart Actions - Expression Evaluator
3
+ * Safe evaluation of expressions within component scope
4
+ *
5
+ * @module evaluator
6
+ */
7
+
8
+ /**
9
+ * Create a function to evaluate an expression within a component scope
10
+ */
11
+ export function createEvaluator(expression, scope, element) {
12
+ const { data, $refs, $store } = scope;
13
+
14
+ // Magic properties
15
+ const magics = {
16
+ $data: data,
17
+ $store: $store,
18
+ $el: element,
19
+ $my: element,
20
+ $refs: $refs,
21
+ $dispatch: (name, detail = {}) => {
22
+ element.dispatchEvent(new CustomEvent(name, {
23
+ detail,
24
+ bubbles: true,
25
+ cancelable: true
26
+ }));
27
+ },
28
+ $watch: (prop, callback) => {
29
+ const watchEffect = () => {
30
+ const value = data[prop];
31
+ callback(value);
32
+ };
33
+ if (data.__subscribers) {
34
+ if (!data.__subscribers.has(prop)) {
35
+ data.__subscribers.set(prop, new Set());
36
+ }
37
+ data.__subscribers.get(prop).add(watchEffect);
38
+ }
39
+ },
40
+ $nextTick: (fn) => {
41
+ queueMicrotask(fn);
42
+ }
43
+ };
44
+
45
+ // Build the function with data properties spread as local variables
46
+ const dataKeys = Object.keys(typeof data === 'object' && data !== null ? data : {});
47
+ const magicKeys = Object.keys(magics);
48
+
49
+ try {
50
+ const fn = new Function(
51
+ ...dataKeys,
52
+ ...magicKeys,
53
+ `with(this) { return (${expression}); }`
54
+ );
55
+
56
+ return function() {
57
+ const dataValues = dataKeys.map(k => data[k]);
58
+ const magicValues = magicKeys.map(k => magics[k]);
59
+ return fn.call(data, ...dataValues, ...magicValues);
60
+ };
61
+ } catch (e) {
62
+ console.error(`[SenangStart] Failed to parse expression: ${expression}`, e);
63
+ return () => undefined;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Create a function to execute a statement (not return value)
69
+ */
70
+ export function createExecutor(expression, scope, element) {
71
+ const { data, $refs, $store } = scope;
72
+
73
+ const magics = {
74
+ $data: data,
75
+ $store: $store,
76
+ $el: element,
77
+ $my: element,
78
+ $refs: $refs,
79
+ $dispatch: (name, detail = {}) => {
80
+ element.dispatchEvent(new CustomEvent(name, {
81
+ detail,
82
+ bubbles: true,
83
+ cancelable: true
84
+ }));
85
+ },
86
+ $watch: (prop, callback) => {
87
+ if (data.__subscribers) {
88
+ if (!data.__subscribers.has(prop)) {
89
+ data.__subscribers.set(prop, new Set());
90
+ }
91
+ data.__subscribers.get(prop).add(() => callback(data[prop]));
92
+ }
93
+ },
94
+ $nextTick: (fn) => {
95
+ queueMicrotask(fn);
96
+ }
97
+ };
98
+
99
+ const dataKeys = Object.keys(typeof data === 'object' && data !== null ? data : {});
100
+ const magicKeys = Object.keys(magics);
101
+
102
+ try {
103
+ const fn = new Function(
104
+ ...dataKeys,
105
+ ...magicKeys,
106
+ `with(this) { ${expression} }`
107
+ );
108
+
109
+ return function() {
110
+ const dataValues = dataKeys.map(k => data[k]);
111
+ const magicValues = magicKeys.map(k => magics[k]);
112
+ return fn.call(data, ...dataValues, ...magicValues);
113
+ };
114
+ } catch (e) {
115
+ console.error(`[SenangStart] Failed to parse expression: ${expression}`, e);
116
+ return () => {};
117
+ }
118
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * SenangStart Actions - Attribute Handlers
3
+ * Handlers for basic ss-* attributes
4
+ *
5
+ * @module handlers/attributes
6
+ */
7
+
8
+ import { createEvaluator, createExecutor } from '../evaluator.js';
9
+ import { runEffect } from '../reactive.js';
10
+
11
+ /**
12
+ * Handle ss-transition animations
13
+ */
14
+ export function handleTransition(el, show, originalDisplay) {
15
+ if (show) {
16
+ // Enter transition
17
+ el.classList.add('ss-enter-from');
18
+ el.classList.add('ss-enter-active');
19
+ el.style.display = originalDisplay;
20
+
21
+ requestAnimationFrame(() => {
22
+ el.classList.remove('ss-enter-from');
23
+ el.classList.add('ss-enter-to');
24
+ });
25
+
26
+ const onEnd = () => {
27
+ el.classList.remove('ss-enter-active', 'ss-enter-to');
28
+ el.removeEventListener('transitionend', onEnd);
29
+ };
30
+ el.addEventListener('transitionend', onEnd);
31
+ } else {
32
+ // Leave transition
33
+ el.classList.add('ss-leave-from');
34
+ el.classList.add('ss-leave-active');
35
+
36
+ requestAnimationFrame(() => {
37
+ el.classList.remove('ss-leave-from');
38
+ el.classList.add('ss-leave-to');
39
+ });
40
+
41
+ const onEnd = () => {
42
+ el.style.display = 'none';
43
+ el.classList.remove('ss-leave-active', 'ss-leave-to');
44
+ el.removeEventListener('transitionend', onEnd);
45
+ };
46
+ el.addEventListener('transitionend', onEnd);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Attribute handlers map
52
+ */
53
+ export const attributeHandlers = {
54
+ /**
55
+ * ss-text: Set element's innerText
56
+ */
57
+ 'ss-text': (el, expr, scope) => {
58
+ const update = () => {
59
+ const evaluator = createEvaluator(expr, scope, el);
60
+ el.innerText = evaluator() ?? '';
61
+ };
62
+ runEffect(update);
63
+ },
64
+
65
+ /**
66
+ * ss-html: Set element's innerHTML
67
+ */
68
+ 'ss-html': (el, expr, scope) => {
69
+ const update = () => {
70
+ const evaluator = createEvaluator(expr, scope, el);
71
+ el.innerHTML = evaluator() ?? '';
72
+ };
73
+ runEffect(update);
74
+ },
75
+
76
+ /**
77
+ * ss-show: Toggle visibility
78
+ */
79
+ 'ss-show': (el, expr, scope) => {
80
+ const originalDisplay = el.style.display || '';
81
+
82
+ const update = () => {
83
+ const evaluator = createEvaluator(expr, scope, el);
84
+ const show = !!evaluator();
85
+
86
+ if (el.hasAttribute('ss-transition')) {
87
+ handleTransition(el, show, originalDisplay);
88
+ } else {
89
+ el.style.display = show ? originalDisplay : 'none';
90
+ }
91
+ };
92
+ runEffect(update);
93
+ },
94
+
95
+ /**
96
+ * ss-model: Two-way binding for inputs
97
+ */
98
+ 'ss-model': (el, expr, scope) => {
99
+ const { data } = scope;
100
+
101
+ // Determine input type
102
+ const isCheckbox = el.type === 'checkbox';
103
+ const isRadio = el.type === 'radio';
104
+ const isSelect = el.tagName === 'SELECT';
105
+
106
+ // Set initial value
107
+ const setInitialValue = () => {
108
+ const evaluator = createEvaluator(expr, scope, el);
109
+ const value = evaluator();
110
+
111
+ if (isCheckbox) {
112
+ el.checked = !!value;
113
+ } else if (isRadio) {
114
+ el.checked = el.value === value;
115
+ } else if (isSelect) {
116
+ el.value = value ?? '';
117
+ } else {
118
+ el.value = value ?? '';
119
+ }
120
+ };
121
+
122
+ runEffect(setInitialValue);
123
+
124
+ // Listen for changes
125
+ const eventType = isCheckbox || isRadio ? 'change' : 'input';
126
+ el.addEventListener(eventType, () => {
127
+ let newValue;
128
+
129
+ if (isCheckbox) {
130
+ newValue = el.checked;
131
+ } else if (isRadio) {
132
+ if (el.checked) newValue = el.value;
133
+ else return; // Don't update if not checked
134
+ } else {
135
+ newValue = el.value;
136
+ }
137
+
138
+ // Set the value on the data object
139
+ data[expr] = newValue;
140
+ });
141
+ },
142
+
143
+ /**
144
+ * ss-ref: Register element reference
145
+ */
146
+ 'ss-ref': (el, name, scope) => {
147
+ scope.$refs[name] = el;
148
+ },
149
+
150
+ /**
151
+ * ss-init: Run initialization code
152
+ */
153
+ 'ss-init': (el, expr, scope) => {
154
+ const executor = createExecutor(expr, scope, el);
155
+ executor();
156
+ },
157
+
158
+ /**
159
+ * ss-effect: Run reactive effect
160
+ */
161
+ 'ss-effect': (el, expr, scope) => {
162
+ const update = () => {
163
+ const executor = createExecutor(expr, scope, el);
164
+ executor();
165
+ };
166
+ runEffect(update);
167
+ }
168
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * SenangStart Actions - Bind Handler
3
+ * Handler for ss-bind:[attr] dynamic attribute binding
4
+ *
5
+ * @module handlers/bind
6
+ */
7
+
8
+ import { createEvaluator } from '../evaluator.js';
9
+ import { runEffect } from '../reactive.js';
10
+
11
+ /**
12
+ * Handle ss-bind:[attr] dynamically
13
+ */
14
+ export function handleBind(el, attrName, expr, scope) {
15
+ const attr = attrName.replace('ss-bind:', '');
16
+
17
+ const update = () => {
18
+ const evaluator = createEvaluator(expr, scope, el);
19
+ const value = evaluator();
20
+
21
+ if (attr === 'class') {
22
+ if (typeof value === 'string') {
23
+ el.className = value;
24
+ } else if (typeof value === 'object') {
25
+ // Object syntax: { 'class-name': condition }
26
+ Object.entries(value).forEach(([className, condition]) => {
27
+ el.classList.toggle(className, !!condition);
28
+ });
29
+ }
30
+ } else if (attr === 'style') {
31
+ if (typeof value === 'string') {
32
+ el.style.cssText = value;
33
+ } else if (typeof value === 'object') {
34
+ Object.assign(el.style, value);
35
+ }
36
+ } else if (value === false || value === null || value === undefined) {
37
+ el.removeAttribute(attr);
38
+ } else if (value === true) {
39
+ el.setAttribute(attr, '');
40
+ } else {
41
+ el.setAttribute(attr, value);
42
+ }
43
+ };
44
+
45
+ runEffect(update);
46
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * SenangStart Actions - Directive Handlers
3
+ * Handlers for ss-for and ss-if template directives
4
+ *
5
+ * @module handlers/directives
6
+ */
7
+
8
+ import { createEvaluator } from '../evaluator.js';
9
+ import { createReactive, runEffect } from '../reactive.js';
10
+
11
+ // Forward declaration - will be set by walker.js
12
+ let walkFn = null;
13
+
14
+ /**
15
+ * Set the walk function reference (to avoid circular imports)
16
+ */
17
+ export function setWalkFunction(fn) {
18
+ walkFn = fn;
19
+ }
20
+
21
+ /**
22
+ * Handle ss-for directive
23
+ */
24
+ export function handleFor(templateEl, expr, scope) {
25
+ // Parse expression: "item in items" or "(item, index) in items"
26
+ const match = expr.match(/^\s*(?:\(([^,]+),\s*([^)]+)\)|([^\s]+))\s+in\s+(.+)$/);
27
+ if (!match) {
28
+ console.error('[SenangStart] Invalid ss-for expression:', expr);
29
+ return;
30
+ }
31
+
32
+ const itemName = match[1] || match[3];
33
+ const indexName = match[2] || 'index';
34
+ const arrayExpr = match[4];
35
+
36
+ const parent = templateEl.parentNode;
37
+ const anchor = document.createComment(`ss-for: ${expr}`);
38
+ parent.insertBefore(anchor, templateEl);
39
+ templateEl.remove();
40
+
41
+ let currentNodes = [];
42
+ let lastItemsJSON = '';
43
+
44
+ const update = () => {
45
+ const evaluator = createEvaluator(arrayExpr, scope, templateEl);
46
+ const items = evaluator() || [];
47
+
48
+ // Check if items actually changed (shallow comparison)
49
+ const itemsJSON = JSON.stringify(items);
50
+ if (itemsJSON === lastItemsJSON) {
51
+ return; // No change, skip re-render
52
+ }
53
+ lastItemsJSON = itemsJSON;
54
+
55
+ // Remove old nodes
56
+ currentNodes.forEach(node => node.remove());
57
+ currentNodes = [];
58
+
59
+ // Create new nodes
60
+ items.forEach((item, index) => {
61
+ const clone = templateEl.content.cloneNode(true);
62
+ const nodes = Array.from(clone.childNodes).filter(n => n.nodeType === 1);
63
+
64
+ // Create child scope with item and index - use parent scope's data for non-item properties
65
+ const itemScope = {
66
+ data: createReactive({
67
+ ...scope.data,
68
+ [itemName]: item,
69
+ [indexName]: index
70
+ }, () => {}),
71
+ $refs: scope.$refs,
72
+ $store: scope.$store,
73
+ parentData: scope.data // Keep reference to parent data
74
+ };
75
+
76
+ nodes.forEach(node => {
77
+ parent.insertBefore(node, anchor);
78
+ currentNodes.push(node);
79
+ if (walkFn) walkFn(node, itemScope);
80
+ });
81
+ });
82
+ };
83
+
84
+ runEffect(update);
85
+ }
86
+
87
+ /**
88
+ * Handle ss-if directive
89
+ */
90
+ export function handleIf(templateEl, expr, scope) {
91
+ const parent = templateEl.parentNode;
92
+ const anchor = document.createComment(`ss-if: ${expr}`);
93
+ parent.insertBefore(anchor, templateEl);
94
+ templateEl.remove();
95
+
96
+ let currentNodes = [];
97
+
98
+ const update = () => {
99
+ const evaluator = createEvaluator(expr, scope, templateEl);
100
+ const condition = !!evaluator();
101
+
102
+ // Remove old nodes
103
+ currentNodes.forEach(node => node.remove());
104
+ currentNodes = [];
105
+
106
+ if (condition) {
107
+ const clone = templateEl.content.cloneNode(true);
108
+ const nodes = Array.from(clone.childNodes).filter(n => n.nodeType === 1);
109
+
110
+ nodes.forEach(node => {
111
+ parent.insertBefore(node, anchor);
112
+ currentNodes.push(node);
113
+ if (walkFn) walkFn(node, scope);
114
+ });
115
+ }
116
+ };
117
+
118
+ runEffect(update);
119
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * SenangStart Actions - Event Handler
3
+ * Handler for ss-on:[event] with modifiers
4
+ *
5
+ * @module handlers/events
6
+ */
7
+
8
+ import { createExecutor } from '../evaluator.js';
9
+
10
+ /**
11
+ * Handle ss-on:[event] dynamically
12
+ */
13
+ export function handleEvent(el, attrName, expr, scope) {
14
+ const parts = attrName.replace('ss-on:', '').split('.');
15
+ const eventName = parts[0];
16
+ const modifiers = parts.slice(1);
17
+
18
+ const executor = createExecutor(expr, scope, el);
19
+
20
+ const handler = (event) => {
21
+ // Handle modifiers
22
+ if (modifiers.includes('prevent')) event.preventDefault();
23
+ if (modifiers.includes('stop')) event.stopPropagation();
24
+ if (modifiers.includes('self') && event.target !== el) return;
25
+ if (modifiers.includes('once')) {
26
+ el.removeEventListener(eventName, handler);
27
+ }
28
+
29
+ // For keyboard events, check key modifiers
30
+ if (event instanceof KeyboardEvent) {
31
+ const key = event.key.toLowerCase();
32
+ const keyModifiers = ['enter', 'escape', 'tab', 'space', 'up', 'down', 'left', 'right'];
33
+ const hasKeyModifier = modifiers.some(m => keyModifiers.includes(m));
34
+
35
+ if (hasKeyModifier) {
36
+ const keyMap = {
37
+ 'enter': 'enter',
38
+ 'escape': 'escape',
39
+ 'tab': 'tab',
40
+ 'space': ' ',
41
+ 'up': 'arrowup',
42
+ 'down': 'arrowdown',
43
+ 'left': 'arrowleft',
44
+ 'right': 'arrowright'
45
+ };
46
+
47
+ const shouldFire = modifiers.some(m => keyMap[m] === key);
48
+ if (!shouldFire) return;
49
+ }
50
+ }
51
+
52
+ // Execute the expression with $event available
53
+ scope.data.$event = event;
54
+ executor();
55
+ delete scope.data.$event;
56
+ };
57
+
58
+ // Special window/document events
59
+ if (modifiers.includes('window')) {
60
+ window.addEventListener(eventName, handler);
61
+ } else if (modifiers.includes('document')) {
62
+ document.addEventListener(eventName, handler);
63
+ } else {
64
+ el.addEventListener(eventName, handler);
65
+ }
66
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SenangStart Actions - Handlers Index
3
+ * Re-exports all handler modules
4
+ *
5
+ * @module handlers
6
+ */
7
+
8
+ export { attributeHandlers, handleTransition } from './attributes.js';
9
+ export { handleBind } from './bind.js';
10
+ export { handleEvent } from './events.js';
11
+ export { handleFor, handleIf, setWalkFunction } from './directives.js';