@ngbplatform/ui 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,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>
|