@livestorm/ui-vue 3.0.0 → 4.0.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/README.md +3 -3
- package/components-vite.js +2 -2
- package/dist/UiButton-uyW8EHRJ.mjs +602 -0
- package/dist/UiTimelineItem-EiOEi1yF.mjs +7293 -0
- package/dist/UiTooltip-DZcPD6_N.mjs +158 -0
- package/dist/a11y-CaZxRWzo.mjs +6 -0
- package/dist/breakpoints.mjs +4 -9
- package/dist/bus.mjs +5 -5
- package/dist/components.mjs +236 -231
- package/dist/i18n-B8QOMuFD.mjs +46 -0
- package/dist/index.mjs +4 -91
- package/dist/integrations.mjs +3 -14
- package/dist/layouts.mjs +228 -236
- package/dist/responsive.mjs +31 -36
- package/dist/slot.mjs +11 -17
- package/dist/toastDSV2-D3xDZPe5.mjs +105 -0
- package/dist/types/index.d.ts +19 -1
- package/dist/types/src/components/Toast/ToastMessage.vue.d.ts +17 -7
- package/dist/types/src/components/Toast/toastDSV2.d.ts +89 -64
- package/dist/types/src/components/UiActionBar/types.d.ts +2 -0
- package/dist/types/src/components/UiAvatar/UiAvatar.vue.d.ts +4 -2
- package/dist/types/src/components/UiAvatarList/UiAvatarList.vue.d.ts +3 -2
- package/dist/types/src/components/UiBanner/UiBanner.vue.d.ts +13 -1
- package/dist/types/src/components/UiButton/UiButton.vue.d.ts +95 -35
- package/dist/types/src/components/UiButtonGroup/UiButtonGroup.vue.d.ts +8 -2
- package/dist/types/src/components/UiButtonGroup/types.d.ts +4 -0
- package/dist/types/src/components/UiChoice/UiChoice.vue.d.ts +71 -0
- package/dist/types/src/components/UiChoice/index.d.ts +2 -0
- package/dist/types/src/components/UiChoice/types.d.ts +7 -0
- package/dist/types/src/components/UiColorField/UiColorField.vue.d.ts +1 -1
- package/dist/types/src/components/UiColorPicker/UiColorPicker.vue.d.ts +2 -2
- package/dist/types/src/components/UiCombobox/UiCombobox.types.d.ts +47 -0
- package/dist/types/src/components/UiCombobox/UiCombobox.vue.d.ts +40 -0
- package/dist/types/src/components/UiCombobox/index.d.ts +2 -0
- package/dist/types/src/components/UiDataTable/UiDataTable.types.d.ts +46 -0
- package/dist/types/src/components/UiDataTable/UiDataTable.vue.d.ts +136 -0
- package/dist/types/src/components/UiDataTable/index.d.ts +5 -0
- package/dist/types/src/components/UiDrawer/UiDrawer.vue.d.ts +28 -0
- package/dist/types/src/components/UiDrawer/UiDrawerContent.vue.d.ts +18 -0
- package/dist/types/src/components/UiDrawer/UiDrawerTrigger.vue.d.ts +14 -0
- package/dist/types/src/components/UiDropdown/DropdownItemCheckbox.vue.d.ts +3 -0
- package/dist/types/src/components/UiDropdown/UiDropdownContent.vue.d.ts +10 -0
- package/dist/types/src/components/UiDropdown/UiDropdownGroup.vue.d.ts +17 -0
- package/dist/types/src/components/UiDropdown/UiDropdownItem.vue.d.ts +8 -5
- package/dist/types/src/components/UiDropdown/UiDropdownTrigger.vue.d.ts +21 -5
- package/dist/types/src/components/UiDropdown/index.d.ts +1 -0
- package/dist/types/src/components/UiEmojiPickerList/UiEmojiPickerList.vue.d.ts +22 -0
- package/dist/types/src/components/UiEmptyState/UiEmptyState.vue.d.ts +8 -3
- package/dist/types/src/components/UiFieldWrapper/UiFieldWrapper.vue.d.ts +2 -2
- package/dist/types/src/components/UiInputCheckbox/CheckboxGlyph.vue.d.ts +17 -0
- package/dist/types/src/components/UiInputCheckbox/UiInputCheckbox.vue.d.ts +35 -4
- package/dist/types/src/components/UiInputRadio/RadioGlyph.vue.d.ts +19 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadio.vue.d.ts +21 -8
- package/dist/types/src/components/UiInputRadio/UiInputRadioGroup.vue.d.ts +6 -1
- package/dist/types/src/components/UiInputRadio/types.d.ts +1 -1
- package/dist/types/src/components/UiInputSelect/UiInputSelect.vue.d.ts +5 -5
- package/dist/types/src/components/UiInputTextarea/UiInputTextarea.vue.d.ts +5 -5
- package/dist/types/src/components/UiListItem/UiListItem.vue.d.ts +18 -4
- package/dist/types/src/components/UiLoader/UiSpinnerIcon.vue.d.ts +3 -0
- package/dist/types/src/components/UiMenu/UiMenuContent.vue.d.ts +15 -2
- package/dist/types/src/components/UiMenu/UiMenuItem.vue.d.ts +9 -3
- package/dist/types/src/components/UiNestedOptions/UiNestedOptions.vue.d.ts +13 -0
- package/dist/types/src/components/UiPage/UiPage.vue.d.ts +79 -15
- package/dist/types/src/components/UiPagination/UiPagination.vue.d.ts +36 -0
- package/dist/types/src/components/UiPagination/index.d.ts +2 -0
- package/dist/types/src/components/UiPagination/types.d.ts +4 -0
- package/dist/types/src/components/UiPane/UiPane.vue.d.ts +22 -0
- package/dist/types/src/components/UiPane/UiPaneContent.vue.d.ts +75 -0
- package/dist/types/src/components/UiPane/UiPaneTrigger.vue.d.ts +14 -0
- package/dist/types/src/components/UiPane/index.d.ts +5 -0
- package/dist/types/src/components/UiRating/UiRating.vue.d.ts +62 -0
- package/dist/types/src/components/UiRating/types.d.ts +1 -0
- package/dist/types/src/components/UiRoomButton/UiRoomButton.vue.d.ts +5 -3
- package/dist/types/src/components/UiRoomCountdown/UiRoomCountdown.vue.d.ts +32 -0
- package/dist/types/src/components/UiShell/UiShell.vue.d.ts +3 -0
- package/dist/types/src/components/UiSideMenu/UiSideMenu.vue.d.ts +22 -1
- package/dist/types/src/components/UiSideMenu/UiSideMenuItemContent.vue.d.ts +2 -2
- package/dist/types/src/components/UiSwitch/UiSwitch.vue.d.ts +10 -5
- package/dist/types/src/components/UiTable/UiTable.vue.d.ts +13 -4
- package/dist/types/src/components/UiTable/UiTableActionsCell.vue.d.ts +49 -0
- package/dist/types/src/components/UiTable/UiTableActionsHeaderCell.vue.d.ts +3 -0
- package/dist/types/src/components/UiTable/UiTableBody.vue.d.ts +11 -1
- package/dist/types/src/components/UiTable/UiTableCell.vue.d.ts +16 -1
- package/dist/types/src/components/UiTable/UiTableHeaderCell.vue.d.ts +31 -2
- package/dist/types/src/components/UiTable/UiTableRow.vue.d.ts +32 -2
- package/dist/types/src/components/UiTable/context.d.ts +3 -0
- package/dist/types/src/components/UiTable/index.d.ts +4 -0
- package/dist/types/src/components/UiTabs/UiTabs.vue.d.ts +3 -0
- package/dist/types/src/components/UiTag/UiTag.types.d.ts +25 -0
- package/dist/types/src/components/UiTag/UiTag.vue.d.ts +5 -26
- package/dist/types/src/components/UiTag/index.d.ts +2 -1
- package/dist/types/src/components/UiTextField/UiTextField.vue.d.ts +10 -5
- package/dist/types/src/components/UiTooltip/UiTooltipProvider.vue.d.ts +1 -0
- package/dist/types/src/components/UiTooltip/UiTooltipTrigger.vue.d.ts +2 -2
- package/dist/types/src/composables/storedThemingApplies.d.ts +17 -0
- package/dist/types/src/composables/useButtonStyles.d.ts +1 -0
- package/dist/types/src/composables/useChoiceboxSurface.d.ts +11 -0
- package/dist/types/src/composables/useContentMode.d.ts +7 -9
- package/dist/types/src/composables/useControlModes.d.ts +7 -0
- package/dist/types/src/composables/useControlShape.d.ts +24 -0
- package/dist/types/src/composables/useFieldFocusEvents.d.ts +12 -0
- package/dist/types/src/composables/useIconSize.d.ts +2 -0
- package/dist/types/src/composables/usePullGesture.d.ts +42 -0
- package/dist/types/src/composables/useThemeAndModes.d.ts +4 -0
- package/dist/types/src/theming/colorScale.d.ts +165 -0
- package/dist/types/src/theming/legacyColorScale.d.ts +27 -0
- package/dist/types/src/theming/presetColors.d.ts +11 -0
- package/dist/types/src/theming/primitiveTokens.d.ts +2 -0
- package/dist/types/src/utils/effectiveOpacity.d.ts +2 -0
- package/dist/types/src/utils/numbers.d.ts +1 -2
- package/dist/types/src/utils/tokenColor.d.ts +2 -0
- package/dist/ui-vue.css +2 -1
- package/dist/utils.mjs +165 -194
- package/index.d.ts +18 -0
- package/index.js +36 -0
- package/package.json +26 -21
- package/src/components/Toast/LayoutToasts.vue +19 -11
- package/src/components/Toast/ToastMessage.vue +125 -68
- package/src/components/Toast/toastDSV2.ts +238 -67
- package/src/components/UiActionBar/UiActionBar.vue +4 -3
- package/src/components/UiActionBar/UiActionBarItem.vue +9 -6
- package/src/components/UiActionBar/types.ts +13 -0
- package/src/components/UiAvatar/UiAvatar.vue +8 -2
- package/src/components/UiAvatarList/UiAvatarList.vue +51 -20
- package/src/components/UiBadge/UiBadge.vue +1 -1
- package/src/components/UiBanner/UiBanner.vue +71 -21
- package/src/components/UiBreadcrumb/UiBreadcrumb.vue +1 -1
- package/src/components/UiButton/UiButton.vue +92 -25
- package/src/components/UiButtonGroup/UiButtonGroup.vue +21 -17
- package/src/components/UiButtonGroup/UiButtonGroupItem.vue +33 -15
- package/src/components/UiButtonGroup/types.ts +19 -0
- package/src/components/UiCard/UiCard.vue +20 -23
- package/src/components/UiChoice/UiChoice.vue +187 -0
- package/src/components/UiChoice/index.ts +2 -0
- package/src/components/UiChoice/types.ts +11 -0
- package/src/components/UiColorField/UiColorField.vue +2 -2
- package/src/components/UiColorPicker/UiColorPicker.vue +11 -8
- package/src/components/UiCombobox/UiCombobox.types.ts +52 -0
- package/src/components/UiCombobox/UiCombobox.vue +296 -0
- package/src/components/UiCombobox/index.ts +2 -0
- package/src/components/UiDataTable/UiDataTable.types.ts +47 -0
- package/src/components/UiDataTable/UiDataTable.vue +422 -0
- package/src/components/UiDataTable/index.ts +12 -0
- package/src/components/UiDialog/UiDialogContent.vue +22 -7
- package/src/components/UiDrawer/UiDrawer.vue +16 -0
- package/src/components/UiDrawer/UiDrawerContent.vue +42 -0
- package/src/components/UiDrawer/UiDrawerTrigger.vue +17 -0
- package/src/components/UiDropdown/DropdownItemCheckbox.vue +10 -0
- package/src/components/UiDropdown/UiDropdownContent.vue +32 -21
- package/src/components/UiDropdown/UiDropdownGroup.vue +23 -0
- package/src/components/UiDropdown/UiDropdownItem.vue +38 -18
- package/src/components/UiDropdown/UiDropdownSeparator.vue +1 -1
- package/src/components/UiDropdown/UiDropdownTrigger.vue +138 -43
- package/src/components/UiDropdown/index.ts +1 -0
- package/src/components/UiEmojiPickerList/UiEmojiPickerList.vue +122 -0
- package/src/components/UiEmptyState/UiEmptyState.vue +16 -4
- package/src/components/UiFieldWrapper/UiFieldWrapper.vue +9 -3
- package/src/components/UiHorizontalSeparator/UiHorizontalSeparator.vue +2 -2
- package/src/components/UiInputCheckbox/CheckboxGlyph.vue +78 -0
- package/src/components/UiInputCheckbox/UiInputCheckbox.vue +213 -177
- package/src/components/UiInputEmoji/UiInputEmoji.vue +18 -95
- package/src/components/UiInputRadio/RadioGlyph.vue +45 -0
- package/src/components/UiInputRadio/UiInputRadio.vue +177 -169
- package/src/components/UiInputRadio/UiInputRadioGroup.vue +15 -5
- package/src/components/UiInputRadio/types.ts +1 -2
- package/src/components/UiInputSelect/UiInputSelect.vue +9 -7
- package/src/components/UiInputTextarea/UiInputTextarea.vue +20 -35
- package/src/components/UiLabel/UiLabel.vue +1 -1
- package/src/components/UiListItem/UiListItem.vue +55 -9
- package/src/components/UiLoader/UiLoader.vue +4 -20
- package/src/components/UiLoader/UiSpinnerIcon.vue +21 -0
- package/src/components/UiMenu/UiMenuContent.vue +24 -3
- package/src/components/UiMenu/UiMenuGroup.vue +1 -1
- package/src/components/UiMenu/UiMenuItem.vue +14 -3
- package/src/components/UiMenu/UiMenuSubContent.vue +1 -1
- package/src/components/UiMenu/UiMenuSubTrigger.vue +1 -1
- package/src/components/UiNestedOptions/UiNestedOptions.vue +6 -0
- package/src/components/UiPage/UiPage.vue +190 -32
- package/src/components/UiPagination/UiPagination.vue +99 -0
- package/src/components/UiPagination/index.ts +2 -0
- package/src/components/UiPagination/types.ts +28 -0
- package/src/components/UiPane/UiPane.vue +20 -0
- package/src/components/UiPane/UiPaneContent.vue +232 -0
- package/src/components/UiPane/UiPaneTrigger.vue +14 -0
- package/src/components/UiPane/index.ts +12 -0
- package/src/components/UiPopover/UiPopoverContent.vue +21 -1
- package/src/components/UiProgress/UiProgress.vue +2 -2
- package/src/components/UiRating/UiRating.vue +258 -0
- package/src/components/UiRating/types.ts +1 -0
- package/src/components/UiRoomButton/UiRoomButton.vue +10 -41
- package/src/components/UiRoomCountdown/UiRoomCountdown.vue +376 -0
- package/src/components/UiShell/UiShell.vue +9 -3
- package/src/components/UiSideMenu/UiSideMenu.vue +155 -18
- package/src/components/UiSideMenu/UiSideMenuItemContent.vue +20 -16
- package/src/components/UiStepper/UiStepper.vue +3 -16
- package/src/components/UiStepper/UiStepperDescription.vue +1 -13
- package/src/components/UiStepper/UiStepperIndicator.vue +11 -48
- package/src/components/UiStepper/UiStepperItem.vue +1 -8
- package/src/components/UiStepper/UiStepperSeparator.vue +5 -16
- package/src/components/UiStepper/UiStepperTitle.vue +1 -10
- package/src/components/UiSwitch/UiSwitch.vue +44 -28
- package/src/components/UiTable/UiTable.vue +98 -8
- package/src/components/UiTable/UiTableActionsCell.vue +135 -0
- package/src/components/UiTable/UiTableActionsHeaderCell.vue +19 -0
- package/src/components/UiTable/UiTableBody.vue +16 -1
- package/src/components/UiTable/UiTableCell.vue +33 -16
- package/src/components/UiTable/UiTableHeader.vue +6 -5
- package/src/components/UiTable/UiTableHeaderCell.vue +61 -9
- package/src/components/UiTable/UiTableRow.vue +46 -10
- package/src/components/UiTable/context.ts +5 -0
- package/src/components/UiTable/index.ts +4 -0
- package/src/components/UiTabs/UiTabs.vue +56 -61
- package/src/components/UiTag/UiTag.types.ts +28 -0
- package/src/components/UiTag/UiTag.vue +24 -42
- package/src/components/UiTag/index.ts +2 -0
- package/src/components/UiTextField/UiTextField.vue +21 -36
- package/src/components/UiTooltip/UiTooltipContent.vue +1 -1
- package/src/components/UiTooltip/UiTooltipProvider.vue +3 -0
- package/src/components/UiTooltip/UiTooltipTrigger.vue +16 -1
- package/src/composables/storedThemingApplies.ts +19 -0
- package/src/composables/useButtonStyles.ts +9 -13
- package/src/composables/useChoiceboxSurface.ts +36 -0
- package/src/composables/useContentMode.ts +29 -16
- package/src/composables/useControlModes.ts +11 -0
- package/src/composables/useControlShape.ts +34 -0
- package/src/composables/useFieldFocusEvents.ts +30 -0
- package/src/composables/useIconSize.ts +15 -0
- package/src/composables/usePullGesture.ts +142 -0
- package/src/composables/useThemeAndModes.ts +28 -1
- package/src/css/body.css +16 -1
- package/src/css/fonts.css +39 -5
- package/src/global.d.ts +36 -0
- package/src/theming/colorScale.ts +173 -0
- package/src/theming/legacyColorScale.ts +58 -0
- package/src/theming/presetColors.ts +33 -0
- package/src/theming/primitiveTokens.ts +71 -0
- package/src/utils/effectiveOpacity.ts +8 -0
- package/src/utils/i18n.ts +6 -0
- package/src/utils/numbers.ts +1 -1
- package/src/utils/responsive.d.ts +14 -0
- package/src/utils/tokenColor.ts +9 -0
- package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-LpdvGf5X.mjs +0 -6093
- package/dist/a11y-BhF_nl7L.mjs +0 -159
- package/dist/i18n-Cktw9hIu.mjs +0 -49
- package/dist/toastDSV2-wMh-UMzW.mjs +0 -74
- package/dist/types/src/components/UiAvatar/index.d.ts +0 -1
- package/dist/types/src/components/UiAvatarList/index.d.ts +0 -1
- package/dist/types/src/components/UiBadge/index.d.ts +0 -1
- package/dist/types/src/components/UiBanner/index.d.ts +0 -2
- package/dist/types/src/components/UiButtonGroup/index.d.ts +0 -3
- package/dist/types/src/components/UiCarousel/index.d.ts +0 -2
- package/dist/types/src/components/UiColorField/index.d.ts +0 -2
- package/dist/types/src/components/UiDialog/MarketplaceHeaderExample.vue.d.ts +0 -28
- package/dist/types/src/components/UiDialog/index.d.ts +0 -7
- package/dist/types/src/components/UiFieldWrapper/index.d.ts +0 -1
- package/dist/types/src/components/UiInputCheckbox/index.d.ts +0 -2
- package/dist/types/src/components/UiInputEmoji/index.d.ts +0 -1
- package/dist/types/src/components/UiInputRadio/index.d.ts +0 -3
- package/dist/types/src/components/UiInputSelect/index.d.ts +0 -2
- package/dist/types/src/components/UiInputTextarea/index.d.ts +0 -2
- package/dist/types/src/components/UiMenu/index.d.ts +0 -10
- package/dist/types/src/components/UiProgress/index.d.ts +0 -2
- package/dist/types/src/components/UiSwitch/index.d.ts +0 -2
- package/dist/types/src/components/UiTabs/index.d.ts +0 -2
- package/dist/types/src/components/UiTimeline/index.d.ts +0 -2
- package/dist/types/src/components/UiTooltip/index.d.ts +0 -4
- package/dist/types/src/mixins/DynamicAction.d.ts +0 -30
- package/dist/useThemeAndModes-BzYwSOEv.mjs +0 -108
- package/src/components/UiAvatar/index.js +0 -1
- package/src/components/UiAvatarList/index.js +0 -1
- package/src/components/UiBadge/index.ts +0 -2
- package/src/components/UiBanner/index.ts +0 -2
- package/src/components/UiButtonGroup/index.ts +0 -3
- package/src/components/UiCarousel/index.ts +0 -2
- package/src/components/UiColorField/index.ts +0 -2
- package/src/components/UiDialog/MarketplaceHeaderExample.vue +0 -217
- package/src/components/UiDialog/index.ts +0 -18
- package/src/components/UiFieldWrapper/index.ts +0 -1
- package/src/components/UiInputCheckbox/index.ts +0 -2
- package/src/components/UiInputEmoji/index.ts +0 -1
- package/src/components/UiInputRadio/index.ts +0 -3
- package/src/components/UiInputSelect/index.ts +0 -2
- package/src/components/UiInputTextarea/index.ts +0 -2
- package/src/components/UiMenu/index.ts +0 -10
- package/src/components/UiProgress/index.ts +0 -2
- package/src/components/UiSwitch/index.ts +0 -2
- package/src/components/UiTabs/index.ts +0 -5
- package/src/components/UiTag/index.js +0 -1
- package/src/components/UiTimeline/index.ts +0 -2
- package/src/components/UiTooltip/index.js +0 -4
- package/src/mixins/DynamicAction.js +0 -61
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { ref } from 'vue'
|
|
2
|
-
import type { Component } from 'vue'
|
|
1
|
+
import { markRaw, ref } from 'vue'
|
|
2
|
+
import type { Component, Ref } from 'vue'
|
|
3
3
|
import type { RouteLocationRaw } from 'vue-router'
|
|
4
4
|
import { createEventHook, type EventHook } from '@vueuse/core'
|
|
5
5
|
|
|
6
6
|
import type { ThemeColorType } from '../../theme'
|
|
7
|
+
import type { Size } from '../../constants'
|
|
7
8
|
|
|
8
9
|
let uid = 1
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* How many toasts render at once. It lives here rather than in `LayoutToasts`
|
|
13
|
+
* because the store schedules the close timers, and only the toasts actually
|
|
14
|
+
* on screen may run one.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_ITEMS_DISPLAYED = 3
|
|
17
|
+
|
|
10
18
|
export type ToastType = 'alert'|'warning'|'info'|'success'
|
|
11
19
|
|
|
12
20
|
const DEFAULT_TIMER_DURATION_PER_TYPE: Record<Extract<ToastType, 'success'|'info'>, number> = {
|
|
@@ -14,14 +22,39 @@ const DEFAULT_TIMER_DURATION_PER_TYPE: Record<Extract<ToastType, 'success'|'info
|
|
|
14
22
|
info: 7 * 1000,
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Identity shown in place of the status icon. Size and variant are fixed by
|
|
27
|
+
* the design, so only the identity-bearing `UiAvatar` props are exposed.
|
|
28
|
+
*/
|
|
29
|
+
export interface ToastAvatar {
|
|
30
|
+
src?: string | null
|
|
31
|
+
firstName?: string | null
|
|
32
|
+
lastName?: string | null
|
|
33
|
+
customText?: string | null
|
|
34
|
+
color?: string | null
|
|
35
|
+
alt?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every field a toast understands, all optional. `openToast` narrows this to
|
|
40
|
+
* `ToastOptions`, which additionally demands something to display.
|
|
41
|
+
*/
|
|
42
|
+
export interface ToastBaseOptions {
|
|
43
|
+
message?: string
|
|
44
|
+
/**
|
|
45
|
+
* Rendered in place of `message`, inside the default layout — `component`
|
|
46
|
+
* replaces the whole body instead. Markup, and sanitizing it, stays with
|
|
47
|
+
* the caller.
|
|
48
|
+
*/
|
|
49
|
+
messageComponent?: Component
|
|
50
|
+
messageComponentProps?: Record<string, unknown>
|
|
19
51
|
id?: PropertyKey
|
|
20
52
|
title?: string
|
|
21
53
|
info?: string
|
|
22
54
|
type?: ThemeColorType
|
|
55
|
+
avatar?: ToastAvatar
|
|
23
56
|
time?: number | boolean // if time is explicitly false, the toast will not auto-close
|
|
24
|
-
|
|
57
|
+
progressIndeterminate?: boolean
|
|
25
58
|
customIcon?: string
|
|
26
59
|
confirmLabel?: string
|
|
27
60
|
confirmHref?: string
|
|
@@ -29,6 +62,7 @@ export interface ToastOptions {
|
|
|
29
62
|
confirmTo?: RouteLocationRaw
|
|
30
63
|
confirmIcon?: string
|
|
31
64
|
confirmColor?: ThemeColorType
|
|
65
|
+
actionsSize?: Size
|
|
32
66
|
cancelLabel?: string
|
|
33
67
|
locked?: boolean
|
|
34
68
|
closable?: boolean
|
|
@@ -36,116 +70,253 @@ export interface ToastOptions {
|
|
|
36
70
|
componentProps?: Record<string, unknown>
|
|
37
71
|
}
|
|
38
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A toast has to show something. Requiring one of the three content fields
|
|
75
|
+
* here means an empty toast is a type error rather than an empty white box.
|
|
76
|
+
*/
|
|
77
|
+
export type ToastContent =
|
|
78
|
+
| { message: string }
|
|
79
|
+
| { messageComponent: Component }
|
|
80
|
+
| { title: string }
|
|
81
|
+
| { component: Component }
|
|
82
|
+
|
|
83
|
+
export type ToastOptions = ToastBaseOptions & ToastContent
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The caller's options as the store keeps them: `time` has been resolved to
|
|
87
|
+
* milliseconds, so nothing downstream has to handle the `true` shorthand.
|
|
88
|
+
*/
|
|
89
|
+
type StoredToastOptions = Omit<ToastBaseOptions, 'time'> & { time?: number }
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A toast on screen: what the caller asked for, kept in `options`, plus the
|
|
93
|
+
* bookkeeping the store needs alongside it. They stay apart so `LayoutToasts`
|
|
94
|
+
* can hand the component the options without the timer handle and the event
|
|
95
|
+
* hooks falling through onto the DOM.
|
|
96
|
+
*/
|
|
39
97
|
interface ToastItem {
|
|
40
98
|
id: PropertyKey
|
|
41
|
-
|
|
42
|
-
title?: string
|
|
43
|
-
info?: string
|
|
44
|
-
type?: ThemeColorType
|
|
45
|
-
icon?: string
|
|
46
|
-
time?: number
|
|
47
|
-
locked?: boolean
|
|
48
|
-
progessIndeterminate?: boolean
|
|
99
|
+
options: StoredToastOptions
|
|
49
100
|
closeTimer: any
|
|
101
|
+
// The pointer is resting on this toast, so it must not be given a timer
|
|
102
|
+
// back when the visible window is recomputed.
|
|
103
|
+
paused: boolean
|
|
104
|
+
// When the current run of the close timer started. The progress bar animation
|
|
105
|
+
// is component state, so without this it has no way to observe a restart.
|
|
106
|
+
timerStartedAt: number
|
|
50
107
|
confirmEventHook: EventHook
|
|
51
108
|
cancelEventHook: EventHook
|
|
52
109
|
}
|
|
53
110
|
|
|
54
|
-
|
|
111
|
+
/**
|
|
112
|
+
* `true` picks the per-type default; a number is used as-is; `false` (or
|
|
113
|
+
* unset) means no auto-close — normalized to undefined so the timer is never
|
|
114
|
+
* started, and so nothing downstream has to divide a boolean by 1000.
|
|
115
|
+
*/
|
|
116
|
+
function resolveTime(time: ToastBaseOptions['time'], type: ThemeColorType): number | undefined {
|
|
117
|
+
const typeWithTimer = type === 'success' || type === 'info'
|
|
118
|
+
|
|
119
|
+
return time === true
|
|
120
|
+
? (typeWithTimer ? DEFAULT_TIMER_DURATION_PER_TYPE[type] : undefined)
|
|
121
|
+
: (typeof time === 'number' ? time : undefined)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cast past `UnwrapRef`, which mangles the `Component` in `options` into a
|
|
125
|
+
// shape that no longer matches ToastItem. Nothing here holds a nested ref, so
|
|
126
|
+
// the unwrapped type and the declared one are the same at runtime.
|
|
127
|
+
const toasts = ref<ToastItem[]>([]) as Ref<ToastItem[]>
|
|
55
128
|
|
|
56
|
-
export function useToasts
|
|
129
|
+
export function useToasts() {
|
|
57
130
|
return {
|
|
58
131
|
toasts,
|
|
59
|
-
closeToast,
|
|
60
132
|
openToast,
|
|
133
|
+
updateToast,
|
|
134
|
+
restartToast,
|
|
135
|
+
closeToast,
|
|
61
136
|
cancelCloseTimer,
|
|
62
137
|
}
|
|
63
138
|
}
|
|
64
139
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
id = item.id
|
|
73
|
-
Object.assign(item, {
|
|
74
|
-
...options,
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const restartTimer = options.id && Object.keys(options).length === 1
|
|
78
|
-
if (restartTimer) {
|
|
79
|
-
startTimer(item)
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
id = options.id ?? uid++
|
|
83
|
-
|
|
84
|
-
const type = options.type || 'info'
|
|
85
|
-
const typeWithTimer = type === 'success' || type === 'info'
|
|
86
|
-
// `true` picks the per-type default; a number is used as-is; `false`
|
|
87
|
-
// (or unset) means no auto-close — normalized to undefined so the timer
|
|
88
|
-
// is never started.
|
|
89
|
-
const time = options.time === true
|
|
90
|
-
? (typeWithTimer ? DEFAULT_TIMER_DURATION_PER_TYPE[type] : undefined)
|
|
91
|
-
: (typeof options.time === 'number' ? options.time : undefined)
|
|
92
|
-
item = {
|
|
93
|
-
...options,
|
|
94
|
-
id,
|
|
95
|
-
closeTimer: null,
|
|
96
|
-
confirmEventHook,
|
|
97
|
-
cancelEventHook,
|
|
98
|
-
time,
|
|
99
|
-
type,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
toasts.value.push(item)
|
|
140
|
+
/** What a caller gets back for the toast it just opened or patched. */
|
|
141
|
+
interface ToastHandle {
|
|
142
|
+
id: PropertyKey
|
|
143
|
+
close: () => void
|
|
144
|
+
onConfirm: EventHook['on']
|
|
145
|
+
onCancel: EventHook['on']
|
|
146
|
+
}
|
|
103
147
|
|
|
104
|
-
|
|
105
|
-
}
|
|
148
|
+
function toastHandle(item: ToastItem): ToastHandle {
|
|
149
|
+
const { id } = item
|
|
106
150
|
|
|
107
151
|
return {
|
|
108
152
|
id,
|
|
109
153
|
close: () => {
|
|
110
154
|
closeToast(id)
|
|
111
155
|
},
|
|
112
|
-
|
|
113
|
-
|
|
156
|
+
// Read off the item, so a patched toast hands back the hooks it was
|
|
157
|
+
// opened with rather than a fresh pair nothing would ever trigger.
|
|
158
|
+
onConfirm: item.confirmEventHook.on,
|
|
159
|
+
onCancel: item.cancelEventHook.on,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The store is a deep ref, so a component definition kept in it would be
|
|
165
|
+
* proxied on every render — Vue warns about exactly that.
|
|
166
|
+
*/
|
|
167
|
+
function withRawComponents<T extends ToastBaseOptions>(options: T): T {
|
|
168
|
+
const { component, messageComponent } = options
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
...options,
|
|
172
|
+
...(component && { component: markRaw(component) }),
|
|
173
|
+
...(messageComponent && { messageComponent: markRaw(messageComponent) }),
|
|
114
174
|
}
|
|
115
175
|
}
|
|
116
176
|
|
|
117
|
-
|
|
177
|
+
function applyPatch(item: ToastItem, patch: Omit<ToastBaseOptions, 'id'>) {
|
|
178
|
+
const { time, ...rest } = patch
|
|
179
|
+
Object.assign(item.options, withRawComponents(rest))
|
|
180
|
+
|
|
181
|
+
// Patching goes through the same normalization as opening, so the store only
|
|
182
|
+
// ever holds milliseconds and the progress bar never divides a boolean.
|
|
183
|
+
if (time !== undefined) {
|
|
184
|
+
item.options.time = resolveTime(time, item.options.type ?? 'info')
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Opens a toast. An `id` is an identity, so opening one that is already on
|
|
190
|
+
* screen patches it in place instead of stacking a second copy — that is what
|
|
191
|
+
* the room's per-user and per-link toasts rely on. It does not restart the
|
|
192
|
+
* close timer; call `restartToast` for that.
|
|
193
|
+
*/
|
|
194
|
+
export function openToast(options: ToastOptions): ToastHandle {
|
|
195
|
+
const open = toasts.value.find(item => item.id === options.id)
|
|
196
|
+
if (open) {
|
|
197
|
+
// Re-opening is a fresh occurrence of the same identity, and the caller is
|
|
198
|
+
// about to bind the handlers for it. Without dropping the previous ones,
|
|
199
|
+
// every re-open leaves another subscriber on hooks nothing ever clears.
|
|
200
|
+
open.confirmEventHook.clear()
|
|
201
|
+
open.cancelEventHook.clear()
|
|
202
|
+
applyPatch(open, options)
|
|
203
|
+
|
|
204
|
+
return toastHandle(open)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const type = options.type || 'info'
|
|
208
|
+
const id = options.id ?? uid++
|
|
209
|
+
const item: ToastItem = {
|
|
210
|
+
id,
|
|
211
|
+
options: {
|
|
212
|
+
...withRawComponents(options),
|
|
213
|
+
id,
|
|
214
|
+
type,
|
|
215
|
+
time: resolveTime(options.time, type),
|
|
216
|
+
},
|
|
217
|
+
closeTimer: null,
|
|
218
|
+
paused: false,
|
|
219
|
+
timerStartedAt: 0,
|
|
220
|
+
// Created with the toast, not on every call, so the hooks outlive a patch.
|
|
221
|
+
confirmEventHook: createEventHook<void>(),
|
|
222
|
+
cancelEventHook: createEventHook<void>(),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
toasts.value.push(item)
|
|
226
|
+
syncTimers()
|
|
227
|
+
|
|
228
|
+
return toastHandle(item)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Patches an open toast. Unlike `openToast` it needs no content, so a caller
|
|
233
|
+
* can change one field. Returns undefined if the toast has already closed.
|
|
234
|
+
*/
|
|
235
|
+
export function updateToast(id: PropertyKey, patch: Omit<ToastBaseOptions, 'id'>): ToastHandle | undefined {
|
|
236
|
+
const item = toasts.value.find(item => item.id === id)
|
|
237
|
+
if (!item) return
|
|
238
|
+
|
|
239
|
+
applyPatch(item, patch)
|
|
240
|
+
|
|
241
|
+
return toastHandle(item)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Runs the close timer again from zero, e.g. when the pointer leaves. */
|
|
245
|
+
export function restartToast(id: PropertyKey) {
|
|
246
|
+
const item = toasts.value.find(item => item.id === id)
|
|
247
|
+
if (!item) return
|
|
248
|
+
|
|
249
|
+
item.paused = false
|
|
250
|
+
startTimer(item)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function closeToast(id: PropertyKey) {
|
|
118
254
|
const item = toasts.value.find(item => item.id === id)
|
|
119
255
|
if (item) {
|
|
120
|
-
|
|
256
|
+
stopTimer(item)
|
|
121
257
|
removeToast(id)
|
|
258
|
+
// Closing one frees a slot, so whatever was queued behind it can start
|
|
259
|
+
// counting down now that it is on screen.
|
|
260
|
+
syncTimers()
|
|
122
261
|
}
|
|
123
262
|
}
|
|
124
263
|
|
|
125
|
-
export function cancelCloseTimer
|
|
264
|
+
export function cancelCloseTimer(id: PropertyKey) {
|
|
126
265
|
const item = toasts.value.find(item => item.id === id)
|
|
127
266
|
if (item) {
|
|
128
|
-
|
|
267
|
+
item.paused = true
|
|
268
|
+
stopTimer(item)
|
|
129
269
|
}
|
|
130
270
|
}
|
|
131
271
|
|
|
132
|
-
export function resetToasts
|
|
272
|
+
export function resetToasts() {
|
|
273
|
+
toasts.value.forEach(stopTimer)
|
|
133
274
|
toasts.value = []
|
|
134
275
|
}
|
|
135
276
|
|
|
136
|
-
function removeToast
|
|
277
|
+
function removeToast(id: PropertyKey) {
|
|
137
278
|
const index = toasts.value.findIndex(item => item.id === id)
|
|
138
279
|
if (index !== -1) {
|
|
139
280
|
toasts.value.splice(index, 1)
|
|
140
281
|
}
|
|
141
282
|
}
|
|
142
283
|
|
|
143
|
-
|
|
144
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Gives a close timer to every toast on screen and takes it away from the ones
|
|
286
|
+
* queued behind them. Without this a queued toast counts down while nothing is
|
|
287
|
+
* rendered and can be removed before it is ever seen.
|
|
288
|
+
*/
|
|
289
|
+
function syncTimers(): void {
|
|
290
|
+
toasts.value.forEach((item, index) => {
|
|
291
|
+
const onScreen = index < MAX_ITEMS_DISPLAYED
|
|
292
|
+
|
|
293
|
+
if (!onScreen || item.paused) {
|
|
294
|
+
stopTimer(item)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Guarded so resettling the window never restarts a countdown that is
|
|
299
|
+
// already running, which would also replay the progress bar.
|
|
300
|
+
if (!item.closeTimer) startTimer(item)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function stopTimer(item: ToastItem): void {
|
|
305
|
+
clearTimeout(item.closeTimer)
|
|
306
|
+
item.closeTimer = null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function startTimer(item: ToastItem): void {
|
|
310
|
+
// Idempotent: without this a restart would orphan the previous timeout,
|
|
311
|
+
// which would go on to close the toast early.
|
|
312
|
+
stopTimer(item)
|
|
313
|
+
|
|
314
|
+
if (item.options.time) {
|
|
315
|
+
item.timerStartedAt = performance.now()
|
|
145
316
|
item.closeTimer = setTimeout(
|
|
146
317
|
() => {
|
|
147
318
|
closeToast(item.id)
|
|
148
|
-
}, item.time
|
|
319
|
+
}, item.options.time
|
|
149
320
|
)
|
|
150
321
|
}
|
|
151
322
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, provide } from 'vue'
|
|
3
3
|
import type { ActionBarOrientation, ActionBarPosition, ActionBarSize, ActionBarTooltipSide } from './types'
|
|
4
|
-
import { ACTION_BAR_CONTEXT_KEY } from './types'
|
|
4
|
+
import { ACTION_BAR_CONTEXT_KEY, ACTION_BAR_RAIL_RADIUS } from './types'
|
|
5
5
|
|
|
6
6
|
interface UiActionBarProps {
|
|
7
7
|
/**
|
|
@@ -54,6 +54,7 @@ const SIZE_CLASSES: Record<ActionBarSize, string> = {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const sizeClasses = computed(() => SIZE_CLASSES[props.size])
|
|
57
|
+
const railRadiusClass = computed(() => ACTION_BAR_RAIL_RADIUS[props.size])
|
|
57
58
|
|
|
58
59
|
// When `position` is set, anchor the bar to an edge of its positioned
|
|
59
60
|
// ancestor. Offsets are inline styles (rem) so any `offset` value works
|
|
@@ -86,8 +87,8 @@ const positionStyle = computed<Record<string, string>>(() => {
|
|
|
86
87
|
:aria-label="ariaLabel"
|
|
87
88
|
:aria-orientation="orientation"
|
|
88
89
|
data-testid="ui-action-bar"
|
|
89
|
-
class="inline-flex items-center bg-surface-neutral-bg-main
|
|
90
|
-
:class="[sizeClasses, { 'flex-col': isVertical, 'absolute z-10': position }]"
|
|
90
|
+
class="inline-flex items-center bg-surface-neutral-bg-main border border-container-border shadow-lg"
|
|
91
|
+
:class="[sizeClasses, railRadiusClass, { 'flex-col': isVertical, 'absolute z-10': position }]"
|
|
91
92
|
:style="positionStyle"
|
|
92
93
|
>
|
|
93
94
|
<slot />
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { RouteLocationRaw } from 'vue-router'
|
|
12
12
|
import { useIconSize } from '../../composables/useIconSize'
|
|
13
13
|
import type { ActionBarSize } from './types'
|
|
14
|
-
import { ACTION_BAR_CONTEXT_KEY } from './types'
|
|
14
|
+
import { ACTION_BAR_CONTEXT_KEY, ACTION_BAR_ITEM_RADIUS } from './types'
|
|
15
15
|
|
|
16
16
|
interface UiActionBarItemProps {
|
|
17
17
|
/**
|
|
@@ -101,6 +101,9 @@ const iconSizeClass = useIconSize(size)
|
|
|
101
101
|
const itemClasses = computed(() => [
|
|
102
102
|
props.showLabel ? sizeConfig.value.padded : sizeConfig.value.square,
|
|
103
103
|
sizeConfig.value.text,
|
|
104
|
+
ACTION_BAR_ITEM_RADIUS[size.value],
|
|
105
|
+
// Labeled items in a vertical rail stretch so their icons align.
|
|
106
|
+
props.showLabel && orientation.value === 'vertical' ? 'w-full justify-start' : 'justify-center',
|
|
104
107
|
{
|
|
105
108
|
'bg-actions-neutral-active-overlay': props.active === true,
|
|
106
109
|
'cursor-pointer': !props.disabled,
|
|
@@ -134,10 +137,10 @@ function onClick (event: MouseEvent) {
|
|
|
134
137
|
:aria-label="label"
|
|
135
138
|
:aria-pressed="active"
|
|
136
139
|
:data-testid="testId"
|
|
137
|
-
class="group inline-flex items-center
|
|
140
|
+
class="group inline-flex items-center shrink-0 outline-none transition-colors
|
|
138
141
|
text-actions-neutral-idle
|
|
139
142
|
hover:bg-actions-neutral-hover-overlay
|
|
140
|
-
focus-visible:
|
|
143
|
+
focus-visible:focus-ring
|
|
141
144
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
142
145
|
:class="itemClasses"
|
|
143
146
|
@click="onClick"
|
|
@@ -156,7 +159,7 @@ function onClick (event: MouseEvent) {
|
|
|
156
159
|
:side="tooltipSide"
|
|
157
160
|
:side-offset="8"
|
|
158
161
|
data-colors-semantic="light"
|
|
159
|
-
class="inline-flex items-center justify-center gap-1.5 py-1.5 px-3 bg-surface-neutral-invert-bg-alpha text-text-neutral-invert-base text-content-text-md-regular rounded-
|
|
162
|
+
class="inline-flex items-center justify-center gap-1.5 py-1.5 px-3 bg-surface-neutral-invert-bg-alpha text-text-neutral-invert-base text-content-text-md-regular rounded-row text-center z-overlay shadow-shadows-drop-shadow-lg
|
|
160
163
|
data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:zoom-in-95"
|
|
161
164
|
>
|
|
162
165
|
{{ label }}
|
|
@@ -183,10 +186,10 @@ function onClick (event: MouseEvent) {
|
|
|
183
186
|
:aria-label="label"
|
|
184
187
|
:aria-pressed="active"
|
|
185
188
|
:data-testid="testId"
|
|
186
|
-
class="group inline-flex items-center
|
|
189
|
+
class="group inline-flex items-center shrink-0 outline-none transition-colors
|
|
187
190
|
text-actions-neutral-idle
|
|
188
191
|
hover:bg-actions-neutral-hover-overlay
|
|
189
|
-
focus-visible:
|
|
192
|
+
focus-visible:focus-ring
|
|
190
193
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
191
194
|
:class="itemClasses"
|
|
192
195
|
@click="onClick"
|
|
@@ -24,3 +24,16 @@ export interface ActionBarContext {
|
|
|
24
24
|
*/
|
|
25
25
|
export const ACTION_BAR_CONTEXT_KEY: InjectionKey<ActionBarContext> =
|
|
26
26
|
Symbol('ui-action-bar-context')
|
|
27
|
+
|
|
28
|
+
export const ACTION_BAR_RAIL_RADIUS: Record<ActionBarSize, string> = {
|
|
29
|
+
xs: 'rounded-button-sm',
|
|
30
|
+
sm: 'rounded-button-sm',
|
|
31
|
+
md: 'rounded-field-lg',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Item radius stays concentric with the rail: rail radius minus rail padding and border.
|
|
35
|
+
export const ACTION_BAR_ITEM_RADIUS: Record<ActionBarSize, string> = {
|
|
36
|
+
xs: 'rounded-[calc(var(--radius-button-sm)_-_3px)]',
|
|
37
|
+
sm: 'rounded-[calc(var(--radius-button-sm)_-_3px)]',
|
|
38
|
+
md: 'rounded-[calc(var(--radius-field-lg)_-_5px)]',
|
|
39
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
:style="hasBadge ? undefined : customStyle"
|
|
5
5
|
:data-size="size"
|
|
6
6
|
:data-variant="variant"
|
|
7
|
+
data-testid="ui-avatar"
|
|
7
8
|
>
|
|
8
9
|
<span
|
|
9
10
|
:class="surfaceClasses"
|
|
@@ -38,6 +39,9 @@
|
|
|
38
39
|
<script setup lang="ts">
|
|
39
40
|
import { computed, ref, useSlots, watch } from 'vue'
|
|
40
41
|
|
|
42
|
+
/** `xs` 24 · `sm` 32 · `md` 40 · `lg` 48 · `xl` 64 · `10xl` 128 */
|
|
43
|
+
export type UiAvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '10xl'
|
|
44
|
+
|
|
41
45
|
interface Props {
|
|
42
46
|
/** Image URL. Initials are shown when missing or when the image fails to load. */
|
|
43
47
|
src?: string | null
|
|
@@ -49,7 +53,7 @@ interface Props {
|
|
|
49
53
|
* Avatar size. Maps to the `--spacing-component-*` tokens:
|
|
50
54
|
* `xs` 24, `sm` 32, `md` 40, `lg` 48, `xl` 64, `10xl` 128.
|
|
51
55
|
*/
|
|
52
|
-
size?:
|
|
56
|
+
size?: UiAvatarSize
|
|
53
57
|
/**
|
|
54
58
|
* `default` is light-on-dark (used standalone or inside an avatar list);
|
|
55
59
|
* `more` is the light-grey `+N` chip used by `UiAvatarList` overflow.
|
|
@@ -90,7 +94,9 @@ const customStyle = computed(() => {
|
|
|
90
94
|
if (props.variant === 'more') return undefined
|
|
91
95
|
if (props.src && !imageFailed.value) return undefined
|
|
92
96
|
if (!props.color) return undefined
|
|
93
|
-
|
|
97
|
+
// A fixed custom color doesn't follow the semantic scope, so the ink must
|
|
98
|
+
// not either: the invert token would go dark-on-dark in the dark scope.
|
|
99
|
+
return { backgroundColor: props.color, color: 'white' }
|
|
94
100
|
})
|
|
95
101
|
|
|
96
102
|
const imageFailed = ref(false)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
:class="['ui-avatar-list inline-flex items-center',
|
|
3
|
+
:class="['ui-avatar-list inline-flex items-center', cutoutClasses]"
|
|
4
4
|
:data-size="size"
|
|
5
5
|
>
|
|
6
6
|
<div
|
|
7
7
|
v-for="(item, idx) in visibleItems"
|
|
8
8
|
:key="item.id ?? idx"
|
|
9
|
-
:class="[
|
|
9
|
+
:class="[...ITEM_CLASSES, idx > 0 && overlapClass]"
|
|
10
10
|
>
|
|
11
11
|
<UiAvatar
|
|
12
12
|
:src="item.src"
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
<div
|
|
20
20
|
v-if="overflowCount > 0"
|
|
21
|
-
|
|
21
|
+
data-testid="ui-avatar-list-overflow"
|
|
22
|
+
:class="[...ITEM_CLASSES, visibleItems.length > 0 && overlapClass]"
|
|
22
23
|
>
|
|
23
24
|
<UiAvatar
|
|
24
25
|
:custom-text="`+${overflowCount}`"
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
<script setup lang="ts">
|
|
33
34
|
import { computed } from 'vue'
|
|
34
35
|
import UiAvatar from '../UiAvatar/UiAvatar.vue'
|
|
36
|
+
import type { UiAvatarSize } from '../UiAvatar/UiAvatar.vue'
|
|
35
37
|
|
|
36
38
|
export interface AvatarListItem {
|
|
37
39
|
id?: string | number
|
|
@@ -53,7 +55,7 @@ interface Props {
|
|
|
53
55
|
/** Maximum visible avatars before showing the `+N` overflow chip. */
|
|
54
56
|
max?: number
|
|
55
57
|
/** Mirrors `UiAvatar`'s size scale. The overlap scales with the size. */
|
|
56
|
-
size?:
|
|
58
|
+
size?: UiAvatarSize
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -68,42 +70,71 @@ const visibleItems = computed(() => props.items.slice(0, props.max))
|
|
|
68
70
|
|
|
69
71
|
const overflowCount = computed(() => Math.max(0, effectiveTotal.value - props.max))
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// the
|
|
73
|
+
const ITEM_CLASSES = ['ui-avatar-list__item', 'flex']
|
|
74
|
+
|
|
75
|
+
// Each avatar after the first is pulled back over its predecessor with a
|
|
76
|
+
// negative inline-start margin. The overlap scales with the diameter so larger
|
|
77
|
+
// sizes don't look disproportionately stacked.
|
|
78
|
+
//
|
|
79
|
+
// The margin deliberately sits on the items after the first, never on the last
|
|
80
|
+
// one. A trailing negative margin would make the root's content box narrower
|
|
81
|
+
// than what it paints, and any host rule that sizes the wrappers against it
|
|
82
|
+
// (DataView tables apply `max-width: 100%` to every `div` in a cell) would then
|
|
83
|
+
// shrink the wrapper and clip the avatar. With the overlap on the leading edge
|
|
84
|
+
// the root is exactly as wide as its content, so such rules are a no-op.
|
|
74
85
|
const overlapClass = computed(() => {
|
|
75
86
|
switch (props.size) {
|
|
76
87
|
case 'sm':
|
|
77
|
-
return '-
|
|
88
|
+
return '-ms-2'
|
|
78
89
|
case 'md':
|
|
79
|
-
return '-
|
|
90
|
+
return '-ms-2.5'
|
|
80
91
|
case 'lg':
|
|
81
|
-
return '-
|
|
92
|
+
return '-ms-3'
|
|
82
93
|
case 'xl':
|
|
83
|
-
return '-
|
|
94
|
+
return '-ms-4'
|
|
84
95
|
case '10xl':
|
|
85
|
-
return '-
|
|
96
|
+
return '-ms-8'
|
|
86
97
|
case 'xs':
|
|
87
98
|
default:
|
|
88
|
-
return '-
|
|
99
|
+
return '-ms-1.5'
|
|
89
100
|
}
|
|
90
101
|
})
|
|
91
102
|
|
|
92
|
-
|
|
103
|
+
// Every avatar but the last is cut by the next one: a transparent hole the size
|
|
104
|
+
// of the next avatar plus a gap, so the stack reads on any background.
|
|
105
|
+
// `--avatar-step` is the distance between two avatar centres (diameter minus
|
|
106
|
+
// overlap), `--avatar-cutout` the hole radius (half diameter plus a gap of
|
|
107
|
+
// 0.5 spacing unit), both in `--spacing` units like the classes above.
|
|
108
|
+
const cutoutClasses = computed(() => {
|
|
93
109
|
switch (props.size) {
|
|
94
110
|
case 'sm':
|
|
95
|
-
return '
|
|
111
|
+
return '[--avatar-step:calc(var(--spacing)*6)] [--avatar-cutout:calc(var(--spacing)*4.5)]'
|
|
96
112
|
case 'md':
|
|
97
|
-
return '
|
|
113
|
+
return '[--avatar-step:calc(var(--spacing)*7.5)] [--avatar-cutout:calc(var(--spacing)*5.5)]'
|
|
98
114
|
case 'lg':
|
|
99
|
-
return '
|
|
115
|
+
return '[--avatar-step:calc(var(--spacing)*9)] [--avatar-cutout:calc(var(--spacing)*6.5)]'
|
|
100
116
|
case 'xl':
|
|
101
|
-
return '
|
|
117
|
+
return '[--avatar-step:calc(var(--spacing)*12)] [--avatar-cutout:calc(var(--spacing)*8.5)]'
|
|
102
118
|
case '10xl':
|
|
103
|
-
return '
|
|
119
|
+
return '[--avatar-step:calc(var(--spacing)*24)] [--avatar-cutout:calc(var(--spacing)*16.5)]'
|
|
104
120
|
case 'xs':
|
|
105
121
|
default:
|
|
106
|
-
return '
|
|
122
|
+
return '[--avatar-step:calc(var(--spacing)*4.5)] [--avatar-cutout:calc(var(--spacing)*3.5)]'
|
|
107
123
|
}
|
|
108
124
|
})
|
|
109
125
|
</script>
|
|
126
|
+
|
|
127
|
+
<style scoped>
|
|
128
|
+
.ui-avatar-list__item:not(:last-child) {
|
|
129
|
+
--avatar-next: 1;
|
|
130
|
+
mask-image: radial-gradient(
|
|
131
|
+
circle at calc(50% + var(--avatar-next) * var(--avatar-step)) 50%,
|
|
132
|
+
transparent var(--avatar-cutout),
|
|
133
|
+
var(--color-surface-neutral-bg-main) calc(var(--avatar-cutout) + 0.5px)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ui-avatar-list__item:dir(rtl) {
|
|
138
|
+
--avatar-next: -1;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -80,7 +80,7 @@ const badgeClasses = computed(() => {
|
|
|
80
80
|
'inline-flex',
|
|
81
81
|
'items-center',
|
|
82
82
|
'justify-center',
|
|
83
|
-
'rounded-
|
|
83
|
+
'rounded-full',
|
|
84
84
|
'shrink-0',
|
|
85
85
|
// Only hint interactivity when a tooltip is attached by the consumer.
|
|
86
86
|
props.hasTooltip ? 'cursor-help' : 'cursor-default',
|