@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,91 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ import NgbIcon from '../primitives/NgbIcon.vue'
5
+
6
+ const props = withDefaults(defineProps<{
7
+ error?: string | null
8
+ warnings?: readonly string[] | null | undefined
9
+ errorTitle?: string
10
+ warningTitle?: string
11
+ warningLimit?: number
12
+ }>(), {
13
+ error: null,
14
+ warnings: () => [],
15
+ errorTitle: 'Dashboard data failed to load',
16
+ warningTitle: 'Some sections are partial',
17
+ warningLimit: 3,
18
+ })
19
+
20
+ const normalizedError = computed(() => String(props.error ?? '').trim())
21
+ const normalizedWarningLimit = computed(() => Math.max(0, Math.trunc(props.warningLimit)))
22
+ const visibleWarnings = computed(() =>
23
+ (props.warnings ?? [])
24
+ .map((value) => String(value ?? '').trim())
25
+ .filter((value, index, items) => value.length > 0 && items.indexOf(value) === index)
26
+ .slice(0, normalizedWarningLimit.value),
27
+ )
28
+ const mode = computed<'error' | 'warn' | null>(() => {
29
+ if (normalizedError.value) return 'error'
30
+ if (visibleWarnings.value.length > 0) return 'warn'
31
+ return null
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <div
37
+ v-if="mode"
38
+ class="ngb-dashboard-status-banner"
39
+ :class="mode === 'error' ? 'ngb-dashboard-status-banner-danger' : 'ngb-dashboard-status-banner-warn'"
40
+ >
41
+ <div class="flex items-start gap-3">
42
+ <div class="ngb-dashboard-status-banner-icon">
43
+ <NgbIcon :name="mode === 'error' ? 'circle-x' : 'help-circle'" :size="18" />
44
+ </div>
45
+
46
+ <div>
47
+ <div class="text-sm font-semibold text-ngb-text">
48
+ {{ mode === 'error' ? errorTitle : warningTitle }}
49
+ </div>
50
+
51
+ <div v-if="mode === 'error'" class="mt-1 text-sm text-ngb-muted">
52
+ {{ normalizedError }}
53
+ </div>
54
+
55
+ <div v-else class="mt-1 space-y-1 text-sm text-ngb-muted">
56
+ <div v-for="warning in visibleWarnings" :key="warning">{{ warning }}</div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <style scoped>
64
+ .ngb-dashboard-status-banner {
65
+ --ngb-dashboard-status-banner-color: var(--ngb-border);
66
+ border: 1px solid color-mix(in srgb, var(--ngb-dashboard-status-banner-color) 24%, var(--ngb-border));
67
+ border-radius: var(--ngb-radius);
68
+ background: color-mix(in srgb, var(--ngb-dashboard-status-banner-color) 10%, var(--ngb-card));
69
+ padding: 1rem 1.1rem;
70
+ }
71
+
72
+ .ngb-dashboard-status-banner-warn {
73
+ --ngb-dashboard-status-banner-color: var(--ngb-warn);
74
+ }
75
+
76
+ .ngb-dashboard-status-banner-danger {
77
+ --ngb-dashboard-status-banner-color: var(--ngb-danger);
78
+ }
79
+
80
+ .ngb-dashboard-status-banner-icon {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ height: 2rem;
85
+ width: 2rem;
86
+ border-radius: 999px;
87
+ border: 1px solid color-mix(in srgb, var(--ngb-dashboard-status-banner-color) 26%, var(--ngb-border));
88
+ background: color-mix(in srgb, var(--ngb-dashboard-status-banner-color) 10%, var(--ngb-card));
89
+ color: var(--ngb-text);
90
+ }
91
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 448 120"
5
+ role="img"
6
+ :aria-label="resolvedTitle"
7
+ focusable="false"
8
+ v-bind="$attrs"
9
+ >
10
+ <title v-if="resolvedTitle">{{ resolvedTitle }}</title>
11
+ <g fill="currentColor" transform="translate(-7 -46)">
12
+ <path
13
+ d="M523 0H333V1161Q333 1184 346 1200Q356 1213 373 1220Q395 1229 414 1225Q430 1221 440 1208L1148 210Q1167 183 1200 176Q1232 169 1266 183Q1296 195 1314 222Q1332 248 1332 281V1450H1522V281Q1522 186 1467 109Q1417 39 1337 6Q1250 -29 1161 -10Q1055 12 993 100L523 762ZM291 0V1161Q291 1194 308 1220Q326 1247 357 1259Q391 1273 422 1266Q455 1259 474 1232L1182 234Q1192 221 1209 217Q1228 213 1250 222Q1267 229 1277 243Q1289 260 1289 281V1450H1099V680L629 1343Q567 1430 461 1452Q372 1471 285 1436Q201 1401 150 1325Q100 1251 100 1161V0Z"
14
+ transform="translate(0.000,160.625) scale(0.078125,-0.078125)"
15
+ />
16
+ <path
17
+ d="M1663 1450V1260H776Q554 1260 397 1103Q241 946 241 725Q241 503 397 347Q554 190 776 190H1088Q1426 190 1541 340Q1623 447 1623 725V746H804V936H1814V725Q1814 384 1692 224Q1520 0 1088 0H776Q475 0 263 212Q50 424 50 725Q50 1025 263 1237Q475 1450 776 1450ZM1663 1217H776Q572 1217 427 1073Q283 929 283 725Q283 521 427 377Q572 232 776 232H1088Q1406 232 1507 365Q1579 459 1581 704H804V513H1373Q1366 493 1356 481Q1312 423 1088 423H776Q651 423 562 511Q473 600 473 725Q473 850 562 938Q651 1027 776 1027H1663Z"
18
+ transform="translate(126.719,160.625) scale(0.078125,-0.078125)"
19
+ />
20
+ <path
21
+ d="M566 0V190H1251Q1366 190 1447 271Q1529 353 1529 468Q1529 583 1447 664Q1397 715 1330 734Q1368 754 1399 785Q1481 866 1481 982Q1481 1097 1399 1178Q1318 1260 1202 1260H291V0H100V1450H1202Q1372 1450 1490 1371Q1597 1299 1641 1179Q1683 1065 1654 947Q1624 824 1529 743Q1645 670 1690 542Q1734 421 1698 296Q1661 167 1551 88Q1430 0 1251 0ZM566 232H1251Q1348 232 1417 301Q1487 370 1487 468Q1487 566 1417 635Q1348 704 1251 704H563V513H1251Q1269 513 1283 500Q1296 487 1296 468Q1296 449 1283 436Q1269 423 1251 423H566ZM563 746H1202Q1300 746 1369 815Q1438 884 1438 982Q1438 1079 1369 1148Q1300 1217 1202 1217H333V0H523V1027H1202Q1221 1027 1234 1014Q1248 1000 1248 982Q1248 963 1234 950Q1221 936 1202 936H563Z"
22
+ transform="translate(273.906,160.625) scale(0.078125,-0.078125)"
23
+ />
24
+ </g>
25
+ <g>
26
+ <rect x="403" y="0" width="16" height="16" fill="currentColor" />
27
+ <rect x="422" y="0" width="16" height="16" fill="currentColor" />
28
+ <rect x="403" y="19" width="16" height="16" fill="currentColor" />
29
+ <rect x="422" y="19" width="16" height="16" fill="var(--ngb-logo-accent, #0F766E)" />
30
+ </g>
31
+ </svg>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { computed } from 'vue';
36
+
37
+ defineOptions({ inheritAttrs: false });
38
+
39
+ const props = defineProps<{
40
+ title?: string;
41
+ }>();
42
+
43
+ const resolvedTitle = computed(() => (props.title?.trim() ? props.title.trim() : 'NGB'));
44
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div
3
+ class="border-b border-ngb-border bg-ngb-card"
4
+ :class="variant === 'compact' ? 'px-4 py-3 sm:px-6' : 'px-4 py-4 sm:px-6'"
5
+ >
6
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center">
7
+ <div class="flex min-w-0 items-start gap-3 sm:flex-1 sm:items-center">
8
+ <button
9
+ v-if="canBack"
10
+ class="ngb-iconbtn"
11
+ @click="$emit('back')"
12
+ title="Back"
13
+ aria-label="Back"
14
+ >
15
+ <NgbIcon name="arrow-left" />
16
+ </button>
17
+
18
+ <div class="min-w-0 flex-1 self-center">
19
+ <div v-if="variant !== 'compact' && breadcrumbs?.length" class="mb-1 truncate text-xs text-ngb-muted">
20
+ <span v-for="(b, i) in breadcrumbs" :key="i">
21
+ <span>{{ b }}</span><span v-if="i < breadcrumbs.length - 1"> / </span>
22
+ </span>
23
+ </div>
24
+ <h1 class="truncate font-semibold" :class="variant === 'compact' ? 'text-base' : 'text-lg'">{{ title }}</h1>
25
+ <div v-if="variant !== 'compact'" class="mt-1 flex min-h-[1.25rem] min-w-0 items-center">
26
+ <slot name="secondary" />
27
+ </div>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
32
+ <slot name="actions" />
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import NgbIcon from '../primitives/NgbIcon.vue';
40
+
41
+ defineProps<{
42
+ title: string;
43
+ breadcrumbs?: string[];
44
+ canBack?: boolean;
45
+ variant?: 'default' | 'compact';
46
+ }>();
47
+
48
+ defineEmits<{ (e: 'back'): void }>();
49
+ </script>
@@ -0,0 +1,286 @@
1
+ <template>
2
+ <div data-testid="site-shell" class="flex h-screen min-h-0 w-full min-w-0 overflow-hidden bg-ngb-bg text-ngb-text">
3
+ <!-- Left sidebar (full height) -->
4
+ <div class="hidden h-full min-h-0 shrink-0 self-stretch md:block relative z-20" :class="sidebarCollapsed ? 'w-[72px]' : 'w-[320px]'">
5
+ <NgbSiteSidebar
6
+ :productTitle="productTitle"
7
+ :brandSubtitle="moduleTitle"
8
+ :pinned="pinned"
9
+ :recent="recent"
10
+ :nodes="nodes"
11
+ :selectedId="selectedId"
12
+ :themeResolved="themeResolved"
13
+ :collapsed="sidebarCollapsed"
14
+ @toggleCollapsed="toggleSidebar"
15
+ @navigate="$emit('navigate', $event)"
16
+ @select="(id, route) => $emit('select', id, route)"
17
+ />
18
+ </div>
19
+
20
+ <!-- Right column -->
21
+ <div class="flex-1 min-w-0 flex flex-col min-h-0">
22
+ <NgbTopBar
23
+ :pageTitle="resolvedPageTitle"
24
+ :canBack="canBack"
25
+ :unreadNotifications="unreadNotifications"
26
+ :userName="resolvedUserName"
27
+ :userEmail="resolvedUserEmail"
28
+ :userMeta="resolvedUserMeta"
29
+ :userMetaIcon="resolvedUserMetaIcon"
30
+ :userRoles="resolvedUserRoles"
31
+ :themeResolved="themeResolved"
32
+ :hasSettings="settingsSections.length > 0"
33
+ :showMainMenu="nodes.length > 0"
34
+ @openMainMenu="mobileMainMenuOpen = true"
35
+ @openPalette="$emit('openPalette')"
36
+ @back="$emit('back')"
37
+ @openNotifications="helpOpen = false; settingsOpen = false; notificationsOpen = true"
38
+ @openHelp="notificationsOpen = false; settingsOpen = false; helpOpen = true"
39
+ @openSettings="notificationsOpen = false; helpOpen = false; settingsOpen = true"
40
+ @signOut="$emit('signOut')"
41
+ @toggleTheme="theme.toggle()"
42
+ />
43
+
44
+ <main data-testid="site-main" class="flex-1 min-h-0 overflow-hidden flex flex-col">
45
+ <slot />
46
+ </main>
47
+ </div>
48
+
49
+ <NgbDrawer
50
+ :open="mobileMainMenuOpen"
51
+ title="Main menu"
52
+ hide-header
53
+ flush-body
54
+ side="left"
55
+ panel-class="max-w-[320px]"
56
+ @update:open="mobileMainMenuOpen = $event"
57
+ >
58
+ <div data-testid="mobile-main-menu-sidebar" class="h-full">
59
+ <NgbSiteSidebar
60
+ :productTitle="productTitle"
61
+ :brandSubtitle="moduleTitle"
62
+ :pinned="pinned"
63
+ :recent="recent"
64
+ :nodes="nodes"
65
+ :selectedId="selectedId"
66
+ :themeResolved="themeResolved"
67
+ :collapsed="false"
68
+ :allowCollapse="false"
69
+ @navigate="handleMobileNavigate"
70
+ @select="handleMobileSelect"
71
+ />
72
+ </div>
73
+ </NgbDrawer>
74
+
75
+ <!-- Notifications drawer (right sidebar) -->
76
+ <NgbDrawer
77
+ :open="notificationsOpen"
78
+ title="Notifications"
79
+ subtitle="Updates and alerts"
80
+ @update:open="notificationsOpen = $event"
81
+ >
82
+ <div class="flex min-h-full items-center justify-center">
83
+ <div class="max-w-[350px] text-center">
84
+ <div class="text-base font-semibold text-ngb-text">No notifications</div>
85
+ <div class="mt-2 text-sm leading-6 text-ngb-muted">
86
+ When you receive a notification, it will appear here
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </NgbDrawer>
91
+
92
+ <!-- Help drawer (right sidebar placeholder) -->
93
+ <NgbDrawer
94
+ :open="helpOpen"
95
+ title="Help"
96
+ subtitle="NGB Assistant"
97
+ @update:open="helpOpen = $event"
98
+ >
99
+ <div class="flex min-h-full items-center justify-center">
100
+ <div class="w-full max-w-[360px] rounded-[24px] border border-ngb-border bg-ngb-bg px-6 py-7 text-center shadow-card">
101
+ <div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full border border-ngb-border bg-ngb-card text-ngb-muted">
102
+ <NgbIcon name="help-circle" :size="26" />
103
+ </div>
104
+
105
+ <div class="mt-5 text-lg font-semibold text-ngb-text">Help is coming soon</div>
106
+ <div class="mt-2 text-sm leading-6 text-ngb-muted">
107
+ NGB Assistant will help you find features, learn workflows, and get answers while you work.
108
+ </div>
109
+
110
+ <div class="mt-6 rounded-[18px] border border-ngb-border bg-ngb-card px-4 py-4 text-left">
111
+ <div class="text-xs font-semibold uppercase tracking-[0.12em] text-ngb-muted">What you'll be able to do</div>
112
+ <div class="mt-3 space-y-3">
113
+ <div class="flex items-start gap-3">
114
+ <span class="mt-0.5 text-ngb-muted">
115
+ <NgbIcon name="search" :size="16" />
116
+ </span>
117
+ <span class="text-sm leading-6 text-ngb-text">Get guided onboarding</span>
118
+ </div>
119
+ <div class="flex items-start gap-3">
120
+ <span class="mt-0.5 text-ngb-muted">
121
+ <NgbIcon name="help-circle" :size="16" />
122
+ </span>
123
+ <span class="text-sm leading-6 text-ngb-text">Ask product questions</span>
124
+ </div>
125
+ <div class="flex items-start gap-3">
126
+ <span class="mt-0.5 text-ngb-muted">
127
+ <NgbIcon name="arrow-right" :size="16" />
128
+ </span>
129
+ <span class="text-sm leading-6 text-ngb-text">Find pages and actions faster</span>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="mt-5 text-xs leading-5 text-ngb-muted">This is a UI placeholder.</div>
135
+ </div>
136
+ </div>
137
+ </NgbDrawer>
138
+
139
+ <!-- Settings drawer (right sidebar) -->
140
+ <NgbDrawer
141
+ :open="settingsOpen"
142
+ title="Settings"
143
+ subtitle="Configuration"
144
+ @update:open="settingsOpen = $event"
145
+ >
146
+ <div class="space-y-6">
147
+ <template v-if="(settingsSections?.length ?? 0) === 0">
148
+ <div class="text-sm text-ngb-muted leading-6">
149
+ No settings shortcuts are configured for this app.
150
+ </div>
151
+ </template>
152
+
153
+ <template v-else>
154
+ <div v-for="s in settingsSections" :key="s.label">
155
+ <div class="text-xs font-semibold text-ngb-muted uppercase tracking-wide">{{ s.label }}</div>
156
+ <div class="mt-2 space-y-1">
157
+ <button
158
+ v-for="it in s.items"
159
+ :key="it.route"
160
+ class="w-full text-left rounded-[var(--ngb-radius)] border border-transparent hover:border-ngb-border hover:bg-ngb-bg px-3 py-2 ngb-focus"
161
+ @click="navigateToSettings(it.route)"
162
+ >
163
+ <div class="flex items-start gap-3">
164
+ <span class="mt-0.5 text-ngb-muted" v-if="it.icon">
165
+ <NgbIcon :name="it.icon" :size="18" />
166
+ </span>
167
+ <span class="mt-1 text-ngb-muted text-[10px]" v-else>•</span>
168
+ <div class="min-w-0">
169
+ <div class="text-sm font-semibold text-ngb-text truncate">{{ it.label }}</div>
170
+ <div v-if="it.description" class="text-xs text-ngb-muted leading-5 mt-0.5">{{ it.description }}</div>
171
+ </div>
172
+ </div>
173
+ </button>
174
+ </div>
175
+ </div>
176
+ </template>
177
+ </div>
178
+ </NgbDrawer>
179
+ <NgbToastHost />
180
+ </div>
181
+ </template>
182
+
183
+ <script setup lang="ts">
184
+ import { computed, ref } from 'vue';
185
+ import NgbTopBar from './NgbTopBar.vue';
186
+ import NgbSiteSidebar from './NgbSiteSidebar.vue';
187
+ import NgbDrawer from '../components/NgbDrawer.vue';
188
+ import NgbIcon from '../primitives/NgbIcon.vue';
189
+ import NgbToastHost from '../primitives/NgbToastHost.vue';
190
+ import type { SiteNavNode, SiteQuickLink, SiteSettingsSection } from './types';
191
+ import { useTheme } from './useTheme';
192
+ import { provideToasts } from '../primitives/toast';
193
+
194
+ const props = defineProps<{
195
+ moduleTitle: string;
196
+ productTitle: string;
197
+ envLabel?: string;
198
+ userName?: string;
199
+ userEmail?: string;
200
+ userMeta?: string;
201
+ userMetaIcon?: 'shield-check' | 'shield' | 'user';
202
+ userRoles?: string[];
203
+ unreadNotifications?: number;
204
+ canBack?: boolean;
205
+ pageTitle?: string;
206
+ pinned: SiteQuickLink[];
207
+ recent: SiteQuickLink[];
208
+ nodes: SiteNavNode[];
209
+ settings?: SiteSettingsSection[];
210
+ selectedId: string | null;
211
+ }>();
212
+
213
+ const emit = defineEmits<{
214
+ (e: 'navigate', route: string): void;
215
+ (e: 'select', id: string, route: string): void;
216
+ (e: 'openPalette'): void;
217
+ (e: 'back'): void;
218
+ (e: 'signOut'): void;
219
+ }>();
220
+
221
+ const sidebarCollapsed = ref(false);
222
+ const theme = useTheme();
223
+ const themeResolved = computed(() => theme.resolved.value);
224
+
225
+ // Global toast stack (available to any child component).
226
+ provideToasts();
227
+
228
+ function toggleSidebar() {
229
+ sidebarCollapsed.value = !sidebarCollapsed.value;
230
+ }
231
+
232
+ const notificationsOpen = ref(false);
233
+ const helpOpen = ref(false);
234
+ const settingsOpen = ref(false);
235
+ const mobileMainMenuOpen = ref(false);
236
+ const settingsSections = computed(() => props.settings ?? []);
237
+
238
+ function navigateToSettings(route: string) {
239
+ settingsOpen.value = false;
240
+ emit('navigate', route);
241
+ }
242
+
243
+ function handleMobileNavigate(route: string) {
244
+ mobileMainMenuOpen.value = false;
245
+ emit('navigate', route);
246
+ }
247
+
248
+ function handleMobileSelect(id: string, route: string) {
249
+ mobileMainMenuOpen.value = false;
250
+ emit('select', id, route);
251
+ }
252
+
253
+ const unreadNotifications = computed(() => props.unreadNotifications ?? 0);
254
+ const canBack = computed(() => props.canBack ?? false);
255
+
256
+ function findNodeLabel(nodes: SiteNavNode[], id: string): string | null {
257
+ for (const n of nodes) {
258
+ if (n.id === id) return n.label;
259
+ if (n.children) {
260
+ const found = findNodeLabel(n.children, id);
261
+ if (found) return found;
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+
267
+ const resolvedUserName = computed(() => (props.userName?.trim() ? props.userName.trim() : 'User'));
268
+ const resolvedUserEmail = computed(() => (props.userEmail?.trim() ? props.userEmail.trim() : ''));
269
+ const resolvedUserMeta = computed(() => (props.userMeta?.trim() ? props.userMeta.trim() : ''));
270
+ const resolvedUserMetaIcon = computed<'shield-check' | 'shield' | 'user'>(() => {
271
+ if (props.userMetaIcon === 'shield-check' || props.userMetaIcon === 'shield') return props.userMetaIcon;
272
+ return 'user';
273
+ });
274
+ const resolvedUserRoles = computed(() => (props.userRoles ?? [])
275
+ .map((role) => String(role ?? '').trim())
276
+ .filter((role) => role.length > 0));
277
+
278
+ const resolvedPageTitle = computed(() => {
279
+ if (props.pageTitle?.trim()) return props.pageTitle.trim();
280
+ if (props.selectedId) {
281
+ const label = findNodeLabel(props.nodes, props.selectedId);
282
+ if (label) return label;
283
+ }
284
+ return props.moduleTitle;
285
+ });
286
+ </script>