fusee-framework 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Fusée Framework 🚀
2
- **v1.2.1 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.3.0 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
3
3
 
4
4
  Fusée is a custom, high-performance fine-grained reactive JavaScript framework built for speed and simplicity. It features a recursive, non-greedy compiler, a signals-based reactivity engine, Dependency Injection for components and a comprehensive CLI for instant application scaffolding.
5
5
 
@@ -44,20 +44,29 @@ A typical Fusée project looks like this:
44
44
  ## 📝 Example Component
45
45
 
46
46
  ```javascript
47
- import { defineComponent, signal } from './framework/core/index.js';
48
-
49
- export const Counter = defineComponent({
47
+ export const Welcome = defineComponent({
50
48
  setup() {
49
+ const name = signal('World');
51
50
  const count = signal(0);
51
+ const double = computed(() => count() * 2);
52
+
52
53
  return {
54
+ name,
53
55
  count,
56
+ double,
54
57
  inc: () => count(count() + 1),
55
- dec: () => count(count() - 1),
56
58
  template: `
57
- <div class="counter-card">
58
- <h2>Counter: {{ count }}</h2>
59
- <button @click="dec">-</button>
60
- <button @click="inc">+</button>
59
+ <div class="card">
60
+ <h2>Hello, {{ name }}!</h2>
61
+ <input f-model="name" placeholder="Name" />
62
+
63
+ <button @click.debounce.300ms="inc">
64
+ Clicked {{ count }} times
65
+ </button>
66
+
67
+ <p f-if="count() > 0">
68
+ Double: <strong>{{ double }}</strong>
69
+ </p>
61
70
  </div>
62
71
  `
63
72
  };
@@ -71,5 +80,6 @@ export const Counter = defineComponent({
71
80
 
72
81
  - [ ] Global Store Management
73
82
  - [ ] Advanced file-based routing support
83
+ - [ ] Integrated lightweight compiler which transforms declarative HTML into optimized runtime instructions
74
84
 
75
85
  Built with ❤️ by me, Sebi Somu, a forward-thinking JavaScript Architect.
@@ -0,0 +1,24 @@
1
+ export const DynamicA = defineComponent({
2
+ props: ['title'],
3
+ setup(props) {
4
+ onMount(() => console.log('✅ DynamicA mounted'))
5
+ onUnmount(() => console.log('❌ DynamicA unmounted'))
6
+
7
+ const localValue = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localValue,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #3b82f6;">
14
+ <h3 style="color: #3b82f6;">Component A</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local state (persists with f-keepAlive):</label><br>
18
+ <input f-model="localValue" placeholder="Type something here..." style="background: #0f172a; color: white; border: 1px solid #3b82f6; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,24 @@
1
+ export const DynamicA = defineComponent({
2
+ props: ['title'],
3
+ setup(props: { title: string }) {
4
+ onMount(() => console.log('✅ DynamicA mounted'))
5
+ onUnmount(() => console.log('❌ DynamicA unmounted'))
6
+
7
+ const localValue = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localValue,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #3b82f6;">
14
+ <h3 style="color: #3b82f6;">Component A</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local state (persists with f-keepAlive):</label><br>
18
+ <input f-model="localValue" placeholder="Type something here..." style="background: #0f172a; color: white; border: 1px solid #3b82f6; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,24 @@
1
+ export const DynamicB = defineComponent({
2
+ props: ['title'],
3
+ setup(props) {
4
+ onMount(() => console.log('✅ DynamicB mounted'))
5
+ onUnmount(() => console.log('❌ DynamicB unmounted'))
6
+
7
+ const localInfo = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localInfo,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #ec4899;">
14
+ <h3 style="color: #ec4899;">Component B</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local info (B):</label><br>
18
+ <input f-model="localInfo" placeholder="B settings..." style="background: #1a1a1a; color: white; border: 1px solid #ec4899; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -0,0 +1,24 @@
1
+ export const DynamicB = defineComponent({
2
+ props: ['title'],
3
+ setup(props: { title: string }) {
4
+ onMount(() => console.log('✅ DynamicB mounted'))
5
+ onUnmount(() => console.log('❌ DynamicB unmounted'))
6
+
7
+ const localInfo = signal('')
8
+
9
+ return {
10
+ title: props.title,
11
+ localInfo,
12
+ template: `
13
+ <div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #ec4899;">
14
+ <h3 style="color: #ec4899;">Component B</h3>
15
+ <p>Prop from parent: <strong>{{ title }}</strong></p>
16
+ <div style="margin-top: 10px;">
17
+ <label>Local info (B):</label><br>
18
+ <input f-model="localInfo" placeholder="B settings..." style="background: #1a1a1a; color: white; border: 1px solid #ec4899; padding: 5px; width: 100%;">
19
+ </div>
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+ })
@@ -2,14 +2,13 @@ export const EmitTest = defineComponent({
2
2
  setup(props, { emit }) {
3
3
  const clickCount = signal(0)
4
4
  const lastEvent = signal('None')
5
-
5
+
6
6
  inspect(clickCount, lastEvent)
7
-
7
+
8
8
  return {
9
9
  clickCount,
10
10
  lastEvent,
11
-
12
- // Butoane pentru test emit
11
+
13
12
  emitSimple: () => {
14
13
  clickCount(clickCount() + 1)
15
14
  emit('simple-click')
@@ -22,7 +21,7 @@ export const EmitTest = defineComponent({
22
21
  lastEvent(`click-with-data: ${count}`)
23
22
  },
24
23
  emitObject: () => {
25
- const data = {
24
+ const data = {
26
25
  timestamp: Date.now(),
27
26
  message: 'Hello from child!',
28
27
  counter: clickCount() + 1
@@ -31,7 +30,7 @@ export const EmitTest = defineComponent({
31
30
  emit('object-event', data)
32
31
  lastEvent(`object-event: ${data.message}`)
33
32
  },
34
-
33
+
35
34
  template: `
36
35
  <div style="background: linear-gradient(135deg, #1e3a5f 0%, #0d1b2a 100%); border: 2px solid #4da6ff; border-radius: 12px; padding: 24px; margin: 20px 0; color: #fff;">
37
36
  <h3 style="margin: 0 0 16px 0; color: #4da6ff;">🧪 EMIT EVENTS TEST LAB</h3>
@@ -0,0 +1,26 @@
1
+ import { defineComponent } from '../../framework/index.js'
2
+
3
+ export const Loading = defineComponent({
4
+ setup() {
5
+ return {
6
+ template: `
7
+ <div style="display: flex; justify-content: center; align-items: center; padding: 2rem;">
8
+ <div style="
9
+ width: 40px;
10
+ height: 40px;
11
+ border: 4px solid #f3f3f3;
12
+ border-top: 4px solid #3b82f6;
13
+ border-radius: 50%;
14
+ animation: spin 1s linear infinite;
15
+ "></div>
16
+ <style>
17
+ @keyframes spin {
18
+ 0% { transform: rotate(0deg); }
19
+ 100% { transform: rotate(360deg); }
20
+ }
21
+ </style>
22
+ </div>
23
+ `
24
+ }
25
+ }
26
+ })
@@ -0,0 +1,26 @@
1
+ import { defineComponent } from '../../framework/index.js'
2
+
3
+ export const Loading = defineComponent({
4
+ setup() {
5
+ return {
6
+ template: `
7
+ <div style="display: flex; justify-content: center; align-items: center; padding: 2rem;">
8
+ <div style="
9
+ width: 40px;
10
+ height: 40px;
11
+ border: 4px solid #f3f3f3;
12
+ border-top: 4px solid #3b82f6;
13
+ border-radius: 50%;
14
+ animation: spin 1s linear infinite;
15
+ "></div>
16
+ <style>
17
+ @keyframes spin {
18
+ 0% { transform: rotate(0deg); }
19
+ 100% { transform: rotate(360deg); }
20
+ }
21
+ </style>
22
+ </div>
23
+ `
24
+ }
25
+ }
26
+ })
package/app/main.js CHANGED
@@ -1,13 +1,39 @@
1
- import { createRouter } from '../framework/index.js'
2
- import { Home } from './routes/Home.js'
3
- import { About } from './routes/About.js'
4
- import { TestFor } from './routes/TestFor.js'
1
+ import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
2
+ import { Loading } from './components/Loading.js'
5
3
 
6
- createRouter([
7
- { path: '/', component: Home },
8
- { path: '/about', component: About },
9
- { path: '/test-for', component: TestFor },
10
- ])
4
+ const routes = [
5
+ {
6
+ path: '/',
7
+ component: defineAsyncComponent({
8
+ loader: () => import('./routes/Home.js'),
9
+ loadingComponent: Loading
10
+ })
11
+ },
12
+ {
13
+ path: '/about',
14
+ component: defineAsyncComponent({
15
+ loader: () => import('./routes/About.js'),
16
+ loadingComponent: Loading
17
+ })
18
+ },
19
+ {
20
+ path: '/test-for',
21
+ component: defineAsyncComponent({
22
+ loader: () => import('./routes/TestFor.js'),
23
+ loadingComponent: Loading
24
+ })
25
+ },
26
+ {
27
+ path: '/dynamic',
28
+ component: defineAsyncComponent({
29
+ loader: () => import('./routes/TestDynamic.js'),
30
+ loadingComponent: Loading
31
+ })
32
+ },
33
+ ]
34
+
35
+ createRouter(routes)
11
36
 
12
37
  const outlet = document.getElementById('app')
38
+ if (!outlet) throw new Error('[app] #app element not found in DOM')
13
39
  mountOutlet(outlet)
package/app/main.ts CHANGED
@@ -1,13 +1,36 @@
1
- import { createRouter, provide, inject } from '../framework/index.js'
1
+ import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
2
2
  import type { Route } from '../framework/types/index.js'
3
- import { Home } from './routes/Home.js'
4
- import { About } from './routes/About.js'
5
- import { TestFor } from './routes/TestFor.js'
3
+ import { Loading } from './components/Loading.js'
6
4
 
7
5
  const routes: Route[] = [
8
- { path: '/', component: Home },
9
- { path: '/about', component: About },
10
- { path: '/test-for', component: TestFor },
6
+ {
7
+ path: '/',
8
+ component: defineAsyncComponent({
9
+ loader: () => import('./routes/Home.js'),
10
+ loadingComponent: Loading
11
+ })
12
+ },
13
+ {
14
+ path: '/about',
15
+ component: defineAsyncComponent({
16
+ loader: () => import('./routes/About.js'),
17
+ loadingComponent: Loading
18
+ })
19
+ },
20
+ {
21
+ path: '/test-for',
22
+ component: defineAsyncComponent({
23
+ loader: () => import('./routes/TestFor.js'),
24
+ loadingComponent: Loading
25
+ })
26
+ },
27
+ {
28
+ path: '/dynamic',
29
+ component: defineAsyncComponent({
30
+ loader: () => import('./routes/TestDynamic.js'),
31
+ loadingComponent: Loading
32
+ })
33
+ },
11
34
  ]
12
35
 
13
36
  createRouter(routes)
@@ -0,0 +1,40 @@
1
+ import { DynamicA } from '../components/DynamicA.js'
2
+ import { DynamicB } from '../components/DynamicB.js'
3
+
4
+ export const TestDynamic = defineComponent({
5
+ components: { DynamicA, DynamicB },
6
+ setup() {
7
+ const currentType = signal('DynamicA')
8
+ const sharedTitle = signal('Test Dinamic active')
9
+
10
+ return {
11
+ currentType,
12
+ sharedTitle,
13
+ toggle: () => {
14
+ currentType(currentType() === 'DynamicA' ? 'DynamicB' : 'DynamicA')
15
+ },
16
+ template: `
17
+ <div class="page">
18
+ <h1>Dynamic Components System (Vue-style)</h1>
19
+ <p>Using the custom <code>f-is</code> directive.</p>
20
+
21
+ <div style="margin: 20px 0; display: flex; gap: 10px; align-items: center;">
22
+ <button @click="toggle" class="primary-btn">Toggle Component</button>
23
+ <span>Active: <strong>{{ currentType }}</strong></span>
24
+ </div>
25
+
26
+ <input f-model="sharedTitle" style="margin-bottom: 20px; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #222; color: white; width: 100%;">
27
+
28
+ <hr style="border: 0; border-top: 1px solid #444; margin: 30px 0;">
29
+
30
+ <div f-is="currentType" f-keepAlive :title="sharedTitle"></div>
31
+
32
+ <p style="margin-top: 40px; color: #888; font-size: 0.9em;">
33
+ Note: The component above changes reactively when you click the button.
34
+ The <code>:title</code> prop is automatically passed to the active component.
35
+ </p>
36
+ </div>
37
+ `
38
+ }
39
+ }
40
+ })
@@ -0,0 +1,40 @@
1
+ import { DynamicA } from '../components/DynamicA.js'
2
+ import { DynamicB } from '../components/DynamicB.js'
3
+
4
+ export const TestDynamic = defineComponent({
5
+ components: { DynamicA, DynamicB },
6
+ setup() {
7
+ const currentType = signal('DynamicA')
8
+ const sharedTitle = signal('Dynamic Test Active')
9
+
10
+ return {
11
+ currentType,
12
+ sharedTitle,
13
+ toggle: () => {
14
+ currentType(currentType() === 'DynamicA' ? 'DynamicB' : 'DynamicA')
15
+ },
16
+ template: `
17
+ <div class="page">
18
+ <h1>Dynamic Components System (Vue-style)</h1>
19
+ <p>Using the custom <code>f-is</code> directive.</p>
20
+
21
+ <div style="margin: 20px 0; display: flex; gap: 10px; align-items: center;">
22
+ <button @click="toggle" class="primary-btn">Toggle Component</button>
23
+ <span>Active: <strong>{{ currentType }}</strong></span>
24
+ </div>
25
+
26
+ <input f-model="sharedTitle" style="margin-bottom: 20px; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #222; color: white; width: 100%;">
27
+
28
+ <hr style="border: 0; border-top: 1px solid #444; margin: 30px 0;">
29
+
30
+ <div f-is="currentType" :title="sharedTitle"></div>
31
+
32
+ <p style="margin-top: 40px; color: #888; font-size: 0.9em;">
33
+ Note: The component above changes reactively when you click the button.
34
+ The <code>:title</code> prop is automatically passed to the active component.
35
+ </p>
36
+ </div>
37
+ `
38
+ }
39
+ }
40
+ })
@@ -187,15 +187,84 @@ export function defineComponent(options) {
187
187
  }
188
188
 
189
189
  export function provide(key, value) {
190
- if(!currentInstance) return
190
+ if (!currentInstance) return
191
191
  currentInstance._provides[key] = value
192
192
  }
193
193
 
194
194
  export function inject(key) {
195
195
  let parent = currentInstance?._parent
196
- while(parent) {
196
+ while (parent) {
197
197
  if (key in parent._provides) return parent._provides[key];
198
198
  parent = parent._parent;
199
199
  }
200
200
  return null;
201
+ }
202
+
203
+ export function defineAsyncComponent(loaderOrOptions) {
204
+ const options = typeof loaderOrOptions === 'function'
205
+ ? { loader: loaderOrOptions }
206
+ : loaderOrOptions
207
+
208
+ return function AsyncComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
209
+ const instance = {
210
+ props,
211
+ _mountHooks: [],
212
+ _unmountHooks: [],
213
+ _effects: [],
214
+ _element: null,
215
+ _provides: {},
216
+ _parent: parent,
217
+ }
218
+
219
+ let childApi = null
220
+ let loadingApi = null
221
+ let isUnmounted = false
222
+
223
+ function render(container) {
224
+ instance._element = container
225
+
226
+ if (options.loadingComponent) {
227
+ loadingApi = options.loadingComponent({}, { parent })
228
+ loadingApi.render(container)
229
+ } else {
230
+ container.innerHTML = '<!-- async component boundary -->'
231
+ }
232
+
233
+ Promise.resolve(options.loader())
234
+ .then(comp => {
235
+ if (isUnmounted) return
236
+ let ComponentFn = comp.default || comp
237
+
238
+ if (typeof ComponentFn === 'object' && ComponentFn !== null) {
239
+ ComponentFn = Object.values(ComponentFn).find(v => typeof v === 'function') || ComponentFn
240
+ }
241
+
242
+ if (loadingApi) {
243
+ loadingApi.unmount()
244
+ loadingApi = null
245
+ }
246
+
247
+ childApi = ComponentFn(props, { listeners, slots, parent })
248
+
249
+ container.innerHTML = ''
250
+ childApi.render(container)
251
+ })
252
+ .catch(err => {
253
+ console.error('[framework] Failed to load async component:', err)
254
+ })
255
+
256
+ return instance
257
+ }
258
+
259
+ function unmount() {
260
+ isUnmounted = true
261
+ if (loadingApi) loadingApi.unmount()
262
+ if (childApi) childApi.unmount()
263
+ if (instance._element && !childApi && !loadingApi) {
264
+ instance._element.innerHTML = ''
265
+ }
266
+ }
267
+
268
+ return { render, unmount, instance }
269
+ }
201
270
  }
@@ -5,6 +5,7 @@ import { compileNode } from './compiler.js'
5
5
  export function processDirectives(root, context, components, effects) {
6
6
  if (processFor(root, context, components, effects)) return true
7
7
  if (processIf(root, context, components, effects)) return true
8
+ if (processIs(root, context, components, effects)) return true
8
9
 
9
10
  processShow(root, context, effects)
10
11
  processModel(root, context, effects)
@@ -275,6 +276,110 @@ export function processIf(el, context, components, effects) {
275
276
  return true
276
277
  }
277
278
 
279
+ export function processIs(el, context, components, effects) {
280
+ if (el.nodeType !== 1 || !el.hasAttribute('f-is')) return false
281
+
282
+ // 1. Prepare directive state
283
+ const expr = el.getAttribute('f-is')
284
+ const keepAlive = processKeepAlive(el)
285
+
286
+ el.removeAttribute('f-is')
287
+
288
+ const anchor = document.createComment('f-keepAlive-anchor')
289
+ el.parentNode.insertBefore(anchor, el)
290
+
291
+ // Keep a clone to reuse attributes (props/@events) for every dynamic instance
292
+ const templateNode = el.cloneNode(true)
293
+ el.remove()
294
+
295
+ const cache = new Map()
296
+ let activeKey = null
297
+ let activeCleanup = []
298
+
299
+ // 2. Reactive loop
300
+ effects.push(effect(() => {
301
+ const rawValue = evaluateExpression(expr, context)
302
+
303
+ // Skip if same component is already active
304
+ if (rawValue === activeKey) return
305
+
306
+ // 3. Handle exiting component
307
+ if (activeKey !== null) {
308
+ const currentEntry = cache.get(activeKey)
309
+ if (currentEntry) {
310
+ if (keepAlive) {
311
+ currentEntry.node.remove()
312
+ } else {
313
+ for (const cleanup of activeCleanup) if (typeof cleanup === 'function') cleanup()
314
+ currentEntry.node.remove()
315
+ cache.delete(activeKey)
316
+ activeCleanup = []
317
+ }
318
+ }
319
+ }
320
+
321
+ if (!rawValue) {
322
+ activeKey = null
323
+ return
324
+ }
325
+
326
+ // 4. Handle entering component
327
+ let entry = cache.get(rawValue)
328
+
329
+ if (entry && keepAlive) {
330
+ // Restore from cache
331
+ anchor.parentNode.insertBefore(entry.node, anchor.nextSibling)
332
+ activeCleanup = entry.cleanup
333
+ } else {
334
+ // Create new instance
335
+ let ComponentFn = typeof rawValue === 'string' ? components[rawValue] : rawValue
336
+
337
+ if (typeof ComponentFn !== 'function') {
338
+ console.warn(`[framework] f-is: Component "${rawValue}" not found or invalid.`)
339
+ return
340
+ }
341
+
342
+ const compEl = templateNode.cloneNode(true)
343
+ compEl._isDynamicComponent = true
344
+
345
+ const tempName = '__dynamic_component__'
346
+ compEl.setAttribute('data-component', tempName)
347
+
348
+ anchor.parentNode.insertBefore(compEl, anchor.nextSibling)
349
+
350
+ const childCleanup = []
351
+ const dynamicComponents = { ...components, [tempName]: ComponentFn }
352
+
353
+ // Trigger the internal compiler to bind props and events
354
+ compileNode(compEl, context, dynamicComponents, childCleanup)
355
+
356
+ entry = { node: compEl, cleanup: childCleanup }
357
+ cache.set(rawValue, entry)
358
+ activeCleanup = childCleanup
359
+ }
360
+
361
+ activeKey = rawValue
362
+ }))
363
+
364
+ // Cleanup all cached components when this directive is destroyed
365
+ effects.push(() => {
366
+ for (const entry of cache.values()) {
367
+ for (const c of entry.cleanup) if (typeof c === 'function') c()
368
+ entry.node.remove()
369
+ }
370
+ cache.clear()
371
+ })
372
+
373
+ return true
374
+ }
375
+
376
+ function processKeepAlive(el) {
377
+ if (el.nodeType !== 1) return false
378
+ const hasKeepAlive = el.hasAttribute('f-keepAlive')
379
+ if (hasKeepAlive) el.removeAttribute('f-keepAlive')
380
+ return hasKeepAlive
381
+ }
382
+
278
383
  export function processShow(el, context, effects) {
279
384
  if (el.nodeType !== 1 || !el.hasAttribute('f-show')) return
280
385
 
@@ -304,6 +409,7 @@ export function processHtml(el, context, effects) {
304
409
  el.innerHTML = String(val ?? '')
305
410
  }))
306
411
  }
412
+
307
413
  export function processEvents(el, context, effects) {
308
414
  if (el.nodeType !== 1 || !el.attributes) return
309
415
 
@@ -317,6 +423,39 @@ export function processEvents(el, context, effects) {
317
423
 
318
424
  el.removeAttribute(attr.name)
319
425
 
426
+ let timeoutId
427
+ let lastRun = 0
428
+ let throttleTimeoutId
429
+
430
+ let debounceMs = 0
431
+ let throttleMs = 0
432
+
433
+ const parseDuration = (idx) => {
434
+ const next = modifiers[idx + 1]
435
+ if (next && /^\d/.test(next)) {
436
+ if (next.endsWith('ms')) return parseInt(next) || 250
437
+ if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
438
+ }
439
+ return 250 // Default
440
+ }
441
+
442
+ const debounceIdx = modifiers.indexOf('debounce')
443
+ if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
444
+
445
+ const throttleIdx = modifiers.indexOf('throttle')
446
+ if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
447
+
448
+ const executeLogic = (e) => {
449
+ batch(() => {
450
+ const potentialFn = context[expr]
451
+ if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
452
+ potentialFn(e)
453
+ } else {
454
+ evaluateExpression(expr, context, { $event: e }, false)
455
+ }
456
+ })
457
+ }
458
+
320
459
  const handler = (e) => {
321
460
  if (modifiers.includes('prevent')) e.preventDefault()
322
461
  if (modifiers.includes('stop')) e.stopPropagation()
@@ -336,14 +475,33 @@ export function processEvents(el, context, effects) {
336
475
  }
337
476
  }
338
477
 
339
- batch(() => {
340
- const potentialFn = context[expr]
341
- if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
342
- potentialFn(e)
478
+ // Throttle logic (ensures execution at most once every X ms)
479
+ if (throttleMs > 0) {
480
+ const now = Date.now()
481
+ const remaining = throttleMs - (now - lastRun)
482
+
483
+ if (remaining <= 0) {
484
+ clearTimeout(throttleTimeoutId)
485
+ lastRun = now
486
+ executeLogic(e)
343
487
  } else {
344
- evaluateExpression(expr, context, { $event: e }, false)
488
+ // Standard throttle with trailing edge support
489
+ clearTimeout(throttleTimeoutId)
490
+ throttleTimeoutId = setTimeout(() => {
491
+ lastRun = Date.now()
492
+ executeLogic(e)
493
+ }, remaining)
345
494
  }
346
- })
495
+ return
496
+ }
497
+
498
+ // Debounce logic (waits for X ms of inactivity)
499
+ if (debounceMs > 0) {
500
+ clearTimeout(timeoutId)
501
+ timeoutId = setTimeout(() => executeLogic(e), debounceMs)
502
+ } else {
503
+ executeLogic(e)
504
+ }
347
505
  }
348
506
 
349
507
  const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : el)
@@ -356,6 +514,8 @@ export function processEvents(el, context, effects) {
356
514
  if (effects) {
357
515
  effects.push(() => {
358
516
  targetNode.removeEventListener(eventName, handler, options)
517
+ if (timeoutId) clearTimeout(timeoutId)
518
+ if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
359
519
  })
360
520
  }
361
521
  }
@@ -1,6 +1,6 @@
1
1
  // ─── Framework Public API ─────────────────────────────────────────────────────
2
2
  export { signal, computed, effect, batch, untrack, inspect } from './core/signal.js'
3
- export { defineComponent, onMount, onUnmount, parseSlots, provide, inject } from './core/component.js'
3
+ export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject } from './core/component.js'
4
4
  export { createRouter, navigate, mountOutlet } from './router/router.js'
5
5
  export { mountTemplate } from './core/compiler.js'
6
6
 
@@ -70,3 +70,14 @@ export declare function inject<T>(key: string): T | null
70
70
  export declare function defineComponent<TProps = EmptyProps>(
71
71
  options: ComponentOptions<TProps>
72
72
  ): ComponentFactory<TProps>
73
+
74
+ export type AsyncComponentLoader<TProps = ComponentProps> = () => Promise<ComponentFactory<TProps> | { default: ComponentFactory<TProps> } | Record<string, any>>
75
+
76
+ export type AsyncComponentOptions<TProps = ComponentProps> = {
77
+ loader: AsyncComponentLoader<TProps>
78
+ loadingComponent?: ComponentFactory<any>
79
+ }
80
+
81
+ export declare function defineAsyncComponent<TProps = ComponentProps>(
82
+ loaderOrOptions: AsyncComponentLoader<TProps> | AsyncComponentOptions<TProps>
83
+ ): ComponentFactory<TProps>
@@ -25,6 +25,13 @@ export function processIf(
25
25
  effects: (() => void)[]
26
26
  ): void
27
27
 
28
+ export function processIs(
29
+ root: HTMLElement,
30
+ context: ComponentResult,
31
+ components: Record<string, ComponentFactory>,
32
+ effects: (() => void)[]
33
+ ): void
34
+
28
35
  export function processShow(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
29
36
  export function processHtml(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
30
37
  export function processText(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
package/index.html CHANGED
@@ -167,7 +167,8 @@
167
167
  <nav>
168
168
  <a href="#/">Home</a>
169
169
  <a href="#/about">About</a>
170
- <a href="#/test-for">Test f-for</a>
170
+ <a href="#/test-for">Loops Test</a>
171
+ <a href="#/dynamic" style="color: #ec4899;">Dynamic Components</a>
171
172
  </nav>
172
173
  <div id="app"></div>
173
174
  <script type="module" src="./app/main.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusee-framework",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Fusée - Signals-first JS Framework | Peak Performance",
5
5
  "main": "framework/index.js",
6
6
  "types": "framework/types/index.d.ts",