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
package/cdom/parser.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LIGHTVIEW CDOM PARSER
|
|
3
|
+
* Responsible for resolving reactive paths and expressions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const helpers = new Map();
|
|
7
|
+
const helperOptions = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers a global helper function.
|
|
11
|
+
*/
|
|
12
|
+
export const registerHelper = (name, fn, options = {}) => {
|
|
13
|
+
helpers.set(name, fn);
|
|
14
|
+
if (options) helperOptions.set(name, options);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getLV = () => globalThis.Lightview || null;
|
|
18
|
+
const getRegistry = () => getLV()?.registry || null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents a mutable target (a property on an object).
|
|
22
|
+
* Allows cdom-bind and mutation helpers to work with plain object properties
|
|
23
|
+
* by treating them as if they had a .value property.
|
|
24
|
+
*/
|
|
25
|
+
export class BindingTarget {
|
|
26
|
+
constructor(parent, key) {
|
|
27
|
+
this.parent = parent;
|
|
28
|
+
this.key = key;
|
|
29
|
+
this.isBindingTarget = true; // Marker for duck-typing when instanceof fails
|
|
30
|
+
}
|
|
31
|
+
get value() { return this.parent[this.key]; }
|
|
32
|
+
set value(v) { this.parent[this.key] = v; }
|
|
33
|
+
get __parent__() { return this.parent; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unwraps a signal-like value to its raw value.
|
|
38
|
+
* This should be used to establish reactive dependencies within a computed context.
|
|
39
|
+
*/
|
|
40
|
+
export const unwrapSignal = (val) => {
|
|
41
|
+
if (val && typeof val === 'function' && 'value' in val) {
|
|
42
|
+
return val.value;
|
|
43
|
+
}
|
|
44
|
+
if (val && typeof val === 'object' && !(globalThis.Node && val instanceof globalThis.Node) && 'value' in val) {
|
|
45
|
+
return val.value;
|
|
46
|
+
}
|
|
47
|
+
return val;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves segments of a path against a root object, unwrapping signals as it goes.
|
|
53
|
+
*/
|
|
54
|
+
const traverse = (root, segments) => {
|
|
55
|
+
let current = root;
|
|
56
|
+
for (const segment of segments) {
|
|
57
|
+
if (!segment) continue;
|
|
58
|
+
current = unwrapSignal(current);
|
|
59
|
+
if (current == null) return undefined;
|
|
60
|
+
|
|
61
|
+
const key = segment.startsWith('[') ? segment.slice(1, -1) : segment;
|
|
62
|
+
current = current[key];
|
|
63
|
+
}
|
|
64
|
+
return unwrapSignal(current);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolves segments but keeps the final value as a proxy/signal for use as context.
|
|
69
|
+
* Only unwraps intermediate values during traversal.
|
|
70
|
+
*/
|
|
71
|
+
const traverseAsContext = (root, segments) => {
|
|
72
|
+
let current = root;
|
|
73
|
+
for (let i = 0; i < segments.length; i++) {
|
|
74
|
+
const segment = segments[i];
|
|
75
|
+
if (!segment) continue;
|
|
76
|
+
const key = segment.startsWith('[') ? segment.slice(1, -1) : segment;
|
|
77
|
+
|
|
78
|
+
const unwrapped = unwrapSignal(current);
|
|
79
|
+
if (unwrapped == null) return undefined;
|
|
80
|
+
|
|
81
|
+
if (i === segments.length - 1) {
|
|
82
|
+
return new BindingTarget(unwrapped, key);
|
|
83
|
+
}
|
|
84
|
+
current = unwrapped[key];
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves a path against a context and the global registry.
|
|
91
|
+
*/
|
|
92
|
+
export const resolvePath = (path, context) => {
|
|
93
|
+
if (typeof path !== 'string') return path;
|
|
94
|
+
|
|
95
|
+
const registry = getRegistry();
|
|
96
|
+
|
|
97
|
+
// Current context: .
|
|
98
|
+
if (path === '.') return unwrapSignal(context);
|
|
99
|
+
|
|
100
|
+
// Global absolute path: $/something
|
|
101
|
+
if (path.startsWith('$/')) {
|
|
102
|
+
const [rootName, ...rest] = path.slice(2).split('/');
|
|
103
|
+
const rootSignal = registry?.get(rootName);
|
|
104
|
+
if (!rootSignal) return undefined;
|
|
105
|
+
|
|
106
|
+
// Root can be a signal or a state proxy
|
|
107
|
+
return traverse(rootSignal, rest);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Relative path from current context
|
|
111
|
+
if (path.startsWith('./')) {
|
|
112
|
+
return traverse(context, path.slice(2).split('/'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parent path
|
|
116
|
+
if (path.startsWith('../')) {
|
|
117
|
+
return traverse(context?.__parent__, path.slice(3).split('/'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Path with separators - treat as relative
|
|
121
|
+
if (path.includes('/') || path.includes('.')) {
|
|
122
|
+
return traverse(context, path.split(/[\/.]/));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if it's a single word that exists in the context
|
|
126
|
+
const unwrappedContext = unwrapSignal(context);
|
|
127
|
+
if (unwrappedContext && typeof unwrappedContext === 'object') {
|
|
128
|
+
if (path in unwrappedContext || unwrappedContext[path] !== undefined) {
|
|
129
|
+
// Use traverse with one segment to ensure signal unwrapping if context[path] is a signal
|
|
130
|
+
return traverse(unwrappedContext, [path]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Return as literal
|
|
135
|
+
return path;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Like resolvePath, but preserves proxy/signal wrappers for use as evaluation context.
|
|
140
|
+
*/
|
|
141
|
+
export const resolvePathAsContext = (path, context) => {
|
|
142
|
+
if (typeof path !== 'string') return path;
|
|
143
|
+
|
|
144
|
+
const registry = getRegistry();
|
|
145
|
+
|
|
146
|
+
// Current context: .
|
|
147
|
+
if (path === '.') return context;
|
|
148
|
+
|
|
149
|
+
// Global absolute path: $/something
|
|
150
|
+
if (path.startsWith('$/')) {
|
|
151
|
+
const segments = path.slice(2).split(/[\/.]/);
|
|
152
|
+
const rootName = segments.shift();
|
|
153
|
+
const rootSignal = registry?.get(rootName);
|
|
154
|
+
if (!rootSignal) return undefined;
|
|
155
|
+
|
|
156
|
+
return traverseAsContext(rootSignal, segments);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Relative path from current context
|
|
160
|
+
if (path.startsWith('./')) {
|
|
161
|
+
return traverseAsContext(context, path.slice(2).split(/[\/.]/));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parent path
|
|
165
|
+
if (path.startsWith('../')) {
|
|
166
|
+
return traverseAsContext(context?.__parent__, path.slice(3).split(/[\/.]/));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Path with separators
|
|
170
|
+
if (path.includes('/') || path.includes('.')) {
|
|
171
|
+
return traverseAsContext(context, path.split(/[\/.]/));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Single property access
|
|
175
|
+
const unwrappedContext = unwrapSignal(context);
|
|
176
|
+
if (unwrappedContext && typeof unwrappedContext === 'object') {
|
|
177
|
+
// If it looks like a variable name, assume it's a property on the context
|
|
178
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(path)) {
|
|
179
|
+
return new BindingTarget(unwrappedContext, path);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return path;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Represents a lazy value that will be resolved later with a specific context.
|
|
188
|
+
* Used for iteration placeholders like '_' and '$event'.
|
|
189
|
+
*/
|
|
190
|
+
class LazyValue {
|
|
191
|
+
constructor(fn) {
|
|
192
|
+
this.fn = fn;
|
|
193
|
+
this.isLazy = true;
|
|
194
|
+
}
|
|
195
|
+
resolve(context) {
|
|
196
|
+
return this.fn(context);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Helper to resolve an argument which could be a literal, a path, or an explosion.
|
|
202
|
+
* @param {string} arg - The argument string
|
|
203
|
+
* @param {object} context - The local context object
|
|
204
|
+
* @param {boolean} globalMode - If true, bare paths are resolved against global registry
|
|
205
|
+
*/
|
|
206
|
+
const resolveArgument = (arg, context, globalMode = false) => {
|
|
207
|
+
// 1. Quoted Strings
|
|
208
|
+
if ((arg.startsWith("'") && arg.endsWith("'")) || (arg.startsWith('"') && arg.endsWith('"'))) {
|
|
209
|
+
return { value: arg.slice(1, -1), isLiteral: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. Numbers
|
|
213
|
+
if (arg !== '' && !isNaN(Number(arg))) {
|
|
214
|
+
return { value: Number(arg), isLiteral: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 3. Booleans / Null
|
|
218
|
+
if (arg === 'true') return { value: true, isLiteral: true };
|
|
219
|
+
if (arg === 'false') return { value: false, isLiteral: true };
|
|
220
|
+
if (arg === 'null') return { value: null, isLiteral: true };
|
|
221
|
+
|
|
222
|
+
// 4. Placeholder / Lazy Evaluation (_)
|
|
223
|
+
if (arg === '_' || arg.startsWith('_/') || arg.startsWith('_.')) {
|
|
224
|
+
return {
|
|
225
|
+
value: new LazyValue((item) => {
|
|
226
|
+
if (arg === '_') return item;
|
|
227
|
+
const path = arg.startsWith('_.') ? arg.slice(2) : arg.slice(2);
|
|
228
|
+
return resolvePath(path, item);
|
|
229
|
+
}),
|
|
230
|
+
isLazy: true
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 5. Event Placeholder ($event)
|
|
235
|
+
if (arg === '$event' || arg.startsWith('$event/') || arg.startsWith('$event.')) {
|
|
236
|
+
return {
|
|
237
|
+
value: new LazyValue((event) => {
|
|
238
|
+
if (arg === '$event') return event;
|
|
239
|
+
const path = arg.startsWith('$event.') ? arg.slice(7) : arg.slice(7);
|
|
240
|
+
return resolvePath(path, event);
|
|
241
|
+
}),
|
|
242
|
+
isLazy: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 6. Expression / Nested Function
|
|
247
|
+
if (arg.includes('(')) {
|
|
248
|
+
// Nested function call - recursively resolve
|
|
249
|
+
let nestedExpr = arg;
|
|
250
|
+
if (arg.startsWith('/')) {
|
|
251
|
+
nestedExpr = '$' + arg;
|
|
252
|
+
} else if (globalMode && !arg.startsWith('$') && !arg.startsWith('./')) {
|
|
253
|
+
nestedExpr = `$/${arg}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const val = resolveExpression(nestedExpr, context);
|
|
257
|
+
if (val instanceof LazyValue) {
|
|
258
|
+
return { value: val, isLazy: true };
|
|
259
|
+
}
|
|
260
|
+
return { value: val, isSignal: false }; // Already resolved in current effect
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 7. Explosion operator
|
|
264
|
+
const isExplosion = arg.endsWith('...');
|
|
265
|
+
const pathStr = isExplosion ? arg.slice(0, -3) : arg;
|
|
266
|
+
|
|
267
|
+
// 8. Path resolution
|
|
268
|
+
let normalizedPath;
|
|
269
|
+
if (pathStr.startsWith('/')) {
|
|
270
|
+
normalizedPath = '$' + pathStr;
|
|
271
|
+
} else if (pathStr.startsWith('$') || pathStr.startsWith('./') || pathStr.startsWith('../')) {
|
|
272
|
+
normalizedPath = pathStr;
|
|
273
|
+
} else if (globalMode) {
|
|
274
|
+
normalizedPath = `$/${pathStr}`;
|
|
275
|
+
} else {
|
|
276
|
+
normalizedPath = `./${pathStr}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// For explosion, we may need to extract a property from each item in an array
|
|
280
|
+
if (isExplosion) {
|
|
281
|
+
const pathParts = normalizedPath.split('/');
|
|
282
|
+
const propName = pathParts.pop();
|
|
283
|
+
const parentPath = pathParts.join('/');
|
|
284
|
+
|
|
285
|
+
const parent = parentPath ? resolvePath(parentPath, context) : context;
|
|
286
|
+
const unwrappedParent = unwrapSignal(parent);
|
|
287
|
+
|
|
288
|
+
if (Array.isArray(unwrappedParent)) {
|
|
289
|
+
const values = unwrappedParent.map(item => {
|
|
290
|
+
const unwrappedItem = unwrapSignal(item);
|
|
291
|
+
return unwrappedItem && unwrappedItem[propName];
|
|
292
|
+
});
|
|
293
|
+
return { value: values, isExplosion: true };
|
|
294
|
+
} else if (unwrappedParent && typeof unwrappedParent === 'object') {
|
|
295
|
+
const val = unwrappedParent[propName];
|
|
296
|
+
return { value: unwrapSignal(val), isExplosion: true };
|
|
297
|
+
}
|
|
298
|
+
return { value: undefined, isExplosion: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const value = resolvePathAsContext(normalizedPath, context);
|
|
302
|
+
return { value, isExplosion: false };
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Core logic to resolve an CDOM expression.
|
|
309
|
+
* This can be called recursively and will register all accessed dependencies
|
|
310
|
+
* against the currently active Lightview effect.
|
|
311
|
+
*/
|
|
312
|
+
export const resolveExpression = (expr, context) => {
|
|
313
|
+
if (typeof expr !== 'string') return expr;
|
|
314
|
+
|
|
315
|
+
const funcStart = expr.indexOf('(');
|
|
316
|
+
if (funcStart !== -1 && expr.endsWith(')')) {
|
|
317
|
+
const fullPath = expr.slice(0, funcStart).trim();
|
|
318
|
+
const argsStr = expr.slice(funcStart + 1, -1);
|
|
319
|
+
|
|
320
|
+
const segments = fullPath.split('/');
|
|
321
|
+
let funcName = segments.pop().replace(/^\$/, '');
|
|
322
|
+
|
|
323
|
+
// Handle case where path ends in / (like $/ for division helper)
|
|
324
|
+
if (funcName === '' && (segments.length > 0 || fullPath === '/')) {
|
|
325
|
+
funcName = '/';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const navPath = segments.join('/');
|
|
329
|
+
|
|
330
|
+
const isGlobalExpr = expr.startsWith('$/') || expr.startsWith('$');
|
|
331
|
+
|
|
332
|
+
let baseContext = context;
|
|
333
|
+
if (navPath && navPath !== '$') {
|
|
334
|
+
baseContext = resolvePathAsContext(navPath, context);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const helper = helpers.get(funcName);
|
|
338
|
+
if (!helper) {
|
|
339
|
+
globalThis.console?.warn(`LightviewCDOM: Helper "${funcName}" not found.`);
|
|
340
|
+
return expr;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const options = helperOptions.get(funcName) || {};
|
|
344
|
+
|
|
345
|
+
// Split arguments respecting quotes and parentheses
|
|
346
|
+
const argsList = [];
|
|
347
|
+
let current = '', depth = 0, quote = null;
|
|
348
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
349
|
+
const char = argsStr[i];
|
|
350
|
+
if (char === quote) quote = null;
|
|
351
|
+
else if (!quote && (char === "'" || char === '"')) quote = char;
|
|
352
|
+
else if (!quote && char === '(') depth++;
|
|
353
|
+
else if (!quote && char === ')') depth--;
|
|
354
|
+
else if (!quote && char === ',' && depth === 0) {
|
|
355
|
+
argsList.push(current.trim());
|
|
356
|
+
current = '';
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
current += char;
|
|
360
|
+
}
|
|
361
|
+
if (current) argsList.push(current.trim());
|
|
362
|
+
|
|
363
|
+
const resolvedArgs = [];
|
|
364
|
+
let hasLazy = false;
|
|
365
|
+
for (let i = 0; i < argsList.length; i++) {
|
|
366
|
+
const arg = argsList[i];
|
|
367
|
+
const useGlobalMode = isGlobalExpr && (navPath === '$' || !navPath);
|
|
368
|
+
const res = resolveArgument(arg, baseContext, useGlobalMode);
|
|
369
|
+
|
|
370
|
+
if (res.isLazy) hasLazy = true;
|
|
371
|
+
|
|
372
|
+
// For mutation helpers, skip unwrapping for specific arguments (usually the first)
|
|
373
|
+
const shouldUnwrap = !(options.pathAware && i === 0);
|
|
374
|
+
|
|
375
|
+
let val = shouldUnwrap ? unwrapSignal(res.value) : res.value;
|
|
376
|
+
|
|
377
|
+
if (res.isExplosion && Array.isArray(val)) {
|
|
378
|
+
resolvedArgs.push(...val.map(v => shouldUnwrap ? unwrapSignal(v) : v));
|
|
379
|
+
} else {
|
|
380
|
+
resolvedArgs.push(val);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (hasLazy) {
|
|
385
|
+
// Return a new LazyValue that resolves all its lazy arguments
|
|
386
|
+
return new LazyValue((contextOverride) => {
|
|
387
|
+
const finalArgs = resolvedArgs.map((arg, i) => {
|
|
388
|
+
const shouldUnwrap = !(options.pathAware && i === 0);
|
|
389
|
+
const resolved = arg instanceof LazyValue ? arg.resolve(contextOverride) : arg;
|
|
390
|
+
return shouldUnwrap ? unwrapSignal(resolved) : resolved;
|
|
391
|
+
});
|
|
392
|
+
return helper(...finalArgs);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const result = helper(...resolvedArgs);
|
|
397
|
+
return unwrapSignal(result);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return unwrapSignal(resolvePath(expr, context));
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Parses an CDOM expression into a reactive signal.
|
|
405
|
+
*/
|
|
406
|
+
export const parseExpression = (expr, context) => {
|
|
407
|
+
const LV = getLV();
|
|
408
|
+
if (!LV || typeof expr !== 'string') return expr;
|
|
409
|
+
|
|
410
|
+
return LV.computed(() => resolveExpression(expr, context));
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Parses CDOMC (Concise CDOM) content into a JSON object.
|
|
415
|
+
* Supports unquoted keys/values and strictly avoids 'eval'.
|
|
416
|
+
*/
|
|
417
|
+
export const parseCDOMC = (input) => {
|
|
418
|
+
let i = 0;
|
|
419
|
+
const len = input.length;
|
|
420
|
+
|
|
421
|
+
const skipWhitespace = () => {
|
|
422
|
+
while (i < len) {
|
|
423
|
+
const char = input[i];
|
|
424
|
+
|
|
425
|
+
// Standard whitespace
|
|
426
|
+
if (/\s/.test(char)) {
|
|
427
|
+
i++;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Comments
|
|
432
|
+
if (char === '/') {
|
|
433
|
+
const next = input[i + 1];
|
|
434
|
+
if (next === '/') {
|
|
435
|
+
// Single-line comment
|
|
436
|
+
i += 2;
|
|
437
|
+
while (i < len && input[i] !== '\n' && input[i] !== '\r') i++;
|
|
438
|
+
continue;
|
|
439
|
+
} else if (next === '*') {
|
|
440
|
+
// Multi-line comment (non-nested)
|
|
441
|
+
i += 2;
|
|
442
|
+
while (i < len) {
|
|
443
|
+
if (input[i] === '*' && input[i + 1] === '/') {
|
|
444
|
+
i += 2;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const parseString = () => {
|
|
458
|
+
const quote = input[i++];
|
|
459
|
+
let res = '';
|
|
460
|
+
while (i < len) {
|
|
461
|
+
const char = input[i++];
|
|
462
|
+
if (char === quote) return new String(res);
|
|
463
|
+
if (char === '\\') {
|
|
464
|
+
const next = input[i++];
|
|
465
|
+
if (next === 'n') res += '\n';
|
|
466
|
+
else if (next === 't') res += '\t';
|
|
467
|
+
else if (next === '"') res += '"';
|
|
468
|
+
else if (next === "'") res += "'";
|
|
469
|
+
else if (next === '\\') res += '\\';
|
|
470
|
+
else res += next;
|
|
471
|
+
} else {
|
|
472
|
+
res += char;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
throw new Error("Unterminated string");
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const parseWord = () => {
|
|
479
|
+
const start = i;
|
|
480
|
+
let depth = 0;
|
|
481
|
+
|
|
482
|
+
while (i < len) {
|
|
483
|
+
const char = input[i];
|
|
484
|
+
|
|
485
|
+
// If inside parentheses, ignore everything except matching parenthesis
|
|
486
|
+
if (depth > 0) {
|
|
487
|
+
if (char === ')') depth--;
|
|
488
|
+
else if (char === '(') depth++;
|
|
489
|
+
i++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Structural characters that end a word (at depth 0)
|
|
494
|
+
if (/[\s:,{}\[\]"'`()]/.test(char)) {
|
|
495
|
+
// Special case: if we see '(', we are entering a function call word
|
|
496
|
+
if (char === '(') {
|
|
497
|
+
depth++;
|
|
498
|
+
i++;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
i++;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const word = input.slice(start, i);
|
|
508
|
+
if (word === 'true') return true;
|
|
509
|
+
if (word === 'false') return false;
|
|
510
|
+
if (word === 'null') return null;
|
|
511
|
+
// Check if valid number
|
|
512
|
+
if (word.trim() !== '' && !isNaN(Number(word))) return Number(word);
|
|
513
|
+
return word;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const parseValue = () => {
|
|
517
|
+
skipWhitespace();
|
|
518
|
+
if (i >= len) return undefined;
|
|
519
|
+
const char = input[i];
|
|
520
|
+
|
|
521
|
+
if (char === '{') return parseObject();
|
|
522
|
+
if (char === '[') return parseArray();
|
|
523
|
+
if (char === '"' || char === "'") return parseString();
|
|
524
|
+
|
|
525
|
+
return parseWord();
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const parseObject = () => {
|
|
529
|
+
i++; // skip '{'
|
|
530
|
+
const obj = {};
|
|
531
|
+
skipWhitespace();
|
|
532
|
+
if (i < len && input[i] === '}') {
|
|
533
|
+
i++;
|
|
534
|
+
return obj;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
while (i < len) {
|
|
538
|
+
skipWhitespace();
|
|
539
|
+
let key;
|
|
540
|
+
if (input[i] === '"' || input[i] === "'") key = parseString();
|
|
541
|
+
else key = parseWord();
|
|
542
|
+
|
|
543
|
+
skipWhitespace();
|
|
544
|
+
if (input[i] !== ':') throw new Error(`Expected ':' at position ${i}, found '${input[i]}'`);
|
|
545
|
+
i++; // skip ':'
|
|
546
|
+
|
|
547
|
+
const value = parseValue();
|
|
548
|
+
obj[String(key)] = value;
|
|
549
|
+
|
|
550
|
+
skipWhitespace();
|
|
551
|
+
if (input[i] === '}') {
|
|
552
|
+
i++;
|
|
553
|
+
return obj;
|
|
554
|
+
}
|
|
555
|
+
if (input[i] === ',') {
|
|
556
|
+
i++;
|
|
557
|
+
skipWhitespace();
|
|
558
|
+
if (input[i] === '}') {
|
|
559
|
+
i++;
|
|
560
|
+
return obj;
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
throw new Error(`Expected '}' or ',' at position ${i}, found '${input[i]}'`);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const parseArray = () => {
|
|
569
|
+
i++; // skip '['
|
|
570
|
+
const arr = [];
|
|
571
|
+
skipWhitespace();
|
|
572
|
+
if (i < len && input[i] === ']') {
|
|
573
|
+
i++;
|
|
574
|
+
return arr;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
while (i < len) {
|
|
578
|
+
const val = parseValue();
|
|
579
|
+
arr.push(val);
|
|
580
|
+
|
|
581
|
+
skipWhitespace();
|
|
582
|
+
if (input[i] === ']') {
|
|
583
|
+
i++;
|
|
584
|
+
return arr;
|
|
585
|
+
}
|
|
586
|
+
if (input[i] === ',') {
|
|
587
|
+
i++;
|
|
588
|
+
skipWhitespace();
|
|
589
|
+
if (input[i] === ']') {
|
|
590
|
+
i++;
|
|
591
|
+
return arr;
|
|
592
|
+
}
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
throw new Error(`Expected ']' or ',' at position ${i}, found '${input[i]}'`);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
skipWhitespace();
|
|
600
|
+
const res = parseValue();
|
|
601
|
+
return res;
|
|
602
|
+
};
|
|
@@ -139,9 +139,22 @@ const Button = (props = {}, ...children) => {
|
|
|
139
139
|
|
|
140
140
|
globalThis.Lightview.tags.Button = Button;
|
|
141
141
|
|
|
142
|
-
// Register as Custom Element
|
|
143
|
-
if (globalThis.LightviewX
|
|
144
|
-
const ButtonElement = globalThis.LightviewX.
|
|
142
|
+
// Register as Custom Element using customElementWrapper
|
|
143
|
+
if (globalThis.LightviewX && typeof customElements !== 'undefined') {
|
|
144
|
+
const ButtonElement = globalThis.LightviewX.customElementWrapper(Button, {
|
|
145
|
+
attributeMap: {
|
|
146
|
+
color: String,
|
|
147
|
+
size: String,
|
|
148
|
+
variant: String,
|
|
149
|
+
disabled: Boolean,
|
|
150
|
+
loading: Boolean,
|
|
151
|
+
active: Boolean,
|
|
152
|
+
glass: Boolean,
|
|
153
|
+
noAnimation: Boolean
|
|
154
|
+
},
|
|
155
|
+
childElements: {} // No child components for Button, uses slot
|
|
156
|
+
});
|
|
157
|
+
|
|
145
158
|
if (!customElements.get('lv-button')) {
|
|
146
159
|
customElements.define('lv-button', ButtonElement);
|
|
147
160
|
}
|
|
@@ -34,8 +34,9 @@ const Swap = (props = {}, ...children) => {
|
|
|
34
34
|
else if (effect === 'flip') classes.push('swap-flip');
|
|
35
35
|
if (className) classes.push(className);
|
|
36
36
|
|
|
37
|
-
// Handle reactive
|
|
38
|
-
const
|
|
37
|
+
// Handle internal state for non-reactive/uncontrolled usage
|
|
38
|
+
const internalActive = signal(active);
|
|
39
|
+
const isActive = typeof active === 'function' ? active : () => internalActive.value;
|
|
39
40
|
|
|
40
41
|
const swapEl = label({
|
|
41
42
|
class: () => {
|
|
@@ -49,7 +50,11 @@ const Swap = (props = {}, ...children) => {
|
|
|
49
50
|
type: 'checkbox',
|
|
50
51
|
checked: isActive,
|
|
51
52
|
onchange: (e) => {
|
|
52
|
-
|
|
53
|
+
const checked = e.target.checked;
|
|
54
|
+
if (typeof active !== 'function') {
|
|
55
|
+
internalActive.value = checked;
|
|
56
|
+
}
|
|
57
|
+
if (onChange) onChange(checked);
|
|
53
58
|
}
|
|
54
59
|
}),
|
|
55
60
|
...children
|
|
@@ -115,4 +120,22 @@ tags.Swap = Swap;
|
|
|
115
120
|
tags['Swap.On'] = Swap.On;
|
|
116
121
|
tags['Swap.Off'] = Swap.Off;
|
|
117
122
|
|
|
123
|
+
// Register as Custom Element using customElementWrapper
|
|
124
|
+
if (globalThis.LightviewX && typeof customElements !== 'undefined') {
|
|
125
|
+
const SwapElement = globalThis.LightviewX.customElementWrapper(Swap, {
|
|
126
|
+
attributeMap: {
|
|
127
|
+
active: Boolean,
|
|
128
|
+
effect: String
|
|
129
|
+
},
|
|
130
|
+
childElements: {
|
|
131
|
+
'on': Swap.On,
|
|
132
|
+
'off': Swap.Off
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!customElements.get('lv-swap')) {
|
|
137
|
+
customElements.define('lv-swap', SwapElement);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
export default Swap;
|
package/components/daisyui.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This module ensures DaisyUI CSS is loaded and provides utilities for components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const DAISYUI_CDN = 'https://cdn.jsdelivr.net/npm/daisyui@
|
|
6
|
+
const DAISYUI_CDN = 'https://cdn.jsdelivr.net/npm/daisyui@5.5.14/daisyui.min.css';
|
|
7
7
|
const TAILWIND_CDN = 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4';
|
|
8
8
|
|
|
9
9
|
let daisyLoaded = false;
|