@ngbplatform/ui 1.3.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.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header data-testid="site-topbar" class="sticky top-0 z-30 border-b border-ngb-border bg-ngb-card px-3 lg:h-14">
|
|
3
|
+
<!--
|
|
4
|
+
Layout goal:
|
|
5
|
+
- Search stays strictly centered.
|
|
6
|
+
- Left is pinned to the left edge: Back + current page title.
|
|
7
|
+
- Right icons are pinned to the right edge.
|
|
8
|
+
-->
|
|
9
|
+
<div class="flex min-w-0 flex-col gap-2 py-2 lg:grid lg:h-full lg:grid-cols-[minmax(0,320px)_minmax(0,1fr)_minmax(0,320px)] lg:items-center lg:gap-3 lg:py-0">
|
|
10
|
+
<!-- Left (intentionally empty to avoid duplicated title/back in list views) -->
|
|
11
|
+
<div class="hidden h-full lg:block"></div>
|
|
12
|
+
|
|
13
|
+
<div class="order-1 flex min-w-0 items-center gap-2 md:hidden">
|
|
14
|
+
<button
|
|
15
|
+
v-if="showMainMenu"
|
|
16
|
+
data-testid="site-topbar-main-menu"
|
|
17
|
+
class="ngb-iconbtn"
|
|
18
|
+
title="Main menu"
|
|
19
|
+
aria-label="Main menu"
|
|
20
|
+
@click="$emit('openMainMenu')"
|
|
21
|
+
>
|
|
22
|
+
<NgbIcon name="panel-left" />
|
|
23
|
+
</button>
|
|
24
|
+
|
|
25
|
+
<div class="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1">
|
|
26
|
+
<!-- Notifications -->
|
|
27
|
+
<button class="ngb-iconbtn relative" title="Notifications" aria-label="Notifications" @click="$emit('openNotifications')">
|
|
28
|
+
<NgbIcon name="bell" />
|
|
29
|
+
<span
|
|
30
|
+
v-if="unreadNotifications > 0"
|
|
31
|
+
class="absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1 rounded-full bg-ngb-danger text-white text-[10px] font-bold leading-[18px] flex items-center justify-center border border-ngb-card"
|
|
32
|
+
>
|
|
33
|
+
{{ unreadNotifications > 99 ? '99+' : unreadNotifications }}
|
|
34
|
+
</span>
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
<button class="ngb-iconbtn" title="Help" aria-label="Help" @click="$emit('openHelp')">
|
|
38
|
+
<NgbIcon name="help-circle" />
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<button v-if="hasSettings" class="ngb-iconbtn" title="Settings" aria-label="Settings" @click="$emit('openSettings')">
|
|
42
|
+
<NgbIcon name="settings" />
|
|
43
|
+
</button>
|
|
44
|
+
|
|
45
|
+
<button
|
|
46
|
+
class="ngb-iconbtn"
|
|
47
|
+
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
48
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
49
|
+
@click="$emit('toggleTheme')"
|
|
50
|
+
>
|
|
51
|
+
<NgbIcon :name="isDark ? 'sun' : 'moon'" />
|
|
52
|
+
</button>
|
|
53
|
+
|
|
54
|
+
<Menu as="div" class="relative" v-slot="{ open }">
|
|
55
|
+
<MenuButton
|
|
56
|
+
class="flex h-9 w-9 items-center justify-center rounded-full border text-white ngb-focus transition-all duration-150"
|
|
57
|
+
:class="open ? 'border-[rgba(11,60,93,.45)] bg-ngb-blue shadow-[0_8px_18px_rgba(11,60,93,.16)]' : 'border-ngb-border bg-ngb-blue hover:border-[rgba(11,60,93,.35)] hover:shadow-[0_4px_12px_rgba(11,60,93,.10)]'"
|
|
58
|
+
title="User"
|
|
59
|
+
>
|
|
60
|
+
<span class="text-[12px] font-bold">{{ initials }}</span>
|
|
61
|
+
</MenuButton>
|
|
62
|
+
|
|
63
|
+
<transition
|
|
64
|
+
enter-active-class="transition duration-150 ease-out"
|
|
65
|
+
enter-from-class="translate-y-1.5 opacity-0 scale-[0.98]"
|
|
66
|
+
enter-to-class="translate-y-0 opacity-100 scale-100"
|
|
67
|
+
leave-active-class="transition duration-100 ease-in"
|
|
68
|
+
leave-from-class="translate-y-0 opacity-100 scale-100"
|
|
69
|
+
leave-to-class="translate-y-1 opacity-0 scale-[0.98]"
|
|
70
|
+
>
|
|
71
|
+
<MenuItems class="absolute right-0 z-30 mt-2 w-[320px] max-w-[calc(100vw-24px)] rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-3 shadow-card focus:outline-none">
|
|
72
|
+
<div class="rounded-[var(--ngb-radius)] bg-ngb-bg px-3 py-3">
|
|
73
|
+
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-ngb-muted">Signed in</div>
|
|
74
|
+
<div class="mt-1 truncate text-sm font-semibold text-ngb-text">{{ resolvedUserName }}</div>
|
|
75
|
+
<div v-if="hasUserEmail" class="mt-1 truncate text-xs text-ngb-muted">{{ userEmail }}</div>
|
|
76
|
+
<div v-if="hasUserRoles" class="mt-3">
|
|
77
|
+
<div class="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-ngb-muted">Application roles</div>
|
|
78
|
+
<div class="flex flex-wrap gap-1.5" :title="userRolesTitle">
|
|
79
|
+
<span
|
|
80
|
+
v-for="role in visibleUserRoles"
|
|
81
|
+
:key="role"
|
|
82
|
+
class="inline-flex max-w-full items-center gap-1.5 rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-text"
|
|
83
|
+
>
|
|
84
|
+
<NgbIcon name="shield" :size="13" />
|
|
85
|
+
<span class="max-w-[210px] truncate">{{ role }}</span>
|
|
86
|
+
</span>
|
|
87
|
+
<span
|
|
88
|
+
v-if="hiddenUserRoleCount > 0"
|
|
89
|
+
class="inline-flex items-center rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-muted"
|
|
90
|
+
>
|
|
91
|
+
+{{ hiddenUserRoleCount }} more
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div v-else-if="hasUserMeta" class="mt-3 inline-flex items-center gap-2 rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-text">
|
|
96
|
+
<NgbIcon :name="userMetaIcon" :size="14" />
|
|
97
|
+
<span>{{ userMeta }}</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="my-2 h-px bg-ngb-border" />
|
|
102
|
+
|
|
103
|
+
<MenuItem v-slot="{ active }">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="group flex w-full items-center gap-3 rounded-[var(--ngb-radius)] px-3 py-2.5 text-left transition-colors duration-150 ngb-focus"
|
|
107
|
+
:class="active ? 'bg-[rgba(155,28,28,.06)] text-ngb-danger' : 'text-ngb-danger hover:bg-[rgba(155,28,28,.04)]'"
|
|
108
|
+
@click="$emit('signOut')"
|
|
109
|
+
>
|
|
110
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center text-current">
|
|
111
|
+
<NgbIcon name="log-out" :size="17" />
|
|
112
|
+
</span>
|
|
113
|
+
|
|
114
|
+
<span class="min-w-0 flex-1">
|
|
115
|
+
<span class="block text-sm font-semibold">Sign out</span>
|
|
116
|
+
<span class="mt-0.5 block text-xs text-ngb-muted">End this secure session on this device</span>
|
|
117
|
+
</span>
|
|
118
|
+
</button>
|
|
119
|
+
</MenuItem>
|
|
120
|
+
</MenuItems>
|
|
121
|
+
</transition>
|
|
122
|
+
</Menu>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="order-1 hidden min-w-0 items-center justify-end gap-1 md:flex lg:hidden">
|
|
127
|
+
<button class="ngb-iconbtn relative" title="Notifications" aria-label="Notifications" @click="$emit('openNotifications')">
|
|
128
|
+
<NgbIcon name="bell" />
|
|
129
|
+
<span
|
|
130
|
+
v-if="unreadNotifications > 0"
|
|
131
|
+
class="absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1 rounded-full bg-ngb-danger text-white text-[10px] font-bold leading-[18px] flex items-center justify-center border border-ngb-card"
|
|
132
|
+
>
|
|
133
|
+
{{ unreadNotifications > 99 ? '99+' : unreadNotifications }}
|
|
134
|
+
</span>
|
|
135
|
+
</button>
|
|
136
|
+
|
|
137
|
+
<button class="ngb-iconbtn" title="Help" aria-label="Help" @click="$emit('openHelp')">
|
|
138
|
+
<NgbIcon name="help-circle" />
|
|
139
|
+
</button>
|
|
140
|
+
|
|
141
|
+
<button v-if="hasSettings" class="ngb-iconbtn" title="Settings" aria-label="Settings" @click="$emit('openSettings')">
|
|
142
|
+
<NgbIcon name="settings" />
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<button
|
|
146
|
+
class="ngb-iconbtn"
|
|
147
|
+
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
148
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
149
|
+
@click="$emit('toggleTheme')"
|
|
150
|
+
>
|
|
151
|
+
<NgbIcon :name="isDark ? 'sun' : 'moon'" />
|
|
152
|
+
</button>
|
|
153
|
+
|
|
154
|
+
<Menu as="div" class="relative" v-slot="{ open }">
|
|
155
|
+
<MenuButton
|
|
156
|
+
class="flex h-9 w-9 items-center justify-center rounded-full border text-white ngb-focus transition-all duration-150"
|
|
157
|
+
:class="open ? 'border-[rgba(11,60,93,.45)] bg-ngb-blue shadow-[0_8px_18px_rgba(11,60,93,.16)]' : 'border-ngb-border bg-ngb-blue hover:border-[rgba(11,60,93,.35)] hover:shadow-[0_4px_12px_rgba(11,60,93,.10)]'"
|
|
158
|
+
title="User"
|
|
159
|
+
aria-label="User"
|
|
160
|
+
>
|
|
161
|
+
<span class="text-[12px] font-bold">{{ initials }}</span>
|
|
162
|
+
</MenuButton>
|
|
163
|
+
|
|
164
|
+
<transition
|
|
165
|
+
enter-active-class="transition duration-150 ease-out"
|
|
166
|
+
enter-from-class="translate-y-1.5 opacity-0 scale-[0.98]"
|
|
167
|
+
enter-to-class="translate-y-0 opacity-100 scale-100"
|
|
168
|
+
leave-active-class="transition duration-100 ease-in"
|
|
169
|
+
leave-from-class="translate-y-0 opacity-100 scale-100"
|
|
170
|
+
leave-to-class="translate-y-1 opacity-0 scale-[0.98]"
|
|
171
|
+
>
|
|
172
|
+
<MenuItems class="absolute right-0 z-30 mt-2 w-[320px] max-w-[calc(100vw-24px)] rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-3 shadow-card focus:outline-none">
|
|
173
|
+
<div class="rounded-[var(--ngb-radius)] bg-ngb-bg px-3 py-3">
|
|
174
|
+
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-ngb-muted">Signed in</div>
|
|
175
|
+
<div class="mt-1 truncate text-sm font-semibold text-ngb-text">{{ resolvedUserName }}</div>
|
|
176
|
+
<div v-if="hasUserEmail" class="mt-1 truncate text-xs text-ngb-muted">{{ userEmail }}</div>
|
|
177
|
+
<div v-if="hasUserRoles" class="mt-3">
|
|
178
|
+
<div class="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-ngb-muted">Application roles</div>
|
|
179
|
+
<div class="flex flex-wrap gap-1.5" :title="userRolesTitle">
|
|
180
|
+
<span
|
|
181
|
+
v-for="role in visibleUserRoles"
|
|
182
|
+
:key="role"
|
|
183
|
+
class="inline-flex max-w-full items-center gap-1.5 rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-text"
|
|
184
|
+
>
|
|
185
|
+
<NgbIcon name="shield" :size="13" />
|
|
186
|
+
<span class="max-w-[210px] truncate">{{ role }}</span>
|
|
187
|
+
</span>
|
|
188
|
+
<span
|
|
189
|
+
v-if="hiddenUserRoleCount > 0"
|
|
190
|
+
class="inline-flex items-center rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-muted"
|
|
191
|
+
>
|
|
192
|
+
+{{ hiddenUserRoleCount }} more
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div v-else-if="hasUserMeta" class="mt-3 inline-flex items-center gap-2 rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-text">
|
|
197
|
+
<NgbIcon :name="userMetaIcon" :size="14" />
|
|
198
|
+
<span>{{ userMeta }}</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="my-2 h-px bg-ngb-border" />
|
|
203
|
+
|
|
204
|
+
<MenuItem v-slot="{ active }">
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
class="group flex w-full items-center gap-3 rounded-[var(--ngb-radius)] px-3 py-2.5 text-left transition-colors duration-150 ngb-focus"
|
|
208
|
+
:class="active ? 'bg-[rgba(155,28,28,.06)] text-ngb-danger' : 'text-ngb-danger hover:bg-[rgba(155,28,28,.04)]'"
|
|
209
|
+
@click="$emit('signOut')"
|
|
210
|
+
>
|
|
211
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center text-current">
|
|
212
|
+
<NgbIcon name="log-out" :size="17" />
|
|
213
|
+
</span>
|
|
214
|
+
|
|
215
|
+
<span class="min-w-0 flex-1">
|
|
216
|
+
<span class="block text-sm font-semibold">Sign out</span>
|
|
217
|
+
<span class="mt-0.5 block text-xs text-ngb-muted">End this secure session on this device</span>
|
|
218
|
+
</span>
|
|
219
|
+
</button>
|
|
220
|
+
</MenuItem>
|
|
221
|
+
</MenuItems>
|
|
222
|
+
</transition>
|
|
223
|
+
</Menu>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Center -->
|
|
227
|
+
<div class="order-2 min-w-0 lg:order-none lg:flex lg:justify-center">
|
|
228
|
+
<!-- Searchbar (kept as-is) -->
|
|
229
|
+
<button
|
|
230
|
+
class="w-full max-w-[720px] h-10 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card hover:bg-ngb-bg ngb-focus flex items-center gap-3 px-3 text-left"
|
|
231
|
+
@click="$emit('openPalette')"
|
|
232
|
+
>
|
|
233
|
+
<span class="text-ngb-muted">
|
|
234
|
+
<NgbIcon name="search" />
|
|
235
|
+
</span>
|
|
236
|
+
<span class="text-ngb-muted text-sm truncate">Search pages, records, reports, or run a command…</span>
|
|
237
|
+
<span class="ml-auto flex items-center gap-1">
|
|
238
|
+
<span class="ngb-kbd">{{ primaryModifier }}</span>
|
|
239
|
+
<span class="ngb-kbd">K</span>
|
|
240
|
+
</span>
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- Right -->
|
|
245
|
+
<div class="hidden min-w-0 flex-wrap items-center justify-end gap-1 lg:flex lg:order-none lg:flex-nowrap">
|
|
246
|
+
<!-- Notifications -->
|
|
247
|
+
<button class="ngb-iconbtn relative" title="Notifications" aria-label="Notifications" @click="$emit('openNotifications')">
|
|
248
|
+
<NgbIcon name="bell" />
|
|
249
|
+
<span
|
|
250
|
+
v-if="unreadNotifications > 0"
|
|
251
|
+
class="absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1 rounded-full bg-ngb-danger text-white text-[10px] font-bold leading-[18px] flex items-center justify-center border border-ngb-card"
|
|
252
|
+
>
|
|
253
|
+
{{ unreadNotifications > 99 ? '99+' : unreadNotifications }}
|
|
254
|
+
</span>
|
|
255
|
+
</button>
|
|
256
|
+
|
|
257
|
+
<!-- Help -->
|
|
258
|
+
<button class="ngb-iconbtn" title="Help" aria-label="Help" @click="$emit('openHelp')">
|
|
259
|
+
<NgbIcon name="help-circle" />
|
|
260
|
+
</button>
|
|
261
|
+
|
|
262
|
+
<!-- Settings -->
|
|
263
|
+
<button v-if="hasSettings" class="ngb-iconbtn" title="Settings" aria-label="Settings" @click="$emit('openSettings')">
|
|
264
|
+
<NgbIcon name="settings" />
|
|
265
|
+
</button>
|
|
266
|
+
|
|
267
|
+
<!-- Theme toggle (icon shows the *target* mode) -->
|
|
268
|
+
<button
|
|
269
|
+
class="ngb-iconbtn"
|
|
270
|
+
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
271
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
272
|
+
@click="$emit('toggleTheme')"
|
|
273
|
+
>
|
|
274
|
+
<NgbIcon :name="isDark ? 'sun' : 'moon'" />
|
|
275
|
+
</button>
|
|
276
|
+
|
|
277
|
+
<!-- User menu (keep as-is) -->
|
|
278
|
+
<Menu as="div" class="relative" v-slot="{ open }">
|
|
279
|
+
<MenuButton
|
|
280
|
+
class="flex h-9 w-9 items-center justify-center rounded-full border text-white ngb-focus transition-all duration-150"
|
|
281
|
+
:class="open ? 'border-[rgba(11,60,93,.45)] bg-ngb-blue shadow-[0_8px_18px_rgba(11,60,93,.16)]' : 'border-ngb-border bg-ngb-blue hover:border-[rgba(11,60,93,.35)] hover:shadow-[0_4px_12px_rgba(11,60,93,.10)]'"
|
|
282
|
+
title="User"
|
|
283
|
+
aria-label="User"
|
|
284
|
+
>
|
|
285
|
+
<span class="text-[12px] font-bold">{{ initials }}</span>
|
|
286
|
+
</MenuButton>
|
|
287
|
+
|
|
288
|
+
<transition
|
|
289
|
+
enter-active-class="transition duration-150 ease-out"
|
|
290
|
+
enter-from-class="translate-y-1.5 opacity-0 scale-[0.98]"
|
|
291
|
+
enter-to-class="translate-y-0 opacity-100 scale-100"
|
|
292
|
+
leave-active-class="transition duration-100 ease-in"
|
|
293
|
+
leave-from-class="translate-y-0 opacity-100 scale-100"
|
|
294
|
+
leave-to-class="translate-y-1 opacity-0 scale-[0.98]"
|
|
295
|
+
>
|
|
296
|
+
<MenuItems class="absolute right-0 z-30 mt-2 w-[320px] max-w-[calc(100vw-24px)] rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-3 shadow-card focus:outline-none">
|
|
297
|
+
<div class="rounded-[var(--ngb-radius)] bg-ngb-bg px-3 py-3">
|
|
298
|
+
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-ngb-muted">Signed in</div>
|
|
299
|
+
<div class="mt-1 truncate text-sm font-semibold text-ngb-text">{{ resolvedUserName }}</div>
|
|
300
|
+
<div v-if="hasUserEmail" class="mt-1 truncate text-xs text-ngb-muted">{{ userEmail }}</div>
|
|
301
|
+
<div v-if="hasUserRoles" class="mt-3">
|
|
302
|
+
<div class="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-ngb-muted">Application roles</div>
|
|
303
|
+
<div class="flex flex-wrap gap-1.5" :title="userRolesTitle">
|
|
304
|
+
<span
|
|
305
|
+
v-for="role in visibleUserRoles"
|
|
306
|
+
:key="role"
|
|
307
|
+
class="inline-flex max-w-full items-center gap-1.5 rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-text"
|
|
308
|
+
>
|
|
309
|
+
<NgbIcon name="shield" :size="13" />
|
|
310
|
+
<span class="max-w-[210px] truncate">{{ role }}</span>
|
|
311
|
+
</span>
|
|
312
|
+
<span
|
|
313
|
+
v-if="hiddenUserRoleCount > 0"
|
|
314
|
+
class="inline-flex items-center rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-muted"
|
|
315
|
+
>
|
|
316
|
+
+{{ hiddenUserRoleCount }} more
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div v-else-if="hasUserMeta" class="mt-3 inline-flex items-center gap-2 rounded-[var(--ngb-radius)] bg-ngb-card px-2.5 py-1 text-[11px] font-semibold text-ngb-text">
|
|
321
|
+
<NgbIcon :name="userMetaIcon" :size="14" />
|
|
322
|
+
<span>{{ userMeta }}</span>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div class="my-2 h-px bg-ngb-border" />
|
|
327
|
+
|
|
328
|
+
<MenuItem v-slot="{ active }">
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
class="group flex w-full items-center gap-3 rounded-[var(--ngb-radius)] px-3 py-2.5 text-left transition-colors duration-150 ngb-focus"
|
|
332
|
+
:class="active ? 'bg-[rgba(155,28,28,.06)] text-ngb-danger' : 'text-ngb-danger hover:bg-[rgba(155,28,28,.04)]'"
|
|
333
|
+
@click="$emit('signOut')"
|
|
334
|
+
>
|
|
335
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center text-current">
|
|
336
|
+
<NgbIcon name="log-out" :size="17" />
|
|
337
|
+
</span>
|
|
338
|
+
|
|
339
|
+
<span class="min-w-0 flex-1">
|
|
340
|
+
<span class="block text-sm font-semibold">Sign out</span>
|
|
341
|
+
<span class="mt-0.5 block text-xs text-ngb-muted">End this secure session on this device</span>
|
|
342
|
+
</span>
|
|
343
|
+
</button>
|
|
344
|
+
</MenuItem>
|
|
345
|
+
</MenuItems>
|
|
346
|
+
</transition>
|
|
347
|
+
</Menu>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</header>
|
|
351
|
+
</template>
|
|
352
|
+
|
|
353
|
+
<script setup lang="ts">
|
|
354
|
+
import { computed } from 'vue';
|
|
355
|
+
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
|
|
356
|
+
import NgbIcon from '../primitives/NgbIcon.vue';
|
|
357
|
+
|
|
358
|
+
const props = defineProps<{
|
|
359
|
+
pageTitle: string;
|
|
360
|
+
canBack: boolean;
|
|
361
|
+
unreadNotifications: number;
|
|
362
|
+
themeResolved: 'light' | 'dark';
|
|
363
|
+
userName?: string;
|
|
364
|
+
userEmail?: string;
|
|
365
|
+
userMeta?: string;
|
|
366
|
+
userMetaIcon?: 'shield-check' | 'shield' | 'user';
|
|
367
|
+
userRoles?: string[];
|
|
368
|
+
hasSettings?: boolean;
|
|
369
|
+
showMainMenu?: boolean;
|
|
370
|
+
}>();
|
|
371
|
+
|
|
372
|
+
defineEmits<{
|
|
373
|
+
(e: 'back'): void;
|
|
374
|
+
(e: 'openMainMenu'): void;
|
|
375
|
+
(e: 'openPalette'): void;
|
|
376
|
+
(e: 'openNotifications'): void;
|
|
377
|
+
(e: 'openHelp'): void;
|
|
378
|
+
(e: 'openSettings'): void;
|
|
379
|
+
(e: 'signOut'): void;
|
|
380
|
+
(e: 'toggleTheme'): void;
|
|
381
|
+
}>();
|
|
382
|
+
|
|
383
|
+
const isDark = computed(() => props.themeResolved === 'dark');
|
|
384
|
+
const isMac = computed(() => {
|
|
385
|
+
if (typeof navigator === 'undefined') return false;
|
|
386
|
+
return /Mac|iPhone|iPad|iPod/i.test(String(navigator.platform ?? ''));
|
|
387
|
+
});
|
|
388
|
+
const primaryModifier = computed(() => (isMac.value ? '⌘' : 'Ctrl'));
|
|
389
|
+
const userEmail = computed(() => String(props.userEmail ?? '').trim());
|
|
390
|
+
const userMeta = computed(() => String(props.userMeta ?? '').trim());
|
|
391
|
+
const userMetaIcon = computed<'shield-check' | 'shield' | 'user'>(() => {
|
|
392
|
+
if (props.userMetaIcon === 'shield-check' || props.userMetaIcon === 'shield') return props.userMetaIcon;
|
|
393
|
+
return 'user';
|
|
394
|
+
});
|
|
395
|
+
const userRoles = computed(() => {
|
|
396
|
+
const seen = new Set<string>();
|
|
397
|
+
return (props.userRoles ?? [])
|
|
398
|
+
.map((role) => String(role ?? '').trim())
|
|
399
|
+
.filter((role) => {
|
|
400
|
+
const key = role.toLowerCase();
|
|
401
|
+
if (!role || seen.has(key)) return false;
|
|
402
|
+
seen.add(key);
|
|
403
|
+
return true;
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
const visibleUserRoles = computed(() => userRoles.value.slice(0, 3));
|
|
407
|
+
const hiddenUserRoleCount = computed(() => Math.max(0, userRoles.value.length - visibleUserRoles.value.length));
|
|
408
|
+
const userRolesTitle = computed(() => userRoles.value.join(', '));
|
|
409
|
+
const hasUserEmail = computed(() => userEmail.value.length > 0);
|
|
410
|
+
const hasUserRoles = computed(() => userRoles.value.length > 0);
|
|
411
|
+
const hasUserMeta = computed(() => !hasUserRoles.value && userMeta.value.length > 0);
|
|
412
|
+
const resolvedUserName = computed(() => {
|
|
413
|
+
const value = String(props.userName ?? '').trim();
|
|
414
|
+
return value || 'User';
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const initials = computed(() => {
|
|
418
|
+
const n = resolvedUserName.value;
|
|
419
|
+
if (!n) return 'U';
|
|
420
|
+
const parts = n.split(/\s+/g).filter(Boolean);
|
|
421
|
+
const a = parts[0]?.[0] ?? 'U';
|
|
422
|
+
const b = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? '') : (parts[0]?.[1] ?? '');
|
|
423
|
+
return (a + b).toUpperCase();
|
|
424
|
+
});
|
|
425
|
+
</script>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
|
+
import VChart from 'vue-echarts'
|
|
4
|
+
import { use } from 'echarts/core'
|
|
5
|
+
import { BarChart, LineChart } from 'echarts/charts'
|
|
6
|
+
import { CanvasRenderer } from 'echarts/renderers'
|
|
7
|
+
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
|
|
8
|
+
|
|
9
|
+
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent])
|
|
10
|
+
|
|
11
|
+
export type NgbTrendChartSeries = {
|
|
12
|
+
label: string
|
|
13
|
+
color: string
|
|
14
|
+
values: number[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<{
|
|
18
|
+
labels: string[]
|
|
19
|
+
series: NgbTrendChartSeries[]
|
|
20
|
+
mode?: 'line' | 'bar'
|
|
21
|
+
}>(), {
|
|
22
|
+
mode: 'line',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const themeVersion = ref(0)
|
|
26
|
+
let themeObserver: MutationObserver | null = null
|
|
27
|
+
|
|
28
|
+
const pointCount = computed(() => {
|
|
29
|
+
const seriesMax = Math.max(0, ...props.series.map((entry) => entry.values.length))
|
|
30
|
+
return Math.max(props.labels.length, seriesMax, 1)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const normalizedSeries = computed(() =>
|
|
34
|
+
props.series.map((entry) => ({
|
|
35
|
+
...entry,
|
|
36
|
+
values: Array.from({ length: pointCount.value }, (_, index) => {
|
|
37
|
+
const raw = Number(entry.values[index] ?? 0)
|
|
38
|
+
return Number.isFinite(raw) ? raw : 0
|
|
39
|
+
}),
|
|
40
|
+
})),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
function readCssVar(name: string, fallback: string): string {
|
|
44
|
+
themeVersion.value
|
|
45
|
+
if (typeof window === 'undefined') return fallback
|
|
46
|
+
const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
|
47
|
+
return value.length > 0 ? value : fallback
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveColor(value: string): string {
|
|
51
|
+
const match = String(value ?? '').trim().match(/^var\((--[^)]+)\)$/)
|
|
52
|
+
return match ? readCssVar(match[1], value) : value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatCompactNumber(value: number): string {
|
|
56
|
+
const numeric = Number(value ?? 0)
|
|
57
|
+
if (!Number.isFinite(numeric)) return '0'
|
|
58
|
+
const abs = Math.abs(numeric)
|
|
59
|
+
if (abs >= 1_000_000) return `${numeric < 0 ? '-' : ''}${(abs / 1_000_000).toFixed(1)}M`
|
|
60
|
+
if (abs >= 1_000) return `${numeric < 0 ? '-' : ''}${(abs / 1_000).toFixed(1)}K`
|
|
61
|
+
if (abs >= 100) return `${Math.round(numeric)}`
|
|
62
|
+
if (abs % 1 > 0.001) return numeric.toFixed(1)
|
|
63
|
+
return `${Math.round(numeric)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const palette = computed(() => ({
|
|
67
|
+
text: readCssVar('--ngb-text', '#1F2933'),
|
|
68
|
+
muted: readCssVar('--ngb-muted', '#4B5563'),
|
|
69
|
+
border: readCssVar('--ngb-border', '#CBD5E1'),
|
|
70
|
+
card: readCssVar('--ngb-card', '#FFFFFF'),
|
|
71
|
+
background: readCssVar('--ngb-bg', '#F5F7FA'),
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
const chartOptions = computed(() => ({
|
|
75
|
+
animationDuration: 420,
|
|
76
|
+
animationDurationUpdate: 260,
|
|
77
|
+
color: normalizedSeries.value.map((entry) => resolveColor(entry.color)),
|
|
78
|
+
legend: {
|
|
79
|
+
show: normalizedSeries.value.length > 1,
|
|
80
|
+
top: 0,
|
|
81
|
+
right: 0,
|
|
82
|
+
icon: 'roundRect',
|
|
83
|
+
itemWidth: 14,
|
|
84
|
+
itemHeight: 8,
|
|
85
|
+
textStyle: {
|
|
86
|
+
color: palette.value.text,
|
|
87
|
+
fontSize: 12,
|
|
88
|
+
fontWeight: 600,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
tooltip: {
|
|
92
|
+
trigger: 'axis',
|
|
93
|
+
confine: true,
|
|
94
|
+
backgroundColor: palette.value.card,
|
|
95
|
+
borderColor: palette.value.border,
|
|
96
|
+
borderWidth: 1,
|
|
97
|
+
textStyle: {
|
|
98
|
+
color: palette.value.text,
|
|
99
|
+
fontSize: 12,
|
|
100
|
+
},
|
|
101
|
+
axisPointer: props.mode === 'bar'
|
|
102
|
+
? {
|
|
103
|
+
type: 'shadow',
|
|
104
|
+
shadowStyle: {
|
|
105
|
+
color: 'rgba(148, 163, 184, 0.12)',
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
: {
|
|
109
|
+
type: 'line',
|
|
110
|
+
lineStyle: {
|
|
111
|
+
color: 'rgba(148, 163, 184, 0.42)',
|
|
112
|
+
width: 1,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
formatter: (params: Array<{ axisValueLabel?: string; seriesName?: string; value?: number; color?: string }>) => {
|
|
116
|
+
const rows = Array.isArray(params) ? params : [params]
|
|
117
|
+
const title = rows[0]?.axisValueLabel ?? ''
|
|
118
|
+
const body = rows.map((row) => {
|
|
119
|
+
const color = String(row?.color ?? palette.value.border)
|
|
120
|
+
const label = String(row?.seriesName ?? '').trim()
|
|
121
|
+
const value = formatCompactNumber(Number(row?.value ?? 0))
|
|
122
|
+
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;min-width:160px;"><span style="display:inline-flex;align-items:center;gap:8px;color:${palette.value.text};"><span style="display:inline-block;width:8px;height:8px;border-radius:999px;background:${color};"></span>${label}</span><strong style="color:${palette.value.text};font-weight:700;">${value}</strong></div>`
|
|
123
|
+
}).join('')
|
|
124
|
+
return `<div style="display:grid;gap:8px;"><div style="font-weight:700;color:${palette.value.text};">${title}</div>${body}</div>`
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
grid: {
|
|
128
|
+
top: normalizedSeries.value.length > 1 ? 48 : 20,
|
|
129
|
+
right: 10,
|
|
130
|
+
bottom: 10,
|
|
131
|
+
left: 6,
|
|
132
|
+
containLabel: true,
|
|
133
|
+
},
|
|
134
|
+
xAxis: {
|
|
135
|
+
type: 'category',
|
|
136
|
+
boundaryGap: props.mode === 'bar',
|
|
137
|
+
data: props.labels,
|
|
138
|
+
axisTick: { show: false },
|
|
139
|
+
axisLine: {
|
|
140
|
+
lineStyle: {
|
|
141
|
+
color: palette.value.border,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
axisLabel: {
|
|
145
|
+
color: palette.value.muted,
|
|
146
|
+
fontSize: 11,
|
|
147
|
+
margin: 12,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
yAxis: {
|
|
151
|
+
type: 'value',
|
|
152
|
+
axisLine: { show: false },
|
|
153
|
+
axisTick: { show: false },
|
|
154
|
+
splitLine: {
|
|
155
|
+
lineStyle: {
|
|
156
|
+
color: palette.value.border,
|
|
157
|
+
opacity: 0.55,
|
|
158
|
+
type: 'dashed',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
axisLabel: {
|
|
162
|
+
color: palette.value.muted,
|
|
163
|
+
fontSize: 11,
|
|
164
|
+
formatter: (value: number) => formatCompactNumber(value),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
series: normalizedSeries.value.map((entry, index) => props.mode === 'bar'
|
|
168
|
+
? {
|
|
169
|
+
name: entry.label,
|
|
170
|
+
type: 'bar',
|
|
171
|
+
data: entry.values,
|
|
172
|
+
barMaxWidth: 38,
|
|
173
|
+
itemStyle: {
|
|
174
|
+
borderRadius: 0,
|
|
175
|
+
},
|
|
176
|
+
emphasis: {
|
|
177
|
+
focus: 'series',
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
: {
|
|
181
|
+
name: entry.label,
|
|
182
|
+
type: 'line',
|
|
183
|
+
data: entry.values,
|
|
184
|
+
smooth: 0.32,
|
|
185
|
+
showSymbol: false,
|
|
186
|
+
symbol: 'circle',
|
|
187
|
+
symbolSize: 7,
|
|
188
|
+
lineStyle: {
|
|
189
|
+
width: 3,
|
|
190
|
+
},
|
|
191
|
+
areaStyle: {
|
|
192
|
+
opacity: index === 0 ? 0.16 : 0.1,
|
|
193
|
+
},
|
|
194
|
+
emphasis: {
|
|
195
|
+
focus: 'series',
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
}))
|
|
199
|
+
|
|
200
|
+
onMounted(() => {
|
|
201
|
+
themeObserver = new MutationObserver(() => {
|
|
202
|
+
themeVersion.value += 1
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
themeObserver.observe(document.documentElement, {
|
|
206
|
+
attributes: true,
|
|
207
|
+
attributeFilter: ['class', 'style'],
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
onBeforeUnmount(() => {
|
|
212
|
+
themeObserver?.disconnect()
|
|
213
|
+
themeObserver = null
|
|
214
|
+
})
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<VChart
|
|
219
|
+
class="h-full w-full"
|
|
220
|
+
:option="chartOptions"
|
|
221
|
+
:init-options="{ renderer: 'canvas' }"
|
|
222
|
+
autoresize
|
|
223
|
+
/>
|
|
224
|
+
</template>
|