lightview 2.0.8 → 2.1.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 (180) hide show
  1. package/README.md +47 -1283
  2. package/build-bundles.mjs +109 -0
  3. package/cdom/helpers/array.js +70 -0
  4. package/cdom/helpers/compare.js +26 -0
  5. package/cdom/helpers/conditional.js +34 -0
  6. package/cdom/helpers/datetime.js +54 -0
  7. package/cdom/helpers/format.js +20 -0
  8. package/cdom/helpers/logic.js +24 -0
  9. package/cdom/helpers/lookup.js +25 -0
  10. package/cdom/helpers/math.js +34 -0
  11. package/cdom/helpers/network.js +41 -0
  12. package/cdom/helpers/state.js +77 -0
  13. package/cdom/helpers/stats.js +39 -0
  14. package/cdom/helpers/string.js +49 -0
  15. package/cdom/parser.js +602 -0
  16. package/components/actions/button.js +19 -6
  17. package/components/actions/dropdown.js +6 -6
  18. package/components/actions/modal.js +9 -9
  19. package/components/actions/swap.js +31 -8
  20. package/components/daisyui.js +1 -1
  21. package/components/data-display/accordion.js +6 -6
  22. package/components/data-display/alert.js +17 -7
  23. package/components/data-display/avatar.js +7 -7
  24. package/components/data-display/badge.js +14 -6
  25. package/components/data-display/card.js +7 -7
  26. package/components/data-display/carousel.js +4 -4
  27. package/components/data-display/chart.js +8 -8
  28. package/components/data-display/chat.js +7 -7
  29. package/components/data-display/collapse.js +5 -5
  30. package/components/data-display/countdown.js +3 -3
  31. package/components/data-display/diff.js +6 -6
  32. package/components/data-display/kbd.js +12 -6
  33. package/components/data-display/loading.js +14 -6
  34. package/components/data-display/progress.js +14 -6
  35. package/components/data-display/radial-progress.js +15 -6
  36. package/components/data-display/stats.js +9 -9
  37. package/components/data-display/table.js +9 -9
  38. package/components/data-display/timeline.js +8 -8
  39. package/components/data-display/toast.js +3 -3
  40. package/components/data-display/tooltip.js +20 -3
  41. package/components/data-input/checkbox.js +5 -5
  42. package/components/data-input/file-input.js +3 -3
  43. package/components/data-input/input.js +5 -5
  44. package/components/data-input/radio.js +9 -9
  45. package/components/data-input/range.js +3 -3
  46. package/components/data-input/rating.js +3 -3
  47. package/components/data-input/select.js +5 -5
  48. package/components/data-input/textarea.js +3 -3
  49. package/components/data-input/toggle.js +5 -5
  50. package/components/layout/divider.js +24 -4
  51. package/components/layout/drawer.js +7 -7
  52. package/components/layout/footer.js +5 -5
  53. package/components/layout/hero.js +5 -5
  54. package/components/layout/indicator.js +18 -4
  55. package/components/layout/join.js +4 -4
  56. package/components/layout/navbar.js +6 -6
  57. package/components/navigation/breadcrumbs.js +4 -4
  58. package/components/navigation/dock.js +5 -5
  59. package/components/navigation/menu.js +6 -6
  60. package/components/navigation/pagination.js +3 -3
  61. package/components/navigation/steps.js +4 -4
  62. package/components/navigation/tabs.js +296 -21
  63. package/components/theme/theme-switch.js +30 -30
  64. package/docs/about.html +141 -7
  65. package/docs/api/computed.html +1 -6
  66. package/docs/api/effects.html +1 -7
  67. package/docs/api/elements.html +130 -58
  68. package/docs/api/enhance.html +1 -6
  69. package/docs/api/hypermedia.html +182 -23
  70. package/docs/api/index.html +11 -12
  71. package/docs/api/nav.html +35 -4
  72. package/docs/api/signals.html +1 -6
  73. package/docs/api/state.html +1 -6
  74. package/docs/assets/js/examplify-sandbox.html +2 -2
  75. package/docs/assets/js/examplify.js +15 -15
  76. package/docs/cdom-nav.html +29 -0
  77. package/docs/cdom.html +362 -0
  78. package/docs/components/accordion.html +4 -4
  79. package/docs/components/alert.html +12 -12
  80. package/docs/components/avatar.html +4 -4
  81. package/docs/components/badge.html +59 -4
  82. package/docs/components/breadcrumbs.html +3 -3
  83. package/docs/components/button.html +83 -97
  84. package/docs/components/card.html +4 -4
  85. package/docs/components/carousel.html +3 -3
  86. package/docs/components/chart-area.html +6 -6
  87. package/docs/components/chart-bar.html +6 -6
  88. package/docs/components/chart-column.html +6 -6
  89. package/docs/components/chart-line.html +6 -6
  90. package/docs/components/chart-pie.html +6 -6
  91. package/docs/components/chart.html +2 -2
  92. package/docs/components/chat.html +4 -4
  93. package/docs/components/checkbox.html +4 -4
  94. package/docs/components/collapse.html +4 -4
  95. package/docs/components/component-nav.html +1 -1
  96. package/docs/components/countdown.html +4 -4
  97. package/docs/components/diff.html +3 -3
  98. package/docs/components/divider.html +68 -24
  99. package/docs/components/dock.html +3 -3
  100. package/docs/components/drawer.html +4 -4
  101. package/docs/components/dropdown.html +4 -4
  102. package/docs/components/file-input.html +4 -4
  103. package/docs/components/footer.html +3 -3
  104. package/docs/components/gallery.html +2 -2
  105. package/docs/components/hero.html +3 -3
  106. package/docs/components/index.css +5 -3
  107. package/docs/components/index.html +4 -4
  108. package/docs/components/indicator.html +88 -34
  109. package/docs/components/input.html +4 -4
  110. package/docs/components/join.html +3 -3
  111. package/docs/components/kbd.html +67 -28
  112. package/docs/components/loading.html +59 -43
  113. package/docs/components/menu.html +4 -4
  114. package/docs/components/modal.html +4 -4
  115. package/docs/components/navbar.html +3 -3
  116. package/docs/components/pagination.html +3 -3
  117. package/docs/components/progress.html +48 -7
  118. package/docs/components/radial-progress.html +35 -15
  119. package/docs/components/radio.html +4 -4
  120. package/docs/components/range.html +4 -4
  121. package/docs/components/rating.html +4 -4
  122. package/docs/components/select.html +4 -4
  123. package/docs/components/sidebar-setup.js +1 -1
  124. package/docs/components/spinner.html +4 -4
  125. package/docs/components/stats.html +4 -4
  126. package/docs/components/steps.html +3 -3
  127. package/docs/components/swap.html +187 -104
  128. package/docs/components/switch.html +4 -4
  129. package/docs/components/table.html +4 -4
  130. package/docs/components/tabs.html +147 -279
  131. package/docs/components/text-input.html +4 -4
  132. package/docs/components/textarea.html +4 -4
  133. package/docs/components/timeline.html +4 -4
  134. package/docs/components/toast.html +4 -4
  135. package/docs/components/toggle.html +4 -4
  136. package/docs/components/tooltip.html +75 -35
  137. package/docs/examples/getting-started-example.html +1 -1
  138. package/docs/examples/index.html +1 -2
  139. package/docs/getting-started/index.html +112 -19
  140. package/docs/index.html +1 -6
  141. package/docs/router-nav.html +13 -0
  142. package/docs/router.html +60 -17
  143. package/docs/styles/index.html +2 -7
  144. package/docs/syntax-nav.html +10 -0
  145. package/docs/syntax.html +146 -0
  146. package/functions/_middleware.js +17 -10
  147. package/functions/processServerScripts.js +127 -0
  148. package/index.html +8 -8
  149. package/lightview-all.js +1 -0
  150. package/lightview-cdom.js +1 -0
  151. package/lightview-router.js +71 -22
  152. package/lightview-x.js +1 -1247
  153. package/lightview.js +1 -760
  154. package/lightview.js.bak +1 -0
  155. package/package.json +37 -26
  156. package/scripts/analysis/README.md +2 -0
  157. package/scripts/analysis/analyze.js +266 -0
  158. package/scripts/analysis/latest_metrics.md +185 -0
  159. package/src/lightview-all.js +10 -0
  160. package/src/lightview-cdom.js +305 -0
  161. package/src/lightview-x.js +1581 -0
  162. package/src/lightview.js +694 -0
  163. package/src/reactivity/signal.js +133 -0
  164. package/src/reactivity/state.js +217 -0
  165. package/test-text-tag.js +6 -0
  166. package/tests/cdom/fixtures/helpers.cdomc +62 -0
  167. package/tests/cdom/fixtures/user.cdom +14 -0
  168. package/tests/cdom/fixtures/user.cdomc +12 -0
  169. package/tests/cdom/fixtures/user.odom +18 -0
  170. package/tests/cdom/fixtures/user.vdom +11 -0
  171. package/tests/cdom/helpers.test.js +121 -0
  172. package/tests/cdom/loader.test.js +125 -0
  173. package/tests/cdom/parser.test.js +108 -0
  174. package/tests/cdom/reactivity.test.js +186 -0
  175. package/tests/text-tag.test.js +77 -0
  176. package/vite.config.mjs +52 -0
  177. package/wrangler.toml +6 -0
  178. package/components/data-display/skeleton.js +0 -66
  179. package/docs/components/skeleton.html +0 -447
  180. package/docs/playground.html +0 -416
package/lightview.js CHANGED
@@ -1,760 +1 @@
1
- (() => {
2
- /**
3
- * LIGHTVIEW CORE
4
- * A minimalist library for signals-based reactivity and functional UI components.
5
- */
6
-
7
- // ============= SIGNALS =============
8
-
9
- let currentEffect = null;
10
-
11
-
12
- /**
13
- * Helper to get a value from a Map or create and set it if it doesn't exist.
14
- */
15
- const getOrSet = (map, key, factory) => {
16
- let v = map.get(key);
17
- if (!v) {
18
- v = factory();
19
- map.set(key, v);
20
- }
21
- return v;
22
- };
23
-
24
- const nodeState = new WeakMap();
25
- const nodeStateFactory = () => ({ effects: [], onmount: null, onunmount: null });
26
-
27
- const signalRegistry = new Map();
28
-
29
- /**
30
- * Creates a reactive signal.
31
- * @param {*} initialValue - The initial value of the signal.
32
- * @param {Object|string} [optionsOrName] - Optional name (for registry) or options object.
33
- */
34
- const signal = (initialValue, optionsOrName) => {
35
- let name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name;
36
- const storage = optionsOrName?.storage;
37
-
38
- if (name && storage) {
39
- const stored = storage.getItem(name);
40
- if (stored !== null) {
41
- initialValue = stored;
42
- try {
43
- initialValue = JSON.parse(stored);
44
- } catch (e) {
45
- // Ignore storage errors
46
- }
47
- }
48
- }
49
-
50
- let value = initialValue;
51
- const subscribers = new Set();
52
-
53
- const f = (...args) => {
54
- if (args.length === 0) return f.value;
55
- f.value = args[0];
56
- };
57
-
58
- Object.defineProperty(f, 'value', {
59
- get() {
60
- if (currentEffect) {
61
- subscribers.add(currentEffect);
62
- currentEffect.dependencies.add(subscribers);
63
- }
64
- return value;
65
- },
66
- set(newValue) {
67
- if (value !== newValue) {
68
- value = newValue;
69
- if (name && storage) {
70
- try {
71
- storage.setItem(name, JSON.stringify(value));
72
- } catch (e) {
73
- // Ignore storage errors
74
- }
75
- }
76
- // Copy subscribers to avoid infinite loop when effect re-subscribes during iteration
77
- [...subscribers].forEach(effect => effect());
78
- }
79
- }
80
- });
81
-
82
- if (name) {
83
- signalRegistry.set(name, f);
84
- }
85
-
86
- return f;
87
- };
88
-
89
- signal.get = (name, defaultValue) => {
90
- if (!signalRegistry.has(name) && defaultValue !== undefined) {
91
- return signal(defaultValue, name);
92
- }
93
- return signalRegistry.get(name);
94
- };
95
-
96
- /**
97
- * Creates a side-effect that automatically tracks and re-runs when its signal dependencies change.
98
- * @param {Function} fn - The function to execute as an effect.
99
- */
100
- const effect = (fn) => {
101
- const execute = () => {
102
- if (!execute.active || execute.running) return;
103
- // Cleanup old dependencies
104
- execute.dependencies.forEach(dep => dep.delete(execute));
105
- execute.dependencies.clear();
106
-
107
- execute.running = true;
108
- currentEffect = execute;
109
- try {
110
- fn();
111
- } finally {
112
- currentEffect = null;
113
- execute.running = false;
114
- }
115
- };
116
-
117
- execute.active = true;
118
- execute.running = false;
119
- execute.dependencies = new Set();
120
- execute.stop = () => {
121
- execute.dependencies.forEach(dep => dep.delete(execute));
122
- execute.dependencies.clear();
123
- execute.active = false;
124
- };
125
- execute();
126
- return execute;
127
- };
128
-
129
- /**
130
- * Assocates an effect with a DOM node for automatic cleanup when the node is removed.
131
- */
132
- const trackEffect = (node, effectFn) => {
133
- const state = getOrSet(nodeState, node, nodeStateFactory);
134
- if (!state.effects) state.effects = [];
135
- state.effects.push(effectFn);
136
- };
137
-
138
- /**
139
- * Creates a read-only signal derived from other signals.
140
- */
141
- const computed = (fn) => {
142
- const sig = signal(undefined);
143
- effect(() => {
144
- sig.value = fn();
145
- });
146
- return sig;
147
- };
148
-
149
-
150
- // ============= SHADOW DOM SUPPORT =============
151
- // Marker symbol to identify shadowDOM directives
152
- const SHADOW_DOM_MARKER = Symbol('lightview.shadowDOM');
153
-
154
- /**
155
- * Create a shadowDOM directive marker
156
- * @param {Object} attributes - { mode: 'open'|'closed', styles?: string[], adoptedStyleSheets?: CSSStyleSheet[] }
157
- * @param {Array} children - Children to render inside the shadow root
158
- * @returns {Object} - Marker object for setupChildren to process
159
- */
160
- const createShadowDOMMarker = (attributes, children) => ({
161
- [SHADOW_DOM_MARKER]: true,
162
- mode: attributes.mode || 'open',
163
- styles: attributes.styles || [],
164
- adoptedStyleSheets: attributes.adoptedStyleSheets || [],
165
- children
166
- });
167
-
168
- /**
169
- * Check if an object is a shadowDOM marker
170
- */
171
- const isShadowDOMMarker = (obj) => obj && typeof obj === 'object' && obj[SHADOW_DOM_MARKER] === true;
172
-
173
- /**
174
- * Process a shadowDOM marker by attaching shadow root and rendering children
175
- * @param {Object} marker - The shadowDOM marker
176
- * @param {HTMLElement} parentNode - The DOM node to attach shadow to
177
- */
178
- const processShadowDOM = (marker, parentNode) => {
179
- // Don't attach if already has shadow root
180
- if (parentNode.shadowRoot) {
181
- console.warn('Lightview: Element already has a shadowRoot, skipping shadowDOM directive');
182
- return;
183
- }
184
-
185
- // Attach shadow root
186
- const shadowRoot = parentNode.attachShadow({ mode: marker.mode });
187
-
188
- // Split adoptedStyleSheets into sheets and urls
189
- const sheets = [];
190
- const linkUrls = [...(marker.styles || [])];
191
-
192
- if (marker.adoptedStyleSheets && marker.adoptedStyleSheets.length > 0) {
193
- marker.adoptedStyleSheets.forEach(item => {
194
- if (item instanceof CSSStyleSheet) {
195
- sheets.push(item);
196
- } else if (typeof item === 'string') {
197
- linkUrls.push(item);
198
- }
199
- });
200
- }
201
-
202
- // Handle adoptedStyleSheets (modern, efficient approach)
203
- if (sheets.length > 0) {
204
- try {
205
- shadowRoot.adoptedStyleSheets = sheets;
206
- } catch (e) {
207
- console.warn('Lightview: adoptedStyleSheets not supported');
208
- }
209
- }
210
-
211
- // Inject stylesheet links
212
- for (const styleUrl of linkUrls) {
213
- const link = document.createElement('link');
214
- link.rel = 'stylesheet';
215
- link.href = styleUrl;
216
- shadowRoot.appendChild(link);
217
- }
218
-
219
- // Setup children inside shadow root
220
- if (marker.children && marker.children.length > 0) {
221
- setupChildrenInTarget(marker.children, shadowRoot);
222
- }
223
- };
224
-
225
- // ============= REACTIVE UI =============
226
- let inSVG = false;
227
-
228
- const domToElement = new WeakMap();
229
-
230
- /**
231
- * Wraps a native DOM element in a Lightview reactive proxy.
232
- */
233
- const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
234
- const el = {
235
- tag,
236
- attributes,
237
- children,
238
- get domEl() { return domNode; }
239
- };
240
- const proxy = makeReactive(el);
241
- domToElement.set(domNode, proxy);
242
- return proxy;
243
- };
244
-
245
- /**
246
- * The core virtual-DOM-to-real-DOM factory.
247
- * Handles tag functions (components), shadow DOM directives, and SVG namespaces.
248
- */
249
- const element = (tag, attributes = {}, children = []) => {
250
- if (customTags[tag]) tag = customTags[tag];
251
- // If tag is a function (component), call it and process the result
252
- if (typeof tag === 'function') {
253
- const result = tag({ ...attributes }, children);
254
- return processComponentResult(result);
255
- }
256
-
257
- // Special handling for shadowDOM pseudo-element
258
- if (tag === 'shadowDOM') {
259
- return createShadowDOMMarker(attributes, children);
260
- }
261
-
262
- const isSVG = tag.toLowerCase() === 'svg';
263
- const wasInSVG = inSVG;
264
- if (isSVG) inSVG = true;
265
-
266
- const domNode = inSVG
267
- ? document.createElementNS('http://www.w3.org/2000/svg', tag)
268
- : document.createElement(tag);
269
-
270
- const proxy = wrapDomElement(domNode, tag, attributes, children);
271
- proxy.attributes = attributes;
272
- proxy.children = children;
273
-
274
- if (isSVG) inSVG = wasInSVG;
275
- return proxy;
276
- };
277
-
278
- // Process component function return value (HTML string, DOM node, vDOM, or Object DOM)
279
- const processComponentResult = (result) => {
280
- if (!result) return null;
281
-
282
- // Already a Lightview element
283
- if (result.domEl) return result;
284
-
285
- // DOM node - wrap it
286
- if (result instanceof HTMLElement) {
287
- return wrapDomElement(result, result.tagName.toLowerCase(), {}, []);
288
- }
289
-
290
- // HTML string - parse and wrap
291
- if (typeof result === 'string') {
292
- const template = document.createElement('template');
293
- template.innerHTML = result.trim();
294
- const content = template.content;
295
- // If single element, return it; otherwise wrap in a fragment-like span
296
- if (content.childNodes.length === 1 && content.firstChild instanceof HTMLElement) {
297
- const el = content.firstChild;
298
- return wrapDomElement(el, el.tagName.toLowerCase(), {}, []);
299
- } else {
300
- const wrapper = document.createElement('span');
301
- wrapper.style.display = 'contents';
302
- wrapper.appendChild(content);
303
- return wrapDomElement(wrapper, 'span', {}, []);
304
- }
305
- }
306
-
307
- // vDOM object with tag property
308
- if (typeof result === 'object' && result.tag) {
309
- return element(result.tag, result.attributes || {}, result.children || []);
310
- }
311
-
312
- // Object DOM syntax will be handled by processChild hook in lightview-x
313
- // But we can do basic detection here
314
- if (typeof result === 'object') {
315
- const keys = Object.keys(result);
316
- if (keys.length === 1 && typeof result[keys[0]] === 'object') {
317
- const tag = keys[0];
318
- const content = result[tag];
319
- const { children, ...attributes } = content;
320
- return element(tag, attributes, children || []);
321
- }
322
- }
323
-
324
- return null;
325
- };
326
-
327
- /**
328
- * Internal proxy to intercept 'attributes' and 'children' updates on an element.
329
- */
330
- const makeReactive = (el) => {
331
- const domNode = el.domEl;
332
-
333
- return new Proxy(el, {
334
- set(target, prop, value) {
335
- if (prop === 'attributes') {
336
- target[prop] = makeReactiveAttributes(value, domNode);
337
- } else if (prop === 'children') {
338
- target[prop] = setupChildren(value, domNode);
339
- } else {
340
- target[prop] = value;
341
- }
342
- return true;
343
- }
344
- });
345
- };
346
-
347
- // Properties that should be set directly on the DOM node object rather than as attributes
348
- const NODE_PROPERTIES = new Set(['value', 'checked', 'selected', 'selectedIndex', 'className', 'innerHTML', 'innerText']);
349
-
350
- // Set attribute with proper handling of boolean attributes and undefined/null values
351
- const setAttributeValue = (domNode, key, value) => {
352
- const isBool = typeof domNode[key] === 'boolean';
353
-
354
- if (NODE_PROPERTIES.has(key) || isBool) {
355
- domNode[key] = isBool ? (value !== null && value !== undefined && value !== false && value !== 'false') : value;
356
- } else if (value === null || value === undefined) {
357
- domNode.removeAttribute(key);
358
- } else {
359
- domNode.setAttribute(key, value);
360
- }
361
- };
362
-
363
- /**
364
- * Processes attributes, handling event listeners, reactive bindings, and special 'onmount' hooks.
365
- */
366
- const makeReactiveAttributes = (attributes, domNode) => {
367
- const reactiveAttrs = {};
368
-
369
- for (let [key, value] of Object.entries(attributes)) {
370
- if (key === 'onmount' || key === 'onunmount') {
371
- const state = getOrSet(nodeState, domNode, nodeStateFactory);
372
- state[key] = value;
373
-
374
- if (key === 'onmount' && domNode.isConnected) {
375
- value(domNode);
376
- }
377
- } else if (key.startsWith('on')) {
378
- // Event handler
379
- if (typeof value === 'function') {
380
- // Function handler - use addEventListener
381
- const eventName = key.slice(2).toLowerCase();
382
- domNode.addEventListener(eventName, value);
383
- } else if (typeof value === 'string') {
384
- // String handler (from parsed HTML) - use setAttribute
385
- // Browser will compile the string into a handler function
386
- domNode.setAttribute(key, value);
387
- }
388
- reactiveAttrs[key] = value;
389
- } else if (typeof value === 'function') {
390
- // Reactive binding
391
- const runner = effect(() => {
392
- const result = value();
393
- if (key === 'style' && typeof result === 'object') {
394
- Object.assign(domNode.style, result);
395
- } else {
396
- setAttributeValue(domNode, key, result);
397
- }
398
- });
399
- trackEffect(domNode, runner);
400
- reactiveAttrs[key] = value;
401
- } else if (key === 'style' && typeof value === 'object') {
402
- // Handle style object which may contain reactive values
403
- Object.entries(value).forEach(([styleKey, styleValue]) => {
404
- if (typeof styleValue === 'function') {
405
- const runner = effect(() => {
406
- domNode.style[styleKey] = styleValue();
407
- });
408
- trackEffect(domNode, runner);
409
- } else {
410
- domNode.style[styleKey] = styleValue;
411
- }
412
- });
413
- reactiveAttrs[key] = value;
414
- } else {
415
- // Static attribute - handle undefined/null/boolean properly
416
- setAttributeValue(domNode, key, value);
417
- reactiveAttrs[key] = value;
418
- }
419
- }
420
-
421
- return reactiveAttrs;
422
- };
423
-
424
- /**
425
- * Core child processing logic - shared between setupChildren and setupChildrenInTarget
426
- * @param {Array} children - Children to process
427
- * @param {HTMLElement|ShadowRoot} targetNode - Where to append children
428
- * @param {boolean} clearExisting - Whether to clear existing content
429
- * @returns {Array} - Processed child elements
430
- */
431
- /**
432
- * Core child processing logic. Recursively handles strings, arrays,
433
- * reactive functions, vDOM objects, and Shadow DOM markers.
434
- */
435
- const processChildren = (children, targetNode, clearExisting = true) => {
436
- if (clearExisting && targetNode.innerHTML !== undefined) {
437
- targetNode.innerHTML = ''; // Clear existing
438
- }
439
- const childElements = [];
440
-
441
- // Check if we're processing children of script or style elements
442
- // These need raw text content preserved, not reactive transformations
443
- const isSpecialElement = targetNode.tagName &&
444
- (targetNode.tagName.toLowerCase() === 'script' || targetNode.tagName.toLowerCase() === 'style');
445
-
446
- const flatChildren = children.flat(Infinity);
447
-
448
- for (let child of flatChildren) {
449
- // Allow extensions to transform children (e.g., template literals)
450
- // BUT skip for script/style elements which need raw content
451
- if (Lightview.hooks.processChild && !isSpecialElement) {
452
- child = Lightview.hooks.processChild(child) ?? child;
453
- }
454
-
455
- // Handle shadowDOM markers - attach shadow to parent and process shadow children
456
- if (isShadowDOMMarker(child)) {
457
- // targetNode is the parent element that should get the shadow root
458
- // For ShadowRoot targets, we can't attach another shadow, so warn
459
- if (targetNode instanceof ShadowRoot) {
460
- console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
461
- continue;
462
- }
463
- processShadowDOM(child, targetNode);
464
- continue;
465
- }
466
-
467
- const type = typeof child;
468
- if (type === 'function') {
469
- const startMarker = document.createComment('lv:s');
470
- const endMarker = document.createComment('lv:e');
471
- targetNode.appendChild(startMarker);
472
- targetNode.appendChild(endMarker);
473
-
474
- let runner;
475
- const update = () => {
476
- // 1. Cleanup: Remove everything between markers
477
- while (startMarker.nextSibling && startMarker.nextSibling !== endMarker) {
478
- startMarker.nextSibling.remove();
479
- // Note: MutationObserver handles cleanupNode(removedNode)
480
- }
481
-
482
- // 2. Execution: Get new value and process it
483
- const val = child();
484
- if (val === undefined || val === null) return;
485
-
486
- // 3. Render: Process children into a fragment and insert before endMarker
487
- const fragment = document.createDocumentFragment();
488
- const childrenToProcess = Array.isArray(val) ? val : [val];
489
-
490
- // Stop the runner if the markers are no longer in the DOM
491
- if (runner && !startMarker.isConnected) {
492
- runner.stop();
493
- return;
494
- }
495
-
496
- processChildren(childrenToProcess, fragment, false);
497
- endMarker.parentNode.insertBefore(fragment, endMarker);
498
- };
499
-
500
- runner = effect(update);
501
- trackEffect(startMarker, runner);
502
- childElements.push(child);
503
- } else if (['string', 'number', 'boolean', 'symbol'].includes(type)) {
504
- // Static text
505
- targetNode.appendChild(document.createTextNode(child));
506
- childElements.push(child);
507
- } else if (child && type === 'object' && child.tag) {
508
- // Child element (already wrapped or plain object) - tag can be string or function
509
- const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
510
- targetNode.appendChild(childEl.domEl);
511
- childElements.push(childEl);
512
- }
513
- }
514
-
515
- return childElements;
516
- };
517
-
518
- /**
519
- * Setup children in a target node (for shadow roots and other targets)
520
- * Does not clear existing content
521
- */
522
- const setupChildrenInTarget = (children, targetNode) => {
523
- return processChildren(children, targetNode, false);
524
- };
525
-
526
- /**
527
- * Setup children on a DOM node, clearing existing content
528
- */
529
- const setupChildren = (children, domNode) => {
530
- return processChildren(children, domNode, true);
531
- };
532
-
533
- // ============= EXPORTS =============
534
- /**
535
- * Enhances an existing DOM element with Lightview reactivity.
536
- */
537
- const enhance = (selectorOrNode, options = {}) => {
538
- const domNode = typeof selectorOrNode === 'string'
539
- ? document.querySelector(selectorOrNode)
540
- : selectorOrNode;
541
-
542
- // If it's already a Lightview element, use its domEl
543
- const node = domNode.domEl || domNode;
544
- if (!(node instanceof HTMLElement)) return null;
545
-
546
- const tagName = node.tagName.toLowerCase();
547
- let el = domToElement.get(node);
548
-
549
- if (!el) {
550
- el = wrapDomElement(node, tagName);
551
- }
552
-
553
- const { innerText, innerHTML, ...attrs } = options;
554
-
555
- if (innerText !== undefined) {
556
- if (typeof innerText === 'function') {
557
- effect(() => { node.innerText = innerText(); });
558
- } else {
559
- node.innerText = innerText;
560
- }
561
- }
562
-
563
- if (innerHTML !== undefined) {
564
- if (typeof innerHTML === 'function') {
565
- effect(() => { node.innerHTML = innerHTML(); });
566
- } else {
567
- node.innerHTML = innerHTML;
568
- }
569
- }
570
-
571
- if (Object.keys(attrs).length > 0) {
572
- // Merge with existing attributes or simply set them triggers the proxy
573
- el.attributes = attrs;
574
- }
575
-
576
- return el;
577
- };
578
-
579
- /**
580
- * Query selector helper that adds a .content() method for easy DOM manipulation.
581
- */
582
- const $ = (cssSelectorOrElement, startingDomEl = document.body) => {
583
- const el = typeof cssSelectorOrElement === 'string' ? startingDomEl.querySelector(cssSelectorOrElement) : cssSelectorOrElement;
584
- if (!el) return null;
585
- Object.defineProperty(el, 'content', {
586
- value(child, location = 'inner') {
587
- location = location.toLowerCase();
588
- const tags = Lightview.tags;
589
-
590
- // Check if target element is script or style
591
- const isSpecialElement = el.tagName &&
592
- (el.tagName.toLowerCase() === 'script' || el.tagName.toLowerCase() === 'style');
593
-
594
- const array = (Array.isArray(child) ? child : [child]).map(item => {
595
- // Allow extensions to transform children (e.g., Object DOM syntax)
596
- // BUT skip for script/style elements which need raw content
597
- if (Lightview.hooks.processChild && !isSpecialElement) {
598
- item = Lightview.hooks.processChild(item) ?? item;
599
- }
600
- if (item.tag && !item.domEl) {
601
- return element(item.tag, item.attributes || {}, item.children || []).domEl;
602
- } else {
603
- return item.domEl || item;
604
- }
605
- });
606
-
607
- const target = location === 'shadow' ? (el.shadowRoot || el.attachShadow({ mode: 'open' })) : el;
608
-
609
- if (location === 'inner' || location === 'shadow') {
610
- target.replaceChildren(...array);
611
- } else if (location === 'outer') {
612
- target.replaceWith(...array);
613
- } else if (location === 'afterbegin') {
614
- target.prepend(...array);
615
- } else if (location === 'beforeend') {
616
- target.append(...array);
617
- } else {
618
- array.forEach(item => el.insertAdjacentElement(location, item));
619
- }
620
- return el;
621
- },
622
- configurable: true,
623
- writable: true
624
- });
625
- return el;
626
- };
627
-
628
- const customTags = {}
629
- /**
630
- * Proxy for accessing or registering tags/components.
631
- * e.g., Lightview.tags.div(...) or Lightview.tags.MyComponent = ...
632
- */
633
- const tags = new Proxy({}, {
634
- get(_, tag) {
635
- if (tag === "_customTags") return { ...customTags };
636
-
637
- const wrapper = (...args) => {
638
- let attributes = {};
639
- let children = args;
640
- const arg0 = args[0];
641
- if (args.length > 0 && arg0 && typeof arg0 === 'object' && !arg0.tag && !arg0.domEl && !Array.isArray(arg0)) {
642
- attributes = arg0;
643
- children = args.slice(1);
644
- }
645
- return element(customTags[tag] || tag, attributes, children);
646
- };
647
-
648
- // Lift static methods/properties from the component onto the wrapper
649
- // This allows patterns like Card.Figure to work when Card is retrieved from tags
650
- if (customTags[tag]) {
651
- Object.assign(wrapper, customTags[tag]);
652
- }
653
-
654
- return wrapper;
655
- },
656
- set(_, tag, value) {
657
- customTags[tag] = value;
658
- return true;
659
- }
660
- });
661
-
662
- const Lightview = {
663
- signal,
664
- computed,
665
- effect,
666
- element, // do not document this
667
- enhance,
668
- tags,
669
- $,
670
- // Extension hooks
671
- hooks: {
672
- onNonStandardHref: null,
673
- processChild: null
674
- },
675
- // Internals exposed for extensions
676
- internals: {
677
- domToElement,
678
- wrapDomElement,
679
- setupChildren
680
- }
681
- };
682
-
683
- // Export for use
684
- if (typeof module !== 'undefined' && module.exports) {
685
- module.exports = Lightview;
686
- }
687
- if (typeof window !== 'undefined') {
688
- window.Lightview = Lightview;
689
-
690
- // Global click handler delegates to hook if registered
691
- window.addEventListener('click', (e) => {
692
- // Support fragment navigation piercing Shadow DOM
693
- // Use composedPath() to find the actual clicked element, even inside shadow roots
694
- const path = e.composedPath();
695
- const link = path.find(el => el.tagName === 'A' && el.getAttribute?.('href')?.startsWith('#'));
696
-
697
- if (link && !e.defaultPrevented) {
698
- const href = link.getAttribute('href');
699
- if (href.length > 1) {
700
- const id = href.slice(1);
701
- const root = link.getRootNode();
702
- const target = (root.getElementById ? root.getElementById(id) : null) ||
703
- (root.querySelector ? root.querySelector(`#${id}`) : null);
704
-
705
- if (target) {
706
- e.preventDefault();
707
- target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
708
- }
709
- }
710
- }
711
-
712
- if (Lightview.hooks.onNonStandardHref) {
713
- Lightview.hooks.onNonStandardHref(e);
714
- }
715
- });
716
-
717
- // Automatic Cleanup & Lifecycle Hooks
718
- const walkNodes = (node, fn) => {
719
- fn(node);
720
- node.childNodes?.forEach(n => walkNodes(n, fn));
721
- if (node.shadowRoot) walkNodes(node.shadowRoot, fn);
722
- };
723
-
724
- const cleanupNode = (node) => walkNodes(node, n => {
725
- const s = nodeState.get(n);
726
- if (s) {
727
- s.effects?.forEach(e => e.stop());
728
- s.onunmount?.(n);
729
- nodeState.delete(n);
730
- }
731
- });
732
-
733
- const mountNode = (node) => walkNodes(node, n => {
734
- nodeState.get(n)?.onmount?.(n);
735
- });
736
-
737
- const observer = new MutationObserver((mutations) => {
738
- mutations.forEach((mutation) => {
739
- mutation.removedNodes.forEach(cleanupNode);
740
- mutation.addedNodes.forEach(mountNode);
741
- });
742
- });
743
-
744
- // Wait for DOM to be ready before observing
745
- const startObserving = () => {
746
- if (document.body) {
747
- observer.observe(document.body, {
748
- childList: true,
749
- subtree: true
750
- });
751
- }
752
- };
753
-
754
- if (document.readyState === 'loading') {
755
- document.addEventListener('DOMContentLoaded', startObserving);
756
- } else {
757
- startObserving();
758
- }
759
- }
760
- })();
1
+ !function(){"use strict";const e=globalThis.__LIGHTVIEW_INTERNALS__||(globalThis.__LIGHTVIEW_INTERNALS__={currentEffect:null,registry:new Map,dependencyMap:new WeakMap}),t=(t,n)=>{let o="string"==typeof n?n:null==n?void 0:n.name;const r=null==n?void 0:n.storage;if(o&&r)try{const e=r.getItem(o);null!==e&&(t=JSON.parse(e))}catch(l){}let s=t;const i=new Set,a=(...e)=>{if(0===e.length)return a.value;a.value=e[0]};if(Object.defineProperty(a,"value",{get:()=>(e.currentEffect&&(i.add(e.currentEffect),e.currentEffect.dependencies.add(i)),s),set(e){if(s!==e){if(s=e,o&&r)try{r.setItem(o,JSON.stringify(s))}catch(l){}[...i].forEach(e=>e())}}}),o)if(e.registry.has(o)){if(e.registry.get(o)!==a)throw new Error(`Lightview: A signal or state with the name "${o}" is already registered.`)}else e.registry.set(o,a);return a};t.get=(n,o)=>e.registry.has(n)||void 0===o?e.registry.get(n):t(o,n);const n=t=>{const n=()=>{if(n.active&&!n.running){n.dependencies.forEach(e=>e.delete(n)),n.dependencies.clear(),n.running=!0,e.currentEffect=n;try{t()}finally{e.currentEffect=null,n.running=!1}}};return n.active=!0,n.running=!1,n.dependencies=new Set,n.stop=()=>{n.dependencies.forEach(e=>e.delete(n)),n.dependencies.clear(),n.active=!1},n(),n},o=(e,t)=>Object.getOwnPropertyNames(e).filter(n=>"function"==typeof e[n]&&t(n));o(Date.prototype,e=>/^(to|get|valueOf)/.test(e)),o(Date.prototype,e=>/^set/.test(e));const r=(e,t,n)=>{let o=e.get(t);return o||(o=n(),e.set(t,o)),o},s={get currentEffect(){return(globalThis.__LIGHTVIEW_INTERNALS__||(globalThis.__LIGHTVIEW_INTERNALS__={})).currentEffect}},i=new WeakMap,a=()=>({effects:[],onmount:null,onunmount:null}),l=e.registry,c=(e,t)=>{const n=r(i,e,a);n.effects||(n.effects=[]),n.effects.push(t)},d=Symbol("lightview.shadowDOM"),u=e=>e&&"object"==typeof e&&!0===e[d],f=(e,t)=>{if(t.shadowRoot)return void console.warn("Lightview: Element already has a shadowRoot, skipping shadowDOM directive");const n=t.attachShadow({mode:e.mode}),o=[],r=[...e.styles||[]];if(e.adoptedStyleSheets&&e.adoptedStyleSheets.length>0&&e.adoptedStyleSheets.forEach(e=>{e instanceof CSSStyleSheet?o.push(e):"string"==typeof e&&r.push(e)}),o.length>0)try{n.adoptedStyleSheets=o}catch(s){console.warn("Lightview: adoptedStyleSheets not supported")}for(const i of r){const e=document.createElement("link");e.rel="stylesheet",e.href=i,n.appendChild(e)}e.children&&e.children.length>0&&L(e.children,n)};let h=!1;const p=new WeakMap,g=(e,t,n={},o=[])=>{const r=b({tag:t,attributes:n,children:o,get domEl(){return e}});return p.set(e,r),r},m=(e,t={},o=[])=>{if(N[e]&&(e=N[e]),"function"==typeof e){const n=e({...t},o);return y(n)}if("shadowDOM"===e)return((e,t)=>({[d]:!0,mode:e.mode||"open",styles:e.styles||[],adoptedStyleSheets:e.adoptedStyleSheets||[],children:t}))(t,o);if("text"===e&&!h){const r=document.createTextNode(""),s={tag:e,attributes:t,children:o,get domEl(){return r}},i=()=>{const e=(Array.isArray(s.children)?s.children:[s.children]).flat(1/0).map(e=>{const t="function"==typeof e?e():e;return t&&"object"==typeof t&&t.domEl?t.domEl.textContent:null==t?"":String(t)});r.textContent=e.join(" ")},a=new Proxy(s,{set:(e,t,n)=>(e[t]=n,"children"===t&&i(),!0)});if(o.flat(1/0).some(e=>"function"==typeof e)){const e=n(i);c(r,e)}return i(),a}const r="svg"===e.toLowerCase(),s=h;r&&(h=!0);const i=h?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e),a=g(i,e,t,o);return a.attributes=t,a.children=o,r&&(h=s),a},y=e=>{if(!e)return null;if(j.hooks.processChild&&(e=j.hooks.processChild(e)??e),e.domEl)return e;const t=typeof e;if("object"===t&&e instanceof HTMLElement)return g(e,e.tagName.toLowerCase(),{},[]);if("object"===t&&e instanceof String){const t=document.createElement("span");return t.textContent=e.toString(),g(t,"span",{},[])}if("string"===t){const t=document.createElement("template");t.innerHTML=e.trim();const n=t.content;if(1===n.childNodes.length&&n.firstChild instanceof HTMLElement){const e=n.firstChild;return g(e,e.tagName.toLowerCase(),{},[])}{const e=document.createElement("span");return e.style.display="contents",e.appendChild(n),g(e,"span",{},[])}}return"object"==typeof e&&e.tag?m(e.tag,e.attributes||{},e.children||[]):null},b=e=>{const t=e.domEl;return new Proxy(e,{set:(e,n,o)=>(e[n]="attributes"===n?v(o,t):"children"===n?C(o,t):o,!0)})},w=new Set(["value","checked","selected","selectedIndex","className","innerHTML","innerText"]),E=(e,t,n)=>{const o="boolean"==typeof e[t];"href"!==t&&"src"!==t||"string"!=typeof n||!/^(javascript|vbscript|data:text\/html|data:application\/javascript)/i.test(n)||(console.warn(`[Lightview] Blocked dangerous protocol in ${t}: ${n}`),n="javascript:void(0)"),w.has(t)||o?e[t]=o?null!=n&&!1!==n&&"false"!==n:n:null==n?e.removeAttribute(t):e.setAttribute(t,n)},v=(e,t)=>{const o={};for(let[s,l]of Object.entries(e))if("onmount"===s||"onunmount"===s){r(i,t,a)[s]=l,"onmount"===s&&t.isConnected&&l(t)}else if(s.startsWith("on")){if("function"==typeof l){const e=s.slice(2).toLowerCase();t.addEventListener(e,l)}else"string"==typeof l&&t.setAttribute(s,l);o[s]=l}else if("function"==typeof l){const e=n(()=>{const e=l();"style"===s&&"object"==typeof e?Object.assign(t.style,e):E(t,s,e)});c(t,e),o[s]=l}else"style"===s&&"object"==typeof l?(Object.entries(l).forEach(([e,o])=>{if("function"==typeof o){const r=n(()=>{t.style[e]=o()});c(t,r)}else t.style[e]=o}),o[s]=l):(E(t,s,l),o[s]=l);return o},S=(e,t,o=!0)=>{o&&void 0!==t.innerHTML&&(t.innerHTML="");const r=[],s=t.tagName&&("script"===t.tagName.toLowerCase()||"style"===t.tagName.toLowerCase()),i=e.flat(1/0);for(let a of i){if(j.hooks.processChild&&!s&&(a=j.hooks.processChild(a)??a),u(a)){if(t instanceof ShadowRoot){console.warn("Lightview: Cannot nest shadowDOM inside another shadowDOM");continue}f(a,t);continue}const e=typeof a;if("function"===e){const e=document.createComment("lv:s"),o=document.createComment("lv:e");let s;t.appendChild(e),t.appendChild(o);const i=()=>{for(;e.nextSibling&&e.nextSibling!==o;)e.nextSibling.remove();const t=a();if(null!=t)if(!s||e.isConnected)if("object"==typeof t&&t instanceof String){const e=document.createTextNode(t);o.parentNode.insertBefore(e,o)}else{const e=document.createDocumentFragment(),n=Array.isArray(t)?t:[t];S(n,e,!1),o.parentNode.insertBefore(e,o)}else s.stop()};s=n(i),c(e,s),r.push(a)}else if(["string","number","boolean","symbol"].includes(e)||a&&"object"===e&&a instanceof String)t.appendChild(document.createTextNode(a)),r.push(a);else if(a&&"object"===e&&a.tag){const e=a.domEl?a:m(a.tag,a.attributes||{},a.children||[]);t.appendChild(e.domEl),r.push(e)}}return r},L=(e,t)=>S(e,t,!1),C=(e,t)=>S(e,t,!0),N={},T=new Proxy({},{get(e,t){if("_customTags"===t)return{...N};const n=(...e)=>{let n={},o=e;const r=e[0];return e.length>0&&r&&"object"==typeof r&&!r.tag&&!r.domEl&&!Array.isArray(r)&&(n=r,o=e.slice(1)),m(N[t]||t,n,o)};return N[t]&&Object.assign(n,N[t]),n},set:(e,t,n)=>(N[t]=n,!0)}),j={signal:t,get:t.get,computed:e=>{const o=t(void 0);return n(()=>{o.value=e()}),o},effect:n,registry:l,element:m,enhance:(e,t={})=>{const o="string"==typeof e?document.querySelector(e):e,r=o.domEl||o;if(!(r instanceof HTMLElement))return null;const s=r.tagName.toLowerCase();let i=p.get(r);i||(i=g(r,s));const{innerText:a,innerHTML:l,...c}=t;return void 0!==a&&("function"==typeof a?n(()=>{r.innerText=a()}):r.innerText=a),void 0!==l&&("function"==typeof l?n(()=>{r.innerHTML=l()}):r.innerHTML=l),Object.keys(c).length>0&&(i.attributes=c),i},tags:T,$:(e,t=document.body)=>{const n="string"==typeof e?t.querySelector(e):e;return n?(Object.defineProperty(n,"content",{value(e,t="inner"){t=t.toLowerCase(),j.tags;const o=n.tagName&&("script"===n.tagName.toLowerCase()||"style"===n.tagName.toLowerCase()),r=(Array.isArray(e)?e:[e]).map(e=>(j.hooks.processChild&&!o&&(e=j.hooks.processChild(e)??e),e.tag&&!e.domEl?m(e.tag,e.attributes||{},e.children||[]).domEl:e.domEl||e)),s="shadow"===t?n.shadowRoot||n.attachShadow({mode:"open"}):n;return"inner"===t||"shadow"===t?s.replaceChildren(...r):"outer"===t?s.replaceWith(...r):"afterbegin"===t?s.prepend(...r):"beforeend"===t?s.append(...r):r.forEach(e=>n.insertAdjacentElement(t,e)),n},configurable:!0,writable:!0}),n):null},hooks:{onNonStandardHref:null,processChild:null,validateUrl:null},internals:{core:s,domToElement:p,wrapDomElement:g,setupChildren:C}};if("undefined"!=typeof module&&module.exports&&(module.exports=j),"undefined"!=typeof window&&(globalThis.Lightview=j,globalThis.addEventListener("click",e=>{const t=e.composedPath().find(e=>{var t,n;return"A"===e.tagName&&(null==(n=null==(t=e.getAttribute)?void 0:t.call(e,"href"))?void 0:n.startsWith("#"))});if(t&&!e.defaultPrevented){const n=t.getAttribute("href");if(n.length>1){const o=n.slice(1),r=t.getRootNode(),s=(r.getElementById?r.getElementById(o):null)||(r.querySelector?r.querySelector(`#${o}`):null);s&&(e.preventDefault(),requestAnimationFrame(()=>{requestAnimationFrame(()=>{s.style.scrollMarginTop="calc(var(--site-nav-height, 0px) + 2rem)",s.scrollIntoView({behavior:"smooth",block:"start",inline:"start"})})}))}}j.hooks.onNonStandardHref&&j.hooks.onNonStandardHref(e)}),"undefined"!=typeof MutationObserver)){const e=(t,n)=>{var o;n(t),null==(o=t.childNodes)||o.forEach(t=>e(t,n)),t.shadowRoot&&e(t.shadowRoot,n)},t=t=>e(t,e=>{var t,n;const o=i.get(e);o&&(null==(t=o.effects)||t.forEach(e=>e.stop()),null==(n=o.onunmount)||n.call(o,e),i.delete(e))}),n=t=>e(t,e=>{var t,n;null==(n=null==(t=i.get(e))?void 0:t.onmount)||n.call(t,e)}),o=new MutationObserver(e=>{e.forEach(e=>{e.removedNodes.forEach(t),e.addedNodes.forEach(n)})}),r=()=>{document.body&&o.observe(document.body,{childList:!0,subtree:!0})};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",r):r()}}();