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.
@@ -0,0 +1,158 @@
1
+ import { defineComponent } from '../../framework/index.js'
2
+ import { counterLogic, mouseTracker } from '../composables/demo.js'
3
+ import { useFetch } from '../composables/useFetch.js'
4
+ import { useLocalStorage } from '../composables/useLocalStorage.js'
5
+
6
+ export const TestComposables = defineComponent({
7
+ setup() {
8
+ // ── 1. counterLogic — pure signal composable, no context needed ──────
9
+ const counter = counterLogic(0, 1)
10
+ const bigCounter = counterLogic(100, 10)
11
+
12
+ // ── 2. mouseTracker — lifecycle composable (needs setup context) ─────
13
+ const mouse = mouseTracker()
14
+
15
+ // ── 3. useLocalStorage — persists across page reloads ────────────────
16
+ const theme = useLocalStorage<string>('demo-theme', 'dark')
17
+ const nickname = useLocalStorage<string>('demo-nickname', '')
18
+
19
+ const toggleTheme = () => theme(theme() === 'dark' ? 'light' : 'dark')
20
+
21
+ // ── 4. useFetch — reactive data fetching ─────────────────────────────
22
+ const { data: posts, loading, error } = useFetch(
23
+ 'https://jsonplaceholder.typicode.com/posts?_limit=3'
24
+ )
25
+ // ── 5. Custom Directives Test ────────────────────────────────────────
26
+ const hColor = signal('#8b8bff33')
27
+ const colors = ['#8b8bff33', '#10b98133', '#f59e0b33', '#ef444433']
28
+ let colorIdx = 0
29
+ const nextColor = () => hColor(colors[++colorIdx % colors.length])
30
+
31
+ return {
32
+ hColor,
33
+ nextColor,
34
+
35
+ // counter
36
+ count: counter.count,
37
+ doubled: counter.doubled,
38
+ isEven: counter.isEven,
39
+ increment: counter.increment,
40
+ decrement: counter.decrement,
41
+ reset: counter.reset,
42
+
43
+ // big counter
44
+ bigCount: bigCounter.count,
45
+ bigIncrement: bigCounter.increment,
46
+ bigDecrement: bigCounter.decrement,
47
+
48
+ // mouse
49
+ mouseX: mouse.x,
50
+ mouseY: mouse.y,
51
+
52
+ // localStorage
53
+ theme,
54
+ nickname,
55
+ toggleTheme,
56
+
57
+ // fetch
58
+ posts,
59
+ loading,
60
+ error,
61
+
62
+ template: `
63
+ <div class="page">
64
+ <h1>🧩 Composables Demo</h1>
65
+ <p style="color:#888; margin-bottom: 2rem;">
66
+ Each section below is powered by a <code style="color:#ff3333">defineComposable()</code> function.
67
+ No naming rules — just reactive logic.
68
+ </p>
69
+
70
+ <!-- ── 1. counterLogic ─────────────────────────────────────── -->
71
+ <section class="demo-card">
72
+ <h2>📦 counterLogic(0, step=1)</h2>
73
+ <p class="demo-desc">Pure signal composable — works without setup() context.</p>
74
+ <div class="counter-display">
75
+ <span class="big-num" :class="isEven ? 'even' : 'odd'">{{ count }}</span>
76
+ <span class="derived-label">× 2 = {{ doubled }}</span>
77
+ </div>
78
+ <div class="btn-row">
79
+ <button @click="decrement" class="cmp-btn minus">−</button>
80
+ <button @click="reset" class="cmp-btn reset">↺</button>
81
+ <button @click="increment" class="cmp-btn plus">+</button>
82
+ </div>
83
+ <p class="badge" :class="isEven ? 'badge-blue' : 'badge-red'">{{ isEven ? 'Even' : 'Odd' }}</p>
84
+ </section>
85
+
86
+ <!-- ── 1b. Second independent counter (step=10) ───────────── -->
87
+ <section class="demo-card">
88
+ <h2>📦 counterLogic(100, step=10)</h2>
89
+ <p class="demo-desc">Same composable, different instance. Fully isolated state.</p>
90
+ <div class="counter-display">
91
+ <span class="big-num">{{ bigCount }}</span>
92
+ </div>
93
+ <div class="btn-row">
94
+ <button @click="bigDecrement" class="cmp-btn minus">−10</button>
95
+ <button @click="bigIncrement" class="cmp-btn plus">+10</button>
96
+ </div>
97
+ </section>
98
+
99
+ <!-- ── 2. mouseTracker ────────────────────────────────────── -->
100
+ <section class="demo-card">
101
+ <h2>🖱️ mouseTracker()</h2>
102
+ <p class="demo-desc">Lifecycle composable — registers window listener on mount, cleans up on unmount.</p>
103
+ <div class="mouse-display">
104
+ <span>X: <strong>{{ mouseX }}</strong></span>
105
+ <span>Y: <strong>{{ mouseY }}</strong></span>
106
+ </div>
107
+ </section>
108
+
109
+ <!-- ── 3. useLocalStorage ─────────────────────────────────── -->
110
+ <section class="demo-card">
111
+ <h2>💾 useLocalStorage()</h2>
112
+ <p class="demo-desc">State persists across page reloads. Try changing the nickname, then refresh.</p>
113
+ <div style="margin-bottom: 12px;">
114
+ <input f-model="nickname"
115
+ class="cmp-input"
116
+ placeholder="Your nickname…" />
117
+ <p style="margin-top: 8px; color: #888; font-size: 0.85rem;">
118
+ Stored: <strong style="color:#ff3333">{{ nickname || '(empty)' }}</strong>
119
+ </p>
120
+ </div>
121
+ <button @click="toggleTheme" class="cmp-btn reset">
122
+ Theme: {{ theme }}
123
+ </button>
124
+ </section>
125
+
126
+ <!-- ── 4. useFetch ────────────────────────────────────────── -->
127
+ <section class="demo-card">
128
+ <h2>🌐 useFetch()</h2>
129
+ <p class="demo-desc">Fetches from JSONPlaceholder. Loading state handled reactively.</p>
130
+ <div f-if="loading">
131
+ <p style="color: #888;">⏳ Loading posts…</p>
132
+ </div>
133
+ <div f-elif="error">
134
+ <p style="color: #ff6b6b;">❌ {{ error }}</p>
135
+ </div>
136
+ <div f-else>
137
+ <div f-for="post in posts" style="margin-bottom: 10px; padding: 10px; background:#1a1a24; border-radius:6px; border-left: 3px solid #ff3333;">
138
+ <strong style="color:#ff9999">{{ post.title }}</strong>
139
+ <p style="margin-top: 4px; font-size: 0.8rem; color: #666;">{{ post.body }}</p>
140
+ </div>
141
+ </div>
142
+ </section>
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
+ })
@@ -5,33 +5,60 @@ export const TestFor = defineComponent({
5
5
  { id: 2, name: 'Item 2' },
6
6
  { id: 3, name: 'Item 3' }
7
7
  ])
8
+ const search = signal('')
9
+
10
+ // ✨ NEW: Using the reactive .filter() method on the signal!
11
+ // This returns a Computed signal that updates automatically.
12
+ const filteredItems = items.filter(item =>
13
+ item.name.toLowerCase().includes(search().toLowerCase())
14
+ )
15
+
8
16
  inspect(items)
9
17
 
10
18
  return {
11
19
  items,
20
+ search,
21
+ filteredItems,
22
+ // 🔗 Chaining test: filter -> map -> join
23
+ namesSummary: filteredItems.map(i => i.name).join(', '),
24
+
25
+ updateItem: (id, newName) => {
26
+ // We update the SOURCE signal so the .filter() can see the new values!
27
+ items(items().map(i => i.id === id ? { ...i, name: newName } : i))
28
+ },
12
29
  addItem: () => {
13
30
  const nextId = Math.max(0, ...items().map(i => i.id)) + 1
14
- items([...items(), { id: nextId, name: `Item ${nextId}` }])
31
+ items.push({ id: nextId, name: `Item ${nextId}` })
15
32
  },
16
33
  shuffleItems: () => {
17
- const shuffled = [...items()].sort(() => Math.random() - 0.5)
18
- items(shuffled)
34
+ items.sort(() => Math.random() - 0.5)
19
35
  },
20
36
  template: `
21
37
  <div class="page">
22
38
  <h1>Keyed f-for Test</h1>
23
39
  <p>Focus an input below, then click "Shuffle" or "Add". The focus should stay!</p>
40
+ <p style="color: #8b8bff; background: #8b8bff11; padding: 10px; border-radius: 4px; border-left: 3px solid #8b8bff;">
41
+ 🔗 <strong>Reactive Summary:</strong> {{ namesSummary || 'No items found' }}
42
+ </p>
24
43
 
25
- <div style="margin-bottom: 20px; display: flex; gap: 10px;">
26
- <button @click="addItem" class="primary-btn">Add Item</button>
27
- <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
44
+ <div style="margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px;">
45
+ <input type="text" f-model="search" placeholder="🔍 Filter items..."
46
+ style="padding: 10px; border-radius: 4px; border: 1px solid #444; background: #111; color: #fff; width: 100%;" />
47
+
48
+ <div style="display: flex; gap: 10px;">
49
+ <button @click="addItem" class="primary-btn">Add Item</button>
50
+ <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
51
+ </div>
28
52
  </div>
29
53
 
30
54
  <div style="display: flex; flex-direction: column; gap: 10px;">
31
- <div f-for="item in items" :key="item.id"
55
+ <div f-for="item in filteredItems" :key="item.id"
32
56
  style="padding: 10px; border: 1px solid #333; display: flex; gap: 10px; align-items: center;">
33
57
  <span>ID: {{ item.id }}</span>
34
- <input type="text" :value="item.name" placeholder="Type here..."
58
+ <input type="text"
59
+ :value="item.name"
60
+ @input="updateItem(item().id, $event.target.value)"
61
+ placeholder="Type here..."
35
62
  style="background: #111; color: #fff; border: 1px solid #444; padding: 5px;" />
36
63
  </div>
37
64
  </div>
@@ -5,6 +5,10 @@ type Item = {
5
5
 
6
6
  type TestForResult = {
7
7
  items: Signal<Item[]>
8
+ search: Signal<string>
9
+ filteredItems: Computed<Item[]>
10
+ namesSummary: Computed<string>
11
+ updateItem: (id: number, name: string) => void
8
12
  addItem: () => void
9
13
  shuffleItems: () => void
10
14
  template: string
@@ -17,33 +21,55 @@ export const TestFor = defineComponent({
17
21
  { id: 2, name: 'Item 2' },
18
22
  { id: 3, name: 'Item 3' }
19
23
  ])
24
+ const search = signal('')
25
+ const filteredItems = items.filter(i =>
26
+ i.name.toLowerCase().includes(search().toLowerCase())
27
+ )
28
+ const namesSummary = filteredItems.map(i => i.name).join(', ')
20
29
 
21
30
  return {
22
31
  items,
32
+ search,
33
+ filteredItems,
34
+ namesSummary,
35
+ updateItem: (id, newName) => {
36
+ items(items().map(i => i.id === id ? { ...i, name: newName } : i))
37
+ },
23
38
  addItem: () => {
24
39
  const nextIds = items().map(i => i.id)
25
40
  const nextId = nextIds.length > 0 ? Math.max(...nextIds) + 1 : 1
26
- items([...items(), { id: nextId, name: `Item ${nextId}` }])
41
+ items.push({ id: nextId, name: `Item ${nextId}` })
27
42
  },
28
43
  shuffleItems: () => {
29
- const shuffled = [...items()].sort(() => Math.random() - 0.5)
30
- items(shuffled)
44
+ items.sort(() => Math.random() - 0.5)
31
45
  },
32
46
  template: `
33
47
  <div class="page">
34
48
  <h1>Keyed f-for Test (TypeScript)</h1>
35
49
  <p>Focus an input below, then click "Shuffle" or "Add". The focus should stay!</p>
50
+
51
+ <p style="color: #8b8bff; background: #8b8bff11; padding: 10px; border-radius: 4px; border-left: 3px solid #8b8bff;">
52
+ 🔗 <strong>Reactive Summary:</strong> {{ namesSummary || 'No items found' }}
53
+ </p>
36
54
 
37
- <div style="margin-bottom: 20px; display: flex; gap: 10px;">
38
- <button @click="addItem" class="primary-btn">Add Item</button>
39
- <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
55
+ <div style="margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px;">
56
+ <input type="text" f-model="search" placeholder="🔍 Filter items..."
57
+ style="padding: 10px; border-radius: 4px; border: 1px solid #444; background: #111; color: #fff; width: 100%;" />
58
+
59
+ <div style="display: flex; gap: 10px;">
60
+ <button @click="addItem" class="primary-btn">Add Item</button>
61
+ <button @click="shuffleItems" class="primary-btn">Shuffle Items</button>
62
+ </div>
40
63
  </div>
41
64
 
42
65
  <div style="display: flex; flex-direction: column; gap: 10px;">
43
- <div f-for="item in items" :key="item.id"
66
+ <div f-for="item in filteredItems" :key="item.id"
44
67
  style="padding: 10px; border: 1px solid #333; display: flex; gap: 10px; align-items: center;">
45
68
  <span>ID: {{ item.id }}</span>
46
- <input type="text" :value="item.name" placeholder="Type here..."
69
+ <input type="text"
70
+ :value="item.name"
71
+ @input="updateItem(item().id, $event.target.value)"
72
+ placeholder="Type here..."
47
73
  style="background: #111; color: #fff; border: 1px solid #444; padding: 5px;" />
48
74
  </div>
49
75
  </div>
package/auto-imports.d.ts CHANGED
@@ -9,6 +9,7 @@ declare global {
9
9
  const batch: typeof import('fusee-framework').batch
10
10
  const computed: typeof import('fusee-framework').computed
11
11
  const defineComponent: typeof import('fusee-framework').defineComponent
12
+ const directive: typeof import('fusee-framework').directive
12
13
  const effect: typeof import('fusee-framework').effect
13
14
  const emit: typeof import('fusee-framework').emit
14
15
  const inject: typeof import('fusee-framework').inject
@@ -3,6 +3,7 @@ export const FuseePreset = {
3
3
  'signal', 'computed', 'effect',
4
4
  'batch', 'untrack', 'inspect',
5
5
  'defineComponent', 'onMount', 'onUnmount',
6
+ 'directive',
6
7
  'mountOutlet', 'mountTemplate', 'emit'
7
8
  ]
8
9
  }
@@ -7,6 +7,10 @@ setEffectHook(eff => {
7
7
  if (currentInstance) currentInstance._effects.push(eff)
8
8
  })
9
9
 
10
+ export function getCurrentInstance() {
11
+ return currentInstance
12
+ }
13
+
10
14
  function resolveProps(schema, received) {
11
15
  const isArray = Array.isArray(schema)
12
16
  const resolved = {}
@@ -68,9 +72,6 @@ function resolveProps(schema, received) {
68
72
  return resolved
69
73
  }
70
74
 
71
- // ── Emit ──────────────────────────────────────────────────────────────────────
72
- // listeners: { change: fn, submit: fn, ... }
73
- // emit('change', 42) → calls listeners.change(42)
74
75
  function createEmit(listeners) {
75
76
  return function emit(eventName, ...args) {
76
77
  const handler = listeners[eventName]
@@ -82,10 +83,6 @@ function createEmit(listeners) {
82
83
  }
83
84
  }
84
85
 
85
- // ── Slots ─────────────────────────────────────────────────────────────────────
86
- // Parses slot content from parent HTML string.
87
- // Named slots: <template slot="header">...</template>
88
- // Default slot: everything else
89
86
  export function parseSlots(slotHTML) {
90
87
  const slots = { default: '' }
91
88
  if (!slotHTML || !slotHTML.trim()) return slots
@@ -105,19 +102,15 @@ export function parseSlots(slotHTML) {
105
102
  return slots
106
103
  }
107
104
 
108
- // Replaces <slot> and <slot name="x"> in template with actual slot content
109
105
  function resolveSlots(template, slots) {
110
- // Handle named slots with any content between tags (including default content)
111
106
  let result = template.replace(/<slot\s+name="([^"]+)"\s*>[\s\S]*?<\/slot>/gi, (match, name) => {
112
107
  return slots[name] ?? ''
113
108
  })
114
- // Handle default slots with any content between tags
115
109
  result = result.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, () => {
116
110
  return slots.default ?? ''
117
111
  })
118
- // Handle self-closing named slots
112
+
119
113
  result = result.replace(/<slot\s+name="([^"]+)"\s*\/>/gi, (_, name) => slots[name] ?? '')
120
- // Handle self-closing default slots
121
114
  result = result.replace(/<slot\s*\/>/gi, () => slots.default ?? '')
122
115
  return result
123
116
  }
@@ -131,10 +124,6 @@ export function onUnmount(fn) {
131
124
  }
132
125
 
133
126
  export function defineComponent(options) {
134
- // ComponentFactory now accepts props + optional { listeners, slots, parent }
135
- // listeners: event handlers from parent @eventName="handler"
136
- // slots: parsed slot HTML from parent
137
- // parent: parent component instance for provide/inject
138
127
  return function ComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
139
128
  const instance = {
140
129
  props: options.props ? resolveProps(options.props, props) : props,
@@ -149,16 +138,13 @@ export function defineComponent(options) {
149
138
  const emit = createEmit(listeners)
150
139
 
151
140
  currentInstance = instance
152
- // setup() receives (props, { emit, slots })
153
141
  const result = options.setup(instance.props, { emit, slots })
154
- // Store instance reference on result for child component access
155
142
  result._instance = instance
156
143
  currentInstance = null
157
144
 
158
145
  function render(container) {
159
146
  instance._element = container
160
147
 
161
- // Resolve slot placeholders before mounting
162
148
  const resolvedTemplate = resolveSlots(result.template, slots)
163
149
 
164
150
  const { effects } = mountTemplate(
@@ -201,8 +187,8 @@ export function inject(key) {
201
187
  }
202
188
 
203
189
  export function defineAsyncComponent(loaderOrOptions) {
204
- const options = typeof loaderOrOptions === 'function'
205
- ? { loader: loaderOrOptions }
190
+ const options = typeof loaderOrOptions === 'function'
191
+ ? { loader: loaderOrOptions }
206
192
  : loaderOrOptions
207
193
 
208
194
  return function AsyncComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
@@ -0,0 +1,27 @@
1
+ import { getCurrentInstance } from './component.js'
2
+
3
+ export function defineComposable(fn) {
4
+ const composable = function (...args) {
5
+ return fn(...args)
6
+ }
7
+
8
+ composable._isComposable = true
9
+
10
+ Object.defineProperty(composable, 'name', {
11
+ value: fn.name || 'composable',
12
+ configurable: true
13
+ })
14
+
15
+ return composable
16
+ }
17
+
18
+ export function assertSetupContext(name) {
19
+ const instance = getCurrentInstance()
20
+ if (!instance) {
21
+ console.warn(
22
+ `[framework] ${name ? name + ' ' : ''}composable was called outside of setup(). ` +
23
+ `Lifecycle hooks (onMount, onUnmount) and injections will not work.`
24
+ )
25
+ }
26
+ return instance
27
+ }
@@ -2,6 +2,73 @@ import { signal, effect, batch } from './signal.js'
2
2
  import { evaluateExpression } from './evaluator.js'
3
3
  import { compileNode } from './compiler.js'
4
4
 
5
+ const customDirectives = new Map()
6
+
7
+ export function directive(name, definition) {
8
+ if (typeof name !== 'string' || !name.trim()) {
9
+ console.error('[framework] directive() requires a non-empty name')
10
+ return
11
+ }
12
+ if (!definition || typeof definition !== 'object') {
13
+ console.error(`[framework] directive("${name}") requires a definition object`)
14
+ return
15
+ }
16
+ customDirectives.set(name, definition)
17
+ }
18
+
19
+ export function processCustomDirectives(el, context, effects) {
20
+ if (el.nodeType !== 1 || !el.hasAttribute || !el.attributes) return
21
+
22
+ for (const attr of [...el.attributes]) {
23
+ const match = attr.name.match(/^f-([a-zA-Z0-9-]+)(?::([a-zA-Z0-9-]+))?(?:\.(.+))?$/)
24
+
25
+ if (!match) continue
26
+
27
+ const directiveName = match[1]
28
+ const arg = match[2] || null
29
+ const modifiersStr = match[3]
30
+ const modifiers = modifiersStr ? modifiersStr.split('.') : []
31
+
32
+ const definition = customDirectives.get(directiveName)
33
+ if (!definition) {
34
+ continue
35
+ }
36
+
37
+ el.removeAttribute(attr.name)
38
+
39
+ const value = evaluateExpression(attr.value, context)
40
+ const expression = attr.value
41
+
42
+ const binding = {
43
+ value,
44
+ expression,
45
+ arg,
46
+ modifiers
47
+ }
48
+
49
+ if (typeof definition.mounted === 'function') {
50
+ definition.mounted(el, binding)
51
+ }
52
+
53
+ if (typeof definition.updated === 'function') {
54
+ effects.push(effect(() => {
55
+ const newValue = evaluateExpression(expression, context)
56
+
57
+ if (!Object.is(binding.value, newValue)) {
58
+ binding.value = newValue
59
+ definition.updated(el, binding)
60
+ }
61
+ }))
62
+ }
63
+
64
+ if (typeof definition.unmounted === 'function') {
65
+ effects.push(() => {
66
+ definition.unmounted(el, binding)
67
+ })
68
+ }
69
+ }
70
+ }
71
+
5
72
  export function processDirectives(root, context, components, effects) {
6
73
  if (processFor(root, context, components, effects)) return true
7
74
  if (processIf(root, context, components, effects)) return true
@@ -13,6 +80,7 @@ export function processDirectives(root, context, components, effects) {
13
80
  processHtml(root, context, effects)
14
81
  processText(root, context, effects)
15
82
  processEvents(root, context, effects)
83
+ processCustomDirectives(root, context, effects)
16
84
  processCloak(root)
17
85
 
18
86
  return false
@@ -282,7 +350,7 @@ export function processIs(el, context, components, effects) {
282
350
  // 1. Prepare directive state
283
351
  const expr = el.getAttribute('f-is')
284
352
  const keepAlive = processKeepAlive(el)
285
-
353
+
286
354
  el.removeAttribute('f-is')
287
355
 
288
356
  const anchor = document.createComment('f-keepAlive-anchor')
@@ -51,6 +51,10 @@ export function signal(initialValue) {
51
51
  }
52
52
 
53
53
  accessor.isSignal = true
54
+
55
+ if (Array.isArray(initialValue)) addMutatingArrayMethods(accessor)
56
+ addReactiveArrayMethods(accessor)
57
+
54
58
  return accessor
55
59
  }
56
60
 
@@ -125,6 +129,9 @@ export function computed(fn) {
125
129
 
126
130
  accessor.isSignal = true
127
131
  accessor.readonly = true
132
+
133
+ addReactiveArrayMethods(accessor)
134
+
128
135
  return accessor
129
136
  }
130
137
 
@@ -138,11 +145,6 @@ export function untrack(fn) {
138
145
  }
139
146
  }
140
147
 
141
- /**
142
- * Reactive log helper similar to Svelte 5's $inspect.
143
- * Watches signals or functions and logs their current values on change.
144
- * Only active in development mode.
145
- */
146
148
  export function inspect(...args) {
147
149
  const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
148
150
  if (!isDev) return
@@ -157,3 +159,88 @@ export function inspect(...args) {
157
159
  console.log('[inspect]', ...values)
158
160
  })
159
161
  }
162
+
163
+ function addReactiveArrayMethods(accessor) {
164
+ // Transformations
165
+ accessor.map = (fn) => computed(() => accessor()?.map?.(fn) ?? [])
166
+ accessor.filter = (fn) => computed(() => accessor()?.filter?.(fn) ?? [])
167
+ accessor.slice = (...args) => computed(() => accessor()?.slice?.(...args) ?? [])
168
+ accessor.concat = (...args) => computed(() => accessor()?.concat?.(...args) ?? [])
169
+ accessor.flat = (depth) => computed(() => accessor()?.flat?.(depth) ?? [])
170
+ accessor.flatMap = (fn) => computed(() => accessor()?.flatMap?.(fn) ?? [])
171
+
172
+ // Searches
173
+ accessor.find = (fn) => computed(() => accessor()?.find?.(fn))
174
+ accessor.findLast = (fn) => computed(() => accessor()?.findLast?.(fn))
175
+ accessor.findIndex = (fn) => computed(() => accessor()?.findIndex?.(fn))
176
+ accessor.findLastIndex = (fn) => computed(() => accessor()?.findLastIndex?.(fn))
177
+ accessor.indexOf = (searchElement, fromIdx) => computed(() => accessor()?.indexOf?.(searchElement, fromIdx))
178
+ accessor.lastIndexOf = (searchElement, fromIdx) => computed(() => accessor()?.lastIndexOf?.(searchElement, fromIdx))
179
+ accessor.includes = (searchElement, fromIdx) => computed(() => accessor()?.includes?.(searchElement, fromIdx))
180
+
181
+ // Validations
182
+ accessor.every = (fn) => computed(() => accessor()?.every?.(fn))
183
+ accessor.some = (fn) => computed(() => accessor()?.some?.(fn))
184
+
185
+ // Accumulators & Access
186
+ accessor.reduce = (...args) => computed(() => accessor()?.reduce?.(...args))
187
+ accessor.at = (index) => computed(() => accessor()?.at?.(index))
188
+ accessor.join = (separator) => computed(() => accessor()?.join?.(separator) ?? '')
189
+ }
190
+
191
+ function addMutatingArrayMethods(accessor) {
192
+ accessor.push = (...items) => {
193
+ const next = [...accessor(), ...items]
194
+ accessor(next)
195
+ return next.length
196
+ }
197
+
198
+ accessor.pop = () => {
199
+ const arr = accessor()
200
+ if (arr.length === 0) return undefined
201
+ const item = arr[arr.length - 1]
202
+ accessor(arr.slice(0, -1))
203
+ return item
204
+ }
205
+
206
+ accessor.shift = () => {
207
+ const arr = accessor()
208
+ if (arr.length === 0) return undefined
209
+ const item = arr[0]
210
+ accessor(arr.slice(1))
211
+ return item
212
+ }
213
+
214
+ accessor.unshift = (...items) => {
215
+ const next = [...items, ...accessor()]
216
+ accessor(next)
217
+ return next.length
218
+ }
219
+
220
+ accessor.splice = (start, deleteCount, ...items) => {
221
+ const arr = [...accessor()]
222
+ const removed = arr.splice(start, deleteCount, ...items)
223
+ accessor(arr)
224
+ return removed
225
+ }
226
+
227
+ accessor.remove = (predicate) => {
228
+ accessor(accessor().filter((item, i) => !predicate(item, i)))
229
+ }
230
+
231
+ accessor.clear = () => {
232
+ accessor([])
233
+ }
234
+
235
+ accessor.sort = (compareFn) => {
236
+ const next = [...accessor()].sort(compareFn)
237
+ accessor(next)
238
+ return accessor
239
+ }
240
+
241
+ accessor.reverse = () => {
242
+ const next = [...accessor()].reverse()
243
+ accessor(next)
244
+ return accessor
245
+ }
246
+ }
@@ -1,8 +1,10 @@
1
1
  // ─── Framework Public API ─────────────────────────────────────────────────────
2
2
  export { signal, computed, effect, batch, untrack, inspect } from './core/signal.js'
3
- export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject } from './core/component.js'
3
+ export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject, getCurrentInstance } from './core/component.js'
4
4
  export { createRouter, navigate, mountOutlet } from './router/router.js'
5
5
  export { mountTemplate } from './core/compiler.js'
6
6
 
7
- // Emit is a placeholder - actual emit function is created per component
7
+ export { defineComposable, assertSetupContext } from './core/composable.js'
8
+ export { directive } from './core/directives.js'
9
+
8
10
  export const emit = () => console.warn('[framework] emit() can only be used inside component setup()')