@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { cn } from "../../../core/src/cn.js";
|
|
2
|
+
import "../../../core/src/index.js";
|
|
3
|
+
import { useLabels } from "../../../labels/src/labels.js";
|
|
4
|
+
import "../../../labels/src/index.js";
|
|
5
|
+
import { TABLE_LABELS } from "./Table.vue_vue_type_script_setup_true_lang.js";
|
|
6
|
+
import "./Table.js";
|
|
7
|
+
import { computed, createBlock, createElementBlock, createElementVNode, defineComponent, normalizeClass, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, unref } from "vue";
|
|
8
|
+
import { ChevronDown, ChevronUp, ChevronsUpDown } from "@lucide/vue";
|
|
9
|
+
//#region packages/primitives/table/src/TableHead.vue?vue&type=script&setup=true&lang.ts
|
|
10
|
+
var _hoisted_1 = ["aria-sort"];
|
|
11
|
+
var _hoisted_2 = { class: "group inline-flex items-center gap-1 font-semibold" };
|
|
12
|
+
var _hoisted_3 = { class: "sr-only" };
|
|
13
|
+
/** Alignment vocabulary shared with `TableCell`. */
|
|
14
|
+
var ALIGN_CLASS = {
|
|
15
|
+
left: "text-left",
|
|
16
|
+
center: "text-center",
|
|
17
|
+
right: "text-right [font-variant-numeric:tabular-nums]"
|
|
18
|
+
};
|
|
19
|
+
var headAlignClass = (align) => align ? ALIGN_CLASS[align] : "";
|
|
20
|
+
/** Cycle order: nothing → ascending → descending → nothing. */
|
|
21
|
+
var nextSort = (current) => current === void 0 ? "asc" : current === "asc" ? "desc" : void 0;
|
|
22
|
+
var TableHead_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
23
|
+
__name: "TableHead",
|
|
24
|
+
props: {
|
|
25
|
+
align: {},
|
|
26
|
+
sortable: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: false
|
|
29
|
+
},
|
|
30
|
+
sort: {},
|
|
31
|
+
labels: {}
|
|
32
|
+
},
|
|
33
|
+
emits: ["update:sort"],
|
|
34
|
+
setup(__props, { emit: __emit }) {
|
|
35
|
+
const props = __props;
|
|
36
|
+
const emit = __emit;
|
|
37
|
+
const text = useLabels("table", TABLE_LABELS, () => props.labels);
|
|
38
|
+
const inner = ref(void 0);
|
|
39
|
+
const current = computed(() => props.sort ?? inner.value);
|
|
40
|
+
function toggle() {
|
|
41
|
+
const next = nextSort(current.value);
|
|
42
|
+
inner.value = next;
|
|
43
|
+
emit("update:sort", next);
|
|
44
|
+
}
|
|
45
|
+
const SORT_ICON = {
|
|
46
|
+
asc: ChevronUp,
|
|
47
|
+
desc: ChevronDown
|
|
48
|
+
};
|
|
49
|
+
const sortIcon = computed(() => current.value ? SORT_ICON[current.value] : ChevronsUpDown);
|
|
50
|
+
const ARIA_SORT = {
|
|
51
|
+
asc: "ascending",
|
|
52
|
+
desc: "descending"
|
|
53
|
+
};
|
|
54
|
+
const ariaSort = computed(() => current.value ? ARIA_SORT[current.value] : void 0);
|
|
55
|
+
return (_ctx, _cache) => {
|
|
56
|
+
return openBlock(), createElementBlock("th", {
|
|
57
|
+
scope: "col",
|
|
58
|
+
"aria-sort": ariaSort.value,
|
|
59
|
+
class: normalizeClass(unref(cn)(headAlignClass(__props.align)))
|
|
60
|
+
}, [__props.sortable ? (openBlock(), createElementBlock("button", {
|
|
61
|
+
key: 0,
|
|
62
|
+
type: "button",
|
|
63
|
+
class: "inline-flex min-h-6 items-center gap-1 rounded-sm px-1 transition-colors duration-fast ease-out hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo",
|
|
64
|
+
onClick: _cache[0] || (_cache[0] = ($event) => toggle())
|
|
65
|
+
}, [
|
|
66
|
+
createElementVNode("span", _hoisted_2, [renderSlot(_ctx.$slots, "default")]),
|
|
67
|
+
(openBlock(), createBlock(resolveDynamicComponent(sortIcon.value), {
|
|
68
|
+
class: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
|
|
69
|
+
"aria-hidden": "true"
|
|
70
|
+
})),
|
|
71
|
+
createElementVNode("span", _hoisted_3, toDisplayString(current.value === "asc" ? unref(text).sortedAscending : current.value === "desc" ? unref(text).sortedDescending : unref(text).sort), 1)
|
|
72
|
+
])) : renderSlot(_ctx.$slots, "default", {}, void 0, void 0, 1)], 10, _hoisted_1);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
//#endregion
|
|
77
|
+
export { TableHead_vue_vue_type_script_setup_true_lang_default as default, headAlignClass, nextSort };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=TableHead.vue_vue_type_script_setup_true_lang.js.map
|
package/dist/packages/primitives/table/src/TableHead.vue_vue_type_script_setup_true_lang.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableHead.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/table/src/TableHead.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TableAlign } from \"./Table.vue\";\n\n/**\n * Sort state for a sortable header. Tri-state by omission: a sortable column\n * that is not the active sort simply has no direction, which is what\n * `aria-sort`'s absent-attribute form expresses.\n */\nexport type TableSort = \"asc\" | \"desc\";\n\n/** Alignment vocabulary shared with `TableCell`. */\nconst ALIGN_CLASS: Record<TableAlign, string> = {\n left: \"text-left\",\n center: \"text-center\",\n // Numeric columns are read down, not across; tabular digits keep the\n // places lined up while the eye travels.\n right: \"text-right [font-variant-numeric:tabular-nums]\",\n};\n\nexport const headAlignClass = (align: TableAlign | undefined): string =>\n align ? ALIGN_CLASS[align] : \"\";\n\n/** Cycle order: nothing → ascending → descending → nothing. */\nexport const nextSort = (current: TableSort | undefined): TableSort | undefined =>\n current === undefined ? \"asc\" : current === \"asc\" ? \"desc\" : undefined;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { ChevronDown, ChevronsUpDown, ChevronUp } from \"@lucide/vue\";\nimport { useLabels, type LabelOverrides, type TableLabels } from \"@ecoma-io/loom-labels\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { TABLE_LABELS } from \"./Table.vue\";\n\nconst props = withDefaults(\n defineProps<{\n /** Text alignment; numeric columns want `\"right\"`. */\n align?: TableAlign;\n /**\n * Renders the header as a sort control cycling asc → desc → none.\n * State is the host's through `v-model:sort`; omit the binding and the\n * header cycles on its own while still reporting each change.\n */\n sortable?: boolean;\n /** The current direction when sortable. Omit it for an uncontrolled cycle. */\n sort?: TableSort | undefined;\n /** Names what the control says on its own account, as any subset of `TableLabels`. */\n labels?: LabelOverrides<TableLabels>;\n }>(),\n { sortable: false },\n);\n\nconst emit = defineEmits<{ \"update:sort\": [value: TableSort | undefined] }>();\n\n// `text`, not `labels`: one of three sources, resolved key by key.\nconst text = useLabels(\"table\", TABLE_LABELS, () => props.labels);\n\n// Uncontrolled fallback lives here so a host can write plain `sortable`.\nconst inner = ref<TableSort | undefined>(undefined);\nconst current = computed(() => props.sort ?? inner.value);\n\nfunction toggle(): void {\n const next = nextSort(current.value);\n inner.value = next;\n emit(\"update:sort\", next);\n}\n\nconst SORT_ICON = { asc: ChevronUp, desc: ChevronDown } as const;\nconst sortIcon = computed(() => (current.value ? SORT_ICON[current.value] : ChevronsUpDown));\n\n// aria-sort takes full ARIA tokens, not our compact union.\nconst ARIA_SORT = { asc: \"ascending\", desc: \"descending\" } as const;\nconst ariaSort = computed(() => (current.value ? ARIA_SORT[current.value] : undefined));\n</script>\n\n<template>\n <th scope=\"col\" :aria-sort=\"ariaSort\" :class=\"cn(headAlignClass(align))\">\n <!-- The sort control carries its state in words as well as glyphs — a\n chevron alone is colour/shape-only semantics. `min-h-6` is the WCAG\n 2.5.8 floor made explicit: a text-xs label alone sits under the 24px\n target every interactive control owes. -->\n <button\n v-if=\"sortable\"\n type=\"button\"\n class=\"inline-flex min-h-6 items-center gap-1 rounded-sm px-1 transition-colors duration-fast ease-out hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n @click=\"toggle()\"\n >\n <span class=\"group inline-flex items-center gap-1 font-semibold\">\n <slot />\n </span>\n <component\n :is=\"sortIcon\"\n class=\"h-3.5 w-3.5 shrink-0 text-muted-foreground\"\n aria-hidden=\"true\"\n />\n <span class=\"sr-only\">{{\n current === \"asc\"\n ? text.sortedAscending\n : current === \"desc\"\n ? text.sortedDescending\n : text.sort\n }}</span>\n </button>\n <template v-else><slot /></template>\n </th>\n</template>\n"],"mappings":";;;;;;;;;;;;;AAWA,IAAM,cAA0C;CAC9C,MAAM;CACN,QAAQ;CAGR,OAAO;AACT;AAEA,IAAa,kBAAkB,UAC7B,QAAQ,YAAY,SAAS;;AAG/B,IAAa,YAAY,YACvB,YAAY,KAAA,IAAY,QAAQ,YAAY,QAAQ,SAAS,KAAA;;;;;;;;;;;;;;EAU/D,MAAM,QAAQ;EAkBd,MAAM,OAAO;EAGb,MAAM,OAAO,UAAU,SAAS,oBAAoB,MAAM,MAAM;EAGhE,MAAM,QAAQ,IAA2B,KAAA,CAAS;EAClD,MAAM,UAAU,eAAe,MAAM,QAAQ,MAAM,KAAK;EAExD,SAAS,SAAe;GACtB,MAAM,OAAO,SAAS,QAAQ,KAAK;GACnC,MAAM,QAAQ;GACd,KAAK,eAAe,IAAI;EAC1B;EAEA,MAAM,YAAY;GAAE,KAAK;GAAW,MAAM;EAAY;EACtD,MAAM,WAAW,eAAgB,QAAQ,QAAQ,UAAU,QAAQ,SAAS,cAAe;EAG3F,MAAM,YAAY;GAAE,KAAK;GAAa,MAAM;EAAa;EACzD,MAAM,WAAW,eAAgB,QAAQ,QAAQ,UAAU,QAAQ,SAAS,KAAA,CAAU;;GAIpF,OAAA,UAAA,GAAA,mBA4BK,MAAA;IA5BD,OAAM;IAAO,aAAW,SAAA;IAAW,OAAK,eAAE,MAAA,EAAA,CAAE,CAAC,eAAe,QAAA,KAAK,CAAA,CAAA;GAM3D,GAAA,CAAA,QAAA,YADR,UAAA,GAAA,mBAqBS,UAAA;;IAnBP,MAAK;IACL,OAAM;IACL,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,OAAM;;IAEd,mBAEO,QAFP,YAEO,CADL,WAAQ,KAAA,QAAA,SAAA,CAAA,CAAA;KAEV,UAAA,GAAA,YAIE,wBAHK,SAAA,KAAQ,GAAA;KACb,OAAM;KACN,eAAY;;IAEd,mBAMS,QANT,YAMS,gBALP,QAAA,UAAO,QAAuB,MAAA,IAAA,CAAI,CAAC,kBAA4B,QAAA,UAAO,SAA0B,MAAA,IAAA,CAAI,CAAC,mBAA+B,MAAA,IAAA,CAAI,CAAC,IAAI,GAAA,CAAA;GAOhI,CAAA,KAAA,WAAQ,KAAA,QAAA,WAAA,CAAA,GAAA,KAAA,GAAA,KAAA,GAAA,CAAA,CAAA,GAAA,IAAA,UAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import TableRow_vue_vue_type_script_setup_true_lang_default, { tableRowVariants } from "./TableRow.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
//#region packages/primitives/table/src/TableRow.vue
|
|
3
|
+
var TableRow_default = TableRow_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { TableRow_default as default, tableRowVariants };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=TableRow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableRow.js","names":[],"sources":["../../../../../packages/primitives/table/src/TableRow.vue"],"sourcesContent":["<script lang=\"ts\">\nimport { cva } from \"class-variance-authority\";\n\n/** Row emphasis. `selected` and `interactive` compose — a picked row that is\n * also clickable reads as both. */\nexport type TableRowState = \"selected\" | \"interactive\" | \"disabled\";\n\nexport const tableRowVariants = cva(\"transition-colors duration-fast ease-out\", {\n variants: {\n state: {\n // Hover is a promise: only an interactive row may answer the pointer.\n interactive:\n \"cursor-pointer hover:bg-subtle/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring focus-visible:bg-subtle/60\",\n selected: \"bg-primary-muted/60\",\n disabled: \"text-muted-foreground\",\n none: \"\",\n },\n },\n});\n</script>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\n\nconst props = withDefaults(\n defineProps<{\n /** Hover/press language plus keyboard activation; the host owns what activation MEANS via `@activate`. */\n interactive?: boolean;\n /** Marks the row as the current choice among its peers (`aria-selected`). */\n selected?: boolean;\n /** Unavailable rather than hidden: muted, inert, still announced. */\n disabled?: boolean;\n }>(),\n { interactive: false, selected: false, disabled: false },\n);\n\nconst emit = defineEmits<{ activate: [] }>();\n\n// A row is not a button, so it cannot lean on native activation. Clicks\n// coming from real controls inside a cell (RowActions, links) belong to\n// those controls — bubbling them up here would fire the row's activation\n// behind the control's back.\nfunction onActivateClick(event: MouseEvent): void {\n if (!props.interactive || props.disabled) return;\n const target = event.target;\n if (\n target instanceof Element &&\n target.closest(\"a, button, input, select, textarea, label, [role='button']\")\n )\n return;\n emit(\"activate\");\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (!props.interactive || props.disabled) return;\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n emit(\"activate\");\n }\n}\n\nconst state = computed(() =>\n props.disabled\n ? \"disabled\"\n : props.interactive\n ? \"interactive\"\n : props.selected\n ? \"selected\"\n : \"none\",\n);\n</script>\n\n<template>\n <tr\n :class=\"\n cn(\n tableRowVariants({ state }),\n // Selected rides above interactive's hover wash, not under it.\n selected && !disabled && 'bg-primary-muted/60',\n )\n \"\n :aria-selected=\"interactive || selected ? selected : undefined\"\n :tabindex=\"interactive && !disabled ? 0 : undefined\"\n @click=\"onActivateClick\"\n @keydown=\"onKeydown\"\n >\n <!-- @slot The row's cells (`TableCell`, or `TableHead` inside `<thead>`). -->\n <slot />\n </tr>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Row emphasis. `selected` and `interactive` compose — a picked row that is
|
|
2
|
+
* also clickable reads as both. */
|
|
3
|
+
export type TableRowState = "selected" | "interactive" | "disabled";
|
|
4
|
+
export declare const tableRowVariants: (props?: ({
|
|
5
|
+
state?: "disabled" | "none" | "interactive" | "selected" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
9
|
+
declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
|
|
10
|
+
/** Hover/press language plus keyboard activation; the host owns what activation MEANS via `@activate`. */
|
|
11
|
+
interactive?: boolean;
|
|
12
|
+
/** Marks the row as the current choice among its peers (`aria-selected`). */
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
/** Unavailable rather than hidden: muted, inert, still announced. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
activate: () => any;
|
|
18
|
+
}, string, import("vue").PublicProps, Readonly<{
|
|
19
|
+
/** Hover/press language plus keyboard activation; the host owns what activation MEANS via `@activate`. */
|
|
20
|
+
interactive?: boolean;
|
|
21
|
+
/** Marks the row as the current choice among its peers (`aria-selected`). */
|
|
22
|
+
selected?: boolean;
|
|
23
|
+
/** Unavailable rather than hidden: muted, inert, still announced. */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}> & Readonly<{
|
|
26
|
+
onActivate?: () => any;
|
|
27
|
+
}>, {
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
interactive: boolean;
|
|
30
|
+
selected: boolean;
|
|
31
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
|
|
32
|
+
default?: (props: {}) => any;
|
|
33
|
+
}>;
|
|
34
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
35
|
+
new (): {
|
|
36
|
+
$slots: S;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=TableRow.vue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableRow.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/table/src/TableRow.vue"],"names":[],"mappings":"AAoGA;mCACmC;AACnC,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,CAAC;AAEpE,eAAO,MAAM,gBAAgB;;8EAW3B,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAEd,0GAA0G;kBAC5F,OAAO;IACrB,6EAA6E;eAClE,OAAO;IAClB,qEAAqE;eAC1D,OAAO;;;;IALlB,0GAA0G;kBAC5F,OAAO;IACrB,6EAA6E;eAClE,OAAO;IAClB,qEAAqE;eAC1D,OAAO;;;;cAAP,OAAO;iBAJJ,OAAO;cAEV,OAAO;;cAuFR,CAAC,KAAK,IAAgB,KAAK,GAAG;EAWxC,CAAC;AAWL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cn } from "../../../core/src/cn.js";
|
|
2
|
+
import "../../../core/src/index.js";
|
|
3
|
+
import { computed, createElementBlock, defineComponent, normalizeClass, openBlock, renderSlot, unref } from "vue";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
//#region packages/primitives/table/src/TableRow.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
+
var _hoisted_1 = ["aria-selected", "tabindex"];
|
|
7
|
+
var tableRowVariants = cva("transition-colors duration-fast ease-out", { variants: { state: {
|
|
8
|
+
interactive: "cursor-pointer hover:bg-subtle/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring focus-visible:bg-subtle/60",
|
|
9
|
+
selected: "bg-primary-muted/60",
|
|
10
|
+
disabled: "text-muted-foreground",
|
|
11
|
+
none: ""
|
|
12
|
+
} } });
|
|
13
|
+
var TableRow_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
14
|
+
__name: "TableRow",
|
|
15
|
+
props: {
|
|
16
|
+
interactive: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
default: false
|
|
19
|
+
},
|
|
20
|
+
selected: {
|
|
21
|
+
type: Boolean,
|
|
22
|
+
default: false
|
|
23
|
+
},
|
|
24
|
+
disabled: {
|
|
25
|
+
type: Boolean,
|
|
26
|
+
default: false
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
emits: ["activate"],
|
|
30
|
+
setup(__props, { emit: __emit }) {
|
|
31
|
+
const props = __props;
|
|
32
|
+
const emit = __emit;
|
|
33
|
+
function onActivateClick(event) {
|
|
34
|
+
if (!props.interactive || props.disabled) return;
|
|
35
|
+
const target = event.target;
|
|
36
|
+
if (target instanceof Element && target.closest("a, button, input, select, textarea, label, [role='button']")) return;
|
|
37
|
+
emit("activate");
|
|
38
|
+
}
|
|
39
|
+
function onKeydown(event) {
|
|
40
|
+
if (!props.interactive || props.disabled) return;
|
|
41
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
emit("activate");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const state = computed(() => props.disabled ? "disabled" : props.interactive ? "interactive" : props.selected ? "selected" : "none");
|
|
47
|
+
return (_ctx, _cache) => {
|
|
48
|
+
return openBlock(), createElementBlock("tr", {
|
|
49
|
+
class: normalizeClass(unref(cn)(unref(tableRowVariants)({ state: state.value }), __props.selected && !__props.disabled && "bg-primary-muted/60")),
|
|
50
|
+
"aria-selected": __props.interactive || __props.selected ? __props.selected : void 0,
|
|
51
|
+
tabindex: __props.interactive && !__props.disabled ? 0 : void 0,
|
|
52
|
+
onClick: onActivateClick,
|
|
53
|
+
onKeydown
|
|
54
|
+
}, [renderSlot(_ctx.$slots, "default")], 42, _hoisted_1);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
//#endregion
|
|
59
|
+
export { TableRow_vue_vue_type_script_setup_true_lang_default as default, tableRowVariants };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=TableRow.vue_vue_type_script_setup_true_lang.js.map
|
package/dist/packages/primitives/table/src/TableRow.vue_vue_type_script_setup_true_lang.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableRow.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/table/src/TableRow.vue"],"sourcesContent":["<script lang=\"ts\">\nimport { cva } from \"class-variance-authority\";\n\n/** Row emphasis. `selected` and `interactive` compose — a picked row that is\n * also clickable reads as both. */\nexport type TableRowState = \"selected\" | \"interactive\" | \"disabled\";\n\nexport const tableRowVariants = cva(\"transition-colors duration-fast ease-out\", {\n variants: {\n state: {\n // Hover is a promise: only an interactive row may answer the pointer.\n interactive:\n \"cursor-pointer hover:bg-subtle/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring focus-visible:bg-subtle/60\",\n selected: \"bg-primary-muted/60\",\n disabled: \"text-muted-foreground\",\n none: \"\",\n },\n },\n});\n</script>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\n\nconst props = withDefaults(\n defineProps<{\n /** Hover/press language plus keyboard activation; the host owns what activation MEANS via `@activate`. */\n interactive?: boolean;\n /** Marks the row as the current choice among its peers (`aria-selected`). */\n selected?: boolean;\n /** Unavailable rather than hidden: muted, inert, still announced. */\n disabled?: boolean;\n }>(),\n { interactive: false, selected: false, disabled: false },\n);\n\nconst emit = defineEmits<{ activate: [] }>();\n\n// A row is not a button, so it cannot lean on native activation. Clicks\n// coming from real controls inside a cell (RowActions, links) belong to\n// those controls — bubbling them up here would fire the row's activation\n// behind the control's back.\nfunction onActivateClick(event: MouseEvent): void {\n if (!props.interactive || props.disabled) return;\n const target = event.target;\n if (\n target instanceof Element &&\n target.closest(\"a, button, input, select, textarea, label, [role='button']\")\n )\n return;\n emit(\"activate\");\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (!props.interactive || props.disabled) return;\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n emit(\"activate\");\n }\n}\n\nconst state = computed(() =>\n props.disabled\n ? \"disabled\"\n : props.interactive\n ? \"interactive\"\n : props.selected\n ? \"selected\"\n : \"none\",\n);\n</script>\n\n<template>\n <tr\n :class=\"\n cn(\n tableRowVariants({ state }),\n // Selected rides above interactive's hover wash, not under it.\n selected && !disabled && 'bg-primary-muted/60',\n )\n \"\n :aria-selected=\"interactive || selected ? selected : undefined\"\n :tabindex=\"interactive && !disabled ? 0 : undefined\"\n @click=\"onActivateClick\"\n @keydown=\"onKeydown\"\n >\n <!-- @slot The row's cells (`TableCell`, or `TableHead` inside `<thead>`). -->\n <slot />\n </tr>\n</template>\n"],"mappings":";;;;;;AAOA,IAAa,mBAAmB,IAAI,4CAA4C,EAC9E,UAAU,EACR,OAAO;CAEL,aACE;CACF,UAAU;CACV,UAAU;CACV,MAAM;AACR,EACF,EACF,CAAC;;;;;;;;;;;;;;;;;;;EAOD,MAAM,QAAQ;EAYd,MAAM,OAAO;EAMb,SAAS,gBAAgB,OAAyB;GAChD,IAAI,CAAC,MAAM,eAAe,MAAM,UAAU;GAC1C,MAAM,SAAS,MAAM;GACrB,IACE,kBAAkB,WAClB,OAAO,QAAQ,4DAA4D,GAE3E;GACF,KAAK,UAAU;EACjB;EAEA,SAAS,UAAU,OAA4B;GAC7C,IAAI,CAAC,MAAM,eAAe,MAAM,UAAU;GAC1C,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;IAC9C,MAAM,eAAe;IACrB,KAAK,UAAU;GACjB;EACF;EAEA,MAAM,QAAQ,eACZ,MAAM,WACF,aACA,MAAM,cACJ,gBACA,MAAM,WACJ,aACA,MACV;;GAIE,OAAA,UAAA,GAAA,mBAeK,MAAA;IAdF,OAAK,eAAS,MAAA,EAAA,CAAE,CAAU,MAAA,gBAAA,CAAgB,CAAA,EAAA,OAAG,MAAA,MAAK,CAAA,GAAqF,QAAA,YAAQ,CAAK,QAAA,YAAQ,qBAAA,CAAA;IAO5J,iBAAe,QAAA,eAAe,QAAA,WAAW,QAAA,WAAW,KAAA;IACpD,UAAU,QAAA,eAAW,CAAK,QAAA,WAAQ,IAAO,KAAA;IACzC,SAAO;IACE;GAGV,GAAA,CAAA,WAAQ,KAAA,QAAA,SAAA,CAAA,GAAA,IAAA,UAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default } from "./Table.vue";
|
|
2
|
+
export { TABLE_LABELS } from "./Table.vue";
|
|
3
|
+
export { default as TableRow, tableRowVariants } from "./TableRow.vue";
|
|
4
|
+
export { default as TableHead, nextSort } from "./TableHead.vue";
|
|
5
|
+
export { default as TableCell } from "./TableCell.vue";
|
|
6
|
+
export type { TableLabels } from "../../../labels/src/index";
|
|
7
|
+
export type { TableDensity, TableAlign } from "./Table.vue";
|
|
8
|
+
export type { TableRowState } from "./TableRow.vue";
|
|
9
|
+
export type { TableSort } from "./TableHead.vue";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TABLE_LABELS } from "./Table.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
import Table_default from "./Table.js";
|
|
3
|
+
import { tableRowVariants } from "./TableRow.vue_vue_type_script_setup_true_lang.js";
|
|
4
|
+
import TableRow_default from "./TableRow.js";
|
|
5
|
+
import "./TableHead.vue_vue_type_script_setup_true_lang.js";
|
|
6
|
+
import TableHead_default from "./TableHead.js";
|
|
7
|
+
import TableCell_default from "./TableCell.js";
|
|
8
|
+
export { TABLE_LABELS, TableCell_default as TableCell, TableHead_default as TableHead, TableRow_default as TableRow, Table_default as default, tableRowVariants };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.js","names":[],"sources":["../../../../../packages/primitives/tabs/src/Tabs.vue"],"sourcesContent":["<script lang=\"ts\">\nexport interface TabItem {\n /** Selects this tab, and names the slot that carries its panel. */\n value: string;\n /** The trigger's visible text. */\n label: string;\n /** Skips this tab in pointer, click and roving-tabindex arrow navigation. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from \"reka-ui\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * Tabs — switch between panels/views, with the active panel's content shown\n * below. For picking one value of a setting with no panel underneath, reach\n * for SegmentedControl instead. Built on Reka UI's Tabs (roving tabindex,\n * arrow-key navigation, a11y sourced from the `tab`/`tabpanel` pattern).\n * Panels are supplied by the host via named slots keyed by each tab's\n * `value` — `<Tabs :tabs=\"tabs\"><template #overview>…</template></Tabs>`.\n */\ndefineProps<{\n /** The selected tab's value. Omit it and Tabs owns its own selection. */\n modelValue?: string;\n /** The ordered set of tabs; each entry needs a like-named slot for its panel. */\n tabs: TabItem[];\n}>();\n\ndefineEmits<{ \"update:modelValue\": [value: string] }>();\n</script>\n\n<template>\n <TabsRoot\n v-bind=\"optional({ modelValue })\"\n orientation=\"horizontal\"\n @update:model-value=\"$emit('update:modelValue', String($event))\"\n >\n <TabsList class=\"relative inline-flex items-center gap-1 border-b border-border\">\n <!-- A drained plate rather than a dim, and the trigger is the awkward\n case that makes the reason clear. Its whole content is the tab's\n label, so `disabled:opacity-50` took that label from 5.25:1 to\n 2.05:1 — but the label is *already* `text-muted-foreground` when the\n tab is merely unselected, so simply dropping the alpha would leave an\n unavailable tab painted exactly like an available one. The fill is\n what carries \"unavailable\" instead: `bg-muted` under the same\n measured text holds 4.67:1, and the plate is visible where a second\n grey would not have been. `rounded-t-sm` is the control radius less\n the list's own padding, and it shapes the focus outline too — the\n plate stops at the list's bottom rule rather than crossing it. -->\n <TabsTrigger\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n v-bind=\"optional({ disabled: tab.disabled })\"\n class=\"relative rounded-t-sm px-3 py-2 text-sm text-muted-foreground transition-colors duration-fast ease-out data-[state=active]:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:bg-muted disabled:text-muted-foreground\"\n >\n {{ tab.label }}\n </TabsTrigger>\n <TabsIndicator\n class=\"absolute bottom-0 left-0 h-0.5 bg-primary\"\n style=\"\n width: var(--reka-tabs-indicator-size);\n transform: translateX(var(--reka-tabs-indicator-position));\n transition:\n width var(--duration-fast) var(--ease-spring),\n transform var(--duration-fast) var(--ease-spring);\n \"\n />\n </TabsList>\n <TabsContent\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n class=\"animate-fade-rise pt-4 focus-visible:outline-
|
|
1
|
+
{"version":3,"file":"Tabs.js","names":[],"sources":["../../../../../packages/primitives/tabs/src/Tabs.vue"],"sourcesContent":["<script lang=\"ts\">\nexport interface TabItem {\n /** Selects this tab, and names the slot that carries its panel. */\n value: string;\n /** The trigger's visible text. */\n label: string;\n /** Skips this tab in pointer, click and roving-tabindex arrow navigation. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from \"reka-ui\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * Tabs — switch between panels/views, with the active panel's content shown\n * below. For picking one value of a setting with no panel underneath, reach\n * for SegmentedControl instead. Built on Reka UI's Tabs (roving tabindex,\n * arrow-key navigation, a11y sourced from the `tab`/`tabpanel` pattern).\n * Panels are supplied by the host via named slots keyed by each tab's\n * `value` — `<Tabs :tabs=\"tabs\"><template #overview>…</template></Tabs>`.\n *\n * The panels are real focus stops: Reka gives each `TabsContent`\n * `tabindex=\"0\"`, which is the ARIA tabs pattern's answer to \"how does a\n * keyboard reader get from the tab list to the content\" — Tab lands on the\n * active panel. An earlier `focus-visible:outline-none` here therefore\n * silenced a legitimate focus indicator (WCAG 2.4.7) rather than a cosmetic\n * one, so the panel wears the canonical ring like every other focusable\n * surface in the library.\n */\ndefineProps<{\n /** The selected tab's value. Omit it and Tabs owns its own selection. */\n modelValue?: string;\n /** The ordered set of tabs; each entry needs a like-named slot for its panel. */\n tabs: TabItem[];\n}>();\n\ndefineEmits<{ \"update:modelValue\": [value: string] }>();\n</script>\n\n<template>\n <TabsRoot\n v-bind=\"optional({ modelValue })\"\n orientation=\"horizontal\"\n @update:model-value=\"$emit('update:modelValue', String($event))\"\n >\n <TabsList class=\"relative inline-flex items-center gap-1 border-b border-border\">\n <!-- A drained plate rather than a dim, and the trigger is the awkward\n case that makes the reason clear. Its whole content is the tab's\n label, so `disabled:opacity-50` took that label from 5.25:1 to\n 2.05:1 — but the label is *already* `text-muted-foreground` when the\n tab is merely unselected, so simply dropping the alpha would leave an\n unavailable tab painted exactly like an available one. The fill is\n what carries \"unavailable\" instead: `bg-muted` under the same\n measured text holds 4.67:1, and the plate is visible where a second\n grey would not have been. `rounded-t-sm` is the control radius less\n the list's own padding, and it shapes the focus outline too — the\n plate stops at the list's bottom rule rather than crossing it. -->\n <TabsTrigger\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n v-bind=\"optional({ disabled: tab.disabled })\"\n class=\"relative rounded-t-sm px-3 py-2 text-sm text-muted-foreground transition-colors duration-fast ease-out data-[state=active]:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:bg-muted disabled:text-muted-foreground\"\n >\n {{ tab.label }}\n </TabsTrigger>\n <TabsIndicator\n class=\"absolute bottom-0 left-0 h-0.5 bg-primary\"\n style=\"\n width: var(--reka-tabs-indicator-size);\n transform: translateX(var(--reka-tabs-indicator-position));\n transition:\n width var(--duration-fast) var(--ease-spring),\n transform var(--duration-fast) var(--ease-spring);\n \"\n />\n </TabsList>\n <TabsContent\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n class=\"animate-fade-rise pt-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n >\n <slot :name=\"tab.value\" />\n </TabsContent>\n </TabsRoot>\n</template>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/tabs/src/Tabs.vue"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Tabs.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/tabs/src/Tabs.vue"],"names":[],"mappings":"AAgGA,MAAM,WAAW,OAAO;IACtB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;wBACoB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAkBhB,yEAAyE;iBAC5D,MAAM;IACnB,iFAAiF;UAC3E,OAAO,EAAE;;;;IAHf,yEAAyE;iBAC5D,MAAM;IACnB,iFAAiF;UAC3E,OAAO,EAAE;;;;iCA4KqD,GAAG;EAUrE,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
|
|
@@ -38,7 +38,7 @@ var Tabs_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
38
38
|
return openBlock(), createBlock(unref(TabsContent), {
|
|
39
39
|
key: tab.value,
|
|
40
40
|
value: tab.value,
|
|
41
|
-
class: "animate-fade-rise pt-4 focus-visible:outline-
|
|
41
|
+
class: "animate-fade-rise pt-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo"
|
|
42
42
|
}, {
|
|
43
43
|
default: withCtx(() => [renderSlot(_ctx.$slots, tab.value)]),
|
|
44
44
|
_: 2
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/tabs/src/Tabs.vue"],"sourcesContent":["<script lang=\"ts\">\nexport interface TabItem {\n /** Selects this tab, and names the slot that carries its panel. */\n value: string;\n /** The trigger's visible text. */\n label: string;\n /** Skips this tab in pointer, click and roving-tabindex arrow navigation. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from \"reka-ui\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * Tabs — switch between panels/views, with the active panel's content shown\n * below. For picking one value of a setting with no panel underneath, reach\n * for SegmentedControl instead. Built on Reka UI's Tabs (roving tabindex,\n * arrow-key navigation, a11y sourced from the `tab`/`tabpanel` pattern).\n * Panels are supplied by the host via named slots keyed by each tab's\n * `value` — `<Tabs :tabs=\"tabs\"><template #overview>…</template></Tabs>`.\n */\ndefineProps<{\n /** The selected tab's value. Omit it and Tabs owns its own selection. */\n modelValue?: string;\n /** The ordered set of tabs; each entry needs a like-named slot for its panel. */\n tabs: TabItem[];\n}>();\n\ndefineEmits<{ \"update:modelValue\": [value: string] }>();\n</script>\n\n<template>\n <TabsRoot\n v-bind=\"optional({ modelValue })\"\n orientation=\"horizontal\"\n @update:model-value=\"$emit('update:modelValue', String($event))\"\n >\n <TabsList class=\"relative inline-flex items-center gap-1 border-b border-border\">\n <!-- A drained plate rather than a dim, and the trigger is the awkward\n case that makes the reason clear. Its whole content is the tab's\n label, so `disabled:opacity-50` took that label from 5.25:1 to\n 2.05:1 — but the label is *already* `text-muted-foreground` when the\n tab is merely unselected, so simply dropping the alpha would leave an\n unavailable tab painted exactly like an available one. The fill is\n what carries \"unavailable\" instead: `bg-muted` under the same\n measured text holds 4.67:1, and the plate is visible where a second\n grey would not have been. `rounded-t-sm` is the control radius less\n the list's own padding, and it shapes the focus outline too — the\n plate stops at the list's bottom rule rather than crossing it. -->\n <TabsTrigger\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n v-bind=\"optional({ disabled: tab.disabled })\"\n class=\"relative rounded-t-sm px-3 py-2 text-sm text-muted-foreground transition-colors duration-fast ease-out data-[state=active]:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:bg-muted disabled:text-muted-foreground\"\n >\n {{ tab.label }}\n </TabsTrigger>\n <TabsIndicator\n class=\"absolute bottom-0 left-0 h-0.5 bg-primary\"\n style=\"\n width: var(--reka-tabs-indicator-size);\n transform: translateX(var(--reka-tabs-indicator-position));\n transition:\n width var(--duration-fast) var(--ease-spring),\n transform var(--duration-fast) var(--ease-spring);\n \"\n />\n </TabsList>\n <TabsContent\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n class=\"animate-fade-rise pt-4 focus-visible:outline-
|
|
1
|
+
{"version":3,"file":"Tabs.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/tabs/src/Tabs.vue"],"sourcesContent":["<script lang=\"ts\">\nexport interface TabItem {\n /** Selects this tab, and names the slot that carries its panel. */\n value: string;\n /** The trigger's visible text. */\n label: string;\n /** Skips this tab in pointer, click and roving-tabindex arrow navigation. */\n disabled?: boolean;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from \"reka-ui\";\nimport { optional } from \"@ecoma-io/loom-core\";\n\n/**\n * Tabs — switch between panels/views, with the active panel's content shown\n * below. For picking one value of a setting with no panel underneath, reach\n * for SegmentedControl instead. Built on Reka UI's Tabs (roving tabindex,\n * arrow-key navigation, a11y sourced from the `tab`/`tabpanel` pattern).\n * Panels are supplied by the host via named slots keyed by each tab's\n * `value` — `<Tabs :tabs=\"tabs\"><template #overview>…</template></Tabs>`.\n *\n * The panels are real focus stops: Reka gives each `TabsContent`\n * `tabindex=\"0\"`, which is the ARIA tabs pattern's answer to \"how does a\n * keyboard reader get from the tab list to the content\" — Tab lands on the\n * active panel. An earlier `focus-visible:outline-none` here therefore\n * silenced a legitimate focus indicator (WCAG 2.4.7) rather than a cosmetic\n * one, so the panel wears the canonical ring like every other focusable\n * surface in the library.\n */\ndefineProps<{\n /** The selected tab's value. Omit it and Tabs owns its own selection. */\n modelValue?: string;\n /** The ordered set of tabs; each entry needs a like-named slot for its panel. */\n tabs: TabItem[];\n}>();\n\ndefineEmits<{ \"update:modelValue\": [value: string] }>();\n</script>\n\n<template>\n <TabsRoot\n v-bind=\"optional({ modelValue })\"\n orientation=\"horizontal\"\n @update:model-value=\"$emit('update:modelValue', String($event))\"\n >\n <TabsList class=\"relative inline-flex items-center gap-1 border-b border-border\">\n <!-- A drained plate rather than a dim, and the trigger is the awkward\n case that makes the reason clear. Its whole content is the tab's\n label, so `disabled:opacity-50` took that label from 5.25:1 to\n 2.05:1 — but the label is *already* `text-muted-foreground` when the\n tab is merely unselected, so simply dropping the alpha would leave an\n unavailable tab painted exactly like an available one. The fill is\n what carries \"unavailable\" instead: `bg-muted` under the same\n measured text holds 4.67:1, and the plate is visible where a second\n grey would not have been. `rounded-t-sm` is the control radius less\n the list's own padding, and it shapes the focus outline too — the\n plate stops at the list's bottom rule rather than crossing it. -->\n <TabsTrigger\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n v-bind=\"optional({ disabled: tab.disabled })\"\n class=\"relative rounded-t-sm px-3 py-2 text-sm text-muted-foreground transition-colors duration-fast ease-out data-[state=active]:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:bg-muted disabled:text-muted-foreground\"\n >\n {{ tab.label }}\n </TabsTrigger>\n <TabsIndicator\n class=\"absolute bottom-0 left-0 h-0.5 bg-primary\"\n style=\"\n width: var(--reka-tabs-indicator-size);\n transform: translateX(var(--reka-tabs-indicator-position));\n transition:\n width var(--duration-fast) var(--ease-spring),\n transform var(--duration-fast) var(--ease-spring);\n \"\n />\n </TabsList>\n <TabsContent\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n :value=\"tab.value\"\n class=\"animate-fade-rise pt-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n >\n <slot :name=\"tab.value\" />\n </TabsContent>\n </TabsRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;GA0CE,OAAA,UAAA,GAAA,YA6CW,MAAA,QAAA,GA7CX,WACU,MA4CC,QAAA,CA5CO,CAAA,EAAA,YAAG,QAAA,WAAU,CAAA,GAAA;IAC7B,aAAY;IACX,uBAAkB,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,qBAAsB,OAAO,MAAM,CAAA;;IAE7D,SAAA,cA+BW,CA/BX,YA+BW,MAAA,QAAA,GAAA,EA/BD,OAAM,iEAAgE,GAAA;KAa5E,SAAA,cAAmB,EADrB,UAAA,IAAA,GAAA,mBAQc,UAAA,MAAA,WAPE,QAAA,OAAP,QAAG;MADZ,OAAA,UAAA,GAAA,YAQc,MAAA,WAAA,GARd,WAQc;OANX,KAAK,IAAI;OACT,OAAO,IAAI;MACJ,GAAA,EAAA,SAAA,KAAA,GAAA,MAAA,QAAA,CAAQ,CAAA,EAAA,UAAa,IAAI,SAAQ,CAAA,GAAA,EACzC,OAAM,6SAA4S,CAAA,GAAA;OAElT,SAAA,cAAe,CAAZ,gBAAA,gBAAA,IAAI,KAAK,GAAA,CAAA,CAAA,CAAA;;;KAEd,CAAA,GAAA,GAAA,IAAA,YASE,MAAA,aAAA,GAAA;MARA,OAAM;MACN,OAAA;OAAA,SAAA;OAAA,aAAA;OAAA,cAAA;MAAA;;;IASJ,CAAA,IAAA,UAAA,IAAA,GAAA,mBAOc,UAAA,MAAA,WANE,QAAA,OAAP,QAAG;KADZ,OAAA,UAAA,GAAA,YAOc,MAAA,WAAA,GAAA;MALX,KAAK,IAAI;MACT,OAAO,IAAI;MACZ,OAAM;;MAEN,SAAA,cAA0B,CAA1B,WAA0B,KAAA,QAAb,IAAI,KAAK,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TagsInput.js","names":[],"sources":["../../../../../packages/primitives/tags-input/src/TagsInput.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type {\n TagsInputLabels,\n TagsInputRejectReason,\n TagsInputRejection,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * Every refusal names the value, because a reader looking at eight tokens\n * cannot tell which \"that one\" is.\n *\n * Loom's English does not name the limit, though `rejected` is handed it: a\n * `max` refusal only ever happens when a limit is set, so a sentence that also\n * had to word the limitless case would be a branch nothing can reach. The\n * number is there for a host that wants \"only 3 can be added\" — and the count\n * description already reads \"3 of 3 tags\" on focus either way.\n */\nfunction describeRejection(rejection: TagsInputRejection): string {\n switch (rejection.reason) {\n case \"duplicate\":\n return `${rejection.value} has already been added.`;\n case \"max\":\n return `${rejection.value} was not added — the field is full.`;\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 * rather than a transcription of it.\n */\nexport const TAGS_INPUT_LABELS: TagsInputLabels = {\n remove: ({ value }) => `Remove ${value}`,\n count: ({ count, max }) =>\n max === undefined\n ? `${String(count)} ${count === 1 ? \"tag\" : \"tags\"}`\n : `${String(count)} of ${String(max)} tags`,\n rejected: ({ rejections }) => rejections.map(describeRejection).join(\" \"),\n clear: \"Clear all tags\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport {\n TagsInputRoot,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemText,\n TagsInputClear,\n} from \"reka-ui\";\nimport { TriangleAlert, X } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TagsInput — several values typed into one field, each committed to a\n * removable token: keywords on an article, recipients on a message, labels on\n * an issue.\n *\n * The line against its neighbours is what the values *are*. A\n * [Select](../Select/Select.vue) or a [Combobox](../Combobox/Combobox.vue)\n * picks from a list somebody else wrote, and cannot produce a value that is not\n * already in it; this field's whole point is that the reader invents the\n * values. A [TextField](../TextField/TextField.vue) holds one value, and a\n * comma-separated string in one is a list the application has to parse and the\n * reader has to punctuate correctly. Reach for\n * [Chip](@ecoma-io/loom-chip) alone when the tokens are a display of something\n * already decided — a chip row nobody types into is not a field.\n *\n * Each token *is* a Chip, in the removable shape Chip already ships, so a token\n * here and a chip elsewhere in the same form are the same object. The remove\n * control is Chip's own rather than Reka's `TagsInputItemDelete`, which is\n * `tabindex=\"-1\"` by design: Reka pairs it with an arrow-key selection that\n * moves no focus and sets no `aria-activedescendant`, so the token it\n * highlights is announced to nobody. Loom keeps the buttons on the tab order\n * and replaces the selection outright — see `onKeydownCapture`.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * the hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`. Every prop below still wins\n * over the row when it is set, in both directions — which is why the four\n * booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states here as everywhere else, and\n * the difference is larger than usual because there are two things to freeze. A\n * read-only field shows its tokens, refuses to grow one and drops every remove\n * control, while staying a tab stop and staying in the submitted data. A\n * disabled field is unavailable: not a tab stop, not submitted, and its tokens\n * drain to the neutral fill.\n *\n * That makes **three** resting appearances rather than two, and each is three\n * changes rather than one — fill, text colour and border weight together. See\n * the box's class list for why one channel is not enough.\n */\nconst props = withDefaults(\n defineProps<{\n /** The committed tokens. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string[] | undefined;\n /** Text shown in the empty text box. It is hidden once a token exists, where it would sit beside the tokens claiming the field is empty. */\n placeholder?: string;\n /** The most tokens the field will hold. Anything past it is refused and reported rather than dropped. */\n max?: number;\n /** Whether the same value may be committed twice. Off, so a repeated value is refused and says so. */\n duplicates?: boolean;\n /** What commits a token besides Enter. Typing it, or pasting text containing it, splits at it. */\n delimiter?: string;\n /** Renders a control that empties the field in one press. */\n clearable?: boolean;\n /** Disables the field: no tab stop, no editing, no remove controls, nothing submitted. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the tokens without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/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`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post. Each token is submitted as `name[0]`, `name[1]`, … Unset defers to a wrapping Field. */\n name?: string;\n /** The accessible name of the text box, when no visible element already labels it. */\n ariaLabel?: string;\n /** The id of the visible element that labels the text box (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TagsInputLabels` — 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: the case it exists for is a field whose tokens are people,\n * where \"Remove Ana Duarte\" reads and \"Remove tag\" does not.\n */\n labels?: LabelOverrides<TagsInputLabels>;\n }>(),\n {\n // Not `[]`. An empty array is a concrete value, so it would say \"the host\n // owns this list\", and a host that binds nothing would watch every token it\n // typed vanish on the next render. `undefined` is what leaves the control\n // managing its own — the same distinction `src/lib/props.ts` exists to\n // preserve for the Reka-backed primitives.\n modelValue: undefined,\n duplicates: false,\n delimiter: \",\",\n clearable: false,\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that past\n // Vue's absent-Boolean-casts-to-false rule. TextField.vue carries the full\n // reasoning.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The full token list after the change, trimmed and de-duplicated. Never a partial list. */\n \"update:modelValue\": [values: string[]];\n /** Every value refused by this one interaction, each with the rule that refused it. Fires once per interaction, not once per value. */\n reject: [rejections: TagsInputRejection[]];\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(\"tagsInput\", TAGS_INPUT_LABELS, () => props.labels);\n\n// `class` sizes the whole control and lands on the outer element; everything\n// else a caller passes — `id`, `data-testid`, `aria-describedby`,\n// `autocomplete` — describes the real `<input>` and lands there. The outer\n// element is not a form control and the input is not the box a caller lays out,\n// so there is no one node both belong on.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\nconst generatedId = useId();\nconst countId = useId();\nconst messageId = useId();\n\nconst inputRef = useTemplateRef<{ $el: HTMLInputElement }>(\"input\");\n\n/**\n * The committed tokens, and the reason this control keeps its own copy rather\n * than forwarding `modelValue` into Reka and letting it manage the state.\n *\n * Loom applies four rules Reka has no notion of — trim, drop the blanks a paste\n * leaves behind, refuse a duplicate, refuse anything past `max` — so Reka is\n * configured to accept everything (`duplicate`, no `max`) and *propose*, and\n * `onProposed` decides. That needs a value flowing back down, which means a\n * `:model-value` that is always defined and always ours.\n *\n * It is `useVModel(…, { passive: true })` written out, and behaves the same way\n * Reka's own would: the change is taken locally and emitted, and a controlled\n * host that declines it is honoured on the next value it does send. Seeded from\n * the prop rather than sanitised, because a list the host already holds is the\n * host's to hold — the rules above are about what this field *accepts*.\n */\nconst tokens = ref<string[]>([...(props.modelValue ?? [])]);\nwatch(\n () => props.modelValue,\n (next) => {\n if (next !== undefined) tokens.value = [...next];\n },\n { deep: true },\n);\n\nconst rejections = ref<TagsInputRejection[]>([]);\nconst message = computed(() =>\n rejections.value.length > 0\n ? text.value.rejected({ rejections: rejections.value, max: props.max })\n : \"\",\n);\n\n// The refusal message reads first and the count second: both describe the\n// field, and the one that just changed is the more specific thing to say. A\n// wrapping Field's own hint or error is merged in after both by\n// `useFieldControl`, never in place of them.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [\n message.value.length > 0 ? messageId : undefined,\n countId,\n attrs[\"aria-describedby\"],\n ]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// The `<label for>` a Field renders has to resolve to the text box, and a\n// caller passing their own id must not end up with a label pointing at nothing.\nconst inputId = computed(() => field.id ?? generatedId);\n\n/**\n * A stable key per token, since the token itself cannot be one: `duplicates`\n * makes two identical strings legal and Vue warns on the collision.\n *\n * The n-th occurrence of a value keys as `n:value`, so removing one of two\n * identical tokens unmounts exactly one node and the survivor keeps the other —\n * indistinguishable, because the two say the same thing. Keying by index\n * instead would re-key every row after the one removed, which patches text into\n * nodes that belong to other tokens.\n */\nconst rows = computed(() => {\n const seen = new Map<string, number>();\n return tokens.value.map((value) => {\n const ordinal = seen.get(value) ?? 0;\n seen.set(value, ordinal + 1);\n return { value, key: `${String(ordinal)}:${value}` };\n });\n});\n\n/**\n * Every rule this field applies to a value, in the order a reader meets them,\n * over the *whole* proposal rather than only its tail — the list on screen can\n * never hold a token the box would have refused, whatever a host seeded it with.\n *\n * `settled` is how many entries were already committed, and only what lies past\n * it is *reported*. Everything before it is normalised in silence: a host that\n * seeded two identical values while asking for none is corrected, but saying\n * \"design has already been added\" about a token that was there when the page\n * loaded, at the moment the reader adds something else entirely, describes\n * nothing they did.\n */\nfunction reduce(\n proposed: readonly string[],\n settled: number,\n): { kept: string[]; refused: TagsInputRejection[] } {\n const kept: string[] = [];\n const refused: TagsInputRejection[] = [];\n\n proposed.forEach((raw, index) => {\n const value = raw.trim();\n // Skipped rather than refused. A blank is what a trailing delimiter leaves\n // behind — \"a, b, c,\" pastes as four segments — and a reader who pasted\n // three values did not ask about a fourth.\n if (value === \"\") return;\n\n const reason: TagsInputRejectReason | undefined =\n props.max !== undefined && kept.length >= props.max\n ? \"max\"\n : !props.duplicates && kept.includes(value)\n ? \"duplicate\"\n : undefined;\n\n if (reason === undefined) kept.push(value);\n else if (index >= settled) refused.push({ value, reason });\n });\n\n return { kept, refused };\n}\n\nfunction same(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\nfunction focusInput(): void {\n void nextTick(() => inputRef.value?.$el.focus());\n}\n\nfunction commit(next: string[]): void {\n // The message named values the reader has since edited past. An error sitting\n // under a field somebody is actively fixing reads as an error about the fix.\n rejections.value = [];\n tokens.value = next;\n emit(\"update:modelValue\", next);\n}\n\n/**\n * Reka has already written `proposed` into its own model by the time this runs.\n * `tokens.value` is assigned on every path, including the one where nothing\n * changed, and that is load-bearing: `reduce` returns a fresh array, and only a\n * new reference on the prop Reka watches puts its internal copy back in step\n * with what this field actually accepted. Skip the assignment and a refused\n * value stays in Reka's model — invisible on screen, and submitted by the\n * hidden inputs it renders for `name`.\n */\nfunction onProposed(proposed: string[]): void {\n const { kept, refused } = reduce(proposed, tokens.value.length);\n const changed = !same(kept, tokens.value);\n\n if (changed) commit(kept);\n else tokens.value = kept;\n\n if (refused.length === 0) return;\n rejections.value = refused;\n emit(\"reject\", refused);\n\n // One refusal from one typed commit is the reader's own draft, and Reka has\n // just cleared the box under them. Put it back: a field that eats what it\n // will not take leaves the reader retyping a value they can no longer see.\n // Only the single case — restoring one of three pasted values would be\n // choosing between them.\n const only = refused.length === 1 ? refused[0] : undefined;\n if (only === undefined) return;\n void nextTick(() => {\n const input = inputRef.value?.$el;\n if (!input || input.value !== \"\") return;\n input.value = only.value;\n input.setSelectionRange(only.value.length, only.value.length);\n });\n}\n\nfunction removeAt(index: number): void {\n if (field.disabled || field.readonly) return;\n commit(tokens.value.filter((_, at) => at !== index));\n // The button that was pressed has just unmounted, and a browser drops focus\n // to `<body>` when it does. The text box is where the reader was heading\n // anyway, and it is the one node in this control that never goes away.\n focusInput();\n}\n\n/**\n * The keys Reka's `onInputKeydown` claims once the caret sits at the very\n * start of the text box, and the reason this handler runs in the capture phase\n * on the way down rather than as a listener on the input itself: a fallthrough\n * handler is merged *after* Reka's and cannot stop it.\n *\n * What Reka does with them is a virtual selection — Backspace marks the last\n * token, arrows walk it, a second Backspace deletes it — that moves no focus\n * and publishes no `aria-activedescendant`. On screen it is a highlight; to a\n * screen reader it is nothing at all, so the second Backspace destroys a value\n * the reader was never told was chosen. That is the failure this control is\n * most likely to ship, so the whole mechanism is stopped here.\n *\n * In its place, one rule: **Backspace on an empty box lifts the last token back\n * into it**, as editable text with the caret at the end. Nothing is destroyed —\n * the value is on screen, in a box the reader is already focused on, and Enter\n * puts it straight back. The token that left is a `commit`, so a host watching\n * `update:modelValue` sees the edit begin.\n */\nconst CARET_KEYS = new Set([\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\nfunction onKeydownCapture(event: KeyboardEvent): void {\n // An IME's own Backspace edits the composition, and Reka's handlers bail on\n // the same flag for the same reason.\n if (event.isComposing) return;\n const input = event.target;\n if (!(input instanceof HTMLInputElement)) return;\n if (!CARET_KEYS.has(event.key)) return;\n if (input.selectionStart !== 0 || input.selectionEnd !== 0) return;\n\n // Stopped only for the keys and only at the caret position where Reka would\n // have acted. Everything else — Enter, the delimiter, every printable\n // character — reaches its handlers untouched.\n event.stopPropagation();\n\n if (event.key !== \"Backspace\" || input.value !== \"\") return;\n if (field.disabled || field.readonly) return;\n const last = tokens.value.at(-1);\n if (last === undefined) return;\n\n event.preventDefault();\n commit(tokens.value.slice(0, -1));\n input.value = last;\n input.setSelectionRange(last.length, last.length);\n}\n\n/**\n * A read-only field takes nothing, and the native `readonly` attribute is not\n * enough to say so. Reka's paste handler reads the clipboard and commits from\n * it directly, without ever consulting the input it fired on, so a paste into a\n * read-only field would grow tokens the reader cannot then remove.\n */\nfunction onPasteCapture(event: ClipboardEvent): void {\n if (!field.readonly && !field.disabled) return;\n event.preventDefault();\n event.stopPropagation();\n}\n</script>\n\n<template>\n <TagsInputRoot\n as=\"div\"\n v-bind=\"optional({ name: field.name })\"\n :model-value=\"tokens\"\n :delimiter=\"delimiter\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n add-on-paste\n duplicate\n :class=\"cn('flex flex-col', attrs.class as string)\"\n @update:model-value=\"onProposed\"\n @keydown.capture=\"onKeydownCapture\"\n @paste.capture=\"onPasteCapture\"\n >\n <!-- `duplicate` and the absent `max` above are deliberate: Reka is told to\n accept every value it is offered, so that one place — `reduce` —\n decides what a token is. Two refusal ladders would be two answers to\n `duplicates`, and only one would be the one `reject` reports.\n\n The state markers live on the box rather than on the root, and not by\n preference: an attribute bound on `TagsInputRoot` is a fallthrough\n attr, and the root renders through Reka's `CollectionSlot`, which does\n not carry one to the element underneath. The box is also the honest\n node for them — it is what the state is painted on. -->\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-2 py-0.5 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus, exactly as TextField's\n // does — a field holding tokens is still a field, and a form row\n // should not have two focus languages in it.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Lifted rather than dimmed, and it keeps its focus ring: a read-only\n // field is a value **on show**, not an unavailable control. The fill\n // rises off `background` to `subtle` so the box reads as settled, and\n // the text stays full strength — 13.45:1 over that fill — because\n // being read is the entire purpose of the state. The border does not\n // move, because nothing about the field's reach has: it is still a\n // tab stop and still submitted.\n field.readonly && 'bg-subtle',\n // Unavailable is a step further down all three channels: the fill\n // drains to `muted`, the text follows to `muted-foreground` (4.67:1\n // over it) and the rim slackens from `input` to the lighter `border`.\n //\n // Read-only and disabled used to share `bg-muted` and part on the\n // text colour alone, which is a distinction made in hue and nothing\n // else — the one thing a reader with a colour deficiency does not\n // receive. Three channels is what makes available → on show →\n // unavailable legible without it.\n //\n // No `opacity-50` anywhere in that, and that is the point. Every\n // token inside is a Chip that has already drained itself to a\n // measured unavailable fill; halving the alpha over the top of that\n // takes its label below the contrast floor. `border-border` and not\n // the strong weight, for the reason `theme.css` gives the strong one:\n // it is for a block that must **assert** its edge, and an unavailable\n // field is the one thing that must not.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` and\n // every token's remove button natively and never touches\n // `field.disabled`, so the rule above cannot fire and the box\n // rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` reads the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree.\n // See TextField.vue for why it is preferred to a `has-[:disabled]:`\n // rule reading this box's own contents.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated for the reason the two rules below are ordered: Tailwind\n // emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would\n // win on emission order alone.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\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 it\n // saw last, so a field that is both in error and unavailable would\n // lose its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- A real list, so a reader browsing the field meets the tokens as\n items rather than as a run of buttons. It is not where the count\n comes from, though: Tailwind's preflight sets `list-style: none` on\n every `ul`, and Safari drops the list role from a list styled that\n way — which is why the count below is published in words instead of\n left to the browser. -->\n <ul v-if=\"tokens.length > 0\" class=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n <TagsInputItem\n v-for=\"(row, index) in rows\"\n :key=\"row.key\"\n as=\"li\"\n :value=\"row.value\"\n class=\"flex animate-fade-rise\"\n >\n <Chip\n size=\"sm\"\n :removable=\"!field.readonly\"\n :disabled=\"field.disabled\"\n :remove-label=\"text.remove({ value: row.value })\"\n @remove=\"removeAt(index)\"\n >\n <TagsInputItemText />\n </Chip>\n </TagsInputItem>\n </ul>\n\n <!-- `field.attrs` spreads **after** the fallthrough attrs, and that\n position is the contract: the bag has already resolved the caller's\n own `id` and `aria-describedby`, and it drops every key that resolved\n to nothing, so it can only add.\n\n `:id` restates what the bag already holds, or supplies the id a\n Field's `<label for>` needs when it holds none. `:name` undoes one\n key of it on purpose — the text box holds a draft, not the value, and\n a `name` on it would post whatever was half-typed instead of the\n tokens. The value is posted by the hidden inputs the root renders. -->\n <TagsInputInput\n :id=\"inputId\"\n ref=\"input\"\n v-bind=\"{\n ...inputAttrs,\n ...field.attrs,\n ...optional({ placeholder: tokens.length === 0 ? placeholder : undefined }),\n }\"\n :name=\"undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :readonly=\"field.readonly\"\n class=\"h-8 min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n\n <TagsInputClear\n v-if=\"clearable && tokens.length > 0 && !field.readonly\"\n :aria-label=\"text.clear\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 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 focus-visible:shadow-halo data-[disabled]:cursor-not-allowed\"\n @click=\"focusInput\"\n >\n <X class=\"h-4 w-4\" />\n </TagsInputClear>\n\n <!-- The count, and the only thing that tells a reader how many tokens are\n in the field without walking them. Reka publishes nothing of the\n sort: its root is an unlabelled `div`. It is wired through\n `aria-describedby` rather than announced, so it is heard on focus and\n on every return to the field, which is when the question is asked. -->\n <span :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length, max }) }}\n </span>\n </div>\n\n <!-- The region is in the DOM from the first render, empty. A live region\n mounted at the same moment as its text is a region assistive tech was\n not yet watching, so the first refusal — the one that matters most — is\n the one it announces least reliably. -->\n <div aria-live=\"polite\" aria-atomic=\"true\">\n <p\n v-if=\"message.length > 0\"\n :id=\"messageId\"\n class=\"mt-1.5 flex animate-fade-rise items-start gap-2 text-small text-destructive-text\"\n >\n <TriangleAlert class=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span>{{ message }}</span>\n </p>\n </div>\n </TagsInputRoot>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"TagsInput.js","names":[],"sources":["../../../../../packages/primitives/tags-input/src/TagsInput.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type {\n TagsInputLabels,\n TagsInputRejectReason,\n TagsInputRejection,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * Every refusal names the value, because a reader looking at eight tokens\n * cannot tell which \"that one\" is.\n *\n * Loom's English does not name the limit, though `rejected` is handed it: a\n * `max` refusal only ever happens when a limit is set, so a sentence that also\n * had to word the limitless case would be a branch nothing can reach. The\n * number is there for a host that wants \"only 3 can be added\" — and the count\n * description already reads \"3 of 3 tags\" on focus either way.\n */\nfunction describeRejection(rejection: TagsInputRejection): string {\n switch (rejection.reason) {\n case \"duplicate\":\n return `${rejection.value} has already been added.`;\n case \"max\":\n return `${rejection.value} was not added — the field is full.`;\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 * rather than a transcription of it.\n */\nexport const TAGS_INPUT_LABELS: TagsInputLabels = {\n remove: ({ value }) => `Remove ${value}`,\n count: ({ count, max }) =>\n max === undefined\n ? `${String(count)} ${count === 1 ? \"tag\" : \"tags\"}`\n : `${String(count)} of ${String(max)} tags`,\n rejected: ({ rejections }) => rejections.map(describeRejection).join(\" \"),\n clear: \"Clear all tags\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport {\n TagsInputRoot,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemText,\n TagsInputClear,\n} from \"reka-ui\";\nimport { TriangleAlert, X } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TagsInput — several values typed into one field, each committed to a\n * removable token: keywords on an article, recipients on a message, labels on\n * an issue.\n *\n * The line against its neighbours is what the values *are*. A\n * [Select](../Select/Select.vue) or a [Combobox](../Combobox/Combobox.vue)\n * picks from a list somebody else wrote, and cannot produce a value that is not\n * already in it; this field's whole point is that the reader invents the\n * values. A [TextField](../TextField/TextField.vue) holds one value, and a\n * comma-separated string in one is a list the application has to parse and the\n * reader has to punctuate correctly. Reach for\n * [Chip](@ecoma-io/loom-chip) alone when the tokens are a display of something\n * already decided — a chip row nobody types into is not a field.\n *\n * Each token *is* a Chip, in the removable shape Chip already ships, so a token\n * here and a chip elsewhere in the same form are the same object. The remove\n * control is Chip's own rather than Reka's `TagsInputItemDelete`, which is\n * `tabindex=\"-1\"` by design: Reka pairs it with an arrow-key selection that\n * moves no focus and sets no `aria-activedescendant`, so the token it\n * highlights is announced to nobody. Loom keeps the buttons on the tab order\n * and replaces the selection outright — see `onKeydownCapture`.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * the hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`. Every prop below still wins\n * over the row when it is set, in both directions — which is why the four\n * booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states here as everywhere else, and\n * the difference is larger than usual because there are two things to freeze. A\n * read-only field shows its tokens, refuses to grow one and drops every remove\n * control, while staying a tab stop and staying in the submitted data. A\n * disabled field is unavailable: not a tab stop, not submitted, and its tokens\n * drain to the neutral fill.\n *\n * That makes **three** resting appearances rather than two, and each is three\n * changes rather than one — fill, text colour and border weight together. See\n * the box's class list for why one channel is not enough.\n */\nconst props = withDefaults(\n defineProps<{\n /** The committed tokens. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string[] | undefined;\n /** Text shown in the empty text box. It is hidden once a token exists, where it would sit beside the tokens claiming the field is empty. */\n placeholder?: string;\n /** The most tokens the field will hold. Anything past it is refused and reported rather than dropped. */\n max?: number;\n /** Whether the same value may be committed twice. Off, so a repeated value is refused and says so. */\n duplicates?: boolean;\n /** What commits a token besides Enter. Typing it, or pasting text containing it, splits at it. */\n delimiter?: string;\n /** Renders a control that empties the field in one press. */\n clearable?: boolean;\n /** Disables the field: no tab stop, no editing, no remove controls, nothing submitted. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the tokens without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/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`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post. Each token is submitted as `name[0]`, `name[1]`, … Unset defers to a wrapping Field. */\n name?: string;\n /** The accessible name of the text box, when no visible element already labels it. */\n ariaLabel?: string;\n /** The id of the visible element that labels the text box (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TagsInputLabels` — 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: the case it exists for is a field whose tokens are people,\n * where \"Remove Ana Duarte\" reads and \"Remove tag\" does not.\n */\n labels?: LabelOverrides<TagsInputLabels>;\n }>(),\n {\n // Not `[]`. An empty array is a concrete value, so it would say \"the host\n // owns this list\", and a host that binds nothing would watch every token it\n // typed vanish on the next render. `undefined` is what leaves the control\n // managing its own — the same distinction `src/lib/props.ts` exists to\n // preserve for the Reka-backed primitives.\n modelValue: undefined,\n duplicates: false,\n delimiter: \",\",\n clearable: false,\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that past\n // Vue's absent-Boolean-casts-to-false rule. TextField.vue carries the full\n // reasoning.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The full token list after the change, trimmed and de-duplicated. Never a partial list. */\n \"update:modelValue\": [values: string[]];\n /** Every value refused by this one interaction, each with the rule that refused it. Fires once per interaction, not once per value. */\n reject: [rejections: TagsInputRejection[]];\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(\"tagsInput\", TAGS_INPUT_LABELS, () => props.labels);\n\n// `class` sizes the whole control and lands on the outer element; everything\n// else a caller passes — `id`, `data-testid`, `aria-describedby`,\n// `autocomplete` — describes the real `<input>` and lands there. The outer\n// element is not a form control and the input is not the box a caller lays out,\n// so there is no one node both belong on.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\nconst generatedId = useId();\nconst countId = useId();\nconst messageId = useId();\n\nconst inputRef = useTemplateRef<{ $el: HTMLInputElement }>(\"input\");\n\n/**\n * The committed tokens, and the reason this control keeps its own copy rather\n * than forwarding `modelValue` into Reka and letting it manage the state.\n *\n * Loom applies four rules Reka has no notion of — trim, drop the blanks a paste\n * leaves behind, refuse a duplicate, refuse anything past `max` — so Reka is\n * configured to accept everything (`duplicate`, no `max`) and *propose*, and\n * `onProposed` decides. That needs a value flowing back down, which means a\n * `:model-value` that is always defined and always ours.\n *\n * It is `useVModel(…, { passive: true })` written out, and behaves the same way\n * Reka's own would: the change is taken locally and emitted, and a controlled\n * host that declines it is honoured on the next value it does send. Seeded from\n * the prop rather than sanitised, because a list the host already holds is the\n * host's to hold — the rules above are about what this field *accepts*.\n */\nconst tokens = ref<string[]>([...(props.modelValue ?? [])]);\nwatch(\n () => props.modelValue,\n (next) => {\n if (next !== undefined) tokens.value = [...next];\n },\n // Shallow, deliberately: the contract is whole-list replacement — `commit`\n // emits the complete token list on every change and a host echoes it\n // straight back down — so in-place mutation is not a case this mirror owes\n // anyone. Same reasoning as Combobox's mirror of its selection.\n);\n\nconst rejections = ref<TagsInputRejection[]>([]);\nconst message = computed(() =>\n rejections.value.length > 0\n ? text.value.rejected({ rejections: rejections.value, max: props.max })\n : \"\",\n);\n\n// The refusal message reads first and the count second: both describe the\n// field, and the one that just changed is the more specific thing to say. A\n// wrapping Field's own hint or error is merged in after both by\n// `useFieldControl`, never in place of them.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [\n message.value.length > 0 ? messageId : undefined,\n countId,\n attrs[\"aria-describedby\"],\n ]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// The `<label for>` a Field renders has to resolve to the text box, and a\n// caller passing their own id must not end up with a label pointing at nothing.\nconst inputId = computed(() => field.id ?? generatedId);\n\n/**\n * A stable key per token, since the token itself cannot be one: `duplicates`\n * makes two identical strings legal and Vue warns on the collision.\n *\n * The n-th occurrence of a value keys as `n:value`, so removing one of two\n * identical tokens unmounts exactly one node and the survivor keeps the other —\n * indistinguishable, because the two say the same thing. Keying by index\n * instead would re-key every row after the one removed, which patches text into\n * nodes that belong to other tokens.\n */\nconst rows = computed(() => {\n const seen = new Map<string, number>();\n return tokens.value.map((value) => {\n const ordinal = seen.get(value) ?? 0;\n seen.set(value, ordinal + 1);\n return { value, key: `${String(ordinal)}:${value}` };\n });\n});\n\n/**\n * Every rule this field applies to a value, in the order a reader meets them,\n * over the *whole* proposal rather than only its tail — the list on screen can\n * never hold a token the box would have refused, whatever a host seeded it with.\n *\n * `settled` is how many entries were already committed, and only what lies past\n * it is *reported*. Everything before it is normalised in silence: a host that\n * seeded two identical values while asking for none is corrected, but saying\n * \"design has already been added\" about a token that was there when the page\n * loaded, at the moment the reader adds something else entirely, describes\n * nothing they did.\n */\nfunction reduce(\n proposed: readonly string[],\n settled: number,\n): { kept: string[]; refused: TagsInputRejection[] } {\n const kept: string[] = [];\n const refused: TagsInputRejection[] = [];\n\n proposed.forEach((raw, index) => {\n const value = raw.trim();\n // Skipped rather than refused. A blank is what a trailing delimiter leaves\n // behind — \"a, b, c,\" pastes as four segments — and a reader who pasted\n // three values did not ask about a fourth.\n if (value === \"\") return;\n\n const reason: TagsInputRejectReason | undefined =\n props.max !== undefined && kept.length >= props.max\n ? \"max\"\n : !props.duplicates && kept.includes(value)\n ? \"duplicate\"\n : undefined;\n\n if (reason === undefined) kept.push(value);\n else if (index >= settled) refused.push({ value, reason });\n });\n\n return { kept, refused };\n}\n\nfunction same(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\nfunction focusInput(): void {\n void nextTick(() => inputRef.value?.$el.focus());\n}\n\nfunction commit(next: string[]): void {\n // The message named values the reader has since edited past. An error sitting\n // under a field somebody is actively fixing reads as an error about the fix.\n rejections.value = [];\n tokens.value = next;\n emit(\"update:modelValue\", next);\n}\n\n/**\n * Reka has already written `proposed` into its own model by the time this runs.\n * `tokens.value` is assigned on every path, including the one where nothing\n * changed, and that is load-bearing: `reduce` returns a fresh array, and only a\n * new reference on the prop Reka watches puts its internal copy back in step\n * with what this field actually accepted. Skip the assignment and a refused\n * value stays in Reka's model — invisible on screen, and submitted by the\n * hidden inputs it renders for `name`.\n */\nfunction onProposed(proposed: string[]): void {\n const { kept, refused } = reduce(proposed, tokens.value.length);\n const changed = !same(kept, tokens.value);\n\n if (changed) commit(kept);\n else tokens.value = kept;\n\n if (refused.length === 0) return;\n rejections.value = refused;\n emit(\"reject\", refused);\n\n // One refusal from one typed commit is the reader's own draft, and Reka has\n // just cleared the box under them. Put it back: a field that eats what it\n // will not take leaves the reader retyping a value they can no longer see.\n // Only the single case — restoring one of three pasted values would be\n // choosing between them.\n const only = refused.length === 1 ? refused[0] : undefined;\n if (only === undefined) return;\n void nextTick(() => {\n const input = inputRef.value?.$el;\n if (!input || input.value !== \"\") return;\n input.value = only.value;\n input.setSelectionRange(only.value.length, only.value.length);\n });\n}\n\nfunction removeAt(index: number): void {\n if (field.disabled || field.readonly) return;\n commit(tokens.value.filter((_, at) => at !== index));\n // The button that was pressed has just unmounted, and a browser drops focus\n // to `<body>` when it does. The text box is where the reader was heading\n // anyway, and it is the one node in this control that never goes away.\n focusInput();\n}\n\n/**\n * The keys Reka's `onInputKeydown` claims once the caret sits at the very\n * start of the text box, and the reason this handler runs in the capture phase\n * on the way down rather than as a listener on the input itself: a fallthrough\n * handler is merged *after* Reka's and cannot stop it.\n *\n * What Reka does with them is a virtual selection — Backspace marks the last\n * token, arrows walk it, a second Backspace deletes it — that moves no focus\n * and publishes no `aria-activedescendant`. On screen it is a highlight; to a\n * screen reader it is nothing at all, so the second Backspace destroys a value\n * the reader was never told was chosen. That is the failure this control is\n * most likely to ship, so the whole mechanism is stopped here.\n *\n * In its place, one rule: **Backspace on an empty box lifts the last token back\n * into it**, as editable text with the caret at the end. Nothing is destroyed —\n * the value is on screen, in a box the reader is already focused on, and Enter\n * puts it straight back. The token that left is a `commit`, so a host watching\n * `update:modelValue` sees the edit begin.\n */\nconst CARET_KEYS = new Set([\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\nfunction onKeydownCapture(event: KeyboardEvent): void {\n // An IME's own Backspace edits the composition, and Reka's handlers bail on\n // the same flag for the same reason.\n if (event.isComposing) return;\n const input = event.target;\n if (!(input instanceof HTMLInputElement)) return;\n if (!CARET_KEYS.has(event.key)) return;\n if (input.selectionStart !== 0 || input.selectionEnd !== 0) return;\n\n // Stopped only for the keys and only at the caret position where Reka would\n // have acted. Everything else — Enter, the delimiter, every printable\n // character — reaches its handlers untouched.\n event.stopPropagation();\n\n if (event.key !== \"Backspace\" || input.value !== \"\") return;\n if (field.disabled || field.readonly) return;\n const last = tokens.value.at(-1);\n if (last === undefined) return;\n\n event.preventDefault();\n commit(tokens.value.slice(0, -1));\n input.value = last;\n input.setSelectionRange(last.length, last.length);\n}\n\n/**\n * A read-only field takes nothing, and the native `readonly` attribute is not\n * enough to say so. Reka's paste handler reads the clipboard and commits from\n * it directly, without ever consulting the input it fired on, so a paste into a\n * read-only field would grow tokens the reader cannot then remove.\n */\nfunction onPasteCapture(event: ClipboardEvent): void {\n if (!field.readonly && !field.disabled) return;\n event.preventDefault();\n event.stopPropagation();\n}\n</script>\n\n<template>\n <TagsInputRoot\n as=\"div\"\n v-bind=\"optional({ name: field.name })\"\n :model-value=\"tokens\"\n :delimiter=\"delimiter\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n add-on-paste\n duplicate\n :class=\"cn('flex flex-col', attrs.class as string)\"\n @update:model-value=\"onProposed\"\n @keydown.capture=\"onKeydownCapture\"\n @paste.capture=\"onPasteCapture\"\n >\n <!-- `duplicate` and the absent `max` above are deliberate: Reka is told to\n accept every value it is offered, so that one place — `reduce` —\n decides what a token is. Two refusal ladders would be two answers to\n `duplicates`, and only one would be the one `reject` reports.\n\n The state markers live on the box rather than on the root, and not by\n preference: an attribute bound on `TagsInputRoot` is a fallthrough\n attr, and the root renders through Reka's `CollectionSlot`, which does\n not carry one to the element underneath. The box is also the honest\n node for them — it is what the state is painted on. -->\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-2 py-0.5 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus, exactly as TextField's\n // does — a field holding tokens is still a field, and a form row\n // should not have two focus languages in it.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Lifted rather than dimmed, and it keeps its focus ring: a read-only\n // field is a value **on show**, not an unavailable control. The fill\n // rises off `background` to `subtle` so the box reads as settled, and\n // the text stays full strength — 13.45:1 over that fill — because\n // being read is the entire purpose of the state. The border does not\n // move, because nothing about the field's reach has: it is still a\n // tab stop and still submitted.\n field.readonly && 'bg-subtle',\n // Unavailable is a step further down all three channels: the fill\n // drains to `muted`, the text follows to `muted-foreground` (4.67:1\n // over it) and the rim slackens from `input` to the lighter `border`.\n //\n // Read-only and disabled used to share `bg-muted` and part on the\n // text colour alone, which is a distinction made in hue and nothing\n // else — the one thing a reader with a colour deficiency does not\n // receive. Three channels is what makes available → on show →\n // unavailable legible without it.\n //\n // No `opacity-50` anywhere in that, and that is the point. Every\n // token inside is a Chip that has already drained itself to a\n // measured unavailable fill; halving the alpha over the top of that\n // takes its label below the contrast floor. `border-border` and not\n // the strong weight, for the reason `theme.css` gives the strong one:\n // it is for a block that must **assert** its edge, and an unavailable\n // field is the one thing that must not.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` and\n // every token's remove button natively and never touches\n // `field.disabled`, so the rule above cannot fire and the box\n // rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` reads the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree.\n // See TextField.vue for why it is preferred to a `has-[:disabled]:`\n // rule reading this box's own contents.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated for the reason the two rules below are ordered: Tailwind\n // emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would\n // win on emission order alone.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\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 it\n // saw last, so a field that is both in error and unavailable would\n // lose its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- A real list, so a reader browsing the field meets the tokens as\n items rather than as a run of buttons. It is not where the count\n comes from, though: Tailwind's preflight sets `list-style: none` on\n every `ul`, and Safari drops the list role from a list styled that\n way — which is why the count below is published in words instead of\n left to the browser. -->\n <ul v-if=\"tokens.length > 0\" class=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n <TagsInputItem\n v-for=\"(row, index) in rows\"\n :key=\"row.key\"\n as=\"li\"\n :value=\"row.value\"\n class=\"flex animate-fade-rise\"\n >\n <Chip\n size=\"sm\"\n :removable=\"!field.readonly\"\n :disabled=\"field.disabled\"\n :remove-label=\"text.remove({ value: row.value })\"\n @remove=\"removeAt(index)\"\n >\n <TagsInputItemText />\n </Chip>\n </TagsInputItem>\n </ul>\n\n <!-- `field.attrs` spreads **after** the fallthrough attrs, and that\n position is the contract: the bag has already resolved the caller's\n own `id` and `aria-describedby`, and it drops every key that resolved\n to nothing, so it can only add.\n\n `:id` restates what the bag already holds, or supplies the id a\n Field's `<label for>` needs when it holds none. `:name` undoes one\n key of it on purpose — the text box holds a draft, not the value, and\n a `name` on it would post whatever was half-typed instead of the\n tokens. The value is posted by the hidden inputs the root renders. -->\n <TagsInputInput\n :id=\"inputId\"\n ref=\"input\"\n v-bind=\"{\n ...inputAttrs,\n ...field.attrs,\n ...optional({ placeholder: tokens.length === 0 ? placeholder : undefined }),\n }\"\n :name=\"undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :readonly=\"field.readonly\"\n class=\"h-8 min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n\n <TagsInputClear\n v-if=\"clearable && tokens.length > 0 && !field.readonly\"\n :aria-label=\"text.clear\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 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 focus-visible:shadow-halo data-[disabled]:cursor-not-allowed\"\n @click=\"focusInput\"\n >\n <X class=\"h-4 w-4\" />\n </TagsInputClear>\n\n <!-- The count, and the only thing that tells a reader how many tokens are\n in the field without walking them. Reka publishes nothing of the\n sort: its root is an unlabelled `div`. It is wired through\n `aria-describedby` rather than announced, so it is heard on focus and\n on every return to the field, which is when the question is asked. -->\n <span :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length, max }) }}\n </span>\n </div>\n\n <!-- The region is in the DOM from the first render, empty. A live region\n mounted at the same moment as its text is a region assistive tech was\n not yet watching, so the first refusal — the one that matters most — is\n the one it announces least reliably. -->\n <div aria-live=\"polite\" aria-atomic=\"true\">\n <p\n v-if=\"message.length > 0\"\n :id=\"messageId\"\n class=\"mt-1.5 flex animate-fade-rise items-start gap-2 text-small text-destructive-text\"\n >\n <TriangleAlert class=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span>{{ message }}</span>\n </p>\n </div>\n </TagsInputRoot>\n</template>\n"],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type LabelOverrides } from "
|
|
2
|
-
import type { TagsInputLabels, TagsInputRejection } from "
|
|
1
|
+
import { type LabelOverrides } from "../../../labels/src/index";
|
|
2
|
+
import type { TagsInputLabels, TagsInputRejection } from "../../../labels/src/index";
|
|
3
3
|
/**
|
|
4
4
|
* Loom's English, co-located with the component so it tree-shakes with it, and
|
|
5
5
|
* exported so a host can build a partial vocabulary against the real thing
|
package/dist/packages/primitives/tags-input/src/TagsInput.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -119,7 +119,7 @@ var TagsInput_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
119
119
|
const tokens = ref([...props.modelValue ?? []]);
|
|
120
120
|
watch(() => props.modelValue, (next) => {
|
|
121
121
|
if (next !== void 0) tokens.value = [...next];
|
|
122
|
-
}
|
|
122
|
+
});
|
|
123
123
|
const rejections = ref([]);
|
|
124
124
|
const message = computed(() => rejections.value.length > 0 ? text.value.rejected({
|
|
125
125
|
rejections: rejections.value,
|