@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,63 @@
1
+ import { useUiStore } from '@core/stores/ui.store'
2
+ import { ifElse } from '@core/utils/if-else.utils'
3
+ import { useLocalStorage, useRafFn, useStyleTag, useToggle } from '@vueuse/core'
4
+ import { defineStore } from 'pinia'
5
+ import { computed, ref } from 'vue'
6
+
7
+ export const useSidebarStore = defineStore('layout', () => {
8
+ const uiStore = useUiStore()
9
+ const isExpanded = useLocalStorage('sidebar.expanded', true)
10
+ const isLocked = useLocalStorage('sidebar.locked', true)
11
+ const width = useLocalStorage('sidebar.width', 350)
12
+ const toggleExpand = useToggle(isExpanded)
13
+ const toggleLock = useToggle(isLocked)
14
+ const isResizing = ref(false)
15
+
16
+ let initialX: number
17
+ let initialWidth: number
18
+ let clientX: number
19
+
20
+ function startResize(event: MouseEvent) {
21
+ clientX = event.clientX
22
+ initialX = clientX
23
+ initialWidth = width.value
24
+ isResizing.value = true
25
+ document.addEventListener('mousemove', handleMouseMove)
26
+ document.addEventListener('mouseup', handleMouseUp)
27
+ }
28
+
29
+ function handleMouseMove(event: MouseEvent) {
30
+ clientX = event.clientX
31
+ }
32
+
33
+ function handleMouseUp() {
34
+ isResizing.value = false
35
+ document.removeEventListener('mousemove', handleMouseMove)
36
+ document.removeEventListener('mouseup', handleMouseUp)
37
+ }
38
+
39
+ const cssWidth = computed(() => (uiStore.isMobile ? '100%' : `${width.value}px`))
40
+
41
+ const cssOffset = computed(() => (isExpanded.value ? 0 : `-${cssWidth.value}`))
42
+
43
+ const { load, unload } = useStyleTag('body { cursor: col-resize !important; }', { immediate: false })
44
+
45
+ const { pause, resume } = useRafFn(
46
+ () => (width.value = Math.min(500, Math.max(200, initialWidth + clientX - initialX))),
47
+ { immediate: false }
48
+ )
49
+
50
+ ifElse(isResizing, [load, resume], [pause, unload])
51
+
52
+ return {
53
+ isExpanded,
54
+ isLocked,
55
+ width,
56
+ toggleExpand,
57
+ toggleLock,
58
+ startResize,
59
+ isResizing,
60
+ cssWidth,
61
+ cssOffset,
62
+ }
63
+ })
@@ -0,0 +1,74 @@
1
+ import type { TooltipDirectiveContent } from '@core/directives/tooltip.directive'
2
+ import { uniqueId } from '@core/utils/unique-id.util'
3
+ import { useEventListener, type WindowEventName } from '@vueuse/core'
4
+ import { defineStore } from 'pinia'
5
+ import type { Options } from 'placement.js'
6
+ import { computed, type EffectScope, effectScope, ref } from 'vue'
7
+
8
+ export type TooltipOptions = {
9
+ content: TooltipDirectiveContent
10
+ placement: Options['placement']
11
+ selector: string | undefined
12
+ vertical: boolean
13
+ }
14
+
15
+ export type TooltipEvents = { on: WindowEventName; off: WindowEventName }
16
+
17
+ export const useTooltipStore = defineStore('tooltip', () => {
18
+ const targetsScopes = new WeakMap<HTMLElement, EffectScope>()
19
+ const targets = ref(new Set<HTMLElement>())
20
+ const targetsOptions = ref(new Map<HTMLElement, TooltipOptions>())
21
+ const targetsIds = ref(new Map<HTMLElement, string>())
22
+
23
+ const register = (target: HTMLElement, options: TooltipOptions, events: TooltipEvents) => {
24
+ const scope = effectScope()
25
+
26
+ targetsScopes.set(target, scope)
27
+ targetsOptions.value.set(target, options)
28
+ targetsIds.value.set(target, uniqueId('tooltip-'))
29
+
30
+ scope.run(() => {
31
+ useEventListener(target, events.on, () => {
32
+ targets.value.add(target)
33
+
34
+ scope.run(() => {
35
+ useEventListener(
36
+ target,
37
+ events.off,
38
+ () => {
39
+ targets.value.delete(target)
40
+ },
41
+ { once: true }
42
+ )
43
+ })
44
+ })
45
+ })
46
+ }
47
+
48
+ const updateOptions = (target: HTMLElement, options: TooltipOptions) => {
49
+ targetsOptions.value.set(target, options)
50
+ }
51
+
52
+ const unregister = (target: HTMLElement) => {
53
+ targets.value.delete(target)
54
+ targetsOptions.value.delete(target)
55
+ targetsScopes.get(target)?.stop()
56
+ targetsScopes.delete(target)
57
+ targetsIds.value.delete(target)
58
+ }
59
+
60
+ return {
61
+ register,
62
+ unregister,
63
+ updateOptions,
64
+ tooltips: computed(() => {
65
+ return Array.from(targets.value.values()).map(target => {
66
+ return {
67
+ target,
68
+ options: targetsOptions.value.get(target)!,
69
+ key: targetsIds.value.get(target)!,
70
+ }
71
+ })
72
+ }),
73
+ }
74
+ })
@@ -0,0 +1,34 @@
1
+ import { useBreakpoints, useColorMode } from '@vueuse/core'
2
+ import { defineStore } from 'pinia'
3
+ import { computed, ref } from 'vue'
4
+ import { useRoute, useRouter } from 'vue-router'
5
+
6
+ export const useUiStore = defineStore('ui', () => {
7
+ const currentHostOpaqueRef = ref()
8
+
9
+ const { store: colorMode } = useColorMode({ initialValue: 'dark' })
10
+
11
+ const { desktop: isDesktop } = useBreakpoints({
12
+ desktop: 1024,
13
+ })
14
+
15
+ const isMobile = computed(() => !isDesktop.value)
16
+
17
+ const router = useRouter()
18
+ const route = useRoute()
19
+
20
+ const hasUi = computed<boolean>({
21
+ get: () => route.query.ui !== '0',
22
+ set: (value: boolean) => {
23
+ void router.replace({ query: { ui: value ? undefined : '0' } })
24
+ },
25
+ })
26
+
27
+ return {
28
+ colorMode,
29
+ currentHostOpaqueRef,
30
+ isDesktop,
31
+ isMobile,
32
+ hasUi,
33
+ }
34
+ })
@@ -0,0 +1,3 @@
1
+ export type ButtonLevel = 'primary' | 'secondary' | 'tertiary'
2
+
3
+ export type ButtonSize = 'extra-small' | 'small' | 'medium'
@@ -0,0 +1,5 @@
1
+ export type Color = 'info' | 'error' | 'danger' | 'warning' | 'success'
2
+
3
+ export type CounterColor = Color | 'black'
4
+
5
+ export type TagColor = Color | 'grey'
@@ -0,0 +1,43 @@
1
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
2
+
3
+ export type ObjectIconSize = 'extra-small' | 'small' | 'medium'
4
+
5
+ export type VmState = 'running' | 'halted' | 'paused' | 'suspended'
6
+
7
+ export type HostState = 'running' | 'halted' | 'maintenance'
8
+
9
+ export type SrState = 'connected' | 'partially-connected' | 'disconnected'
10
+
11
+ export type BackupRepositoryState = 'connected' | 'disconnected'
12
+
13
+ export type NetworkState = 'connected' | 'disconnected'
14
+
15
+ export type SupportedStateByType = {
16
+ host: HostState
17
+ vm: VmState
18
+ sr: SrState
19
+ 'backup-repository': BackupRepositoryState
20
+ network: NetworkState
21
+ }
22
+
23
+ export type SupportedType = keyof SupportedStateByType
24
+
25
+ export type SupportedState<TType extends SupportedType> = SupportedStateByType[TType]
26
+
27
+ export type StatusConfig = {
28
+ icon: IconDefinition
29
+ color: `--${string}`
30
+ translate: {
31
+ x: [number, number, number]
32
+ y: [number, number, number]
33
+ }
34
+ }
35
+
36
+ export type TypeConfig<TType extends SupportedType> = {
37
+ mainIcon: IconDefinition
38
+ states: Record<SupportedState<TType>, StatusConfig>
39
+ }
40
+
41
+ export type ObjectIconConfig = {
42
+ [K in SupportedType]: TypeConfig<K>
43
+ }
@@ -0,0 +1 @@
1
+ export type POWER_STATE = 'running' | 'paused' | 'halted' | 'suspended'
@@ -0,0 +1,3 @@
1
+ export type CounterSize = 'small' | 'medium'
2
+
3
+ export type UserLogoSize = 'extra-small' | 'small' | 'medium'
@@ -0,0 +1,21 @@
1
+ import type { MaybeRefOrGetter, Ref } from 'vue'
2
+
3
+ export type SubscribeContext<TContext, TDefer extends boolean = false> = TDefer extends true
4
+ ? TContext & {
5
+ deferred: true
6
+ start: () => void
7
+ stop: () => void
8
+ isStarted: Readonly<Ref<boolean>>
9
+ }
10
+ : TContext & { deferred: false }
11
+
12
+ export type Subscribe<TDefer extends boolean = false> = (options?: {
13
+ defer?: TDefer
14
+ }) => SubscribeContext<object, TDefer>
15
+
16
+ export type SubscribableStoreConfig<TContext> = {
17
+ context: TContext
18
+ onSubscribe: () => void
19
+ onUnsubscribe: () => void
20
+ isEnabled?: MaybeRefOrGetter<boolean>
21
+ }
@@ -0,0 +1 @@
1
+ export type MaybeArray<T> = T | T[]
@@ -0,0 +1,66 @@
1
+ import type { SubscribableStoreConfig, Subscribe, SubscribeContext } from '@core/types/subscribable-store.type'
2
+ import { ifElse } from '@core/utils/if-else.utils'
3
+ import { computed, onBeforeUnmount, readonly, type Ref, ref, toValue } from 'vue'
4
+
5
+ export function createSubscribableStoreContext<TContext>(
6
+ config: SubscribableStoreConfig<TContext>,
7
+ dependsOn: Record<any, { subscribe: Subscribe<boolean> }>
8
+ ) {
9
+ const subscriptions = ref(new Set()) as Ref<Set<symbol>>
10
+ const hasSubscriptions = computed(() => subscriptions.value.size > 0)
11
+
12
+ ifElse(() => toValue(config.isEnabled ?? true) && hasSubscriptions.value, config.onSubscribe, config.onUnsubscribe)
13
+
14
+ function subscribe(options?: { defer: false }): SubscribeContext<TContext>
15
+ function subscribe(options: { defer: true }): SubscribeContext<TContext, true>
16
+ function subscribe<TDefer extends boolean = false>(options?: { defer?: TDefer }): SubscribeContext<TContext, TDefer>
17
+ function subscribe<TDefer extends boolean>(options?: { defer?: TDefer }): SubscribeContext<TContext, TDefer> {
18
+ const dependencyControls = [] as { start: () => void; stop: () => void }[]
19
+
20
+ Object.values(dependsOn).forEach(dep => {
21
+ const context = dep.subscribe({ defer: options?.defer })
22
+
23
+ if (context.deferred) {
24
+ dependencyControls.push({ start: context.start, stop: context.stop })
25
+ }
26
+ })
27
+
28
+ const id = Symbol('Store subscription ID')
29
+ const isStarted = ref(false)
30
+
31
+ const start = () => {
32
+ isStarted.value = true
33
+ dependencyControls.forEach(({ start }) => start())
34
+ subscriptions.value.add(id)
35
+ }
36
+
37
+ const stop = () => {
38
+ subscriptions.value.delete(id)
39
+ dependencyControls.forEach(({ stop }) => stop())
40
+ isStarted.value = false
41
+ }
42
+
43
+ onBeforeUnmount(() => stop())
44
+
45
+ if (!options?.defer) {
46
+ start()
47
+ return {
48
+ ...config.context,
49
+ deferred: false,
50
+ } as SubscribeContext<TContext, TDefer>
51
+ }
52
+
53
+ return {
54
+ ...config.context,
55
+ deferred: true,
56
+ start,
57
+ stop,
58
+ isStarted: readonly(isStarted),
59
+ } as SubscribeContext<TContext, TDefer>
60
+ }
61
+
62
+ return {
63
+ $context: config.context,
64
+ subscribe,
65
+ }
66
+ }
@@ -0,0 +1,11 @@
1
+ export const hasEllipsis = (target: Element | undefined | null, { vertical = false }: { vertical?: boolean } = {}) => {
2
+ if (target == null) {
3
+ return false
4
+ }
5
+
6
+ if (vertical) {
7
+ return target.clientHeight < target.scrollHeight
8
+ }
9
+
10
+ return target.clientWidth < target.scrollWidth
11
+ }
@@ -0,0 +1,27 @@
1
+ import type { MaybeArray } from '@core/types/utility.type'
2
+ import { toArray } from '@core/utils/to-array.utils'
3
+ import { watch, type WatchOptions, type WatchSource } from 'vue'
4
+
5
+ export interface IfElseOptions extends Pick<WatchOptions, 'immediate'> {}
6
+
7
+ export function ifElse(
8
+ source: WatchSource<boolean>,
9
+ onTrue: MaybeArray<VoidFunction>,
10
+ onFalse: MaybeArray<VoidFunction>,
11
+ options?: IfElseOptions
12
+ ) {
13
+ const onTrueFunctions = toArray(onTrue)
14
+ const onFalseFunctions = toArray(onFalse)
15
+
16
+ return watch(
17
+ source,
18
+ value => {
19
+ if (value) {
20
+ onTrueFunctions.forEach(func => func())
21
+ } else {
22
+ onFalseFunctions.forEach(func => func())
23
+ }
24
+ },
25
+ options
26
+ )
27
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComputedRef, InjectionKey, Ref } from 'vue'
2
+
3
+ export const IK_TREE_ITEM_HAS_CHILDREN = Symbol('IK_TREE_ITEM_HAS_CHILDREN') as InjectionKey<Ref<boolean>>
4
+
5
+ export const IK_TREE_ITEM_TOGGLE = Symbol('IK_TREE_ITEM_TOGGLE') as InjectionKey<(force?: boolean) => void>
6
+
7
+ export const IK_TREE_ITEM_EXPANDED = Symbol('IK_TREE_ITEM_EXPANDED') as InjectionKey<Ref<boolean>>
8
+
9
+ export const IK_TREE_LIST_DEPTH = Symbol('IK_TREE_LIST_DEPTH') as InjectionKey<number>
10
+
11
+ export const IK_DROPDOWN_CHECKBOX = Symbol('IK_DROPDOWN_CHECKBOX') as InjectionKey<ComputedRef<boolean>>
12
+
13
+ export const IK_MENU_HORIZONTAL = Symbol('IK_MENU_HORIZONTAL') as InjectionKey<ComputedRef<boolean>>
14
+
15
+ export const IK_CLOSE_MENU = Symbol('IK_CLOSE_MENU') as InjectionKey<() => void>
16
+
17
+ export const IK_MENU_TELEPORTED = Symbol('IK_MENU_TELEPORTED') as InjectionKey<boolean>
@@ -0,0 +1,6 @@
1
+ export function sortByNameLabel<TObject extends { name_label: string }>(
2
+ { name_label: label1 }: TObject,
3
+ { name_label: label2 }: TObject
4
+ ) {
5
+ return label1.localeCompare(label2, undefined, { numeric: true })
6
+ }
@@ -0,0 +1,9 @@
1
+ import type { MaybeArray } from '@core/types/utility.type'
2
+
3
+ export function toArray<T>(value: MaybeArray<T> | undefined): T[] {
4
+ if (value === undefined) {
5
+ return []
6
+ }
7
+
8
+ return Array.isArray(value) ? value : [value]
9
+ }
@@ -0,0 +1,8 @@
1
+ const uniqueIds = new Map<string | undefined, number>()
2
+
3
+ export const uniqueId = (prefix?: string) => {
4
+ const id = uniqueIds.get(prefix) || 0
5
+ uniqueIds.set(prefix, id + 1)
6
+
7
+ return prefix !== undefined ? `${prefix}-${id}` : `${id}`
8
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@xen-orchestra/web-core",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "private": false,
6
+ "exports": {
7
+ "./*": {
8
+ "types": "./lib/*",
9
+ "import": "./lib/*"
10
+ }
11
+ },
12
+ "devDependencies": {
13
+ "@fortawesome/fontawesome-svg-core": "^6.5.1",
14
+ "@fortawesome/free-regular-svg-icons": "^6.5.1",
15
+ "@fortawesome/free-solid-svg-icons": "^6.5.1",
16
+ "@fortawesome/vue-fontawesome": "^3.0.5",
17
+ "@types/lodash-es": "^4.17.12",
18
+ "@vue/tsconfig": "^0.5.1",
19
+ "@vueuse/core": "^10.7.1",
20
+ "lodash-es": "^4.17.21",
21
+ "pinia": "^2.1.7",
22
+ "placement.js": "^1.0.0-beta.5",
23
+ "vue": "^3.4.13",
24
+ "vue-i18n": "^9.9.0",
25
+ "vue-router": "^4.2.5"
26
+ },
27
+ "homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
28
+ "bugs": "https://github.com/vatesfr/xen-orchestra/issues",
29
+ "repository": {
30
+ "directory": "@xen-orchestra/web-core",
31
+ "type": "git",
32
+ "url": "https://github.com/vatesfr/xen-orchestra.git"
33
+ },
34
+ "author": {
35
+ "name": "Vates SAS",
36
+ "url": "https://vates.fr"
37
+ },
38
+ "license": "AGPL-3.0-or-later",
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "postversion": "npm publish --access public"
44
+ }
45
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
4
+ "exclude": ["lib/**/__tests__/*"],
5
+ "compilerOptions": {
6
+ "noEmit": true,
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@core/*": ["./lib/*"]
10
+ }
11
+ }
12
+ }