@ecoma-io/loom 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/a11y.js +2 -2
- package/dist/index.js +36 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/CalendarDate.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/CalendarDate.js.map +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js.map +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/conversion.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/conversion.js.map +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/manipulation.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/manipulation.js.map +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/queries.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/queries.js.map +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/string.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/string.js.map +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/utils.js +1 -1
- package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/utils.js.map +1 -1
- package/dist/packages/blocks/empty-state/src/EmptyState.js.map +1 -1
- package/dist/packages/blocks/empty-state/src/EmptyState.vue.d.ts.map +1 -1
- package/dist/packages/blocks/empty-state/src/EmptyState.vue_vue_type_script_setup_true_lang.js +5 -6
- package/dist/packages/blocks/empty-state/src/EmptyState.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/blocks/error-summary/src/ErrorSummary.js +7 -0
- package/dist/packages/blocks/error-summary/src/ErrorSummary.js.map +1 -0
- package/dist/packages/blocks/error-summary/src/ErrorSummary.vue.d.ts +31 -0
- package/dist/packages/blocks/error-summary/src/ErrorSummary.vue_vue_type_script_setup_true_lang.js +90 -0
- package/dist/packages/blocks/error-summary/src/ErrorSummary.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/blocks/error-summary/src/index.d.ts +4 -0
- package/dist/packages/blocks/error-summary/src/index.js +3 -0
- package/dist/packages/composition/scroll-reel/src/ScrollReel.js.map +1 -1
- package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/composition/sidebar/src/Sidebar.js.map +1 -1
- package/dist/packages/composition/sidebar/src/Sidebar.vue.d.ts +4 -0
- package/dist/packages/composition/sidebar/src/Sidebar.vue.d.ts.map +1 -1
- package/dist/packages/composition/sidebar/src/Sidebar.vue_vue_type_script_setup_true_lang.js +13 -4
- package/dist/packages/composition/sidebar/src/Sidebar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/composition/split/src/Split.js.map +1 -1
- package/dist/packages/composition/split/src/Split.vue.d.ts +11 -12
- package/dist/packages/composition/split/src/Split.vue.d.ts.map +1 -1
- package/dist/packages/composition/split/src/Split.vue_vue_type_script_setup_true_lang.js +15 -5
- package/dist/packages/composition/split/src/Split.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/core/src/a11y-scope.d.ts +71 -5
- package/dist/packages/core/src/a11y-scope.d.ts.map +1 -1
- package/dist/packages/core/src/a11y-scope.js +126 -6
- package/dist/packages/core/src/a11y-scope.js.map +1 -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/labels/src/component-shapes.d.ts +163 -1
- package/dist/packages/labels/src/component-shapes.d.ts.map +1 -1
- package/dist/packages/labels/src/date-labels.d.ts +82 -5
- package/dist/packages/labels/src/date-labels.d.ts.map +1 -1
- package/dist/packages/labels/src/date-labels.js +54 -7
- package/dist/packages/labels/src/date-labels.js.map +1 -1
- package/dist/packages/labels/src/field-context.d.ts +2 -2
- package/dist/packages/labels/src/field-context.js.map +1 -1
- package/dist/packages/labels/src/index.d.ts +3 -3
- package/dist/packages/labels/src/index.d.ts.map +1 -1
- package/dist/packages/labels/src/index.js +2 -2
- package/dist/packages/labels/src/label-registry.d.ts +9 -1
- package/dist/packages/labels/src/label-registry.d.ts.map +1 -1
- package/dist/packages/layouts/dashboard/src/Dashboard.js.map +1 -1
- package/dist/packages/layouts/dashboard/src/Dashboard.vue.d.ts +4 -4
- package/dist/packages/layouts/dashboard/src/Dashboard.vue.d.ts.map +1 -1
- package/dist/packages/layouts/dashboard/src/Dashboard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/layouts/settings/src/Settings.js.map +1 -1
- package/dist/packages/layouts/settings/src/Settings.vue.d.ts +3 -3
- package/dist/packages/layouts/settings/src/Settings.vue.d.ts.map +1 -1
- package/dist/packages/layouts/settings/src/Settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/layouts/split-layout/src/SplitLayout.js.map +1 -1
- package/dist/packages/layouts/split-layout/src/SplitLayout.vue.d.ts +4 -6
- package/dist/packages/layouts/split-layout/src/SplitLayout.vue.d.ts.map +1 -1
- package/dist/packages/layouts/split-layout/src/SplitLayout.vue_vue_type_script_setup_true_lang.js +20 -5
- package/dist/packages/layouts/split-layout/src/SplitLayout.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/layouts/split-layout/src/index.d.ts +1 -1
- package/dist/packages/layouts/split-layout/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 +27 -1
- package/dist/packages/primitives/alert/src/Alert.js.map +1 -1
- package/dist/packages/primitives/alert/src/Alert.vue.d.ts +18 -0
- package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js +10 -7
- package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/avatar/src/Avatar.js.map +1 -1
- package/dist/packages/primitives/avatar/src/Avatar.vue.d.ts +2 -2
- package/dist/packages/primitives/avatar/src/Avatar.vue.d.ts.map +1 -1
- package/dist/packages/primitives/avatar/src/Avatar.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/packages/primitives/avatar/src/Avatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/calendar/src/Calendar.js +7 -0
- package/dist/packages/primitives/calendar/src/Calendar.js.map +1 -0
- package/dist/packages/primitives/calendar/src/Calendar.vue.d.ts +95 -0
- package/dist/packages/primitives/calendar/src/Calendar.vue_vue_type_script_setup_true_lang.js +204 -0
- package/dist/packages/primitives/calendar/src/Calendar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/calendar/src/index.d.ts +3 -0
- package/dist/packages/primitives/calendar/src/index.js +3 -0
- package/dist/packages/primitives/combobox/src/Combobox.vue.d.ts +1 -1
- package/dist/packages/primitives/command/src/Command.js +7 -0
- package/dist/packages/primitives/command/src/Command.js.map +1 -0
- package/dist/packages/primitives/command/src/Command.vue.d.ts +77 -0
- package/dist/packages/primitives/command/src/Command.vue_vue_type_script_setup_true_lang.js +333 -0
- package/dist/packages/primitives/command/src/Command.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/command/src/index.d.ts +4 -0
- package/dist/packages/primitives/command/src/index.js +3 -0
- package/dist/packages/primitives/copy-button/src/CopyButton.js +7 -0
- package/dist/packages/primitives/copy-button/src/CopyButton.js.map +1 -0
- package/dist/packages/primitives/copy-button/src/CopyButton.vue.d.ts +64 -0
- package/dist/packages/primitives/copy-button/src/CopyButton.vue_vue_type_script_setup_true_lang.js +113 -0
- package/dist/packages/primitives/copy-button/src/CopyButton.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/copy-button/src/index.d.ts +4 -0
- package/dist/packages/primitives/copy-button/src/index.js +3 -0
- package/dist/packages/primitives/data-grid/src/DataGrid.js +7 -0
- package/dist/packages/primitives/data-grid/src/DataGrid.js.map +1 -0
- package/dist/packages/primitives/data-grid/src/DataGrid.vue.d.ts +128 -0
- package/dist/packages/primitives/data-grid/src/DataGrid.vue_vue_type_script_setup_true_lang.js +347 -0
- package/dist/packages/primitives/data-grid/src/DataGrid.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/data-grid/src/index.d.ts +4 -0
- package/dist/packages/primitives/data-grid/src/index.js +3 -0
- package/dist/packages/primitives/date-picker/src/DatePicker.js.map +1 -1
- package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js +13 -13
- 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_vue_type_script_setup_true_lang.js +21 -18
- 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-time-picker/src/DateTimePicker.js.map +1 -1
- package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js +17 -16
- 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_vue_type_script_setup_true_lang.js +17 -16
- 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/dialog/src/Dialog.js.map +1 -1
- package/dist/packages/primitives/dialog/src/Dialog.vue.d.ts +15 -7
- package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js +5 -3
- package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/drawer/src/Drawer.js.map +1 -1
- package/dist/packages/primitives/drawer/src/Drawer.vue.d.ts +10 -2
- package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js +11 -3
- package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/hover-card/src/index.d.ts.map +1 -1
- package/dist/packages/primitives/live-region/src/LiveRegion.js +7 -0
- package/dist/packages/primitives/live-region/src/LiveRegion.js.map +1 -0
- package/dist/packages/primitives/live-region/src/LiveRegion.vue.d.ts +36 -0
- package/dist/packages/primitives/live-region/src/LiveRegion.vue.d.ts.map +1 -0
- package/dist/packages/primitives/live-region/src/LiveRegion.vue_vue_type_script_setup_true_lang.js +59 -0
- package/dist/packages/primitives/live-region/src/LiveRegion.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/live-region/src/context.d.ts +40 -0
- package/dist/packages/primitives/live-region/src/context.d.ts.map +1 -0
- package/dist/packages/primitives/live-region/src/context.js +85 -0
- package/dist/packages/primitives/live-region/src/context.js.map +1 -0
- package/dist/packages/primitives/live-region/src/index.d.ts +4 -0
- package/dist/packages/primitives/live-region/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/live-region/src/index.js +3 -0
- package/dist/packages/primitives/live-region/src/useAnnounce.d.ts +17 -0
- package/dist/packages/primitives/live-region/src/useAnnounce.d.ts.map +1 -0
- package/dist/packages/primitives/live-region/src/useAnnounce.js +25 -0
- package/dist/packages/primitives/live-region/src/useAnnounce.js.map +1 -0
- package/dist/packages/primitives/menubar/src/Menubar.js.map +1 -1
- 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 +33 -0
- package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/meter/src/Meter.js +7 -0
- package/dist/packages/primitives/meter/src/Meter.js.map +1 -0
- package/dist/packages/primitives/meter/src/Meter.vue.d.ts +87 -0
- package/dist/packages/primitives/meter/src/Meter.vue_vue_type_script_setup_true_lang.js +236 -0
- package/dist/packages/primitives/meter/src/Meter.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/meter/src/index.d.ts +4 -0
- package/dist/packages/primitives/meter/src/index.js +3 -0
- package/dist/packages/primitives/navigation-menu/src/NavigationMenu.js +7 -0
- package/dist/packages/primitives/navigation-menu/src/NavigationMenu.js.map +1 -0
- package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue.d.ts +69 -0
- package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue.d.ts.map +1 -0
- package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue_vue_type_script_setup_true_lang.js +118 -0
- package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/navigation-menu/src/index.d.ts +3 -0
- package/dist/packages/primitives/navigation-menu/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/navigation-menu/src/index.js +2 -0
- 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 +1 -1
- package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/skip-link/src/SkipLink.js +7 -0
- package/dist/packages/primitives/skip-link/src/SkipLink.js.map +1 -0
- package/dist/packages/primitives/skip-link/src/SkipLink.vue.d.ts +62 -0
- package/dist/packages/primitives/skip-link/src/SkipLink.vue_vue_type_script_setup_true_lang.js +64 -0
- package/dist/packages/primitives/skip-link/src/SkipLink.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/skip-link/src/index.d.ts +3 -0
- package/dist/packages/primitives/skip-link/src/index.js +3 -0
- package/dist/packages/primitives/stepper/src/Stepper.js.map +1 -1
- package/dist/packages/primitives/stepper/src/Stepper.vue_vue_type_script_setup_true_lang.js +7 -5
- package/dist/packages/primitives/stepper/src/Stepper.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/switch/src/Switch.js.map +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 +1 -1
- package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/table/src/index.d.ts +1 -1
- package/dist/packages/primitives/table/src/index.js +2 -2
- package/dist/packages/primitives/time-picker/src/TimePicker.js.map +1 -1
- package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js +10 -8
- package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/toast/src/ToastItem.js.map +1 -1
- package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/packages/primitives/toggle-group/src/ToggleGroup.js +7 -0
- package/dist/packages/primitives/toggle-group/src/ToggleGroup.js.map +1 -0
- package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue.d.ts +53 -0
- package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue.d.ts.map +1 -0
- package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue_vue_type_script_setup_true_lang.js +111 -0
- package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/toggle-group/src/index.d.ts +3 -0
- package/dist/packages/primitives/toggle-group/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/toggle-group/src/index.js +2 -0
- package/dist/packages/primitives/toolbar/src/Toolbar.js +7 -0
- package/dist/packages/primitives/toolbar/src/Toolbar.js.map +1 -0
- package/dist/packages/primitives/toolbar/src/Toolbar.vue.d.ts +31 -0
- package/dist/packages/primitives/toolbar/src/Toolbar.vue.d.ts.map +1 -0
- package/dist/packages/primitives/toolbar/src/Toolbar.vue_vue_type_script_setup_true_lang.js +149 -0
- package/dist/packages/primitives/toolbar/src/Toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/toolbar/src/ToolbarSeparator.js +7 -0
- package/dist/packages/primitives/toolbar/src/ToolbarSeparator.js.map +1 -0
- package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue.d.ts +24 -0
- package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue.d.ts.map +1 -0
- package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue_vue_type_script_setup_true_lang.js +40 -0
- package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/toolbar/src/index.d.ts +4 -0
- package/dist/packages/primitives/toolbar/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/toolbar/src/index.js +3 -0
- package/dist/packages/primitives/tree-view/src/TreeView.js +7 -0
- package/dist/packages/primitives/tree-view/src/TreeView.js.map +1 -0
- package/dist/packages/primitives/tree-view/src/TreeView.vue.d.ts +99 -0
- package/dist/packages/primitives/tree-view/src/TreeView.vue_vue_type_script_setup_true_lang.js +310 -0
- package/dist/packages/primitives/tree-view/src/TreeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/tree-view/src/TreeViewNode.js +7 -0
- package/dist/packages/primitives/tree-view/src/TreeViewNode.js.map +1 -0
- package/dist/packages/primitives/tree-view/src/TreeViewNode.vue.d.ts +15 -0
- package/dist/packages/primitives/tree-view/src/TreeViewNode.vue.d.ts.map +1 -0
- package/dist/packages/primitives/tree-view/src/TreeViewNode.vue_vue_type_script_setup_true_lang.js +103 -0
- package/dist/packages/primitives/tree-view/src/TreeViewNode.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/tree-view/src/context.d.ts +39 -0
- package/dist/packages/primitives/tree-view/src/context.d.ts.map +1 -0
- package/dist/packages/primitives/tree-view/src/context.js +6 -0
- package/dist/packages/primitives/tree-view/src/context.js.map +1 -0
- package/dist/packages/primitives/tree-view/src/index.d.ts +4 -0
- package/dist/packages/primitives/tree-view/src/index.js +3 -0
- package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.js +7 -0
- package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.js.map +1 -0
- package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue.d.ts +21 -0
- package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue.d.ts.map +1 -0
- package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue_vue_type_script_setup_true_lang.js +27 -0
- package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/packages/primitives/visually-hidden/src/index.d.ts +3 -0
- package/dist/packages/primitives/visually-hidden/src/index.d.ts.map +1 -0
- package/dist/packages/primitives/visually-hidden/src/index.js +2 -0
- package/package.json +29 -28
package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/switch/src/Switch.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { SwitchRoot, SwitchThumb } from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\n\n/**\n * Switch — toggles a boolean setting that takes effect immediately, with no\n * \"Save\" step. Built on Reka UI's Switch for role=\"switch\" + aria-checked +\n * Space/Enter. Label and description live on the surrounding setting row —\n * wire them in with `aria-labelledby`, since Switch renders a button, not a\n * labelable input.\n *\n * Inside a [Field](../Field/Field.vue) that row is the wrapper itself, and the\n * wiring stops being the caller's job: `useFieldControl()` puts the row's id on\n * the button — labelable, so the row's `<label for>` names the control that has\n * no name of its own — and brings the row's description, `name`, `required` and\n * `invalid` with it.\n *\n * There is no `readonly` here, and a row's is ignored rather than approximated.\n * A switch that cannot be flipped is disabled; one that looks live and refuses\n * the flip is the only state worse than either.\n */\nconst props = withDefaults(\n defineProps<{\n /** Whether the setting is currently on. Omit to let the switch own its own state. */\n modelValue?: boolean | undefined;\n /** Blocks both click and keyboard input, and drains the control to the neutral well. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n }>(),\n {\n // Not `false` — see Checkbox: a concrete default claims host ownership and\n // leaves an unbound switch emitting updates while never moving.\n modelValue: undefined,\n // Not `false` either: absent has to stay tellable from `false` for a Field\n // above to disable the row and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n },\n);\n\ndefineEmits<{ \"update:modelValue\": [value: boolean] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: switchAttrs } = useSplitAttrs();\n\n// One rendered node, so the split is only about `class` — a caller's class\n// needs a Tailwind-aware merge rather than Vue's concatenating fallthrough.\n//\n// `id` and `aria-describedby` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values; `field.attrs` then spreads\n// **after** `switchAttrs`, and that position is the contract (see\n// TextField.vue). `name` in that bag reaches SwitchRoot's own `name` prop,\n// which is what mints the hidden input a `<form>` submits.\n//\n// `name`, `required` and `invalid` are omitted below because Switch has no prop\n// for any of them: the row's answer stands unopposed. `readonly` is omitted for\n// the reason the docblock gives.\n//\n// The resolved `required` then reaches the element through Reka's own prop\n// rather than the `aria-required` in `field.attrs` — see Checkbox.vue, which\n// carries the reasoning. SwitchRoot happens to spread the fallthrough last, so\n// the attribute would win here; going through the prop keeps the four\n// Reka-rooted controls one shape instead of four merge orders to re-derive.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n disabled: props.disabled,\n}));\n</script>\n\n<template>\n <SwitchRoot\n v-bind=\"{ modelValue, ...switchAttrs, ...field.attrs }\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n :class=\"\n cn(\n 'relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full border border-transparent',\n 'transition-[background-color,filter,box-shadow] duration-instant ease-out',\n 'hover:brightness-95',\n // Checked = a decision (Loom law): the track fills with primary.\n 'data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',\n // Focus draws the weave tight: the brand ring blooms.\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Drained, not dimmed. `opacity-50` faded track and thumb together,\n // compositing both below their measured contrast — the same failure\n // Button records for its own dim. The unchecked track falls to\n // `bg-muted`, the well every unavailable control drains to, with the\n // rim Chip slackens to; a *checked* one keeps its primary fill,\n // because switched-on is information (Chip and Pagination keep the\n // wash a selected control already wears) and the native `disabled`\n // attribute carries unavailability to assistive tech regardless.\n // The compound `disabled:data-[state=unchecked]:` form is deliberate:\n // Tailwind sorts data variants after plain `disabled:`, so a bare\n // `disabled:bg-muted` would lose to `data-[state=unchecked]:bg-*` and\n // never fire.\n 'disabled:cursor-not-allowed disabled:data-[state=unchecked]:bg-muted disabled:data-[state=unchecked]:border-border',\n attrs.class as string,\n )\n \"\n @update:model-value=\"$emit('update:modelValue', $event)\"\n >\n <SwitchThumb\n style=\"transition: transform var(--duration-fast) var(--ease-spring)\"\n :class=\"\n cn(\n 'absolute top-1/2
|
|
1
|
+
{"version":3,"file":"Switch.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/switch/src/Switch.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { SwitchRoot, SwitchThumb } from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\n\n/**\n * Switch — toggles a boolean setting that takes effect immediately, with no\n * \"Save\" step. Built on Reka UI's Switch for role=\"switch\" + aria-checked +\n * Space/Enter. Label and description live on the surrounding setting row —\n * wire them in with `aria-labelledby`, since Switch renders a button, not a\n * labelable input.\n *\n * Inside a [Field](../Field/Field.vue) that row is the wrapper itself, and the\n * wiring stops being the caller's job: `useFieldControl()` puts the row's id on\n * the button — labelable, so the row's `<label for>` names the control that has\n * no name of its own — and brings the row's description, `name`, `required` and\n * `invalid` with it.\n *\n * There is no `readonly` here, and a row's is ignored rather than approximated.\n * A switch that cannot be flipped is disabled; one that looks live and refuses\n * the flip is the only state worse than either.\n */\nconst props = withDefaults(\n defineProps<{\n /** Whether the setting is currently on. Omit to let the switch own its own state. */\n modelValue?: boolean | undefined;\n /** Blocks both click and keyboard input, and drains the control to the neutral well. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n }>(),\n {\n // Not `false` — see Checkbox: a concrete default claims host ownership and\n // leaves an unbound switch emitting updates while never moving.\n modelValue: undefined,\n // Not `false` either: absent has to stay tellable from `false` for a Field\n // above to disable the row and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n },\n);\n\ndefineEmits<{ \"update:modelValue\": [value: boolean] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: switchAttrs } = useSplitAttrs();\n\n// One rendered node, so the split is only about `class` — a caller's class\n// needs a Tailwind-aware merge rather than Vue's concatenating fallthrough.\n//\n// `id` and `aria-describedby` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values; `field.attrs` then spreads\n// **after** `switchAttrs`, and that position is the contract (see\n// TextField.vue). `name` in that bag reaches SwitchRoot's own `name` prop,\n// which is what mints the hidden input a `<form>` submits.\n//\n// `name`, `required` and `invalid` are omitted below because Switch has no prop\n// for any of them: the row's answer stands unopposed. `readonly` is omitted for\n// the reason the docblock gives.\n//\n// The resolved `required` then reaches the element through Reka's own prop\n// rather than the `aria-required` in `field.attrs` — see Checkbox.vue, which\n// carries the reasoning. SwitchRoot happens to spread the fallthrough last, so\n// the attribute would win here; going through the prop keeps the four\n// Reka-rooted controls one shape instead of four merge orders to re-derive.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n disabled: props.disabled,\n}));\n</script>\n\n<template>\n <SwitchRoot\n v-bind=\"{ modelValue, ...switchAttrs, ...field.attrs }\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n :class=\"\n cn(\n 'relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full border border-transparent',\n 'transition-[background-color,filter,box-shadow] duration-instant ease-out',\n 'hover:brightness-95',\n // Checked = a decision (Loom law): the track fills with primary.\n 'data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',\n // Focus draws the weave tight: the brand ring blooms.\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Drained, not dimmed. `opacity-50` faded track and thumb together,\n // compositing both below their measured contrast — the same failure\n // Button records for its own dim. The unchecked track falls to\n // `bg-muted`, the well every unavailable control drains to, with the\n // rim Chip slackens to; a *checked* one keeps its primary fill,\n // because switched-on is information (Chip and Pagination keep the\n // wash a selected control already wears) and the native `disabled`\n // attribute carries unavailability to assistive tech regardless.\n // The compound `disabled:data-[state=unchecked]:` form is deliberate:\n // Tailwind sorts data variants after plain `disabled:`, so a bare\n // `disabled:bg-muted` would lose to `data-[state=unchecked]:bg-*` and\n // never fire.\n 'disabled:cursor-not-allowed disabled:data-[state=unchecked]:bg-muted disabled:data-[state=unchecked]:border-border',\n attrs.class as string,\n )\n \"\n @update:model-value=\"$emit('update:modelValue', $event)\"\n >\n <SwitchThumb\n style=\"transition: transform var(--duration-fast) var(--ease-spring)\"\n :class=\"\n cn(\n 'absolute top-1/2 start-0.5 block h-5 w-5 -translate-y-1/2 rounded-full bg-background shadow-sm',\n 'active:scale-x-[1.15] active:scale-y-[0.85]',\n // The anchor is the *inline* start edge, so the off state rests on\n // the reading-order side in every direction — physically left under\n // LTR (pixel-identical to the old `left-0.5`) and right under RTL,\n // where a physical `left` would pin both states to one edge and\n // make the motion read backwards.\n //\n // The travel is still a translate, not an inset change: animating an\n // inset relayouts the track on every frame, while a transform stays\n // on the compositor. Its sign is the one thing direction has left to\n // say — checked moves toward the inline-end edge, +x under LTR, -x\n // under RTL — and it is written through this custom property rather\n // than a second competing utility because Tailwind's `rtl:` wraps in\n // a zero-specificity `:where()`, which would leave the flip's winner\n // to stylesheet order. One rule writes the variable; everything else\n // reads the fallback, so LTR needs no rule at all (and survives even\n // a browser without `:dir()`).\n 'data-[state=checked]:translate-x-[var(--switch-travel-x,1rem)]',\n 'rtl:[--switch-travel-x:-1rem]',\n )\n \"\n />\n </SwitchRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuBA,MAAM,QAAQ;EAsBd,MAAM,EAAE,OAAO,MAAM,gBAAgB,cAAc;EAoBnD,MAAM,QAAQ,uBAAuB;GACnC,IAAI,MAAM;GACV,aAAa,MAAM;GACnB,UAAU,MAAM;EAClB,EAAE;;GAIA,OAAA,UAAA,GAAA,YA0Da,MAAA,UAAA,GA1Db,WA0Da;IAAA,YAzDD,QAAA;IAAU,GAAK,MAAA,WAAA;IAAW,GAAK,MAAA,KAAA,CAAK,CAAC;GAAK,GAAA;IACnD,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,OAAc,MAAA,EAAA,CAAE,CAAA,6GAAA,6EAAA,uBAAA,iFAAA,+GAAA,sHAAqiD,MAAA,KAAA,CAAK,CAAC,KAAA;IAyB3jD,uBAAkB,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,qBAAsB,MAAM;;IAEtD,SAAA,cA0BE,CA1BF,YA0BE,MAAA,WAAA,GAAA;KAzBA,OAAA,EAAA,cAAA,oDAAA;KACC,OAAK,eAAW,MAAA,EAAA,CAAE,CAAA,kGAAA,+CAAA,kEAAA,+BAAA,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { default } from "./Table.vue";
|
|
2
2
|
export { TABLE_LABELS } from "./Table.vue";
|
|
3
3
|
export { default as TableRow, tableRowVariants } from "./TableRow.vue";
|
|
4
|
-
export { default as TableHead, nextSort } from "./TableHead.vue";
|
|
4
|
+
export { default as TableHead, headAlignClass, nextSort } from "./TableHead.vue";
|
|
5
5
|
export { default as TableCell } from "./TableCell.vue";
|
|
6
6
|
export type { TableLabels } from "../../../labels/src/index";
|
|
7
7
|
export type { TableDensity, TableAlign } from "./Table.vue";
|
|
@@ -2,7 +2,7 @@ import { TABLE_LABELS } from "./Table.vue_vue_type_script_setup_true_lang.js";
|
|
|
2
2
|
import Table_default from "./Table.js";
|
|
3
3
|
import { tableRowVariants } from "./TableRow.vue_vue_type_script_setup_true_lang.js";
|
|
4
4
|
import TableRow_default from "./TableRow.js";
|
|
5
|
-
import "./TableHead.vue_vue_type_script_setup_true_lang.js";
|
|
5
|
+
import { headAlignClass, nextSort } from "./TableHead.vue_vue_type_script_setup_true_lang.js";
|
|
6
6
|
import TableHead_default from "./TableHead.js";
|
|
7
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 };
|
|
8
|
+
export { TABLE_LABELS, TableCell_default as TableCell, TableHead_default as TableHead, TableRow_default as TableRow, Table_default as default, headAlignClass, nextSort, tableRowVariants };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimePicker.js","names":[],"sources":["../../../../../packages/primitives/time-picker/src/TimePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TimeSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology: the four names\n * Reka UI would otherwise give the segments in English of its own accord.\n * There is nothing else — the icon is decoration and hidden, and the field\n * group takes its name from the caller or from a wrapping Field.\n *\n * An alias for the shared slice rather than a fifth interface. The same four\n * names are said by `DateTimePicker` and `DateTimeRangePicker`, they come from\n * one Reka implementation, and `src/lib/date-labels.ts` carries why they are\n * declared once. The alias exists so this control's `labels` prop reads like\n * every other one's; annotate with `LabelOverrides<TimePickerLabels>` rather\n * than with this, which is total.\n */\nexport type TimePickerLabels = TimeSegmentLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { TimeFieldInput, TimeFieldRoot, type TimeValue } from \"reka-ui\";\nimport { Clock } from \"@lucide/vue\";\nimport { parseTime } from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport {\n TIME_SEGMENT_LABELS,\n emptySegmentValueText,\n isTimeSegmentPart,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * TimePicker — a time of day, typed into a segmented field. Reach for it\n * wherever a clock time is a value: a meeting start, a reminder, opening hours.\n *\n * **There is no clock face and no dropdown, deliberately.** A clock dial is\n * slower than typing for every reader who can type, and unusable for several\n * who cannot; a 96-row list of quarter hours is worse again. The segmented\n * field already is the control — arrow keys change the segment under the\n * cursor, digits fill it and advance — so a panel would only be a second, worse\n * path to the same value. That is the one place this diverges from DatePicker,\n * where the calendar earns its panel by answering \"which Tuesday?\", a question\n * a time never has.\n *\n * **The model value is a plain `\"HH:mm\"` string in 24-hour time**, or\n * `\"HH:mm:ss\"` when `granularity` asks for seconds — the same boundary\n * DatePicker draws for `\"YYYY-MM-DD\"`, and drawn here for the same reason: a\n * host has a string from its API and should not have to learn a date library to\n * bind a time. `hourCycle` moves the *display* to 12-hour and never the value,\n * because conflating the two is how a 12-hour locale ends up storing `\"01:30\"`\n * for half past one in the afternoon.\n *\n * **A time is not a date.** There is no timezone here, no offset, and no `Date`\n * anywhere near the public surface: `\"09:00\"` is nine in the morning wherever\n * it is read, and the moment that turns into is the host's question, not this\n * component's. Pair it with a DatePicker when an instant is what is wanted.\n *\n * Unlike DatePicker there are no `min`/`max` bounds. A time of day has no\n * ordering that survives midnight — a night shift is \"not before 22:00\" *and*\n * \"not after 02:00\", two bounds that cross rather than fence — so a min/max\n * pair would quietly call 01:00 out of range for the exact case it was reached\n * for. A window like that belongs to the host, which knows which day each end\n * is on.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so `<Field\n * label=\"Starts at\" error=\"…\"><TimePicker /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather than\n * `false`. `readonly` and `disabled` are different states, so the field rests in\n * three appearances rather than two: read-only is a time on show — still a Tab\n * stop, still submitted, a lifted fill with the segments at full strength —\n * where disabled is unavailable and drained a step further, in fill, text\n * colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen time as `\"HH:mm\"`, or `\"HH:mm:ss\"` at second granularity, in\n * 24-hour time whatever the field displays. Unset, empty or unparseable\n * means no time is chosen. The explicit `| undefined` is what lets\n * `v-model` bind a ref that clears — the emitted value drops to `undefined`\n * when the field is emptied, and a plain optional prop refuses to take it\n * back.\n */\n modelValue?: string | undefined;\n /**\n * Whether the field reads as 12-hour with an AM/PM segment or as 24-hour.\n * Display only: the value stays 24-hour either way. Unset follows `locale`,\n * which is what a reader of that locale expects to see.\n */\n hourCycle?: 12 | 24;\n /** How far down the field goes. `\"hour\"` and `\"minute\"` report `\"HH:mm\"`; `\"second\"` adds a seconds segment and reports `\"HH:mm:ss\"`. */\n granularity?: \"hour\" | \"minute\" | \"second\";\n /** BCP 47 tag driving the order of the segments, the separator between them, and the 12-or-24-hour default. */\n locale?: string;\n /** Unavailable: dims the control and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the time without allowing an edit: the field stays a Tab stop, stays submitted, and is filled rather than dimmed. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the time through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for the four segments, as any subset of `TimePickerLabels` — the\n * rest stay as the host's `provideLoomLabels` vocabulary left them, and then\n * as Loom's English.\n *\n * Every one of them replaces a literal Reka writes itself, so this is not\n * decoration: leave the vocabulary unset and the segments are named in\n * English rather than named not at all.\n */\n labels?: LabelOverrides<TimePickerLabels>;\n }>(),\n {\n granularity: \"minute\",\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning: `false` is a caller's decision that must beat the row, absent\n // is a caller saying nothing, and only `undefined` survives to mean that\n // past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen time as `\"HH:mm\"` (or `\"HH:mm:ss\"`), or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\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(\"timeSegments\", TIME_SEGMENT_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. `undefined` for the `literal` separators, which\n// Reka renders `aria-hidden` and which must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return isTimeSegmentPart(part) ? text.value[part] : undefined;\n}\n\n// The companion to `segmentLabel`, and the reason it returns an object rather\n// than a string is in `emptySegmentValueText`: Reka's `aria-valuetext` is a\n// hard-coded \"Empty\" only while the segment holds nothing, so this replaces\n// that case and leaves the filled one — a number — exactly as Reka wrote it.\n//\n// The dayPeriod is the exception. Reka writes `\"AM\"` or `\"PM\"` from its own\n// hour-cycle logic, not from `Intl.DateTimeFormat`, so a localised field\n// still announces the period in English. `filledDayPeriod` lets a host replace\n// those with the locale's own period names (Arabic `\"ص\"`/`\"م\"`, Japanese\n// `\"午前\"`/`\"午後\"`).\nfunction segmentValueText(part: string, value: string): { \"aria-valuetext\"?: string } {\n const empty = emptySegmentValueText(part, value, text.value.empty);\n if (empty[\"aria-valuetext\"]) return empty;\n if (part === \"dayPeriod\") {\n return { \"aria-valuetext\": text.value.filledDayPeriod({ dayPeriod: value }) };\n }\n return {};\n}\n\n// One rendered node, so both halves of the fallthrough land on it — the split\n// exists only because `class` needs the Tailwind-aware merge and Vue's own\n// fallthrough would concatenate a caller's `h-11` after our `h-9` instead of\n// replacing it. That node is Reka's `role=\"group\"`, which is also where\n// `aria-label` belongs: a screen reader announces a group's name once on\n// entering it, rather than once per segment.\n//\n// It is also why `invalid` is announced with `aria-invalid` and nothing else.\n// DatePicker moves its `data-invalid` marker onto the anchor because the field\n// group already spells that attribute with Reka's own meaning; here there is no\n// second element to move it to, and an attribute Reka binds itself wins outright\n// — its template merges `$attrs` first and its own bindings after, so a\n// `data-invalid` passed in from out here is overwritten by Reka's own\n// `undefined` before it reaches the DOM. Setting it would look like it worked\n// and silently do nothing.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The time segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so Reka's hidden input went inert inside a disabled group\n * while the segments a reader actually uses still typed. Measured in jsdom: an\n * empty hour segment became `12` on an arrow key.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are handed in as this caller's own values: a caller who\n// sets either still beats the row, and the row's description is merged into\n// theirs rather than replacing it. `field.attrs` then spreads onto\n// `TimeFieldRoot` **after** `fieldAttrs`, which is the node both were already\n// landing on.\n//\n// **Where each key ends up was checked rather than assumed**, given what the\n// note above says about attributes this node overwrites. The set Reka spells\n// itself is `role`, `aria-disabled`, `data-disabled`, `data-readonly`,\n// `data-invalid` and `dir`, and `field.attrs` spells none of them:\n// `aria-describedby`, `aria-required` and `aria-invalid` are undeclared and land\n// on the `role=\"group\"` as intended, while `id` and `name` are declared props\n// and land on the visually-hidden `<input>` the field submits through — which is\n// the element a `<label for>` can actually name, a `role=\"group\"` div not being\n// labelable.\n//\n// The `:aria-invalid` binding that used to sit on the root is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // native input Reka renders inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row — DatePicker's\n// reasoning, and the same three-way guard.\nconst groupLabelledBy = computed(() =>\n attrs[\"aria-label\"] || attrs[\"aria-labelledby\"] ? undefined : field.labelledBy,\n);\n\nfunction fromClock(value: string | undefined): TimeValue | undefined {\n if (!value) return undefined;\n try {\n return parseTime(value);\n } catch {\n // Same contract as DatePicker: a half-typed or wrongly-shaped value is read\n // as no time chosen. A component library cannot decide that a host's bad\n // data is fatal.\n return undefined;\n }\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n// Written out rather than taken from `Time.toString()`, which always prints\n// seconds and appends a fractional part when the value carries one — so a\n// minute-granularity field would report `\"09:30:00\"`, and a value the host\n// parsed from `\"09:30:00.5\"` would round-trip a half second the field never\n// showed. The string that comes back is the string the segments are showing.\nfunction toClock(time: TimeValue): string {\n const hhmm = `${pad(time.hour)}:${pad(time.minute)}`;\n return props.granularity === \"second\" ? `${hhmm}:${pad(time.second)}` : hhmm;\n}\n\n// `optional()` rather than plain bindings: Reka reads an explicit `undefined`\n// `modelValue` as a value the host owns and is holding empty, which turns an\n// uncontrolled field into a permanently empty one, and an `hourCycle` of\n// `undefined` is how it is told to follow the locale. Dropping the keys says\n// what is true.\nconst rootValue = computed(() =>\n optional({ modelValue: fromClock(props.modelValue), hourCycle: props.hourCycle }),\n);\n\nfunction onTimeChange(time: TimeValue | undefined) {\n emit(\"update:modelValue\", time ? toClock(time) : undefined);\n}\n\n// The field is one Tab stop, not four. The reasoning, and the reason Reka's own\n// ArrowLeft/ArrowRight handling survives it untouched, is written out in\n// DatePicker — it moves focus with `focus()` directly, which ignores tabindex.\n// It matters more here: a 12-hour field with seconds is four segments, so\n// leaving Reka's default in place costs a keyboard reader four Tab presses to\n// cross one control.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // Read-only keeps every stop it had, unlike disabled: the time is on show and\n // a reader still arrows across the segments to read it.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a granularity or locale change takes\n // it away — a field whose only tab stop is a seconds segment that no longer\n // exists is a field nothing can reach.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// Reka reads `hourCycle` once, when it builds its formatter, and seeds its\n// segment values from the granularity in the same breath — neither is watched.\n// Observed: a field showing 13:30 that is moved to a 12-hour cycle relabels\n// itself \"1:30 AM\", half a day wrong, because the AM/PM segment appears with\n// its initial value and nothing recomputes it from the hour. Widening the\n// granularity on an empty field is the same fault with a different symptom: the\n// new seconds segment arrives with no entry to read, so it renders without a\n// role at all.\n//\n// Both are structural changes — they add and remove segments — so keying the\n// field on them rebuilds it rather than patching a half-migrated one. `locale`\n// is deliberately not in the key: Reka does watch that, and rebuilding on it\n// would throw away focus mid-edit for a change it handles correctly.\nconst shape = computed(() => `${props.granularity}-${props.hourCycle}`);\n\n/**\n * Reka's hour segment publishes `aria-valuenow` and `aria-valuetext` straight\n * off the internal 24-hour value while publishing `aria-valuemin`/`max` as\n * 1–12, so half past one in the afternoon announces itself as \"13 PM\", out of\n * its own declared range. The displayed hour is the one the reader is being\n * asked about, so the announcement is rebuilt from what the segment actually\n * shows. Only under an explicit 12-hour cycle: left to the locale, Reka defers\n * to the formatter and the pair is already consistent.\n */\nfunction hourAnnouncement(\n segments: readonly { part: string; value: string }[],\n): Record<string, string | number> | undefined {\n if (props.hourCycle !== 12) return undefined;\n const shown = Number(segments.find((segment) => segment.part === \"hour\")?.value);\n const period = segments.find((segment) => segment.part === \"dayPeriod\")?.value;\n // An unfilled hour reads as its placeholder rather than a number, and there is\n // no number to announce for it. The empty case is not left to Reka's English\n // \"Empty\" either — `segmentValueText` binds the vocabulary's word for it on\n // the same element, and the two never collide: this returns a value only for\n // a filled hour and that one only for an empty segment.\n if (!Number.isInteger(shown) || period === undefined) return undefined;\n return { \"aria-valuenow\": shown, \"aria-valuetext\": `${shown} ${period}` };\n}\n</script>\n\n<template>\n <TimeFieldRoot\n ref=\"root\"\n :key=\"shape\"\n v-slot=\"{ segments }\"\n v-bind=\"{ ...rootValue, ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :locale=\"locale\"\n :granularity=\"granularity\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :class=\"\n cn(\n // The text input's own height scale, so a time sitting in a form row\n // beside a text field lines up rather than nearly lining up. No gap:\n // the colon between two segments is itself a segment, so spacing them\n // apart writes the time as `09 : 30`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring and the focused segment below carries a fill\n // instead of a second outline, exactly as DatePicker's field does — a\n // ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and each is painted on this group rather\n // than on the segments inside it: a per-segment fill leaves the colon\n // between them on `background`, and the time comes out striped.\n //\n // Read-only lifts to `subtle` and keeps its focus ring and its\n // full-strength segments — 13.46:1 over that fill — because a read-only\n // time is a value on show, not an unavailable control. The rim stays\n // `input`: the field is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this node carries the same state to assistive tech,\n // so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Drained rather than faded, for the reason DatePicker and\n // DateTimePicker both write out — this control was the one member of\n // the family the sweep still caught. `opacity-50` took the segments\n // down with the box: measured on the built site, the hour and the\n // period read 3.08:1 and the colon between them 2.07:1, and those\n // segments *are* the value. The fill drains one step past read-only,\n // the muted text over it is 4.68:1, and the rim slackens from `input`\n // to `border` — three channels, so the two states do not part on hue\n // alone. Set on the group because no segment declares a resting colour\n // of its own, so each inherits this one.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour: `cn()` resolves one by\n // whichever class it saw last, so a field both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onTimeChange\"\n >\n <!-- Leading, where DatePicker's calendar button is trailing: that trailing\n slot is a button in every other member of this family, and a glyph\n sitting in it that opens nothing would promise a panel this control\n does not have. Decoration, so it is hidden from the accessibility\n tree — the group's own name says what the field is. -->\n <Clock aria-hidden=\"true\" class=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\n\n <!-- The `:aria-label` replaces the one Reka writes in English inside its\n own render function. It reaches the DOM node as a fallthrough\n attribute, which Vue merges last, so it is a replacement rather than a\n second name. Removing it does not fall back to nothing; it falls back\n to Reka's English. -->\n <TimeFieldInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"{\n ...segmentValueText(item.part, item.value),\n ...(item.part === 'hour' ? hourAnnouncement(segments) : undefined),\n }\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </TimeFieldInput>\n </TimeFieldRoot>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"TimePicker.js","names":[],"sources":["../../../../../packages/primitives/time-picker/src/TimePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TimeSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology: the four names\n * Reka UI would otherwise give the segments in English of its own accord.\n * There is nothing else — the icon is decoration and hidden, and the field\n * group takes its name from the caller or from a wrapping Field.\n *\n * An alias for the shared slice rather than a fifth interface. The same four\n * names are said by `DateTimePicker` and `DateTimeRangePicker`, they come from\n * one Reka implementation, and `src/lib/date-labels.ts` carries why they are\n * declared once. The alias exists so this control's `labels` prop reads like\n * every other one's; annotate with `LabelOverrides<TimePickerLabels>` rather\n * than with this, which is total.\n */\nexport type TimePickerLabels = TimeSegmentLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { TimeFieldInput, TimeFieldRoot, type TimeValue } from \"reka-ui\";\nimport { Clock } from \"@lucide/vue\";\nimport { parseTime } from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { TIME_SEGMENT_LABELS, segmentAriaLabel, segmentAriaValueText } from \"@ecoma-io/loom-labels\";\n\n/**\n * TimePicker — a time of day, typed into a segmented field. Reach for it\n * wherever a clock time is a value: a meeting start, a reminder, opening hours.\n *\n * **There is no clock face and no dropdown, deliberately.** A clock dial is\n * slower than typing for every reader who can type, and unusable for several\n * who cannot; a 96-row list of quarter hours is worse again. The segmented\n * field already is the control — arrow keys change the segment under the\n * cursor, digits fill it and advance — so a panel would only be a second, worse\n * path to the same value. That is the one place this diverges from DatePicker,\n * where the calendar earns its panel by answering \"which Tuesday?\", a question\n * a time never has.\n *\n * **The model value is a plain `\"HH:mm\"` string in 24-hour time**, or\n * `\"HH:mm:ss\"` when `granularity` asks for seconds — the same boundary\n * DatePicker draws for `\"YYYY-MM-DD\"`, and drawn here for the same reason: a\n * host has a string from its API and should not have to learn a date library to\n * bind a time. `hourCycle` moves the *display* to 12-hour and never the value,\n * because conflating the two is how a 12-hour locale ends up storing `\"01:30\"`\n * for half past one in the afternoon.\n *\n * **A time is not a date.** There is no timezone here, no offset, and no `Date`\n * anywhere near the public surface: `\"09:00\"` is nine in the morning wherever\n * it is read, and the moment that turns into is the host's question, not this\n * component's. Pair it with a DatePicker when an instant is what is wanted.\n *\n * Unlike DatePicker there are no `min`/`max` bounds. A time of day has no\n * ordering that survives midnight — a night shift is \"not before 22:00\" *and*\n * \"not after 02:00\", two bounds that cross rather than fence — so a min/max\n * pair would quietly call 01:00 out of range for the exact case it was reached\n * for. A window like that belongs to the host, which knows which day each end\n * is on.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so `<Field\n * label=\"Starts at\" error=\"…\"><TimePicker /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather than\n * `false`. `readonly` and `disabled` are different states, so the field rests in\n * three appearances rather than two: read-only is a time on show — still a Tab\n * stop, still submitted, a lifted fill with the segments at full strength —\n * where disabled is unavailable and drained a step further, in fill, text\n * colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen time as `\"HH:mm\"`, or `\"HH:mm:ss\"` at second granularity, in\n * 24-hour time whatever the field displays. Unset, empty or unparseable\n * means no time is chosen. The explicit `| undefined` is what lets\n * `v-model` bind a ref that clears — the emitted value drops to `undefined`\n * when the field is emptied, and a plain optional prop refuses to take it\n * back.\n */\n modelValue?: string | undefined;\n /**\n * Whether the field reads as 12-hour with an AM/PM segment or as 24-hour.\n * Display only: the value stays 24-hour either way. Unset follows `locale`,\n * which is what a reader of that locale expects to see.\n */\n hourCycle?: 12 | 24;\n /** How far down the field goes. `\"hour\"` and `\"minute\"` report `\"HH:mm\"`; `\"second\"` adds a seconds segment and reports `\"HH:mm:ss\"`. */\n granularity?: \"hour\" | \"minute\" | \"second\";\n /** BCP 47 tag driving the order of the segments, the separator between them, and the 12-or-24-hour default. */\n locale?: string;\n /** Unavailable: dims the control and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the time without allowing an edit: the field stays a Tab stop, stays submitted, and is filled rather than dimmed. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the time through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for the four segments, as any subset of `TimePickerLabels` — the\n * rest stay as the host's `provideLoomLabels` vocabulary left them, and then\n * as Loom's English.\n *\n * Every one of them replaces a literal Reka writes itself, so this is not\n * decoration: leave the vocabulary unset and the segments are named in\n * English rather than named not at all.\n */\n labels?: LabelOverrides<TimePickerLabels>;\n }>(),\n {\n granularity: \"minute\",\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning: `false` is a caller's decision that must beat the row, absent\n // is a caller saying nothing, and only `undefined` survives to mean that\n // past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen time as `\"HH:mm\"` (or `\"HH:mm:ss\"`), or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\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(\"timeSegments\", TIME_SEGMENT_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. The resolution rules themselves are the shared\n// helpers' — five hand-copied pairs of these two functions drifted once\n// already, so the branches live in `@ecoma-io/loom-labels` now and only the\n// refs stay here. `undefined` for the `literal` separators, which Reka renders\n// `aria-hidden` and which must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return segmentAriaLabel({ time: text.value }, part);\n}\n\nfunction segmentValueText(part: string, value: string): { \"aria-valuetext\"?: string } {\n return segmentAriaValueText({ time: text.value, part, value, locale: props.locale });\n}\n\n// One rendered node, so both halves of the fallthrough land on it — the split\n// exists only because `class` needs the Tailwind-aware merge and Vue's own\n// fallthrough would concatenate a caller's `h-11` after our `h-9` instead of\n// replacing it. That node is Reka's `role=\"group\"`, which is also where\n// `aria-label` belongs: a screen reader announces a group's name once on\n// entering it, rather than once per segment.\n//\n// It is also why `invalid` is announced with `aria-invalid` and nothing else.\n// DatePicker moves its `data-invalid` marker onto the anchor because the field\n// group already spells that attribute with Reka's own meaning; here there is no\n// second element to move it to, and an attribute Reka binds itself wins outright\n// — its template merges `$attrs` first and its own bindings after, so a\n// `data-invalid` passed in from out here is overwritten by Reka's own\n// `undefined` before it reaches the DOM. Setting it would look like it worked\n// and silently do nothing.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The time segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so Reka's hidden input went inert inside a disabled group\n * while the segments a reader actually uses still typed. Measured in jsdom: an\n * empty hour segment became `12` on an arrow key.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are handed in as this caller's own values: a caller who\n// sets either still beats the row, and the row's description is merged into\n// theirs rather than replacing it. `field.attrs` then spreads onto\n// `TimeFieldRoot` **after** `fieldAttrs`, which is the node both were already\n// landing on.\n//\n// **Where each key ends up was checked rather than assumed**, given what the\n// note above says about attributes this node overwrites. The set Reka spells\n// itself is `role`, `aria-disabled`, `data-disabled`, `data-readonly`,\n// `data-invalid` and `dir`, and `field.attrs` spells none of them:\n// `aria-describedby`, `aria-required` and `aria-invalid` are undeclared and land\n// on the `role=\"group\"` as intended, while `id` and `name` are declared props\n// and land on the visually-hidden `<input>` the field submits through — which is\n// the element a `<label for>` can actually name, a `role=\"group\"` div not being\n// labelable.\n//\n// The `:aria-invalid` binding that used to sit on the root is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // native input Reka renders inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row — DatePicker's\n// reasoning, minus the old third arm of the guard: a caller's own labelledby\n// has to survive here, because the binding below beats the spread and an\n// `undefined` would strip it instead of deferring to it.\nconst groupLabelledBy = computed(() => (attrs[\"aria-label\"] ? undefined : field.labelledBy));\n\nfunction fromClock(value: string | undefined): TimeValue | undefined {\n if (!value) return undefined;\n try {\n return parseTime(value);\n } catch {\n // Same contract as DatePicker: a half-typed or wrongly-shaped value is read\n // as no time chosen. A component library cannot decide that a host's bad\n // data is fatal.\n return undefined;\n }\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n// Written out rather than taken from `Time.toString()`, which always prints\n// seconds and appends a fractional part when the value carries one — so a\n// minute-granularity field would report `\"09:30:00\"`, and a value the host\n// parsed from `\"09:30:00.5\"` would round-trip a half second the field never\n// showed. The string that comes back is the string the segments are showing.\nfunction toClock(time: TimeValue): string {\n const hhmm = `${pad(time.hour)}:${pad(time.minute)}`;\n return props.granularity === \"second\" ? `${hhmm}:${pad(time.second)}` : hhmm;\n}\n\n// `optional()` rather than plain bindings: Reka reads an explicit `undefined`\n// `modelValue` as a value the host owns and is holding empty, which turns an\n// uncontrolled field into a permanently empty one, and an `hourCycle` of\n// `undefined` is how it is told to follow the locale. Dropping the keys says\n// what is true.\nconst rootValue = computed(() =>\n optional({ modelValue: fromClock(props.modelValue), hourCycle: props.hourCycle }),\n);\n\nfunction onTimeChange(time: TimeValue | undefined) {\n emit(\"update:modelValue\", time ? toClock(time) : undefined);\n}\n\n// The field is one Tab stop, not four. The reasoning, and the reason Reka's own\n// ArrowLeft/ArrowRight handling survives it untouched, is written out in\n// DatePicker — it moves focus with `focus()` directly, which ignores tabindex.\n// It matters more here: a 12-hour field with seconds is four segments, so\n// leaving Reka's default in place costs a keyboard reader four Tab presses to\n// cross one control.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // Read-only keeps every stop it had, unlike disabled: the time is on show and\n // a reader still arrows across the segments to read it.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a granularity or locale change takes\n // it away — a field whose only tab stop is a seconds segment that no longer\n // exists is a field nothing can reach.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// Reka reads `hourCycle` once, when it builds its formatter, and seeds its\n// segment values from the granularity in the same breath — neither is watched.\n// Observed: a field showing 13:30 that is moved to a 12-hour cycle relabels\n// itself \"1:30 AM\", half a day wrong, because the AM/PM segment appears with\n// its initial value and nothing recomputes it from the hour. Widening the\n// granularity on an empty field is the same fault with a different symptom: the\n// new seconds segment arrives with no entry to read, so it renders without a\n// role at all.\n//\n// Both are structural changes — they add and remove segments — so keying the\n// field on them rebuilds it rather than patching a half-migrated one. `locale`\n// is deliberately not in the key: Reka does watch that, and rebuilding on it\n// would throw away focus mid-edit for a change it handles correctly.\nconst shape = computed(() => `${props.granularity}-${props.hourCycle}`);\n\n/**\n * Reka's hour segment publishes `aria-valuenow` and `aria-valuetext` straight\n * off the internal 24-hour value while publishing `aria-valuemin`/`max` as\n * 1–12, so half past one in the afternoon announces itself as \"13 PM\", out of\n * its own declared range. The displayed hour is the one the reader is being\n * asked about, so the announcement is rebuilt from what the segment actually\n * shows. Only under an explicit 12-hour cycle: left to the locale, Reka defers\n * to the formatter and the pair is already consistent.\n */\nfunction hourAnnouncement(\n segments: readonly { part: string; value: string }[],\n): Record<string, string | number> | undefined {\n if (props.hourCycle !== 12) return undefined;\n const shown = Number(segments.find((segment) => segment.part === \"hour\")?.value);\n const period = segments.find((segment) => segment.part === \"dayPeriod\")?.value;\n // An unfilled hour reads as its placeholder rather than a number, and there is\n // no number to announce for it. The empty case is not left to Reka's English\n // \"Empty\" either — `segmentValueText` binds the vocabulary's word for it on\n // the same element, and the two never collide: this returns a value only for\n // a filled hour and that one only for an empty segment.\n if (!Number.isInteger(shown) || period === undefined) return undefined;\n return { \"aria-valuenow\": shown, \"aria-valuetext\": `${shown} ${period}` };\n}\n</script>\n\n<template>\n <TimeFieldRoot\n ref=\"root\"\n :key=\"shape\"\n v-slot=\"{ segments }\"\n v-bind=\"{ ...rootValue, ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :locale=\"locale\"\n :granularity=\"granularity\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :class=\"\n cn(\n // The text input's own height scale, so a time sitting in a form row\n // beside a text field lines up rather than nearly lining up. No gap:\n // the colon between two segments is itself a segment, so spacing them\n // apart writes the time as `09 : 30`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring and the focused segment below carries a fill\n // instead of a second outline, exactly as DatePicker's field does — a\n // ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and each is painted on this group rather\n // than on the segments inside it: a per-segment fill leaves the colon\n // between them on `background`, and the time comes out striped.\n //\n // Read-only lifts to `subtle` and keeps its focus ring and its\n // full-strength segments — 13.46:1 over that fill — because a read-only\n // time is a value on show, not an unavailable control. The rim stays\n // `input`: the field is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this node carries the same state to assistive tech,\n // so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Drained rather than faded, for the reason DatePicker and\n // DateTimePicker both write out — this control was the one member of\n // the family the sweep still caught. `opacity-50` took the segments\n // down with the box: measured on the built site, the hour and the\n // period read 3.08:1 and the colon between them 2.07:1, and those\n // segments *are* the value. The fill drains one step past read-only,\n // the muted text over it is 4.68:1, and the rim slackens from `input`\n // to `border` — three channels, so the two states do not part on hue\n // alone. Set on the group because no segment declares a resting colour\n // of its own, so each inherits this one.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour: `cn()` resolves one by\n // whichever class it saw last, so a field both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onTimeChange\"\n >\n <!-- Leading, where DatePicker's calendar button is trailing: that trailing\n slot is a button in every other member of this family, and a glyph\n sitting in it that opens nothing would promise a panel this control\n does not have. Decoration, so it is hidden from the accessibility\n tree — the group's own name says what the field is. -->\n <Clock aria-hidden=\"true\" class=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\n\n <!-- The `:aria-label` replaces the one Reka writes in English inside its\n own render function. It reaches the DOM node as a fallthrough\n attribute, which Vue merges last, so it is a replacement rather than a\n second name. Removing it does not fall back to nothing; it falls back\n to Reka's English. -->\n <TimeFieldInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"{\n ...segmentValueText(item.part, item.value),\n ...(item.part === 'hour' ? hourAnnouncement(segments) : undefined),\n }\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </TimeFieldInput>\n </TimeFieldRoot>\n</template>\n"],"mappings":""}
|
package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -3,8 +3,8 @@ import { optional } from "../../../core/src/props.js";
|
|
|
3
3
|
import { useSplitAttrs } from "../../../core/src/attrs.js";
|
|
4
4
|
import "../../../core/src/index.js";
|
|
5
5
|
import { useLabels } from "../../../labels/src/labels.js";
|
|
6
|
-
import { parseTime as $58246871e4652552$export$c9698ec7f05a07e1 } from "../../../../node_modules/.pnpm/@internationalized_date@3.12.
|
|
7
|
-
import { TIME_SEGMENT_LABELS,
|
|
6
|
+
import { parseTime as $58246871e4652552$export$c9698ec7f05a07e1 } from "../../../../node_modules/.pnpm/@internationalized_date@3.12.4/node_modules/@internationalized/date/dist/private/string.js";
|
|
7
|
+
import { TIME_SEGMENT_LABELS, segmentAriaLabel, segmentAriaValueText } from "../../../labels/src/date-labels.js";
|
|
8
8
|
import { useFieldControl } from "../../../labels/src/field-context.js";
|
|
9
9
|
import { useAncestorDisabled } from "../../../labels/src/ancestor-disabled.js";
|
|
10
10
|
import "../../../labels/src/index.js";
|
|
@@ -45,13 +45,15 @@ var TimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
|
|
|
45
45
|
const emit = __emit;
|
|
46
46
|
const text = useLabels("timeSegments", TIME_SEGMENT_LABELS, () => props.labels);
|
|
47
47
|
function segmentLabel(part) {
|
|
48
|
-
return
|
|
48
|
+
return segmentAriaLabel({ time: text.value }, part);
|
|
49
49
|
}
|
|
50
50
|
function segmentValueText(part, value) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
return segmentAriaValueText({
|
|
52
|
+
time: text.value,
|
|
53
|
+
part,
|
|
54
|
+
value,
|
|
55
|
+
locale: props.locale
|
|
56
|
+
});
|
|
55
57
|
}
|
|
56
58
|
const { attrs, rest: fieldAttrs } = useSplitAttrs();
|
|
57
59
|
/**
|
|
@@ -76,7 +78,7 @@ var TimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
|
|
|
76
78
|
invalid: props.invalid,
|
|
77
79
|
required: props.required
|
|
78
80
|
}));
|
|
79
|
-
const groupLabelledBy = computed(() => attrs["aria-label"]
|
|
81
|
+
const groupLabelledBy = computed(() => attrs["aria-label"] ? void 0 : field.labelledBy);
|
|
80
82
|
function fromClock(value) {
|
|
81
83
|
if (!value) return void 0;
|
|
82
84
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimePicker.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/time-picker/src/TimePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TimeSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology: the four names\n * Reka UI would otherwise give the segments in English of its own accord.\n * There is nothing else — the icon is decoration and hidden, and the field\n * group takes its name from the caller or from a wrapping Field.\n *\n * An alias for the shared slice rather than a fifth interface. The same four\n * names are said by `DateTimePicker` and `DateTimeRangePicker`, they come from\n * one Reka implementation, and `src/lib/date-labels.ts` carries why they are\n * declared once. The alias exists so this control's `labels` prop reads like\n * every other one's; annotate with `LabelOverrides<TimePickerLabels>` rather\n * than with this, which is total.\n */\nexport type TimePickerLabels = TimeSegmentLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { TimeFieldInput, TimeFieldRoot, type TimeValue } from \"reka-ui\";\nimport { Clock } from \"@lucide/vue\";\nimport { parseTime } from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport {\n TIME_SEGMENT_LABELS,\n emptySegmentValueText,\n isTimeSegmentPart,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * TimePicker — a time of day, typed into a segmented field. Reach for it\n * wherever a clock time is a value: a meeting start, a reminder, opening hours.\n *\n * **There is no clock face and no dropdown, deliberately.** A clock dial is\n * slower than typing for every reader who can type, and unusable for several\n * who cannot; a 96-row list of quarter hours is worse again. The segmented\n * field already is the control — arrow keys change the segment under the\n * cursor, digits fill it and advance — so a panel would only be a second, worse\n * path to the same value. That is the one place this diverges from DatePicker,\n * where the calendar earns its panel by answering \"which Tuesday?\", a question\n * a time never has.\n *\n * **The model value is a plain `\"HH:mm\"` string in 24-hour time**, or\n * `\"HH:mm:ss\"` when `granularity` asks for seconds — the same boundary\n * DatePicker draws for `\"YYYY-MM-DD\"`, and drawn here for the same reason: a\n * host has a string from its API and should not have to learn a date library to\n * bind a time. `hourCycle` moves the *display* to 12-hour and never the value,\n * because conflating the two is how a 12-hour locale ends up storing `\"01:30\"`\n * for half past one in the afternoon.\n *\n * **A time is not a date.** There is no timezone here, no offset, and no `Date`\n * anywhere near the public surface: `\"09:00\"` is nine in the morning wherever\n * it is read, and the moment that turns into is the host's question, not this\n * component's. Pair it with a DatePicker when an instant is what is wanted.\n *\n * Unlike DatePicker there are no `min`/`max` bounds. A time of day has no\n * ordering that survives midnight — a night shift is \"not before 22:00\" *and*\n * \"not after 02:00\", two bounds that cross rather than fence — so a min/max\n * pair would quietly call 01:00 out of range for the exact case it was reached\n * for. A window like that belongs to the host, which knows which day each end\n * is on.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so `<Field\n * label=\"Starts at\" error=\"…\"><TimePicker /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather than\n * `false`. `readonly` and `disabled` are different states, so the field rests in\n * three appearances rather than two: read-only is a time on show — still a Tab\n * stop, still submitted, a lifted fill with the segments at full strength —\n * where disabled is unavailable and drained a step further, in fill, text\n * colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen time as `\"HH:mm\"`, or `\"HH:mm:ss\"` at second granularity, in\n * 24-hour time whatever the field displays. Unset, empty or unparseable\n * means no time is chosen. The explicit `| undefined` is what lets\n * `v-model` bind a ref that clears — the emitted value drops to `undefined`\n * when the field is emptied, and a plain optional prop refuses to take it\n * back.\n */\n modelValue?: string | undefined;\n /**\n * Whether the field reads as 12-hour with an AM/PM segment or as 24-hour.\n * Display only: the value stays 24-hour either way. Unset follows `locale`,\n * which is what a reader of that locale expects to see.\n */\n hourCycle?: 12 | 24;\n /** How far down the field goes. `\"hour\"` and `\"minute\"` report `\"HH:mm\"`; `\"second\"` adds a seconds segment and reports `\"HH:mm:ss\"`. */\n granularity?: \"hour\" | \"minute\" | \"second\";\n /** BCP 47 tag driving the order of the segments, the separator between them, and the 12-or-24-hour default. */\n locale?: string;\n /** Unavailable: dims the control and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the time without allowing an edit: the field stays a Tab stop, stays submitted, and is filled rather than dimmed. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the time through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for the four segments, as any subset of `TimePickerLabels` — the\n * rest stay as the host's `provideLoomLabels` vocabulary left them, and then\n * as Loom's English.\n *\n * Every one of them replaces a literal Reka writes itself, so this is not\n * decoration: leave the vocabulary unset and the segments are named in\n * English rather than named not at all.\n */\n labels?: LabelOverrides<TimePickerLabels>;\n }>(),\n {\n granularity: \"minute\",\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning: `false` is a caller's decision that must beat the row, absent\n // is a caller saying nothing, and only `undefined` survives to mean that\n // past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen time as `\"HH:mm\"` (or `\"HH:mm:ss\"`), or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\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(\"timeSegments\", TIME_SEGMENT_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. `undefined` for the `literal` separators, which\n// Reka renders `aria-hidden` and which must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return isTimeSegmentPart(part) ? text.value[part] : undefined;\n}\n\n// The companion to `segmentLabel`, and the reason it returns an object rather\n// than a string is in `emptySegmentValueText`: Reka's `aria-valuetext` is a\n// hard-coded \"Empty\" only while the segment holds nothing, so this replaces\n// that case and leaves the filled one — a number — exactly as Reka wrote it.\n//\n// The dayPeriod is the exception. Reka writes `\"AM\"` or `\"PM\"` from its own\n// hour-cycle logic, not from `Intl.DateTimeFormat`, so a localised field\n// still announces the period in English. `filledDayPeriod` lets a host replace\n// those with the locale's own period names (Arabic `\"ص\"`/`\"م\"`, Japanese\n// `\"午前\"`/`\"午後\"`).\nfunction segmentValueText(part: string, value: string): { \"aria-valuetext\"?: string } {\n const empty = emptySegmentValueText(part, value, text.value.empty);\n if (empty[\"aria-valuetext\"]) return empty;\n if (part === \"dayPeriod\") {\n return { \"aria-valuetext\": text.value.filledDayPeriod({ dayPeriod: value }) };\n }\n return {};\n}\n\n// One rendered node, so both halves of the fallthrough land on it — the split\n// exists only because `class` needs the Tailwind-aware merge and Vue's own\n// fallthrough would concatenate a caller's `h-11` after our `h-9` instead of\n// replacing it. That node is Reka's `role=\"group\"`, which is also where\n// `aria-label` belongs: a screen reader announces a group's name once on\n// entering it, rather than once per segment.\n//\n// It is also why `invalid` is announced with `aria-invalid` and nothing else.\n// DatePicker moves its `data-invalid` marker onto the anchor because the field\n// group already spells that attribute with Reka's own meaning; here there is no\n// second element to move it to, and an attribute Reka binds itself wins outright\n// — its template merges `$attrs` first and its own bindings after, so a\n// `data-invalid` passed in from out here is overwritten by Reka's own\n// `undefined` before it reaches the DOM. Setting it would look like it worked\n// and silently do nothing.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The time segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so Reka's hidden input went inert inside a disabled group\n * while the segments a reader actually uses still typed. Measured in jsdom: an\n * empty hour segment became `12` on an arrow key.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are handed in as this caller's own values: a caller who\n// sets either still beats the row, and the row's description is merged into\n// theirs rather than replacing it. `field.attrs` then spreads onto\n// `TimeFieldRoot` **after** `fieldAttrs`, which is the node both were already\n// landing on.\n//\n// **Where each key ends up was checked rather than assumed**, given what the\n// note above says about attributes this node overwrites. The set Reka spells\n// itself is `role`, `aria-disabled`, `data-disabled`, `data-readonly`,\n// `data-invalid` and `dir`, and `field.attrs` spells none of them:\n// `aria-describedby`, `aria-required` and `aria-invalid` are undeclared and land\n// on the `role=\"group\"` as intended, while `id` and `name` are declared props\n// and land on the visually-hidden `<input>` the field submits through — which is\n// the element a `<label for>` can actually name, a `role=\"group\"` div not being\n// labelable.\n//\n// The `:aria-invalid` binding that used to sit on the root is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // native input Reka renders inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row — DatePicker's\n// reasoning, and the same three-way guard.\nconst groupLabelledBy = computed(() =>\n attrs[\"aria-label\"] || attrs[\"aria-labelledby\"] ? undefined : field.labelledBy,\n);\n\nfunction fromClock(value: string | undefined): TimeValue | undefined {\n if (!value) return undefined;\n try {\n return parseTime(value);\n } catch {\n // Same contract as DatePicker: a half-typed or wrongly-shaped value is read\n // as no time chosen. A component library cannot decide that a host's bad\n // data is fatal.\n return undefined;\n }\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n// Written out rather than taken from `Time.toString()`, which always prints\n// seconds and appends a fractional part when the value carries one — so a\n// minute-granularity field would report `\"09:30:00\"`, and a value the host\n// parsed from `\"09:30:00.5\"` would round-trip a half second the field never\n// showed. The string that comes back is the string the segments are showing.\nfunction toClock(time: TimeValue): string {\n const hhmm = `${pad(time.hour)}:${pad(time.minute)}`;\n return props.granularity === \"second\" ? `${hhmm}:${pad(time.second)}` : hhmm;\n}\n\n// `optional()` rather than plain bindings: Reka reads an explicit `undefined`\n// `modelValue` as a value the host owns and is holding empty, which turns an\n// uncontrolled field into a permanently empty one, and an `hourCycle` of\n// `undefined` is how it is told to follow the locale. Dropping the keys says\n// what is true.\nconst rootValue = computed(() =>\n optional({ modelValue: fromClock(props.modelValue), hourCycle: props.hourCycle }),\n);\n\nfunction onTimeChange(time: TimeValue | undefined) {\n emit(\"update:modelValue\", time ? toClock(time) : undefined);\n}\n\n// The field is one Tab stop, not four. The reasoning, and the reason Reka's own\n// ArrowLeft/ArrowRight handling survives it untouched, is written out in\n// DatePicker — it moves focus with `focus()` directly, which ignores tabindex.\n// It matters more here: a 12-hour field with seconds is four segments, so\n// leaving Reka's default in place costs a keyboard reader four Tab presses to\n// cross one control.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // Read-only keeps every stop it had, unlike disabled: the time is on show and\n // a reader still arrows across the segments to read it.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a granularity or locale change takes\n // it away — a field whose only tab stop is a seconds segment that no longer\n // exists is a field nothing can reach.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// Reka reads `hourCycle` once, when it builds its formatter, and seeds its\n// segment values from the granularity in the same breath — neither is watched.\n// Observed: a field showing 13:30 that is moved to a 12-hour cycle relabels\n// itself \"1:30 AM\", half a day wrong, because the AM/PM segment appears with\n// its initial value and nothing recomputes it from the hour. Widening the\n// granularity on an empty field is the same fault with a different symptom: the\n// new seconds segment arrives with no entry to read, so it renders without a\n// role at all.\n//\n// Both are structural changes — they add and remove segments — so keying the\n// field on them rebuilds it rather than patching a half-migrated one. `locale`\n// is deliberately not in the key: Reka does watch that, and rebuilding on it\n// would throw away focus mid-edit for a change it handles correctly.\nconst shape = computed(() => `${props.granularity}-${props.hourCycle}`);\n\n/**\n * Reka's hour segment publishes `aria-valuenow` and `aria-valuetext` straight\n * off the internal 24-hour value while publishing `aria-valuemin`/`max` as\n * 1–12, so half past one in the afternoon announces itself as \"13 PM\", out of\n * its own declared range. The displayed hour is the one the reader is being\n * asked about, so the announcement is rebuilt from what the segment actually\n * shows. Only under an explicit 12-hour cycle: left to the locale, Reka defers\n * to the formatter and the pair is already consistent.\n */\nfunction hourAnnouncement(\n segments: readonly { part: string; value: string }[],\n): Record<string, string | number> | undefined {\n if (props.hourCycle !== 12) return undefined;\n const shown = Number(segments.find((segment) => segment.part === \"hour\")?.value);\n const period = segments.find((segment) => segment.part === \"dayPeriod\")?.value;\n // An unfilled hour reads as its placeholder rather than a number, and there is\n // no number to announce for it. The empty case is not left to Reka's English\n // \"Empty\" either — `segmentValueText` binds the vocabulary's word for it on\n // the same element, and the two never collide: this returns a value only for\n // a filled hour and that one only for an empty segment.\n if (!Number.isInteger(shown) || period === undefined) return undefined;\n return { \"aria-valuenow\": shown, \"aria-valuetext\": `${shown} ${period}` };\n}\n</script>\n\n<template>\n <TimeFieldRoot\n ref=\"root\"\n :key=\"shape\"\n v-slot=\"{ segments }\"\n v-bind=\"{ ...rootValue, ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :locale=\"locale\"\n :granularity=\"granularity\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :class=\"\n cn(\n // The text input's own height scale, so a time sitting in a form row\n // beside a text field lines up rather than nearly lining up. No gap:\n // the colon between two segments is itself a segment, so spacing them\n // apart writes the time as `09 : 30`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring and the focused segment below carries a fill\n // instead of a second outline, exactly as DatePicker's field does — a\n // ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and each is painted on this group rather\n // than on the segments inside it: a per-segment fill leaves the colon\n // between them on `background`, and the time comes out striped.\n //\n // Read-only lifts to `subtle` and keeps its focus ring and its\n // full-strength segments — 13.46:1 over that fill — because a read-only\n // time is a value on show, not an unavailable control. The rim stays\n // `input`: the field is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this node carries the same state to assistive tech,\n // so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Drained rather than faded, for the reason DatePicker and\n // DateTimePicker both write out — this control was the one member of\n // the family the sweep still caught. `opacity-50` took the segments\n // down with the box: measured on the built site, the hour and the\n // period read 3.08:1 and the colon between them 2.07:1, and those\n // segments *are* the value. The fill drains one step past read-only,\n // the muted text over it is 4.68:1, and the rim slackens from `input`\n // to `border` — three channels, so the two states do not part on hue\n // alone. Set on the group because no segment declares a resting colour\n // of its own, so each inherits this one.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour: `cn()` resolves one by\n // whichever class it saw last, so a field both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onTimeChange\"\n >\n <!-- Leading, where DatePicker's calendar button is trailing: that trailing\n slot is a button in every other member of this family, and a glyph\n sitting in it that opens nothing would promise a panel this control\n does not have. Decoration, so it is hidden from the accessibility\n tree — the group's own name says what the field is. -->\n <Clock aria-hidden=\"true\" class=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\n\n <!-- The `:aria-label` replaces the one Reka writes in English inside its\n own render function. It reaches the DOM node as a fallthrough\n attribute, which Vue merges last, so it is a replacement rather than a\n second name. Removing it does not fall back to nothing; it falls back\n to Reka's English. -->\n <TimeFieldInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"{\n ...segmentValueText(item.part, item.value),\n ...(item.part === 'hour' ? hourAnnouncement(segments) : undefined),\n }\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </TimeFieldInput>\n </TimeFieldRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiFA,MAAM,QAAQ;EAwDd,MAAM,OAAO;EAQb,MAAM,OAAO,UAAU,gBAAgB,2BAA2B,MAAM,MAAM;EAM9E,SAAS,aAAa,MAAkC;GACtD,OAAO,kBAAkB,IAAI,IAAI,KAAK,MAAM,QAAQ,KAAA;EACtD;EAYA,SAAS,iBAAiB,MAAc,OAA8C;GACpF,MAAM,QAAQ,sBAAsB,MAAM,OAAO,KAAK,MAAM,KAAK;GACjE,IAAI,MAAM,mBAAmB,OAAO;GACpC,IAAI,SAAS,aACX,OAAO,EAAE,kBAAkB,KAAK,MAAM,gBAAgB,EAAE,WAAW,MAAM,CAAC,EAAE;GAE9E,OAAO,CAAC;EACV;EAkBA,MAAM,EAAE,OAAO,MAAM,eAAe,cAAc;;;;;;;;;;;EAYlD,MAAM,OAAO,eAAkC,MAAM;EACrD,MAAM,gBAAgB,0BAA0B,KAAK,OAAO,GAAG;EAsB/D,MAAM,QAAQ,uBAAuB;GACnC,IAAI,MAAM;GACV,aAAa,MAAM;GACnB,gBAAgB,MAAM;GACtB,MAAM,MAAM;GAGZ,UAAU,cAAc,QAAQ,OAAO,MAAM;GAC7C,UAAU,MAAM;GAChB,SAAS,MAAM;GACf,UAAU,MAAM;EAClB,EAAE;EAIF,MAAM,kBAAkB,eACtB,MAAM,iBAAiB,MAAM,qBAAqB,KAAA,IAAY,MAAM,UACtE;EAEA,SAAS,UAAU,OAAkD;GACnE,IAAI,CAAC,OAAO,OAAO,KAAA;GACnB,IAAI;IACF,OAAO,0CAAU,KAAK;GACxB,QAAQ;IAIN;GACF;EACF;EAEA,SAAS,IAAI,OAAuB;GAClC,OAAO,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;EACtC;EAOA,SAAS,QAAQ,MAAyB;GACxC,MAAM,OAAO,GAAG,IAAI,KAAK,IAAI,EAAE,GAAG,IAAI,KAAK,MAAM;GACjD,OAAO,MAAM,gBAAgB,WAAW,GAAG,KAAK,GAAG,IAAI,KAAK,MAAM,MAAM;EAC1E;EAOA,MAAM,YAAY,eAChB,SAAS;GAAE,YAAY,UAAU,MAAM,UAAU;GAAG,WAAW,MAAM;EAAU,CAAC,CAClF;EAEA,SAAS,aAAa,MAA6B;GACjD,KAAK,qBAAqB,OAAO,QAAQ,IAAI,IAAI,KAAA,CAAS;EAC5D;EAQA,MAAM,cAAc,IAAY;EAEhC,SAAS,gBAAgB,MAAc,UAA2D;GAGhG,IAAI,MAAM,YAAY,SAAS,WAAW,OAAO,KAAA;GACjD,MAAM,WAAW,SAAS,QAAQ,YAAY,QAAQ,SAAS,SAAS;GASxE,OAAO,UAJL,YAAY,UAAU,KAAA,KACtB,SAAS,MAAM,YAAY,QAAQ,SAAS,YAAY,KAAK,IACzD,YAAY,QACZ,SAAS,EAAE,EAAE,QACM,IAAI;EAC/B;EAeA,MAAM,QAAQ,eAAe,GAAG,MAAM,YAAY,GAAG,MAAM,WAAW;;;;;;;;;;EAWtE,SAAS,iBACP,UAC6C;GAC7C,IAAI,MAAM,cAAc,IAAI,OAAO,KAAA;GACnC,MAAM,QAAQ,OAAO,SAAS,MAAM,YAAY,QAAQ,SAAS,MAAM,CAAC,EAAE,KAAK;GAC/E,MAAM,SAAS,SAAS,MAAM,YAAY,QAAQ,SAAS,WAAW,CAAC,EAAE;GAMzE,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,WAAW,KAAA,GAAW,OAAO,KAAA;GAC7D,OAAO;IAAE,iBAAiB;IAAO,kBAAkB,GAAG,MAAM,GAAG;GAAS;EAC1E;;GAIE,OAAA,UAAA,GAAA,YAyFgB,MAAA,aAAA,GAzFhB,WAyFgB;IAxFV,SAAA;IAAJ,KAAI;IACH,KAAK,MAAA;GAEO,GAAA;IAAA,GAAA,UAAA;IAAS,GAAK,MAAA,UAAA;IAAU,GAAK,MAAA,KAAA,CAAK,CAAC;GAAK,GAAA;IACpD,mBAAiB,gBAAA;IACjB,QAAQ,QAAA;IACR,aAAa,QAAA;IACb,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,OAAc,MAAA,EAAA,CAAE,CAAA,0GAAA,sFAAA,kFAA+yB,CAAA,MAAA,KAAA,CAAK,CAAC,WAAO,4BAAitB,MAAA,KAAA,CAAK,CAAC,YAAQ,aAA2vB,MAAA,KAAA,CAAK,CAAC,YAAQ,mEAAwT,MAAA,KAAA,CAAK,CAAC,WAAO,uDAAmE,MAAA,KAAA,CAAK,CAAC,KAAA;IA0ClsF,uBAAoB;;IAOrB,SAAA,SAAgF,EAxDtE,eAAQ,CAwDlB,YAAgF,MAAA,KAAA,GAAA;KAAzE,eAAY;KAAO,OAAM;IAOhC,CAAA,IAAA,UAAA,IAAA,GAAA,mBAsBiB,UAAA,MAAA,WArBS,WAAhB,MAAM,UAAK;KADrB,OAAA,UAAA,GAAA,YAsBiB,MAAA,cAAA,GAtBjB,WAsBiB;MApBd,KAAK;MACL,MAAM,KAAK;MACX,UAAU,gBAAgB,KAAK,MAAM,QAAQ;MAC7C,cAAY,aAAa,KAAK,IAAI;;MACd,GAAA,iBAAiB,KAAK,MAAM,KAAK,KAAK;MAAe,GAAA,KAAK,SAAI,SAAc,iBAAiB,QAAQ,IAAI,KAAA;;MAI7H,OAAgB,MAAA,EAAA,CAAE,CAAA,0CAAA,+CAAA,kDAAA,4CAA2O,KAAK,SAAI,aAAA,uBAAA;MAStQ,YAAO,WAAE,YAAA,QAAc,KAAK;;MAE7B,SAAA,cAAgB,CAAb,gBAAA,gBAAA,KAAK,KAAK,GAAA,CAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"TimePicker.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/time-picker/src/TimePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TimeSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology: the four names\n * Reka UI would otherwise give the segments in English of its own accord.\n * There is nothing else — the icon is decoration and hidden, and the field\n * group takes its name from the caller or from a wrapping Field.\n *\n * An alias for the shared slice rather than a fifth interface. The same four\n * names are said by `DateTimePicker` and `DateTimeRangePicker`, they come from\n * one Reka implementation, and `src/lib/date-labels.ts` carries why they are\n * declared once. The alias exists so this control's `labels` prop reads like\n * every other one's; annotate with `LabelOverrides<TimePickerLabels>` rather\n * than with this, which is total.\n */\nexport type TimePickerLabels = TimeSegmentLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { TimeFieldInput, TimeFieldRoot, type TimeValue } from \"reka-ui\";\nimport { Clock } from \"@lucide/vue\";\nimport { parseTime } from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { TIME_SEGMENT_LABELS, segmentAriaLabel, segmentAriaValueText } from \"@ecoma-io/loom-labels\";\n\n/**\n * TimePicker — a time of day, typed into a segmented field. Reach for it\n * wherever a clock time is a value: a meeting start, a reminder, opening hours.\n *\n * **There is no clock face and no dropdown, deliberately.** A clock dial is\n * slower than typing for every reader who can type, and unusable for several\n * who cannot; a 96-row list of quarter hours is worse again. The segmented\n * field already is the control — arrow keys change the segment under the\n * cursor, digits fill it and advance — so a panel would only be a second, worse\n * path to the same value. That is the one place this diverges from DatePicker,\n * where the calendar earns its panel by answering \"which Tuesday?\", a question\n * a time never has.\n *\n * **The model value is a plain `\"HH:mm\"` string in 24-hour time**, or\n * `\"HH:mm:ss\"` when `granularity` asks for seconds — the same boundary\n * DatePicker draws for `\"YYYY-MM-DD\"`, and drawn here for the same reason: a\n * host has a string from its API and should not have to learn a date library to\n * bind a time. `hourCycle` moves the *display* to 12-hour and never the value,\n * because conflating the two is how a 12-hour locale ends up storing `\"01:30\"`\n * for half past one in the afternoon.\n *\n * **A time is not a date.** There is no timezone here, no offset, and no `Date`\n * anywhere near the public surface: `\"09:00\"` is nine in the morning wherever\n * it is read, and the moment that turns into is the host's question, not this\n * component's. Pair it with a DatePicker when an instant is what is wanted.\n *\n * Unlike DatePicker there are no `min`/`max` bounds. A time of day has no\n * ordering that survives midnight — a night shift is \"not before 22:00\" *and*\n * \"not after 02:00\", two bounds that cross rather than fence — so a min/max\n * pair would quietly call 01:00 out of range for the exact case it was reached\n * for. A window like that belongs to the host, which knows which day each end\n * is on.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so `<Field\n * label=\"Starts at\" error=\"…\"><TimePicker /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather than\n * `false`. `readonly` and `disabled` are different states, so the field rests in\n * three appearances rather than two: read-only is a time on show — still a Tab\n * stop, still submitted, a lifted fill with the segments at full strength —\n * where disabled is unavailable and drained a step further, in fill, text\n * colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen time as `\"HH:mm\"`, or `\"HH:mm:ss\"` at second granularity, in\n * 24-hour time whatever the field displays. Unset, empty or unparseable\n * means no time is chosen. The explicit `| undefined` is what lets\n * `v-model` bind a ref that clears — the emitted value drops to `undefined`\n * when the field is emptied, and a plain optional prop refuses to take it\n * back.\n */\n modelValue?: string | undefined;\n /**\n * Whether the field reads as 12-hour with an AM/PM segment or as 24-hour.\n * Display only: the value stays 24-hour either way. Unset follows `locale`,\n * which is what a reader of that locale expects to see.\n */\n hourCycle?: 12 | 24;\n /** How far down the field goes. `\"hour\"` and `\"minute\"` report `\"HH:mm\"`; `\"second\"` adds a seconds segment and reports `\"HH:mm:ss\"`. */\n granularity?: \"hour\" | \"minute\" | \"second\";\n /** BCP 47 tag driving the order of the segments, the separator between them, and the 12-or-24-hour default. */\n locale?: string;\n /** Unavailable: dims the control and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the time without allowing an edit: the field stays a Tab stop, stays submitted, and is filled rather than dimmed. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and ring, and sets `aria-invalid`. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the time through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for the four segments, as any subset of `TimePickerLabels` — the\n * rest stay as the host's `provideLoomLabels` vocabulary left them, and then\n * as Loom's English.\n *\n * Every one of them replaces a literal Reka writes itself, so this is not\n * decoration: leave the vocabulary unset and the segments are named in\n * English rather than named not at all.\n */\n labels?: LabelOverrides<TimePickerLabels>;\n }>(),\n {\n granularity: \"minute\",\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning: `false` is a caller's decision that must beat the row, absent\n // is a caller saying nothing, and only `undefined` survives to mean that\n // past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen time as `\"HH:mm\"` (or `\"HH:mm:ss\"`), or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\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(\"timeSegments\", TIME_SEGMENT_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. The resolution rules themselves are the shared\n// helpers' — five hand-copied pairs of these two functions drifted once\n// already, so the branches live in `@ecoma-io/loom-labels` now and only the\n// refs stay here. `undefined` for the `literal` separators, which Reka renders\n// `aria-hidden` and which must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return segmentAriaLabel({ time: text.value }, part);\n}\n\nfunction segmentValueText(part: string, value: string): { \"aria-valuetext\"?: string } {\n return segmentAriaValueText({ time: text.value, part, value, locale: props.locale });\n}\n\n// One rendered node, so both halves of the fallthrough land on it — the split\n// exists only because `class` needs the Tailwind-aware merge and Vue's own\n// fallthrough would concatenate a caller's `h-11` after our `h-9` instead of\n// replacing it. That node is Reka's `role=\"group\"`, which is also where\n// `aria-label` belongs: a screen reader announces a group's name once on\n// entering it, rather than once per segment.\n//\n// It is also why `invalid` is announced with `aria-invalid` and nothing else.\n// DatePicker moves its `data-invalid` marker onto the anchor because the field\n// group already spells that attribute with Reka's own meaning; here there is no\n// second element to move it to, and an attribute Reka binds itself wins outright\n// — its template merges `$attrs` first and its own bindings after, so a\n// `data-invalid` passed in from out here is overwritten by Reka's own\n// `undefined` before it reaches the DOM. Setting it would look like it worked\n// and silently do nothing.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The time segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so Reka's hidden input went inert inside a disabled group\n * while the segments a reader actually uses still typed. Measured in jsdom: an\n * empty hour segment became `12` on an arrow key.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are handed in as this caller's own values: a caller who\n// sets either still beats the row, and the row's description is merged into\n// theirs rather than replacing it. `field.attrs` then spreads onto\n// `TimeFieldRoot` **after** `fieldAttrs`, which is the node both were already\n// landing on.\n//\n// **Where each key ends up was checked rather than assumed**, given what the\n// note above says about attributes this node overwrites. The set Reka spells\n// itself is `role`, `aria-disabled`, `data-disabled`, `data-readonly`,\n// `data-invalid` and `dir`, and `field.attrs` spells none of them:\n// `aria-describedby`, `aria-required` and `aria-invalid` are undeclared and land\n// on the `role=\"group\"` as intended, while `id` and `name` are declared props\n// and land on the visually-hidden `<input>` the field submits through — which is\n// the element a `<label for>` can actually name, a `role=\"group\"` div not being\n// labelable.\n//\n// The `:aria-invalid` binding that used to sit on the root is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // native input Reka renders inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row — DatePicker's\n// reasoning, minus the old third arm of the guard: a caller's own labelledby\n// has to survive here, because the binding below beats the spread and an\n// `undefined` would strip it instead of deferring to it.\nconst groupLabelledBy = computed(() => (attrs[\"aria-label\"] ? undefined : field.labelledBy));\n\nfunction fromClock(value: string | undefined): TimeValue | undefined {\n if (!value) return undefined;\n try {\n return parseTime(value);\n } catch {\n // Same contract as DatePicker: a half-typed or wrongly-shaped value is read\n // as no time chosen. A component library cannot decide that a host's bad\n // data is fatal.\n return undefined;\n }\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n// Written out rather than taken from `Time.toString()`, which always prints\n// seconds and appends a fractional part when the value carries one — so a\n// minute-granularity field would report `\"09:30:00\"`, and a value the host\n// parsed from `\"09:30:00.5\"` would round-trip a half second the field never\n// showed. The string that comes back is the string the segments are showing.\nfunction toClock(time: TimeValue): string {\n const hhmm = `${pad(time.hour)}:${pad(time.minute)}`;\n return props.granularity === \"second\" ? `${hhmm}:${pad(time.second)}` : hhmm;\n}\n\n// `optional()` rather than plain bindings: Reka reads an explicit `undefined`\n// `modelValue` as a value the host owns and is holding empty, which turns an\n// uncontrolled field into a permanently empty one, and an `hourCycle` of\n// `undefined` is how it is told to follow the locale. Dropping the keys says\n// what is true.\nconst rootValue = computed(() =>\n optional({ modelValue: fromClock(props.modelValue), hourCycle: props.hourCycle }),\n);\n\nfunction onTimeChange(time: TimeValue | undefined) {\n emit(\"update:modelValue\", time ? toClock(time) : undefined);\n}\n\n// The field is one Tab stop, not four. The reasoning, and the reason Reka's own\n// ArrowLeft/ArrowRight handling survives it untouched, is written out in\n// DatePicker — it moves focus with `focus()` directly, which ignores tabindex.\n// It matters more here: a 12-hour field with seconds is four segments, so\n// leaving Reka's default in place costs a keyboard reader four Tab presses to\n// cross one control.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // Read-only keeps every stop it had, unlike disabled: the time is on show and\n // a reader still arrows across the segments to read it.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a granularity or locale change takes\n // it away — a field whose only tab stop is a seconds segment that no longer\n // exists is a field nothing can reach.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// Reka reads `hourCycle` once, when it builds its formatter, and seeds its\n// segment values from the granularity in the same breath — neither is watched.\n// Observed: a field showing 13:30 that is moved to a 12-hour cycle relabels\n// itself \"1:30 AM\", half a day wrong, because the AM/PM segment appears with\n// its initial value and nothing recomputes it from the hour. Widening the\n// granularity on an empty field is the same fault with a different symptom: the\n// new seconds segment arrives with no entry to read, so it renders without a\n// role at all.\n//\n// Both are structural changes — they add and remove segments — so keying the\n// field on them rebuilds it rather than patching a half-migrated one. `locale`\n// is deliberately not in the key: Reka does watch that, and rebuilding on it\n// would throw away focus mid-edit for a change it handles correctly.\nconst shape = computed(() => `${props.granularity}-${props.hourCycle}`);\n\n/**\n * Reka's hour segment publishes `aria-valuenow` and `aria-valuetext` straight\n * off the internal 24-hour value while publishing `aria-valuemin`/`max` as\n * 1–12, so half past one in the afternoon announces itself as \"13 PM\", out of\n * its own declared range. The displayed hour is the one the reader is being\n * asked about, so the announcement is rebuilt from what the segment actually\n * shows. Only under an explicit 12-hour cycle: left to the locale, Reka defers\n * to the formatter and the pair is already consistent.\n */\nfunction hourAnnouncement(\n segments: readonly { part: string; value: string }[],\n): Record<string, string | number> | undefined {\n if (props.hourCycle !== 12) return undefined;\n const shown = Number(segments.find((segment) => segment.part === \"hour\")?.value);\n const period = segments.find((segment) => segment.part === \"dayPeriod\")?.value;\n // An unfilled hour reads as its placeholder rather than a number, and there is\n // no number to announce for it. The empty case is not left to Reka's English\n // \"Empty\" either — `segmentValueText` binds the vocabulary's word for it on\n // the same element, and the two never collide: this returns a value only for\n // a filled hour and that one only for an empty segment.\n if (!Number.isInteger(shown) || period === undefined) return undefined;\n return { \"aria-valuenow\": shown, \"aria-valuetext\": `${shown} ${period}` };\n}\n</script>\n\n<template>\n <TimeFieldRoot\n ref=\"root\"\n :key=\"shape\"\n v-slot=\"{ segments }\"\n v-bind=\"{ ...rootValue, ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :locale=\"locale\"\n :granularity=\"granularity\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :class=\"\n cn(\n // The text input's own height scale, so a time sitting in a form row\n // beside a text field lines up rather than nearly lining up. No gap:\n // the colon between two segments is itself a segment, so spacing them\n // apart writes the time as `09 : 30`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring and the focused segment below carries a fill\n // instead of a second outline, exactly as DatePicker's field does — a\n // ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and each is painted on this group rather\n // than on the segments inside it: a per-segment fill leaves the colon\n // between them on `background`, and the time comes out striped.\n //\n // Read-only lifts to `subtle` and keeps its focus ring and its\n // full-strength segments — 13.46:1 over that fill — because a read-only\n // time is a value on show, not an unavailable control. The rim stays\n // `input`: the field is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this node carries the same state to assistive tech,\n // so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Drained rather than faded, for the reason DatePicker and\n // DateTimePicker both write out — this control was the one member of\n // the family the sweep still caught. `opacity-50` took the segments\n // down with the box: measured on the built site, the hour and the\n // period read 3.08:1 and the colon between them 2.07:1, and those\n // segments *are* the value. The fill drains one step past read-only,\n // the muted text over it is 4.68:1, and the rim slackens from `input`\n // to `border` — three channels, so the two states do not part on hue\n // alone. Set on the group because no segment declares a resting colour\n // of its own, so each inherits this one.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour: `cn()` resolves one by\n // whichever class it saw last, so a field both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onTimeChange\"\n >\n <!-- Leading, where DatePicker's calendar button is trailing: that trailing\n slot is a button in every other member of this family, and a glyph\n sitting in it that opens nothing would promise a panel this control\n does not have. Decoration, so it is hidden from the accessibility\n tree — the group's own name says what the field is. -->\n <Clock aria-hidden=\"true\" class=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\n\n <!-- The `:aria-label` replaces the one Reka writes in English inside its\n own render function. It reaches the DOM node as a fallthrough\n attribute, which Vue merges last, so it is a replacement rather than a\n second name. Removing it does not fall back to nothing; it falls back\n to Reka's English. -->\n <TimeFieldInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"{\n ...segmentValueText(item.part, item.value),\n ...(item.part === 'hour' ? hourAnnouncement(segments) : undefined),\n }\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </TimeFieldInput>\n </TimeFieldRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6EA,MAAM,QAAQ;EAwDd,MAAM,OAAO;EAQb,MAAM,OAAO,UAAU,gBAAgB,2BAA2B,MAAM,MAAM;EAS9E,SAAS,aAAa,MAAkC;GACtD,OAAO,iBAAiB,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI;EACpD;EAEA,SAAS,iBAAiB,MAAc,OAA8C;GACpF,OAAO,qBAAqB;IAAE,MAAM,KAAK;IAAO;IAAM;IAAO,QAAQ,MAAM;GAAO,CAAC;EACrF;EAkBA,MAAM,EAAE,OAAO,MAAM,eAAe,cAAc;;;;;;;;;;;EAYlD,MAAM,OAAO,eAAkC,MAAM;EACrD,MAAM,gBAAgB,0BAA0B,KAAK,OAAO,GAAG;EAsB/D,MAAM,QAAQ,uBAAuB;GACnC,IAAI,MAAM;GACV,aAAa,MAAM;GACnB,gBAAgB,MAAM;GACtB,MAAM,MAAM;GAGZ,UAAU,cAAc,QAAQ,OAAO,MAAM;GAC7C,UAAU,MAAM;GAChB,SAAS,MAAM;GACf,UAAU,MAAM;EAClB,EAAE;EAMF,MAAM,kBAAkB,eAAgB,MAAM,gBAAgB,KAAA,IAAY,MAAM,UAAW;EAE3F,SAAS,UAAU,OAAkD;GACnE,IAAI,CAAC,OAAO,OAAO,KAAA;GACnB,IAAI;IACF,OAAO,0CAAU,KAAK;GACxB,QAAQ;IAIN;GACF;EACF;EAEA,SAAS,IAAI,OAAuB;GAClC,OAAO,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;EACtC;EAOA,SAAS,QAAQ,MAAyB;GACxC,MAAM,OAAO,GAAG,IAAI,KAAK,IAAI,EAAE,GAAG,IAAI,KAAK,MAAM;GACjD,OAAO,MAAM,gBAAgB,WAAW,GAAG,KAAK,GAAG,IAAI,KAAK,MAAM,MAAM;EAC1E;EAOA,MAAM,YAAY,eAChB,SAAS;GAAE,YAAY,UAAU,MAAM,UAAU;GAAG,WAAW,MAAM;EAAU,CAAC,CAClF;EAEA,SAAS,aAAa,MAA6B;GACjD,KAAK,qBAAqB,OAAO,QAAQ,IAAI,IAAI,KAAA,CAAS;EAC5D;EAQA,MAAM,cAAc,IAAY;EAEhC,SAAS,gBAAgB,MAAc,UAA2D;GAGhG,IAAI,MAAM,YAAY,SAAS,WAAW,OAAO,KAAA;GACjD,MAAM,WAAW,SAAS,QAAQ,YAAY,QAAQ,SAAS,SAAS;GASxE,OAAO,UAJL,YAAY,UAAU,KAAA,KACtB,SAAS,MAAM,YAAY,QAAQ,SAAS,YAAY,KAAK,IACzD,YAAY,QACZ,SAAS,EAAE,EAAE,QACM,IAAI;EAC/B;EAeA,MAAM,QAAQ,eAAe,GAAG,MAAM,YAAY,GAAG,MAAM,WAAW;;;;;;;;;;EAWtE,SAAS,iBACP,UAC6C;GAC7C,IAAI,MAAM,cAAc,IAAI,OAAO,KAAA;GACnC,MAAM,QAAQ,OAAO,SAAS,MAAM,YAAY,QAAQ,SAAS,MAAM,CAAC,EAAE,KAAK;GAC/E,MAAM,SAAS,SAAS,MAAM,YAAY,QAAQ,SAAS,WAAW,CAAC,EAAE;GAMzE,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,WAAW,KAAA,GAAW,OAAO,KAAA;GAC7D,OAAO;IAAE,iBAAiB;IAAO,kBAAkB,GAAG,MAAM,GAAG;GAAS;EAC1E;;GAIE,OAAA,UAAA,GAAA,YAyFgB,MAAA,aAAA,GAzFhB,WAyFgB;IAxFV,SAAA;IAAJ,KAAI;IACH,KAAK,MAAA;GAEO,GAAA;IAAA,GAAA,UAAA;IAAS,GAAK,MAAA,UAAA;IAAU,GAAK,MAAA,KAAA,CAAK,CAAC;GAAK,GAAA;IACpD,mBAAiB,gBAAA;IACjB,QAAQ,QAAA;IACR,aAAa,QAAA;IACb,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,OAAc,MAAA,EAAA,CAAE,CAAA,0GAAA,sFAAA,kFAA+yB,CAAA,MAAA,KAAA,CAAK,CAAC,WAAO,4BAAitB,MAAA,KAAA,CAAK,CAAC,YAAQ,aAA2vB,MAAA,KAAA,CAAK,CAAC,YAAQ,mEAAwT,MAAA,KAAA,CAAK,CAAC,WAAO,uDAAmE,MAAA,KAAA,CAAK,CAAC,KAAA;IA0ClsF,uBAAoB;;IAOrB,SAAA,SAAgF,EAxDtE,eAAQ,CAwDlB,YAAgF,MAAA,KAAA,GAAA;KAAzE,eAAY;KAAO,OAAM;IAOhC,CAAA,IAAA,UAAA,IAAA,GAAA,mBAsBiB,UAAA,MAAA,WArBS,WAAhB,MAAM,UAAK;KADrB,OAAA,UAAA,GAAA,YAsBiB,MAAA,cAAA,GAtBjB,WAsBiB;MApBd,KAAK;MACL,MAAM,KAAK;MACX,UAAU,gBAAgB,KAAK,MAAM,QAAQ;MAC7C,cAAY,aAAa,KAAK,IAAI;;MACd,GAAA,iBAAiB,KAAK,MAAM,KAAK,KAAK;MAAe,GAAA,KAAK,SAAI,SAAc,iBAAiB,QAAQ,IAAI,KAAA;;MAI7H,OAAgB,MAAA,EAAA,CAAE,CAAA,0CAAA,+CAAA,kDAAA,4CAA2O,KAAK,SAAI,aAAA,uBAAA;MAStQ,YAAO,WAAE,YAAA,QAAc,KAAK;;MAE7B,SAAA,cAAgB,CAAb,gBAAA,gBAAA,KAAK,KAAK,GAAA,CAAA,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToastItem.js","names":[],"sources":["../../../../../packages/primitives/toast/src/ToastItem.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ToastLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n */\nexport const TOAST_LABELS: ToastLabels = {\n close: \"Close\",\n announce: \"Notification\",\n region: ({ hotkey }) => `Notifications (${hotkey})`,\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose } from \"reka-ui\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { Info, CircleCheck, TriangleAlert, CircleX, Sparkles, X } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport type { ToastVariant } from \"./Toast.vue\";\n\n/**\n * ToastItem — one toast card (ToastRoot + Loom treatment), INTERNAL to\n * the design system. It must live inside a `ToastProvider` whose\n * `ToastViewport` it teleports into: `Toast` bundles that pair for the\n * standalone case. Not exported from the barrel — hosts use `Toast`.\n */\nconst props = withDefaults(\n defineProps<{\n /** Controls visibility; pair with `v-model:open`. Omit to let the toast own its own open state. */\n open?: boolean | undefined;\n /** The toast's headline — always shown. */\n title: string;\n /** An optional second line of detail below the title. */\n description?: string;\n /** Selects the accent icon and colour. */\n variant?: ToastVariant;\n /** ms before auto-dismiss; the Reka default pauses on hover/focus. */\n duration?: number;\n /** Shows the ✕ close button. */\n closable?: boolean;\n /** Renders a single inline action button; emits `action` when pressed. */\n actionLabel?: string;\n /** Names for what the card says on its own account, as any subset of `ToastLabels`. */\n labels?: LabelOverrides<ToastLabels>;\n }>(),\n {\n // See Toast.vue: `undefined` is the uncontrolled state, and only an\n // explicit default keeps it reachable past Vue's Boolean casting.\n open: undefined,\n variant: \"info\",\n duration: 5000,\n closable: true,\n },\n);\n\ndefineEmits<{ \"update:open\": [value: boolean]; action: [] }>();\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(\"toast\", TOAST_LABELS, () => props.labels);\n\n// `satisfies` binds this map to ToastVariant so a variant added to the type\n// without a matching entry here fails at compile time (Button's own idiom).\nconst toastAccents = {\n info: { icon: Info, color: \"text-info-text\" },\n success: { icon: CircleCheck, color: \"text-success-text\" },\n // `-text` like its four siblings. The bare `warning` hue is a fill colour —\n // as text it fails on the dark theme, and the functional `-text` tokens are\n // the ones measured legible on every surface (theme.css carries the ratios).\n warning: { icon: TriangleAlert, color: \"text-warning-text\" },\n destructive: { icon: CircleX, color: \"text-destructive-text\" },\n accent: { icon: Sparkles, color: \"text-primary-text\" },\n} satisfies Record<ToastVariant, { icon: typeof Info; color: string }>;\n\nconst accent = computed(() => toastAccents[props.variant]);\n</script>\n\n<template>\n <!-- Entrance and exit deliberately do not share an element, and the split is\n forced by Reka rather than taste. The swipe gesture marks ToastRoot\n itself (data-swipe plus --reka-toast-swipe-* custom properties), so the\n root's transform stays reserved and the entrance rides the inner card.\n The exit cannot ride along: Presence decides an element's fate by\n reading the computed animation-name of the node it mounted — never a\n descendant's — and by waiting for an animationend whose target is that\n same node (usePresence.js). A toast-out confined to the inner card would\n leave the root's own name `none` and be unmounted before its first\n frame; on ToastRoot it genuinely plays. -->\n <ToastRoot\n v-bind=\"optional({ open })\"\n :duration=\"duration\"\n class=\"outline-none data-[state=closed]:animate-toast-out\"\n @update:open=\"$emit('update:open', $event)\"\n >\n <div\n :class=\"\n cn(\n 'relative flex animate-toast-in items-start gap-3 rounded-md border border-border bg-popover p-3 pr-8 text-popover-foreground shadow-lg',\n // Accent variant: a highlighted border to draw attention to the\n // toast's distinctiveness from the standard variants.\n variant === 'accent' && 'border-primary/40',\n )\n \"\n >\n <component\n :is=\"accent.icon\"\n :class=\"cn('mt-0.5 h-4 w-4 shrink-0', accent.color)\"\n aria-hidden=\"true\"\n />\n <div class=\"min-w-0 flex-1\">\n <ToastTitle class=\"text-sm font-medium\">{{ title }}</ToastTitle>\n <ToastDescription v-if=\"description\" class=\"mt-0.5 text-xs text-muted-foreground\">\n {{ description }}\n </ToastDescription>\n </div>\n\n <ToastAction v-if=\"actionLabel\" :alt-text=\"actionLabel\" as-child @click=\"$emit('action')\">\n <button\n type=\"button\"\n class=\"shrink-0 rounded-sm px-2 py-1 text-xs font-medium text-primary-text transition-colors duration-fast ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n >\n {{ actionLabel }}\n </button>\n </ToastAction>\n\n <ToastClose\n v-if=\"closable\"\n :aria-label=\"text.close\"\n class=\"absolute right-2 top-2 inline-flex h-6 w-6 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\"\n >\n <X class=\"h-3.5 w-3.5\" />\n </ToastClose>\n </div>\n </ToastRoot>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"ToastItem.js","names":[],"sources":["../../../../../packages/primitives/toast/src/ToastItem.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ToastLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n */\nexport const TOAST_LABELS: ToastLabels = {\n close: \"Close\",\n announce: \"Notification\",\n region: ({ hotkey }) => `Notifications (${hotkey})`,\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose } from \"reka-ui\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { Info, CircleCheck, TriangleAlert, CircleX, Sparkles, X } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport type { ToastVariant } from \"./Toast.vue\";\n\n/**\n * ToastItem — one toast card (ToastRoot + Loom treatment), INTERNAL to\n * the design system. It must live inside a `ToastProvider` whose\n * `ToastViewport` it teleports into: `Toast` bundles that pair for the\n * standalone case. Not exported from the barrel — hosts use `Toast`.\n */\nconst props = withDefaults(\n defineProps<{\n /** Controls visibility; pair with `v-model:open`. Omit to let the toast own its own open state. */\n open?: boolean | undefined;\n /** The toast's headline — always shown. */\n title: string;\n /** An optional second line of detail below the title. */\n description?: string;\n /** Selects the accent icon and colour. */\n variant?: ToastVariant;\n /** ms before auto-dismiss; the Reka default pauses on hover/focus. */\n duration?: number;\n /** Shows the ✕ close button. */\n closable?: boolean;\n /** Renders a single inline action button; emits `action` when pressed. */\n actionLabel?: string;\n /** Names for what the card says on its own account, as any subset of `ToastLabels`. */\n labels?: LabelOverrides<ToastLabels>;\n }>(),\n {\n // See Toast.vue: `undefined` is the uncontrolled state, and only an\n // explicit default keeps it reachable past Vue's Boolean casting.\n open: undefined,\n variant: \"info\",\n duration: 5000,\n closable: true,\n },\n);\n\ndefineEmits<{ \"update:open\": [value: boolean]; action: [] }>();\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(\"toast\", TOAST_LABELS, () => props.labels);\n\n// `satisfies` binds this map to ToastVariant so a variant added to the type\n// without a matching entry here fails at compile time (Button's own idiom).\nconst toastAccents = {\n info: { icon: Info, color: \"text-info-text\" },\n success: { icon: CircleCheck, color: \"text-success-text\" },\n // `-text` like its four siblings. The bare `warning` hue is a fill colour —\n // as text it fails on the dark theme, and the functional `-text` tokens are\n // the ones measured legible on every surface (theme.css carries the ratios).\n warning: { icon: TriangleAlert, color: \"text-warning-text\" },\n destructive: { icon: CircleX, color: \"text-destructive-text\" },\n accent: { icon: Sparkles, color: \"text-primary-text\" },\n} satisfies Record<ToastVariant, { icon: typeof Info; color: string }>;\n\nconst accent = computed(() => toastAccents[props.variant]);\n\n// Severity alone decides how the toast interrupts (issue #91): a destructive\n// report announced politely can be swallowed by whatever the screen reader is\n// already saying — exactly the messages that must not be missed. The knob is\n// Reka's own `type`: it hard-wires the hidden announce region's `role` to\n// `alert` and picks that region's politeness from this prop — `foreground`\n// announces `aria-live=\"assertive\"`, `background` politely. Destructive\n// routes foreground; every other severity stays polite, so an ordinary\n// notification never interrupts. Deliberately no prop of Loom's own: the\n// variant the card already carries *is* the severity, and a second way to set\n// it would only let the two disagree.\nconst announceType = computed<\"foreground\" | \"background\">(() =>\n props.variant === \"destructive\" ? \"foreground\" : \"background\",\n);\n</script>\n\n<template>\n <!-- Entrance and exit deliberately do not share an element, and the split is\n forced by Reka rather than taste. The swipe gesture marks ToastRoot\n itself (data-swipe plus --reka-toast-swipe-* custom properties), so the\n root's transform stays reserved and the entrance rides the inner card.\n The exit cannot ride along: Presence decides an element's fate by\n reading the computed animation-name of the node it mounted — never a\n descendant's — and by waiting for an animationend whose target is that\n same node (usePresence.js). A toast-out confined to the inner card would\n leave the root's own name `none` and be unmounted before its first\n frame; on ToastRoot it genuinely plays. -->\n <ToastRoot\n v-bind=\"optional({ open })\"\n :type=\"announceType\"\n :duration=\"duration\"\n class=\"outline-none data-[state=closed]:animate-toast-out\"\n @update:open=\"$emit('update:open', $event)\"\n >\n <div\n :class=\"\n cn(\n 'relative flex animate-toast-in items-start gap-3 rounded-md border border-border bg-popover p-3 pr-8 text-popover-foreground shadow-lg',\n // Accent variant: a highlighted border to draw attention to the\n // toast's distinctiveness from the standard variants.\n variant === 'accent' && 'border-primary/40',\n )\n \"\n >\n <component\n :is=\"accent.icon\"\n :class=\"cn('mt-0.5 h-4 w-4 shrink-0', accent.color)\"\n aria-hidden=\"true\"\n />\n <div class=\"min-w-0 flex-1\">\n <ToastTitle class=\"text-sm font-medium\">{{ title }}</ToastTitle>\n <ToastDescription v-if=\"description\" class=\"mt-0.5 text-xs text-muted-foreground\">\n {{ description }}\n </ToastDescription>\n </div>\n\n <ToastAction v-if=\"actionLabel\" :alt-text=\"actionLabel\" as-child @click=\"$emit('action')\">\n <button\n type=\"button\"\n class=\"shrink-0 rounded-sm px-2 py-1 text-xs font-medium text-primary-text transition-colors duration-fast ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n >\n {{ actionLabel }}\n </button>\n </ToastAction>\n\n <ToastClose\n v-if=\"closable\"\n :aria-label=\"text.close\"\n class=\"absolute right-2 top-2 inline-flex h-6 w-6 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\"\n >\n <X class=\"h-3.5 w-3.5\" />\n </ToastClose>\n </div>\n </ToastRoot>\n</template>\n"],"mappings":""}
|
|
@@ -68,8 +68,10 @@ var ToastItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
70
|
const accent = computed(() => toastAccents[props.variant]);
|
|
71
|
+
const announceType = computed(() => props.variant === "destructive" ? "foreground" : "background");
|
|
71
72
|
return (_ctx, _cache) => {
|
|
72
73
|
return openBlock(), createBlock(unref(ToastRoot), mergeProps(unref(optional)({ open: __props.open }), {
|
|
74
|
+
type: announceType.value,
|
|
73
75
|
duration: __props.duration,
|
|
74
76
|
class: "outline-none data-[state=closed]:animate-toast-out",
|
|
75
77
|
"onUpdate:open": _cache[1] || (_cache[1] = ($event) => _ctx.$emit("update:open", $event))
|
|
@@ -108,7 +110,7 @@ var ToastItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
108
110
|
}, 8, ["aria-label"])) : createCommentVNode("", true)
|
|
109
111
|
], 2)]),
|
|
110
112
|
_: 1
|
|
111
|
-
}, 16, ["duration"]);
|
|
113
|
+
}, 16, ["type", "duration"]);
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
116
|
});
|
package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToastItem.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/toast/src/ToastItem.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ToastLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n */\nexport const TOAST_LABELS: ToastLabels = {\n close: \"Close\",\n announce: \"Notification\",\n region: ({ hotkey }) => `Notifications (${hotkey})`,\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose } from \"reka-ui\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { Info, CircleCheck, TriangleAlert, CircleX, Sparkles, X } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport type { ToastVariant } from \"./Toast.vue\";\n\n/**\n * ToastItem — one toast card (ToastRoot + Loom treatment), INTERNAL to\n * the design system. It must live inside a `ToastProvider` whose\n * `ToastViewport` it teleports into: `Toast` bundles that pair for the\n * standalone case. Not exported from the barrel — hosts use `Toast`.\n */\nconst props = withDefaults(\n defineProps<{\n /** Controls visibility; pair with `v-model:open`. Omit to let the toast own its own open state. */\n open?: boolean | undefined;\n /** The toast's headline — always shown. */\n title: string;\n /** An optional second line of detail below the title. */\n description?: string;\n /** Selects the accent icon and colour. */\n variant?: ToastVariant;\n /** ms before auto-dismiss; the Reka default pauses on hover/focus. */\n duration?: number;\n /** Shows the ✕ close button. */\n closable?: boolean;\n /** Renders a single inline action button; emits `action` when pressed. */\n actionLabel?: string;\n /** Names for what the card says on its own account, as any subset of `ToastLabels`. */\n labels?: LabelOverrides<ToastLabels>;\n }>(),\n {\n // See Toast.vue: `undefined` is the uncontrolled state, and only an\n // explicit default keeps it reachable past Vue's Boolean casting.\n open: undefined,\n variant: \"info\",\n duration: 5000,\n closable: true,\n },\n);\n\ndefineEmits<{ \"update:open\": [value: boolean]; action: [] }>();\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(\"toast\", TOAST_LABELS, () => props.labels);\n\n// `satisfies` binds this map to ToastVariant so a variant added to the type\n// without a matching entry here fails at compile time (Button's own idiom).\nconst toastAccents = {\n info: { icon: Info, color: \"text-info-text\" },\n success: { icon: CircleCheck, color: \"text-success-text\" },\n // `-text` like its four siblings. The bare `warning` hue is a fill colour —\n // as text it fails on the dark theme, and the functional `-text` tokens are\n // the ones measured legible on every surface (theme.css carries the ratios).\n warning: { icon: TriangleAlert, color: \"text-warning-text\" },\n destructive: { icon: CircleX, color: \"text-destructive-text\" },\n accent: { icon: Sparkles, color: \"text-primary-text\" },\n} satisfies Record<ToastVariant, { icon: typeof Info; color: string }>;\n\nconst accent = computed(() => toastAccents[props.variant]);\n</script>\n\n<template>\n <!-- Entrance and exit deliberately do not share an element, and the split is\n forced by Reka rather than taste. The swipe gesture marks ToastRoot\n itself (data-swipe plus --reka-toast-swipe-* custom properties), so the\n root's transform stays reserved and the entrance rides the inner card.\n The exit cannot ride along: Presence decides an element's fate by\n reading the computed animation-name of the node it mounted — never a\n descendant's — and by waiting for an animationend whose target is that\n same node (usePresence.js). A toast-out confined to the inner card would\n leave the root's own name `none` and be unmounted before its first\n frame; on ToastRoot it genuinely plays. -->\n <ToastRoot\n v-bind=\"optional({ open })\"\n :duration=\"duration\"\n class=\"outline-none data-[state=closed]:animate-toast-out\"\n @update:open=\"$emit('update:open', $event)\"\n >\n <div\n :class=\"\n cn(\n 'relative flex animate-toast-in items-start gap-3 rounded-md border border-border bg-popover p-3 pr-8 text-popover-foreground shadow-lg',\n // Accent variant: a highlighted border to draw attention to the\n // toast's distinctiveness from the standard variants.\n variant === 'accent' && 'border-primary/40',\n )\n \"\n >\n <component\n :is=\"accent.icon\"\n :class=\"cn('mt-0.5 h-4 w-4 shrink-0', accent.color)\"\n aria-hidden=\"true\"\n />\n <div class=\"min-w-0 flex-1\">\n <ToastTitle class=\"text-sm font-medium\">{{ title }}</ToastTitle>\n <ToastDescription v-if=\"description\" class=\"mt-0.5 text-xs text-muted-foreground\">\n {{ description }}\n </ToastDescription>\n </div>\n\n <ToastAction v-if=\"actionLabel\" :alt-text=\"actionLabel\" as-child @click=\"$emit('action')\">\n <button\n type=\"button\"\n class=\"shrink-0 rounded-sm px-2 py-1 text-xs font-medium text-primary-text transition-colors duration-fast ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n >\n {{ actionLabel }}\n </button>\n </ToastAction>\n\n <ToastClose\n v-if=\"closable\"\n :aria-label=\"text.close\"\n class=\"absolute right-2 top-2 inline-flex h-6 w-6 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\"\n >\n <X class=\"h-3.5 w-3.5\" />\n </ToastClose>\n </div>\n </ToastRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AASA,IAAa,eAA4B;CACvC,OAAO;CACP,UAAU;CACV,SAAS,EAAE,aAAa,kBAAkB,OAAO;AACnD;;;;;;;;;;;;;;;;;;;;;EAkBA,MAAM,QAAQ;EAkCd,MAAM,OAAO,UAAU,SAAS,oBAAoB,MAAM,MAAM;EAIhE,MAAM,eAAe;GACnB,MAAM;IAAE,MAAM;IAAM,OAAO;GAAiB;GAC5C,SAAS;IAAE,MAAM;IAAa,OAAO;GAAoB;GAIzD,SAAS;IAAE,MAAM;IAAe,OAAO;GAAoB;GAC3D,aAAa;IAAE,MAAM;IAAS,OAAO;GAAwB;GAC7D,QAAQ;IAAE,MAAM;IAAU,OAAO;GAAoB;EACvD;EAEA,MAAM,SAAS,eAAe,aAAa,MAAM,QAAQ;;
|
|
1
|
+
{"version":3,"file":"ToastItem.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/toast/src/ToastItem.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ToastLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n */\nexport const TOAST_LABELS: ToastLabels = {\n close: \"Close\",\n announce: \"Notification\",\n region: ({ hotkey }) => `Notifications (${hotkey})`,\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose } from \"reka-ui\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { Info, CircleCheck, TriangleAlert, CircleX, Sparkles, X } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport type { ToastVariant } from \"./Toast.vue\";\n\n/**\n * ToastItem — one toast card (ToastRoot + Loom treatment), INTERNAL to\n * the design system. It must live inside a `ToastProvider` whose\n * `ToastViewport` it teleports into: `Toast` bundles that pair for the\n * standalone case. Not exported from the barrel — hosts use `Toast`.\n */\nconst props = withDefaults(\n defineProps<{\n /** Controls visibility; pair with `v-model:open`. Omit to let the toast own its own open state. */\n open?: boolean | undefined;\n /** The toast's headline — always shown. */\n title: string;\n /** An optional second line of detail below the title. */\n description?: string;\n /** Selects the accent icon and colour. */\n variant?: ToastVariant;\n /** ms before auto-dismiss; the Reka default pauses on hover/focus. */\n duration?: number;\n /** Shows the ✕ close button. */\n closable?: boolean;\n /** Renders a single inline action button; emits `action` when pressed. */\n actionLabel?: string;\n /** Names for what the card says on its own account, as any subset of `ToastLabels`. */\n labels?: LabelOverrides<ToastLabels>;\n }>(),\n {\n // See Toast.vue: `undefined` is the uncontrolled state, and only an\n // explicit default keeps it reachable past Vue's Boolean casting.\n open: undefined,\n variant: \"info\",\n duration: 5000,\n closable: true,\n },\n);\n\ndefineEmits<{ \"update:open\": [value: boolean]; action: [] }>();\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(\"toast\", TOAST_LABELS, () => props.labels);\n\n// `satisfies` binds this map to ToastVariant so a variant added to the type\n// without a matching entry here fails at compile time (Button's own idiom).\nconst toastAccents = {\n info: { icon: Info, color: \"text-info-text\" },\n success: { icon: CircleCheck, color: \"text-success-text\" },\n // `-text` like its four siblings. The bare `warning` hue is a fill colour —\n // as text it fails on the dark theme, and the functional `-text` tokens are\n // the ones measured legible on every surface (theme.css carries the ratios).\n warning: { icon: TriangleAlert, color: \"text-warning-text\" },\n destructive: { icon: CircleX, color: \"text-destructive-text\" },\n accent: { icon: Sparkles, color: \"text-primary-text\" },\n} satisfies Record<ToastVariant, { icon: typeof Info; color: string }>;\n\nconst accent = computed(() => toastAccents[props.variant]);\n\n// Severity alone decides how the toast interrupts (issue #91): a destructive\n// report announced politely can be swallowed by whatever the screen reader is\n// already saying — exactly the messages that must not be missed. The knob is\n// Reka's own `type`: it hard-wires the hidden announce region's `role` to\n// `alert` and picks that region's politeness from this prop — `foreground`\n// announces `aria-live=\"assertive\"`, `background` politely. Destructive\n// routes foreground; every other severity stays polite, so an ordinary\n// notification never interrupts. Deliberately no prop of Loom's own: the\n// variant the card already carries *is* the severity, and a second way to set\n// it would only let the two disagree.\nconst announceType = computed<\"foreground\" | \"background\">(() =>\n props.variant === \"destructive\" ? \"foreground\" : \"background\",\n);\n</script>\n\n<template>\n <!-- Entrance and exit deliberately do not share an element, and the split is\n forced by Reka rather than taste. The swipe gesture marks ToastRoot\n itself (data-swipe plus --reka-toast-swipe-* custom properties), so the\n root's transform stays reserved and the entrance rides the inner card.\n The exit cannot ride along: Presence decides an element's fate by\n reading the computed animation-name of the node it mounted — never a\n descendant's — and by waiting for an animationend whose target is that\n same node (usePresence.js). A toast-out confined to the inner card would\n leave the root's own name `none` and be unmounted before its first\n frame; on ToastRoot it genuinely plays. -->\n <ToastRoot\n v-bind=\"optional({ open })\"\n :type=\"announceType\"\n :duration=\"duration\"\n class=\"outline-none data-[state=closed]:animate-toast-out\"\n @update:open=\"$emit('update:open', $event)\"\n >\n <div\n :class=\"\n cn(\n 'relative flex animate-toast-in items-start gap-3 rounded-md border border-border bg-popover p-3 pr-8 text-popover-foreground shadow-lg',\n // Accent variant: a highlighted border to draw attention to the\n // toast's distinctiveness from the standard variants.\n variant === 'accent' && 'border-primary/40',\n )\n \"\n >\n <component\n :is=\"accent.icon\"\n :class=\"cn('mt-0.5 h-4 w-4 shrink-0', accent.color)\"\n aria-hidden=\"true\"\n />\n <div class=\"min-w-0 flex-1\">\n <ToastTitle class=\"text-sm font-medium\">{{ title }}</ToastTitle>\n <ToastDescription v-if=\"description\" class=\"mt-0.5 text-xs text-muted-foreground\">\n {{ description }}\n </ToastDescription>\n </div>\n\n <ToastAction v-if=\"actionLabel\" :alt-text=\"actionLabel\" as-child @click=\"$emit('action')\">\n <button\n type=\"button\"\n class=\"shrink-0 rounded-sm px-2 py-1 text-xs font-medium text-primary-text transition-colors duration-fast ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n >\n {{ actionLabel }}\n </button>\n </ToastAction>\n\n <ToastClose\n v-if=\"closable\"\n :aria-label=\"text.close\"\n class=\"absolute right-2 top-2 inline-flex h-6 w-6 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\"\n >\n <X class=\"h-3.5 w-3.5\" />\n </ToastClose>\n </div>\n </ToastRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AASA,IAAa,eAA4B;CACvC,OAAO;CACP,UAAU;CACV,SAAS,EAAE,aAAa,kBAAkB,OAAO;AACnD;;;;;;;;;;;;;;;;;;;;;EAkBA,MAAM,QAAQ;EAkCd,MAAM,OAAO,UAAU,SAAS,oBAAoB,MAAM,MAAM;EAIhE,MAAM,eAAe;GACnB,MAAM;IAAE,MAAM;IAAM,OAAO;GAAiB;GAC5C,SAAS;IAAE,MAAM;IAAa,OAAO;GAAoB;GAIzD,SAAS;IAAE,MAAM;IAAe,OAAO;GAAoB;GAC3D,aAAa;IAAE,MAAM;IAAS,OAAO;GAAwB;GAC7D,QAAQ;IAAE,MAAM;IAAU,OAAO;GAAoB;EACvD;EAEA,MAAM,SAAS,eAAe,aAAa,MAAM,QAAQ;EAYzD,MAAM,eAAe,eACnB,MAAM,YAAY,gBAAgB,eAAe,YACnD;;GAcE,OAAA,UAAA,GAAA,YA8CY,MAAA,SAAA,GA9CZ,WACU,MA6CE,QAAA,CA7CM,CAAA,EAAA,MAAG,QAAA,KAAI,CAAA,GAAA;IACtB,MAAM,aAAA;IACN,UAAU,QAAA;IACX,OAAM;IACL,iBAAW,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,eAAgB,MAAM;;IAEzC,SAAA,cAsCM,CAtCN,mBAsCM,OAAA,EArCH,OAAK,eAAW,MAAA,EAAA,CAAE,CAAA,0IAA4S,QAAA,YAAO,YAAA,mBAAA,CAAA,EAAA,GAAA;MAStU,UAAA,GAAA,YAIE,wBAHK,OAAA,MAAO,IAAI,GAAA;MACf,OAAK,eAAE,MAAA,EAAA,CAAE,CAAA,2BAA4B,OAAA,MAAO,KAAK,CAAA;MAClD,eAAY;;KAEd,mBAKM,OALN,YAKM,CAJJ,YAAgE,MAAA,UAAA,GAAA,EAApD,OAAM,sBAAqB,GAAA;MAAC,SAAA,cAAW,CAAR,gBAAA,gBAAA,QAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;KACxB,CAAA,GAAA,QAAA,eAAxB,UAAA,GAAA,YAEmB,MAAA,gBAAA,GAAA;;MAFkB,OAAM;;MACzC,SAAA,cAAiB,CAAd,gBAAA,gBAAA,QAAA,WAAW,GAAA,CAAA,CAAA,CAAA;;;KAIC,QAAA,eAAnB,UAAA,GAAA,YAOc,MAAA,WAAA,GAAA;;MAPmB,YAAU,QAAA;MAAa,YAAA;MAAU,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,QAAA;;MAC5E,SAAA,cAKS,CALT,mBAKS,UALT,YAKS,gBADJ,QAAA,WAAW,GAAA,CAAA,CAAA,CAAA;;;KAKV,QAAA,YADR,UAAA,GAAA,YAMa,MAAA,UAAA,GAAA;;MAJV,cAAY,MAAA,IAAA,CAAI,CAAC;MAClB,OAAM;;MAEN,SAAA,cAAyB,CAAzB,YAAyB,MAAA,CAAA,GAAA,EAAtB,OAAM,cAAa,CAAA,CAAA,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import ToggleGroup_vue_vue_type_script_setup_true_lang_default from "./ToggleGroup.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
//#region packages/primitives/toggle-group/src/ToggleGroup.vue
|
|
3
|
+
var ToggleGroup_default = ToggleGroup_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { ToggleGroup_default as default };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=ToggleGroup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToggleGroup.js","names":[],"sources":["../../../../../packages/primitives/toggle-group/src/ToggleGroup.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { Component } from \"vue\";\n\n/** One toggle inside a {@link ToggleGroup}. */\nexport interface ToggleGroupItem {\n /** Matched against `modelValue`. */\n value: string;\n /** Visible label, rendered inside the button. */\n label: string;\n /** Optional glyph rendered before the label; hidden from the tree, the label is the name. */\n icon?: Component;\n /** Disables this button alone, leaving its neighbours toggleable. */\n disabled?: boolean;\n /** Forwarded to the button's DOM node — lets hosts keep stable test hooks. */\n testId?: string;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, useTemplateRef } from \"vue\";\nimport { ToggleGroupRoot, ToggleGroupItem as RekaToggleGroupItem } from \"reka-ui\";\nimport type { AcceptableValue } from \"reka-ui\";\nimport { useAncestorDisabled, useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { cn, optional, useSplitAttrs } from \"@ecoma-io/loom-core\";\n\n/**\n * ToggleGroup — a set of toggle actions or filters whose pressed state is the\n * value: bold/italic/underline, column filters, metric toggles. Each item is\n * a real `<button aria-pressed>` — deliberately **not** `aria-checked`/radio\n * semantics, which is the whole distinction from\n * [SegmentedControl](../../segmented-control): a segmented control is a value\n * picker where one option is always active, while here zero-or-more buttons\n * can be on at once and the group's `type` says which contract the model\n * follows — `\"single\"` holds one value or `null`, `\"multiple\"` a `string[]`.\n * Built on Reka UI's ToggleGroup: the group is one Tab stop (roving\n * tabindex), Arrow keys move focus between buttons skipping disabled ones —\n * they move *focus only* and never flip a toggle, unlike a radio group where\n * the arrows change the value. Styled with Loom tokens; a pressed button\n * fills with the primary token and takes weight, so the state is carried by\n * `aria-pressed` plus fill plus weight — never colour alone.\n *\n * Inside a [Field](../Field/Field.vue) the row's description, `required` and\n * `invalid` reach the group through `useFieldControl()`, and a\n * `<Fieldset disabled>` above it disables every button.\n *\n * **A row's label does not name this group.** `<label for>` names a labelable\n * element and this renders a `div[role=\"group\"]`, so the row's label resolves\n * to the element and names nobody. Name it with a\n * [Fieldset](../Fieldset/Fieldset.vue)'s real `<legend>`, or with an\n * `aria-label`/`aria-labelledby` of its own — which is what every example\n * already does, because the component carries no label text of its own and\n * therefore no labels prop: the item labels are the caller's content.\n *\n * There is no `readonly`: a toggle that displays a state but cannot change it\n * is a disabled one showing an answer, so a row's `readonly` arrives as\n * nothing at all rather than as that disguise.\n */\nconst props = withDefaults(\n defineProps<{\n /** The pressed value (`\"single\"`), the pressed values (`\"multiple\"`), or nothing pressed. */\n modelValue?: string | string[] | null;\n /** The buttons to render, in order. */\n items: ToggleGroupItem[];\n /** `\"single\"` models one value or `null`; `\"multiple\"` a `string[]`. */\n type?: \"single\" | \"multiple\";\n /** The track's resting look; a pressed button fills with the primary token in every variant. */\n variant?: \"secondary\" | \"outline\" | \"ghost\";\n /** `\"sm\"` is the compressed form for dense chrome — e.g. an editor toolbar. */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Disables every button at once, distinct from a single item's own `disabled`. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n }>(),\n {\n // Not `false`: absent has to stay tellable from `false` for a Field above\n // to disable the group and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n type: \"single\",\n variant: \"secondary\",\n size: \"md\",\n },\n);\n\nconst emit = defineEmits<{ \"update:modelValue\": [value: string | null | string[]] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: groupAttrs } = useSplitAttrs();\n\n/**\n * The buttons are real `<button>`s, so a `<fieldset disabled>` above this\n * group already makes each of them inert — but Reka's roving focus is what\n * decides where Tab lands, and it is driven by the group's own `disabled`,\n * which the fieldset never reaches. Its own `disabled` prop sets that to\n * `-1`, exactly as SegmentedControl does.\n *\n * Read off the DOM rather than taken from the Field context, which publishes\n * no `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n // The fieldset beats the prop in both directions, exactly as it does for\n // the buttons themselves.\n disabled: groupDisabled.value ? true : props.disabled,\n}));\n\n/**\n * Reka's own model is `AcceptableValue` for `\"single\"` and `AcceptableValue[]`\n * for `\"multiple\"`, and its typing has no `undefined` rung under\n * `exactOptionalPropertyTypes`. The seam's contract adds `null` for \"nothing\n * pressed\" and keeps a `null`/absent model from reaching Reka as a stray\n * non-string: a single group with nothing pressed shows Reka `\"\"`, which is\n * no item's value — and `data-state=\"off\"` on every button — in both engines.\n */\nconst rootValue = computed<string | string[]>(() => {\n if (props.type === \"multiple\") {\n return Array.isArray(props.modelValue) ? props.modelValue : [];\n }\n return typeof props.modelValue === \"string\" ? props.modelValue : \"\";\n});\n\nfunction onUpdate(value: AcceptableValue | AcceptableValue[]) {\n if (props.type === \"multiple\") {\n // Every item's value is a caller-provided `string`, so the array Reka\n // hands back can only hold those strings.\n emit(\"update:modelValue\", Array.isArray(value) ? (value as string[]) : []);\n } else {\n // Reka hands back `undefined` when the pressed button is pressed again —\n // the seam publishes that as `null`, the model's spelling of \"nothing\".\n emit(\"update:modelValue\", typeof value === \"string\" && value !== \"\" ? value : null);\n }\n}\n\nconst iconClass = computed(() => (props.size === \"sm\" ? \"size-3.5\" : \"size-4\"));\n</script>\n\n<template>\n <ToggleGroupRoot\n ref=\"root\"\n :type=\"type\"\n :model-value=\"rootValue\"\n orientation=\"horizontal\"\n :disabled=\"field.disabled\"\n v-bind=\"{ ...groupAttrs, ...field.attrs }\"\n :class=\"\n cn(\n 'inline-flex items-center gap-0.5 rounded-md p-0.5',\n // The track has three resting looks. `secondary` is SegmentedControl's\n // own track — a muted well with a rim; `outline` keeps the rim but\n // sits on the page ground; `ghost` has neither rim nor well and leans\n // entirely on the buttons.\n variant === 'secondary' && 'border border-input bg-muted',\n variant === 'outline' && 'border border-input bg-background',\n variant === 'ghost' && 'bg-transparent',\n // The one thing the *track* has left to say when the whole group is\n // unavailable: the rim slackening from `input` to the lighter\n // `border` — the well is already spent. A ghost track has no rim to\n // slacken, so it says it through its buttons alone.\n field.disabled && variant !== 'ghost' && 'border-border cursor-not-allowed',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onUpdate\"\n >\n <RekaToggleGroupItem\n v-for=\"opt in items\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n :disabled=\"opt.disabled ?? false\"\n v-bind=\"optional({ 'data-testid': opt.testId })\"\n :class=\"\n cn(\n // Named, not the bare `group`: this control is dropped into\n // application chrome, and a bare `group-*` would also answer to any\n // ancestor a host happened to mark up as one.\n // `min-h-6`/`min-w-6` are the WCAG 2.5.8 target floors made\n // explicit in the sm form, whose one-character labels would\n // otherwise collapse under 24px.\n 'group/item relative inline-flex items-center justify-center gap-1.5 rounded-sm text-muted-foreground',\n size === 'sm' && 'min-h-6 min-w-6 px-1.5 py-px text-micro',\n size === 'md' && 'min-h-8 px-3 py-1 text-sm',\n size === 'lg' && 'min-h-10 px-4 py-1.5',\n 'data-[state=off]:hover:bg-subtle',\n // The pressed state is fill plus weight plus `aria-pressed` — never\n // colour alone.\n 'data-[state=on]:bg-primary data-[state=on]:font-medium data-[state=on]:text-primary-foreground data-[state=on]:shadow-sm',\n 'active:scale-press',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // No `opacity` here either, for SegmentedControl's reason: the label\n // is most of the button, and a dim took it to 1.96:1. Unavailability\n // is a fill instead — the cell drains to the page ground — and the\n // mute lives on the span, where it beats whatever the button\n // resolved to.\n 'disabled:pointer-events-none data-[disabled]:bg-background',\n )\n \"\n >\n <component\n :is=\"opt.icon\"\n v-if=\"opt.icon\"\n aria-hidden=\"true\"\n :class=\"cn('shrink-0', iconClass)\"\n />\n <span class=\"group-data-[disabled]/item:text-muted-foreground\">{{ opt.label }}</span>\n </RekaToggleGroupItem>\n </ToggleGroupRoot>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
/** One toggle inside a {@link ToggleGroup}. */
|
|
3
|
+
export interface ToggleGroupItem {
|
|
4
|
+
/** Matched against `modelValue`. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Visible label, rendered inside the button. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Optional glyph rendered before the label; hidden from the tree, the label is the name. */
|
|
9
|
+
icon?: Component;
|
|
10
|
+
/** Disables this button alone, leaving its neighbours toggleable. */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** Forwarded to the button's DOM node — lets hosts keep stable test hooks. */
|
|
13
|
+
testId?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<{
|
|
18
|
+
/** The pressed value (`"single"`), the pressed values (`"multiple"`), or nothing pressed. */
|
|
19
|
+
modelValue?: string | string[] | null;
|
|
20
|
+
/** The buttons to render, in order. */
|
|
21
|
+
items: ToggleGroupItem[];
|
|
22
|
+
/** `"single"` models one value or `null`; `"multiple"` a `string[]`. */
|
|
23
|
+
type?: "single" | "multiple";
|
|
24
|
+
/** The track's resting look; a pressed button fills with the primary token in every variant. */
|
|
25
|
+
variant?: "secondary" | "outline" | "ghost";
|
|
26
|
+
/** `"sm"` is the compressed form for dense chrome — e.g. an editor toolbar. */
|
|
27
|
+
size?: "sm" | "md" | "lg";
|
|
28
|
+
/** Disables every button at once, distinct from a single item's own `disabled`. Unset defers to a wrapping Field. */
|
|
29
|
+
disabled?: boolean | undefined;
|
|
30
|
+
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
31
|
+
"update:modelValue": (value: string | string[] | null) => any;
|
|
32
|
+
}, string, import("vue").PublicProps, Readonly<{
|
|
33
|
+
/** The pressed value (`"single"`), the pressed values (`"multiple"`), or nothing pressed. */
|
|
34
|
+
modelValue?: string | string[] | null;
|
|
35
|
+
/** The buttons to render, in order. */
|
|
36
|
+
items: ToggleGroupItem[];
|
|
37
|
+
/** `"single"` models one value or `null`; `"multiple"` a `string[]`. */
|
|
38
|
+
type?: "single" | "multiple";
|
|
39
|
+
/** The track's resting look; a pressed button fills with the primary token in every variant. */
|
|
40
|
+
variant?: "secondary" | "outline" | "ghost";
|
|
41
|
+
/** `"sm"` is the compressed form for dense chrome — e.g. an editor toolbar. */
|
|
42
|
+
size?: "sm" | "md" | "lg";
|
|
43
|
+
/** Disables every button at once, distinct from a single item's own `disabled`. Unset defers to a wrapping Field. */
|
|
44
|
+
disabled?: boolean | undefined;
|
|
45
|
+
}> & Readonly<{
|
|
46
|
+
"onUpdate:modelValue"?: (value: string | string[] | null) => any;
|
|
47
|
+
}>, {
|
|
48
|
+
size: "sm" | "md" | "lg";
|
|
49
|
+
type: "single" | "multiple";
|
|
50
|
+
disabled: boolean | undefined;
|
|
51
|
+
variant: "secondary" | "outline" | "ghost";
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
//# sourceMappingURL=ToggleGroup.vue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToggleGroup.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/toggle-group/src/ToggleGroup.vue"],"names":[],"mappings":"AA6NA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAErC,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,6FAA6F;IAC7F,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;wBACoB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAkCd,6FAA6F;iBAChF,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IACrC,uCAAuC;WAChC,eAAe,EAAE;IACxB,wEAAwE;WACjE,QAAQ,GAAG,UAAU;IAC5B,gGAAgG;cACtF,WAAW,GAAG,SAAS,GAAG,OAAO;IAC3C,+EAA+E;WACxE,IAAI,GAAG,IAAI,GAAG,IAAI;IACzB,qHAAqH;eAC1G,OAAO,GAAG,SAAS;;;;IAX9B,6FAA6F;iBAChF,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IACrC,uCAAuC;WAChC,eAAe,EAAE;IACxB,wEAAwE;WACjE,QAAQ,GAAG,UAAU;IAC5B,gGAAgG;cACtF,WAAW,GAAG,SAAS,GAAG,OAAO;IAC3C,+EAA+E;WACxE,IAAI,GAAG,IAAI,GAAG,IAAI;IACzB,qHAAqH;eAC1G,OAAO,GAAG,SAAS;;;;UAFvB,IAAI,GAAG,IAAI,GAAG,IAAI;UAJlB,QAAQ,GAAG,UAAU;cAMjB,OAAO,GAAG,SAAS;aAJpB,WAAW,GAAG,SAAS,GAAG,OAAO;6EAiS3C,CAAC"}
|