@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/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lux — Lightweight Web Components Framework
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ─── Core exports ───
|
|
6
|
+
|
|
7
|
+
// Template engine
|
|
8
|
+
export { html, svg, css, render, isTemplateResult, nothing, escapeHtml } from './template.js';
|
|
9
|
+
|
|
10
|
+
// Directives
|
|
11
|
+
export { repeat, when, show, classMap, styleMap, guard } from './template.js';
|
|
12
|
+
|
|
13
|
+
// Element base class
|
|
14
|
+
export { LuxElement, registerComponent, createComponent } from './element.js';
|
|
15
|
+
|
|
16
|
+
export { createRouter } from './router.js';
|
|
17
|
+
|
|
18
|
+
export { createI18n, msg, number, date, getLocale } from './i18n/index.js';
|
|
19
|
+
|
|
20
|
+
export { createTheme } from './theme/index.js';
|
|
21
|
+
|
|
22
|
+
export const version = '0.1.0';
|
package/src/router.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lux Router - Hash-based SPA routing
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const router = createRouter({ routes: [...] });
|
|
6
|
+
* router.push('/users/123');
|
|
7
|
+
*
|
|
8
|
+
* Routes accept component classes or lazy loaders:
|
|
9
|
+
* { path: '/', component: HomePage } // direct class
|
|
10
|
+
* { path: '/admin', component: () => import('./admin.js') } // lazy
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function registerComponent(name, component) {
|
|
14
|
+
if (!name.includes('-')) throw new Error(`Component name must contain a hyphen: ${name}`);
|
|
15
|
+
if (!customElements.get(name)) customElements.define(name, component);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Route matching ───
|
|
19
|
+
|
|
20
|
+
function pathToRegex(pattern) {
|
|
21
|
+
let seg = pattern.replace(/\/+$/, '') || '/';
|
|
22
|
+
if (seg === '/') return /^\/?$/;
|
|
23
|
+
const re = '^' + seg.replace(/:(\w+)/g, '([^/]+)').replace(/\(\.\*\)/g, '(.*)') + '$';
|
|
24
|
+
return new RegExp(re);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractParamNames(pattern) {
|
|
28
|
+
const names = [];
|
|
29
|
+
const re = /:(\w+)/g;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = re.exec(pattern))) names.push(m[1]);
|
|
32
|
+
return names;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseQuery(hash) {
|
|
36
|
+
const idx = hash.indexOf('?');
|
|
37
|
+
if (idx === -1) return {};
|
|
38
|
+
const params = {};
|
|
39
|
+
for (const pair of hash.slice(idx + 1).split('&')) {
|
|
40
|
+
const [k, v] = pair.split('=');
|
|
41
|
+
if (k) params[decodeURIComponent(k)] = v ? decodeURIComponent(v) : '';
|
|
42
|
+
}
|
|
43
|
+
return params;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPathFromHash(hash) {
|
|
47
|
+
return hash.replace(/^#/, '').split('?')[0] || '/';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Component registry ───
|
|
51
|
+
|
|
52
|
+
const componentRegistry = new Map(); // tagName → class
|
|
53
|
+
|
|
54
|
+
function resolveComponent(componentOrLoader) {
|
|
55
|
+
if (typeof componentOrLoader === 'string') {
|
|
56
|
+
return Promise.resolve(componentOrLoader);
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
typeof componentOrLoader === 'function' &&
|
|
60
|
+
componentOrLoader.prototype instanceof HTMLElement
|
|
61
|
+
) {
|
|
62
|
+
return ensureRegistered(componentOrLoader);
|
|
63
|
+
}
|
|
64
|
+
if (typeof componentOrLoader === 'function') {
|
|
65
|
+
// Assume lazy loader: () => import(...)
|
|
66
|
+
return componentOrLoader().then((mod) => {
|
|
67
|
+
const Comp = mod.default || mod[Object.keys(mod)[0]];
|
|
68
|
+
return ensureRegistered(Comp);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return Promise.resolve(null);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureRegistered(ComponentClass) {
|
|
75
|
+
if (!ComponentClass || !ComponentClass.prototype) return Promise.resolve(null);
|
|
76
|
+
|
|
77
|
+
const tagName = ComponentClass.tagName || classToTag(ComponentClass.name);
|
|
78
|
+
|
|
79
|
+
// Already registered?
|
|
80
|
+
if (customElements.get(tagName)) {
|
|
81
|
+
return Promise.resolve(tagName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
registerComponent(tagName, ComponentClass);
|
|
85
|
+
return Promise.resolve(tagName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function classToTag(name) {
|
|
89
|
+
// HomePage → home-page, UserProfile → user-profile
|
|
90
|
+
return name
|
|
91
|
+
.replace(/([A-Z])/g, '-$1')
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/^-/, '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Router class ───
|
|
97
|
+
|
|
98
|
+
class Router {
|
|
99
|
+
constructor(options = {}) {
|
|
100
|
+
this.routes = (options.routes || []).map((r) => ({
|
|
101
|
+
...r,
|
|
102
|
+
_regex: pathToRegex(r.path),
|
|
103
|
+
_paramNames: extractParamNames(r.path),
|
|
104
|
+
_children: (r.children || []).map((c) => ({
|
|
105
|
+
...c,
|
|
106
|
+
_regex: pathToRegex(c.path),
|
|
107
|
+
_paramNames: extractParamNames(c.path),
|
|
108
|
+
})),
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
this._guards = [];
|
|
112
|
+
this._listeners = [];
|
|
113
|
+
this.current = null;
|
|
114
|
+
this._prev = null;
|
|
115
|
+
this._onHashChange = this._onHashChange.bind(this);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
start() {
|
|
119
|
+
window.addEventListener('hashchange', this._onHashChange);
|
|
120
|
+
this._resolve();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
stop() {
|
|
124
|
+
window.removeEventListener('hashchange', this._onHashChange);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
push(path) {
|
|
128
|
+
location.hash = path;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
replace(path) {
|
|
132
|
+
const url = new URL(location.href);
|
|
133
|
+
url.hash = path;
|
|
134
|
+
history.replaceState(null, '', url.toString());
|
|
135
|
+
this._onHashChange();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
back() {
|
|
139
|
+
history.back();
|
|
140
|
+
}
|
|
141
|
+
forward() {
|
|
142
|
+
history.forward();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
beforeEach(fn) {
|
|
146
|
+
this._guards.push(fn);
|
|
147
|
+
return () => {
|
|
148
|
+
const i = this._guards.indexOf(fn);
|
|
149
|
+
if (i >= 0) this._guards.splice(i, 1);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_subscribe(fn) {
|
|
154
|
+
this._listeners.push(fn);
|
|
155
|
+
return () => {
|
|
156
|
+
const i = this._listeners.indexOf(fn);
|
|
157
|
+
if (i >= 0) this._listeners.splice(i, 1);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_onHashChange() {
|
|
162
|
+
this._resolve();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_resolve() {
|
|
166
|
+
const hash = location.hash || '#/';
|
|
167
|
+
const path = getPathFromHash(hash);
|
|
168
|
+
const query = parseQuery(hash);
|
|
169
|
+
|
|
170
|
+
let matched = null;
|
|
171
|
+
for (const route of this.routes) {
|
|
172
|
+
const hasChildren = route._children.length > 0;
|
|
173
|
+
|
|
174
|
+
// For routes with children, use prefix matching
|
|
175
|
+
// For leaf routes, use exact matching
|
|
176
|
+
let m;
|
|
177
|
+
if (hasChildren) {
|
|
178
|
+
// Prefix match: path must start with route path
|
|
179
|
+
const prefix = route.path === '/' ? '' : route.path.replace(/\/+$/, '');
|
|
180
|
+
if (
|
|
181
|
+
path === prefix ||
|
|
182
|
+
path.startsWith(prefix + '/') ||
|
|
183
|
+
(prefix === '' && path.startsWith('/'))
|
|
184
|
+
) {
|
|
185
|
+
m = [path]; // fake match for prefix
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
m = path.match(route._regex);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (m) {
|
|
192
|
+
const params = {};
|
|
193
|
+
route._paramNames.forEach((name, i) => {
|
|
194
|
+
if (m[i + 1] !== undefined) params[name] = decodeURIComponent(m[i + 1]);
|
|
195
|
+
});
|
|
196
|
+
matched = { route, params: { ...params }, query, path };
|
|
197
|
+
|
|
198
|
+
// Child matching (check BEFORE redirect)
|
|
199
|
+
if (hasChildren) {
|
|
200
|
+
const prefix = route.path === '/' ? '' : route.path.replace(/\/+$/, '');
|
|
201
|
+
const childPath = path.slice(prefix.length) || '/';
|
|
202
|
+
let childMatched = false;
|
|
203
|
+
for (const child of route._children) {
|
|
204
|
+
const cm = childPath.match(child._regex);
|
|
205
|
+
if (cm) {
|
|
206
|
+
const childParams = {};
|
|
207
|
+
child._paramNames.forEach((name, i) => {
|
|
208
|
+
childParams[name] = decodeURIComponent(cm[i + 1]);
|
|
209
|
+
});
|
|
210
|
+
matched.child = {
|
|
211
|
+
route: child,
|
|
212
|
+
params: { ...params, ...childParams },
|
|
213
|
+
query,
|
|
214
|
+
path,
|
|
215
|
+
};
|
|
216
|
+
childMatched = true;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// No child matched — redirect only if path equals parent path
|
|
221
|
+
if (
|
|
222
|
+
!childMatched &&
|
|
223
|
+
route.redirect &&
|
|
224
|
+
path === (route.path === '/' ? '/' : route.path)
|
|
225
|
+
) {
|
|
226
|
+
this.replace(route.redirect);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// No child matched and no redirect — skip this parent, try other routes
|
|
230
|
+
if (!childMatched) {
|
|
231
|
+
matched = null;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
} else if (route.redirect) {
|
|
235
|
+
this.replace(route.redirect);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!matched) {
|
|
243
|
+
this.current = { route: null, params: {}, query, path };
|
|
244
|
+
} else {
|
|
245
|
+
this.current = matched;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const guard of this._guards) {
|
|
249
|
+
if (guard(this.current, this._prev) === false) {
|
|
250
|
+
if (this._prev) history.replaceState(null, '', '#' + this._prev.path);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this._prev = { ...this.current };
|
|
256
|
+
for (const fn of this._listeners) fn(this.current);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Create router + register <router-outlet> ───
|
|
261
|
+
|
|
262
|
+
export function createRouter(options) {
|
|
263
|
+
const router = new Router(options);
|
|
264
|
+
|
|
265
|
+
if (!customElements.get('router-outlet')) {
|
|
266
|
+
class RouterOutlet extends HTMLElement {
|
|
267
|
+
connectedCallback() {
|
|
268
|
+
this.style.display = 'contents';
|
|
269
|
+
this._unsub = router._subscribe(() => this._update());
|
|
270
|
+
this._update();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
disconnectedCallback() {
|
|
274
|
+
this._unsub?.();
|
|
275
|
+
this._loadingAbort?.abort();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async _update() {
|
|
279
|
+
const match = this._getMatch();
|
|
280
|
+
this._loadingAbort?.abort();
|
|
281
|
+
|
|
282
|
+
if (!match || !match.route || !match.route.component) {
|
|
283
|
+
this.textContent = '';
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Skip re-render if the same route is already rendered
|
|
288
|
+
if (this._lastRoute === match.route) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
this._loadingAbort = controller;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const tagName = await resolveComponent(match.route.component);
|
|
297
|
+
if (controller.signal.aborted) return;
|
|
298
|
+
|
|
299
|
+
this.textContent = '';
|
|
300
|
+
if (tagName) {
|
|
301
|
+
const el = document.createElement(tagName);
|
|
302
|
+
if (match.params) el.params = match.params;
|
|
303
|
+
if (match.query) el.query = match.query;
|
|
304
|
+
this.appendChild(el);
|
|
305
|
+
this._lastRoute = match.route;
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
if (!controller.signal.aborted) {
|
|
309
|
+
console.error('[Lux Router] Failed to load component:', e);
|
|
310
|
+
this.textContent = '';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_getMatch() {
|
|
316
|
+
const level = parseInt(this.getAttribute('level')) || 0;
|
|
317
|
+
let match = router.current;
|
|
318
|
+
for (let i = 0; i < level; i++) {
|
|
319
|
+
if (!match || !match.child) return null;
|
|
320
|
+
match = match.child;
|
|
321
|
+
}
|
|
322
|
+
return match;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
registerComponent('router-outlet', RouterOutlet);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
router.start();
|
|
329
|
+
return router;
|
|
330
|
+
}
|