@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Announce } from "./context";
|
|
2
|
+
/**
|
|
3
|
+
* The announcement seam. Call once in `setup()`:
|
|
4
|
+
*
|
|
5
|
+
* const announce = useAnnounce();
|
|
6
|
+
* announce("3 results found"); // politely
|
|
7
|
+
* announce("Upload failed", "assertive"); // interrupting
|
|
8
|
+
*
|
|
9
|
+
* Under a mounted `<LiveRegion>` the message goes to the region registered
|
|
10
|
+
* for that politeness; with no region in the tree it goes to the standalone
|
|
11
|
+
* pair that this call mounts on `document.body` — mounted here, at setup
|
|
12
|
+
* time, so the regions pre-exist the first message rather than appearing in
|
|
13
|
+
* the same tick as it. SSR-safe: with no `document` there is nowhere to
|
|
14
|
+
* announce to, and the function is a no-op.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useAnnounce(): Announce;
|
|
17
|
+
//# sourceMappingURL=useAnnounce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAnnounce.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/live-region/src/useAnnounce.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,IAAI,QAAQ,CAGtC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LIVE_REGION_KEY, announce, ensureStandaloneRegions } from "./context.js";
|
|
2
|
+
import { inject } from "vue";
|
|
3
|
+
//#region packages/primitives/live-region/src/useAnnounce.ts
|
|
4
|
+
/**
|
|
5
|
+
* The announcement seam. Call once in `setup()`:
|
|
6
|
+
*
|
|
7
|
+
* const announce = useAnnounce();
|
|
8
|
+
* announce("3 results found"); // politely
|
|
9
|
+
* announce("Upload failed", "assertive"); // interrupting
|
|
10
|
+
*
|
|
11
|
+
* Under a mounted `<LiveRegion>` the message goes to the region registered
|
|
12
|
+
* for that politeness; with no region in the tree it goes to the standalone
|
|
13
|
+
* pair that this call mounts on `document.body` — mounted here, at setup
|
|
14
|
+
* time, so the regions pre-exist the first message rather than appearing in
|
|
15
|
+
* the same tick as it. SSR-safe: with no `document` there is nowhere to
|
|
16
|
+
* announce to, and the function is a no-op.
|
|
17
|
+
*/
|
|
18
|
+
function useAnnounce() {
|
|
19
|
+
ensureStandaloneRegions();
|
|
20
|
+
return inject(LIVE_REGION_KEY, null)?.announce ?? announce;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { useAnnounce };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=useAnnounce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAnnounce.js","names":[],"sources":["../../../../../packages/primitives/live-region/src/useAnnounce.ts"],"sourcesContent":["import { inject } from \"vue\";\nimport { LIVE_REGION_KEY, ensureStandaloneRegions, announce } from \"./context\";\nimport type { Announce } from \"./context\";\n\n/**\n * The announcement seam. Call once in `setup()`:\n *\n * const announce = useAnnounce();\n * announce(\"3 results found\"); // politely\n * announce(\"Upload failed\", \"assertive\"); // interrupting\n *\n * Under a mounted `<LiveRegion>` the message goes to the region registered\n * for that politeness; with no region in the tree it goes to the standalone\n * pair that this call mounts on `document.body` — mounted here, at setup\n * time, so the regions pre-exist the first message rather than appearing in\n * the same tick as it. SSR-safe: with no `document` there is nowhere to\n * announce to, and the function is a no-op.\n */\nexport function useAnnounce(): Announce {\n ensureStandaloneRegions();\n return inject(LIVE_REGION_KEY, null)?.announce ?? announce;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,SAAgB,cAAwB;CACtC,wBAAwB;CACxB,OAAO,OAAO,iBAAiB,IAAI,CAAC,EAAE,YAAY;AACpD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menubar.js","names":[],"sources":["../../../../../packages/primitives/menubar/src/Menubar.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Menubar — the classic desktop-app menu strip (File · View · Help …).\n *\n * Data-driven: pass a `menus` array; the component owns open/close, hover-to-switch,\n * keyboard nav and click-outside. Selecting an item emits `select` with its `command`\n * id — the host maps that id to an action (keeps the primitive free of app logic).\n *\n * A11y: WAI-ARIA menubar semantics over real DOM focus, not aria-activedescendant.\n * Opening a menu moves focus onto its first enabled row and the arrows walk it from\n * there, so \"the highlighted row\" and \"the focused row\" are the same thing by\n * construction and a screen reader announces each walked row natively — no\n * bookkeeping that could drift out of sync with what is actually announced.\n */\nexport interface MenubarItem {\n /** Visible label. */\n label: string;\n /** Command id emitted on select. Omit for a non-actionable row. */\n command?: string;\n /** Right-aligned accelerator hint, e.g. \"Ctrl+S\". */\n shortcut?: string;\n /** Render a divider instead of an item (label ignored). */\n separator?: boolean;\n disabled?: boolean;\n}\n\nexport interface MenubarMenu {\n id: string;\n label: string;\n items: MenubarItem[];\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, nextTick } from \"vue\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\n\nconst props = defineProps<{\n /** The top-level menus; their reading order follows `dir`. */\n menus: MenubarMenu[];\n /**\n * Reading direction. Right-to-left mirrors the strip's arrow keys —\n * ArrowRight travels toward the visual left — which a hand-rolled\n * handler must do itself, unlike its Reka siblings.\n */\n dir?: \"ltr\" | \"rtl\";\n}>();\nconst emit = defineEmits<{ select: [command: string] }>();\n\n// Horizontal arrows are visual, not logical: in right-to-left, ArrowRight\n// moves toward the previous menu.\nconst horizontal = computed(() => (props.dir === \"rtl\" ? -1 : 1) as 1 | -1);\n\nconst openId = ref<string | null>(null);\nconst activeIndex = ref(-1); // the focused row within the open menu\n// String refs would do here only outside a v-for; inside one Vue collects them\n// into arrays. Function refs keyed BY MENU ID instead: during a menu switch the\n// incoming panel can mount before the outgoing one unmounts, so a single\n// latest-wins slot would let the outgoing null clobber the just-bound element.\nconst panelEls = new Map<string, HTMLElement>();\nconst triggerEls = new Map<string, HTMLButtonElement>();\n\nfunction bindPanel(id: string, el: unknown): void {\n if (el instanceof HTMLElement) panelEls.set(id, el);\n else panelEls.delete(id);\n}\n\nfunction bindTrigger(id: string, el: unknown): void {\n if (el instanceof HTMLButtonElement) triggerEls.set(id, el);\n else triggerEls.delete(id);\n}\n\nfunction boundPanel(): HTMLElement | null {\n return openId.value ? (panelEls.get(openId.value) ?? null) : null;\n}\n\nconst openMenu = (menu: MenubarMenu | undefined) => menu?.items ?? [];\nconst firstEnabled = (items: MenubarItem[], from = 0, dir = 1): number => {\n for (let i = from; i >= 0 && i < items.length; i += dir) {\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n};\n\n/** One arrow-step away from `from`, wrapping through every slot exactly once. */\nfunction neighborEnabled(items: MenubarItem[], from: number, dir: 1 | -1): number {\n const n = items.length;\n for (let step = 1; step <= n; step++) {\n const i = (((from + dir * step) % n) + n) % n;\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n}\n\n/** Rows render only for non-separator items, so an item index maps to its button's ordinal among the rendered ones. */\nfunction rowOrdinal(items: MenubarItem[], index: number): number {\n let ordinal = -1;\n for (let k = 0; k <= index && k < items.length; k++) {\n if (!items[k]?.separator) ordinal++;\n }\n return ordinal;\n}\n\nfunction rowButtons(): HTMLButtonElement[] {\n const panel = boundPanel();\n return panel\n ? Array.from(panel.querySelectorAll<HTMLButtonElement>('button[role=\"menuitem\"]'))\n : [];\n}\n\nfunction focusRow(items: MenubarItem[], index: number): void {\n rowButtons()[rowOrdinal(items, index)]?.focus();\n}\n\nfunction goTo(items: MenubarItem[], index: number): void {\n if (index < 0) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\nasync function openAndFocusFirst(id: string, items: MenubarItem[]): Promise<void> {\n await nextTick();\n // A hover or another key can supersede this open within the same tick; only\n // the menu that still owns `openId` may claim focus.\n if (openId.value !== id) return;\n goTo(items, firstEnabled(items));\n}\n\nasync function toggle(id: string): Promise<void> {\n if (openId.value === id) {\n close(true);\n return;\n }\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n await openAndFocusFirst(id, menu.items);\n}\n\nasync function hover(id: string, via: \"pointer\" | \"trigger-focus\"): Promise<void> {\n if (openId.value === null || openId.value === id) return;\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n // Pointer switching takes focus into the new menu like a desktop menubar.\n // A switch caused by tabbing between triggers must not yank focus out of\n // the tab order mid-Tab, so it switches the panel and leaves focus alone.\n if (via === \"pointer\") await openAndFocusFirst(id, menu.items);\n}\n\nfunction choose(item: MenubarItem): void {\n if (item.separator || item.disabled) return;\n close(true);\n if (item.command) emit(\"select\", item.command);\n}\n\nfunction close(restoreFocus = false): void {\n const id = openId.value;\n openId.value = null;\n activeIndex.value = -1;\n // Escape and selection hand focus back to the owning trigger; Tab-out and\n // outside clicks do not — focus has already gone where the user sent it.\n if (restoreFocus && id) triggerEls.get(id)?.focus();\n}\n\nasync function moveMenu(dir: 1 | -1): Promise<void> {\n const ids = props.menus.map((m) => m.id);\n const cur = openId.value ? ids.indexOf(openId.value) : 0;\n const id = ids[(cur + dir + ids.length) % ids.length];\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n}\n\n/**\n * Pointer highlight of an enabled row moves focus with it — under the roving\n * model the highlighted row IS the focused row, and screen readers announce\n * focus changes, not CSS classes.\n */\nfunction pointAtRow(items: MenubarItem[], item: MenubarItem, index: number): void {\n if (item.disabled || item.separator || activeIndex.value === index) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\n/** Native focus landing on a row keeps `activeIndex` truthful about it. */\nfunction noteFocus(item: MenubarItem, index: number): void {\n if (item.disabled || item.separator) return;\n activeIndex.value = index;\n}\n\n/**\n * The APG's closed-state traversal: Left/Right across the strip land on the\n * neighbouring trigger without opening anything — opening is ArrowDown,\n * Enter or Space's job.\n */\nfunction moveTriggerFocus(from: string, dir: 1 | -1): void {\n const ids = props.menus.map((m) => m.id);\n const cur = ids.indexOf(from);\n if (cur === -1) return;\n triggerEls.get(ids[(cur + dir + ids.length) % ids.length] ?? \"\")?.focus();\n}\n\nasync function onTriggerKeydown(e: KeyboardEvent, menu: MenubarMenu): Promise<void> {\n switch (e.key) {\n case \"ArrowDown\":\n case \"Enter\":\n case \" \":\n e.preventDefault(); // suppresses the button's own activation click\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n break;\n case \"ArrowRight\":\n e.preventDefault();\n if (openId.value) void moveMenu(horizontal.value);\n else moveTriggerFocus(menu.id, horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n if (openId.value) void moveMenu(-horizontal.value as 1 | -1);\n else moveTriggerFocus(menu.id, -horizontal.value as 1 | -1);\n break;\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n }\n}\n\nfunction onMenuKeydown(e: KeyboardEvent, menu: MenubarMenu): void {\n const items = menu.items;\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, 1));\n break;\n case \"ArrowUp\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, -1));\n break;\n case \"Home\":\n e.preventDefault();\n goTo(items, firstEnabled(items, 0, 1));\n break;\n case \"End\":\n e.preventDefault();\n goTo(items, firstEnabled(items, items.length - 1, -1));\n break;\n case \"ArrowRight\":\n e.preventDefault();\n void moveMenu(horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n void moveMenu(-horizontal.value as 1 | -1);\n break;\n // Handled here rather than left to the button's native activation:\n // preventDefault stops the browser's synthesized click, which would run\n // choose() a second time alongside this delegated handler.\n case \"Enter\":\n case \" \": {\n e.preventDefault();\n const item = items[activeIndex.value];\n if (item) choose(item);\n break;\n }\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n }\n}\n\nfunction onRootFocusout(e: FocusEvent): void {\n if (!openId.value) return;\n const root = e.currentTarget;\n const next = e.relatedTarget;\n // Tab out of the strip dismisses the menu and leaves focus where the browser\n // put it. Moves that stay inside the menubar (opening handing focus to a\n // row) keep it.\n if (!(root instanceof Element) || !(next instanceof Node) || !root.contains(next)) close();\n}\n\n// A wrapper, not `close` itself: the listener's event argument would otherwise\n// arrive as `restoreFocus`, and an outside click would yank focus back to the\n// trigger instead of leaving it where the press landed.\nconst onWindowClick = (): void => close();\nonMounted(() => window.addEventListener(\"click\", onWindowClick));\nonBeforeUnmount(() => window.removeEventListener(\"click\", onWindowClick));\n</script>\n\n<template>\n <!-- The attribute rides the strip itself, not just the prop: a host may\n pass dir without setting document direction, and both the flex order\n and the mirrored keys must agree with what is on screen. -->\n <div\n :dir=\"dir\"\n role=\"menubar\"\n class=\"flex items-center gap-0.5\"\n style=\"-webkit-app-region: no-drag\"\n @click.stop\n @focusout=\"onRootFocusout\"\n >\n <div v-for=\"menu in menus\" :key=\"menu.id\" class=\"relative\">\n <button\n :id=\"`menubar-trigger-${menu.id}`\"\n :ref=\"(el) => bindTrigger(menu.id, el)\"\n type=\"button\"\n role=\"menuitem\"\n :aria-haspopup=\"'menu'\"\n :aria-expanded=\"openId === menu.id\"\n class=\"min-h-6 rounded-sm px-2 py-1 text-xs text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:bg-subtle focus-visible:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n :class=\"openId === menu.id && 'bg-subtle text-foreground'\"\n @click.stop=\"toggle(menu.id)\"\n @mouseenter=\"hover(menu.id, 'pointer')\"\n @focus=\"hover(menu.id, 'trigger-focus')\"\n @keydown=\"onTriggerKeydown($event, menu)\"\n >\n {{ menu.label }}\n </button>\n\n <!-- Scoped presence, matching every sibling overlay: enter animates in,\n leave answers with scale-out. The unconditional mount-only animation\n this used to carry never plays on close at all. In jsdom, with no\n stylesheet loaded, Vue computes zero animation duration and ends both\n phases immediately, so tests see plain unmount. -->\n <Transition enter-active-class=\"animate-scale-in\" leave-active-class=\"animate-scale-out\">\n <div\n v-if=\"openId === menu.id\"\n :ref=\"(el) => bindPanel(menu.id, el)\"\n role=\"menu\"\n :aria-label=\"menu.label\"\n tabindex=\"-1\"\n class=\"absolute left-0 top-full z-overlay mt-1 min-w-[13rem] origin-top-left rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\"\n @click.stop\n @keydown=\"onMenuKeydown($event, menu)\"\n >\n <template v-for=\"(item, i) in openMenu(menu)\" :key=\"i\">\n <div v-if=\"item.separator\" role=\"separator\" class=\"my-1 h-px bg-border\" />\n <!-- The row's colour, not its alpha, says it is unavailable. A menu\n row is nothing but its label, and `disabled:opacity-40` composited\n that label to 2.40:1 on the popover — a disabled row a reader\n cannot make out tells them nothing about what is unavailable,\n which is the only thing it is there to say. Muted instead: 5.76:1\n at rest, and a clear drop from the 15.46:1 an available row wears,\n so the state still reads at a glance. The shortcut beside it\n declares `text-muted-foreground` on its own element, so it lands\n on the same colour rather than inheriting this one.\n\n Rows sit at text-sm/py-1.5 like DropdownMenu's — one row grammar\n across menus; the compact trigger above stays desktop chrome.\n Every row carries tabindex -1: focus arrives programmatically,\n never by tabbing through the strip. -->\n <button\n v-else\n type=\"button\"\n role=\"menuitem\"\n tabindex=\"-1\"\n :disabled=\"item.disabled\"\n :data-highlighted=\"activeIndex === i || undefined\"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n class=\"flex w-full items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-left text-sm text-foreground transition-colors duration-fast ease-out hover:bg-subtle disabled:pointer-events-none disabled:text-muted-foreground data-[highlighted]:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo animate-fade-rise\"\n @click=\"choose(item)\"\n @mouseenter=\"pointAtRow(openMenu(menu), item, i)\"\n @focus=\"noteFocus(item, i)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </button>\n </template>\n </div>\n </Transition>\n </div>\n </div>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"Menubar.js","names":[],"sources":["../../../../../packages/primitives/menubar/src/Menubar.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Menubar — the classic desktop-app menu strip (File · View · Help …).\n *\n * Data-driven: pass a `menus` array; the component owns open/close, hover-to-switch,\n * keyboard nav and click-outside. Selecting an item emits `select` with its `command`\n * id — the host maps that id to an action (keeps the primitive free of app logic).\n *\n * A11y: WAI-ARIA menubar semantics over real DOM focus, not aria-activedescendant.\n * Opening a menu moves focus onto its first enabled row and the arrows walk it from\n * there, so \"the highlighted row\" and \"the focused row\" are the same thing by\n * construction and a screen reader announces each walked row natively — no\n * bookkeeping that could drift out of sync with what is actually announced.\n */\nexport interface MenubarItem {\n /** Visible label. */\n label: string;\n /** Command id emitted on select. Omit for a non-actionable row. */\n command?: string;\n /** Right-aligned accelerator hint, e.g. \"Ctrl+S\". */\n shortcut?: string;\n /** Render a divider instead of an item (label ignored). */\n separator?: boolean;\n disabled?: boolean;\n}\n\nexport interface MenubarMenu {\n id: string;\n label: string;\n items: MenubarItem[];\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, nextTick } from \"vue\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\n\nconst props = defineProps<{\n /** The top-level menus; their reading order follows `dir`. */\n menus: MenubarMenu[];\n /**\n * Reading direction. Right-to-left mirrors the strip's arrow keys —\n * ArrowRight travels toward the visual left — which a hand-rolled\n * handler must do itself, unlike its Reka siblings.\n */\n dir?: \"ltr\" | \"rtl\";\n}>();\nconst emit = defineEmits<{ select: [command: string] }>();\n\n// Horizontal arrows are visual, not logical: in right-to-left, ArrowRight\n// moves toward the previous menu.\nconst horizontal = computed(() => (props.dir === \"rtl\" ? -1 : 1) as 1 | -1);\n\nconst openId = ref<string | null>(null);\nconst activeIndex = ref(-1); // the focused row within the open menu\n// String refs would do here only outside a v-for; inside one Vue collects them\n// into arrays. Function refs keyed BY MENU ID instead: during a menu switch the\n// incoming panel can mount before the outgoing one unmounts, so a single\n// latest-wins slot would let the outgoing null clobber the just-bound element.\nconst panelEls = new Map<string, HTMLElement>();\nconst triggerEls = new Map<string, HTMLButtonElement>();\n\nfunction bindPanel(id: string, el: unknown): void {\n if (el instanceof HTMLElement) panelEls.set(id, el);\n else panelEls.delete(id);\n}\n\nfunction bindTrigger(id: string, el: unknown): void {\n if (el instanceof HTMLButtonElement) triggerEls.set(id, el);\n else triggerEls.delete(id);\n}\n\nfunction boundPanel(): HTMLElement | null {\n return openId.value ? (panelEls.get(openId.value) ?? null) : null;\n}\n\nconst openMenu = (menu: MenubarMenu | undefined) => menu?.items ?? [];\nconst firstEnabled = (items: MenubarItem[], from = 0, dir = 1): number => {\n for (let i = from; i >= 0 && i < items.length; i += dir) {\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n};\n\n/** One arrow-step away from `from`, wrapping through every slot exactly once. */\nfunction neighborEnabled(items: MenubarItem[], from: number, dir: 1 | -1): number {\n const n = items.length;\n for (let step = 1; step <= n; step++) {\n const i = (((from + dir * step) % n) + n) % n;\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n}\n\n/** Rows render only for non-separator items, so an item index maps to its button's ordinal among the rendered ones. */\nfunction rowOrdinal(items: MenubarItem[], index: number): number {\n let ordinal = -1;\n for (let k = 0; k <= index && k < items.length; k++) {\n if (!items[k]?.separator) ordinal++;\n }\n return ordinal;\n}\n\nfunction rowButtons(): HTMLButtonElement[] {\n const panel = boundPanel();\n return panel\n ? Array.from(panel.querySelectorAll<HTMLButtonElement>('button[role=\"menuitem\"]'))\n : [];\n}\n\nfunction focusRow(items: MenubarItem[], index: number): void {\n rowButtons()[rowOrdinal(items, index)]?.focus();\n}\n\nfunction goTo(items: MenubarItem[], index: number): void {\n if (index < 0) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\nasync function openAndFocusFirst(id: string, items: MenubarItem[]): Promise<void> {\n await nextTick();\n // A hover or another key can supersede this open within the same tick; only\n // the menu that still owns `openId` may claim focus.\n if (openId.value !== id) return;\n goTo(items, firstEnabled(items));\n}\n\nasync function toggle(id: string): Promise<void> {\n if (openId.value === id) {\n close(true);\n return;\n }\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n await openAndFocusFirst(id, menu.items);\n}\n\nasync function hover(id: string, via: \"pointer\" | \"trigger-focus\"): Promise<void> {\n if (openId.value === null || openId.value === id) return;\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n // Pointer switching takes focus into the new menu like a desktop menubar.\n // A switch caused by tabbing between triggers must not yank focus out of\n // the tab order mid-Tab, so it switches the panel and leaves focus alone.\n if (via === \"pointer\") await openAndFocusFirst(id, menu.items);\n}\n\nfunction choose(item: MenubarItem): void {\n if (item.separator || item.disabled) return;\n close(true);\n if (item.command) emit(\"select\", item.command);\n}\n\nfunction close(restoreFocus = false): void {\n const id = openId.value;\n openId.value = null;\n activeIndex.value = -1;\n // Escape and selection hand focus back to the owning trigger; Tab-out and\n // outside clicks do not — focus has already gone where the user sent it.\n if (restoreFocus && id) triggerEls.get(id)?.focus();\n}\n\nasync function moveMenu(dir: 1 | -1): Promise<void> {\n const ids = props.menus.map((m) => m.id);\n const cur = openId.value ? ids.indexOf(openId.value) : 0;\n const id = ids[(cur + dir + ids.length) % ids.length];\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n}\n\n/**\n * Pointer highlight of an enabled row moves focus with it — under the roving\n * model the highlighted row IS the focused row, and screen readers announce\n * focus changes, not CSS classes.\n */\nfunction pointAtRow(items: MenubarItem[], item: MenubarItem, index: number): void {\n if (item.disabled || item.separator || activeIndex.value === index) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\n/** Native focus landing on a row keeps `activeIndex` truthful about it. */\nfunction noteFocus(item: MenubarItem, index: number): void {\n if (item.disabled || item.separator) return;\n activeIndex.value = index;\n}\n/*\n * The typeahead. APG menubars match a prefix of the item's label, starting\n * after the focused row and wrapping; a character's repeated key re-cycles\n * to its own next match rather than extending the prefix to a non-match, so\n * the pending buffer is kept but the repeated final character is not\n * appended. The buffer is 500 ms, case-insensitive, and the timer is released\n * on unmount so a menubar torn down mid-typeahead leaves nothing pending —\n * the same contract the TreeView sibling keeps.\n */\nconst TYPEAHEAD_RESET_MS = 500;\nlet typeaheadBuffer = \"\";\nlet typeaheadTimer: ReturnType<typeof setTimeout> | undefined;\n\nfunction typeaheadJump(items: MenubarItem[], from: number): void {\n const query = typeaheadBuffer;\n // The whole buffered prefix decides the match: repeated characters extend\n // it, and a fresh character restarts it, so \"Co\" reaches \"Copy\" only when\n // both keys follow each other within the timeout.\n const n = items.length;\n for (let step = 1; step <= n; step++) {\n const i = (from + step) % n;\n const item = items[i];\n if (item && !item.separator && !item.disabled && item.label.toLowerCase().startsWith(query)) {\n goTo(items, i);\n break;\n }\n }\n}\n\nfunction handleTypeahead(e: KeyboardEvent, items: MenubarItem[]): void {\n if (e.key.length !== 1 || e.altKey || e.ctrlKey || e.metaKey) return;\n if (e.key === \" \" || e.key === \"Enter\") return; // activation keys, handled above\n const char = e.key.toLowerCase();\n if (typeaheadBuffer.slice(-1) !== char) typeaheadBuffer += char;\n e.preventDefault();\n typeaheadJump(items, activeIndex.value);\n if (typeaheadTimer !== undefined) clearTimeout(typeaheadTimer);\n typeaheadTimer = setTimeout(() => {\n typeaheadBuffer = \"\";\n typeaheadTimer = undefined;\n }, TYPEAHEAD_RESET_MS);\n}\n\nonBeforeUnmount(() => {\n if (typeaheadTimer !== undefined) clearTimeout(typeaheadTimer);\n});\n\n/**\n * The APG's closed-state traversal: Left/Right across the strip land on the\n * neighbouring trigger without opening anything — opening is ArrowDown,\n * Enter or Space's job.\n */\nfunction moveTriggerFocus(from: string, dir: 1 | -1): void {\n const ids = props.menus.map((m) => m.id);\n const cur = ids.indexOf(from);\n if (cur === -1) return;\n triggerEls.get(ids[(cur + dir + ids.length) % ids.length] ?? \"\")?.focus();\n}\n\nasync function onTriggerKeydown(e: KeyboardEvent, menu: MenubarMenu): Promise<void> {\n switch (e.key) {\n case \"ArrowDown\":\n case \"Enter\":\n case \" \":\n e.preventDefault(); // suppresses the button's own activation click\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n break;\n case \"ArrowRight\":\n e.preventDefault();\n if (openId.value) void moveMenu(horizontal.value);\n else moveTriggerFocus(menu.id, horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n if (openId.value) void moveMenu(-horizontal.value as 1 | -1);\n else moveTriggerFocus(menu.id, -horizontal.value as 1 | -1);\n break;\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n }\n}\n\nfunction onMenuKeydown(e: KeyboardEvent, menu: MenubarMenu): void {\n const items = menu.items;\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, 1));\n break;\n case \"ArrowUp\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, -1));\n break;\n case \"Home\":\n e.preventDefault();\n goTo(items, firstEnabled(items, 0, 1));\n break;\n case \"End\":\n e.preventDefault();\n goTo(items, firstEnabled(items, items.length - 1, -1));\n break;\n case \"ArrowRight\":\n e.preventDefault();\n void moveMenu(horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n void moveMenu(-horizontal.value as 1 | -1);\n break;\n // Handled here rather than left to the button's native activation:\n // preventDefault stops the browser's synthesized click, which would run\n // choose() a second time alongside this delegated handler.\n case \"Enter\":\n case \" \": {\n e.preventDefault();\n const item = items[activeIndex.value];\n if (item) choose(item);\n break;\n }\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n // Every other key is a printable character: APG typeahead moves focus to\n // the next item whose label begins with it. `default` sees only these —\n // Space/Enter/Escape each have their own case above.\n default:\n handleTypeahead(e, items);\n break;\n }\n}\n\nfunction onRootFocusout(e: FocusEvent): void {\n if (!openId.value) return;\n const root = e.currentTarget;\n const next = e.relatedTarget;\n // Tab out of the strip dismisses the menu and leaves focus where the browser\n // put it. Moves that stay inside the menubar (opening handing focus to a\n // row) keep it.\n if (!(root instanceof Element) || !(next instanceof Node) || !root.contains(next)) close();\n}\n\n// A wrapper, not `close` itself: the listener's event argument would otherwise\n// arrive as `restoreFocus`, and an outside click would yank focus back to the\n// trigger instead of leaving it where the press landed.\nconst onWindowClick = (): void => close();\nonMounted(() => window.addEventListener(\"click\", onWindowClick));\nonBeforeUnmount(() => window.removeEventListener(\"click\", onWindowClick));\n</script>\n\n<template>\n <!-- The attribute rides the strip itself, not just the prop: a host may\n pass dir without setting document direction, and both the flex order\n and the mirrored keys must agree with what is on screen. -->\n <div\n :dir=\"dir\"\n role=\"menubar\"\n class=\"flex items-center gap-0.5\"\n style=\"-webkit-app-region: no-drag\"\n @click.stop\n @focusout=\"onRootFocusout\"\n >\n <div v-for=\"menu in menus\" :key=\"menu.id\" class=\"relative\">\n <button\n :id=\"`menubar-trigger-${menu.id}`\"\n :ref=\"(el) => bindTrigger(menu.id, el)\"\n type=\"button\"\n role=\"menuitem\"\n :aria-haspopup=\"'menu'\"\n :aria-expanded=\"openId === menu.id\"\n class=\"min-h-6 rounded-sm px-2 py-1 text-xs text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:bg-subtle focus-visible:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n :class=\"openId === menu.id && 'bg-subtle text-foreground'\"\n @click.stop=\"toggle(menu.id)\"\n @mouseenter=\"hover(menu.id, 'pointer')\"\n @focus=\"hover(menu.id, 'trigger-focus')\"\n @keydown=\"onTriggerKeydown($event, menu)\"\n >\n {{ menu.label }}\n </button>\n\n <!-- Scoped presence, matching every sibling overlay: enter animates in,\n leave answers with scale-out. The unconditional mount-only animation\n this used to carry never plays on close at all. In jsdom, with no\n stylesheet loaded, Vue computes zero animation duration and ends both\n phases immediately, so tests see plain unmount. -->\n <Transition enter-active-class=\"animate-scale-in\" leave-active-class=\"animate-scale-out\">\n <div\n v-if=\"openId === menu.id\"\n :ref=\"(el) => bindPanel(menu.id, el)\"\n role=\"menu\"\n :aria-label=\"menu.label\"\n tabindex=\"-1\"\n class=\"absolute left-0 top-full z-overlay mt-1 min-w-[13rem] origin-top-left rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\"\n @click.stop\n @keydown=\"onMenuKeydown($event, menu)\"\n >\n <template v-for=\"(item, i) in openMenu(menu)\" :key=\"i\">\n <div v-if=\"item.separator\" role=\"separator\" class=\"my-1 h-px bg-border\" />\n <!-- The row's colour, not its alpha, says it is unavailable. A menu\n row is nothing but its label, and `disabled:opacity-40` composited\n that label to 2.40:1 on the popover — a disabled row a reader\n cannot make out tells them nothing about what is unavailable,\n which is the only thing it is there to say. Muted instead: 5.76:1\n at rest, and a clear drop from the 15.46:1 an available row wears,\n so the state still reads at a glance. The shortcut beside it\n declares `text-muted-foreground` on its own element, so it lands\n on the same colour rather than inheriting this one.\n\n Rows sit at text-sm/py-1.5 like DropdownMenu's — one row grammar\n across menus; the compact trigger above stays desktop chrome.\n Every row carries tabindex -1: focus arrives programmatically,\n never by tabbing through the strip. -->\n <button\n v-else\n type=\"button\"\n role=\"menuitem\"\n tabindex=\"-1\"\n :disabled=\"item.disabled\"\n :data-highlighted=\"activeIndex === i || undefined\"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n class=\"flex w-full items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-left text-sm text-foreground transition-colors duration-fast ease-out hover:bg-subtle disabled:pointer-events-none disabled:text-muted-foreground data-[highlighted]:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo animate-fade-rise\"\n @click=\"choose(item)\"\n @mouseenter=\"pointAtRow(openMenu(menu), item, i)\"\n @focus=\"noteFocus(item, i)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </button>\n </template>\n </div>\n </Transition>\n </div>\n </div>\n</template>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menubar.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/menubar/src/Menubar.vue"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Menubar.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/menubar/src/Menubar.vue"],"names":[],"mappings":"AA2bA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,WAAW;IAC1B,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;wBACoB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAEhB,8DAA8D;WACvD,WAAW,EAAE;IACpB;;;;OAIG;UACG,KAAK,GAAG,KAAK;;;;IAPnB,8DAA8D;WACvD,WAAW,EAAE;IACpB;;;;OAIG;UACG,KAAK,GAAG,KAAK;;;kFA+hBjB,CAAC"}
|
|
@@ -149,6 +149,37 @@ var Menubar_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
|
|
|
149
149
|
if (item.disabled || item.separator) return;
|
|
150
150
|
activeIndex.value = index;
|
|
151
151
|
}
|
|
152
|
+
const TYPEAHEAD_RESET_MS = 500;
|
|
153
|
+
let typeaheadBuffer = "";
|
|
154
|
+
let typeaheadTimer;
|
|
155
|
+
function typeaheadJump(items, from) {
|
|
156
|
+
const query = typeaheadBuffer;
|
|
157
|
+
const n = items.length;
|
|
158
|
+
for (let step = 1; step <= n; step++) {
|
|
159
|
+
const i = (from + step) % n;
|
|
160
|
+
const item = items[i];
|
|
161
|
+
if (item && !item.separator && !item.disabled && item.label.toLowerCase().startsWith(query)) {
|
|
162
|
+
goTo(items, i);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function handleTypeahead(e, items) {
|
|
168
|
+
if (e.key.length !== 1 || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
169
|
+
if (e.key === " " || e.key === "Enter") return;
|
|
170
|
+
const char = e.key.toLowerCase();
|
|
171
|
+
if (typeaheadBuffer.slice(-1) !== char) typeaheadBuffer += char;
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
typeaheadJump(items, activeIndex.value);
|
|
174
|
+
if (typeaheadTimer !== void 0) clearTimeout(typeaheadTimer);
|
|
175
|
+
typeaheadTimer = setTimeout(() => {
|
|
176
|
+
typeaheadBuffer = "";
|
|
177
|
+
typeaheadTimer = void 0;
|
|
178
|
+
}, TYPEAHEAD_RESET_MS);
|
|
179
|
+
}
|
|
180
|
+
onBeforeUnmount(() => {
|
|
181
|
+
if (typeaheadTimer !== void 0) clearTimeout(typeaheadTimer);
|
|
182
|
+
});
|
|
152
183
|
/**
|
|
153
184
|
* The APG's closed-state traversal: Left/Right across the strip land on the
|
|
154
185
|
* neighbouring trigger without opening anything — opening is ArrowDown,
|
|
@@ -222,6 +253,8 @@ var Menubar_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
|
|
|
222
253
|
case "Escape":
|
|
223
254
|
e.preventDefault();
|
|
224
255
|
close(true);
|
|
256
|
+
break;
|
|
257
|
+
default: handleTypeahead(e, items);
|
|
225
258
|
}
|
|
226
259
|
}
|
|
227
260
|
function onRootFocusout(e) {
|
package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menubar.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/menubar/src/Menubar.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Menubar — the classic desktop-app menu strip (File · View · Help …).\n *\n * Data-driven: pass a `menus` array; the component owns open/close, hover-to-switch,\n * keyboard nav and click-outside. Selecting an item emits `select` with its `command`\n * id — the host maps that id to an action (keeps the primitive free of app logic).\n *\n * A11y: WAI-ARIA menubar semantics over real DOM focus, not aria-activedescendant.\n * Opening a menu moves focus onto its first enabled row and the arrows walk it from\n * there, so \"the highlighted row\" and \"the focused row\" are the same thing by\n * construction and a screen reader announces each walked row natively — no\n * bookkeeping that could drift out of sync with what is actually announced.\n */\nexport interface MenubarItem {\n /** Visible label. */\n label: string;\n /** Command id emitted on select. Omit for a non-actionable row. */\n command?: string;\n /** Right-aligned accelerator hint, e.g. \"Ctrl+S\". */\n shortcut?: string;\n /** Render a divider instead of an item (label ignored). */\n separator?: boolean;\n disabled?: boolean;\n}\n\nexport interface MenubarMenu {\n id: string;\n label: string;\n items: MenubarItem[];\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, nextTick } from \"vue\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\n\nconst props = defineProps<{\n /** The top-level menus; their reading order follows `dir`. */\n menus: MenubarMenu[];\n /**\n * Reading direction. Right-to-left mirrors the strip's arrow keys —\n * ArrowRight travels toward the visual left — which a hand-rolled\n * handler must do itself, unlike its Reka siblings.\n */\n dir?: \"ltr\" | \"rtl\";\n}>();\nconst emit = defineEmits<{ select: [command: string] }>();\n\n// Horizontal arrows are visual, not logical: in right-to-left, ArrowRight\n// moves toward the previous menu.\nconst horizontal = computed(() => (props.dir === \"rtl\" ? -1 : 1) as 1 | -1);\n\nconst openId = ref<string | null>(null);\nconst activeIndex = ref(-1); // the focused row within the open menu\n// String refs would do here only outside a v-for; inside one Vue collects them\n// into arrays. Function refs keyed BY MENU ID instead: during a menu switch the\n// incoming panel can mount before the outgoing one unmounts, so a single\n// latest-wins slot would let the outgoing null clobber the just-bound element.\nconst panelEls = new Map<string, HTMLElement>();\nconst triggerEls = new Map<string, HTMLButtonElement>();\n\nfunction bindPanel(id: string, el: unknown): void {\n if (el instanceof HTMLElement) panelEls.set(id, el);\n else panelEls.delete(id);\n}\n\nfunction bindTrigger(id: string, el: unknown): void {\n if (el instanceof HTMLButtonElement) triggerEls.set(id, el);\n else triggerEls.delete(id);\n}\n\nfunction boundPanel(): HTMLElement | null {\n return openId.value ? (panelEls.get(openId.value) ?? null) : null;\n}\n\nconst openMenu = (menu: MenubarMenu | undefined) => menu?.items ?? [];\nconst firstEnabled = (items: MenubarItem[], from = 0, dir = 1): number => {\n for (let i = from; i >= 0 && i < items.length; i += dir) {\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n};\n\n/** One arrow-step away from `from`, wrapping through every slot exactly once. */\nfunction neighborEnabled(items: MenubarItem[], from: number, dir: 1 | -1): number {\n const n = items.length;\n for (let step = 1; step <= n; step++) {\n const i = (((from + dir * step) % n) + n) % n;\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n}\n\n/** Rows render only for non-separator items, so an item index maps to its button's ordinal among the rendered ones. */\nfunction rowOrdinal(items: MenubarItem[], index: number): number {\n let ordinal = -1;\n for (let k = 0; k <= index && k < items.length; k++) {\n if (!items[k]?.separator) ordinal++;\n }\n return ordinal;\n}\n\nfunction rowButtons(): HTMLButtonElement[] {\n const panel = boundPanel();\n return panel\n ? Array.from(panel.querySelectorAll<HTMLButtonElement>('button[role=\"menuitem\"]'))\n : [];\n}\n\nfunction focusRow(items: MenubarItem[], index: number): void {\n rowButtons()[rowOrdinal(items, index)]?.focus();\n}\n\nfunction goTo(items: MenubarItem[], index: number): void {\n if (index < 0) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\nasync function openAndFocusFirst(id: string, items: MenubarItem[]): Promise<void> {\n await nextTick();\n // A hover or another key can supersede this open within the same tick; only\n // the menu that still owns `openId` may claim focus.\n if (openId.value !== id) return;\n goTo(items, firstEnabled(items));\n}\n\nasync function toggle(id: string): Promise<void> {\n if (openId.value === id) {\n close(true);\n return;\n }\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n await openAndFocusFirst(id, menu.items);\n}\n\nasync function hover(id: string, via: \"pointer\" | \"trigger-focus\"): Promise<void> {\n if (openId.value === null || openId.value === id) return;\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n // Pointer switching takes focus into the new menu like a desktop menubar.\n // A switch caused by tabbing between triggers must not yank focus out of\n // the tab order mid-Tab, so it switches the panel and leaves focus alone.\n if (via === \"pointer\") await openAndFocusFirst(id, menu.items);\n}\n\nfunction choose(item: MenubarItem): void {\n if (item.separator || item.disabled) return;\n close(true);\n if (item.command) emit(\"select\", item.command);\n}\n\nfunction close(restoreFocus = false): void {\n const id = openId.value;\n openId.value = null;\n activeIndex.value = -1;\n // Escape and selection hand focus back to the owning trigger; Tab-out and\n // outside clicks do not — focus has already gone where the user sent it.\n if (restoreFocus && id) triggerEls.get(id)?.focus();\n}\n\nasync function moveMenu(dir: 1 | -1): Promise<void> {\n const ids = props.menus.map((m) => m.id);\n const cur = openId.value ? ids.indexOf(openId.value) : 0;\n const id = ids[(cur + dir + ids.length) % ids.length];\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n}\n\n/**\n * Pointer highlight of an enabled row moves focus with it — under the roving\n * model the highlighted row IS the focused row, and screen readers announce\n * focus changes, not CSS classes.\n */\nfunction pointAtRow(items: MenubarItem[], item: MenubarItem, index: number): void {\n if (item.disabled || item.separator || activeIndex.value === index) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\n/** Native focus landing on a row keeps `activeIndex` truthful about it. */\nfunction noteFocus(item: MenubarItem, index: number): void {\n if (item.disabled || item.separator) return;\n activeIndex.value = index;\n}\n\n/**\n * The APG's closed-state traversal: Left/Right across the strip land on the\n * neighbouring trigger without opening anything — opening is ArrowDown,\n * Enter or Space's job.\n */\nfunction moveTriggerFocus(from: string, dir: 1 | -1): void {\n const ids = props.menus.map((m) => m.id);\n const cur = ids.indexOf(from);\n if (cur === -1) return;\n triggerEls.get(ids[(cur + dir + ids.length) % ids.length] ?? \"\")?.focus();\n}\n\nasync function onTriggerKeydown(e: KeyboardEvent, menu: MenubarMenu): Promise<void> {\n switch (e.key) {\n case \"ArrowDown\":\n case \"Enter\":\n case \" \":\n e.preventDefault(); // suppresses the button's own activation click\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n break;\n case \"ArrowRight\":\n e.preventDefault();\n if (openId.value) void moveMenu(horizontal.value);\n else moveTriggerFocus(menu.id, horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n if (openId.value) void moveMenu(-horizontal.value as 1 | -1);\n else moveTriggerFocus(menu.id, -horizontal.value as 1 | -1);\n break;\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n }\n}\n\nfunction onMenuKeydown(e: KeyboardEvent, menu: MenubarMenu): void {\n const items = menu.items;\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, 1));\n break;\n case \"ArrowUp\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, -1));\n break;\n case \"Home\":\n e.preventDefault();\n goTo(items, firstEnabled(items, 0, 1));\n break;\n case \"End\":\n e.preventDefault();\n goTo(items, firstEnabled(items, items.length - 1, -1));\n break;\n case \"ArrowRight\":\n e.preventDefault();\n void moveMenu(horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n void moveMenu(-horizontal.value as 1 | -1);\n break;\n // Handled here rather than left to the button's native activation:\n // preventDefault stops the browser's synthesized click, which would run\n // choose() a second time alongside this delegated handler.\n case \"Enter\":\n case \" \": {\n e.preventDefault();\n const item = items[activeIndex.value];\n if (item) choose(item);\n break;\n }\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n }\n}\n\nfunction onRootFocusout(e: FocusEvent): void {\n if (!openId.value) return;\n const root = e.currentTarget;\n const next = e.relatedTarget;\n // Tab out of the strip dismisses the menu and leaves focus where the browser\n // put it. Moves that stay inside the menubar (opening handing focus to a\n // row) keep it.\n if (!(root instanceof Element) || !(next instanceof Node) || !root.contains(next)) close();\n}\n\n// A wrapper, not `close` itself: the listener's event argument would otherwise\n// arrive as `restoreFocus`, and an outside click would yank focus back to the\n// trigger instead of leaving it where the press landed.\nconst onWindowClick = (): void => close();\nonMounted(() => window.addEventListener(\"click\", onWindowClick));\nonBeforeUnmount(() => window.removeEventListener(\"click\", onWindowClick));\n</script>\n\n<template>\n <!-- The attribute rides the strip itself, not just the prop: a host may\n pass dir without setting document direction, and both the flex order\n and the mirrored keys must agree with what is on screen. -->\n <div\n :dir=\"dir\"\n role=\"menubar\"\n class=\"flex items-center gap-0.5\"\n style=\"-webkit-app-region: no-drag\"\n @click.stop\n @focusout=\"onRootFocusout\"\n >\n <div v-for=\"menu in menus\" :key=\"menu.id\" class=\"relative\">\n <button\n :id=\"`menubar-trigger-${menu.id}`\"\n :ref=\"(el) => bindTrigger(menu.id, el)\"\n type=\"button\"\n role=\"menuitem\"\n :aria-haspopup=\"'menu'\"\n :aria-expanded=\"openId === menu.id\"\n class=\"min-h-6 rounded-sm px-2 py-1 text-xs text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:bg-subtle focus-visible:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n :class=\"openId === menu.id && 'bg-subtle text-foreground'\"\n @click.stop=\"toggle(menu.id)\"\n @mouseenter=\"hover(menu.id, 'pointer')\"\n @focus=\"hover(menu.id, 'trigger-focus')\"\n @keydown=\"onTriggerKeydown($event, menu)\"\n >\n {{ menu.label }}\n </button>\n\n <!-- Scoped presence, matching every sibling overlay: enter animates in,\n leave answers with scale-out. The unconditional mount-only animation\n this used to carry never plays on close at all. In jsdom, with no\n stylesheet loaded, Vue computes zero animation duration and ends both\n phases immediately, so tests see plain unmount. -->\n <Transition enter-active-class=\"animate-scale-in\" leave-active-class=\"animate-scale-out\">\n <div\n v-if=\"openId === menu.id\"\n :ref=\"(el) => bindPanel(menu.id, el)\"\n role=\"menu\"\n :aria-label=\"menu.label\"\n tabindex=\"-1\"\n class=\"absolute left-0 top-full z-overlay mt-1 min-w-[13rem] origin-top-left rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\"\n @click.stop\n @keydown=\"onMenuKeydown($event, menu)\"\n >\n <template v-for=\"(item, i) in openMenu(menu)\" :key=\"i\">\n <div v-if=\"item.separator\" role=\"separator\" class=\"my-1 h-px bg-border\" />\n <!-- The row's colour, not its alpha, says it is unavailable. A menu\n row is nothing but its label, and `disabled:opacity-40` composited\n that label to 2.40:1 on the popover — a disabled row a reader\n cannot make out tells them nothing about what is unavailable,\n which is the only thing it is there to say. Muted instead: 5.76:1\n at rest, and a clear drop from the 15.46:1 an available row wears,\n so the state still reads at a glance. The shortcut beside it\n declares `text-muted-foreground` on its own element, so it lands\n on the same colour rather than inheriting this one.\n\n Rows sit at text-sm/py-1.5 like DropdownMenu's — one row grammar\n across menus; the compact trigger above stays desktop chrome.\n Every row carries tabindex -1: focus arrives programmatically,\n never by tabbing through the strip. -->\n <button\n v-else\n type=\"button\"\n role=\"menuitem\"\n tabindex=\"-1\"\n :disabled=\"item.disabled\"\n :data-highlighted=\"activeIndex === i || undefined\"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n class=\"flex w-full items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-left text-sm text-foreground transition-colors duration-fast ease-out hover:bg-subtle disabled:pointer-events-none disabled:text-muted-foreground data-[highlighted]:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo animate-fade-rise\"\n @click=\"choose(item)\"\n @mouseenter=\"pointAtRow(openMenu(menu), item, i)\"\n @focus=\"noteFocus(item, i)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </button>\n </template>\n </div>\n </Transition>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Menubar.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/menubar/src/Menubar.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Menubar — the classic desktop-app menu strip (File · View · Help …).\n *\n * Data-driven: pass a `menus` array; the component owns open/close, hover-to-switch,\n * keyboard nav and click-outside. Selecting an item emits `select` with its `command`\n * id — the host maps that id to an action (keeps the primitive free of app logic).\n *\n * A11y: WAI-ARIA menubar semantics over real DOM focus, not aria-activedescendant.\n * Opening a menu moves focus onto its first enabled row and the arrows walk it from\n * there, so \"the highlighted row\" and \"the focused row\" are the same thing by\n * construction and a screen reader announces each walked row natively — no\n * bookkeeping that could drift out of sync with what is actually announced.\n */\nexport interface MenubarItem {\n /** Visible label. */\n label: string;\n /** Command id emitted on select. Omit for a non-actionable row. */\n command?: string;\n /** Right-aligned accelerator hint, e.g. \"Ctrl+S\". */\n shortcut?: string;\n /** Render a divider instead of an item (label ignored). */\n separator?: boolean;\n disabled?: boolean;\n}\n\nexport interface MenubarMenu {\n id: string;\n label: string;\n items: MenubarItem[];\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, nextTick } from \"vue\";\nimport { listStaggerDelay } from \"@ecoma-io/loom-core\";\n\nconst props = defineProps<{\n /** The top-level menus; their reading order follows `dir`. */\n menus: MenubarMenu[];\n /**\n * Reading direction. Right-to-left mirrors the strip's arrow keys —\n * ArrowRight travels toward the visual left — which a hand-rolled\n * handler must do itself, unlike its Reka siblings.\n */\n dir?: \"ltr\" | \"rtl\";\n}>();\nconst emit = defineEmits<{ select: [command: string] }>();\n\n// Horizontal arrows are visual, not logical: in right-to-left, ArrowRight\n// moves toward the previous menu.\nconst horizontal = computed(() => (props.dir === \"rtl\" ? -1 : 1) as 1 | -1);\n\nconst openId = ref<string | null>(null);\nconst activeIndex = ref(-1); // the focused row within the open menu\n// String refs would do here only outside a v-for; inside one Vue collects them\n// into arrays. Function refs keyed BY MENU ID instead: during a menu switch the\n// incoming panel can mount before the outgoing one unmounts, so a single\n// latest-wins slot would let the outgoing null clobber the just-bound element.\nconst panelEls = new Map<string, HTMLElement>();\nconst triggerEls = new Map<string, HTMLButtonElement>();\n\nfunction bindPanel(id: string, el: unknown): void {\n if (el instanceof HTMLElement) panelEls.set(id, el);\n else panelEls.delete(id);\n}\n\nfunction bindTrigger(id: string, el: unknown): void {\n if (el instanceof HTMLButtonElement) triggerEls.set(id, el);\n else triggerEls.delete(id);\n}\n\nfunction boundPanel(): HTMLElement | null {\n return openId.value ? (panelEls.get(openId.value) ?? null) : null;\n}\n\nconst openMenu = (menu: MenubarMenu | undefined) => menu?.items ?? [];\nconst firstEnabled = (items: MenubarItem[], from = 0, dir = 1): number => {\n for (let i = from; i >= 0 && i < items.length; i += dir) {\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n};\n\n/** One arrow-step away from `from`, wrapping through every slot exactly once. */\nfunction neighborEnabled(items: MenubarItem[], from: number, dir: 1 | -1): number {\n const n = items.length;\n for (let step = 1; step <= n; step++) {\n const i = (((from + dir * step) % n) + n) % n;\n const item = items[i];\n if (item && !item.separator && !item.disabled) return i;\n }\n return -1;\n}\n\n/** Rows render only for non-separator items, so an item index maps to its button's ordinal among the rendered ones. */\nfunction rowOrdinal(items: MenubarItem[], index: number): number {\n let ordinal = -1;\n for (let k = 0; k <= index && k < items.length; k++) {\n if (!items[k]?.separator) ordinal++;\n }\n return ordinal;\n}\n\nfunction rowButtons(): HTMLButtonElement[] {\n const panel = boundPanel();\n return panel\n ? Array.from(panel.querySelectorAll<HTMLButtonElement>('button[role=\"menuitem\"]'))\n : [];\n}\n\nfunction focusRow(items: MenubarItem[], index: number): void {\n rowButtons()[rowOrdinal(items, index)]?.focus();\n}\n\nfunction goTo(items: MenubarItem[], index: number): void {\n if (index < 0) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\nasync function openAndFocusFirst(id: string, items: MenubarItem[]): Promise<void> {\n await nextTick();\n // A hover or another key can supersede this open within the same tick; only\n // the menu that still owns `openId` may claim focus.\n if (openId.value !== id) return;\n goTo(items, firstEnabled(items));\n}\n\nasync function toggle(id: string): Promise<void> {\n if (openId.value === id) {\n close(true);\n return;\n }\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n await openAndFocusFirst(id, menu.items);\n}\n\nasync function hover(id: string, via: \"pointer\" | \"trigger-focus\"): Promise<void> {\n if (openId.value === null || openId.value === id) return;\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = id;\n activeIndex.value = -1;\n // Pointer switching takes focus into the new menu like a desktop menubar.\n // A switch caused by tabbing between triggers must not yank focus out of\n // the tab order mid-Tab, so it switches the panel and leaves focus alone.\n if (via === \"pointer\") await openAndFocusFirst(id, menu.items);\n}\n\nfunction choose(item: MenubarItem): void {\n if (item.separator || item.disabled) return;\n close(true);\n if (item.command) emit(\"select\", item.command);\n}\n\nfunction close(restoreFocus = false): void {\n const id = openId.value;\n openId.value = null;\n activeIndex.value = -1;\n // Escape and selection hand focus back to the owning trigger; Tab-out and\n // outside clicks do not — focus has already gone where the user sent it.\n if (restoreFocus && id) triggerEls.get(id)?.focus();\n}\n\nasync function moveMenu(dir: 1 | -1): Promise<void> {\n const ids = props.menus.map((m) => m.id);\n const cur = openId.value ? ids.indexOf(openId.value) : 0;\n const id = ids[(cur + dir + ids.length) % ids.length];\n const menu = props.menus.find((m) => m.id === id);\n if (!menu) return;\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n}\n\n/**\n * Pointer highlight of an enabled row moves focus with it — under the roving\n * model the highlighted row IS the focused row, and screen readers announce\n * focus changes, not CSS classes.\n */\nfunction pointAtRow(items: MenubarItem[], item: MenubarItem, index: number): void {\n if (item.disabled || item.separator || activeIndex.value === index) return;\n activeIndex.value = index;\n focusRow(items, index);\n}\n\n/** Native focus landing on a row keeps `activeIndex` truthful about it. */\nfunction noteFocus(item: MenubarItem, index: number): void {\n if (item.disabled || item.separator) return;\n activeIndex.value = index;\n}\n/*\n * The typeahead. APG menubars match a prefix of the item's label, starting\n * after the focused row and wrapping; a character's repeated key re-cycles\n * to its own next match rather than extending the prefix to a non-match, so\n * the pending buffer is kept but the repeated final character is not\n * appended. The buffer is 500 ms, case-insensitive, and the timer is released\n * on unmount so a menubar torn down mid-typeahead leaves nothing pending —\n * the same contract the TreeView sibling keeps.\n */\nconst TYPEAHEAD_RESET_MS = 500;\nlet typeaheadBuffer = \"\";\nlet typeaheadTimer: ReturnType<typeof setTimeout> | undefined;\n\nfunction typeaheadJump(items: MenubarItem[], from: number): void {\n const query = typeaheadBuffer;\n // The whole buffered prefix decides the match: repeated characters extend\n // it, and a fresh character restarts it, so \"Co\" reaches \"Copy\" only when\n // both keys follow each other within the timeout.\n const n = items.length;\n for (let step = 1; step <= n; step++) {\n const i = (from + step) % n;\n const item = items[i];\n if (item && !item.separator && !item.disabled && item.label.toLowerCase().startsWith(query)) {\n goTo(items, i);\n break;\n }\n }\n}\n\nfunction handleTypeahead(e: KeyboardEvent, items: MenubarItem[]): void {\n if (e.key.length !== 1 || e.altKey || e.ctrlKey || e.metaKey) return;\n if (e.key === \" \" || e.key === \"Enter\") return; // activation keys, handled above\n const char = e.key.toLowerCase();\n if (typeaheadBuffer.slice(-1) !== char) typeaheadBuffer += char;\n e.preventDefault();\n typeaheadJump(items, activeIndex.value);\n if (typeaheadTimer !== undefined) clearTimeout(typeaheadTimer);\n typeaheadTimer = setTimeout(() => {\n typeaheadBuffer = \"\";\n typeaheadTimer = undefined;\n }, TYPEAHEAD_RESET_MS);\n}\n\nonBeforeUnmount(() => {\n if (typeaheadTimer !== undefined) clearTimeout(typeaheadTimer);\n});\n\n/**\n * The APG's closed-state traversal: Left/Right across the strip land on the\n * neighbouring trigger without opening anything — opening is ArrowDown,\n * Enter or Space's job.\n */\nfunction moveTriggerFocus(from: string, dir: 1 | -1): void {\n const ids = props.menus.map((m) => m.id);\n const cur = ids.indexOf(from);\n if (cur === -1) return;\n triggerEls.get(ids[(cur + dir + ids.length) % ids.length] ?? \"\")?.focus();\n}\n\nasync function onTriggerKeydown(e: KeyboardEvent, menu: MenubarMenu): Promise<void> {\n switch (e.key) {\n case \"ArrowDown\":\n case \"Enter\":\n case \" \":\n e.preventDefault(); // suppresses the button's own activation click\n openId.value = menu.id;\n activeIndex.value = -1;\n await openAndFocusFirst(menu.id, menu.items);\n break;\n case \"ArrowRight\":\n e.preventDefault();\n if (openId.value) void moveMenu(horizontal.value);\n else moveTriggerFocus(menu.id, horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n if (openId.value) void moveMenu(-horizontal.value as 1 | -1);\n else moveTriggerFocus(menu.id, -horizontal.value as 1 | -1);\n break;\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n }\n}\n\nfunction onMenuKeydown(e: KeyboardEvent, menu: MenubarMenu): void {\n const items = menu.items;\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, 1));\n break;\n case \"ArrowUp\":\n e.preventDefault();\n goTo(items, neighborEnabled(items, activeIndex.value, -1));\n break;\n case \"Home\":\n e.preventDefault();\n goTo(items, firstEnabled(items, 0, 1));\n break;\n case \"End\":\n e.preventDefault();\n goTo(items, firstEnabled(items, items.length - 1, -1));\n break;\n case \"ArrowRight\":\n e.preventDefault();\n void moveMenu(horizontal.value);\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n void moveMenu(-horizontal.value as 1 | -1);\n break;\n // Handled here rather than left to the button's native activation:\n // preventDefault stops the browser's synthesized click, which would run\n // choose() a second time alongside this delegated handler.\n case \"Enter\":\n case \" \": {\n e.preventDefault();\n const item = items[activeIndex.value];\n if (item) choose(item);\n break;\n }\n case \"Escape\":\n e.preventDefault();\n close(true);\n break;\n // Every other key is a printable character: APG typeahead moves focus to\n // the next item whose label begins with it. `default` sees only these —\n // Space/Enter/Escape each have their own case above.\n default:\n handleTypeahead(e, items);\n break;\n }\n}\n\nfunction onRootFocusout(e: FocusEvent): void {\n if (!openId.value) return;\n const root = e.currentTarget;\n const next = e.relatedTarget;\n // Tab out of the strip dismisses the menu and leaves focus where the browser\n // put it. Moves that stay inside the menubar (opening handing focus to a\n // row) keep it.\n if (!(root instanceof Element) || !(next instanceof Node) || !root.contains(next)) close();\n}\n\n// A wrapper, not `close` itself: the listener's event argument would otherwise\n// arrive as `restoreFocus`, and an outside click would yank focus back to the\n// trigger instead of leaving it where the press landed.\nconst onWindowClick = (): void => close();\nonMounted(() => window.addEventListener(\"click\", onWindowClick));\nonBeforeUnmount(() => window.removeEventListener(\"click\", onWindowClick));\n</script>\n\n<template>\n <!-- The attribute rides the strip itself, not just the prop: a host may\n pass dir without setting document direction, and both the flex order\n and the mirrored keys must agree with what is on screen. -->\n <div\n :dir=\"dir\"\n role=\"menubar\"\n class=\"flex items-center gap-0.5\"\n style=\"-webkit-app-region: no-drag\"\n @click.stop\n @focusout=\"onRootFocusout\"\n >\n <div v-for=\"menu in menus\" :key=\"menu.id\" class=\"relative\">\n <button\n :id=\"`menubar-trigger-${menu.id}`\"\n :ref=\"(el) => bindTrigger(menu.id, el)\"\n type=\"button\"\n role=\"menuitem\"\n :aria-haspopup=\"'menu'\"\n :aria-expanded=\"openId === menu.id\"\n class=\"min-h-6 rounded-sm px-2 py-1 text-xs text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:bg-subtle focus-visible:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo\"\n :class=\"openId === menu.id && 'bg-subtle text-foreground'\"\n @click.stop=\"toggle(menu.id)\"\n @mouseenter=\"hover(menu.id, 'pointer')\"\n @focus=\"hover(menu.id, 'trigger-focus')\"\n @keydown=\"onTriggerKeydown($event, menu)\"\n >\n {{ menu.label }}\n </button>\n\n <!-- Scoped presence, matching every sibling overlay: enter animates in,\n leave answers with scale-out. The unconditional mount-only animation\n this used to carry never plays on close at all. In jsdom, with no\n stylesheet loaded, Vue computes zero animation duration and ends both\n phases immediately, so tests see plain unmount. -->\n <Transition enter-active-class=\"animate-scale-in\" leave-active-class=\"animate-scale-out\">\n <div\n v-if=\"openId === menu.id\"\n :ref=\"(el) => bindPanel(menu.id, el)\"\n role=\"menu\"\n :aria-label=\"menu.label\"\n tabindex=\"-1\"\n class=\"absolute left-0 top-full z-overlay mt-1 min-w-[13rem] origin-top-left rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md\"\n @click.stop\n @keydown=\"onMenuKeydown($event, menu)\"\n >\n <template v-for=\"(item, i) in openMenu(menu)\" :key=\"i\">\n <div v-if=\"item.separator\" role=\"separator\" class=\"my-1 h-px bg-border\" />\n <!-- The row's colour, not its alpha, says it is unavailable. A menu\n row is nothing but its label, and `disabled:opacity-40` composited\n that label to 2.40:1 on the popover — a disabled row a reader\n cannot make out tells them nothing about what is unavailable,\n which is the only thing it is there to say. Muted instead: 5.76:1\n at rest, and a clear drop from the 15.46:1 an available row wears,\n so the state still reads at a glance. The shortcut beside it\n declares `text-muted-foreground` on its own element, so it lands\n on the same colour rather than inheriting this one.\n\n Rows sit at text-sm/py-1.5 like DropdownMenu's — one row grammar\n across menus; the compact trigger above stays desktop chrome.\n Every row carries tabindex -1: focus arrives programmatically,\n never by tabbing through the strip. -->\n <button\n v-else\n type=\"button\"\n role=\"menuitem\"\n tabindex=\"-1\"\n :disabled=\"item.disabled\"\n :data-highlighted=\"activeIndex === i || undefined\"\n :style=\"{ animationDelay: listStaggerDelay(i) }\"\n class=\"flex w-full items-center justify-between gap-6 rounded-sm px-2 py-1.5 text-left text-sm text-foreground transition-colors duration-fast ease-out hover:bg-subtle disabled:pointer-events-none disabled:text-muted-foreground data-[highlighted]:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo animate-fade-rise\"\n @click=\"choose(item)\"\n @mouseenter=\"pointAtRow(openMenu(menu), item, i)\"\n @focus=\"noteFocus(item, i)\"\n >\n <span>{{ item.label }}</span>\n <span v-if=\"item.shortcut\" class=\"tabular text-micro text-muted-foreground\">\n {{ item.shortcut }}\n </span>\n </button>\n </template>\n </div>\n </Transition>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Meter_vue_vue_type_script_setup_true_lang_default, { METER_LABELS, meterVariants } from "./Meter.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
//#region packages/primitives/meter/src/Meter.vue
|
|
3
|
+
var Meter_default = Meter_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { METER_LABELS, Meter_default as default, meterVariants };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=Meter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Meter.js","names":[],"sources":["../../../../../packages/primitives/meter/src/Meter.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { MeterLabels } from \"@ecoma-io/loom-labels\";\nimport { cva } from \"class-variance-authority\";\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 METER_LABELS: MeterLabels = {\n name: \"Meter\",\n valueText: ({ value, max }) => `${value} of ${max}`,\n optimal: \"Optimal\",\n cautionary: \"Caution\",\n critical: \"Critical\",\n};\n\n/**\n * The track's thickness, the same scale as Progress — a meter is a gauge, not a\n * form control, so it stays a stroke rather than aligning with input height.\n */\nexport type MeterSize = \"sm\" | \"md\" | \"lg\";\n\nexport const meterVariants = cva(\"relative w-full overflow-hidden rounded-full bg-muted\", {\n variants: {\n size: {\n sm: \"h-1\",\n md: \"h-2\",\n lg: \"h-3\",\n } satisfies Record<MeterSize, string>,\n },\n defaultVariants: { size: \"md\" },\n});\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, useId } from \"vue\";\nimport { Check, CircleAlert, X } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * Meter — a scalar capacity gauge on the WAI-ARIA meter role, visually a sibling\n * of Progress (same stroke scale, track/fill anatomy). Renders a measured\n * quantity against a known capacity — disk usage, budget consumption, seat\n * allocation — things that are not tasks and should never say \"loading.\"\n *\n * Built directly on `<div role=\"meter\">` rather than a headless component: the\n * ARIA meter role carries the same contract as the HTML meter element (min, max,\n * value, low, high, optimum) and a headless abstraction would add nothing but\n * an extra dependency — Progress uses Reka UI because progressbar's indeterminate\n * animation requires real DOM transitions Meter does not need.\n *\n * The accessible name is mandatory: a meter without a name is a number with no\n * referent. Four sources, resolved in order:\n * 1. `ariaLabelledby` (external element)\n * 2. `ariaLabel` (explicit string, no visible text)\n * 3. `label` (visible text, same string as the name)\n * 4. `text.name` (the labels-seam fallback — guaranteed non-empty)\n *\n * A `value` above `max` paints the bar full but `aria-valuenow` reports the\n * clamped value, because a spoken number outside the declared range is a lie\n * — the Progress lesson, carried forward.\n */\nconst props = withDefaults(\n defineProps<{\n /** The measured amount, clamped into [min, max] for both the fill and the announced value. */\n value: number;\n /** The value at which the gauge reads 0% full. */\n min?: number;\n /** The value at which the gauge reads 100% full. */\n max?: number;\n /** Low threshold — the boundary between the low and medium regions (HTML meter semantics). */\n low?: number;\n /** High threshold — the boundary between the medium and high regions (HTML meter semantics). */\n high?: number;\n /** The optimal value; decides which region is the good one. Defaults to the midpoint. */\n optimum?: number;\n /** Visible text that also supplies the accessible name when no ariaLabel/ariaLabelledby is set. */\n label?: string;\n /** Accessible name for the gauge, in place of a visible label. */\n ariaLabel?: string;\n /** Accessible name sourced from another element's id, in place of `ariaLabel`/`label`. */\n ariaLabelledby?: string;\n /** Track thickness. Its own scale, not the control-height scale — a gauge is a stroke. */\n size?: MeterSize;\n /** Opt-in band recolouring (success/warning/destructive) with a redundant non-colour cue per band. */\n threshold?: boolean;\n /** Print the localised value text beside the track. Hidden from AT — the role already announces it. */\n showValue?: boolean;\n /**\n * How the gauge speaks, as any subset of `MeterLabels` — the rest stay as\n * the host's `provideLoomLabels` vocabulary left them, and then as Loom's\n * English.\n */\n labels?: LabelOverrides<MeterLabels>;\n }>(),\n {\n min: 0,\n max: 100,\n size: \"md\",\n threshold: false,\n showValue: false,\n },\n);\n\nconst text = useLabels(\"meter\", METER_LABELS, () => props.labels);\n\n/** Track label text sizing, matching Progress's readout scale exactly. */\nconst VALUE_TEXT = {\n sm: \"text-micro\",\n md: \"text-xs\",\n lg: \"text-sm\",\n} satisfies Record<MeterSize, string>;\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest } = useSplitAttrs();\nconst labelElId = useId();\n\n// ---------------------------------------------------------------------------\n// Value clamping — the Progress lesson: a spoken value outside [min, max] is a lie.\n// ---------------------------------------------------------------------------\n\n/** Clamped value, for the fill AND the aria attributes — never out of range. */\nconst clamped = computed<number>(() => {\n const v = props.value;\n if (Number.isNaN(v)) return props.min;\n return Math.min(Math.max(v, props.min), props.max);\n});\n\n/** Percentage 0–100, from the clamped value. */\nconst pct = computed<number>(() => {\n const range = props.max - props.min;\n if (range <= 0) return 100;\n return ((clamped.value - props.min) / range) * 100;\n});\n\n// ---------------------------------------------------------------------------\n// Band logic — HTML meter element's three-quality model, exactly.\n//\n// Three regions: low [min, low), medium [low, high], high (high, max].\n// The optimum point selects which region is preferred, and how the others rank.\n//\n// Per HTML spec §4.10.16:\n// optimum in low → low=preferred, medium=suboptimal, high=even-less-good\n// optimum in medium → medium=preferred, low=suboptimal, high=suboptimal\n// optimum in high → high=preferred, medium=suboptimal, low=even-less-good\n//\n// When a region is \"even less good\" it maps to critical; \"suboptimal\" maps\n// to cautionary. No critical band exists when optimum is in the medium region\n// (both extremes are equally suboptimal).\n// ---------------------------------------------------------------------------\n\ntype Band = \"optimal\" | \"cautionary\" | \"critical\";\n\nconst band = computed<Band | undefined>(() => {\n if (!props.threshold) return undefined;\n\n let lo = props.low ?? props.min;\n let hi = props.high ?? props.max;\n\n // Clamp both boundaries into [min, max], then reject an inverted pair the\n // way the HTML meter element does: treat both as unset rather than build a\n // region model out of crossed boundaries.\n lo = Math.min(Math.max(lo, props.min), props.max);\n hi = Math.min(Math.max(hi, props.min), props.max);\n if (lo > hi) {\n lo = props.min;\n hi = props.max;\n }\n\n const o = Math.min(Math.max(props.optimum ?? (props.min + props.max) / 2, props.min), props.max);\n const v = clamped.value;\n\n // Determine which region contains the optimum.\n // Boundaries: low < value < high belongs to medium; low ≤ optimum ≤ high is medium.\n // Region 0: [min, lo) Region 1: [lo, hi] Region 2: (hi, max]\n const optRegion = o < lo ? 0 : o <= hi ? 1 : 2;\n\n // Map regions to qualities per the table.\n const regionMap: { optimal: number; cautionary: number[]; critical: number | null } =\n optRegion === 0\n ? { optimal: 0, cautionary: [1], critical: 2 }\n : optRegion === 1\n ? { optimal: 1, cautionary: [0, 2], critical: null }\n : { optimal: 2, cautionary: [1], critical: 0 };\n\n // Determine which region the current value falls into.\n const valueRegion = v < lo ? 0 : v <= hi ? 1 : 2;\n\n if (valueRegion === regionMap.optimal) return \"optimal\";\n if (regionMap.cautionary.includes(valueRegion)) return \"cautionary\";\n if (regionMap.critical === valueRegion) return \"critical\";\n return \"optimal\"; // fallback — should not be reached\n});\n\n/** The fill colour when threshold is on; stays primary otherwise. */\nconst fillClass = computed(() => {\n if (!props.threshold) return \"bg-primary\";\n const b = band.value;\n if (b === \"optimal\") return \"bg-success\";\n if (b === \"cautionary\") return \"bg-warning\";\n return \"bg-destructive\";\n});\n\n/** The band cue word from labels. */\nconst cueWord = computed(() => {\n if (!band.value) return \"\";\n return text.value[band.value];\n});\n\n/** The band cue icon component. */\nconst cueIcon = computed(() => {\n if (band.value === \"optimal\") return Check;\n if (band.value === \"cautionary\") return CircleAlert;\n return X;\n});\n\n/** The cue colour class — the `-text` token of the band, never the fill token:\n * `--color-success` in dark is a fill lightness and fails text contrast. */\nconst cueColorClass = computed(() => {\n if (band.value === \"optimal\") return \"text-success-text\";\n if (band.value === \"cautionary\") return \"text-warning-text\";\n return \"text-destructive-text\";\n});\n\n/** The fill indicator style — translateX slides the fill from right to left. */\nconst indicatorStyle = computed(() => ({\n transition:\n \"transform var(--duration-slow) var(--ease-out), background-color var(--duration-slow) var(--ease-out)\",\n transform: `translateX(${pct.value - 100}%)`,\n}));\n\n/** aria-valuetext — localisable; never absent because the role is meaningless without it. */\nconst valuetext = computed(() =>\n text.value.valueText({ value: clamped.value, min: props.min, max: props.max }),\n);\n\n// ---------------------------------------------------------------------------\n// Accessible name resolution — four sources, in precedence order.\n// ---------------------------------------------------------------------------\n\n/**\n * `label` is a content prop and always renders; `ariaLabel`/`ariaLabelledby`\n * choose only where the NAME comes from. A caller passing both wants visible\n * text under one name and a different announced name — silencing the visible\n * text would make the pair unrepresentable. And because assistive tech reads\n * `aria-labelledby` ahead of `aria-label`, whichever source loses must not\n * emit its attribute at all: an `ariaLabel` beside a label-pointing\n * `aria-labelledby` would be read past, not preferred.\n */\nconst ariaLabelAttr = computed(() => {\n if (props.ariaLabelledby) return undefined;\n if (props.ariaLabel) return props.ariaLabel;\n if (props.label) return undefined; // named through the span's id instead\n return text.value.name; // the seam fallback — a name is never absent\n});\n\nconst ariaLabelledbyAttr = computed(() => {\n if (props.ariaLabelledby) return props.ariaLabelledby;\n if (props.ariaLabel) return undefined; // ariaLabel wins the name\n if (props.label) return labelElId;\n return undefined;\n});\n\n/** Whether the visible `label` text renders. */\nconst hasVisibleLabel = computed(() => !!props.label);\n\n/** Whether the gauge row needs extra content beside the track. */\nconst hasRow = computed(() => props.showValue || (props.threshold && band.value));\n</script>\n\n<template>\n <!-- Visible label — only when `label` is set and no ariaLabel/ariaLabelledby overrides it.\n Rendered as a <span> rather than <label> because a div[role=\"meter\"] is not a\n labelable element; the association is through aria-labelledby pointing at the span's id. -->\n <span\n v-if=\"hasVisibleLabel\"\n :id=\"labelElId\"\n :class=\"cn('block text-sm font-medium text-foreground mb-1', VALUE_TEXT[size])\"\n >\n {{ label }}\n </span>\n\n <!-- Branch 1: bare track — nothing beside it, no wrapper. Mirrors Progress's\n default branch: the component's box is the track itself, and it keeps\n the exact sizing it had before `showValue`/`threshold` existed. -->\n <div\n v-if=\"!hasRow\"\n role=\"meter\"\n :aria-valuenow=\"clamped\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n :aria-valuetext=\"valuetext\"\n :aria-label=\"ariaLabelAttr\"\n :aria-labelledby=\"ariaLabelledbyAttr\"\n v-bind=\"rest\"\n :class=\"cn(meterVariants({ size }), attrs.class as string)\"\n >\n <div :class=\"cn('h-full rounded-full', fillClass)\" :style=\"indicatorStyle\" />\n </div>\n\n <!-- Branch 2: gauge row with cue/readout — a flex row so the track and the\n side content share a single box. `class` from the caller lands on this\n row (the outermost box the caller sees), and everything else lands on the\n meter element through `rest`. -->\n <div v-else :class=\"cn('flex w-full items-center gap-3', attrs.class as string)\">\n <div\n role=\"meter\"\n :aria-valuenow=\"clamped\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n :aria-valuetext=\"valuetext\"\n :aria-label=\"ariaLabelAttr\"\n :aria-labelledby=\"ariaLabelledbyAttr\"\n v-bind=\"rest\"\n :class=\"cn(meterVariants({ size }), 'w-auto min-w-0 flex-1')\"\n >\n <div :class=\"cn('h-full rounded-full', fillClass)\" :style=\"indicatorStyle\" />\n </div>\n\n <!-- Value readout + band cue, aria-hidden (the meter role already announces the\n value via aria-valuetext, and the band is derivable from it). Fixed width so\n the row does not resize as the digits change. -->\n <div aria-hidden=\"true\" :class=\"cn('flex items-center gap-2 shrink-0')\">\n <span\n v-if=\"showValue\"\n :class=\"cn('tabular w-10 text-right text-muted-foreground', VALUE_TEXT[size])\"\n >\n {{ valuetext }}\n </span>\n <span\n v-if=\"threshold && band\"\n :class=\"cn('flex items-center gap-1 whitespace-nowrap', cueColorClass, VALUE_TEXT[size])\"\n >\n <component :is=\"cueIcon\" class=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n <span>{{ cueWord }}</span>\n </span>\n </div>\n </div>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type LabelOverrides } from "../../../labels/src/index";
|
|
2
|
+
import type { MeterLabels } from "../../../labels/src/index";
|
|
3
|
+
/**
|
|
4
|
+
* Loom's English, co-located with the component so it tree-shakes with it, and
|
|
5
|
+
* exported so a host can build a partial vocabulary against the real thing.
|
|
6
|
+
*/
|
|
7
|
+
export declare const METER_LABELS: MeterLabels;
|
|
8
|
+
/**
|
|
9
|
+
* The track's thickness, the same scale as Progress — a meter is a gauge, not a
|
|
10
|
+
* form control, so it stays a stroke rather than aligning with input height.
|
|
11
|
+
*/
|
|
12
|
+
export type MeterSize = "sm" | "md" | "lg";
|
|
13
|
+
export declare const meterVariants: (props?: ({
|
|
14
|
+
size?: "sm" | "md" | "lg" | null | undefined;
|
|
15
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
18
|
+
declare const __VLS_export: import("vue").DefineComponent<{
|
|
19
|
+
/** The measured amount, clamped into [min, max] for both the fill and the announced value. */
|
|
20
|
+
value: number;
|
|
21
|
+
/** The value at which the gauge reads 0% full. */
|
|
22
|
+
min?: number;
|
|
23
|
+
/** The value at which the gauge reads 100% full. */
|
|
24
|
+
max?: number;
|
|
25
|
+
/** Low threshold — the boundary between the low and medium regions (HTML meter semantics). */
|
|
26
|
+
low?: number;
|
|
27
|
+
/** High threshold — the boundary between the medium and high regions (HTML meter semantics). */
|
|
28
|
+
high?: number;
|
|
29
|
+
/** The optimal value; decides which region is the good one. Defaults to the midpoint. */
|
|
30
|
+
optimum?: number;
|
|
31
|
+
/** Visible text that also supplies the accessible name when no ariaLabel/ariaLabelledby is set. */
|
|
32
|
+
label?: string;
|
|
33
|
+
/** Accessible name for the gauge, in place of a visible label. */
|
|
34
|
+
ariaLabel?: string;
|
|
35
|
+
/** Accessible name sourced from another element's id, in place of `ariaLabel`/`label`. */
|
|
36
|
+
ariaLabelledby?: string;
|
|
37
|
+
/** Track thickness. Its own scale, not the control-height scale — a gauge is a stroke. */
|
|
38
|
+
size?: MeterSize;
|
|
39
|
+
/** Opt-in band recolouring (success/warning/destructive) with a redundant non-colour cue per band. */
|
|
40
|
+
threshold?: boolean;
|
|
41
|
+
/** Print the localised value text beside the track. Hidden from AT — the role already announces it. */
|
|
42
|
+
showValue?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* How the gauge speaks, as any subset of `MeterLabels` — the rest stay as
|
|
45
|
+
* the host's `provideLoomLabels` vocabulary left them, and then as Loom's
|
|
46
|
+
* English.
|
|
47
|
+
*/
|
|
48
|
+
labels?: LabelOverrides<MeterLabels>;
|
|
49
|
+
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
|
|
50
|
+
/** The measured amount, clamped into [min, max] for both the fill and the announced value. */
|
|
51
|
+
value: number;
|
|
52
|
+
/** The value at which the gauge reads 0% full. */
|
|
53
|
+
min?: number;
|
|
54
|
+
/** The value at which the gauge reads 100% full. */
|
|
55
|
+
max?: number;
|
|
56
|
+
/** Low threshold — the boundary between the low and medium regions (HTML meter semantics). */
|
|
57
|
+
low?: number;
|
|
58
|
+
/** High threshold — the boundary between the medium and high regions (HTML meter semantics). */
|
|
59
|
+
high?: number;
|
|
60
|
+
/** The optimal value; decides which region is the good one. Defaults to the midpoint. */
|
|
61
|
+
optimum?: number;
|
|
62
|
+
/** Visible text that also supplies the accessible name when no ariaLabel/ariaLabelledby is set. */
|
|
63
|
+
label?: string;
|
|
64
|
+
/** Accessible name for the gauge, in place of a visible label. */
|
|
65
|
+
ariaLabel?: string;
|
|
66
|
+
/** Accessible name sourced from another element's id, in place of `ariaLabel`/`label`. */
|
|
67
|
+
ariaLabelledby?: string;
|
|
68
|
+
/** Track thickness. Its own scale, not the control-height scale — a gauge is a stroke. */
|
|
69
|
+
size?: MeterSize;
|
|
70
|
+
/** Opt-in band recolouring (success/warning/destructive) with a redundant non-colour cue per band. */
|
|
71
|
+
threshold?: boolean;
|
|
72
|
+
/** Print the localised value text beside the track. Hidden from AT — the role already announces it. */
|
|
73
|
+
showValue?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* How the gauge speaks, as any subset of `MeterLabels` — the rest stay as
|
|
76
|
+
* the host's `provideLoomLabels` vocabulary left them, and then as Loom's
|
|
77
|
+
* English.
|
|
78
|
+
*/
|
|
79
|
+
labels?: LabelOverrides<MeterLabels>;
|
|
80
|
+
}> & Readonly<{}>, {
|
|
81
|
+
size: MeterSize;
|
|
82
|
+
max: number;
|
|
83
|
+
showValue: boolean;
|
|
84
|
+
min: number;
|
|
85
|
+
threshold: boolean;
|
|
86
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
87
|
+
//# sourceMappingURL=Meter.vue.d.ts.map
|