@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,120 @@
1
+ import type { Router } from 'vue-router'
2
+
3
+ export type CommandPaletteScope = 'commands' | 'pages' | 'reports' | 'documents' | 'catalogs'
4
+
5
+ export type CommandPaletteGroupCode = 'actions' | 'go-to' | 'documents' | 'catalogs' | 'reports' | 'recent'
6
+
7
+ export type CommandPaletteItemKind = 'page' | 'document' | 'catalog' | 'report' | 'command' | 'recent'
8
+
9
+ export type CommandPaletteExecutionMode = 'default' | 'new-tab'
10
+
11
+ export type CommandPaletteAction = () => void | Promise<void>
12
+
13
+ export type CommandPaletteItem = {
14
+ key: string
15
+ group: CommandPaletteGroupCode
16
+ kind: CommandPaletteItemKind
17
+ scope: CommandPaletteScope
18
+ title: string
19
+ subtitle?: string | null
20
+ icon?: string | null
21
+ badge?: string | null
22
+ hint?: string | null
23
+ route?: string | null
24
+ commandCode?: string | null
25
+ status?: string | null
26
+ openInNewTabSupported?: boolean
27
+ keywords?: string[]
28
+ perform?: CommandPaletteAction
29
+ defaultRank: number
30
+ score: number
31
+ isCurrentContext?: boolean
32
+ isFavorite?: boolean
33
+ isRecent?: boolean
34
+ source: 'local' | 'remote'
35
+ }
36
+
37
+ export type CommandPaletteItemSeed = Omit<CommandPaletteItem, 'score' | 'source'>
38
+
39
+ export type CommandPaletteGroup = {
40
+ code: CommandPaletteGroupCode
41
+ label: string
42
+ items: CommandPaletteItem[]
43
+ }
44
+
45
+ export type CommandPaletteExplicitContext = {
46
+ entityType?: 'document' | 'catalog' | 'report' | 'page'
47
+ documentType?: string | null
48
+ catalogType?: string | null
49
+ entityId?: string | null
50
+ title?: string | null
51
+ actions: CommandPaletteItemSeed[]
52
+ }
53
+
54
+ export type CommandPaletteRecentEntry = {
55
+ key: string
56
+ kind: CommandPaletteItemKind
57
+ scope: CommandPaletteScope
58
+ title: string
59
+ subtitle?: string | null
60
+ icon?: string | null
61
+ badge?: string | null
62
+ route?: string | null
63
+ status?: string | null
64
+ openInNewTabSupported?: boolean
65
+ timestamp: string
66
+ }
67
+
68
+ export type CommandPaletteSearchContextDto = {
69
+ entityType?: string | null
70
+ documentType?: string | null
71
+ catalogType?: string | null
72
+ entityId?: string | null
73
+ }
74
+
75
+ export type CommandPaletteSearchRequestDto = {
76
+ query: string
77
+ scope?: CommandPaletteScope | null
78
+ limit?: number
79
+ currentRoute?: string | null
80
+ context?: CommandPaletteSearchContextDto | null
81
+ }
82
+
83
+ export type CommandPaletteResultItemDto = {
84
+ key: string
85
+ kind: CommandPaletteItemKind | 'page'
86
+ title: string
87
+ subtitle?: string | null
88
+ icon?: string | null
89
+ badge?: string | null
90
+ route?: string | null
91
+ commandCode?: string | null
92
+ status?: string | null
93
+ openInNewTabSupported: boolean
94
+ score: number
95
+ }
96
+
97
+ export type CommandPaletteGroupDto = {
98
+ code: 'documents' | 'catalogs' | 'reports' | 'go-to' | 'actions' | 'recent'
99
+ label: string
100
+ items: CommandPaletteResultItemDto[]
101
+ }
102
+
103
+ export type CommandPaletteSearchResponseDto = {
104
+ groups: CommandPaletteGroupDto[]
105
+ }
106
+
107
+ export type CommandPaletteStoreConfig = {
108
+ router: Router
109
+ recentStorageKey: string
110
+ searchRemote?: (
111
+ request: CommandPaletteSearchRequestDto,
112
+ signal?: AbortSignal,
113
+ ) => Promise<CommandPaletteSearchResponseDto>
114
+ loadReportItems?: () => Promise<CommandPaletteItemSeed[]>
115
+ buildHeuristicCurrentActions?: (fullRoute: string) => CommandPaletteItemSeed[]
116
+ favoriteItems?: CommandPaletteItemSeed[]
117
+ createItems?: CommandPaletteItemSeed[]
118
+ specialPageItems?: CommandPaletteItemSeed[]
119
+ }
120
+
@@ -0,0 +1,27 @@
1
+ import { onBeforeUnmount, onMounted } from 'vue'
2
+ import { useCommandPaletteStore } from './store'
3
+
4
+ export function useCommandPaletteHotkeys(): void {
5
+ const store = useCommandPaletteStore()
6
+
7
+ function onKeyDown(event: KeyboardEvent): void {
8
+ const isCommandShortcut = (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'k'
9
+ if (!isCommandShortcut) return
10
+ if (!store.isOpen && isEditableTarget(event.target)) return
11
+
12
+ event.preventDefault()
13
+ store.open()
14
+ }
15
+
16
+ onMounted(() => window.addEventListener('keydown', onKeyDown))
17
+ onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown))
18
+ }
19
+
20
+ function isEditableTarget(target: EventTarget | null): boolean {
21
+ if (!(target instanceof HTMLElement)) return false
22
+ if (target.isContentEditable) return true
23
+
24
+ const closest = target.closest('input, textarea, select, [contenteditable="true"], [role="textbox"]')
25
+ return closest instanceof HTMLElement
26
+ }
27
+
@@ -0,0 +1,18 @@
1
+ import { getCurrentInstance, onBeforeUnmount, watchEffect } from 'vue'
2
+ import type { CommandPaletteExplicitContext } from './types'
3
+ import { useCommandPaletteStore } from './store'
4
+
5
+ export function useCommandPalettePageContext(resolve: () => CommandPaletteExplicitContext | null | undefined): void {
6
+ const store = useCommandPaletteStore()
7
+ const instance = getCurrentInstance()
8
+ const ownerId = `command-palette:${instance?.uid ?? Math.random().toString(36).slice(2)}`
9
+
10
+ watchEffect(() => {
11
+ store.setExplicitContext(ownerId, resolve() ?? null)
12
+ })
13
+
14
+ onBeforeUnmount(() => {
15
+ store.clearExplicitContext(ownerId)
16
+ })
17
+ }
18
+
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <NgbModalShell :open="open" max-width-class="max-w-[480px]" @close="cancel">
3
+ <div class="p-5">
4
+ <div class="flex items-start gap-3">
5
+ <div class="shrink-0 mt-0.5">
6
+ <div
7
+ class="w-9 h-9 rounded-full flex items-center justify-center"
8
+ :class="danger ? 'bg-[rgba(155,28,28,.08)] text-ngb-danger' : 'bg-[rgba(11,60,93,.08)] text-ngb-blue'"
9
+ >
10
+ <NgbIcon :name="danger ? 'trash' : 'help-circle'" />
11
+ </div>
12
+ </div>
13
+
14
+ <div class="min-w-0 flex-1">
15
+ <DialogTitle class="text-base font-semibold text-ngb-text">{{ title }}</DialogTitle>
16
+ <div class="text-sm text-ngb-muted mt-1 leading-5">{{ message }}</div>
17
+ <div v-if="$slots.default" class="mt-3">
18
+ <slot />
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="mt-5 flex items-center justify-end gap-2">
24
+ <NgbButton variant="secondary" @click="cancel">{{ cancelTextComputed }}</NgbButton>
25
+ <NgbButton :variant="danger ? 'danger' : 'primary'" :loading="confirmLoading" @click="confirm">{{ confirmTextComputed }}</NgbButton>
26
+ </div>
27
+ </div>
28
+ </NgbModalShell>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { computed } from 'vue'
33
+ import { DialogTitle } from '@headlessui/vue'
34
+ import NgbModalShell from './NgbModalShell.vue'
35
+ import NgbButton from '../primitives/NgbButton.vue'
36
+ import NgbIcon from '../primitives/NgbIcon.vue'
37
+
38
+ const props = defineProps<{
39
+ open: boolean
40
+ title: string
41
+ message: string
42
+ confirmText?: string
43
+ cancelText?: string
44
+ danger?: boolean
45
+ confirmLoading?: boolean
46
+ }>()
47
+
48
+ const emit = defineEmits<{
49
+ (e: 'update:open', value: boolean): void
50
+ (e: 'confirm'): void
51
+ }>()
52
+
53
+ const confirmTextComputed = computed(() => props.confirmText ?? (props.danger ? 'Discard' : 'Confirm'))
54
+ const cancelTextComputed = computed(() => props.cancelText ?? 'Cancel')
55
+
56
+ function cancel() {
57
+ emit('update:open', false)
58
+ }
59
+
60
+ function confirm() {
61
+ emit('confirm')
62
+ }
63
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <NgbModalShell :open="open" max-width-class="max-w-[560px]" @close="$emit('update:open', false)">
3
+ <div class="px-5 py-4 border-b border-ngb-border">
4
+ <DialogTitle class="text-base font-semibold text-ngb-text">{{ title }}</DialogTitle>
5
+ <div v-if="subtitle" class="text-sm text-ngb-muted mt-1">{{ subtitle }}</div>
6
+ </div>
7
+
8
+ <div class="px-5 py-4">
9
+ <slot />
10
+ </div>
11
+
12
+ <div class="px-5 py-4 border-t border-ngb-border flex items-center justify-end gap-2">
13
+ <slot name="footer">
14
+ <NgbButton variant="secondary" @click="$emit('update:open', false)">{{ cancelText }}</NgbButton>
15
+ <NgbButton :variant="danger ? 'danger' : 'primary'" :loading="confirmLoading" @click="$emit('confirm')">{{ confirmText }}</NgbButton>
16
+ </slot>
17
+ </div>
18
+ </NgbModalShell>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed } from 'vue'
23
+ import { DialogTitle } from '@headlessui/vue'
24
+ import NgbModalShell from './NgbModalShell.vue'
25
+ import NgbButton from '../primitives/NgbButton.vue'
26
+
27
+ const props = defineProps<{
28
+ open: boolean
29
+ title: string
30
+ subtitle?: string
31
+ confirmText?: string
32
+ cancelText?: string
33
+ danger?: boolean
34
+ confirmLoading?: boolean
35
+ }>()
36
+
37
+ defineEmits<{
38
+ (e: 'update:open', value: boolean): void
39
+ (e: 'confirm'): void
40
+ }>()
41
+
42
+ const confirmText = computed(() => props.confirmText ?? 'Confirm')
43
+ const cancelText = computed(() => props.cancelText ?? 'Cancel')
44
+ </script>
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <TransitionRoot appear :show="open" as="template" @after-leave="restoreLastFocus">
3
+ <Dialog as="div" class="relative z-40" @close="maybeClose">
4
+ <TransitionChild
5
+ as="template"
6
+ enter="duration-150 ease-out"
7
+ enter-from="opacity-0"
8
+ enter-to="opacity-100"
9
+ leave="duration-120 ease-in"
10
+ leave-from="opacity-100"
11
+ leave-to="opacity-0"
12
+ >
13
+ <div data-testid="drawer-overlay" class="fixed inset-0 bg-[rgba(0,0,0,.18)]" />
14
+ </TransitionChild>
15
+
16
+ <div class="fixed inset-0 overflow-hidden">
17
+ <div class="absolute inset-0 overflow-hidden">
18
+ <div
19
+ class="pointer-events-none fixed inset-y-0 flex max-w-full"
20
+ :class="side === 'left' ? 'left-0 pr-10' : 'right-0 pl-10'"
21
+ >
22
+ <TransitionChild
23
+ as="template"
24
+ enter="duration-150 ease-out"
25
+ :enter-from="side === 'left' ? '-translate-x-full' : 'translate-x-full'"
26
+ enter-to="translate-x-0"
27
+ leave="duration-120 ease-in"
28
+ leave-from="translate-x-0"
29
+ :leave-to="side === 'left' ? '-translate-x-full' : 'translate-x-full'"
30
+ >
31
+ <DialogPanel
32
+ data-testid="drawer-panel"
33
+ class="pointer-events-auto w-screen bg-ngb-card shadow-card"
34
+ :class="[side === 'left' ? 'border-r border-ngb-border' : 'border-l border-ngb-border', panelClass || 'max-w-[520px]']"
35
+ >
36
+ <DialogTitle v-if="hideHeader" class="sr-only">{{ title }}</DialogTitle>
37
+
38
+ <div class="h-full flex flex-col">
39
+ <div v-if="!hideHeader" data-testid="drawer-header" class="px-5 py-4 border-b border-ngb-border flex items-center gap-3">
40
+ <div class="min-w-0 flex-1">
41
+ <DialogTitle class="text-base font-semibold text-ngb-text truncate">{{ title }}</DialogTitle>
42
+ <div v-if="subtitle" class="text-sm text-ngb-muted mt-1 truncate">{{ subtitle }}</div>
43
+ </div>
44
+
45
+ <div class="flex items-center justify-end gap-2">
46
+ <slot name="actions" />
47
+
48
+ <button v-if="showClose" class="ngb-iconbtn" @click="maybeClose" title="Close">
49
+ <NgbIcon name="x" />
50
+ </button>
51
+ </div>
52
+ </div>
53
+
54
+ <div data-testid="drawer-body" :class="['flex-1 overflow-auto', flushBody ? 'p-0' : 'px-5 py-4']">
55
+ <slot />
56
+ </div>
57
+
58
+ <div v-if="$slots.footer" class="px-5 py-4 border-t border-ngb-border">
59
+ <slot name="footer" />
60
+ </div>
61
+ </div>
62
+ </DialogPanel>
63
+ </TransitionChild>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </Dialog>
68
+ </TransitionRoot>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
73
+ import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
74
+
75
+ import NgbIcon from '../primitives/NgbIcon.vue';
76
+
77
+ const props = withDefaults(
78
+ defineProps<{
79
+ open: boolean;
80
+ title: string;
81
+ subtitle?: string;
82
+ showClose?: boolean;
83
+ hideHeader?: boolean;
84
+ flushBody?: boolean;
85
+ side?: 'left' | 'right';
86
+ panelClass?: string;
87
+ // Allows parent to intercept drawer close (overlay click, Esc, or close button).
88
+ // Return false to prevent closing.
89
+ beforeClose?: (() => boolean | Promise<boolean>) | null;
90
+ }>(),
91
+ {
92
+ showClose: true,
93
+ hideHeader: false,
94
+ flushBody: false,
95
+ side: 'right',
96
+ panelClass: '',
97
+ beforeClose: null,
98
+ },
99
+ );
100
+
101
+ const emit = defineEmits<{
102
+ (e: 'update:open', value: boolean): void;
103
+ }>();
104
+
105
+ const lastFocusedElement = ref<HTMLElement | null>(null);
106
+
107
+ function onDocumentFocusIn(event: FocusEvent) {
108
+ if (props.open) return;
109
+ const target = event.target instanceof HTMLElement ? event.target : null;
110
+ if (!target || target === document.body) return;
111
+ lastFocusedElement.value = target;
112
+ }
113
+
114
+ onMounted(() => {
115
+ if (typeof document === 'undefined') return;
116
+ document.addEventListener('focusin', onDocumentFocusIn, true);
117
+ });
118
+
119
+ onBeforeUnmount(() => {
120
+ if (typeof document === 'undefined') return;
121
+ document.removeEventListener('focusin', onDocumentFocusIn, true);
122
+ });
123
+
124
+ watch(
125
+ () => props.open,
126
+ (open) => {
127
+ if (typeof window === 'undefined') return;
128
+
129
+ if (open) {
130
+ const activeElement = document.activeElement;
131
+ if (activeElement instanceof HTMLElement && activeElement !== document.body) {
132
+ lastFocusedElement.value = activeElement;
133
+ }
134
+ }
135
+ },
136
+ );
137
+
138
+ function restoreLastFocus() {
139
+ if (props.open) return;
140
+ window.setTimeout(() => {
141
+ if (props.open) return;
142
+ lastFocusedElement.value?.focus?.();
143
+ }, 0);
144
+ }
145
+
146
+ async function maybeClose() {
147
+ if (!props.showClose) return;
148
+
149
+ if (props.beforeClose) {
150
+ const ok = await props.beforeClose();
151
+ if (!ok) return;
152
+ }
153
+
154
+ emit('update:open', false);
155
+ }
156
+ </script>
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <div class="inline-flex max-w-full items-center gap-1">
3
+ <div v-if="primaryActions.length > 0" class="flex min-w-0 items-center gap-1">
4
+ <button
5
+ v-for="item in primaryActions"
6
+ :key="item.key"
7
+ type="button"
8
+ class="flex h-8 w-8 items-center justify-center rounded-[calc(var(--ngb-radius)-1px)] text-ngb-muted transition-colors hover:bg-ngb-bg hover:text-ngb-text ngb-focus disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-ngb-muted"
9
+ :title="item.title"
10
+ :aria-label="item.title"
11
+ :disabled="item.disabled"
12
+ @click="emit('action', item.key)"
13
+ >
14
+ <NgbIcon :name="item.icon" :size="17" />
15
+ </button>
16
+ </div>
17
+
18
+ <Menu v-if="hasMoreActions" as="div" class="relative shrink-0">
19
+ <MenuButton
20
+ class="flex h-8 w-8 items-center justify-center rounded-[calc(var(--ngb-radius)-1px)] text-ngb-muted transition-colors hover:bg-ngb-bg hover:text-ngb-text ngb-focus disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-ngb-muted"
21
+ title="More actions"
22
+ aria-label="More actions"
23
+ >
24
+ <NgbIcon name="more-vertical" :size="17" />
25
+ </MenuButton>
26
+
27
+ <MenuItems class="absolute right-0 z-20 mt-2 w-64 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-1.5 shadow-card focus:outline-none">
28
+ <div v-for="(group, groupIndex) in visibleMoreGroups" :key="group.key">
29
+ <div v-if="group.label" class="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-ngb-muted">
30
+ {{ group.label }}
31
+ </div>
32
+
33
+ <MenuItem
34
+ v-for="item in group.items"
35
+ :key="item.key"
36
+ as="template"
37
+ :disabled="item.disabled"
38
+ v-slot="{ active, disabled, close }"
39
+ >
40
+ <button
41
+ type="button"
42
+ class="flex w-full items-center gap-3 rounded-[var(--ngb-radius)] px-2.5 py-2 text-left text-sm transition-colors"
43
+ :class="[
44
+ disabled ? 'cursor-not-allowed opacity-40' : '',
45
+ active && !disabled ? 'bg-ngb-bg text-ngb-text' : 'text-ngb-text',
46
+ ]"
47
+ :disabled="disabled"
48
+ @click="close(); emit('action', item.key)"
49
+ >
50
+ <span class="flex h-4 w-4 items-center justify-center text-ngb-muted">
51
+ <NgbIcon :name="item.icon" :size="16" />
52
+ </span>
53
+ <span class="truncate">{{ item.title }}</span>
54
+ </button>
55
+ </MenuItem>
56
+
57
+ <div v-if="groupIndex < visibleMoreGroups.length - 1" class="my-1 h-px bg-ngb-border" />
58
+ </div>
59
+ </MenuItems>
60
+ </Menu>
61
+
62
+ <div class="mx-1 h-6 w-px shrink-0 bg-ngb-border" />
63
+
64
+ <button
65
+ type="button"
66
+ class="flex h-8 w-8 shrink-0 items-center justify-center rounded-[calc(var(--ngb-radius)-1px)] text-ngb-muted transition-colors hover:bg-ngb-bg hover:text-ngb-text ngb-focus disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-ngb-muted"
67
+ title="Close"
68
+ aria-label="Close"
69
+ :disabled="closeDisabled"
70
+ @click="emit('close')"
71
+ >
72
+ <NgbIcon name="x" :size="17" />
73
+ </button>
74
+ </div>
75
+ </template>
76
+
77
+ <script setup lang="ts">
78
+ import { computed } from 'vue'
79
+ import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
80
+
81
+ import NgbIcon from '../primitives/NgbIcon.vue'
82
+
83
+ type HeaderActionIconName =
84
+ | 'panel-right'
85
+ | 'open-in-new'
86
+ | 'copy'
87
+ | 'printer'
88
+ | 'trash'
89
+ | 'trash-restore'
90
+ | 'save'
91
+ | 'check'
92
+ | 'undo'
93
+ | 'file-apply'
94
+ | 'share'
95
+ | 'history'
96
+ | 'effects-flow'
97
+ | 'document-flow'
98
+
99
+ type HeaderActionItem = {
100
+ key: string
101
+ title: string
102
+ icon: HeaderActionIconName
103
+ disabled?: boolean
104
+ }
105
+
106
+ type HeaderActionGroup = {
107
+ key: string
108
+ label?: string
109
+ items: HeaderActionItem[]
110
+ }
111
+
112
+ const props = withDefaults(
113
+ defineProps<{
114
+ primaryActions?: HeaderActionItem[]
115
+ moreGroups?: HeaderActionGroup[]
116
+ closeDisabled?: boolean
117
+ }>(),
118
+ {
119
+ primaryActions: () => [],
120
+ moreGroups: () => [],
121
+ closeDisabled: false,
122
+ },
123
+ )
124
+
125
+ const emit = defineEmits<{
126
+ (e: 'action', key: string): void
127
+ (e: 'close'): void
128
+ }>()
129
+
130
+ const visibleMoreGroups = computed(() =>
131
+ props.moreGroups
132
+ .map((group) => ({
133
+ ...group,
134
+ items: (group.items ?? []).filter(Boolean),
135
+ }))
136
+ .filter((group) => group.items.length > 0),
137
+ )
138
+
139
+ const hasMoreActions = computed(() => visibleMoreGroups.value.length > 0)
140
+ </script>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <TransitionRoot appear :show="open" as="template" @after-leave="restoreLastFocus">
3
+ <Dialog as="div" class="relative z-50" @close="emit('close')">
4
+ <TransitionChild
5
+ as="template"
6
+ enter="duration-150 ease-out"
7
+ enter-from="opacity-0"
8
+ enter-to="opacity-100"
9
+ leave="duration-120 ease-in"
10
+ leave-from="opacity-100"
11
+ leave-to="opacity-0"
12
+ >
13
+ <div class="fixed inset-0 bg-[rgba(0,0,0,.28)]" />
14
+ </TransitionChild>
15
+
16
+ <div class="fixed inset-0 overflow-y-auto">
17
+ <div class="min-h-full flex items-center justify-center p-4">
18
+ <TransitionChild
19
+ as="template"
20
+ enter="duration-150 ease-out"
21
+ enter-from="opacity-0 translate-y-2 scale-[0.98]"
22
+ enter-to="opacity-100 translate-y-0 scale-100"
23
+ leave="duration-120 ease-in"
24
+ leave-from="opacity-100 translate-y-0 scale-100"
25
+ leave-to="opacity-0 translate-y-2 scale-[0.98]"
26
+ >
27
+ <DialogPanel
28
+ class="w-full rounded-[var(--ngb-radius)] bg-ngb-card border border-ngb-border shadow-card overflow-hidden"
29
+ :class="maxWidthClass"
30
+ >
31
+ <slot />
32
+ </DialogPanel>
33
+ </TransitionChild>
34
+ </div>
35
+ </div>
36
+ </Dialog>
37
+ </TransitionRoot>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
42
+ import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
43
+
44
+ const props = withDefaults(defineProps<{
45
+ open: boolean
46
+ maxWidthClass?: string
47
+ }>(), {
48
+ maxWidthClass: 'max-w-[560px]',
49
+ })
50
+
51
+ const emit = defineEmits<{
52
+ (e: 'close'): void
53
+ }>()
54
+
55
+ const lastFocusedElement = ref<HTMLElement | null>(null)
56
+
57
+ function onDocumentFocusIn(event: FocusEvent) {
58
+ if (props.open) return
59
+ const target = event.target instanceof HTMLElement ? event.target : null
60
+ if (!target || target === document.body) return
61
+ lastFocusedElement.value = target
62
+ }
63
+
64
+ onMounted(() => {
65
+ if (typeof document === 'undefined') return
66
+ document.addEventListener('focusin', onDocumentFocusIn, true)
67
+ })
68
+
69
+ onBeforeUnmount(() => {
70
+ if (typeof document === 'undefined') return
71
+ document.removeEventListener('focusin', onDocumentFocusIn, true)
72
+ })
73
+
74
+ watch(
75
+ () => props.open,
76
+ (open) => {
77
+ if (typeof window === 'undefined') return
78
+
79
+ if (open) {
80
+ const activeElement = document.activeElement
81
+ if (activeElement instanceof HTMLElement && activeElement !== document.body) {
82
+ lastFocusedElement.value = activeElement
83
+ }
84
+ }
85
+ },
86
+ )
87
+
88
+ function restoreLastFocus(attempt = 0) {
89
+ if (props.open) return
90
+ const restoreTarget = lastFocusedElement.value
91
+ window.setTimeout(() => {
92
+ if (props.open) return
93
+ restoreTarget?.focus?.()
94
+ if (document.activeElement === restoreTarget) return
95
+ if (attempt >= 4) return
96
+ restoreLastFocus(attempt + 1)
97
+ }, attempt === 0 ? 0 : 16)
98
+ }
99
+ </script>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="ngb-card flex flex-col gap-3 rounded-[var(--ngb-radius)] px-4 py-3 shadow-none sm:flex-row sm:items-center">
3
+ <div class="flex w-full min-w-0 flex-wrap items-center gap-2 sm:flex-1">
4
+ <slot name="left" />
5
+ </div>
6
+ <div class="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
7
+ <slot />
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ // Actions row for docs/registers/reports.
14
+ </script>