@ecoma-io/loom 0.3.0 → 0.4.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/dist/index.js +38 -6
- package/dist/packages/blocks/app-header/src/AppHeader.js.map +1 -1
- package/dist/packages/blocks/app-header/src/AppHeader.vue.d.ts.map +1 -1
- package/dist/packages/blocks/app-header/src/AppHeader.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/packages/blocks/app-header/src/AppHeader.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/blocks/desktop-app-shell/src/DesktopAppShell.vue.d.ts +3 -3
- package/dist/packages/blocks/metric-card/src/MetricCard.js.map +1 -1
- package/dist/packages/blocks/metric-card/src/MetricCard.vue.d.ts +22 -4
- package/dist/packages/blocks/metric-card/src/MetricCard.vue.d.ts.map +1 -1
- package/dist/packages/blocks/metric-card/src/MetricCard.vue_vue_type_script_setup_true_lang.js +38 -15
- package/dist/packages/blocks/metric-card/src/MetricCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/blocks/title-bar/src/TitleBar.vue.d.ts +3 -3
- package/dist/packages/blocks/toast-stack/src/ToastStack.vue.d.ts +3 -3
- package/dist/packages/composition/scroll-reel/src/ScrollReel.js +2 -2
- package/dist/packages/composition/scroll-reel/src/ScrollReel.js.map +1 -1
- package/dist/packages/composition/scroll-reel/src/ScrollReel.vue.d.ts +26 -0
- package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js +27 -7
- package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/composition/scroll-reel/src/index.d.ts +2 -1
- package/dist/packages/composition/scroll-reel/src/index.js +2 -1
- package/dist/packages/core/src/index.d.ts +1 -1
- package/dist/packages/core/src/index.d.ts.map +1 -1
- package/dist/packages/core/src/index.js +2 -2
- package/dist/packages/core/src/motion.d.ts +13 -0
- package/dist/packages/core/src/motion.d.ts.map +1 -1
- package/dist/packages/core/src/motion.js +20 -13
- package/dist/packages/core/src/motion.js.map +1 -1
- package/dist/packages/core/src/theme.d.ts +9 -0
- package/dist/packages/core/src/theme.d.ts.map +1 -1
- package/dist/packages/core/src/theme.js +36 -10
- package/dist/packages/core/src/theme.js.map +1 -1
- package/dist/packages/labels/src/ancestor-disabled.d.ts +5 -1
- package/dist/packages/labels/src/ancestor-disabled.d.ts.map +1 -1
- package/dist/packages/labels/src/ancestor-disabled.js +5 -1
- package/dist/packages/labels/src/ancestor-disabled.js.map +1 -1
- package/dist/packages/labels/src/component-shapes.d.ts +73 -0
- package/dist/packages/labels/src/component-shapes.d.ts.map +1 -1
- package/dist/packages/labels/src/date-labels.d.ts +8 -6
- package/dist/packages/labels/src/date-labels.d.ts.map +1 -1
- package/dist/packages/labels/src/date-labels.js +8 -6
- package/dist/packages/labels/src/date-labels.js.map +1 -1
- package/dist/packages/labels/src/index.d.ts +2 -2
- package/dist/packages/labels/src/index.d.ts.map +1 -1
- package/dist/packages/labels/src/label-registry.d.ts +7 -1
- package/dist/packages/labels/src/label-registry.d.ts.map +1 -1
- package/dist/packages/layouts/reading/src/Reading.js.map +1 -1
- package/dist/packages/layouts/reading/src/Reading.vue.d.ts +16 -15
- package/dist/packages/layouts/reading/src/Reading.vue.d.ts.map +1 -1
- package/dist/packages/layouts/reading/src/Reading.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/layouts/reading/src/index.d.ts +1 -0
- package/dist/packages/layouts/reading/src/index.d.ts.map +1 -1
- package/dist/packages/loom/src/a11y.d.ts +1 -1
- package/dist/packages/loom/src/index.d.ts +191 -174
- package/dist/packages/loom/src/theme.d.ts +3 -0
- package/dist/packages/primitives/alert/src/Alert.js +7 -0
- package/dist/packages/primitives/alert/src/Alert.js.map +1 -0
- package/dist/packages/primitives/alert/src/Alert.vue.d.ts +73 -0
- package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js +127 -0
- package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/alert/src/index.d.ts +4 -0
- package/dist/packages/primitives/alert/src/index.js +3 -0
- package/dist/packages/primitives/alert-dialog/src/AlertDialog.js.map +1 -1
- package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue.d.ts +2 -2
- package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/alert-dialog/src/index.d.ts +1 -1
- package/dist/packages/primitives/avatar-group/src/AvatarGroup.vue.d.ts +3 -3
- package/dist/packages/primitives/avatar-group/src/index.d.ts +1 -1
- package/dist/packages/primitives/badge/src/Badge.js.map +1 -1
- package/dist/packages/primitives/badge/src/Badge.vue.d.ts.map +1 -1
- package/dist/packages/primitives/badge/src/Badge.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/badge/src/Badge.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/breadcrumb/src/Breadcrumb.js +2 -2
- package/dist/packages/primitives/breadcrumb/src/Breadcrumb.js.map +1 -1
- package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue.d.ts +28 -0
- package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue_vue_type_script_setup_true_lang.js +16 -5
- package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/breadcrumb/src/index.d.ts +2 -1
- package/dist/packages/primitives/breadcrumb/src/index.js +2 -1
- package/dist/packages/primitives/button/src/Button.js.map +1 -1
- package/dist/packages/primitives/button/src/Button.vue.d.ts.map +1 -1
- package/dist/packages/primitives/button/src/Button.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/button/src/index.js +2 -2
- package/dist/packages/primitives/card/src/Card.js +7 -0
- package/dist/packages/primitives/card/src/Card.js.map +1 -0
- package/dist/packages/primitives/card/src/Card.vue.d.ts +72 -0
- package/dist/packages/primitives/card/src/Card.vue_vue_type_script_setup_true_lang.js +65 -0
- package/dist/packages/primitives/card/src/Card.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/card/src/index.d.ts +3 -0
- package/dist/packages/primitives/card/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/card/src/index.js +2 -0
- package/dist/packages/primitives/carousel/src/Carousel.js +7 -0
- package/dist/packages/primitives/carousel/src/Carousel.js.map +1 -0
- package/dist/packages/primitives/carousel/src/Carousel.vue.d.ts +34 -0
- package/dist/packages/primitives/carousel/src/Carousel.vue_vue_type_script_setup_true_lang.js +175 -0
- package/dist/packages/primitives/carousel/src/Carousel.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/carousel/src/index.d.ts +3 -0
- package/dist/packages/primitives/carousel/src/index.js +3 -0
- package/dist/packages/primitives/checkbox/src/Checkbox.js.map +1 -1
- package/dist/packages/primitives/checkbox/src/Checkbox.vue.d.ts +1 -1
- package/dist/packages/primitives/checkbox/src/Checkbox.vue.d.ts.map +1 -1
- package/dist/packages/primitives/checkbox/src/Checkbox.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/packages/primitives/checkbox/src/Checkbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/chip/src/Chip.js.map +1 -1
- package/dist/packages/primitives/chip/src/Chip.vue.d.ts.map +1 -1
- package/dist/packages/primitives/chip/src/Chip.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/chip/src/Chip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/collapse/src/Collapse.js +7 -0
- package/dist/packages/primitives/collapse/src/Collapse.js.map +1 -0
- package/dist/packages/primitives/collapse/src/Collapse.vue.d.ts +56 -0
- package/dist/packages/primitives/collapse/src/Collapse.vue.d.ts.map +1 -0
- package/dist/packages/primitives/collapse/src/Collapse.vue_vue_type_script_setup_true_lang.js +81 -0
- package/dist/packages/primitives/collapse/src/Collapse.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/collapse/src/index.d.ts +2 -0
- package/dist/packages/primitives/collapse/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/collapse/src/index.js +2 -0
- package/dist/packages/primitives/color-picker/src/ColorPicker.js.map +1 -1
- package/dist/packages/primitives/color-picker/src/ColorPicker.vue.d.ts +4 -4
- package/dist/packages/primitives/color-picker/src/ColorPicker.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/packages/primitives/color-picker/src/ColorPicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/color-picker/src/index.d.ts +1 -1
- package/dist/packages/primitives/combobox/src/Combobox.js.map +1 -1
- package/dist/packages/primitives/combobox/src/Combobox.vue.d.ts +2 -2
- package/dist/packages/primitives/combobox/src/Combobox.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/packages/primitives/combobox/src/Combobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/combobox/src/index.d.ts +1 -1
- package/dist/packages/primitives/context-menu/src/ContextMenu.js.map +1 -1
- package/dist/packages/primitives/context-menu/src/ContextMenu.vue.d.ts +12 -0
- package/dist/packages/primitives/context-menu/src/ContextMenu.vue.d.ts.map +1 -1
- package/dist/packages/primitives/context-menu/src/ContextMenu.vue_vue_type_script_setup_true_lang.js +7 -3
- package/dist/packages/primitives/context-menu/src/ContextMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/date-picker/src/DatePicker.js.map +1 -1
- package/dist/packages/primitives/date-picker/src/DatePicker.vue.d.ts +2 -2
- package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/date-range-picker/src/DateRangePicker.js.map +1 -1
- package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue.d.ts +3 -3
- package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/date-range-picker/src/index.d.ts +1 -1
- package/dist/packages/primitives/date-time-picker/src/DateTimePicker.js.map +1 -1
- package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue.d.ts +2 -2
- package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.js.map +1 -1
- package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue.d.ts +3 -3
- package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/date-time-range-picker/src/index.d.ts +1 -1
- package/dist/packages/primitives/dialog/src/Dialog.js.map +1 -1
- package/dist/packages/primitives/dialog/src/Dialog.vue.d.ts +12 -4
- package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/dialog/src/index.d.ts +1 -1
- package/dist/packages/primitives/drawer/src/Drawer.js.map +1 -1
- package/dist/packages/primitives/drawer/src/Drawer.vue.d.ts +2 -2
- package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/drawer/src/index.d.ts +1 -1
- package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.js.map +1 -1
- package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue.d.ts +26 -0
- package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue.d.ts.map +1 -1
- package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue_vue_type_script_setup_true_lang.js +10 -4
- package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/editable/src/Editable.js.map +1 -1
- package/dist/packages/primitives/editable/src/Editable.vue.d.ts +2 -2
- package/dist/packages/primitives/editable/src/Editable.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/editable/src/Editable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/editable/src/index.d.ts +1 -1
- package/dist/packages/primitives/file-upload/src/FileUpload.js.map +1 -1
- package/dist/packages/primitives/file-upload/src/FileUpload.vue.d.ts +2 -2
- package/dist/packages/primitives/file-upload/src/FileUpload.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/file-upload/src/FileUpload.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/file-upload/src/index.d.ts +1 -1
- package/dist/packages/primitives/hover-card/src/HoverCard.js.map +1 -1
- package/dist/packages/primitives/hover-card/src/HoverCard.vue.d.ts.map +1 -1
- package/dist/packages/primitives/hover-card/src/HoverCard.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/hover-card/src/HoverCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/icon-button/src/IconButton.js.map +1 -1
- package/dist/packages/primitives/icon-button/src/IconButton.vue.d.ts +10 -4
- package/dist/packages/primitives/icon-button/src/IconButton.vue.d.ts.map +1 -1
- package/dist/packages/primitives/icon-button/src/IconButton.vue_vue_type_script_setup_true_lang.js +18 -6
- package/dist/packages/primitives/icon-button/src/IconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/indicator/src/Indicator.vue.d.ts +2 -2
- package/dist/packages/primitives/indicator/src/index.d.ts +1 -1
- package/dist/packages/primitives/kbd/src/Kbd.js +7 -0
- package/dist/packages/primitives/kbd/src/Kbd.js.map +1 -0
- package/dist/packages/primitives/kbd/src/Kbd.vue.d.ts +30 -0
- package/dist/packages/primitives/kbd/src/Kbd.vue.d.ts.map +1 -0
- package/dist/packages/primitives/kbd/src/Kbd.vue_vue_type_script_setup_true_lang.js +29 -0
- package/dist/packages/primitives/kbd/src/Kbd.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/kbd/src/index.d.ts +3 -0
- package/dist/packages/primitives/kbd/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/kbd/src/index.js +3 -0
- package/dist/packages/primitives/link/src/Link.js.map +1 -1
- package/dist/packages/primitives/link/src/Link.vue.d.ts.map +1 -1
- package/dist/packages/primitives/link/src/Link.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/packages/primitives/link/src/Link.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/list/src/List.js +7 -0
- package/dist/packages/primitives/list/src/List.js.map +1 -0
- package/dist/packages/primitives/list/src/List.vue.d.ts +22 -0
- package/dist/packages/primitives/list/src/List.vue.d.ts.map +1 -0
- package/dist/packages/primitives/list/src/List.vue_vue_type_script_setup_true_lang.js +27 -0
- package/dist/packages/primitives/list/src/List.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/list/src/ListItem.js +7 -0
- package/dist/packages/primitives/list/src/ListItem.js.map +1 -0
- package/dist/packages/primitives/list/src/ListItem.vue.d.ts +75 -0
- package/dist/packages/primitives/list/src/ListItem.vue.d.ts.map +1 -0
- package/dist/packages/primitives/list/src/ListItem.vue_vue_type_script_setup_true_lang.js +101 -0
- package/dist/packages/primitives/list/src/ListItem.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/list/src/index.d.ts +4 -0
- package/dist/packages/primitives/list/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/list/src/index.js +3 -0
- package/dist/packages/primitives/menubar/src/Menubar.js.map +1 -1
- package/dist/packages/primitives/menubar/src/Menubar.vue.d.ts +20 -6
- package/dist/packages/primitives/menubar/src/Menubar.vue.d.ts.map +1 -1
- package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js +178 -64
- package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/number-field/src/NumberField.js.map +1 -1
- package/dist/packages/primitives/number-field/src/NumberField.vue.d.ts +2 -2
- package/dist/packages/primitives/number-field/src/NumberField.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/packages/primitives/number-field/src/NumberField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/number-field/src/index.d.ts +1 -1
- package/dist/packages/primitives/otp-input/src/OtpInput.vue.d.ts +2 -2
- package/dist/packages/primitives/otp-input/src/index.d.ts +1 -1
- package/dist/packages/primitives/pagination/src/Pagination.js.map +1 -1
- package/dist/packages/primitives/pagination/src/Pagination.vue.d.ts +3 -3
- package/dist/packages/primitives/pagination/src/Pagination.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/pagination/src/Pagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/pagination/src/index.d.ts +1 -1
- package/dist/packages/primitives/popover/src/Popover.js.map +1 -1
- package/dist/packages/primitives/popover/src/Popover.vue.d.ts.map +1 -1
- package/dist/packages/primitives/popover/src/Popover.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/popover/src/Popover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/progress/src/Progress.vue.d.ts +2 -2
- package/dist/packages/primitives/progress/src/index.d.ts +1 -1
- package/dist/packages/primitives/radial-progress/src/RadialProgress.vue.d.ts +2 -2
- package/dist/packages/primitives/radial-progress/src/index.d.ts +1 -1
- package/dist/packages/primitives/radio-group/src/RadioGroup.js.map +1 -1
- package/dist/packages/primitives/radio-group/src/RadioGroup.vue.d.ts.map +1 -1
- package/dist/packages/primitives/radio-group/src/RadioGroup.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/radio-group/src/RadioGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/rating/src/Rating.vue.d.ts +2 -2
- package/dist/packages/primitives/rating/src/index.d.ts +1 -1
- package/dist/packages/primitives/segmented-control/src/SegmentedControl.js.map +1 -1
- package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue.d.ts.map +1 -1
- package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js +2 -3
- package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/select/src/Select.js.map +1 -1
- package/dist/packages/primitives/select/src/Select.vue.d.ts.map +1 -1
- package/dist/packages/primitives/select/src/Select.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/select/src/Select.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/slider/src/Slider.js.map +1 -1
- package/dist/packages/primitives/slider/src/Slider.vue.d.ts +6 -3
- package/dist/packages/primitives/slider/src/Slider.vue.d.ts.map +1 -1
- package/dist/packages/primitives/slider/src/Slider.vue_vue_type_script_setup_true_lang.js +15 -10
- package/dist/packages/primitives/slider/src/Slider.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/stepper/src/Stepper.vue.d.ts +2 -2
- package/dist/packages/primitives/stepper/src/index.d.ts +1 -1
- package/dist/packages/primitives/switch/src/Switch.js.map +1 -1
- package/dist/packages/primitives/switch/src/Switch.vue.d.ts +1 -1
- package/dist/packages/primitives/switch/src/Switch.vue.d.ts.map +1 -1
- package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/table/src/Table.js +7 -0
- package/dist/packages/primitives/table/src/Table.js.map +1 -0
- package/dist/packages/primitives/table/src/Table.vue.d.ts +45 -0
- package/dist/packages/primitives/table/src/Table.vue_vue_type_script_setup_true_lang.js +72 -0
- package/dist/packages/primitives/table/src/Table.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/table/src/TableCell.js +7 -0
- package/dist/packages/primitives/table/src/TableCell.js.map +1 -0
- package/dist/packages/primitives/table/src/TableCell.vue.d.ts +19 -0
- package/dist/packages/primitives/table/src/TableCell.vue.d.ts.map +1 -0
- package/dist/packages/primitives/table/src/TableCell.vue_vue_type_script_setup_true_lang.js +17 -0
- package/dist/packages/primitives/table/src/TableCell.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/table/src/TableHead.js +7 -0
- package/dist/packages/primitives/table/src/TableHead.js.map +1 -0
- package/dist/packages/primitives/table/src/TableHead.vue.d.ts +56 -0
- package/dist/packages/primitives/table/src/TableHead.vue_vue_type_script_setup_true_lang.js +79 -0
- package/dist/packages/primitives/table/src/TableHead.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/table/src/TableRow.js +7 -0
- package/dist/packages/primitives/table/src/TableRow.js.map +1 -0
- package/dist/packages/primitives/table/src/TableRow.vue.d.ts +39 -0
- package/dist/packages/primitives/table/src/TableRow.vue.d.ts.map +1 -0
- package/dist/packages/primitives/table/src/TableRow.vue_vue_type_script_setup_true_lang.js +61 -0
- package/dist/packages/primitives/table/src/TableRow.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/table/src/index.d.ts +10 -0
- package/dist/packages/primitives/table/src/index.js +8 -0
- package/dist/packages/primitives/tabs/src/Tabs.js.map +1 -1
- package/dist/packages/primitives/tabs/src/Tabs.vue.d.ts.map +1 -1
- package/dist/packages/primitives/tabs/src/Tabs.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/tabs/src/Tabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/tags-input/src/TagsInput.js.map +1 -1
- package/dist/packages/primitives/tags-input/src/TagsInput.vue.d.ts +2 -2
- package/dist/packages/primitives/tags-input/src/TagsInput.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/tags-input/src/TagsInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/tags-input/src/index.d.ts +1 -1
- package/dist/packages/primitives/text-field/src/TextField.js.map +1 -1
- package/dist/packages/primitives/text-field/src/TextField.vue.d.ts +2 -2
- package/dist/packages/primitives/text-field/src/TextField.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/text-field/src/TextField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/text-field/src/index.d.ts +1 -1
- package/dist/packages/primitives/text-field/src/index.js +2 -2
- package/dist/packages/primitives/textarea/src/Textarea.js.map +1 -1
- package/dist/packages/primitives/textarea/src/Textarea.vue.d.ts +2 -2
- package/dist/packages/primitives/textarea/src/Textarea.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/textarea/src/Textarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/textarea/src/index.d.ts +1 -1
- package/dist/packages/primitives/textarea/src/index.js +2 -2
- package/dist/packages/primitives/time-picker/src/TimePicker.vue.d.ts +2 -2
- package/dist/packages/primitives/timeline/src/Timeline.js +7 -0
- package/dist/packages/primitives/timeline/src/Timeline.js.map +1 -0
- package/dist/packages/primitives/timeline/src/Timeline.vue.d.ts +37 -0
- package/dist/packages/primitives/timeline/src/Timeline.vue_vue_type_script_setup_true_lang.js +44 -0
- package/dist/packages/primitives/timeline/src/Timeline.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/timeline/src/TimelineItem.js +7 -0
- package/dist/packages/primitives/timeline/src/TimelineItem.js.map +1 -0
- package/dist/packages/primitives/timeline/src/TimelineItem.vue.d.ts +50 -0
- package/dist/packages/primitives/timeline/src/TimelineItem.vue_vue_type_script_setup_true_lang.js +96 -0
- package/dist/packages/primitives/timeline/src/TimelineItem.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/timeline/src/index.d.ts +5 -0
- package/dist/packages/primitives/timeline/src/index.js +5 -0
- package/dist/packages/primitives/toast/src/Toast.vue.d.ts +2 -2
- package/dist/packages/primitives/toast/src/ToastItem.js.map +1 -1
- package/dist/packages/primitives/toast/src/ToastItem.vue.d.ts +2 -2
- package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/toast/src/index.d.ts +1 -1
- package/dist/packages/primitives/tooltip/src/Tooltip.js.map +1 -1
- package/dist/packages/primitives/tooltip/src/Tooltip.vue.d.ts.map +1 -1
- package/dist/packages/primitives/tooltip/src/Tooltip.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/tooltip/src/Tooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/window-controls/src/WindowControls.vue.d.ts +2 -2
- package/dist/packages/primitives/window-controls/src/index.d.ts +1 -1
- package/dist/styles/global.css +59 -0
- package/dist/styles/theme.css +13 -0
- package/dist/theme.js +3 -0
- package/package.json +13 -7
- package/dist/packages/blocks/desktop-app-shell/src/DesktopAppShell.vue.d.ts.map +0 -1
- package/dist/packages/blocks/title-bar/src/TitleBar.vue.d.ts.map +0 -1
- package/dist/packages/blocks/toast-stack/src/ToastStack.vue.d.ts.map +0 -1
- package/dist/packages/composition/scroll-reel/src/ScrollReel.vue.d.ts.map +0 -1
- package/dist/packages/composition/scroll-reel/src/index.d.ts.map +0 -1
- package/dist/packages/loom/src/a11y.d.ts.map +0 -1
- package/dist/packages/loom/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue.d.ts.map +0 -1
- package/dist/packages/primitives/alert-dialog/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/avatar-group/src/AvatarGroup.vue.d.ts.map +0 -1
- package/dist/packages/primitives/avatar-group/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue.d.ts.map +0 -1
- package/dist/packages/primitives/breadcrumb/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/color-picker/src/ColorPicker.vue.d.ts.map +0 -1
- package/dist/packages/primitives/color-picker/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/combobox/src/Combobox.vue.d.ts.map +0 -1
- package/dist/packages/primitives/combobox/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/date-picker/src/DatePicker.vue.d.ts.map +0 -1
- package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue.d.ts.map +0 -1
- package/dist/packages/primitives/date-range-picker/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue.d.ts.map +0 -1
- package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue.d.ts.map +0 -1
- package/dist/packages/primitives/date-time-range-picker/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/dialog/src/Dialog.vue.d.ts.map +0 -1
- package/dist/packages/primitives/dialog/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/drawer/src/Drawer.vue.d.ts.map +0 -1
- package/dist/packages/primitives/drawer/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/editable/src/Editable.vue.d.ts.map +0 -1
- package/dist/packages/primitives/editable/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/file-upload/src/FileUpload.vue.d.ts.map +0 -1
- package/dist/packages/primitives/file-upload/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/indicator/src/Indicator.vue.d.ts.map +0 -1
- package/dist/packages/primitives/indicator/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/number-field/src/NumberField.vue.d.ts.map +0 -1
- package/dist/packages/primitives/number-field/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/otp-input/src/OtpInput.vue.d.ts.map +0 -1
- package/dist/packages/primitives/otp-input/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/pagination/src/Pagination.vue.d.ts.map +0 -1
- package/dist/packages/primitives/pagination/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/progress/src/Progress.vue.d.ts.map +0 -1
- package/dist/packages/primitives/progress/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/radial-progress/src/RadialProgress.vue.d.ts.map +0 -1
- package/dist/packages/primitives/radial-progress/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/rating/src/Rating.vue.d.ts.map +0 -1
- package/dist/packages/primitives/rating/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/stepper/src/Stepper.vue.d.ts.map +0 -1
- package/dist/packages/primitives/stepper/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/tags-input/src/TagsInput.vue.d.ts.map +0 -1
- package/dist/packages/primitives/tags-input/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/text-field/src/TextField.vue.d.ts.map +0 -1
- package/dist/packages/primitives/text-field/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/textarea/src/Textarea.vue.d.ts.map +0 -1
- package/dist/packages/primitives/textarea/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/time-picker/src/TimePicker.vue.d.ts.map +0 -1
- package/dist/packages/primitives/toast/src/Toast.vue.d.ts.map +0 -1
- package/dist/packages/primitives/toast/src/ToastItem.vue.d.ts.map +0 -1
- package/dist/packages/primitives/toast/src/index.d.ts.map +0 -1
- package/dist/packages/primitives/window-controls/src/WindowControls.vue.d.ts.map +0 -1
- package/dist/packages/primitives/window-controls/src/index.d.ts.map +0 -1
- package/dist/styles/theme.contrast.test.ts +0 -460
package/dist/packages/primitives/combobox/src/Combobox.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/combobox/src/Combobox.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ComboboxLabels } from \"@ecoma-io/loom-labels\";\nimport { cva } from \"class-variance-authority\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n */\nexport const COMBOBOX_LABELS: ComboboxLabels = {\n trigger: \"Show options\",\n empty: \"No results\",\n count: ({ count }) => `${String(count)} selected`,\n overflow: ({ hidden }) => `+${String(hidden)} more`,\n};\n\n/**\n * One row of the list. `value` is what the model carries; `label` is what a\n * reader sees, what the filter matches, and what the input shows once the row\n * is chosen.\n *\n * Identical to `SelectOption` today, and deliberately not the same\n * declaration: the two surfaces are independent, and the first field either\n * one grows — a secondary line here, a group key there — is a field the other\n * should not inherit by accident. A shared alias would make that change look\n * free when it is not.\n */\nexport interface ComboboxOption {\n /** The value bound to `v-model` when this row is chosen. Unique within the list. */\n value: string;\n /** The text shown on the row, matched by the filter, and put in the input once chosen. */\n label: string;\n /** Present but unchoosable — visible, skipped by the keyboard, unclickable. */\n disabled?: boolean;\n}\n\n/**\n * The control's height scale. It matches the text input and Select scales\n * exactly, so a form row mixing a Combobox beside either never comes out\n * uneven — which is why reaching past `size` for an `h-*` class is always the\n * wrong fix.\n */\nexport type ComboboxSize = \"sm\" | \"md\" | \"lg\";\n\nexport const comboboxVariants = cva(\n [\n \"flex w-full items-center gap-2 rounded-md border border-input bg-background text-foreground\",\n \"transition-[color,background-color,border-color,box-shadow] duration-fast ease-out\",\n // The focusable node is the input *inside* this box, so the ring is drawn\n // with `focus-within` rather than `focus-visible` — the same choice\n // TextField makes for the same reason, and what keeps the two controls\n // lighting up identically in a form row.\n \"focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring\",\n ],\n {\n // `satisfies` keeps the two halves honest: the union above is the name a\n // consumer imports and the documentation prints, the map below is the\n // classes. Adding a member to one and not the other stops compiling.\n variants: {\n size: {\n sm: \"h-8 px-2.5 text-xs\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-11 px-4 text-base\",\n } satisfies Record<ComboboxSize, string>,\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * The same three numbers as a floor rather than as a fixed height, for the\n * multi-select box only.\n *\n * A box holding tokens is sized by its content, which is TagsInput's call and\n * the reason its field is `min-h-9`. What keeps that from turning into an\n * unbounded control here is `visibleValues`: the token row never wraps, so in\n * practice the box resolves to exactly the height above and a multi-select\n * still lines up with the single-select beside it. The `h-auto` that goes with\n * it in the template is what releases the fixed height `comboboxVariants`\n * already set — Tailwind's merge resolves the two, `min-h-*` does not.\n */\nconst multiSelectHeights = {\n sm: \"min-h-8\",\n md: \"min-h-9\",\n lg: \"min-h-11\",\n} satisfies Record<ComboboxSize, string>;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from \"vue\";\nimport {\n ComboboxRoot,\n ComboboxAnchor,\n ComboboxInput,\n ComboboxTrigger,\n ComboboxPortal,\n ComboboxContent,\n ComboboxViewport,\n ComboboxItem,\n ComboboxItemIndicator,\n ComboboxEmpty,\n} from \"reka-ui\";\nimport { Check, ChevronDown } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * Combobox — a text input that narrows a list as you type, for the list too\n * long to scan. Countries, currencies, repositories, people: anywhere a reader\n * knows roughly what they are looking for and would rather type three letters\n * than scroll past two hundred rows.\n *\n * Its neighbour is `Select`, and the boundary is the length of the list rather\n * than the shape of the control. Three to fifteen rows a reader can read\n * through belong in a Select — a search box over eight options is a keystroke\n * charged for nothing. Two to five options that should all be visible at once\n * belong in a segmented control. Past roughly fifteen, searching beats\n * scrolling and this is the control that offers it.\n *\n * Built on Reka UI's Combobox, which supplies the editable-combobox semantics:\n * the `role=\"combobox\"` input wired to the listbox by `aria-expanded` and\n * `aria-controls`, `aria-autocomplete=\"list\"`, and the active row tracked by\n * `aria-activedescendant` while real focus stays in the input. That last part\n * is the one genuine behavioural difference from Select, which moves focus\n * onto the row itself — a combobox cannot, because the reader is still typing.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * its hint or error line, `required`, `invalid`, `disabled` and the name the\n * chosen value is submitted under all arrive through `useFieldControl()`, so\n * `<Field label=\"Country\" error=\"…\"><Combobox … /></Field>` needs no attributes\n * at the call site. Both booleans below still win over the row when they are\n * set, in both directions — which is why they default to `undefined` rather\n * than `false`.\n *\n * A row's `readonly` is deliberately ignored. The text in the box is not the\n * value — it is the chosen option's label, rewritten on every choice — so a\n * `readonly` input here would freeze a caption rather than protect a value,\n * while the list underneath still opened on click. A choice nobody may change\n * is a disabled Combobox, or the label rendered as text.\n *\n * `multiple` turns it into a many-of-many picker. The chosen values render as\n * tokens in the box — the same `Chip` a\n * [TagsInput](../TagsInput/TagsInput.vue) commits its values to, so a selection\n * looks the same wherever a form shows one — and the input goes back to being\n * only a search box. The tokens are **inert**, deliberately: the list is where\n * a choice is made and where it is unmade, so the control stays the single Tab\n * stop it is in single-select, and a value hidden behind the overflow count is\n * reachable by exactly the same gesture as a visible one. That is the one place\n * this diverges from TagsInput, whose tokens carry their own remove control\n * because it has no list to send a reader back to.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen option's `value`, or every chosen value when `multiple`.\n *\n * The union is one prop rather than a generic component, and the invariant\n * is the mode: single-select emits and accepts a `string` and never an\n * array, multi-select an array and never a bare string. A generic would\n * express that in the type system and cost more than it buys — it would\n * print a conditional type in the generated API table below, and it would\n * break `InstanceType<typeof Combobox>`, which is how a consumer types a\n * template ref.\n */\n modelValue?: string | string[] | undefined;\n /** The rows, in the order they are shown. */\n options: ComboboxOption[];\n /** Lets several rows be chosen at once: chosen rows toggle instead of closing the list, and the box shows the selection as tokens. */\n multiple?: boolean;\n /**\n * How many tokens the box shows before the rest collapse into a count.\n *\n * The box must not grow with the selection — it sits in a form row beside\n * controls on a fixed height scale — so past this many the tail becomes one\n * `labels.overflow` summary: five chosen shows three and \"+2 more\", fifty\n * shows three and \"+47 more\". Raise it for a wide box; the row never wraps,\n * so a number the box has no room for is a number that truncates.\n */\n visibleValues?: number;\n /** Shown in the empty input, before anything is typed or chosen. */\n placeholder?: string;\n /** Shown as a row of its own when the filter matches nothing. Defaults to `labels.empty`, so say what was searched for rather than restating the default in a second language. */\n emptyMessage?: string | undefined;\n /** Unavailable: dims the control, refuses to open and refuses to take text. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Control height. It shares the text input scale so mixed form rows stay aligned. */\n size?: ComboboxSize;\n /**\n * Filter `options` against the typed text. Turn it off when the host is\n * producing the list itself — from a server, in response to\n * `update:query` — so a result that does not literally contain what was\n * typed is not then hidden a second time by a filter running underneath it.\n */\n filter?: boolean;\n /**\n * Names for the two strings this control supplies itself, as any subset of\n * `ComboboxLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application. `emptyMessage` is the shorter way to reach `empty` for one\n * box; this is what reaches `trigger`.\n */\n labels?: LabelOverrides<ComboboxLabels>;\n }>(),\n {\n // `modelValue: undefined` is load-bearing rather than redundant: paired\n // with `optional()` below it is what keeps the uncontrolled third state\n // reachable, instead of pinning the control to a value nobody chose.\n modelValue: undefined,\n multiple: false,\n // Three, and the number is a judgement rather than a measurement: two\n // tokens plus a count reads as a control that shows almost nothing, and\n // four leaves a `md` box no room to type in at the default width. It is a\n // prop because the right answer is the box's width, which only the caller\n // knows.\n visibleValues: 3,\n // No English default here any more. The wording moved into `COMBOBOX_LABELS`\n // so a host can set it once for the whole application rather than on every\n // box, and an absent prop is what lets the resolved label through.\n emptyMessage: undefined,\n // Not `false`, for either. `false` is a caller saying \"this control is fine\n // / enabled even though its row is not\"; absent is a caller saying nothing,\n // and only `undefined` survives to mean that past Vue's\n // absent-Boolean-casts-to-false rule — see TextField.vue, which carries the\n // full reasoning.\n disabled: undefined,\n invalid: undefined,\n size: \"md\",\n filter: true,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen option's `value` — or the whole chosen list, when `multiple`. Single-select fires once per choice and the list closes with it; multi-select fires once per toggle and the list stays open. */\n \"update:modelValue\": [value: string | string[]];\n /** The text now in the input — as the reader types, and again when a choice puts its label there. */\n \"update:query\": [query: string];\n}>();\n\n// `text`, not `labels`: the prop of that name is one of the three sources this\n// resolves, and a template reading the raw prop would be reading the overrides\n// rather than the answer.\nconst text = useLabels(\"combobox\", COMBOBOX_LABELS, () => props.labels);\n\nconst query = ref(\"\");\n\n/**\n * The stagger is the list's *reveal*, and re-filtering is not a reveal. This\n * is true from the moment the list opens until the first keystroke, after\n * which every row arrives immediately.\n *\n * Two things go wrong without it. A delay recomputed per keystroke makes the\n * list feel like it is rebuilding under the reader's fingers — the opposite of\n * what a filter is for. And raising `animation-delay` on a row that survived\n * the keystroke rewinds its entrance: the row is still mounted, so the\n * animation is not restarted, it is re-timed, and a row that had finished\n * fading in drops back to half-opacity. Going the other way — stagger to `0ms`\n * — only ever fast-forwards, which is why this flag falls and does not rise\n * again until the content has unmounted and remounted with the next open.\n */\nconst revealing = ref(true);\n\nconst countId = useId();\n\n/**\n * The multi-select's own copy of the selection, and the reason it keeps one:\n * the tokens have to render whether or not the host bound `v-model`.\n *\n * Left uncontrolled, `modelValue` stays `undefined` while Reka manages the\n * choice internally — which is the third state `optional()` exists to preserve\n * — so a trigger reading the prop would sit empty while the list showed three\n * checked rows. This mirrors rather than drives: it is never fed back into\n * Reka, so nothing about the uncontrolled control changes.\n *\n * Seeded from the prop rather than sanitised, for TagsInput's reason: a list\n * the host already holds is the host's to hold.\n */\nconst selected = ref<string[]>(Array.isArray(props.modelValue) ? [...props.modelValue] : []);\nwatch(\n () => props.modelValue,\n (next) => {\n if (Array.isArray(next)) selected.value = [...next];\n },\n { deep: true },\n);\n\n/**\n * The chosen values as the box shows them, and the fallback is the point: a\n * host driving `options` from a search hands back only the rows matching the\n * current query, so a value chosen from an earlier one has no option to look\n * its label up in. Falling back to the value keeps the token on screen — a\n * chosen value that vanished from the trigger while it was still in the model\n * would be the control lying about what it holds.\n */\nconst tokens = computed(() =>\n selected.value.map((value) => ({\n value,\n label: props.options.find((option) => option.value === value)?.label ?? value,\n })),\n);\n\nconst visibleTokens = computed(() => tokens.value.slice(0, Math.max(0, props.visibleValues)));\nconst hiddenTokens = computed(() => tokens.value.length - visibleTokens.value.length);\n\n/**\n * What the input shows once a row is chosen. Without this Reka falls back to\n * the model value's own `toString()`, which would leave `en` in the box where\n * the reader picked \"English\".\n *\n * A multi-select shows nothing: the tokens are the selection, and the input\n * beside them is a search box. Reka calls this on close rather than on choose,\n * so the query the reader typed survives every toggle and one search can pick\n * several rows — which is the whole reason the list stays open.\n */\nfunction displayValue(value: unknown): string {\n if (props.multiple) return \"\";\n return props.options.find((option) => option.value === value)?.label ?? \"\";\n}\n\n/**\n * Reka hands back the whole selection on every toggle, never a delta, so the\n * emitted array is always the complete list — the same contract TagsInput's\n * `update:modelValue` carries, and the reason a host can assign it straight\n * back.\n */\nfunction onChoice(value: unknown): void {\n if (!props.multiple) {\n emit(\"update:modelValue\", String(value));\n return;\n }\n const values = Array.isArray(value) ? value.map(String) : [];\n selected.value = values;\n emit(\"update:modelValue\", values);\n}\n\nfunction onQuery(text: string): void {\n // Reka rewrites the input's text on its own account — once as the component\n // mounts, and again when a choice puts the chosen label back in the box —\n // and each of those arrives here as an update. Reporting a query that did\n // not change would have a host running the same search twice on mount, so\n // only a real change is passed on.\n if (text === query.value) return;\n query.value = text;\n revealing.value = false;\n emit(\"update:query\", text);\n}\n\n// The root renders no DOM node of its own, so fallthrough attributes would be\n// dropped on the floor. They go to the input, which is the element they\n// describe and the only one a name can land on — the chevron trigger already\n// carries Reka's own `aria-label`, and naming the control means naming the\n// `role=\"combobox\"` node.\n//\n// `class` is pulled out of that spread and merged through `cn()` onto the\n// anchor instead, because the anchor is the box a caller means when they pass\n// `w-64`. Spreading it alongside the anchor's own `:class` would only\n// concatenate the two lists — Vue's merge is generic, not Tailwind-aware — so\n// a caller's width would win or lose depending on which utility Tailwind\n// happened to emit last.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are read off `attrs` and handed in as this caller's own\n// values — a caller who sets either still beats the row, and the row's\n// description is merged into theirs rather than replacing it. `name`,\n// `readonly` and `required` are absent from this object because there is no\n// prop to oppose the row with; the row's answer stands unopposed, which is\n// what is wanted.\n//\n// The multi-select's count joins that list rather than replacing anything: it\n// is one more thing this control says about itself, and it reads after the\n// caller's own description and before the row's message — the same\n// most-specific-first order `useFieldControl` applies one layer out. Wired\n// through `aria-describedby` rather than announced, which is TagsInput's call\n// for the same fact: it is heard on focus and on every return to the box, which\n// is when the question is asked, and a live region would speak the whole total\n// over the top of the `aria-selected` change the row itself already announces.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [attrs[\"aria-describedby\"] as string | undefined, props.multiple ? countId : \"\"]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n disabled: props.disabled,\n invalid: props.invalid,\n}));\n\n// `name` is the one resolved key that does not travel with the bag, and here\n// it is a defect rather than a nicety: the bag lands on the input, and what\n// the input holds is the chosen option's *label*. A `name` on it would post\n// \"Viet Nam\" where the model carries `vn`. Reka submits the value itself\n// through a hidden control it renders from the root's own `name`, so that is\n// where the row's name goes. Everything else in the bag — the id, the merged\n// description, `aria-required`, `aria-invalid` — belongs on the\n// `role=\"combobox\"` node, which is the input.\nconst inputFieldAttrs = computed(() => {\n // Removed from a copy rather than listed positively, so a key added to the\n // resolved bag later reaches this input without anyone remembering to come\n // back here.\n const aria: Record<string, unknown> = { ...field.attrs };\n delete aria.name;\n return aria;\n});\n</script>\n\n<template>\n <!-- `open-on-click` and not `open-on-focus`: clicking the box is a reader\n asking to see the list, while focus arrives by Tab as often as by\n intent, and a list that unfolds over the next field every time someone\n tabs through a form is an obstruction. Typing opens it either way. -->\n <ComboboxRoot\n v-bind=\"optional({ modelValue, name: field.name })\"\n :multiple=\"multiple\"\n :disabled=\"field.disabled\"\n :ignore-filter=\"!filter\"\n open-on-click\n @update:model-value=\"onChoice\"\n @update:open=\"revealing = true\"\n >\n <ComboboxAnchor\n :data-disabled=\"field.disabled || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n comboboxVariants({ size }),\n // A box holding tokens is sized by them — see `multiSelectHeights`.\n // `h-auto` is what releases the fixed height the variant just set;\n // Tailwind's merge resolves two `h-*` utilities and leaves a `min-h-*`\n // beside either of them alone.\n multiple && ['h-auto', multiSelectHeights[size], 'gap-1.5'],\n !field.invalid && 'focus-within:shadow-halo',\n field.invalid && 'border-destructive focus-within:outline-destructive',\n // The box holds the chosen value, so it is drained rather than\n // dimmed: `opacity-50` composited the input's text down to 3.06:1,\n // and what a reader needs from an unavailable Combobox is precisely\n // what it is set to. The neutral well says unavailable on its own —\n // the input's own `disabled:text-muted-foreground` below carries the\n // text to 4.67:1 over it.\n //\n // The hairline recedes with the fill, and that second channel is the\n // point: unavailable is a difference in *shape* as well as in colour,\n // so it survives a reader who cannot resolve `muted` from\n // `background`. This control has no read-only state to be told apart\n // from — see the docblock — so it takes the unavailable row alone.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n attrs.class as string,\n )\n \"\n >\n <!-- The selection, as the tokens a TagsInput commits its values to, so a\n chosen set looks the same wherever a form shows one. Inert on\n purpose: the list is where a value is chosen and where it is\n unchosen, which keeps the whole control one Tab stop and keeps a\n value behind the overflow count reachable by the same gesture as a\n visible one. -->\n <template v-if=\"multiple\">\n <Chip\n v-for=\"token in visibleTokens\"\n :key=\"token.value\"\n size=\"sm\"\n :disabled=\"field.disabled\"\n class=\"min-w-0 animate-fade-rise\"\n >\n {{ token.label }}\n </Chip>\n <!-- Not a token: it stands for values rather than being one, and a\n fourth pill saying \"+47 more\" would read as a forty-eighth\n choice. -->\n <span v-if=\"hiddenTokens > 0\" class=\"tabular shrink-0 text-small text-muted-foreground\">\n {{ text.overflow({ hidden: hiddenTokens }) }}\n </span>\n </template>\n\n <ComboboxInput\n v-bind=\"{ ...inputAttrs, ...inputFieldAttrs }\"\n :model-value=\"query\"\n :display-value=\"displayValue\"\n :placeholder=\"multiple && tokens.length > 0 ? undefined : placeholder\"\n class=\"min-w-6 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:text-muted-foreground\"\n @update:model-value=\"onQuery\"\n />\n <!-- Reka takes this button out of the tab order itself, so the whole\n control is one Tab stop: the input. It also names it, in English —\n `aria-label=\"Show popup\"`, written inside its own render function\n with no prop to reach it by — and the binding below replaces that\n rather than competing with it, because Vue merges a fallthrough\n attribute onto the root vnode after the render function produced it.\n Removing it does not fall back to nothing; it falls back to Reka's\n English, which no test of Loom's own strings would catch. -->\n <ComboboxTrigger\n :aria-label=\"text.trigger\"\n class=\"group min-h-6 min-w-6 shrink-0 text-muted-foreground disabled:cursor-not-allowed\"\n >\n <ChevronDown\n class=\"h-4 w-4 transition-transform duration-fast ease-out group-data-[state=open]:rotate-180\"\n />\n </ComboboxTrigger>\n\n <!-- The size of the selection, and the only thing that carries it to a\n reader who cannot see the tokens: the visible row shows the first few\n and a count for the rest, and neither is announced. Reka publishes\n nothing of the sort — the listbox's `aria-multiselectable` says the\n control takes several values, never how many it holds. -->\n <span v-if=\"multiple\" :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length }) }}\n </span>\n </ComboboxAnchor>\n\n <ComboboxPortal>\n <ComboboxContent\n position=\"popper\"\n :side-offset=\"6\"\n :class=\"\n cn(\n 'z-50 max-h-72 min-w-[var(--reka-combobox-trigger-width)] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md',\n // Scoped to the open state, never unconditional. Reka keeps the\n // closed content mounted while it waits for an animationend that a\n // mount-only animation never fires again — an invisible overlay\n // eating clicks. Scoping it makes the closed element compute\n // `animation-name: none`, which is the immediate-unmount branch.\n 'data-[state=open]:animate-fade-rise',\n )\n \"\n >\n <ComboboxViewport class=\"p-1\">\n <!-- A filter that matches nothing has to say so. An empty popover\n reads as a broken one, and a screen reader is told nothing at\n all — so the message is a row inside the listbox rather than a\n caption beside it. `role=\"option\"` because a bare div is not a\n legitimate child of `role=\"listbox\"` and is skipped; disabled\n because it is a statement, not a choice. -->\n <ComboboxEmpty\n role=\"option\"\n aria-disabled=\"true\"\n class=\"animate-fade-rise px-3 py-1.5 text-sm text-muted-foreground\"\n >\n {{ emptyMessage ?? text.empty }}\n </ComboboxEmpty>\n\n <!-- Keyed by value rather than by index: a row that survives a\n keystroke keeps its DOM node, so its entrance animation is not\n re-run and the list settles instead of flashing. Keyed by index,\n every row below a filtered-out one would be reused for different\n content while the tail remounted — an animation playing on the\n wrong rows.\n\n `text-value` pins the filter to the label. Left off, Reka falls\n back to whatever the row happens to render, which is the label\n today and the label plus a badge, a hint or a count the first\n time a row grows one. -->\n <ComboboxItem\n v-for=\"(option, index) in options\"\n :key=\"option.value\"\n :value=\"option.value\"\n :text-value=\"option.label\"\n :disabled=\"option.disabled ?? false\"\n :style=\"{ animationDelay: revealing ? listStaggerDelay(index) : '0ms' }\"\n :class=\"\n cn(\n 'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-3 py-1.5 pr-8 text-sm text-foreground outline-none',\n 'transition-colors duration-fast ease-out',\n 'animate-fade-rise',\n 'data-[highlighted]:bg-subtle',\n 'data-[state=checked]:bg-primary-muted data-[state=checked]:text-primary-text',\n // Inert, but still readable. `data-[disabled]:opacity-50` took\n // the label to 3.13:1 on the popover; the colour on the span\n // below leaves it at 5.76:1 and still visibly lighter than the\n // rows around it.\n 'data-[disabled]:pointer-events-none',\n )\n \"\n >\n <!-- The mute lives on this span rather than on the row, and it has\n to. `data-[disabled]:` sorts *before* `data-[state=checked]:`\n in Tailwind's variant order, so a disabled row that is also the\n chosen one would take `text-primary-text` from the container — and a\n container colour is only inherited. Declared here it wins\n whatever the row resolved to. -->\n <span :class=\"option.disabled && 'text-muted-foreground'\">{{ option.label }}</span>\n <ComboboxItemIndicator class=\"absolute right-2 inline-flex items-center\">\n <Check class=\"h-4 w-4\" />\n </ComboboxItemIndicator>\n </ComboboxItem>\n </ComboboxViewport>\n </ComboboxContent>\n </ComboboxPortal>\n </ComboboxRoot>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Combobox.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/combobox/src/Combobox.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ComboboxLabels } from \"@ecoma-io/loom-labels\";\nimport { cva } from \"class-variance-authority\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n */\nexport const COMBOBOX_LABELS: ComboboxLabels = {\n trigger: \"Show options\",\n empty: \"No results\",\n count: ({ count }) => `${String(count)} selected`,\n overflow: ({ hidden }) => `+${String(hidden)} more`,\n};\n\n/**\n * One row of the list. `value` is what the model carries; `label` is what a\n * reader sees, what the filter matches, and what the input shows once the row\n * is chosen.\n *\n * Identical to `SelectOption` today, and deliberately not the same\n * declaration: the two surfaces are independent, and the first field either\n * one grows — a secondary line here, a group key there — is a field the other\n * should not inherit by accident. A shared alias would make that change look\n * free when it is not.\n */\nexport interface ComboboxOption {\n /** The value bound to `v-model` when this row is chosen. Unique within the list. */\n value: string;\n /** The text shown on the row, matched by the filter, and put in the input once chosen. */\n label: string;\n /** Present but unchoosable — visible, skipped by the keyboard, unclickable. */\n disabled?: boolean;\n}\n\n/**\n * The control's height scale. It matches the text input and Select scales\n * exactly, so a form row mixing a Combobox beside either never comes out\n * uneven — which is why reaching past `size` for an `h-*` class is always the\n * wrong fix.\n */\nexport type ComboboxSize = \"sm\" | \"md\" | \"lg\";\n\nexport const comboboxVariants = cva(\n [\n \"flex w-full items-center gap-2 rounded-md border border-input bg-background text-foreground\",\n \"transition-[color,background-color,border-color,box-shadow] duration-fast ease-out\",\n // The focusable node is the input *inside* this box, so the ring is drawn\n // with `focus-within` rather than `focus-visible` — the same choice\n // TextField makes for the same reason, and what keeps the two controls\n // lighting up identically in a form row.\n \"focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring\",\n ],\n {\n // `satisfies` keeps the two halves honest: the union above is the name a\n // consumer imports and the documentation prints, the map below is the\n // classes. Adding a member to one and not the other stops compiling.\n variants: {\n size: {\n sm: \"h-8 px-2.5 text-xs\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-11 px-4 text-base\",\n } satisfies Record<ComboboxSize, string>,\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * The same three numbers as a floor rather than as a fixed height, for the\n * multi-select box only.\n *\n * A box holding tokens is sized by its content, which is TagsInput's call and\n * the reason its field is `min-h-9`. What keeps that from turning into an\n * unbounded control here is `visibleValues`: the token row never wraps, so in\n * practice the box resolves to exactly the height above and a multi-select\n * still lines up with the single-select beside it. The `h-auto` that goes with\n * it in the template is what releases the fixed height `comboboxVariants`\n * already set — Tailwind's merge resolves the two, `min-h-*` does not.\n */\nconst multiSelectHeights = {\n sm: \"min-h-8\",\n md: \"min-h-9\",\n lg: \"min-h-11\",\n} satisfies Record<ComboboxSize, string>;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from \"vue\";\nimport {\n ComboboxRoot,\n ComboboxAnchor,\n ComboboxInput,\n ComboboxTrigger,\n ComboboxPortal,\n ComboboxContent,\n ComboboxViewport,\n ComboboxItem,\n ComboboxItemIndicator,\n ComboboxEmpty,\n} from \"reka-ui\";\nimport { Check, ChevronDown } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * Combobox — a text input that narrows a list as you type, for the list too\n * long to scan. Countries, currencies, repositories, people: anywhere a reader\n * knows roughly what they are looking for and would rather type three letters\n * than scroll past two hundred rows.\n *\n * Its neighbour is `Select`, and the boundary is the length of the list rather\n * than the shape of the control. Three to fifteen rows a reader can read\n * through belong in a Select — a search box over eight options is a keystroke\n * charged for nothing. Two to five options that should all be visible at once\n * belong in a segmented control. Past roughly fifteen, searching beats\n * scrolling and this is the control that offers it.\n *\n * Built on Reka UI's Combobox, which supplies the editable-combobox semantics:\n * the `role=\"combobox\"` input wired to the listbox by `aria-expanded` and\n * `aria-controls`, `aria-autocomplete=\"list\"`, and the active row tracked by\n * `aria-activedescendant` while real focus stays in the input. That last part\n * is the one genuine behavioural difference from Select, which moves focus\n * onto the row itself — a combobox cannot, because the reader is still typing.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * its hint or error line, `required`, `invalid`, `disabled` and the name the\n * chosen value is submitted under all arrive through `useFieldControl()`, so\n * `<Field label=\"Country\" error=\"…\"><Combobox … /></Field>` needs no attributes\n * at the call site. Both booleans below still win over the row when they are\n * set, in both directions — which is why they default to `undefined` rather\n * than `false`.\n *\n * A row's `readonly` is deliberately ignored. The text in the box is not the\n * value — it is the chosen option's label, rewritten on every choice — so a\n * `readonly` input here would freeze a caption rather than protect a value,\n * while the list underneath still opened on click. A choice nobody may change\n * is a disabled Combobox, or the label rendered as text.\n *\n * `multiple` turns it into a many-of-many picker. The chosen values render as\n * tokens in the box — the same `Chip` a\n * [TagsInput](../TagsInput/TagsInput.vue) commits its values to, so a selection\n * looks the same wherever a form shows one — and the input goes back to being\n * only a search box. The tokens are **inert**, deliberately: the list is where\n * a choice is made and where it is unmade, so the control stays the single Tab\n * stop it is in single-select, and a value hidden behind the overflow count is\n * reachable by exactly the same gesture as a visible one. That is the one place\n * this diverges from TagsInput, whose tokens carry their own remove control\n * because it has no list to send a reader back to.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen option's `value`, or every chosen value when `multiple`.\n *\n * The union is one prop rather than a generic component, and the invariant\n * is the mode: single-select emits and accepts a `string` and never an\n * array, multi-select an array and never a bare string. A generic would\n * express that in the type system and cost more than it buys — it would\n * print a conditional type in the generated API table below, and it would\n * break `InstanceType<typeof Combobox>`, which is how a consumer types a\n * template ref.\n */\n modelValue?: string | string[] | undefined;\n /** The rows, in the order they are shown. */\n options: ComboboxOption[];\n /** Lets several rows be chosen at once: chosen rows toggle instead of closing the list, and the box shows the selection as tokens. */\n multiple?: boolean;\n /**\n * How many tokens the box shows before the rest collapse into a count.\n *\n * The box must not grow with the selection — it sits in a form row beside\n * controls on a fixed height scale — so past this many the tail becomes one\n * `labels.overflow` summary: five chosen shows three and \"+2 more\", fifty\n * shows three and \"+47 more\". Raise it for a wide box; the row never wraps,\n * so a number the box has no room for is a number that truncates.\n */\n visibleValues?: number;\n /** Shown in the empty input, before anything is typed or chosen. */\n placeholder?: string;\n /** Shown as a row of its own when the filter matches nothing. Defaults to `labels.empty`, so say what was searched for rather than restating the default in a second language. */\n emptyMessage?: string | undefined;\n /** Unavailable: dims the control, refuses to open and refuses to take text. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Control height. It shares the text input scale so mixed form rows stay aligned. */\n size?: ComboboxSize;\n /**\n * Filter `options` against the typed text. Turn it off when the host is\n * producing the list itself — from a server, in response to\n * `update:query` — so a result that does not literally contain what was\n * typed is not then hidden a second time by a filter running underneath it.\n */\n filter?: boolean;\n /**\n * Names for the two strings this control supplies itself, as any subset of\n * `ComboboxLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application. `emptyMessage` is the shorter way to reach `empty` for one\n * box; this is what reaches `trigger`.\n */\n labels?: LabelOverrides<ComboboxLabels>;\n }>(),\n {\n // `modelValue: undefined` is load-bearing rather than redundant: paired\n // with `optional()` below it is what keeps the uncontrolled third state\n // reachable, instead of pinning the control to a value nobody chose.\n modelValue: undefined,\n multiple: false,\n // Three, and the number is a judgement rather than a measurement: two\n // tokens plus a count reads as a control that shows almost nothing, and\n // four leaves a `md` box no room to type in at the default width. It is a\n // prop because the right answer is the box's width, which only the caller\n // knows.\n visibleValues: 3,\n // No English default here any more. The wording moved into `COMBOBOX_LABELS`\n // so a host can set it once for the whole application rather than on every\n // box, and an absent prop is what lets the resolved label through.\n emptyMessage: undefined,\n // Not `false`, for either. `false` is a caller saying \"this control is fine\n // / enabled even though its row is not\"; absent is a caller saying nothing,\n // and only `undefined` survives to mean that past Vue's\n // absent-Boolean-casts-to-false rule — see TextField.vue, which carries the\n // full reasoning.\n disabled: undefined,\n invalid: undefined,\n size: \"md\",\n filter: true,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen option's `value` — or the whole chosen list, when `multiple`. Single-select fires once per choice and the list closes with it; multi-select fires once per toggle and the list stays open. */\n \"update:modelValue\": [value: string | string[]];\n /** The text now in the input — as the reader types, and again when a choice puts its label there. */\n \"update:query\": [query: string];\n}>();\n\n// `text`, not `labels`: the prop of that name is one of the three sources this\n// resolves, and a template reading the raw prop would be reading the overrides\n// rather than the answer.\nconst text = useLabels(\"combobox\", COMBOBOX_LABELS, () => props.labels);\n\nconst query = ref(\"\");\n\n/**\n * The stagger is the list's *reveal*, and re-filtering is not a reveal. This\n * is true from the moment the list opens until the first keystroke, after\n * which every row arrives immediately.\n *\n * Two things go wrong without it. A delay recomputed per keystroke makes the\n * list feel like it is rebuilding under the reader's fingers — the opposite of\n * what a filter is for. And raising `animation-delay` on a row that survived\n * the keystroke rewinds its entrance: the row is still mounted, so the\n * animation is not restarted, it is re-timed, and a row that had finished\n * fading in drops back to half-opacity. Going the other way — stagger to `0ms`\n * — only ever fast-forwards, which is why this flag falls and does not rise\n * again until the content has unmounted and remounted with the next open.\n */\nconst revealing = ref(true);\n\nconst countId = useId();\n\n/**\n * The multi-select's own copy of the selection, and the reason it keeps one:\n * the tokens have to render whether or not the host bound `v-model`.\n *\n * Left uncontrolled, `modelValue` stays `undefined` while Reka manages the\n * choice internally — which is the third state `optional()` exists to preserve\n * — so a trigger reading the prop would sit empty while the list showed three\n * checked rows. This mirrors rather than drives: it is never fed back into\n * Reka, so nothing about the uncontrolled control changes.\n *\n * Seeded from the prop rather than sanitised, for TagsInput's reason: a list\n * the host already holds is the host's to hold.\n */\nconst selected = ref<string[]>(Array.isArray(props.modelValue) ? [...props.modelValue] : []);\nwatch(\n () => props.modelValue,\n (next) => {\n if (Array.isArray(next)) selected.value = [...next];\n },\n // Shallow, deliberately: the contract is whole-list replacement — Reka hands\n // back the complete selection on every toggle and a host echoes it straight\n // back down — so in-place mutation is not a case this mirror owes anyone.\n);\n\n/**\n * The chosen values as the box shows them, and the fallback is the point: a\n * host driving `options` from a search hands back only the rows matching the\n * current query, so a value chosen from an earlier one has no option to look\n * its label up in. Falling back to the value keeps the token on screen — a\n * chosen value that vanished from the trigger while it was still in the model\n * would be the control lying about what it holds.\n */\nconst tokens = computed(() =>\n selected.value.map((value) => ({\n value,\n label: props.options.find((option) => option.value === value)?.label ?? value,\n })),\n);\n\nconst visibleTokens = computed(() => tokens.value.slice(0, Math.max(0, props.visibleValues)));\nconst hiddenTokens = computed(() => tokens.value.length - visibleTokens.value.length);\n\n/**\n * What the input shows once a row is chosen. Without this Reka falls back to\n * the model value's own `toString()`, which would leave `en` in the box where\n * the reader picked \"English\".\n *\n * A multi-select shows nothing: the tokens are the selection, and the input\n * beside them is a search box. Reka calls this on close rather than on choose,\n * so the query the reader typed survives every toggle and one search can pick\n * several rows — which is the whole reason the list stays open.\n */\nfunction displayValue(value: unknown): string {\n if (props.multiple) return \"\";\n return props.options.find((option) => option.value === value)?.label ?? \"\";\n}\n\n/**\n * Reka hands back the whole selection on every toggle, never a delta, so the\n * emitted array is always the complete list — the same contract TagsInput's\n * `update:modelValue` carries, and the reason a host can assign it straight\n * back.\n */\nfunction onChoice(value: unknown): void {\n if (!props.multiple) {\n emit(\"update:modelValue\", String(value));\n return;\n }\n const values = Array.isArray(value) ? value.map(String) : [];\n selected.value = values;\n emit(\"update:modelValue\", values);\n}\n\nfunction onQuery(text: string): void {\n // Reka rewrites the input's text on its own account — once as the component\n // mounts, and again when a choice puts the chosen label back in the box —\n // and each of those arrives here as an update. Reporting a query that did\n // not change would have a host running the same search twice on mount, so\n // only a real change is passed on.\n if (text === query.value) return;\n query.value = text;\n revealing.value = false;\n emit(\"update:query\", text);\n}\n\n// The root renders no DOM node of its own, so fallthrough attributes would be\n// dropped on the floor. They go to the input, which is the element they\n// describe and the only one a name can land on — the chevron trigger already\n// carries Reka's own `aria-label`, and naming the control means naming the\n// `role=\"combobox\"` node.\n//\n// `class` is pulled out of that spread and merged through `cn()` onto the\n// anchor instead, because the anchor is the box a caller means when they pass\n// `w-64`. Spreading it alongside the anchor's own `:class` would only\n// concatenate the two lists — Vue's merge is generic, not Tailwind-aware — so\n// a caller's width would win or lose depending on which utility Tailwind\n// happened to emit last.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are read off `attrs` and handed in as this caller's own\n// values — a caller who sets either still beats the row, and the row's\n// description is merged into theirs rather than replacing it. `name`,\n// `readonly` and `required` are absent from this object because there is no\n// prop to oppose the row with; the row's answer stands unopposed, which is\n// what is wanted.\n//\n// The multi-select's count joins that list rather than replacing anything: it\n// is one more thing this control says about itself, and it reads after the\n// caller's own description and before the row's message — the same\n// most-specific-first order `useFieldControl` applies one layer out. Wired\n// through `aria-describedby` rather than announced, which is TagsInput's call\n// for the same fact: it is heard on focus and on every return to the box, which\n// is when the question is asked, and a live region would speak the whole total\n// over the top of the `aria-selected` change the row itself already announces.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [attrs[\"aria-describedby\"] as string | undefined, props.multiple ? countId : \"\"]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n disabled: props.disabled,\n invalid: props.invalid,\n}));\n\n// `name` is the one resolved key that does not travel with the bag, and here\n// it is a defect rather than a nicety: the bag lands on the input, and what\n// the input holds is the chosen option's *label*. A `name` on it would post\n// \"Viet Nam\" where the model carries `vn`. Reka submits the value itself\n// through a hidden control it renders from the root's own `name`, so that is\n// where the row's name goes. Everything else in the bag — the id, the merged\n// description, `aria-required`, `aria-invalid` — belongs on the\n// `role=\"combobox\"` node, which is the input.\nconst inputFieldAttrs = computed(() => {\n // Removed from a copy rather than listed positively, so a key added to the\n // resolved bag later reaches this input without anyone remembering to come\n // back here.\n const aria: Record<string, unknown> = { ...field.attrs };\n delete aria.name;\n return aria;\n});\n</script>\n\n<template>\n <!-- `open-on-click` and not `open-on-focus`: clicking the box is a reader\n asking to see the list, while focus arrives by Tab as often as by\n intent, and a list that unfolds over the next field every time someone\n tabs through a form is an obstruction. Typing opens it either way. -->\n <ComboboxRoot\n v-bind=\"optional({ modelValue, name: field.name })\"\n :multiple=\"multiple\"\n :disabled=\"field.disabled\"\n :ignore-filter=\"!filter\"\n open-on-click\n @update:model-value=\"onChoice\"\n @update:open=\"revealing = true\"\n >\n <ComboboxAnchor\n :data-disabled=\"field.disabled || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n comboboxVariants({ size }),\n // A box holding tokens is sized by them — see `multiSelectHeights`.\n // `h-auto` is what releases the fixed height the variant just set;\n // Tailwind's merge resolves two `h-*` utilities and leaves a `min-h-*`\n // beside either of them alone.\n multiple && ['h-auto', multiSelectHeights[size], 'gap-1.5'],\n !field.invalid && 'focus-within:shadow-halo',\n field.invalid && 'border-destructive focus-within:outline-destructive',\n // The box holds the chosen value, so it is drained rather than\n // dimmed: `opacity-50` composited the input's text down to 3.06:1,\n // and what a reader needs from an unavailable Combobox is precisely\n // what it is set to. The neutral well says unavailable on its own —\n // the input's own `disabled:text-muted-foreground` below carries the\n // text to 4.67:1 over it.\n //\n // The hairline recedes with the fill, and that second channel is the\n // point: unavailable is a difference in *shape* as well as in colour,\n // so it survives a reader who cannot resolve `muted` from\n // `background`. This control has no read-only state to be told apart\n // from — see the docblock — so it takes the unavailable row alone.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n attrs.class as string,\n )\n \"\n >\n <!-- The selection, as the tokens a TagsInput commits its values to, so a\n chosen set looks the same wherever a form shows one. Inert on\n purpose: the list is where a value is chosen and where it is\n unchosen, which keeps the whole control one Tab stop and keeps a\n value behind the overflow count reachable by the same gesture as a\n visible one. -->\n <template v-if=\"multiple\">\n <Chip\n v-for=\"token in visibleTokens\"\n :key=\"token.value\"\n size=\"sm\"\n :disabled=\"field.disabled\"\n class=\"min-w-0 animate-fade-rise\"\n >\n {{ token.label }}\n </Chip>\n <!-- Not a token: it stands for values rather than being one, and a\n fourth pill saying \"+47 more\" would read as a forty-eighth\n choice. -->\n <span v-if=\"hiddenTokens > 0\" class=\"tabular shrink-0 text-small text-muted-foreground\">\n {{ text.overflow({ hidden: hiddenTokens }) }}\n </span>\n </template>\n\n <ComboboxInput\n v-bind=\"{ ...inputAttrs, ...inputFieldAttrs }\"\n :model-value=\"query\"\n :display-value=\"displayValue\"\n :placeholder=\"multiple && tokens.length > 0 ? undefined : placeholder\"\n class=\"min-w-6 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:text-muted-foreground\"\n @update:model-value=\"onQuery\"\n />\n <!-- Reka takes this button out of the tab order itself, so the whole\n control is one Tab stop: the input. It also names it, in English —\n `aria-label=\"Show popup\"`, written inside its own render function\n with no prop to reach it by — and the binding below replaces that\n rather than competing with it, because Vue merges a fallthrough\n attribute onto the root vnode after the render function produced it.\n Removing it does not fall back to nothing; it falls back to Reka's\n English, which no test of Loom's own strings would catch. -->\n <ComboboxTrigger\n :aria-label=\"text.trigger\"\n class=\"group min-h-6 min-w-6 shrink-0 text-muted-foreground disabled:cursor-not-allowed\"\n >\n <ChevronDown\n class=\"h-4 w-4 transition-transform duration-fast ease-out group-data-[state=open]:rotate-180\"\n />\n </ComboboxTrigger>\n\n <!-- The size of the selection, and the only thing that carries it to a\n reader who cannot see the tokens: the visible row shows the first few\n and a count for the rest, and neither is announced. Reka publishes\n nothing of the sort — the listbox's `aria-multiselectable` says the\n control takes several values, never how many it holds. -->\n <span v-if=\"multiple\" :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length }) }}\n </span>\n </ComboboxAnchor>\n\n <ComboboxPortal>\n <ComboboxContent\n position=\"popper\"\n :side-offset=\"6\"\n :class=\"\n cn(\n 'z-overlay max-h-72 min-w-[var(--reka-combobox-trigger-width)] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md',\n // Each half of the motion is scoped to its own state, never\n // unconditional. Closing swaps the computed animation-name to\n // fade-fall's, which is what Reka's Presence reads: it holds the\n // panel mounted until that exit's animationend and only then takes\n // it away. Unconditional, a finished entrance animation sat on the\n // closed element and stranded it invisible over the page, eating\n // clicks.\n 'data-[state=open]:animate-fade-rise data-[state=closed]:animate-fade-fall',\n )\n \"\n >\n <ComboboxViewport class=\"p-1\">\n <!-- A filter that matches nothing has to say so. An empty popover\n reads as a broken one, and a screen reader is told nothing at\n all — so the message is a row inside the listbox rather than a\n caption beside it. `role=\"option\"` because a bare div is not a\n legitimate child of `role=\"listbox\"` and is skipped; disabled\n because it is a statement, not a choice. -->\n <ComboboxEmpty\n role=\"option\"\n aria-disabled=\"true\"\n class=\"animate-fade-rise px-3 py-1.5 text-sm text-muted-foreground\"\n >\n {{ emptyMessage ?? text.empty }}\n </ComboboxEmpty>\n\n <!-- Keyed by value rather than by index: a row that survives a\n keystroke keeps its DOM node, so its entrance animation is not\n re-run and the list settles instead of flashing. Keyed by index,\n every row below a filtered-out one would be reused for different\n content while the tail remounted — an animation playing on the\n wrong rows.\n\n `text-value` pins the filter to the label. Left off, Reka falls\n back to whatever the row happens to render, which is the label\n today and the label plus a badge, a hint or a count the first\n time a row grows one. -->\n <ComboboxItem\n v-for=\"(option, index) in options\"\n :key=\"option.value\"\n :value=\"option.value\"\n :text-value=\"option.label\"\n :disabled=\"option.disabled ?? false\"\n :style=\"{ animationDelay: revealing ? listStaggerDelay(index) : '0ms' }\"\n :class=\"\n cn(\n 'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-3 py-1.5 pr-8 text-sm text-foreground outline-none',\n 'transition-colors duration-fast ease-out',\n 'animate-fade-rise',\n 'data-[highlighted]:bg-subtle',\n 'data-[state=checked]:bg-primary-muted data-[state=checked]:text-primary-text',\n // Inert, but still readable. `data-[disabled]:opacity-50` took\n // the label to 3.13:1 on the popover; the colour on the span\n // below leaves it at 5.76:1 and still visibly lighter than the\n // rows around it.\n 'data-[disabled]:pointer-events-none',\n )\n \"\n >\n <!-- The mute lives on this span rather than on the row, and it has\n to. `data-[disabled]:` sorts *before* `data-[state=checked]:`\n in Tailwind's variant order, so a disabled row that is also the\n chosen one would take `text-primary-text` from the container — and a\n container colour is only inherited. Declared here it wins\n whatever the row resolved to. -->\n <span :class=\"option.disabled && 'text-muted-foreground'\">{{ option.label }}</span>\n <ComboboxItemIndicator class=\"absolute right-2 inline-flex items-center\">\n <Check class=\"h-4 w-4\" />\n </ComboboxItemIndicator>\n </ComboboxItem>\n </ComboboxViewport>\n </ComboboxContent>\n </ComboboxPortal>\n </ComboboxRoot>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default, comboboxVariants, COMBOBOX_LABELS } from "./Combobox.vue";
|
|
2
2
|
export type { ComboboxOption, ComboboxSize } from "./Combobox.vue";
|
|
3
|
-
export type { ComboboxLabels } from "
|
|
3
|
+
export type { ComboboxLabels } from "../../../labels/src/index";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.js","names":[],"sources":["../../../../../packages/primitives/context-menu/src/ContextMenu.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * One row of the menu, and the three kinds a menu is built from are told apart\n * by which fields are set rather than by a `kind` discriminator: a `separator`\n * is a rule, a `heading` is a non-interactive group label, and anything else\n * is a command.\n *\n * The list is data rather than markup because the primitive stays free of app\n * logic — selecting a row emits its `value` and the host decides what that\n * command means.\n */\nexport interface ContextMenuEntry {\n /** The visible text of a command or a heading. */\n label?: string;\n /** The command id emitted on select. An entry without one is not selectable. */\n value?: string;\n /** Accelerator hint shown at the trailing edge, e.g. `⌘S`. Display only — the primitive binds no keys. */\n shortcut?: string;\n /** Render a divider. Every other field is ignored. */\n separator?: boolean;\n /** Render a non-interactive section heading rather than a command. */\n heading?: boolean;\n /** A destructive command — painted in the destructive token so it is not one indistinguishable row. */\n danger?: boolean;\n /** Visible but inert: announced as disabled, and selecting it emits nothing. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ContextMenuRoot,\n ContextMenuTrigger,\n ContextMenuPortal,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n} from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * ContextMenu — a right-click command list, with roving focus, typeahead and\n * arrow-key navigation.\n *\n * The escape contract mirrors DropdownMenu: opening moves focus into the menu\n * and onto the first command; Esc and an outside click both close it; closing\n * returns focus to the trigger's element. The menu is modal, so while it is\n * open the page behind does not scroll and the rest of the page is hidden from\n * assistive technology.\n *\n * Selecting an entry emits `select` with its `value`; the host maps that id to\n * an action, which is what keeps the primitive free of app logic.\n *\n * Reach for `DropdownMenu` when the trigger should be a visible button rather\n * than a right-click, and for a select control when the list *is* a field's\n * value.\n */\nwithDefaults(\n defineProps<{\n /** The rows, in order. Three kinds, told apart by which fields are set. */\n items: ContextMenuEntry[];\n /** Drive the menu from the host with `v-model:open`; omit it and the menu owns its own state. */\n open?: boolean | undefined;\n }>(),\n // `open: undefined` is load-bearing, not a redundant default — it is what\n // keeps the uncontrolled third state reachable past Vue's absent-Boolean\n // casting. `optional()` in the template is the other half; its docblock\n // carries the reasoning for both.\n { open: undefined },\n);\nconst emit = defineEmits<{ select: [value: string]; \"update:open\": [value: boolean] }>();\n\n/**\n * A disabled entry, and an entry with no command id, select nothing. Reka\n * already blocks pointer events on the disabled row; this is the second half,\n * for the keyboard path that reaches it anyway.\n */\nfunction choose(item: ContextMenuEntry): void {\n if (item.disabled || item.value === undefined) return;\n emit(\"select\", item.value);\n}\n</script>\n\n<template>\n <ContextMenuRoot v-bind=\"optional({ open })\" @update:open=\"$emit('update:open', $event)\">\n <!-- @slot The element that receives the right-click. Rendered `as-child`,\n so the caller's own element *is* the trigger. -->\n <ContextMenuTrigger as-child>\n <slot name=\"trigger\" />\n </ContextMenuTrigger>\n\n <ContextMenuPortal>\n <ContextMenuContent\n :side-offset=\"6\"\n align=\"start\"\n :class=\"\n cn(\n 'z-
|
|
1
|
+
{"version":3,"file":"ContextMenu.js","names":[],"sources":["../../../../../packages/primitives/context-menu/src/ContextMenu.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * One row of the menu, and the three kinds a menu is built from are told apart\n * by which fields are set rather than by a `kind` discriminator: a `separator`\n * is a rule, a `heading` is a non-interactive group label, and anything else\n * is a command.\n *\n * The list is data rather than markup because the primitive stays free of app\n * logic — selecting a row emits its `value` and the host decides what that\n * command means.\n */\nexport interface ContextMenuEntry {\n /** The visible text of a command or a heading. */\n label?: string;\n /** The command id emitted on select. An entry without one is not selectable. */\n value?: string;\n /** Accelerator hint shown at the trailing edge, e.g. `⌘S`. Display only — the primitive binds no keys. */\n shortcut?: string;\n /** Render a divider. Every other field is ignored. */\n separator?: boolean;\n /** Render a non-interactive section heading rather than a command. */\n heading?: boolean;\n /** A destructive command — painted in the destructive token so it is not one indistinguishable row. */\n danger?: boolean;\n /** Visible but inert: announced as disabled, and selecting it emits nothing. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ContextMenuRoot,\n ContextMenuTrigger,\n ContextMenuPortal,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n} from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * ContextMenu — a right-click command list, with roving focus, typeahead and\n * arrow-key navigation.\n *\n * The escape contract mirrors DropdownMenu: opening moves focus into the menu\n * and onto the first command; Esc and an outside click both close it; closing\n * returns focus to the trigger's element. The menu is modal, so while it is\n * open the page behind does not scroll and the rest of the page is hidden from\n * assistive technology.\n *\n * Selecting an entry emits `select` with its `value`; the host maps that id to\n * an action, which is what keeps the primitive free of app logic.\n *\n * Reach for `DropdownMenu` when the trigger should be a visible button rather\n * than a right-click, and for a select control when the list *is* a field's\n * value.\n */\nwithDefaults(\n defineProps<{\n /** The rows, in order. Three kinds, told apart by which fields are set. */\n items: ContextMenuEntry[];\n /** Drive the menu from the host with `v-model:open`; omit it and the menu owns its own state. */\n open?: boolean | undefined;\n /**\n * Reading direction for the menu and its arrow-key navigation. Reka\n * mirrors traversal from this; without forwarding it a right-to-left\n * host gets left-to-right menus no matter what the page declares.\n */\n dir?: \"ltr\" | \"rtl\";\n }>(),\n // `open: undefined` is load-bearing, not a redundant default — it is what\n // keeps the uncontrolled third state reachable past Vue's absent-Boolean\n // casting. `optional()` in the template is the other half; its docblock\n // carries the reasoning for both.\n { open: undefined },\n);\nconst emit = defineEmits<{ select: [value: string]; \"update:open\": [value: boolean] }>();\n\n/**\n * A disabled entry, and an entry with no command id, select nothing. Reka\n * already blocks pointer events on the disabled row; this is the second half,\n * for the keyboard path that reaches it anyway.\n */\nfunction choose(item: ContextMenuEntry): void {\n if (item.disabled || item.value === undefined) return;\n emit(\"select\", item.value);\n}\n</script>\n\n<template>\n <ContextMenuRoot v-bind=\"optional({ open, dir })\" @update:open=\"$emit('update:open', $event)\">\n <!-- @slot The element that receives the right-click. Rendered `as-child`,\n so the caller's own element *is* the trigger. -->\n <ContextMenuTrigger as-child>\n <slot name=\"trigger\" />\n </ContextMenuTrigger>\n\n <ContextMenuPortal>\n <ContextMenuContent\n :side-offset=\"6\"\n align=\"start\"\n :class=\"\n cn(\n 'z-overlay min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none',\n // Both states scoped, never unconditional. Reka's Presence\n // keeps closed content mounted until an animationend arrives, and a\n // mount-only animation never fires a second one — so an\n // unconditional animation class strands an invisible menu over the\n // page, catching clicks meant for what is behind it. Scoped, the\n // closed element computes `animation-name: none` and Presence\n // unmounts it at once; the paired exit is precisely what Presence\n // is waiting to hold it for. The origin below makes the menu grow\n // out of the click point rather than out of its own centre — on\n // the way out as well as in.\n 'data-[state=open]:animate-scale-in data-[state=closed]:animate-scale-out',\n )\n \"\n style=\"transform-origin: var(--reka-popper-transform-origin)\"\n >\n <template v-for=\"(item, i) in items\" :key=\"i\">\n <ContextMenuSeparator v-if=\"item.separator\" class=\"my-1 h-px bg-border\" />\n <ContextMenuLabel\n v-else-if=\"item.heading\"\n class=\"px-2 py-1.5 text-micro font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {{ item.label }}\n </ContextMenuLabel>\n <!-- The reveal delay comes from the shared stagger vocabulary, capped\n there so a long menu does not tail off, rather than from a\n per-menu number written by hand.\n\n `disabled` is coerced rather than forwarded raw: an entry that\n simply omits the field is not disabled, and Reka's own prop does\n not accept `undefined` as a way of saying so. -->\n <ContextMenuItem\n v-else\n :disabled=\"item.disabled ?? false\"\n :class=\"\n cn(\n 'flex cursor-pointer items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none',\n 'transition-colors duration-fast ease-out',\n // The rows reveal in sequence as the menu opens, which reads as\n // a list arriving rather than a block appearing. The hover\n // colour transition above is a separate channel, so a row\n // highlighted mid-reveal still responds at once.\n 'animate-fade-rise',\n 'data-[highlighted]:bg-subtle',\n // The row's colour, not its alpha. A menu row is nothing but\n // its label, and `data-[disabled]:opacity-40` composited that\n // label to 2.40:1 on the popover and its shortcut to 1.79:1 —\n // a disabled row a reader cannot make out tells them nothing\n // about what is unavailable, which is the only thing it is\n // there to say. Muted instead: 5.76:1, and a clear drop from\n // the 15.46:1 an available row wears.\n //\n // It takes the danger colour with it, deliberately. An\n // attribute selector outranks the plain `text-destructive-text`\n // below whatever order Tailwind emits them in, and that is the\n // right way round: red is a warning about an action, and there\n // is no action here to warn about.\n 'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground',\n item.danger &&\n 'text-destructive-text data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive-text',\n )\n \"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n @select=\"choose(item)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </ContextMenuItem>\n </template>\n </ContextMenuContent>\n </ContextMenuPortal>\n </ContextMenuRoot>\n</template>\n"],"mappings":""}
|
|
@@ -31,6 +31,12 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
|
|
|
31
31
|
items: ContextMenuEntry[];
|
|
32
32
|
/** Drive the menu from the host with `v-model:open`; omit it and the menu owns its own state. */
|
|
33
33
|
open?: boolean | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Reading direction for the menu and its arrow-key navigation. Reka
|
|
36
|
+
* mirrors traversal from this; without forwarding it a right-to-left
|
|
37
|
+
* host gets left-to-right menus no matter what the page declares.
|
|
38
|
+
*/
|
|
39
|
+
dir?: "ltr" | "rtl";
|
|
34
40
|
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
35
41
|
select: (value: string) => any;
|
|
36
42
|
"update:open": (value: boolean) => any;
|
|
@@ -39,6 +45,12 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
|
|
|
39
45
|
items: ContextMenuEntry[];
|
|
40
46
|
/** Drive the menu from the host with `v-model:open`; omit it and the menu owns its own state. */
|
|
41
47
|
open?: boolean | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Reading direction for the menu and its arrow-key navigation. Reka
|
|
50
|
+
* mirrors traversal from this; without forwarding it a right-to-left
|
|
51
|
+
* host gets left-to-right menus no matter what the page declares.
|
|
52
|
+
*/
|
|
53
|
+
dir?: "ltr" | "rtl";
|
|
42
54
|
}> & Readonly<{
|
|
43
55
|
onSelect?: (value: string) => any;
|
|
44
56
|
"onUpdate:open"?: (value: boolean) => any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/context-menu/src/ContextMenu.vue"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContextMenu.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/context-menu/src/ContextMenu.vue"],"names":[],"mappings":"AAsMA;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uGAAuG;IACvG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;wBACoB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAmBd,2EAA2E;WACpE,gBAAgB,EAAE;IACzB,iGAAiG;WAC1F,OAAO,GAAG,SAAS;IAC1B;;;;OAIG;UACG,KAAK,GAAG,KAAK;;;;;IATnB,2EAA2E;WACpE,gBAAgB,EAAE;IACzB,iGAAiG;WAC1F,OAAO,GAAG,SAAS;IAC1B;;;;OAIG;UACG,KAAK,GAAG,KAAK;;;;;UANZ,OAAO,GAAG,SAAS;;cAyThB,CAAC,KAAK,IAAiB,KAAK,GAAG;EAWzC,CAAC;AAWL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
|
package/dist/packages/primitives/context-menu/src/ContextMenu.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -16,7 +16,8 @@ var ContextMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
|
|
|
16
16
|
open: {
|
|
17
17
|
type: Boolean,
|
|
18
18
|
default: void 0
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
dir: {}
|
|
20
21
|
},
|
|
21
22
|
emits: ["select", "update:open"],
|
|
22
23
|
setup(__props, { emit: __emit }) {
|
|
@@ -31,7 +32,10 @@ var ContextMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
|
|
|
31
32
|
emit("select", item.value);
|
|
32
33
|
}
|
|
33
34
|
return (_ctx, _cache) => {
|
|
34
|
-
return openBlock(), createBlock(unref(ContextMenuRoot), mergeProps(unref(optional)({
|
|
35
|
+
return openBlock(), createBlock(unref(ContextMenuRoot), mergeProps(unref(optional)({
|
|
36
|
+
open: __props.open,
|
|
37
|
+
dir: __props.dir
|
|
38
|
+
}), { "onUpdate:open": _cache[0] || (_cache[0] = ($event) => _ctx.$emit("update:open", $event)) }), {
|
|
35
39
|
default: withCtx(() => [createVNode(unref(ContextMenuTrigger), { "as-child": "" }, {
|
|
36
40
|
default: withCtx(() => [renderSlot(_ctx.$slots, "trigger")]),
|
|
37
41
|
_: 3
|
|
@@ -39,7 +43,7 @@ var ContextMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
|
|
|
39
43
|
default: withCtx(() => [createVNode(unref(ContextMenuContent), {
|
|
40
44
|
"side-offset": 6,
|
|
41
45
|
align: "start",
|
|
42
|
-
class: normalizeClass(unref(cn)("z-
|
|
46
|
+
class: normalizeClass(unref(cn)("z-overlay min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none", "data-[state=open]:animate-scale-in data-[state=closed]:animate-scale-out")),
|
|
43
47
|
style: { "transform-origin": "var(--reka-popper-transform-origin)" }
|
|
44
48
|
}, {
|
|
45
49
|
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item, i) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/context-menu/src/ContextMenu.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * One row of the menu, and the three kinds a menu is built from are told apart\n * by which fields are set rather than by a `kind` discriminator: a `separator`\n * is a rule, a `heading` is a non-interactive group label, and anything else\n * is a command.\n *\n * The list is data rather than markup because the primitive stays free of app\n * logic — selecting a row emits its `value` and the host decides what that\n * command means.\n */\nexport interface ContextMenuEntry {\n /** The visible text of a command or a heading. */\n label?: string;\n /** The command id emitted on select. An entry without one is not selectable. */\n value?: string;\n /** Accelerator hint shown at the trailing edge, e.g. `⌘S`. Display only — the primitive binds no keys. */\n shortcut?: string;\n /** Render a divider. Every other field is ignored. */\n separator?: boolean;\n /** Render a non-interactive section heading rather than a command. */\n heading?: boolean;\n /** A destructive command — painted in the destructive token so it is not one indistinguishable row. */\n danger?: boolean;\n /** Visible but inert: announced as disabled, and selecting it emits nothing. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ContextMenuRoot,\n ContextMenuTrigger,\n ContextMenuPortal,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n} from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * ContextMenu — a right-click command list, with roving focus, typeahead and\n * arrow-key navigation.\n *\n * The escape contract mirrors DropdownMenu: opening moves focus into the menu\n * and onto the first command; Esc and an outside click both close it; closing\n * returns focus to the trigger's element. The menu is modal, so while it is\n * open the page behind does not scroll and the rest of the page is hidden from\n * assistive technology.\n *\n * Selecting an entry emits `select` with its `value`; the host maps that id to\n * an action, which is what keeps the primitive free of app logic.\n *\n * Reach for `DropdownMenu` when the trigger should be a visible button rather\n * than a right-click, and for a select control when the list *is* a field's\n * value.\n */\nwithDefaults(\n defineProps<{\n /** The rows, in order. Three kinds, told apart by which fields are set. */\n items: ContextMenuEntry[];\n /** Drive the menu from the host with `v-model:open`; omit it and the menu owns its own state. */\n open?: boolean | undefined;\n }>(),\n // `open: undefined` is load-bearing, not a redundant default — it is what\n // keeps the uncontrolled third state reachable past Vue's absent-Boolean\n // casting. `optional()` in the template is the other half; its docblock\n // carries the reasoning for both.\n { open: undefined },\n);\nconst emit = defineEmits<{ select: [value: string]; \"update:open\": [value: boolean] }>();\n\n/**\n * A disabled entry, and an entry with no command id, select nothing. Reka\n * already blocks pointer events on the disabled row; this is the second half,\n * for the keyboard path that reaches it anyway.\n */\nfunction choose(item: ContextMenuEntry): void {\n if (item.disabled || item.value === undefined) return;\n emit(\"select\", item.value);\n}\n</script>\n\n<template>\n <ContextMenuRoot v-bind=\"optional({ open })\" @update:open=\"$emit('update:open', $event)\">\n <!-- @slot The element that receives the right-click. Rendered `as-child`,\n so the caller's own element *is* the trigger. -->\n <ContextMenuTrigger as-child>\n <slot name=\"trigger\" />\n </ContextMenuTrigger>\n\n <ContextMenuPortal>\n <ContextMenuContent\n :side-offset=\"6\"\n align=\"start\"\n :class=\"\n cn(\n 'z-50 min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none',\n // Scoped to the open state, never unconditional. Reka's Presence\n // keeps closed content mounted until an animationend arrives, and a\n // mount-only animation never fires a second one — so an\n // unconditional animation class strands an invisible menu over the\n // page, catching clicks meant for what is behind it. Scoped, the\n // closed element computes `animation-name: none` and Presence\n // unmounts it at once. The origin below makes the menu grow out of\n // the click point rather than out of its own centre.\n 'data-[state=open]:animate-scale-in',\n )\n \"\n style=\"transform-origin: var(--reka-popper-transform-origin)\"\n >\n <template v-for=\"(item, i) in items\" :key=\"i\">\n <ContextMenuSeparator v-if=\"item.separator\" class=\"my-1 h-px bg-border\" />\n <ContextMenuLabel\n v-else-if=\"item.heading\"\n class=\"px-2 py-1.5 text-micro font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {{ item.label }}\n </ContextMenuLabel>\n <!-- The reveal delay comes from the shared stagger vocabulary, capped\n there so a long menu does not tail off, rather than from a\n per-menu number written by hand.\n\n `disabled` is coerced rather than forwarded raw: an entry that\n simply omits the field is not disabled, and Reka's own prop does\n not accept `undefined` as a way of saying so. -->\n <ContextMenuItem\n v-else\n :disabled=\"item.disabled ?? false\"\n :class=\"\n cn(\n 'flex cursor-pointer items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none',\n 'transition-colors duration-fast ease-out',\n // The rows reveal in sequence as the menu opens, which reads as\n // a list arriving rather than a block appearing. The hover\n // colour transition above is a separate channel, so a row\n // highlighted mid-reveal still responds at once.\n 'animate-fade-rise',\n 'data-[highlighted]:bg-subtle',\n // The row's colour, not its alpha. A menu row is nothing but\n // its label, and `data-[disabled]:opacity-40` composited that\n // label to 2.40:1 on the popover and its shortcut to 1.79:1 —\n // a disabled row a reader cannot make out tells them nothing\n // about what is unavailable, which is the only thing it is\n // there to say. Muted instead: 5.76:1, and a clear drop from\n // the 15.46:1 an available row wears.\n //\n // It takes the danger colour with it, deliberately. An\n // attribute selector outranks the plain `text-destructive-text`\n // below whatever order Tailwind emits them in, and that is the\n // right way round: red is a warning about an action, and there\n // is no action here to warn about.\n 'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground',\n item.danger &&\n 'text-destructive-text data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive-text',\n )\n \"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n @select=\"choose(item)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </ContextMenuItem>\n </template>\n </ContextMenuContent>\n </ContextMenuPortal>\n </ContextMenuRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;EAyEA,MAAM,OAAO;;;;;;EAOb,SAAS,OAAO,MAA8B;GAC5C,IAAI,KAAK,YAAY,KAAK,UAAU,KAAA,GAAW;GAC/C,KAAK,UAAU,KAAK,KAAK;EAC3B;;GAIE,OAAA,UAAA,GAAA,YAoFkB,MAAA,eAAA,GApFlB,WAAyB,MAoFP,QAAA,CApFe,CAAA,EAAA,MAAG,QAAA,KAAI,CAAA,GAAA,EAAM,iBAAW,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,eAAgB,MAAM,GAAA,CAAA,GAAA;IAGpF,SAAA,cAEqB,CAFrB,YAEqB,MAAA,kBAAA,GAAA,EAFD,YAAA,GAAQ,GAAA;KAC1B,SAAA,cAAuB,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;IAGzB,CAAA,GAAA,YA4EoB,MAAA,iBAAA,GAAA,MAAA;KA3ElB,SAAA,cA0EqB,CA1ErB,YA0EqB,MAAA,kBAAA,GAAA;MAzElB,eAAa;MACd,OAAM;MACL,OAAK,eAAa,MAAA,EAAA,CAAE,CAAA,oHAAA,oCAAA,CAAA;MAcrB,OAAA,EAAA,oBAAA,sCAAA;;MAEU,SAAA,cAA0B,EAApC,UAAA,IAAA,GAAA,mBAsDW,UAAA,MAAA,WAtDmB,QAAA,QAAZ,MAAM,MAAC;OAAkB,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,EAAC,GAAA,CACd,KAAK,aAAjC,UAAA,GAAA,YAA0E,MAAA,oBAAA,GAAA;;QAA9B,OAAM;OAErC,CAAA,KAAA,KAAK,WADlB,UAAA,GAAA,YAKmB,MAAA,gBAAA,GAAA;;QAHjB,OAAM;;QAEN,SAAA,cAAgB,CAAb,gBAAA,gBAAA,KAAK,KAAK,GAAA,CAAA,CAAA,CAAA;;OASf,GAAA,IAAA,MAAA,UAAA,GAAA,YAsCkB,MAAA,eAAA,GAAA;;QApCf,UAAU,KAAK,YAAQ;QACvB,OAAK,eAAiB,MAAA,EAAA,CAAE,CAAA,sHAAA,4CAAA,qBAAA,gCAAA,6EAA2jD,KAAK,UAAA,qGAAA,CAAA;QA4BxlD,OAAK,eAAA,EAAA,gBAAoB,MAAA,gBAAA,CAAgB,CAAC,CAAC,EAAA,CAAA;QAC3C,WAAM,WAAE,OAAO,IAAI;;QAEpB,SAAA,cAA6B,CAA7B,mBAA6B,QAAA,MAAA,gBAApB,KAAK,KAAK,GAAA,CAAA,GACP,KAAK,YAAjB,UAAA,GAAA,mBAEO,QAFP,YAEO,gBADF,KAAK,QAAQ,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"ContextMenu.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/context-menu/src/ContextMenu.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * One row of the menu, and the three kinds a menu is built from are told apart\n * by which fields are set rather than by a `kind` discriminator: a `separator`\n * is a rule, a `heading` is a non-interactive group label, and anything else\n * is a command.\n *\n * The list is data rather than markup because the primitive stays free of app\n * logic — selecting a row emits its `value` and the host decides what that\n * command means.\n */\nexport interface ContextMenuEntry {\n /** The visible text of a command or a heading. */\n label?: string;\n /** The command id emitted on select. An entry without one is not selectable. */\n value?: string;\n /** Accelerator hint shown at the trailing edge, e.g. `⌘S`. Display only — the primitive binds no keys. */\n shortcut?: string;\n /** Render a divider. Every other field is ignored. */\n separator?: boolean;\n /** Render a non-interactive section heading rather than a command. */\n heading?: boolean;\n /** A destructive command — painted in the destructive token so it is not one indistinguishable row. */\n danger?: boolean;\n /** Visible but inert: announced as disabled, and selecting it emits nothing. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ContextMenuRoot,\n ContextMenuTrigger,\n ContextMenuPortal,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n} from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * ContextMenu — a right-click command list, with roving focus, typeahead and\n * arrow-key navigation.\n *\n * The escape contract mirrors DropdownMenu: opening moves focus into the menu\n * and onto the first command; Esc and an outside click both close it; closing\n * returns focus to the trigger's element. The menu is modal, so while it is\n * open the page behind does not scroll and the rest of the page is hidden from\n * assistive technology.\n *\n * Selecting an entry emits `select` with its `value`; the host maps that id to\n * an action, which is what keeps the primitive free of app logic.\n *\n * Reach for `DropdownMenu` when the trigger should be a visible button rather\n * than a right-click, and for a select control when the list *is* a field's\n * value.\n */\nwithDefaults(\n defineProps<{\n /** The rows, in order. Three kinds, told apart by which fields are set. */\n items: ContextMenuEntry[];\n /** Drive the menu from the host with `v-model:open`; omit it and the menu owns its own state. */\n open?: boolean | undefined;\n /**\n * Reading direction for the menu and its arrow-key navigation. Reka\n * mirrors traversal from this; without forwarding it a right-to-left\n * host gets left-to-right menus no matter what the page declares.\n */\n dir?: \"ltr\" | \"rtl\";\n }>(),\n // `open: undefined` is load-bearing, not a redundant default — it is what\n // keeps the uncontrolled third state reachable past Vue's absent-Boolean\n // casting. `optional()` in the template is the other half; its docblock\n // carries the reasoning for both.\n { open: undefined },\n);\nconst emit = defineEmits<{ select: [value: string]; \"update:open\": [value: boolean] }>();\n\n/**\n * A disabled entry, and an entry with no command id, select nothing. Reka\n * already blocks pointer events on the disabled row; this is the second half,\n * for the keyboard path that reaches it anyway.\n */\nfunction choose(item: ContextMenuEntry): void {\n if (item.disabled || item.value === undefined) return;\n emit(\"select\", item.value);\n}\n</script>\n\n<template>\n <ContextMenuRoot v-bind=\"optional({ open, dir })\" @update:open=\"$emit('update:open', $event)\">\n <!-- @slot The element that receives the right-click. Rendered `as-child`,\n so the caller's own element *is* the trigger. -->\n <ContextMenuTrigger as-child>\n <slot name=\"trigger\" />\n </ContextMenuTrigger>\n\n <ContextMenuPortal>\n <ContextMenuContent\n :side-offset=\"6\"\n align=\"start\"\n :class=\"\n cn(\n 'z-overlay min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none',\n // Both states scoped, never unconditional. Reka's Presence\n // keeps closed content mounted until an animationend arrives, and a\n // mount-only animation never fires a second one — so an\n // unconditional animation class strands an invisible menu over the\n // page, catching clicks meant for what is behind it. Scoped, the\n // closed element computes `animation-name: none` and Presence\n // unmounts it at once; the paired exit is precisely what Presence\n // is waiting to hold it for. The origin below makes the menu grow\n // out of the click point rather than out of its own centre — on\n // the way out as well as in.\n 'data-[state=open]:animate-scale-in data-[state=closed]:animate-scale-out',\n )\n \"\n style=\"transform-origin: var(--reka-popper-transform-origin)\"\n >\n <template v-for=\"(item, i) in items\" :key=\"i\">\n <ContextMenuSeparator v-if=\"item.separator\" class=\"my-1 h-px bg-border\" />\n <ContextMenuLabel\n v-else-if=\"item.heading\"\n class=\"px-2 py-1.5 text-micro font-medium uppercase tracking-wide text-muted-foreground\"\n >\n {{ item.label }}\n </ContextMenuLabel>\n <!-- The reveal delay comes from the shared stagger vocabulary, capped\n there so a long menu does not tail off, rather than from a\n per-menu number written by hand.\n\n `disabled` is coerced rather than forwarded raw: an entry that\n simply omits the field is not disabled, and Reka's own prop does\n not accept `undefined` as a way of saying so. -->\n <ContextMenuItem\n v-else\n :disabled=\"item.disabled ?? false\"\n :class=\"\n cn(\n 'flex cursor-pointer items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none',\n 'transition-colors duration-fast ease-out',\n // The rows reveal in sequence as the menu opens, which reads as\n // a list arriving rather than a block appearing. The hover\n // colour transition above is a separate channel, so a row\n // highlighted mid-reveal still responds at once.\n 'animate-fade-rise',\n 'data-[highlighted]:bg-subtle',\n // The row's colour, not its alpha. A menu row is nothing but\n // its label, and `data-[disabled]:opacity-40` composited that\n // label to 2.40:1 on the popover and its shortcut to 1.79:1 —\n // a disabled row a reader cannot make out tells them nothing\n // about what is unavailable, which is the only thing it is\n // there to say. Muted instead: 5.76:1, and a clear drop from\n // the 15.46:1 an available row wears.\n //\n // It takes the danger colour with it, deliberately. An\n // attribute selector outranks the plain `text-destructive-text`\n // below whatever order Tailwind emits them in, and that is the\n // right way round: red is a warning about an action, and there\n // is no action here to warn about.\n 'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground',\n item.danger &&\n 'text-destructive-text data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive-text',\n )\n \"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n @select=\"choose(item)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </ContextMenuItem>\n </template>\n </ContextMenuContent>\n </ContextMenuPortal>\n </ContextMenuRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;EA+EA,MAAM,OAAO;;;;;;EAOb,SAAS,OAAO,MAA8B;GAC5C,IAAI,KAAK,YAAY,KAAK,UAAU,KAAA,GAAW;GAC/C,KAAK,UAAU,KAAK,KAAK;EAC3B;;GAIE,OAAA,UAAA,GAAA,YAsFkB,MAAA,eAAA,GAtFlB,WAAyB,MAsFP,QAAA,CAtFe,CAAA;IAAA,MAAG,QAAA;IAAI,KAAE,QAAA;GAAG,CAAA,GAAA,EAAM,iBAAW,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,eAAgB,MAAM,GAAA,CAAA,GAAA;IAGzF,SAAA,cAEqB,CAFrB,YAEqB,MAAA,kBAAA,GAAA,EAFD,YAAA,GAAQ,GAAA;KAC1B,SAAA,cAAuB,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;IAGzB,CAAA,GAAA,YA8EoB,MAAA,iBAAA,GAAA,MAAA;KA7ElB,SAAA,cA4EqB,CA5ErB,YA4EqB,MAAA,kBAAA,GAAA;MA3ElB,eAAa;MACd,OAAM;MACL,OAAK,eAAa,MAAA,EAAA,CAAE,CAAA,yHAAA,0EAAA,CAAA;MAgBrB,OAAA,EAAA,oBAAA,sCAAA;;MAEU,SAAA,cAA0B,EAApC,UAAA,IAAA,GAAA,mBAsDW,UAAA,MAAA,WAtDmB,QAAA,QAAZ,MAAM,MAAC;OAAkB,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,EAAC,GAAA,CACd,KAAK,aAAjC,UAAA,GAAA,YAA0E,MAAA,oBAAA,GAAA;;QAA9B,OAAM;OAErC,CAAA,KAAA,KAAK,WADlB,UAAA,GAAA,YAKmB,MAAA,gBAAA,GAAA;;QAHjB,OAAM;;QAEN,SAAA,cAAgB,CAAb,gBAAA,gBAAA,KAAK,KAAK,GAAA,CAAA,CAAA,CAAA;;OASf,GAAA,IAAA,MAAA,UAAA,GAAA,YAsCkB,MAAA,eAAA,GAAA;;QApCf,UAAU,KAAK,YAAQ;QACvB,OAAK,eAAiB,MAAA,EAAA,CAAE,CAAA,sHAAA,4CAAA,qBAAA,gCAAA,6EAA2jD,KAAK,UAAA,qGAAA,CAAA;QA4BxlD,OAAK,eAAA,EAAA,gBAAoB,MAAA,gBAAA,CAAgB,CAAC,CAAC,EAAA,CAAA;QAC3C,WAAM,WAAE,OAAO,IAAI;;QAEpB,SAAA,cAA6B,CAA7B,mBAA6B,QAAA,MAAA,gBAApB,KAAK,KAAK,GAAA,CAAA,GACP,KAAK,YAAjB,UAAA,GAAA,mBAEO,QAFP,YAEO,gBADF,KAAK,QAAQ,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatePicker.js","names":[],"sources":["../../../../../packages/primitives/date-picker/src/DatePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { CalendarPanelLabels, DateSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology, which is two\n * shared slices and nothing of its own: the names of the field's segments and\n * the calendar popover's chrome. Both are declared in `src/lib/date-labels.ts`,\n * because `DateTimePicker`, `DateRangePicker` and `DateTimeRangePicker` say the\n * same words for the same reason — they are all one Reka implementation\n * underneath — and four copies of `Previous month` is a translator writing the\n * same word four times.\n *\n * An intersection rather than a third interface, so this name is the type a\n * consumer annotates their own bag with and the two halves stay one definition\n * each. Annotate with `LabelOverrides<DatePickerLabels>` and never with this\n * directly: this one is total, and a key added in a later release would stop a\n * consumer's build.\n */\nexport type DatePickerLabels = DateSegmentLabels & CalendarPanelLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, useTemplateRef } from \"vue\";\nimport {\n DatePickerRoot,\n DatePickerAnchor,\n DatePickerField,\n DatePickerInput,\n DatePickerTrigger,\n DatePickerContent,\n DatePickerCalendar,\n DatePickerHeader,\n DatePickerPrev,\n DatePickerHeading,\n DatePickerNext,\n DatePickerGrid,\n DatePickerGridHead,\n DatePickerGridRow,\n DatePickerHeadCell,\n DatePickerGridBody,\n DatePickerCell,\n DatePickerCellTrigger,\n} from \"reka-ui\";\nimport { CalendarDays, ChevronLeft, ChevronRight } from \"@lucide/vue\";\nimport {\n getLocalTimeZone,\n isToday,\n parseDate,\n toCalendarDate,\n type DateValue,\n} from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport {\n CALENDAR_PANEL_LABELS,\n DATE_SEGMENT_LABELS,\n emptySegmentValueText,\n isDateSegmentPart,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * DatePicker — one calendar date, entered either by typing it into a segmented\n * field or by picking it out of a calendar popover. Reach for it wherever a\n * date is a value rather than a search: a due date, a start date, a date of\n * birth.\n *\n * The segmented field is the reason this is not an `<input type=\"date\">`.\n * Arrow keys change the segment under the cursor, typing digits fills it and\n * advances, and the segment order and the month names follow `locale` rather\n * than the browser's own — which is what lets a Vietnamese reader and an\n * American reader see the same date written the way each of them writes it.\n * The calendar is the second path to the same value, not the only one.\n *\n * **The model value is a plain ISO date string, `\"YYYY-MM-DD\"`, never a\n * `DateValue`.** Reka and `@internationalized/date` model a date as a calendar\n * object, and that object is the right shape *inside* a date control — it\n * knows about calendars other than the Gregorian one, about month lengths, and\n * about what \"next month\" means on the 31st. It is the wrong shape at the\n * boundary: a host binding a due date has a string from its API and wants a\n * string back for it, and nobody should have to learn a date library to bind a\n * date. So the string is parsed on the way in and re-serialised on the way\n * out, the same instinct as Slider exposing a scalar where Reka models an\n * array. `min`, `max` and the emitted value all speak that one format.\n *\n * A value that does not parse — a half-typed string, an empty one, a\n * datetime — is read as *no date chosen* rather than thrown on. A component\n * library cannot decide that a host's bad data is fatal, and a date field that\n * crashes the page while someone types into the host's own text input beside\n * it is the worst possible reading of \"strict\".\n *\n * For a span rather than a single day, compose two of these — a range control\n * shares almost none of this one's model. For a month or a year alone, neither\n * this nor a Select is right; that is a control this library does not have yet.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * its hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`, so `<Field label=\"Due date\"\n * error=\"…\"><DatePicker /></Field>` needs no attributes at the call site. Every\n * prop below still wins over the row when it is set, in both directions — which\n * is why the four booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states, not two dials on one, so the\n * field has three resting appearances rather than two. A read-only date is a\n * value on show: the field stays a Tab stop, its segments stay readable and\n * copyable at full strength, and it takes a lifted fill. A disabled one is\n * unavailable: no Tab stop, nothing submitted, and drained a step further in\n * fill, text colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen date as ISO `YYYY-MM-DD`. Unset, empty or unparseable means no\n * date is chosen. The explicit `| undefined` is what lets `v-model` bind a\n * ref that clears — the emitted value drops to `undefined` when the field\n * is emptied, and a plain optional prop refuses to take it back.\n */\n modelValue?: string | undefined;\n /** The earliest choosable date, same ISO format. Days before it are announced disabled and skipped by the keyboard. */\n min?: string;\n /** The latest choosable date, same ISO format. Days after it are announced disabled and skipped by the keyboard. */\n max?: string;\n /** BCP 47 tag driving the order of the field's segments, the month and weekday names, and which day a week starts on. */\n locale?: string;\n /** Unavailable: dims the control, refuses the calendar, and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the date without allowing an edit: the field stays a Tab stop and stays submitted, and the calendar button is dropped because nothing in the calendar could be chosen. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the date through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `DatePickerLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: six of the nine names replace English literals Reka UI\n * writes of its own accord, and a language is set once with\n * `provideLoomLabels`.\n */\n labels?: LabelOverrides<DatePickerLabels>;\n }>(),\n {\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning. `false` is a caller deciding this one control is fine,\n // enabled or editable even though its row is not; absent is a caller\n // saying nothing, and only `undefined` survives to mean that past Vue's\n // absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen date as ISO `YYYY-MM-DD`, or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\n}>();\n\n// Two slots, one prop. `text` is the name everywhere else in the library, and\n// there is no single `text` here because there is no single slot: the segment\n// names and the popover's chrome are separate vocabularies with separate\n// registry entries, and naming them apart is what keeps a reader of the\n// template able to see which one a binding came from. The one `labels` prop\n// feeds both — a getter returning the intersection satisfies either half, and\n// the keys the other half does not know are ignored by `useLabels`' own return\n// type.\nconst segmentText = useLabels(\"dateSegments\", DATE_SEGMENT_LABELS, () => props.labels);\nconst panelText = useLabels(\"calendarPanel\", CALENDAR_PANEL_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. `undefined` for the parts this slice does not\n// name — the `literal` separators, which Reka renders `aria-hidden` and which\n// must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return isDateSegmentPart(part) ? segmentText.value[part] : undefined;\n}\n\n// The companion to `segmentLabel`, and the reason it returns an object rather\n// than a string is in `emptySegmentValueText`: Reka's `aria-valuetext` is a\n// hard-coded \"Empty\" only while the segment holds nothing, so this replaces\n// that case and leaves the filled one — a number, and the locale's own month\n// name — exactly as Reka wrote it.\n//\n// The month is the exception. Reka writes `\"5 - May\"`, with the month name from\n// `Intl.DateTimeFormat` but the separator and order hard-coded. `filledMonth`\n// lets a host compose the string their language needs (Japanese `\"5月\"`,\n// Vietnamese `\"tháng 5\"`). The numeric month arrives from the parsed model value\n// rather than from the segment's displayed text, because `item.value` is the\n// locale-formatted display and may not contain a parseable number.\nfunction segmentValueText(\n part: string,\n value: string,\n monthValue?: number,\n): { \"aria-valuetext\"?: string } {\n const empty = emptySegmentValueText(part, value, segmentText.value.empty);\n if (empty[\"aria-valuetext\"]) return empty;\n if (part === \"month\" && monthValue !== undefined) {\n return {\n \"aria-valuetext\": segmentText.value.filledMonth({ value: monthValue, locale: props.locale }),\n };\n }\n return {};\n}\n\n// `class` sizes the whole control, so it lands on the anchor — the element the\n// caller's `max-w-xs` has to reach, and the one the popover measures itself\n// against. Every other fallthrough attribute goes one level in, onto the\n// `role=\"group\"` field: `aria-label`, `aria-labelledby` and `aria-describedby`\n// name and describe the thing being entered, and a group is what a screen\n// reader announces that name on entering. Putting them on the anchor instead\n// would name a layout div nothing ever lands in.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The date segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so the calendar trigger inside a disabled group went inert\n * while the segments beside it still typed a date on an arrow key. Measured in\n * jsdom: an empty `mm` segment became `8`.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst anchor = useTemplateRef<{ $el?: Element }>(\"anchor\");\nconst groupDisabled = useAncestorDisabled(() => anchor.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are read off `attrs` and handed in as this caller's own\n// values — a caller who sets either still beats the row, and the row's\n// description is merged into theirs rather than replacing it.\n//\n// `field.attrs` spreads onto `DatePickerField` **after** `fieldAttrs`, which is\n// the node those two were already landing on, and the position is the contract:\n// the bag has resolved the caller's own values already and drops every key that\n// resolved to nothing, so it can only add. Where each key ends up is Reka's\n// decision and worth knowing: `DateFieldRoot` declares `id` and `name` as props\n// and puts both on the visually-hidden `<input>` it submits the date through —\n// which is the element a `<label for>` can actually name, a `role=\"group\"` div\n// being unlabelable — while `aria-describedby`, `aria-required` and\n// `aria-invalid` are undeclared, fall through to `$attrs`, and land on the group\n// itself, where a screen reader announces them once on entering the field.\n//\n// The `:aria-invalid` binding that used to sit on the field is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // trigger button inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row. A caller who passes\n// `aria-labelledby` as a fallthrough attr wins (it reaches this control's own\n// `ariaLabelledby` through `useFieldControl`). A caller who passes `aria-label`\n// wins too — `aria-labelledby` overrides `aria-label` in ARIA, so the row's\n// label must not appear when the caller named the control themselves. Otherwise,\n// the row's label (published by Field as `labelledBy`) is the accessible name.\nconst groupLabelledBy = computed(() =>\n attrs[\"aria-label\"] || attrs[\"aria-labelledby\"] ? undefined : field.labelledBy,\n);\n\nfunction fromIso(value: string | undefined): DateValue | undefined {\n if (!value) return undefined;\n try {\n return parseDate(value);\n } catch {\n return undefined;\n }\n}\n\n// `toString()` already normalises to the Gregorian ISO form whatever calendar\n// the value carries — a locale such as `th-TH` makes Reka's own placeholder a\n// Buddhist date whose year reads 2569, and it still prints `2026-08-11`. What\n// `toCalendarDate` adds is dropping a time component before it can turn the\n// string into `2026-08-11T00:00:00`. `granularity=\"day\"` below is what makes\n// that unreachable today; this is what keeps it unreachable if it ever moves.\nfunction toIso(date: DateValue): string {\n return toCalendarDate(date).toString();\n}\n\n// `optional()` rather than three separate bindings: an absent bound forwarded\n// as an explicit `undefined` is still a key Reka sees, and an absent\n// `modelValue` forwarded that way is what turns an uncontrolled field into a\n// permanently empty one. Dropping the key says what is true.\nconst rootValue = computed(() =>\n optional({\n modelValue: fromIso(props.modelValue),\n minValue: fromIso(props.min),\n maxValue: fromIso(props.max),\n }),\n);\n\n// The panel's open state is owned here rather than left to Reka's\n// `closeOnSelect`, which closes by watching its *own* model value — and that\n// value only moves for a host that echoes the emitted date straight back down.\n// A host that validates before accepting one, or that binds nothing at all,\n// would pick a day and watch the calendar stay open over the field it just\n// filled. Closing on the choice itself does not depend on anyone answering.\nconst open = ref(false);\n\nfunction onDateChange(date: DateValue | undefined) {\n open.value = false;\n emit(\"update:modelValue\", date ? toIso(date) : undefined);\n}\n\n// Today is a state, and it may not be carried by the marker dot alone — a\n// reader who cannot see the dot gets `aria-current=\"date\"`, which is the one\n// attribute screen readers already announce for \"this is the current one\".\n// Reka publishes today only as a `data-` attribute and a slot prop, neither of\n// which can reach an attribute binding on the cell itself.\nfunction isDateToday(date: DateValue): boolean {\n return isToday(date, getLocalTimeZone());\n}\n\n// The field is one Tab stop, not three. Reka leaves every segment tabbable,\n// which turns a single date into three stops a keyboard user has to walk\n// through — and the control everyone already knows, the browser's own date\n// input, is one stop with the arrow keys moving inside it. So a roving\n// tabindex names exactly one segment as the stop, and Reka's own\n// ArrowLeft/ArrowRight handling (and its auto-advance after a filled segment)\n// moves between them by calling `focus()` directly, which ignores tabindex\n// entirely and so keeps working untouched.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // A disabled field has no stop at all; returning `undefined` removes the\n // attribute, which is exactly what Reka does for the same case. A read-only\n // one keeps every stop it had — the value is on show and a reader still\n // arrows across it to read it, which is the whole difference between the two\n // states.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a locale change takes it away —\n // a field with no tab stop is unreachable, and `en` has no era segment that\n // `ja-JP-u-ca-japanese` does.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// The month/year heading names the grid, so the two are wired together rather\n// than the grid being an unnamed table of numbers.\nconst headingId = useId();\n\nconst panel = ref<HTMLElement | null>(null);\n\n// Reka's focus scope would land on the first tabbable thing in the panel,\n// which is the previous-month button — so a keyboard user opening the calendar\n// arrives one Tab away from the grid and two from the day they came for.\n// Focus goes to the day the calendar is already sitting on instead: the\n// selected date, or today when nothing is selected, which is the cell Reka has\n// given the grid's own roving tabindex to.\nfunction onOpenAutoFocus(event: Event) {\n const day = panel.value?.querySelector<HTMLElement>(\n '[data-reka-calendar-cell-trigger][tabindex=\"0\"]',\n );\n if (!day) return;\n event.preventDefault();\n day.focus();\n}\n</script>\n\n<template>\n <DatePickerRoot\n v-model:open=\"open\"\n v-bind=\"rootValue\"\n :locale=\"locale\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n granularity=\"day\"\n prevent-deselect\n @update:model-value=\"onDateChange\"\n >\n <!-- The invalid marker lives out here, on the control as a whole, because\n the field group below already carries a `data-invalid` of Reka's own\n with a different meaning: that the *typed* date falls outside `min` and\n `max`. Two markers spelled the same on one element is how a style rule\n starts answering a question nobody asked it. -->\n <DatePickerAnchor\n ref=\"anchor\"\n :data-invalid=\"field.invalid || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :class=\"cn('block w-full', attrs.class as string)\"\n >\n <DatePickerField\n v-slot=\"{ segments }\"\n v-bind=\"{ ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :class=\"\n cn(\n // The text input's own height scale, so a date sitting in a form\n // row beside a text field lines up rather than nearly lining up.\n // No gap: the separator between two segments is itself a segment,\n // so spacing them apart writes the date as `3 / 14 / 2026`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring, exactly as TextField's wrapper does,\n // and the focused segment below carries a fill instead of a second\n // outline — a ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and every one of them belongs to this\n // group rather than to the segments inside it. A fill painted per\n // segment leaves the literal separators between them on\n // `background`, and the control comes out striped.\n //\n // Read-only keeps its focus ring and its full-strength text: a\n // read-only date is a value on show, not an unavailable control,\n // and the two must not look alike. The fill lifts off `background`\n // to `subtle`, which holds the segments at 13.46:1, and the rim\n // stays `input` because nothing about the field's reach has moved —\n // it is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this group carries the same state to assistive\n // tech, so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Unavailable is a *colour* and a *weight*, never an opacity.\n // `opacity-50` here faded the segments along with the box, and the\n // segments are the whole content of the control: `--color-foreground`\n // measures 14.09:1 on the resting fill and 2.99:1 once composited at\n // half alpha, with the literal separators and any placeholder\n // segment down at 2.02:1. The fill drains one step past read-only,\n // the text follows it to a measured 4.68:1, and the rim slackens\n // from `input` to `border` — so the two unavailable-looking states\n // part on three channels rather than on hue alone, which is what a\n // reader who cannot separate two greys has to have.\n //\n // The colour is set here rather than on each segment because no\n // segment declares one of its own in the resting state, so each\n // inherits this; `focus:text-primary-text` below is unreachable on a\n // control that cannot take focus.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class\n // it saw last, so a field that is both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- Every `:aria-label` from here down replaces one Reka writes in\n English inside its own render function. Each reaches the DOM node\n as a fallthrough attribute, which Vue merges last, so it is a\n replacement rather than a second name. Removing one does not fall\n back to nothing; it falls back to Reka's English, which is the\n failure the label seam exists to close and which no test of Loom's\n own strings would catch. -->\n <DatePickerInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"segmentValueText(item.part, item.value, fromIso(props.modelValue)?.month)\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </DatePickerInput>\n\n <!-- Dropped entirely while read-only rather than disabled beside a\n field that is plainly still alive. Reka's `readonly` reaches the\n calendar too and makes every cell's click a no-op, so the button\n would open a panel in which nothing can be chosen — a dead end that\n looks like a working control, which is worse than not offering it.\n The segments are still there to read the date off. -->\n <DatePickerTrigger\n v-if=\"!field.readonly\"\n :aria-label=\"panelText.openCalendar\"\n class=\"ml-auto inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-not-allowed\"\n >\n <CalendarDays class=\"h-4 w-4\" />\n </DatePickerTrigger>\n </DatePickerField>\n </DatePickerAnchor>\n\n <DatePickerContent\n align=\"start\"\n :side-offset=\"6\"\n :class=\"\n cn(\n 'z-50 rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md outline-none',\n // Scoped to the open state, never unconditional. Reka's Presence\n // keeps the closed content mounted until an animationend arrives,\n // and a mount-only animation never fires a second one — so an\n // unconditional class strands an invisible panel over the page,\n // eating clicks. Scoped, the closed element computes\n // `animation-name: none` and Presence unmounts it immediately.\n 'data-[state=open]:animate-fade-rise',\n )\n \"\n @open-auto-focus=\"onOpenAutoFocus\"\n >\n <!-- The whole panel rises as one piece. The 42 cells deliberately carry\n no stagger of their own: `listStaggerDelay` is the vocabulary for a\n list of rows arriving in reading order, and a grid rippling in cell\n by cell reads as a rendering glitch rather than as an entrance. -->\n <div ref=\"panel\" class=\"flex flex-col gap-3\">\n <!-- `calendarLabel` is a real Reka prop rather than a literal to\n override, and it defaults to `\"Event Date\"` — a name for somebody\n else's application. It is published twice: as the calendar\n container's `aria-label`, and inside a visually-hidden live region\n Reka renders itself, which a fallthrough attribute could not have\n reached. -->\n <DatePickerCalendar v-slot=\"{ grid, weekDays }\" :calendar-label=\"panelText.calendar\">\n <DatePickerHeader class=\"flex items-center justify-between gap-2\">\n <!-- The two pagers keep `data-[disabled]:opacity-50` where the\n field and the cells gave it up, and the difference is what is\n inside them: a chevron and nothing else. Fading a glyph costs a\n reader nothing, and the buttons carry their name in\n `aria-label` rather than in dimmed text. -->\n <DatePickerPrev\n :aria-label=\"panelText.previousMonth\"\n class=\"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50\"\n >\n <ChevronLeft class=\"h-4 w-4\" />\n </DatePickerPrev>\n\n <!-- Paging the calendar changes nothing a reader is looking at\n except this line, and nothing moves focus — so without the\n live region the month silently becomes a different month. -->\n <DatePickerHeading\n :id=\"headingId\"\n aria-live=\"polite\"\n class=\"text-sm font-medium text-foreground\"\n />\n\n <DatePickerNext\n :aria-label=\"panelText.nextMonth\"\n class=\"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50\"\n >\n <ChevronRight class=\"h-4 w-4\" />\n </DatePickerNext>\n </DatePickerHeader>\n\n <DatePickerGrid\n v-for=\"month in grid\"\n :key=\"month.value.toString()\"\n role=\"grid\"\n :aria-labelledby=\"headingId\"\n class=\"border-collapse select-none\"\n >\n <!-- Reka renders `role=\"application\"` here, which hands every key\n straight to the page and takes the table's own semantics away\n with it. A calendar is a grid: `role=\"grid\"` is what makes the\n cells rows and columns to a screen reader rather than 42\n unrelated buttons, and the arrow-key handling Reka installs on\n the cells is what a grid promises anyway.\n\n The rows and cells below are laid out by the table itself\n rather than by flex, and that is load-bearing: `display: flex`\n on a `tr` drops its implicit `row` mapping in Chromium and\n WebKit, which leaves `role=\"gridcell\"` inside a grid with no\n row between them — a broken grid that looks identical. -->\n <DatePickerGridHead>\n <DatePickerGridRow>\n <DatePickerHeadCell\n v-for=\"(day, index) in weekDays\"\n :key=\"index\"\n class=\"w-9 text-xs font-normal text-muted-foreground\"\n >\n {{ day }}\n </DatePickerHeadCell>\n </DatePickerGridRow>\n </DatePickerGridHead>\n\n <DatePickerGridBody>\n <DatePickerGridRow v-for=\"(week, weekIndex) in month.rows\" :key=\"weekIndex\">\n <DatePickerCell\n v-for=\"day in week\"\n :key=\"day.toString()\"\n :date=\"day\"\n class=\"p-0 text-center\"\n >\n <!-- A day out of `min`/`max` range is drawn in colour rather\n than faded: the number is the cell's whole content, and\n `opacity-50` took `--color-foreground` from 14.09:1 on\n the popover to 3.13:1. Muted is 5.76:1 and the rule is\n written as `:not([data-selected])` so it can never order\n against the selected fill's own text colour — the same\n trick the range pickers use on their four competing\n fills. The strike is the second, hueless cue, and it is\n what keeps an unavailable day apart from an adjacent\n month's, which is muted too and still selectable. -->\n <DatePickerCellTrigger\n v-slot=\"{ dayValue, today }\"\n :day=\"day\"\n :month=\"month.value\"\n :aria-current=\"isDateToday(day) ? 'date' : undefined\"\n class=\"group relative inline-flex h-9 w-9 items-center justify-center rounded-sm text-sm text-foreground transition-colors duration-fast ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo data-[outside-view]:text-muted-foreground data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[disabled]:pointer-events-none data-[disabled]:line-through [&[data-disabled]:not([data-selected])]:text-muted-foreground\"\n >\n {{ dayValue }}\n <!-- Today's second cue. The dot is decoration to a screen\n reader — `aria-current` on the cell is what it reads —\n but it is the whole cue for a reader who is looking at\n the grid, and it is a shape rather than a hue so it\n survives a colour deficiency and forced-colors both. -->\n <span\n v-if=\"today\"\n aria-hidden=\"true\"\n class=\"absolute bottom-1 h-1 w-1 rounded-full bg-primary group-data-[selected]:bg-primary-foreground\"\n />\n </DatePickerCellTrigger>\n </DatePickerCell>\n </DatePickerGridRow>\n </DatePickerGridBody>\n </DatePickerGrid>\n </DatePickerCalendar>\n </div>\n </DatePickerContent>\n </DatePickerRoot>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"DatePicker.js","names":[],"sources":["../../../../../packages/primitives/date-picker/src/DatePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { CalendarPanelLabels, DateSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology, which is two\n * shared slices and nothing of its own: the names of the field's segments and\n * the calendar popover's chrome. Both are declared in `src/lib/date-labels.ts`,\n * because `DateTimePicker`, `DateRangePicker` and `DateTimeRangePicker` say the\n * same words for the same reason — they are all one Reka implementation\n * underneath — and four copies of `Previous month` is a translator writing the\n * same word four times.\n *\n * An intersection rather than a third interface, so this name is the type a\n * consumer annotates their own bag with and the two halves stay one definition\n * each. Annotate with `LabelOverrides<DatePickerLabels>` and never with this\n * directly: this one is total, and a key added in a later release would stop a\n * consumer's build.\n */\nexport type DatePickerLabels = DateSegmentLabels & CalendarPanelLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, useTemplateRef } from \"vue\";\nimport {\n DatePickerRoot,\n DatePickerAnchor,\n DatePickerField,\n DatePickerInput,\n DatePickerTrigger,\n DatePickerContent,\n DatePickerCalendar,\n DatePickerHeader,\n DatePickerPrev,\n DatePickerHeading,\n DatePickerNext,\n DatePickerGrid,\n DatePickerGridHead,\n DatePickerGridRow,\n DatePickerHeadCell,\n DatePickerGridBody,\n DatePickerCell,\n DatePickerCellTrigger,\n} from \"reka-ui\";\nimport { CalendarDays, ChevronLeft, ChevronRight } from \"@lucide/vue\";\nimport {\n getLocalTimeZone,\n isToday,\n parseDate,\n toCalendarDate,\n type DateValue,\n} from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport {\n CALENDAR_PANEL_LABELS,\n DATE_SEGMENT_LABELS,\n emptySegmentValueText,\n isDateSegmentPart,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * DatePicker — one calendar date, entered either by typing it into a segmented\n * field or by picking it out of a calendar popover. Reach for it wherever a\n * date is a value rather than a search: a due date, a start date, a date of\n * birth.\n *\n * The segmented field is the reason this is not an `<input type=\"date\">`.\n * Arrow keys change the segment under the cursor, typing digits fills it and\n * advances, and the segment order and the month names follow `locale` rather\n * than the browser's own — which is what lets a Vietnamese reader and an\n * American reader see the same date written the way each of them writes it.\n * The calendar is the second path to the same value, not the only one.\n *\n * **The model value is a plain ISO date string, `\"YYYY-MM-DD\"`, never a\n * `DateValue`.** Reka and `@internationalized/date` model a date as a calendar\n * object, and that object is the right shape *inside* a date control — it\n * knows about calendars other than the Gregorian one, about month lengths, and\n * about what \"next month\" means on the 31st. It is the wrong shape at the\n * boundary: a host binding a due date has a string from its API and wants a\n * string back for it, and nobody should have to learn a date library to bind a\n * date. So the string is parsed on the way in and re-serialised on the way\n * out, the same instinct as Slider exposing a scalar where Reka models an\n * array. `min`, `max` and the emitted value all speak that one format.\n *\n * A value that does not parse — a half-typed string, an empty one, a\n * datetime — is read as *no date chosen* rather than thrown on. A component\n * library cannot decide that a host's bad data is fatal, and a date field that\n * crashes the page while someone types into the host's own text input beside\n * it is the worst possible reading of \"strict\".\n *\n * For a span rather than a single day, compose two of these — a range control\n * shares almost none of this one's model. For a month or a year alone, neither\n * this nor a Select is right; that is a control this library does not have yet.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * its hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`, so `<Field label=\"Due date\"\n * error=\"…\"><DatePicker /></Field>` needs no attributes at the call site. Every\n * prop below still wins over the row when it is set, in both directions — which\n * is why the four booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states, not two dials on one, so the\n * field has three resting appearances rather than two. A read-only date is a\n * value on show: the field stays a Tab stop, its segments stay readable and\n * copyable at full strength, and it takes a lifted fill. A disabled one is\n * unavailable: no Tab stop, nothing submitted, and drained a step further in\n * fill, text colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen date as ISO `YYYY-MM-DD`. Unset, empty or unparseable means no\n * date is chosen. The explicit `| undefined` is what lets `v-model` bind a\n * ref that clears — the emitted value drops to `undefined` when the field\n * is emptied, and a plain optional prop refuses to take it back.\n */\n modelValue?: string | undefined;\n /** The earliest choosable date, same ISO format. Days before it are announced disabled and skipped by the keyboard. */\n min?: string;\n /** The latest choosable date, same ISO format. Days after it are announced disabled and skipped by the keyboard. */\n max?: string;\n /** BCP 47 tag driving the order of the field's segments, the month and weekday names, and which day a week starts on. */\n locale?: string;\n /** Unavailable: dims the control, refuses the calendar, and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the date without allowing an edit: the field stays a Tab stop and stays submitted, and the calendar button is dropped because nothing in the calendar could be chosen. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the date through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `DatePickerLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: six of the nine names replace English literals Reka UI\n * writes of its own accord, and a language is set once with\n * `provideLoomLabels`.\n */\n labels?: LabelOverrides<DatePickerLabels>;\n }>(),\n {\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning. `false` is a caller deciding this one control is fine,\n // enabled or editable even though its row is not; absent is a caller\n // saying nothing, and only `undefined` survives to mean that past Vue's\n // absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen date as ISO `YYYY-MM-DD`, or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\n}>();\n\n// Two slots, one prop. `text` is the name everywhere else in the library, and\n// there is no single `text` here because there is no single slot: the segment\n// names and the popover's chrome are separate vocabularies with separate\n// registry entries, and naming them apart is what keeps a reader of the\n// template able to see which one a binding came from. The one `labels` prop\n// feeds both — a getter returning the intersection satisfies either half, and\n// the keys the other half does not know are ignored by `useLabels`' own return\n// type.\nconst segmentText = useLabels(\"dateSegments\", DATE_SEGMENT_LABELS, () => props.labels);\nconst panelText = useLabels(\"calendarPanel\", CALENDAR_PANEL_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. `undefined` for the parts this slice does not\n// name — the `literal` separators, which Reka renders `aria-hidden` and which\n// must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return isDateSegmentPart(part) ? segmentText.value[part] : undefined;\n}\n\n// The companion to `segmentLabel`, and the reason it returns an object rather\n// than a string is in `emptySegmentValueText`: Reka's `aria-valuetext` is a\n// hard-coded \"Empty\" only while the segment holds nothing, so this replaces\n// that case and leaves the filled one — a number, and the locale's own month\n// name — exactly as Reka wrote it.\n//\n// The month is the exception. Reka writes `\"5 - May\"`, with the month name from\n// `Intl.DateTimeFormat` but the separator and order hard-coded. `filledMonth`\n// lets a host compose the string their language needs (Japanese `\"5月\"`,\n// Vietnamese `\"tháng 5\"`). The numeric month arrives from the parsed model value\n// rather than from the segment's displayed text, because `item.value` is the\n// locale-formatted display and may not contain a parseable number.\nfunction segmentValueText(\n part: string,\n value: string,\n monthValue?: number,\n): { \"aria-valuetext\"?: string } {\n const empty = emptySegmentValueText(part, value, segmentText.value.empty);\n if (empty[\"aria-valuetext\"]) return empty;\n if (part === \"month\" && monthValue !== undefined) {\n return {\n \"aria-valuetext\": segmentText.value.filledMonth({ value: monthValue, locale: props.locale }),\n };\n }\n return {};\n}\n\n// `class` sizes the whole control, so it lands on the anchor — the element the\n// caller's `max-w-xs` has to reach, and the one the popover measures itself\n// against. Every other fallthrough attribute goes one level in, onto the\n// `role=\"group\"` field: `aria-label`, `aria-labelledby` and `aria-describedby`\n// name and describe the thing being entered, and a group is what a screen\n// reader announces that name on entering. Putting them on the anchor instead\n// would name a layout div nothing ever lands in.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The date segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so the calendar trigger inside a disabled group went inert\n * while the segments beside it still typed a date on an arrow key. Measured in\n * jsdom: an empty `mm` segment became `8`.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst anchor = useTemplateRef<{ $el?: Element }>(\"anchor\");\nconst groupDisabled = useAncestorDisabled(() => anchor.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are read off `attrs` and handed in as this caller's own\n// values — a caller who sets either still beats the row, and the row's\n// description is merged into theirs rather than replacing it.\n//\n// `field.attrs` spreads onto `DatePickerField` **after** `fieldAttrs`, which is\n// the node those two were already landing on, and the position is the contract:\n// the bag has resolved the caller's own values already and drops every key that\n// resolved to nothing, so it can only add. Where each key ends up is Reka's\n// decision and worth knowing: `DateFieldRoot` declares `id` and `name` as props\n// and puts both on the visually-hidden `<input>` it submits the date through —\n// which is the element a `<label for>` can actually name, a `role=\"group\"` div\n// being unlabelable — while `aria-describedby`, `aria-required` and\n// `aria-invalid` are undeclared, fall through to `$attrs`, and land on the group\n// itself, where a screen reader announces them once on entering the field.\n//\n// The `:aria-invalid` binding that used to sit on the field is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // trigger button inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row. A caller who passes\n// `aria-labelledby` as a fallthrough attr wins (it reaches this control's own\n// `ariaLabelledby` through `useFieldControl`). A caller who passes `aria-label`\n// wins too — `aria-labelledby` overrides `aria-label` in ARIA, so the row's\n// label must not appear when the caller named the control themselves. Otherwise,\n// the row's label (published by Field as `labelledBy`) is the accessible name.\nconst groupLabelledBy = computed(() =>\n attrs[\"aria-label\"] || attrs[\"aria-labelledby\"] ? undefined : field.labelledBy,\n);\n\nfunction fromIso(value: string | undefined): DateValue | undefined {\n if (!value) return undefined;\n try {\n return parseDate(value);\n } catch {\n return undefined;\n }\n}\n\n// `toString()` already normalises to the Gregorian ISO form whatever calendar\n// the value carries — a locale such as `th-TH` makes Reka's own placeholder a\n// Buddhist date whose year reads 2569, and it still prints `2026-08-11`. What\n// `toCalendarDate` adds is dropping a time component before it can turn the\n// string into `2026-08-11T00:00:00`. `granularity=\"day\"` below is what makes\n// that unreachable today; this is what keeps it unreachable if it ever moves.\nfunction toIso(date: DateValue): string {\n return toCalendarDate(date).toString();\n}\n\n// `optional()` rather than three separate bindings: an absent bound forwarded\n// as an explicit `undefined` is still a key Reka sees, and an absent\n// `modelValue` forwarded that way is what turns an uncontrolled field into a\n// permanently empty one. Dropping the key says what is true.\nconst rootValue = computed(() =>\n optional({\n modelValue: fromIso(props.modelValue),\n minValue: fromIso(props.min),\n maxValue: fromIso(props.max),\n }),\n);\n\n// The panel's open state is owned here rather than left to Reka's\n// `closeOnSelect`, which closes by watching its *own* model value — and that\n// value only moves for a host that echoes the emitted date straight back down.\n// A host that validates before accepting one, or that binds nothing at all,\n// would pick a day and watch the calendar stay open over the field it just\n// filled. Closing on the choice itself does not depend on anyone answering.\nconst open = ref(false);\n\nfunction onDateChange(date: DateValue | undefined) {\n open.value = false;\n emit(\"update:modelValue\", date ? toIso(date) : undefined);\n}\n\n// Today is a state, and it may not be carried by the marker dot alone — a\n// reader who cannot see the dot gets `aria-current=\"date\"`, which is the one\n// attribute screen readers already announce for \"this is the current one\".\n// Reka publishes today only as a `data-` attribute and a slot prop, neither of\n// which can reach an attribute binding on the cell itself.\nfunction isDateToday(date: DateValue): boolean {\n return isToday(date, getLocalTimeZone());\n}\n\n// The field is one Tab stop, not three. Reka leaves every segment tabbable,\n// which turns a single date into three stops a keyboard user has to walk\n// through — and the control everyone already knows, the browser's own date\n// input, is one stop with the arrow keys moving inside it. So a roving\n// tabindex names exactly one segment as the stop, and Reka's own\n// ArrowLeft/ArrowRight handling (and its auto-advance after a filled segment)\n// moves between them by calling `focus()` directly, which ignores tabindex\n// entirely and so keeps working untouched.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // A disabled field has no stop at all; returning `undefined` removes the\n // attribute, which is exactly what Reka does for the same case. A read-only\n // one keeps every stop it had — the value is on show and a reader still\n // arrows across it to read it, which is the whole difference between the two\n // states.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a locale change takes it away —\n // a field with no tab stop is unreachable, and `en` has no era segment that\n // `ja-JP-u-ca-japanese` does.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// The month/year heading names the grid, so the two are wired together rather\n// than the grid being an unnamed table of numbers.\nconst headingId = useId();\n\nconst panel = ref<HTMLElement | null>(null);\n\n// Reka's focus scope would land on the first tabbable thing in the panel,\n// which is the previous-month button — so a keyboard user opening the calendar\n// arrives one Tab away from the grid and two from the day they came for.\n// Focus goes to the day the calendar is already sitting on instead: the\n// selected date, or today when nothing is selected, which is the cell Reka has\n// given the grid's own roving tabindex to.\nfunction onOpenAutoFocus(event: Event) {\n const day = panel.value?.querySelector<HTMLElement>(\n '[data-reka-calendar-cell-trigger][tabindex=\"0\"]',\n );\n if (!day) return;\n event.preventDefault();\n day.focus();\n}\n</script>\n\n<template>\n <DatePickerRoot\n v-model:open=\"open\"\n v-bind=\"rootValue\"\n :locale=\"locale\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n granularity=\"day\"\n prevent-deselect\n @update:model-value=\"onDateChange\"\n >\n <!-- The invalid marker lives out here, on the control as a whole, because\n the field group below already carries a `data-invalid` of Reka's own\n with a different meaning: that the *typed* date falls outside `min` and\n `max`. Two markers spelled the same on one element is how a style rule\n starts answering a question nobody asked it. -->\n <DatePickerAnchor\n ref=\"anchor\"\n :data-invalid=\"field.invalid || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :class=\"cn('block w-full', attrs.class as string)\"\n >\n <DatePickerField\n v-slot=\"{ segments }\"\n v-bind=\"{ ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :class=\"\n cn(\n // The text input's own height scale, so a date sitting in a form\n // row beside a text field lines up rather than nearly lining up.\n // No gap: the separator between two segments is itself a segment,\n // so spacing them apart writes the date as `3 / 14 / 2026`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring, exactly as TextField's wrapper does,\n // and the focused segment below carries a fill instead of a second\n // outline — a ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and every one of them belongs to this\n // group rather than to the segments inside it. A fill painted per\n // segment leaves the literal separators between them on\n // `background`, and the control comes out striped.\n //\n // Read-only keeps its focus ring and its full-strength text: a\n // read-only date is a value on show, not an unavailable control,\n // and the two must not look alike. The fill lifts off `background`\n // to `subtle`, which holds the segments at 13.46:1, and the rim\n // stays `input` because nothing about the field's reach has moved —\n // it is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this group carries the same state to assistive\n // tech, so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Unavailable is a *colour* and a *weight*, never an opacity.\n // `opacity-50` here faded the segments along with the box, and the\n // segments are the whole content of the control: `--color-foreground`\n // measures 14.09:1 on the resting fill and 2.99:1 once composited at\n // half alpha, with the literal separators and any placeholder\n // segment down at 2.02:1. The fill drains one step past read-only,\n // the text follows it to a measured 4.68:1, and the rim slackens\n // from `input` to `border` — so the two unavailable-looking states\n // part on three channels rather than on hue alone, which is what a\n // reader who cannot separate two greys has to have.\n //\n // The colour is set here rather than on each segment because no\n // segment declares one of its own in the resting state, so each\n // inherits this; `focus:text-primary-text` below is unreachable on a\n // control that cannot take focus.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class\n // it saw last, so a field that is both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- Every `:aria-label` from here down replaces one Reka writes in\n English inside its own render function. Each reaches the DOM node\n as a fallthrough attribute, which Vue merges last, so it is a\n replacement rather than a second name. Removing one does not fall\n back to nothing; it falls back to Reka's English, which is the\n failure the label seam exists to close and which no test of Loom's\n own strings would catch. -->\n <DatePickerInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"segmentValueText(item.part, item.value, fromIso(props.modelValue)?.month)\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </DatePickerInput>\n\n <!-- Dropped entirely while read-only rather than disabled beside a\n field that is plainly still alive. Reka's `readonly` reaches the\n calendar too and makes every cell's click a no-op, so the button\n would open a panel in which nothing can be chosen — a dead end that\n looks like a working control, which is worse than not offering it.\n The segments are still there to read the date off. -->\n <DatePickerTrigger\n v-if=\"!field.readonly\"\n :aria-label=\"panelText.openCalendar\"\n class=\"ml-auto inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-not-allowed\"\n >\n <CalendarDays class=\"h-4 w-4\" />\n </DatePickerTrigger>\n </DatePickerField>\n </DatePickerAnchor>\n\n <DatePickerContent\n align=\"start\"\n :side-offset=\"6\"\n :class=\"\n cn(\n 'z-overlay rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md outline-none',\n // Both halves of the motion are scoped to their own state, never\n // unconditional. Reka's Presence decides unmounting from the\n // computed animation-name: an unconditional entrance left the closed\n // panel computing a finished animation it would wait on forever — an\n // invisible layer over the page, eating clicks. Scoped, closing\n // swaps the computed animation to fade-fall's, and Presence holds\n // the panel exactly one exit long, releasing it at animationend.\n 'data-[state=open]:animate-fade-rise data-[state=closed]:animate-fade-fall',\n )\n \"\n @open-auto-focus=\"onOpenAutoFocus\"\n >\n <!-- The whole panel rises as one piece. The 42 cells deliberately carry\n no stagger of their own: `listStaggerDelay` is the vocabulary for a\n list of rows arriving in reading order, and a grid rippling in cell\n by cell reads as a rendering glitch rather than as an entrance. -->\n <div ref=\"panel\" class=\"flex flex-col gap-3\">\n <!-- `calendarLabel` is a real Reka prop rather than a literal to\n override, and it defaults to `\"Event Date\"` — a name for somebody\n else's application. It is published twice: as the calendar\n container's `aria-label`, and inside a visually-hidden live region\n Reka renders itself, which a fallthrough attribute could not have\n reached. -->\n <DatePickerCalendar v-slot=\"{ grid, weekDays }\" :calendar-label=\"panelText.calendar\">\n <DatePickerHeader class=\"flex items-center justify-between gap-2\">\n <!-- The two pagers keep `data-[disabled]:opacity-50` where the\n field and the cells gave it up, and the difference is what is\n inside them: a chevron and nothing else. Fading a glyph costs a\n reader nothing, and the buttons carry their name in\n `aria-label` rather than in dimmed text. -->\n <DatePickerPrev\n :aria-label=\"panelText.previousMonth\"\n class=\"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50\"\n >\n <ChevronLeft class=\"h-4 w-4\" />\n </DatePickerPrev>\n\n <!-- Paging the calendar changes nothing a reader is looking at\n except this line, and nothing moves focus — so without the\n live region the month silently becomes a different month. -->\n <DatePickerHeading\n :id=\"headingId\"\n aria-live=\"polite\"\n class=\"text-sm font-medium text-foreground\"\n />\n\n <DatePickerNext\n :aria-label=\"panelText.nextMonth\"\n class=\"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50\"\n >\n <ChevronRight class=\"h-4 w-4\" />\n </DatePickerNext>\n </DatePickerHeader>\n\n <DatePickerGrid\n v-for=\"month in grid\"\n :key=\"month.value.toString()\"\n role=\"grid\"\n :aria-labelledby=\"headingId\"\n class=\"border-collapse select-none\"\n >\n <!-- Reka renders `role=\"application\"` here, which hands every key\n straight to the page and takes the table's own semantics away\n with it. A calendar is a grid: `role=\"grid\"` is what makes the\n cells rows and columns to a screen reader rather than 42\n unrelated buttons, and the arrow-key handling Reka installs on\n the cells is what a grid promises anyway.\n\n The rows and cells below are laid out by the table itself\n rather than by flex, and that is load-bearing: `display: flex`\n on a `tr` drops its implicit `row` mapping in Chromium and\n WebKit, which leaves `role=\"gridcell\"` inside a grid with no\n row between them — a broken grid that looks identical. -->\n <DatePickerGridHead>\n <DatePickerGridRow>\n <DatePickerHeadCell\n v-for=\"(day, index) in weekDays\"\n :key=\"index\"\n class=\"w-9 text-xs font-normal text-muted-foreground\"\n >\n {{ day }}\n </DatePickerHeadCell>\n </DatePickerGridRow>\n </DatePickerGridHead>\n\n <DatePickerGridBody>\n <DatePickerGridRow v-for=\"(week, weekIndex) in month.rows\" :key=\"weekIndex\">\n <DatePickerCell\n v-for=\"day in week\"\n :key=\"day.toString()\"\n :date=\"day\"\n class=\"p-0 text-center\"\n >\n <!-- A day out of `min`/`max` range is drawn in colour rather\n than faded: the number is the cell's whole content, and\n `opacity-50` took `--color-foreground` from 14.09:1 on\n the popover to 3.13:1. Muted is 5.76:1 and the rule is\n written as `:not([data-selected])` so it can never order\n against the selected fill's own text colour — the same\n trick the range pickers use on their four competing\n fills. The strike is the second, hueless cue, and it is\n what keeps an unavailable day apart from an adjacent\n month's, which is muted too and still selectable. -->\n <DatePickerCellTrigger\n v-slot=\"{ dayValue, today }\"\n :day=\"day\"\n :month=\"month.value\"\n :aria-current=\"isDateToday(day) ? 'date' : undefined\"\n class=\"group relative inline-flex h-9 w-9 items-center justify-center rounded-sm text-sm text-foreground transition-colors duration-fast ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo data-[outside-view]:text-muted-foreground data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[disabled]:pointer-events-none data-[disabled]:line-through [&[data-disabled]:not([data-selected])]:text-muted-foreground\"\n >\n {{ dayValue }}\n <!-- Today's second cue. The dot is decoration to a screen\n reader — `aria-current` on the cell is what it reads —\n but it is the whole cue for a reader who is looking at\n the grid, and it is a shape rather than a hue so it\n survives a colour deficiency and forced-colors both. -->\n <span\n v-if=\"today\"\n aria-hidden=\"true\"\n class=\"absolute bottom-1 h-1 w-1 rounded-full bg-primary group-data-[selected]:bg-primary-foreground\"\n />\n </DatePickerCellTrigger>\n </DatePickerCell>\n </DatePickerGridRow>\n </DatePickerGridBody>\n </DatePickerGrid>\n </DatePickerCalendar>\n </div>\n </DatePickerContent>\n </DatePickerRoot>\n</template>\n"],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type LabelOverrides } from "
|
|
2
|
-
import type { CalendarPanelLabels, DateSegmentLabels } from "
|
|
1
|
+
import { type LabelOverrides } from "../../../labels/src/index";
|
|
2
|
+
import type { CalendarPanelLabels, DateSegmentLabels } from "../../../labels/src/index";
|
|
3
3
|
/**
|
|
4
4
|
* Everything this control publishes to assistive technology, which is two
|
|
5
5
|
* shared slices and nothing of its own: the names of the field's segments and
|
package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -189,7 +189,7 @@ var DatePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
|
|
|
189
189
|
]), createVNode(unref(DatePickerContent), {
|
|
190
190
|
align: "start",
|
|
191
191
|
"side-offset": 6,
|
|
192
|
-
class: normalizeClass(unref(cn)("z-
|
|
192
|
+
class: normalizeClass(unref(cn)("z-overlay rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md outline-none", "data-[state=open]:animate-fade-rise data-[state=closed]:animate-fade-fall")),
|
|
193
193
|
onOpenAutoFocus
|
|
194
194
|
}, {
|
|
195
195
|
default: withCtx(() => [createElementVNode("div", {
|