fusee-framework 1.5.2 → 1.5.3

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.5.2 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.5.3 — 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
 
@@ -9,29 +9,67 @@ export const Home = defineComponent({
9
9
  setup() {
10
10
  const title = signal('Welcome!')
11
11
  inspect(title)
12
-
12
+
13
+ const isActive = signal(false)
14
+ const isDisabled = signal(false)
15
+ const isLarge = signal(false)
16
+ const basicClasses = computed(() => ({
17
+ active: isActive(),
18
+ disabled: isDisabled(),
19
+ 'is-large': isLarge()
20
+ }))
21
+
22
+ const toggleActive = () => isActive(!isActive())
23
+ const toggleDisabled = () => isDisabled(!isDisabled())
24
+ const toggleLarge = () => isLarge(!isLarge())
25
+
13
26
  return {
14
27
  title,
15
28
  changeTitle: () => title('Changed Title!'),
16
-
29
+
30
+ isActive,
31
+ isDisabled,
32
+ isLarge,
33
+ basicClasses,
34
+ toggleActive,
35
+ toggleDisabled,
36
+ toggleLarge,
37
+
17
38
  // 🧪 Emit Event Handlers
18
39
  handleCounterChange: (value) => console.log('📢 Counter changed to:', value),
19
40
  handleCounterReset: () => console.log('🔄 Counter reset requested!'),
20
41
  handleCustomEvent: (count, multiplier) => console.log('🎯 Custom event:', { count, multiplier }),
21
-
42
+
22
43
  // 🧪 Emit Test Handlers
23
44
  handleSimpleClick: () => console.log('✅ SIMPLE CLICK received from child!'),
24
45
  handleClickWithData: (count, label, timestamp) => console.log('✅ CLICK WITH DATA:', { count, label, timestamp }),
25
46
  handleObjectEvent: (data) => console.log('✅ OBJECT EVENT received:', data),
26
-
47
+
27
48
  template: `
28
49
  <div class="page">
29
50
  <h1>{{ title.toUpperCase() }}</h1>
30
51
  <p>This is an SPA built with my own JavaScript custom framework!</p>
31
52
  <p>Made with passion by Sebastian Șomu</p>
32
-
53
+
33
54
  <button @click="changeTitle" class="primary-btn" style="margin-bottom: 20px;">Change App Title</button>
34
-
55
+
56
+ <!-- 🎨 f-classList Demo -->
57
+ <h3 style="margin-top: 40px; margin-bottom: 20px; color: #f59e0b;">🎨 f-classList Demo</h3>
58
+ <div class="demo-box" f-classList="basicClasses">
59
+ Demo Box (hover me!)
60
+ </div>
61
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem;">
62
+ <button @click="toggleActive" :class="isActive ? 'btn-on' : 'btn-off'" style="padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer;">
63
+ {{ isActive ? '✓ Active' : '○ Active' }}
64
+ </button>
65
+ <button @click="toggleDisabled" :class="isDisabled ? 'btn-on' : 'btn-off'" style="padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer;">
66
+ {{ isDisabled ? '✓ Disabled' : '○ Disabled' }}
67
+ </button>
68
+ <button @click="toggleLarge" :class="isLarge ? 'btn-on' : 'btn-off'" style="padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer;">
69
+ {{ isLarge ? '✓ Large' : '○ Large' }}
70
+ </button>
71
+ </div>
72
+
35
73
  {{ UserForm }}
36
74
 
37
75
  {{ CounterSection @counter-change="handleCounterChange" @counter-reset="handleCounterReset" @custom-event="handleCustomEvent" :pageTitle="title" }}
@@ -4,26 +4,72 @@ import { CounterSection } from '../components/CounterSection.js'
4
4
  type HomeResult = {
5
5
  title: Signal<string>
6
6
  changeTitle: () => void
7
+ isActive: Signal<boolean>
8
+ isDisabled: Signal<boolean>
9
+ isLarge: Signal<boolean>
10
+ basicClasses: Computed<ClassListObject>
11
+ toggleActive: () => void
12
+ toggleDisabled: () => void
13
+ toggleLarge: () => void
7
14
  template: string
8
15
  }
9
16
 
17
+ type ClassListObject = Record<string, boolean>
18
+
10
19
  export const Home = defineComponent({
11
20
  components: { UserForm, CounterSection },
12
21
  setup(): HomeResult {
13
22
  const title = signal<string>('Welcome!')
23
+
24
+ const isActive = signal(false)
25
+ const isDisabled = signal(false)
26
+ const isLarge = signal(false)
27
+ const basicClasses = computed(() => ({
28
+ active: isActive(),
29
+ disabled: isDisabled(),
30
+ 'is-large': isLarge()
31
+ }))
32
+
33
+ const toggleActive = () => isActive(!isActive())
34
+ const toggleDisabled = () => isDisabled(!isDisabled())
35
+ const toggleLarge = () => isLarge(!isLarge())
36
+
14
37
  return {
15
38
  title,
16
39
  changeTitle: () => title('Changed Title!'),
40
+ isActive,
41
+ isDisabled,
42
+ isLarge,
43
+ basicClasses,
44
+ toggleActive,
45
+ toggleDisabled,
46
+ toggleLarge,
17
47
  template: `
18
48
  <div class="page">
19
49
  <h1>{{ title.toUpperCase() }}</h1>
20
50
  <p>This is an SPA built with my own (TypeScript!) custom framework!</p>
21
51
  <p>Made with passion by Sebastian Șomu</p>
22
-
52
+
23
53
  <button @click="changeTitle" class="primary-btn" style="margin-bottom: 20px;">Change App Title</button>
24
-
54
+
55
+ <h3 style="margin-top: 40px; margin-bottom: 20px; color: #f59e0b;">🎨 f-classList Demo</h3>
56
+ <div class="demo-box" f-classList="basicClasses">
57
+ Demo Box (hover me!)
58
+ </div>
59
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem;">
60
+ <button @click="toggleActive" :class="isActive ? 'btn-on' : 'btn-off'" style="padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer;">
61
+ {{ isActive ? '✓ Active' : '○ Active' }}
62
+ </button>
63
+ <button @click="toggleDisabled" :class="isDisabled ? 'btn-on' : 'btn-off'" style="padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer;">
64
+ {{ isDisabled ? '✓ Disabled' : '○ Disabled' }}
65
+ </button>
66
+ <button @click="toggleLarge" :class="isLarge ? 'btn-on' : 'btn-off'" style="padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer;">
67
+ {{ isLarge ? '✓ Large' : '○ Large' }}
68
+ </button>
69
+ </div>
70
+
25
71
  {{ UserForm }}
26
-
72
+
27
73
  {{ CounterSection :pageTitle="title" }}
28
74
  </div>
29
75
  `
@@ -81,6 +81,7 @@ export function processDirectives(root, context, components, effects) {
81
81
  processText(root, context, effects)
82
82
  processEvents(root, context, effects)
83
83
  processCustomDirectives(root, context, effects)
84
+ processClassList(root, context, effects)
84
85
  processCloak(root)
85
86
 
86
87
  return false
@@ -589,3 +590,43 @@ export function processCloak(el) {
589
590
  const els = el.querySelectorAll('[f-cloak]')
590
591
  for (const e of els) e.removeAttribute('f-cloak')
591
592
  }
593
+
594
+ export function processClassList(el, context, effects) {
595
+ if (el.nodeType !== 1 || !el.hasAttribute('f-classList')) return
596
+
597
+ const expr = el.getAttribute('f-classList')
598
+ el.removeAttribute('f-classList')
599
+
600
+ const appliedClasses = new Set()
601
+
602
+ const e = effect(() => {
603
+ const result = evaluateExpression(expr, context)
604
+
605
+ if (!result || typeof result !== 'object') {
606
+ console.warn(`[framework] f-classList must evaluate to an object, got: ${typeof result}`)
607
+ return
608
+ }
609
+
610
+ for (const [className, shouldApply] of Object.entries(result)) {
611
+ const isApplied = appliedClasses.has(className)
612
+
613
+ if (shouldApply && !isApplied) {
614
+ el.classList.add(className)
615
+ appliedClasses.add(className)
616
+ } else if (!shouldApply && isApplied) {
617
+ el.classList.remove(className)
618
+ appliedClasses.delete(className)
619
+ }
620
+ }
621
+
622
+ const currentClasses = new Set(Object.keys(result))
623
+ for (const className of [...appliedClasses]) {
624
+ if (!currentClasses.has(className)) {
625
+ el.classList.remove(className)
626
+ appliedClasses.delete(className)
627
+ }
628
+ }
629
+ })
630
+
631
+ effects.push(e)
632
+ }
@@ -61,6 +61,7 @@ export function signal(initialValue) {
61
61
  export function effect(fn) {
62
62
  let active = true
63
63
  let running = false
64
+ const nestedEffects = new Set()
64
65
 
65
66
  const run = () => {
66
67
  if (!active || running) {
@@ -69,14 +70,28 @@ export function effect(fn) {
69
70
  }
70
71
 
71
72
  running = true
73
+
74
+ for (const childCleanup of nestedEffects) {
75
+ childCleanup()
76
+ }
77
+ nestedEffects.clear()
78
+
72
79
  for (const dep of run.deps) dep.delete(run)
73
80
  run.deps.clear()
74
81
 
75
82
  const prevEffect = currentEffect
83
+ const prevOnEffectCreated = onEffectCreated
84
+
85
+ onEffectCreated = (childCleanup) => {
86
+ nestedEffects.add(childCleanup)
87
+ if (prevOnEffectCreated) prevOnEffectCreated(childCleanup)
88
+ }
89
+
76
90
  currentEffect = run
77
91
  try { fn() }
78
92
  finally {
79
93
  currentEffect = prevEffect
94
+ onEffectCreated = prevOnEffectCreated
80
95
  running = false
81
96
  }
82
97
  }
@@ -88,6 +103,12 @@ export function effect(fn) {
88
103
  pendingEffects.delete(run)
89
104
  for (const dep of run.deps) dep.delete(run)
90
105
  run.deps.clear()
106
+
107
+ // Cleanup all nested effects when parent stops
108
+ for (const childCleanup of nestedEffects) {
109
+ childCleanup()
110
+ }
111
+ nestedEffects.clear()
91
112
  }
92
113
 
93
114
  if (onEffectCreated) onEffectCreated(cleanup)
@@ -36,6 +36,7 @@ export function processShow(root: HTMLElement, context: ComponentResult, effects
36
36
  export function processHtml(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
37
37
  export function processText(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
38
38
  export function processEvents(root: HTMLElement, context: ComponentResult): void
39
+ export function processClassList(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
39
40
  export function processCloak(root: HTMLElement): void
40
41
 
41
42
  export interface DirectiveBinding<V = any> {
package/index.html CHANGED
@@ -293,6 +293,259 @@
293
293
  .cmp-input:focus {
294
294
  border-color: #8b8bff;
295
295
  }
296
+
297
+ /* ── f-classList demo styles (Home page) ─────────────────────── */
298
+ .demo-box {
299
+ width: 200px;
300
+ height: 100px;
301
+ background: #1a1a24;
302
+ border: 2px solid #3a3a5a;
303
+ border-radius: 8px;
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: center;
307
+ font-weight: 600;
308
+ transition: all 0.3s ease;
309
+ margin-top: 1rem;
310
+ }
311
+
312
+ .demo-box.active {
313
+ border-color: #8b8bff;
314
+ background: rgba(139, 139, 255, 0.1);
315
+ box-shadow: 0 0 20px rgba(139, 139, 255, 0.3);
316
+ }
317
+
318
+ .demo-box.disabled {
319
+ opacity: 0.5;
320
+ cursor: not-allowed;
321
+ }
322
+
323
+ .demo-box.is-large {
324
+ width: 300px;
325
+ height: 150px;
326
+ font-size: 1.2rem;
327
+ }
328
+
329
+ .btn-on {
330
+ background: #8b8bff !important;
331
+ border-color: #8b8bff !important;
332
+ color: #fff !important;
333
+ }
334
+
335
+ .btn-off {
336
+ background: #1a1a24 !important;
337
+ border-color: #555 !important;
338
+ color: #888 !important;
339
+ }
340
+
341
+ .test-classlist h1 {
342
+ color: #f59e0b;
343
+ text-shadow: 0 0 20px rgba(245, 158, 11, 0.3);
344
+ }
345
+
346
+ .test-classlist .subtitle {
347
+ color: #aaa;
348
+ font-size: 0.9rem;
349
+ margin-bottom: 2rem;
350
+ }
351
+
352
+ .demo-section {
353
+ background: #13131d;
354
+ border: 1px solid #2a2a3a;
355
+ border-radius: 12px;
356
+ padding: 1.5rem;
357
+ margin-bottom: 1.5rem;
358
+ }
359
+
360
+ .demo-section h2 {
361
+ font-size: 1rem;
362
+ font-weight: 600;
363
+ color: #e8e8f0;
364
+ margin-bottom: 1rem;
365
+ }
366
+
367
+ .demo-box {
368
+ width: 200px;
369
+ height: 100px;
370
+ background: #1a1a24;
371
+ border: 2px solid #3a3a5a;
372
+ border-radius: 8px;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ font-weight: 600;
377
+ transition: all 0.3s ease;
378
+ margin-bottom: 1rem;
379
+ }
380
+
381
+ .demo-box.active {
382
+ border-color: #8b8bff;
383
+ background: rgba(139, 139, 255, 0.1);
384
+ box-shadow: 0 0 20px rgba(139, 139, 255, 0.3);
385
+ }
386
+
387
+ .demo-box.disabled {
388
+ opacity: 0.5;
389
+ cursor: not-allowed;
390
+ }
391
+
392
+ .demo-box.is-large {
393
+ width: 300px;
394
+ height: 150px;
395
+ font-size: 1.2rem;
396
+ }
397
+
398
+ .controls {
399
+ display: flex;
400
+ gap: 0.5rem;
401
+ flex-wrap: wrap;
402
+ }
403
+
404
+ .controls button {
405
+ padding: 0.5rem 1rem;
406
+ border: 1px solid #3a3a5a;
407
+ background: #0f0f1a;
408
+ color: #e8e8f0;
409
+ border-radius: 6px;
410
+ cursor: pointer;
411
+ transition: all 0.15s;
412
+ }
413
+
414
+ .controls button:hover {
415
+ background: #2a2a3a;
416
+ }
417
+
418
+ .controls button.btn-on {
419
+ background: #8b8bff;
420
+ border-color: #8b8bff;
421
+ color: #fff;
422
+ }
423
+
424
+ .controls button.btn-off {
425
+ background: #1a1a24;
426
+ border-color: #555;
427
+ color: #888;
428
+ }
429
+
430
+ .theme-box {
431
+ padding: 1.5rem;
432
+ border-radius: 8px;
433
+ border: 2px solid #3a3a5a;
434
+ transition: all 0.3s ease;
435
+ margin-bottom: 1rem;
436
+ }
437
+
438
+ .theme-box.theme-light {
439
+ background: rgba(245, 158, 11, 0.1);
440
+ border-color: #f59e0b;
441
+ color: #f59e0b;
442
+ }
443
+
444
+ .theme-box.theme-dark {
445
+ background: rgba(139, 139, 255, 0.1);
446
+ border-color: #8b8bff;
447
+ color: #8b8bff;
448
+ }
449
+
450
+ .theme-toggle {
451
+ padding: 0.6rem 1.25rem;
452
+ background: #f59e0b;
453
+ color: #000;
454
+ border: none;
455
+ border-radius: 6px;
456
+ font-weight: 600;
457
+ cursor: pointer;
458
+ transition: background 0.15s;
459
+ }
460
+
461
+ .theme-toggle:hover {
462
+ background: #d97706;
463
+ }
464
+
465
+ .action-btn {
466
+ padding: 0.75rem 1.5rem;
467
+ border: 2px solid #3a3a5a;
468
+ background: #1a1a24;
469
+ color: #e8e8f0;
470
+ border-radius: 8px;
471
+ font-weight: 600;
472
+ cursor: pointer;
473
+ transition: all 0.3s ease;
474
+ margin-bottom: 1rem;
475
+ }
476
+
477
+ .action-btn.btn-primary {
478
+ border-color: #8b8bff;
479
+ color: #8b8bff;
480
+ }
481
+
482
+ .action-btn.btn-primary:hover {
483
+ background: rgba(139, 139, 255, 0.1);
484
+ }
485
+
486
+ .action-btn.btn-loading {
487
+ border-color: #f59e0b;
488
+ color: #f59e0b;
489
+ animation: pulse 1.5s infinite;
490
+ }
491
+
492
+ .action-btn.btn-error {
493
+ border-color: #ff3333;
494
+ background: rgba(255, 51, 51, 0.1);
495
+ color: #ff3333;
496
+ }
497
+
498
+ .action-btn.btn-active {
499
+ transform: scale(0.95);
500
+ }
501
+
502
+ @keyframes pulse {
503
+ 0%, 100% { opacity: 1; }
504
+ 50% { opacity: 0.5; }
505
+ }
506
+
507
+ .info-section {
508
+ background: #0f0f1a;
509
+ border: 1px solid #2a2a3a;
510
+ border-radius: 12px;
511
+ padding: 1.5rem;
512
+ margin-top: 2rem;
513
+ }
514
+
515
+ .info-section h2 {
516
+ color: #34d399;
517
+ margin-bottom: 1rem;
518
+ }
519
+
520
+ .info-section ul {
521
+ margin-left: 1.5rem;
522
+ margin-bottom: 1.5rem;
523
+ }
524
+
525
+ .info-section li {
526
+ color: #aaa;
527
+ margin-bottom: 0.5rem;
528
+ line-height: 1.5;
529
+ }
530
+
531
+ .info-section li strong {
532
+ color: #e8e8f0;
533
+ }
534
+
535
+ .code-example {
536
+ background: #1a1a24;
537
+ border: 1px solid #2a2a3a;
538
+ border-radius: 8px;
539
+ padding: 1rem;
540
+ }
541
+
542
+ .code-example pre {
543
+ color: #8b8bff;
544
+ font-family: 'Courier New', monospace;
545
+ font-size: 0.85rem;
546
+ margin: 0;
547
+ overflow-x: auto;
548
+ }
296
549
  </style>
297
550
  </head>
298
551
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusee-framework",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
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",