@livestorm/ui-vue 2.0.5 → 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 +9 -6
- 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 -222
- package/dist/i18n-B8QOMuFD.mjs +46 -0
- package/dist/index.mjs +4 -87
- 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 +23 -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/UiActionBar.vue.d.ts +3 -1
- package/dist/types/src/components/UiActionBar/UiActionBarItem.vue.d.ts +18 -3
- package/dist/types/src/components/UiActionBar/types.d.ts +5 -1
- 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 +102 -38
- package/dist/types/src/components/UiButtonGroup/UiButtonGroup.vue.d.ts +8 -2
- package/dist/types/src/components/UiButtonGroup/UiButtonGroupItem.vue.d.ts +3 -0
- package/dist/types/src/components/UiButtonGroup/types.d.ts +5 -1
- 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 +9 -4
- 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/UiInputEmoji/UiInputEmoji.vue.d.ts +45 -0
- package/dist/types/src/components/UiInputRadio/RadioGlyph.vue.d.ts +19 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadio.vue.d.ts +80 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadioGroup.vue.d.ts +86 -0
- package/dist/types/src/components/UiInputRadio/types.d.ts +19 -0
- 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/UiLoader.vue.d.ts +6 -5
- package/dist/types/src/components/UiLoader/UiSpinnerIcon.vue.d.ts +3 -0
- package/dist/types/src/components/UiLogo/UiLogo.vue.d.ts +9 -3
- 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 +106 -8
- 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/UiProgress/UiProgress.vue.d.ts +1 -1
- 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 +114 -0
- package/dist/types/src/components/UiSwitch/types.d.ts +3 -0
- 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 +8 -2
- 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 +23 -0
- 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 +13 -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/constants/sizes.d.ts +1 -1
- 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 +22 -0
- package/index.js +44 -0
- package/package.json +28 -20
- package/src/components/Toast/LayoutToasts.vue +19 -11
- package/src/components/Toast/ToastMessage.vue +130 -73
- package/src/components/Toast/toastDSV2.ts +238 -67
- package/src/components/UiActionBar/UiActionBar.vue +15 -5
- package/src/components/UiActionBar/UiActionBarItem.vue +65 -38
- package/src/components/UiActionBar/types.ts +16 -1
- package/src/components/UiAvatar/UiAvatar.vue +9 -3
- package/src/components/UiAvatarList/UiAvatarList.vue +51 -20
- package/src/components/UiBadge/UiBadge.vue +1 -1
- package/src/components/UiBanner/UiBanner.vue +77 -27
- package/src/components/UiBreadcrumb/UiBreadcrumb.vue +1 -1
- package/src/components/UiButton/UiButton.vue +96 -29
- package/src/components/UiButtonGroup/UiButtonGroup.vue +22 -16
- package/src/components/UiButtonGroup/UiButtonGroupItem.vue +44 -14
- package/src/components/UiButtonGroup/types.ts +20 -1
- package/src/components/UiCard/UiCard.vue +20 -23
- package/src/components/UiCarousel/UiCarousel.vue +2 -2
- 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 +16 -13
- 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/UiDialogClose.vue +1 -1
- 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 +45 -18
- package/src/components/UiDropdown/UiDropdownSeparator.vue +1 -1
- package/src/components/UiDropdown/UiDropdownTrigger.vue +139 -45
- 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 +10 -4
- 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 +96 -0
- package/src/components/UiInputRadio/RadioGlyph.vue +45 -0
- package/src/components/UiInputRadio/UiInputRadio.vue +382 -0
- package/src/components/UiInputRadio/UiInputRadioGroup.vue +166 -0
- package/src/components/UiInputRadio/types.ts +24 -0
- 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 +11 -29
- package/src/components/UiLoader/UiSpinnerIcon.vue +21 -0
- package/src/components/UiLogo/UiLogo.vue +41 -7
- 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 +2 -2
- package/src/components/UiNestedOptions/UiNestedOptions.vue +6 -0
- package/src/components/UiPage/UiPage.vue +273 -47
- package/src/components/UiPageHeader/UiPageHeader.vue +29 -9
- 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 +14 -45
- package/src/components/UiRoomCountdown/UiRoomCountdown.vue +376 -0
- package/src/components/UiShell/UiShell.vue +10 -4
- package/src/components/UiSideMenu/UiSideMenu.vue +158 -18
- package/src/components/UiSideMenu/UiSideMenuItemContent.vue +22 -18
- package/src/components/UiStepper/UiStepper.vue +3 -16
- package/src/components/UiStepper/UiStepperDescription.vue +1 -13
- package/src/components/UiStepper/UiStepperIndicator.vue +17 -57
- 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 +375 -0
- package/src/components/UiSwitch/types.ts +5 -0
- 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 +61 -63
- 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 +27 -47
- package/src/components/UiTimeline/README.md +6 -6
- package/src/components/UiTimeline/UiTimelineItem.vue +3 -3
- 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 +66 -0
- 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 +43 -0
- package/src/composables/usePullGesture.ts +142 -0
- package/src/composables/useThemeAndModes.ts +28 -1
- package/src/constants/sizes.ts +1 -1
- package/src/css/body.css +16 -1
- package/src/css/fonts.css +39 -5
- package/src/global.d.ts +44 -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-DJFdCn2R.mjs +0 -5456
- 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/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/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/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/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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { RadioGroupItem, RadioGroupIndicator } from 'reka-ui'
|
|
4
|
+
import type { RadioValue } from './types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
inputId: string
|
|
8
|
+
value: RadioValue
|
|
9
|
+
disabled: boolean
|
|
10
|
+
error: boolean
|
|
11
|
+
ariaDescribedby?: string
|
|
12
|
+
radioClasses: string[]
|
|
13
|
+
indicatorClasses: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
defineProps<Props>()
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
select: [event: Event]
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const itemRef = ref<InstanceType<typeof RadioGroupItem> | null>(null)
|
|
23
|
+
|
|
24
|
+
defineExpose({
|
|
25
|
+
el: (): HTMLElement | undefined => (itemRef.value?.$el as HTMLElement | undefined) ?? undefined,
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<RadioGroupItem
|
|
31
|
+
:id="inputId"
|
|
32
|
+
ref="itemRef"
|
|
33
|
+
:value="value"
|
|
34
|
+
:disabled="disabled"
|
|
35
|
+
:class="radioClasses"
|
|
36
|
+
:aria-invalid="error || undefined"
|
|
37
|
+
:aria-describedby="ariaDescribedby"
|
|
38
|
+
@select="emit('select', $event)"
|
|
39
|
+
>
|
|
40
|
+
<RadioGroupIndicator
|
|
41
|
+
data-testid="ui-input-radio-dot"
|
|
42
|
+
:class="indicatorClasses"
|
|
43
|
+
/>
|
|
44
|
+
</RadioGroupItem>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, provide, ref } from 'vue'
|
|
3
|
+
import { Label, useId } from 'reka-ui'
|
|
4
|
+
import type { RadioValue } from './types'
|
|
5
|
+
import { RADIO_GROUP_KEY } from './types'
|
|
6
|
+
import { CHOICE_ROW_KEY } from '../UiChoice/types'
|
|
7
|
+
import type { ControlPosition } from '../UiChoice/types'
|
|
8
|
+
import UiTooltip from '../UiTooltip/UiTooltip.vue'
|
|
9
|
+
import RadioGlyph from './RadioGlyph.vue'
|
|
10
|
+
|
|
11
|
+
interface UiInputRadioProps {
|
|
12
|
+
/**
|
|
13
|
+
* The value this option represents. `null` is a valid value;
|
|
14
|
+
* object values must be reference-stable.
|
|
15
|
+
*/
|
|
16
|
+
value: RadioValue
|
|
17
|
+
/**
|
|
18
|
+
* The label text
|
|
19
|
+
*/
|
|
20
|
+
label?: string
|
|
21
|
+
/**
|
|
22
|
+
* Description text below the label
|
|
23
|
+
*/
|
|
24
|
+
description?: string
|
|
25
|
+
/**
|
|
26
|
+
* Info tooltip for the label
|
|
27
|
+
*/
|
|
28
|
+
labelInfo?: string
|
|
29
|
+
/**
|
|
30
|
+
* Icon class rendered between the radio and the text (e.g. `i-ph-bell`).
|
|
31
|
+
* In vertical orientation it sits above the text instead.
|
|
32
|
+
*/
|
|
33
|
+
icon?: string
|
|
34
|
+
/**
|
|
35
|
+
* Whether this option is disabled
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
disabled?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Layout orientation of the radio relative to the label
|
|
41
|
+
* @default 'horizontal'
|
|
42
|
+
*/
|
|
43
|
+
orientation?: 'horizontal' | 'vertical'
|
|
44
|
+
/**
|
|
45
|
+
* Which side of the text the radio sits on, in horizontal orientation
|
|
46
|
+
* @default 'start'
|
|
47
|
+
*/
|
|
48
|
+
controlPosition?: ControlPosition
|
|
49
|
+
/**
|
|
50
|
+
* Radio id attribute
|
|
51
|
+
*/
|
|
52
|
+
id?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const props = withDefaults(defineProps<UiInputRadioProps>(), {
|
|
56
|
+
label: undefined,
|
|
57
|
+
description: undefined,
|
|
58
|
+
labelInfo: undefined,
|
|
59
|
+
icon: undefined,
|
|
60
|
+
id: undefined,
|
|
61
|
+
controlPosition: 'start',
|
|
62
|
+
disabled: false,
|
|
63
|
+
orientation: 'horizontal',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
select: [event: Event]
|
|
68
|
+
}>()
|
|
69
|
+
|
|
70
|
+
// Group context: UiInputRadio is only usable inside UiInputRadioGroup
|
|
71
|
+
// (reka's RadioGroupItem enforces this too)
|
|
72
|
+
const group = inject(RADIO_GROUP_KEY, undefined)
|
|
73
|
+
const inChoiceRow = inject(CHOICE_ROW_KEY, false)
|
|
74
|
+
// Controls in the content slot are not the row's control
|
|
75
|
+
provide(CHOICE_ROW_KEY, false)
|
|
76
|
+
|
|
77
|
+
const generatedId = useId()
|
|
78
|
+
const inputId = computed(() => props.id ?? generatedId)
|
|
79
|
+
|
|
80
|
+
// Styling state. Comparison is strict equality: matches reka's isEqual for
|
|
81
|
+
// primitives and null; object values must be reference-stable.
|
|
82
|
+
const isChecked = computed(() => {
|
|
83
|
+
if (!group || group.modelValue.value === undefined) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
return group.modelValue.value === props.value
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const isDisabled = computed(() => props.disabled || (group?.disabled.value ?? false))
|
|
90
|
+
const error = computed(() => group?.error.value ?? false)
|
|
91
|
+
const size = computed(() => group?.size.value ?? 'md')
|
|
92
|
+
const isOptionRow = computed(() => props.orientation === 'horizontal')
|
|
93
|
+
// Side by side, a choice keeps its title on one line: the choice widens or the row
|
|
94
|
+
// wraps instead. A title wider than the whole row truncates.
|
|
95
|
+
const titleOnOneLine = computed(() => inChoiceRow && group?.orientation.value === 'horizontal')
|
|
96
|
+
const controlAtEnd = computed(() => props.orientation === 'horizontal' && props.controlPosition === 'end')
|
|
97
|
+
|
|
98
|
+
const descriptionId = computed(() => props.description ? `${inputId.value}-description` : undefined)
|
|
99
|
+
|
|
100
|
+
const ariaDescribedby = computed(() => {
|
|
101
|
+
const ids = [descriptionId.value, group?.messageId.value].filter(Boolean)
|
|
102
|
+
return ids.length ? ids.join(' ') : undefined
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const radioClasses = computed(() => {
|
|
106
|
+
const baseClasses = [
|
|
107
|
+
'relative shrink-0 rounded-full border',
|
|
108
|
+
'focus:outline-hidden focus-indicator-none',
|
|
109
|
+
'cursor-pointer disabled:cursor-not-allowed',
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const sizeClasses = {
|
|
113
|
+
sm: 'size-unit-0-75rem',
|
|
114
|
+
md: 'size-check-sm',
|
|
115
|
+
lg: 'size-check-md',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
baseClasses.push(sizeClasses[size.value])
|
|
119
|
+
|
|
120
|
+
// Checked is a thin ring around a dot on the light surface; unchecked is a denser
|
|
121
|
+
// fill than the fields, which once disabled falls back to the field fill with an outline
|
|
122
|
+
if (isChecked.value) {
|
|
123
|
+
baseClasses.push('bg-surface-neutral-bg-main', checkedRingClass.value)
|
|
124
|
+
} else if (isDisabled.value && !error.value) {
|
|
125
|
+
baseClasses.push('bg-control-field-bg border-control-disabled-border')
|
|
126
|
+
} else {
|
|
127
|
+
baseClasses.push('bg-control-check-bg', error.value ? 'border-control-error-border' : 'border-control-field-border')
|
|
128
|
+
if (!isDisabled.value) {
|
|
129
|
+
baseClasses.push('group-hover/option:bg-control-check-bg-hover')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return baseClasses
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Transform centering (not inset/margins): layout boxes get snapped to
|
|
137
|
+
// device pixels, which renders fractional gaps visibly off-center
|
|
138
|
+
const dotClasses = computed(() => {
|
|
139
|
+
const sizeClasses = {
|
|
140
|
+
sm: 'size-unit-0-375rem',
|
|
141
|
+
md: 'size-unit-0-5rem',
|
|
142
|
+
lg: 'size-unit-0-625rem',
|
|
143
|
+
}
|
|
144
|
+
return ['absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full', sizeClasses[size.value]]
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const checkedRingClass = computed(() => {
|
|
148
|
+
if (error.value) {
|
|
149
|
+
return 'border-actions-danger-idle'
|
|
150
|
+
}
|
|
151
|
+
return isDisabled.value ? 'border-borders-neutral-strong' : 'border-actions-primary-idle'
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const indicatorClasses = computed(() => {
|
|
155
|
+
if (error.value) {
|
|
156
|
+
return [...dotClasses.value, 'bg-actions-danger-idle']
|
|
157
|
+
}
|
|
158
|
+
return [...dotClasses.value, isDisabled.value ? 'bg-text-neutral-secondary' : 'bg-actions-primary-idle']
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// A choicebox tints its text with the selection. Disabled, a choicebox recedes
|
|
162
|
+
// whole, while a plain option keeps its selection legible in neutral.
|
|
163
|
+
const tone = computed(() => {
|
|
164
|
+
if (error.value) {
|
|
165
|
+
return 'danger'
|
|
166
|
+
}
|
|
167
|
+
if (isDisabled.value) {
|
|
168
|
+
return isChecked.value && !inChoiceRow ? 'neutral' : 'disabled'
|
|
169
|
+
}
|
|
170
|
+
return isChecked.value && inChoiceRow ? 'selected' : 'neutral'
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const labelClasses = computed(() => ({
|
|
174
|
+
'text-content-text-md-medium': inChoiceRow,
|
|
175
|
+
'text-content-text-md-regular': !inChoiceRow,
|
|
176
|
+
'text-text-neutral-base': tone.value === 'neutral',
|
|
177
|
+
'text-text-neutral-tertiary': tone.value === 'disabled',
|
|
178
|
+
'text-text-danger-base': tone.value === 'danger',
|
|
179
|
+
'text-text-primary-title': tone.value === 'selected',
|
|
180
|
+
'flex items-center min-w-0': titleOnOneLine.value,
|
|
181
|
+
'cursor-not-allowed': isDisabled.value,
|
|
182
|
+
'cursor-pointer': !isDisabled.value,
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
const descriptionClasses = computed(() => ({
|
|
186
|
+
'text-center': props.orientation === 'vertical',
|
|
187
|
+
'text-text-neutral-secondary': tone.value === 'neutral',
|
|
188
|
+
'text-text-neutral-tertiary': tone.value === 'disabled',
|
|
189
|
+
'text-text-danger-tertiary': tone.value === 'danger',
|
|
190
|
+
'text-text-primary-base': tone.value === 'selected',
|
|
191
|
+
'cursor-not-allowed': isDisabled.value,
|
|
192
|
+
'cursor-pointer': !isDisabled.value,
|
|
193
|
+
}))
|
|
194
|
+
|
|
195
|
+
const innerWrapperClasses = computed(() => {
|
|
196
|
+
const classes: string[] = ['group/option relative flex h-full']
|
|
197
|
+
|
|
198
|
+
classes.push(props.orientation === 'vertical' ? 'flex-col items-center' : 'items-start')
|
|
199
|
+
|
|
200
|
+
if (isOptionRow.value && !inChoiceRow) {
|
|
201
|
+
// The row bleeds around the option without taking layout space, so a one-line
|
|
202
|
+
// option is a 32px hit target; hover shows on the glyph only. Inside UiChoice the choice draws the row.
|
|
203
|
+
classes.push('isolate before:absolute before:-inset-x-3 before:-inset-y-1.5 before:-z-10 before:rounded-field-md has-[[role=radio]:focus-visible]:before:focus-ring')
|
|
204
|
+
} else if (props.orientation === 'vertical') {
|
|
205
|
+
classes.push(inChoiceRow ? 'rounded-field-md' : 'rounded-field-md has-[[role=radio]:focus-visible]:focus-ring')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
classes.push(isDisabled.value ? 'cursor-not-allowed' : 'cursor-pointer')
|
|
209
|
+
|
|
210
|
+
return classes
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const glyphProps = computed(() => ({
|
|
214
|
+
inputId: inputId.value,
|
|
215
|
+
value: props.value,
|
|
216
|
+
disabled: props.disabled,
|
|
217
|
+
error: error.value,
|
|
218
|
+
ariaDescribedby: ariaDescribedby.value,
|
|
219
|
+
radioClasses: radioClasses.value,
|
|
220
|
+
indicatorClasses: indicatorClasses.value,
|
|
221
|
+
}))
|
|
222
|
+
|
|
223
|
+
const glyphRef = ref<InstanceType<typeof RadioGlyph> | null>(null)
|
|
224
|
+
|
|
225
|
+
function radioElement (): HTMLElement | undefined {
|
|
226
|
+
return glyphRef.value?.el()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Make the whole wrapper (description, padding) select the radio.
|
|
230
|
+
// Clicks on the button itself or on the label are skipped: the button handles
|
|
231
|
+
// its own clicks and the label forwards to the button natively via `for`.
|
|
232
|
+
function onWrapperClick (event: MouseEvent) {
|
|
233
|
+
if (isDisabled.value) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const target = event.target as HTMLElement
|
|
238
|
+
const radioEl = radioElement()
|
|
239
|
+
|
|
240
|
+
if (!radioEl || target === radioEl || radioEl.contains(target)) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (target.closest('label')) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
// Controls in the content slot handle their own clicks
|
|
247
|
+
if (target.closest('input, textarea, select, button, a')) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
radioEl.click()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
defineExpose({
|
|
255
|
+
focus: () => radioElement()?.focus(),
|
|
256
|
+
blur: () => radioElement()?.blur(),
|
|
257
|
+
})
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<template>
|
|
261
|
+
<!-- An option that hosts a field or pushes its radio to the end fills its
|
|
262
|
+
slot; otherwise it shrink-wraps to keep the click zone on content -->
|
|
263
|
+
<div
|
|
264
|
+
class="ui-input-radio-wrapper"
|
|
265
|
+
data-testid="ui-input-radio"
|
|
266
|
+
:class="[$slots.content || controlAtEnd ? 'w-full' : 'w-fit', { 'max-w-full': titleOnOneLine }]"
|
|
267
|
+
>
|
|
268
|
+
<div
|
|
269
|
+
:class="innerWrapperClasses"
|
|
270
|
+
data-testid="ui-input-radio-row"
|
|
271
|
+
@click="onWrapperClick"
|
|
272
|
+
>
|
|
273
|
+
<!-- Radio - horizontal only. The box matches the label's line-height so
|
|
274
|
+
the glyph centers on the label line, and the checkbox's width so
|
|
275
|
+
labels line up; the glyph hugs the outer edge like a checkbox does -->
|
|
276
|
+
<span
|
|
277
|
+
v-if="orientation === 'horizontal'"
|
|
278
|
+
class="flex size-5 items-center shrink-0"
|
|
279
|
+
:class="controlAtEnd ? ['order-last justify-end', inChoiceRow ? 'ms-5' : 'ms-3'] : ['justify-start', inChoiceRow ? 'me-3' : 'me-1']"
|
|
280
|
+
>
|
|
281
|
+
<RadioGlyph
|
|
282
|
+
ref="glyphRef"
|
|
283
|
+
v-bind="glyphProps"
|
|
284
|
+
@select="emit('select', $event)"
|
|
285
|
+
/>
|
|
286
|
+
</span>
|
|
287
|
+
|
|
288
|
+
<!-- Optional icon: between the radio and the text, or above the text in
|
|
289
|
+
vertical orientation. min-h-5 keeps a small icon on the label line -->
|
|
290
|
+
<span
|
|
291
|
+
v-if="icon || $slots.icon"
|
|
292
|
+
class="flex items-center justify-center shrink-0"
|
|
293
|
+
data-testid="ui-input-radio-icon"
|
|
294
|
+
:class="orientation === 'vertical' ? 'mb-2' : 'mr-2 min-h-5'"
|
|
295
|
+
>
|
|
296
|
+
<slot name="icon">
|
|
297
|
+
<span
|
|
298
|
+
class="size-4"
|
|
299
|
+
:class="[icon, {
|
|
300
|
+
'text-text-neutral-base': tone === 'neutral',
|
|
301
|
+
'text-text-neutral-tertiary': tone === 'disabled',
|
|
302
|
+
'text-text-danger-base': tone === 'danger',
|
|
303
|
+
'text-text-primary-title': tone === 'selected',
|
|
304
|
+
}]"
|
|
305
|
+
/>
|
|
306
|
+
</slot>
|
|
307
|
+
</span>
|
|
308
|
+
|
|
309
|
+
<div
|
|
310
|
+
class="flex flex-col min-w-0"
|
|
311
|
+
:class="[orientation === 'vertical' ? 'items-center' : 'flex-1', inChoiceRow ? 'gap-1' : 'gap-0.5']"
|
|
312
|
+
data-testid="ui-input-radio-text"
|
|
313
|
+
>
|
|
314
|
+
<Label
|
|
315
|
+
:for="inputId"
|
|
316
|
+
:class="labelClasses"
|
|
317
|
+
>
|
|
318
|
+
<span
|
|
319
|
+
v-if="titleOnOneLine"
|
|
320
|
+
class="truncate"
|
|
321
|
+
>
|
|
322
|
+
<slot name="label">
|
|
323
|
+
{{ label }}
|
|
324
|
+
</slot>
|
|
325
|
+
</span>
|
|
326
|
+
<slot
|
|
327
|
+
v-else
|
|
328
|
+
name="label"
|
|
329
|
+
>
|
|
330
|
+
{{ label }}
|
|
331
|
+
</slot>
|
|
332
|
+
|
|
333
|
+
<!-- icon: inline so it wraps with the text instead of centering against the whole label block,
|
|
334
|
+
and the word joiner keeps it on the line of the last word.
|
|
335
|
+
-translate-y-px because the icon aligns with the text so that its visually centered -->
|
|
336
|
+
<span
|
|
337
|
+
v-if="labelInfo"
|
|
338
|
+
class="whitespace-nowrap before:content-['\2060']"
|
|
339
|
+
><UiTooltip :content="labelInfo">
|
|
340
|
+
<span
|
|
341
|
+
class="inline-flex items-center justify-center size-4 ms-2 align-middle -translate-y-px cursor-help shrink-0"
|
|
342
|
+
:class="{
|
|
343
|
+
'text-text-neutral-tertiary': tone === 'neutral' || tone === 'disabled',
|
|
344
|
+
'text-text-danger-tertiary': tone === 'danger',
|
|
345
|
+
'text-text-primary-tertiary': tone === 'selected',
|
|
346
|
+
}"
|
|
347
|
+
data-testid="ui-input-radio-label-info"
|
|
348
|
+
@click.stop.prevent
|
|
349
|
+
>
|
|
350
|
+
<span class="i-ph-info-fill size-4" />
|
|
351
|
+
</span>
|
|
352
|
+
</UiTooltip></span>
|
|
353
|
+
</Label>
|
|
354
|
+
|
|
355
|
+
<span
|
|
356
|
+
v-if="description"
|
|
357
|
+
:id="descriptionId"
|
|
358
|
+
class="text-content-text-md-regular"
|
|
359
|
+
:class="descriptionClasses"
|
|
360
|
+
>
|
|
361
|
+
{{ description }}
|
|
362
|
+
</span>
|
|
363
|
+
|
|
364
|
+
<!-- Extra content under the text, e.g. a field the option configures.
|
|
365
|
+
Sits outside the label so it stays out of the accessible name -->
|
|
366
|
+
<slot name="content" />
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<!-- Radio - vertical only -->
|
|
370
|
+
<span
|
|
371
|
+
v-if="orientation === 'vertical'"
|
|
372
|
+
class="flex justify-center mt-2"
|
|
373
|
+
>
|
|
374
|
+
<RadioGlyph
|
|
375
|
+
ref="glyphRef"
|
|
376
|
+
v-bind="glyphProps"
|
|
377
|
+
@select="emit('select', $event)"
|
|
378
|
+
/>
|
|
379
|
+
</span>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</template>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, provide, ref, toRef, watch } from 'vue'
|
|
3
|
+
import { RadioGroupRoot, useId } from 'reka-ui'
|
|
4
|
+
import type { RadioSize, RadioValue } from './types'
|
|
5
|
+
import { RADIO_GROUP_KEY } from './types'
|
|
6
|
+
|
|
7
|
+
interface UiInputRadioGroupProps {
|
|
8
|
+
/**
|
|
9
|
+
* The selected value. `undefined` means no selection;
|
|
10
|
+
* `null` is a valid option value.
|
|
11
|
+
*/
|
|
12
|
+
modelValue?: RadioValue
|
|
13
|
+
/**
|
|
14
|
+
* Name attribute, submitted with the form via a hidden input
|
|
15
|
+
*/
|
|
16
|
+
name?: string
|
|
17
|
+
/**
|
|
18
|
+
* Whether the whole group is disabled
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* Whether arrow-key navigation loops from last item to first
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
loop?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Whether a selection is required
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
required?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Whether the group is in an error state
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
error?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Error message displayed below the group
|
|
39
|
+
*/
|
|
40
|
+
errorMessage?: string
|
|
41
|
+
/**
|
|
42
|
+
* Size of the radio items
|
|
43
|
+
* @default 'md'
|
|
44
|
+
*/
|
|
45
|
+
size?: RadioSize
|
|
46
|
+
/**
|
|
47
|
+
* Layout direction of the items. Also sets the group's `aria-orientation`
|
|
48
|
+
* and the arrow-key navigation axis to match.
|
|
49
|
+
* @default 'vertical'
|
|
50
|
+
*/
|
|
51
|
+
orientation?: 'vertical' | 'horizontal'
|
|
52
|
+
/**
|
|
53
|
+
* Whether the group stretches to fill its container instead of
|
|
54
|
+
* shrink-wrapping its items
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
fullWidth?: boolean
|
|
58
|
+
/**
|
|
59
|
+
* Id of the element naming the group, set on the radiogroup itself
|
|
60
|
+
*/
|
|
61
|
+
ariaLabelledby?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const props = withDefaults(defineProps<UiInputRadioGroupProps>(), {
|
|
65
|
+
modelValue: undefined,
|
|
66
|
+
name: undefined,
|
|
67
|
+
errorMessage: undefined,
|
|
68
|
+
disabled: false,
|
|
69
|
+
loop: true,
|
|
70
|
+
required: false,
|
|
71
|
+
error: false,
|
|
72
|
+
size: 'md',
|
|
73
|
+
orientation: 'vertical',
|
|
74
|
+
fullWidth: false,
|
|
75
|
+
ariaLabelledby: undefined,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const emit = defineEmits<{
|
|
79
|
+
'update:modelValue': [value: RadioValue]
|
|
80
|
+
}>()
|
|
81
|
+
|
|
82
|
+
// Track the current value locally so items can style themselves even when
|
|
83
|
+
// the consumer does not bind v-model (uncontrolled usage)
|
|
84
|
+
const currentValue = ref<RadioValue | undefined>(props.modelValue)
|
|
85
|
+
|
|
86
|
+
watch(() => props.modelValue, (value) => {
|
|
87
|
+
currentValue.value = value
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function onUpdateModelValue (value: RadioValue) {
|
|
91
|
+
currentValue.value = value
|
|
92
|
+
emit('update:modelValue', value)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const groupId = useId()
|
|
96
|
+
|
|
97
|
+
const displayMessage = computed(() => {
|
|
98
|
+
if (props.error && props.errorMessage) {
|
|
99
|
+
return props.errorMessage
|
|
100
|
+
}
|
|
101
|
+
return undefined
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const messageId = computed(() => displayMessage.value ? `${groupId}-message` : undefined)
|
|
105
|
+
|
|
106
|
+
// items-stretch keeps card tiles equal-height when they share a row. The 12px
|
|
107
|
+
// gap holds the 6px bleed of each option row, so one-line rows stay 32px hit
|
|
108
|
+
// targets that never overlap.
|
|
109
|
+
const rootClasses = computed(() =>
|
|
110
|
+
props.orientation === 'horizontal'
|
|
111
|
+
? 'flex flex-row flex-wrap items-stretch gap-3'
|
|
112
|
+
: 'flex flex-col gap-3',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// mt-1.5 clears the 6px bleed of the last option row
|
|
116
|
+
const displayMessageClasses = computed(() => {
|
|
117
|
+
const baseClasses = ['text-content-legends-bold-md mt-1.5']
|
|
118
|
+
|
|
119
|
+
if (props.error) {
|
|
120
|
+
baseClasses.push('text-text-danger-body')
|
|
121
|
+
} else {
|
|
122
|
+
baseClasses.push('text-text-neutral-secondary')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return baseClasses
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
provide(RADIO_GROUP_KEY, {
|
|
129
|
+
modelValue: currentValue,
|
|
130
|
+
error: toRef(props, 'error'),
|
|
131
|
+
size: toRef(props, 'size'),
|
|
132
|
+
orientation: toRef(props, 'orientation'),
|
|
133
|
+
disabled: toRef(props, 'disabled'),
|
|
134
|
+
messageId,
|
|
135
|
+
})
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<div
|
|
140
|
+
class="ui-input-radio-group-wrapper"
|
|
141
|
+
:class="fullWidth ? 'w-full' : 'w-fit'"
|
|
142
|
+
>
|
|
143
|
+
<RadioGroupRoot
|
|
144
|
+
:model-value="currentValue"
|
|
145
|
+
:name="name"
|
|
146
|
+
:disabled="disabled"
|
|
147
|
+
:loop="loop"
|
|
148
|
+
:required="required"
|
|
149
|
+
:orientation="orientation"
|
|
150
|
+
:aria-labelledby="ariaLabelledby"
|
|
151
|
+
:class="rootClasses"
|
|
152
|
+
@update:model-value="onUpdateModelValue"
|
|
153
|
+
>
|
|
154
|
+
<slot />
|
|
155
|
+
</RadioGroupRoot>
|
|
156
|
+
|
|
157
|
+
<!-- Error Message -->
|
|
158
|
+
<p
|
|
159
|
+
v-if="displayMessage"
|
|
160
|
+
:id="messageId"
|
|
161
|
+
:class="displayMessageClasses"
|
|
162
|
+
>
|
|
163
|
+
{{ displayMessage }}
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
import type { AcceptableValue } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
export type RadioSize = 'sm' | 'md' | 'lg'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Valid radio option values. `null` is a supported option value;
|
|
8
|
+
* `undefined` means "no selection". Object values must be reference-stable.
|
|
9
|
+
*/
|
|
10
|
+
export type RadioValue = AcceptableValue
|
|
11
|
+
|
|
12
|
+
export interface RadioGroupContext {
|
|
13
|
+
/** Current group value (kept in sync in both controlled and uncontrolled mode) */
|
|
14
|
+
modelValue: Readonly<Ref<RadioValue | undefined>>
|
|
15
|
+
error: Readonly<Ref<boolean>>
|
|
16
|
+
size: Readonly<Ref<RadioSize>>
|
|
17
|
+
orientation: Readonly<Ref<'vertical' | 'horizontal'>>
|
|
18
|
+
disabled: Readonly<Ref<boolean>>
|
|
19
|
+
/** id of the group error message, for aria-describedby on items */
|
|
20
|
+
messageId: Readonly<Ref<string | undefined>>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const RADIO_GROUP_KEY: InjectionKey<RadioGroupContext> =
|
|
24
|
+
Symbol('ui-input-radio-group')
|
|
@@ -7,6 +7,7 @@ import UiDropdownContent from '../UiDropdown/UiDropdownContent.vue'
|
|
|
7
7
|
import UiDropdownItem from '../UiDropdown/UiDropdownItem.vue'
|
|
8
8
|
import type { AcceptableValue } from 'reka-ui'
|
|
9
9
|
import type { SelectSize } from './types'
|
|
10
|
+
import { useControlShape } from '../../composables/useControlShape'
|
|
10
11
|
|
|
11
12
|
interface UiInputSelectProps {
|
|
12
13
|
size?: SelectSize
|
|
@@ -57,6 +58,8 @@ const props = withDefaults(defineProps<UiInputSelectProps>(), {
|
|
|
57
58
|
multiple: false,
|
|
58
59
|
})
|
|
59
60
|
|
|
61
|
+
const shape = useControlShape('field', () => props.size)
|
|
62
|
+
|
|
60
63
|
// Sentinel value for the "clear" option — intentionally verbose to avoid collisions with real values
|
|
61
64
|
const CLEAR_VALUE = '__ui-input-select-clear__'
|
|
62
65
|
|
|
@@ -110,13 +113,12 @@ function handleUpdate (value: AcceptableValue) {
|
|
|
110
113
|
<!-- Loading State -->
|
|
111
114
|
<div
|
|
112
115
|
v-if="loading"
|
|
113
|
-
class="flex items-center gap-2 w-full
|
|
114
|
-
:class="{
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
|
|
119
|
-
}"
|
|
116
|
+
class="flex items-center gap-2 w-full border border-solid border-control-field-border bg-control-field-bg"
|
|
117
|
+
:class="[shape.radius, shape.height, {
|
|
118
|
+
'px-2': size === 'sm',
|
|
119
|
+
'px-3': size === 'md',
|
|
120
|
+
'px-4': size === 'lg' || size === 'xl',
|
|
121
|
+
}]"
|
|
120
122
|
>
|
|
121
123
|
<div class="h-3 w-24 bg-surface-neutral-alpha-400 rounded-md animate-pulse" />
|
|
122
124
|
</div>
|