vanilla-jui 1.5.7 → 1.5.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jui",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "private": false,
5
5
  "description": "A lightweight, reactive, and fully customizable vanilla JS UI library.",
6
6
  "keywords": [
@@ -1,4 +1,13 @@
1
- import { For, createDeepStore, flushSync, jsx } from 'vanilla-signal';
1
+ import {
2
+ For,
3
+ type MaybeAccessor,
4
+ access,
5
+ createDeepStore,
6
+ createEffect,
7
+ createMemo,
8
+ flushSync,
9
+ jsx,
10
+ } from 'vanilla-signal';
2
11
  import { t } from 'vanilla-signal-i18n';
3
12
 
4
13
  import {
@@ -18,6 +27,7 @@ import {
18
27
 
19
28
  export type MenuType = string | undefined;
20
29
  export type MenuItemId = string | number;
30
+ export type MenuItemRenderType = 0 | 1 | 2;
21
31
 
22
32
  export interface MenuClassNames {
23
33
  root: string;
@@ -37,6 +47,7 @@ export type MenuClassNameConfig = Partial<MenuClassNames>;
37
47
  export interface MenuItem extends Record<string, unknown> {
38
48
  id?: MenuItemId;
39
49
  title: string | number;
50
+ type?: MenuItemRenderType;
40
51
  url?: string;
41
52
  target?: string;
42
53
  classes?: string | string[];
@@ -46,7 +57,8 @@ export interface MenuItem extends Record<string, unknown> {
46
57
  export interface MenuProps extends Record<string, unknown> {
47
58
  type?: MenuType;
48
59
  id?: string | null;
49
- data?: MenuItem[];
60
+ user?: MaybeAccessor<number>;
61
+ data?: MaybeAccessor<MenuItem[]>;
50
62
  backText?: string;
51
63
  className?: MenuClassNameConfig;
52
64
  }
@@ -54,12 +66,14 @@ export interface MenuProps extends Record<string, unknown> {
54
66
  interface ResolvedMenuProps extends Record<string, unknown> {
55
67
  type?: MenuType;
56
68
  id: string;
57
- data: MenuItem[];
69
+ user: MaybeAccessor<number>;
70
+ data: MaybeAccessor<MenuItem[]>;
58
71
  backText: string;
59
72
  className: MenuClassNames;
60
73
  }
61
74
 
62
75
  interface MenuState extends Record<string, unknown> {
76
+ user: number;
63
77
  data: MenuItem[];
64
78
  activeKeys: string[];
65
79
  }
@@ -80,7 +94,7 @@ const DEFAULT_CLASS_NAMES: MenuClassNames = {
80
94
  const MENU_ITEM_RULE = {
81
95
  type: 'object',
82
96
  validate: isMenuItem,
83
- message: 'expects { title, id?, url?, target?, classes?, children? }.',
97
+ message: 'expects { title, id?, type?, url?, target?, classes?, children? }.',
84
98
  };
85
99
 
86
100
  const MENU_DATA_RULE = {
@@ -90,6 +104,7 @@ const MENU_DATA_RULE = {
90
104
 
91
105
  const MENU_PROPS_SCHEMA = {
92
106
  type: { default: undefined, types: ['string', 'undefined'] },
107
+ user: { default: 0, types: ['number', 'function'] },
93
108
  id: {
94
109
  default: null,
95
110
  types: ['string', 'null'],
@@ -104,9 +119,7 @@ const MENU_PROPS_SCHEMA = {
104
119
  },
105
120
  data: {
106
121
  default: [],
107
- type: 'array',
108
- normalize: cloneMenuData,
109
- items: MENU_ITEM_RULE,
122
+ types: ['array', 'function'],
110
123
  },
111
124
  backText: { default: t('b', locales), type: 'string' },
112
125
  className: {
@@ -120,6 +133,7 @@ const MENU_PROPS_SCHEMA = {
120
133
  } satisfies ResolveSchema<MenuProps>;
121
134
 
122
135
  const MENU_STATE_SCHEMA = {
136
+ user: { type: 'number' },
123
137
  data: MENU_DATA_RULE,
124
138
  activeKeys: { type: 'array', items: 'string' },
125
139
  };
@@ -133,7 +147,8 @@ function normalizeProps(input: MenuProps = {}): ResolvedMenuProps {
133
147
 
134
148
  return {
135
149
  ...props,
136
- data: cloneMenuData(props.data),
150
+ user: props.user,
151
+ data: props.data,
137
152
  className: { ...props.className },
138
153
  };
139
154
  }
@@ -165,6 +180,10 @@ function isMenuItem(value: unknown): value is MenuItem {
165
180
  (item.id == null ||
166
181
  typeof item.id === 'string' ||
167
182
  typeof item.id === 'number') &&
183
+ (item.type == null ||
184
+ item.type === 0 ||
185
+ item.type === 1 ||
186
+ item.type === 2) &&
168
187
  (item.url == null || typeof item.url === 'string') &&
169
188
  (item.target == null || typeof item.target === 'string') &&
170
189
  (item.classes == null ||
@@ -181,6 +200,7 @@ function cloneMenuData(data: unknown): MenuItem[] {
181
200
  return data.map((item) => {
182
201
  validateParam('item', item, MENU_ITEM_RULE, 'Menu.data');
183
202
  const nextItem = { ...(item as MenuItem) };
203
+ nextItem.type = nextItem.type ?? 0;
184
204
  if (Array.isArray(nextItem.classes)) {
185
205
  nextItem.classes = nextItem.classes.slice();
186
206
  }
@@ -191,6 +211,20 @@ function cloneMenuData(data: unknown): MenuItem[] {
191
211
  });
192
212
  }
193
213
 
214
+ function resolveMenuUser(user: MaybeAccessor<number> | undefined): number {
215
+ const value = access(user ?? 0);
216
+ validateParam('user', value, 'number', 'Menu.props');
217
+ return value;
218
+ }
219
+
220
+ function resolveMenuData(
221
+ data: MaybeAccessor<MenuItem[]> | undefined
222
+ ): MenuItem[] {
223
+ const value = access(data ?? []);
224
+ validateParam('data', value, MENU_DATA_RULE, 'Menu.props');
225
+ return cloneMenuData(value);
226
+ }
227
+
194
228
  export type Menu = FunctionalComponent<
195
229
  ResolvedMenuProps,
196
230
  MenuState,
@@ -200,7 +234,8 @@ export type Menu = FunctionalComponent<
200
234
  export function createMenu(input: MenuProps = {}): Menu {
201
235
  const props = normalizeProps(input);
202
236
  const state = createDeepStore({
203
- data: cloneMenuData(props.data),
237
+ user: resolveMenuUser(props.user),
238
+ data: resolveMenuData(props.data),
204
239
  activeKeys: [],
205
240
  }) as MenuState;
206
241
  const generatedKeys = new WeakMap<object, string>();
@@ -213,6 +248,16 @@ export function createMenu(input: MenuProps = {}): Menu {
213
248
  return generatedKeys.get(item) as string;
214
249
  };
215
250
  const isActive = (key: string): boolean => state.activeKeys.includes(key);
251
+ const isLoggedIn = (): boolean => state.user > 0;
252
+ const shouldRenderItem = (item: MenuItem): boolean => {
253
+ if (item.type === 1) return isLoggedIn();
254
+ if (item.type === 2) return !isLoggedIn();
255
+ return true;
256
+ };
257
+ const visibleItems = (items: readonly MenuItem[] | undefined): MenuItem[] => {
258
+ if (!Array.isArray(items)) return [];
259
+ return items.filter(shouldRenderItem);
260
+ };
216
261
  const open = (key: string): void => {
217
262
  flushSync(() => {
218
263
  state.activeKeys =
@@ -282,12 +327,16 @@ export function createMenu(input: MenuProps = {}): Menu {
282
327
  key: (item: MenuItem) => itemKey(item),
283
328
  children: (itemAccessor: () => MenuItem) => {
284
329
  const key = itemKey(itemAccessor());
330
+ const childrenAccessor = createMemo(() =>
331
+ visibleItems(itemAccessor().children)
332
+ );
333
+ const hasChildren = (): boolean => childrenAccessor().length > 0;
285
334
  return jsx('li', {
286
335
  className: () => {
287
336
  const item = itemAccessor();
288
337
  return joinClasses(
289
338
  props.className.item,
290
- item.children?.length ? props.className.hasChildren : '',
339
+ hasChildren() ? props.className.hasChildren : '',
291
340
  ...normalizeItemClasses(item.classes),
292
341
  isActive(key) ? props.className.active : ''
293
342
  );
@@ -298,13 +347,12 @@ export function createMenu(input: MenuProps = {}): Menu {
298
347
  : itemId(itemAccessor().id),
299
348
  'data-menu-item': () =>
300
349
  itemAccessor().id == null ? '' : String(itemAccessor().id),
301
- 'data-menu-has-children': () =>
302
- itemAccessor().children?.length ? '' : null,
350
+ 'data-menu-has-children': () => (hasChildren() ? '' : null),
303
351
  onClick: (event: Event) => {
304
352
  if (
305
353
  props.type === 'mobile' &&
306
354
  event.target === event.currentTarget &&
307
- itemAccessor().children?.length
355
+ hasChildren()
308
356
  ) {
309
357
  open(key);
310
358
  }
@@ -312,9 +360,9 @@ export function createMenu(input: MenuProps = {}): Menu {
312
360
  children: [
313
361
  () => {
314
362
  const item = itemAccessor();
315
- const hasChildren = !!item.children?.length;
363
+ const itemHasChildren = hasChildren();
316
364
  const url = itemUrl(item.url);
317
- if (!hasChildren && !url) {
365
+ if (!itemHasChildren && !url) {
318
366
  return jsx('span', {
319
367
  className: props.className.link,
320
368
  'data-menu-link': '',
@@ -327,7 +375,7 @@ export function createMenu(input: MenuProps = {}): Menu {
327
375
  target: item.target,
328
376
  'data-menu-link': '',
329
377
  onClick: (event: Event) => {
330
- if (!hasChildren) return;
378
+ if (!itemHasChildren) return;
331
379
  if (props.type === 'mobile' || props.type === 'bottom') {
332
380
  event.preventDefault();
333
381
  if (props.type === 'bottom') toggle(key);
@@ -338,13 +386,8 @@ export function createMenu(input: MenuProps = {}): Menu {
338
386
  });
339
387
  },
340
388
  () => {
341
- const children = itemAccessor().children;
342
- if (!children?.length) return null;
343
- return listView(
344
- () => itemAccessor().children || [],
345
- false,
346
- key
347
- );
389
+ if (!hasChildren()) return null;
390
+ return listView(childrenAccessor, false, key);
348
391
  },
349
392
  ],
350
393
  });
@@ -359,6 +402,9 @@ export function createMenu(input: MenuProps = {}): Menu {
359
402
  state,
360
403
  normalizeStatePatch(patch) {
361
404
  const next = { ...patch };
405
+ if (Object.hasOwn(next, 'user')) {
406
+ next.user = resolveMenuUser(next.user as MaybeAccessor<number>);
407
+ }
362
408
  if (Object.hasOwn(next, 'data')) next.data = cloneMenuData(next.data);
363
409
  return next;
364
410
  },
@@ -377,12 +423,19 @@ export function createMenu(input: MenuProps = {}): Menu {
377
423
  }
378
424
  },
379
425
  view: () => {
380
- validateParam('data', state.data, MENU_DATA_RULE, 'Menu');
426
+ const renderedData = createMemo(() => visibleItems(state.data));
427
+ createEffect(() => {
428
+ state.user = resolveMenuUser(props.user);
429
+ state.data = resolveMenuData(props.data);
430
+ });
431
+ createEffect(() =>
432
+ validateParam('data', state.data, MENU_DATA_RULE, 'Menu')
433
+ );
381
434
  return jsx('nav', {
382
435
  className: props.className.root,
383
436
  'data-menu': 'root',
384
437
  'data-menu-type': props.type,
385
- children: listView(() => state.data, true),
438
+ children: listView(renderedData, true),
386
439
  }) as HTMLElement;
387
440
  },
388
441
  onBuild(context) {
@@ -360,6 +360,7 @@
360
360
  gap: 12px;
361
361
  padding: 0.5rem 1rem 0;
362
362
  font-size: 0.78rem;
363
+ color: var(--ui-fg-soft);
363
364
  }
364
365
 
365
366
  .j-code-editor .code-header .code-dots {
@@ -594,7 +595,7 @@
594
595
  }
595
596
 
596
597
  .j-content tbody tr:hover {
597
- background-color: var(--ui-surface-hover);
598
+ background-color: var(--content-bg-subtle);
598
599
  }
599
600
 
600
601
  .j-content tbody tr:last-child td {
@@ -47,18 +47,11 @@
47
47
  --toast-border-color: var(--tone-border);
48
48
  }
49
49
 
50
- .j-toast.is-action {
50
+ .j-toast.is-confirm {
51
51
  --toast-gap: calc(var(--space)*4);
52
52
  --toast-min-width: 280px;
53
53
  }
54
54
 
55
- .j-toast.is-action .toast-actions {
56
- width: 100%;
57
- display: flex;
58
- justify-content: flex-end;
59
- gap: var(--space);
60
- }
61
-
62
55
  .j-toast-lite {
63
56
  font-size: var(--font-size-md);
64
57
  color: var(--ui-bg);
package/src/css/toast.css CHANGED
@@ -37,6 +37,7 @@
37
37
  .j-toast .el-icon {
38
38
  margin-right: var(--s2);
39
39
  width: var(--toast-icon-size);
40
+ position: relative;
40
41
  color: inherit;
41
42
  }
42
43
 
@@ -58,9 +59,17 @@
58
59
  transform: translate(-50%, -50%);
59
60
  }
60
61
 
61
- .j-toast.is-action {
62
+ .j-toast.is-confirm {
62
63
  flex-direction: column;
63
64
  align-items: flex-start;
64
65
  cursor: default;
65
66
  }
67
+
68
+ .j-toast.is-confirm .toast-buttons {
69
+ width: 100%;
70
+ display: flex;
71
+ justify-content: flex-end;
72
+ gap: var(--space);
73
+ position: relative;
74
+ }
66
75
  }
@@ -45,5 +45,6 @@ export default {
45
45
  wn: '您的浏览器版本过低,存在安全风险。请升级浏览器后再访问本站。',
46
46
  Submit: '提交',
47
47
  Reset: '重置',
48
+ 'Loading...': '加载中...',
48
49
  },
49
50
  };
@@ -289,7 +289,6 @@ export function createTheme(options: ThemeOptions = {}): ThemeInstance {
289
289
  children: groups.map((group) =>
290
290
  jsx('div', {
291
291
  className: className.item,
292
- 'data-palette': group.type,
293
292
  'data-theme-group': group.type,
294
293
  children: [
295
294
  jsx('div', {
@@ -308,7 +307,7 @@ export function createTheme(options: ThemeOptions = {}): ThemeInstance {
308
307
  className.button,
309
308
  isActive && className.active
310
309
  ),
311
- 'data-palette': value,
310
+ 'data-action': 'customTheme',
312
311
  'data-theme-button': group.type,
313
312
  'data-theme-value': value,
314
313
  'aria-selected': isActive ? 'true' : null,