lightview
Version:
A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation
741 lines (740 loc) • 27.2 kB
JavaScript
(function() {
"use strict";
const _LV = globalThis.__LIGHTVIEW_INTERNALS__ || (globalThis.__LIGHTVIEW_INTERNALS__ = {
currentEffect: null,
registry: /* @__PURE__ */ new Map(),
// Global name -> Signal/Proxy
localRegistries: /* @__PURE__ */ new WeakMap(),
// Object/Element -> Map(name -> Signal/Proxy)
futureSignals: /* @__PURE__ */ new Map(),
// name -> Set of (signal) => void
schemas: /* @__PURE__ */ new Map(),
// name -> Schema (Draft 7+ or Shorthand)
parents: /* @__PURE__ */ new WeakMap(),
// Proxy -> Parent (Proxy/Element)
helpers: /* @__PURE__ */ new Map(),
// name -> function (used for transforms and expressions)
hooks: {
validate: (value, schema) => true
// Hook for extensions (like JPRX) to provide full validation
}
});
const lookup = (name, scope) => {
let current = scope;
while (current && typeof current === "object") {
const registry2 = _LV.localRegistries.get(current);
if (registry2 && registry2.has(name)) return registry2.get(name);
current = current.parentElement || _LV.parents.get(current);
}
return _LV.registry.get(name);
};
const signal = (initialValue, optionsOrName) => {
const name = typeof optionsOrName === "string" ? optionsOrName : optionsOrName == null ? void 0 : optionsOrName.name;
const storage = optionsOrName == null ? void 0 : optionsOrName.storage;
const scope = optionsOrName == null ? void 0 : optionsOrName.scope;
if (name && storage) {
try {
const stored = storage.getItem(name);
if (stored !== null) initialValue = JSON.parse(stored);
} catch (e) {
}
}
let value = initialValue;
const subscribers = /* @__PURE__ */ new Set();
const f = (...args) => args.length === 0 ? f.value : f.value = args[0];
Object.defineProperty(f, "value", {
get() {
if (_LV.currentEffect) {
subscribers.add(_LV.currentEffect);
_LV.currentEffect.dependencies.add(subscribers);
}
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
if (name && storage) {
try {
storage.setItem(name, JSON.stringify(value));
} catch (e) {
}
}
[...subscribers].forEach((effect2) => effect2());
}
}
});
if (name) {
const registry2 = scope && typeof scope === "object" ? _LV.localRegistries.get(scope) || _LV.localRegistries.set(scope, /* @__PURE__ */ new Map()).get(scope) : _LV.registry;
if (registry2 && registry2.has(name) && registry2.get(name) !== f) {
throw new Error(`Lightview: A signal or state with the name "${name}" is already registered.`);
}
if (registry2) registry2.set(name, f);
const futures = _LV.futureSignals.get(name);
if (futures) {
futures.forEach((resolve) => resolve(f));
}
}
return f;
};
const getSignal = (name, defaultValueOrOptions) => {
const options = typeof defaultValueOrOptions === "object" && defaultValueOrOptions !== null ? defaultValueOrOptions : { defaultValue: defaultValueOrOptions };
const { scope, defaultValue } = options;
const existing = lookup(name, scope);
if (existing) return existing;
if (defaultValue !== void 0) return signal(defaultValue, { name, scope });
const future = signal(void 0);
const handler = (realSignal) => {
const hasValue = realSignal && (typeof realSignal === "object" || typeof realSignal === "function") && "value" in realSignal;
if (hasValue) {
future.value = realSignal.value;
effect(() => {
future.value = realSignal.value;
});
} else {
future.value = realSignal;
}
};
if (!_LV.futureSignals.has(name)) _LV.futureSignals.set(name, /* @__PURE__ */ new Set());
_LV.futureSignals.get(name).add(handler);
return future;
};
signal.get = getSignal;
const effect = (fn) => {
const execute = () => {
if (!execute.active || execute.running) return;
execute.dependencies.forEach((dep) => dep.delete(execute));
execute.dependencies.clear();
execute.running = true;
_LV.currentEffect = execute;
try {
fn();
} finally {
_LV.currentEffect = null;
execute.running = false;
}
};
execute.active = true;
execute.running = false;
execute.dependencies = /* @__PURE__ */ new Set();
execute.stop = () => {
execute.dependencies.forEach((dep) => dep.delete(execute));
execute.dependencies.clear();
execute.active = false;
};
execute();
return execute;
};
const computed = (fn) => {
const sig = signal(void 0);
effect(() => {
sig.value = fn();
});
return sig;
};
const getRegistry = () => _LV.registry;
const internals = _LV;
const { parents, schemas, hooks } = internals;
const protoMethods = (proto, test) => Object.getOwnPropertyNames(proto).filter((k) => typeof proto[k] === "function" && test(k));
protoMethods(Date.prototype, (k) => /^(to|get|valueOf)/.test(k));
protoMethods(Date.prototype, (k) => /^set/.test(k));
const getOrSet = (map, key, factory) => {
let v = map.get(key);
if (!v) {
v = factory();
map.set(key, v);
}
return v;
};
const core = {
get currentEffect() {
return (globalThis.__LIGHTVIEW_INTERNALS__ || (globalThis.__LIGHTVIEW_INTERNALS__ = {})).currentEffect;
}
};
const nodeState = /* @__PURE__ */ new WeakMap();
const nodeStateFactory = () => ({ effects: [], onmount: null, onunmount: null });
const registry = getRegistry();
const scrollMemory = /* @__PURE__ */ new Map();
const initScrollMemory = () => {
if (typeof document === "undefined") return;
document.addEventListener("scroll", (e) => {
const el = e.target;
if (el === document || el === document.documentElement) return;
const key = el.id || el.getAttribute && el.getAttribute("data-preserve-scroll");
if (key) {
scrollMemory.set(key, { top: el.scrollTop, left: el.scrollLeft });
}
}, true);
};
if (typeof document !== "undefined") {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initScrollMemory);
} else {
initScrollMemory();
}
}
const saveScrolls = () => new Map(scrollMemory);
const restoreScrolls = (map, root = document) => {
if (!map || map.size === 0) return;
requestAnimationFrame(() => {
map.forEach((pos, key) => {
const node = document.getElementById(key) || document.querySelector(`[data-preserve-scroll="${key}"]`);
if (node) {
node.scrollTop = pos.top;
node.scrollLeft = pos.left;
}
});
});
};
const trackEffect = (node, effectFn) => {
const state = getOrSet(nodeState, node, nodeStateFactory);
if (!state.effects) state.effects = [];
state.effects.push(effectFn);
};
const SHADOW_DOM_MARKER = Symbol("lightview.shadowDOM");
const createShadowDOMMarker = (attributes, children) => ({
[SHADOW_DOM_MARKER]: true,
mode: attributes.mode || "open",
styles: attributes.styles || [],
adoptedStyleSheets: attributes.adoptedStyleSheets || [],
children
});
const isShadowDOMMarker = (obj) => obj && typeof obj === "object" && obj[SHADOW_DOM_MARKER] === true;
const processShadowDOM = (marker, parentNode) => {
if (parentNode.shadowRoot) {
console.warn("Lightview: Element already has a shadowRoot, skipping shadowDOM directive");
return;
}
const shadowRoot = parentNode.attachShadow({ mode: marker.mode });
const sheets = [];
const linkUrls = [...marker.styles || []];
if (marker.adoptedStyleSheets && marker.adoptedStyleSheets.length > 0) {
marker.adoptedStyleSheets.forEach((item) => {
if (item instanceof CSSStyleSheet) {
sheets.push(item);
} else if (typeof item === "string") {
linkUrls.push(item);
}
});
}
if (sheets.length > 0) {
try {
shadowRoot.adoptedStyleSheets = sheets;
} catch (e) {
console.warn("Lightview: adoptedStyleSheets not supported");
}
}
for (const styleUrl of linkUrls) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = styleUrl;
shadowRoot.appendChild(link);
}
if (marker.children && marker.children.length > 0) {
setupChildrenInTarget(marker.children, shadowRoot);
}
};
let inSVG = false;
const domToElement = /* @__PURE__ */ new WeakMap();
const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
const el = {
tag,
attributes,
children,
isProxy: true,
get domEl() {
return domNode;
}
};
const proxy = makeReactive(el);
domToElement.set(domNode, proxy);
return proxy;
};
const someRecursive = (item, predicate) => {
if (Array.isArray(item)) return item.some((i) => someRecursive(i, predicate));
return predicate(item);
};
const element = (tag, attributes = {}, children = []) => {
if (customTags[tag]) tag = customTags[tag];
if (typeof tag === "function") {
const result = tag({ ...attributes }, children);
return processComponentResult(result);
}
if (tag === "shadowDOM") {
return createShadowDOMMarker(attributes, children);
}
if (tag === "text" && !inSVG) {
const domNode2 = document.createTextNode("");
const el = {
tag,
attributes,
children,
get domEl() {
return domNode2;
}
};
const update = () => {
const bits = [];
const walk = (c) => {
if (Array.isArray(c)) {
for (let i = 0; i < c.length; i++) walk(c[i]);
return;
}
const val = typeof c === "function" ? c() : c;
if (val && typeof val === "object" && val.domEl) bits.push(val.domEl.textContent);
else bits.push(val === null || val === void 0 ? "" : String(val));
};
walk(el.children);
domNode2.textContent = bits.join(" ");
};
const proxy = new Proxy(el, {
set(target, prop, value) {
target[prop] = value;
if (prop === "children") update();
return true;
}
});
const hasReactive = someRecursive(children, (c) => typeof c === "function");
if (hasReactive) {
const runner = effect(update);
trackEffect(domNode2, runner);
}
update();
return proxy;
}
const isSVG = tag.toLowerCase() === "svg";
const wasInSVG = inSVG;
if (isSVG) inSVG = true;
const domNode = inSVG ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
const hasReactiveAttr = Object.values(attributes).some((v) => typeof v === "function");
const hasReactiveChild = someRecursive(children, (c) => typeof c === "function" || c && c.isProxy);
if (hasReactiveAttr || hasReactiveChild) {
const proxy = wrapDomElement(domNode, tag, attributes, children);
proxy.attributes = attributes;
proxy.children = children;
if (isSVG) inSVG = wasInSVG;
return proxy;
}
makeReactiveAttributes(attributes, domNode);
setupChildren(children, domNode);
if (isSVG) inSVG = wasInSVG;
return {
tag,
attributes,
children,
domEl: domNode
};
};
const processComponentResult = (result) => {
if (!result) return null;
if (Lightview.hooks.processChild) {
result = Lightview.hooks.processChild(result) ?? result;
}
if (result.domEl) return result;
const type = typeof result;
if (type === "object" && result && result.nodeType === 1) {
return wrapDomElement(result, result.tagName.toLowerCase(), {}, []);
}
if (type === "object" && result instanceof String) {
const span = document.createElement("span");
span.textContent = result.toString();
return wrapDomElement(span, "span", {}, []);
}
if (type === "string") {
const template = document.createElement("template");
template.innerHTML = result.trim();
const content = template.content;
if (content.childNodes.length === 1 && content.firstChild && content.firstChild.nodeType === 1) {
const el = content.firstChild;
return wrapDomElement(el, el.tagName.toLowerCase(), {}, []);
} else {
const wrapper = document.createElement("span");
wrapper.style.display = "contents";
wrapper.appendChild(content);
return wrapDomElement(wrapper, "span", {}, []);
}
}
if (typeof result === "object" && result.tag) {
return element(result.tag, result.attributes || {}, result.children || []);
}
return null;
};
const makeReactive = (el) => {
const domNode = el.domEl;
return new Proxy(el, {
set(target, prop, value) {
if (prop === "attributes") {
target[prop] = makeReactiveAttributes(value, domNode);
} else if (prop === "children") {
target[prop] = setupChildren(value, domNode);
} else {
target[prop] = value;
}
return true;
}
});
};
const NODE_PROPERTIES = /* @__PURE__ */ new Set(["value", "checked", "selected", "selectedIndex", "className", "innerHTML", "innerText"]);
const setAttributeValue = (domNode, key, value) => {
const isBool = typeof domNode[key] === "boolean";
if ((key === "href" || key === "src") && typeof value === "string" && /^(javascript|vbscript|data:text\/html|data:application\/javascript)/i.test(value)) {
console.warn(`[Lightview] Blocked dangerous protocol in ${key}: ${value}`);
value = "javascript:void(0)";
}
if (NODE_PROPERTIES.has(key) || isBool || key.startsWith("cdom-")) {
domNode[key] = isBool ? value !== null && value !== void 0 && value !== false && value !== "false" : value;
} else if (value === null || value === void 0) {
domNode.removeAttribute(key);
} else {
domNode.setAttribute(key, value);
}
};
const makeReactiveAttributes = (attributes = {}, domNode) => {
const reactiveAttrs = {};
for (const key in attributes) {
const value = attributes[key];
const type = typeof value;
if (value && type === "object" && value.__xpath__ && value.__static__) {
domNode.setAttribute(`data-xpath-${key}`, value.__xpath__);
reactiveAttrs[key] = value;
continue;
}
if (key === "onmount" || key === "onunmount") {
const state = getOrSet(nodeState, domNode, nodeStateFactory);
state[key] = value;
if (key === "onmount" && domNode.isConnected) {
value(domNode);
}
} else if (key.startsWith("on")) {
if (type === "function") {
domNode.addEventListener(key.slice(2).toLowerCase(), value);
} else if (type === "string") {
domNode.setAttribute(key, value);
}
reactiveAttrs[key] = value;
} else if (typeof value === "object" && value !== null && Lightview.hooks.processAttribute) {
const processed = Lightview.hooks.processAttribute(domNode, key, value);
if (processed !== void 0) {
reactiveAttrs[key] = processed;
} else if (key === "style") {
for (const styleKey in entries) {
const styleValue = entries[styleKey];
if (typeof styleValue === "function") {
const runner = effect(() => {
domNode.style[styleKey] = styleValue();
});
trackEffect(domNode, runner);
} else {
domNode.style[styleKey] = styleValue;
}
}
reactiveAttrs[key] = value;
} else {
setAttributeValue(domNode, key, value);
reactiveAttrs[key] = value;
}
} else if (type === "function") {
const runner = effect(() => {
const result = value();
if (key === "style" && typeof result === "object") {
Object.assign(domNode.style, result);
} else {
setAttributeValue(domNode, key, result);
}
});
trackEffect(domNode, runner);
reactiveAttrs[key] = value;
} else {
setAttributeValue(domNode, key, value);
reactiveAttrs[key] = value;
}
}
return reactiveAttrs;
};
const processChildren = (children, targetNode, clearExisting = true) => {
if (clearExisting && targetNode.innerHTML !== void 0) {
targetNode.innerHTML = "";
}
const childElements = [];
const isSpecialElement = targetNode.tagName && (targetNode.tagName.toLowerCase() === "script" || targetNode.tagName.toLowerCase() === "style");
const walk = (child) => {
if (Array.isArray(child)) {
for (let i = 0; i < child.length; i++) walk(child[i]);
return;
}
if (child === null || child === void 0) return;
if (Lightview.hooks.processChild && !isSpecialElement) {
child = Lightview.hooks.processChild(child) ?? child;
}
const type = typeof child;
if (child && type === "object" && child.tag) {
const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
targetNode.appendChild(childEl.domEl);
childElements.push(childEl);
} else if (["string", "number", "boolean", "symbol"].includes(type) || child && type === "object" && child instanceof String) {
targetNode.appendChild(document.createTextNode(child));
childElements.push(child);
} else if (type === "function") {
const startMarker = document.createComment("lv:s");
const endMarker = document.createComment("lv:e");
targetNode.appendChild(startMarker);
targetNode.appendChild(endMarker);
let runner;
const update = () => {
while (startMarker.nextSibling && startMarker.nextSibling !== endMarker) {
startMarker.nextSibling.remove();
}
const val = child();
if (val === void 0 || val === null) return;
if (runner && !startMarker.isConnected) {
runner.stop();
return;
}
if (typeof val === "object" && val instanceof String) {
const textNode = document.createTextNode(val);
endMarker.parentNode.insertBefore(textNode, endMarker);
} else {
const fragment = document.createDocumentFragment();
const childrenToProcess = Array.isArray(val) ? val : [val];
processChildren(childrenToProcess, fragment, false);
endMarker.parentNode.insertBefore(fragment, endMarker);
}
};
runner = effect(update);
trackEffect(startMarker, runner);
childElements.push(child);
} else if (child instanceof Node) {
const node = child.domEl || child;
if (node.nodeType === 1) {
const wrapped = wrapDomElement(node, node.tagName.toLowerCase());
targetNode.appendChild(node);
childElements.push(wrapped);
} else {
targetNode.appendChild(node);
childElements.push(child);
}
} else if (isShadowDOMMarker(child)) {
if (targetNode instanceof ShadowRoot) {
console.warn("Lightview: Cannot nest shadowDOM inside another shadowDOM");
return;
}
processShadowDOM(child, targetNode);
} else if (child && typeof child === "object" && child.__xpath__ && child.__static__) {
const textNode = document.createTextNode("");
textNode.__xpathExpr = child.__xpath__;
targetNode.appendChild(textNode);
childElements.push(child);
}
};
walk(children);
return childElements;
};
const setupChildrenInTarget = (children, targetNode) => {
return processChildren(children, targetNode, false);
};
const setupChildren = (children, domNode) => {
return processChildren(children, domNode, true);
};
const enhance = (selectorOrNode, options = {}) => {
const domNode = typeof selectorOrNode === "string" ? document.querySelector(selectorOrNode) : selectorOrNode;
const node = domNode.domEl || domNode;
if (!node || node.nodeType !== 1) return null;
const tagName = node.tagName.toLowerCase();
let el = domToElement.get(node);
if (!el) {
el = wrapDomElement(node, tagName);
}
const { innerText, innerHTML, ...attrs } = options;
if (innerText !== void 0) {
if (typeof innerText === "function") {
effect(() => {
node.innerText = innerText();
});
} else {
node.innerText = innerText;
}
}
if (innerHTML !== void 0) {
if (typeof innerHTML === "function") {
effect(() => {
node.innerHTML = innerHTML();
});
} else {
node.innerHTML = innerHTML;
}
}
if (Object.keys(attrs).length > 0) {
el.attributes = attrs;
}
return el;
};
const $ = (cssSelectorOrElement, startingDomEl = document.body) => {
const el = typeof cssSelectorOrElement === "string" ? startingDomEl.querySelector(cssSelectorOrElement) : cssSelectorOrElement;
if (!el) return null;
Object.defineProperty(el, "content", {
value(child, location = "inner") {
location = location.toLowerCase();
Lightview.tags;
const isSpecialElement = el.tagName && (el.tagName.toLowerCase() === "script" || el.tagName.toLowerCase() === "style");
const array = (Array.isArray(child) ? child : [child]).map((item) => {
if (Lightview.hooks.processChild && !isSpecialElement) {
item = Lightview.hooks.processChild(item) ?? item;
}
if (item.tag && !item.domEl) {
return element(item.tag, item.attributes || {}, item.children || []).domEl;
} else {
return item.domEl || item;
}
});
const target = location === "shadow" ? el.shadowRoot || el.attachShadow({ mode: "open" }) : el;
if (location === "inner" || location === "shadow") {
target.replaceChildren(...array);
} else if (location === "outer") {
target.replaceWith(...array);
} else if (location === "afterbegin") {
target.prepend(...array);
} else if (location === "beforeend") {
target.append(...array);
} else {
array.forEach((item) => el.insertAdjacentElement(location, item));
}
return el;
},
configurable: true,
writable: true
});
return el;
};
const customTags = {};
const tags = new Proxy({}, {
get(_, tag) {
if (tag === "_customTags") return { ...customTags };
const wrapper = (...args) => {
let attributes = {};
let children = args;
const arg0 = args[0];
if (args.length > 0 && arg0 && typeof arg0 === "object" && !arg0.tag && !arg0.domEl && !Array.isArray(arg0)) {
attributes = arg0;
children = args.slice(1);
}
return element(customTags[tag] || tag, attributes, children);
};
if (customTags[tag]) {
Object.assign(wrapper, customTags[tag]);
}
return wrapper;
},
set(_, tag, value) {
customTags[tag] = value;
return true;
}
});
const Lightview = {
registerSchema: (name, definition) => internals.schemas.set(name, definition),
signal,
get: signal.get,
computed,
effect,
registry,
element,
// do not document this
enhance,
tags,
$,
// Extension hooks
hooks: {
onNonStandardHref: null,
processChild: null,
processAttribute: null,
validateUrl: null,
validate: (value, schema) => internals.hooks.validate(value, schema)
},
// Internals exposed for extensions
internals: {
core,
domToElement,
wrapDomElement,
setupChildren,
trackEffect,
saveScrolls,
restoreScrolls,
localRegistries: internals.localRegistries,
futureSignals: internals.futureSignals,
schemas: internals.schemas,
parents: internals.parents,
hooks: internals.hooks
}
};
if (typeof module !== "undefined" && module.exports) {
module.exports = Lightview;
}
if (typeof window !== "undefined") {
globalThis.Lightview = Lightview;
globalThis.addEventListener("click", (e) => {
const path = e.composedPath();
const link = path.find((el) => {
var _a, _b;
return el.tagName === "A" && ((_b = (_a = el.getAttribute) == null ? void 0 : _a.call(el, "href")) == null ? void 0 : _b.startsWith("#"));
});
if (link && !e.defaultPrevented) {
const href = link.getAttribute("href");
if (href.length > 1) {
const id = href.slice(1);
const root = link.getRootNode();
const target = (root.getElementById ? root.getElementById(id) : null) || (root.querySelector ? root.querySelector(`#${id}`) : null);
if (target) {
e.preventDefault();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
target.style.scrollMarginTop = "calc(var(--site-nav-height, 0px) + 2rem)";
target.scrollIntoView({ behavior: "smooth", block: "start", inline: "start" });
});
});
}
}
}
if (Lightview.hooks.onNonStandardHref) {
Lightview.hooks.onNonStandardHref(e);
}
});
if (typeof MutationObserver !== "undefined") {
const walkNodes = (node, fn) => {
var _a;
fn(node);
(_a = node.childNodes) == null ? void 0 : _a.forEach((n) => walkNodes(n, fn));
if (node.shadowRoot) walkNodes(node.shadowRoot, fn);
};
const cleanupNode = (node) => walkNodes(node, (n) => {
var _a, _b;
const s = nodeState.get(n);
if (s) {
(_a = s.effects) == null ? void 0 : _a.forEach((e) => e.stop());
(_b = s.onunmount) == null ? void 0 : _b.call(s, n);
nodeState.delete(n);
}
});
const mountNode = (node) => walkNodes(node, (n) => {
var _a, _b;
(_b = (_a = nodeState.get(n)) == null ? void 0 : _a.onmount) == null ? void 0 : _b.call(_a, n);
});
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach(cleanupNode);
mutation.addedNodes.forEach(mountNode);
});
});
const startObserving = () => {
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startObserving);
} else {
startObserving();
}
}
}
})();