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