@xen-orchestra/web-core 0.0.1

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 (91) hide show
  1. package/lib/assets/css/_colors.pcss +125 -0
  2. package/lib/assets/css/_context.pcss +99 -0
  3. package/lib/assets/css/_fonts.pcss +6 -0
  4. package/lib/assets/css/_reset.pcss +42 -0
  5. package/lib/assets/css/_shadows.pcss +36 -0
  6. package/lib/assets/css/_typography.pcss +6 -0
  7. package/lib/assets/css/base.pcss +91 -0
  8. package/lib/assets/css/typography/_legacy.pcss +123 -0
  9. package/lib/assets/css/typography/_letter-spacing.pcss +27 -0
  10. package/lib/assets/css/typography/_line-height.pcss +19 -0
  11. package/lib/assets/css/typography/_size.pcss +95 -0
  12. package/lib/assets/css/typography/_style.pcss +34 -0
  13. package/lib/assets/css/typography/_weight.pcss +57 -0
  14. package/lib/assets/user.png +0 -0
  15. package/lib/components/PowerStateIcon.vue +46 -0
  16. package/lib/components/StatusPill.vue +32 -0
  17. package/lib/components/UiCounter.vue +89 -0
  18. package/lib/components/UiSpinner.vue +48 -0
  19. package/lib/components/UiTag.vue +97 -0
  20. package/lib/components/button/ButtonGroup.vue +33 -0
  21. package/lib/components/button/ButtonIcon.vue +199 -0
  22. package/lib/components/button/UiButton.vue +207 -0
  23. package/lib/components/chip/ChipIcon.vue +29 -0
  24. package/lib/components/chip/ChipRemoveIcon.vue +13 -0
  25. package/lib/components/chip/UiChip.vue +138 -0
  26. package/lib/components/dropdown/DropdownItem.vue +192 -0
  27. package/lib/components/dropdown/DropdownList.vue +31 -0
  28. package/lib/components/dropdown/DropdownTitle.vue +65 -0
  29. package/lib/components/icon/ComplexIcon.vue +51 -0
  30. package/lib/components/icon/ObjectIcon.vue +243 -0
  31. package/lib/components/icon/UiIcon.vue +47 -0
  32. package/lib/components/icon/VmIcon.vue +30 -0
  33. package/lib/components/layout/LayoutSidebar.vue +100 -0
  34. package/lib/components/menu/MenuItem.vue +82 -0
  35. package/lib/components/menu/MenuList.vue +104 -0
  36. package/lib/components/menu/MenuSeparator.vue +27 -0
  37. package/lib/components/menu/MenuTrigger.vue +52 -0
  38. package/lib/components/tab/TabItem.vue +100 -0
  39. package/lib/components/tab/TabList.vue +32 -0
  40. package/lib/components/tooltip/TooltipItem.vue +80 -0
  41. package/lib/components/tooltip/TooltipList.vue +15 -0
  42. package/lib/components/tree/TreeItem.vue +34 -0
  43. package/lib/components/tree/TreeItemError.vue +13 -0
  44. package/lib/components/tree/TreeItemLabel.vue +128 -0
  45. package/lib/components/tree/TreeLine.vue +51 -0
  46. package/lib/components/tree/TreeList.vue +14 -0
  47. package/lib/components/tree/TreeLoadingItem.vue +64 -0
  48. package/lib/components/user/UserLink.vue +72 -0
  49. package/lib/components/user/UserLogo.vue +57 -0
  50. package/lib/composables/context.composable.ts +34 -0
  51. package/lib/composables/tree/branch-definition.ts +17 -0
  52. package/lib/composables/tree/branch.ts +143 -0
  53. package/lib/composables/tree/build-nodes.ts +20 -0
  54. package/lib/composables/tree/define-branch.ts +23 -0
  55. package/lib/composables/tree/define-leaf.ts +16 -0
  56. package/lib/composables/tree/define-tree.ts +65 -0
  57. package/lib/composables/tree/leaf-definition.ts +8 -0
  58. package/lib/composables/tree/leaf.ts +34 -0
  59. package/lib/composables/tree/tree-node-base.ts +103 -0
  60. package/lib/composables/tree/tree-node-definition-base.ts +12 -0
  61. package/lib/composables/tree/types.ts +92 -0
  62. package/lib/composables/tree-filter.composable.ts +12 -0
  63. package/lib/composables/tree.composable.ts +85 -0
  64. package/lib/context.ts +10 -0
  65. package/lib/directives/tooltip.directive.md +117 -0
  66. package/lib/directives/tooltip.directive.ts +52 -0
  67. package/lib/i18n.ts +158 -0
  68. package/lib/layouts/CoreLayout.vue +182 -0
  69. package/lib/locales/de.json +6 -0
  70. package/lib/locales/en.json +15 -0
  71. package/lib/locales/fr.json +15 -0
  72. package/lib/stores/panel.store.ts +12 -0
  73. package/lib/stores/sidebar.store.ts +63 -0
  74. package/lib/stores/tooltip.store.ts +74 -0
  75. package/lib/stores/ui.store.ts +34 -0
  76. package/lib/types/button.type.ts +3 -0
  77. package/lib/types/color.type.ts +5 -0
  78. package/lib/types/object-icon.type.ts +43 -0
  79. package/lib/types/power-state.type.ts +1 -0
  80. package/lib/types/size.type.ts +3 -0
  81. package/lib/types/subscribable-store.type.ts +21 -0
  82. package/lib/types/utility.type.ts +1 -0
  83. package/lib/utils/create-subscribable-store-context.util.ts +66 -0
  84. package/lib/utils/has-ellipsis.util.ts +11 -0
  85. package/lib/utils/if-else.utils.ts +27 -0
  86. package/lib/utils/injection-keys.util.ts +17 -0
  87. package/lib/utils/sort-by-name-label.util.ts +6 -0
  88. package/lib/utils/to-array.utils.ts +9 -0
  89. package/lib/utils/unique-id.util.ts +8 -0
  90. package/package.json +45 -0
  91. package/tsconfig.json +12 -0
@@ -0,0 +1,51 @@
1
+ <!-- v1.1 -->
2
+ <template>
3
+ <FontAwesomeLayers :class="size" class="complex-icon">
4
+ <slot />
5
+ </FontAwesomeLayers>
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
10
+
11
+ withDefaults(
12
+ defineProps<{
13
+ size?: 'small' | 'medium' | 'large'
14
+ }>(),
15
+ { size: 'medium' }
16
+ )
17
+ </script>
18
+
19
+ <style lang="postcss" scoped>
20
+ /* SIZE VARIANTS */
21
+ .complex-icon {
22
+ &.small {
23
+ --font-size: 1.2rem;
24
+ }
25
+
26
+ &.medium {
27
+ --font-size: 1.6rem;
28
+ }
29
+
30
+ &.large {
31
+ --font-size: 2rem;
32
+ }
33
+ }
34
+
35
+ /* IMPLEMENTATION */
36
+ .complex-icon {
37
+ font-size: var(--font-size);
38
+
39
+ :nth-child(2) {
40
+ font-size: 0.5em;
41
+ transform: translate(100%, 80%);
42
+
43
+ :deep(path) {
44
+ stroke: var(--color-grey-600);
45
+ stroke-width: 100px;
46
+ stroke-linejoin: round;
47
+ paint-order: stroke;
48
+ }
49
+ }
50
+ }
51
+ </style>
@@ -0,0 +1,243 @@
1
+ <!-- VM v1.0 -->
2
+ <!-- Host v1.0 -->
3
+ <!-- SR v1.0 -->
4
+ <!-- Backup Repository v1.0 -->
5
+ <!-- Network v1.0 -->
6
+ <template>
7
+ <FontAwesomeLayers :class="[size, { disabled: stateConfig === undefined }]" class="object-icon">
8
+ <UiIcon :icon="mainIcon" />
9
+ <UiIcon :icon="stateIcon" :style="stateIconStyle" class="state" />
10
+ </FontAwesomeLayers>
11
+ </template>
12
+
13
+ <script generic="TType extends SupportedType" lang="ts" setup>
14
+ import UiIcon from '@core/components/icon/UiIcon.vue'
15
+ import type { ObjectIconConfig, ObjectIconSize, SupportedState, SupportedType } from '@core/types/object-icon.type'
16
+ import {
17
+ faCheckCircle,
18
+ faCircleMinus,
19
+ faCircleXmark,
20
+ faDatabase,
21
+ faDisplay,
22
+ faMoon,
23
+ faNetworkWired,
24
+ faPause,
25
+ faPlay,
26
+ faServer,
27
+ faStop,
28
+ faTriangleExclamation,
29
+ faWarehouse,
30
+ } from '@fortawesome/free-solid-svg-icons'
31
+ import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
32
+ import { computed } from 'vue'
33
+
34
+ const props = withDefaults(
35
+ defineProps<{
36
+ type: TType
37
+ state?: SupportedState<TType> | 'disabled'
38
+ size?: ObjectIconSize
39
+ }>(),
40
+ { size: 'small', state: 'disabled' }
41
+ )
42
+
43
+ const config: ObjectIconConfig = {
44
+ vm: {
45
+ mainIcon: faDisplay,
46
+ states: {
47
+ running: {
48
+ icon: faPlay,
49
+ color: '--color-green-base',
50
+ translate: {
51
+ x: [100, 132, 148],
52
+ y: [65, 75, 90],
53
+ },
54
+ },
55
+ halted: {
56
+ icon: faStop,
57
+ color: '--color-red-base',
58
+ translate: {
59
+ x: [100, 136, 144],
60
+ y: [65, 75, 90],
61
+ },
62
+ },
63
+ suspended: {
64
+ icon: faMoon,
65
+ color: '--color-purple-d60',
66
+ translate: {
67
+ x: [88, 130, 140],
68
+ y: [65, 75, 90],
69
+ },
70
+ },
71
+ paused: {
72
+ icon: faPause,
73
+ color: '--color-purple-l40',
74
+ translate: {
75
+ x: [110, 154, 170],
76
+ y: [65, 75, 90],
77
+ },
78
+ },
79
+ },
80
+ },
81
+ host: {
82
+ mainIcon: faServer,
83
+ states: {
84
+ running: {
85
+ icon: faPlay,
86
+ color: '--color-green-base',
87
+ translate: {
88
+ x: [82, 122, 136],
89
+ y: [66, 70, 90],
90
+ },
91
+ },
92
+ halted: {
93
+ icon: faStop,
94
+ color: '--color-red-base',
95
+ translate: {
96
+ x: [90, 122, 134],
97
+ y: [65, 72, 85],
98
+ },
99
+ },
100
+ maintenance: {
101
+ icon: faTriangleExclamation,
102
+ color: '--color-orange-base',
103
+ translate: {
104
+ x: [60, 88, 100],
105
+ y: [68, 72, 82],
106
+ },
107
+ },
108
+ },
109
+ },
110
+ sr: {
111
+ mainIcon: faDatabase,
112
+ states: {
113
+ connected: {
114
+ icon: faCheckCircle,
115
+ color: '--color-green-base',
116
+ translate: {
117
+ x: [60, 90, 100],
118
+ y: [65, 75, 90],
119
+ },
120
+ },
121
+ 'partially-connected': {
122
+ icon: faCircleMinus,
123
+ color: '--color-orange-base',
124
+ translate: {
125
+ x: [60, 90, 100],
126
+ y: [65, 75, 90],
127
+ },
128
+ },
129
+ disconnected: {
130
+ icon: faCircleXmark,
131
+ color: '--color-red-base',
132
+ translate: {
133
+ x: [60, 90, 100],
134
+ y: [65, 75, 90],
135
+ },
136
+ },
137
+ },
138
+ },
139
+ 'backup-repository': {
140
+ mainIcon: faWarehouse,
141
+ states: {
142
+ connected: {
143
+ icon: faCheckCircle,
144
+ color: '--color-green-base',
145
+ translate: {
146
+ x: [112, 130, 162],
147
+ y: [74, 78, 102],
148
+ },
149
+ },
150
+ disconnected: {
151
+ icon: faCircleXmark,
152
+ color: '--color-red-base',
153
+ translate: {
154
+ x: [112, 130, 162],
155
+ y: [74, 78, 102],
156
+ },
157
+ },
158
+ },
159
+ },
160
+ network: {
161
+ mainIcon: faNetworkWired,
162
+ states: {
163
+ connected: {
164
+ icon: faCheckCircle,
165
+ color: '--color-green-base',
166
+ translate: {
167
+ x: [84, 110, 128],
168
+ y: [66, 72, 88],
169
+ },
170
+ },
171
+ disconnected: {
172
+ icon: faCircleXmark,
173
+ color: '--color-red-base',
174
+ translate: {
175
+ x: [84, 110, 128],
176
+ y: [66, 72, 88],
177
+ },
178
+ },
179
+ },
180
+ },
181
+ }
182
+
183
+ const mainIcon = computed(() => config[props.type].mainIcon)
184
+ const stateConfig = computed(() => (props.state === 'disabled' ? undefined : config[props.type].states[props.state]))
185
+ const stateIcon = computed(() => stateConfig.value?.icon)
186
+
187
+ const stateIconStyle = computed(() => {
188
+ if (stateConfig.value === undefined) {
189
+ return undefined
190
+ }
191
+
192
+ let translateIndex: number
193
+
194
+ if (props.size === 'extra-small') {
195
+ translateIndex = 0
196
+ } else if (props.size === 'medium') {
197
+ translateIndex = 2
198
+ } else {
199
+ translateIndex = 1
200
+ }
201
+
202
+ return {
203
+ '--color': `var(${stateConfig.value.color})`,
204
+ '--state-icon-translate-x': `${stateConfig.value.translate.x[translateIndex]}%`,
205
+ '--state-icon-translate-y': `${stateConfig.value.translate.y[translateIndex]}%`,
206
+ }
207
+ })
208
+ </script>
209
+
210
+ <style lang="postcss" scoped>
211
+ /* SIZE VARIANTS */
212
+ .object-icon {
213
+ &.extra-small {
214
+ --font-size: 0.8rem;
215
+ --state-icon-font-size: 0.75em;
216
+ }
217
+
218
+ &.small {
219
+ --font-size: 1.6rem;
220
+ --state-icon-font-size: 0.625em;
221
+ }
222
+
223
+ &.medium {
224
+ --font-size: 2.4rem;
225
+ --state-icon-font-size: 0.5em;
226
+ }
227
+ }
228
+
229
+ /* IMPLEMENTATION */
230
+ .object-icon {
231
+ flex-shrink: 0;
232
+ font-size: var(--font-size);
233
+
234
+ &.disabled {
235
+ color: var(--color-grey-400);
236
+ }
237
+ }
238
+
239
+ .state {
240
+ font-size: var(--state-icon-font-size);
241
+ transform: translate(var(--state-icon-translate-x), var(--state-icon-translate-y));
242
+ }
243
+ </style>
@@ -0,0 +1,47 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <UiSpinner v-if="busy" class="ui-icon" />
4
+ <FontAwesomeIcon v-else-if="icon !== undefined" :class="color" :fixed-width="fixedWidth" :icon class="ui-icon" />
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import UiSpinner from '@core/components/UiSpinner.vue'
9
+ import type { Color } from '@core/types/color.type'
10
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
11
+ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
12
+
13
+ defineProps<{
14
+ busy?: boolean
15
+ icon?: IconDefinition
16
+ fixedWidth?: boolean
17
+ color?: Color
18
+ }>()
19
+ </script>
20
+
21
+ <style lang="postcss" scoped>
22
+ /* COLOR VARIANTS */
23
+ .ui-icon {
24
+ --color: currentColor;
25
+
26
+ &.info {
27
+ --color: var(--color-purple-base);
28
+ }
29
+
30
+ &.success {
31
+ --color: var(--color-green-base);
32
+ }
33
+
34
+ &.warning {
35
+ --color: var(--color-orange-base);
36
+ }
37
+
38
+ &.error {
39
+ --color: var(--color-red-base);
40
+ }
41
+ }
42
+
43
+ /* IMPLEMENTATION */
44
+ .ui-icon {
45
+ color: var(--color);
46
+ }
47
+ </style>
@@ -0,0 +1,30 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <FontAwesomeLayers class="vm-icon">
4
+ <UiIcon :icon="faDisplay" />
5
+ <PowerStateIcon :state class="state" />
6
+ </FontAwesomeLayers>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import PowerStateIcon from '@core/components/PowerStateIcon.vue'
11
+ import UiIcon from '@core/components/icon/UiIcon.vue'
12
+ import type { POWER_STATE } from '@core/types/power-state.type'
13
+ import { faDisplay } from '@fortawesome/free-solid-svg-icons'
14
+ import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
15
+
16
+ defineProps<{
17
+ state: POWER_STATE
18
+ }>()
19
+ </script>
20
+
21
+ <style lang="postcss" scoped>
22
+ .vm-icon {
23
+ flex-shrink: 0;
24
+ }
25
+
26
+ .state {
27
+ font-size: 0.7em;
28
+ transform: translate(80%, 70%);
29
+ }
30
+ </style>
@@ -0,0 +1,100 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div
4
+ :class="{ locked: sidebar.isLocked && !ui.isMobile, expanded: sidebar.isExpanded, mobile: ui.isMobile }"
5
+ class="layout-sidebar"
6
+ >
7
+ <div v-if="!ui.isMobile" class="lock">
8
+ <UiButtonIcon
9
+ v-tooltip="{
10
+ content: sidebar.isLocked ? $t('core.sidebar.unlock') : $t('core.sidebar.lock'),
11
+ placement: 'right',
12
+ }"
13
+ :icon="sidebar.isLocked ? faLock : faLockOpen"
14
+ @click="sidebar.toggleLock()"
15
+ />
16
+ </div>
17
+ <div v-if="$slots.header">
18
+ <slot name="header" />
19
+ </div>
20
+ <div class="content">
21
+ <slot />
22
+ </div>
23
+ <div v-if="$slots.footer">
24
+ <slot name="footer" />
25
+ </div>
26
+ <div
27
+ v-if="!ui.isMobile"
28
+ :class="{ active: sidebar.isResizing }"
29
+ class="resize-handle"
30
+ @mousedown="sidebar.startResize"
31
+ />
32
+ </div>
33
+ </template>
34
+
35
+ <script lang="ts" setup>
36
+ import UiButtonIcon from '@core/components/button/ButtonIcon.vue'
37
+ import { vTooltip } from '@core/directives/tooltip.directive'
38
+ import { useSidebarStore } from '@core/stores/sidebar.store'
39
+ import { useUiStore } from '@core/stores/ui.store'
40
+ import { faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons'
41
+
42
+ defineSlots<{
43
+ header(): any
44
+ default(): any
45
+ footer(): any
46
+ }>()
47
+
48
+ const sidebar = useSidebarStore()
49
+ const ui = useUiStore()
50
+ </script>
51
+
52
+ <style lang="postcss" scoped>
53
+ .layout-sidebar {
54
+ display: flex;
55
+ flex-direction: column;
56
+ height: 100%;
57
+ background-color: var(--background-color-secondary);
58
+ border-right: 0.1rem solid var(--color-grey-500);
59
+ width: v-bind('sidebar.cssWidth');
60
+ z-index: 1001;
61
+ transition:
62
+ margin-left 0.25s,
63
+ transform 0.25s;
64
+
65
+ &.locked {
66
+ margin-left: v-bind('sidebar.cssOffset');
67
+ }
68
+
69
+ &:not(.locked) {
70
+ position: absolute;
71
+ transform: translateX(v-bind('sidebar.cssOffset'));
72
+ }
73
+ }
74
+
75
+ .lock {
76
+ text-align: right;
77
+ padding: 0.8rem;
78
+ }
79
+
80
+ .content {
81
+ flex: 1;
82
+ overflow: auto;
83
+ }
84
+
85
+ .resize-handle {
86
+ position: absolute;
87
+ inset: 0 0 0 auto;
88
+ width: 0.8rem;
89
+ background-color: transparent;
90
+ cursor: col-resize;
91
+ transition: background-color 0.4s;
92
+ user-select: none;
93
+
94
+ &:hover,
95
+ &.active {
96
+ background-color: var(--color-grey-500);
97
+ transition: background-color 0.05s;
98
+ }
99
+ }
100
+ </style>
@@ -0,0 +1,82 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <li class="menu-item">
4
+ <MenuTrigger
5
+ v-if="!$slots.submenu"
6
+ :active="isBusy"
7
+ :busy="isBusy"
8
+ :disabled="isDisabled"
9
+ :icon
10
+ @click="handleClick"
11
+ >
12
+ <slot />
13
+ </MenuTrigger>
14
+ <MenuList v-else :disabled="isDisabled" shadow>
15
+ <template #trigger="{ open, isOpen }">
16
+ <MenuTrigger :active="isOpen" :busy="isBusy" :disabled="isDisabled" :icon @click="open">
17
+ <slot />
18
+ <UiIcon :fixed-width="false" :icon="submenuIcon" class="submenu-icon" />
19
+ </MenuTrigger>
20
+ </template>
21
+ <slot name="submenu" />
22
+ </MenuList>
23
+ </li>
24
+ </template>
25
+
26
+ <script lang="ts" setup>
27
+ import UiIcon from '@core/components/icon/UiIcon.vue'
28
+ import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
29
+ import MenuList from '@core/components/menu/MenuList.vue'
30
+ import { useContext } from '@core/composables/context.composable'
31
+ import { DisabledContext } from '@core/context'
32
+ import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from '@core/utils/injection-keys.util'
33
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
34
+ import { faAngleDown, faAngleRight } from '@fortawesome/free-solid-svg-icons'
35
+ import { computed, inject, ref } from 'vue'
36
+
37
+ const props = withDefaults(
38
+ defineProps<{
39
+ icon?: IconDefinition
40
+ onClick?: () => any
41
+ disabled?: boolean
42
+ busy?: boolean
43
+ }>(),
44
+ { disabled: undefined }
45
+ )
46
+
47
+ const isParentHorizontal = inject(
48
+ IK_MENU_HORIZONTAL,
49
+ computed(() => false)
50
+ )
51
+ const isDisabled = useContext(DisabledContext, () => props.disabled)
52
+
53
+ const submenuIcon = computed(() => (isParentHorizontal.value ? faAngleDown : faAngleRight))
54
+
55
+ const isHandlingClick = ref(false)
56
+ const isBusy = computed(() => isHandlingClick.value || props.busy === true)
57
+ const closeMenu = inject(IK_CLOSE_MENU, undefined)
58
+
59
+ const handleClick = async () => {
60
+ if (isDisabled.value || isBusy.value) {
61
+ return
62
+ }
63
+
64
+ isHandlingClick.value = true
65
+ try {
66
+ await props.onClick?.()
67
+ closeMenu?.()
68
+ } finally {
69
+ isHandlingClick.value = false
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <style lang="postcss" scoped>
75
+ .menu-item {
76
+ color: var(--color-grey-000);
77
+ }
78
+
79
+ .submenu-icon {
80
+ margin-left: auto;
81
+ }
82
+ </style>
@@ -0,0 +1,104 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <slot :is-open="isOpen" :open="open" name="trigger" />
4
+ <Teleport :disabled="!shouldTeleport" to="body">
5
+ <ul v-if="!hasTrigger || isOpen" ref="menu" :class="{ horizontal, shadow }" class="menu-list" v-bind="$attrs">
6
+ <slot />
7
+ </ul>
8
+ </Teleport>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import { useContext } from '@core/composables/context.composable'
13
+ import { DisabledContext } from '@core/context'
14
+ import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL, IK_MENU_TELEPORTED } from '@core/utils/injection-keys.util'
15
+ import placementJs, { type Options } from 'placement.js'
16
+ import { computed, inject, nextTick, provide, ref, useSlots } from 'vue'
17
+ import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ horizontal?: boolean
22
+ shadow?: boolean
23
+ disabled?: boolean
24
+ placement?: Options['placement']
25
+ }>(),
26
+ { disabled: undefined }
27
+ )
28
+
29
+ defineOptions({
30
+ inheritAttrs: false,
31
+ })
32
+
33
+ const slots = useSlots()
34
+ const isOpen = ref(false)
35
+ const menu = ref()
36
+ const isParentHorizontal = inject(
37
+ IK_MENU_HORIZONTAL,
38
+ computed(() => false)
39
+ )
40
+ provide(
41
+ IK_MENU_HORIZONTAL,
42
+ computed(() => props.horizontal ?? false)
43
+ )
44
+
45
+ useContext(DisabledContext, () => props.disabled)
46
+
47
+ let clearClickOutsideEvent: (() => void) | undefined
48
+
49
+ const hasTrigger = useSlots().trigger !== undefined
50
+
51
+ const shouldTeleport = hasTrigger && !inject(IK_MENU_TELEPORTED, false)
52
+
53
+ if (shouldTeleport) {
54
+ provide(IK_MENU_TELEPORTED, true)
55
+ }
56
+
57
+ whenever(
58
+ () => !isOpen.value,
59
+ () => clearClickOutsideEvent?.()
60
+ )
61
+ if (slots.trigger && inject(IK_CLOSE_MENU, undefined) === undefined) {
62
+ provide(IK_CLOSE_MENU, () => (isOpen.value = false))
63
+ }
64
+
65
+ const open = (event: MouseEvent) => {
66
+ if (isOpen.value) {
67
+ return (isOpen.value = false)
68
+ }
69
+
70
+ isOpen.value = true
71
+
72
+ nextTick(() => {
73
+ clearClickOutsideEvent = onClickOutside(menu, () => (isOpen.value = false), {
74
+ ignore: [event.currentTarget as HTMLElement],
75
+ })
76
+
77
+ placementJs(event.currentTarget as HTMLElement, unrefElement(menu), {
78
+ placement: props.placement ?? (isParentHorizontal.value ? 'bottom-start' : 'right-start'),
79
+ })
80
+ })
81
+ }
82
+ </script>
83
+
84
+ <style lang="postcss" scoped>
85
+ .menu-list {
86
+ z-index: 1;
87
+ display: inline-flex;
88
+ flex-direction: column;
89
+ padding: 0.4rem;
90
+ cursor: default;
91
+ color: var(--color-grey-200);
92
+ border-radius: 0.4rem;
93
+ background-color: var(--color-grey-600);
94
+ gap: 0.2rem;
95
+
96
+ &.horizontal {
97
+ flex-direction: row;
98
+ }
99
+
100
+ &.shadow {
101
+ box-shadow: var(--shadow-300);
102
+ }
103
+ }
104
+ </style>
@@ -0,0 +1,27 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <li :class="{ horizontal }" class="menu-separator" />
4
+ </template>
5
+
6
+ <script lang="ts" setup>
7
+ import { IK_MENU_HORIZONTAL } from '@core/utils/injection-keys.util'
8
+ import { computed, inject } from 'vue'
9
+
10
+ const horizontal = inject(
11
+ IK_MENU_HORIZONTAL,
12
+ computed(() => false)
13
+ )
14
+ </script>
15
+
16
+ <style lang="postcss" scoped>
17
+ .menu-separator {
18
+ &.horizontal {
19
+ margin: 0 0.5rem;
20
+ border-right: 1px solid var(--color-grey-500);
21
+ }
22
+
23
+ &:not(.horizontal) {
24
+ border-bottom: 1px solid var(--color-grey-500);
25
+ }
26
+ }
27
+ </style>