@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.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/README_zh-CN.md +127 -0
- package/dist/components/lux-button/index.iife.min.js +292 -0
- package/dist/components/lux-button/index.min.js +292 -0
- package/dist/components/lux-code/index.iife.min.js +290 -0
- package/dist/components/lux-code/index.min.js +290 -0
- package/dist/components/lux-dropdown/index.iife.min.js +162 -0
- package/dist/components/lux-dropdown/index.min.js +162 -0
- package/dist/components/lux-example/index.iife.min.js +88 -0
- package/dist/components/lux-example/index.min.js +88 -0
- package/dist/components/lux-icon/index.iife.min.js +22 -0
- package/dist/components/lux-icon/index.min.js +22 -0
- package/dist/components/lux-input/index.iife.min.js +238 -0
- package/dist/components/lux-input/index.min.js +238 -0
- package/dist/components/lux-layout/index.iife.min.js +90 -0
- package/dist/components/lux-layout/index.min.js +90 -0
- package/dist/components/lux-menu/index.iife.min.js +193 -0
- package/dist/components/lux-menu/index.min.js +193 -0
- package/dist/components/lux-scroll/index.iife.min.js +137 -0
- package/dist/components/lux-scroll/index.min.js +137 -0
- package/dist/components/lux-switch/index.iife.min.js +116 -0
- package/dist/components/lux-switch/index.min.js +116 -0
- package/dist/components/lux-table/index.iife.min.js +67 -0
- package/dist/components/lux-table/index.min.js +67 -0
- package/dist/lux.core.min.js +1 -0
- package/dist/lux.i18n.min.js +1 -0
- package/dist/lux.iife.js +1822 -0
- package/dist/lux.iife.js.map +1 -0
- package/dist/lux.iife.min.js +1 -0
- package/dist/lux.js +1792 -0
- package/dist/lux.js.map +1 -0
- package/dist/lux.min.js +1 -0
- package/dist/lux.router.min.js +1 -0
- package/dist/lux.template.min.js +1 -0
- package/dist/lux.theme.min.js +1 -0
- package/dist/themes/dark.css +130 -0
- package/dist/themes/light.css +128 -0
- package/package.json +64 -0
- package/src/components/lux-button/index.js +319 -0
- package/src/components/lux-code/index.js +382 -0
- package/src/components/lux-dropdown/index.js +256 -0
- package/src/components/lux-example/index.js +117 -0
- package/src/components/lux-icon/index.js +180 -0
- package/src/components/lux-input/index.js +363 -0
- package/src/components/lux-layout/index.js +222 -0
- package/src/components/lux-menu/index.js +283 -0
- package/src/components/lux-scroll/index.js +349 -0
- package/src/components/lux-switch/index.js +203 -0
- package/src/components/lux-table/index.js +105 -0
- package/src/core.js +7 -0
- package/src/element.js +477 -0
- package/src/i18n/format.js +108 -0
- package/src/i18n/index.js +102 -0
- package/src/i18n/locale.js +26 -0
- package/src/index.js +22 -0
- package/src/router.js +330 -0
- package/src/template.js +402 -0
- package/src/theme/color.js +148 -0
- package/src/theme/create.js +97 -0
- package/src/theme/index.js +2 -0
- package/src/theme/tokens.js +128 -0
- package/src/themes/dark.css +130 -0
- package/src/themes/light.css +128 -0
package/src/element.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LuxElement — Base class for Web Components.
|
|
3
|
+
*
|
|
4
|
+
* Inherits: HTMLElement → LuxElement
|
|
5
|
+
* Features: reactive properties, Shadow DOM, lifecycle, controllers, update batching
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, render, isTemplateResult } from './template.js';
|
|
9
|
+
|
|
10
|
+
// ─── Update scheduling ───
|
|
11
|
+
const pendingUpdates = new Set();
|
|
12
|
+
let updateScheduled = false;
|
|
13
|
+
|
|
14
|
+
function scheduleMicroTask(fn) {
|
|
15
|
+
Promise.resolve().then(fn);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function enqueueUpdate(element) {
|
|
19
|
+
pendingUpdates.add(element);
|
|
20
|
+
if (!updateScheduled) {
|
|
21
|
+
updateScheduled = true;
|
|
22
|
+
scheduleMicroTask(() => {
|
|
23
|
+
updateScheduled = false;
|
|
24
|
+
const elements = [...pendingUpdates];
|
|
25
|
+
pendingUpdates.clear();
|
|
26
|
+
for (const el of elements) {
|
|
27
|
+
el._$performUpdate();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Default converter (attribute ↔ property) ───
|
|
34
|
+
const defaultConverter = {
|
|
35
|
+
toAttribute(value, type) {
|
|
36
|
+
switch (type) {
|
|
37
|
+
case Boolean:
|
|
38
|
+
return value ? '' : null;
|
|
39
|
+
case Object:
|
|
40
|
+
case Array:
|
|
41
|
+
return value == null ? null : JSON.stringify(value);
|
|
42
|
+
default:
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
fromAttribute(value, type) {
|
|
47
|
+
switch (type) {
|
|
48
|
+
case Boolean:
|
|
49
|
+
return value !== null;
|
|
50
|
+
case Number:
|
|
51
|
+
return Number(value);
|
|
52
|
+
case Object:
|
|
53
|
+
case Array:
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(value);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
default:
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ─── LuxElement ───
|
|
66
|
+
export class LuxElement extends HTMLElement {
|
|
67
|
+
// ─── Static: properties declaration ───
|
|
68
|
+
static properties = {};
|
|
69
|
+
static styles = undefined;
|
|
70
|
+
|
|
71
|
+
// ─── Static: finalization ───
|
|
72
|
+
static finalized = false;
|
|
73
|
+
static elementProperties = new Map();
|
|
74
|
+
|
|
75
|
+
// ─── Static: __prepare() — subclass isolation ───
|
|
76
|
+
static __prepare() {
|
|
77
|
+
if (this.hasOwnProperty('elementProperties')) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Finalize any superclasses first
|
|
81
|
+
const superCtor = Object.getPrototypeOf(this);
|
|
82
|
+
if (superCtor.finalize) {
|
|
83
|
+
superCtor.finalize();
|
|
84
|
+
}
|
|
85
|
+
// Copy superclass elementProperties into own Map
|
|
86
|
+
this.elementProperties = new Map(superCtor.elementProperties);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static get observedAttributes() {
|
|
90
|
+
this.finalize();
|
|
91
|
+
return this.__attributeToPropertyMap ? [...this.__attributeToPropertyMap.keys()] : [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static finalize() {
|
|
95
|
+
if (this.hasOwnProperty('finalized')) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.finalized = true;
|
|
99
|
+
this.__prepare();
|
|
100
|
+
|
|
101
|
+
// Create properties from the static properties block
|
|
102
|
+
if (this.hasOwnProperty('properties')) {
|
|
103
|
+
const props = this.properties;
|
|
104
|
+
const propKeys = [
|
|
105
|
+
...Object.getOwnPropertyNames(props),
|
|
106
|
+
...Object.getOwnPropertySymbols(props),
|
|
107
|
+
];
|
|
108
|
+
for (const p of propKeys) {
|
|
109
|
+
this.createProperty(p, props[p]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create the attribute-to-property map
|
|
114
|
+
this.__attributeToPropertyMap = new Map();
|
|
115
|
+
for (const [p, options] of this.elementProperties) {
|
|
116
|
+
const attr = this.__attributeNameForProperty(p, options);
|
|
117
|
+
if (attr !== undefined) {
|
|
118
|
+
this.__attributeToPropertyMap.set(attr, p);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.elementStyles = this.styles;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static __attributeNameForProperty(name, options) {
|
|
126
|
+
const attribute = options.attribute;
|
|
127
|
+
return attribute === false
|
|
128
|
+
? undefined
|
|
129
|
+
: typeof attribute === 'string'
|
|
130
|
+
? attribute
|
|
131
|
+
: typeof name === 'string'
|
|
132
|
+
? name.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
133
|
+
: undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Static: createProperty ───
|
|
137
|
+
static createProperty(name, options = {}) {
|
|
138
|
+
this.__prepare();
|
|
139
|
+
if (this.elementProperties.has(name)) return;
|
|
140
|
+
|
|
141
|
+
if (options.state) {
|
|
142
|
+
options = { ...options, attribute: false };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.elementProperties.set(name, options);
|
|
146
|
+
|
|
147
|
+
if (!options.noAccessor) {
|
|
148
|
+
const key = Symbol();
|
|
149
|
+
const descriptor = this.getPropertyDescriptor(name, key, options);
|
|
150
|
+
if (descriptor !== undefined) {
|
|
151
|
+
Object.defineProperty(this.prototype, name, descriptor);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static getPropertyDescriptor(name, key, options) {
|
|
157
|
+
const { type, reflect, converter } = options;
|
|
158
|
+
const toAttribute = converter?.toAttribute ?? defaultConverter.toAttribute;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
get() {
|
|
162
|
+
return this[key];
|
|
163
|
+
},
|
|
164
|
+
set(value) {
|
|
165
|
+
const oldValue = this[key];
|
|
166
|
+
this[key] = value;
|
|
167
|
+
this.requestUpdate(name, oldValue, options);
|
|
168
|
+
},
|
|
169
|
+
configurable: true,
|
|
170
|
+
enumerable: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Static: addInitializer ───
|
|
175
|
+
static _initializers = [];
|
|
176
|
+
|
|
177
|
+
static addInitializer(fn) {
|
|
178
|
+
this._initializers = [...(this._initializers || []), fn];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Static: shadowRootOptions ───
|
|
182
|
+
static shadowRootOptions = { mode: 'open' };
|
|
183
|
+
|
|
184
|
+
// ─── Constructor ───
|
|
185
|
+
constructor() {
|
|
186
|
+
super();
|
|
187
|
+
|
|
188
|
+
this.__updatePending = false;
|
|
189
|
+
this.__hasUpdated = false;
|
|
190
|
+
this.__controllers = new Set();
|
|
191
|
+
this.__reflectingProperty = null;
|
|
192
|
+
|
|
193
|
+
// Ensure finalized
|
|
194
|
+
if (!this.constructor.hasOwnProperty('finalized')) {
|
|
195
|
+
this.constructor.finalize();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// CloneNode support: save any properties set before upgrade
|
|
199
|
+
this.__saveInstanceProperties();
|
|
200
|
+
|
|
201
|
+
this.requestUpdate();
|
|
202
|
+
|
|
203
|
+
for (const fn of this.constructor._initializers || []) {
|
|
204
|
+
fn(this);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── Instance properties save/replay (cloneNode support) ───
|
|
209
|
+
__saveInstanceProperties() {
|
|
210
|
+
const elementProperties = this.constructor.elementProperties;
|
|
211
|
+
if (!elementProperties) return;
|
|
212
|
+
|
|
213
|
+
for (const [name, options] of elementProperties) {
|
|
214
|
+
if (options.noAccessor) continue;
|
|
215
|
+
|
|
216
|
+
if (this.hasOwnProperty(name)) {
|
|
217
|
+
// Plain property set before setter was defined (cloneNode)
|
|
218
|
+
const value = this[name];
|
|
219
|
+
delete this[name];
|
|
220
|
+
// Store via the Symbol key
|
|
221
|
+
this.__instancePropertyValues = this.__instancePropertyValues || new Map();
|
|
222
|
+
this.__instancePropertyValues.set(name, value);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Lifecycle callbacks ───
|
|
228
|
+
connectedCallback() {
|
|
229
|
+
this.__controllers.forEach((c) => c.hostConnected?.());
|
|
230
|
+
|
|
231
|
+
// Ensure finalized
|
|
232
|
+
if (!this.constructor.hasOwnProperty('finalized')) {
|
|
233
|
+
this.constructor.finalize();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Sync existing attributes to properties (upgrade scenario)
|
|
237
|
+
// attributeChangedCallback doesn't fire for pre-existing attributes
|
|
238
|
+
const attrMap = this.constructor.__attributeToPropertyMap;
|
|
239
|
+
if (attrMap) {
|
|
240
|
+
for (const [attrName, propName] of attrMap) {
|
|
241
|
+
const value = this.getAttribute(attrName);
|
|
242
|
+
if (value !== null) {
|
|
243
|
+
const options = this.constructor.elementProperties.get(propName);
|
|
244
|
+
if (options) {
|
|
245
|
+
const fromAttr =
|
|
246
|
+
options.converter?.fromAttribute ?? defaultConverter.fromAttribute;
|
|
247
|
+
this[propName] = fromAttr(value, options.type);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Replay instance properties saved by __saveInstanceProperties (cloneNode)
|
|
254
|
+
if (this.__instancePropertyValues) {
|
|
255
|
+
for (const [name, value] of this.__instancePropertyValues) {
|
|
256
|
+
this[name] = value;
|
|
257
|
+
}
|
|
258
|
+
this.__instancePropertyValues = undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Ensure renderRoot is created
|
|
262
|
+
this.renderRoot;
|
|
263
|
+
|
|
264
|
+
// Trigger first update if not yet done
|
|
265
|
+
if (!this.__hasUpdated) {
|
|
266
|
+
this.requestUpdate();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
disconnectedCallback() {
|
|
271
|
+
this.__controllers.forEach((c) => c.hostDisconnected?.());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
attributeChangedCallback(name, _old, value) {
|
|
275
|
+
const propName = this.constructor.__attributeToPropertyMap?.get(name);
|
|
276
|
+
if (propName !== undefined && this.__reflectingProperty !== propName) {
|
|
277
|
+
const options = this.constructor.elementProperties.get(propName);
|
|
278
|
+
if (options) {
|
|
279
|
+
const converter =
|
|
280
|
+
typeof options.converter === 'function'
|
|
281
|
+
? { fromAttribute: options.converter }
|
|
282
|
+
: options.converter?.fromAttribute !== undefined
|
|
283
|
+
? options.converter
|
|
284
|
+
: defaultConverter;
|
|
285
|
+
this.__reflectingProperty = propName;
|
|
286
|
+
const convertedValue = converter.fromAttribute(value, options.type);
|
|
287
|
+
this[propName] = convertedValue;
|
|
288
|
+
this.__reflectingProperty = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── RenderRoot & Styles ───
|
|
294
|
+
get renderRoot() {
|
|
295
|
+
if (!this._renderRoot) {
|
|
296
|
+
this._renderRoot = this.createRenderRoot();
|
|
297
|
+
this.__adoptStyles();
|
|
298
|
+
}
|
|
299
|
+
return this._renderRoot;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
createRenderRoot() {
|
|
303
|
+
return this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
__adoptStyles() {
|
|
307
|
+
const styles = this.constructor.elementStyles;
|
|
308
|
+
if (!styles) return;
|
|
309
|
+
|
|
310
|
+
const items = Array.isArray(styles) ? styles : [styles];
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const item of items) {
|
|
313
|
+
if (typeof item === 'string') {
|
|
314
|
+
parts.push(item);
|
|
315
|
+
} else if (isTemplateResult(item)) {
|
|
316
|
+
parts.push(item.strings.join(''));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (parts.length === 0) return;
|
|
320
|
+
|
|
321
|
+
if (this._renderRoot.adoptedStyleSheets !== undefined) {
|
|
322
|
+
const sheet = new CSSStyleSheet();
|
|
323
|
+
sheet.replaceSync(parts.join('\n'));
|
|
324
|
+
this._renderRoot.adoptedStyleSheets = [...this._renderRoot.adoptedStyleSheets, sheet];
|
|
325
|
+
} else {
|
|
326
|
+
const tag = document.createElement('style');
|
|
327
|
+
tag.textContent = parts.join('\n');
|
|
328
|
+
this._renderRoot.appendChild(tag);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Update lifecycle ───
|
|
333
|
+
enableUpdating(_requestedUpdate) {}
|
|
334
|
+
|
|
335
|
+
get updateComplete() {
|
|
336
|
+
return this.__updateCompletePromise ?? Promise.resolve();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get hasUpdated() {
|
|
340
|
+
return this.__hasUpdated;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get isUpdatePending() {
|
|
344
|
+
return this.__updatePending;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
requestUpdate(name, oldValue, options) {
|
|
348
|
+
if (name !== undefined) {
|
|
349
|
+
const hasChanged = options?.hasChanged ?? ((v, o) => !Object.is(v, o));
|
|
350
|
+
if (!hasChanged(this[name], oldValue)) return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!this.__updatePending) {
|
|
354
|
+
this.__updatePending = true;
|
|
355
|
+
enqueueUpdate(this);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async performUpdate() {
|
|
360
|
+
if (!this.isUpdatePending) return;
|
|
361
|
+
|
|
362
|
+
this.__updatePending = false;
|
|
363
|
+
|
|
364
|
+
this.willUpdate(new Map());
|
|
365
|
+
|
|
366
|
+
this.__controllers.forEach((c) => c.hostUpdate?.());
|
|
367
|
+
|
|
368
|
+
this.update(new Map());
|
|
369
|
+
|
|
370
|
+
this.__hasUpdated = true;
|
|
371
|
+
|
|
372
|
+
// Reflect properties to attributes after update
|
|
373
|
+
for (const [name, options] of this.constructor.elementProperties) {
|
|
374
|
+
if (options.reflect) {
|
|
375
|
+
const value = this[name];
|
|
376
|
+
const toAttribute = options.converter?.toAttribute ?? defaultConverter.toAttribute;
|
|
377
|
+
const attr = this.constructor.__attributeNameForProperty(name, options);
|
|
378
|
+
if (attr !== undefined) {
|
|
379
|
+
this.__reflectingProperty = name;
|
|
380
|
+
const attrValue = toAttribute(value, options.type);
|
|
381
|
+
if (attrValue === null || attrValue === undefined) {
|
|
382
|
+
this.removeAttribute(attr);
|
|
383
|
+
} else {
|
|
384
|
+
this.setAttribute(attr, String(attrValue));
|
|
385
|
+
}
|
|
386
|
+
this.__reflectingProperty = null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.__controllers.forEach((c) => c.hostUpdated?.());
|
|
392
|
+
this.firstUpdated(new Map());
|
|
393
|
+
this.updated(new Map());
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
_$performUpdate() {
|
|
397
|
+
this.performUpdate();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── Lifecycle hooks ───
|
|
401
|
+
shouldUpdate(_changedProperties) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
willUpdate(_changedProperties) {}
|
|
406
|
+
|
|
407
|
+
update(_changedProperties) {
|
|
408
|
+
const result = this.render();
|
|
409
|
+
if (result) {
|
|
410
|
+
render(result, this.renderRoot, { host: this });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
render() {
|
|
415
|
+
return html``;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
firstUpdated(_changedProperties) {}
|
|
419
|
+
updated(_changedProperties) {}
|
|
420
|
+
propertyChangedCallback(_name, _oldValue, _newValue) {}
|
|
421
|
+
|
|
422
|
+
// ─── Controller API ───
|
|
423
|
+
addController(controller) {
|
|
424
|
+
this.__controllers.add(controller);
|
|
425
|
+
if (this.isConnected) {
|
|
426
|
+
controller.hostConnected?.();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
removeController(controller) {
|
|
431
|
+
this.__controllers.delete(controller);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── Utilities ───
|
|
435
|
+
emit(type, detail, options = {}) {
|
|
436
|
+
return this.dispatchEvent(
|
|
437
|
+
new CustomEvent(type, {
|
|
438
|
+
detail,
|
|
439
|
+
bubbles: true,
|
|
440
|
+
composed: true,
|
|
441
|
+
...options,
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
$(selector) {
|
|
447
|
+
return this.renderRoot.querySelector(selector);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
$$(selector) {
|
|
451
|
+
return this.renderRoot.querySelectorAll(selector);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── Register component ───
|
|
456
|
+
export function registerComponent(name, component) {
|
|
457
|
+
if (!name.includes('-')) {
|
|
458
|
+
throw new Error(`Component name must contain a hyphen: ${name}`);
|
|
459
|
+
}
|
|
460
|
+
if (!customElements.get(name)) {
|
|
461
|
+
customElements.define(name, component);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ─── createComponent (function-based components) ───
|
|
466
|
+
export function createComponent(renderFn, options = {}) {
|
|
467
|
+
const { properties = {} } = options;
|
|
468
|
+
|
|
469
|
+
class DynamicComponent extends LuxElement {
|
|
470
|
+
static properties = properties;
|
|
471
|
+
render() {
|
|
472
|
+
return renderFn.call(this, this);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return DynamicComponent;
|
|
477
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICU MessageFormat subset parser
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Simple interpolation: "Hello, {name}"
|
|
6
|
+
* - Plural: "{count, plural, =0{No items} =1{# item} other{# items}}"
|
|
7
|
+
* - Select: "{gender, select, male{He} female{She} other{They}}"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the matching closing brace for an opening brace at position `start`.
|
|
12
|
+
*/
|
|
13
|
+
function findMatchingBrace(str, start) {
|
|
14
|
+
let depth = 1;
|
|
15
|
+
for (let i = start + 1; i < str.length; i++) {
|
|
16
|
+
if (str[i] === '{') depth++;
|
|
17
|
+
else if (str[i] === '}') {
|
|
18
|
+
depth--;
|
|
19
|
+
if (depth === 0) return i;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract all top-level {...} expressions from the template.
|
|
27
|
+
*/
|
|
28
|
+
function extractExpressions(template) {
|
|
29
|
+
const parts = [];
|
|
30
|
+
let i = 0;
|
|
31
|
+
while (i < template.length) {
|
|
32
|
+
if (template[i] === '{') {
|
|
33
|
+
const end = findMatchingBrace(template, i);
|
|
34
|
+
if (end !== -1) {
|
|
35
|
+
parts.push({ start: i, end, expr: template.slice(i + 1, end) });
|
|
36
|
+
i = end + 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
return parts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatMessage(template, values = {}) {
|
|
46
|
+
if (!template || typeof template !== 'string') return '';
|
|
47
|
+
|
|
48
|
+
const exprs = extractExpressions(template);
|
|
49
|
+
let result = '';
|
|
50
|
+
let lastIdx = 0;
|
|
51
|
+
|
|
52
|
+
for (const { start, end, expr } of exprs) {
|
|
53
|
+
result += template.slice(lastIdx, start);
|
|
54
|
+
const trimmed = expr.trim();
|
|
55
|
+
|
|
56
|
+
// Plural: {key, plural, =N{...} other{...}}
|
|
57
|
+
const pluralMatch = trimmed.match(/^(\w+),\s*plural\s*,\s*(.+)$/);
|
|
58
|
+
if (pluralMatch) {
|
|
59
|
+
const [, key, rules] = pluralMatch;
|
|
60
|
+
const count = Number(values[key]) || 0;
|
|
61
|
+
result += formatPlural(count, rules);
|
|
62
|
+
lastIdx = end + 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Select: {key, select, val1{...} val2{...}}
|
|
67
|
+
const selectMatch = trimmed.match(/^(\w+),\s*select\s*,\s*(.+)$/);
|
|
68
|
+
if (selectMatch) {
|
|
69
|
+
const [, key, rules] = selectMatch;
|
|
70
|
+
const value = String(values[key] || '');
|
|
71
|
+
result += formatSelect(value, rules);
|
|
72
|
+
lastIdx = end + 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Simple interpolation: {key}
|
|
77
|
+
result += values[trimmed] ?? template.slice(start, end + 1);
|
|
78
|
+
lastIdx = end + 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result += template.slice(lastIdx);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatPlural(count, rulesStr) {
|
|
86
|
+
const rules = parseRules(rulesStr);
|
|
87
|
+
const exact = rules[`=${count}`];
|
|
88
|
+
if (exact !== undefined) return exact.replace(/#/g, String(count));
|
|
89
|
+
|
|
90
|
+
const key = count === 1 ? 'one' : 'other';
|
|
91
|
+
const rule = rules[key] || rules['other'] || '';
|
|
92
|
+
return rule.replace(/#/g, String(count));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatSelect(value, rulesStr) {
|
|
96
|
+
const rules = parseRules(rulesStr);
|
|
97
|
+
return rules[value] || rules['other'] || '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseRules(rulesStr) {
|
|
101
|
+
const rules = {};
|
|
102
|
+
const re = /(\w+|=\d+)\{([^}]*)\}/g;
|
|
103
|
+
let m;
|
|
104
|
+
while ((m = re.exec(rulesStr))) {
|
|
105
|
+
rules[m[1]] = m[2];
|
|
106
|
+
}
|
|
107
|
+
return rules;
|
|
108
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lux i18n — Lightweight internationalization
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { createI18n, msg, number, date } from 'lux/i18n';
|
|
6
|
+
*
|
|
7
|
+
* const i18n = createI18n({
|
|
8
|
+
* locale: 'zh-CN',
|
|
9
|
+
* messages: {
|
|
10
|
+
* 'zh-CN': { hello: '你好', welcome: '欢迎, {name}' },
|
|
11
|
+
* 'en': { hello: 'Hello', welcome: 'Welcome, {name}' },
|
|
12
|
+
* }
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // In templates
|
|
16
|
+
* html`<h1>${msg('hello')}</h1>`
|
|
17
|
+
* html`<p>${msg('welcome', { name: 'Alice' })}</p>`
|
|
18
|
+
* html`<span>${msg('items', { count: 5 })}</span>`
|
|
19
|
+
*
|
|
20
|
+
* // Number/date formatting
|
|
21
|
+
* html`<span>${number(1234567)}</span>`
|
|
22
|
+
* html`<span>${date(new Date())}</span>`
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { formatMessage } from './format.js';
|
|
26
|
+
import { getDefaultLocale, formatNumber, formatDate } from './locale.js';
|
|
27
|
+
|
|
28
|
+
let currentLocale = '';
|
|
29
|
+
let currentMessages = {};
|
|
30
|
+
let listeners = [];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the current translation for a key.
|
|
34
|
+
*/
|
|
35
|
+
export function msg(key, values = {}) {
|
|
36
|
+
const dict = currentMessages[currentLocale] || currentMessages;
|
|
37
|
+
const template = dict[key] ?? key;
|
|
38
|
+
return formatMessage(template, values);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format a number using Intl.NumberFormat.
|
|
43
|
+
*/
|
|
44
|
+
export function number(value, options) {
|
|
45
|
+
return formatNumber(
|
|
46
|
+
value,
|
|
47
|
+
typeof options === 'string' ? { style: options } : options,
|
|
48
|
+
currentLocale
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format a date using Intl.DateTimeFormat.
|
|
54
|
+
*/
|
|
55
|
+
export function date(value, options) {
|
|
56
|
+
return formatDate(value, options, currentLocale);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the current locale string.
|
|
61
|
+
*/
|
|
62
|
+
export function getLocale() {
|
|
63
|
+
return currentLocale;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create an i18n instance.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} options
|
|
70
|
+
* @param {string} options.locale - Initial locale (default: navigator.language)
|
|
71
|
+
* @param {Object} messages - { 'zh-CN': {...}, 'en': {...} }
|
|
72
|
+
* @returns {Object} i18n instance
|
|
73
|
+
*/
|
|
74
|
+
export function createI18n(options = {}) {
|
|
75
|
+
currentLocale = options.locale || getDefaultLocale();
|
|
76
|
+
currentMessages = options.messages || {};
|
|
77
|
+
|
|
78
|
+
function setLocale(locale) {
|
|
79
|
+
currentLocale = locale;
|
|
80
|
+
for (const fn of listeners) fn(locale);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onLocaleChange(fn) {
|
|
84
|
+
listeners.push(fn);
|
|
85
|
+
return () => {
|
|
86
|
+
const i = listeners.indexOf(fn);
|
|
87
|
+
if (i >= 0) listeners.splice(i, 1);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
get locale() {
|
|
93
|
+
return currentLocale;
|
|
94
|
+
},
|
|
95
|
+
setLocale,
|
|
96
|
+
onLocaleChange,
|
|
97
|
+
msg,
|
|
98
|
+
number,
|
|
99
|
+
date,
|
|
100
|
+
getLocale,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale detection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function getDefaultLocale() {
|
|
6
|
+
if (typeof navigator !== 'undefined' && navigator.language) {
|
|
7
|
+
return navigator.language;
|
|
8
|
+
}
|
|
9
|
+
return 'en';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatNumber(value, options = {}, locale) {
|
|
13
|
+
try {
|
|
14
|
+
return new Intl.NumberFormat(locale, options).format(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return String(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatDate(value, options = {}, locale) {
|
|
21
|
+
try {
|
|
22
|
+
return new Intl.DateTimeFormat(locale, options).format(value);
|
|
23
|
+
} catch {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
}
|