@ahriknow/lux 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/README_zh-CN.md +127 -0
  4. package/dist/components/lux-button/index.iife.min.js +292 -0
  5. package/dist/components/lux-button/index.min.js +292 -0
  6. package/dist/components/lux-code/index.iife.min.js +290 -0
  7. package/dist/components/lux-code/index.min.js +290 -0
  8. package/dist/components/lux-dropdown/index.iife.min.js +162 -0
  9. package/dist/components/lux-dropdown/index.min.js +162 -0
  10. package/dist/components/lux-example/index.iife.min.js +88 -0
  11. package/dist/components/lux-example/index.min.js +88 -0
  12. package/dist/components/lux-icon/index.iife.min.js +22 -0
  13. package/dist/components/lux-icon/index.min.js +22 -0
  14. package/dist/components/lux-input/index.iife.min.js +238 -0
  15. package/dist/components/lux-input/index.min.js +238 -0
  16. package/dist/components/lux-layout/index.iife.min.js +90 -0
  17. package/dist/components/lux-layout/index.min.js +90 -0
  18. package/dist/components/lux-menu/index.iife.min.js +193 -0
  19. package/dist/components/lux-menu/index.min.js +193 -0
  20. package/dist/components/lux-scroll/index.iife.min.js +137 -0
  21. package/dist/components/lux-scroll/index.min.js +137 -0
  22. package/dist/components/lux-switch/index.iife.min.js +116 -0
  23. package/dist/components/lux-switch/index.min.js +116 -0
  24. package/dist/components/lux-table/index.iife.min.js +67 -0
  25. package/dist/components/lux-table/index.min.js +67 -0
  26. package/dist/lux.core.min.js +1 -0
  27. package/dist/lux.i18n.min.js +1 -0
  28. package/dist/lux.iife.js +1822 -0
  29. package/dist/lux.iife.js.map +1 -0
  30. package/dist/lux.iife.min.js +1 -0
  31. package/dist/lux.js +1792 -0
  32. package/dist/lux.js.map +1 -0
  33. package/dist/lux.min.js +1 -0
  34. package/dist/lux.router.min.js +1 -0
  35. package/dist/lux.template.min.js +1 -0
  36. package/dist/lux.theme.min.js +1 -0
  37. package/dist/themes/dark.css +130 -0
  38. package/dist/themes/light.css +128 -0
  39. package/package.json +64 -0
  40. package/src/components/lux-button/index.js +319 -0
  41. package/src/components/lux-code/index.js +382 -0
  42. package/src/components/lux-dropdown/index.js +256 -0
  43. package/src/components/lux-example/index.js +117 -0
  44. package/src/components/lux-icon/index.js +180 -0
  45. package/src/components/lux-input/index.js +363 -0
  46. package/src/components/lux-layout/index.js +222 -0
  47. package/src/components/lux-menu/index.js +283 -0
  48. package/src/components/lux-scroll/index.js +349 -0
  49. package/src/components/lux-switch/index.js +203 -0
  50. package/src/components/lux-table/index.js +105 -0
  51. package/src/core.js +7 -0
  52. package/src/element.js +477 -0
  53. package/src/i18n/format.js +108 -0
  54. package/src/i18n/index.js +102 -0
  55. package/src/i18n/locale.js +26 -0
  56. package/src/index.js +22 -0
  57. package/src/router.js +330 -0
  58. package/src/template.js +402 -0
  59. package/src/theme/color.js +148 -0
  60. package/src/theme/create.js +97 -0
  61. package/src/theme/index.js +2 -0
  62. package/src/theme/tokens.js +128 -0
  63. package/src/themes/dark.css +130 -0
  64. package/src/themes/light.css +128 -0
@@ -0,0 +1,222 @@
1
+ import { html, css, LuxElement, registerComponent } from '../../index.js';
2
+
3
+ /**
4
+ * lux-layout — Aside-left layout component.
5
+ *
6
+ * Structure:
7
+ * aside | header
8
+ * | main (flex:1, overflow:auto)
9
+ * | footer
10
+ *
11
+ * Usage:
12
+ * <lux-layout aside-width="240px" aside-min="120px" aside-max="400px">
13
+ * <header slot="header">Header</header>
14
+ * <aside slot="aside">Sidebar</aside>
15
+ * <main slot="main">Content</main>
16
+ * <footer slot="footer">Footer</footer>
17
+ * </lux-layout>
18
+ *
19
+ * Props:
20
+ * aside-width — aside width (number=px, string='20rem')
21
+ * aside-min — min width for aside drag
22
+ * aside-max — max width for aside drag
23
+ * aside-scroll — aside overflow scroll
24
+ * no-header — hide header
25
+ * no-footer — hide footer
26
+ * no-aside — hide aside
27
+ */
28
+
29
+ const styles = css`
30
+ :host {
31
+ display: block;
32
+ width: 100%;
33
+ height: 100%;
34
+ overflow: hidden;
35
+ }
36
+
37
+ .root {
38
+ display: flex;
39
+ width: 100%;
40
+ height: 100%;
41
+ overflow: hidden;
42
+ }
43
+
44
+ .slot-aside {
45
+ z-index: 3;
46
+ flex-shrink: 0;
47
+ width: var(--aside-w, 240px);
48
+ min-width: var(--aside-min, 120px);
49
+ max-width: var(--aside-max, 50vw);
50
+ overflow: hidden;
51
+ position: relative;
52
+ border-right: 1px solid rgb(var(--lux-border, 51 65 85));
53
+ background: rgb(var(--lux-card, 30 41 59));
54
+ }
55
+
56
+ :host([aside-scroll]) .slot-aside {
57
+ overflow-y: auto;
58
+ }
59
+
60
+ .slot-right {
61
+ flex: 1;
62
+ display: flex;
63
+ flex-direction: column;
64
+ min-width: 0;
65
+ }
66
+
67
+ .slot-header {
68
+ flex-shrink: 0;
69
+ z-index: 2;
70
+ }
71
+ .slot-footer {
72
+ flex-shrink: 0;
73
+ z-index: 2;
74
+ }
75
+ .slot-main {
76
+ flex: 1;
77
+ overflow: auto;
78
+ min-height: 0;
79
+ z-index: 1;
80
+ }
81
+
82
+ :host([no-header]) .slot-header {
83
+ display: none;
84
+ }
85
+ :host([no-footer]) .slot-footer {
86
+ display: none;
87
+ }
88
+ :host([no-aside]) .slot-aside {
89
+ display: none;
90
+ }
91
+
92
+ .resize-handle {
93
+ position: absolute;
94
+ top: 0;
95
+ right: 0;
96
+ width: 6px;
97
+ height: 100%;
98
+ cursor: col-resize;
99
+ z-index: 10;
100
+ background: transparent;
101
+ transition: background var(--lux-transition, 150ms ease);
102
+ }
103
+ .resize-handle:hover {
104
+ background: rgb(var(--lux-primary-400, 129 120 247) / 0.3);
105
+ }
106
+ `;
107
+
108
+ class LuxLayout extends LuxElement {
109
+ static styles = styles;
110
+
111
+ static properties = {
112
+ asideWidth: { type: String, attribute: 'aside-width', reflect: true },
113
+ asideMin: { type: String, attribute: 'aside-min', reflect: true },
114
+ asideMax: { type: String, attribute: 'aside-max', reflect: true },
115
+ asideScroll: { type: Boolean, attribute: 'aside-scroll', reflect: true },
116
+ noHeader: { type: Boolean, attribute: 'no-header', reflect: true },
117
+ noFooter: { type: Boolean, attribute: 'no-footer', reflect: true },
118
+ noAside: { type: Boolean, attribute: 'no-aside', reflect: true },
119
+ };
120
+
121
+ constructor() {
122
+ super();
123
+ this.asideWidth = '240px';
124
+ this.asideMin = '120px';
125
+ this.asideMax = '50vw';
126
+ }
127
+
128
+ firstUpdated() {
129
+ this._applyAside();
130
+ this._initDrag();
131
+ }
132
+
133
+ updated() {
134
+ if (!this._dragging) this._applyAside();
135
+ }
136
+
137
+ _parseWidth(val) {
138
+ if (val == null) return null;
139
+ if (typeof val === 'number') return val + 'px';
140
+ if (/^\d+(\.\d+)?$/.test(val)) return val + 'px';
141
+ return val;
142
+ }
143
+
144
+ _toPx(val) {
145
+ const w = this._parseWidth(val);
146
+ if (!w) return 0;
147
+ if (w.endsWith('vw')) return (parseFloat(w) / 100) * window.innerWidth;
148
+ if (w.endsWith('em') || w.endsWith('rem')) return parseFloat(w) * 16;
149
+ return parseFloat(w) || 0;
150
+ }
151
+
152
+ _applyAside() {
153
+ const w = this._parseWidth(this.asideWidth) || '240px';
154
+ const min = this._parseWidth(this.asideMin) || '120px';
155
+ const max = this._parseWidth(this.asideMax) || '50vw';
156
+ this.style.setProperty('--aside-w', w);
157
+ this.style.setProperty('--aside-min', min);
158
+ this.style.setProperty('--aside-max', max);
159
+ }
160
+
161
+ _initDrag() {
162
+ const handle = this.renderRoot.querySelector('.resize-handle');
163
+ if (!handle) return;
164
+
165
+ handle.addEventListener('mousedown', (e) => {
166
+ e.preventDefault();
167
+ const aside = this.renderRoot.querySelector('.slot-aside');
168
+ if (!aside) return;
169
+
170
+ const min = this._toPx(this.asideMin) || 120;
171
+ const max = this._toPx(this.asideMax) || 9999;
172
+ const startW = aside.offsetWidth || 240;
173
+ const startX = e.clientX;
174
+
175
+ this._dragging = true;
176
+ handle.classList.add('dragging');
177
+
178
+ const onMove = (ev) => {
179
+ const w = Math.min(max, Math.max(min, startW + ev.clientX - startX));
180
+ aside.style.width = w + 'px';
181
+ };
182
+
183
+ const onUp = () => {
184
+ this._dragging = false;
185
+ handle.classList.remove('dragging');
186
+ document.removeEventListener('mousemove', onMove);
187
+ document.removeEventListener('mouseup', onUp);
188
+ this.asideWidth = aside.offsetWidth + 'px';
189
+ this.style.setProperty('--aside-w', this.asideWidth);
190
+ this.dispatchEvent(
191
+ new CustomEvent('aside-resize', {
192
+ detail: { width: this.asideWidth },
193
+ bubbles: true,
194
+ composed: true,
195
+ })
196
+ );
197
+ };
198
+
199
+ document.addEventListener('mousemove', onMove);
200
+ document.addEventListener('mouseup', onUp);
201
+ });
202
+ }
203
+
204
+ render() {
205
+ return html`
206
+ <div class="root">
207
+ <div class="slot-aside">
208
+ <slot name="aside"></slot>
209
+ <div class="resize-handle"></div>
210
+ </div>
211
+ <div class="slot-right">
212
+ <div class="slot-header"><slot name="header"></slot></div>
213
+ <div class="slot-main"><slot name="main"></slot></div>
214
+ <div class="slot-footer"><slot name="footer"></slot></div>
215
+ </div>
216
+ </div>
217
+ `;
218
+ }
219
+ }
220
+
221
+ registerComponent('lux-layout', LuxLayout);
222
+ export default LuxLayout;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * lux-menu — Data-driven navigation menu.
3
+ *
4
+ * Props:
5
+ * items — array of menu items
6
+ * collapse-width — auto-collapse when host width < this value (px)
7
+ * collapsed — force collapsed state (boolean)
8
+ * active-key — currently active item key
9
+ *
10
+ * Events:
11
+ * select — fired when item clicked, detail: { key, item }
12
+ */
13
+
14
+ import { html, css, LuxElement, registerComponent, classMap } from '../../index.js';
15
+ import '../lux-icon/index.js';
16
+
17
+ const styles = css`
18
+ :host {
19
+ display: block;
20
+ width: 100%;
21
+ }
22
+
23
+ .menu {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 2px;
27
+ }
28
+
29
+ .item {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 10px;
33
+ padding: 6px 12px;
34
+ border-radius: 6px;
35
+ cursor: pointer;
36
+ color: rgb(var(--lux-text-secondary));
37
+ font-size: 14px;
38
+ white-space: nowrap;
39
+ user-select: none;
40
+ min-height: 36px;
41
+ transition: all 150ms ease;
42
+ text-decoration: none;
43
+ }
44
+ .item:hover {
45
+ background: rgb(var(--lux-hover));
46
+ color: rgb(var(--lux-text));
47
+ }
48
+ .item.active {
49
+ background: rgb(var(--lux-primary-400) / 10%);
50
+ color: rgb(var(--lux-primary-400));
51
+ font-weight: 500;
52
+ }
53
+ .item.disabled {
54
+ opacity: 0.4;
55
+ pointer-events: none;
56
+ }
57
+
58
+ .item-icon {
59
+ flex-shrink: 0;
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 22px;
64
+ height: 22px;
65
+ }
66
+ .item-label {
67
+ flex: 1;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ }
71
+ .item-arrow {
72
+ flex-shrink: 0;
73
+ display: inline-flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ width: 24px;
77
+ height: 24px;
78
+ transition: transform 150ms ease;
79
+ font-size: 22px;
80
+ }
81
+ .item-arrow.open {
82
+ transform: rotate(90deg);
83
+ }
84
+
85
+ .divider {
86
+ height: 1px;
87
+ background: rgb(var(--lux-border));
88
+ margin: 4px 12px;
89
+ }
90
+ .children {
91
+ padding-left: 16px;
92
+ }
93
+
94
+ :host([collapsed]) .menu {
95
+ width: 48px;
96
+ align-items: center;
97
+ }
98
+ :host([collapsed]) .item {
99
+ justify-content: center;
100
+ padding: 8px;
101
+ width: 36px;
102
+ height: 36px;
103
+ min-height: auto;
104
+ position: relative;
105
+ }
106
+ :host([collapsed]) .item-label,
107
+ :host([collapsed]) .item-arrow {
108
+ display: none;
109
+ }
110
+ :host([collapsed]) .children {
111
+ display: none;
112
+ }
113
+ :host([collapsed]) .divider {
114
+ width: 24px;
115
+ margin: 4px auto;
116
+ }
117
+
118
+ :host([collapsed]) .item::after {
119
+ content: attr(data-label);
120
+ display: none;
121
+ position: absolute;
122
+ left: 100%;
123
+ top: 50%;
124
+ transform: translateY(-50%);
125
+ margin-left: 8px;
126
+ padding: 4px 8px;
127
+ background: rgb(var(--lux-card));
128
+ border: 1px solid rgb(var(--lux-border));
129
+ border-radius: 6px;
130
+ box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
131
+ font-size: 12px;
132
+ color: rgb(var(--lux-text));
133
+ white-space: nowrap;
134
+ z-index: 1001;
135
+ pointer-events: none;
136
+ }
137
+ :host([collapsed]) .item:hover::after {
138
+ display: block;
139
+ }
140
+
141
+ :host([collapsed]) .popup-wrap {
142
+ position: relative;
143
+ display: inline-flex;
144
+ }
145
+ :host([collapsed]) .children-popup {
146
+ display: none;
147
+ position: absolute;
148
+ left: 100%;
149
+ top: 0;
150
+ min-width: 180px;
151
+ background: rgb(var(--lux-card));
152
+ border: 1px solid rgb(var(--lux-border));
153
+ border-radius: 6px;
154
+ box-shadow: 0 4px 12px rgb(0 0 0 / 20%);
155
+ padding: 4px;
156
+ z-index: 1000;
157
+ }
158
+ :host([collapsed]) .popup-wrap:hover > .children-popup {
159
+ display: block;
160
+ }
161
+ :host([collapsed]) .children-popup .item {
162
+ width: auto;
163
+ min-height: auto;
164
+ justify-content: flex-start;
165
+ padding: 6px 12px;
166
+ }
167
+ :host([collapsed]) .children-popup .item-label {
168
+ display: block;
169
+ }
170
+ :host([collapsed]) .children-popup .children {
171
+ display: block;
172
+ padding-left: 12px;
173
+ }
174
+ :host([collapsed]) .children-popup .divider {
175
+ margin: 4px 8px;
176
+ }
177
+ `;
178
+
179
+ class LuxMenu extends LuxElement {
180
+ static styles = styles;
181
+
182
+ static properties = {
183
+ items: { type: Array, attribute: false },
184
+ collapseWidth: { type: Number, attribute: 'collapse-width', reflect: true },
185
+ collapsed: { type: Boolean, reflect: true },
186
+ activeKey: { type: String, attribute: 'active-key', reflect: true },
187
+ };
188
+
189
+ constructor() {
190
+ super();
191
+ this._expandedKeys = new Set();
192
+ this._resizeObserver = null;
193
+ }
194
+
195
+ connectedCallback() {
196
+ super.connectedCallback();
197
+
198
+ // Sync items from multiple sources:
199
+ // 1. .items property binding (may have been set before upgrade via cloneNode)
200
+ // 2. data-items attribute (JSON string)
201
+ // 3. LuxElement property system (after _initializeProperties)
202
+ this._syncItems();
203
+
204
+ this._resizeObserver = new ResizeObserver(() => {
205
+ const cw = parseInt(this.getAttribute('collapse-width'));
206
+ if (cw) this.collapsed = this.offsetWidth < cw;
207
+ });
208
+ this._resizeObserver.observe(this);
209
+ }
210
+
211
+ disconnectedCallback() {
212
+ this._resizeObserver?.disconnect();
213
+ }
214
+
215
+ _syncItems() {
216
+ // Priority: this.items (property binding) > data-items attribute > empty
217
+ const items = this.items;
218
+ if (Array.isArray(items) && items.length > 0) {
219
+ this.requestUpdate();
220
+ return;
221
+ }
222
+ // Fallback: read data-items attribute
223
+ const dataAttr = this.getAttribute('data-items');
224
+ if (dataAttr) {
225
+ try {
226
+ this.items = JSON.parse(dataAttr);
227
+ } catch {}
228
+ }
229
+ }
230
+
231
+ attributeChangedCallback(name, oldValue, newValue) {
232
+ if (name === 'data-items' && newValue !== oldValue) {
233
+ try {
234
+ this.items = JSON.parse(newValue);
235
+ } catch {}
236
+ }
237
+ }
238
+
239
+ _toggleExpand(key) {
240
+ if (this._expandedKeys.has(key)) this._expandedKeys.delete(key);
241
+ else this._expandedKeys.add(key);
242
+ this.requestUpdate();
243
+ }
244
+
245
+ _onItemClick(item) {
246
+ if (item.disabled) return;
247
+ if (item.children && item.children.length) this._toggleExpand(item.key);
248
+ this.activeKey = item.key;
249
+ this.setAttribute('active-key', item.key);
250
+ this.requestUpdate();
251
+ this.emit('select', { key: item.key, item });
252
+ }
253
+
254
+ _renderItems(items) {
255
+ if (!items || !items.length) return '';
256
+ const activeKey = this.activeKey || this.getAttribute('active-key') || '';
257
+ return items.map((item) => {
258
+ if (item.divider) return html`<div class="divider"></div>`;
259
+ const has = item.children && item.children.length > 0;
260
+ const isAct = item.key === activeKey;
261
+ const isExp = this._expandedKeys.has(item.key);
262
+ return html`
263
+ <div
264
+ class=${classMap({ item: true, active: isAct, disabled: item.disabled })}
265
+ data-label="${item.label || ''}"
266
+ @click=${() => this._onItemClick(item)}
267
+ >
268
+ ${item.icon ? html`<span class="item-icon"><lux-icon .name=${item.icon} size="20px"></lux-icon></span>` : ''}
269
+ <span class="item-label">${item.label || ''}</span>
270
+ ${has ? html`<span class=${classMap({ 'item-arrow': true, open: isExp })}>›</span>` : ''}
271
+ </div>
272
+ ${has && isExp ? html`<div class="children">${this._renderItems(item.children)}</div>` : ''}
273
+ `;
274
+ });
275
+ }
276
+
277
+ render() {
278
+ return html`<div class="menu">${this._renderItems(this.items || [])}</div>`;
279
+ }
280
+ }
281
+
282
+ registerComponent('lux-menu', LuxMenu);
283
+ export default LuxMenu;