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.
Files changed (77) hide show
  1. package/build-bundles.mjs +109 -0
  2. package/cdom/helpers/array.js +70 -0
  3. package/cdom/helpers/compare.js +26 -0
  4. package/cdom/helpers/conditional.js +34 -0
  5. package/cdom/helpers/datetime.js +54 -0
  6. package/cdom/helpers/format.js +20 -0
  7. package/cdom/helpers/logic.js +24 -0
  8. package/cdom/helpers/lookup.js +25 -0
  9. package/cdom/helpers/math.js +34 -0
  10. package/cdom/helpers/network.js +41 -0
  11. package/cdom/helpers/state.js +77 -0
  12. package/cdom/helpers/stats.js +39 -0
  13. package/cdom/helpers/string.js +49 -0
  14. package/cdom/parser.js +602 -0
  15. package/components/actions/button.js +16 -3
  16. package/components/actions/swap.js +26 -3
  17. package/components/daisyui.js +1 -1
  18. package/components/data-display/alert.js +13 -3
  19. package/components/data-display/badge.js +11 -3
  20. package/components/data-display/kbd.js +9 -3
  21. package/components/data-display/loading.js +11 -3
  22. package/components/data-display/progress.js +11 -3
  23. package/components/data-display/radial-progress.js +12 -3
  24. package/components/data-display/tooltip.js +17 -0
  25. package/components/layout/divider.js +21 -1
  26. package/components/layout/indicator.js +14 -0
  27. package/components/navigation/tabs.js +291 -16
  28. package/docs/api/elements.html +125 -49
  29. package/docs/api/hypermedia.html +29 -2
  30. package/docs/api/index.html +6 -2
  31. package/docs/api/nav.html +18 -4
  32. package/docs/cdom-nav.html +29 -0
  33. package/docs/cdom.html +362 -0
  34. package/docs/components/alert.html +8 -8
  35. package/docs/components/badge.html +55 -0
  36. package/docs/components/button.html +78 -92
  37. package/docs/components/component-nav.html +1 -1
  38. package/docs/components/divider.html +65 -21
  39. package/docs/components/indicator.html +85 -31
  40. package/docs/components/kbd.html +64 -25
  41. package/docs/components/loading.html +55 -39
  42. package/docs/components/progress.html +44 -3
  43. package/docs/components/radial-progress.html +32 -12
  44. package/docs/components/swap.html +183 -100
  45. package/docs/components/tabs.html +146 -278
  46. package/docs/components/tooltip.html +71 -31
  47. package/docs/getting-started/index.html +7 -5
  48. package/docs/index.html +1 -1
  49. package/docs/syntax-nav.html +10 -0
  50. package/docs/syntax.html +8 -6
  51. package/index.html +2 -2
  52. package/lightview-all.js +1 -0
  53. package/lightview-cdom.js +1 -0
  54. package/lightview-x.js +1 -1608
  55. package/lightview.js +1 -766
  56. package/lightview.js.bak +1 -0
  57. package/package.json +6 -2
  58. package/src/lightview-all.js +10 -0
  59. package/src/lightview-cdom.js +305 -0
  60. package/src/lightview-x.js +1581 -0
  61. package/src/lightview.js +694 -0
  62. package/src/reactivity/signal.js +133 -0
  63. package/src/reactivity/state.js +217 -0
  64. package/test-text-tag.js +6 -0
  65. package/tests/cdom/fixtures/helpers.cdomc +62 -0
  66. package/tests/cdom/fixtures/user.cdom +14 -0
  67. package/tests/cdom/fixtures/user.cdomc +12 -0
  68. package/tests/cdom/fixtures/user.odom +18 -0
  69. package/tests/cdom/fixtures/user.vdom +11 -0
  70. package/tests/cdom/helpers.test.js +121 -0
  71. package/tests/cdom/loader.test.js +125 -0
  72. package/tests/cdom/parser.test.js +108 -0
  73. package/tests/cdom/reactivity.test.js +186 -0
  74. package/tests/text-tag.test.js +77 -0
  75. package/vite.config.mjs +52 -0
  76. package/components/data-display/skeleton.js +0 -66
  77. 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?.createCustomElement) {
144
- const ButtonElement = globalThis.LightviewX.createCustomElement(Button);
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 active state
38
- const isActive = typeof active === 'function' ? active : () => active;
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
- if (onChange) onChange(e.target.checked);
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;
@@ -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@4.12.10/dist/full.min.css';
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;