@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.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. 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>