@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,107 @@
1
+ export type YearMonthParts = {
2
+ year: number
3
+ month: number
4
+ }
5
+
6
+ const YEAR_MONTH_RE = /^(\d{4})-(\d{2})$/
7
+ const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/
8
+
9
+ export function parseMonthValue(value: string | null | undefined): YearMonthParts | null {
10
+ const normalized = String(value ?? '').trim()
11
+ const match = YEAR_MONTH_RE.exec(normalized)
12
+ if (!match) return null
13
+
14
+ const year = Number(match[1])
15
+ const month = Number(match[2])
16
+ if (!Number.isInteger(year) || month < 1 || month > 12) return null
17
+
18
+ return { year, month }
19
+ }
20
+
21
+ export function normalizeMonthValue(value: unknown): string | null {
22
+ const parsed = parseMonthValue(String(value ?? '').trim())
23
+ return parsed ? toMonthValue(parsed.year, parsed.month) : null
24
+ }
25
+
26
+ export function toMonthValue(year: number, month: number): string {
27
+ return `${year}-${String(month).padStart(2, '0')}`
28
+ }
29
+
30
+ export function currentMonthValue(now = new Date()): string {
31
+ return toMonthValue(now.getFullYear(), now.getMonth() + 1)
32
+ }
33
+
34
+ export function monthValueYear(value: string | null | undefined): number | null {
35
+ return parseMonthValue(value)?.year ?? null
36
+ }
37
+
38
+ export function shiftMonthValue(value: string | null | undefined, offset: number): string | null {
39
+ const parsed = parseMonthValue(value)
40
+ if (!parsed || !Number.isFinite(offset)) return null
41
+
42
+ const candidate = new Date(parsed.year, parsed.month - 1 + Math.trunc(offset), 1)
43
+ return toMonthValue(candidate.getFullYear(), candidate.getMonth() + 1)
44
+ }
45
+
46
+ export function relativeMonthValue(offset = 0, now = new Date()): string {
47
+ const candidate = new Date(now.getFullYear(), now.getMonth() + Math.trunc(offset), 1)
48
+ return toMonthValue(candidate.getFullYear(), candidate.getMonth() + 1)
49
+ }
50
+
51
+ export function monthValueToDateOnly(value: string | null | undefined): string | null {
52
+ const normalized = normalizeMonthValue(value)
53
+ return normalized ? `${normalized}-01` : null
54
+ }
55
+
56
+ export function dateOnlyToMonthValue(value: string | null | undefined): string | null {
57
+ const normalized = normalizeDateOnlyValue(value)
58
+ return normalized ? normalized.slice(0, 7) : null
59
+ }
60
+
61
+ export function formatMonthValue(
62
+ value: string | null | undefined,
63
+ options: Intl.DateTimeFormatOptions = { month: 'long', year: 'numeric' },
64
+ ): string | null {
65
+ const parsed = parseMonthValue(value)
66
+ if (!parsed) return null
67
+
68
+ return new Date(parsed.year, parsed.month - 1, 1).toLocaleString(undefined, options)
69
+ }
70
+
71
+ export function normalizeDateOnlyValue(value: unknown): string | null {
72
+ const normalized = String(value ?? '').trim()
73
+ const match = DATE_ONLY_RE.exec(normalized)
74
+ if (!match) return null
75
+
76
+ const year = Number(match[1])
77
+ const month = Number(match[2])
78
+ const day = Number(match[3])
79
+ if (!Number.isInteger(year) || month < 1 || month > 12 || day < 1 || day > 31) return null
80
+
81
+ const candidate = new Date(year, month - 1, day)
82
+ if (
83
+ candidate.getFullYear() !== year
84
+ || candidate.getMonth() !== month - 1
85
+ || candidate.getDate() !== day
86
+ ) {
87
+ return null
88
+ }
89
+
90
+ return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
91
+ }
92
+
93
+ export function parseDateOnlyValue(value: string | null | undefined): Date | null {
94
+ const normalized = normalizeDateOnlyValue(value)
95
+ if (!normalized) return null
96
+
97
+ const [year, month, day] = normalized.split('-').map(Number)
98
+ return new Date(year, month - 1, day)
99
+ }
100
+
101
+ export function toDateOnlyValue(date: Date): string {
102
+ return [
103
+ String(date.getFullYear()).padStart(4, '0'),
104
+ String(date.getMonth() + 1).padStart(2, '0'),
105
+ String(date.getDate()).padStart(2, '0'),
106
+ ].join('-')
107
+ }
@@ -0,0 +1,14 @@
1
+ export function toErrorMessage(error: unknown, fallback = 'Request failed.'): string {
2
+ if (error instanceof Error) {
3
+ const message = error.message.trim()
4
+ if (message) return message
5
+ }
6
+
7
+ if (error && typeof error === 'object' && 'message' in error) {
8
+ const message = String((error as { message?: unknown }).message ?? '').trim()
9
+ if (message) return message
10
+ }
11
+
12
+ const raw = String(error ?? '').trim()
13
+ return raw || fallback
14
+ }
@@ -0,0 +1,21 @@
1
+ const GUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
2
+
3
+ export const EMPTY_GUID = '00000000-0000-0000-0000-000000000000'
4
+
5
+ export function isGuidString(value: unknown): value is string {
6
+ return typeof value === 'string' && GUID_RE.test(value.trim())
7
+ }
8
+
9
+ export function isEmptyGuid(value: unknown): boolean {
10
+ return typeof value === 'string' && value.trim() === EMPTY_GUID
11
+ }
12
+
13
+ export function isNonEmptyGuid(value: unknown): value is string {
14
+ return isGuidString(value) && !isEmptyGuid(value)
15
+ }
16
+
17
+ export function shortGuid(value: unknown): string {
18
+ const normalized = typeof value === 'string' ? value.trim() : ''
19
+ if (!normalized) return '—'
20
+ return normalized.length > 12 ? `${normalized.slice(0, 8)}…${normalized.slice(-4)}` : normalized
21
+ }
@@ -0,0 +1,21 @@
1
+ function normalizeStableValue(value: unknown, seen: WeakSet<object>): unknown {
2
+ if (value == null || typeof value !== 'object') return value
3
+ if (seen.has(value)) return '[Circular]'
4
+ seen.add(value)
5
+
6
+ if (Array.isArray(value)) return value.map((entry) => normalizeStableValue(entry, seen))
7
+
8
+ const output: Record<string, unknown> = {}
9
+ for (const key of Object.keys(value).sort()) {
10
+ output[key] = normalizeStableValue((value as Record<string, unknown>)[key], seen)
11
+ }
12
+ return output
13
+ }
14
+
15
+ export function stableStringify(value: unknown): string {
16
+ return JSON.stringify(normalizeStableValue(value, new WeakSet<object>()))
17
+ }
18
+
19
+ export function stableEquals(left: unknown, right: unknown): boolean {
20
+ return stableStringify(left) === stableStringify(right)
21
+ }
@@ -0,0 +1,135 @@
1
+ export type StorageScope = 'local' | 'session'
2
+
3
+ export type CookieOptions = {
4
+ path?: string
5
+ maxAgeSeconds?: number
6
+ sameSite?: 'Lax' | 'Strict' | 'None'
7
+ secure?: boolean
8
+ domain?: string | null
9
+ }
10
+
11
+ function resolveBrowserStorage(scope: StorageScope): Storage | null {
12
+ if (typeof window === 'undefined') return null
13
+
14
+ try {
15
+ return scope === 'session' ? window.sessionStorage : window.localStorage
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ export function canUseStorage(scope: StorageScope): boolean {
22
+ return resolveBrowserStorage(scope) !== null
23
+ }
24
+
25
+ export function readStorageString(scope: StorageScope, key: string): string | null {
26
+ const storage = resolveBrowserStorage(scope)
27
+ if (!storage) return null
28
+
29
+ try {
30
+ return storage.getItem(key)
31
+ } catch {
32
+ return null
33
+ }
34
+ }
35
+
36
+ export function writeStorageString(scope: StorageScope, key: string, value: string): boolean {
37
+ const storage = resolveBrowserStorage(scope)
38
+ if (!storage) return false
39
+
40
+ try {
41
+ storage.setItem(key, value)
42
+ return true
43
+ } catch {
44
+ return false
45
+ }
46
+ }
47
+
48
+ export function removeStorageItem(scope: StorageScope, key: string): void {
49
+ const storage = resolveBrowserStorage(scope)
50
+ if (!storage) return
51
+
52
+ try {
53
+ storage.removeItem(key)
54
+ } catch {
55
+ // Ignore storage cleanup failures.
56
+ }
57
+ }
58
+
59
+ export function listStorageKeys(scope: StorageScope): string[] {
60
+ const storage = resolveBrowserStorage(scope)
61
+ if (!storage) return []
62
+
63
+ try {
64
+ return Object.keys(storage)
65
+ } catch {
66
+ return []
67
+ }
68
+ }
69
+
70
+ export function readStorageJson<T>(scope: StorageScope, key: string, fallback: T): T {
71
+ const raw = readStorageString(scope, key)
72
+ if (!raw) return fallback
73
+
74
+ try {
75
+ return JSON.parse(raw) as T
76
+ } catch {
77
+ return fallback
78
+ }
79
+ }
80
+
81
+ export function readStorageJsonOrNull<T>(scope: StorageScope, key: string): T | null {
82
+ const raw = readStorageString(scope, key)
83
+ if (!raw) return null
84
+
85
+ try {
86
+ return JSON.parse(raw) as T
87
+ } catch {
88
+ return null
89
+ }
90
+ }
91
+
92
+ export function writeStorageJson(scope: StorageScope, key: string, value: unknown): boolean {
93
+ try {
94
+ return writeStorageString(scope, key, JSON.stringify(value))
95
+ } catch {
96
+ return false
97
+ }
98
+ }
99
+
100
+ export function loadJson<T>(key: string, fallback: T): T {
101
+ return readStorageJson('local', key, fallback)
102
+ }
103
+
104
+ export function saveJson(key: string, value: unknown): void {
105
+ void writeStorageJson('local', key, value)
106
+ }
107
+
108
+ export function readCookie(name: string): string | null {
109
+ if (typeof document === 'undefined') return null
110
+
111
+ const prefix = `${name}=`
112
+ const cookie = document.cookie
113
+ .split(';')
114
+ .map((part) => part.trim())
115
+ .find((part) => part.startsWith(prefix))
116
+
117
+ if (!cookie) return null
118
+ return decodeURIComponent(cookie.slice(prefix.length))
119
+ }
120
+
121
+ export function writeCookie(name: string, value: string, options: CookieOptions = {}): void {
122
+ if (typeof document === 'undefined') return
123
+
124
+ const parts = [
125
+ `${name}=${encodeURIComponent(value)}`,
126
+ `Path=${options.path ?? '/'}`,
127
+ `SameSite=${options.sameSite ?? 'Lax'}`,
128
+ ]
129
+
130
+ if (Number.isFinite(options.maxAgeSeconds)) parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAgeSeconds ?? 0))}`)
131
+ if (options.secure) parts.push('Secure')
132
+ if (options.domain) parts.push(`Domain=${options.domain}`)
133
+
134
+ document.cookie = parts.join('; ')
135
+ }
@@ -0,0 +1,165 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* NGB Design Tokens (CSS vars for theming) */
6
+ :root {
7
+ /* Make native form controls (date picker, number spinners, etc.) render in a light theme by default. */
8
+ color-scheme: light;
9
+ --ngb-blue: #0B3C5D;
10
+ --ngb-blue-dark: #072A40;
11
+ --ngb-blue-hover: #0E4A75;
12
+ --ngb-primary-rgb: 11 60 93;
13
+ --ngb-primary-hover-rgb: 14 74 117;
14
+
15
+ /* Brand logo */
16
+ --ngb-logo: #0B3C5D;
17
+ --ngb-logo-accent: #0F766E;
18
+
19
+ --ngb-text: #1F2933;
20
+ --ngb-muted: #4B5563;
21
+ --ngb-border: #CBD5E1;
22
+ --ngb-bg: #F5F7FA;
23
+ --ngb-card: #FFFFFF;
24
+
25
+ --ngb-danger: #9B1C1C;
26
+ --ngb-warn: #B45309;
27
+ --ngb-success: #2E7D32;
28
+ --ngb-neutral-subtle: #F1F5F9;
29
+ --ngb-success-subtle: #F3FBF4;
30
+ --ngb-warn-subtle: #FFF7ED;
31
+ --ngb-danger-subtle: #FEF2F2;
32
+ --ngb-success-border: rgba(46,125,50,.35);
33
+ --ngb-warn-border: rgba(180,83,9,.35);
34
+ --ngb-danger-border: rgba(155,28,28,.35);
35
+
36
+ --ngb-accent-1: #006D6F;
37
+ --ngb-accent-2: #5B2D8B;
38
+ --ngb-accent-3: #C05621;
39
+ --ngb-accent-4: #B45309;
40
+
41
+ /* Spacing scale (4/8/12/16/24/32) */
42
+ --ngb-space-4: 4px;
43
+ --ngb-space-8: 8px;
44
+ --ngb-space-12: 12px;
45
+ --ngb-space-16: 16px;
46
+ --ngb-space-24: 24px;
47
+ --ngb-space-32: 32px;
48
+
49
+ /* Minimal elevation */
50
+ --ngb-shadow-0: none;
51
+ --ngb-shadow-1: 0 6px 24px rgba(0,0,0,.08);
52
+ --ngb-shadow-2: 0 12px 38px rgba(0,0,0,.12);
53
+
54
+ --ngb-radius: 4px;
55
+
56
+ /* Grid surfaces */
57
+ --ngb-grid-header: #F1F5F9;
58
+ --ngb-row-hover: #F8FAFC;
59
+ --ngb-row-selected: #EEF6FF;
60
+ }
61
+
62
+ /* Dark theme (minimal, ERP-friendly) */
63
+ html.dark {
64
+ /* Native form controls (date picker, number spinners, etc.) should follow dark theme. */
65
+ color-scheme: dark;
66
+ --ngb-text: #E5E7EB;
67
+ --ngb-muted: #9CA3AF;
68
+ --ngb-border: #1F2937;
69
+ --ngb-bg: #0B1220;
70
+ --ngb-card: #0F172A;
71
+
72
+ /* brand */
73
+ --ngb-blue-hover: #0F5F95;
74
+ --ngb-primary-hover-rgb: 15 95 149;
75
+
76
+ /* Brand logo: lighter for contrast in dark mode */
77
+ --ngb-logo: #93C5FD;
78
+ --ngb-logo-accent: #34D399;
79
+
80
+ /* statuses */
81
+ --ngb-danger: #F87171;
82
+ --ngb-warn: #FBBF24;
83
+ --ngb-success: #34D399;
84
+ --ngb-neutral-subtle: rgba(255,255,255,.06);
85
+ --ngb-success-subtle: rgba(52,211,153,.12);
86
+ --ngb-warn-subtle: rgba(251,191,36,.12);
87
+ --ngb-danger-subtle: rgba(248,113,113,.12);
88
+ --ngb-success-border: rgba(52,211,153,.35);
89
+ --ngb-warn-border: rgba(251,191,36,.35);
90
+ --ngb-danger-border: rgba(248,113,113,.35);
91
+
92
+ --ngb-shadow-1: 0 10px 34px rgba(0,0,0,.45);
93
+ --ngb-shadow-2: 0 18px 52px rgba(0,0,0,.55);
94
+
95
+ /* Grid surfaces */
96
+ --ngb-grid-header: #0B1220;
97
+ --ngb-row-hover: #101B33;
98
+ --ngb-row-selected: #122A46;
99
+ }
100
+
101
+ /* --------------------------------------
102
+ Native inputs: dark-mode ergonomics
103
+ -------------------------------------- */
104
+
105
+ /* Keep number inputs ERP-clean: remove native spinners (they render poorly across OS/themes). */
106
+ input[type='number']::-webkit-outer-spin-button,
107
+ input[type='number']::-webkit-inner-spin-button {
108
+ -webkit-appearance: none;
109
+ margin: 0;
110
+ }
111
+ input[type='number'] {
112
+ -moz-appearance: textfield;
113
+ appearance: textfield;
114
+ }
115
+
116
+ @layer base {
117
+ html, body { height: 100%; }
118
+ body {
119
+ @apply bg-ngb-bg text-ngb-text;
120
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
121
+ }
122
+ }
123
+
124
+ @layer utilities {
125
+ .ngb-focus {
126
+ @apply outline-none ring-0 focus-visible:ring-4 focus-visible:ring-[rgba(11,60,93,.12)] focus-visible:border-[rgba(11,60,93,.55)];
127
+ }
128
+
129
+ /* Icon-only button (ChatGPT/QuickBooks-like) */
130
+ .ngb-iconbtn {
131
+ @apply h-9 w-9 rounded-[var(--ngb-radius)] flex items-center justify-center
132
+ text-ngb-muted hover:text-ngb-text hover:bg-ngb-bg
133
+ transition-colors cursor-pointer ngb-focus
134
+ disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-ngb-muted;
135
+ }
136
+
137
+ .ngb-card {
138
+ @apply rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card;
139
+ }
140
+
141
+ .ngb-divider { @apply bg-ngb-border; }
142
+
143
+ .ngb-mono {
144
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
145
+ }
146
+
147
+ /* Numeric alignment (ERP-like) */
148
+ .ngb-num { font-variant-numeric: tabular-nums; text-align: right; }
149
+ .ngb-num-inline { font-variant-numeric: tabular-nums; }
150
+
151
+ .ngb-spinner {
152
+ width: 14px;
153
+ height: 14px;
154
+ border: 2px solid rgba(11,60,93,.18);
155
+ border-top-color: rgba(11,60,93,.65);
156
+ border-radius: 999px;
157
+ animation: ngb-spin 700ms linear infinite;
158
+ }
159
+ @keyframes ngb-spin { to { transform: rotate(360deg); } }
160
+
161
+ .ngb-kbd {
162
+ @apply px-1.5 py-0.5 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card text-[11px] text-ngb-muted;
163
+ font-variant-numeric: tabular-nums;
164
+ }
165
+ }
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ type DevRequestLike = {
4
+ url?: string
5
+ }
6
+
7
+ type DevResponseLike = {
8
+ statusCode: number
9
+ setHeader: (name: string, value: string) => void
10
+ end: (body?: string | Uint8Array) => void
11
+ }
12
+
13
+ type DevServerLike = {
14
+ middlewares: {
15
+ use: (handler: (req: DevRequestLike, res: DevResponseLike, next: () => void) => void) => void
16
+ }
17
+ }
18
+
19
+ type AssetEmitterLike = {
20
+ emitFile: (asset: {
21
+ type: 'asset'
22
+ fileName: string
23
+ source: string | Uint8Array
24
+ }) => void
25
+ }
26
+
27
+ type PublicAssetDefinition = {
28
+ requestPath: string
29
+ fileName: string
30
+ contentType: string
31
+ source: string | Uint8Array
32
+ }
33
+
34
+ export type NgbUiFrameworkPublicAssetsPluginOptions = {
35
+ faviconFileName?: string
36
+ silentCheckSsoFileName?: string
37
+ }
38
+
39
+ function normalizePublicFileName(value: string | null | undefined, fallback: string): string {
40
+ const normalized = String(value ?? '').trim().replace(/^\/+/, '')
41
+ return normalized || fallback
42
+ }
43
+
44
+ function readUiFrameworkPublicAsset(relativePath: string, encoding?: BufferEncoding): string | Uint8Array {
45
+ const url = new URL(`../../public/${relativePath}`, import.meta.url)
46
+ return encoding ? readFileSync(url, encoding) : readFileSync(url)
47
+ }
48
+
49
+ function buildPublicAssets(options: NgbUiFrameworkPublicAssetsPluginOptions): PublicAssetDefinition[] {
50
+ return [
51
+ {
52
+ requestPath: '/favicon.svg',
53
+ fileName: normalizePublicFileName(options.faviconFileName, 'favicon.svg'),
54
+ contentType: 'image/svg+xml; charset=utf-8',
55
+ source: readUiFrameworkPublicAsset('favicon.svg'),
56
+ },
57
+ {
58
+ requestPath: '/silent-check-sso.html',
59
+ fileName: normalizePublicFileName(options.silentCheckSsoFileName, 'silent-check-sso.html'),
60
+ contentType: 'text/html; charset=utf-8',
61
+ source: readUiFrameworkPublicAsset('silent-check-sso.html', 'utf8'),
62
+ },
63
+ ]
64
+ }
65
+
66
+ function requestPathname(url: string | undefined): string {
67
+ try {
68
+ return new URL(url ?? '/', 'http://localhost').pathname
69
+ } catch {
70
+ return '/'
71
+ }
72
+ }
73
+
74
+ export function ngbUiFrameworkPublicAssetsPlugin(options: NgbUiFrameworkPublicAssetsPluginOptions = {}) {
75
+ const assets = buildPublicAssets(options)
76
+
77
+ return {
78
+ name: 'ngb-ui-framework-public-assets',
79
+ configureServer(server: DevServerLike) {
80
+ server.middlewares.use((req, res, next) => {
81
+ const pathname = requestPathname(req.url)
82
+ const asset = assets.find((entry) => entry.requestPath === pathname)
83
+
84
+ if (!asset) {
85
+ next()
86
+ return
87
+ }
88
+
89
+ res.statusCode = 200
90
+ res.setHeader('Content-Type', asset.contentType)
91
+ res.end(asset.source)
92
+ })
93
+ },
94
+ generateBundle(this: AssetEmitterLike) {
95
+ for (const asset of assets) {
96
+ this.emitFile({
97
+ type: 'asset',
98
+ fileName: asset.fileName,
99
+ source: asset.source,
100
+ })
101
+ }
102
+ },
103
+ }
104
+ }
@@ -0,0 +1,68 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ function normalizePublicFileName(value, fallback) {
4
+ const normalized = String(value ?? '').trim().replace(/^\/+/, '')
5
+ return normalized || fallback
6
+ }
7
+
8
+ function readUiFrameworkPublicAsset(relativePath, encoding) {
9
+ const url = new URL(`./public/${relativePath}`, import.meta.url)
10
+ return encoding ? readFileSync(url, encoding) : readFileSync(url)
11
+ }
12
+
13
+ function buildPublicAssets(options) {
14
+ return [
15
+ {
16
+ requestPath: '/favicon.svg',
17
+ fileName: normalizePublicFileName(options.faviconFileName, 'favicon.svg'),
18
+ contentType: 'image/svg+xml; charset=utf-8',
19
+ source: readUiFrameworkPublicAsset('favicon.svg'),
20
+ },
21
+ {
22
+ requestPath: '/silent-check-sso.html',
23
+ fileName: normalizePublicFileName(options.silentCheckSsoFileName, 'silent-check-sso.html'),
24
+ contentType: 'text/html; charset=utf-8',
25
+ source: readUiFrameworkPublicAsset('silent-check-sso.html', 'utf8'),
26
+ },
27
+ ]
28
+ }
29
+
30
+ function requestPathname(url) {
31
+ try {
32
+ return new URL(url ?? '/', 'http://localhost').pathname
33
+ } catch {
34
+ return '/'
35
+ }
36
+ }
37
+
38
+ export function ngbUiFrameworkPublicAssetsPlugin(options = {}) {
39
+ const assets = buildPublicAssets(options)
40
+
41
+ return {
42
+ name: 'ngb-ui-framework-public-assets',
43
+ configureServer(server) {
44
+ server.middlewares.use((req, res, next) => {
45
+ const pathname = requestPathname(req.url)
46
+ const asset = assets.find((entry) => entry.requestPath === pathname)
47
+
48
+ if (!asset) {
49
+ next()
50
+ return
51
+ }
52
+
53
+ res.statusCode = 200
54
+ res.setHeader('Content-Type', asset.contentType)
55
+ res.end(asset.source)
56
+ })
57
+ },
58
+ generateBundle() {
59
+ for (const asset of assets) {
60
+ this.emitFile({
61
+ type: 'asset',
62
+ fileName: asset.fileName,
63
+ source: asset.source,
64
+ })
65
+ }
66
+ },
67
+ }
68
+ }