vanilla-jui 1.3.11 → 1.4.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 (106) hide show
  1. package/dist/index.d.ts +2921 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.umd.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +4 -3
  6. package/src/components/accordion.ts +522 -0
  7. package/src/components/drop.ts +497 -0
  8. package/src/components/flow.ts +1579 -0
  9. package/src/components/form.ts +953 -0
  10. package/src/components/{icons.js → icons.ts} +24 -11
  11. package/src/components/menu.ts +615 -0
  12. package/src/components/{modal.js → modal.ts} +560 -237
  13. package/src/components/offcanvas.ts +457 -0
  14. package/src/components/{pagination.js → pagination.ts} +251 -76
  15. package/src/components/parabola.ts +335 -0
  16. package/src/components/{sticky.js → sticky.ts} +123 -98
  17. package/src/components/{swiper.js → swiper.ts} +510 -232
  18. package/src/components/tabs.ts +984 -0
  19. package/src/components/theme.ts +437 -0
  20. package/src/components/toast.ts +371 -0
  21. package/src/components/toc.ts +385 -0
  22. package/src/components/tooltip.ts +179 -0
  23. package/src/components/validator.ts +596 -0
  24. package/src/core/{Component.js → Component.ts} +134 -27
  25. package/src/css/accordion.css +0 -45
  26. package/src/css/animation.css +31 -1
  27. package/src/css/avatar.css +7 -33
  28. package/src/css/badge.css +11 -59
  29. package/src/css/breadcrumb.css +9 -32
  30. package/src/css/button.css +23 -224
  31. package/src/css/card.css +1 -1
  32. package/src/css/color.css +863 -1013
  33. package/src/css/color_p3.css +822 -975
  34. package/src/css/components.css +1 -0
  35. package/src/css/drop.css +7 -7
  36. package/src/css/flow.css +1 -1
  37. package/src/css/form.css +1 -1
  38. package/src/css/icon.css +7 -12
  39. package/src/css/index.css +5 -2
  40. package/src/css/loading.css +36 -20
  41. package/src/css/menu.css +1 -1
  42. package/src/css/modal.css +15 -44
  43. package/src/css/offcanvas.css +14 -40
  44. package/src/css/popup.css +4 -4
  45. package/src/css/status.css +0 -2
  46. package/src/css/swiper.css +35 -50
  47. package/src/css/tabs.css +9 -31
  48. package/src/css/tag.css +16 -77
  49. package/src/css/themes/default/accordion.css +62 -0
  50. package/src/css/themes/default/avatar.css +39 -0
  51. package/src/css/themes/default/badge.css +63 -0
  52. package/src/css/themes/default/breadcrumb.css +28 -0
  53. package/src/css/themes/default/button.css +204 -0
  54. package/src/css/themes/default/drop.css +11 -0
  55. package/src/css/themes/default/index.css +16 -0
  56. package/src/css/themes/default/loading.css +23 -0
  57. package/src/css/themes/default/modal.css +40 -0
  58. package/src/css/themes/default/offcanvas.css +13 -0
  59. package/src/css/themes/default/popup.css +7 -0
  60. package/src/css/themes/default/swiper.css +35 -0
  61. package/src/css/themes/default/tabs.css +37 -0
  62. package/src/css/themes/default/tag.css +85 -0
  63. package/src/css/themes/default/tip.css +49 -0
  64. package/src/css/themes/default/toast.css +70 -0
  65. package/src/css/themes/default/tooltip.css +14 -0
  66. package/src/css/themes/index.css +1 -0
  67. package/src/css/tip.css +20 -39
  68. package/src/css/toast.css +18 -80
  69. package/src/css/tokens.css +8 -3
  70. package/src/css/tooltip.css +10 -10
  71. package/src/index.ts +37 -0
  72. package/src/locales/{index.js → index.ts} +1 -2
  73. package/src/types.d.ts +1 -0
  74. package/src/utilities/browser.ts +131 -0
  75. package/src/utilities/cache.ts +57 -0
  76. package/src/utilities/core.ts +614 -0
  77. package/src/utilities/dom.ts +468 -0
  78. package/src/utilities/events.ts +106 -0
  79. package/src/utilities/http.ts +35 -0
  80. package/src/utilities/storage.ts +46 -0
  81. package/src/components/accordion.js +0 -373
  82. package/src/components/components.js +0 -0
  83. package/src/components/drop.js +0 -437
  84. package/src/components/flow.js +0 -1133
  85. package/src/components/form.js +0 -585
  86. package/src/components/menu.js +0 -422
  87. package/src/components/offcanvas.js +0 -332
  88. package/src/components/parabola.js +0 -295
  89. package/src/components/tabs.js +0 -746
  90. package/src/components/theme.js +0 -353
  91. package/src/components/toast.js +0 -329
  92. package/src/components/toc.js +0 -226
  93. package/src/components/tooltip.js +0 -86
  94. package/src/components/validator.js +0 -586
  95. package/src/index.js +0 -45
  96. package/src/locales/index.min.js +0 -1
  97. package/src/utilities/browser.js +0 -111
  98. package/src/utilities/cache.js +0 -159
  99. package/src/utilities/core.js +0 -502
  100. package/src/utilities/debug.js +0 -103
  101. package/src/utilities/dom.js +0 -313
  102. package/src/utilities/events.js +0 -69
  103. package/src/utilities/http.js +0 -30
  104. package/src/utilities/legacy-dom.js +0 -179
  105. package/src/utilities/storage.js +0 -46
  106. /package/src/icons/{index.js → index.ts} +0 -0
@@ -0,0 +1,437 @@
1
+ import {
2
+ bindAttr,
3
+ bindClass,
4
+ createDeepStore,
5
+ createRoot,
6
+ flushSync,
7
+ jsx,
8
+ } from 'vanilla-signal';
9
+ import { t } from 'vanilla-signal-i18n';
10
+
11
+ import locales from '../locales/index.ts';
12
+ import { all } from '../utilities/dom.ts';
13
+ import { createEventManager, type IEventManager } from '../utilities/events.ts';
14
+ import { getCookie, setCookie } from '../utilities/storage.ts';
15
+
16
+ export type ThemeConfigKey = 'mode' | 'theme' | 'radius' | 'shadow' | 'font';
17
+
18
+ export interface ThemeClassNames {
19
+ panel: string;
20
+ closeWrap: string;
21
+ closeButton: string;
22
+ title: string;
23
+ description: string;
24
+ container: string;
25
+ item: string;
26
+ itemTitle: string;
27
+ items: string;
28
+ button: string;
29
+ active: string;
30
+ prefix: string;
31
+ swatch: string;
32
+ buttonText: string;
33
+ }
34
+
35
+ export type ThemeClassNameConfig = Partial<ThemeClassNames>;
36
+
37
+ export interface ThemeOptions extends Record<string, unknown> {
38
+ mode?: string;
39
+ theme?: string;
40
+ radius?: string;
41
+ shadow?: string;
42
+ font?: string;
43
+ key?: string;
44
+ className?: ThemeClassNameConfig;
45
+ }
46
+
47
+ export interface ThemePanelGroup {
48
+ title: string;
49
+ type: ThemeConfigKey;
50
+ buttons: Array<[string, string]>;
51
+ }
52
+
53
+ interface ResolvedThemeOptions {
54
+ mode: string;
55
+ theme: string;
56
+ radius: string;
57
+ shadow: string;
58
+ font: string;
59
+ key: string;
60
+ className: ThemeClassNames;
61
+ }
62
+
63
+ interface ThemeCleanup {
64
+ bindings: Set<() => void>;
65
+ events: IEventManager;
66
+ }
67
+
68
+ const DEFAULT_CLASS_NAMES: ThemeClassNames = {
69
+ panel: 'j-theme-palette',
70
+ closeWrap: 'theme-palette-close',
71
+ closeButton: 'j-button is-text',
72
+ title: 'theme-palette-title',
73
+ description: 'theme-palette-description',
74
+ container: 'palette-container',
75
+ item: 'palette-item',
76
+ itemTitle: 'item-title',
77
+ items: 'items',
78
+ button: 'j-button is-default',
79
+ active: 'is-active',
80
+ prefix: 'el-prefix',
81
+ swatch: 'item-hex',
82
+ buttonText: 'button-text',
83
+ };
84
+
85
+ const DEFAULT_OPTIONS: ResolvedThemeOptions = {
86
+ mode: 'dark',
87
+ theme: 'indigo',
88
+ radius: 'sm',
89
+ shadow: 'sm',
90
+ font: 'sm',
91
+ key: 'ui-theme',
92
+ className: DEFAULT_CLASS_NAMES,
93
+ };
94
+
95
+ const THEME_CLASS_PREFIX: Record<Exclude<ThemeConfigKey, 'mode'>, string> = {
96
+ theme: 'j-theme-',
97
+ radius: 'j-radius-',
98
+ shadow: 'j-shadow-',
99
+ font: 'j-font-',
100
+ };
101
+
102
+ function mergeClassNames(className?: ThemeClassNameConfig): ThemeClassNames {
103
+ return { ...DEFAULT_CLASS_NAMES, ...className };
104
+ }
105
+
106
+ function normalizeOptions(options: ThemeOptions = {}): ResolvedThemeOptions {
107
+ return {
108
+ ...DEFAULT_OPTIONS,
109
+ ...options,
110
+ className: mergeClassNames(options.className),
111
+ };
112
+ }
113
+
114
+ function isThemeConfigKey(value: string | undefined): value is ThemeConfigKey {
115
+ return (
116
+ value === 'mode' ||
117
+ value === 'theme' ||
118
+ value === 'radius' ||
119
+ value === 'shadow' ||
120
+ value === 'font'
121
+ );
122
+ }
123
+
124
+ function joinClasses(
125
+ ...classes: Array<string | null | undefined | false>
126
+ ): string {
127
+ return classes.filter(Boolean).join(' ');
128
+ }
129
+
130
+ /**
131
+ * 主题管理组件。
132
+ *
133
+ * 负责主题配置的实例化、主题面板交互和 Cookie 读写。实例初始化不修改 html
134
+ * 类名,仅在面板点击交互时同步当前点击项对应的 html class。
135
+ */
136
+ export class Theme {
137
+ props: ResolvedThemeOptions;
138
+ languages: typeof locales;
139
+ cleanup: ThemeCleanup | null;
140
+ runtime: { destroyed: boolean };
141
+
142
+ constructor(options: ThemeOptions = {}) {
143
+ this.props = createDeepStore(normalizeOptions(options));
144
+ this.languages = locales;
145
+ this.cleanup = null;
146
+ this.runtime = { destroyed: false };
147
+
148
+ this.init();
149
+ }
150
+
151
+ private init(): void {
152
+ this.cleanup = { bindings: new Set(), events: createEventManager() };
153
+ this.loadConfig();
154
+ this.bindEvent();
155
+ }
156
+
157
+ private translate(key: string): string {
158
+ return t(key, this.languages);
159
+ }
160
+
161
+ private loadConfig(): void {
162
+ try {
163
+ const result = getCookie(this.props.key);
164
+ if (!result?.trim()) return;
165
+ const parsed = JSON.parse(result) as Partial<ThemeOptions>;
166
+ Object.assign(this.props, parsed, {
167
+ className: mergeClassNames({
168
+ ...this.props.className,
169
+ ...parsed.className,
170
+ }),
171
+ });
172
+ } catch {}
173
+ }
174
+
175
+ private saveConfig(): void {
176
+ const { mode, theme, radius, shadow, font } = this.props;
177
+ const render = mode === 'auto' ? this.scheme() : mode;
178
+ setCookie(
179
+ this.props.key,
180
+ JSON.stringify({ mode, theme, radius, shadow, font, render })
181
+ );
182
+ }
183
+
184
+ private scheme(): string {
185
+ return window.matchMedia?.('(prefers-color-scheme: dark)').matches
186
+ ? 'dark'
187
+ : 'light';
188
+ }
189
+
190
+ private bindActiveButtons(root: HTMLElement): void {
191
+ const dispose = createRoot((dispose) => {
192
+ const items = all<HTMLElement>('[data-theme-group]', root);
193
+ for (const item of items) {
194
+ const type = item.dataset.themeGroup;
195
+ if (!isThemeConfigKey(type)) continue;
196
+
197
+ const buttons = all<HTMLButtonElement>('[data-theme-button]', item);
198
+ for (const button of buttons) {
199
+ const isActive = () =>
200
+ button.dataset.themeValue === String(this.props[type]);
201
+ bindClass(button, this.props.className.active, isActive);
202
+ bindAttr(button, 'aria-selected', () => (isActive() ? 'true' : null));
203
+ }
204
+ }
205
+
206
+ return dispose;
207
+ });
208
+ this.cleanup?.bindings.add(dispose);
209
+ }
210
+
211
+ private bindEvent(): void {
212
+ this.cleanup?.events.on('palette', document.body, 'click', (event) => {
213
+ if (!(event.target instanceof Element)) return;
214
+
215
+ const button = event.target.closest<HTMLButtonElement>(
216
+ '[data-theme-button]'
217
+ );
218
+ if (!button) return;
219
+
220
+ const groupEl = button.closest<HTMLElement>('[data-theme-group]');
221
+ const type = groupEl?.dataset.themeGroup;
222
+ const value = button.dataset.themeValue;
223
+ if (!groupEl || !isThemeConfigKey(type) || !value) return;
224
+ if (button.classList.contains(this.props.className.active)) return;
225
+
226
+ const previous = this.props[type];
227
+ flushSync(() => {
228
+ this.props[type] = value;
229
+ });
230
+
231
+ const root = document.documentElement;
232
+ if (type === 'mode') {
233
+ const actual = value === 'auto' ? this.scheme() : value;
234
+ root.classList.remove('light', 'dark', previous);
235
+ root.classList.add(actual);
236
+ } else {
237
+ const prefix = THEME_CLASS_PREFIX[type];
238
+ const toRemove = Array.from(root.classList).filter((className) =>
239
+ className.startsWith(prefix)
240
+ );
241
+ if (toRemove.length) root.classList.remove(...toRemove);
242
+ root.classList.add(`${prefix}${value}`);
243
+ }
244
+
245
+ this.saveConfig();
246
+ });
247
+ }
248
+
249
+ private unbindEvent(): void {
250
+ this.cleanup?.events.clear();
251
+ }
252
+
253
+ createPanel(
254
+ containerClass: string | null = null,
255
+ panelConfig: ThemePanelGroup[] | null = null
256
+ ): HTMLElement {
257
+ const className = this.props.className;
258
+ const groups = panelConfig || this.defaultPanelConfig();
259
+ const panel = jsx('div', {
260
+ className: containerClass || className.panel,
261
+ 'data-theme-palette': 'root',
262
+ children: [
263
+ jsx('div', {
264
+ className: className.closeWrap,
265
+ 'data-theme-close-wrap': '',
266
+ style: {
267
+ display: 'flex',
268
+ justifyContent: 'center',
269
+ },
270
+ children: jsx('button', {
271
+ className: className.closeButton,
272
+ 'data-action': 'close',
273
+ children: this.translate('b'),
274
+ }),
275
+ }),
276
+ jsx('h3', {
277
+ className: className.title,
278
+ 'data-theme-title': '',
279
+ children: this.translate('t'),
280
+ }),
281
+ jsx('p', {
282
+ className: className.description,
283
+ 'data-theme-description': '',
284
+ style: {
285
+ marginTop: 0,
286
+ fontSize: 'var(--text-sm, 0.875rem)',
287
+ },
288
+ children: this.translate('d'),
289
+ }),
290
+ jsx('div', {
291
+ className: className.container,
292
+ 'data-theme-container': '',
293
+ children: groups.map((group) =>
294
+ jsx('div', {
295
+ className: className.item,
296
+ 'data-palette': group.type,
297
+ 'data-theme-group': group.type,
298
+ children: [
299
+ jsx('div', {
300
+ className: className.itemTitle,
301
+ 'data-theme-group-title': group.type,
302
+ children: group.title,
303
+ }),
304
+ jsx('div', {
305
+ className: className.items,
306
+ 'data-theme-items': group.type,
307
+ children: group.buttons.map(([value, label]) => {
308
+ const isActive =
309
+ String(this.props[group.type]) === String(value);
310
+ return jsx('button', {
311
+ className: joinClasses(
312
+ className.button,
313
+ isActive && className.active
314
+ ),
315
+ 'data-palette': value,
316
+ 'data-theme-button': group.type,
317
+ 'data-theme-value': value,
318
+ 'aria-selected': isActive ? 'true' : null,
319
+ children: [
320
+ group.type === 'theme'
321
+ ? jsx('span', {
322
+ className: joinClasses(
323
+ className.prefix,
324
+ className.swatch
325
+ ),
326
+ 'data-theme-swatch': value,
327
+ style: {
328
+ backgroundColor: `var(--ui-${value})`,
329
+ },
330
+ })
331
+ : null,
332
+ jsx('span', {
333
+ className: className.buttonText,
334
+ 'data-theme-button-text': value,
335
+ children: label,
336
+ }),
337
+ ],
338
+ });
339
+ }),
340
+ }),
341
+ ],
342
+ })
343
+ ),
344
+ }),
345
+ ],
346
+ }) as HTMLElement;
347
+
348
+ this.bindActiveButtons(panel);
349
+ return panel;
350
+ }
351
+
352
+ setConfig(newConfig: ThemeOptions): void {
353
+ flushSync(() => {
354
+ Object.assign(this.props, newConfig, {
355
+ className: mergeClassNames({
356
+ ...this.props.className,
357
+ ...newConfig.className,
358
+ }),
359
+ });
360
+ });
361
+ this.saveConfig();
362
+ }
363
+
364
+ destroy(): void {
365
+ if (this.runtime.destroyed) return;
366
+ this.runtime.destroyed = true;
367
+ this.unbindEvent();
368
+ this.cleanup?.bindings.forEach((dispose) => dispose());
369
+ this.cleanup?.bindings.clear();
370
+ this.cleanup = null;
371
+ }
372
+
373
+ private defaultPanelConfig(): ThemePanelGroup[] {
374
+ return [
375
+ {
376
+ title: this.translate('Theme'),
377
+ type: 'theme',
378
+ buttons: [
379
+ ['gray', this.translate('Gray')],
380
+ ['olive', this.translate('Olive')],
381
+ ['tomato', this.translate('Tomato')],
382
+ ['ruby', this.translate('Ruby')],
383
+ ['pink', this.translate('Pink')],
384
+ ['violet', this.translate('Violet')],
385
+ ['indigo', this.translate('Indigo')],
386
+ ['blue', this.translate('Blue')],
387
+ ['teal', this.translate('Teal')],
388
+ ['grass', this.translate('Grass')],
389
+ ['mint', this.translate('Mint')],
390
+ ['lime', this.translate('Lime')],
391
+ ['yellow', this.translate('Yellow')],
392
+ ['orange', this.translate('Orange')],
393
+ ['gold', this.translate('Gold')],
394
+ ],
395
+ },
396
+ {
397
+ title: this.translate('Radius'),
398
+ type: 'radius',
399
+ buttons: [
400
+ ['none', this.translate('None')],
401
+ ['sm', this.translate('sm')],
402
+ ['md', this.translate('md')],
403
+ ['lg', this.translate('lg')],
404
+ ['xl', this.translate('XL')],
405
+ ['round', this.translate('Round')],
406
+ ],
407
+ },
408
+ {
409
+ title: this.translate('Shadow'),
410
+ type: 'shadow',
411
+ buttons: [
412
+ ['none', this.translate('None')],
413
+ ['sm', this.translate('sm')],
414
+ ['md', this.translate('md')],
415
+ ['lg', this.translate('lg')],
416
+ ],
417
+ },
418
+ {
419
+ title: this.translate('Font'),
420
+ type: 'font',
421
+ buttons: [
422
+ ['sm', this.translate('sm')],
423
+ ['md', this.translate('md')],
424
+ ],
425
+ },
426
+ {
427
+ title: this.translate('Mode'),
428
+ type: 'mode',
429
+ buttons: [
430
+ ['light', this.translate('Light')],
431
+ ['dark', this.translate('Dark')],
432
+ ['auto', this.translate('Auto')],
433
+ ],
434
+ },
435
+ ];
436
+ }
437
+ }