feat(web-core): new menu management system (#8123)

This commit is contained in:
Thierry Goettelmann
2024-12-09 10:02:54 +01:00
committed by GitHub
parent c08f84bf04
commit 708b9b5bc8
19 changed files with 828 additions and 1 deletions

View File

@@ -105,6 +105,8 @@ module.exports = {
],
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'no-void': ['error', { allowAsStatement: true }],
'n/no-missing-import': 'off', // using 'import' plugin instead, to support TS aliases
'no-redeclare': 'off', // automatically checked by the TypeScript compiler

View File

@@ -15,6 +15,7 @@
},
"devDependencies": {
"@csstools/postcss-global-data": "^2.1.1",
"@floating-ui/vue": "^1.1.5",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",

View File

@@ -26,3 +26,4 @@
# Packages
- [Job System](../lib/packages/job/README.md)
- [Menu System](../lib/packages/menu/README.md)

View File

@@ -0,0 +1,194 @@
# Menu System
A menu system for Vue applications supporting props binding for actions, links, router links, and nested submenus.
## Basic Usage
```vue
<template>
<MenuList>
<li><MenuTrigger v-bind="menu.save">Save</MenuTrigger></li>
<li><MenuTrigger v-bind="menu.doc">Documentation</MenuTrigger></li>
<li><MenuTrigger v-bind="menu.profile">Profile</MenuTrigger></li>
<li>
<MenuTrigger v-bind="menu.more.$trigger">More...</MenuTrigger>
<MenuList v-bind="menu.more.$target">
<MenuTrigger v-bind="menu.more.settings">Settings</MenuTrigger>
<MenuTrigger v-bind="menu.more.logout">Logout</MenuTrigger>
</MenuList>
</li>
</MenuList>
</template>
<script lang="ts" setup>
import { action, link, routerLink, toggle, useMenu } from '@core/packages/menu'
const menu = useMenu({
save: action(() => console.log('Saving...')),
doc: link('https://docs.example.com'),
profile: routerLink({ name: 'profile' }),
more: toggle({
settings: action(() => console.log('Settings clicked')),
logout: action(() => console.log('Logout clicked')),
}),
})
</script>
```
## Core Composables
### useMenu
Create a root menu with multiple items and/or submenus:
```ts
const fileMenu = useMenu({
// Menu structure
edit: action(() => handleEdit()),
save: action(() => handleSave(), {
disabled: () => !canSave.value,
busy: isSaving,
}),
})
```
### useMenuAction
Create an action button. It must be attached to a menu.
Mostly useful when splitting a menu into subcomponents.
```ts
const props = defineProps<{
menu: MenuLike
}>()
const action = useMenuAction(menu, () => handleClick(), {
disabled: isDisabled,
busy: isLoading,
})
```
### useMenuToggle
Create a toggle menu (dropdown) with nested items.
You can pass a `parent: MenuLike` as option to attach the toggle to a parent Menu.
Skip this option to create a root menu toggle.
```ts
const dropdown = useMenuToggle(
{
behavior: 'click', // or 'mouseenter'
placement: 'bottom-start',
},
{
edit: action(() => handleEdit()),
delete: action(() => handleDelete()),
}
)
```
## Component Integration
Once you create the menu, you can bind its items as props to your template.
For now, these bindings are meant to be used with a Vue component configured with the same props
For other component or HTML Element, you can still bind the props manually.
`action()` items will generate the following props binding:
```ts
type Props = {
as: 'button'
type: 'button'
disabled: boolean
busy: boolean
tooltip: string | false
onMouseenter: () => void
onClick: () => void
'data-menu-id': string
}
```
`link()` items will generate the following props binding:
```ts
type Props = {
as: 'a'
href: string
rel: 'noreferrer noopener'
target: '_blank'
onMouseenter: () => void
onClick: () => void
'data-menu-id': string
}
```
`routerLink()` items will generate the following props binding:
```ts
type Props = {
as: RouterLink
to: RouteLocationRaw
onMouseenter: () => void
onClick: () => void
'data-menu-id': string
}
```
`toggle()` items will generate an object containing `$trigger`, `$target` and `$isOpen` (`ComputedRef<boolean>`) properties with the following props binding:
```ts
// $trigger
type Props = {
as: 'button'
type: 'button'
submenu: true
ref: (el: any) => void
active: boolean
onClick: () => void
onMouseenter: () => void
'data-menu-id': string
}
// $target
type Props = {
ref: (el: any) => void
style: object
'data-menu-id': string
}
```
### Example
```vue
<template>
<MenuTrigger v-bind="menu.save">Save</MenuTrigger>
<MenuTrigger v-bind="menu.docs">Documentation</MenuTrigger>
<MenuTrigger v-bind="menu.profile">Profile</MenuTrigger>
<!-- Toggle/Dropdown menu -->
<MenuTrigger v-bind="menu.more.$trigger">More</MenuTrigger>
<div v-bind="menu.more.$target">
<MenuTrigger v-bind="menu.more.settings">Settings</MenuTrigger>
<MenuTrigger v-bind="menu.more.logout">Logout</MenuTrigger>
</div>
</template>
```
If you want to bind props to an HTML element or a Vue component not supporting all the props,
you can use VueUse's `objectOmit` for example or bind the props completely manually.
```vue
<script>
import { objectOmit as omit } from '@vueuse/shared'
</script>
<template>
<MyCustomElement v-bind="omit(menu.save, ['as', 'type'])">Save</MyCustomElement>
<button v-bind="omit(menu.save, ['as', busy, 'tooltip'])" :class="{ busy: menu.save.busy }">Save</button>
</template>
```

View File

@@ -0,0 +1,101 @@
import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
import { computed, type MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
export interface MenuActionConfig {
handler: () => any
disabled?: MaybeRefOrGetter<boolean | string | undefined>
busy?: MaybeRefOrGetter<boolean | string | undefined>
}
export class MenuActionConfigHolder {
constructor(public config: MenuActionConfig) {}
}
export interface MenuActionProps {
as: 'button'
type: 'button'
disabled: boolean
busy: boolean
tooltip: string | false
onClick: () => void
onMouseenter: () => void
'data-menu-id': string
}
export class MenuAction extends BaseItem {
isRunning = ref(false)
constructor(
public menu: Menu,
public config: MenuActionConfig
) {
super(menu)
}
get busyConfig() {
return computed(() => toValue(this.config.busy) ?? false)
}
get isBusy() {
return computed(() => this.isRunning.value || this.busyConfig.value !== false)
}
get busyReason() {
return computed(() => (typeof this.busyConfig.value === 'string' ? this.busyConfig.value : undefined))
}
get disabledConfig() {
return computed(() => toValue(this.config.disabled) ?? false)
}
get isDisabled() {
return computed(() => this.isBusy.value || this.disabledConfig.value !== false)
}
get disabledReason() {
return computed(() => (typeof this.disabledConfig.value === 'string' ? this.disabledConfig.value : undefined))
}
get tooltip() {
return computed(() => this.disabledReason.value ?? this.busyReason.value ?? false)
}
get props(): MenuActionProps {
return reactive({
as: 'button',
type: 'button',
onClick: async () => {
if (this.isDisabled.value) {
return
}
this.isRunning.value = true
try {
await this.config.handler()
this.deactivate()
} finally {
this.isRunning.value = false
}
},
onMouseenter: () => this.activate(),
disabled: this.isDisabled,
busy: this.isBusy,
tooltip: this.tooltip,
'data-menu-id': this.menu.context.id,
})
}
}
export function action(handler: () => void, config: Omit<MenuActionConfig, 'handler'> = {}): MenuActionConfigHolder {
return new MenuActionConfigHolder({
...config,
handler,
})
}
export function useMenuAction(config: MenuActionConfig & { parent: MenuLike }) {
const { parent, handler, ...configRest } = config
return parseConfigHolder(parent, action(handler, configRest))
}

View File

@@ -0,0 +1,26 @@
import type { MenuAction, MenuLink, MenuRouterLink, MenuToggleTrigger, Menu } from '@core/packages/menu'
import { computed } from 'vue'
export type MenuItem = MenuAction | MenuLink | MenuRouterLink | MenuToggleTrigger
export abstract class BaseItem {
id = Symbol('Menu Item')
protected constructor(public menu: Menu) {
menu.addItem(this as any)
}
get isActive() {
return computed(() => this.menu.context.activeItemId.value === this.id)
}
activate() {
this.menu.context.activeItemId.value = this.id
}
deactivate() {
if (this.isActive.value) {
this.menu.context.activeItemId.value = undefined
}
}
}

View File

@@ -0,0 +1,27 @@
import { uniqueId } from '@core/utils/unique-id.util'
import { useEventListener } from '@vueuse/core'
import { type Ref, ref } from 'vue'
export class MenuContext {
id: string
activeItemId: Ref<symbol | undefined> = ref()
constructor() {
this.id = uniqueId()
useEventListener(window, 'click', (event: PointerEvent) => {
if (this.hasSameController(event)) {
return
}
this.activeItemId.value = undefined
})
}
hasSameController(event: PointerEvent) {
return Array.from(window.document.querySelectorAll(`[data-menu-id="${this.id}"]`)).some(
el => el === event.target || event.composedPath().includes(el)
)
}
}

View File

@@ -0,0 +1,10 @@
export * from './base'
export * from './action'
export * from './context'
export * from './link'
export * from './menu'
export * from './router-link'
export * from './structure'
export * from './toggle'
export * from './toggle-target'
export * from './toggle-trigger'

View File

@@ -0,0 +1,15 @@
import type { Job } from '@core/packages/job'
import { action } from '@core/packages/menu/action'
import type { MenuLike } from '@core/packages/menu/menu'
import { parseConfigHolder } from '@core/packages/menu/structure'
export function job(job: Job<any>) {
return action(() => job.run(), {
busy: job.isRunning,
disabled: job.errorMessage,
})
}
export function useMenuJob(config: { job: Job<any>; parent: MenuLike }) {
return parseConfigHolder(config.parent, job(config.job))
}

View File

@@ -0,0 +1,56 @@
import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
import { computed, type MaybeRefOrGetter, reactive, toValue } from 'vue'
export interface MenuLinkConfig {
href: MaybeRefOrGetter<string>
rel?: MaybeRefOrGetter<string>
target?: MaybeRefOrGetter<string>
}
export class MenuLinkConfigHolder {
constructor(public config: MenuLinkConfig) {}
}
export interface MenuLinkProps {
as: 'a'
href: string
rel: string
target: string
onClick: () => void
onMouseenter: () => void
'data-menu-id': string
}
export class MenuLink extends BaseItem {
constructor(
public menu: Menu,
public config: MenuLinkConfig
) {
super(menu)
}
get props(): MenuLinkProps {
return reactive({
as: 'a',
onMouseenter: () => this.activate(),
onClick: () => this.deactivate(),
href: computed(() => toValue(this.config.href)),
rel: computed(() => toValue(this.config.rel) ?? 'noreferrer noopener'),
target: computed(() => toValue(this.config.target) ?? '_blank'),
'data-menu-id': this.menu.context.id,
})
}
}
export function link(href: MaybeRefOrGetter<string>, config: Omit<MenuLinkConfig, 'href'> = {}) {
return new MenuLinkConfigHolder({
...config,
href,
})
}
export function useMenuLink(config: MenuLinkConfig & { parent: MenuLike }) {
const { parent, href, ...configRest } = config
return parseConfigHolder(parent, link(href, configRest))
}

View File

@@ -0,0 +1,50 @@
import {
MenuAction,
MenuContext,
MenuLink,
MenuRouterLink,
type MenuStructure,
MenuToggleTrigger,
type ParseStructure,
parseStructure,
} from '@core/packages/menu'
import { extendRef } from '@vueuse/core'
import { computed, type ComputedRef, type MaybeRefOrGetter, shallowReactive, type ShallowReactive } from 'vue'
export const MENU_SYMBOL = Symbol('Submenu')
export type WithMenu = { [MENU_SYMBOL]: Menu }
export type MenuLike = Menu | WithMenu
type MenuItem = MenuAction | MenuLink | MenuRouterLink | MenuToggleTrigger
export class Menu {
items: ShallowReactive<Map<symbol, MenuItem>> = shallowReactive(new Map())
isActive = computed(() => Array.from(this.items.values()).some(item => item.isActive.value))
constructor(public context: MenuContext) {}
addItem(item: MenuItem) {
this.items.set(item.id, item)
}
}
export function useMenu<const TStructure extends MenuStructure>(
structure: MaybeRefOrGetter<TStructure>
): ComputedRef<ParseStructure<TStructure> & WithMenu> & WithMenu {
const context = new MenuContext()
const menu = new Menu(context)
return extendRef(
computed(() => {
return {
...parseStructure(menu, structure),
[MENU_SYMBOL]: menu,
}
}),
{ [MENU_SYMBOL]: menu }
)
}

View File

@@ -0,0 +1,51 @@
import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
import { computed, markRaw, type MaybeRefOrGetter, reactive, toValue } from 'vue'
import { type RouteLocationRaw, RouterLink } from 'vue-router'
export interface MenuRouterLinkConfig {
to: MaybeRefOrGetter<RouteLocationRaw>
}
export class MenuRouterLinkConfigHolder {
constructor(public config: MenuRouterLinkConfig) {}
}
export interface MenuRouterLinkProps {
as: typeof RouterLink
to: RouteLocationRaw
onClick: () => void
onMouseenter: () => void
'data-menu-id': string
}
export class MenuRouterLink extends BaseItem {
constructor(
public menu: Menu,
public config: MenuRouterLinkConfig
) {
super(menu)
}
get props(): MenuRouterLinkProps {
return reactive({
as: markRaw(RouterLink),
onMouseenter: () => this.activate(),
onClick: () => this.deactivate(),
to: computed(() => toValue(this.config.to)),
'data-menu-id': this.menu.context.id,
})
}
}
export function routerLink(to: MaybeRefOrGetter<RouteLocationRaw>, config: Omit<MenuRouterLinkConfig, 'to'> = {}) {
return new MenuRouterLinkConfigHolder({
...config,
to,
})
}
export function useMenuRouterLink(config: MenuRouterLinkConfig & { parent: MenuLike }) {
const { parent, to, ...configRest } = config
return parseConfigHolder(parent, routerLink(to, configRest))
}

View File

@@ -0,0 +1,88 @@
import {
Menu,
MENU_SYMBOL,
MenuAction,
type MenuActionProps,
MenuActionConfigHolder,
type MenuLike,
MenuLink,
type MenuLinkProps,
MenuLinkConfigHolder,
MenuRouterLink,
type MenuRouterLinkProps,
MenuRouterLinkConfigHolder,
type MenuToggleProps,
MenuToggleConfigHolder,
MenuToggleTarget,
MenuToggleTrigger,
} from '@core/packages/menu'
import { type MaybeRefOrGetter, toValue } from 'vue'
export type MenuStructure = {
[K: string]: ConfigHolder
}
export type ConfigHolder =
| MenuActionConfigHolder
| MenuLinkConfigHolder
| MenuRouterLinkConfigHolder
| MenuToggleConfigHolder<any>
export type ParseConfigHolder<TConfigHolder extends ConfigHolder> = TConfigHolder extends MenuActionConfigHolder
? MenuActionProps
: TConfigHolder extends MenuLinkConfigHolder
? MenuLinkProps
: TConfigHolder extends MenuRouterLinkConfigHolder
? MenuRouterLinkProps
: TConfigHolder extends MenuToggleConfigHolder<infer TItems>
? MenuToggleProps<TItems>
: never
export type ParseStructure<TStructure extends MenuStructure> = {
[K in keyof TStructure]: ParseConfigHolder<TStructure[K]>
}
export function parseConfigHolder<TConfigHolder extends ConfigHolder>(
menuLike: MenuLike,
configHolder: TConfigHolder
): ParseConfigHolder<TConfigHolder> {
const menu = menuLike instanceof Menu ? menuLike : menuLike[MENU_SYMBOL]
if (configHolder instanceof MenuActionConfigHolder) {
return new MenuAction(menu, configHolder.config).props as ParseConfigHolder<TConfigHolder>
}
if (configHolder instanceof MenuLinkConfigHolder) {
return new MenuLink(menu, configHolder.config).props as ParseConfigHolder<TConfigHolder>
}
if (configHolder instanceof MenuRouterLinkConfigHolder) {
return new MenuRouterLink(menu, configHolder.config).props as ParseConfigHolder<TConfigHolder>
}
if (configHolder instanceof MenuToggleConfigHolder) {
const trigger = new MenuToggleTrigger(menu, configHolder.config)
const target = new MenuToggleTarget(trigger, configHolder.config)
return {
$trigger: trigger.props,
$target: target.props,
$isOpen: trigger.isOpen,
[MENU_SYMBOL]: trigger.subMenu,
...parseStructure(trigger.subMenu, configHolder.config.items),
} as ParseConfigHolder<TConfigHolder>
}
throw new Error('Unsupported config')
}
export function parseStructure<TStructure extends MenuStructure>(
menu: Menu,
structure: MaybeRefOrGetter<TStructure>
): ParseStructure<TStructure> {
return Object.fromEntries(
Object.entries(toValue(structure)).map(([key, configHolder]) => {
return [key, parseConfigHolder(menu, configHolder)]
})
) as ParseStructure<TStructure>
}

View File

@@ -0,0 +1,59 @@
import type { MenuToggleTrigger } from '@core/packages/menu'
import { autoUpdate, flip, type Placement, shift, useFloating, type UseFloatingReturn } from '@floating-ui/vue'
import { unrefElement } from '@vueuse/core'
import { computed, reactive, ref, type Ref, toValue, type UnwrapRef } from 'vue'
export interface MenuToggleTargetConfig {
placement?: Placement
}
export interface MenuToggleTargetProps {
ref: (el: any) => void
style: {
display: string | undefined
zIndex: number
} & UnwrapRef<UseFloatingReturn['floatingStyles']>
'data-menu-id': string
}
export class MenuToggleTarget {
element: Ref<HTMLElement | null> = ref(null)
styles: UseFloatingReturn['floatingStyles']
constructor(
public trigger: MenuToggleTrigger,
public config: MenuToggleTargetConfig
) {
const { floatingStyles } = useFloating(trigger.element, this.element, {
placement: computed(() => toValue(config.placement) ?? 'bottom-start'),
open: trigger.isOpen,
whileElementsMounted: autoUpdate,
middleware: [shift(), flip()],
})
this.styles = floatingStyles
}
get props(): MenuToggleTargetProps {
return reactive({
as: 'button',
type: 'button',
ref: (element: HTMLElement | null | undefined) => {
const newElement = unrefElement(element)
if (newElement !== this.element.value) {
this.element.value = newElement ?? null
}
},
style: computed(() => {
return {
display: this.trigger.isOpen.value ? undefined : 'none',
zIndex: 9999,
...this.styles.value,
}
}),
'data-menu-id': this.trigger.menu.context.id,
})
}
}

View File

@@ -0,0 +1,72 @@
import { BaseItem, Menu } from '@core/packages/menu'
import { unrefElement, whenever } from '@vueuse/core'
import { computed, type ComputedRef, type MaybeRefOrGetter, reactive, ref, type Ref, toValue } from 'vue'
export interface MenuToggleTriggerConfig {
behavior?: MaybeRefOrGetter<'click' | 'mouseenter'>
}
export interface MenuToggleTriggerProps {
ref: (el: any) => void
as: 'button'
type: 'button'
submenu: true
onClick: () => void
onMouseenter: () => void
selected: boolean
'data-menu-id': string
}
export class MenuToggleTrigger extends BaseItem {
element: Ref<HTMLElement | null> = ref(null)
subMenu: Menu
isSelfOpen = ref(false)
isOpen = computed(() => this.isSelfOpen.value || this.subMenu.isActive.value)
constructor(
public menu: Menu,
public config: MenuToggleTriggerConfig
) {
super(menu)
this.subMenu = new Menu(menu.context)
whenever(
() => !this.isActive.value && !this.subMenu.isActive.value,
() => {
this.isSelfOpen.value = false
}
)
}
get isActive(): ComputedRef<boolean> {
return computed(() => super.isActive.value || this.subMenu?.isActive.value)
}
get props(): MenuToggleTriggerProps {
return reactive({
ref: (element: HTMLElement | null | undefined) => {
const newElement = unrefElement(element)
if (newElement !== this.element.value) {
this.element.value = newElement ?? null
}
},
as: 'button',
type: 'button',
submenu: true,
onClick: () => (this.isSelfOpen.value = !this.isOpen.value),
onMouseenter: () => {
this.activate()
if (toValue(this.config.behavior) === 'mouseenter') {
this.isSelfOpen.value = true
}
},
selected: computed(() => this.isOpen.value),
'data-menu-id': this.menu.context.id,
})
}
}

View File

@@ -0,0 +1,43 @@
import {
Menu,
MenuContext,
type MenuLike,
type MenuStructure,
type MenuToggleTargetConfig,
type MenuToggleTargetProps,
type MenuToggleTriggerConfig,
type MenuToggleTriggerProps,
parseConfigHolder,
type ParseStructure,
type WithMenu,
} from '@core/packages/menu'
import { computed, type ComputedRef, type MaybeRefOrGetter } from 'vue'
export type MenuToggleConfig<TStructure extends MenuStructure> = MenuToggleTriggerConfig &
MenuToggleTargetConfig & {
items?: MaybeRefOrGetter<TStructure>
}
export class MenuToggleConfigHolder<TStructure extends MenuStructure> {
constructor(public config: MenuToggleConfig<TStructure>) {}
}
export type MenuToggleProps<TStructure extends MenuStructure> = WithMenu &
ParseStructure<TStructure> & {
$trigger: MenuToggleTriggerProps
$target: MenuToggleTargetProps
$isOpen: ComputedRef<boolean>
}
export function toggle<const TStructure extends MenuStructure>(config: MenuToggleConfig<TStructure>) {
return new MenuToggleConfigHolder(config)
}
export function useMenuToggle<const TStructure extends MenuStructure>(
config: MenuToggleConfig<TStructure> & { parent?: MenuLike } = {}
) {
const { parent, ...toggleConfig } = config
const menu = parent ?? new Menu(new MenuContext())
return computed(() => parseConfigHolder(menu, toggle<TStructure>(toggleConfig)))
}

View File

@@ -10,6 +10,7 @@
}
},
"dependencies": {
"@floating-ui/vue": "^1.1.5",
"@fontsource/poppins": "^5.0.14",
"@fortawesome/fontawesome-common-types": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",

View File

@@ -12,6 +12,7 @@
},
"devDependencies": {
"@csstools/postcss-global-data": "^2.1.1",
"@floating-ui/vue": "^1.1.5",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",

View File

@@ -2064,6 +2064,35 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
"@floating-ui/core@^1.6.0":
version "1.6.8"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12"
integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==
dependencies:
"@floating-ui/utils" "^0.2.8"
"@floating-ui/dom@^1.0.0":
version "1.6.12"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556"
integrity sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==
dependencies:
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.8"
"@floating-ui/utils@^0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@floating-ui/vue@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@floating-ui/vue/-/vue-1.1.5.tgz#dcbb5d7a2f9035b0c96a9c30cdf794977d360809"
integrity sha512-ynL1p5Z+woPVSwgMGqeDrx6HrJfGIDzFyESFkyqJKilGW1+h/8yVY29Khn0LaU6wHBRwZ13ntG6reiHWK6jyzw==
dependencies:
"@floating-ui/dom" "^1.0.0"
"@floating-ui/utils" "^0.2.8"
vue-demi ">=0.13.0"
"@fontsource/poppins@^5.0.14":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@fontsource/poppins/-/poppins-5.1.0.tgz#919205f53d99ec4751a945057ef525f06681df8a"
@@ -19529,7 +19558,7 @@ vscode-uri@^3.0.8:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
vue-demi@>=0.14.8, vue-demi@^0.14.10:
vue-demi@>=0.13.0, vue-demi@>=0.14.8, vue-demi@^0.14.10:
version "0.14.10"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==