vanilla-jui 1.3.11 → 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 (71) 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/animation.css +31 -1
  26. package/src/css/card.css +1 -1
  27. package/src/css/color.css +863 -1013
  28. package/src/css/color_p3.css +822 -975
  29. package/src/css/components.css +1 -0
  30. package/src/css/form.css +1 -1
  31. package/src/css/icon.css +17 -1
  32. package/src/css/index.css +2 -2
  33. package/src/css/menu.css +1 -1
  34. package/src/css/tabs.css +1 -0
  35. package/src/css/tokens.css +1 -1
  36. package/src/index.ts +37 -0
  37. package/src/locales/{index.js → index.ts} +1 -2
  38. package/src/types.d.ts +1 -0
  39. package/src/utilities/browser.ts +131 -0
  40. package/src/utilities/cache.ts +57 -0
  41. package/src/utilities/core.ts +614 -0
  42. package/src/utilities/dom.ts +468 -0
  43. package/src/utilities/events.ts +106 -0
  44. package/src/utilities/http.ts +35 -0
  45. package/src/utilities/storage.ts +46 -0
  46. package/src/components/accordion.js +0 -373
  47. package/src/components/drop.js +0 -437
  48. package/src/components/flow.js +0 -1133
  49. package/src/components/form.js +0 -585
  50. package/src/components/menu.js +0 -422
  51. package/src/components/offcanvas.js +0 -332
  52. package/src/components/parabola.js +0 -295
  53. package/src/components/tabs.js +0 -746
  54. package/src/components/theme.js +0 -353
  55. package/src/components/toast.js +0 -329
  56. package/src/components/toc.js +0 -226
  57. package/src/components/tooltip.js +0 -86
  58. package/src/components/validator.js +0 -586
  59. package/src/index.js +0 -45
  60. package/src/locales/index.min.js +0 -1
  61. package/src/utilities/browser.js +0 -111
  62. package/src/utilities/cache.js +0 -159
  63. package/src/utilities/core.js +0 -502
  64. package/src/utilities/debug.js +0 -103
  65. package/src/utilities/dom.js +0 -313
  66. package/src/utilities/events.js +0 -69
  67. package/src/utilities/http.js +0 -30
  68. package/src/utilities/legacy-dom.js +0 -179
  69. package/src/utilities/storage.js +0 -46
  70. /package/src/{components/components.js → css/themes/index.css} +0 -0
  71. /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
+ }