@ahriknow/lux 0.0.1

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/README_zh-CN.md +127 -0
  4. package/dist/components/lux-button/index.iife.min.js +292 -0
  5. package/dist/components/lux-button/index.min.js +292 -0
  6. package/dist/components/lux-code/index.iife.min.js +290 -0
  7. package/dist/components/lux-code/index.min.js +290 -0
  8. package/dist/components/lux-dropdown/index.iife.min.js +162 -0
  9. package/dist/components/lux-dropdown/index.min.js +162 -0
  10. package/dist/components/lux-example/index.iife.min.js +88 -0
  11. package/dist/components/lux-example/index.min.js +88 -0
  12. package/dist/components/lux-icon/index.iife.min.js +22 -0
  13. package/dist/components/lux-icon/index.min.js +22 -0
  14. package/dist/components/lux-input/index.iife.min.js +238 -0
  15. package/dist/components/lux-input/index.min.js +238 -0
  16. package/dist/components/lux-layout/index.iife.min.js +90 -0
  17. package/dist/components/lux-layout/index.min.js +90 -0
  18. package/dist/components/lux-menu/index.iife.min.js +193 -0
  19. package/dist/components/lux-menu/index.min.js +193 -0
  20. package/dist/components/lux-scroll/index.iife.min.js +137 -0
  21. package/dist/components/lux-scroll/index.min.js +137 -0
  22. package/dist/components/lux-switch/index.iife.min.js +116 -0
  23. package/dist/components/lux-switch/index.min.js +116 -0
  24. package/dist/components/lux-table/index.iife.min.js +67 -0
  25. package/dist/components/lux-table/index.min.js +67 -0
  26. package/dist/lux.core.min.js +1 -0
  27. package/dist/lux.i18n.min.js +1 -0
  28. package/dist/lux.iife.js +1822 -0
  29. package/dist/lux.iife.js.map +1 -0
  30. package/dist/lux.iife.min.js +1 -0
  31. package/dist/lux.js +1792 -0
  32. package/dist/lux.js.map +1 -0
  33. package/dist/lux.min.js +1 -0
  34. package/dist/lux.router.min.js +1 -0
  35. package/dist/lux.template.min.js +1 -0
  36. package/dist/lux.theme.min.js +1 -0
  37. package/dist/themes/dark.css +130 -0
  38. package/dist/themes/light.css +128 -0
  39. package/package.json +64 -0
  40. package/src/components/lux-button/index.js +319 -0
  41. package/src/components/lux-code/index.js +382 -0
  42. package/src/components/lux-dropdown/index.js +256 -0
  43. package/src/components/lux-example/index.js +117 -0
  44. package/src/components/lux-icon/index.js +180 -0
  45. package/src/components/lux-input/index.js +363 -0
  46. package/src/components/lux-layout/index.js +222 -0
  47. package/src/components/lux-menu/index.js +283 -0
  48. package/src/components/lux-scroll/index.js +349 -0
  49. package/src/components/lux-switch/index.js +203 -0
  50. package/src/components/lux-table/index.js +105 -0
  51. package/src/core.js +7 -0
  52. package/src/element.js +477 -0
  53. package/src/i18n/format.js +108 -0
  54. package/src/i18n/index.js +102 -0
  55. package/src/i18n/locale.js +26 -0
  56. package/src/index.js +22 -0
  57. package/src/router.js +330 -0
  58. package/src/template.js +402 -0
  59. package/src/theme/color.js +148 -0
  60. package/src/theme/create.js +97 -0
  61. package/src/theme/index.js +2 -0
  62. package/src/theme/tokens.js +128 -0
  63. package/src/themes/dark.css +130 -0
  64. package/src/themes/light.css +128 -0
@@ -0,0 +1,1822 @@
1
+ var Lux = (function (exports) {
2
+ 'use strict';
3
+
4
+ /**
5
+ * Lux Template System
6
+ *
7
+ * Supports:
8
+ * - html/svg tagged templates with template caching
9
+ * - Child text interpolation: <div>${value}</div>
10
+ * - Event binding: <button @click=${handler}>
11
+ * - Property binding: <input .value=${val}>
12
+ * - Boolean attribute: <div ?hidden=${cond}>
13
+ * - Plain attribute: <div id=${val}>
14
+ * - Class binding: <div class=${classMap({...})}>
15
+ * - Style binding: <div style=${styleMap({...})}>
16
+ * - Ref binding: <input ref=${el => ...}>
17
+ * - Directives: repeat, when, classMap, styleMap, guard, show
18
+ * - Template caching, incremental DOM updates
19
+ * - nothing sentinel for clearing content
20
+ */
21
+
22
+ // ─── Template type markers ───
23
+ const HTML_RESULT = 1;
24
+ const SVG_RESULT = 2;
25
+
26
+ // ─── Sentinels ───
27
+ const nothing = Symbol.for('lux-nothing');
28
+
29
+ // ─── html / svg tagged template functions ───
30
+ function html(strings, ...values) {
31
+ return { _$luxType$: HTML_RESULT, strings, values };
32
+ }
33
+
34
+ function svg(strings, ...values) {
35
+ return { _$luxType$: SVG_RESULT, strings, values };
36
+ }
37
+
38
+ function css(strings, ...values) {
39
+ return { _$luxType$: 3, strings, values };
40
+ }
41
+
42
+ function isTemplateResult(value) {
43
+ return value && value._$luxType$ !== undefined;
44
+ }
45
+
46
+ // ─── HTML escaping ──
47
+ function escapeHtml(value) {
48
+ if (value == null) return '';
49
+ const str = String(value);
50
+ if (str.indexOf('<') === -1 && str.indexOf('>') === -1 && str.indexOf('&') === -1) return str;
51
+ return str.replace(
52
+ /[<>&"']/g,
53
+ (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' })[c]
54
+ );
55
+ }
56
+
57
+ // ─── Part marker regex ──
58
+ const ATTR_BINDING_RE = /(?:([@.?][\w-]+)|(ref)|(class))=\s*$/;
59
+
60
+ // ─── Template parsing ──
61
+ function parseTemplate(strings) {
62
+ const parts = [];
63
+ let html = '';
64
+ for (let i = 0; i < strings.length; i++) {
65
+ html += strings[i];
66
+ if (i < strings.length - 1) {
67
+ const attrMatch = html.match(ATTR_BINDING_RE);
68
+ if (attrMatch) {
69
+ const prefixed = attrMatch[1];
70
+ const isRef = !!attrMatch[2];
71
+ const isClass = !!attrMatch[3];
72
+ let prefix = '';
73
+ let name = '';
74
+ if (prefixed) {
75
+ prefix = prefixed[0];
76
+ name = prefixed.slice(1);
77
+ } else if (isRef) {
78
+ prefix = 'ref';
79
+ name = 'ref';
80
+ } else if (isClass) {
81
+ prefix = 'class';
82
+ name = 'class';
83
+ }
84
+ const marker = `lux-attr-${i}`;
85
+ html = html.slice(0, -attrMatch[0].length) + `${marker}="${name}" `;
86
+ parts.push({ type: 'attr', name, prefix, marker, index: i });
87
+ } else {
88
+ const marker = `lux-${i}`;
89
+ html += `<!--${marker}-->`;
90
+ parts.push({ type: 'child', marker, index: i });
91
+ }
92
+ }
93
+ }
94
+ return { html, parts };
95
+ }
96
+
97
+ // ─── Commit functions ──
98
+
99
+ function commitValue(container, value, context) {
100
+ if (value === nothing || value == null) {
101
+ clearContainer(container);
102
+ return;
103
+ }
104
+
105
+ if (isRepeatResult(value)) {
106
+ commitRepeat(container, value);
107
+ return;
108
+ }
109
+
110
+ if (isTemplateResult(value)) {
111
+ render(value, container);
112
+ return;
113
+ }
114
+
115
+ if (Array.isArray(value)) {
116
+ clearContainer(container);
117
+ for (const item of value) {
118
+ if (isTemplateResult(item)) {
119
+ const span = document.createElement('span');
120
+ span.style.display = 'contents';
121
+ render(item, span);
122
+ container.appendChild(span);
123
+ } else if (item instanceof Node) {
124
+ container.appendChild(item);
125
+ } else if (item != null && item !== false) {
126
+ container.appendChild(document.createTextNode(String(item)));
127
+ }
128
+ }
129
+ return;
130
+ }
131
+
132
+ if (value instanceof Node) {
133
+ clearContainer(container);
134
+ container.appendChild(value);
135
+ return;
136
+ }
137
+
138
+ // Primitive — replace content (don't append)
139
+ if (container.childNodes.length === 1 && container.firstChild.nodeType === Node.TEXT_NODE) {
140
+ container.firstChild.textContent = String(value);
141
+ } else {
142
+ clearContainer(container);
143
+ container.appendChild(document.createTextNode(String(value)));
144
+ }
145
+ }
146
+
147
+ function clearContainer(container) {
148
+ while (container.firstChild) {
149
+ container.removeChild(container.firstChild);
150
+ }
151
+ }
152
+
153
+ function commitAttrPart(part, value) {
154
+ const { element, name, prefix } = part;
155
+
156
+ switch (prefix) {
157
+ case '@': {
158
+ // Event binding
159
+ const oldHandler = part.committedValue;
160
+
161
+ // Determine if we should remove/add based on value changes
162
+ const isNothing = value == null || value === nothing;
163
+ const shouldRemoveListener =
164
+ (isNothing && oldHandler != null) ||
165
+ value?.capture !== oldHandler?.capture ||
166
+ value?.once !== oldHandler?.once ||
167
+ value?.passive !== oldHandler?.passive;
168
+
169
+ const shouldAddListener = !isNothing && (oldHandler == null || shouldRemoveListener);
170
+
171
+ if (shouldRemoveListener) {
172
+ element.removeEventListener(name, part._handler || oldHandler, oldHandler);
173
+ }
174
+
175
+ if (shouldAddListener) {
176
+ if (typeof value === 'function') {
177
+ // Wrap handler to preserve `this` as the host element
178
+ const host =
179
+ part._host ||
180
+ (part._host = {
181
+ handleEvent(e) {
182
+ const fn = part.committedValue;
183
+ if (typeof fn === 'function') {
184
+ // Use host element (custom element instance) as `this`
185
+ const hostEl = element.getRootNode().host || element;
186
+ fn.call(hostEl, e);
187
+ }
188
+ },
189
+ });
190
+ part._handler = host;
191
+ element.addEventListener(name, host);
192
+ } else if (typeof value === 'object' && value?.handleEvent) {
193
+ // EventListenerObject: { handleEvent(event) }
194
+ const host = { handleEvent: (e) => value.handleEvent.call(value, e) };
195
+ part._handler = host;
196
+ element.addEventListener(name, host, {
197
+ capture: value.capture,
198
+ once: value.once,
199
+ passive: value.passive,
200
+ });
201
+ }
202
+ }
203
+
204
+ part.committedValue = value;
205
+ break;
206
+ }
207
+ case '.': {
208
+ // Property binding
209
+ element[name] = value === nothing ? undefined : value;
210
+ break;
211
+ }
212
+ case '?': {
213
+ // Boolean attribute binding
214
+ element.toggleAttribute(name, !!value);
215
+ break;
216
+ }
217
+ case 'ref': {
218
+ // Ref binding
219
+ if (typeof value === 'function') {
220
+ value(element);
221
+ }
222
+ break;
223
+ }
224
+ case 'class': {
225
+ // Class binding
226
+ if (typeof value === 'string') {
227
+ element.className = value;
228
+ } else if (typeof value === 'object' && value != null) {
229
+ const existing = element.className || '';
230
+ const names = existing ? existing.split(/\s+/) : [];
231
+ for (const [cls, active] of Object.entries(value)) {
232
+ if (active) {
233
+ if (!names.includes(cls)) names.push(cls);
234
+ } else {
235
+ const idx = names.indexOf(cls);
236
+ if (idx !== -1) names.splice(idx, 1);
237
+ }
238
+ }
239
+ element.className = names.filter(Boolean).join(' ');
240
+ }
241
+ break;
242
+ }
243
+ default: {
244
+ // Plain attribute
245
+ if (value == null || value === false) {
246
+ element.removeAttribute(name);
247
+ } else {
248
+ element.setAttribute(name, value === true ? '' : String(value));
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ // ─── render() function ──
255
+
256
+ function render(result, container, context) {
257
+ if (!isTemplateResult(result)) {
258
+ container.textContent = result ?? '';
259
+ return;
260
+ }
261
+
262
+ const { strings, values } = result;
263
+
264
+ // Incremental update
265
+ if (container._luxActiveParts && container._luxStrings === strings) {
266
+ for (const part of container._luxActiveParts) {
267
+ if (part.index < values.length) {
268
+ if (part.type === 'child') {
269
+ commitValue(part.span, values[part.index]);
270
+ } else {
271
+ commitAttrPart(part, values[part.index]);
272
+ }
273
+ }
274
+ }
275
+ return;
276
+ }
277
+
278
+ // First render — build part list
279
+ const tpl = document.createElement('template');
280
+ const parsed = parseTemplate(strings);
281
+ tpl.innerHTML = parsed.html;
282
+ const fragment = tpl.content.cloneNode(true);
283
+ const activeParts = [];
284
+
285
+ for (const part of parsed.parts) {
286
+ if (part.type === 'child') {
287
+ const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_COMMENT);
288
+ let node;
289
+ while ((node = walker.nextNode())) {
290
+ if (node.data === part.marker) {
291
+ const span = document.createElement('span');
292
+ span.style.display = 'contents';
293
+ node.parentNode.replaceChild(span, node);
294
+ commitValue(span, values[part.index]);
295
+ activeParts.push({ type: 'child', span, index: part.index });
296
+ break;
297
+ }
298
+ }
299
+ } else {
300
+ const element = fragment.querySelector(`[${part.marker}]`);
301
+ if (element) {
302
+ element.removeAttribute(part.marker);
303
+ const ap = {
304
+ type: 'attr',
305
+ element,
306
+ name: part.name,
307
+ prefix: part.prefix,
308
+ index: part.index,
309
+ committedValue: undefined,
310
+ };
311
+ commitAttrPart(ap, values[part.index]);
312
+ activeParts.push(ap);
313
+ }
314
+ }
315
+ }
316
+
317
+ container.textContent = '';
318
+ container.appendChild(fragment);
319
+ container._luxActiveParts = activeParts;
320
+ container._luxStrings = strings;
321
+ }
322
+
323
+ // ─── Directives ──
324
+
325
+ /**
326
+ * repeat(items, keyFn, renderFn) — keyed list rendering
327
+ */
328
+ function repeat(items, keyFn, renderFn) {
329
+ return { _type: 'repeat', items, keyFn, renderFn };
330
+ }
331
+
332
+ function isRepeatResult(value) {
333
+ return value && value._type === 'repeat';
334
+ }
335
+
336
+ function commitRepeat(container, value, context) {
337
+ const { items, keyFn, renderFn } = value;
338
+ const state = container._repeatState;
339
+
340
+ // Clear old content
341
+ if (state) {
342
+ for (const entry of state.entries) {
343
+ if (entry.span && entry.span.parentNode) {
344
+ entry.span.parentNode.removeChild(entry.span);
345
+ }
346
+ }
347
+ }
348
+
349
+ // Build new content
350
+ const frag = document.createDocumentFragment();
351
+ const entries = [];
352
+ for (const item of items) {
353
+ const key = keyFn(item);
354
+ const result = renderFn(item);
355
+ const span = document.createElement('span');
356
+ span.style.display = 'contents';
357
+ if (isTemplateResult(result)) {
358
+ render(result, span);
359
+ } else if (result != null && result !== false) {
360
+ span.appendChild(document.createTextNode(String(result)));
361
+ }
362
+ frag.appendChild(span);
363
+ entries.push({ key, result, span });
364
+ }
365
+ container.appendChild(frag);
366
+ container._repeatState = { entries };
367
+ }
368
+
369
+ /**
370
+ * when(condition, trueFn, falseFn) — conditional rendering
371
+ */
372
+ function when(condition, trueFn, falseFn = () => '') {
373
+ return condition ? trueFn() : falseFn();
374
+ }
375
+
376
+ /**
377
+ * show(condition, content) — show/hide element
378
+ */
379
+ function show(condition, content = '') {
380
+ return condition ? content : '';
381
+ }
382
+
383
+ /**
384
+ * classMap(classes) — dynamic class binding helper
385
+ */
386
+ function classMap(classes) {
387
+ return classes;
388
+ }
389
+
390
+ /**
391
+ * styleMap(styles) — dynamic style binding helper
392
+ */
393
+ function styleMap(styles) {
394
+ return styles;
395
+ }
396
+
397
+ /**
398
+ * guard(dependencies, fn) — re-render only when dependencies change
399
+ */
400
+ function guard(dependencies, fn) {
401
+ return fn();
402
+ }
403
+
404
+ /**
405
+ * LuxElement — Base class for Web Components.
406
+ *
407
+ * Inherits: HTMLElement → LuxElement
408
+ * Features: reactive properties, Shadow DOM, lifecycle, controllers, update batching
409
+ */
410
+
411
+
412
+ // ─── Update scheduling ───
413
+ const pendingUpdates = new Set();
414
+ let updateScheduled = false;
415
+
416
+ function scheduleMicroTask(fn) {
417
+ Promise.resolve().then(fn);
418
+ }
419
+
420
+ function enqueueUpdate(element) {
421
+ pendingUpdates.add(element);
422
+ if (!updateScheduled) {
423
+ updateScheduled = true;
424
+ scheduleMicroTask(() => {
425
+ updateScheduled = false;
426
+ const elements = [...pendingUpdates];
427
+ pendingUpdates.clear();
428
+ for (const el of elements) {
429
+ el._$performUpdate();
430
+ }
431
+ });
432
+ }
433
+ }
434
+
435
+ // ─── Default converter (attribute ↔ property) ───
436
+ const defaultConverter = {
437
+ toAttribute(value, type) {
438
+ switch (type) {
439
+ case Boolean:
440
+ return value ? '' : null;
441
+ case Object:
442
+ case Array:
443
+ return value == null ? null : JSON.stringify(value);
444
+ default:
445
+ return value;
446
+ }
447
+ },
448
+ fromAttribute(value, type) {
449
+ switch (type) {
450
+ case Boolean:
451
+ return value !== null;
452
+ case Number:
453
+ return Number(value);
454
+ case Object:
455
+ case Array:
456
+ try {
457
+ return JSON.parse(value);
458
+ } catch {
459
+ return null;
460
+ }
461
+ default:
462
+ return value;
463
+ }
464
+ },
465
+ };
466
+
467
+ // ─── LuxElement ───
468
+ class LuxElement extends HTMLElement {
469
+ // ─── Static: properties declaration ───
470
+ static properties = {};
471
+ static styles = undefined;
472
+
473
+ // ─── Static: finalization ───
474
+ static finalized = false;
475
+ static elementProperties = new Map();
476
+
477
+ // ─── Static: __prepare() — subclass isolation ───
478
+ static __prepare() {
479
+ if (this.hasOwnProperty('elementProperties')) {
480
+ return;
481
+ }
482
+ // Finalize any superclasses first
483
+ const superCtor = Object.getPrototypeOf(this);
484
+ if (superCtor.finalize) {
485
+ superCtor.finalize();
486
+ }
487
+ // Copy superclass elementProperties into own Map
488
+ this.elementProperties = new Map(superCtor.elementProperties);
489
+ }
490
+
491
+ static get observedAttributes() {
492
+ this.finalize();
493
+ return this.__attributeToPropertyMap ? [...this.__attributeToPropertyMap.keys()] : [];
494
+ }
495
+
496
+ static finalize() {
497
+ if (this.hasOwnProperty('finalized')) {
498
+ return;
499
+ }
500
+ this.finalized = true;
501
+ this.__prepare();
502
+
503
+ // Create properties from the static properties block
504
+ if (this.hasOwnProperty('properties')) {
505
+ const props = this.properties;
506
+ const propKeys = [
507
+ ...Object.getOwnPropertyNames(props),
508
+ ...Object.getOwnPropertySymbols(props),
509
+ ];
510
+ for (const p of propKeys) {
511
+ this.createProperty(p, props[p]);
512
+ }
513
+ }
514
+
515
+ // Create the attribute-to-property map
516
+ this.__attributeToPropertyMap = new Map();
517
+ for (const [p, options] of this.elementProperties) {
518
+ const attr = this.__attributeNameForProperty(p, options);
519
+ if (attr !== undefined) {
520
+ this.__attributeToPropertyMap.set(attr, p);
521
+ }
522
+ }
523
+
524
+ this.elementStyles = this.styles;
525
+ }
526
+
527
+ static __attributeNameForProperty(name, options) {
528
+ const attribute = options.attribute;
529
+ return attribute === false
530
+ ? undefined
531
+ : typeof attribute === 'string'
532
+ ? attribute
533
+ : typeof name === 'string'
534
+ ? name.replace(/([A-Z])/g, '-$1').toLowerCase()
535
+ : undefined;
536
+ }
537
+
538
+ // ─── Static: createProperty ───
539
+ static createProperty(name, options = {}) {
540
+ this.__prepare();
541
+ if (this.elementProperties.has(name)) return;
542
+
543
+ if (options.state) {
544
+ options = { ...options, attribute: false };
545
+ }
546
+
547
+ this.elementProperties.set(name, options);
548
+
549
+ if (!options.noAccessor) {
550
+ const key = Symbol();
551
+ const descriptor = this.getPropertyDescriptor(name, key, options);
552
+ if (descriptor !== undefined) {
553
+ Object.defineProperty(this.prototype, name, descriptor);
554
+ }
555
+ }
556
+ }
557
+
558
+ static getPropertyDescriptor(name, key, options) {
559
+ const { type, reflect, converter } = options;
560
+ converter?.toAttribute ?? defaultConverter.toAttribute;
561
+
562
+ return {
563
+ get() {
564
+ return this[key];
565
+ },
566
+ set(value) {
567
+ const oldValue = this[key];
568
+ this[key] = value;
569
+ this.requestUpdate(name, oldValue, options);
570
+ },
571
+ configurable: true,
572
+ enumerable: true,
573
+ };
574
+ }
575
+
576
+ // ─── Static: addInitializer ───
577
+ static _initializers = [];
578
+
579
+ static addInitializer(fn) {
580
+ this._initializers = [...(this._initializers || []), fn];
581
+ }
582
+
583
+ // ─── Static: shadowRootOptions ───
584
+ static shadowRootOptions = { mode: 'open' };
585
+
586
+ // ─── Constructor ───
587
+ constructor() {
588
+ super();
589
+
590
+ this.__updatePending = false;
591
+ this.__hasUpdated = false;
592
+ this.__controllers = new Set();
593
+ this.__reflectingProperty = null;
594
+
595
+ // Ensure finalized
596
+ if (!this.constructor.hasOwnProperty('finalized')) {
597
+ this.constructor.finalize();
598
+ }
599
+
600
+ // CloneNode support: save any properties set before upgrade
601
+ this.__saveInstanceProperties();
602
+
603
+ this.requestUpdate();
604
+
605
+ for (const fn of this.constructor._initializers || []) {
606
+ fn(this);
607
+ }
608
+ }
609
+
610
+ // ─── Instance properties save/replay (cloneNode support) ───
611
+ __saveInstanceProperties() {
612
+ const elementProperties = this.constructor.elementProperties;
613
+ if (!elementProperties) return;
614
+
615
+ for (const [name, options] of elementProperties) {
616
+ if (options.noAccessor) continue;
617
+
618
+ if (this.hasOwnProperty(name)) {
619
+ // Plain property set before setter was defined (cloneNode)
620
+ const value = this[name];
621
+ delete this[name];
622
+ // Store via the Symbol key
623
+ this.__instancePropertyValues = this.__instancePropertyValues || new Map();
624
+ this.__instancePropertyValues.set(name, value);
625
+ }
626
+ }
627
+ }
628
+
629
+ // ─── Lifecycle callbacks ───
630
+ connectedCallback() {
631
+ this.__controllers.forEach((c) => c.hostConnected?.());
632
+
633
+ // Ensure finalized
634
+ if (!this.constructor.hasOwnProperty('finalized')) {
635
+ this.constructor.finalize();
636
+ }
637
+
638
+ // Sync existing attributes to properties (upgrade scenario)
639
+ // attributeChangedCallback doesn't fire for pre-existing attributes
640
+ const attrMap = this.constructor.__attributeToPropertyMap;
641
+ if (attrMap) {
642
+ for (const [attrName, propName] of attrMap) {
643
+ const value = this.getAttribute(attrName);
644
+ if (value !== null) {
645
+ const options = this.constructor.elementProperties.get(propName);
646
+ if (options) {
647
+ const fromAttr =
648
+ options.converter?.fromAttribute ?? defaultConverter.fromAttribute;
649
+ this[propName] = fromAttr(value, options.type);
650
+ }
651
+ }
652
+ }
653
+ }
654
+
655
+ // Replay instance properties saved by __saveInstanceProperties (cloneNode)
656
+ if (this.__instancePropertyValues) {
657
+ for (const [name, value] of this.__instancePropertyValues) {
658
+ this[name] = value;
659
+ }
660
+ this.__instancePropertyValues = undefined;
661
+ }
662
+
663
+ // Ensure renderRoot is created
664
+ this.renderRoot;
665
+
666
+ // Trigger first update if not yet done
667
+ if (!this.__hasUpdated) {
668
+ this.requestUpdate();
669
+ }
670
+ }
671
+
672
+ disconnectedCallback() {
673
+ this.__controllers.forEach((c) => c.hostDisconnected?.());
674
+ }
675
+
676
+ attributeChangedCallback(name, _old, value) {
677
+ const propName = this.constructor.__attributeToPropertyMap?.get(name);
678
+ if (propName !== undefined && this.__reflectingProperty !== propName) {
679
+ const options = this.constructor.elementProperties.get(propName);
680
+ if (options) {
681
+ const converter =
682
+ typeof options.converter === 'function'
683
+ ? { fromAttribute: options.converter }
684
+ : options.converter?.fromAttribute !== undefined
685
+ ? options.converter
686
+ : defaultConverter;
687
+ this.__reflectingProperty = propName;
688
+ const convertedValue = converter.fromAttribute(value, options.type);
689
+ this[propName] = convertedValue;
690
+ this.__reflectingProperty = null;
691
+ }
692
+ }
693
+ }
694
+
695
+ // ─── RenderRoot & Styles ───
696
+ get renderRoot() {
697
+ if (!this._renderRoot) {
698
+ this._renderRoot = this.createRenderRoot();
699
+ this.__adoptStyles();
700
+ }
701
+ return this._renderRoot;
702
+ }
703
+
704
+ createRenderRoot() {
705
+ return this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
706
+ }
707
+
708
+ __adoptStyles() {
709
+ const styles = this.constructor.elementStyles;
710
+ if (!styles) return;
711
+
712
+ const items = Array.isArray(styles) ? styles : [styles];
713
+ const parts = [];
714
+ for (const item of items) {
715
+ if (typeof item === 'string') {
716
+ parts.push(item);
717
+ } else if (isTemplateResult(item)) {
718
+ parts.push(item.strings.join(''));
719
+ }
720
+ }
721
+ if (parts.length === 0) return;
722
+
723
+ if (this._renderRoot.adoptedStyleSheets !== undefined) {
724
+ const sheet = new CSSStyleSheet();
725
+ sheet.replaceSync(parts.join('\n'));
726
+ this._renderRoot.adoptedStyleSheets = [...this._renderRoot.adoptedStyleSheets, sheet];
727
+ } else {
728
+ const tag = document.createElement('style');
729
+ tag.textContent = parts.join('\n');
730
+ this._renderRoot.appendChild(tag);
731
+ }
732
+ }
733
+
734
+ // ─── Update lifecycle ───
735
+ enableUpdating(_requestedUpdate) {}
736
+
737
+ get updateComplete() {
738
+ return this.__updateCompletePromise ?? Promise.resolve();
739
+ }
740
+
741
+ get hasUpdated() {
742
+ return this.__hasUpdated;
743
+ }
744
+
745
+ get isUpdatePending() {
746
+ return this.__updatePending;
747
+ }
748
+
749
+ requestUpdate(name, oldValue, options) {
750
+ if (name !== undefined) {
751
+ const hasChanged = options?.hasChanged ?? ((v, o) => !Object.is(v, o));
752
+ if (!hasChanged(this[name], oldValue)) return;
753
+ }
754
+
755
+ if (!this.__updatePending) {
756
+ this.__updatePending = true;
757
+ enqueueUpdate(this);
758
+ }
759
+ }
760
+
761
+ async performUpdate() {
762
+ if (!this.isUpdatePending) return;
763
+
764
+ this.__updatePending = false;
765
+
766
+ this.willUpdate(new Map());
767
+
768
+ this.__controllers.forEach((c) => c.hostUpdate?.());
769
+
770
+ this.update(new Map());
771
+
772
+ this.__hasUpdated = true;
773
+
774
+ // Reflect properties to attributes after update
775
+ for (const [name, options] of this.constructor.elementProperties) {
776
+ if (options.reflect) {
777
+ const value = this[name];
778
+ const toAttribute = options.converter?.toAttribute ?? defaultConverter.toAttribute;
779
+ const attr = this.constructor.__attributeNameForProperty(name, options);
780
+ if (attr !== undefined) {
781
+ this.__reflectingProperty = name;
782
+ const attrValue = toAttribute(value, options.type);
783
+ if (attrValue === null || attrValue === undefined) {
784
+ this.removeAttribute(attr);
785
+ } else {
786
+ this.setAttribute(attr, String(attrValue));
787
+ }
788
+ this.__reflectingProperty = null;
789
+ }
790
+ }
791
+ }
792
+
793
+ this.__controllers.forEach((c) => c.hostUpdated?.());
794
+ this.firstUpdated(new Map());
795
+ this.updated(new Map());
796
+ }
797
+
798
+ _$performUpdate() {
799
+ this.performUpdate();
800
+ }
801
+
802
+ // ─── Lifecycle hooks ───
803
+ shouldUpdate(_changedProperties) {
804
+ return true;
805
+ }
806
+
807
+ willUpdate(_changedProperties) {}
808
+
809
+ update(_changedProperties) {
810
+ const result = this.render();
811
+ if (result) {
812
+ render(result, this.renderRoot);
813
+ }
814
+ }
815
+
816
+ render() {
817
+ return html``;
818
+ }
819
+
820
+ firstUpdated(_changedProperties) {}
821
+ updated(_changedProperties) {}
822
+ propertyChangedCallback(_name, _oldValue, _newValue) {}
823
+
824
+ // ─── Controller API ───
825
+ addController(controller) {
826
+ this.__controllers.add(controller);
827
+ if (this.isConnected) {
828
+ controller.hostConnected?.();
829
+ }
830
+ }
831
+
832
+ removeController(controller) {
833
+ this.__controllers.delete(controller);
834
+ }
835
+
836
+ // ─── Utilities ───
837
+ emit(type, detail, options = {}) {
838
+ return this.dispatchEvent(
839
+ new CustomEvent(type, {
840
+ detail,
841
+ bubbles: true,
842
+ composed: true,
843
+ ...options,
844
+ })
845
+ );
846
+ }
847
+
848
+ $(selector) {
849
+ return this.renderRoot.querySelector(selector);
850
+ }
851
+
852
+ $$(selector) {
853
+ return this.renderRoot.querySelectorAll(selector);
854
+ }
855
+ }
856
+
857
+ // ─── Register component ───
858
+ function registerComponent$1(name, component) {
859
+ if (!name.includes('-')) {
860
+ throw new Error(`Component name must contain a hyphen: ${name}`);
861
+ }
862
+ if (!customElements.get(name)) {
863
+ customElements.define(name, component);
864
+ }
865
+ }
866
+
867
+ // ─── createComponent (function-based components) ───
868
+ function createComponent(renderFn, options = {}) {
869
+ const { properties = {} } = options;
870
+
871
+ class DynamicComponent extends LuxElement {
872
+ static properties = properties;
873
+ render() {
874
+ return renderFn.call(this, this);
875
+ }
876
+ }
877
+
878
+ return DynamicComponent;
879
+ }
880
+
881
+ /**
882
+ * Lux Router - Hash-based SPA routing
883
+ *
884
+ * Usage:
885
+ * const router = createRouter({ routes: [...] });
886
+ * router.push('/users/123');
887
+ *
888
+ * Routes accept component classes or lazy loaders:
889
+ * { path: '/', component: HomePage } // direct class
890
+ * { path: '/admin', component: () => import('./admin.js') } // lazy
891
+ */
892
+
893
+ function registerComponent(name, component) {
894
+ if (!name.includes('-')) throw new Error(`Component name must contain a hyphen: ${name}`);
895
+ if (!customElements.get(name)) customElements.define(name, component);
896
+ }
897
+
898
+ // ─── Route matching ───
899
+
900
+ function pathToRegex(pattern) {
901
+ let seg = pattern.replace(/\/+$/, '') || '/';
902
+ if (seg === '/') return /^\/?$/;
903
+ const re = '^' + seg.replace(/:(\w+)/g, '([^/]+)').replace(/\(\.\*\)/g, '(.*)') + '$';
904
+ return new RegExp(re);
905
+ }
906
+
907
+ function extractParamNames(pattern) {
908
+ const names = [];
909
+ const re = /:(\w+)/g;
910
+ let m;
911
+ while ((m = re.exec(pattern))) names.push(m[1]);
912
+ return names;
913
+ }
914
+
915
+ function parseQuery(hash) {
916
+ const idx = hash.indexOf('?');
917
+ if (idx === -1) return {};
918
+ const params = {};
919
+ for (const pair of hash.slice(idx + 1).split('&')) {
920
+ const [k, v] = pair.split('=');
921
+ if (k) params[decodeURIComponent(k)] = v ? decodeURIComponent(v) : '';
922
+ }
923
+ return params;
924
+ }
925
+
926
+ function getPathFromHash(hash) {
927
+ return hash.replace(/^#/, '').split('?')[0] || '/';
928
+ }
929
+
930
+ function resolveComponent(componentOrLoader) {
931
+ if (typeof componentOrLoader === 'string') {
932
+ return Promise.resolve(componentOrLoader);
933
+ }
934
+ if (
935
+ typeof componentOrLoader === 'function' &&
936
+ componentOrLoader.prototype instanceof HTMLElement
937
+ ) {
938
+ return ensureRegistered(componentOrLoader);
939
+ }
940
+ if (typeof componentOrLoader === 'function') {
941
+ // Assume lazy loader: () => import(...)
942
+ return componentOrLoader().then((mod) => {
943
+ const Comp = mod.default || mod[Object.keys(mod)[0]];
944
+ return ensureRegistered(Comp);
945
+ });
946
+ }
947
+ return Promise.resolve(null);
948
+ }
949
+
950
+ function ensureRegistered(ComponentClass) {
951
+ if (!ComponentClass || !ComponentClass.prototype) return Promise.resolve(null);
952
+
953
+ const tagName = ComponentClass.tagName || classToTag(ComponentClass.name);
954
+
955
+ // Already registered?
956
+ if (customElements.get(tagName)) {
957
+ return Promise.resolve(tagName);
958
+ }
959
+
960
+ registerComponent(tagName, ComponentClass);
961
+ return Promise.resolve(tagName);
962
+ }
963
+
964
+ function classToTag(name) {
965
+ // HomePage → home-page, UserProfile → user-profile
966
+ return name
967
+ .replace(/([A-Z])/g, '-$1')
968
+ .toLowerCase()
969
+ .replace(/^-/, '');
970
+ }
971
+
972
+ // ─── Router class ───
973
+
974
+ class Router {
975
+ constructor(options = {}) {
976
+ this.routes = (options.routes || []).map((r) => ({
977
+ ...r,
978
+ _regex: pathToRegex(r.path),
979
+ _paramNames: extractParamNames(r.path),
980
+ _children: (r.children || []).map((c) => ({
981
+ ...c,
982
+ _regex: pathToRegex(c.path),
983
+ _paramNames: extractParamNames(c.path),
984
+ })),
985
+ }));
986
+
987
+ this._guards = [];
988
+ this._listeners = [];
989
+ this.current = null;
990
+ this._prev = null;
991
+ this._onHashChange = this._onHashChange.bind(this);
992
+ }
993
+
994
+ start() {
995
+ window.addEventListener('hashchange', this._onHashChange);
996
+ this._resolve();
997
+ }
998
+
999
+ stop() {
1000
+ window.removeEventListener('hashchange', this._onHashChange);
1001
+ }
1002
+
1003
+ push(path) {
1004
+ location.hash = path;
1005
+ }
1006
+
1007
+ replace(path) {
1008
+ const url = new URL(location.href);
1009
+ url.hash = path;
1010
+ history.replaceState(null, '', url.toString());
1011
+ this._onHashChange();
1012
+ }
1013
+
1014
+ back() {
1015
+ history.back();
1016
+ }
1017
+ forward() {
1018
+ history.forward();
1019
+ }
1020
+
1021
+ beforeEach(fn) {
1022
+ this._guards.push(fn);
1023
+ return () => {
1024
+ const i = this._guards.indexOf(fn);
1025
+ if (i >= 0) this._guards.splice(i, 1);
1026
+ };
1027
+ }
1028
+
1029
+ _subscribe(fn) {
1030
+ this._listeners.push(fn);
1031
+ return () => {
1032
+ const i = this._listeners.indexOf(fn);
1033
+ if (i >= 0) this._listeners.splice(i, 1);
1034
+ };
1035
+ }
1036
+
1037
+ _onHashChange() {
1038
+ this._resolve();
1039
+ }
1040
+
1041
+ _resolve() {
1042
+ const hash = location.hash || '#/';
1043
+ const path = getPathFromHash(hash);
1044
+ const query = parseQuery(hash);
1045
+
1046
+ let matched = null;
1047
+ for (const route of this.routes) {
1048
+ const hasChildren = route._children.length > 0;
1049
+
1050
+ // For routes with children, use prefix matching
1051
+ // For leaf routes, use exact matching
1052
+ let m;
1053
+ if (hasChildren) {
1054
+ // Prefix match: path must start with route path
1055
+ const prefix = route.path === '/' ? '' : route.path.replace(/\/+$/, '');
1056
+ if (
1057
+ path === prefix ||
1058
+ path.startsWith(prefix + '/') ||
1059
+ (prefix === '' && path.startsWith('/'))
1060
+ ) {
1061
+ m = [path]; // fake match for prefix
1062
+ }
1063
+ } else {
1064
+ m = path.match(route._regex);
1065
+ }
1066
+
1067
+ if (m) {
1068
+ const params = {};
1069
+ route._paramNames.forEach((name, i) => {
1070
+ if (m[i + 1] !== undefined) params[name] = decodeURIComponent(m[i + 1]);
1071
+ });
1072
+ matched = { route, params: { ...params }, query, path };
1073
+
1074
+ // Child matching (check BEFORE redirect)
1075
+ if (hasChildren) {
1076
+ const prefix = route.path === '/' ? '' : route.path.replace(/\/+$/, '');
1077
+ const childPath = path.slice(prefix.length) || '/';
1078
+ let childMatched = false;
1079
+ for (const child of route._children) {
1080
+ const cm = childPath.match(child._regex);
1081
+ if (cm) {
1082
+ const childParams = {};
1083
+ child._paramNames.forEach((name, i) => {
1084
+ childParams[name] = decodeURIComponent(cm[i + 1]);
1085
+ });
1086
+ matched.child = {
1087
+ route: child,
1088
+ params: { ...params, ...childParams },
1089
+ query,
1090
+ path,
1091
+ };
1092
+ childMatched = true;
1093
+ break;
1094
+ }
1095
+ }
1096
+ // No child matched — redirect only if path equals parent path
1097
+ if (
1098
+ !childMatched &&
1099
+ route.redirect &&
1100
+ path === (route.path === '/' ? '/' : route.path)
1101
+ ) {
1102
+ this.replace(route.redirect);
1103
+ return;
1104
+ }
1105
+ // No child matched and no redirect — skip this parent, try other routes
1106
+ if (!childMatched) {
1107
+ matched = null;
1108
+ continue;
1109
+ }
1110
+ } else if (route.redirect) {
1111
+ this.replace(route.redirect);
1112
+ return;
1113
+ }
1114
+ break;
1115
+ }
1116
+ }
1117
+
1118
+ if (!matched) {
1119
+ this.current = { route: null, params: {}, query, path };
1120
+ } else {
1121
+ this.current = matched;
1122
+ }
1123
+
1124
+ for (const guard of this._guards) {
1125
+ if (guard(this.current, this._prev) === false) {
1126
+ if (this._prev) history.replaceState(null, '', '#' + this._prev.path);
1127
+ return;
1128
+ }
1129
+ }
1130
+
1131
+ this._prev = { ...this.current };
1132
+ for (const fn of this._listeners) fn(this.current);
1133
+ }
1134
+ }
1135
+
1136
+ // ─── Create router + register <router-outlet> ───
1137
+
1138
+ function createRouter(options) {
1139
+ const router = new Router(options);
1140
+
1141
+ if (!customElements.get('router-outlet')) {
1142
+ class RouterOutlet extends HTMLElement {
1143
+ connectedCallback() {
1144
+ this.style.display = 'contents';
1145
+ this._unsub = router._subscribe(() => this._update());
1146
+ this._update();
1147
+ }
1148
+
1149
+ disconnectedCallback() {
1150
+ this._unsub?.();
1151
+ this._loadingAbort?.abort();
1152
+ }
1153
+
1154
+ async _update() {
1155
+ const match = this._getMatch();
1156
+ this._loadingAbort?.abort();
1157
+
1158
+ if (!match || !match.route || !match.route.component) {
1159
+ this.textContent = '';
1160
+ return;
1161
+ }
1162
+
1163
+ // Skip re-render if the same route is already rendered
1164
+ if (this._lastRoute === match.route) {
1165
+ return;
1166
+ }
1167
+
1168
+ const controller = new AbortController();
1169
+ this._loadingAbort = controller;
1170
+
1171
+ try {
1172
+ const tagName = await resolveComponent(match.route.component);
1173
+ if (controller.signal.aborted) return;
1174
+
1175
+ this.textContent = '';
1176
+ if (tagName) {
1177
+ const el = document.createElement(tagName);
1178
+ if (match.params) el.params = match.params;
1179
+ if (match.query) el.query = match.query;
1180
+ this.appendChild(el);
1181
+ this._lastRoute = match.route;
1182
+ }
1183
+ } catch (e) {
1184
+ if (!controller.signal.aborted) {
1185
+ console.error('[Lux Router] Failed to load component:', e);
1186
+ this.textContent = '';
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ _getMatch() {
1192
+ const level = parseInt(this.getAttribute('level')) || 0;
1193
+ let match = router.current;
1194
+ for (let i = 0; i < level; i++) {
1195
+ if (!match || !match.child) return null;
1196
+ match = match.child;
1197
+ }
1198
+ return match;
1199
+ }
1200
+ }
1201
+ registerComponent('router-outlet', RouterOutlet);
1202
+ }
1203
+
1204
+ router.start();
1205
+ return router;
1206
+ }
1207
+
1208
+ /**
1209
+ * ICU MessageFormat subset parser
1210
+ *
1211
+ * Supports:
1212
+ * - Simple interpolation: "Hello, {name}"
1213
+ * - Plural: "{count, plural, =0{No items} =1{# item} other{# items}}"
1214
+ * - Select: "{gender, select, male{He} female{She} other{They}}"
1215
+ */
1216
+
1217
+ /**
1218
+ * Find the matching closing brace for an opening brace at position `start`.
1219
+ */
1220
+ function findMatchingBrace(str, start) {
1221
+ let depth = 1;
1222
+ for (let i = start + 1; i < str.length; i++) {
1223
+ if (str[i] === '{') depth++;
1224
+ else if (str[i] === '}') {
1225
+ depth--;
1226
+ if (depth === 0) return i;
1227
+ }
1228
+ }
1229
+ return -1;
1230
+ }
1231
+
1232
+ /**
1233
+ * Extract all top-level {...} expressions from the template.
1234
+ */
1235
+ function extractExpressions(template) {
1236
+ const parts = [];
1237
+ let i = 0;
1238
+ while (i < template.length) {
1239
+ if (template[i] === '{') {
1240
+ const end = findMatchingBrace(template, i);
1241
+ if (end !== -1) {
1242
+ parts.push({ start: i, end, expr: template.slice(i + 1, end) });
1243
+ i = end + 1;
1244
+ continue;
1245
+ }
1246
+ }
1247
+ i++;
1248
+ }
1249
+ return parts;
1250
+ }
1251
+
1252
+ function formatMessage(template, values = {}) {
1253
+ if (!template || typeof template !== 'string') return '';
1254
+
1255
+ const exprs = extractExpressions(template);
1256
+ let result = '';
1257
+ let lastIdx = 0;
1258
+
1259
+ for (const { start, end, expr } of exprs) {
1260
+ result += template.slice(lastIdx, start);
1261
+ const trimmed = expr.trim();
1262
+
1263
+ // Plural: {key, plural, =N{...} other{...}}
1264
+ const pluralMatch = trimmed.match(/^(\w+),\s*plural\s*,\s*(.+)$/);
1265
+ if (pluralMatch) {
1266
+ const [, key, rules] = pluralMatch;
1267
+ const count = Number(values[key]) || 0;
1268
+ result += formatPlural(count, rules);
1269
+ lastIdx = end + 1;
1270
+ continue;
1271
+ }
1272
+
1273
+ // Select: {key, select, val1{...} val2{...}}
1274
+ const selectMatch = trimmed.match(/^(\w+),\s*select\s*,\s*(.+)$/);
1275
+ if (selectMatch) {
1276
+ const [, key, rules] = selectMatch;
1277
+ const value = String(values[key] || '');
1278
+ result += formatSelect(value, rules);
1279
+ lastIdx = end + 1;
1280
+ continue;
1281
+ }
1282
+
1283
+ // Simple interpolation: {key}
1284
+ result += values[trimmed] ?? template.slice(start, end + 1);
1285
+ lastIdx = end + 1;
1286
+ }
1287
+
1288
+ result += template.slice(lastIdx);
1289
+ return result;
1290
+ }
1291
+
1292
+ function formatPlural(count, rulesStr) {
1293
+ const rules = parseRules(rulesStr);
1294
+ const exact = rules[`=${count}`];
1295
+ if (exact !== undefined) return exact.replace(/#/g, String(count));
1296
+
1297
+ const key = count === 1 ? 'one' : 'other';
1298
+ const rule = rules[key] || rules['other'] || '';
1299
+ return rule.replace(/#/g, String(count));
1300
+ }
1301
+
1302
+ function formatSelect(value, rulesStr) {
1303
+ const rules = parseRules(rulesStr);
1304
+ return rules[value] || rules['other'] || '';
1305
+ }
1306
+
1307
+ function parseRules(rulesStr) {
1308
+ const rules = {};
1309
+ const re = /(\w+|=\d+)\{([^}]*)\}/g;
1310
+ let m;
1311
+ while ((m = re.exec(rulesStr))) {
1312
+ rules[m[1]] = m[2];
1313
+ }
1314
+ return rules;
1315
+ }
1316
+
1317
+ /**
1318
+ * Locale detection
1319
+ */
1320
+
1321
+ function getDefaultLocale() {
1322
+ if (typeof navigator !== 'undefined' && navigator.language) {
1323
+ return navigator.language;
1324
+ }
1325
+ return 'en';
1326
+ }
1327
+
1328
+ function formatNumber(value, options = {}, locale) {
1329
+ try {
1330
+ return new Intl.NumberFormat(locale, options).format(value);
1331
+ } catch {
1332
+ return String(value);
1333
+ }
1334
+ }
1335
+
1336
+ function formatDate(value, options = {}, locale) {
1337
+ try {
1338
+ return new Intl.DateTimeFormat(locale, options).format(value);
1339
+ } catch {
1340
+ return String(value);
1341
+ }
1342
+ }
1343
+
1344
+ /**
1345
+ * Lux i18n — Lightweight internationalization
1346
+ *
1347
+ * Usage:
1348
+ * import { createI18n, msg, number, date } from 'lux/i18n';
1349
+ *
1350
+ * const i18n = createI18n({
1351
+ * locale: 'zh-CN',
1352
+ * messages: {
1353
+ * 'zh-CN': { hello: '你好', welcome: '欢迎, {name}' },
1354
+ * 'en': { hello: 'Hello', welcome: 'Welcome, {name}' },
1355
+ * }
1356
+ * });
1357
+ *
1358
+ * // In templates
1359
+ * html`<h1>${msg('hello')}</h1>`
1360
+ * html`<p>${msg('welcome', { name: 'Alice' })}</p>`
1361
+ * html`<span>${msg('items', { count: 5 })}</span>`
1362
+ *
1363
+ * // Number/date formatting
1364
+ * html`<span>${number(1234567)}</span>`
1365
+ * html`<span>${date(new Date())}</span>`
1366
+ */
1367
+
1368
+
1369
+ let currentLocale = '';
1370
+ let currentMessages = {};
1371
+ let listeners = [];
1372
+
1373
+ /**
1374
+ * Get the current translation for a key.
1375
+ */
1376
+ function msg(key, values = {}) {
1377
+ const dict = currentMessages[currentLocale] || currentMessages;
1378
+ const template = dict[key] ?? key;
1379
+ return formatMessage(template, values);
1380
+ }
1381
+
1382
+ /**
1383
+ * Format a number using Intl.NumberFormat.
1384
+ */
1385
+ function number(value, options) {
1386
+ return formatNumber(
1387
+ value,
1388
+ typeof options === 'string' ? { style: options } : options,
1389
+ currentLocale
1390
+ );
1391
+ }
1392
+
1393
+ /**
1394
+ * Format a date using Intl.DateTimeFormat.
1395
+ */
1396
+ function date(value, options) {
1397
+ return formatDate(value, options, currentLocale);
1398
+ }
1399
+
1400
+ /**
1401
+ * Get the current locale string.
1402
+ */
1403
+ function getLocale() {
1404
+ return currentLocale;
1405
+ }
1406
+
1407
+ /**
1408
+ * Create an i18n instance.
1409
+ *
1410
+ * @param {Object} options
1411
+ * @param {string} options.locale - Initial locale (default: navigator.language)
1412
+ * @param {Object} messages - { 'zh-CN': {...}, 'en': {...} }
1413
+ * @returns {Object} i18n instance
1414
+ */
1415
+ function createI18n(options = {}) {
1416
+ currentLocale = options.locale || getDefaultLocale();
1417
+ currentMessages = options.messages || {};
1418
+
1419
+ function setLocale(locale) {
1420
+ currentLocale = locale;
1421
+ for (const fn of listeners) fn(locale);
1422
+ }
1423
+
1424
+ function onLocaleChange(fn) {
1425
+ listeners.push(fn);
1426
+ return () => {
1427
+ const i = listeners.indexOf(fn);
1428
+ if (i >= 0) listeners.splice(i, 1);
1429
+ };
1430
+ }
1431
+
1432
+ return {
1433
+ get locale() {
1434
+ return currentLocale;
1435
+ },
1436
+ setLocale,
1437
+ onLocaleChange,
1438
+ msg,
1439
+ number,
1440
+ date,
1441
+ getLocale,
1442
+ };
1443
+ }
1444
+
1445
+ /**
1446
+ * Color utilities — HSL-based color generation from a seed color
1447
+ */
1448
+
1449
+ // ─── Conversion ───
1450
+
1451
+ function hexToRgb(hex) {
1452
+ hex = hex.replace('#', '');
1453
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
1454
+ return {
1455
+ r: parseInt(hex.slice(0, 2), 16),
1456
+ g: parseInt(hex.slice(2, 4), 16),
1457
+ b: parseInt(hex.slice(4, 6), 16),
1458
+ };
1459
+ }
1460
+
1461
+ function rgbToHsl(r, g, b) {
1462
+ r /= 255;
1463
+ g /= 255;
1464
+ b /= 255;
1465
+ const max = Math.max(r, g, b),
1466
+ min = Math.min(r, g, b);
1467
+ let h,
1468
+ s,
1469
+ l = (max + min) / 2;
1470
+
1471
+ if (max === min) {
1472
+ h = s = 0;
1473
+ } else {
1474
+ const d = max - min;
1475
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1476
+ switch (max) {
1477
+ case r:
1478
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
1479
+ break;
1480
+ case g:
1481
+ h = ((b - r) / d + 2) / 6;
1482
+ break;
1483
+ case b:
1484
+ h = ((r - g) / d + 4) / 6;
1485
+ break;
1486
+ }
1487
+ }
1488
+ return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
1489
+ }
1490
+
1491
+ function hslToRgb(h, s, l) {
1492
+ h /= 360;
1493
+ s /= 100;
1494
+ l /= 100;
1495
+ let r, g, b;
1496
+
1497
+ if (s === 0) {
1498
+ r = g = b = l;
1499
+ } else {
1500
+ const hue2rgb = (p, q, t) => {
1501
+ if (t < 0) t += 1;
1502
+ if (t > 1) t -= 1;
1503
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
1504
+ if (t < 1 / 2) return q;
1505
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1506
+ return p;
1507
+ };
1508
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1509
+ const p = 2 * l - q;
1510
+ r = hue2rgb(p, q, h + 1 / 3);
1511
+ g = hue2rgb(p, q, h);
1512
+ b = hue2rgb(p, q, h - 1 / 3);
1513
+ }
1514
+
1515
+ return {
1516
+ r: Math.round(r * 255),
1517
+ g: Math.round(g * 255),
1518
+ b: Math.round(b * 255),
1519
+ };
1520
+ }
1521
+
1522
+ function hexToHsl(hex) {
1523
+ const { r, g, b } = hexToRgb(hex);
1524
+ return rgbToHsl(r, g, b);
1525
+ }
1526
+
1527
+ function hslToHex(h, s, l) {
1528
+ const { r, g, b } = hslToRgb(h, s, l);
1529
+ return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('');
1530
+ }
1531
+
1532
+ function rgbString(r, g, b) {
1533
+ return `${r} ${g} ${b}`;
1534
+ }
1535
+
1536
+ // ─── Palette generation ───
1537
+
1538
+ /**
1539
+ * Generate a 10-step palette (50-900) from a seed HSL.
1540
+ * Lightness curve: 50→97%, 100→93%, 200→86%, 300→76%, 400→64%,
1541
+ * 500→seed%, 600→45%, 700→38%, 800→28%, 900→18%
1542
+ */
1543
+ function generatePalette(h, s, l) {
1544
+ const steps = [
1545
+ [50, 97, 90],
1546
+ [100, 93, 85],
1547
+ [200, 86, 78],
1548
+ [300, 76, 68],
1549
+ [400, 64, 58],
1550
+ [500, s, l], // seed
1551
+ [600, s, 45],
1552
+ [700, s, 38],
1553
+ [800, s, 28],
1554
+ [900, s, 18],
1555
+ ];
1556
+
1557
+ const palette = {};
1558
+ for (const [step, sat, light] of steps) {
1559
+ palette[step] = hslToHex(h, sat, light);
1560
+ }
1561
+ return palette;
1562
+ }
1563
+
1564
+ /**
1565
+ * Generate CSS custom properties (tokens) from a palette.
1566
+ */
1567
+
1568
+
1569
+ function rgb(hex) {
1570
+ const { r, g, b } = hexToRgb(hex);
1571
+ return rgbString(r, g, b);
1572
+ }
1573
+
1574
+ /**
1575
+ * Generate light theme tokens from palette + semantic colors.
1576
+ */
1577
+ function generateLightTokens(palette, semantic) {
1578
+ return {
1579
+ // Primary palette
1580
+ 'lux-primary-50': rgb(palette[50]),
1581
+ 'lux-primary-100': rgb(palette[100]),
1582
+ 'lux-primary-200': rgb(palette[200]),
1583
+ 'lux-primary-300': rgb(palette[300]),
1584
+ 'lux-primary-400': rgb(palette[400]),
1585
+ 'lux-primary-500': rgb(palette[500]),
1586
+ 'lux-primary-600': rgb(palette[600]),
1587
+ 'lux-primary-700': rgb(palette[700]),
1588
+ 'lux-primary-800': rgb(palette[800]),
1589
+ 'lux-primary-900': rgb(palette[900]),
1590
+
1591
+ // Semantic
1592
+ 'lux-success': rgb(semantic.success[500]),
1593
+ 'lux-warning': rgb(semantic.warning[500]),
1594
+ 'lux-error': rgb(semantic.error[500]),
1595
+ 'lux-info': rgb(semantic.info[500]),
1596
+
1597
+ // Surface / background (light mode)
1598
+ 'lux-bg': '15 23 42',
1599
+ 'lux-bg-alt': '30 41 59',
1600
+ 'lux-surface': '15 23 42',
1601
+ 'lux-card': '30 41 59',
1602
+ 'lux-overlay': '0 0 0 / 50%',
1603
+
1604
+ // Text
1605
+ 'lux-text': '241 245 249',
1606
+ 'lux-text-dim': '148 163 184',
1607
+ 'lux-text-muted': '100 116 139',
1608
+
1609
+ // Border
1610
+ 'lux-border': '51 65 85',
1611
+ 'lux-border-light': '71 85 105',
1612
+
1613
+ // Interactive
1614
+ 'lux-hover': '255 255 255 / 5%',
1615
+ 'lux-active': '255 255 255 / 10%',
1616
+ 'lux-focus': '0 0 0 / 40%',
1617
+
1618
+ // Radius
1619
+ 'lux-radius-sm': '6px',
1620
+ 'lux-radius': '8px',
1621
+ 'lux-radius-lg': '12px',
1622
+ 'lux-radius-xl': '16px',
1623
+ 'lux-radius-full': '9999px',
1624
+ };
1625
+ }
1626
+
1627
+ /**
1628
+ * Generate dark theme tokens.
1629
+ */
1630
+ function generateDarkTokens(palette, semantic) {
1631
+ return {
1632
+ // Primary — use lighter variants for dark mode
1633
+ 'lux-primary-50': rgb(palette[900]),
1634
+ 'lux-primary-100': rgb(palette[800]),
1635
+ 'lux-primary-200': rgb(palette[700]),
1636
+ 'lux-primary-300': rgb(palette[600]),
1637
+ 'lux-primary-400': rgb(palette[400]),
1638
+ 'lux-primary-500': rgb(palette[500]),
1639
+ 'lux-primary-600': rgb(palette[300]),
1640
+ 'lux-primary-700': rgb(palette[200]),
1641
+ 'lux-primary-800': rgb(palette[100]),
1642
+ 'lux-primary-900': rgb(palette[50]),
1643
+
1644
+ // Semantic
1645
+ 'lux-success': rgb(semantic.success[500]),
1646
+ 'lux-warning': rgb(semantic.warning[500]),
1647
+ 'lux-error': rgb(semantic.error[500]),
1648
+ 'lux-info': rgb(semantic.info[500]),
1649
+
1650
+ // Surface (dark mode)
1651
+ 'lux-bg': '15 23 42',
1652
+ 'lux-bg-alt': '30 41 59',
1653
+ 'lux-surface': '15 23 42',
1654
+ 'lux-card': '30 41 59',
1655
+ 'lux-overlay': '0 0 0 / 70%',
1656
+
1657
+ // Text
1658
+ 'lux-text': '241 245 249',
1659
+ 'lux-text-dim': '148 163 184',
1660
+ 'lux-text-muted': '100 116 139',
1661
+
1662
+ // Border
1663
+ 'lux-border': '51 65 85',
1664
+ 'lux-border-light': '71 85 105',
1665
+
1666
+ // Interactive
1667
+ 'lux-hover': '255 255 255 / 8%',
1668
+ 'lux-active': '255 255 255 / 12%',
1669
+ 'lux-focus': '0 0 0 / 50%',
1670
+
1671
+ // Radius (same as light)
1672
+ 'lux-radius-sm': '6px',
1673
+ 'lux-radius': '8px',
1674
+ 'lux-radius-lg': '12px',
1675
+ 'lux-radius-xl': '16px',
1676
+ 'lux-radius-full': '9999px',
1677
+ };
1678
+ }
1679
+
1680
+ /**
1681
+ * Convert token map to CSS string.
1682
+ */
1683
+ function tokensToCSS(tokens, indent = ' ') {
1684
+ let css = ':root {\n';
1685
+ for (const [key, value] of Object.entries(tokens)) {
1686
+ css += `${indent}--${key}: ${value};\n`;
1687
+ }
1688
+ css += '}';
1689
+ return css;
1690
+ }
1691
+
1692
+ /**
1693
+ * createTheme — generate a complete theme from colors.
1694
+ *
1695
+ * Usage:
1696
+ * const theme = createTheme({ primary: '#6c5ce7' });
1697
+ * const theme = createTheme({
1698
+ * primary: '#6c5ce7', success: '#10b981',
1699
+ * warning: '#f59e0b', error: '#ef4444', info: '#3b82f6'
1700
+ * });
1701
+ * theme.setColors({ success: '#22c55e' });
1702
+ * theme.apply();
1703
+ */
1704
+
1705
+
1706
+ const defaultColors = {
1707
+ primary: '#6c5ce7',
1708
+ success: '#10b981',
1709
+ warning: '#f59e0b',
1710
+ error: '#ef4444',
1711
+ info: '#3b82f6',
1712
+ };
1713
+
1714
+ function createTheme(options = {}) {
1715
+ if (typeof options === 'string') options = { primary: options };
1716
+
1717
+ const _colors = { ...defaultColors, ...options.colors };
1718
+ let _dark = options.dark ?? false;
1719
+ let _radius = options.radius ?? '8px';
1720
+ let _font =
1721
+ options.font ?? '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif';
1722
+
1723
+ function _build() {
1724
+ const { h, s, l } = hexToHsl(_colors.primary);
1725
+ const palette = generatePalette(h, s, l);
1726
+
1727
+ const semantic = {};
1728
+ for (const [name, hex] of Object.entries(_colors)) {
1729
+ if (name === 'primary') continue;
1730
+ const { h: sh} = hexToHsl(hex);
1731
+ semantic[name] = { 500: hex, light: hslToHex(sh, 60, 93) };
1732
+ }
1733
+
1734
+ const tokens = _dark
1735
+ ? generateDarkTokens(palette, semantic)
1736
+ : generateLightTokens(palette, semantic);
1737
+
1738
+ tokens['lux-radius'] = _radius;
1739
+ tokens['lux-font'] = _font;
1740
+
1741
+ return tokens;
1742
+ }
1743
+
1744
+ function _inject(tokens) {
1745
+ let el = document.getElementById('lux-theme');
1746
+ if (!el) {
1747
+ el = document.createElement('style');
1748
+ el.id = 'lux-theme';
1749
+ document.head.appendChild(el);
1750
+ }
1751
+ el.textContent = tokensToCSS(tokens);
1752
+ }
1753
+
1754
+ return {
1755
+ apply() {
1756
+ _inject(_build());
1757
+ },
1758
+
1759
+ setColors(patch) {
1760
+ Object.assign(_colors, patch);
1761
+ this.apply();
1762
+ },
1763
+
1764
+ setDark(dark) {
1765
+ _dark = dark;
1766
+ this.apply();
1767
+ },
1768
+ getCSS() {
1769
+ return tokensToCSS(_build());
1770
+ },
1771
+ getTokens() {
1772
+ return _build();
1773
+ },
1774
+ getColors() {
1775
+ return { ..._colors };
1776
+ },
1777
+
1778
+ get primary() {
1779
+ return _colors.primary;
1780
+ },
1781
+ get dark() {
1782
+ return _dark;
1783
+ },
1784
+ };
1785
+ }
1786
+
1787
+ /**
1788
+ * Lux — Lightweight Web Components Framework
1789
+ */
1790
+
1791
+
1792
+ const version = '0.1.0';
1793
+
1794
+ exports.LuxElement = LuxElement;
1795
+ exports.classMap = classMap;
1796
+ exports.createComponent = createComponent;
1797
+ exports.createI18n = createI18n;
1798
+ exports.createRouter = createRouter;
1799
+ exports.createTheme = createTheme;
1800
+ exports.css = css;
1801
+ exports.date = date;
1802
+ exports.escapeHtml = escapeHtml;
1803
+ exports.getLocale = getLocale;
1804
+ exports.guard = guard;
1805
+ exports.html = html;
1806
+ exports.isTemplateResult = isTemplateResult;
1807
+ exports.msg = msg;
1808
+ exports.nothing = nothing;
1809
+ exports.number = number;
1810
+ exports.registerComponent = registerComponent$1;
1811
+ exports.render = render;
1812
+ exports.repeat = repeat;
1813
+ exports.show = show;
1814
+ exports.styleMap = styleMap;
1815
+ exports.svg = svg;
1816
+ exports.version = version;
1817
+ exports.when = when;
1818
+
1819
+ return exports;
1820
+
1821
+ })({});
1822
+ //# sourceMappingURL=lux.iife.js.map