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 +1 -1
- package/app/composables/demo.js +29 -0
- package/app/composables/demo.ts +30 -0
- package/app/composables/useFetch.js +53 -0
- package/app/composables/useFetch.ts +63 -0
- package/app/composables/useLocalStorage.js +31 -0
- package/app/composables/useLocalStorage.ts +26 -0
- package/app/main.js +35 -12
- package/app/main.ts +27 -4
- package/app/routes/TestComposables.js +158 -0
- package/app/routes/TestComposables.ts +158 -0
- package/app/routes/TestFor.js +35 -8
- package/app/routes/TestFor.ts +34 -8
- package/auto-imports.d.ts +1 -0
- package/framework/auto-import-preset.js +1 -0
- package/framework/core/component.js +7 -21
- package/framework/core/composable.js +27 -0
- package/framework/core/directives.js +69 -1
- package/framework/core/signal.js +92 -5
- package/framework/index.js +4 -2
- package/framework/router/router.js +4 -4
- package/framework/types/composable.d.ts +14 -0
- package/framework/types/directives.d.ts +15 -0
- package/framework/types/index.d.ts +5 -4
- package/framework/types/signal.d.ts +38 -3
- package/index.html +134 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# FusΓ©e Framework π
|
|
2
|
-
**v1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
})
|