Files
sigpro/sigpro-lite.js
2026-04-11 00:07:37 +02:00

232 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// SIGPRO CORE · Simple, Fast, MemorySafe
// ============================================================
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
}
// Helper para crear elementos (con soporte SVG)
const h = (tag, props, ...children) => {
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/i.test(tag)
const el = isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: 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
}
// ===== IF / FOR / ROUTER (API IDÉNTICA A B) =====
const If = createComponent((cond, thenFn) => {
const anchor = document.createTextNode('')
const container = h('div', { style: 'display:contents' }, anchor)
let current = null
createEffect(() => {
const show = !!cond()
if (show && !current) {
current = createRoot(() => thenFn())
container.insertBefore(current, anchor)
} else if (!show && current) {
if (current._destroy) current._destroy()
else current.remove()
current = null
}
})
onCleanup(() => current?._destroy?.())
return container
})
const For = createComponent((source, itemFn) => {
const container = h('div', { style: 'display:contents' })
const marker = document.createTextNode('')
container.appendChild(marker)
let cache = new Map()
createEffect(() => {
const items = source() || []
const newCache = new Map()
const order = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const key = (item && typeof item === 'object' && 'id' in item) ? item.id : i
let view = cache.get(key)
if (!view) view = createRoot(() => itemFn(item, i))
newCache.set(key, view)
order.push(key)
cache.delete(key)
}
cache.forEach(v => v._destroy ? v._destroy() : v.remove?.())
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 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) || '/'
// ===== API PÚBLICA =====
const SigPro = {
createRoot, createComponent, createSignal, createEffect,
onCleanup, onMount, mount, h,
If, For, Router
}
// ===== EXPOSICIÓN GLOBAL (OPCIONAL, COMO EN B) =====
if (typeof window !== 'undefined') {
Object.assign(window, SigPro)
const tags = 'div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg'.split(' ')
tags.forEach(t => {
const helper = t[0].toUpperCase() + t.slice(1)
window[helper] = (props, ...children) => h(t, props, children)
})
}
export {
createRoot, createComponent, createSignal, createEffect,
onCleanup, onMount, mount, h,
If, For, Router
}
export default SigPro