fusee-framework 1.3.0 β†’ 1.4.1

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.3.0 β€” Signals-First JS Framework | Atomic Reactivity | Peak Performance**
2
+ **v1.4.1 β€” 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
 
@@ -0,0 +1,29 @@
1
+ import { defineComposable, assertSetupContext, signal, computed, onMount, onUnmount } from '../../framework/index.js'
2
+
3
+ // ─── counterLogic ─────────────────────────────────────────────────────────────
4
+ export const counterLogic = defineComposable((initial = 0, step = 1) => {
5
+ const count = signal(initial)
6
+ const doubled = computed(() => count() * 2)
7
+ const isEven = computed(() => count() % 2 === 0)
8
+
9
+ const increment = () => count(count() + step)
10
+ const decrement = () => count(count() - step)
11
+ const reset = () => count(initial)
12
+
13
+ return { count, doubled, isEven, increment, decrement, reset }
14
+ })
15
+
16
+ // ─── mouseTracker ─────────────────────────────────────────────────────────────
17
+ export const mouseTracker = defineComposable(() => {
18
+ assertSetupContext('mouseTracker')
19
+
20
+ const x = signal(0)
21
+ const y = signal(0)
22
+
23
+ const onMove = (e) => { x(e.clientX); y(e.clientY) }
24
+
25
+ onMount(() => window.addEventListener('mousemove', onMove))
26
+ onUnmount(() => window.removeEventListener('mousemove', onMove))
27
+
28
+ return { x, y }
29
+ })
@@ -0,0 +1,30 @@
1
+ import { defineComposable, assertSetupContext, signal, computed, onMount, onUnmount } from '../../framework/index.js'
2
+
3
+ export const counterLogic = defineComposable((initial: number = 0, step: number = 1) => {
4
+ const count = signal(initial)
5
+ const doubled = computed(() => count() * 2)
6
+ const isEven = computed(() => count() % 2 === 0)
7
+
8
+ const increment = () => count(count() + step)
9
+ const decrement = () => count(count() - step)
10
+ const reset = () => count(initial)
11
+
12
+ return { count, doubled, isEven, increment, decrement, reset }
13
+ })
14
+
15
+ export const mouseTracker = defineComposable(() => {
16
+ assertSetupContext('mouseTracker')
17
+
18
+ const x = signal(0)
19
+ const y = signal(0)
20
+
21
+ const onMove = (e: MouseEvent) => {
22
+ x(e.clientX)
23
+ y(e.clientY)
24
+ }
25
+
26
+ onMount(() => window.addEventListener('mousemove', onMove))
27
+ onUnmount(() => window.removeEventListener('mousemove', onMove))
28
+
29
+ return { x, y }
30
+ })
@@ -0,0 +1,53 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ /**
4
+ * @template T
5
+ * @param {string | (() => string)} urlOrSignal
6
+ * @param {{ immediate?: boolean, transform?: (raw: any) => T }} [options]
7
+ */
8
+ export const useFetch = defineComposable((urlOrSignal, options = {}) => {
9
+ const { immediate = true, transform = null } = options
10
+
11
+ const data = signal(null)
12
+ const loading = signal(false)
13
+ const error = signal(null)
14
+
15
+ let controller = null
16
+
17
+ const execute = () => {
18
+ const url = typeof urlOrSignal === 'function' ? urlOrSignal() : urlOrSignal
19
+ if (!url) return
20
+
21
+ if (controller) controller.abort()
22
+ controller = new AbortController()
23
+
24
+ loading(true)
25
+ error(null)
26
+
27
+ fetch(url, { signal: controller.signal })
28
+ .then(res => {
29
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
30
+ return res.json()
31
+ })
32
+ .then(raw => {
33
+ data(transform ? transform(raw) : raw)
34
+ loading(false)
35
+ })
36
+ .catch(err => {
37
+ if (err.name === 'AbortError') return
38
+ error(err.message ?? String(err))
39
+ loading(false)
40
+ })
41
+ }
42
+
43
+ if (typeof urlOrSignal === 'function') {
44
+ effect(() => {
45
+ urlOrSignal()
46
+ execute()
47
+ })
48
+ } else if (immediate) {
49
+ execute()
50
+ }
51
+
52
+ return { data, loading, error, refetch: execute }
53
+ })
@@ -0,0 +1,63 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ export interface FetchOptions<T> {
4
+ immediate?: boolean
5
+ transform?: (raw: any) => T
6
+ }
7
+
8
+ export interface FetchResult<T> {
9
+ data: Signal<T | null>
10
+ loading: Signal<boolean>
11
+ error: Signal<string | null>
12
+ refetch: () => void
13
+ }
14
+
15
+ export const useFetch = defineComposable(<T = any>(
16
+ urlOrSignal: string | (() => string),
17
+ options: FetchOptions<T> = {}
18
+ ): FetchResult<T> => {
19
+ const { immediate = true, transform = null } = options
20
+
21
+ const data = signal<T | null>(null)
22
+ const loading = signal(false)
23
+ const error = signal<string | null>(null)
24
+
25
+ let controller: AbortController | null = null
26
+
27
+ const execute = () => {
28
+ const url = typeof urlOrSignal === 'function' ? urlOrSignal() : urlOrSignal
29
+ if (!url) return
30
+
31
+ if (controller) controller.abort()
32
+ controller = new AbortController()
33
+
34
+ loading(true)
35
+ error(null)
36
+
37
+ fetch(url, { signal: controller.signal })
38
+ .then(res => {
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
40
+ return res.json()
41
+ })
42
+ .then(raw => {
43
+ data(transform ? transform(raw) : raw)
44
+ loading(false)
45
+ })
46
+ .catch(err => {
47
+ if (err.name === 'AbortError') return
48
+ error(err.message ?? String(err))
49
+ loading(false)
50
+ })
51
+ }
52
+
53
+ if (typeof urlOrSignal === 'function') {
54
+ effect(() => {
55
+ urlOrSignal()
56
+ execute()
57
+ })
58
+ } else if (immediate) {
59
+ execute()
60
+ }
61
+
62
+ return { data, loading, error, refetch: execute }
63
+ })
@@ -0,0 +1,31 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ /**
4
+ * @template T
5
+ * @param {string} key
6
+ * @param {T} defaultValue
7
+ */
8
+ export const useLocalStorage = defineComposable((key, defaultValue = null) => {
9
+ const stored = localStorage.getItem(key)
10
+ let initial
11
+
12
+ try {
13
+ initial = stored !== null ? JSON.parse(stored) : defaultValue
14
+ } catch {
15
+ initial = defaultValue
16
+ localStorage.removeItem(key)
17
+ }
18
+
19
+ const value = signal(initial)
20
+
21
+ effect(() => {
22
+ const current = value()
23
+ if (current === null || current === undefined) {
24
+ localStorage.removeItem(key)
25
+ } else {
26
+ localStorage.setItem(key, JSON.stringify(current))
27
+ }
28
+ })
29
+
30
+ return value
31
+ })
@@ -0,0 +1,26 @@
1
+ import { defineComposable, signal, effect } from '../../framework/index.js'
2
+
3
+ export const useLocalStorage = defineComposable(<T>(key: string, defaultValue: T | null = null): Signal<T | null> => {
4
+ const stored = localStorage.getItem(key)
5
+ let initial: T | null
6
+
7
+ try {
8
+ initial = stored !== null ? JSON.parse(stored) : defaultValue
9
+ } catch {
10
+ initial = defaultValue
11
+ localStorage.removeItem(key)
12
+ }
13
+
14
+ const value = signal<T | null>(initial)
15
+
16
+ effect(() => {
17
+ const current = value()
18
+ if (current === null || current === undefined) {
19
+ localStorage.removeItem(key)
20
+ } else {
21
+ localStorage.setItem(key, JSON.stringify(current))
22
+ }
23
+ })
24
+
25
+ return value
26
+ })
package/app/main.js CHANGED
@@ -1,34 +1,57 @@
1
1
  import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
2
2
  import { Loading } from './components/Loading.js'
3
3
 
4
+ directive('focus', {
5
+ mounted(el) {
6
+ el.focus()
7
+ }
8
+ })
9
+
10
+ directive('highlight', {
11
+ mounted(el, binding) {
12
+ el.style.backgroundColor = binding.value || '#ffff0033'
13
+ el.style.transition = 'background-color 0.3s'
14
+ },
15
+ updated(el, binding) {
16
+ el.style.backgroundColor = binding.value || '#ffff0033'
17
+ }
18
+ })
19
+
4
20
  const routes = [
5
- {
6
- path: '/',
21
+ {
22
+ path: '/',
7
23
  component: defineAsyncComponent({
8
24
  loader: () => import('./routes/Home.js'),
9
25
  loadingComponent: Loading
10
- })
26
+ })
11
27
  },
12
- {
13
- path: '/about',
28
+ {
29
+ path: '/about',
14
30
  component: defineAsyncComponent({
15
31
  loader: () => import('./routes/About.js'),
16
32
  loadingComponent: Loading
17
- })
33
+ })
18
34
  },
19
- {
20
- path: '/test-for',
35
+ {
36
+ path: '/test-for',
21
37
  component: defineAsyncComponent({
22
38
  loader: () => import('./routes/TestFor.js'),
23
39
  loadingComponent: Loading
24
- })
40
+ })
25
41
  },
26
- {
27
- path: '/dynamic',
42
+ {
43
+ path: '/dynamic',
28
44
  component: defineAsyncComponent({
29
45
  loader: () => import('./routes/TestDynamic.js'),
30
46
  loadingComponent: Loading
31
- })
47
+ })
48
+ },
49
+ {
50
+ path: '/composables',
51
+ component: defineAsyncComponent({
52
+ loader: () => import('./routes/TestComposables.js'),
53
+ loadingComponent: Loading
54
+ })
32
55
  },
33
56
  ]
34
57
 
package/app/main.ts CHANGED
@@ -2,35 +2,58 @@ import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/in
2
2
  import type { Route } from '../framework/types/index.js'
3
3
  import { Loading } from './components/Loading.js'
4
4
 
5
+ directive('focus', {
6
+ mounted(el) {
7
+ el.focus()
8
+ }
9
+ })
10
+
11
+ directive('highlight', {
12
+ mounted(el, binding) {
13
+ el.style.backgroundColor = binding.value || '#ffff0033'
14
+ el.style.transition = 'background-color 0.3s'
15
+ },
16
+ updated(el, binding) {
17
+ el.style.backgroundColor = binding.value || '#ffff0033'
18
+ }
19
+ })
20
+
5
21
  const routes: Route[] = [
6
22
  {
7
23
  path: '/',
8
24
  component: defineAsyncComponent({
9
- loader: () => import('./routes/Home.js'),
25
+ loader: () => import('./routes/Home.ts'),
10
26
  loadingComponent: Loading
11
27
  })
12
28
  },
13
29
  {
14
30
  path: '/about',
15
31
  component: defineAsyncComponent({
16
- loader: () => import('./routes/About.js'),
32
+ loader: () => import('./routes/About.ts'),
17
33
  loadingComponent: Loading
18
34
  })
19
35
  },
20
36
  {
21
37
  path: '/test-for',
22
38
  component: defineAsyncComponent({
23
- loader: () => import('./routes/TestFor.js'),
39
+ loader: () => import('./routes/TestFor.ts'),
24
40
  loadingComponent: Loading
25
41
  })
26
42
  },
27
43
  {
28
44
  path: '/dynamic',
29
45
  component: defineAsyncComponent({
30
- loader: () => import('./routes/TestDynamic.js'),
46
+ loader: () => import('./routes/TestDynamic.ts'),
31
47
  loadingComponent: Loading
32
48
  })
33
49
  },
50
+ {
51
+ path: '/composables',
52
+ component: defineAsyncComponent({
53
+ loader: () => import('./routes/TestComposables.ts'),
54
+ loadingComponent: Loading
55
+ })
56
+ },
34
57
  ]
35
58
 
36
59
  createRouter(routes)
@@ -0,0 +1,158 @@
1
+ import { counterLogic, mouseTracker } from '../composables/demo.js'
2
+ import { useFetch } from '../composables/useFetch.js'
3
+ import { useLocalStorage } from '../composables/useLocalStorage.js'
4
+
5
+ export const TestComposables = defineComponent({
6
+ setup() {
7
+ // ── 1. counterLogic β€” pure signal composable, no context needed ──────
8
+ const counter = counterLogic(0, 1)
9
+ const bigCounter = counterLogic(100, 10)
10
+
11
+ // ── 2. mouseTracker β€” lifecycle composable (needs setup context) ─────
12
+ const mouse = mouseTracker()
13
+
14
+ // ── 3. useLocalStorage β€” persists across page reloads ────────────────
15
+ const theme = useLocalStorage('demo-theme', 'dark')
16
+ const nickname = useLocalStorage('demo-nickname', '')
17
+
18
+ const toggleTheme = () => theme(theme() === 'dark' ? 'light' : 'dark')
19
+
20
+ // ── 4. useFetch β€” reactive data fetching ─────────────────────────────
21
+ const { data: posts, loading, error } = useFetch(
22
+ 'https://jsonplaceholder.typicode.com/posts?_limit=3'
23
+ )
24
+ // ── 5. Custom Directives Test ────────────────────────────────────────
25
+ const hColor = signal('#8b8bff33')
26
+ const colors = ['#8b8bff33', '#10b98133', '#f59e0b33', '#ef444433']
27
+ let colorIdx = 0
28
+ const nextColor = () => hColor(colors[++colorIdx % colors.length])
29
+
30
+ return {
31
+ hColor,
32
+ nextColor,
33
+
34
+ // counter
35
+ count: counter.count,
36
+ doubled: counter.doubled,
37
+ isEven: counter.isEven,
38
+ increment: counter.increment,
39
+ decrement: counter.decrement,
40
+ reset: counter.reset,
41
+
42
+ // big counter
43
+ bigCount: bigCounter.count,
44
+ bigIncrement: bigCounter.increment,
45
+ bigDecrement: bigCounter.decrement,
46
+
47
+ // mouse
48
+ mouseX: mouse.x,
49
+ mouseY: mouse.y,
50
+
51
+ // localStorage
52
+ theme,
53
+ nickname,
54
+ toggleTheme,
55
+
56
+ // fetch
57
+ posts,
58
+ loading,
59
+ error,
60
+
61
+ template: `
62
+ <div class="page">
63
+ <h1>🧩 Composables Demo</h1>
64
+ <p style="color:#888; margin-bottom: 2rem;">
65
+ Each section below is powered by a <code style="color:#ff3333">defineComposable()</code> function.
66
+ No naming rules β€” just reactive logic.
67
+ </p>
68
+
69
+ <!-- ── 1. counterLogic ─────────────────────────────────────── -->
70
+ <section class="demo-card">
71
+ <h2>πŸ“¦ counterLogic(0, step=1)</h2>
72
+ <p class="demo-desc">Pure signal composable β€” works without setup() context.</p>
73
+ <div class="counter-display">
74
+ <span class="big-num" :class="isEven ? 'even' : 'odd'">{{ count }}</span>
75
+ <span class="derived-label">Γ— 2 = {{ doubled }}</span>
76
+ </div>
77
+ <div class="btn-row">
78
+ <button @click="decrement" class="cmp-btn minus">βˆ’</button>
79
+ <button @click="reset" class="cmp-btn reset">β†Ί</button>
80
+ <button @click="increment" class="cmp-btn plus">+</button>
81
+ </div>
82
+ <p class="badge" :class="isEven ? 'badge-blue' : 'badge-red'">{{ isEven ? 'Even' : 'Odd' }}</p>
83
+ </section>
84
+
85
+ <!-- ── 1b. Second independent counter (step=10) ───────────── -->
86
+ <section class="demo-card">
87
+ <h2>πŸ“¦ counterLogic(100, step=10)</h2>
88
+ <p class="demo-desc">Same composable, different instance. Fully isolated state.</p>
89
+ <div class="counter-display">
90
+ <span class="big-num">{{ bigCount }}</span>
91
+ </div>
92
+ <div class="btn-row">
93
+ <button @click="bigDecrement" class="cmp-btn minus">βˆ’10</button>
94
+ <button @click="bigIncrement" class="cmp-btn plus">+10</button>
95
+ </div>
96
+ </section>
97
+
98
+ <!-- ── 2. mouseTracker ────────────────────────────────────── -->
99
+ <section class="demo-card">
100
+ <h2>πŸ–±οΈ mouseTracker()</h2>
101
+ <p class="demo-desc">Lifecycle composable β€” registers window listener on mount, cleans up on unmount.</p>
102
+ <div class="mouse-display">
103
+ <span>X: <strong>{{ mouseX }}</strong></span>
104
+ <span>Y: <strong>{{ mouseY }}</strong></span>
105
+ </div>
106
+ </section>
107
+
108
+ <!-- ── 3. useLocalStorage ─────────────────────────────────── -->
109
+ <section class="demo-card">
110
+ <h2>πŸ’Ύ useLocalStorage()</h2>
111
+ <p class="demo-desc">State persists across page reloads. Try changing the nickname, then refresh.</p>
112
+ <div style="margin-bottom: 12px;">
113
+ <input f-model="nickname"
114
+ class="cmp-input"
115
+ placeholder="Your nickname…" />
116
+ <p style="margin-top: 8px; color: #888; font-size: 0.85rem;">
117
+ Stored: <strong style="color:#ff3333">{{ nickname || '(empty)' }}</strong>
118
+ </p>
119
+ </div>
120
+ <button @click="toggleTheme" class="cmp-btn reset">
121
+ Theme: {{ theme }}
122
+ </button>
123
+ </section>
124
+
125
+ <!-- ── 4. useFetch ────────────────────────────────────────── -->
126
+ <section class="demo-card">
127
+ <h2>🌐 useFetch()</h2>
128
+ <p class="demo-desc">Fetches from JSONPlaceholder. Loading state handled reactively.</p>
129
+ <div f-if="loading">
130
+ <p style="color: #888;">⏳ Loading posts…</p>
131
+ </div>
132
+ <div f-elif="error">
133
+ <p style="color: #ff6b6b;">❌ {{ error }}</p>
134
+ </div>
135
+ <div f-else>
136
+ <div f-for="post in posts" style="margin-bottom: 10px; padding: 10px; background:#1a1a24; border-radius:6px; border-left: 3px solid #ff3333;">
137
+ <strong style="color:#ff9999">{{ post.title }}</strong>
138
+ <p style="margin-top: 4px; font-size: 0.8rem; color: #666;">{{ post.body }}</p>
139
+ </div>
140
+ </div>
141
+ </section>
142
+
143
+ <!-- ── 6. Custom Directives ────────────────────────────────────── -->
144
+ <section class="demo-card">
145
+ <h2>πŸ› οΈ Custom Directives</h2>
146
+ <p class="demo-desc">Testing <code>f-focus</code> and <code>f-highlight</code>.</p>
147
+
148
+ <div f-highlight="hColor" style="padding: 15px; border-radius: 8px; margin-bottom: 10px;">
149
+ <input f-focus class="cmp-input" placeholder="I have f-focus!" />
150
+ </div>
151
+
152
+ <button @click="nextColor" class="cmp-btn plus">Change Highlight Color</button>
153
+ </section>
154
+ </div>
155
+ `
156
+ }
157
+ }
158
+ })