fusee-framework 1.2.1 → 1.4.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 +19 -9
- package/app/components/DynamicA.js +24 -0
- package/app/components/DynamicA.ts +24 -0
- package/app/components/DynamicB.js +24 -0
- package/app/components/DynamicB.ts +24 -0
- package/app/components/EmitTest.js +5 -6
- package/app/components/Loading.js +26 -0
- package/app/components/Loading.ts +26 -0
- 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 +58 -9
- package/app/main.ts +53 -7
- package/app/routes/TestComposables.js +158 -0
- package/app/routes/TestComposables.ts +158 -0
- package/app/routes/TestDynamic.js +40 -0
- package/app/routes/TestDynamic.ts +40 -0
- package/app/routes/TestFor.js +2 -3
- package/app/routes/TestFor.ts +2 -3
- package/auto-imports.d.ts +1 -0
- package/framework/auto-import-preset.js +1 -0
- package/framework/core/component.js +76 -21
- package/framework/core/composable.js +27 -0
- package/framework/core/directives.js +234 -6
- package/framework/core/signal.js +58 -0
- package/framework/index.js +4 -2
- package/framework/types/component.d.ts +11 -0
- package/framework/types/composable.d.ts +14 -0
- package/framework/types/directives.d.ts +22 -0
- package/framework/types/index.d.ts +5 -4
- package/framework/types/signal.d.ts +15 -2
- package/index.html +137 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Fusée Framework 🚀
|
|
2
|
-
**v1.
|
|
2
|
+
**v1.4.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
|
-
|
|
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="
|
|
58
|
-
<h2>
|
|
59
|
-
<
|
|
60
|
-
|
|
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
|
+
})
|
|
@@ -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,13 +1,62 @@
|
|
|
1
|
-
import { createRouter } from '../framework/index.js'
|
|
2
|
-
import {
|
|
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
|
-
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
|
|
20
|
+
const routes = [
|
|
21
|
+
{
|
|
22
|
+
path: '/',
|
|
23
|
+
component: defineAsyncComponent({
|
|
24
|
+
loader: () => import('./routes/Home.js'),
|
|
25
|
+
loadingComponent: Loading
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: '/about',
|
|
30
|
+
component: defineAsyncComponent({
|
|
31
|
+
loader: () => import('./routes/About.js'),
|
|
32
|
+
loadingComponent: Loading
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: '/test-for',
|
|
37
|
+
component: defineAsyncComponent({
|
|
38
|
+
loader: () => import('./routes/TestFor.js'),
|
|
39
|
+
loadingComponent: Loading
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: '/dynamic',
|
|
44
|
+
component: defineAsyncComponent({
|
|
45
|
+
loader: () => import('./routes/TestDynamic.js'),
|
|
46
|
+
loadingComponent: Loading
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: '/composables',
|
|
51
|
+
component: defineAsyncComponent({
|
|
52
|
+
loader: () => import('./routes/TestComposables.js'),
|
|
53
|
+
loadingComponent: Loading
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
createRouter(routes)
|
|
11
59
|
|
|
12
60
|
const outlet = document.getElementById('app')
|
|
61
|
+
if (!outlet) throw new Error('[app] #app element not found in DOM')
|
|
13
62
|
mountOutlet(outlet)
|
package/app/main.ts
CHANGED
|
@@ -1,13 +1,59 @@
|
|
|
1
|
-
import { createRouter,
|
|
1
|
+
import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
|
|
2
2
|
import type { Route } from '../framework/types/index.js'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { Loading } from './components/Loading.js'
|
|
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
|
+
})
|
|
6
20
|
|
|
7
21
|
const routes: Route[] = [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
{
|
|
23
|
+
path: '/',
|
|
24
|
+
component: defineAsyncComponent({
|
|
25
|
+
loader: () => import('./routes/Home.ts'),
|
|
26
|
+
loadingComponent: Loading
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: '/about',
|
|
31
|
+
component: defineAsyncComponent({
|
|
32
|
+
loader: () => import('./routes/About.ts'),
|
|
33
|
+
loadingComponent: Loading
|
|
34
|
+
})
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: '/test-for',
|
|
38
|
+
component: defineAsyncComponent({
|
|
39
|
+
loader: () => import('./routes/TestFor.ts'),
|
|
40
|
+
loadingComponent: Loading
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '/dynamic',
|
|
45
|
+
component: defineAsyncComponent({
|
|
46
|
+
loader: () => import('./routes/TestDynamic.ts'),
|
|
47
|
+
loadingComponent: Loading
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
path: '/composables',
|
|
52
|
+
component: defineAsyncComponent({
|
|
53
|
+
loader: () => import('./routes/TestComposables.ts'),
|
|
54
|
+
loadingComponent: Loading
|
|
55
|
+
})
|
|
56
|
+
},
|
|
11
57
|
]
|
|
12
58
|
|
|
13
59
|
createRouter(routes)
|