@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,473 @@
1
+ <template>
2
+ <div class="w-full min-w-0 overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card" :class="fillHeight ? 'flex flex-col min-h-0' : ''">
3
+ <div v-if="showPanel" class="px-4 py-3 border-b border-ngb-border flex items-center justify-between gap-3">
4
+ <div class="min-w-0">
5
+ <div class="text-sm font-semibold text-ngb-text truncate">{{ title }}</div>
6
+ <div class="text-xs text-ngb-muted truncate">{{ subtitle }}</div>
7
+ </div>
8
+
9
+ <div class="flex items-center gap-2">
10
+ <slot name="toolbar" />
11
+ </div>
12
+ </div>
13
+
14
+ <!-- Sticky header -->
15
+ <div class="bg-[var(--ngb-grid-header)] border-b border-ngb-border sticky top-0 z-10">
16
+ <div
17
+ class="grid items-center text-sm font-semibold text-ngb-text select-none"
18
+ :style="{ gridTemplateColumns }"
19
+ >
20
+ <div v-if="showStatusColumn" class="px-0 py-2 flex items-center justify-center">
21
+ <!--
22
+ Status column header is intentionally blank by default.
23
+ Some screens (e.g. Chart of Accounts) may use this as a global group expand/collapse toggle.
24
+ -->
25
+ <slot
26
+ name="statusHeader"
27
+ :has-groups="hasGroups"
28
+ :all-groups-expanded="allGroupsExpanded"
29
+ :toggle-all-groups="toggleAllGroups"
30
+ />
31
+ </div>
32
+
33
+ <div
34
+ v-for="col in visibleColumns"
35
+ :key="col.key"
36
+ class="relative px-3 py-2 whitespace-nowrap flex items-center gap-2 border-l border-ngb-border min-w-0 overflow-hidden"
37
+ :style="stickyStyle(col)"
38
+ draggable="true"
39
+ @dragstart="onDragStart(col.key)"
40
+ @dragover.prevent
41
+ @drop="onDrop(col.key)"
42
+ >
43
+ <button
44
+ v-if="col.sortable !== false"
45
+ class="flex items-center gap-1 hover:text-ngb-blue ngb-focus rounded-[var(--ngb-radius)] px-1 -mx-1 min-w-0"
46
+ @click="toggleSort(col.key, $event)"
47
+ :title="sortHint(col.key)"
48
+ >
49
+ <span class="truncate">{{ col.title }}</span>
50
+ <span v-if="sortIndex(col.key) >= 0" class="text-xs text-ngb-muted">
51
+ {{ sortBadge(col.key) }}
52
+ </span>
53
+ </button>
54
+ <span v-else>{{ col.title }}</span>
55
+
56
+ <!-- resize handle -->
57
+ <div
58
+ class="absolute right-0 top-0 h-full w-2 cursor-col-resize"
59
+ @pointerdown.prevent="startResize(col.key, $event)"
60
+ title="Resize"
61
+ />
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- viewport -->
67
+ <div
68
+ ref="viewport"
69
+ :class="fillHeight ? 'flex-1 min-h-0 overflow-auto' : 'overflow-auto'"
70
+ :style="fillHeight ? {} : { height: heightPx + 'px' }"
71
+ tabindex="0"
72
+ @scroll="onScroll"
73
+ @keydown="onKeyDown"
74
+ >
75
+ <div class="relative" :style="{ height: totalHeight + 'px' }">
76
+ <div class="absolute left-0 right-0" :style="{ transform: `translateY(${offsetTop}px)` }">
77
+ <div
78
+ v-for="(row, idx) in visibleRows"
79
+ :key="row.key"
80
+ class="border-b border-ngb-border"
81
+ >
82
+ <!-- group header -->
83
+ <button
84
+ v-if="row.type === 'group'"
85
+ class="w-full text-left grid items-center text-sm bg-ngb-card hover:bg-[var(--ngb-row-hover)]"
86
+ :style="{ gridTemplateColumns }"
87
+ @click="toggleGroup(row.groupId)"
88
+ >
89
+ <div v-if="showStatusColumn" class="px-0 py-2 flex items-center justify-center">
90
+ <span
91
+ v-if="showGroupToggleIcons"
92
+ class="inline-flex h-5 w-5 items-center justify-center text-ngb-muted"
93
+ aria-hidden="true"
94
+ >
95
+ <NgbIcon :name="isGroupExpanded(row.groupId) ? 'minus' : 'plus'" :size="14" />
96
+ </span>
97
+ </div>
98
+ <div class="px-3 py-2 flex items-center gap-2" :style="{ gridColumn: showStatusColumn ? '2 / -1' : '1 / -1' }">
99
+ <span class="font-semibold text-ngb-text">
100
+ {{ row.label }}<template v-if="showGroupCounts"> ({{ row.count }})</template>
101
+ </span>
102
+ <span v-if="showTotals" class="ml-auto text-xs text-ngb-muted tabular-nums">
103
+ D {{ fmt(row.totalDebit) }} · C {{ fmt(row.totalCredit) }}
104
+ </span>
105
+ </div>
106
+ </button>
107
+
108
+ <!-- data row -->
109
+ <div
110
+ v-else
111
+ class="grid text-sm"
112
+ :class="rowClass(row)"
113
+ :style="{ gridTemplateColumns }"
114
+ @click="onRowClick(row.key, row.__index, $event)"
115
+ >
116
+ <div v-if="showStatusColumn" class="px-3 py-2 flex items-center justify-center">
117
+ <NgbStatusIcon v-if="showRowStatusIcons" :status="inferRowStatus(row)" :title="statusTitle(row)" />
118
+ </div>
119
+
120
+ <div
121
+ v-for="(col, colIdx) in visibleColumns"
122
+ :key="col.key"
123
+ class="px-3 py-2 min-w-0 overflow-hidden border-l border-ngb-border self-stretch"
124
+ :class="cellClass(col, row[col.key])"
125
+ :style="cellStickyStyle(col)"
126
+ >
127
+ <div class="flex h-full min-w-0 gap-2 items-center" :class="cellInnerClass(col)">
128
+ <template v-if="Array.isArray(row[col.key])">
129
+ <div class="min-w-0 py-0.5 self-center" :class="col.wrap ? 'whitespace-normal break-words' : ''">
130
+ <div
131
+ v-for="(line, lineIdx) in formatCellLines(col, row[col.key], row)"
132
+ :key="lineIdx"
133
+ :class="col.wrap ? 'leading-5 whitespace-normal break-words' : 'truncate leading-5'"
134
+ >{{ line }}</div>
135
+ </div>
136
+ </template>
137
+ <template v-else>
138
+ <input
139
+ v-if="col.type === 'checkbox'"
140
+ type="checkbox"
141
+ class="h-4 w-4 rounded border-ngb-border text-ngb-blue"
142
+ :checked="asBool(row[col.key])"
143
+ :aria-label="`${col.title}: ${asBool(row[col.key]) ? 'Yes' : 'No'}`"
144
+ disabled
145
+ >
146
+ <span v-else :class="col.wrap ? 'whitespace-normal break-words leading-5' : 'truncate'">{{ formatCell(col, row[col.key], row) }}</span>
147
+ </template>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div v-if="showTotals" class="px-4 py-3 text-sm flex items-center justify-between bg-ngb-card">
157
+ <div class="text-ngb-muted">Total</div>
158
+ <div class="flex items-center gap-6 tabular-nums">
159
+ <div><span class="text-ngb-muted">Debit</span> <span class="font-semibold">{{ fmt(totals.debit) }}</span></div>
160
+ <div><span class="text-ngb-muted">Credit</span> <span class="font-semibold">{{ fmt(totals.credit) }}</span></div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </template>
165
+
166
+ <script setup lang="ts">
167
+ import { computed, ref } from 'vue';
168
+ import NgbStatusIcon from '../../primitives/NgbStatusIcon.vue';
169
+ import NgbIcon from '../../primitives/NgbIcon.vue';
170
+ import type { RegisterColumn, RegisterDataRow, RegisterSortSpec } from './registerTypes';
171
+ import { useRegisterColumnResize } from './useRegisterColumnResize';
172
+ import { useRegisterColumnState } from './useRegisterColumnState';
173
+ import { useRegisterRows } from './useRegisterRows';
174
+ import { useRegisterViewport } from './useRegisterViewport';
175
+
176
+ const props = withDefaults(defineProps<{
177
+ title?: string;
178
+ subtitle?: string;
179
+ showPanel?: boolean;
180
+ heightPx?: number;
181
+ fillHeight?: boolean;
182
+ columns: RegisterColumn[];
183
+ rows: RegisterDataRow[];
184
+ groupBy?: string[];
185
+ showTotals?: boolean;
186
+ defaultExpanded?: boolean;
187
+ storageKey?: string; // persist column order + widths + pinned/visible for demo
188
+ visibleColumnKeys?: string[];
189
+ sortBy?: RegisterSortSpec[];
190
+ selectedKeys?: string[];
191
+ // If true, single-click activates the row (emits rowActivate).
192
+ // Multi-select modifiers (Ctrl/Meta/Shift) keep selection behavior without activation.
193
+ activateOnRowClick?: boolean;
194
+ rowHeightPx?: number;
195
+ showStatusColumn?: boolean;
196
+ showGroupCounts?: boolean;
197
+ showGroupToggleIcons?: boolean;
198
+ showRowStatusIcons?: boolean;
199
+ }>(), {
200
+ showPanel: true,
201
+ heightPx: 460,
202
+ fillHeight: false,
203
+ showTotals: true,
204
+ defaultExpanded: false,
205
+ activateOnRowClick: false,
206
+ rowHeightPx: 36,
207
+ showStatusColumn: true,
208
+ showGroupCounts: true,
209
+ showGroupToggleIcons: false,
210
+ showRowStatusIcons: true,
211
+ });
212
+
213
+ const emit = defineEmits<{
214
+ (e: 'update:groupBy', v: string[]): void;
215
+ (e: 'update:visibleColumnKeys', v: string[]): void;
216
+ (e: 'update:sortBy', v: RegisterSortSpec[]): void;
217
+ (e: 'update:selectedKeys', v: string[]): void;
218
+ (e: 'rowActivate', key: string): void;
219
+ }>();
220
+
221
+ const title = computed(() => props.title ?? 'Register');
222
+ const subtitle = computed(() => props.subtitle ?? '');
223
+ const showPanel = computed(() => props.showPanel);
224
+ const heightPx = computed(() => props.heightPx);
225
+ const fillHeight = computed(() => props.fillHeight);
226
+ const showTotals = computed(() => props.showTotals);
227
+
228
+ const statusColWidth = computed(() => props.showStatusColumn ? 40 : 0); // px (icon-only status column)
229
+ const showStatusColumn = computed(() => props.showStatusColumn);
230
+ const showGroupCounts = computed(() => props.showGroupCounts);
231
+ const showGroupToggleIcons = computed(() => props.showGroupToggleIcons);
232
+ const showRowStatusIcons = computed(() => props.showRowStatusIcons);
233
+
234
+ const rowHeight = computed(() => props.rowHeightPx);
235
+ const viewport = ref<HTMLElement | null>(null);
236
+
237
+ function fmt(v: number) {
238
+ if (!v) return '—';
239
+ const n = Math.round(v * 100) / 100;
240
+ return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
241
+ }
242
+
243
+ const sortBy = computed<RegisterSortSpec[]>(() => props.sortBy ?? []);
244
+ const groupBy = computed(() => props.groupBy ?? []);
245
+ const defaultExpanded = computed(() => props.defaultExpanded);
246
+
247
+ const {
248
+ localOrder,
249
+ localWidths,
250
+ visibleColumns,
251
+ gridTemplateColumns,
252
+ colWidth,
253
+ stickyStyle,
254
+ cellStickyStyle,
255
+ } = useRegisterColumnState({
256
+ columns: computed(() => props.columns),
257
+ visibleColumnKeys: computed(() => props.visibleColumnKeys),
258
+ storageKey: computed(() => props.storageKey),
259
+ showStatusColumn,
260
+ statusColWidth,
261
+ emitVisibleColumnKeys: (value) => emit('update:visibleColumnKeys', value),
262
+ });
263
+
264
+ const { startResize } = useRegisterColumnResize({
265
+ columns: computed(() => props.columns),
266
+ localWidths,
267
+ colWidth,
268
+ });
269
+
270
+ const {
271
+ hasGroups,
272
+ displayRows,
273
+ dataRows,
274
+ totals,
275
+ allGroupsExpanded,
276
+ isGroupExpanded,
277
+ toggleGroup,
278
+ toggleAllGroups,
279
+ inferRowStatus,
280
+ } = useRegisterRows({
281
+ rows: computed(() => props.rows),
282
+ groupBy,
283
+ sortBy,
284
+ defaultExpanded,
285
+ });
286
+
287
+ const { onScroll, totalHeight, offsetTop, visibleRows } = useRegisterViewport({
288
+ viewport,
289
+ heightPx,
290
+ rowHeight,
291
+ displayRows,
292
+ });
293
+
294
+ function sortIndex(key: string) {
295
+ return sortBy.value.findIndex(s => s.key === key);
296
+ }
297
+ function sortBadge(key: string) {
298
+ const i = sortIndex(key);
299
+ if (i < 0) return '';
300
+ const dir = sortBy.value[i].dir === 'asc' ? '↑' : '↓';
301
+ return `${i + 1}${dir}`;
302
+ }
303
+ function sortHint(key: string) {
304
+ return 'Click: sort · Shift+Click: multi-sort';
305
+ }
306
+ function toggleSort(key: string, ev: MouseEvent) {
307
+ const multi = ev.shiftKey;
308
+ const current = [...sortBy.value];
309
+ const idx = current.findIndex(s => s.key === key);
310
+ if (idx < 0) {
311
+ const next = multi ? [...current, { key, dir: 'asc' as const }] : [{ key, dir: 'asc' as const }];
312
+ emit('update:sortBy', next);
313
+ return;
314
+ }
315
+ const dir = current[idx].dir === 'asc' ? 'desc' : 'asc';
316
+ current[idx] = { key, dir };
317
+ emit('update:sortBy', multi ? current : [current[idx]]);
318
+ }
319
+
320
+ /* ---------------- Selection + keyboard ---------------- */
321
+ const selected = computed(() => new Set(props.selectedKeys ?? []));
322
+ const activeIndex = ref<number>(0);
323
+ const lastSelectedIndex = ref<number | null>(null);
324
+
325
+ function rowClass(row: RegisterDataRow) {
326
+ const key = row.key;
327
+ const isSel = selected.value.has(key);
328
+ const isMarked = inferRowStatus(row) === 'marked';
329
+ return [
330
+ 'hover:bg-[var(--ngb-row-hover)] cursor-pointer',
331
+ isSel ? 'bg-[var(--ngb-row-selected)]' : 'bg-ngb-card',
332
+ isMarked ? 'opacity-70' : '',
333
+ ].join(' ');
334
+ }
335
+
336
+ function statusTitle(row: RegisterDataRow): string | undefined {
337
+ return typeof row.__statusTitle === 'string' && row.__statusTitle.trim()
338
+ ? row.__statusTitle
339
+ : undefined;
340
+ }
341
+
342
+ function onRowClick(key: string, index: number, ev: MouseEvent) {
343
+ // ignore group rows (they don't call this)
344
+ const mult = ev.metaKey || ev.ctrlKey;
345
+ const range = ev.shiftKey;
346
+
347
+ const current = new Set(selected.value);
348
+
349
+ if (range && lastSelectedIndex.value != null) {
350
+ const a = Math.min(lastSelectedIndex.value, index);
351
+ const b = Math.max(lastSelectedIndex.value, index);
352
+ const rowKeys = dataRows.value.map((row) => row.key);
353
+ for (let i = a; i <= b && i < rowKeys.length; i++) current.add(rowKeys[i]);
354
+ } else if (mult) {
355
+ if (current.has(key)) current.delete(key);
356
+ else current.add(key);
357
+ lastSelectedIndex.value = index;
358
+ } else {
359
+ current.clear();
360
+ current.add(key);
361
+ lastSelectedIndex.value = index;
362
+ }
363
+
364
+ activeIndex.value = index;
365
+ emit('update:selectedKeys', Array.from(current));
366
+
367
+ // Activation: open on single-click when enabled.
368
+ if (props.activateOnRowClick && !mult && !range) {
369
+ emit('rowActivate', key);
370
+ }
371
+ }
372
+
373
+ function onKeyDown(ev: KeyboardEvent) {
374
+ const rowKeys = dataRows.value;
375
+ if (!rowKeys.length) return;
376
+
377
+ const clamp = (v: number) => Math.max(0, Math.min(rowKeys.length - 1, v));
378
+
379
+ if (ev.key === 'ArrowDown') {
380
+ ev.preventDefault();
381
+ activeIndex.value = clamp(activeIndex.value + 1);
382
+ scrollToActive();
383
+ return;
384
+ }
385
+ if (ev.key === 'ArrowUp') {
386
+ ev.preventDefault();
387
+ activeIndex.value = clamp(activeIndex.value - 1);
388
+ scrollToActive();
389
+ return;
390
+ }
391
+ if (ev.key === 'Enter') {
392
+ ev.preventDefault();
393
+ const key = rowKeys[activeIndex.value]?.key;
394
+ if (key) emit('rowActivate', key);
395
+ return;
396
+ }
397
+ if (ev.key === ' ' || ev.key === 'Spacebar') {
398
+ ev.preventDefault();
399
+ const key = rowKeys[activeIndex.value]?.key;
400
+ if (!key) return;
401
+ const current = new Set(selected.value);
402
+ if (current.has(key)) current.delete(key);
403
+ else current.add(key);
404
+ emit('update:selectedKeys', Array.from(current));
405
+ }
406
+ }
407
+
408
+ function scrollToActive() {
409
+ const vp = viewport.value;
410
+ if (!vp) return;
411
+ const top = activeIndex.value * rowHeight.value;
412
+ const bottom = top + rowHeight.value;
413
+ if (top < vp.scrollTop) vp.scrollTop = top;
414
+ else if (bottom > vp.scrollTop + vp.clientHeight) vp.scrollTop = bottom - vp.clientHeight;
415
+ }
416
+
417
+ /* ---------------- Cell formatting ---------------- */
418
+ function formatCell(col: RegisterColumn, v: unknown, row: RegisterDataRow) {
419
+ if (col.format) return col.format(v, row);
420
+ if (v == null || v === '') return '—';
421
+ if (typeof v === 'number') {
422
+ if (col.align === 'right') return fmt(v);
423
+ return String(v);
424
+ }
425
+ return String(v);
426
+ }
427
+
428
+ function formatCellLines(col: RegisterColumn, v: unknown, row: RegisterDataRow): string[] {
429
+ if (Array.isArray(v)) {
430
+ const lines = v
431
+ .map((item) => formatCell(col, item, row))
432
+ .filter((item) => item && item !== '—');
433
+ return lines.length > 0 ? lines : ['—'];
434
+ }
435
+
436
+ return [formatCell(col, v, row)];
437
+ }
438
+
439
+ function asBool(v: unknown): boolean {
440
+ return v === true || v === 'true' || v === 1 || v === '1';
441
+ }
442
+
443
+ function cellClass(col: RegisterColumn, v: unknown) {
444
+ const align = col.align === 'right' ? 'text-right tabular-nums' : col.align === 'center' ? 'text-center' : '';
445
+ const neg = typeof v === 'number' && v < 0 ? 'text-ngb-danger' : '';
446
+ return [align, neg].filter(Boolean).join(' ');
447
+ }
448
+
449
+ function cellInnerClass(col: RegisterColumn) {
450
+ if (col.align === 'right') return 'justify-end';
451
+ if (col.align === 'center') return 'justify-center';
452
+ return '';
453
+ }
454
+
455
+ /* ---------------- Column reorder (drag) ---------------- */
456
+ const dragKey = ref<string | null>(null);
457
+
458
+ function onDragStart(key: string) {
459
+ dragKey.value = key;
460
+ }
461
+
462
+ function onDrop(targetKey: string) {
463
+ if (!dragKey.value || dragKey.value === targetKey) return;
464
+ const order = [...localOrder.value];
465
+ const from = order.indexOf(dragKey.value);
466
+ const to = order.indexOf(targetKey);
467
+ if (from < 0 || to < 0) return;
468
+ order.splice(from, 1);
469
+ order.splice(to, 0, dragKey.value);
470
+ localOrder.value = order;
471
+ dragKey.value = null;
472
+ }
473
+ </script>
@@ -0,0 +1,51 @@
1
+ export type RegisterColumn = {
2
+ key: string;
3
+ title: string;
4
+ width?: number;
5
+ minWidth?: number;
6
+ align?: 'left' | 'right' | 'center';
7
+ pinned?: 'left';
8
+ sortable?: boolean;
9
+ wrap?: boolean;
10
+ type?: 'text' | 'checkbox';
11
+ format?: (value: unknown, row: RegisterDataRow) => string;
12
+ };
13
+
14
+ export type RegisterDataRow = Record<string, unknown> & {
15
+ key: string;
16
+ debit?: number;
17
+ credit?: number;
18
+ __status?: 'active' | 'saved' | 'posted' | 'marked';
19
+ __statusTitle?: string;
20
+ isMarkedForDeletion?: boolean;
21
+ isDeleted?: boolean;
22
+ isActive?: boolean;
23
+ };
24
+
25
+ export type RegisterSortSpec = {
26
+ key: string;
27
+ dir: 'asc' | 'desc';
28
+ };
29
+
30
+ export type DisplayGroupRow = {
31
+ type: 'group';
32
+ key: string;
33
+ groupId: string;
34
+ label: string;
35
+ count: number;
36
+ totalDebit: number;
37
+ totalCredit: number;
38
+ };
39
+
40
+ export type DisplayDataRow = RegisterDataRow & {
41
+ type: 'row';
42
+ __index: number;
43
+ };
44
+
45
+ export type DisplayRow = DisplayGroupRow | DisplayDataRow;
46
+
47
+ export type RowStatus = 'active' | 'saved' | 'posted' | 'marked';
48
+
49
+ export function isDisplayDataRow(row: DisplayRow): row is DisplayDataRow {
50
+ return row.type === 'row';
51
+ }
@@ -0,0 +1,99 @@
1
+ import { onBeforeUnmount, type ComputedRef, type Ref } from 'vue';
2
+ import type { RegisterColumn } from './registerTypes';
3
+
4
+ type UseRegisterColumnResizeArgs = {
5
+ columns: ComputedRef<RegisterColumn[]>;
6
+ localWidths: Ref<Record<string, number>>;
7
+ colWidth: (column: RegisterColumn) => number;
8
+ };
9
+
10
+ type ResizeSession = {
11
+ key: string;
12
+ pointerId: number;
13
+ startX: number;
14
+ startWidth: number;
15
+ minWidth: number;
16
+ handle: HTMLElement | null;
17
+ controller: AbortController;
18
+ };
19
+
20
+ export function useRegisterColumnResize(args: UseRegisterColumnResizeArgs) {
21
+ let activeSession: ResizeSession | null = null;
22
+
23
+ function setPointerCaptureSafely(handle: HTMLElement | null, pointerId: number) {
24
+ if (!handle?.setPointerCapture) return;
25
+
26
+ try {
27
+ handle.setPointerCapture(pointerId);
28
+ } catch {
29
+ // Firefox can reject synthetic pointer ids in tests; resizing still works without capture.
30
+ }
31
+ }
32
+
33
+ function releasePointerCaptureSafely(handle: HTMLElement | null, pointerId: number) {
34
+ if (!handle?.releasePointerCapture) return;
35
+
36
+ try {
37
+ handle.releasePointerCapture(pointerId);
38
+ } catch {
39
+ // Ignore invalid-pointer errors during teardown and fall back to listener cleanup.
40
+ }
41
+ }
42
+
43
+ function stopResize() {
44
+ if (!activeSession) return;
45
+ activeSession.controller.abort();
46
+ releasePointerCaptureSafely(activeSession.handle, activeSession.pointerId);
47
+ activeSession = null;
48
+ }
49
+
50
+ function startResize(key: string, event: PointerEvent) {
51
+ const column = args.columns.value.find((entry) => entry.key === key);
52
+ if (!column) return;
53
+
54
+ stopResize();
55
+
56
+ const controller = new AbortController();
57
+ const handle = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
58
+ const session: ResizeSession = {
59
+ key,
60
+ pointerId: event.pointerId,
61
+ startX: event.clientX,
62
+ startWidth: args.colWidth(column),
63
+ minWidth: column.minWidth ?? 80,
64
+ handle,
65
+ controller,
66
+ };
67
+
68
+ activeSession = session;
69
+ setPointerCaptureSafely(handle, event.pointerId);
70
+
71
+ const onMove = (nextEvent: PointerEvent) => {
72
+ if (!activeSession || nextEvent.pointerId !== session.pointerId) return;
73
+ const delta = nextEvent.clientX - session.startX;
74
+ const nextWidth = Math.max(session.minWidth, session.startWidth + delta);
75
+ args.localWidths.value = {
76
+ ...args.localWidths.value,
77
+ [session.key]: nextWidth,
78
+ };
79
+ };
80
+
81
+ const onFinish = () => {
82
+ stopResize();
83
+ };
84
+
85
+ const target: HTMLElement | Window = handle ?? window;
86
+ target.addEventListener('pointermove', onMove, { signal: controller.signal });
87
+ target.addEventListener('pointerup', onFinish, { signal: controller.signal });
88
+ target.addEventListener('pointercancel', onFinish, { signal: controller.signal });
89
+ }
90
+
91
+ onBeforeUnmount(() => {
92
+ stopResize();
93
+ });
94
+
95
+ return {
96
+ startResize,
97
+ stopResize,
98
+ };
99
+ }