cabloy 5.1.247 → 5.1.249

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 (50) hide show
  1. package/.cabloy-version +1 -1
  2. package/.vscode/settings.json +1 -2
  3. package/CHANGELOG.md +27 -0
  4. package/package.json +1 -7
  5. package/repo-docs/frontend/column-configuration-guide.md +3 -1
  6. package/repo-docs/frontend/table-guide.md +27 -2
  7. package/repo-e2e/specs/a-commerce.spec.ts +32 -0
  8. package/repo-e2e/specs/cabloy-basic.spec.ts +1236 -0
  9. package/vona/package.original.json +1 -7
  10. package/vona/pnpm-lock.yaml +139 -1011
  11. package/zova/package.original.json +0 -6
  12. package/zova/packages-cli/cli/package.json +2 -2
  13. package/zova/packages-cli/cli-set-front/package.json +2 -2
  14. package/zova/packages-utils/zova-vite/package.json +1 -1
  15. package/zova/packages-utils/zova-vite/src/teleports.ts +7 -0
  16. package/zova/packages-utils/zova-vite/test/teleports.test.ts +114 -0
  17. package/zova/packages-zova/zova/package.json +2 -2
  18. package/zova/pnpm-lock.yaml +6 -958
  19. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +267 -0
  20. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.header.tsx +42 -10
  21. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.locale.tsx +10 -5
  22. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx +97 -11
  23. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.theme.tsx +19 -10
  24. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +11 -0
  25. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/style.ts +128 -0
  26. package/zova/src/suite/a-home/modules/home-layoutadmin/src/config/locale/en-us.ts +6 -0
  27. package/zova/src/suite/a-home/modules/home-layoutadmin/src/config/locale/zh-cn.ts +6 -0
  28. package/zova/src/suite/a-home/modules/home-layoutweb/src/.metadata/component/layoutWeb.ts +10 -8
  29. package/zova/src/suite/a-home/modules/home-layoutweb/src/.metadata/index.ts +25 -0
  30. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +83 -0
  31. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +37 -8
  32. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.locale.tsx +25 -8
  33. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.tabs.tsx +3 -1
  34. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.theme.tsx +10 -5
  35. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +64 -21
  36. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/style.ts +42 -0
  37. package/zova/src/suite/a-home/modules/home-layoutweb/src/config/locale/en-us.ts +4 -0
  38. package/zova/src/suite/a-home/modules/home-layoutweb/src/config/locale/zh-cn.ts +4 -0
  39. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/rowOperations/controller.tsx +2 -1
  40. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/controller.tsx +6 -10
  41. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +9 -6
  42. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  43. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +17 -1
  44. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrProfile.ts +15 -0
  45. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/ssr.ts +1 -0
  46. package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrProfile.test.ts +23 -1
  47. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
  48. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/src/service/ssrHandler.ts +11 -11
  49. package/zova/src/suite-vendor/a-zova/package.json +3 -3
  50. package/vona/.textlintrc.json +0 -19
@@ -1,5 +1,6 @@
1
1
  import type { ModelTabs, ModelTabsOptions } from 'zova-module-a-routertabs';
2
2
 
3
+ import { nextTick, useId } from 'vue';
3
4
  import { BeanControllerBase, Use, usePrepareArg } from 'zova';
4
5
  import { Controller } from 'zova-module-a-bean';
5
6
  import { $QueryEnsureLoaded } from 'zova-module-a-model';
@@ -40,14 +41,95 @@ export class ControllerLayoutAdmin extends BeanControllerBase {
40
41
  private viewportWidth: number = 0;
41
42
  private _windowResizeHandler?: () => void;
42
43
 
44
+ headerActionsOpen = false;
45
+ headerActionsId = 'home-layoutadmin-header-actions';
46
+ private _headerActionsElement?: HTMLDivElement;
47
+ private _headerOutsideHandler: (event: PointerEvent) => void;
48
+ private _headerClickHandler: (event: MouseEvent) => void;
49
+
50
+ tabItemsWidth: number | undefined = undefined;
51
+ tabItemsOverflowStart = false;
52
+ tabItemsOverflowEnd = false;
53
+ tabItemsMenuOpen = false;
54
+ tabItemsMenuId: string;
55
+ private _tabItemsElement?: HTMLDivElement;
56
+ private _tabItemsMenuElement?: HTMLDivElement;
57
+ private _tabItemsObserver?: ResizeObserver;
58
+ private _tabItemsMounted = false;
59
+ private _tabItemsMenuFrame?: number;
60
+ private _tabItemsOnScroll: () => void;
61
+ private _tabItemsOnToggle: (event: ToggleEvent) => void;
62
+ private _tabItemsOnViewport: (event: Event) => void;
63
+ private _tabItemsOnKeydown: (event: KeyboardEvent) => void;
64
+
65
+ get tabItemsCompact() {
66
+ return this.tabItemsWidth !== undefined && this.tabItemsWidth < 240;
67
+ }
68
+
43
69
  protected async __init__() {
70
+ this.tabItemsMenuId = this.$composable(useId);
71
+ this._tabItemsOnScroll = () => this.measureTabItems();
72
+ this._tabItemsOnToggle = event => this.onTabItemsMenuToggle(event);
73
+ this._tabItemsOnViewport = event => {
74
+ if (event.target instanceof Node && this._tabItemsMenuElement?.contains(event.target)) return;
75
+ this.closeTabItemsMenu();
76
+ };
77
+ this._tabItemsOnKeydown = event => {
78
+ if (event.key !== 'Escape' || event.defaultPrevented || !this.tabItemsMenuOpen) return;
79
+ const active = this._tabItemsMenuElement?.ownerDocument.activeElement;
80
+ const trigger = this._tabItemsElement?.querySelector('[data-testid="tab-items-more"]');
81
+ if (active?.closest('dialog[open], [role="dialog"]')) return;
82
+ if (
83
+ active !== document.body &&
84
+ active !== trigger &&
85
+ !this._tabItemsMenuElement?.contains(active ?? null)
86
+ ) {
87
+ return;
88
+ }
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ this.closeTabItemsMenu(true);
92
+ };
93
+ this.$controllerMounted(() => {
94
+ this.$ssr.handleDirectOrOnHydrated(() => {
95
+ if (this.ctx.disposed) return;
96
+ this._tabItemsMounted = true;
97
+ this.observeTabItems();
98
+ });
99
+ });
100
+ this.$watch(
101
+ () => [
102
+ this.$$modelTabs?.tabKeyCurrent,
103
+ this.$$modelTabs?.componentKeyCurrent,
104
+ this.$$modelTabs?.tabCurrent?.items?.map(item => [
105
+ item.componentKey,
106
+ item.pageMeta?.pageTitle,
107
+ item.pageMeta?.pageDirty,
108
+ item.pageMeta?.formMeta?.formScene,
109
+ ]),
110
+ this.tabItemsCompact,
111
+ ],
112
+ ([tabKey], [previousTabKey]) => {
113
+ if (tabKey !== previousTabKey) this.closeTabItemsMenu();
114
+ void nextTick(() => this.observeTabItems());
115
+ },
116
+ );
117
+ this._headerClickHandler = event => this.onHeaderActionsClick(event);
118
+ this._headerOutsideHandler = event => {
119
+ if (event.target instanceof Node && !this._headerActionsElement?.contains(event.target)) {
120
+ this.closeHeaderActions();
121
+ }
122
+ };
44
123
  // viewport
45
124
  if (process.env.CLIENT) {
46
125
  this._windowResizeHandler = () => {
47
126
  this.viewportWidth = document.documentElement.clientWidth;
127
+ this.closeHeaderActions();
128
+ this.closeTabItemsMenu();
48
129
  };
49
130
  this._windowResizeHandler();
50
131
  window.addEventListener('resize', this._windowResizeHandler);
132
+ document.addEventListener('pointerdown', this._headerOutsideHandler);
51
133
  }
52
134
  // belowBreakpoint
53
135
  this.belowBreakpoint = this.$computed(() => {
@@ -82,9 +164,194 @@ export class ControllerLayoutAdmin extends BeanControllerBase {
82
164
  await this._initTabs();
83
165
  }
84
166
 
167
+ setTabItemsElement(element?: HTMLDivElement) {
168
+ if (element === this._tabItemsElement) return;
169
+ this._tabItemsObserver?.disconnect();
170
+ this._tabItemsElement?.removeEventListener('scroll', this._tabItemsOnScroll, true);
171
+ this._tabItemsElement = element;
172
+ element?.addEventListener('scroll', this._tabItemsOnScroll, true);
173
+ this.observeTabItems();
174
+ }
175
+
176
+ private observeTabItems() {
177
+ const element = this._tabItemsElement;
178
+ if (!this._tabItemsMounted || this.ctx.disposed || !element) return;
179
+ this._tabItemsObserver?.disconnect();
180
+ this._tabItemsObserver = new ResizeObserver(() => {
181
+ this.measureTabItems();
182
+ this.revealTabItem();
183
+ });
184
+ this._tabItemsObserver.observe(element);
185
+ const content = element.querySelector('.tab-items-content');
186
+ if (content) this._tabItemsObserver.observe(content);
187
+ this.measureTabItems();
188
+ this.revealTabItem();
189
+ }
190
+
191
+ private measureTabItems() {
192
+ const element = this._tabItemsElement;
193
+ if (this.ctx.disposed || !element) return;
194
+ this.tabItemsWidth = element.clientWidth;
195
+ const scroller = element.querySelector('.tab-items-scroll');
196
+ const content = element.querySelector('.tab-items-content');
197
+ const bounds = scroller?.getBoundingClientRect();
198
+ const items = content?.getBoundingClientRect();
199
+ this.tabItemsOverflowStart = !!bounds && !!items && items.left < bounds.left - 1;
200
+ this.tabItemsOverflowEnd = !!bounds && !!items && items.right > bounds.right + 1;
201
+ }
202
+
203
+ private revealTabItem() {
204
+ const element = this._tabItemsElement;
205
+ const scroller = element?.querySelector('.tab-items-scroll');
206
+ const active = scroller?.querySelector('[aria-selected="true"]');
207
+ if (!scroller || !active || this.ctx.disposed) return;
208
+ const bounds = scroller.getBoundingClientRect();
209
+ const item = active.getBoundingClientRect();
210
+ const delta =
211
+ item.left < bounds.left ? item.left - bounds.left : Math.max(0, item.right - bounds.right);
212
+ if (delta) scroller.scrollBy({ left: delta, behavior: 'instant' });
213
+ this.measureTabItems();
214
+ }
215
+
216
+ setTabItemsMenuElement(element?: HTMLDivElement) {
217
+ if (element === this._tabItemsMenuElement) return;
218
+ this.closeTabItemsMenu();
219
+ this._tabItemsMenuElement?.removeEventListener('beforetoggle', this._tabItemsOnToggle);
220
+ this._tabItemsMenuElement = element;
221
+ element?.addEventListener('beforetoggle', this._tabItemsOnToggle);
222
+ }
223
+
224
+ closeTabItemsMenu(restoreFocus = false) {
225
+ const element = this._tabItemsMenuElement;
226
+ if (this.tabItemsMenuOpen && element?.isConnected) element.hidePopover();
227
+ this.clearTabItemsMenu();
228
+ if (restoreFocus) {
229
+ this._tabItemsElement
230
+ ?.querySelector<HTMLButtonElement>('[data-testid="tab-items-more"]')
231
+ ?.focus({ preventScroll: true });
232
+ }
233
+ }
234
+
235
+ private clearTabItemsMenu() {
236
+ this.tabItemsMenuOpen = false;
237
+ if (this._tabItemsMenuFrame !== undefined) {
238
+ window.cancelAnimationFrame(this._tabItemsMenuFrame);
239
+ this._tabItemsMenuFrame = undefined;
240
+ }
241
+ if (process.env.CLIENT) {
242
+ window.removeEventListener('scroll', this._tabItemsOnViewport, true);
243
+ document.removeEventListener('keydown', this._tabItemsOnKeydown);
244
+ }
245
+ this._tabItemsMenuElement?.style.removeProperty('visibility');
246
+ }
247
+
248
+ private onTabItemsMenuToggle(event: ToggleEvent) {
249
+ if (event.target !== this._tabItemsMenuElement) return;
250
+ if (event.newState !== 'open') {
251
+ this.clearTabItemsMenu();
252
+ return;
253
+ }
254
+ this.closeHeaderActions();
255
+ this.tabItemsMenuOpen = true;
256
+ const element = this._tabItemsMenuElement!;
257
+ element.style.visibility = 'hidden';
258
+ this._tabItemsMenuFrame = window.requestAnimationFrame(() => {
259
+ this._tabItemsMenuFrame = undefined;
260
+ if (!this.tabItemsMenuOpen) return;
261
+ this.positionTabItemsMenu();
262
+ element.style.removeProperty('visibility');
263
+ element.querySelector<HTMLButtonElement>('.tab-items-select')?.focus({ preventScroll: true });
264
+ window.addEventListener('scroll', this._tabItemsOnViewport, true);
265
+ });
266
+ document.addEventListener('keydown', this._tabItemsOnKeydown);
267
+ }
268
+
269
+ private positionTabItemsMenu() {
270
+ const element = this._tabItemsMenuElement;
271
+ const trigger = this._tabItemsElement?.querySelector('[data-testid="tab-items-more"]');
272
+ if (!element || !trigger) return;
273
+ const anchor = trigger.getBoundingClientRect();
274
+ const popup = element.getBoundingClientRect();
275
+ const viewport = element.ownerDocument.documentElement;
276
+ element.style.left = `${Math.max(8, Math.min(anchor.right - popup.width, viewport.clientWidth - popup.width - 8))}px`;
277
+ element.style.top = `${Math.max(8, Math.min(anchor.bottom + 4, viewport.clientHeight - popup.height - 8))}px`;
278
+ }
279
+
85
280
  protected __dispose__() {
281
+ this._tabItemsMounted = false;
282
+ this.setTabItemsElement();
283
+ this.setTabItemsMenuElement();
284
+ this.setHeaderActionsElement();
86
285
  if (this._windowResizeHandler) {
87
286
  window.removeEventListener('resize', this._windowResizeHandler);
287
+ document.removeEventListener('pointerdown', this._headerOutsideHandler);
288
+ }
289
+ }
290
+
291
+ setHeaderActionsElement(element?: HTMLDivElement) {
292
+ if (element === this._headerActionsElement) return;
293
+ this._headerActionsElement?.removeEventListener('click', this._headerClickHandler, true);
294
+ this._headerActionsElement = element;
295
+ element?.addEventListener('click', this._headerClickHandler, true);
296
+ }
297
+
298
+ toggleHeaderActions() {
299
+ if (this.headerActionsOpen) {
300
+ this.closeHeaderActions();
301
+ } else {
302
+ this.headerActionsOpen = true;
303
+ }
304
+ }
305
+
306
+ closeHeaderActions(restoreFocus = false) {
307
+ this.headerActionsOpen = false;
308
+ this._headerActionsElement
309
+ ?.querySelectorAll<HTMLDetailsElement>('details[open]')
310
+ .forEach(details => {
311
+ details.open = false;
312
+ });
313
+ if (restoreFocus) {
314
+ this._headerActionsElement?.querySelector<HTMLButtonElement>('button')?.focus();
315
+ }
316
+ }
317
+
318
+ onHeaderActionsFocusout(event: FocusEvent) {
319
+ if (
320
+ this.headerActionsOpen &&
321
+ (!(event.relatedTarget instanceof Node) ||
322
+ !this._headerActionsElement?.contains(event.relatedTarget))
323
+ ) {
324
+ this.closeHeaderActions();
325
+ }
326
+ }
327
+
328
+ onHeaderActionsClick(event: MouseEvent) {
329
+ if (!this.headerActionsOpen || !(event.target instanceof Element)) return;
330
+ const action = event.target.closest('a, button');
331
+ if (action?.closest('details') && !action.matches(':disabled, [aria-disabled="true"]')) {
332
+ this.closeHeaderActions(true);
333
+ }
334
+ }
335
+
336
+ onHeaderActionsKeydown(event: KeyboardEvent) {
337
+ if (
338
+ event.key !== 'Escape' ||
339
+ event.defaultPrevented ||
340
+ !(event.target instanceof Element) ||
341
+ event.target.closest('dialog[open], [role="dialog"]')
342
+ ) {
343
+ return;
344
+ }
345
+ const details = event.target.closest<HTMLDetailsElement>('details[open]');
346
+ if (details) {
347
+ event.preventDefault();
348
+ event.stopPropagation();
349
+ details.open = false;
350
+ details.querySelector<HTMLElement>('summary')?.focus();
351
+ } else if (this.headerActionsOpen) {
352
+ event.preventDefault();
353
+ event.stopPropagation();
354
+ this.closeHeaderActions(true);
88
355
  }
89
356
  }
90
357
 
@@ -1,5 +1,7 @@
1
+ import { classes } from 'typestyle';
1
2
  import { BeanRenderBase, Use } from 'zova';
2
3
  import { Render } from 'zova-module-a-bean';
4
+ import { ZIcon } from 'zova-module-a-icon';
3
5
 
4
6
  import type { RenderLayoutAdmin } from './render.tsx';
5
7
 
@@ -11,7 +13,7 @@ export class RenderHeader extends BeanRenderBase {
11
13
  public render() {
12
14
  return (
13
15
  <>
14
- <div class="navbar bg-base-300 w-full">
16
+ <div class={classes('navbar bg-base-300 w-full', this.cHeader)}>
15
17
  <div class="flex-none">
16
18
  <button
17
19
  class="btn btn-square btn-ghost"
@@ -34,15 +36,45 @@ export class RenderHeader extends BeanRenderBase {
34
36
  </svg>
35
37
  </button>
36
38
  </div>
37
- <div class="text-xl px-4">{this.sys.env.APP_TITLE}</div>
38
- <div class="mx-2 min-w-0 flex-2 px-2">{this.$$r.$$renderTabs.renderTabItems()}</div>
39
- <div class="hidden flex-none lg:block">
40
- <ul class="menu menu-horizontal">
41
- {this.$$r.$$renderLocale.render()}
42
- {this.$$r.$$renderTheme.renderThemeDark()}
43
- {this.$$r.$$renderTheme.renderThemeName()}
44
- {this.$$r.$$renderUser.render()}
45
- </ul>
39
+ <div class="header-title text-xl px-4">{this.sys.env.APP_TITLE}</div>
40
+ <div class="header-navigation mx-2 min-w-0 flex-2 px-2">
41
+ {this.$$r.$$renderTabs.renderTabItems()}
42
+ </div>
43
+ <div
44
+ ref={ref => {
45
+ if (!this.ctx.disposed)
46
+ this.setHeaderActionsElement?.((ref as HTMLDivElement | null) ?? undefined);
47
+ }}
48
+ class={classes('relative flex-none', this.cHeaderActions)}
49
+ data-testid="header-actions"
50
+ onFocusout={event => this.onHeaderActionsFocusout(event)}
51
+ onKeydown={event => this.onHeaderActionsKeydown(event)}
52
+ >
53
+ <button
54
+ type="button"
55
+ class="btn btn-square btn-ghost lg:hidden"
56
+ aria-label={this.scope.locale.MoreActions()}
57
+ aria-expanded={this.headerActionsOpen}
58
+ aria-controls={this.headerActionsId}
59
+ onClick={() => this.toggleHeaderActions()}
60
+ >
61
+ <ZIcon name="::more-horiz" width={24} aria-hidden="true" />
62
+ </button>
63
+ <div
64
+ id={this.headerActionsId}
65
+ class={classes(
66
+ 'header-actions-panel absolute right-0 top-full z-30 rounded-box bg-base-100 shadow-lg lg:static lg:bg-transparent lg:shadow-none',
67
+ this.headerActionsOpen ? 'block' : 'hidden',
68
+ 'lg:block',
69
+ )}
70
+ >
71
+ <ul class="menu w-full lg:w-auto lg:menu-horizontal">
72
+ {this.$$r.$$renderLocale.render()}
73
+ {this.$$r.$$renderTheme.renderThemeDark()}
74
+ {this.$$r.$$renderTheme.renderThemeName()}
75
+ {this.$$r.$$renderUser.render()}
76
+ </ul>
77
+ </div>
46
78
  </div>
47
79
  </div>
48
80
  <div class="navbar bg-base-300 w-full">{this.$$r.$$renderTabs.renderTabs()}</div>
@@ -1,6 +1,7 @@
1
1
  import { BeanRenderBase, ClientOnly } from 'zova';
2
2
  import { Render } from 'zova-module-a-bean';
3
3
  import { $icon } from 'zova-module-a-icon';
4
+ import { ZButton } from 'zova-module-basic-button';
4
5
  import { closeNearestDetails } from 'zova-module-home-base';
5
6
 
6
7
  @Render()
@@ -10,22 +11,26 @@ export class RenderLocale extends BeanRenderBase {
10
11
  return (
11
12
  <li>
12
13
  <details>
13
- <summary>{$icon('::language', 24)}</summary>
14
+ <summary aria-label={this.scope.locale.Language()} title={this.scope.locale.Language()}>
15
+ {$icon('::language', 24)}
16
+ <span class="lg:hidden">{this.scope.locale.Language()}</span>
17
+ </summary>
14
18
  <ClientOnly>
15
19
  <ul class="bg-base-100 rounded-t-none p-2 w-48">
16
20
  {Object.keys(locales).map(key => {
17
21
  const title = this.$scopeBase.locale[locales[key]]();
18
22
  return (
19
23
  <li key={key} class={this.app.meta.locale.current === key ? 'disabled' : ''}>
20
- <a
21
- onClick={event => {
24
+ <ZButton
25
+ disabled={this.app.meta.locale.current === key}
26
+ onPerform={event => {
22
27
  this.app.meta.locale.current = key as any;
23
- closeNearestDetails(event);
28
+ closeNearestDetails({ currentTarget: event.target });
24
29
  }}
25
30
  >
26
31
  {$icon(this.app.meta.locale.current === key ? '::done' : '::none', 24)}
27
32
  {title}
28
- </a>
33
+ </ZButton>
29
34
  </li>
30
35
  );
31
36
  })}
@@ -6,6 +6,7 @@ import { Render } from 'zova-module-a-bean';
6
6
  import { $iconName, IIconRecord, ZIcon } from 'zova-module-a-icon';
7
7
  import { IRouteViewRouteItem } from 'zova-module-a-router';
8
8
  import { RouteTab, ZRouterViewTabs } from 'zova-module-a-routertabs';
9
+ import { ZButton } from 'zova-module-basic-button';
9
10
 
10
11
  @Render()
11
12
  export class RenderTabs extends BeanRenderBase {
@@ -77,22 +78,31 @@ export class RenderTabs extends BeanRenderBase {
77
78
  return <ClientOnly>{domWrapper}</ClientOnly>;
78
79
  }
79
80
 
81
+ const tabItems = tabCurrent.items.filter(item => item.componentKey !== tabKey);
82
+ if (!tabItems.length) return;
83
+ const visibleItems = this.tabItemsCompact
84
+ ? tabItems.filter(item => item.componentKey === $$modelTabs.componentKeyCurrent)
85
+ : tabItems;
80
86
  const domTabs: VNode[] = [];
81
- for (const tabItem of tabCurrent.items) {
82
- // ignore first
83
- if (tabItem.componentKey === tabKey) continue;
84
- const { componentKey, pageMeta } = tabItem;
85
- const className =
86
- componentKey === $$modelTabs.componentKeyCurrent ? 'tab-active text-primary' : '';
87
- const pageContent = pageMeta?.onCustomRender
88
- ? pageMeta.onCustomRender(tabItem)
89
- : pageMeta?.pageTitle || '';
87
+ for (const tabItem of visibleItems) {
88
+ const { componentKey } = tabItem;
89
+ const current = componentKey === $$modelTabs.componentKeyCurrent;
90
+ const className = current ? 'tab-active text-primary' : '';
91
+ const pageContent = this.getTabItemContent(tabItem);
90
92
  const tabItemIcon = this.getTabItemIcon(tabItem);
91
93
  const domTab = (
92
94
  <a
93
95
  key={componentKey}
94
96
  role="tab"
97
+ aria-selected={current}
98
+ tabindex={0}
95
99
  class={`tab flex items-center ${className} ${this.cTab}`}
100
+ onKeydown={event => {
101
+ if (event.key === 'Enter' || event.key === ' ') {
102
+ event.preventDefault();
103
+ void $$modelTabs.activeTabItem(tabKey, componentKey);
104
+ }
105
+ }}
96
106
  onClick={() => {
97
107
  $$modelTabs.activeTabItem(tabKey, componentKey);
98
108
  }}
@@ -118,14 +128,90 @@ export class RenderTabs extends BeanRenderBase {
118
128
  domTabs.push(domTab);
119
129
  }
120
130
  const domWrapper = (
121
- <div role="tablist" class="tabs tabs-border">
122
- {domTabs}
131
+ <div
132
+ class={`tab-items ${this.cTabItems} ${this.tabItemsCompact ? 'is-compact' : ''}`}
133
+ data-testid="tab-items"
134
+ data-compact={this.tabItemsCompact ? 'true' : 'false'}
135
+ ref={ref => {
136
+ if (!this.ctx.disposed)
137
+ this.setTabItemsElement?.((ref as HTMLDivElement | null) ?? undefined);
138
+ }}
139
+ >
140
+ <div
141
+ class={`tab-items-viewport ${this.tabItemsOverflowStart ? 'has-start' : ''} ${this.tabItemsOverflowEnd ? 'has-end' : ''}`}
142
+ >
143
+ <div class="tab-items-scroll">
144
+ <div role="tablist" class="tab-items-content tabs tabs-border">
145
+ {domTabs}
146
+ </div>
147
+ </div>
148
+ </div>
149
+ <ZButton
150
+ class="tab-items-more btn btn-square btn-ghost"
151
+ data-testid="tab-items-more"
152
+ aria-label={this.scope.locale.AllTabs()}
153
+ title={this.scope.locale.AllTabs()}
154
+ aria-controls={this.tabItemsMenuId}
155
+ aria-expanded={this.tabItemsMenuOpen}
156
+ {...{ 'popovertarget': this.tabItemsMenuId, 'data-hydrate-ignore-popovertarget': '' }}
157
+ >
158
+ <span aria-hidden="true">▾</span>
159
+ </ZButton>
160
+ <div
161
+ id={this.tabItemsMenuId}
162
+ ref={ref => {
163
+ if (!this.ctx.disposed)
164
+ this.setTabItemsMenuElement?.((ref as HTMLDivElement | null) ?? undefined);
165
+ }}
166
+ {...{ popover: 'auto' }}
167
+ class={`${this.cTabItemsMenu} bg-base-100 border border-base-300 rounded-box shadow-lg p-2`}
168
+ data-testid="tab-items-list"
169
+ role="group"
170
+ aria-label={this.scope.locale.AllTabs()}
171
+ >
172
+ {tabItems.map(tabItem => {
173
+ const current = tabItem.componentKey === $$modelTabs.componentKeyCurrent;
174
+ const icon = this.getTabItemIcon(tabItem);
175
+ return (
176
+ <div key={tabItem.componentKey} class="tab-items-row flex items-center gap-1">
177
+ <ZButton
178
+ class={`tab-items-select btn btn-ghost ${current ? 'bg-base-200 text-primary' : ''}`}
179
+ aria-current={current ? 'page' : undefined}
180
+ onPerform={async () => {
181
+ this.closeTabItemsMenu();
182
+ await $$modelTabs.activeTabItem(tabKey, tabItem.componentKey);
183
+ }}
184
+ >
185
+ {!!icon && <ZIcon name={icon} width={20} aria-hidden="true" />}
186
+ <span class="tab-items-label">{this.getTabItemContent(tabItem)}</span>
187
+ {current && <ZIcon name="::done" width={16} aria-hidden="true" />}
188
+ </ZButton>
189
+ <ZButton
190
+ class="btn btn-square btn-ghost shrink-0"
191
+ aria-label={this.scope.locale.CloseTab(tabItem.pageMeta?.pageTitle || '')}
192
+ title={this.scope.locale.CloseTab(tabItem.pageMeta?.pageTitle || '')}
193
+ onPerform={async () => {
194
+ await $$modelTabs.deleteTabItem(tabKey, tabItem.componentKey, false);
195
+ if (tabItems.length === 1) this.closeTabItemsMenu();
196
+ }}
197
+ >
198
+ <ZIcon name="::close" width={16} aria-hidden="true" />
199
+ </ZButton>
200
+ </div>
201
+ );
202
+ })}
203
+ </div>
123
204
  </div>
124
205
  );
125
206
  if (!this.$$modelTabs.cache) return domWrapper;
126
207
  return <ClientOnly>{domWrapper}</ClientOnly>;
127
208
  }
128
209
 
210
+ private getTabItemContent(tabItem: IRouteViewRouteItem) {
211
+ const pageMeta = tabItem.pageMeta;
212
+ return pageMeta?.onCustomRender ? pageMeta.onCustomRender(tabItem) : pageMeta?.pageTitle || '';
213
+ }
214
+
129
215
  public getTabIcon(tab: RouteTab) {
130
216
  const { info, items } = tab;
131
217
  // pageDirty
@@ -3,6 +3,7 @@ import type { IThemeRecord } from 'zova-module-a-style';
3
3
  import { BeanRenderBase, ClientOnly } from 'zova';
4
4
  import { Render } from 'zova-module-a-bean';
5
5
  import { $icon } from 'zova-module-a-icon';
6
+ import { ZButton } from 'zova-module-basic-button';
6
7
  import { closeNearestDetails } from 'zova-module-home-base';
7
8
 
8
9
  @Render()
@@ -25,7 +26,10 @@ export class RenderTheme extends BeanRenderBase {
25
26
  return (
26
27
  <li>
27
28
  <details>
28
- <summary>{$icon('::dark-theme', 24)}</summary>
29
+ <summary aria-label={this.scope.locale.ColorMode()} title={this.scope.locale.ColorMode()}>
30
+ {$icon('::dark-theme', 24)}
31
+ <span class="lg:hidden">{this.scope.locale.ColorMode()}</span>
32
+ </summary>
29
33
  <ClientOnly>
30
34
  <ul class="bg-base-100 rounded-t-none p-2 w-48">
31
35
  {themes.map(item => {
@@ -34,15 +38,16 @@ export class RenderTheme extends BeanRenderBase {
34
38
  key={item.mode.toString()}
35
39
  class={this.$theme.darkMode === item.mode ? 'disabled' : ''}
36
40
  >
37
- <a
38
- onClick={event => {
41
+ <ZButton
42
+ disabled={this.$theme.darkMode === item.mode}
43
+ onPerform={event => {
39
44
  this.$theme.darkMode = item.mode as any;
40
- closeNearestDetails(event);
45
+ closeNearestDetails({ currentTarget: event.target });
41
46
  }}
42
47
  >
43
48
  {$icon(this.$theme.darkMode === item.mode ? '::done' : '::none', 24)}
44
49
  {item.title}
45
- </a>
50
+ </ZButton>
46
51
  </li>
47
52
  );
48
53
  })}
@@ -58,21 +63,25 @@ export class RenderTheme extends BeanRenderBase {
58
63
  return (
59
64
  <li>
60
65
  <details>
61
- <summary>{$icon(':outline:theme-outline', 24)}</summary>
66
+ <summary aria-label={this.scope.locale.Theme()} title={this.scope.locale.Theme()}>
67
+ {$icon(':outline:theme-outline', 24)}
68
+ <span class="lg:hidden">{this.scope.locale.Theme()}</span>
69
+ </summary>
62
70
  <ClientOnly>
63
71
  <ul class="bg-base-100 rounded-t-none p-2 w-48">
64
72
  {themes.map(item => {
65
73
  return (
66
74
  <li key={item.name} class={this.$theme.name === item.name ? 'disabled' : ''}>
67
- <a
68
- onClick={event => {
75
+ <ZButton
76
+ disabled={this.$theme.name === item.name}
77
+ onPerform={event => {
69
78
  this.$theme.name = item.name as keyof IThemeRecord;
70
- closeNearestDetails(event);
79
+ closeNearestDetails({ currentTarget: event.target });
71
80
  }}
72
81
  >
73
82
  {$icon(this.$theme.name === item.name ? '::done' : '::none', 24)}
74
83
  {item.title}
75
- </a>
84
+ </ZButton>
76
85
  </li>
77
86
  );
78
87
  })}
@@ -24,7 +24,18 @@ export class RenderUser extends BeanRenderBase {
24
24
  <ul class="bg-base-100 rounded-t-none p-2 w-32">
25
25
  <li>
26
26
  <a
27
+ href={this.$router.getPagePath('/home/user/account')}
27
28
  onClick={event => {
29
+ if (
30
+ event.button !== 0 ||
31
+ event.metaKey ||
32
+ event.ctrlKey ||
33
+ event.shiftKey ||
34
+ event.altKey
35
+ ) {
36
+ return;
37
+ }
38
+ event.preventDefault();
28
39
  this.app.$gotoPage('/home/user/account');
29
40
  closeNearestDetails(event);
30
41
  }}