@xen-orchestra/web-core 0.7.0 → 0.8.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.
Files changed (36) hide show
  1. package/lib/assets/css/typography/_style.pcss +1 -0
  2. package/lib/components/layout/VtsLayoutSidebar.vue +1 -1
  3. package/lib/components/state-hero/VtsErrorNoDataHero.vue +11 -0
  4. package/lib/components/state-hero/VtsNoSelectionHero.vue +13 -0
  5. package/lib/components/state-hero/VtsObjectNotFoundHero.vue +3 -2
  6. package/lib/components/state-hero/VtsStateHero.vue +30 -2
  7. package/lib/components/ui/donut-chart/UiDonutChart.vue +2 -2
  8. package/lib/components/ui/dropdown-button/UiDropdownButton.vue +81 -0
  9. package/lib/components/ui/link/UiLink.vue +75 -0
  10. package/lib/components/ui/tag/UiTagsList.vue +14 -0
  11. package/lib/composables/link-component.composable.ts +53 -0
  12. package/lib/locales/cs.json +1 -0
  13. package/lib/locales/de.json +2 -0
  14. package/lib/locales/en.json +4 -0
  15. package/lib/locales/fa.json +2 -0
  16. package/lib/locales/fr.json +4 -0
  17. package/lib/packages/job/README.md +130 -0
  18. package/lib/packages/job/define-job-arg.ts +12 -0
  19. package/lib/packages/job/define-job.ts +130 -0
  20. package/lib/packages/job/index.ts +4 -0
  21. package/lib/packages/job/job-error.ts +14 -0
  22. package/lib/packages/job/use-job-store.ts +44 -0
  23. package/lib/packages/menu/README.md +194 -0
  24. package/lib/packages/menu/action.ts +101 -0
  25. package/lib/packages/menu/base.ts +26 -0
  26. package/lib/packages/menu/context.ts +27 -0
  27. package/lib/packages/menu/index.ts +10 -0
  28. package/lib/packages/menu/job.ts +15 -0
  29. package/lib/packages/menu/link.ts +56 -0
  30. package/lib/packages/menu/menu.ts +50 -0
  31. package/lib/packages/menu/router-link.ts +51 -0
  32. package/lib/packages/menu/structure.ts +88 -0
  33. package/lib/packages/menu/toggle-target.ts +59 -0
  34. package/lib/packages/menu/toggle-trigger.ts +72 -0
  35. package/lib/packages/menu/toggle.ts +43 -0
  36. package/package.json +2 -1
@@ -0,0 +1,130 @@
1
+ import type { JobArg, JobIdentity } from '@core/packages/job/define-job-arg'
2
+ import { JobError, JobRunningError } from '@core/packages/job/job-error'
3
+ import { useJobStore } from '@core/packages/job/use-job-store'
4
+ import type { MaybeArray } from '@core/types/utility.type'
5
+ import { toArray as convertToArray } from '@core/utils/to-array.utils'
6
+ import { computed, type ComputedRef, type MaybeRefOrGetter, toValue } from 'vue'
7
+
8
+ export type JobRunArgs<TJobArgs> = TJobArgs extends [infer TJobArg, ...infer TRest]
9
+ ? TJobArg extends JobArg<infer TType, infer TToArray>
10
+ ? [TToArray extends true ? TType[] : TType, ...JobRunArgs<TRest>]
11
+ : []
12
+ : []
13
+
14
+ export type JobValidateArgs<TJobArgs> = TJobArgs extends [infer TJobArg, ...infer TRest]
15
+ ? TJobArg extends JobArg<infer TType, infer TToArray>
16
+ ? [TToArray extends true ? TType[] : TType | undefined, ...JobValidateArgs<TRest>]
17
+ : []
18
+ : []
19
+
20
+ export type JobUseArgs<TJobArgs> = TJobArgs extends [infer TJobArg, ...infer TRest]
21
+ ? TJobArg extends JobArg<infer TType, infer TToArray>
22
+ ? [
23
+ TToArray extends true ? MaybeRefOrGetter<MaybeArray<TType | undefined>> : MaybeRefOrGetter<TType | undefined>,
24
+ ...JobUseArgs<TRest>,
25
+ ]
26
+ : []
27
+ : []
28
+
29
+ export type JobSetup<TJobArgs extends JobArg[], TRunResult> = () => {
30
+ run: (...args: JobRunArgs<TJobArgs>) => TRunResult
31
+ validate: (isRunning: boolean, ...args: JobValidateArgs<TJobArgs>) => void
32
+ }
33
+
34
+ export type Job<TRunResult> = {
35
+ run: () => Promise<TRunResult>
36
+ canRun: ComputedRef<boolean>
37
+ error: ComputedRef<JobError | undefined>
38
+ errorMessage: ComputedRef<string | undefined>
39
+ isRunning: ComputedRef<boolean>
40
+ }
41
+
42
+ export function defineJob<const TJobArgs extends JobArg[], TRunResult>(
43
+ name: string,
44
+ jobArgs: TJobArgs,
45
+ setup: JobSetup<TJobArgs, TRunResult>
46
+ ) {
47
+ const jobId = Symbol('jobId')
48
+
49
+ return (...useArgs: JobUseArgs<TJobArgs>) => {
50
+ const config = setup()
51
+ const jobStore = useJobStore()
52
+
53
+ const args = computed(() =>
54
+ useArgs.map((useArg, index) => {
55
+ if (jobArgs[index].toArray) {
56
+ return convertToArray(toValue(useArg))
57
+ }
58
+
59
+ return toValue(useArg)
60
+ })
61
+ )
62
+
63
+ const identities = computed<JobIdentity[][]>(() =>
64
+ args.value.map<JobIdentity[]>((arg, index) => {
65
+ const { toArray, identify } = jobArgs[index]
66
+
67
+ if (identify === false) {
68
+ return [undefined]
69
+ }
70
+
71
+ if (identify === true) {
72
+ return convertToArray(arg)
73
+ }
74
+
75
+ if (toArray) {
76
+ return arg.map(identify)
77
+ }
78
+
79
+ return [identify(arg)]
80
+ })
81
+ )
82
+
83
+ function validate() {
84
+ config.validate(jobStore.isRunning(jobId, identities.value), ...(args.value as JobValidateArgs<TJobArgs>))
85
+ }
86
+
87
+ const error = computed(() => {
88
+ try {
89
+ validate()
90
+
91
+ return undefined
92
+ } catch (error) {
93
+ if (error instanceof JobError) {
94
+ error.args = args.value
95
+ error.jobName = name
96
+
97
+ return error
98
+ }
99
+
100
+ return new JobError('Unknown job error', name, args.value, error)
101
+ }
102
+ })
103
+
104
+ const isRunning = computed(() => error.value instanceof JobRunningError)
105
+
106
+ const errorMessage = computed(() => error.value?.message)
107
+
108
+ const canRun = computed(() => error.value === undefined)
109
+
110
+ async function run() {
111
+ validate()
112
+
113
+ const runId = jobStore.start(jobId, identities.value)
114
+
115
+ try {
116
+ return await config.run(...(args.value as JobRunArgs<TJobArgs>))
117
+ } finally {
118
+ jobStore.stop(runId)
119
+ }
120
+ }
121
+
122
+ return {
123
+ run,
124
+ canRun,
125
+ error,
126
+ errorMessage,
127
+ isRunning,
128
+ } satisfies Job<TRunResult>
129
+ }
130
+ }
@@ -0,0 +1,4 @@
1
+ export * from './define-job'
2
+ export * from './define-job-arg'
3
+ export * from './job-error'
4
+ export * from './use-job-store'
@@ -0,0 +1,14 @@
1
+ export class JobError extends Error {
2
+ jobName: string | undefined
3
+ args: any[] | undefined
4
+ previousError: any
5
+
6
+ constructor(message: string, jobName?: string, args?: any[], previousError?: any) {
7
+ super(message)
8
+ this.args = args
9
+ this.jobName = jobName
10
+ this.previousError = previousError
11
+ }
12
+ }
13
+
14
+ export class JobRunningError extends JobError {}
@@ -0,0 +1,44 @@
1
+ import type { JobIdentity } from '@core/packages/job/define-job-arg'
2
+ import { toArray } from '@core/utils/to-array.utils'
3
+ import { defineStore } from 'pinia'
4
+ import { shallowReactive } from 'vue'
5
+
6
+ export const useJobStore = defineStore('job', () => {
7
+ const runningJobs = shallowReactive(new Map<symbol, { jobId: symbol; identities: JobIdentity[][] }>())
8
+
9
+ function start(id: symbol, identities: JobIdentity[][]) {
10
+ const runId = Symbol(`Job run ID`)
11
+
12
+ runningJobs.set(runId, {
13
+ jobId: id,
14
+ identities,
15
+ })
16
+
17
+ return runId
18
+ }
19
+
20
+ function stop(runId: symbol) {
21
+ runningJobs.delete(runId)
22
+ }
23
+
24
+ function isRunning(id: symbol, identitiesToCheck: JobIdentity[][]) {
25
+ return Array.from(runningJobs.values()).some(runningJob => {
26
+ if (runningJob.jobId !== id) {
27
+ return false
28
+ }
29
+
30
+ return (
31
+ identitiesToCheck.length === 0 ||
32
+ identitiesToCheck.every((identityToCheck, index) =>
33
+ toArray(identityToCheck).some(identity => runningJob.identities[index]?.includes(identity))
34
+ )
35
+ )
36
+ })
37
+ }
38
+
39
+ return {
40
+ start,
41
+ stop,
42
+ isRunning,
43
+ }
44
+ })
@@ -0,0 +1,194 @@
1
+ # Menu System
2
+
3
+ A menu system for Vue applications supporting props binding for actions, links, router links, and nested submenus.
4
+
5
+ ## Basic Usage
6
+
7
+ ```vue
8
+ <template>
9
+ <MenuList>
10
+ <li><MenuTrigger v-bind="menu.save">Save</MenuTrigger></li>
11
+ <li><MenuTrigger v-bind="menu.doc">Documentation</MenuTrigger></li>
12
+ <li><MenuTrigger v-bind="menu.profile">Profile</MenuTrigger></li>
13
+ <li>
14
+ <MenuTrigger v-bind="menu.more.$trigger">More...</MenuTrigger>
15
+ <MenuList v-bind="menu.more.$target">
16
+ <MenuTrigger v-bind="menu.more.settings">Settings</MenuTrigger>
17
+ <MenuTrigger v-bind="menu.more.logout">Logout</MenuTrigger>
18
+ </MenuList>
19
+ </li>
20
+ </MenuList>
21
+ </template>
22
+
23
+ <script lang="ts" setup>
24
+ import { action, link, routerLink, toggle, useMenu } from '@core/packages/menu'
25
+
26
+ const menu = useMenu({
27
+ save: action(() => console.log('Saving...')),
28
+ doc: link('https://docs.example.com'),
29
+ profile: routerLink({ name: 'profile' }),
30
+ more: toggle({
31
+ settings: action(() => console.log('Settings clicked')),
32
+ logout: action(() => console.log('Logout clicked')),
33
+ }),
34
+ })
35
+ </script>
36
+ ```
37
+
38
+ ## Core Composables
39
+
40
+ ### useMenu
41
+
42
+ Create a root menu with multiple items and/or submenus:
43
+
44
+ ```ts
45
+ const fileMenu = useMenu({
46
+ // Menu structure
47
+ edit: action(() => handleEdit()),
48
+ save: action(() => handleSave(), {
49
+ disabled: () => !canSave.value,
50
+ busy: isSaving,
51
+ }),
52
+ })
53
+ ```
54
+
55
+ ### useMenuAction
56
+
57
+ Create an action button. It must be attached to a menu.
58
+
59
+ Mostly useful when splitting a menu into subcomponents.
60
+
61
+ ```ts
62
+ const props = defineProps<{
63
+ menu: MenuLike
64
+ }>()
65
+
66
+ const action = useMenuAction(menu, () => handleClick(), {
67
+ disabled: isDisabled,
68
+ busy: isLoading,
69
+ })
70
+ ```
71
+
72
+ ### useMenuToggle
73
+
74
+ Create a toggle menu (dropdown) with nested items.
75
+
76
+ You can pass a `parent: MenuLike` as option to attach the toggle to a parent Menu.
77
+
78
+ Skip this option to create a root menu toggle.
79
+
80
+ ```ts
81
+ const dropdown = useMenuToggle(
82
+ {
83
+ behavior: 'click', // or 'mouseenter'
84
+ placement: 'bottom-start',
85
+ },
86
+ {
87
+ edit: action(() => handleEdit()),
88
+ delete: action(() => handleDelete()),
89
+ }
90
+ )
91
+ ```
92
+
93
+ ## Component Integration
94
+
95
+ Once you create the menu, you can bind its items as props to your template.
96
+
97
+ For now, these bindings are meant to be used with a Vue component configured with the same props
98
+
99
+ For other component or HTML Element, you can still bind the props manually.
100
+
101
+ `action()` items will generate the following props binding:
102
+
103
+ ```ts
104
+ type Props = {
105
+ as: 'button'
106
+ type: 'button'
107
+ disabled: boolean
108
+ busy: boolean
109
+ tooltip: string | false
110
+ onMouseenter: () => void
111
+ onClick: () => void
112
+ 'data-menu-id': string
113
+ }
114
+ ```
115
+
116
+ `link()` items will generate the following props binding:
117
+
118
+ ```ts
119
+ type Props = {
120
+ as: 'a'
121
+ href: string
122
+ rel: 'noreferrer noopener'
123
+ target: '_blank'
124
+ onMouseenter: () => void
125
+ onClick: () => void
126
+ 'data-menu-id': string
127
+ }
128
+ ```
129
+
130
+ `routerLink()` items will generate the following props binding:
131
+
132
+ ```ts
133
+ type Props = {
134
+ as: RouterLink
135
+ to: RouteLocationRaw
136
+ onMouseenter: () => void
137
+ onClick: () => void
138
+ 'data-menu-id': string
139
+ }
140
+ ```
141
+
142
+ `toggle()` items will generate an object containing `$trigger`, `$target` and `$isOpen` (`ComputedRef<boolean>`) properties with the following props binding:
143
+
144
+ ```ts
145
+ // $trigger
146
+ type Props = {
147
+ as: 'button'
148
+ type: 'button'
149
+ submenu: true
150
+ ref: (el: any) => void
151
+ active: boolean
152
+ onClick: () => void
153
+ onMouseenter: () => void
154
+ 'data-menu-id': string
155
+ }
156
+
157
+ // $target
158
+ type Props = {
159
+ ref: (el: any) => void
160
+ style: object
161
+ 'data-menu-id': string
162
+ }
163
+ ```
164
+
165
+ ### Example
166
+
167
+ ```vue
168
+ <template>
169
+ <MenuTrigger v-bind="menu.save">Save</MenuTrigger>
170
+ <MenuTrigger v-bind="menu.docs">Documentation</MenuTrigger>
171
+ <MenuTrigger v-bind="menu.profile">Profile</MenuTrigger>
172
+
173
+ <!-- Toggle/Dropdown menu -->
174
+ <MenuTrigger v-bind="menu.more.$trigger">More</MenuTrigger>
175
+ <div v-bind="menu.more.$target">
176
+ <MenuTrigger v-bind="menu.more.settings">Settings</MenuTrigger>
177
+ <MenuTrigger v-bind="menu.more.logout">Logout</MenuTrigger>
178
+ </div>
179
+ </template>
180
+ ```
181
+
182
+ If you want to bind props to an HTML element or a Vue component not supporting all the props,
183
+ you can use VueUse's `objectOmit` for example or bind the props completely manually.
184
+
185
+ ```vue
186
+ <script>
187
+ import { objectOmit as omit } from '@vueuse/shared'
188
+ </script>
189
+
190
+ <template>
191
+ <MyCustomElement v-bind="omit(menu.save, ['as', 'type'])">Save</MyCustomElement>
192
+ <button v-bind="omit(menu.save, ['as', busy, 'tooltip'])" :class="{ busy: menu.save.busy }">Save</button>
193
+ </template>
194
+ ```
@@ -0,0 +1,101 @@
1
+ import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
+ import { computed, type MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
3
+
4
+ export interface MenuActionConfig {
5
+ handler: () => any
6
+ disabled?: MaybeRefOrGetter<boolean | string | undefined>
7
+ busy?: MaybeRefOrGetter<boolean | string | undefined>
8
+ }
9
+
10
+ export class MenuActionConfigHolder {
11
+ constructor(public config: MenuActionConfig) {}
12
+ }
13
+
14
+ export interface MenuActionProps {
15
+ as: 'button'
16
+ type: 'button'
17
+ disabled: boolean
18
+ busy: boolean
19
+ tooltip: string | false
20
+ onClick: () => void
21
+ onMouseenter: () => void
22
+ 'data-menu-id': string
23
+ }
24
+
25
+ export class MenuAction extends BaseItem {
26
+ isRunning = ref(false)
27
+
28
+ constructor(
29
+ public menu: Menu,
30
+ public config: MenuActionConfig
31
+ ) {
32
+ super(menu)
33
+ }
34
+
35
+ get busyConfig() {
36
+ return computed(() => toValue(this.config.busy) ?? false)
37
+ }
38
+
39
+ get isBusy() {
40
+ return computed(() => this.isRunning.value || this.busyConfig.value !== false)
41
+ }
42
+
43
+ get busyReason() {
44
+ return computed(() => (typeof this.busyConfig.value === 'string' ? this.busyConfig.value : undefined))
45
+ }
46
+
47
+ get disabledConfig() {
48
+ return computed(() => toValue(this.config.disabled) ?? false)
49
+ }
50
+
51
+ get isDisabled() {
52
+ return computed(() => this.isBusy.value || this.disabledConfig.value !== false)
53
+ }
54
+
55
+ get disabledReason() {
56
+ return computed(() => (typeof this.disabledConfig.value === 'string' ? this.disabledConfig.value : undefined))
57
+ }
58
+
59
+ get tooltip() {
60
+ return computed(() => this.disabledReason.value ?? this.busyReason.value ?? false)
61
+ }
62
+
63
+ get props(): MenuActionProps {
64
+ return reactive({
65
+ as: 'button',
66
+ type: 'button',
67
+ onClick: async () => {
68
+ if (this.isDisabled.value) {
69
+ return
70
+ }
71
+
72
+ this.isRunning.value = true
73
+
74
+ try {
75
+ await this.config.handler()
76
+ this.deactivate()
77
+ } finally {
78
+ this.isRunning.value = false
79
+ }
80
+ },
81
+ onMouseenter: () => this.activate(),
82
+ disabled: this.isDisabled,
83
+ busy: this.isBusy,
84
+ tooltip: this.tooltip,
85
+ 'data-menu-id': this.menu.context.id,
86
+ })
87
+ }
88
+ }
89
+
90
+ export function action(handler: () => void, config: Omit<MenuActionConfig, 'handler'> = {}): MenuActionConfigHolder {
91
+ return new MenuActionConfigHolder({
92
+ ...config,
93
+ handler,
94
+ })
95
+ }
96
+
97
+ export function useMenuAction(config: MenuActionConfig & { parent: MenuLike }) {
98
+ const { parent, handler, ...configRest } = config
99
+
100
+ return parseConfigHolder(parent, action(handler, configRest))
101
+ }
@@ -0,0 +1,26 @@
1
+ import type { MenuAction, MenuLink, MenuRouterLink, MenuToggleTrigger, Menu } from '@core/packages/menu'
2
+ import { computed } from 'vue'
3
+
4
+ export type MenuItem = MenuAction | MenuLink | MenuRouterLink | MenuToggleTrigger
5
+
6
+ export abstract class BaseItem {
7
+ id = Symbol('Menu Item')
8
+
9
+ protected constructor(public menu: Menu) {
10
+ menu.addItem(this as any)
11
+ }
12
+
13
+ get isActive() {
14
+ return computed(() => this.menu.context.activeItemId.value === this.id)
15
+ }
16
+
17
+ activate() {
18
+ this.menu.context.activeItemId.value = this.id
19
+ }
20
+
21
+ deactivate() {
22
+ if (this.isActive.value) {
23
+ this.menu.context.activeItemId.value = undefined
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,27 @@
1
+ import { uniqueId } from '@core/utils/unique-id.util'
2
+ import { useEventListener } from '@vueuse/core'
3
+ import { type Ref, ref } from 'vue'
4
+
5
+ export class MenuContext {
6
+ id: string
7
+
8
+ activeItemId: Ref<symbol | undefined> = ref()
9
+
10
+ constructor() {
11
+ this.id = uniqueId()
12
+
13
+ useEventListener(window, 'click', (event: PointerEvent) => {
14
+ if (this.hasSameController(event)) {
15
+ return
16
+ }
17
+
18
+ this.activeItemId.value = undefined
19
+ })
20
+ }
21
+
22
+ hasSameController(event: PointerEvent) {
23
+ return Array.from(window.document.querySelectorAll(`[data-menu-id="${this.id}"]`)).some(
24
+ el => el === event.target || event.composedPath().includes(el)
25
+ )
26
+ }
27
+ }
@@ -0,0 +1,10 @@
1
+ export * from './base'
2
+ export * from './action'
3
+ export * from './context'
4
+ export * from './link'
5
+ export * from './menu'
6
+ export * from './router-link'
7
+ export * from './structure'
8
+ export * from './toggle'
9
+ export * from './toggle-target'
10
+ export * from './toggle-trigger'
@@ -0,0 +1,15 @@
1
+ import type { Job } from '@core/packages/job'
2
+ import { action } from '@core/packages/menu/action'
3
+ import type { MenuLike } from '@core/packages/menu/menu'
4
+ import { parseConfigHolder } from '@core/packages/menu/structure'
5
+
6
+ export function job(job: Job<any>) {
7
+ return action(() => job.run(), {
8
+ busy: job.isRunning,
9
+ disabled: job.errorMessage,
10
+ })
11
+ }
12
+
13
+ export function useMenuJob(config: { job: Job<any>; parent: MenuLike }) {
14
+ return parseConfigHolder(config.parent, job(config.job))
15
+ }
@@ -0,0 +1,56 @@
1
+ import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
+ import { computed, type MaybeRefOrGetter, reactive, toValue } from 'vue'
3
+
4
+ export interface MenuLinkConfig {
5
+ href: MaybeRefOrGetter<string>
6
+ rel?: MaybeRefOrGetter<string>
7
+ target?: MaybeRefOrGetter<string>
8
+ }
9
+
10
+ export class MenuLinkConfigHolder {
11
+ constructor(public config: MenuLinkConfig) {}
12
+ }
13
+
14
+ export interface MenuLinkProps {
15
+ as: 'a'
16
+ href: string
17
+ rel: string
18
+ target: string
19
+ onClick: () => void
20
+ onMouseenter: () => void
21
+ 'data-menu-id': string
22
+ }
23
+
24
+ export class MenuLink extends BaseItem {
25
+ constructor(
26
+ public menu: Menu,
27
+ public config: MenuLinkConfig
28
+ ) {
29
+ super(menu)
30
+ }
31
+
32
+ get props(): MenuLinkProps {
33
+ return reactive({
34
+ as: 'a',
35
+ onMouseenter: () => this.activate(),
36
+ onClick: () => this.deactivate(),
37
+ href: computed(() => toValue(this.config.href)),
38
+ rel: computed(() => toValue(this.config.rel) ?? 'noreferrer noopener'),
39
+ target: computed(() => toValue(this.config.target) ?? '_blank'),
40
+ 'data-menu-id': this.menu.context.id,
41
+ })
42
+ }
43
+ }
44
+
45
+ export function link(href: MaybeRefOrGetter<string>, config: Omit<MenuLinkConfig, 'href'> = {}) {
46
+ return new MenuLinkConfigHolder({
47
+ ...config,
48
+ href,
49
+ })
50
+ }
51
+
52
+ export function useMenuLink(config: MenuLinkConfig & { parent: MenuLike }) {
53
+ const { parent, href, ...configRest } = config
54
+
55
+ return parseConfigHolder(parent, link(href, configRest))
56
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ MenuAction,
3
+ MenuContext,
4
+ MenuLink,
5
+ MenuRouterLink,
6
+ type MenuStructure,
7
+ MenuToggleTrigger,
8
+ type ParseStructure,
9
+ parseStructure,
10
+ } from '@core/packages/menu'
11
+ import { extendRef } from '@vueuse/core'
12
+ import { computed, type ComputedRef, type MaybeRefOrGetter, shallowReactive, type ShallowReactive } from 'vue'
13
+
14
+ export const MENU_SYMBOL = Symbol('Submenu')
15
+
16
+ export type WithMenu = { [MENU_SYMBOL]: Menu }
17
+
18
+ export type MenuLike = Menu | WithMenu
19
+
20
+ type MenuItem = MenuAction | MenuLink | MenuRouterLink | MenuToggleTrigger
21
+
22
+ export class Menu {
23
+ items: ShallowReactive<Map<symbol, MenuItem>> = shallowReactive(new Map())
24
+
25
+ isActive = computed(() => Array.from(this.items.values()).some(item => item.isActive.value))
26
+
27
+ constructor(public context: MenuContext) {}
28
+
29
+ addItem(item: MenuItem) {
30
+ this.items.set(item.id, item)
31
+ }
32
+ }
33
+
34
+ export function useMenu<const TStructure extends MenuStructure>(
35
+ structure: MaybeRefOrGetter<TStructure>
36
+ ): ComputedRef<ParseStructure<TStructure> & WithMenu> & WithMenu {
37
+ const context = new MenuContext()
38
+
39
+ const menu = new Menu(context)
40
+
41
+ return extendRef(
42
+ computed(() => {
43
+ return {
44
+ ...parseStructure(menu, structure),
45
+ [MENU_SYMBOL]: menu,
46
+ }
47
+ }),
48
+ { [MENU_SYMBOL]: menu }
49
+ )
50
+ }