@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,342 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <TransitionRoot appear :show="store.isOpen" as="template" @after-leave="restoreLastFocus">
4
+ <Dialog as="div" class="relative z-[90]" :initialFocus="inputRef" @close="store.close()">
5
+ <TransitionChild
6
+ as="template"
7
+ enter="duration-150 ease-out"
8
+ enter-from="opacity-0"
9
+ enter-to="opacity-100"
10
+ leave="duration-120 ease-in"
11
+ leave-from="opacity-100"
12
+ leave-to="opacity-0"
13
+ >
14
+ <div class="fixed inset-0 bg-[rgba(7,12,20,.48)] backdrop-blur-[2px]" />
15
+ </TransitionChild>
16
+
17
+ <div class="fixed inset-0 overflow-y-auto">
18
+ <div class="flex min-h-full items-start justify-center px-4 pb-8 pt-[10vh]">
19
+ <TransitionChild
20
+ as="template"
21
+ enter="duration-150 ease-out"
22
+ enter-from="opacity-0 translate-y-3 scale-[0.985]"
23
+ enter-to="opacity-100 translate-y-0 scale-100"
24
+ leave="duration-120 ease-in"
25
+ leave-from="opacity-100 translate-y-0 scale-100"
26
+ leave-to="opacity-0 translate-y-3 scale-[0.985]"
27
+ >
28
+ <DialogPanel data-testid="command-palette-dialog" class="w-full max-w-[860px] overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card">
29
+ <DialogTitle class="sr-only">Command palette</DialogTitle>
30
+
31
+ <div class="border-b border-ngb-border px-4 py-3">
32
+ <div class="flex h-10 items-center gap-3 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3">
33
+ <span class="text-ngb-muted">
34
+ <NgbIcon name="search" :size="18" />
35
+ </span>
36
+
37
+ <input
38
+ ref="inputRef"
39
+ data-testid="command-palette-input"
40
+ :value="store.query"
41
+ class="h-full flex-1 border-0 bg-transparent px-0 text-sm text-ngb-text outline-none placeholder:text-ngb-muted/70"
42
+ :placeholder="placeholder"
43
+ role="combobox"
44
+ aria-autocomplete="list"
45
+ :aria-expanded="store.hasResults"
46
+ :aria-controls="listboxId"
47
+ :aria-activedescendant="activeDescendantId"
48
+ @input="store.setQuery(String(($event.target as HTMLInputElement)?.value ?? ''))"
49
+ @keydown="onInputKeyDown"
50
+ />
51
+
52
+ <span class="hidden items-center gap-1 md:flex">
53
+ <template v-if="isMac">
54
+ <span class="ngb-kbd">⌘</span>
55
+ </template>
56
+ <template v-else>
57
+ <span class="ngb-kbd">Ctrl</span>
58
+ </template>
59
+ <span class="ngb-kbd">K</span>
60
+ </span>
61
+ </div>
62
+
63
+ <div v-if="scopeBadgeLabel || store.showRemoteLoading" class="mt-2 flex flex-wrap items-center gap-2 text-xs text-ngb-muted">
64
+ <span v-if="scopeBadgeLabel" class="rounded-full border border-ngb-border bg-ngb-bg px-2.5 py-1">
65
+ {{ scopeBadgeLabel }}
66
+ </span>
67
+ <span v-if="store.showRemoteLoading" class="rounded-full border border-ngb-border bg-ngb-bg px-2.5 py-1">
68
+ Updating remote results…
69
+ </span>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="sr-only" aria-live="polite">
74
+ {{ liveRegionText }}
75
+ </div>
76
+
77
+ <div class="max-h-[560px] overflow-y-auto p-2" :id="listboxId" role="listbox">
78
+ <div
79
+ v-if="store.hasRemoteError && store.groups.length > 0"
80
+ class="mx-2 mb-2 rounded-[var(--ngb-radius)] border border-[rgba(185,28,28,.16)] bg-[rgba(254,242,242,.92)] px-3 py-2 text-xs text-[#7f1d1d]"
81
+ >
82
+ {{ store.remoteError }}
83
+ </div>
84
+
85
+ <template v-if="store.groups.length > 0">
86
+ <section v-for="group in store.groups" :key="group.code" class="py-1.5">
87
+ <div class="px-3 pb-2 pt-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-ngb-muted">
88
+ {{ group.label }}
89
+ </div>
90
+
91
+ <div class="space-y-1">
92
+ <button
93
+ v-for="item in group.items"
94
+ :id="optionId(item.key)"
95
+ :key="item.key"
96
+ :data-option-id="optionId(item.key)"
97
+ :data-item-key="item.key"
98
+ type="button"
99
+ role="option"
100
+ :aria-selected="isActive(item.key)"
101
+ class="flex w-full items-start gap-3 rounded-[var(--ngb-radius)] border px-3 py-2.5 text-left transition-colors"
102
+ :class="isActive(item.key)
103
+ ? 'border-[rgba(11,60,93,.22)] bg-[rgba(11,60,93,.08)]'
104
+ : 'border-transparent hover:border-ngb-border hover:bg-ngb-bg'"
105
+ @mouseenter="setActive(item.key)"
106
+ @click="onItemClick(item, $event)"
107
+ >
108
+ <span
109
+ class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--ngb-radius)]"
110
+ :class="isActive(item.key) ? 'bg-transparent text-ngb-text' : 'bg-transparent text-ngb-muted'"
111
+ >
112
+ <NgbIcon :name="resolveCommandPaletteIcon(item.icon)" :size="17" />
113
+ </span>
114
+
115
+ <span class="min-w-0 flex-1">
116
+ <span class="flex items-start justify-between gap-3">
117
+ <span class="min-w-0">
118
+ <span class="block truncate text-sm font-semibold text-ngb-text">{{ item.title }}</span>
119
+ <span class="mt-0.5 block truncate text-xs leading-5 text-ngb-muted">{{ resolveCommandPaletteSubtitle(item) }}</span>
120
+ </span>
121
+
122
+ <span class="flex shrink-0 items-center gap-2">
123
+ <NgbBadge
124
+ v-if="resolveCommandPaletteBadge(item)"
125
+ tone="neutral"
126
+ >
127
+ {{ resolveCommandPaletteBadge(item) }}
128
+ </NgbBadge>
129
+ <span v-if="item.openInNewTabSupported" class="hidden text-[11px] text-ngb-muted lg:inline">
130
+ {{ primaryModifier }}+Enter
131
+ </span>
132
+ </span>
133
+ </span>
134
+ </span>
135
+ </button>
136
+ </div>
137
+ </section>
138
+ </template>
139
+
140
+ <NgbCommandPaletteEmptyState
141
+ v-else
142
+ :query="store.cleanQuery"
143
+ :loading="store.showRemoteLoading"
144
+ :error="store.hasRemoteError ? store.remoteError : null"
145
+ />
146
+ </div>
147
+
148
+ <NgbCommandPaletteFooterHints :primary-modifier="primaryModifier" />
149
+ </DialogPanel>
150
+ </TransitionChild>
151
+ </div>
152
+ </div>
153
+ </Dialog>
154
+ </TransitionRoot>
155
+ </Teleport>
156
+ </template>
157
+
158
+ <script setup lang="ts">
159
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
160
+ import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
161
+ import NgbBadge from '../primitives/NgbBadge.vue'
162
+ import NgbIcon from '../primitives/NgbIcon.vue'
163
+ import { useCommandPaletteStore } from './store'
164
+ import type { CommandPaletteItem } from './types'
165
+ import {
166
+ resolveCommandPaletteBadge,
167
+ resolveCommandPaletteIcon,
168
+ resolveCommandPaletteSubtitle,
169
+ } from './presentation'
170
+ import NgbCommandPaletteEmptyState from './NgbCommandPaletteEmptyState.vue'
171
+ import NgbCommandPaletteFooterHints from './NgbCommandPaletteFooterHints.vue'
172
+
173
+ const store = useCommandPaletteStore()
174
+ const inputRef = ref<HTMLInputElement | null>(null)
175
+ const lastFocusedElement = ref<HTMLElement | null>(null)
176
+ let restoreFocusTimer: ReturnType<typeof window.setTimeout> | null = null
177
+
178
+ const listboxId = 'ngb-command-palette-listbox'
179
+ const placeholder = 'Search pages, records, reports, or run a command…'
180
+ const isMac = /Mac|iPhone|iPad|iPod/i.test(String(globalThis.navigator?.platform ?? ''))
181
+ const primaryModifier = computed(() => (isMac ? '⌘' : 'Ctrl'))
182
+
183
+ function onDocumentFocusIn(event: FocusEvent): void {
184
+ if (store.isOpen) return
185
+ const target = event.target instanceof HTMLElement ? event.target : null
186
+ if (!target || target === document.body) return
187
+ lastFocusedElement.value = target
188
+ }
189
+
190
+ const flatItems = computed(() => store.flatItems)
191
+ const indexByKey = computed(() => {
192
+ const map = new Map<string, number>()
193
+ flatItems.value.forEach((item, index) => map.set(item.key, index))
194
+ return map
195
+ })
196
+
197
+ const activeItem = computed(() => flatItems.value[store.activeIndex] ?? null)
198
+ const activeDescendantId = computed(() => (activeItem.value ? optionId(activeItem.value.key) : undefined))
199
+ const scopeBadgeLabel = computed<string | null>(() => {
200
+ switch (store.activeScope) {
201
+ case 'commands':
202
+ return 'Commands'
203
+ case 'pages':
204
+ return 'Pages'
205
+ case 'reports':
206
+ return 'Reports'
207
+ case 'documents':
208
+ return 'Documents'
209
+ case 'catalogs':
210
+ return 'Catalogs'
211
+ default:
212
+ return null
213
+ }
214
+ })
215
+
216
+ const liveRegionText = computed(() => {
217
+ if (store.hasRemoteError) return store.remoteError ?? 'Remote search is unavailable.'
218
+ if (store.showRemoteLoading) return 'Updating remote results.'
219
+ if (store.groups.length === 0) return store.cleanQuery ? `No results for ${store.cleanQuery}.` : 'Showing command palette shortcuts.'
220
+ return `${flatItems.value.length} results available.`
221
+ })
222
+
223
+ watch(
224
+ () => store.isOpen,
225
+ async (open) => {
226
+ if (open) {
227
+ if (restoreFocusTimer) {
228
+ window.clearTimeout(restoreFocusTimer)
229
+ restoreFocusTimer = null
230
+ }
231
+
232
+ const activeElement = document.activeElement
233
+ if (activeElement instanceof HTMLElement && activeElement !== document.body)
234
+ lastFocusedElement.value = activeElement
235
+ await nextTick()
236
+ focusInput(true)
237
+ return
238
+ }
239
+
240
+ const restoreTarget = lastFocusedElement.value
241
+ restoreFocusTimer = window.setTimeout(() => {
242
+ restoreFocusTimer = null
243
+ if (store.isOpen) return
244
+ restoreTarget?.focus?.()
245
+ }, 180)
246
+ },
247
+ )
248
+
249
+ watch(
250
+ () => store.focusRequestKey,
251
+ async () => {
252
+ if (!store.isOpen) return
253
+ await nextTick()
254
+ focusInput(true)
255
+ },
256
+ )
257
+
258
+ watch(
259
+ () => activeDescendantId.value,
260
+ async (id) => {
261
+ if (!id) return
262
+ await nextTick()
263
+ document.querySelector<HTMLElement>(`[data-option-id="${id}"]`)?.scrollIntoView({ block: 'nearest' })
264
+ },
265
+ )
266
+
267
+ function focusInput(selectAll = false, attempt = 0): void {
268
+ inputRef.value?.focus()
269
+ if (selectAll) inputRef.value?.select()
270
+ window.setTimeout(() => {
271
+ if (!store.isOpen) return
272
+ if (document.activeElement === inputRef.value) return
273
+ if (attempt >= 4) return
274
+ focusInput(selectAll, attempt + 1)
275
+ }, attempt === 0 ? 0 : 16)
276
+ }
277
+
278
+ function restoreLastFocus(): void {
279
+ if (store.isOpen) return
280
+ if (restoreFocusTimer) {
281
+ window.clearTimeout(restoreFocusTimer)
282
+ restoreFocusTimer = null
283
+ }
284
+ window.setTimeout(() => {
285
+ if (store.isOpen) return
286
+ lastFocusedElement.value?.focus?.()
287
+ }, 0)
288
+ }
289
+
290
+ onMounted(() => {
291
+ document.addEventListener('focusin', onDocumentFocusIn, true)
292
+ })
293
+
294
+ onBeforeUnmount(() => {
295
+ document.removeEventListener('focusin', onDocumentFocusIn, true)
296
+ })
297
+
298
+ function isActive(key: string): boolean {
299
+ return activeItem.value?.key === key
300
+ }
301
+
302
+ function setActive(key: string): void {
303
+ const index = indexByKey.value.get(key)
304
+ if (index == null) return
305
+ store.setActiveIndex(index)
306
+ }
307
+
308
+ function optionId(key: string): string {
309
+ return `ngb-command-palette-option-${key.replace(/[^a-z0-9_-]+/gi, '-')}`
310
+ }
311
+
312
+ function onInputKeyDown(event: KeyboardEvent): void {
313
+ if (event.key === 'ArrowDown') {
314
+ event.preventDefault()
315
+ store.moveActive(1)
316
+ return
317
+ }
318
+
319
+ if (event.key === 'ArrowUp') {
320
+ event.preventDefault()
321
+ store.moveActive(-1)
322
+ return
323
+ }
324
+
325
+ if (event.key === 'Escape') {
326
+ event.preventDefault()
327
+ event.stopPropagation()
328
+ store.close()
329
+ return
330
+ }
331
+
332
+ if (event.key === 'Enter') {
333
+ event.preventDefault()
334
+ void store.executeActive((event.metaKey || event.ctrlKey) ? 'new-tab' : 'default')
335
+ }
336
+ }
337
+
338
+ function onItemClick(item: CommandPaletteItem, event: MouseEvent): void {
339
+ const mode = (event.metaKey || event.ctrlKey) && item.openInNewTabSupported ? 'new-tab' : 'default'
340
+ void store.executeItem(item, mode)
341
+ }
342
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="px-5 py-8">
3
+ <template v-if="error">
4
+ <div
5
+ role="alert"
6
+ aria-live="assertive"
7
+ class="rounded-[var(--ngb-radius)] border border-[rgba(185,28,28,.18)] bg-[rgba(254,242,242,.9)] px-4 py-3"
8
+ >
9
+ <div class="text-sm font-semibold text-[#991b1b]">Remote search is temporarily unavailable</div>
10
+ <div class="mt-1 text-sm text-[#7f1d1d]">{{ error }}</div>
11
+ </div>
12
+ </template>
13
+
14
+ <template v-else-if="loading">
15
+ <div role="status" aria-live="polite">
16
+ <div class="text-sm font-semibold text-ngb-text">Searching records…</div>
17
+ <div class="mt-2 text-sm text-ngb-muted">Pages, commands, and cached reports stay available while remote results load.</div>
18
+ </div>
19
+ </template>
20
+
21
+ <template v-else-if="!query">
22
+ <div class="text-sm font-semibold text-ngb-text">Search pages, records, reports, or run a command</div>
23
+ <div class="mt-2 text-sm text-ngb-muted">
24
+ Try <span class="font-medium text-ngb-text">trial balance</span> or <span class="font-medium text-ngb-text">create journal entry</span>.
25
+ </div>
26
+ <div class="mt-4 flex flex-wrap gap-2 text-xs text-ngb-muted">
27
+ <span class="ngb-kbd">&gt;</span><span class="mr-3">Commands</span>
28
+ <span class="ngb-kbd">/</span><span class="mr-3">Pages</span>
29
+ <span class="ngb-kbd">#</span><span class="mr-3">Reports</span>
30
+ <span class="ngb-kbd">:</span><span class="mr-3">Documents</span>
31
+ <span class="ngb-kbd">@</span><span>Catalogs</span>
32
+ </div>
33
+ </template>
34
+
35
+ <template v-else>
36
+ <div class="text-sm font-semibold text-ngb-text">No results for “{{ query }}”</div>
37
+ <div class="mt-2 text-sm text-ngb-muted">Try a document number, a page name, or a scope prefix to narrow the search.</div>
38
+ <div class="mt-4 flex flex-wrap gap-2 text-xs text-ngb-muted">
39
+ <span class="ngb-kbd">&gt;</span><span class="mr-3">Commands</span>
40
+ <span class="ngb-kbd">/</span><span class="mr-3">Pages</span>
41
+ <span class="ngb-kbd">#</span><span class="mr-3">Reports</span>
42
+ <span class="ngb-kbd">:</span><span class="mr-3">Documents</span>
43
+ <span class="ngb-kbd">@</span><span>Catalogs</span>
44
+ </div>
45
+ </template>
46
+ </div>
47
+ </template>
48
+
49
+ <script setup lang="ts">
50
+ defineProps<{
51
+ query: string
52
+ loading: boolean
53
+ error?: string | null
54
+ }>()
55
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="flex flex-wrap items-center justify-between gap-3 border-t border-ngb-border px-4 py-3 text-xs text-ngb-muted">
3
+ <div class="flex flex-wrap items-center gap-2">
4
+ <span class="ngb-kbd">↑</span>
5
+ <span class="ngb-kbd">↓</span>
6
+ <span>Navigate</span>
7
+ <span class="ngb-kbd">Enter</span>
8
+ <span>Open</span>
9
+ <span class="ngb-kbd">{{ primaryModifier }}</span>
10
+ <span class="ngb-kbd">Enter</span>
11
+ <span>New tab</span>
12
+ </div>
13
+
14
+ <div class="flex flex-wrap items-center gap-2">
15
+ <span class="ngb-kbd">Esc</span>
16
+ <span>Close</span>
17
+ <span class="ngb-kbd">{{ primaryModifier }}</span>
18
+ <span class="ngb-kbd">K</span>
19
+ <span>Focus</span>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ defineProps<{
26
+ primaryModifier: string
27
+ }>()
28
+ </script>
29
+
@@ -0,0 +1,10 @@
1
+ import { httpPost } from '../api/http'
2
+ import type { CommandPaletteSearchRequestDto, CommandPaletteSearchResponseDto } from './types'
3
+
4
+ export async function searchCommandPalette(
5
+ request: CommandPaletteSearchRequestDto,
6
+ signal?: AbortSignal,
7
+ ): Promise<CommandPaletteSearchResponseDto> {
8
+ return await httpPost<CommandPaletteSearchResponseDto>('/api/search/command-palette', request, { signal })
9
+ }
10
+
@@ -0,0 +1,16 @@
1
+ import type { CommandPaletteStoreConfig } from './types'
2
+
3
+ let commandPaletteConfig: CommandPaletteStoreConfig | null = null
4
+
5
+ export function configureNgbCommandPalette(config: CommandPaletteStoreConfig): void {
6
+ commandPaletteConfig = config
7
+ }
8
+
9
+ export function getConfiguredNgbCommandPalette(): CommandPaletteStoreConfig {
10
+ if (!commandPaletteConfig) {
11
+ throw new Error('NGB command palette is not configured. Call configureNgbCommandPalette(...) during app bootstrap.')
12
+ }
13
+
14
+ return commandPaletteConfig
15
+ }
16
+
@@ -0,0 +1,85 @@
1
+ import { coerceNgbIconName, type NgbIconName } from '../primitives/iconNames'
2
+ import type { CommandPaletteItem } from './types'
3
+
4
+ const GENERIC_BADGES = new Set(['Favorite', 'Recent'])
5
+ const GENERIC_SUBTITLES = new Set([
6
+ 'Favorite',
7
+ 'Favorite report',
8
+ 'Recent',
9
+ 'Report',
10
+ 'Page',
11
+ 'Document',
12
+ 'Catalog',
13
+ ])
14
+
15
+ function cleanMeta(value: string | null | undefined): string | null {
16
+ const next = String(value ?? '').trim()
17
+ return next.length > 0 ? next : null
18
+ }
19
+
20
+ export function resolveCommandPaletteIcon(icon: string | null | undefined): NgbIconName {
21
+ return coerceNgbIconName(icon, 'search')
22
+ }
23
+
24
+ export function resolveCommandPaletteBadge(item: CommandPaletteItem): string | null {
25
+ const badge = cleanMeta(item.badge)
26
+ if (badge && !GENERIC_BADGES.has(badge)) return badge
27
+
28
+ switch (item.kind) {
29
+ case 'page':
30
+ return 'Page'
31
+ case 'document':
32
+ return 'Document'
33
+ case 'catalog':
34
+ return 'Catalog'
35
+ case 'report':
36
+ return 'Report'
37
+ case 'command':
38
+ return 'Action'
39
+ case 'recent':
40
+ switch (item.scope) {
41
+ case 'reports':
42
+ return 'Report'
43
+ case 'documents':
44
+ return 'Document'
45
+ case 'catalogs':
46
+ return 'Catalog'
47
+ default:
48
+ return 'Page'
49
+ }
50
+ default:
51
+ return null
52
+ }
53
+ }
54
+
55
+ export function resolveCommandPaletteSubtitle(item: CommandPaletteItem): string {
56
+ const subtitle = cleanMeta(item.subtitle)
57
+ if (subtitle && !GENERIC_SUBTITLES.has(subtitle)) return subtitle
58
+
59
+ switch (item.kind) {
60
+ case 'page':
61
+ return 'Open this page'
62
+ case 'document':
63
+ return 'Open this document'
64
+ case 'catalog':
65
+ return 'Open this catalog record'
66
+ case 'report':
67
+ return 'Run this report'
68
+ case 'command':
69
+ return 'Run this command'
70
+ case 'recent':
71
+ switch (item.scope) {
72
+ case 'reports':
73
+ return 'Open this recent report'
74
+ case 'documents':
75
+ return 'Open this recent document'
76
+ case 'catalogs':
77
+ return 'Open this recent catalog record'
78
+ default:
79
+ return 'Open this recent page'
80
+ }
81
+ default:
82
+ return 'Open this item'
83
+ }
84
+ }
85
+
@@ -0,0 +1,130 @@
1
+ import type { CommandPaletteGroupCode, CommandPaletteScope } from './types'
2
+
3
+ type SearchField = {
4
+ value?: string | null
5
+ exact: number
6
+ prefix: number
7
+ wordPrefix: number
8
+ contains: number
9
+ }
10
+
11
+ export function normalizeSearchText(value: string | null | undefined): string {
12
+ const source = String(value ?? '').trim().toLowerCase()
13
+ if (!source) return ''
14
+
15
+ let out = ''
16
+ let lastWasSpace = false
17
+
18
+ for (const char of source) {
19
+ if (/[a-z0-9]/.test(char)) {
20
+ out += char
21
+ lastWasSpace = false
22
+ continue
23
+ }
24
+
25
+ if (lastWasSpace) continue
26
+ out += ' '
27
+ lastWasSpace = true
28
+ }
29
+
30
+ return out.trim()
31
+ }
32
+
33
+ export function parseCommandPaletteQuery(value: string): { rawQuery: string; query: string; scope: CommandPaletteScope | null } {
34
+ const rawQuery = String(value ?? '')
35
+ const trimmed = rawQuery.trimStart()
36
+ const prefix = trimmed.charAt(0)
37
+
38
+ const scope = prefixToScope(prefix)
39
+ if (!scope) {
40
+ return {
41
+ rawQuery,
42
+ query: rawQuery.trim(),
43
+ scope: null,
44
+ }
45
+ }
46
+
47
+ return {
48
+ rawQuery,
49
+ query: trimmed.slice(1).trim(),
50
+ scope,
51
+ }
52
+ }
53
+
54
+ export function prefixToScope(prefix: string): CommandPaletteScope | null {
55
+ switch (prefix) {
56
+ case '>':
57
+ return 'commands'
58
+ case '/':
59
+ return 'pages'
60
+ case '#':
61
+ return 'reports'
62
+ case ':':
63
+ return 'documents'
64
+ case '@':
65
+ return 'catalogs'
66
+ default:
67
+ return null
68
+ }
69
+ }
70
+
71
+ export function scoreSearchText(query: string, fields: SearchField[]): number {
72
+ const normalizedQuery = normalizeSearchText(query)
73
+ if (!normalizedQuery) return 0
74
+
75
+ let best = 0
76
+ for (const field of fields) {
77
+ best = Math.max(best, scoreField(normalizedQuery, field))
78
+ }
79
+
80
+ return best
81
+ }
82
+
83
+ export function groupOrder(code: CommandPaletteGroupCode): number {
84
+ switch (code) {
85
+ case 'actions':
86
+ return 0
87
+ case 'go-to':
88
+ return 1
89
+ case 'documents':
90
+ return 2
91
+ case 'catalogs':
92
+ return 3
93
+ case 'reports':
94
+ return 4
95
+ case 'recent':
96
+ return 5
97
+ default:
98
+ return 99
99
+ }
100
+ }
101
+
102
+ export function defaultSearchFields(...values: Array<string | null | undefined>): SearchField[] {
103
+ return values
104
+ .filter((value) => String(value ?? '').trim().length > 0)
105
+ .map((value, index) => ({
106
+ value,
107
+ exact: index === 0 ? 1 : 0.86,
108
+ prefix: index === 0 ? 0.92 : 0.8,
109
+ wordPrefix: index === 0 ? 0.88 : 0.76,
110
+ contains: index === 0 ? 0.76 : 0.68,
111
+ }))
112
+ }
113
+
114
+ function scoreField(normalizedQuery: string, field: SearchField): number {
115
+ const normalizedValue = normalizeSearchText(field.value)
116
+ if (!normalizedValue) return 0
117
+
118
+ if (normalizedValue === normalizedQuery) return field.exact
119
+ if (normalizedValue.startsWith(normalizedQuery)) return field.prefix
120
+ if (containsWordPrefix(normalizedValue, normalizedQuery)) return field.wordPrefix
121
+ if (normalizedValue.includes(normalizedQuery)) return field.contains
122
+
123
+ return 0
124
+ }
125
+
126
+ function containsWordPrefix(value: string, query: string): boolean {
127
+ const parts = value.split(' ')
128
+ return parts.some((part) => part.startsWith(query))
129
+ }
130
+