diff --git a/vue/primitives/src/context-menu/ContextMenuArrow.vue b/vue/primitives/src/context-menu/ContextMenuArrow.vue
new file mode 100644
index 0000000..bbc7866
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuArrow.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue b/vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue
new file mode 100644
index 0000000..c3e6eba
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuContent.vue b/vue/primitives/src/context-menu/ContextMenuContent.vue
new file mode 100644
index 0000000..c996faa
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuContent.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuGroup.vue b/vue/primitives/src/context-menu/ContextMenuGroup.vue
new file mode 100644
index 0000000..00b6c90
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuGroup.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuItem.vue b/vue/primitives/src/context-menu/ContextMenuItem.vue
new file mode 100644
index 0000000..cddf23b
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuItem.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuItemIndicator.vue b/vue/primitives/src/context-menu/ContextMenuItemIndicator.vue
new file mode 100644
index 0000000..422c63c
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuItemIndicator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuLabel.vue b/vue/primitives/src/context-menu/ContextMenuLabel.vue
new file mode 100644
index 0000000..6bf59b7
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuLabel.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuPortal.vue b/vue/primitives/src/context-menu/ContextMenuPortal.vue
new file mode 100644
index 0000000..eed0027
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuPortal.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuRadioGroup.vue b/vue/primitives/src/context-menu/ContextMenuRadioGroup.vue
new file mode 100644
index 0000000..0761c76
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuRadioGroup.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuRadioItem.vue b/vue/primitives/src/context-menu/ContextMenuRadioItem.vue
new file mode 100644
index 0000000..3206fec
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuRadioItem.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuRoot.vue b/vue/primitives/src/context-menu/ContextMenuRoot.vue
new file mode 100644
index 0000000..8448d1e
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuRoot.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuSeparator.vue b/vue/primitives/src/context-menu/ContextMenuSeparator.vue
new file mode 100644
index 0000000..f5e2f6c
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuSeparator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuSub.vue b/vue/primitives/src/context-menu/ContextMenuSub.vue
new file mode 100644
index 0000000..f7c9203
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuSub.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuSubContent.vue b/vue/primitives/src/context-menu/ContextMenuSubContent.vue
new file mode 100644
index 0000000..210cb63
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuSubContent.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuSubTrigger.vue b/vue/primitives/src/context-menu/ContextMenuSubTrigger.vue
new file mode 100644
index 0000000..58b8bec
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuSubTrigger.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/ContextMenuTrigger.vue b/vue/primitives/src/context-menu/ContextMenuTrigger.vue
new file mode 100644
index 0000000..653d3c0
--- /dev/null
+++ b/vue/primitives/src/context-menu/ContextMenuTrigger.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/context-menu/context.ts b/vue/primitives/src/context-menu/context.ts
new file mode 100644
index 0000000..af2f918
--- /dev/null
+++ b/vue/primitives/src/context-menu/context.ts
@@ -0,0 +1,14 @@
+import type { Ref } from 'vue';
+
+import { useContextFactory } from '@robonen/vue';
+
+export interface ContextMenuRootContext {
+ open: Ref;
+ onOpenChange: (open: boolean) => void;
+ modal: Ref;
+}
+
+export const {
+ inject: useContextMenuRootContext,
+ provide: provideContextMenuRootContext,
+} = useContextFactory('ContextMenuRootContext');
diff --git a/vue/primitives/src/context-menu/index.ts b/vue/primitives/src/context-menu/index.ts
new file mode 100644
index 0000000..b0153f3
--- /dev/null
+++ b/vue/primitives/src/context-menu/index.ts
@@ -0,0 +1,17 @@
+export { useContextMenuRootContext } from './context';
+export { default as ContextMenuArrow, type ContextMenuArrowProps } from './ContextMenuArrow.vue';
+export { default as ContextMenuCheckboxItem, type ContextMenuCheckboxItemEmits, type ContextMenuCheckboxItemProps } from './ContextMenuCheckboxItem.vue';
+export { default as ContextMenuContent, type ContextMenuContentEmits, type ContextMenuContentProps } from './ContextMenuContent.vue';
+export { default as ContextMenuGroup, type ContextMenuGroupProps } from './ContextMenuGroup.vue';
+export { default as ContextMenuItem, type ContextMenuItemEmits, type ContextMenuItemProps } from './ContextMenuItem.vue';
+export { default as ContextMenuItemIndicator, type ContextMenuItemIndicatorProps } from './ContextMenuItemIndicator.vue';
+export { default as ContextMenuLabel, type ContextMenuLabelProps } from './ContextMenuLabel.vue';
+export { default as ContextMenuPortal, type ContextMenuPortalProps } from './ContextMenuPortal.vue';
+export { default as ContextMenuRadioGroup, type ContextMenuRadioGroupEmits, type ContextMenuRadioGroupProps } from './ContextMenuRadioGroup.vue';
+export { default as ContextMenuRadioItem, type ContextMenuRadioItemEmits, type ContextMenuRadioItemProps } from './ContextMenuRadioItem.vue';
+export { default as ContextMenuRoot, type ContextMenuRootEmits, type ContextMenuRootProps } from './ContextMenuRoot.vue';
+export { default as ContextMenuSeparator, type ContextMenuSeparatorProps } from './ContextMenuSeparator.vue';
+export { default as ContextMenuSub, type ContextMenuSubEmits, type ContextMenuSubProps } from './ContextMenuSub.vue';
+export { default as ContextMenuSubContent, type ContextMenuSubContentEmits, type ContextMenuSubContentProps } from './ContextMenuSubContent.vue';
+export { default as ContextMenuSubTrigger, type ContextMenuSubTriggerProps } from './ContextMenuSubTrigger.vue';
+export { default as ContextMenuTrigger, type ContextMenuTriggerProps } from './ContextMenuTrigger.vue';
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue b/vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue
new file mode 100644
index 0000000..98b2a80
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue b/vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue
new file mode 100644
index 0000000..d2ecbf9
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuContent.vue b/vue/primitives/src/dropdown-menu/DropdownMenuContent.vue
new file mode 100644
index 0000000..1d5f622
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuContent.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue b/vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue
new file mode 100644
index 0000000..355961e
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuItem.vue b/vue/primitives/src/dropdown-menu/DropdownMenuItem.vue
new file mode 100644
index 0000000..db12231
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuItem.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue b/vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue
new file mode 100644
index 0000000..bdab714
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue b/vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue
new file mode 100644
index 0000000..82a917a
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue b/vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue
new file mode 100644
index 0000000..7506f63
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue b/vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue
new file mode 100644
index 0000000..63a431e
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue b/vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue
new file mode 100644
index 0000000..1c98568
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue b/vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue
new file mode 100644
index 0000000..e5d7686
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue
new file mode 100644
index 0000000..4930a23
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSub.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSub.vue
new file mode 100644
index 0000000..62648f6
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuSub.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue
new file mode 100644
index 0000000..891d270
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue
new file mode 100644
index 0000000..ae0e888
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue b/vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue
new file mode 100644
index 0000000..800dc45
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/dropdown-menu/context.ts b/vue/primitives/src/dropdown-menu/context.ts
new file mode 100644
index 0000000..b8304ac
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/context.ts
@@ -0,0 +1,15 @@
+import type { ComputedRef, ShallowRef } from 'vue';
+
+import { useContextFactory } from '@robonen/vue';
+
+export interface DropdownMenuRootContext {
+ triggerId: ComputedRef;
+ triggerRef: ShallowRef;
+ contentId: ComputedRef;
+ onTriggerChange: (el: HTMLElement | null) => void;
+}
+
+export const {
+ inject: useDropdownMenuRootContext,
+ provide: provideDropdownMenuRootContext,
+} = useContextFactory('DropdownMenuRootContext');
diff --git a/vue/primitives/src/dropdown-menu/index.ts b/vue/primitives/src/dropdown-menu/index.ts
new file mode 100644
index 0000000..2b9989a
--- /dev/null
+++ b/vue/primitives/src/dropdown-menu/index.ts
@@ -0,0 +1,17 @@
+export { useDropdownMenuRootContext } from './context';
+export { default as DropdownMenuArrow, type DropdownMenuArrowProps } from './DropdownMenuArrow.vue';
+export { default as DropdownMenuCheckboxItem, type DropdownMenuCheckboxItemEmits, type DropdownMenuCheckboxItemProps } from './DropdownMenuCheckboxItem.vue';
+export { default as DropdownMenuContent, type DropdownMenuContentEmits, type DropdownMenuContentProps } from './DropdownMenuContent.vue';
+export { default as DropdownMenuGroup, type DropdownMenuGroupProps } from './DropdownMenuGroup.vue';
+export { default as DropdownMenuItem, type DropdownMenuItemEmits, type DropdownMenuItemProps } from './DropdownMenuItem.vue';
+export { default as DropdownMenuItemIndicator, type DropdownMenuItemIndicatorProps } from './DropdownMenuItemIndicator.vue';
+export { default as DropdownMenuLabel, type DropdownMenuLabelProps } from './DropdownMenuLabel.vue';
+export { default as DropdownMenuPortal, type DropdownMenuPortalProps } from './DropdownMenuPortal.vue';
+export { default as DropdownMenuRadioGroup, type DropdownMenuRadioGroupEmits, type DropdownMenuRadioGroupProps } from './DropdownMenuRadioGroup.vue';
+export { default as DropdownMenuRadioItem, type DropdownMenuRadioItemEmits, type DropdownMenuRadioItemProps } from './DropdownMenuRadioItem.vue';
+export { default as DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps } from './DropdownMenuRoot.vue';
+export { default as DropdownMenuSeparator, type DropdownMenuSeparatorProps } from './DropdownMenuSeparator.vue';
+export { default as DropdownMenuSub, type DropdownMenuSubEmits, type DropdownMenuSubProps } from './DropdownMenuSub.vue';
+export { default as DropdownMenuSubContent, type DropdownMenuSubContentEmits, type DropdownMenuSubContentProps } from './DropdownMenuSubContent.vue';
+export { default as DropdownMenuSubTrigger, type DropdownMenuSubTriggerProps } from './DropdownMenuSubTrigger.vue';
+export { default as DropdownMenuTrigger, type DropdownMenuTriggerProps } from './DropdownMenuTrigger.vue';
diff --git a/vue/primitives/src/index.ts b/vue/primitives/src/index.ts
index ded042a..d3767ee 100644
--- a/vue/primitives/src/index.ts
+++ b/vue/primitives/src/index.ts
@@ -1,5 +1,44 @@
export * from './config-provider';
export * from './primitive';
export * from './presence';
+export * from './collection';
+export * from './roving-focus';
export * from './pagination';
export * from './focus-scope';
+export * from './visually-hidden';
+export * from './teleport';
+export * from './dismissable-layer';
+export * from './dialog';
+export * from './alert-dialog';
+export * from './scroll-area';
+export * from './separator';
+export * from './label';
+export * from './aspect-ratio';
+export * from './toggle';
+export * from './switch';
+export * from './progress';
+export * from './collapsible';
+export * from './avatar';
+export * from './slider';
+export * from './checkbox';
+export * from './toolbar';
+export * from './radio-group';
+export * from './toggle-group';
+export * from './number-field';
+export * from './pin-input';
+export * from './tabs';
+export * from './accordion';
+export * from './popper';
+export * from './hover-card';
+export * from './popover';
+export * from './tooltip';
+export * from './tree';
+export * from './stepper';
+export * from './editable';
+export * from './tags-input';
+export * from './listbox';
+
+export * from './menu';
+export * from './dropdown-menu';
+export * from './context-menu';
+export * from './menubar';
diff --git a/vue/primitives/src/menu/MenuAnchor.vue b/vue/primitives/src/menu/MenuAnchor.vue
new file mode 100644
index 0000000..0a3f9ae
--- /dev/null
+++ b/vue/primitives/src/menu/MenuAnchor.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuArrow.vue b/vue/primitives/src/menu/MenuArrow.vue
new file mode 100644
index 0000000..13d62f7
--- /dev/null
+++ b/vue/primitives/src/menu/MenuArrow.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuCheckboxItem.vue b/vue/primitives/src/menu/MenuCheckboxItem.vue
new file mode 100644
index 0000000..a91cfc8
--- /dev/null
+++ b/vue/primitives/src/menu/MenuCheckboxItem.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuContent.vue b/vue/primitives/src/menu/MenuContent.vue
new file mode 100644
index 0000000..1ff4dd6
--- /dev/null
+++ b/vue/primitives/src/menu/MenuContent.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuContentImpl.vue b/vue/primitives/src/menu/MenuContentImpl.vue
new file mode 100644
index 0000000..74cca31
--- /dev/null
+++ b/vue/primitives/src/menu/MenuContentImpl.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+ {
+ emit('entryFocus', event)
+ if (!rootCtx.isUsingKeyboardRef.value) event.preventDefault()
+ }"
+ >
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuGroup.vue b/vue/primitives/src/menu/MenuGroup.vue
new file mode 100644
index 0000000..f6a5ca9
--- /dev/null
+++ b/vue/primitives/src/menu/MenuGroup.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuItem.vue b/vue/primitives/src/menu/MenuItem.vue
new file mode 100644
index 0000000..97bc355
--- /dev/null
+++ b/vue/primitives/src/menu/MenuItem.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuItemImpl.vue b/vue/primitives/src/menu/MenuItemImpl.vue
new file mode 100644
index 0000000..9249ad7
--- /dev/null
+++ b/vue/primitives/src/menu/MenuItemImpl.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuItemIndicator.vue b/vue/primitives/src/menu/MenuItemIndicator.vue
new file mode 100644
index 0000000..bcab342
--- /dev/null
+++ b/vue/primitives/src/menu/MenuItemIndicator.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuLabel.vue b/vue/primitives/src/menu/MenuLabel.vue
new file mode 100644
index 0000000..994cdb7
--- /dev/null
+++ b/vue/primitives/src/menu/MenuLabel.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuPortal.vue b/vue/primitives/src/menu/MenuPortal.vue
new file mode 100644
index 0000000..f27d2d6
--- /dev/null
+++ b/vue/primitives/src/menu/MenuPortal.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuRadioGroup.vue b/vue/primitives/src/menu/MenuRadioGroup.vue
new file mode 100644
index 0000000..e1cc51a
--- /dev/null
+++ b/vue/primitives/src/menu/MenuRadioGroup.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuRadioItem.vue b/vue/primitives/src/menu/MenuRadioItem.vue
new file mode 100644
index 0000000..89f258e
--- /dev/null
+++ b/vue/primitives/src/menu/MenuRadioItem.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuRoot.vue b/vue/primitives/src/menu/MenuRoot.vue
new file mode 100644
index 0000000..f845355
--- /dev/null
+++ b/vue/primitives/src/menu/MenuRoot.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuRootContentModal.vue b/vue/primitives/src/menu/MenuRootContentModal.vue
new file mode 100644
index 0000000..140203b
--- /dev/null
+++ b/vue/primitives/src/menu/MenuRootContentModal.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuRootContentNonModal.vue b/vue/primitives/src/menu/MenuRootContentNonModal.vue
new file mode 100644
index 0000000..24bf5f2
--- /dev/null
+++ b/vue/primitives/src/menu/MenuRootContentNonModal.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuSeparator.vue b/vue/primitives/src/menu/MenuSeparator.vue
new file mode 100644
index 0000000..9538de3
--- /dev/null
+++ b/vue/primitives/src/menu/MenuSeparator.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuSub.vue b/vue/primitives/src/menu/MenuSub.vue
new file mode 100644
index 0000000..fdaaa9d
--- /dev/null
+++ b/vue/primitives/src/menu/MenuSub.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuSubContent.vue b/vue/primitives/src/menu/MenuSubContent.vue
new file mode 100644
index 0000000..889887d
--- /dev/null
+++ b/vue/primitives/src/menu/MenuSubContent.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ { event.preventDefault(); emit('closeAutoFocus', event) }"
+ @escape-key-down="(event: KeyboardEvent) => {
+ emit('escapeKeyDown', event)
+ menuCtx.onOpenChange(false)
+ }"
+ @pointer-down-outside="emit('pointerDownOutside', $event)"
+ @focus-outside="(event: FocusEvent) => {
+ if (subCtx.trigger.value?.contains(event.target as Node)) event.preventDefault()
+ emit('focusOutside', event)
+ }"
+ @interact-outside="emit('interactOutside', $event)"
+ @dismiss="menuCtx.onOpenChange(false)"
+ @entry-focus="emit('entryFocus', $event)"
+ @open-auto-focus="emit('openAutoFocus', $event)"
+ >
+
+
+
+
diff --git a/vue/primitives/src/menu/MenuSubTrigger.vue b/vue/primitives/src/menu/MenuSubTrigger.vue
new file mode 100644
index 0000000..85deebe
--- /dev/null
+++ b/vue/primitives/src/menu/MenuSubTrigger.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menu/context.ts b/vue/primitives/src/menu/context.ts
new file mode 100644
index 0000000..aebb2f1
--- /dev/null
+++ b/vue/primitives/src/menu/context.ts
@@ -0,0 +1,62 @@
+import type { CheckedState } from './types';
+import type { ComputedRef, Ref, ShallowRef } from 'vue';
+import type { Direction } from '../config-provider';
+
+import { useContextFactory } from '@robonen/vue';
+
+export interface MenuContext {
+ open: Ref;
+ onOpenChange: (open: boolean) => void;
+ content: Ref;
+ onContentChange: (el: HTMLElement | null) => void;
+}
+export const { inject: useMenuContext, provide: provideMenuContext }
+ = useContextFactory('MenuContext');
+
+export interface MenuRootContext {
+ onClose: () => void;
+ dir: Ref;
+ isUsingKeyboardRef: Ref;
+ modal: Ref;
+}
+export const { inject: useMenuRootContext, provide: provideMenuRootContext }
+ = useContextFactory('MenuRootContext');
+
+export interface MenuContentContext {
+ onItemEnter: (event: PointerEvent) => boolean;
+ onItemLeave: (event: PointerEvent) => void;
+ onTriggerLeave: (event: PointerEvent) => boolean;
+ searchRef: Ref;
+ pointerGraceTimerRef: Ref;
+ onPointerGraceIntentChange: (intent: { area: Array<{ x: number; y: number }>; side: 'left' | 'right' } | null) => void;
+}
+export const { inject: useMenuContentContext, provide: provideMenuContentContext }
+ = useContextFactory('MenuContentContext');
+
+export interface MenuSubContext {
+ contentId: ComputedRef;
+ triggerId: ComputedRef;
+ trigger: ShallowRef;
+ onTriggerChange: (el: HTMLElement | null) => void;
+}
+export const { inject: useMenuSubContext, provide: provideMenuSubContext }
+ = useContextFactory('MenuSubContext');
+
+export interface MenuRadioGroupContext {
+ modelValue: Ref;
+ onValueChange: (value: string) => void;
+}
+export const { inject: useMenuRadioGroupContext, provide: provideMenuRadioGroupContext }
+ = useContextFactory('MenuRadioGroupContext');
+
+export interface MenuItemIndicatorContext {
+ checkedState: Ref;
+}
+export const { inject: useMenuItemIndicatorContext, provide: provideMenuItemIndicatorContext }
+ = useContextFactory('MenuItemIndicatorContext');
+
+export interface MenuGroupContext {
+ id: string;
+}
+export const { inject: useMenuGroupContext, provide: provideMenuGroupContext }
+ = useContextFactory('MenuGroupContext');
diff --git a/vue/primitives/src/menu/index.ts b/vue/primitives/src/menu/index.ts
new file mode 100644
index 0000000..d3078e9
--- /dev/null
+++ b/vue/primitives/src/menu/index.ts
@@ -0,0 +1,20 @@
+export type { CheckedState } from './types';
+
+export { useMenuContext, useMenuContentContext, useMenuRootContext, useMenuSubContext } from './context';
+export { default as MenuAnchor, type MenuAnchorProps } from './MenuAnchor.vue';
+export { default as MenuArrow, type MenuArrowProps } from './MenuArrow.vue';
+export { default as MenuCheckboxItem, type MenuCheckboxItemEmits, type MenuCheckboxItemProps } from './MenuCheckboxItem.vue';
+export { default as MenuContent, type MenuContentEmits, type MenuContentProps } from './MenuContent.vue';
+export { default as MenuGroup, type MenuGroupProps } from './MenuGroup.vue';
+export { default as MenuItem, type MenuItemEmits, type MenuItemProps } from './MenuItem.vue';
+export { default as MenuItemImpl, type MenuItemImplEmits, type MenuItemImplProps } from './MenuItemImpl.vue';
+export { default as MenuItemIndicator, type MenuItemIndicatorProps } from './MenuItemIndicator.vue';
+export { default as MenuLabel, type MenuLabelProps } from './MenuLabel.vue';
+export { default as MenuPortal, type MenuPortalProps } from './MenuPortal.vue';
+export { default as MenuRadioGroup, type MenuRadioGroupEmits, type MenuRadioGroupProps } from './MenuRadioGroup.vue';
+export { default as MenuRadioItem, type MenuRadioItemEmits, type MenuRadioItemProps } from './MenuRadioItem.vue';
+export { default as MenuRoot, type MenuRootEmits, type MenuRootProps } from './MenuRoot.vue';
+export { default as MenuSeparator, type MenuSeparatorProps } from './MenuSeparator.vue';
+export { default as MenuSub, type MenuSubEmits, type MenuSubProps } from './MenuSub.vue';
+export { default as MenuSubContent, type MenuSubContentEmits, type MenuSubContentProps } from './MenuSubContent.vue';
+export { default as MenuSubTrigger, type MenuSubTriggerProps } from './MenuSubTrigger.vue';
diff --git a/vue/primitives/src/menu/types.ts b/vue/primitives/src/menu/types.ts
new file mode 100644
index 0000000..6df28d2
--- /dev/null
+++ b/vue/primitives/src/menu/types.ts
@@ -0,0 +1 @@
+export type CheckedState = boolean | 'indeterminate';
diff --git a/vue/primitives/src/menu/useIsUsingKeyboard.ts b/vue/primitives/src/menu/useIsUsingKeyboard.ts
new file mode 100644
index 0000000..c5c0b98
--- /dev/null
+++ b/vue/primitives/src/menu/useIsUsingKeyboard.ts
@@ -0,0 +1,23 @@
+import { ref } from 'vue';
+
+const isUsingKeyboard = ref(false);
+let initialized = false;
+
+function init() {
+ if (initialized || typeof document === 'undefined') return;
+ initialized = true;
+ document.addEventListener('keydown', () => {
+ isUsingKeyboard.value = true;
+ }, { capture: true, passive: true });
+ document.addEventListener('pointerdown', () => {
+ isUsingKeyboard.value = false;
+ }, { capture: true, passive: true });
+ document.addEventListener('pointermove', () => {
+ isUsingKeyboard.value = false;
+ }, { capture: true, passive: true });
+}
+
+export function useIsUsingKeyboard() {
+ init();
+ return isUsingKeyboard;
+}
diff --git a/vue/primitives/src/menu/utils.ts b/vue/primitives/src/menu/utils.ts
new file mode 100644
index 0000000..1b06501
--- /dev/null
+++ b/vue/primitives/src/menu/utils.ts
@@ -0,0 +1,86 @@
+import type { CheckedState } from './types';
+
+import { getActiveElement } from '@robonen/platform/browsers';
+
+export const ITEM_SELECT = 'menu.itemSelect';
+export const SELECTION_KEYS = ['Enter', ' '];
+export const FIRST_KEYS = ['ArrowDown', 'PageUp', 'Home'];
+export const LAST_KEYS = ['ArrowUp', 'PageDown', 'End'];
+export const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
+export const SUB_OPEN_KEYS: Record = {
+ ltr: [...SELECTION_KEYS, 'ArrowRight'],
+ rtl: [...SELECTION_KEYS, 'ArrowLeft'],
+};
+export const SUB_CLOSE_KEYS: Record = {
+ ltr: ['ArrowLeft'],
+ rtl: ['ArrowRight'],
+};
+
+export function getOpenState(open: boolean): 'open' | 'closed' {
+ return open ? 'open' : 'closed';
+}
+
+export function isIndeterminate(checked: CheckedState): checked is 'indeterminate' {
+ return checked === 'indeterminate';
+}
+
+export function getCheckedState(checked: CheckedState): 'checked' | 'unchecked' | 'indeterminate' {
+ if (isIndeterminate(checked)) return 'indeterminate';
+ return checked ? 'checked' : 'unchecked';
+}
+
+export function focusFirst(candidates: HTMLElement[]): void {
+ for (const candidate of candidates) {
+ const prev = getActiveElement();
+ candidate.focus({ preventScroll: true });
+ if (getActiveElement() !== prev) return;
+ }
+}
+
+export function getNextMatch(
+ items: HTMLElement[],
+ search: string,
+ currentItem?: HTMLElement | null,
+): HTMLElement | undefined {
+ const isRepeating = search.length > 1 && Array.from(search).every(c => c === search[0]);
+ const normalizedSearch = isRepeating ? search[0]! : search;
+
+ const currentIndex = currentItem ? items.indexOf(currentItem) : -1;
+ const wrappedItems = currentIndex !== -1
+ ? [...items.slice(currentIndex + 1), ...items.slice(0, currentIndex + 1)]
+ : items;
+
+ const getText = (el: HTMLElement) =>
+ el.dataset['primitiveMenuItemTextValue'] ?? el.textContent?.trim() ?? '';
+
+ return wrappedItems.find(item =>
+ getText(item).toLowerCase().startsWith(normalizedSearch.toLowerCase()),
+ );
+}
+
+export interface Point { x: number; y: number };
+export type Polygon = Point[];
+export type Side = 'left' | 'right';
+export interface GraceIntent { area: Polygon; side: Side }
+
+export function isPointInPolygon(point: Point, polygon: Polygon): boolean {
+ const { x, y } = point;
+ let inside = false;
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+ const xi = polygon[i]!.x;
+ const yi = polygon[i]!.y;
+ const xj = polygon[j]!.x;
+ const yj = polygon[j]!.y;
+ const intersects = (yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
+ if (intersects) inside = !inside;
+ }
+ return inside;
+}
+
+export function isPointerInGraceArea(event: PointerEvent, area: Polygon): boolean {
+ return isPointInPolygon({ x: event.clientX, y: event.clientY }, area);
+}
+
+export function isMouseEvent(event: Event): event is MouseEvent {
+ return ['mousedown', 'mouseup', 'mousemove', 'click'].includes(event.type);
+}
diff --git a/vue/primitives/src/menubar/MenubarArrow.vue b/vue/primitives/src/menubar/MenubarArrow.vue
new file mode 100644
index 0000000..eb82864
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarArrow.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarCheckboxItem.vue b/vue/primitives/src/menubar/MenubarCheckboxItem.vue
new file mode 100644
index 0000000..bd8fce4
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarCheckboxItem.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarContent.vue b/vue/primitives/src/menubar/MenubarContent.vue
new file mode 100644
index 0000000..19fc7e6
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarContent.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+ {
+ if (!menuCtx.wasKeyboardTriggerOpenRef.value) event.preventDefault()
+ menuCtx.wasKeyboardTriggerOpenRef.value = false
+ menuCtx.triggerRef.value?.focus({ preventScroll: true })
+ emit('closeAutoFocus', event)
+ }"
+ @escape-key-down="emit('escapeKeyDown', $event)"
+ @pointer-down-outside="(event: PointerEvent | MouseEvent) => {
+ const target = event.target as Node
+ const isMenubarTrigger = menuCtx.triggerRef.value?.contains(target)
+ if (isMenubarTrigger) event.preventDefault()
+ emit('pointerDownOutside', event)
+ }"
+ @focus-outside="emit('focusOutside', $event)"
+ @interact-outside="emit('interactOutside', $event)"
+ @dismiss="rootCtx.onMenuClose()"
+ @entry-focus="emit('entryFocus', $event)"
+ @open-auto-focus="emit('openAutoFocus', $event)"
+ >
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarGroup.vue b/vue/primitives/src/menubar/MenubarGroup.vue
new file mode 100644
index 0000000..aec69d7
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarGroup.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarItem.vue b/vue/primitives/src/menubar/MenubarItem.vue
new file mode 100644
index 0000000..671cd8e
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarItem.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarItemIndicator.vue b/vue/primitives/src/menubar/MenubarItemIndicator.vue
new file mode 100644
index 0000000..dce9565
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarItemIndicator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarLabel.vue b/vue/primitives/src/menubar/MenubarLabel.vue
new file mode 100644
index 0000000..d15ec5f
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarLabel.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarMenu.vue b/vue/primitives/src/menubar/MenubarMenu.vue
new file mode 100644
index 0000000..80f29a3
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarMenu.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+ {
+ if (!v) rootCtx.onMenuClose()
+ }"
+ >
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarPortal.vue b/vue/primitives/src/menubar/MenubarPortal.vue
new file mode 100644
index 0000000..b2eb31a
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarPortal.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarRadioGroup.vue b/vue/primitives/src/menubar/MenubarRadioGroup.vue
new file mode 100644
index 0000000..a82fe52
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarRadioGroup.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarRadioItem.vue b/vue/primitives/src/menubar/MenubarRadioItem.vue
new file mode 100644
index 0000000..c206501
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarRadioItem.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarRoot.vue b/vue/primitives/src/menubar/MenubarRoot.vue
new file mode 100644
index 0000000..40ed22a
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarRoot.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarSeparator.vue b/vue/primitives/src/menubar/MenubarSeparator.vue
new file mode 100644
index 0000000..103e646
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarSeparator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarSub.vue b/vue/primitives/src/menubar/MenubarSub.vue
new file mode 100644
index 0000000..d140014
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarSub.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarSubContent.vue b/vue/primitives/src/menubar/MenubarSubContent.vue
new file mode 100644
index 0000000..daf1e27
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarSubContent.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarSubTrigger.vue b/vue/primitives/src/menubar/MenubarSubTrigger.vue
new file mode 100644
index 0000000..2536df4
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarSubTrigger.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/MenubarTrigger.vue b/vue/primitives/src/menubar/MenubarTrigger.vue
new file mode 100644
index 0000000..f53d828
--- /dev/null
+++ b/vue/primitives/src/menubar/MenubarTrigger.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vue/primitives/src/menubar/context.ts b/vue/primitives/src/menubar/context.ts
new file mode 100644
index 0000000..54d4b6f
--- /dev/null
+++ b/vue/primitives/src/menubar/context.ts
@@ -0,0 +1,32 @@
+import type { ComputedRef, Ref, ShallowRef } from 'vue';
+import type { Direction } from '../config-provider';
+
+import { useContextFactory } from '@robonen/vue';
+
+export interface MenubarRootContext {
+ value: Ref;
+ dir: Ref;
+ loop: Ref;
+ onMenuOpen: (value: string) => void;
+ onMenuClose: () => void;
+ onMenuToggle: (value: string) => void;
+}
+
+export const {
+ inject: useMenubarRootContext,
+ provide: provideMenubarRootContext,
+} = useContextFactory('MenubarRootContext');
+
+export interface MenubarMenuContext {
+ value: string;
+ triggerId: ComputedRef;
+ contentId: ComputedRef;
+ triggerRef: ShallowRef;
+ onTriggerChange: (el: HTMLElement | null) => void;
+ wasKeyboardTriggerOpenRef: Ref;
+}
+
+export const {
+ inject: useMenubarMenuContext,
+ provide: provideMenubarMenuContext,
+} = useContextFactory('MenubarMenuContext');
diff --git a/vue/primitives/src/menubar/index.ts b/vue/primitives/src/menubar/index.ts
new file mode 100644
index 0000000..490f39e
--- /dev/null
+++ b/vue/primitives/src/menubar/index.ts
@@ -0,0 +1,50 @@
+export { default as MenubarRoot } from './MenubarRoot.vue';
+export type { MenubarRootProps, MenubarRootEmits } from './MenubarRoot.vue';
+
+export { default as MenubarMenu } from './MenubarMenu.vue';
+export type { MenubarMenuProps } from './MenubarMenu.vue';
+
+export { default as MenubarTrigger } from './MenubarTrigger.vue';
+export type { MenubarTriggerProps } from './MenubarTrigger.vue';
+
+export { default as MenubarContent } from './MenubarContent.vue';
+export type { MenubarContentProps, MenubarContentEmits } from './MenubarContent.vue';
+
+export { default as MenubarPortal } from './MenubarPortal.vue';
+export type { MenubarPortalProps } from './MenubarPortal.vue';
+
+export { default as MenubarArrow } from './MenubarArrow.vue';
+export type { MenubarArrowProps } from './MenubarArrow.vue';
+
+export { default as MenubarSeparator } from './MenubarSeparator.vue';
+export type { MenubarSeparatorProps } from './MenubarSeparator.vue';
+
+export { default as MenubarLabel } from './MenubarLabel.vue';
+export type { MenubarLabelProps } from './MenubarLabel.vue';
+
+export { default as MenubarGroup } from './MenubarGroup.vue';
+export type { MenubarGroupProps } from './MenubarGroup.vue';
+
+export { default as MenubarItem } from './MenubarItem.vue';
+export type { MenubarItemProps, MenubarItemEmits } from './MenubarItem.vue';
+
+export { default as MenubarCheckboxItem } from './MenubarCheckboxItem.vue';
+export type { MenubarCheckboxItemProps, MenubarCheckboxItemEmits } from './MenubarCheckboxItem.vue';
+
+export { default as MenubarRadioGroup } from './MenubarRadioGroup.vue';
+export type { MenubarRadioGroupProps, MenubarRadioGroupEmits } from './MenubarRadioGroup.vue';
+
+export { default as MenubarRadioItem } from './MenubarRadioItem.vue';
+export type { MenubarRadioItemProps, MenubarRadioItemEmits } from './MenubarRadioItem.vue';
+
+export { default as MenubarItemIndicator } from './MenubarItemIndicator.vue';
+export type { MenubarItemIndicatorProps } from './MenubarItemIndicator.vue';
+
+export { default as MenubarSub } from './MenubarSub.vue';
+export type { MenubarSubProps, MenubarSubEmits } from './MenubarSub.vue';
+
+export { default as MenubarSubTrigger } from './MenubarSubTrigger.vue';
+export type { MenubarSubTriggerProps } from './MenubarSubTrigger.vue';
+
+export { default as MenubarSubContent } from './MenubarSubContent.vue';
+export type { MenubarSubContentProps, MenubarSubContentEmits } from './MenubarSubContent.vue';