Añadir sigpro-lite.js

This commit is contained in:
2026-04-10 23:56:08 +02:00
parent 9b9b345e48
commit 02ee84f8e5

192
sigpro-lite.js Normal file
View File

@@ -0,0 +1,192 @@
let currentContext = null
const getContext = () => currentContext
const runInContext = (ctx, fn) => {
const prev = currentContext
currentContext = ctx
try { return fn() }
finally { currentContext = prev }
}
const onCleanup = (fn) => {
const ctx = getContext()
if (!ctx) throw new Error('onCleanup must be called within a reactive root')
ctx.cleanups.add(fn)
}
const onMount = (fn) => {
const ctx = getContext()
if (!ctx) throw new Error('onMount must be called within a reactive root')
queueMicrotask(() => runInContext(ctx, fn))
}
const createRoot = (fn) => {
const ctx = { cleanups: new Set(), parent: currentContext }
return runInContext(ctx, () => {
const result = fn(ctx)
const destroy = () => { ctx.cleanups.forEach(c => c()); ctx.cleanups.clear() }
if (result instanceof Node) result._destroy = destroy
return result
})
}
const createComponent = (fn) => (...args) => {
const parent = getContext()
const ctx = { cleanups: new Set(), parent }
if (parent) parent.cleanups.add(() => { ctx.cleanups.forEach(c => c()); ctx.cleanups.clear() })
return runInContext(ctx, () => fn(...args))
}
const createSignal = (initialValue) => {
const subscribers = new Set()
let value = initialValue
const signal = (...args) => {
if (args.length === 0) {
const ctx = getContext()
if (ctx?.activeEffect) {
subscribers.add(ctx.activeEffect)
ctx.activeEffect.deps.add(subscribers)
}
return value
}
const next = typeof args[0] === 'function' ? args[0](value) : args[0]
if (!Object.is(value, next)) {
value = next
;[...subscribers].forEach(e => e.run())
}
return value
}
return signal
}
const createEffect = (fn) => {
const ctx = getContext()
if (!ctx) throw new Error('createEffect must be called within a reactive root')
const effect = {
deps: new Set(),
run: () => {
effect.deps.forEach(d => d.delete(effect))
effect.deps.clear()
const prev = ctx.activeEffect
ctx.activeEffect = effect
try { fn() }
finally { ctx.activeEffect = prev }
}
}
onCleanup(() => {
effect.deps.forEach(d => d.delete(effect))
effect.deps.clear()
})
effect.run()
}
const mount = (component, selector) => {
const root = document.querySelector(selector)
if (!root) throw new Error(`Selector "${selector}" not found`)
if (root._destroy) root._destroy()
root.innerHTML = ''
const app = createRoot(component)
root.appendChild(app)
root._destroy = app._destroy
return app
}
const h = (tag, props, ...children) => {
const el = document.createElement(tag)
if (props) {
Object.entries(props).forEach(([k, v]) => {
if (k.startsWith('on')) {
const event = k.slice(2).toLowerCase()
el.addEventListener(event, v)
onCleanup(() => el.removeEventListener(event, v))
} else if (k === 'style' && typeof v === 'object') {
Object.assign(el.style, v)
} else {
el.setAttribute(k, v)
}
})
}
children.flat().forEach(c => {
if (c instanceof Node) el.appendChild(c)
else el.appendChild(document.createTextNode(String(c ?? '')))
})
return el
}
const For = createComponent(({ each, children }) => {
const container = h('div', { style: 'display:contents' })
const marker = document.createTextNode('')
container.appendChild(marker)
let cache = new Map()
const source = typeof each === 'function' ? each : () => each
createEffect(() => {
const items = source() || []
const newCache = new Map()
const order = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const key = typeof item === 'object' && item !== null && 'id' in item ? item.id : i
let view = cache.get(key)
if (!view) view = createRoot(() => children(item, i))
newCache.set(key, view)
order.push(key)
cache.delete(key)
}
cache.forEach(v => v._destroy ? v._destroy() : (v.remove?.(), v._destroy?.()))
cache = newCache
let anchor = marker
for (let i = order.length - 1; i >= 0; i--) {
const view = newCache.get(order[i])
if (view instanceof Node && view.nextSibling !== anchor)
container.insertBefore(view, anchor)
anchor = view
}
})
onCleanup(() => cache.forEach(v => v._destroy ? v._destroy() : v.remove?.()))
return container
})
const If = createComponent(({ when, children }) => {
const anchor = document.createTextNode('')
const container = h('div', { style: 'display:contents' }, anchor)
let current = null
const cond = typeof when === 'function' ? when : () => when
createEffect(() => {
const show = !!cond()
if (show && !current) {
current = createRoot(() => children())
container.insertBefore(current, anchor)
} else if (!show && current) {
if (current._destroy) current._destroy()
else current.remove()
current = null
}
})
onCleanup(() => current?._destroy?.())
return container
})
const Router = createComponent(({ routes }) => {
const getHash = () => window.location.hash.slice(1) || '/'
const path = createSignal(getHash())
const handler = () => path(getHash())
window.addEventListener('hashchange', handler)
onCleanup(() => window.removeEventListener('hashchange', handler))
const outlet = h('div', {})
let currentView = null
createEffect(() => {
const cur = path()
const match = routes.find(r => {
const rParts = r.path.split('/').filter(Boolean)
const pParts = cur.split('/').filter(Boolean)
return rParts.length === pParts.length && rParts.every((p, i) => p.startsWith(':') || p === pParts[i])
}) || routes.find(r => r.path === '*')
if (match) {
if (currentView?._destroy) currentView._destroy()
else if (currentView) currentView.remove()
const params = {}
match.path.split('/').filter(Boolean).forEach((p, i) => {
if (p.startsWith(':')) params[p.slice(1)] = cur.split('/').filter(Boolean)[i]
})
currentView = createRoot(() => match.component(params))
outlet.appendChild(currentView)
}
})
onCleanup(() => currentView?._destroy?.())
return outlet
})
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, '#/')
Router.back = () => window.history.back()
Router.path = () => window.location.hash.slice(1) || '/'
const SigPro = { createRoot, createComponent, createSignal, createEffect, onCleanup, onMount, mount, h, For, If, Router }
if (typeof window !== 'undefined') Object.assign(window, SigPro)
export { createRoot, createComponent, createSignal, createEffect, onCleanup, onMount, mount, h, For, If, Router }
export default SigPro