1inui 1.0.0

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.
@@ -0,0 +1,646 @@
1
+ /**
2
+ * 1in UI - JavaScript 组件库
3
+ * Version: 1.1.0
4
+ */
5
+
6
+
7
+ 'use strict';
8
+
9
+ const UI = {
10
+ version: '1.1.0',
11
+
12
+ // ========================================
13
+ // Theme / 主题切换
14
+ // ========================================
15
+ theme: {
16
+ _key: 'ui-theme',
17
+
18
+ init() {
19
+ const saved = localStorage.getItem(this._key);
20
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
21
+
22
+ if (saved === 'dark' || (!saved && prefersDark)) {
23
+ document.documentElement.classList.add('dark');
24
+ }
25
+
26
+ // 监听系统主题变化
27
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
28
+ if (!localStorage.getItem(this._key)) {
29
+ document.documentElement.classList.toggle('dark', e.matches);
30
+ }
31
+ });
32
+ },
33
+
34
+ toggle() {
35
+ const isDark = document.documentElement.classList.toggle('dark');
36
+ localStorage.setItem(this._key, isDark ? 'dark' : 'light');
37
+ return isDark;
38
+ },
39
+
40
+ set(theme) {
41
+ if (theme === 'dark') {
42
+ document.documentElement.classList.add('dark');
43
+ } else if (theme === 'light') {
44
+ document.documentElement.classList.remove('dark');
45
+ } else {
46
+ localStorage.removeItem(this._key);
47
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
48
+ document.documentElement.classList.toggle('dark', prefersDark);
49
+ return;
50
+ }
51
+ localStorage.setItem(this._key, theme);
52
+ },
53
+
54
+ get() {
55
+ return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
56
+ }
57
+ },
58
+
59
+ // ========================================
60
+ // Modal / 模态框
61
+ // 完全匹配实际页面样式
62
+ // ========================================
63
+ modal: {
64
+ _stack: [],
65
+
66
+ open(options = {}) {
67
+ const {
68
+ title = '',
69
+ content = '',
70
+ html = null,
71
+ size = 'default',
72
+ closable = true,
73
+ showClose = false,
74
+ onClose = null,
75
+ footer = null
76
+ } = options;
77
+
78
+ const overlay = document.createElement('div');
79
+ overlay.className = 'ui-modal-overlay';
80
+ overlay.setAttribute('data-ui-modal', '');
81
+
82
+ const modal = document.createElement('div');
83
+ modal.className = 'ui-modal';
84
+ if (size === 'lg') modal.classList.add('ui-modal-lg');
85
+ if (size === 'xl') modal.classList.add('ui-modal-xl');
86
+
87
+ // 关闭按钮(右上角 X)
88
+ const closeBtn = showClose ? `
89
+ <button class="ui-modal-close" data-close>
90
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
91
+ </button>
92
+ ` : '';
93
+
94
+ // 标题
95
+ const titleHtml = title ? `<h3 class="ui-modal-title">${title}</h3>` : '';
96
+
97
+ // 内容
98
+ const contentHtml = html || (content ? `<p class="ui-modal-desc">${content}</p>` : '');
99
+
100
+ // 底部按钮
101
+ const footerHtml = footer ? `<div class="ui-modal-actions">${footer}</div>` : '';
102
+
103
+ modal.innerHTML = `
104
+ ${closeBtn}
105
+ ${titleHtml}
106
+ ${contentHtml}
107
+ ${footerHtml}
108
+ `;
109
+
110
+ overlay.appendChild(modal);
111
+ document.body.appendChild(overlay);
112
+ document.body.style.overflow = 'hidden';
113
+
114
+ const instance = { overlay, modal, onClose };
115
+ this._stack.push(instance);
116
+
117
+ // 关闭事件
118
+ if (closable) {
119
+ overlay.addEventListener('click', (e) => {
120
+ if (e.target === overlay) this.close(instance);
121
+ });
122
+ modal.querySelector('[data-close]')?.addEventListener('click', () => this.close(instance));
123
+ }
124
+
125
+ // ESC 关闭
126
+ const escHandler = (e) => {
127
+ if (e.key === 'Escape' && closable) {
128
+ this.close(instance);
129
+ document.removeEventListener('keydown', escHandler);
130
+ }
131
+ };
132
+ document.addEventListener('keydown', escHandler);
133
+ instance.escHandler = escHandler;
134
+
135
+ return instance;
136
+ },
137
+
138
+ close(instance) {
139
+ if (!instance) {
140
+ instance = this._stack.pop();
141
+ } else {
142
+ const idx = this._stack.indexOf(instance);
143
+ if (idx > -1) this._stack.splice(idx, 1);
144
+ }
145
+
146
+ if (!instance) return;
147
+
148
+ instance.overlay.style.opacity = '0';
149
+ setTimeout(() => {
150
+ instance.overlay.remove();
151
+ if (this._stack.length === 0) {
152
+ document.body.style.overflow = '';
153
+ }
154
+ if (instance.onClose) instance.onClose();
155
+ }, 200);
156
+
157
+ if (instance.escHandler) {
158
+ document.removeEventListener('keydown', instance.escHandler);
159
+ }
160
+ },
161
+
162
+ closeAll() {
163
+ while (this._stack.length) {
164
+ this.close();
165
+ }
166
+ },
167
+
168
+ // 确认对话框 - 完全匹配实际页面样式
169
+ confirm(options = {}) {
170
+ return new Promise((resolve) => {
171
+ const {
172
+ title = '确认',
173
+ content = '确定要执行此操作吗?',
174
+ confirmText = '确定',
175
+ cancelText = '取消',
176
+ danger = false,
177
+ icon = null
178
+ } = options;
179
+
180
+ // 图标 HTML
181
+ let iconHtml = '';
182
+ if (icon || danger) {
183
+ const iconType = danger ? 'danger' : (icon || 'warning');
184
+ const iconSvg = {
185
+ danger: '<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>',
186
+ warning: '<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
187
+ success: '<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
188
+ };
189
+ iconHtml = `<div class="ui-modal-icon ${iconType}">${iconSvg[iconType] || iconSvg.warning}</div>`;
190
+ }
191
+
192
+ const instance = this.open({
193
+ html: `
194
+ ${iconHtml}
195
+ <h3 class="ui-modal-title">${title}</h3>
196
+ <p class="ui-modal-desc">${content}</p>
197
+ `,
198
+ footer: `
199
+ <button class="ui-modal-btn-cancel" data-action="cancel">${cancelText}</button>
200
+ <button class="ui-modal-btn-confirm ${danger ? 'danger' : ''}" data-action="confirm">${confirmText}</button>
201
+ `,
202
+ onClose: () => resolve(false)
203
+ });
204
+
205
+ instance.modal.querySelector('[data-action="cancel"]').addEventListener('click', () => {
206
+ this.close(instance);
207
+ resolve(false);
208
+ });
209
+
210
+ instance.modal.querySelector('[data-action="confirm"]').addEventListener('click', () => {
211
+ this.close(instance);
212
+ resolve(true);
213
+ });
214
+ });
215
+ }
216
+ },
217
+
218
+
219
+ // ========================================
220
+ // Toast / 轻提示 - Discord 风格
221
+ // ========================================
222
+ toast: {
223
+ _container: null,
224
+
225
+ _getContainer() {
226
+ if (!this._container) {
227
+ this._container = document.createElement('div');
228
+ this._container.style.cssText = `
229
+ position: fixed;
230
+ bottom: 1.5rem;
231
+ left: 50%;
232
+ transform: translateX(-50%);
233
+ z-index: 9999;
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 0.5rem;
237
+ align-items: center;
238
+ pointer-events: none;
239
+ `;
240
+ document.body.appendChild(this._container);
241
+ }
242
+ return this._container;
243
+ },
244
+
245
+ show(message, options = {}) {
246
+ const {
247
+ type = 'info',
248
+ duration = 3000
249
+ } = options;
250
+
251
+ const icons = {
252
+ success: '<svg width="20" height="20" fill="none" stroke="#3ba55c" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>',
253
+ danger: '<svg width="20" height="20" fill="none" stroke="#ed4245" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
254
+ warning: '<svg width="20" height="20" fill="none" stroke="#faa61a" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
255
+ info: '<svg width="20" height="20" fill="none" stroke="#5865f2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
256
+ };
257
+
258
+ const toast = document.createElement('div');
259
+ toast.style.cssText = `
260
+ background: #36393f;
261
+ color: #dcddde;
262
+ padding: 0.75rem 1rem;
263
+ border-radius: 0.5rem;
264
+ font-size: 0.875rem;
265
+ font-weight: 500;
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 0.625rem;
269
+ box-shadow: 0 8px 16px rgba(0,0,0,0.24);
270
+ pointer-events: auto;
271
+ transform: translateY(20px);
272
+ opacity: 0;
273
+ transition: all 0.2s ease;
274
+ max-width: 24rem;
275
+ `;
276
+
277
+ toast.innerHTML = `
278
+ <span style="flex-shrink:0;display:flex;">${icons[type] || icons.info}</span>
279
+ <span>${message}</span>
280
+ `;
281
+
282
+ this._getContainer().appendChild(toast);
283
+
284
+ // 动画进入
285
+ requestAnimationFrame(() => {
286
+ toast.style.transform = 'translateY(0)';
287
+ toast.style.opacity = '1';
288
+ });
289
+
290
+ const remove = () => {
291
+ toast.style.transform = 'translateY(20px)';
292
+ toast.style.opacity = '0';
293
+ setTimeout(() => toast.remove(), 200);
294
+ };
295
+
296
+ if (duration > 0) {
297
+ setTimeout(remove, duration);
298
+ }
299
+
300
+ return { remove };
301
+ },
302
+
303
+ success(message, options = {}) {
304
+ return this.show(message, { ...options, type: 'success' });
305
+ },
306
+
307
+ error(message, options = {}) {
308
+ return this.show(message, { ...options, type: 'danger' });
309
+ },
310
+
311
+ warning(message, options = {}) {
312
+ return this.show(message, { ...options, type: 'warning' });
313
+ },
314
+
315
+ info(message, options = {}) {
316
+ return this.show(message, { ...options, type: 'info' });
317
+ }
318
+ },
319
+
320
+ // ========================================
321
+ // Dropdown / 下拉菜单
322
+ // ========================================
323
+ dropdown: {
324
+ init() {
325
+ document.addEventListener('click', (e) => {
326
+ const trigger = e.target.closest('[data-dropdown]');
327
+
328
+ if (trigger) {
329
+ e.preventDefault();
330
+ const dropdown = trigger.closest('.ui-dropdown');
331
+ if (dropdown) {
332
+ dropdown.classList.toggle('open');
333
+ }
334
+ } else {
335
+ // 点击外部关闭所有下拉
336
+ document.querySelectorAll('.ui-dropdown.open').forEach(d => d.classList.remove('open'));
337
+ }
338
+ });
339
+ },
340
+
341
+ open(element) {
342
+ element.classList.add('open');
343
+ },
344
+
345
+ close(element) {
346
+ element.classList.remove('open');
347
+ },
348
+
349
+ toggle(element) {
350
+ element.classList.toggle('open');
351
+ }
352
+ },
353
+
354
+ // ========================================
355
+ // Sidebar / 侧边栏
356
+ // ========================================
357
+ sidebar: {
358
+ open(selector) {
359
+ const sidebar = document.querySelector(selector);
360
+ const overlay = document.querySelector(`${selector}-overlay`);
361
+
362
+ if (sidebar) {
363
+ sidebar.classList.add('open');
364
+ document.body.style.overflow = 'hidden';
365
+ }
366
+ if (overlay) {
367
+ overlay.style.display = 'block';
368
+ overlay.addEventListener('click', () => this.close(selector), { once: true });
369
+ }
370
+ },
371
+
372
+ close(selector) {
373
+ const sidebar = document.querySelector(selector);
374
+ const overlay = document.querySelector(`${selector}-overlay`);
375
+
376
+ if (sidebar) {
377
+ sidebar.classList.remove('open');
378
+ document.body.style.overflow = '';
379
+ }
380
+ if (overlay) {
381
+ overlay.style.display = 'none';
382
+ }
383
+ },
384
+
385
+ toggle(selector) {
386
+ const sidebar = document.querySelector(selector);
387
+ if (sidebar?.classList.contains('open')) {
388
+ this.close(selector);
389
+ } else {
390
+ this.open(selector);
391
+ }
392
+ }
393
+ },
394
+
395
+ // ========================================
396
+ // Accordion / 手风琴
397
+ // ========================================
398
+ accordion: {
399
+ init() {
400
+ document.addEventListener('click', (e) => {
401
+ const header = e.target.closest('.ui-accordion-header');
402
+ if (header) {
403
+ const item = header.closest('.ui-accordion-item');
404
+ if (item) {
405
+ item.classList.toggle('open');
406
+ }
407
+ }
408
+ });
409
+ },
410
+
411
+ open(item) {
412
+ if (typeof item === 'string') item = document.querySelector(item);
413
+ item?.classList.add('open');
414
+ },
415
+
416
+ close(item) {
417
+ if (typeof item === 'string') item = document.querySelector(item);
418
+ item?.classList.remove('open');
419
+ },
420
+
421
+ toggle(item) {
422
+ if (typeof item === 'string') item = document.querySelector(item);
423
+ item?.classList.toggle('open');
424
+ }
425
+ },
426
+
427
+ // ========================================
428
+ // Tabs / 标签页切换
429
+ // ========================================
430
+ tabs: {
431
+ init() {
432
+ document.addEventListener('click', (e) => {
433
+ const tab = e.target.closest('.ui-tab[data-tab]');
434
+ if (tab) {
435
+ const tabs = tab.closest('.ui-tabs');
436
+ const target = tab.dataset.tab;
437
+
438
+ // 切换 tab 激活状态
439
+ tabs.querySelectorAll('.ui-tab').forEach(t => t.classList.remove('active'));
440
+ tab.classList.add('active');
441
+
442
+ // 切换内容面板
443
+ const container = tabs.closest('[data-tabs-container]') || tabs.parentElement;
444
+ container.querySelectorAll('.ui-tab-panel').forEach(p => {
445
+ p.classList.toggle('active', p.dataset.panel === target);
446
+ p.style.display = p.dataset.panel === target ? '' : 'none';
447
+ });
448
+ }
449
+ });
450
+ }
451
+ },
452
+
453
+ // ========================================
454
+ // Loading / 加载状态
455
+ // ========================================
456
+ loading: {
457
+ _overlay: null,
458
+
459
+ show(options = {}) {
460
+ const { text = '加载中...', target = document.body } = options;
461
+
462
+ if (this._overlay) this.hide();
463
+
464
+ this._overlay = document.createElement('div');
465
+ this._overlay.style.cssText = `
466
+ position: ${target === document.body ? 'fixed' : 'absolute'};
467
+ inset: 0;
468
+ background: rgba(255, 255, 255, 0.8);
469
+ backdrop-filter: blur(4px);
470
+ display: flex;
471
+ flex-direction: column;
472
+ align-items: center;
473
+ justify-content: center;
474
+ gap: 1rem;
475
+ z-index: 9999;
476
+ `;
477
+
478
+ this._overlay.innerHTML = `
479
+ <div class="ui-spinner"></div>
480
+ ${text ? `<div style="font-weight:700;color:var(--ui-text-muted);">${text}</div>` : ''}
481
+ `;
482
+
483
+ if (target !== document.body) {
484
+ target.style.position = 'relative';
485
+ }
486
+ target.appendChild(this._overlay);
487
+ },
488
+
489
+ hide() {
490
+ if (this._overlay) {
491
+ this._overlay.remove();
492
+ this._overlay = null;
493
+ }
494
+ }
495
+ },
496
+
497
+ // ========================================
498
+ // Form Validation / 表单验证
499
+ // ========================================
500
+ validate: {
501
+ rules: {
502
+ required: (value) => value.trim() !== '' || '此字段为必填项',
503
+ email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || '请输入有效的邮箱地址',
504
+ minLength: (min) => (value) => value.length >= min || `最少需要 ${min} 个字符`,
505
+ maxLength: (max) => (value) => value.length <= max || `最多允许 ${max} 个字符`,
506
+ pattern: (regex, msg) => (value) => regex.test(value) || msg,
507
+ match: (selector, msg) => (value) => {
508
+ const target = document.querySelector(selector);
509
+ return target && value === target.value || msg || '两次输入不一致';
510
+ }
511
+ },
512
+
513
+ field(input, rules) {
514
+ const value = input.value;
515
+ const errors = [];
516
+
517
+ for (const rule of rules) {
518
+ const result = typeof rule === 'function' ? rule(value) : rule;
519
+ if (result !== true) {
520
+ errors.push(result);
521
+ }
522
+ }
523
+
524
+ // 显示/隐藏错误
525
+ let errorEl = input.parentElement.querySelector('.ui-field-error');
526
+ if (errors.length > 0) {
527
+ input.style.borderColor = 'var(--ui-danger)';
528
+ if (!errorEl) {
529
+ errorEl = document.createElement('div');
530
+ errorEl.className = 'ui-field-error';
531
+ errorEl.style.cssText = 'color:var(--ui-danger);font-size:0.75rem;font-weight:700;margin-top:0.25rem;';
532
+ input.parentElement.appendChild(errorEl);
533
+ }
534
+ errorEl.textContent = errors[0];
535
+ } else {
536
+ input.style.borderColor = '';
537
+ if (errorEl) errorEl.remove();
538
+ }
539
+
540
+ return errors.length === 0;
541
+ },
542
+
543
+ form(formElement, config) {
544
+ let isValid = true;
545
+
546
+ for (const [name, rules] of Object.entries(config)) {
547
+ const input = formElement.querySelector(`[name="${name}"]`);
548
+ if (input && !this.field(input, rules)) {
549
+ isValid = false;
550
+ }
551
+ }
552
+
553
+ return isValid;
554
+ }
555
+ },
556
+
557
+ // ========================================
558
+ // Utils / 工具函数
559
+ // ========================================
560
+ utils: {
561
+ // 防抖
562
+ debounce(fn, delay = 300) {
563
+ let timer;
564
+ return function(...args) {
565
+ clearTimeout(timer);
566
+ timer = setTimeout(() => fn.apply(this, args), delay);
567
+ };
568
+ },
569
+
570
+ // 节流
571
+ throttle(fn, limit = 300) {
572
+ let inThrottle;
573
+ return function(...args) {
574
+ if (!inThrottle) {
575
+ fn.apply(this, args);
576
+ inThrottle = true;
577
+ setTimeout(() => inThrottle = false, limit);
578
+ }
579
+ };
580
+ },
581
+
582
+ // 格式化日期
583
+ formatDate(date, format = 'YYYY-MM-DD HH:mm') {
584
+ const d = new Date(date);
585
+ const map = {
586
+ 'YYYY': d.getFullYear(),
587
+ 'MM': String(d.getMonth() + 1).padStart(2, '0'),
588
+ 'DD': String(d.getDate()).padStart(2, '0'),
589
+ 'HH': String(d.getHours()).padStart(2, '0'),
590
+ 'mm': String(d.getMinutes()).padStart(2, '0'),
591
+ 'ss': String(d.getSeconds()).padStart(2, '0')
592
+ };
593
+ return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);
594
+ },
595
+
596
+ // 复制到剪贴板
597
+ async copyToClipboard(text) {
598
+ try {
599
+ await navigator.clipboard.writeText(text);
600
+ return true;
601
+ } catch {
602
+ // 降级方案
603
+ const textarea = document.createElement('textarea');
604
+ textarea.value = text;
605
+ textarea.style.position = 'fixed';
606
+ textarea.style.opacity = '0';
607
+ document.body.appendChild(textarea);
608
+ textarea.select();
609
+ const success = document.execCommand('copy');
610
+ document.body.removeChild(textarea);
611
+ return success;
612
+ }
613
+ },
614
+
615
+ // 生成唯一ID
616
+ uniqueId(prefix = 'ui') {
617
+ return `${prefix}_${Math.random().toString(36).substr(2, 9)}`;
618
+ }
619
+ },
620
+
621
+ // ========================================
622
+ // Init / 初始化
623
+ // ========================================
624
+ init() {
625
+ this.theme.init();
626
+ this.dropdown.init();
627
+ this.accordion.init();
628
+ this.tabs.init();
629
+
630
+ // 自动绑定主题切换按钮
631
+ document.querySelectorAll('[data-theme-toggle]').forEach(btn => {
632
+ btn.addEventListener('click', () => this.theme.toggle());
633
+ });
634
+
635
+ console.log(`1in UI v${this.version} initialized`);
636
+ }
637
+ };
638
+
639
+ // 暴露到全局
640
+ export { UI };
641
+
642
+
643
+
644
+
645
+
646
+ export default UI;