@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/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
+ }