lightview 2.0.9 → 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.
- package/build-bundles.mjs +109 -0
- package/cdom/helpers/array.js +70 -0
- package/cdom/helpers/compare.js +26 -0
- package/cdom/helpers/conditional.js +34 -0
- package/cdom/helpers/datetime.js +54 -0
- package/cdom/helpers/format.js +20 -0
- package/cdom/helpers/logic.js +24 -0
- package/cdom/helpers/lookup.js +25 -0
- package/cdom/helpers/math.js +34 -0
- package/cdom/helpers/network.js +41 -0
- package/cdom/helpers/state.js +77 -0
- package/cdom/helpers/stats.js +39 -0
- package/cdom/helpers/string.js +49 -0
- package/cdom/parser.js +602 -0
- package/components/actions/button.js +16 -3
- package/components/actions/swap.js +26 -3
- package/components/daisyui.js +1 -1
- package/components/data-display/alert.js +13 -3
- package/components/data-display/badge.js +11 -3
- package/components/data-display/kbd.js +9 -3
- package/components/data-display/loading.js +11 -3
- package/components/data-display/progress.js +11 -3
- package/components/data-display/radial-progress.js +12 -3
- package/components/data-display/tooltip.js +17 -0
- package/components/layout/divider.js +21 -1
- package/components/layout/indicator.js +14 -0
- package/components/navigation/tabs.js +291 -16
- package/docs/api/elements.html +125 -49
- package/docs/api/hypermedia.html +29 -2
- package/docs/api/index.html +6 -2
- package/docs/api/nav.html +18 -4
- package/docs/cdom-nav.html +29 -0
- package/docs/cdom.html +362 -0
- package/docs/components/alert.html +8 -8
- package/docs/components/badge.html +55 -0
- package/docs/components/button.html +78 -92
- package/docs/components/component-nav.html +1 -1
- package/docs/components/divider.html +65 -21
- package/docs/components/indicator.html +85 -31
- package/docs/components/kbd.html +64 -25
- package/docs/components/loading.html +55 -39
- package/docs/components/progress.html +44 -3
- package/docs/components/radial-progress.html +32 -12
- package/docs/components/swap.html +183 -100
- package/docs/components/tabs.html +146 -278
- package/docs/components/tooltip.html +71 -31
- package/docs/getting-started/index.html +7 -5
- package/docs/index.html +1 -1
- package/docs/syntax-nav.html +10 -0
- package/docs/syntax.html +8 -6
- package/index.html +2 -2
- package/lightview-all.js +1 -0
- package/lightview-cdom.js +1 -0
- package/lightview-x.js +1 -1608
- package/lightview.js +1 -766
- package/lightview.js.bak +1 -0
- package/package.json +6 -2
- package/src/lightview-all.js +10 -0
- package/src/lightview-cdom.js +305 -0
- package/src/lightview-x.js +1581 -0
- package/src/lightview.js +694 -0
- package/src/reactivity/signal.js +133 -0
- package/src/reactivity/state.js +217 -0
- package/test-text-tag.js +6 -0
- package/tests/cdom/fixtures/helpers.cdomc +62 -0
- package/tests/cdom/fixtures/user.cdom +14 -0
- package/tests/cdom/fixtures/user.cdomc +12 -0
- package/tests/cdom/fixtures/user.odom +18 -0
- package/tests/cdom/fixtures/user.vdom +11 -0
- package/tests/cdom/helpers.test.js +121 -0
- package/tests/cdom/loader.test.js +125 -0
- package/tests/cdom/parser.test.js +108 -0
- package/tests/cdom/reactivity.test.js +186 -0
- package/tests/text-tag.test.js +77 -0
- package/vite.config.mjs +52 -0
- package/components/data-display/skeleton.js +0 -66
- package/docs/components/skeleton.html +0 -447
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import Lightview from '../../src/lightview.js';
|
|
6
|
+
import LightviewX from '../../src/lightview-x.js';
|
|
7
|
+
import LightviewCDOM from '../../src/lightview-cdom.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
describe('cdom Full Loader Tests', () => {
|
|
12
|
+
let handleSrc;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
globalThis.window = globalThis;
|
|
16
|
+
globalThis.__DEBUG__ = true;
|
|
17
|
+
globalThis.Lightview = Lightview;
|
|
18
|
+
globalThis.LightviewX = LightviewX;
|
|
19
|
+
globalThis.LightviewCDOM = LightviewCDOM;
|
|
20
|
+
|
|
21
|
+
if (typeof document !== 'undefined') {
|
|
22
|
+
document.body.innerHTML = '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Lightview.registry.clear();
|
|
26
|
+
LightviewX.state({
|
|
27
|
+
user: {
|
|
28
|
+
name: 'Alice',
|
|
29
|
+
age: 30,
|
|
30
|
+
role: 'Admin',
|
|
31
|
+
status: 'Available',
|
|
32
|
+
score: 100,
|
|
33
|
+
level: 5,
|
|
34
|
+
points: [10, 20, 30],
|
|
35
|
+
isVip: true,
|
|
36
|
+
account: { type: 'Premium' },
|
|
37
|
+
details: { city: 'NYC', zip: '10001' },
|
|
38
|
+
discount: 10,
|
|
39
|
+
activity: {
|
|
40
|
+
purchases: [5, 10, 15]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, 'app');
|
|
44
|
+
|
|
45
|
+
if (globalThis.LightviewX?.internals?.handleSrcAttribute) {
|
|
46
|
+
handleSrc = globalThis.LightviewX.internals.handleSrcAttribute;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
globalThis.fetch = vi.fn().mockImplementation((url) => {
|
|
50
|
+
const fileName = url.toString().split('/').pop();
|
|
51
|
+
const filePath = path.join(__dirname, 'fixtures', fileName);
|
|
52
|
+
if (!fs.existsSync(filePath)) return Promise.resolve({ ok: false, status: 404 });
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
return Promise.resolve({
|
|
55
|
+
ok: true,
|
|
56
|
+
text: () => Promise.resolve(content),
|
|
57
|
+
json: () => Promise.resolve(JSON.parse(content)),
|
|
58
|
+
url: url.toString()
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const cleanHTML = (html) => html.replace(/<!--lv:[se]-->/g, '').replace(/\s+/g, ' ').trim();
|
|
64
|
+
|
|
65
|
+
it('should load and parse user.vdom with cdom expressions', async () => {
|
|
66
|
+
const container = Lightview.tags.div({ src: '/user.vdom' });
|
|
67
|
+
await handleSrc(container, '/user.vdom', 'div', {
|
|
68
|
+
element: Lightview.element,
|
|
69
|
+
setupChildren: Lightview.internals.setupChildren
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const html = cleanHTML(container.domEl.innerHTML);
|
|
73
|
+
expect(html).toContain('Alice');
|
|
74
|
+
expect(html).toContain('Age: 30');
|
|
75
|
+
expect(html).toContain('Account: Premium');
|
|
76
|
+
expect(html).toContain('VIP Status: Yes');
|
|
77
|
+
expect(html).toContain('Location: NYC');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should load and parse user.odom with cdom expressions', async () => {
|
|
81
|
+
const container = Lightview.tags.div({ src: '/user.odom' });
|
|
82
|
+
await handleSrc(container, '/user.odom', 'div', {
|
|
83
|
+
element: Lightview.element,
|
|
84
|
+
setupChildren: Lightview.internals.setupChildren
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const html = cleanHTML(container.domEl.innerHTML);
|
|
88
|
+
expect(html).toContain('Alice');
|
|
89
|
+
expect(html).toContain('Score: 100');
|
|
90
|
+
expect(html).toContain('Level: 5');
|
|
91
|
+
// Activity: purchases [5, 10, 15] -> sum = 30
|
|
92
|
+
expect(html).toContain('Activity Total: 30');
|
|
93
|
+
// Relative path: ../discount (10) * sum(purchases...) (30) = 300
|
|
94
|
+
expect(html).toContain('With Discount: 300');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should load and parse user.cdom (JSON ODOM file)', async () => {
|
|
98
|
+
const container = Lightview.tags.div({ src: '/user.cdom' });
|
|
99
|
+
await handleSrc(container, '/user.cdom', 'div', {
|
|
100
|
+
element: Lightview.element,
|
|
101
|
+
setupChildren: Lightview.internals.setupChildren
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const html = cleanHTML(container.domEl.innerHTML);
|
|
105
|
+
expect(html).toContain('cdom-card');
|
|
106
|
+
expect(html).toContain('Welcome, Alice');
|
|
107
|
+
expect(html).toContain('(VIP)');
|
|
108
|
+
expect(html).toContain('Discount: 20%');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should load and parse user.cdomc (Unquoted Properties/Expressions)', async () => {
|
|
112
|
+
const container = Lightview.tags.div({ src: '/user.cdomc' });
|
|
113
|
+
await handleSrc(container, '/user.cdomc', 'div', {
|
|
114
|
+
element: Lightview.element,
|
|
115
|
+
setupChildren: Lightview.internals.setupChildren
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const html = cleanHTML(container.domEl.innerHTML);
|
|
119
|
+
if (__DEBUG__) console.log('ACTUAL HTML:', html);
|
|
120
|
+
expect(html).toContain('profile-compiled');
|
|
121
|
+
expect(html).toContain('Alice');
|
|
122
|
+
expect(html).toContain('Available');
|
|
123
|
+
expect(html).toContain('Tier: Platinum');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import Lightview from '../../src/lightview.js';
|
|
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';
|
|
16
|
+
|
|
17
|
+
describe('cdom Parser', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Clear registry before each test
|
|
20
|
+
Lightview.registry.clear();
|
|
21
|
+
// Register standard helpers
|
|
22
|
+
registerMathHelpers(registerHelper);
|
|
23
|
+
registerLogicHelpers(registerHelper);
|
|
24
|
+
registerStringHelpers(registerHelper);
|
|
25
|
+
registerArrayHelpers(registerHelper);
|
|
26
|
+
registerCompareHelpers(registerHelper);
|
|
27
|
+
registerConditionalHelpers(registerHelper);
|
|
28
|
+
registerDateTimeHelpers(registerHelper);
|
|
29
|
+
registerFormatHelpers(registerHelper);
|
|
30
|
+
registerLookupHelpers(registerHelper);
|
|
31
|
+
registerStatsHelpers(registerHelper);
|
|
32
|
+
registerStateHelpers((name, fn) => registerHelper(name, fn, { pathAware: true }));
|
|
33
|
+
|
|
34
|
+
// Attach to global for the parser to find it
|
|
35
|
+
globalThis.Lightview = Lightview;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Path Resolution', () => {
|
|
39
|
+
it('resolves absolute global paths', () => {
|
|
40
|
+
Lightview.signal('Alice', 'userName');
|
|
41
|
+
expect(resolvePath('$/userName')).toBe('Alice');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('resolves deep global paths in state', () => {
|
|
45
|
+
LightviewX.state({ profile: { name: 'Bob' } }, 'user');
|
|
46
|
+
expect(resolvePath('$/user/profile/name')).toBe('Bob');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('resolves relative paths against context', () => {
|
|
50
|
+
const context = { age: 30, city: 'London' };
|
|
51
|
+
expect(resolvePath('./age', context)).toBe(30);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('resolves array indices', () => {
|
|
55
|
+
const context = { items: ['apple', 'banana'] };
|
|
56
|
+
expect(resolvePath('./items/1', context)).toBe('banana');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Expression Parsing', () => {
|
|
61
|
+
it('creates a computed signal for a path', () => {
|
|
62
|
+
const sig = Lightview.signal(10, 'count');
|
|
63
|
+
const expr = parseExpression('$/count');
|
|
64
|
+
|
|
65
|
+
expect(expr.value).toBe(10);
|
|
66
|
+
|
|
67
|
+
sig.value = 20;
|
|
68
|
+
expect(expr.value).toBe(20);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('supports math helpers', () => {
|
|
72
|
+
Lightview.signal(5, 'a');
|
|
73
|
+
Lightview.signal(10, 'b');
|
|
74
|
+
const expr = parseExpression('$/+(a, b)');
|
|
75
|
+
|
|
76
|
+
expect(expr.value).toBe(15);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('supports logical helpers', () => {
|
|
80
|
+
Lightview.signal(true, 'isVip');
|
|
81
|
+
const expr = parseExpression('$/if(isVip, "Gold", "Silver")');
|
|
82
|
+
|
|
83
|
+
expect(expr.value).toBe('Gold');
|
|
84
|
+
|
|
85
|
+
Lightview.get('isVip').value = false;
|
|
86
|
+
expect(expr.value).toBe('Silver');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('supports the explosion operator (...)', () => {
|
|
90
|
+
LightviewX.state({ scores: [10, 20, 30] }, 'game');
|
|
91
|
+
const expr = parseExpression('$/sum(game/scores...)');
|
|
92
|
+
|
|
93
|
+
expect(expr.value).toBe(60);
|
|
94
|
+
|
|
95
|
+
const scores = Lightview.get('game').scores;
|
|
96
|
+
scores.push(40);
|
|
97
|
+
expect(expr.value).toBe(100);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handles nested navigation with helpers ($/user/upper(name))', () => {
|
|
101
|
+
LightviewX.state({ user: { name: 'charlie' } }, 'app');
|
|
102
|
+
// This tests: navigate to app/user, then call upper() on app/user.name
|
|
103
|
+
const expr = parseExpression('$/app/user/upper(name)');
|
|
104
|
+
|
|
105
|
+
expect(expr.value).toBe('CHARLIE');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import Lightview from '../../src/lightview.js';
|
|
3
|
+
import LightviewX from '../../src/lightview-x.js';
|
|
4
|
+
import LightviewCDOM from '../../src/lightview-cdom.js';
|
|
5
|
+
|
|
6
|
+
describe('cdom Reactivity', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
Lightview.registry.clear();
|
|
9
|
+
globalThis.Lightview = Lightview;
|
|
10
|
+
globalThis.LightviewCDOM = LightviewCDOM;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should update DOM-like values when signals change', () => {
|
|
14
|
+
const title = Lightview.signal('Hello', 'pageTitle');
|
|
15
|
+
const expr = LightviewCDOM.parseExpression('$/pageTitle');
|
|
16
|
+
|
|
17
|
+
expect(expr.value).toBe('Hello');
|
|
18
|
+
|
|
19
|
+
title.value = 'World';
|
|
20
|
+
expect(expr.value).toBe('World');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should handle deep state reactivity within a collection', () => {
|
|
24
|
+
const bills = LightviewX.state([
|
|
25
|
+
{ id: 1, amount: 100 },
|
|
26
|
+
{ id: 2, amount: 200 }
|
|
27
|
+
], 'bills');
|
|
28
|
+
|
|
29
|
+
const totalExpr = LightviewCDOM.parseExpression('$/sum(bills/amount...)');
|
|
30
|
+
expect(totalExpr.value).toBe(300);
|
|
31
|
+
|
|
32
|
+
// Update an existing item
|
|
33
|
+
bills[0].amount = 150;
|
|
34
|
+
expect(totalExpr.value).toBe(350);
|
|
35
|
+
|
|
36
|
+
// Add a new item
|
|
37
|
+
bills.push({ id: 3, amount: 50 });
|
|
38
|
+
expect(totalExpr.value).toBe(400);
|
|
39
|
+
|
|
40
|
+
// Remove an item
|
|
41
|
+
bills.pop();
|
|
42
|
+
expect(totalExpr.value).toBe(350);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle conditional logic reactively', () => {
|
|
46
|
+
const user = LightviewX.state({ loggedIn: false, name: 'Guest' }, 'user');
|
|
47
|
+
const greeting = LightviewCDOM.parseExpression('$/user/if(loggedIn, concat("Welcome, ", ./name), "Please Login")');
|
|
48
|
+
|
|
49
|
+
expect(greeting.value).toBe('Please Login');
|
|
50
|
+
|
|
51
|
+
user.loggedIn = true;
|
|
52
|
+
user.name = 'Alice';
|
|
53
|
+
expect(greeting.value).toBe('Welcome, Alice');
|
|
54
|
+
});
|
|
55
|
+
it('should handle reactivity on multi-level nested objects', () => {
|
|
56
|
+
const settings = LightviewX.state({
|
|
57
|
+
theme: {
|
|
58
|
+
colors: {
|
|
59
|
+
primary: 'blue',
|
|
60
|
+
secondary: 'gray'
|
|
61
|
+
},
|
|
62
|
+
font: {
|
|
63
|
+
size: '16px'
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
notifications: {
|
|
67
|
+
email: true
|
|
68
|
+
}
|
|
69
|
+
}, 'settings');
|
|
70
|
+
|
|
71
|
+
const colorExpr = LightviewCDOM.parseExpression('$/settings/theme/colors/primary');
|
|
72
|
+
const sizeExpr = LightviewCDOM.parseExpression('$/settings/theme/font/size');
|
|
73
|
+
const deepExpr = LightviewCDOM.parseExpression('$/concat(settings/theme/colors/primary, "-", settings/theme/font/size)');
|
|
74
|
+
|
|
75
|
+
expect(colorExpr.value).toBe('blue');
|
|
76
|
+
expect(sizeExpr.value).toBe('16px');
|
|
77
|
+
expect(deepExpr.value).toBe('blue-16px');
|
|
78
|
+
|
|
79
|
+
// Update deep property
|
|
80
|
+
settings.theme.colors.primary = 'red';
|
|
81
|
+
expect(colorExpr.value).toBe('red');
|
|
82
|
+
expect(deepExpr.value).toBe('red-16px');
|
|
83
|
+
|
|
84
|
+
// Update another deep branch
|
|
85
|
+
settings.theme.font.size = '18px';
|
|
86
|
+
expect(sizeExpr.value).toBe('18px');
|
|
87
|
+
expect(deepExpr.value).toBe('red-18px');
|
|
88
|
+
|
|
89
|
+
// Update via swapping nested object
|
|
90
|
+
settings.theme.colors = { primary: 'green', secondary: 'white' };
|
|
91
|
+
expect(colorExpr.value).toBe('green');
|
|
92
|
+
expect(deepExpr.value).toBe('green-18px');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle reactivity in a multi-level cdomC structure', () => {
|
|
96
|
+
// 1. Setup Multi-level State
|
|
97
|
+
const profile = LightviewX.state({
|
|
98
|
+
user: {
|
|
99
|
+
details: {
|
|
100
|
+
name: 'Bob',
|
|
101
|
+
avatar: { src: 'img.png', alt: 'Profile' }
|
|
102
|
+
},
|
|
103
|
+
stats: {
|
|
104
|
+
posts: 10,
|
|
105
|
+
followers: 50
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
ui: {
|
|
109
|
+
theme: 'dark'
|
|
110
|
+
}
|
|
111
|
+
}, 'profile');
|
|
112
|
+
|
|
113
|
+
// 2. Define Multi-level cdomC Structure using expressions
|
|
114
|
+
// This simulates parsing a nested .cdomc file
|
|
115
|
+
const cdomcStructure = {
|
|
116
|
+
div: {
|
|
117
|
+
id: 'profile-card',
|
|
118
|
+
class: '$/profile/ui/theme', // Bound to ui.theme
|
|
119
|
+
children: [
|
|
120
|
+
{
|
|
121
|
+
div: {
|
|
122
|
+
class: 'header',
|
|
123
|
+
children: [
|
|
124
|
+
{ h1: { children: ['$/profile/user/details/name'] } }, // Bound to user.details.name
|
|
125
|
+
{ img: { src: '$/profile/user/details/avatar/src', alt: '$/profile/user/details/avatar/alt' } }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
div: {
|
|
131
|
+
class: 'stats',
|
|
132
|
+
children: [
|
|
133
|
+
{ span: { children: ["Posts: ", '$/profile/user/stats/posts'] } }, // Bound to user.stats.posts
|
|
134
|
+
{ span: { children: ["Followers: ", '$/profile/user/stats/followers'] } }
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
// New section with reactive helpers
|
|
139
|
+
{
|
|
140
|
+
div: {
|
|
141
|
+
class: 'helpers-test',
|
|
142
|
+
children: [
|
|
143
|
+
// Logic + String helpers: "HELLO BOB" or "HELLO USER"
|
|
144
|
+
{ p: { children: ['$/upper(if(profile/user/details/name, profile/user/details/name, "User"))'] } },
|
|
145
|
+
// Math + Stats helpers: Sum of posts and followers
|
|
146
|
+
{ p: { children: ['Total interactions: ', '$/sum(profile/user/stats/posts, profile/user/stats/followers)'] } }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// 3. Hydrate the structure
|
|
155
|
+
const hydrated = LightviewCDOM.hydrate(cdomcStructure);
|
|
156
|
+
|
|
157
|
+
// 4. Inspect & Assert Initial State
|
|
158
|
+
const root = hydrated.div;
|
|
159
|
+
const header = root.children[0].div;
|
|
160
|
+
const stats = root.children[1].div;
|
|
161
|
+
const helpers = root.children[2].div;
|
|
162
|
+
|
|
163
|
+
expect(root.class.value).toBe('dark');
|
|
164
|
+
expect(header.children[0].h1.children[0].value).toBe('Bob');
|
|
165
|
+
expect(header.children[1].img.src.value).toBe('img.png');
|
|
166
|
+
expect(stats.children[0].span.children[1].value).toBe(10);
|
|
167
|
+
|
|
168
|
+
// Assert Helpers Initial State
|
|
169
|
+
expect(helpers.children[0].p.children[0].value).toBe('BOB'); // upper('Bob')
|
|
170
|
+
expect(helpers.children[1].p.children[1].value).toBe(60); // sum(10, 50)
|
|
171
|
+
|
|
172
|
+
// 5. Trigger Deep Updates
|
|
173
|
+
profile.user.details.name = 'Robert';
|
|
174
|
+
profile.ui.theme = 'light';
|
|
175
|
+
profile.user.stats.posts = 20;
|
|
176
|
+
|
|
177
|
+
// 6. Assert Updates Propagated to cdomC Structure
|
|
178
|
+
expect(root.class.value).toBe('light'); // Top-level attr
|
|
179
|
+
expect(header.children[0].h1.children[0].value).toBe('Robert'); // Deep nested text
|
|
180
|
+
expect(stats.children[0].span.children[1].value).toBe(20); // Deep nested sibling
|
|
181
|
+
|
|
182
|
+
// Assert Helpers Updated State
|
|
183
|
+
expect(helpers.children[0].p.children[0].value).toBe('ROBERT'); // upper('Robert')
|
|
184
|
+
expect(helpers.children[1].p.children[1].value).toBe(70); // sum(20, 50)
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
3
|
+
import { parseHTML } from 'linkedom';
|
|
4
|
+
import Lightview from '../src/lightview.js';
|
|
5
|
+
|
|
6
|
+
describe('text tag', () => {
|
|
7
|
+
let document;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
const dom = parseHTML('<!DOCTYPE html><html><body></body></html>');
|
|
11
|
+
document = dom.document;
|
|
12
|
+
globalThis.document = document;
|
|
13
|
+
globalThis.Text = dom.Text;
|
|
14
|
+
globalThis.HTMLElement = dom.HTMLElement;
|
|
15
|
+
globalThis.ShadowRoot = dom.ShadowRoot;
|
|
16
|
+
globalThis.MutationObserver = class { observe() { } disconnect() { } };
|
|
17
|
+
globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0);
|
|
18
|
+
globalThis.CSSStyleSheet = class { };
|
|
19
|
+
|
|
20
|
+
Lightview.registry.clear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should create a single text node with space-separated children', () => {
|
|
24
|
+
const el = Lightview.tags.text('Hello', 'World');
|
|
25
|
+
expect(el.domEl).toBeInstanceOf(globalThis.Text);
|
|
26
|
+
expect(el.domEl.textContent).toBe('Hello World');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle reactive children', async () => {
|
|
30
|
+
const name = Lightview.signal('Alice');
|
|
31
|
+
const el = Lightview.tags.text('Hello', () => name.value);
|
|
32
|
+
|
|
33
|
+
expect(el.domEl.textContent).toBe('Hello Alice');
|
|
34
|
+
|
|
35
|
+
name.value = 'Bob';
|
|
36
|
+
// Lightview effects run in a microtask or immediately depending on configuration
|
|
37
|
+
// Usually, Lightview signals in this project run synchronously if triggered?
|
|
38
|
+
// Let's check reactivity.
|
|
39
|
+
|
|
40
|
+
expect(el.domEl.textContent).toBe('Hello Bob');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle nested arrays and other types', () => {
|
|
44
|
+
const el = Lightview.tags.text('Count:', [1, 2, 3], true);
|
|
45
|
+
expect(el.domEl.textContent).toBe('Count: 1 2 3 true');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should ignore null and undefined values but keep the space', () => {
|
|
49
|
+
// Based on my implementation: .map(c => ... return val === null ? '' : String(val)).join(' ')
|
|
50
|
+
const el = Lightview.tags.text('a', null, 'b');
|
|
51
|
+
expect(el.domEl.textContent).toBe('a b');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should work with Lightview.tags.text(...) as a child of another element', () => {
|
|
55
|
+
const div = Lightview.tags.div(
|
|
56
|
+
Lightview.tags.text('Part', '1'),
|
|
57
|
+
' ',
|
|
58
|
+
Lightview.tags.text('Part', '2')
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(div.domEl.childNodes.length).toBe(3);
|
|
62
|
+
expect(div.domEl.childNodes[0]).toBeInstanceOf(globalThis.Text);
|
|
63
|
+
expect(div.domEl.childNodes[2]).toBeInstanceOf(globalThis.Text);
|
|
64
|
+
expect(div.domEl.textContent).toBe('Part 1 Part 2');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should use standard SVG text element when in SVG context', () => {
|
|
68
|
+
const svg = Lightview.element('svg', {}, [
|
|
69
|
+
{ tag: 'text', children: ['Hello SVG'] }
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const textElement = svg.domEl.firstChild;
|
|
73
|
+
expect(textElement.tagName.toLowerCase()).toBe('text');
|
|
74
|
+
expect(textElement.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
75
|
+
expect(textElement).not.toBeInstanceOf(globalThis.Text);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/vite.config.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
lib: {
|
|
7
|
+
// Define multiple entry points
|
|
8
|
+
entry: {
|
|
9
|
+
'lightview': resolve(__dirname, 'src/lightview.js'),
|
|
10
|
+
'lightview-x': resolve(__dirname, 'src/lightview-x.js'),
|
|
11
|
+
'lightview-cdom': resolve(__dirname, 'src/lightview-cdom.js'),
|
|
12
|
+
'lightview-all': resolve(__dirname, 'src/lightview-all.js')
|
|
13
|
+
},
|
|
14
|
+
name: 'Lightview',
|
|
15
|
+
formats: ['iife', 'es'],
|
|
16
|
+
fileName: (format, entryName) => `${entryName}.${format === 'iife' ? 'js' : 'mjs'}`
|
|
17
|
+
},
|
|
18
|
+
outDir: 'build_tmp',
|
|
19
|
+
emptyOutDir: false,
|
|
20
|
+
rollupOptions: {
|
|
21
|
+
// Ensure components and docs are NOT part of the library bundle
|
|
22
|
+
external: (id) => id.includes('/components/') || id.includes('/docs/'),
|
|
23
|
+
output: {
|
|
24
|
+
// Handle global names for IIFE builds
|
|
25
|
+
globals: {
|
|
26
|
+
'lightview': 'Lightview',
|
|
27
|
+
'lightview-x': 'LightviewX',
|
|
28
|
+
'lightview-cdom': 'LightviewCDOM'
|
|
29
|
+
},
|
|
30
|
+
extend: true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
minify: 'terser',
|
|
34
|
+
terserOptions: {
|
|
35
|
+
compress: {
|
|
36
|
+
drop_console: false, // Keep console warnings for now
|
|
37
|
+
pure_funcs: ['console.debug']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
test: {
|
|
42
|
+
environment: 'jsdom',
|
|
43
|
+
globals: true,
|
|
44
|
+
include: ['tests/**/*.test.js']
|
|
45
|
+
},
|
|
46
|
+
server: {
|
|
47
|
+
open: '/docs/index.html',
|
|
48
|
+
watch: {
|
|
49
|
+
usePolling: true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightview Skeleton Component (DaisyUI)
|
|
3
|
-
* @see https://daisyui.com/components/skeleton/
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import '../daisyui.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Skeleton Component - loading placeholder
|
|
10
|
-
* @param {Object} props
|
|
11
|
-
* @param {string} props.shape - 'circle' | 'rect'
|
|
12
|
-
* @param {string} props.width - Width class or value
|
|
13
|
-
* @param {string} props.height - Height class or value
|
|
14
|
-
* @param {boolean} props.useShadow - Render in Shadow DOM with isolated DaisyUI styles
|
|
15
|
-
*/
|
|
16
|
-
const Skeleton = (props = {}, ...children) => {
|
|
17
|
-
const { tags } = globalThis.Lightview || {};
|
|
18
|
-
const LVX = globalThis.LightviewX || {};
|
|
19
|
-
|
|
20
|
-
if (!tags) return null;
|
|
21
|
-
|
|
22
|
-
const { div, shadowDOM } = tags;
|
|
23
|
-
|
|
24
|
-
const {
|
|
25
|
-
shape,
|
|
26
|
-
width = 'w-full',
|
|
27
|
-
height = 'h-4',
|
|
28
|
-
useShadow,
|
|
29
|
-
class: className = '',
|
|
30
|
-
...rest
|
|
31
|
-
} = props;
|
|
32
|
-
|
|
33
|
-
const classes = ['skeleton', width, height];
|
|
34
|
-
if (shape === 'circle') classes.push('rounded-full');
|
|
35
|
-
if (className) classes.push(className);
|
|
36
|
-
|
|
37
|
-
const skeletonEl = div({ class: classes.join(' '), ...rest }, ...children);
|
|
38
|
-
|
|
39
|
-
// Check if we should use shadow DOM
|
|
40
|
-
let usesShadow = false;
|
|
41
|
-
if (LVX.shouldUseShadow) {
|
|
42
|
-
usesShadow = LVX.shouldUseShadow(useShadow);
|
|
43
|
-
} else {
|
|
44
|
-
usesShadow = useShadow === true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (usesShadow) {
|
|
48
|
-
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
|
|
49
|
-
|
|
50
|
-
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
|
|
51
|
-
|
|
52
|
-
return div({ class: 'contents' },
|
|
53
|
-
shadowDOM({ mode: 'open', adoptedStyleSheets },
|
|
54
|
-
div({ 'data-theme': themeValue },
|
|
55
|
-
skeletonEl
|
|
56
|
-
)
|
|
57
|
-
)
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return skeletonEl;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
globalThis.Lightview.tags.Skeleton = Skeleton;
|
|
65
|
-
|
|
66
|
-
export default Skeleton;
|