@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.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- 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
|
+
}
|