lightview 2.1.0 → 2.2.2

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 (71) hide show
  1. package/build-bundles.mjs +2 -6
  2. package/build.js +236 -46
  3. package/components/data-display/avatar.js +25 -1
  4. package/components/data-display/chart.js +22 -5
  5. package/components/data-display/countdown.js +3 -2
  6. package/components/data-input/checkbox.js +23 -1
  7. package/components/data-input/input.js +24 -1
  8. package/components/data-input/radio.js +37 -2
  9. package/components/data-input/select.js +24 -1
  10. package/components/data-input/toggle.js +21 -1
  11. package/components/navigation/breadcrumbs.js +42 -2
  12. package/docs/assets/js/examplify.js +1 -1
  13. package/docs/cdom-nav.html +32 -6
  14. package/docs/cdom.html +610 -180
  15. package/docs/components/avatar.html +24 -54
  16. package/docs/components/badge.html +14 -14
  17. package/docs/components/breadcrumbs.html +95 -29
  18. package/docs/components/chart-area.html +3 -3
  19. package/docs/components/chart-bar.html +4 -181
  20. package/docs/components/chart-column.html +4 -189
  21. package/docs/components/chart-line.html +3 -3
  22. package/docs/components/chart-pie.html +112 -166
  23. package/docs/components/chart.html +11 -13
  24. package/docs/components/checkbox.html +48 -28
  25. package/docs/components/collapse.html +6 -6
  26. package/docs/components/countdown.html +12 -12
  27. package/docs/components/dropdown.html +1 -1
  28. package/docs/components/file-input.html +4 -4
  29. package/docs/components/footer.html +11 -11
  30. package/docs/components/input.html +45 -29
  31. package/docs/components/join.html +4 -4
  32. package/docs/components/kbd.html +3 -3
  33. package/docs/components/loading.html +41 -53
  34. package/docs/components/pagination.html +4 -4
  35. package/docs/components/progress.html +6 -4
  36. package/docs/components/radio.html +42 -31
  37. package/docs/components/select.html +48 -59
  38. package/docs/components/toggle.html +44 -25
  39. package/docs/getting-started/index.html +4 -4
  40. package/jprx/LICENSE +21 -0
  41. package/jprx/README.md +130 -0
  42. package/{cdom → jprx}/helpers/array.js +9 -4
  43. package/{cdom → jprx}/helpers/state.js +6 -3
  44. package/jprx/index.js +69 -0
  45. package/jprx/package.json +24 -0
  46. package/jprx/parser.js +1517 -0
  47. package/lightview-all.js +3785 -1
  48. package/lightview-cdom.js +2128 -1
  49. package/lightview-router.js +179 -208
  50. package/lightview-x.js +1435 -1
  51. package/lightview.js +613 -1
  52. package/package.json +5 -2
  53. package/src/lightview-cdom.js +201 -49
  54. package/src/lightview-router.js +210 -0
  55. package/src/lightview-x.js +104 -55
  56. package/src/lightview.js +12 -1
  57. package/{watch.js → start-dev.js} +2 -1
  58. package/tests/cdom/parser.test.js +83 -12
  59. package/wrangler.toml +0 -3
  60. package/cdom/parser.js +0 -602
  61. package/test-text-tag.js +0 -6
  62. /package/{cdom → jprx}/helpers/compare.js +0 -0
  63. /package/{cdom → jprx}/helpers/conditional.js +0 -0
  64. /package/{cdom → jprx}/helpers/datetime.js +0 -0
  65. /package/{cdom → jprx}/helpers/format.js +0 -0
  66. /package/{cdom → jprx}/helpers/logic.js +0 -0
  67. /package/{cdom → jprx}/helpers/lookup.js +0 -0
  68. /package/{cdom → jprx}/helpers/math.js +0 -0
  69. /package/{cdom → jprx}/helpers/network.js +0 -0
  70. /package/{cdom → jprx}/helpers/stats.js +0 -0
  71. /package/{cdom → jprx}/helpers/string.js +0 -0
@@ -92,7 +92,7 @@ const convertObjectDOM = (obj) => {
92
92
  // ============= COMPONENT CONFIGURATION =============
93
93
  // Global configuration for Lightview components
94
94
 
95
- const DAISYUI_CDN = 'https://cdn.jsdelivr.net/npm/daisyui@3.9.4/dist/full.min.css';
95
+ const DAISYUI_CDN = 'https://cdn.jsdelivr.net/npm/daisyui@4.12.23/dist/full.min.css';
96
96
 
97
97
  // Component configuration (set by initComponents)
98
98
  const componentConfig = {
@@ -101,7 +101,8 @@ const componentConfig = {
101
101
  daisyStyleSheet: null,
102
102
  themeStyleSheet: null, // Global theme stylesheet
103
103
  componentStyleSheets: new Map(),
104
- customStyleSheets: new Map() // Registry for named custom stylesheets
104
+ customStyleSheets: new Map(), // Registry for named custom stylesheets
105
+ customStyleSheetPromises: new Map() // Cache for pending stylesheet fetches
105
106
  };
106
107
 
107
108
  /**
@@ -111,36 +112,45 @@ const componentConfig = {
111
112
  * @returns {Promise<void>}
112
113
  */
113
114
  const registerStyleSheet = async (nameOrIdOrUrl, cssText) => {
114
- if (componentConfig.customStyleSheets.has(nameOrIdOrUrl)) return;
115
+ if (componentConfig.customStyleSheets.has(nameOrIdOrUrl)) return componentConfig.customStyleSheets.get(nameOrIdOrUrl);
116
+ if (componentConfig.customStyleSheetPromises.has(nameOrIdOrUrl)) return componentConfig.customStyleSheetPromises.get(nameOrIdOrUrl);
115
117
 
116
- try {
117
- let finalCss = cssText;
118
-
119
- if (finalCss === undefined) {
120
- if (nameOrIdOrUrl.startsWith('#')) {
121
- // ID selector - search synchronously
122
- const el = document.querySelector(nameOrIdOrUrl);
123
- if (el) {
124
- finalCss = el.textContent;
118
+ const promise = (async () => {
119
+ try {
120
+ let finalCss = cssText;
121
+
122
+ if (finalCss === undefined) {
123
+ if (nameOrIdOrUrl.startsWith('#')) {
124
+ // ID selector - search synchronously
125
+ const el = document.querySelector(nameOrIdOrUrl);
126
+ if (el) {
127
+ finalCss = el.textContent;
128
+ } else {
129
+ throw new Error(`Style block '${nameOrIdOrUrl}' not found`);
130
+ }
125
131
  } else {
126
- throw new Error(`Style block '${nameOrIdOrUrl}' not found`);
132
+ // Assume URL
133
+ const response = await fetch(nameOrIdOrUrl);
134
+ if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
135
+ finalCss = await response.text();
127
136
  }
128
- } else {
129
- // Assume URL
130
- const response = await fetch(nameOrIdOrUrl);
131
- if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
132
- finalCss = await response.text();
133
137
  }
134
- }
135
138
 
136
- if (finalCss !== undefined) {
137
- const sheet = new CSSStyleSheet();
138
- sheet.replaceSync(finalCss);
139
- componentConfig.customStyleSheets.set(nameOrIdOrUrl, sheet);
139
+ if (finalCss !== undefined) {
140
+ const sheet = new CSSStyleSheet();
141
+ sheet.replaceSync(finalCss);
142
+ componentConfig.customStyleSheets.set(nameOrIdOrUrl, sheet);
143
+ return sheet;
144
+ }
145
+ } catch (e) {
146
+ console.error(`LightviewX: Failed to register stylesheet '${nameOrIdOrUrl}':`, e);
147
+ } finally {
148
+ componentConfig.customStyleSheetPromises.delete(nameOrIdOrUrl);
140
149
  }
141
- } catch (e) {
142
- console.error(`LightviewX: Failed to register stylesheet '${nameOrIdOrUrl}':`, e);
143
- }
150
+ })();
151
+
152
+ componentConfig.customStyleSheetPromises.set(nameOrIdOrUrl, promise);
153
+ return promise;
144
154
  };
145
155
 
146
156
  // Theme Signal
@@ -1043,7 +1053,11 @@ const activateReactiveSyntax = (root, LV) => {
1043
1053
  try {
1044
1054
  const val = fn(LV.state, LV.signal);
1045
1055
  if (isAttr) {
1046
- (val === null || val === undefined || val === false) ? node.removeAttribute(attrName) : node.setAttribute(attrName, val);
1056
+ if (attrName.startsWith('cdom-')) {
1057
+ node[attrName] = val;
1058
+ } else {
1059
+ (val === null || val === undefined || val === false) ? node.removeAttribute(attrName) : node.setAttribute(attrName, val);
1060
+ }
1047
1061
  } else node.textContent = val !== undefined ? val : '';
1048
1062
  } catch (e) { /* Effect execution failed */ }
1049
1063
  });
@@ -1399,11 +1413,28 @@ const customElementWrapper = (Component, config = {}) => {
1399
1413
  }
1400
1414
 
1401
1415
  connectedCallback() {
1402
- // Load DaisyUI stylesheet into shadow DOM
1403
- const link = document.createElement('link');
1404
- link.rel = 'stylesheet';
1405
- link.href = 'https://cdn.jsdelivr.net/npm/daisyui@5.5.14/daisyui.min.css';
1406
- this.shadowRoot.appendChild(link);
1416
+ let adopted = false;
1417
+ // Attempt to use pre-parsed adopted stylesheets for performance
1418
+ if (componentConfig.daisyStyleSheet) {
1419
+ try {
1420
+ const sheets = [componentConfig.daisyStyleSheet];
1421
+ if (componentConfig.themeStyleSheet) {
1422
+ sheets.push(componentConfig.themeStyleSheet);
1423
+ }
1424
+ this.shadowRoot.adoptedStyleSheets = sheets;
1425
+ adopted = true;
1426
+ } catch (e) {
1427
+ // Browser might not support adoptedStyleSheets
1428
+ }
1429
+ }
1430
+
1431
+ // Fallback to link tag if adoption failed or sheet wasn't loaded yet
1432
+ if (!adopted) {
1433
+ const link = document.createElement('link');
1434
+ link.rel = 'stylesheet';
1435
+ link.href = DAISYUI_CDN;
1436
+ this.shadowRoot.appendChild(link);
1437
+ }
1407
1438
 
1408
1439
  // Sync theme from document
1409
1440
  const themeWrapper = document.createElement('div');
@@ -1458,22 +1489,30 @@ const customElementWrapper = (Component, config = {}) => {
1458
1489
 
1459
1490
  if (!componentInfo) return null;
1460
1491
 
1461
- const { component, attributeMap = {}, innerHTML = false } = componentInfo;
1492
+ const { component, attributeMap = {} } = componentInfo;
1462
1493
  const attributes = {};
1463
1494
 
1464
- // Parse attributes based on map
1465
- Object.entries(attributeMap).forEach(([attr, type]) => {
1466
- const value = child.getAttribute(attr);
1467
- if (value !== null) {
1468
- if (type === Boolean) {
1469
- attributes[attr] = value === 'true' || value === '';
1470
- } else if (type === Number) {
1471
- attributes[attr] = Number(value);
1472
- } else {
1473
- attributes[attr] = value;
1495
+ // Parse all attributes
1496
+ for (const attr of child.attributes) {
1497
+ const name = attr.name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
1498
+ const type = attributeMap[name];
1499
+ const value = attr.value;
1500
+
1501
+ if (type === Boolean) {
1502
+ attributes[name] = value === 'true' || value === '';
1503
+ } else if (type === Number) {
1504
+ attributes[name] = Number(value);
1505
+ } else if (type === Array || type === Object) {
1506
+ try {
1507
+ attributes[name] = JSON.parse(value);
1508
+ } catch (e) {
1509
+ console.warn(`[Lightview] Failed to parse child attribute ${name} as JSON:`, value);
1510
+ attributes[name] = value;
1474
1511
  }
1512
+ } else {
1513
+ attributes[name] = value;
1475
1514
  }
1476
- });
1515
+ }
1477
1516
 
1478
1517
  // Copy event handlers
1479
1518
  if (child.onclick) attributes.onclick = child.onclick.bind(child);
@@ -1481,7 +1520,7 @@ const customElementWrapper = (Component, config = {}) => {
1481
1520
  return {
1482
1521
  tag: component,
1483
1522
  attributes,
1484
- children: innerHTML ? [child.innerHTML] : [child.textContent]
1523
+ children: Array.from(child.childNodes)
1485
1524
  };
1486
1525
  }).filter(Boolean);
1487
1526
  }
@@ -1489,18 +1528,28 @@ const customElementWrapper = (Component, config = {}) => {
1489
1528
  render() {
1490
1529
  // Build props from attributes
1491
1530
  const props = { useShadow: false }; // Wrapper already created shadow DOM
1492
- Object.entries(attributeMap).forEach(([attr, type]) => {
1493
- const value = this.getAttribute(attr);
1494
- if (value !== null) {
1495
- if (type === Boolean) {
1496
- props[attr] = value === 'true' || value === '';
1497
- } else if (type === Number) {
1498
- props[attr] = Number(value);
1499
- } else {
1500
- props[attr] = value;
1531
+
1532
+ // Collect all attributes
1533
+ for (const attr of this.attributes) {
1534
+ const name = attr.name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
1535
+ const type = attributeMap[name];
1536
+ const value = attr.value;
1537
+
1538
+ if (type === Boolean) {
1539
+ props[name] = value === 'true' || value === '';
1540
+ } else if (type === Number) {
1541
+ props[name] = Number(value);
1542
+ } else if (type === Array || type === Object) {
1543
+ try {
1544
+ props[name] = JSON.parse(value);
1545
+ } catch (e) {
1546
+ console.warn(`[Lightview] Failed to parse ${name} as JSON:`, value);
1547
+ props[name] = value;
1501
1548
  }
1549
+ } else {
1550
+ props[name] = value;
1502
1551
  }
1503
- });
1552
+ }
1504
1553
 
1505
1554
  const vdomChildren = this.parseChildrenToVDOM();
1506
1555
  // If no child elements are mapped, use a slot to project light DOM
package/src/lightview.js CHANGED
@@ -270,7 +270,7 @@ const setAttributeValue = (domNode, key, value) => {
270
270
  value = 'javascript:void(0)'; // Safer fallback than # which might trigger scroll or router
271
271
  }
272
272
 
273
- if (NODE_PROPERTIES.has(key) || isBool) {
273
+ if (NODE_PROPERTIES.has(key) || isBool || key.startsWith('cdom-')) {
274
274
  domNode[key] = isBool ? (value !== null && value !== undefined && value !== false && value !== 'false') : value;
275
275
  } else if (value === null || value === undefined) {
276
276
  domNode.removeAttribute(key);
@@ -428,6 +428,17 @@ const processChildren = (children, targetNode, clearExisting = true) => {
428
428
  // Static text
429
429
  targetNode.appendChild(document.createTextNode(child));
430
430
  childElements.push(child);
431
+ } else if (child instanceof Node) {
432
+ // Raw DOM node
433
+ const node = child.domEl || child;
434
+ if (node instanceof HTMLElement || node instanceof SVGElement) {
435
+ const wrapped = wrapDomElement(node, node.tagName.toLowerCase());
436
+ targetNode.appendChild(node);
437
+ childElements.push(wrapped);
438
+ } else {
439
+ targetNode.appendChild(node);
440
+ childElements.push(child);
441
+ }
431
442
  } else if (child && type === 'object' && child.tag) {
432
443
  // Child element (already wrapped or plain object) - tag can be string or function
433
444
  const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
@@ -14,7 +14,8 @@ const allowedDirs = ['docs', 'components', 'middleware'];
14
14
  function build() {
15
15
  try {
16
16
  console.log('Running full build...');
17
- execSync('node build.js', { stdio: 'inherit' });
17
+ execSync('node build-bundles.mjs',, { stdio: 'inherit' });
18
+ execSync('node build.js --env=dev', { stdio: 'inherit' });
18
19
  } catch (e) {
19
20
  console.error('Build failed:', e);
20
21
  }
@@ -1,18 +1,19 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import Lightview from '../../src/lightview.js';
3
3
  import LightviewX from '../../src/lightview-x.js';
4
- import { resolvePath, parseExpression, registerHelper } from '../../cdom/parser.js';
5
- import { registerMathHelpers } from '../../cdom/helpers/math.js';
6
- import { registerLogicHelpers } from '../../cdom/helpers/logic.js';
7
- import { registerStringHelpers } from '../../cdom/helpers/string.js';
8
- import { registerArrayHelpers } from '../../cdom/helpers/array.js';
9
- import { registerCompareHelpers } from '../../cdom/helpers/compare.js';
10
- import { registerConditionalHelpers } from '../../cdom/helpers/conditional.js';
11
- import { registerDateTimeHelpers } from '../../cdom/helpers/datetime.js';
12
- import { registerFormatHelpers } from '../../cdom/helpers/format.js';
13
- import { registerLookupHelpers } from '../../cdom/helpers/lookup.js';
14
- import { registerStatsHelpers } from '../../cdom/helpers/stats.js';
15
- import { registerStateHelpers } from '../../cdom/helpers/state.js';
4
+ import { resolvePath, parseExpression, registerHelper, parseCDOMC } from '../../jprx/parser.js';
5
+ import { registerMathHelpers } from '../../jprx/helpers/math.js';
6
+ import { registerLogicHelpers } from '../../jprx/helpers/logic.js';
7
+ import { registerStringHelpers } from '../../jprx/helpers/string.js';
8
+ import { registerArrayHelpers } from '../../jprx/helpers/array.js';
9
+ import { registerCompareHelpers } from '../../jprx/helpers/compare.js';
10
+ import { registerConditionalHelpers } from '../../jprx/helpers/conditional.js';
11
+ import { registerDateTimeHelpers } from '../../jprx/helpers/datetime.js';
12
+ import { registerFormatHelpers } from '../../jprx/helpers/format.js';
13
+ import { registerLookupHelpers } from '../../jprx/helpers/lookup.js';
14
+ import { registerStatsHelpers } from '../../jprx/helpers/stats.js';
15
+ import { registerStateHelpers } from '../../jprx/helpers/state.js';
16
+ import { hydrate } from '../../src/lightview-cdom.js';
16
17
 
17
18
  describe('cdom Parser', () => {
18
19
  beforeEach(() => {
@@ -105,4 +106,74 @@ describe('cdom Parser', () => {
105
106
  expect(expr.value).toBe('CHARLIE');
106
107
  });
107
108
  });
109
+
110
+ describe('CDOMC Parser', () => {
111
+ it('parses unquoted $ expressions as strings', () => {
112
+ const input = '{ button: { onclick: $increment($/count), children: "Click" } }';
113
+ const result = parseCDOMC(input);
114
+
115
+ expect(result).toEqual({
116
+ button: {
117
+ onclick: '$increment($/count)',
118
+ children: 'Click'
119
+ }
120
+ });
121
+ });
122
+
123
+ it('parses unquoted cdom-state with object value', () => {
124
+ const input = '{ div: { cdom-state: { count: 0 }, children: [] } }';
125
+ const result = parseCDOMC(input);
126
+
127
+ expect(result).toEqual({
128
+ div: {
129
+ 'cdom-state': { count: 0 },
130
+ children: []
131
+ }
132
+ });
133
+ });
134
+
135
+ it('preserves $ prefix in simple paths', () => {
136
+ const input = '{ input: { cdom-bind: $/user/name } }';
137
+ const result = parseCDOMC(input);
138
+
139
+ expect(result).toEqual({
140
+ input: {
141
+ 'cdom-bind': '$/user/name'
142
+ }
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('Hydration', () => {
148
+ it('converts event handler $ expressions to functions', () => {
149
+
150
+ const input = {
151
+ button: {
152
+ onclick: '$increment($/count)',
153
+ children: ['Click']
154
+ }
155
+ };
156
+
157
+ const result = hydrate(input);
158
+
159
+ // onclick should now be a function
160
+ expect(typeof result.button.onclick).toBe('function');
161
+ expect(result.button.children).toEqual(['Click']);
162
+ });
163
+
164
+ it('preserves non-$ event handlers as strings', () => {
165
+
166
+ const input = {
167
+ button: {
168
+ onclick: 'alert("hello")',
169
+ children: ['Click']
170
+ }
171
+ };
172
+
173
+ const result = hydrate(input);
174
+
175
+ // onclick should remain a string (non-$ expression)
176
+ expect(result.button.onclick).toBe('alert("hello")');
177
+ });
178
+ });
108
179
  });
package/wrangler.toml CHANGED
@@ -1,9 +1,6 @@
1
1
  name = "lightview"
2
2
  compatibility_date = "2024-01-01"
3
3
 
4
- # Enable eval() and new Function() for SSR script processing
5
- [unsafe]
6
- eval = true
7
4
 
8
5
  # [build]
9
6
  # command = "npm run build"