fusee-framework 1.1.0 → 1.2.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.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/app/components/Card.js +32 -0
  3. package/app/components/Card.ts +41 -0
  4. package/app/components/Counter.js +4 -5
  5. package/app/components/Counter.ts +2 -5
  6. package/app/components/CounterSection.js +10 -4
  7. package/app/components/CounterSection.ts +0 -1
  8. package/app/components/EmitTest.js +77 -0
  9. package/app/components/EmitTest.ts +90 -0
  10. package/app/components/UserForm.js +1 -0
  11. package/app/components/UserForm.ts +0 -2
  12. package/app/main.ts +3 -1
  13. package/app/providers/ChildComponent.js +44 -0
  14. package/app/providers/ChildComponent.ts +47 -0
  15. package/app/providers/GrandchildComponent.js +41 -0
  16. package/app/providers/GrandchildComponent.ts +48 -0
  17. package/app/providers/ParentComponent.js +48 -0
  18. package/app/providers/ParentComponent.ts +53 -0
  19. package/app/providers/index.js +3 -0
  20. package/app/providers/index.ts +3 -0
  21. package/app/routes/About.js +1 -0
  22. package/app/routes/About.ts +1 -1
  23. package/app/routes/Home.js +46 -2
  24. package/app/routes/Home.ts +0 -1
  25. package/app/routes/TestFor.js +1 -0
  26. package/app/routes/TestFor.ts +0 -2
  27. package/auto-imports.d.ts +12 -1
  28. package/framework/auto-import-preset.js +2 -2
  29. package/framework/bin/create-fusee.js +1 -3
  30. package/framework/core/compiler.js +100 -56
  31. package/framework/core/component.js +85 -3
  32. package/framework/core/evaluator.js +4 -1
  33. package/framework/core/signal.js +20 -0
  34. package/framework/index.js +6 -2
  35. package/framework/types/component.d.ts +31 -3
  36. package/framework/types/index.d.ts +7 -0
  37. package/framework/types/signal.d.ts +1 -0
  38. package/index.html +0 -4
  39. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ import { provide, inject } from '../../framework/index.js'
2
+ import { ChildComponent } from './ChildComponent.js'
3
+
4
+ interface UserData {
5
+ name: string
6
+ age: number
7
+ }
8
+
9
+ // ParentComponent - provides values to child components
10
+ export const ParentComponent = defineComponent({
11
+ components: { ChildComponent },
12
+ setup() {
13
+ const count = signal(42)
14
+ const message = signal('Hello from Parent!')
15
+ const userData: UserData = { name: 'John', age: 30 }
16
+
17
+ // Provide values to descendant components
18
+ provide('count', count)
19
+ provide('message', message)
20
+ provide('userData', userData)
21
+ provide('appVersion', '1.0.0')
22
+
23
+ function updateCount(): void {
24
+ count(count() + 10)
25
+ }
26
+
27
+ return {
28
+ count,
29
+ message,
30
+ userData,
31
+ updateCount,
32
+ template: `
33
+ <div style="padding: 20px; border: 2px solid #4CAF50; border-radius: 8px; margin: 20px 0; color: #000;">
34
+ <h2 style="color: #4CAF50;">Parent Component</h2>
35
+ <p>I'm providing these values to my children:</p>
36
+ <ul style="background: #333; padding: 15px; border-radius: 4px; color: #fff;">
37
+ <li><strong>count:</strong> {{ count }}</li>
38
+ <li><strong>message:</strong> {{ message }}</li>
39
+ <li><strong>userData:</strong> {{ userData.name }} ({{ userData.age }} years)</li>
40
+ <li><strong>appVersion:</strong> 1.0.0</li>
41
+ </ul>
42
+ <button @click="updateCount" style="margin-top: 10px; padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
43
+ Update Count (+10)
44
+ </button>
45
+
46
+ <div style="margin-top: 20px;">
47
+ {{ ChildComponent }}
48
+ </div>
49
+ </div>
50
+ `
51
+ }
52
+ }
53
+ })
@@ -0,0 +1,3 @@
1
+ export { ParentComponent } from './ParentComponent.js'
2
+ export { ChildComponent } from './ChildComponent.js'
3
+ export { GrandchildComponent } from './GrandchildComponent.js'
@@ -0,0 +1,3 @@
1
+ export { ParentComponent } from './ParentComponent.js'
2
+ export { ChildComponent } from './ChildComponent.js'
3
+ export { GrandchildComponent } from './GrandchildComponent.js'
@@ -2,6 +2,7 @@ export const About = defineComponent({
2
2
  props: [],
3
3
  setup() {
4
4
  const loaded = signal(false)
5
+ inspect(loaded)
5
6
 
6
7
  onMount(() => {
7
8
  setTimeout(() => loaded(true), 300)
@@ -1,4 +1,4 @@
1
- import { defineComponent, signal, onMount, type Signal, type EmptyProps } from '../../framework/index.js'
1
+ import type { Signal, EmptyProps } from '../../framework/index.js'
2
2
 
3
3
  type AboutResult = {
4
4
  loaded: Signal<boolean>
@@ -1,13 +1,29 @@
1
1
  import { UserForm } from '../components/UserForm.js'
2
2
  import { CounterSection } from '../components/CounterSection.js'
3
+ import { Card } from '../components/Card.js'
4
+ import { EmitTest } from '../components/EmitTest.js'
5
+ import { ParentComponent } from '../providers/index.js'
3
6
 
4
7
  export const Home = defineComponent({
5
- components: { UserForm, CounterSection },
8
+ components: { UserForm, CounterSection, Card, EmitTest, ParentComponent },
6
9
  setup() {
7
10
  const title = signal('Welcome!')
11
+ inspect(title)
12
+
8
13
  return {
9
14
  title,
10
15
  changeTitle: () => title('Changed Title!'),
16
+
17
+ // 🧪 Emit Event Handlers
18
+ handleCounterChange: (value) => console.log('📢 Counter changed to:', value),
19
+ handleCounterReset: () => console.log('🔄 Counter reset requested!'),
20
+ handleCustomEvent: (count, multiplier) => console.log('🎯 Custom event:', { count, multiplier }),
21
+
22
+ // 🧪 Emit Test Handlers
23
+ handleSimpleClick: () => console.log('✅ SIMPLE CLICK received from child!'),
24
+ handleClickWithData: (count, label, timestamp) => console.log('✅ CLICK WITH DATA:', { count, label, timestamp }),
25
+ handleObjectEvent: (data) => console.log('✅ OBJECT EVENT received:', data),
26
+
11
27
  template: `
12
28
  <div class="page">
13
29
  <h1>{{ title.toUpperCase() }}</h1>
@@ -18,7 +34,35 @@ export const Home = defineComponent({
18
34
 
19
35
  {{ UserForm }}
20
36
 
21
- {{ CounterSection :pageTitle="title" }}
37
+ {{ CounterSection @counter-change="handleCounterChange" @counter-reset="handleCounterReset" @custom-event="handleCustomEvent" :pageTitle="title" }}
38
+
39
+ <!-- 🧪 EMIT TEST - NEW SEPARATE COMPONENT -->
40
+ <h3 style="margin-top: 40px; margin-bottom: 20px; color: #4da6ff;">🧪 EMIT EVENTS TEST LAB</h3>
41
+ {{ EmitTest @simple-click="handleSimpleClick" @click-with-data="handleClickWithData" @object-event="handleObjectEvent" }}
42
+
43
+ <!-- 🧪 Slots Test -->
44
+ <h3 style="margin-top: 40px; margin-bottom: 20px;">🎴 Slots Test</h3>
45
+
46
+ {{ Card title="Custom Card" }}
47
+ <template slot="header">
48
+ <p style="color: #10b981; font-weight: bold;">🎯 Custom header from parent!</p>
49
+ </template>
50
+ <p style="color: #f59e0b;">This is custom content passed through the default slot.</p>
51
+ <p>You can pass any HTML content here!</p>
52
+ <template slot="footer">
53
+ <button style="background: #3b82f6; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
54
+ Custom Footer Button
55
+ </button>
56
+ </template>
57
+ {{ /Card }}
58
+
59
+ {{ Card title="Default Slots Card" }}
60
+ <p>This card uses only the default slot - no named slots provided.</p>
61
+ {{ /Card }}
62
+
63
+ <!-- 🧪 PROVIDE / INJECT TEST -->
64
+ <h3 style="margin-top: 40px; margin-bottom: 20px; color: #4CAF50;">🔗 PROVIDE / INJECT TEST</h3>
65
+ {{ ParentComponent }}
22
66
  </div>
23
67
  `
24
68
  }
@@ -1,4 +1,3 @@
1
- import type { Signal } from 'fusee-framework'
2
1
  import { UserForm } from '../components/UserForm.js'
3
2
  import { CounterSection } from '../components/CounterSection.js'
4
3
 
@@ -5,6 +5,7 @@ export const TestFor = defineComponent({
5
5
  { id: 2, name: 'Item 2' },
6
6
  { id: 3, name: 'Item 3' }
7
7
  ])
8
+ inspect(items)
8
9
 
9
10
  return {
10
11
  items,
@@ -1,5 +1,3 @@
1
- import type { Signal } from 'fusee-framework'
2
-
3
1
  type Item = {
4
2
  id: number
5
3
  name: string
package/auto-imports.d.ts CHANGED
@@ -1,12 +1,23 @@
1
- export { }
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ // @ts-nocheck
4
+ // noinspection JSUnusedGlobalSymbols
5
+ // Generated by unplugin-auto-import
6
+ // biome-ignore lint: disable
7
+ export {}
2
8
  declare global {
3
9
  const batch: typeof import('fusee-framework').batch
4
10
  const computed: typeof import('fusee-framework').computed
5
11
  const defineComponent: typeof import('fusee-framework').defineComponent
6
12
  const effect: typeof import('fusee-framework').effect
13
+ const emit: typeof import('fusee-framework').emit
14
+ const inject: typeof import('fusee-framework').inject
15
+ const inspect: typeof import('fusee-framework').inspect
7
16
  const mountOutlet: typeof import('fusee-framework').mountOutlet
17
+ const mountTemplate: typeof import('fusee-framework').mountTemplate
8
18
  const onMount: typeof import('fusee-framework').onMount
9
19
  const onUnmount: typeof import('fusee-framework').onUnmount
20
+ const provide: typeof import('fusee-framework').provide
10
21
  const signal: typeof import('fusee-framework').signal
11
22
  const untrack: typeof import('fusee-framework').untrack
12
23
  }
@@ -1,8 +1,8 @@
1
1
  export const FuseePreset = {
2
2
  'fusee-framework': [
3
3
  'signal', 'computed', 'effect',
4
- 'batch', 'untrack',
4
+ 'batch', 'untrack', 'inspect',
5
5
  'defineComponent', 'onMount', 'onUnmount',
6
- 'mountOutlet', 'mountTemplate'
6
+ 'mountOutlet', 'mountTemplate', 'emit'
7
7
  ]
8
8
  }
@@ -137,9 +137,7 @@ export default defineConfig({
137
137
  // 5. Counter component
138
138
  const btnStyle = "background:#ff3333; color:white; border:none; width:50px; height:50px; border-radius:15px; cursor:pointer; font-size:1.5rem; transition: 0.2s;";
139
139
  const counterContent = choice === 'ts'
140
- ? `import type { Signal } from 'fusee-framework'
141
-
142
- interface CounterResult {
140
+ ? `interface CounterResult {
143
141
  count: Signal<number>
144
142
  inc: () => void
145
143
  dec: () => void
@@ -1,5 +1,6 @@
1
1
  import { effect, batch } from './signal.js'
2
2
  import { processDirectives } from './directives.js'
3
+ import { parseSlots } from './component.js'
3
4
  import {
4
5
  evaluateExpression,
5
6
  parseInterpolation,
@@ -12,24 +13,21 @@ export function mountTemplate(template, container, context, components) {
12
13
  let processed = template.replace(/(<[a-zA-Z0-9-]+\s[^>]*?)\s:([a-zA-Z][a-zA-Z0-9-]*)=/g, '$1 data-bind-$2=')
13
14
 
14
15
  for (const name of Object.keys(components)) {
15
- const re = new RegExp(`\\{\\{\\s*${name}(\\s+.*?|\\s*)\\}\\}`, 'g')
16
- processed = processed.replace(re, (match, propsStr) => {
17
- let attrs = ''
18
- if (propsStr && propsStr.trim()) {
19
- const attrRegex = /(:?[a-zA-Z0-9-]+)="([^"]*)"/g
20
- let attrMatch
21
- while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
22
- const attrName = attrMatch[1]
23
- const attrValue = attrMatch[2]
24
-
25
- if (attrName.startsWith(':')) {
26
- attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
27
- } else {
28
- attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
29
- }
30
- }
31
- }
32
- return `<div data-component="${name}"${attrs}></div>`
16
+ const reWithSlot = new RegExp(
17
+ `\\{\\{\\s*${name}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${name}\\s*\\}\\}`,
18
+ 'g'
19
+ )
20
+ const reNoSlot = new RegExp(`\\{\\{\\s*${name}(\\s+.*?|\\s*)\\}\\}`, 'g')
21
+
22
+ processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
23
+ const attrs = parseComponentAttrs(propsStr || '')
24
+ const encodedSlot = encodeURIComponent(slotContent.trim())
25
+ return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
26
+ })
27
+
28
+ processed = processed.replace(reNoSlot, (match, propsStr) => {
29
+ const attrs = parseComponentAttrs(propsStr || '')
30
+ return `<div data-component="${name}"${attrs}></div>`
33
31
  })
34
32
  }
35
33
 
@@ -39,26 +37,47 @@ export function mountTemplate(template, container, context, components) {
39
37
  return { effects }
40
38
  }
41
39
 
40
+ // Parses props string from component mustache into HTML attr string
41
+ function parseComponentAttrs(propsStr) {
42
+ let attrs = ''
43
+ if (!propsStr || !propsStr.trim()) return attrs
44
+
45
+ const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
46
+ let attrMatch
47
+ while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
48
+ const attrName = attrMatch[1]
49
+ const attrValue = attrMatch[2]
50
+
51
+ if (attrName.startsWith('@')) {
52
+ // Event listener: @change="handler" → data-on-change="handler"
53
+ attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
54
+ } else if (attrName.startsWith(':')) {
55
+ // Dynamic prop
56
+ attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
57
+ } else {
58
+ // Static prop
59
+ attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
60
+ }
61
+ }
62
+ return attrs
63
+ }
64
+
42
65
  export function compileNode(node, context, components, effects) {
43
- // Handle f-once (render once and discard reactive tracking for this subtree)
44
66
  if (node.nodeType === 1 && node.hasAttribute('f-once')) {
45
67
  node.removeAttribute('f-once')
46
68
  const temporaryEffects = []
47
69
 
48
- // Process ONLY this node and its children ONCE
49
70
  processDirectives(node, context, components, temporaryEffects)
50
71
  processBindingAttrs(node, context, temporaryEffects)
51
72
  processMustaches(node, context, temporaryEffects)
52
73
  bindComponents(node, components, context, temporaryEffects)
53
74
 
54
- // Recurse children but with temporary effects
55
75
  let child = node.firstChild
56
76
  while (child) {
57
77
  compileOnce(child, context, components, temporaryEffects)
58
78
  child = child.nextSibling
59
79
  }
60
80
 
61
- // Cleanup immediately
62
81
  for (const cleanup of temporaryEffects) if (typeof cleanup === 'function') cleanup()
63
82
  return
64
83
  }
@@ -172,46 +191,71 @@ function bindComponents(el, components, context, effects) {
172
191
  const name = el.getAttribute('data-component')
173
192
  if (!name) return
174
193
 
175
- // Remove the attribute immediately to prevent recursive compilers from
176
- // re-identifying this element as a component placeholder.
177
194
  el.removeAttribute('data-component')
178
195
 
179
196
  const ComponentFn = components[name]
180
- if (ComponentFn) {
181
- const props = {}
182
- for (const attr of [...el.attributes]) {
183
- const attrName = attr.name.toLowerCase()
184
-
185
- if (attrName.startsWith('data-prop-')) {
186
- props[attrName.slice(10)] = attr.value
187
- } else if (attrName.startsWith('data-bind-prop-')) {
188
- const propName = attrName.slice(15)
189
- const expression = attr.value.trim()
190
-
191
- if (expression in context) {
192
- Object.defineProperty(props, propName, {
193
- get() {
194
- const val = context[expression]
195
- return typeof val === 'function' && val.isSignal ? val() : val
196
- },
197
- enumerable: true,
198
- configurable: true
199
- })
197
+ if (!ComponentFn) return
198
+
199
+ // ── Props ────────────────────────────────────────────────────────────────
200
+ const props = {}
201
+ const listeners = {}
202
+
203
+ for (const attr of [...el.attributes]) {
204
+ const attrName = attr.name.toLowerCase()
205
+
206
+ if (attrName.startsWith('data-prop-')) {
207
+ props[attrName.slice(10)] = attr.value
208
+
209
+ } else if (attrName.startsWith('data-bind-prop-')) {
210
+ const propName = attrName.slice(15)
211
+ const expression = attr.value.trim()
212
+
213
+ if (expression in context) {
214
+ Object.defineProperty(props, propName, {
215
+ get() {
216
+ const val = context[expression]
217
+ return typeof val === 'function' && val.isSignal ? val() : val
218
+ },
219
+ enumerable: true,
220
+ configurable: true
221
+ })
222
+ } else {
223
+ Object.defineProperty(props, propName, {
224
+ get() { return evaluateExpression(expression, context) },
225
+ enumerable: true,
226
+ configurable: true
227
+ })
228
+ }
229
+
230
+ } else if (attrName.startsWith('data-on-')) {
231
+ // ── Emit listeners ───────────────────────────────────────────────
232
+ // @change="handler" → data-on-change="handler"
233
+ // When child emits 'change', calls context.handler(value)
234
+ const eventName = attrName.slice(8) // "data-on-" → eventName
235
+ const handlerExpr = attr.value.trim()
236
+
237
+ listeners[eventName] = (...args) => {
238
+ const handler = context[handlerExpr]
239
+ if (typeof handler === 'function') {
240
+ handler(...args)
200
241
  } else {
201
- Object.defineProperty(props, propName, {
202
- get() {
203
- return evaluateExpression(expression, context)
204
- },
205
- enumerable: true,
206
- configurable: true
207
- })
242
+ // Support inline expressions: @change="count(count() + 1)"
243
+ evaluateExpression(handlerExpr, context)
208
244
  }
209
245
  }
210
246
  }
211
- const childComponent = ComponentFn(props)
212
- childComponent.render(el)
213
- if (effects) {
214
- effects.push(() => childComponent.unmount())
215
- }
247
+ }
248
+
249
+ // ── Slots ────────────────────────────────────────────────────────────────
250
+ // data-slot attribute contains URI-encoded slot HTML from parent
251
+ const rawSlot = el.getAttribute('data-slot')
252
+ const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
253
+ el.removeAttribute('data-slot')
254
+
255
+ const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
256
+ childComponent.render(el)
257
+
258
+ if (effects) {
259
+ effects.push(() => childComponent.unmount())
216
260
  }
217
261
  }
@@ -68,6 +68,60 @@ function resolveProps(schema, received) {
68
68
  return resolved
69
69
  }
70
70
 
71
+ // ── Emit ──────────────────────────────────────────────────────────────────────
72
+ // listeners: { change: fn, submit: fn, ... }
73
+ // emit('change', 42) → calls listeners.change(42)
74
+ function createEmit(listeners) {
75
+ return function emit(eventName, ...args) {
76
+ const handler = listeners[eventName]
77
+ if (typeof handler === 'function') {
78
+ batch(() => handler(...args))
79
+ } else if (handler !== undefined) {
80
+ console.warn(`[framework] emit("${eventName}"): listener is not a function`)
81
+ }
82
+ }
83
+ }
84
+
85
+ // ── Slots ─────────────────────────────────────────────────────────────────────
86
+ // Parses slot content from parent HTML string.
87
+ // Named slots: <template slot="header">...</template>
88
+ // Default slot: everything else
89
+ export function parseSlots(slotHTML) {
90
+ const slots = { default: '' }
91
+ if (!slotHTML || !slotHTML.trim()) return slots
92
+
93
+ const namedSlotRe = /<template\s+slot="([^"]+)"[^>]*>([\s\S]*?)<\/template>/gi
94
+ let remaining = slotHTML
95
+ let match
96
+
97
+ while ((match = namedSlotRe.exec(slotHTML)) !== null) {
98
+ slots[match[1]] = match[2].trim()
99
+ remaining = remaining.replace(match[0], '')
100
+ }
101
+
102
+ const defaultContent = remaining.trim()
103
+ if (defaultContent) slots.default = defaultContent
104
+
105
+ return slots
106
+ }
107
+
108
+ // Replaces <slot> and <slot name="x"> in template with actual slot content
109
+ function resolveSlots(template, slots) {
110
+ // Handle named slots with any content between tags (including default content)
111
+ let result = template.replace(/<slot\s+name="([^"]+)"\s*>[\s\S]*?<\/slot>/gi, (match, name) => {
112
+ return slots[name] ?? ''
113
+ })
114
+ // Handle default slots with any content between tags
115
+ result = result.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, () => {
116
+ return slots.default ?? ''
117
+ })
118
+ // Handle self-closing named slots
119
+ result = result.replace(/<slot\s+name="([^"]+)"\s*\/>/gi, (_, name) => slots[name] ?? '')
120
+ // Handle self-closing default slots
121
+ result = result.replace(/<slot\s*\/>/gi, () => slots.default ?? '')
122
+ return result
123
+ }
124
+
71
125
  export function onMount(fn) {
72
126
  if (currentInstance) currentInstance._mountHooks.push(fn)
73
127
  }
@@ -77,24 +131,38 @@ export function onUnmount(fn) {
77
131
  }
78
132
 
79
133
  export function defineComponent(options) {
80
- return function ComponentFactory(props = {}) {
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
+ return function ComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
81
139
  const instance = {
82
140
  props: options.props ? resolveProps(options.props, props) : props,
83
141
  _mountHooks: [],
84
142
  _unmountHooks: [],
85
143
  _effects: [],
86
144
  _element: null,
145
+ _provides: {},
146
+ _parent: parent,
87
147
  }
88
148
 
149
+ const emit = createEmit(listeners)
150
+
89
151
  currentInstance = instance
90
- const result = options.setup(instance.props)
152
+ // setup() receives (props, { emit, slots })
153
+ const result = options.setup(instance.props, { emit, slots })
154
+ // Store instance reference on result for child component access
155
+ result._instance = instance
91
156
  currentInstance = null
92
157
 
93
158
  function render(container) {
94
159
  instance._element = container
95
160
 
161
+ // Resolve slot placeholders before mounting
162
+ const resolvedTemplate = resolveSlots(result.template, slots)
163
+
96
164
  const { effects } = mountTemplate(
97
- result.template,
165
+ resolvedTemplate,
98
166
  container,
99
167
  result,
100
168
  options.components || {}
@@ -116,4 +184,18 @@ export function defineComponent(options) {
116
184
 
117
185
  return { render, unmount, instance }
118
186
  }
187
+ }
188
+
189
+ export function provide(key, value) {
190
+ if(!currentInstance) return
191
+ currentInstance._provides[key] = value
192
+ }
193
+
194
+ export function inject(key) {
195
+ let parent = currentInstance?._parent
196
+ while(parent) {
197
+ if (key in parent._provides) return parent._provides[key];
198
+ parent = parent._parent;
199
+ }
200
+ return null;
119
201
  }
@@ -6,7 +6,10 @@ const DANGEROUS_SCHEMES = /^(javascript|data|vbscript|file):/i
6
6
  export function evaluateExpression(expr, context, extraContext = {}, unwrapSignals = true) {
7
7
  const keys = []
8
8
  const values = []
9
- const identifiers = new Set(expr.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [])
9
+
10
+ // Remove string literals before extracting identifiers to avoid matching words inside strings
11
+ const exprWithoutStrings = expr.replace(/'[^']*'|"[^"]*"/g, ' ')
12
+ const identifiers = new Set(exprWithoutStrings.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [])
10
13
 
11
14
  // Map global context (only what is used in expression)
12
15
  for (const k in context) {
@@ -135,3 +135,23 @@ export function untrack(fn) {
135
135
  currentEffect = prevEffect
136
136
  }
137
137
  }
138
+
139
+ /**
140
+ * Reactive log helper similar to Svelte 5's $inspect.
141
+ * Watches signals or functions and logs their current values on change.
142
+ * Only active in development mode.
143
+ */
144
+ export function inspect(...args) {
145
+ const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
146
+ if (!isDev) return
147
+
148
+ return effect(() => {
149
+ const values = args.map(arg => {
150
+ if (typeof arg === 'function') {
151
+ return arg()
152
+ }
153
+ return arg
154
+ })
155
+ console.log('[inspect]', ...values)
156
+ })
157
+ }
@@ -1,4 +1,8 @@
1
1
  // ─── Framework Public API ─────────────────────────────────────────────────────
2
- export { signal, computed, effect, batch, untrack } from './core/signal.js'
3
- export { defineComponent, onMount, onUnmount } from './core/component.js'
2
+ export { signal, computed, effect, batch, untrack, inspect } from './core/signal.js'
3
+ export { defineComponent, onMount, onUnmount, parseSlots, provide, inject } from './core/component.js'
4
4
  export { createRouter, navigate, mountOutlet } from './router/router.js'
5
+ export { mountTemplate } from './core/compiler.js'
6
+
7
+ // Emit is a placeholder - actual emit function is created per component
8
+ export const emit = () => console.warn('[framework] emit() can only be used inside component setup()')
@@ -12,12 +12,29 @@ export type PropConfig = {
12
12
  required?: boolean
13
13
  }
14
14
 
15
+ // ── Emit ──────────────────────────────────────────────────────────────────────
16
+ // emit('change', value) — calls parent listener registered with @change="handler"
17
+ export type EmitFn = (eventName: string, ...args: any[]) => void
18
+
19
+ // ── Slots ─────────────────────────────────────────────────────────────────────
20
+ // slots.default — default slot HTML string
21
+ // slots.header — named slot HTML string
22
+ export type Slots = Record<string, string>
23
+
24
+ // ── Setup context ─────────────────────────────────────────────────────────────
25
+ export type SetupContext = {
26
+ emit: EmitFn
27
+ slots: Slots
28
+ }
29
+
15
30
  export type ComponentInstance = {
16
31
  props: ComponentProps
17
32
  _mountHooks: (() => void)[]
18
33
  _unmountHooks: (() => void)[]
19
34
  _effects: import('./signal').EffectRunner[]
20
35
  _element: HTMLElement | null
36
+ _provides: Record<string, any>
37
+ _parent: ComponentInstance | null
21
38
  }
22
39
 
23
40
  export type ComponentResult = {
@@ -28,10 +45,13 @@ export type ComponentResult = {
28
45
  export type ComponentOptions<TProps = ComponentProps> = {
29
46
  props?: PropSchema
30
47
  components?: Record<string, ComponentFactory<any>>
31
- setup: (props: TProps) => ComponentResult
48
+ setup: (props: TProps, ctx: SetupContext) => ComponentResult
32
49
  }
33
50
 
34
- export type ComponentFactory<TProps = ComponentProps> = (props?: TProps) => ComponentApi
51
+ export type ComponentFactory<TProps = ComponentProps> = (
52
+ props?: TProps,
53
+ options?: { listeners?: Record<string, Function>, slots?: Slots, parent?: ComponentInstance }
54
+ ) => ComponentApi
35
55
 
36
56
  export type ComponentApi = {
37
57
  render: (container: HTMLElement) => ComponentInstance
@@ -41,4 +61,12 @@ export type ComponentApi = {
41
61
 
42
62
  export declare function onMount(fn: () => void): void
43
63
  export declare function onUnmount(fn: () => void): void
44
- export declare function defineComponent<TProps = EmptyProps>(options: ComponentOptions<TProps>): ComponentFactory<TProps>
64
+ export declare function parseSlots(slotHTML: string): Slots
65
+
66
+ // ── Provide / Inject ───────────────────────────────────────────────────────────
67
+ export declare function provide<T>(key: string, value: T): void
68
+ export declare function inject<T>(key: string): T | null
69
+
70
+ export declare function defineComponent<TProps = EmptyProps>(
71
+ options: ComponentOptions<TProps>
72
+ ): ComponentFactory<TProps>