fusee-framework 1.5.2 โ 1.6.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 +3 -2
- package/app/components/Counter.ts +1 -1
- package/app/main.ts +6 -6
- package/app/routes/Home.js +44 -6
- package/app/routes/Home.ts +49 -3
- package/framework/core/directives.js +198 -3
- package/framework/core/event-delegation.js +172 -0
- package/framework/core/signal.js +20 -0
- package/framework/types/component.d.ts +0 -4
- package/framework/types/directives.d.ts +1 -0
- package/framework/types/event-delegation.d.ts +90 -0
- package/framework/types/index.d.ts +3 -0
- package/index.html +316 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Fusรฉe Framework ๐
|
|
2
|
-
**v1.
|
|
2
|
+
**v1.6.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
|
|
|
@@ -80,7 +80,7 @@ export const Welcome = defineComponent({
|
|
|
80
80
|
|
|
81
81
|
Fusรฉe is built with a test-driven mindset to ensure the reliability of its reactivity engine and component lifecycle.
|
|
82
82
|
|
|
83
|
-
- **Total Tests:** `
|
|
83
|
+
- **Total Tests:** `385`
|
|
84
84
|
- **Status:** `All Passed`
|
|
85
85
|
- **Framework:** [Vitest](https://vitest.dev/)
|
|
86
86
|
- **Environment:** JSDOM (Browser simulation)
|
|
@@ -106,6 +106,7 @@ npx vitest
|
|
|
106
106
|
* **Component System:** Props, Slots, Async Components, and Lifecycle Hooks.
|
|
107
107
|
* **Dependency Injection:** Nested Provide/Inject and Shadowing.
|
|
108
108
|
* **Directives:** `f-if`, `f-for`, `f-model`, and Event Modifiers.
|
|
109
|
+
* **Events:** Event delegation and native event handling.
|
|
109
110
|
* **Memory Safety:** Explicit checks for memory leaks and automatic effect disposal.
|
|
110
111
|
* **Type Safety:** Comprehensive TypeScript definitions and type tests.
|
|
111
112
|
* **Integration Tests:** Advanced module combinations and edge cases.
|
|
@@ -23,7 +23,7 @@ export const Counter = defineComponent({
|
|
|
23
23
|
initialValue: { type: String, default: '0' },
|
|
24
24
|
parentTitle: { type: String, default: '' }
|
|
25
25
|
},
|
|
26
|
-
setup(props: CounterProps): CounterResult {
|
|
26
|
+
setup(props: CounterProps, { emit }: { emit: (eventName: string, ...args: any[]) => void }): CounterResult {
|
|
27
27
|
const count = signal<number>(Number(props.initialValue) || 0)
|
|
28
28
|
const multiplier = signal<number>(1)
|
|
29
29
|
inspect(count, multiplier)
|
package/app/main.ts
CHANGED
|
@@ -33,19 +33,19 @@ const routes: Route[] = [
|
|
|
33
33
|
loadingComponent: Loading
|
|
34
34
|
})
|
|
35
35
|
},
|
|
36
|
-
{
|
|
37
|
-
path: '/test-for',
|
|
36
|
+
{
|
|
37
|
+
path: '/test-for',
|
|
38
38
|
component: defineAsyncComponent({
|
|
39
39
|
loader: () => import('./routes/TestFor.ts'),
|
|
40
40
|
loadingComponent: Loading
|
|
41
|
-
})
|
|
41
|
+
})
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
path: '/dynamic',
|
|
43
|
+
{
|
|
44
|
+
path: '/dynamic',
|
|
45
45
|
component: defineAsyncComponent({
|
|
46
46
|
loader: () => import('./routes/TestDynamic.ts'),
|
|
47
47
|
loadingComponent: Loading
|
|
48
|
-
})
|
|
48
|
+
})
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
path: '/composables',
|
package/app/routes/Home.js
CHANGED
|
@@ -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" }}
|
package/app/routes/Home.ts
CHANGED
|
@@ -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
|
`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { signal, effect, batch } from './signal.js'
|
|
2
2
|
import { evaluateExpression } from './evaluator.js'
|
|
3
3
|
import { compileNode } from './compiler.js'
|
|
4
|
+
import { registerDelegatedEvent, createEventHandler, isDelegatedEvent } from './event-delegation.js'
|
|
4
5
|
|
|
5
6
|
const customDirectives = new Map()
|
|
6
7
|
|
|
@@ -81,6 +82,7 @@ export function processDirectives(root, context, components, effects) {
|
|
|
81
82
|
processText(root, context, effects)
|
|
82
83
|
processEvents(root, context, effects)
|
|
83
84
|
processCustomDirectives(root, context, effects)
|
|
85
|
+
processClassList(root, context, effects)
|
|
84
86
|
processCloak(root)
|
|
85
87
|
|
|
86
88
|
return false
|
|
@@ -468,8 +470,9 @@ export function processEvents(el, context, effects) {
|
|
|
468
470
|
if (el.nodeType !== 1 || !el.attributes) return
|
|
469
471
|
|
|
470
472
|
for (const attr of [...el.attributes]) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
+
// Handle native events: on:click, on:scroll, on:custom-event
|
|
474
|
+
if (attr.name.startsWith('on:')) {
|
|
475
|
+
const fullEventName = attr.name.slice(3) // Remove 'on:' prefix
|
|
473
476
|
const parts = fullEventName.split('.')
|
|
474
477
|
const eventName = parts[0]
|
|
475
478
|
const modifiers = parts.slice(1)
|
|
@@ -490,7 +493,7 @@ export function processEvents(el, context, effects) {
|
|
|
490
493
|
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
491
494
|
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
492
495
|
}
|
|
493
|
-
return 250
|
|
496
|
+
return 250
|
|
494
497
|
}
|
|
495
498
|
|
|
496
499
|
const debounceIdx = modifiers.indexOf('debounce')
|
|
@@ -568,6 +571,158 @@ export function processEvents(el, context, effects) {
|
|
|
568
571
|
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
569
572
|
})
|
|
570
573
|
}
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Handle delegated events: @click, @input, etc.
|
|
578
|
+
if (attr.name.startsWith('@')) {
|
|
579
|
+
const fullEventName = attr.name.slice(1) // Remove '@' prefix
|
|
580
|
+
const parts = fullEventName.split('.')
|
|
581
|
+
const eventName = parts[0]
|
|
582
|
+
const modifiers = parts.slice(1)
|
|
583
|
+
const expr = attr.value
|
|
584
|
+
|
|
585
|
+
el.removeAttribute(attr.name)
|
|
586
|
+
|
|
587
|
+
const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : null)
|
|
588
|
+
|
|
589
|
+
// Window/document events are always native (can't delegate to window/document from document)
|
|
590
|
+
if (targetNode) {
|
|
591
|
+
// Native handler for window/document events
|
|
592
|
+
const handler = createEventHandler(
|
|
593
|
+
typeof context[expr] === 'function' && !context[expr].isSignal
|
|
594
|
+
? context[expr]
|
|
595
|
+
: expr,
|
|
596
|
+
modifiers,
|
|
597
|
+
context,
|
|
598
|
+
expr
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
const options = {
|
|
602
|
+
once: modifiers.includes('once'),
|
|
603
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
targetNode.addEventListener(eventName, handler, options)
|
|
607
|
+
if (effects) {
|
|
608
|
+
effects.push(() => {
|
|
609
|
+
targetNode.removeEventListener(eventName, handler, options)
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
} else if (isDelegatedEvent(eventName) && !modifiers.includes('capture') && !modifiers.includes('away')) {
|
|
613
|
+
// Use event delegation for supported events
|
|
614
|
+
const potentialFn = context[expr]
|
|
615
|
+
const isFunction = typeof potentialFn === 'function' && !potentialFn.isSignal
|
|
616
|
+
|
|
617
|
+
const wrappedHandler = createEventHandler(
|
|
618
|
+
isFunction ? potentialFn : expr,
|
|
619
|
+
modifiers,
|
|
620
|
+
context,
|
|
621
|
+
expr
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, {
|
|
625
|
+
modifiers,
|
|
626
|
+
context,
|
|
627
|
+
expr
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
if (effects) {
|
|
631
|
+
effects.push(cleanup)
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
// Fallback to native for non-delegated events or when capture/away modifiers are used
|
|
635
|
+
let timeoutId
|
|
636
|
+
let lastRun = 0
|
|
637
|
+
let throttleTimeoutId
|
|
638
|
+
|
|
639
|
+
let debounceMs = 0
|
|
640
|
+
let throttleMs = 0
|
|
641
|
+
|
|
642
|
+
const parseDuration = (idx) => {
|
|
643
|
+
const next = modifiers[idx + 1]
|
|
644
|
+
if (next && /^\d/.test(next)) {
|
|
645
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
646
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
647
|
+
}
|
|
648
|
+
return 250
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
652
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
653
|
+
|
|
654
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
655
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
656
|
+
|
|
657
|
+
const executeLogic = (e) => {
|
|
658
|
+
batch(() => {
|
|
659
|
+
const potentialFn = context[expr]
|
|
660
|
+
if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
|
|
661
|
+
potentialFn(e)
|
|
662
|
+
} else {
|
|
663
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const handler = (e) => {
|
|
669
|
+
if (modifiers.includes('prevent')) e.preventDefault()
|
|
670
|
+
if (modifiers.includes('stop')) e.stopPropagation()
|
|
671
|
+
if (modifiers.includes('self') && e.target !== el) return
|
|
672
|
+
|
|
673
|
+
if (e instanceof KeyboardEvent) {
|
|
674
|
+
const key = e.key.toLowerCase()
|
|
675
|
+
const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
|
|
676
|
+
if (keyModifiers.length > 0) {
|
|
677
|
+
const match = keyModifiers.some(m => {
|
|
678
|
+
if (m === 'escape') return key === 'escape' || key === 'esc'
|
|
679
|
+
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
680
|
+
return key === m
|
|
681
|
+
})
|
|
682
|
+
if (!match) return
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (throttleMs > 0) {
|
|
687
|
+
const now = Date.now()
|
|
688
|
+
const remaining = throttleMs - (now - lastRun)
|
|
689
|
+
|
|
690
|
+
if (remaining <= 0) {
|
|
691
|
+
clearTimeout(throttleTimeoutId)
|
|
692
|
+
lastRun = now
|
|
693
|
+
executeLogic(e)
|
|
694
|
+
} else {
|
|
695
|
+
clearTimeout(throttleTimeoutId)
|
|
696
|
+
throttleTimeoutId = setTimeout(() => {
|
|
697
|
+
lastRun = Date.now()
|
|
698
|
+
executeLogic(e)
|
|
699
|
+
}, remaining)
|
|
700
|
+
}
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (debounceMs > 0) {
|
|
705
|
+
clearTimeout(timeoutId)
|
|
706
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
707
|
+
} else {
|
|
708
|
+
executeLogic(e)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const options = {
|
|
713
|
+
once: modifiers.includes('once'),
|
|
714
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
el.addEventListener(eventName, handler, options)
|
|
718
|
+
if (effects) {
|
|
719
|
+
effects.push(() => {
|
|
720
|
+
el.removeEventListener(eventName, handler, options)
|
|
721
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
722
|
+
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
}
|
|
571
726
|
}
|
|
572
727
|
}
|
|
573
728
|
}
|
|
@@ -589,3 +744,43 @@ export function processCloak(el) {
|
|
|
589
744
|
const els = el.querySelectorAll('[f-cloak]')
|
|
590
745
|
for (const e of els) e.removeAttribute('f-cloak')
|
|
591
746
|
}
|
|
747
|
+
|
|
748
|
+
export function processClassList(el, context, effects) {
|
|
749
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-classList')) return
|
|
750
|
+
|
|
751
|
+
const expr = el.getAttribute('f-classList')
|
|
752
|
+
el.removeAttribute('f-classList')
|
|
753
|
+
|
|
754
|
+
const appliedClasses = new Set()
|
|
755
|
+
|
|
756
|
+
const e = effect(() => {
|
|
757
|
+
const result = evaluateExpression(expr, context)
|
|
758
|
+
|
|
759
|
+
if (!result || typeof result !== 'object') {
|
|
760
|
+
console.warn(`[framework] f-classList must evaluate to an object, got: ${typeof result}`)
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
for (const [className, shouldApply] of Object.entries(result)) {
|
|
765
|
+
const isApplied = appliedClasses.has(className)
|
|
766
|
+
|
|
767
|
+
if (shouldApply && !isApplied) {
|
|
768
|
+
el.classList.add(className)
|
|
769
|
+
appliedClasses.add(className)
|
|
770
|
+
} else if (!shouldApply && isApplied) {
|
|
771
|
+
el.classList.remove(className)
|
|
772
|
+
appliedClasses.delete(className)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const currentClasses = new Set(Object.keys(result))
|
|
777
|
+
for (const className of [...appliedClasses]) {
|
|
778
|
+
if (!currentClasses.has(className)) {
|
|
779
|
+
el.classList.remove(className)
|
|
780
|
+
appliedClasses.delete(className)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
effects.push(e)
|
|
786
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { batch } from './signal.js'
|
|
2
|
+
import { evaluateExpression } from './evaluator.js'
|
|
3
|
+
|
|
4
|
+
export const DELEGATED_EVENTS = new Set([
|
|
5
|
+
'beforeinput',
|
|
6
|
+
'click',
|
|
7
|
+
'dblclick',
|
|
8
|
+
'contextmenu',
|
|
9
|
+
'focusin',
|
|
10
|
+
'focusout',
|
|
11
|
+
'input',
|
|
12
|
+
'keydown',
|
|
13
|
+
'keyup',
|
|
14
|
+
'mousedown',
|
|
15
|
+
'mousemove',
|
|
16
|
+
'mouseout',
|
|
17
|
+
'mouseover',
|
|
18
|
+
'mouseup',
|
|
19
|
+
'pointerdown',
|
|
20
|
+
'pointermove',
|
|
21
|
+
'pointerout',
|
|
22
|
+
'pointerover',
|
|
23
|
+
'pointerup',
|
|
24
|
+
'touchend',
|
|
25
|
+
'touchmove',
|
|
26
|
+
'touchstart'
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
const delegatedListeners = new Map()
|
|
30
|
+
const documentListeners = new Map()
|
|
31
|
+
|
|
32
|
+
export function isDelegatedEvent(eventName) {
|
|
33
|
+
return DELEGATED_EVENTS.has(eventName.toLowerCase())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDocumentListener(eventType) {
|
|
37
|
+
if (documentListeners.has(eventType)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handler = (e) => {
|
|
42
|
+
const listeners = delegatedListeners.get(eventType)
|
|
43
|
+
if (!listeners || listeners.size === 0) return
|
|
44
|
+
|
|
45
|
+
let target = e.target
|
|
46
|
+
while (target && target !== document.body && target !== document.documentElement) {
|
|
47
|
+
for (const entry of listeners) {
|
|
48
|
+
if (entry.element === target) {
|
|
49
|
+
entry.handler(e)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
target = target.parentNode
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.addEventListener(eventType, handler, true)
|
|
57
|
+
documentListeners.set(eventType, handler)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerDelegatedEvent(element, eventType, handler, options = {}) {
|
|
61
|
+
const { modifiers = [], context, expr } = options
|
|
62
|
+
const normalizedEvent = eventType.toLowerCase()
|
|
63
|
+
|
|
64
|
+
ensureDocumentListener(normalizedEvent)
|
|
65
|
+
|
|
66
|
+
if (!delegatedListeners.has(normalizedEvent))
|
|
67
|
+
delegatedListeners.set(normalizedEvent, new Set())
|
|
68
|
+
|
|
69
|
+
const entry = {
|
|
70
|
+
element,
|
|
71
|
+
handler,
|
|
72
|
+
modifiers,
|
|
73
|
+
context,
|
|
74
|
+
expr,
|
|
75
|
+
cleanup: null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
delegatedListeners.get(normalizedEvent).add(entry)
|
|
79
|
+
|
|
80
|
+
return function cleanup() {
|
|
81
|
+
const listeners = delegatedListeners.get(normalizedEvent)
|
|
82
|
+
if (listeners) {
|
|
83
|
+
listeners.delete(entry)
|
|
84
|
+
if (listeners.size === 0) {
|
|
85
|
+
const docHandler = documentListeners.get(normalizedEvent)
|
|
86
|
+
if (docHandler) {
|
|
87
|
+
document.removeEventListener(normalizedEvent, docHandler, true)
|
|
88
|
+
documentListeners.delete(normalizedEvent)
|
|
89
|
+
}
|
|
90
|
+
delegatedListeners.delete(normalizedEvent)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createEventHandler(handler, modifiers, context, expr) {
|
|
97
|
+
let timeoutId
|
|
98
|
+
let lastRun = 0
|
|
99
|
+
let throttleTimeoutId
|
|
100
|
+
|
|
101
|
+
let debounceMs = 0
|
|
102
|
+
let throttleMs = 0
|
|
103
|
+
|
|
104
|
+
const parseDuration = (idx) => {
|
|
105
|
+
const next = modifiers[idx + 1]
|
|
106
|
+
if (next && /^\d/.test(next)) {
|
|
107
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
108
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
109
|
+
}
|
|
110
|
+
return 250
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
114
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
115
|
+
|
|
116
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
117
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
118
|
+
|
|
119
|
+
const executeLogic = (e) => {
|
|
120
|
+
batch(() => {
|
|
121
|
+
if (typeof handler === 'function') {
|
|
122
|
+
handler(e)
|
|
123
|
+
} else {
|
|
124
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (e) => {
|
|
130
|
+
if (modifiers.includes('prevent')) e.preventDefault()
|
|
131
|
+
if (modifiers.includes('stop')) e.stopPropagation()
|
|
132
|
+
if (modifiers.includes('self') && e.target !== e.currentTarget) return
|
|
133
|
+
|
|
134
|
+
if (e instanceof KeyboardEvent) {
|
|
135
|
+
const key = e.key.toLowerCase()
|
|
136
|
+
const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
|
|
137
|
+
if (keyModifiers.length > 0) {
|
|
138
|
+
const match = keyModifiers.some(m => {
|
|
139
|
+
if (m === 'escape') return key === 'escape' || key === 'esc'
|
|
140
|
+
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
141
|
+
return key === m
|
|
142
|
+
})
|
|
143
|
+
if (!match) return
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (throttleMs > 0) {
|
|
148
|
+
const now = Date.now()
|
|
149
|
+
const remaining = throttleMs - (now - lastRun)
|
|
150
|
+
|
|
151
|
+
if (remaining <= 0) {
|
|
152
|
+
clearTimeout(throttleTimeoutId)
|
|
153
|
+
lastRun = now
|
|
154
|
+
executeLogic(e)
|
|
155
|
+
} else {
|
|
156
|
+
clearTimeout(throttleTimeoutId)
|
|
157
|
+
throttleTimeoutId = setTimeout(() => {
|
|
158
|
+
lastRun = Date.now()
|
|
159
|
+
executeLogic(e)
|
|
160
|
+
}, remaining)
|
|
161
|
+
}
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (debounceMs > 0) {
|
|
166
|
+
clearTimeout(timeoutId)
|
|
167
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
168
|
+
} else {
|
|
169
|
+
executeLogic(e)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
package/framework/core/signal.js
CHANGED
|
@@ -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,11 @@ 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
|
+
for (const childCleanup of nestedEffects) {
|
|
108
|
+
childCleanup()
|
|
109
|
+
}
|
|
110
|
+
nestedEffects.clear()
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
if (onEffectCreated) onEffectCreated(cleanup)
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// โโโ Component Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
-
|
|
3
1
|
export type ComponentProps = Record<string, any>
|
|
4
|
-
|
|
5
2
|
export type EmptyProps = Record<string, never>
|
|
6
|
-
|
|
7
3
|
export type PropSchema = string[] | Record<string, PropConfig>
|
|
8
4
|
|
|
9
5
|
export type PropConfig = {
|
|
@@ -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> {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// โโโ Event Delegation Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Set of standard events that support delegation.
|
|
5
|
+
* These events bubble and are commonly used in UI interactions.
|
|
6
|
+
*/
|
|
7
|
+
export const DELEGATED_EVENTS: Set<string>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if an event type is delegated.
|
|
11
|
+
* @param eventName - The event name to check (e.g., 'click', 'keydown')
|
|
12
|
+
* @returns True if the event can be delegated
|
|
13
|
+
*/
|
|
14
|
+
export function isDelegatedEvent(eventName: string): boolean
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for registering a delegated event.
|
|
18
|
+
*/
|
|
19
|
+
export interface DelegatedEventOptions {
|
|
20
|
+
/** Event modifiers (e.g., 'prevent', 'stop', 'debounce', 'throttle') */
|
|
21
|
+
modifiers?: string[]
|
|
22
|
+
/** Component context for evaluating expressions */
|
|
23
|
+
context?: Record<string, any>
|
|
24
|
+
/** Expression string to evaluate */
|
|
25
|
+
expr?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a delegated event handler for an element.
|
|
30
|
+
* Creates a single document-level listener if not already present.
|
|
31
|
+
*
|
|
32
|
+
* @param element - The DOM element to register
|
|
33
|
+
* @param eventType - The event type (e.g., 'click', 'input')
|
|
34
|
+
* @param handler - The event handler function
|
|
35
|
+
* @param options - Optional configuration
|
|
36
|
+
* @returns Cleanup function to remove the handler
|
|
37
|
+
*/
|
|
38
|
+
export function registerDelegatedEvent(
|
|
39
|
+
element: Element,
|
|
40
|
+
eventType: string,
|
|
41
|
+
handler: (event: Event) => void,
|
|
42
|
+
options?: DelegatedEventOptions
|
|
43
|
+
): () => void
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a wrapped event handler with modifier support.
|
|
47
|
+
* Supports: prevent, stop, self, debounce, throttle, keyboard modifiers.
|
|
48
|
+
*
|
|
49
|
+
* @param handler - The original handler (function or expression)
|
|
50
|
+
* @param modifiers - Array of modifier names
|
|
51
|
+
* @param context - Component context for evaluating expressions
|
|
52
|
+
* @param expr - Expression string (when handler is a string)
|
|
53
|
+
* @returns Wrapped handler function
|
|
54
|
+
*/
|
|
55
|
+
export function createEventHandler(
|
|
56
|
+
handler: Function | string,
|
|
57
|
+
modifiers: string[],
|
|
58
|
+
context: Record<string, any>,
|
|
59
|
+
expr: string
|
|
60
|
+
): (event: Event) => void
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Event handler syntax preference.
|
|
64
|
+
* Use @ prefix for delegated events (optimized, single document listener).
|
|
65
|
+
* Use on: prefix for native events (individual element listeners, full event control).
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Delegated - efficient for many elements
|
|
69
|
+
* <button @click="handleClick">Click</button>
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Native - full control including stopPropagation
|
|
73
|
+
* <button on:click="handleClick">Click</button>
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // With modifiers
|
|
77
|
+
* <form @submit.prevent="handleSubmit">Submit</form>
|
|
78
|
+
* <input @keydown.enter="handleEnter" />
|
|
79
|
+
* <input @input.debounce="handleInput" />
|
|
80
|
+
*/
|
|
81
|
+
export interface EventHandlerSyntax {
|
|
82
|
+
/** Delegated event: @click, @input, @keydown, etc. */
|
|
83
|
+
delegated: `@${string}`
|
|
84
|
+
/** Native event: on:click, on:input, on:custom, etc. */
|
|
85
|
+
native: `on:${string}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
declare global {
|
|
89
|
+
const isDelegatedEvent: typeof import('./event-delegation').isDelegatedEvent
|
|
90
|
+
}
|
|
@@ -5,6 +5,7 @@ export * from './router'
|
|
|
5
5
|
export * from './compiler'
|
|
6
6
|
export * from './directives'
|
|
7
7
|
export * from './store'
|
|
8
|
+
export * from './event-delegation'
|
|
8
9
|
|
|
9
10
|
declare global {
|
|
10
11
|
type Signal<T = any> = import('./signal').Signal<T>
|
|
@@ -13,7 +14,9 @@ declare global {
|
|
|
13
14
|
type Composable<T extends (...args: any[]) => any> = import('./composable').Composable<T>
|
|
14
15
|
type DirectiveBinding<V = any> = import('./directives').DirectiveBinding<V>
|
|
15
16
|
type DirectiveHooks<V = any> = import('./directives').DirectiveHooks<V>
|
|
17
|
+
type DelegatedEventOptions = import('./event-delegation').DelegatedEventOptions
|
|
16
18
|
const defineStore: typeof import('./store').defineStore
|
|
19
|
+
const isDelegatedEvent: typeof import('./event-delegation').isDelegatedEvent
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export as namespace Fusee
|
package/index.html
CHANGED
|
@@ -293,6 +293,322 @@
|
|
|
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
|
+
}
|
|
549
|
+
|
|
550
|
+
/* โโ Event Delegation Demo Styles โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
|
551
|
+
.test-events h1 {
|
|
552
|
+
color: #ff3333;
|
|
553
|
+
text-shadow: 0 0 20px rgba(255, 51, 51, 0.4);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.test-events .subtitle {
|
|
557
|
+
color: #aaa;
|
|
558
|
+
font-size: 0.9rem;
|
|
559
|
+
margin-bottom: 2rem;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.button-grid {
|
|
563
|
+
display: grid;
|
|
564
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
565
|
+
gap: 0.75rem;
|
|
566
|
+
margin-bottom: 1rem;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.demo-btn {
|
|
570
|
+
padding: 0.75rem 1rem;
|
|
571
|
+
border: 2px solid #3a3a5a;
|
|
572
|
+
background: #0f0f1a;
|
|
573
|
+
color: #e8e8f0;
|
|
574
|
+
border-radius: 8px;
|
|
575
|
+
cursor: pointer;
|
|
576
|
+
font-weight: 600;
|
|
577
|
+
transition: all 0.15s;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.demo-btn:hover {
|
|
581
|
+
background: #2a2a3a;
|
|
582
|
+
transform: translateY(-2px);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.demo-btn.native {
|
|
586
|
+
border-color: #34d399;
|
|
587
|
+
color: #34d399;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.demo-btn.native:hover {
|
|
591
|
+
background: rgba(52, 211, 153, 0.1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.counter-display {
|
|
595
|
+
font-size: 1.1rem;
|
|
596
|
+
color: #aaa;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.counter-display strong {
|
|
600
|
+
color: #e8e8f0;
|
|
601
|
+
font-size: 1.3rem;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.stop-demo {
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
transition: background 0.15s;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.stop-demo:hover {
|
|
610
|
+
background: #252535;
|
|
611
|
+
}
|
|
296
612
|
</style>
|
|
297
613
|
</head>
|
|
298
614
|
|