@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,66 @@
1
+ import { inject, provide, reactive } from 'vue';
2
+
3
+ export type ToastTone = 'neutral' | 'success' | 'warn' | 'danger';
4
+
5
+ export type Toast = {
6
+ id: string;
7
+ title: string;
8
+ message?: string;
9
+ tone?: ToastTone;
10
+ timeoutMs?: number;
11
+ };
12
+
13
+ export type ToastApi = {
14
+ toasts: Toast[];
15
+ push: (t: Omit<Toast, 'id'>) => void;
16
+ remove: (id: string) => void;
17
+ };
18
+
19
+ const KEY = Symbol('ngb-toasts');
20
+
21
+ export function provideToasts(): ToastApi {
22
+ const state = reactive({
23
+ toasts: [] as Toast[],
24
+ });
25
+
26
+ function remove(id: string) {
27
+ state.toasts = state.toasts.filter(t => t.id !== id);
28
+ }
29
+
30
+ function push(t: Omit<Toast, 'id'>) {
31
+ const id = crypto?.randomUUID?.() ?? String(Date.now() + Math.random());
32
+ const toast: Toast = {
33
+ id,
34
+ tone: 'neutral',
35
+ timeoutMs: 3500,
36
+ ...t,
37
+ };
38
+ state.toasts = [toast, ...state.toasts].slice(0, 5);
39
+
40
+ const timeout = toast.timeoutMs ?? 0;
41
+ if (timeout > 0) {
42
+ window.setTimeout(() => remove(id), timeout);
43
+ }
44
+ }
45
+
46
+ const api: ToastApi = {
47
+ get toasts() {
48
+ return state.toasts;
49
+ },
50
+ push,
51
+ remove,
52
+ };
53
+
54
+ provide(KEY, api);
55
+ return api;
56
+ }
57
+
58
+ export function useToasts(): ToastApi {
59
+ const api = inject<ToastApi>(KEY);
60
+ if (!api) throw new Error('useToasts(): missing provideToasts()');
61
+ return api;
62
+ }
63
+
64
+ export function useOptionalToasts(): ToastApi | null {
65
+ return inject<ToastApi>(KEY, null);
66
+ }
@@ -0,0 +1,85 @@
1
+ import { computed, onBeforeUnmount, ref, watch, type ComputedRef } from 'vue'
2
+
3
+ type UseAsyncComboboxQueryArgs<TItem> = {
4
+ disabled: ComputedRef<boolean>
5
+ items: ComputedRef<readonly TItem[]>
6
+ emitQuery: (query: string) => void
7
+ debounceMs?: number
8
+ }
9
+
10
+ type ResetOptions = {
11
+ emitEmptyQuery?: boolean
12
+ }
13
+
14
+ export function useAsyncComboboxQuery<TItem>(args: UseAsyncComboboxQueryArgs<TItem>) {
15
+ const query = ref('')
16
+ const pendingEmit = ref(false)
17
+ const pendingResults = ref(false)
18
+ const lastSentQuery = ref('')
19
+ const debounceMs = args.debounceMs ?? 220
20
+
21
+ let timer: ReturnType<typeof setTimeout> | null = null
22
+
23
+ const isSearching = computed(() => pendingEmit.value || pendingResults.value)
24
+
25
+ function clearTimer(): void {
26
+ if (!timer) return
27
+ clearTimeout(timer)
28
+ timer = null
29
+ }
30
+
31
+ function clearPendingState(): void {
32
+ pendingEmit.value = false
33
+ pendingResults.value = false
34
+ lastSentQuery.value = ''
35
+ }
36
+
37
+ function resetQueryState(options: ResetOptions = {}): void {
38
+ query.value = ''
39
+ clearTimer()
40
+ clearPendingState()
41
+ if (options.emitEmptyQuery) args.emitQuery('')
42
+ }
43
+
44
+ function onInput(value: string): void {
45
+ if (args.disabled.value) return
46
+
47
+ query.value = value
48
+ const trimmed = value.trim()
49
+
50
+ if (!trimmed) {
51
+ resetQueryState({ emitEmptyQuery: true })
52
+ return
53
+ }
54
+
55
+ pendingEmit.value = true
56
+ pendingResults.value = false
57
+
58
+ clearTimer()
59
+ timer = setTimeout(() => {
60
+ pendingEmit.value = false
61
+ pendingResults.value = true
62
+ lastSentQuery.value = query.value
63
+ args.emitQuery(query.value)
64
+ }, debounceMs)
65
+ }
66
+
67
+ watch(
68
+ () => args.items.value,
69
+ () => {
70
+ if (pendingResults.value && query.value === lastSentQuery.value) {
71
+ pendingResults.value = false
72
+ }
73
+ },
74
+ )
75
+
76
+ onBeforeUnmount(() => clearTimer())
77
+
78
+ return {
79
+ query,
80
+ isSearching,
81
+ clearPendingState,
82
+ resetQueryState,
83
+ onInput,
84
+ }
85
+ }
@@ -0,0 +1,129 @@
1
+ import { computed, onBeforeUnmount, onMounted, ref, type ComponentPublicInstance, type Ref } from 'vue'
2
+
3
+ type FloatingHost = Element | ComponentPublicInstance | null
4
+ type FloatingHostRef = Ref<FloatingHost>
5
+
6
+ const DROPDOWN_GAP_PX = 8
7
+ const VIEWPORT_PADDING_PX = 8
8
+ const DEFAULT_DROPDOWN_HEIGHT_PX = 288
9
+
10
+ function resolveHostElement(host: FloatingHost | undefined): HTMLElement | null {
11
+ const raw = host && '$el' in host ? host.$el : host
12
+ return raw instanceof HTMLElement ? raw : null
13
+ }
14
+
15
+ export function useFloatingDropdownPosition(anchorRef: FloatingHostRef, overlayRefs: FloatingHostRef[]) {
16
+ const floatingLeft = ref(0)
17
+ const floatingTop = ref(0)
18
+ const floatingWidth = ref(0)
19
+ const floatingMaxHeight = ref(DEFAULT_DROPDOWN_HEIGHT_PX)
20
+ let rafId: number | null = null
21
+
22
+ const floatingStyle = computed(() => ({
23
+ left: `${floatingLeft.value}px`,
24
+ top: `${floatingTop.value}px`,
25
+ width: `${floatingWidth.value}px`,
26
+ maxHeight: `${floatingMaxHeight.value}px`,
27
+ }))
28
+
29
+ function resolveViewportBounds() {
30
+ if (typeof window === 'undefined') {
31
+ return {
32
+ width: 0,
33
+ height: 0,
34
+ }
35
+ }
36
+
37
+ const viewport = window.visualViewport
38
+ return {
39
+ width: Math.round(viewport?.width ?? window.innerWidth ?? Number.MAX_SAFE_INTEGER),
40
+ height: Math.round(viewport?.height ?? window.innerHeight ?? Number.MAX_SAFE_INTEGER),
41
+ }
42
+ }
43
+
44
+ function resolveOverlayHeight() {
45
+ const overlayEl = overlayRefs
46
+ .map((overlayRef) => resolveHostElement(overlayRef.value))
47
+ .find((element) => !!element)
48
+
49
+ const renderedHeight = overlayEl?.offsetHeight ?? 0
50
+ return renderedHeight > 0 ? renderedHeight : DEFAULT_DROPDOWN_HEIGHT_PX
51
+ }
52
+
53
+ function computePosition() {
54
+ const anchorEl = resolveHostElement(anchorRef.value)
55
+ if (!anchorEl) return
56
+
57
+ const rect = anchorEl.getBoundingClientRect()
58
+ const viewport = resolveViewportBounds()
59
+ const overlayHeight = resolveOverlayHeight()
60
+ const maxWidth = Math.max(0, viewport.width - VIEWPORT_PADDING_PX * 2)
61
+ const width = Math.max(0, Math.min(Math.round(rect.width), maxWidth))
62
+ const left = Math.min(
63
+ Math.max(Math.round(rect.left), VIEWPORT_PADDING_PX),
64
+ Math.max(VIEWPORT_PADDING_PX, viewport.width - VIEWPORT_PADDING_PX - width),
65
+ )
66
+
67
+ const availableBelow = Math.max(0, viewport.height - rect.bottom - DROPDOWN_GAP_PX - VIEWPORT_PADDING_PX)
68
+ const availableAbove = Math.max(0, rect.top - DROPDOWN_GAP_PX - VIEWPORT_PADDING_PX)
69
+ const shouldOpenAbove = availableAbove > availableBelow && availableBelow < overlayHeight
70
+ const availableHeight = shouldOpenAbove ? availableAbove : availableBelow
71
+ const maxHeight = Math.max(0, Math.min(DEFAULT_DROPDOWN_HEIGHT_PX, Math.round(availableHeight)))
72
+ const renderedHeight = Math.min(overlayHeight, maxHeight || overlayHeight)
73
+ const top = shouldOpenAbove
74
+ ? Math.max(VIEWPORT_PADDING_PX, Math.round(rect.top - DROPDOWN_GAP_PX - renderedHeight))
75
+ : Math.round(rect.bottom + DROPDOWN_GAP_PX)
76
+
77
+ floatingLeft.value = left
78
+ floatingTop.value = top
79
+ floatingWidth.value = width
80
+ floatingMaxHeight.value = maxHeight
81
+ }
82
+
83
+ function schedulePositionRecalc() {
84
+ if (typeof window === 'undefined') return
85
+ if (typeof window.requestAnimationFrame !== 'function') {
86
+ computePosition()
87
+ return
88
+ }
89
+ if (rafId != null && typeof window.cancelAnimationFrame === 'function') {
90
+ window.cancelAnimationFrame(rafId)
91
+ }
92
+ rafId = window.requestAnimationFrame(() => {
93
+ rafId = null
94
+ computePosition()
95
+ })
96
+ }
97
+
98
+ function updatePosition() {
99
+ computePosition()
100
+ schedulePositionRecalc()
101
+ }
102
+
103
+ function onWindowChange() {
104
+ const hasVisibleOverlay = overlayRefs.some((overlayRef) => !!resolveHostElement(overlayRef.value))
105
+ if (!hasVisibleOverlay) return
106
+ computePosition()
107
+ }
108
+
109
+ onMounted(() => {
110
+ if (typeof window === 'undefined') return
111
+ window.addEventListener('resize', onWindowChange)
112
+ window.addEventListener('scroll', onWindowChange, true)
113
+ })
114
+
115
+ onBeforeUnmount(() => {
116
+ if (typeof window === 'undefined') return
117
+ window.removeEventListener('resize', onWindowChange)
118
+ window.removeEventListener('scroll', onWindowChange, true)
119
+ if (rafId != null && typeof window.cancelAnimationFrame === 'function') {
120
+ window.cancelAnimationFrame(rafId)
121
+ }
122
+ rafId = null
123
+ })
124
+
125
+ return {
126
+ floatingStyle,
127
+ updatePosition,
128
+ }
129
+ }
@@ -0,0 +1,106 @@
1
+ <script setup lang="ts" generic="TItem">
2
+ import type { VNodeChild } from 'vue'
3
+ import NgbButton from '../primitives/NgbButton.vue'
4
+ import NgbIcon from '../primitives/NgbIcon.vue'
5
+
6
+ type ComposerCollectionColumn = {
7
+ title: string
8
+ width?: string
9
+ }
10
+
11
+ const props = withDefaults(defineProps<{
12
+ title: string
13
+ addLabel: string
14
+ items: TItem[]
15
+ columns: ComposerCollectionColumn[]
16
+ emptyMessage: string
17
+ section: string
18
+ rowKey: (item: TItem, index: number) => string
19
+ addDisabled?: boolean
20
+ tableClass?: string
21
+ }>(), {
22
+ addDisabled: false,
23
+ tableClass: 'w-full table-fixed text-sm',
24
+ })
25
+
26
+ const emit = defineEmits<{
27
+ (e: 'add'): void
28
+ (e: 'remove', index: number): void
29
+ (e: 'dragstart', payload: { section: string; index: number; event: DragEvent }): void
30
+ (e: 'dragover', event: DragEvent): void
31
+ (e: 'drop', payload: { section: string; index: number; event: DragEvent }): void
32
+ }>()
33
+
34
+ defineSlots<{
35
+ cells(props: { item: TItem; index: number }): VNodeChild
36
+ }>()
37
+ </script>
38
+
39
+ <template>
40
+ <div class="space-y-4">
41
+ <div class="flex items-center justify-between gap-3">
42
+ <div class="text-xs font-semibold uppercase tracking-wide text-ngb-muted">{{ title }}</div>
43
+ <NgbButton size="sm" :disabled="addDisabled" @click="emit('add')">
44
+ <NgbIcon name="plus" :size="16" />
45
+ <span>{{ addLabel }}</span>
46
+ </NgbButton>
47
+ </div>
48
+
49
+ <div class="overflow-x-auto overflow-y-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card">
50
+ <table v-if="items.length > 0" :class="tableClass">
51
+ <colgroup>
52
+ <col style="width: 28px" />
53
+ <col v-for="(column, index) in columns" :key="`${title}:col:${index}`" :style="column.width ? `width: ${column.width}` : undefined" />
54
+ <col style="width: 40px" />
55
+ </colgroup>
56
+ <thead class="bg-ngb-bg text-xs text-ngb-muted">
57
+ <tr>
58
+ <th class="px-2 py-2"></th>
59
+ <th
60
+ v-for="(column, index) in columns"
61
+ :key="`${title}:head:${index}`"
62
+ class="border-r border-dotted border-ngb-border px-3 py-2 text-left font-semibold truncate"
63
+ >
64
+ {{ column.title }}
65
+ </th>
66
+ <th class="px-2 py-2"></th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ <tr
71
+ v-for="(item, index) in items"
72
+ :key="rowKey(item, index)"
73
+ class="border-t border-ngb-border transition-colors hover:bg-ngb-bg align-top"
74
+ draggable="true"
75
+ @dragstart="emit('dragstart', { section, index, event: $event })"
76
+ @dragover="emit('dragover', $event)"
77
+ @drop="emit('drop', { section, index, event: $event })"
78
+ >
79
+ <td class="px-1 py-1 align-middle">
80
+ <div class="flex h-8 w-6 items-center justify-center text-ngb-muted cursor-grab active:cursor-grabbing" title="Drag to reorder">
81
+ <NgbIcon name="grip-vertical" :size="16" />
82
+ </div>
83
+ </td>
84
+
85
+ <slot name="cells" :item="item" :index="index" />
86
+
87
+ <td class="px-1 py-1 align-middle">
88
+ <button
89
+ type="button"
90
+ class="flex h-8 w-8 items-center justify-center rounded-[var(--ngb-radius)] text-ngb-muted hover:bg-ngb-bg hover:text-ngb-text ngb-focus"
91
+ title="Delete"
92
+ @click="emit('remove', index)"
93
+ >
94
+ <NgbIcon name="trash" :size="16" />
95
+ </button>
96
+ </td>
97
+ </tr>
98
+ </tbody>
99
+ </table>
100
+
101
+ <div v-else class="px-4 py-3 text-sm text-ngb-muted">
102
+ {{ emptyMessage }}
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </template>