diff --git a/codex-ui/dev/Playground.vue b/codex-ui/dev/Playground.vue
index f17b7a6b..5c4d86d6 100644
--- a/codex-ui/dev/Playground.vue
+++ b/codex-ui/dev/Playground.vue
@@ -29,6 +29,7 @@
+
@@ -36,7 +37,8 @@
import { computed } from 'vue';
import {
VerticalMenu,
- Tabbar
+ Tabbar,
+ Popover
} from '../src/vue';
import { useTheme } from '../src/vue/composables/useTheme';
diff --git a/codex-ui/dev/pages/components/Popover.vue b/codex-ui/dev/pages/components/Popover.vue
index d8410d8c..cd2c6b66 100644
--- a/codex-ui/dev/pages/components/Popover.vue
+++ b/codex-ui/dev/pages/components/Popover.vue
@@ -5,12 +5,117 @@
Component that will appear near a particular element. Can contain any content.
+
+ Aligning
+
+ You can choose vertical aligning from below
and above
and horizontal aligning from left
and right
.
+
+
+
+ Width
+
+
+ By default, width of the popover depends on its content. You can also set it to fit-target
to stretch to the width of the target.
+
+
diff --git a/codex-ui/src/styles/index.pcss b/codex-ui/src/styles/index.pcss
index 7aa817eb..d610b922 100644
--- a/codex-ui/src/styles/index.pcss
+++ b/codex-ui/src/styles/index.pcss
@@ -2,3 +2,4 @@
@import './dimensions.pcss';
@import './typography.pcss';
@import './mixins.pcss';
+@import './z-axis.pcss';
diff --git a/codex-ui/src/styles/z-axis.pcss b/codex-ui/src/styles/z-axis.pcss
new file mode 100644
index 00000000..0e7ac9c8
--- /dev/null
+++ b/codex-ui/src/styles/z-axis.pcss
@@ -0,0 +1,3 @@
+:root {
+ --z-popover: 3;
+}
diff --git a/codex-ui/src/vue/components/popover/Popover.types.ts b/codex-ui/src/vue/components/popover/Popover.types.ts
new file mode 100644
index 00000000..a4806351
--- /dev/null
+++ b/codex-ui/src/vue/components/popover/Popover.types.ts
@@ -0,0 +1,45 @@
+import type { Component } from 'vue';
+
+/**
+ * Popover content: component and props
+ */
+export interface PopoverContent {
+ /**
+ * Component to render in the popover
+ */
+ component: Component;
+
+ /**
+ * Props to pass to the component
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ props: Record;
+}
+
+/**
+ * Popover showing configuration
+ */
+export interface PopoverShowParams {
+ /**
+ * Element to move popover to
+ */
+ targetEl: HTMLElement;
+
+ /**
+ * Popover content: component and props
+ */
+ with: PopoverContent;
+
+ /**
+ * Vertical and horizontal alignment
+ */
+ align: {
+ vertically: 'above' | 'below';
+ horizontally: 'left' | 'right';
+ };
+
+ /**
+ * Allows to stretch popover to the target element width
+ */
+ width?: 'fit-target' | 'auto';
+}
diff --git a/codex-ui/src/vue/components/popover/Popover.vue b/codex-ui/src/vue/components/popover/Popover.vue
new file mode 100644
index 00000000..706e3e48
--- /dev/null
+++ b/codex-ui/src/vue/components/popover/Popover.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/codex-ui/src/vue/components/popover/index.ts b/codex-ui/src/vue/components/popover/index.ts
new file mode 100644
index 00000000..3cd00853
--- /dev/null
+++ b/codex-ui/src/vue/components/popover/index.ts
@@ -0,0 +1,5 @@
+import Popover from './Popover.vue';
+
+export * from './usePopover';
+export * from './Popover.types';
+export { Popover };
diff --git a/codex-ui/src/vue/components/popover/usePopover.ts b/codex-ui/src/vue/components/popover/usePopover.ts
new file mode 100644
index 00000000..ea8e5766
--- /dev/null
+++ b/codex-ui/src/vue/components/popover/usePopover.ts
@@ -0,0 +1,163 @@
+import { reactive, ref, shallowRef } from 'vue';
+import { createSharedComposable } from '@vueuse/core';
+import type { PopoverContent, PopoverShowParams } from './Popover.types';
+
+/**
+ * Shared composable for the Popover component
+ *
+ * Popover is the empty container that can be moved to the target element and contain any component
+ * @example
+ * showPopover({
+ * targetEl: el, // element to move popover to
+ * with: {
+ * component: ContextMenu,
+ * props: {
+ * items: [
+ * { title: 'Item 1' },
+ * { title: 'Item 2' },
+ * { title: 'Item 3' },
+ * ],
+ * },
+ * },
+ * align: {
+ * vertically: 'below',
+ * horizontally: 'left',
+ * },
+ * width: 'fit-target',
+ * });
+ */
+export const usePopover = createSharedComposable(() => {
+ /**
+ * Popover opening state
+ */
+ const isOpen = ref(false);
+
+ /**
+ * Popover position info used in the Popover component
+ */
+ const position = reactive({
+ left: 0,
+ top: 0,
+ transform: 'translate(0, 0)',
+ });
+
+ /**
+ * Popover width. Allows to stretch popover to the target element width
+ */
+ const width = ref('auto');
+
+ /**
+ * Popover content: component and props
+ */
+ const content = shallowRef(null);
+
+ /**
+ * Move popover to the target element
+ * Also, align and set width
+ * @param targetEl - element to move popover to
+ * @param align - vertical and horizontal alignment
+ * @param widthConfig - allows to stretch popover to the target element width
+ */
+ function move(targetEl: HTMLElement, align: PopoverShowParams['align'], widthConfig: PopoverShowParams['width'] = 'auto'): void {
+ const MARGIN = 6;
+
+ const rect = targetEl.getBoundingClientRect();
+
+ let top = 0;
+ let left = 0;
+ let transformX = '0';
+ let transformY = '0';
+
+ switch (align.vertically) {
+ case 'above':
+ top = rect.top - MARGIN + window.scrollY;
+ transformY = '-100%';
+ break;
+ case 'below':
+ top = rect.bottom + MARGIN + window.scrollY;
+ transformY = '0';
+ break;
+ }
+
+ switch (align.horizontally) {
+ case 'left':
+ left = rect.left;
+ transformX = '0';
+ break;
+ case 'right':
+ left = rect.right;
+ transformX = '-100';
+ break;
+ }
+
+ switch (widthConfig) {
+ case 'fit-target':
+ width.value = `${rect.width}px`;
+ break;
+ case 'auto':
+ width.value = 'auto';
+ break;
+ }
+
+ position.left = left;
+ position.top = top;
+ position.transform = `translate(${transformX}%, ${transformY})`;
+ }
+
+ /**
+ * Show popover
+ */
+ function show(): void {
+ isOpen.value = true;
+ }
+
+ /**
+ * Method for attaching a component to the popover
+ * @param component - component to attach
+ * @param props - props to pass to the component
+ */
+ function mountComponent(component: PopoverContent['component'], props: PopoverContent['props']): void {
+ content.value = {
+ component,
+ props,
+ };
+ }
+
+ /**
+ * Move popover to the passed element and show it
+ * @param params - popover showing configuration
+ */
+ function showPopover(params: PopoverShowParams): void {
+ move(params.targetEl, params.align, params.width);
+ mountComponent(params.with.component, params.with.props);
+ show();
+ }
+
+ /**
+ * Empty content, position and hide popover
+ */
+ function resetPopover(): void {
+ content.value = null;
+ position.left = 0;
+ position.top = 0;
+ position.transform = 'translate(0, 0)';
+
+ isOpen.value = false;
+ }
+
+ /**
+ * Hides and resets the popover
+ */
+ function hide(): void {
+ resetPopover();
+ }
+
+ return {
+ isOpen,
+ showPopover,
+ position,
+ hide,
+ content,
+ width,
+ };
+});
diff --git a/codex-ui/src/vue/index.ts b/codex-ui/src/vue/index.ts
index eb768950..64ae4d4c 100644
--- a/codex-ui/src/vue/index.ts
+++ b/codex-ui/src/vue/index.ts
@@ -15,4 +15,5 @@ export * from './components/context-menu';
export * from './components/vertical-menu';
export * from './components/colorShemeIcons';
export * from './components/theme-preview';
+export * from './components/popover';
export * from './composables/useTheme';
diff --git a/src/App.vue b/src/App.vue
index a15070aa..ac2bd447 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,12 +1,13 @@
+