vanilla-jui 1.4.8 → 1.5.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 (53) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/content.css +18 -24
  24. package/src/css/drop.css +4 -1
  25. package/src/css/form.css +8 -0
  26. package/src/css/menu.css +10 -5
  27. package/src/css/modal.css +2 -1
  28. package/src/css/offcanvas.css +5 -17
  29. package/src/css/swiper.css +2 -1
  30. package/src/css/tabs.css +8 -17
  31. package/src/css/themes/default/accordion.css +0 -48
  32. package/src/css/themes/default/swiper.css +1 -1
  33. package/src/css/themes/default/tabs.css +4 -6
  34. package/src/css/toast.css +1 -17
  35. package/src/css/tooltip.css +2 -1
  36. package/src/index.ts +8 -3
  37. package/src/locales/index.ts +3 -0
  38. package/src/primitives/drop.ts +253 -262
  39. package/src/primitives/loading.ts +22 -4
  40. package/src/primitives/parabola.ts +123 -164
  41. package/src/primitives/sticky.ts +297 -0
  42. package/src/primitives/theme.ts +164 -222
  43. package/src/primitives/toast.ts +309 -284
  44. package/src/primitives/tooltip.ts +65 -66
  45. package/src/utilities/browser.ts +38 -0
  46. package/src/utilities/dom.ts +0 -54
  47. package/src/utilities/refs.ts +61 -0
  48. package/src/utilities/state.ts +73 -0
  49. package/src/utilities/timer.ts +11 -31
  50. package/src/validation/validator.ts +289 -389
  51. package/src/components/sticky.ts +0 -297
  52. package/src/core/Component.ts +0 -377
  53. package/src/utilities/scheduler.ts +0 -156
@@ -12,7 +12,7 @@ import { t } from 'vanilla-signal-i18n';
12
12
  import locales from '../locales/index.ts';
13
13
  import { joinClasses } from '../utilities/class-name.ts';
14
14
  import { all } from '../utilities/dom.ts';
15
- import { createEventManager, type IEventManager } from '../utilities/events.ts';
15
+ import { createEventManager } from '../utilities/events.ts';
16
16
  import { isPlainObject } from '../utilities/object.ts';
17
17
 
18
18
  export type ThemeConfigKey = 'mode' | 'theme' | 'radius' | 'shadow' | 'font';
@@ -59,11 +59,6 @@ export interface ThemeResolvedOptions {
59
59
  className: ThemeClassNames;
60
60
  }
61
61
 
62
- interface ThemeCleanup {
63
- bindings: Set<() => void>;
64
- events: IEventManager;
65
- }
66
-
67
62
  export interface ThemeInstance {
68
63
  props: ThemeResolvedOptions;
69
64
  createPanel(
@@ -135,12 +130,14 @@ function createThemeStorage(): Storage {
135
130
  return createStorage({
136
131
  driver: 'cookie',
137
132
  codec: 'json',
133
+ namespace: '',
134
+ keySeparator: '',
138
135
  driverOptions: {
139
136
  path: '/',
140
137
  sameSite: 'Lax',
141
138
  secure: typeof location !== 'undefined' && location.protocol === 'https:',
142
139
  },
143
- ttl: 864e5,
140
+ ttl: 864e5 * 7, // 7 days
144
141
  });
145
142
  }
146
143
 
@@ -159,151 +156,121 @@ function normalizeStoredConfig(value: unknown): Partial<ThemeOptions> | null {
159
156
  return Object.keys(config).length ? config : null;
160
157
  }
161
158
 
162
- /**
163
- * 主题管理组件。
164
- *
165
- * 负责主题配置的实例化、主题面板交互和 createStorage 持久化。实例初始化不修改
166
- * html 类名,仅在面板点击交互时同步当前点击项对应的 html class。
167
- */
168
- class Theme implements ThemeInstance {
169
- props: ThemeResolvedOptions;
170
- languages: typeof locales;
171
- cleanup: ThemeCleanup | null;
172
- runtime: { configVersion: number; destroyed: boolean };
173
- storage: Storage;
174
-
175
- constructor(options: ThemeOptions = {}) {
176
- this.props = createDeepStore(normalizeOptions(options));
177
- this.languages = locales;
178
- this.cleanup = null;
179
- this.runtime = { configVersion: 0, destroyed: false };
180
- this.storage = createThemeStorage();
181
-
182
- this.init();
183
- }
184
-
185
- private init(): void {
186
- this.cleanup = { bindings: new Set(), events: createEventManager() };
187
- this.loadConfig();
188
- this.bindEvent();
189
- }
190
-
191
- private translate(key: string): string {
192
- return t(key, this.languages);
193
- }
194
-
195
- private loadConfig(): void {
196
- const configVersion = this.runtime.configVersion;
197
- void this.storage
198
- .get<unknown>(this.props.key)
199
- .then((result) => {
200
- if (
201
- this.runtime.destroyed ||
202
- this.runtime.configVersion !== configVersion
203
- )
204
- return;
205
-
206
- const config = normalizeStoredConfig(result);
207
- if (!config) return;
208
-
209
- flushSync(() => {
210
- Object.assign(this.props, config);
211
- });
212
- })
213
- .catch(() => {});
214
- }
159
+ function defaultPanelConfig(
160
+ translate: (key: string) => string
161
+ ): ThemePanelGroup[] {
162
+ return [
163
+ {
164
+ title: translate('Primary'),
165
+ type: 'theme',
166
+ buttons: [
167
+ ['gray', translate('Gray')],
168
+ ['olive', translate('Olive')],
169
+ ['tomato', translate('Tomato')],
170
+ ['ruby', translate('Ruby')],
171
+ ['pink', translate('Pink')],
172
+ ['violet', translate('Violet')],
173
+ ['indigo', translate('Indigo')],
174
+ ['blue', translate('Blue')],
175
+ ['teal', translate('Teal')],
176
+ ['grass', translate('Grass')],
177
+ ['mint', translate('Mint')],
178
+ ['lime', translate('Lime')],
179
+ ['yellow', translate('Yellow')],
180
+ ['orange', translate('Orange')],
181
+ ['gold', translate('Gold')],
182
+ ],
183
+ },
184
+ {
185
+ title: translate('Radius'),
186
+ type: 'radius',
187
+ buttons: [
188
+ ['none', translate('None')],
189
+ ['sm', translate('sm')],
190
+ ['md', translate('md')],
191
+ ['lg', translate('lg')],
192
+ ['xl', translate('XL')],
193
+ ['round', translate('Round')],
194
+ ],
195
+ },
196
+ {
197
+ title: translate('Shadow'),
198
+ type: 'shadow',
199
+ buttons: [
200
+ ['none', translate('None')],
201
+ ['sm', translate('sm')],
202
+ ['md', translate('md')],
203
+ ['lg', translate('lg')],
204
+ ],
205
+ },
206
+ {
207
+ title: translate('Font'),
208
+ type: 'font',
209
+ buttons: [
210
+ ['sm', translate('sm')],
211
+ ['md', translate('md')],
212
+ ],
213
+ },
214
+ {
215
+ title: translate('Mode'),
216
+ type: 'mode',
217
+ buttons: [
218
+ ['light', translate('Light')],
219
+ ['dark', translate('Dark')],
220
+ ['auto', translate('Auto')],
221
+ ],
222
+ },
223
+ ];
224
+ }
215
225
 
216
- private saveConfig(): void {
217
- const { mode, theme, radius, shadow, font } = this.props;
218
- const render = mode === 'auto' ? this.scheme() : mode;
226
+ export function createTheme(options: ThemeOptions = {}): ThemeInstance {
227
+ const props = createDeepStore(normalizeOptions(options));
228
+ const runtime = { configVersion: 0, destroyed: false };
229
+ const storage = createThemeStorage();
230
+ const bindings = new Set<() => void>();
231
+ const events = createEventManager();
232
+ const translate = (key: string): string => t(key, locales);
233
+ const scheme = (): string =>
234
+ window.matchMedia?.('(prefers-color-scheme: dark)').matches
235
+ ? 'dark'
236
+ : 'light';
237
+ const saveConfig = (): void => {
238
+ const { mode, theme, radius, shadow, font } = props;
219
239
  const config: ThemeStoredConfig = {
220
240
  mode,
221
241
  theme,
222
242
  radius,
223
243
  shadow,
224
244
  font,
225
- render,
245
+ render: mode === 'auto' ? scheme() : mode,
226
246
  };
227
-
228
- void this.storage.set(this.props.key, config).catch(() => {});
229
- }
230
-
231
- private scheme(): string {
232
- return window.matchMedia?.('(prefers-color-scheme: dark)').matches
233
- ? 'dark'
234
- : 'light';
235
- }
236
-
237
- private bindActiveButtons(root: HTMLElement): void {
247
+ void storage.set(props.key, config).catch(() => {});
248
+ };
249
+ const bindActiveButtons = (root: HTMLElement): void => {
238
250
  const dispose = createRoot((dispose) => {
239
- const items = all<HTMLElement>('[data-theme-group]', root);
240
- for (const item of items) {
251
+ for (const item of all<HTMLElement>('[data-theme-group]', root)) {
241
252
  const type = item.dataset.themeGroup;
242
253
  if (!isThemeConfigKey(type)) continue;
243
-
244
- const buttons = all<HTMLButtonElement>('[data-theme-button]', item);
245
- for (const button of buttons) {
254
+ for (const button of all<HTMLButtonElement>(
255
+ '[data-theme-button]',
256
+ item
257
+ )) {
246
258
  const isActive = () =>
247
- button.dataset.themeValue === String(this.props[type]);
248
- bindClass(button, this.props.className.active, isActive);
259
+ button.dataset.themeValue === String(props[type]);
260
+ bindClass(button, props.className.active, isActive);
249
261
  bindAttr(button, 'aria-selected', () => (isActive() ? 'true' : null));
250
262
  }
251
263
  }
252
-
253
264
  return dispose;
254
265
  });
255
- this.cleanup?.bindings.add(dispose);
256
- }
257
-
258
- private bindEvent(): void {
259
- this.cleanup?.events.on('palette', document.body, 'click', (event) => {
260
- if (!(event.target instanceof Element)) return;
261
-
262
- const button = event.target.closest<HTMLButtonElement>(
263
- '[data-theme-button]'
264
- );
265
- if (!button) return;
266
-
267
- const groupEl = button.closest<HTMLElement>('[data-theme-group]');
268
- const type = groupEl?.dataset.themeGroup;
269
- const value = button.dataset.themeValue;
270
- if (!groupEl || !isThemeConfigKey(type) || !value) return;
271
- if (button.classList.contains(this.props.className.active)) return;
272
-
273
- const previous = this.props[type];
274
- this.runtime.configVersion += 1;
275
- flushSync(() => {
276
- this.props[type] = value;
277
- });
278
-
279
- const root = document.documentElement;
280
- if (type === 'mode') {
281
- const actual = value === 'auto' ? this.scheme() : value;
282
- root.classList.remove('light', 'dark', previous);
283
- root.classList.add(actual);
284
- } else {
285
- const prefix = THEME_CLASS_PREFIX[type];
286
- const toRemove = Array.from(root.classList).filter((className) =>
287
- className.startsWith(prefix)
288
- );
289
- if (toRemove.length) root.classList.remove(...toRemove);
290
- root.classList.add(`${prefix}${value}`);
291
- }
292
-
293
- this.saveConfig();
294
- });
295
- }
296
-
297
- private unbindEvent(): void {
298
- this.cleanup?.events.clear();
299
- }
300
-
301
- createPanel(
302
- containerClass: string | null = null,
303
- panelConfig: ThemePanelGroup[] | null = null
304
- ): HTMLElement {
305
- const className = this.props.className;
306
- const groups = panelConfig || this.defaultPanelConfig();
266
+ bindings.add(dispose);
267
+ };
268
+ const createPanel: ThemeInstance['createPanel'] = (
269
+ containerClass = null,
270
+ panelConfig = null
271
+ ) => {
272
+ const className = props.className;
273
+ const groups = panelConfig || defaultPanelConfig(translate);
307
274
  const panel = jsx('div', {
308
275
  className: containerClass || className.panel,
309
276
  'data-theme-palette': 'root',
@@ -314,7 +281,7 @@ class Theme implements ThemeInstance {
314
281
  style: {
315
282
  margin: '0 0 1rem',
316
283
  },
317
- children: this.translate('t'),
284
+ children: translate('t'),
318
285
  }),
319
286
  jsx('div', {
320
287
  className: className.container,
@@ -335,7 +302,7 @@ class Theme implements ThemeInstance {
335
302
  'data-theme-items': group.type,
336
303
  children: group.buttons.map(([value, label]) => {
337
304
  const isActive =
338
- String(this.props[group.type]) === String(value);
305
+ String(props[group.type]) === String(value);
339
306
  return jsx('button', {
340
307
  className: joinClasses(
341
308
  className.button,
@@ -374,99 +341,74 @@ class Theme implements ThemeInstance {
374
341
  ],
375
342
  }) as HTMLElement;
376
343
 
377
- this.bindActiveButtons(panel);
344
+ bindActiveButtons(panel);
378
345
  return panel;
379
- }
380
-
381
- setConfig(newConfig: ThemeOptions): void {
382
- this.runtime.configVersion += 1;
346
+ };
347
+ const setConfig = (newConfig: ThemeOptions): void => {
348
+ runtime.configVersion += 1;
383
349
  flushSync(() => {
384
- Object.assign(this.props, newConfig, {
350
+ Object.assign(props, newConfig, {
385
351
  className: mergeClassNames({
386
- ...this.props.className,
352
+ ...props.className,
387
353
  ...newConfig.className,
388
354
  }),
389
355
  });
390
356
  });
391
- this.saveConfig();
392
- }
357
+ saveConfig();
358
+ };
359
+ const destroy = (): void => {
360
+ if (runtime.destroyed) return;
361
+ runtime.destroyed = true;
362
+ events.clear();
363
+ for (const dispose of bindings) dispose();
364
+ bindings.clear();
365
+ void storage.close().catch(() => {});
366
+ };
393
367
 
394
- destroy(): void {
395
- if (this.runtime.destroyed) return;
396
- this.runtime.destroyed = true;
397
- this.unbindEvent();
398
- this.cleanup?.bindings.forEach((dispose) => dispose());
399
- this.cleanup?.bindings.clear();
400
- this.cleanup = null;
401
- void this.storage.close().catch(() => {});
402
- }
368
+ events.on('palette', document.body, 'click', (event) => {
369
+ if (!(event.target instanceof Element)) return;
370
+ const button = event.target.closest<HTMLButtonElement>(
371
+ '[data-theme-button]'
372
+ );
373
+ const group = button?.closest<HTMLElement>('[data-theme-group]');
374
+ const type = group?.dataset.themeGroup;
375
+ const value = button?.dataset.themeValue;
376
+ if (!button || !isThemeConfigKey(type) || !value) return;
377
+ if (button.classList.contains(props.className.active)) return;
378
+
379
+ const previous = props[type];
380
+ runtime.configVersion += 1;
381
+ flushSync(() => {
382
+ props[type] = value;
383
+ });
403
384
 
404
- private defaultPanelConfig(): ThemePanelGroup[] {
405
- return [
406
- {
407
- title: this.translate('Primary'),
408
- type: 'theme',
409
- buttons: [
410
- ['gray', this.translate('Gray')],
411
- ['olive', this.translate('Olive')],
412
- ['tomato', this.translate('Tomato')],
413
- ['ruby', this.translate('Ruby')],
414
- ['pink', this.translate('Pink')],
415
- ['violet', this.translate('Violet')],
416
- ['indigo', this.translate('Indigo')],
417
- ['blue', this.translate('Blue')],
418
- ['teal', this.translate('Teal')],
419
- ['grass', this.translate('Grass')],
420
- ['mint', this.translate('Mint')],
421
- ['lime', this.translate('Lime')],
422
- ['yellow', this.translate('Yellow')],
423
- ['orange', this.translate('Orange')],
424
- ['gold', this.translate('Gold')],
425
- ],
426
- },
427
- {
428
- title: this.translate('Radius'),
429
- type: 'radius',
430
- buttons: [
431
- ['none', this.translate('None')],
432
- ['sm', this.translate('sm')],
433
- ['md', this.translate('md')],
434
- ['lg', this.translate('lg')],
435
- ['xl', this.translate('XL')],
436
- ['round', this.translate('Round')],
437
- ],
438
- },
439
- {
440
- title: this.translate('Shadow'),
441
- type: 'shadow',
442
- buttons: [
443
- ['none', this.translate('None')],
444
- ['sm', this.translate('sm')],
445
- ['md', this.translate('md')],
446
- ['lg', this.translate('lg')],
447
- ],
448
- },
449
- {
450
- title: this.translate('Font'),
451
- type: 'font',
452
- buttons: [
453
- ['sm', this.translate('sm')],
454
- ['md', this.translate('md')],
455
- ],
456
- },
457
- {
458
- title: this.translate('Mode'),
459
- type: 'mode',
460
- buttons: [
461
- ['light', this.translate('Light')],
462
- ['dark', this.translate('Dark')],
463
- ['auto', this.translate('Auto')],
464
- ],
465
- },
466
- ];
467
- }
468
- }
385
+ const root = document.documentElement;
386
+ if (type === 'mode') {
387
+ root.classList.remove('light', 'dark', previous);
388
+ root.classList.add(value === 'auto' ? scheme() : value);
389
+ } else {
390
+ const prefix = THEME_CLASS_PREFIX[type];
391
+ const obsolete = Array.from(root.classList).filter((className) =>
392
+ className.startsWith(prefix)
393
+ );
394
+ if (obsolete.length) root.classList.remove(...obsolete);
395
+ root.classList.add(`${prefix}${value}`);
396
+ }
397
+ saveConfig();
398
+ });
469
399
 
470
- export function createTheme(options: ThemeOptions = {}): ThemeInstance {
471
- return new Theme(options);
400
+ const configVersion = runtime.configVersion;
401
+ void storage
402
+ .get<unknown>(props.key)
403
+ .then((result) => {
404
+ if (runtime.destroyed || runtime.configVersion !== configVersion) return;
405
+ const config = normalizeStoredConfig(result);
406
+ if (!config) return;
407
+ flushSync(() => {
408
+ Object.assign(props, config);
409
+ });
410
+ })
411
+ .catch(() => {});
412
+
413
+ return { props, createPanel, setConfig, destroy };
472
414
  }