lightview 2.2.2 → 2.3.4

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.
package/lightview.js CHANGED
@@ -3,27 +3,45 @@
3
3
  const _LV = globalThis.__LIGHTVIEW_INTERNALS__ || (globalThis.__LIGHTVIEW_INTERNALS__ = {
4
4
  currentEffect: null,
5
5
  registry: /* @__PURE__ */ new Map(),
6
- dependencyMap: /* @__PURE__ */ new WeakMap()
7
- // Tracking signals -> subscribers
6
+ // Global name -> Signal/Proxy
7
+ localRegistries: /* @__PURE__ */ new WeakMap(),
8
+ // Object/Element -> Map(name -> Signal/Proxy)
9
+ futureSignals: /* @__PURE__ */ new Map(),
10
+ // name -> Set of (signal) => void
11
+ schemas: /* @__PURE__ */ new Map(),
12
+ // name -> Schema (Draft 7+ or Shorthand)
13
+ parents: /* @__PURE__ */ new WeakMap(),
14
+ // Proxy -> Parent (Proxy/Element)
15
+ helpers: /* @__PURE__ */ new Map(),
16
+ // name -> function (used for transforms and expressions)
17
+ hooks: {
18
+ validate: (value, schema) => true
19
+ // Hook for extensions (like JPRX) to provide full validation
20
+ }
8
21
  });
22
+ const lookup = (name, scope) => {
23
+ let current = scope;
24
+ while (current && typeof current === "object") {
25
+ const registry2 = _LV.localRegistries.get(current);
26
+ if (registry2 && registry2.has(name)) return registry2.get(name);
27
+ current = current.parentElement || _LV.parents.get(current);
28
+ }
29
+ return _LV.registry.get(name);
30
+ };
9
31
  const signal = (initialValue, optionsOrName) => {
10
- let name = typeof optionsOrName === "string" ? optionsOrName : optionsOrName == null ? void 0 : optionsOrName.name;
32
+ const name = typeof optionsOrName === "string" ? optionsOrName : optionsOrName == null ? void 0 : optionsOrName.name;
11
33
  const storage = optionsOrName == null ? void 0 : optionsOrName.storage;
34
+ const scope = optionsOrName == null ? void 0 : optionsOrName.scope;
12
35
  if (name && storage) {
13
36
  try {
14
37
  const stored = storage.getItem(name);
15
- if (stored !== null) {
16
- initialValue = JSON.parse(stored);
17
- }
38
+ if (stored !== null) initialValue = JSON.parse(stored);
18
39
  } catch (e) {
19
40
  }
20
41
  }
21
42
  let value = initialValue;
22
43
  const subscribers = /* @__PURE__ */ new Set();
23
- const f = (...args) => {
24
- if (args.length === 0) return f.value;
25
- f.value = args[0];
26
- };
44
+ const f = (...args) => args.length === 0 ? f.value : f.value = args[0];
27
45
  Object.defineProperty(f, "value", {
28
46
  get() {
29
47
  if (_LV.currentEffect) {
@@ -46,21 +64,39 @@
46
64
  }
47
65
  });
48
66
  if (name) {
49
- if (_LV.registry.has(name)) {
50
- if (_LV.registry.get(name) !== f) {
51
- throw new Error(`Lightview: A signal or state with the name "${name}" is already registered.`);
52
- }
53
- } else {
54
- _LV.registry.set(name, f);
67
+ const registry2 = scope && typeof scope === "object" ? _LV.localRegistries.get(scope) || _LV.localRegistries.set(scope, /* @__PURE__ */ new Map()).get(scope) : _LV.registry;
68
+ if (registry2 && registry2.has(name) && registry2.get(name) !== f) {
69
+ throw new Error(`Lightview: A signal or state with the name "${name}" is already registered.`);
70
+ }
71
+ if (registry2) registry2.set(name, f);
72
+ const futures = _LV.futureSignals.get(name);
73
+ if (futures) {
74
+ futures.forEach((resolve) => resolve(f));
55
75
  }
56
76
  }
57
77
  return f;
58
78
  };
59
- const getSignal = (name, defaultValue) => {
60
- if (!_LV.registry.has(name) && defaultValue !== void 0) {
61
- return signal(defaultValue, name);
62
- }
63
- return _LV.registry.get(name);
79
+ const getSignal = (name, defaultValueOrOptions) => {
80
+ const options = typeof defaultValueOrOptions === "object" && defaultValueOrOptions !== null ? defaultValueOrOptions : { defaultValue: defaultValueOrOptions };
81
+ const { scope, defaultValue } = options;
82
+ const existing = lookup(name, scope);
83
+ if (existing) return existing;
84
+ if (defaultValue !== void 0) return signal(defaultValue, { name, scope });
85
+ const future = signal(void 0);
86
+ const handler = (realSignal) => {
87
+ const hasValue = realSignal && (typeof realSignal === "object" || typeof realSignal === "function") && "value" in realSignal;
88
+ if (hasValue) {
89
+ future.value = realSignal.value;
90
+ effect(() => {
91
+ future.value = realSignal.value;
92
+ });
93
+ } else {
94
+ future.value = realSignal;
95
+ }
96
+ };
97
+ if (!_LV.futureSignals.has(name)) _LV.futureSignals.set(name, /* @__PURE__ */ new Set());
98
+ _LV.futureSignals.get(name).add(handler);
99
+ return future;
64
100
  };
65
101
  signal.get = getSignal;
66
102
  const effect = (fn) => {
@@ -96,9 +132,66 @@
96
132
  return sig;
97
133
  };
98
134
  const getRegistry = () => _LV.registry;
135
+ const internals = _LV;
136
+ const stateCache = /* @__PURE__ */ new WeakMap();
137
+ const stateSignals = /* @__PURE__ */ new WeakMap();
138
+ const stateSchemas = /* @__PURE__ */ new WeakMap();
139
+ const { parents, schemas, hooks } = internals;
140
+ const validate = (target, prop, value, schema) => {
141
+ var _a, _b;
142
+ const current = target[prop];
143
+ const type = typeof current;
144
+ const isNew = !(prop in target);
145
+ let behavior = schema;
146
+ if (typeof schema === "object" && schema !== null) behavior = schema.type;
147
+ if (behavior === "auto" && isNew) throw new Error(`Lightview: Cannot add new property "${prop}" to fixed 'auto' state.`);
148
+ if (behavior === "polymorphic" || typeof behavior === "object" && (behavior == null ? void 0 : behavior.coerce)) {
149
+ if (type === "number") return Number(value);
150
+ if (type === "boolean") return Boolean(value);
151
+ if (type === "string") return String(value);
152
+ } else if (behavior === "auto" || behavior === "dynamic") {
153
+ if (!isNew && typeof value !== type) {
154
+ throw new Error(`Lightview: Type mismatch for "${prop}". Expected ${type}, got ${typeof value}.`);
155
+ }
156
+ }
157
+ if (typeof schema === "object" && schema !== null && schema.transform) {
158
+ const trans = schema.transform;
159
+ const transformFn = typeof trans === "function" ? trans : internals.helpers.get(trans) || ((_b = (_a = globalThis.Lightview) == null ? void 0 : _a.helpers) == null ? void 0 : _b[trans]);
160
+ if (transformFn) value = transformFn(value);
161
+ }
162
+ if (hooks.validate(value, schema) === false) {
163
+ throw new Error(`Lightview: Validation failed for "${prop}".`);
164
+ }
165
+ return value;
166
+ };
99
167
  const protoMethods = (proto, test) => Object.getOwnPropertyNames(proto).filter((k) => typeof proto[k] === "function" && test(k));
100
- protoMethods(Date.prototype, (k) => /^(to|get|valueOf)/.test(k));
101
- protoMethods(Date.prototype, (k) => /^set/.test(k));
168
+ const DATE_TRACKING = protoMethods(Date.prototype, (k) => /^(to|get|valueOf)/.test(k));
169
+ const DATE_MUTATING = protoMethods(Date.prototype, (k) => /^set/.test(k));
170
+ const ARRAY_TRACKING = [
171
+ "map",
172
+ "forEach",
173
+ "filter",
174
+ "find",
175
+ "findIndex",
176
+ "some",
177
+ "every",
178
+ "reduce",
179
+ "reduceRight",
180
+ "includes",
181
+ "indexOf",
182
+ "lastIndexOf",
183
+ "join",
184
+ "slice",
185
+ "concat",
186
+ "flat",
187
+ "flatMap",
188
+ "at",
189
+ "entries",
190
+ "keys",
191
+ "values"
192
+ ];
193
+ const ARRAY_MUTATING = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"];
194
+ const ARRAY_ITERATION = ["map", "forEach", "filter", "find", "findIndex", "some", "every", "flatMap"];
102
195
  const getOrSet = (map, key, factory) => {
103
196
  let v = map.get(key);
104
197
  if (!v) {
@@ -107,6 +200,169 @@
107
200
  }
108
201
  return v;
109
202
  };
203
+ const proxyGet = (target, prop, receiver, signals) => {
204
+ if (prop === "__parent__") return parents.get(receiver);
205
+ if (!signals.has(prop)) {
206
+ signals.set(prop, signal(Reflect.get(target, prop, receiver)));
207
+ }
208
+ const signal$1 = signals.get(prop);
209
+ const val = signal$1.value;
210
+ if (typeof val === "object" && val !== null) {
211
+ const childProxy = state(val);
212
+ parents.set(childProxy, receiver);
213
+ return childProxy;
214
+ }
215
+ return val;
216
+ };
217
+ const proxySet = (target, prop, value, receiver, signals) => {
218
+ const schema = stateSchemas.get(receiver);
219
+ const validatedValue = schema ? validate(target, prop, value, schema) : value;
220
+ if (!signals.has(prop)) {
221
+ signals.set(prop, signal(Reflect.get(target, prop, receiver)));
222
+ }
223
+ const success = Reflect.set(target, prop, validatedValue, receiver);
224
+ const signal$1 = signals.get(prop);
225
+ if (success && signal$1) signal$1.value = validatedValue;
226
+ return success;
227
+ };
228
+ const createSpecialProxy = (obj, monitor, trackingProps = []) => {
229
+ const signals = getOrSet(stateSignals, obj, () => /* @__PURE__ */ new Map());
230
+ if (!signals.has(monitor)) {
231
+ const initialValue = typeof obj[monitor] === "function" ? obj[monitor].call(obj) : obj[monitor];
232
+ signals.set(monitor, signal(initialValue));
233
+ }
234
+ const isDate = obj instanceof Date;
235
+ const isArray = Array.isArray(obj);
236
+ const trackingMethods = isDate ? DATE_TRACKING : isArray ? ARRAY_TRACKING : trackingProps;
237
+ const mutatingMethods = isDate ? DATE_MUTATING : isArray ? ARRAY_MUTATING : [];
238
+ return new Proxy(obj, {
239
+ get(target, prop, receiver) {
240
+ if (prop === "__parent__") return parents.get(receiver);
241
+ const value = target[prop];
242
+ if (typeof value === "function") {
243
+ const isTracking = trackingMethods.includes(prop);
244
+ const isMutating = mutatingMethods.includes(prop);
245
+ return function(...args) {
246
+ if (isTracking) {
247
+ const sig = signals.get(monitor);
248
+ if (sig) void sig.value;
249
+ }
250
+ const startValue = typeof target[monitor] === "function" ? target[monitor].call(target) : target[monitor];
251
+ if (isArray && ARRAY_ITERATION.includes(prop) && typeof args[0] === "function") {
252
+ const originalCallback = args[0];
253
+ args[0] = function(element2, index, array) {
254
+ const wrappedElement = typeof element2 === "object" && element2 !== null ? state(element2) : element2;
255
+ if (wrappedElement && typeof wrappedElement === "object") {
256
+ parents.set(wrappedElement, receiver);
257
+ }
258
+ return originalCallback.call(this, wrappedElement, index, array);
259
+ };
260
+ }
261
+ const result = value.apply(target, args);
262
+ const endValue = typeof target[monitor] === "function" ? target[monitor].call(target) : target[monitor];
263
+ if (startValue !== endValue || isMutating) {
264
+ const sig = signals.get(monitor);
265
+ if (sig && sig.value !== endValue) {
266
+ sig.value = endValue;
267
+ }
268
+ }
269
+ return result;
270
+ };
271
+ }
272
+ if (prop === monitor) {
273
+ const sig = signals.get(monitor);
274
+ return sig ? sig.value : Reflect.get(target, prop, receiver);
275
+ }
276
+ if (isArray && !isNaN(parseInt(prop))) {
277
+ const monitorSig = signals.get(monitor);
278
+ if (monitorSig) void monitorSig.value;
279
+ }
280
+ return proxyGet(target, prop, receiver, signals);
281
+ },
282
+ set(target, prop, value, receiver) {
283
+ if (prop === monitor) {
284
+ const success = Reflect.set(target, prop, value, receiver);
285
+ if (success) {
286
+ const sig = signals.get(monitor);
287
+ if (sig) sig.value = value;
288
+ }
289
+ return success;
290
+ }
291
+ return proxySet(target, prop, value, receiver, signals);
292
+ }
293
+ });
294
+ };
295
+ const state = (obj, optionsOrName) => {
296
+ if (typeof obj !== "object" || obj === null) return obj;
297
+ const name = typeof optionsOrName === "string" ? optionsOrName : optionsOrName == null ? void 0 : optionsOrName.name;
298
+ const storage = optionsOrName == null ? void 0 : optionsOrName.storage;
299
+ const scope = optionsOrName == null ? void 0 : optionsOrName.scope;
300
+ const schema = optionsOrName == null ? void 0 : optionsOrName.schema;
301
+ if (name && storage) {
302
+ try {
303
+ const item = storage.getItem(name);
304
+ if (item) {
305
+ const loaded = JSON.parse(item);
306
+ Array.isArray(obj) && Array.isArray(loaded) ? (obj.length = 0, obj.push(...loaded)) : Object.assign(obj, loaded);
307
+ }
308
+ } catch (e) {
309
+ }
310
+ }
311
+ let proxy = stateCache.get(obj);
312
+ if (!proxy) {
313
+ const isArray = Array.isArray(obj), isDate = obj instanceof Date;
314
+ const isSpecial = isArray || isDate;
315
+ const monitor = isArray ? "length" : isDate ? "getTime" : null;
316
+ if (isSpecial || !(obj instanceof RegExp || obj instanceof Map || obj instanceof Set || obj instanceof WeakMap || obj instanceof WeakSet)) {
317
+ proxy = isSpecial ? createSpecialProxy(obj, monitor) : new Proxy(obj, {
318
+ get(t, p, r) {
319
+ if (p === "__parent__") return parents.get(r);
320
+ return proxyGet(t, p, r, getOrSet(stateSignals, t, () => /* @__PURE__ */ new Map()));
321
+ },
322
+ set(t, p, v, r) {
323
+ return proxySet(t, p, v, r, getOrSet(stateSignals, t, () => /* @__PURE__ */ new Map()));
324
+ }
325
+ });
326
+ stateCache.set(obj, proxy);
327
+ } else return obj;
328
+ }
329
+ if (schema) stateSchemas.set(proxy, schema);
330
+ if (name && storage) {
331
+ effect(() => {
332
+ try {
333
+ storage.setItem(name, JSON.stringify(proxy));
334
+ } catch (e) {
335
+ }
336
+ });
337
+ }
338
+ if (name) {
339
+ const registry2 = scope && typeof scope === "object" ? internals.localRegistries.get(scope) || internals.localRegistries.set(scope, /* @__PURE__ */ new Map()).get(scope) : getRegistry();
340
+ if (registry2 && registry2.has(name) && registry2.get(name) !== proxy) {
341
+ throw new Error(`Lightview: A signal or state with the name "${name}" is already registered.`);
342
+ }
343
+ if (registry2) registry2.set(name, proxy);
344
+ const futures = internals.futureSignals.get(name);
345
+ if (futures) {
346
+ futures.forEach((resolve) => resolve(proxy));
347
+ }
348
+ }
349
+ return proxy;
350
+ };
351
+ const getState = (name, defaultValueOrOptions) => {
352
+ const options = typeof defaultValueOrOptions === "object" && defaultValueOrOptions !== null ? defaultValueOrOptions : { defaultValue: defaultValueOrOptions };
353
+ const { scope, defaultValue } = options;
354
+ const existing = lookup(name, scope);
355
+ if (existing) return existing;
356
+ if (defaultValue !== void 0) return state(defaultValue, { name, scope });
357
+ const future = signal(void 0);
358
+ const handler = (realState) => {
359
+ future.value = realState;
360
+ };
361
+ if (!internals.futureSignals.has(name)) internals.futureSignals.set(name, /* @__PURE__ */ new Set());
362
+ internals.futureSignals.get(name).add(handler);
363
+ return future;
364
+ };
365
+ state.get = getState;
110
366
  const core = {
111
367
  get currentEffect() {
112
368
  return (globalThis.__LIGHTVIEW_INTERNALS__ || (globalThis.__LIGHTVIEW_INTERNALS__ = {})).currentEffect;
@@ -115,10 +371,42 @@
115
371
  const nodeState = /* @__PURE__ */ new WeakMap();
116
372
  const nodeStateFactory = () => ({ effects: [], onmount: null, onunmount: null });
117
373
  const registry = getRegistry();
374
+ const scrollMemory = /* @__PURE__ */ new Map();
375
+ const initScrollMemory = () => {
376
+ if (typeof document === "undefined") return;
377
+ document.addEventListener("scroll", (e) => {
378
+ const el = e.target;
379
+ if (el === document || el === document.documentElement) return;
380
+ const key = el.id || el.getAttribute && el.getAttribute("data-preserve-scroll");
381
+ if (key) {
382
+ scrollMemory.set(key, { top: el.scrollTop, left: el.scrollLeft });
383
+ }
384
+ }, true);
385
+ };
386
+ if (typeof document !== "undefined") {
387
+ if (document.readyState === "loading") {
388
+ document.addEventListener("DOMContentLoaded", initScrollMemory);
389
+ } else {
390
+ initScrollMemory();
391
+ }
392
+ }
393
+ const saveScrolls = () => new Map(scrollMemory);
394
+ const restoreScrolls = (map, root = document) => {
395
+ if (!map || map.size === 0) return;
396
+ requestAnimationFrame(() => {
397
+ map.forEach((pos, key) => {
398
+ const node = document.getElementById(key) || document.querySelector(`[data-preserve-scroll="${key}"]`);
399
+ if (node) {
400
+ node.scrollTop = pos.top;
401
+ node.scrollLeft = pos.left;
402
+ }
403
+ });
404
+ });
405
+ };
118
406
  const trackEffect = (node, effectFn) => {
119
- const state = getOrSet(nodeState, node, nodeStateFactory);
120
- if (!state.effects) state.effects = [];
121
- state.effects.push(effectFn);
407
+ const state2 = getOrSet(nodeState, node, nodeStateFactory);
408
+ if (!state2.effects) state2.effects = [];
409
+ state2.effects.push(effectFn);
122
410
  };
123
411
  const SHADOW_DOM_MARKER = Symbol("lightview.shadowDOM");
124
412
  const createShadowDOMMarker = (attributes, children) => ({
@@ -299,8 +587,8 @@
299
587
  const reactiveAttrs = {};
300
588
  for (let [key, value] of Object.entries(attributes)) {
301
589
  if (key === "onmount" || key === "onunmount") {
302
- const state = getOrSet(nodeState, domNode, nodeStateFactory);
303
- state[key] = value;
590
+ const state2 = getOrSet(nodeState, domNode, nodeStateFactory);
591
+ state2[key] = value;
304
592
  if (key === "onmount" && domNode.isConnected) {
305
593
  value(domNode);
306
594
  }
@@ -312,6 +600,26 @@
312
600
  domNode.setAttribute(key, value);
313
601
  }
314
602
  reactiveAttrs[key] = value;
603
+ } else if (typeof value === "object" && value !== null && Lightview.hooks.processAttribute) {
604
+ const processed = Lightview.hooks.processAttribute(domNode, key, value);
605
+ if (processed !== void 0) {
606
+ reactiveAttrs[key] = processed;
607
+ } else if (key === "style") {
608
+ Object.entries(value).forEach(([styleKey, styleValue]) => {
609
+ if (typeof styleValue === "function") {
610
+ const runner = effect(() => {
611
+ domNode.style[styleKey] = styleValue();
612
+ });
613
+ trackEffect(domNode, runner);
614
+ } else {
615
+ domNode.style[styleKey] = styleValue;
616
+ }
617
+ });
618
+ reactiveAttrs[key] = value;
619
+ } else {
620
+ setAttributeValue(domNode, key, value);
621
+ reactiveAttrs[key] = value;
622
+ }
315
623
  } else if (typeof value === "function") {
316
624
  const runner = effect(() => {
317
625
  const result = value();
@@ -323,18 +631,6 @@
323
631
  });
324
632
  trackEffect(domNode, runner);
325
633
  reactiveAttrs[key] = value;
326
- } else if (key === "style" && typeof value === "object") {
327
- Object.entries(value).forEach(([styleKey, styleValue]) => {
328
- if (typeof styleValue === "function") {
329
- const runner = effect(() => {
330
- domNode.style[styleKey] = styleValue();
331
- });
332
- trackEffect(domNode, runner);
333
- } else {
334
- domNode.style[styleKey] = styleValue;
335
- }
336
- });
337
- reactiveAttrs[key] = value;
338
634
  } else {
339
635
  setAttributeValue(domNode, key, value);
340
636
  reactiveAttrs[key] = value;
@@ -513,6 +809,9 @@
513
809
  }
514
810
  });
515
811
  const Lightview = {
812
+ state,
813
+ getState,
814
+ registerSchema: (name, definition) => internals.schemas.set(name, definition),
516
815
  signal,
517
816
  get: signal.get,
518
817
  computed,
@@ -527,14 +826,24 @@
527
826
  hooks: {
528
827
  onNonStandardHref: null,
529
828
  processChild: null,
530
- validateUrl: null
829
+ processAttribute: null,
830
+ validateUrl: null,
831
+ validate: (value, schema) => internals.hooks.validate(value, schema)
531
832
  },
532
833
  // Internals exposed for extensions
533
834
  internals: {
534
835
  core,
535
836
  domToElement,
536
837
  wrapDomElement,
537
- setupChildren
838
+ setupChildren,
839
+ trackEffect,
840
+ saveScrolls,
841
+ restoreScrolls,
842
+ localRegistries: internals.localRegistries,
843
+ futureSignals: internals.futureSignals,
844
+ schemas: internals.schemas,
845
+ parents: internals.parents,
846
+ hooks: internals.hooks
538
847
  }
539
848
  };
540
849
  if (typeof module !== "undefined" && module.exports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "2.2.2",
3
+ "version": "2.3.4",
4
4
  "description": "A lightweight reactive UI library with features of Bau, Juris, and HTMX",
5
5
  "main": "lightview.js",
6
6
  "workspaces": [
@@ -35,6 +35,7 @@
35
35
  "wrangler": "^4.54.0"
36
36
  },
37
37
  "dependencies": {
38
+ "jprx": "^1.2.0",
38
39
  "linkedom": "^0.18.12",
39
40
  "marked": "^17.0.1"
40
41
  }