@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,13 @@
1
+ <template>
2
+ <div class="space-y-4">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ // Wrapper for form sections.
9
+ </script>
10
+
11
+ <style scoped>
12
+ /* no-op */
13
+ </style>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div class="grid grid-cols-1 items-start gap-3" :class="dense ? 'sm:grid-cols-[180px_minmax(0,1fr)]' : 'sm:grid-cols-[220px_minmax(0,1fr)]'">
3
+ <div class="pt-0 sm:pt-2">
4
+ <div class="text-sm font-medium text-ngb-text">{{ label }}</div>
5
+ <div v-if="hint" class="text-xs text-ngb-muted mt-1">{{ hint }}</div>
6
+ </div>
7
+ <div class="min-w-0">
8
+ <slot />
9
+ <div v-if="error" class="text-xs text-ngb-danger mt-1">{{ error }}</div>
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ defineProps<{
16
+ label: string;
17
+ hint?: string;
18
+ error?: string;
19
+ dense?: boolean;
20
+ }>();
21
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <section class="ngb-card p-4">
3
+ <div v-if="title" class="text-sm font-semibold text-ngb-text">{{ title }}</div>
4
+ <div v-if="description" class="text-xs text-ngb-muted mt-1">{{ description }}</div>
5
+ <div class="mt-4 space-y-3">
6
+ <slot />
7
+ </div>
8
+ </section>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ defineProps<{
13
+ title?: string;
14
+ description?: string;
15
+ }>();
16
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div
3
+ v-if="messages?.length"
4
+ role="alert"
5
+ aria-live="assertive"
6
+ aria-atomic="true"
7
+ class="rounded-[var(--ngb-radius)] border border-ngb-border bg-[#FFFBEB] dark:bg-[rgba(251,191,36,.10)] dark:border-[rgba(251,191,36,.25)] px-4 py-3"
8
+ >
9
+ <div class="text-sm font-semibold text-ngb-text">Please fix the following</div>
10
+ <ul class="mt-2 space-y-1 text-sm text-ngb-muted list-disc pl-5">
11
+ <li v-for="(m, idx) in messages" :key="idx">{{ m }}</li>
12
+ </ul>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ defineProps<{ messages: string[] }>();
18
+ </script>
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <aside class="w-full h-full bg-white border-r border-ngb-border">
3
+ <div class="p-3 border-b border-ngb-border">
4
+ <div class="text-xs font-semibold text-ngb-muted mb-2">Navigation</div>
5
+ <input
6
+ v-model="q"
7
+ class="h-9 w-full rounded-[var(--ngb-radius)] border border-ngb-border bg-white px-3 text-sm text-ngb-text placeholder:text-ngb-muted/70 ngb-focus"
8
+ placeholder="Search…"
9
+ aria-label="Search navigation"
10
+ />
11
+ </div>
12
+
13
+ <div class="p-2 overflow-auto h-[calc(100%-61px)]" role="tree" aria-label="Navigation">
14
+ <NgbTreeNode
15
+ v-for="n in visibleNodes"
16
+ :key="n.id"
17
+ :node="n"
18
+ :level="0"
19
+ :expanded="expanded"
20
+ :selectedId="modelValue ?? null"
21
+ @toggle="toggle"
22
+ @select="select"
23
+ />
24
+ </div>
25
+ </aside>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed, ref } from 'vue';
30
+ import NgbTreeNode, { type NavNode } from './NgbTreeNode.vue';
31
+
32
+ const props = defineProps<{
33
+ nodes: NavNode[];
34
+ modelValue?: string | null;
35
+ }>();
36
+
37
+ const emit = defineEmits<{
38
+ (e: 'update:modelValue', v: string | null): void;
39
+ (e: 'select', node: NavNode): void;
40
+ }>();
41
+
42
+ const q = ref('');
43
+ const expanded = ref<Set<string>>(new Set(['documents', 'catalogs', 'reports']));
44
+
45
+ function toggle(id: string) {
46
+ const s = new Set(expanded.value);
47
+ if (s.has(id)) s.delete(id);
48
+ else s.add(id);
49
+ expanded.value = s;
50
+ }
51
+
52
+ function select(node: NavNode) {
53
+ emit('update:modelValue', node.id);
54
+ emit('select', node);
55
+ }
56
+
57
+ function matchNode(node: NavNode, query: string): boolean {
58
+ const hit = node.label.toLowerCase().includes(query);
59
+ const childHit = (node.children ?? []).some(c => matchNode(c, query));
60
+ return hit || childHit;
61
+ }
62
+
63
+ const visibleNodes = computed(() => {
64
+ const query = q.value.trim().toLowerCase();
65
+ if (!query) return props.nodes;
66
+ return props.nodes.filter(n => matchNode(n, query));
67
+ });
68
+ </script>
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <div class="select-none">
3
+ <div
4
+ role="treeitem"
5
+ tabindex="0"
6
+ :aria-level="level + 1"
7
+ :aria-selected="selectedId === node.id"
8
+ :aria-expanded="hasChildren ? isExpanded : undefined"
9
+ class="h-9 rounded-[var(--ngb-radius)] flex items-center gap-2 px-2 text-sm cursor-pointer"
10
+ :class="selectedId === node.id ? 'bg-[#F1F5F9] border border-[#E2E8F0]' : 'hover:bg-[#F8FAFC]'"
11
+ :style="{ paddingLeft: `${8 + level * 14}px` }"
12
+ @click="$emit('select', node)"
13
+ @keydown="onRowKeyDown"
14
+ >
15
+ <button
16
+ v-if="hasChildren"
17
+ type="button"
18
+ class="w-6 h-6 inline-flex items-center justify-center rounded hover:bg-[#F1F5F9] text-ngb-muted"
19
+ @click.stop="$emit('toggle', node.id)"
20
+ aria-label="Toggle"
21
+ >
22
+ {{ isExpanded ? '▾' : '▸' }}
23
+ </button>
24
+ <span v-else class="w-6 h-6 inline-flex items-center justify-center text-ngb-muted">•</span>
25
+
26
+ <span class="truncate flex-1 text-ngb-text">{{ node.label }}</span>
27
+
28
+ <span
29
+ v-if="node.badge"
30
+ class="text-[11px] font-semibold px-2 py-0.5 rounded-full border border-ngb-border text-ngb-muted"
31
+ >
32
+ {{ node.badge }}
33
+ </span>
34
+ </div>
35
+
36
+ <div v-if="hasChildren && isExpanded" class="mt-1" role="group">
37
+ <NgbTreeNode
38
+ v-for="c in node.children"
39
+ :key="c.id"
40
+ :node="c"
41
+ :level="level + 1"
42
+ :expanded="expanded"
43
+ :selectedId="selectedId"
44
+ @toggle="$emit('toggle', $event)"
45
+ @select="$emit('select', $event)"
46
+ />
47
+ </div>
48
+ </div>
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ import { computed } from 'vue';
53
+
54
+ export type NavNode = {
55
+ id: string;
56
+ label: string;
57
+ children?: NavNode[];
58
+ badge?: string;
59
+ };
60
+
61
+ const props = defineProps<{
62
+ node: NavNode;
63
+ level: number;
64
+ expanded: Set<string>;
65
+ selectedId: string | null;
66
+ }>();
67
+
68
+ const emit = defineEmits<{
69
+ (e: 'toggle', id: string): void;
70
+ (e: 'select', node: NavNode): void;
71
+ }>();
72
+
73
+ const hasChildren = computed(() => !!(props.node.children && props.node.children.length));
74
+ const isExpanded = computed(() => props.expanded.has(props.node.id));
75
+
76
+ function onRowKeyDown(event: KeyboardEvent) {
77
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
78
+ event.preventDefault();
79
+ emit('select', props.node);
80
+ return;
81
+ }
82
+
83
+ if (event.key === 'ArrowRight' && hasChildren.value && !isExpanded.value) {
84
+ event.preventDefault();
85
+ emit('toggle', props.node.id);
86
+ return;
87
+ }
88
+
89
+ if (event.key === 'ArrowLeft' && hasChildren.value && isExpanded.value) {
90
+ event.preventDefault();
91
+ emit('toggle', props.node.id);
92
+ }
93
+ }
94
+ </script>
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <div class="relative" ref="root">
3
+ <NgbButton variant="ghost" @click="open = !open">Columns ▾</NgbButton>
4
+ <div v-if="open" class="absolute right-0 mt-2 w-72 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card p-2 z-10">
5
+ <div class="px-2 py-2 text-sm font-semibold text-ngb-text">Columns</div>
6
+ <div class="max-h-[320px] overflow-auto px-2 pb-2">
7
+ <label v-for="c in columns" :key="c.id" class="flex items-center gap-2 py-1.5 text-sm">
8
+ <input type="checkbox" :checked="visibleSet.has(c.id)" @change="toggle(c.id)" />
9
+ <span class="flex-1">{{ c.label }}</span>
10
+ </label>
11
+ </div>
12
+ <div class="px-2 pt-2 border-t border-ngb-border flex items-center justify-between gap-2">
13
+ <button class="text-sm text-ngb-muted hover:text-ngb-text" @click="reset">Reset</button>
14
+ <div class="flex items-center gap-2">
15
+ <NgbButton variant="secondary" @click="open = false">Close</NgbButton>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
24
+ import NgbButton from '../../primitives/NgbButton.vue';
25
+ import { loadJson, saveJson } from '../../utils/storage';
26
+
27
+ export type ColumnDef = { id: string; label: string; };
28
+ type PersistedColumnChooserState = string[] | {
29
+ visible?: string[];
30
+ order?: string[];
31
+ widths?: Record<string, number>;
32
+ };
33
+
34
+ const props = defineProps<{
35
+ columns: ColumnDef[];
36
+ modelValue: string[];
37
+ storageKey?: string;
38
+ }>();
39
+
40
+ const emit = defineEmits<{
41
+ (e: 'update:modelValue', value: string[]): void;
42
+ }>();
43
+
44
+ const open = ref(false);
45
+ const root = ref<HTMLElement | null>(null);
46
+
47
+ const visibleSet = computed(() => new Set(props.modelValue));
48
+
49
+ function readStoredVisibleColumns(storageKey: string): string[] | null {
50
+ const stored = loadJson<PersistedColumnChooserState | null>(storageKey, null);
51
+ if (!stored) return null;
52
+ if (Array.isArray(stored)) return [...stored];
53
+ return Array.isArray(stored.visible) ? [...stored.visible] : null;
54
+ }
55
+
56
+ function saveStoredVisibleColumns(storageKey: string, visible: string[]) {
57
+ const stored = loadJson<PersistedColumnChooserState | null>(storageKey, null);
58
+ if (stored && !Array.isArray(stored)) {
59
+ saveJson(storageKey, {
60
+ ...stored,
61
+ visible,
62
+ });
63
+ return;
64
+ }
65
+
66
+ saveJson(storageKey, {
67
+ visible,
68
+ });
69
+ }
70
+
71
+ function toggle(id: string) {
72
+ const next = new Set(props.modelValue);
73
+ if (next.has(id)) next.delete(id);
74
+ else next.add(id);
75
+ const list = props.columns.map(c => c.id).filter(cid => next.has(cid));
76
+ emit('update:modelValue', list);
77
+ if (props.storageKey) saveStoredVisibleColumns(props.storageKey, list);
78
+ }
79
+
80
+ function reset() {
81
+ const list = props.columns.map(c => c.id);
82
+ emit('update:modelValue', list);
83
+ if (props.storageKey) saveStoredVisibleColumns(props.storageKey, list);
84
+ }
85
+
86
+ function onDocClick(e: MouseEvent) {
87
+ if (!open.value) return;
88
+ const t = e.target as Node;
89
+ if (!root.value) return;
90
+ if (!root.value.contains(t)) open.value = false;
91
+ }
92
+
93
+ onMounted(() => {
94
+ document.addEventListener('mousedown', onDocClick);
95
+ if (props.storageKey) {
96
+ const stored = readStoredVisibleColumns(props.storageKey);
97
+ if (stored) emit('update:modelValue', stored);
98
+ }
99
+ });
100
+ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocClick));
101
+ </script>