@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,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-ngb-muted/10"
|
|
5
|
+
@click="emit('click')"
|
|
6
|
+
>
|
|
7
|
+
<svg
|
|
8
|
+
class="h-5 w-5 text-ngb-muted"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
stroke-width="1.7"
|
|
13
|
+
stroke-linecap="round"
|
|
14
|
+
stroke-linejoin="round"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
>
|
|
17
|
+
<path v-if="direction === 'prev'" d="M15 18l-6-6 6-6" />
|
|
18
|
+
<path v-else d="M9 6l6 6-6 6" />
|
|
19
|
+
</svg>
|
|
20
|
+
</button>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
defineProps<{
|
|
25
|
+
direction: 'prev' | 'next'
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
(e: 'click'): void
|
|
30
|
+
}>()
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
displayValue: string
|
|
7
|
+
placeholder: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
readonly?: boolean
|
|
10
|
+
grouped?: boolean
|
|
11
|
+
panelClass?: string
|
|
12
|
+
}>(), {
|
|
13
|
+
panelClass: 'w-[320px]',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const triggerClass = computed(() => {
|
|
17
|
+
const base = 'relative flex w-full items-center justify-start text-left ngb-focus'
|
|
18
|
+
const disabled = props.disabled || props.readonly ? ' opacity-60 cursor-not-allowed' : ''
|
|
19
|
+
if (props.grouped) {
|
|
20
|
+
return `${base} h-full min-h-full bg-transparent px-3 py-0 pr-8 text-xs text-ngb-text placeholder:text-ngb-muted/70${disabled}`
|
|
21
|
+
}
|
|
22
|
+
return `${base} h-9 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 py-0 pr-10 text-sm text-ngb-text placeholder:text-ngb-muted/70${disabled}`
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const popoverClass = computed(() => (props.grouped ? 'relative block h-full w-full' : 'relative block w-full'))
|
|
26
|
+
const panelClasses = computed(() => [
|
|
27
|
+
'absolute right-0 z-50 mt-2 max-w-[calc(100vw-2rem)] overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-lg',
|
|
28
|
+
props.panelClass,
|
|
29
|
+
])
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<Popover v-slot="{ close }" :class="popoverClass">
|
|
34
|
+
<PopoverButton
|
|
35
|
+
as="button"
|
|
36
|
+
type="button"
|
|
37
|
+
:class="triggerClass"
|
|
38
|
+
:disabled="disabled || readonly"
|
|
39
|
+
>
|
|
40
|
+
<span
|
|
41
|
+
v-if="displayValue"
|
|
42
|
+
class="inline-flex h-full min-w-0 flex-1 items-center truncate leading-none"
|
|
43
|
+
>
|
|
44
|
+
{{ displayValue }}
|
|
45
|
+
</span>
|
|
46
|
+
<span
|
|
47
|
+
v-else
|
|
48
|
+
class="inline-flex h-full min-w-0 flex-1 items-center truncate leading-none text-ngb-muted/70"
|
|
49
|
+
>
|
|
50
|
+
{{ placeholder }}
|
|
51
|
+
</span>
|
|
52
|
+
|
|
53
|
+
<span class="pointer-events-none absolute right-2 top-1/2 inline-flex -translate-y-1/2 items-center">
|
|
54
|
+
<svg class="h-4 w-4 text-ngb-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
55
|
+
<path d="M8 2v4" />
|
|
56
|
+
<path d="M16 2v4" />
|
|
57
|
+
<path d="M3 9h18" />
|
|
58
|
+
<path d="M5 6h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" />
|
|
59
|
+
</svg>
|
|
60
|
+
</span>
|
|
61
|
+
</PopoverButton>
|
|
62
|
+
|
|
63
|
+
<PopoverPanel :class="panelClasses">
|
|
64
|
+
<div class="p-3">
|
|
65
|
+
<div v-if="$slots.header" class="flex items-center justify-between gap-2">
|
|
66
|
+
<slot name="header" :close="close" />
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div :class="$slots.header ? 'mt-3' : ''">
|
|
70
|
+
<slot :close="close" />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div v-if="$slots.footer" class="mt-3 flex items-center justify-between text-sm">
|
|
74
|
+
<slot name="footer" :close="close" />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</PopoverPanel>
|
|
78
|
+
</Popover>
|
|
79
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full">
|
|
3
|
+
<div v-if="label" class="block text-xs font-semibold text-ngb-muted mb-1">{{ label }}</div>
|
|
4
|
+
|
|
5
|
+
<div class="flex flex-col gap-2">
|
|
6
|
+
<label
|
|
7
|
+
v-for="o in options"
|
|
8
|
+
:key="o.value"
|
|
9
|
+
class="inline-flex items-center gap-2 select-none"
|
|
10
|
+
:class="disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
|
|
11
|
+
>
|
|
12
|
+
<input
|
|
13
|
+
class="sr-only"
|
|
14
|
+
type="radio"
|
|
15
|
+
:name="name"
|
|
16
|
+
:value="o.value"
|
|
17
|
+
:checked="modelValue === o.value"
|
|
18
|
+
:disabled="disabled"
|
|
19
|
+
@change="$emit('update:modelValue', o.value)"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<span
|
|
23
|
+
class="h-4 w-4 rounded-full border border-ngb-border bg-white flex items-center justify-center"
|
|
24
|
+
:class="modelValue === o.value ? 'border-[rgba(11,60,93,.55)] bg-[rgba(11,60,93,.06)]' : ''"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
>
|
|
27
|
+
<span v-if="modelValue === o.value" class="h-2 w-2 rounded-full bg-ngb-blue" />
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
<span class="text-sm text-ngb-text">{{ o.label }}</span>
|
|
31
|
+
</label>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div v-if="hint" class="mt-1 text-xs text-ngb-muted">{{ hint }}</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
export type RadioOption = { value: string; label: string };
|
|
40
|
+
|
|
41
|
+
defineProps<{
|
|
42
|
+
modelValue: string;
|
|
43
|
+
options: RadioOption[];
|
|
44
|
+
name?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
hint?: string;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
defineEmits<{
|
|
51
|
+
(e: 'update:modelValue', v: string): void;
|
|
52
|
+
}>();
|
|
53
|
+
</script>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full">
|
|
3
|
+
<label v-if="label" class="block text-xs font-semibold text-ngb-muted mb-1">{{ label }}</label>
|
|
4
|
+
|
|
5
|
+
<Listbox :modelValue="modelValue" @update:modelValue="(value) => emit('update:modelValue', value)" :disabled="disabled">
|
|
6
|
+
<div class="relative">
|
|
7
|
+
<ListboxButton
|
|
8
|
+
ref="btnRef"
|
|
9
|
+
:class="[buttonClass, disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer']"
|
|
10
|
+
@mousedown.capture="updatePosition"
|
|
11
|
+
@keydown.enter.capture="updatePosition"
|
|
12
|
+
@keydown.space.capture="updatePosition"
|
|
13
|
+
@keydown.arrow-down.capture="updatePosition"
|
|
14
|
+
>
|
|
15
|
+
<span class="truncate">{{ displayValue }}</span>
|
|
16
|
+
<span class="text-ngb-muted">▾</span>
|
|
17
|
+
</ListboxButton>
|
|
18
|
+
|
|
19
|
+
<!--
|
|
20
|
+
IMPORTANT: this select is used inside line grids (tables) where ancestors often have overflow hidden.
|
|
21
|
+
HeadlessUI renders options as an absolutely positioned element under the button, which gets clipped.
|
|
22
|
+
We teleport the dropdown to <body> and position it using the button's viewport rect.
|
|
23
|
+
-->
|
|
24
|
+
<Teleport to="body">
|
|
25
|
+
<ListboxOptions
|
|
26
|
+
ref="optionsRef"
|
|
27
|
+
:style="floatingStyle"
|
|
28
|
+
class="fixed z-[1000] max-h-64 overflow-auto rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card p-1 focus:outline-none"
|
|
29
|
+
>
|
|
30
|
+
<ListboxOption
|
|
31
|
+
v-for="o in options"
|
|
32
|
+
:key="o.value"
|
|
33
|
+
:value="o.value"
|
|
34
|
+
v-slot="{ active, selected }"
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
class="px-3 py-2 rounded-[var(--ngb-radius)] text-sm flex items-center justify-between"
|
|
38
|
+
:class="active ? 'bg-[var(--ngb-row-hover)]' : ''"
|
|
39
|
+
>
|
|
40
|
+
<span class="truncate" :class="selected ? 'font-semibold' : ''">{{ o.label }}</span>
|
|
41
|
+
<span v-if="selected" class="text-ngb-blue">✓</span>
|
|
42
|
+
</div>
|
|
43
|
+
</ListboxOption>
|
|
44
|
+
</ListboxOptions>
|
|
45
|
+
</Teleport>
|
|
46
|
+
</div>
|
|
47
|
+
</Listbox>
|
|
48
|
+
|
|
49
|
+
<div v-if="hint" class="mt-1 text-xs text-ngb-muted">{{ hint }}</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { computed, ref, type ComponentPublicInstance } from 'vue'
|
|
55
|
+
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
|
|
56
|
+
import { useFloatingDropdownPosition } from './useFloatingDropdownPosition'
|
|
57
|
+
|
|
58
|
+
export type SelectOption = {
|
|
59
|
+
value: unknown
|
|
60
|
+
label: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type SelectVariant = 'default' | 'grid' | 'compact'
|
|
64
|
+
type FloatingTarget = HTMLElement | ComponentPublicInstance | null
|
|
65
|
+
|
|
66
|
+
const props = withDefaults(defineProps<{
|
|
67
|
+
modelValue: unknown
|
|
68
|
+
options: SelectOption[]
|
|
69
|
+
label?: string
|
|
70
|
+
hint?: string
|
|
71
|
+
disabled?: boolean
|
|
72
|
+
// Visual variant.
|
|
73
|
+
// - default: standard select (bordered)
|
|
74
|
+
// - grid: borderless cell-style select (used inside line grids)
|
|
75
|
+
// - compact: compact select (used in page headers/toolbars)
|
|
76
|
+
variant?: SelectVariant
|
|
77
|
+
}>(), {
|
|
78
|
+
label: '',
|
|
79
|
+
hint: '',
|
|
80
|
+
disabled: false,
|
|
81
|
+
variant: 'default',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const emit = defineEmits<{
|
|
85
|
+
(e: 'update:modelValue', value: unknown): void
|
|
86
|
+
}>()
|
|
87
|
+
|
|
88
|
+
const displayValue = computed(() => {
|
|
89
|
+
const found = props.options.find((option) => option.value === props.modelValue)
|
|
90
|
+
return found ? found.label : 'Select…'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const buttonClass = computed(() => {
|
|
94
|
+
if (props.variant === 'grid') {
|
|
95
|
+
return 'h-8 w-full rounded-none border border-transparent bg-transparent px-2 text-sm text-ngb-text flex items-center justify-between ngb-focus'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (props.variant === 'compact') {
|
|
99
|
+
return 'h-[26px] w-full rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-2.5 text-xs text-ngb-text flex items-center justify-between shadow-card ngb-focus'
|
|
100
|
+
}
|
|
101
|
+
return 'h-9 w-full rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 text-sm text-ngb-text flex items-center justify-between ngb-focus'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const btnRef = ref<FloatingTarget>(null)
|
|
105
|
+
const optionsRef = ref<FloatingTarget>(null)
|
|
106
|
+
|
|
107
|
+
const { floatingStyle, updatePosition } = useFloatingDropdownPosition(btnRef, [optionsRef])
|
|
108
|
+
</script>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class="inline-flex items-center justify-center leading-none"
|
|
4
|
+
:title="title ?? defaultTitle"
|
|
5
|
+
:class="colorClass"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
>
|
|
8
|
+
<svg
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
class="w-6 h-6 block"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
stroke-width="2"
|
|
14
|
+
stroke-linecap="round"
|
|
15
|
+
stroke-linejoin="round"
|
|
16
|
+
>
|
|
17
|
+
<!-- Consistent, compact ERP-style status glyphs. -->
|
|
18
|
+
<circle cx="12" cy="12" r="7" />
|
|
19
|
+
|
|
20
|
+
<!-- Posted: check in the ring -->
|
|
21
|
+
<template v-if="status === 'posted'">
|
|
22
|
+
<path d="M8.7 12.1l2.0 2.1 4.6-4.8" />
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<!-- Marked: X in the ring -->
|
|
26
|
+
<template v-else-if="status === 'marked'">
|
|
27
|
+
<path d="M9 9l6 6" />
|
|
28
|
+
<path d="M15 9l-6 6" />
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<!-- Active/Saved: ring only -->
|
|
32
|
+
</svg>
|
|
33
|
+
</span>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { computed } from 'vue';
|
|
38
|
+
|
|
39
|
+
const props = defineProps<{
|
|
40
|
+
status: 'active' | 'saved' | 'posted' | 'marked';
|
|
41
|
+
title?: string;
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
const defaultTitle = computed(() => {
|
|
45
|
+
switch (props.status) {
|
|
46
|
+
case 'active':
|
|
47
|
+
return 'Active';
|
|
48
|
+
case 'saved':
|
|
49
|
+
return 'Saved';
|
|
50
|
+
case 'posted':
|
|
51
|
+
return 'Posted';
|
|
52
|
+
case 'marked':
|
|
53
|
+
return 'Marked for deletion';
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const colorClass = computed(() => {
|
|
58
|
+
if (props.status === 'posted') return 'text-ngb-success';
|
|
59
|
+
if (props.status === 'marked') return 'text-ngb-danger';
|
|
60
|
+
// Keep the same subdued “active” color as catalogs.
|
|
61
|
+
return 'text-ngb-muted opacity-30';
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
role="switch"
|
|
5
|
+
:aria-checked="modelValue ? 'true' : 'false'"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
class="inline-flex items-center gap-2 select-none"
|
|
8
|
+
:class="disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
|
|
9
|
+
@click="toggle"
|
|
10
|
+
>
|
|
11
|
+
<span
|
|
12
|
+
class="relative inline-flex h-[1.125rem] w-8 rounded-full border bg-white transition-colors duration-150 dark:bg-ngb-card"
|
|
13
|
+
:class="modelValue
|
|
14
|
+
? 'border-[rgba(11,60,93,.40)] dark:border-[rgba(147,197,253,.40)]'
|
|
15
|
+
: 'border-ngb-border dark:border-ngb-border'"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
>
|
|
18
|
+
<span
|
|
19
|
+
class="absolute left-0.5 top-1/2 h-[0.875rem] w-[0.875rem] -translate-y-1/2 rounded-full bg-ngb-blue shadow-[0_0_0_1px_rgba(11,60,93,.04)] transition-transform duration-150 dark:bg-white dark:shadow-[0_0_0_1px_rgba(255,255,255,.10)]"
|
|
20
|
+
:class="modelValue ? 'translate-x-[0.75rem]' : 'translate-x-0'"
|
|
21
|
+
/>
|
|
22
|
+
</span>
|
|
23
|
+
<span v-if="label" class="text-sm text-ngb-text">{{ label }}</span>
|
|
24
|
+
</button>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
const props = defineProps<{
|
|
29
|
+
modelValue: boolean;
|
|
30
|
+
label?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{
|
|
35
|
+
(e: 'update:modelValue', v: boolean): void;
|
|
36
|
+
}>();
|
|
37
|
+
|
|
38
|
+
function toggle() {
|
|
39
|
+
if (props.disabled) return;
|
|
40
|
+
emit('update:modelValue', !props.modelValue);
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="fill ? 'w-full h-full flex flex-col min-h-0' : 'w-full'">
|
|
3
|
+
<div
|
|
4
|
+
ref="tablistRef"
|
|
5
|
+
role="tablist"
|
|
6
|
+
aria-orientation="horizontal"
|
|
7
|
+
:class="fullWidthBar ? 'flex w-full rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-1' : 'inline-flex rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-1'"
|
|
8
|
+
>
|
|
9
|
+
<button
|
|
10
|
+
v-for="(t, index) in tabs"
|
|
11
|
+
:key="keyOf(t)"
|
|
12
|
+
type="button"
|
|
13
|
+
role="tab"
|
|
14
|
+
:id="tabId(keyOf(t))"
|
|
15
|
+
:data-tab-index="index"
|
|
16
|
+
:aria-selected="keyOf(t) === modelValue"
|
|
17
|
+
:aria-controls="panelId"
|
|
18
|
+
:tabindex="keyOf(t) === modelValue ? 0 : -1"
|
|
19
|
+
class="h-8 px-3 rounded-[var(--ngb-radius)] text-sm font-medium transition-colors ngb-focus"
|
|
20
|
+
:class="keyOf(t) === modelValue
|
|
21
|
+
? 'bg-ngb-bg text-ngb-text shadow-sm'
|
|
22
|
+
: 'bg-transparent text-ngb-muted hover:bg-ngb-bg hover:text-ngb-text'
|
|
23
|
+
"
|
|
24
|
+
@click="emit('update:modelValue', keyOf(t))"
|
|
25
|
+
@keydown="onTabKeyDown(index, $event)"
|
|
26
|
+
>
|
|
27
|
+
{{ t.label }}
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
:id="panelId"
|
|
33
|
+
role="tabpanel"
|
|
34
|
+
:aria-labelledby="activeTabId"
|
|
35
|
+
:class="fill ? 'mt-3 flex-1 min-h-0' : 'mt-3'"
|
|
36
|
+
>
|
|
37
|
+
<slot :active="modelValue" />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed, nextTick, ref } from 'vue'
|
|
44
|
+
|
|
45
|
+
export type TabItem = { key?: string; id?: string; label: string };
|
|
46
|
+
|
|
47
|
+
const props = withDefaults(
|
|
48
|
+
defineProps<{
|
|
49
|
+
modelValue: string;
|
|
50
|
+
tabs: TabItem[];
|
|
51
|
+
fill?: boolean;
|
|
52
|
+
fullWidthBar?: boolean;
|
|
53
|
+
}>(),
|
|
54
|
+
{
|
|
55
|
+
fill: false,
|
|
56
|
+
fullWidthBar: false,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const emit = defineEmits<{
|
|
61
|
+
(e: 'update:modelValue', v: string): void;
|
|
62
|
+
}>()
|
|
63
|
+
|
|
64
|
+
const tablistRef = ref<HTMLElement | null>(null)
|
|
65
|
+
const activeTabId = computed(() => tabId(props.modelValue))
|
|
66
|
+
const panelId = computed(() => `${tabId(props.modelValue)}-panel`)
|
|
67
|
+
|
|
68
|
+
function keyOf(t: TabItem) {
|
|
69
|
+
return t.key ?? t.id ?? '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tabId(key: string) {
|
|
73
|
+
const normalized = String(key ?? '').trim().replace(/[^a-z0-9_-]+/gi, '-')
|
|
74
|
+
return `ngb-tab-${normalized || 'item'}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function focusTab(index: number) {
|
|
78
|
+
tablistRef.value
|
|
79
|
+
?.querySelector<HTMLElement>(`[data-tab-index="${index}"]`)
|
|
80
|
+
?.focus()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function emitActiveIndex(index: number) {
|
|
84
|
+
const tab = props.tabs[index]
|
|
85
|
+
if (!tab) return
|
|
86
|
+
emit('update:modelValue', keyOf(tab))
|
|
87
|
+
void nextTick(() => focusTab(index))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onTabKeyDown(index: number, event: KeyboardEvent) {
|
|
91
|
+
if (!props.tabs.length) return
|
|
92
|
+
|
|
93
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
94
|
+
event.preventDefault()
|
|
95
|
+
emitActiveIndex((index + 1) % props.tabs.length)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
emitActiveIndex((index - 1 + props.tabs.length) % props.tabs.length)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (event.key === 'Home') {
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
emitActiveIndex(0)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (event.key === 'End') {
|
|
112
|
+
event.preventDefault()
|
|
113
|
+
emitActiveIndex(props.tabs.length - 1)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
role="region"
|
|
4
|
+
aria-label="Notifications"
|
|
5
|
+
aria-live="polite"
|
|
6
|
+
aria-atomic="false"
|
|
7
|
+
aria-relevant="additions text"
|
|
8
|
+
class="fixed z-[60] top-4 right-4 w-[360px] max-w-[calc(100vw-32px)] flex flex-col gap-2"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
v-for="t in api.toasts"
|
|
12
|
+
:key="t.id"
|
|
13
|
+
class="ngb-card p-3"
|
|
14
|
+
>
|
|
15
|
+
<div class="flex items-start justify-between gap-3">
|
|
16
|
+
<div class="min-w-0">
|
|
17
|
+
<div class="text-sm font-semibold" :class="toneClass(t.tone)">
|
|
18
|
+
{{ t.title }}
|
|
19
|
+
</div>
|
|
20
|
+
<div v-if="t.message" class="text-sm text-ngb-muted mt-1">{{ t.message }}</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="h-7 w-7 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card text-ngb-muted hover:text-ngb-text hover:bg-ngb-bg transition-colors ngb-focus"
|
|
26
|
+
@click="api.remove(t.id)"
|
|
27
|
+
aria-label="Close"
|
|
28
|
+
>
|
|
29
|
+
×
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { useToasts, type ToastTone } from './toast';
|
|
38
|
+
|
|
39
|
+
const api = useToasts();
|
|
40
|
+
|
|
41
|
+
function toneClass(tone: ToastTone | undefined) {
|
|
42
|
+
return {
|
|
43
|
+
neutral: 'text-ngb-text',
|
|
44
|
+
success: 'text-ngb-success',
|
|
45
|
+
warn: 'text-ngb-warn',
|
|
46
|
+
danger: 'text-ngb-danger',
|
|
47
|
+
}[tone ?? 'neutral'];
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class="relative inline-flex"
|
|
4
|
+
@mouseenter="open = true"
|
|
5
|
+
@mouseleave="open = false"
|
|
6
|
+
@focusin="open = true"
|
|
7
|
+
@focusout="open = false"
|
|
8
|
+
>
|
|
9
|
+
<slot />
|
|
10
|
+
|
|
11
|
+
<span
|
|
12
|
+
v-if="open"
|
|
13
|
+
class="absolute z-50 -top-2 left-1/2 -translate-x-1/2 -translate-y-full px-2 py-1 rounded-[var(--ngb-radius)] border border-ngb-border bg-white shadow-card text-xs text-ngb-text w-max max-w-[240px]"
|
|
14
|
+
role="tooltip"
|
|
15
|
+
>
|
|
16
|
+
{{ text }}
|
|
17
|
+
</span>
|
|
18
|
+
</span>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { ref } from 'vue';
|
|
23
|
+
|
|
24
|
+
defineProps<{ text: string }>();
|
|
25
|
+
|
|
26
|
+
const open = ref(false);
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const NGB_ICON_NAMES = [
|
|
2
|
+
'arrow-left',
|
|
3
|
+
'arrow-right',
|
|
4
|
+
'plus',
|
|
5
|
+
'minus',
|
|
6
|
+
'trash',
|
|
7
|
+
'trash-restore',
|
|
8
|
+
'check',
|
|
9
|
+
'undo',
|
|
10
|
+
'panel-left',
|
|
11
|
+
'panel-right',
|
|
12
|
+
'grip-vertical',
|
|
13
|
+
'bell',
|
|
14
|
+
'help-circle',
|
|
15
|
+
'settings',
|
|
16
|
+
'cogs',
|
|
17
|
+
'sun',
|
|
18
|
+
'moon',
|
|
19
|
+
'search',
|
|
20
|
+
'filter',
|
|
21
|
+
'composer',
|
|
22
|
+
'play',
|
|
23
|
+
'refresh',
|
|
24
|
+
'download',
|
|
25
|
+
'open-in-new',
|
|
26
|
+
'load-variant',
|
|
27
|
+
'x',
|
|
28
|
+
'save',
|
|
29
|
+
'edit',
|
|
30
|
+
'share',
|
|
31
|
+
'copy',
|
|
32
|
+
'printer',
|
|
33
|
+
'more-vertical',
|
|
34
|
+
'log-out',
|
|
35
|
+
'history',
|
|
36
|
+
'circle-x',
|
|
37
|
+
'eye',
|
|
38
|
+
'eye-off',
|
|
39
|
+
'home',
|
|
40
|
+
'grid',
|
|
41
|
+
'list',
|
|
42
|
+
'file-text',
|
|
43
|
+
'bar-chart',
|
|
44
|
+
'calculator',
|
|
45
|
+
'book-open',
|
|
46
|
+
'receipt',
|
|
47
|
+
'user',
|
|
48
|
+
'users',
|
|
49
|
+
'building-2',
|
|
50
|
+
'coins',
|
|
51
|
+
'wallet',
|
|
52
|
+
'wrench',
|
|
53
|
+
'clipboard-list',
|
|
54
|
+
'check-square',
|
|
55
|
+
'tag',
|
|
56
|
+
'landmark',
|
|
57
|
+
'calendar-check',
|
|
58
|
+
'scale',
|
|
59
|
+
'git-merge',
|
|
60
|
+
'shield',
|
|
61
|
+
'shield-check',
|
|
62
|
+
'heart-pulse',
|
|
63
|
+
'inspect-check',
|
|
64
|
+
'selected-check',
|
|
65
|
+
'close-month',
|
|
66
|
+
'chevrons-left',
|
|
67
|
+
'chevrons-right',
|
|
68
|
+
'effects-flow',
|
|
69
|
+
'file-apply',
|
|
70
|
+
'document-flow',
|
|
71
|
+
] as const
|
|
72
|
+
|
|
73
|
+
export type NgbIconName = typeof NGB_ICON_NAMES[number]
|
|
74
|
+
|
|
75
|
+
const iconNameSet = new Set<string>(NGB_ICON_NAMES)
|
|
76
|
+
|
|
77
|
+
export function isNgbIconName(value: string | null | undefined): value is NgbIconName {
|
|
78
|
+
return typeof value === 'string' && iconNameSet.has(value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function coerceNgbIconName(value: string | null | undefined, fallback: NgbIconName): NgbIconName {
|
|
82
|
+
return isNgbIconName(value) ? value : fallback
|
|
83
|
+
}
|