vanilla-jui 1.3.10 → 1.4.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.
Files changed (90) hide show
  1. package/dist/index.d.ts +2927 -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 +496 -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 +2 -2
  26. package/src/css/animation.css +31 -1
  27. package/src/css/breadcrumb.css +1 -1
  28. package/src/css/button.css +5 -5
  29. package/src/css/card.css +7 -7
  30. package/src/css/color.css +865 -1028
  31. package/src/css/color_p3.css +871 -1036
  32. package/src/css/components.css +1 -0
  33. package/src/css/drop.css +1 -1
  34. package/src/css/flow.css +1 -1
  35. package/src/css/form.css +21 -21
  36. package/src/css/gradient.css +17 -4
  37. package/src/css/icon.css +17 -1
  38. package/src/css/index.css +3 -3
  39. package/src/css/layout.css +1 -1
  40. package/src/css/menu.css +27 -27
  41. package/src/css/modal.css +10 -27
  42. package/src/css/offcanvas.css +2 -2
  43. package/src/css/pagination.css +1 -1
  44. package/src/css/popup.css +2 -2
  45. package/src/css/swiper.css +12 -12
  46. package/src/css/tabs.css +3 -2
  47. package/src/css/tag.css +3 -4
  48. package/src/css/theme-palette.css +6 -6
  49. package/src/css/tip.css +1 -1
  50. package/src/css/toast.css +8 -8
  51. package/src/css/tokens.css +197 -226
  52. package/src/css/toolbar.css +3 -3
  53. package/src/css/tooltip.css +1 -1
  54. package/src/index.ts +37 -0
  55. package/src/locales/{index.js → index.ts} +1 -2
  56. package/src/types.d.ts +1 -0
  57. package/src/utilities/browser.ts +131 -0
  58. package/src/utilities/cache.ts +57 -0
  59. package/src/utilities/core.ts +614 -0
  60. package/src/utilities/dom.ts +468 -0
  61. package/src/utilities/events.ts +106 -0
  62. package/src/utilities/http.ts +35 -0
  63. package/src/utilities/storage.ts +46 -0
  64. package/src/components/accordion.js +0 -373
  65. package/src/components/drop.js +0 -437
  66. package/src/components/flow.js +0 -1133
  67. package/src/components/form.js +0 -585
  68. package/src/components/menu.js +0 -422
  69. package/src/components/offcanvas.js +0 -332
  70. package/src/components/parabola.js +0 -295
  71. package/src/components/tabs.js +0 -746
  72. package/src/components/theme.js +0 -353
  73. package/src/components/toast.js +0 -329
  74. package/src/components/toc.js +0 -226
  75. package/src/components/tooltip.js +0 -86
  76. package/src/components/validator.js +0 -586
  77. package/src/css/toast.min.css +0 -1
  78. package/src/index.js +0 -45
  79. package/src/locales/index.min.js +0 -1
  80. package/src/utilities/browser.js +0 -111
  81. package/src/utilities/cache.js +0 -159
  82. package/src/utilities/core.js +0 -502
  83. package/src/utilities/debug.js +0 -103
  84. package/src/utilities/dom.js +0 -313
  85. package/src/utilities/events.js +0 -69
  86. package/src/utilities/http.js +0 -30
  87. package/src/utilities/legacy-dom.js +0 -179
  88. package/src/utilities/storage.js +0 -46
  89. /package/src/{components/components.js → css/themes/index.css} +0 -0
  90. /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
+ }