@xen-orchestra/web-core 0.18.0 → 0.20.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 (50) hide show
  1. package/lib/components/backdrop/VtsBackdrop.vue +1 -1
  2. package/lib/components/column/VtsColumn.vue +21 -0
  3. package/lib/components/columns/VtsColumns.vue +38 -0
  4. package/lib/components/copy-button/VtsCopyButton.vue +29 -0
  5. package/lib/components/enabled-state/VtsEnabledState.vue +23 -0
  6. package/lib/components/icon/VtsIcon.vue +9 -1
  7. package/lib/components/input-wrapper/VtsInputWrapper.vue +1 -0
  8. package/lib/components/layout/VtsLayoutSidebar.vue +1 -1
  9. package/lib/components/quick-info-column/VtsQuickInfoColumn.vue +1 -1
  10. package/lib/components/quick-info-row/VtsQuickInfoRow.vue +26 -7
  11. package/lib/components/relative-time/VtsRelativeTime.vue +18 -0
  12. package/lib/components/select/VtsOption.vue +24 -0
  13. package/lib/components/select/VtsSelect.vue +96 -0
  14. package/lib/components/state-hero/VtsLoadingHero.vue +45 -4
  15. package/lib/components/tree/VtsTreeItem.vue +11 -1
  16. package/lib/components/ui/alert/UiAlert.vue +105 -0
  17. package/lib/components/ui/circle-progress-bar/UiCircleProgressBar.vue +212 -0
  18. package/lib/components/ui/dropdown/UiDropdownList.vue +10 -2
  19. package/lib/components/ui/head-bar/UiHeadBar.vue +2 -2
  20. package/lib/components/ui/info/UiInfo.vue +5 -3
  21. package/lib/components/ui/input/UiInput.vue +126 -109
  22. package/lib/composables/chart-theme.composable.ts +3 -3
  23. package/lib/composables/relative-time.composable.ts +1 -1
  24. package/lib/i18n.ts +4 -0
  25. package/lib/locales/cs.json +65 -18
  26. package/lib/locales/en.json +65 -1
  27. package/lib/locales/es.json +60 -13
  28. package/lib/locales/fa.json +59 -12
  29. package/lib/locales/fr.json +67 -3
  30. package/lib/locales/it.json +145 -7
  31. package/lib/locales/nl.json +502 -0
  32. package/lib/locales/ru.json +91 -1
  33. package/lib/locales/sv.json +75 -19
  34. package/lib/packages/collection/README.md +172 -0
  35. package/lib/packages/collection/create-collection.ts +74 -0
  36. package/lib/packages/collection/create-item.ts +39 -0
  37. package/lib/packages/collection/guess-item-id.ts +26 -0
  38. package/lib/packages/collection/index.ts +2 -0
  39. package/lib/packages/collection/types.ts +57 -0
  40. package/lib/packages/collection/use-collection.ts +47 -0
  41. package/lib/packages/collection/use-flag-registry.ts +64 -0
  42. package/lib/packages/form-select/README.md +96 -0
  43. package/lib/packages/form-select/index.ts +2 -0
  44. package/lib/packages/form-select/types.ts +75 -0
  45. package/lib/packages/form-select/use-form-option-controller.ts +50 -0
  46. package/lib/packages/form-select/use-form-select-controller.ts +205 -0
  47. package/lib/packages/form-select/use-form-select-keyboard-navigation.ts +157 -0
  48. package/lib/packages/form-select/use-form-select.ts +193 -0
  49. package/lib/stores/sidebar.store.ts +14 -1
  50. package/package.json +1 -1
@@ -0,0 +1,212 @@
1
+ <!-- v4 -->
2
+ <template>
3
+ <div class="ui-circle-progress-bar" :class="className">
4
+ <svg
5
+ :width="circleSize"
6
+ :height="circleSize"
7
+ :viewBox="`0 0 ${circleSize} ${circleSize}`"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ >
10
+ <circle :r="radius" :cx="circleSize / 2" :cy="circleSize / 2" fill="transparent" class="background" />
11
+ <circle
12
+ :r="radius"
13
+ :cx="circleSize / 2"
14
+ :cy="circleSize / 2"
15
+ fill="transparent"
16
+ class="fill"
17
+ :class="{ success: isCompleteWithSuccess }"
18
+ />
19
+ </svg>
20
+ <div v-if="size !== 'extra-small'" class="overlay">
21
+ <VtsIcon v-if="isComplete" class="icon" :icon :accent="iconAccent" />
22
+ <span v-else>{{ percentValue }}</span>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import VtsIcon from '@core/components/icon/VtsIcon.vue'
29
+ import { toVariants } from '@core/utils/to-variants.util.ts'
30
+ import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons'
31
+ import { useClamp, useMax } from '@vueuse/math'
32
+ import { computed } from 'vue'
33
+ import { useI18n } from 'vue-i18n'
34
+
35
+ const {
36
+ accent,
37
+ size,
38
+ value: _value,
39
+ maxValue = 100,
40
+ } = defineProps<{
41
+ accent: CircleProgressBarAccent
42
+ size: CircleProgressBarSize
43
+ value: number
44
+ maxValue?: number
45
+ }>()
46
+
47
+ const { n } = useI18n()
48
+
49
+ type CircleProgressBarAccent = 'info' | 'warning' | 'danger'
50
+ type CircleProgressBarSize = 'extra-small' | 'small' | 'medium' | 'large'
51
+
52
+ const circleSizeMap = {
53
+ 'extra-small': 16,
54
+ small: 40,
55
+ medium: 64,
56
+ large: 164,
57
+ }
58
+
59
+ const strokeWidthMap = {
60
+ 'extra-small': 2,
61
+ small: 4,
62
+ medium: 6,
63
+ large: 16,
64
+ }
65
+
66
+ const fontClassesMap = {
67
+ 'extra-small': undefined,
68
+ small: 'typo-body-bold-small',
69
+ medium: 'typo-h5',
70
+ large: 'typo-h3',
71
+ }
72
+
73
+ const circleSize = computed(() => circleSizeMap[size])
74
+ const fontClass = computed(() => fontClassesMap[size])
75
+ const strokeWidth = computed(() => strokeWidthMap[size])
76
+
77
+ const radius = computed(() => (circleSize.value - strokeWidth.value) / 2)
78
+ const circumference = computed(() => 2 * Math.PI * radius.value)
79
+
80
+ const value = useMax(0, () => _value)
81
+ const valuePercent = useClamp(() => (value.value / maxValue) * 100 || 0, 0, 100)
82
+
83
+ const isComplete = computed(() => valuePercent.value >= 100)
84
+ const isCompleteWithSuccess = computed(() => isComplete.value && accent === 'info')
85
+
86
+ const dashOffset = computed(() => {
87
+ if (valuePercent.value > 100) return
88
+ return circumference.value * (1 - valuePercent.value / 100)
89
+ })
90
+
91
+ const percentValue = computed(() => n(valuePercent.value / 100, 'percent'))
92
+
93
+ const iconAccent = computed(() => (isCompleteWithSuccess.value ? 'success' : accent))
94
+ const icon = computed(() => (accent === 'warning' || accent === 'danger' ? faExclamation : faCheck))
95
+
96
+ const className = computed(() => [
97
+ fontClass.value,
98
+ toVariants({
99
+ accent,
100
+ size,
101
+ }),
102
+ ])
103
+ </script>
104
+
105
+ <style lang="postcss" scoped>
106
+ .ui-circle-progress-bar {
107
+ position: relative;
108
+ display: inline-flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+
112
+ /* ACCENT */
113
+
114
+ &.accent--info {
115
+ color: var(--color-info-item-base);
116
+
117
+ .fill {
118
+ stroke: var(--color-info-item-base);
119
+ }
120
+
121
+ .background {
122
+ stroke: var(--color-info-background-selected);
123
+ }
124
+
125
+ .success {
126
+ stroke: var(--color-success-item-base);
127
+ }
128
+ }
129
+
130
+ &.accent--warning {
131
+ color: var(--color-warning-item-base);
132
+
133
+ .fill {
134
+ stroke: var(--color-warning-item-base);
135
+ }
136
+
137
+ .background {
138
+ stroke: var(--color-warning-background-selected);
139
+ }
140
+ }
141
+
142
+ &.accent--danger {
143
+ color: var(--color-danger-item-base);
144
+
145
+ .fill {
146
+ stroke: var(--color-danger-item-base);
147
+ }
148
+
149
+ .background {
150
+ stroke: var(--color-danger-background-selected);
151
+ }
152
+ }
153
+
154
+ /* SIZE */
155
+
156
+ &.size--extra-small {
157
+ .background,
158
+ .fill {
159
+ stroke-width: 2;
160
+ }
161
+ }
162
+
163
+ &.size--small {
164
+ .icon {
165
+ font-size: 1.6rem;
166
+ }
167
+
168
+ .background,
169
+ .fill {
170
+ stroke-width: 4;
171
+ }
172
+ }
173
+
174
+ &.size--medium {
175
+ .icon {
176
+ font-size: 3.2rem;
177
+ }
178
+
179
+ .background,
180
+ .fill {
181
+ stroke-width: 6;
182
+ }
183
+ }
184
+
185
+ &.size--large {
186
+ .icon {
187
+ font-size: 4.8rem;
188
+ }
189
+
190
+ .background,
191
+ .fill {
192
+ stroke-width: 16;
193
+ }
194
+ }
195
+
196
+ .fill {
197
+ stroke-linecap: butt;
198
+ transition: stroke-dashoffset 0.3s ease;
199
+ transform: rotate(-90deg);
200
+ transform-origin: center;
201
+ stroke-dasharray: v-bind(circumference);
202
+ stroke-dashoffset: v-bind(dashOffset);
203
+ }
204
+
205
+ .overlay {
206
+ position: absolute;
207
+ top: 50%;
208
+ left: 50%;
209
+ transform: translate(-50%, -50%);
210
+ }
211
+ }
212
+ </style>
@@ -1,6 +1,9 @@
1
1
  <template>
2
2
  <div class="ui-dropdown-list">
3
- <slot />
3
+ <slot name="before" />
4
+ <div class="container">
5
+ <slot />
6
+ </div>
4
7
  </div>
5
8
  </template>
6
9
 
@@ -8,9 +11,14 @@
8
11
  .ui-dropdown-list {
9
12
  display: flex;
10
13
  flex-direction: column;
11
- overflow: auto;
12
14
  background: var(--color-neutral-background-primary);
13
15
  border: 0.1rem solid var(--color-neutral-border);
14
16
  border-radius: 0.4rem;
17
+
18
+ .container {
19
+ display: flex;
20
+ flex-direction: column;
21
+ overflow: auto;
22
+ }
15
23
  }
16
24
  </style>
@@ -1,4 +1,4 @@
1
- <!-- v3 -->
1
+ <!-- v4 -->
2
2
  <template>
3
3
  <div class="ui-head-bar">
4
4
  <div class="label-wrapper">
@@ -41,7 +41,7 @@ const slots = defineSlots<{
41
41
  .ui-head-bar {
42
42
  padding: 0.8rem 1.6rem;
43
43
  display: flex;
44
- gap: 4.8rem;
44
+ gap: 1.6rem;
45
45
  align-items: center;
46
46
  border-bottom: 0.1rem solid var(--color-neutral-border);
47
47
  background-color: var(--color-neutral-background-primary);
@@ -1,8 +1,8 @@
1
- <!-- v2 -->
1
+ <!-- v4 -->
2
2
  <template>
3
3
  <div class="ui-info">
4
4
  <VtsIcon :accent class="icon" :icon="faCircle" :overlay-icon="icon" />
5
- <p v-tooltip="!wrap" class="typo-form-info" :class="{ 'text-ellipsis': !wrap }">
5
+ <p v-tooltip="!wrap" class="typo-body-regular-small" :class="{ 'text-ellipsis': !wrap }">
6
6
  <slot />
7
7
  </p>
8
8
  </div>
@@ -16,12 +16,13 @@ import {
16
16
  faCircle,
17
17
  faExclamation,
18
18
  faInfo,
19
+ faMinus,
19
20
  faXmark,
20
21
  type IconDefinition,
21
22
  } from '@fortawesome/free-solid-svg-icons'
22
23
  import { computed } from 'vue'
23
24
 
24
- export type InfoAccent = 'info' | 'success' | 'warning' | 'danger'
25
+ export type InfoAccent = 'info' | 'success' | 'warning' | 'danger' | 'muted'
25
26
 
26
27
  const { accent } = defineProps<{
27
28
  accent: InfoAccent
@@ -37,6 +38,7 @@ const iconByAccent: Record<InfoAccent, IconDefinition> = {
37
38
  success: faCheck,
38
39
  warning: faExclamation,
39
40
  danger: faXmark,
41
+ muted: faMinus,
40
42
  }
41
43
 
42
44
  const icon = computed(() => iconByAccent[accent])
@@ -1,41 +1,41 @@
1
1
  <!-- v5 -->
2
2
  <template>
3
- <div class="ui-input" :class="toVariants({ accent })">
4
- <UiLabel v-if="slots.default || label" :accent="labelAccent" :required :icon="labelIcon" :href :for="id">
5
- <slot>{{ label }}</slot>
6
- </UiLabel>
7
- <div>
8
- <VtsIcon :icon accent="current" class="before" />
9
- <input
10
- :id
11
- v-model.trim="modelValue"
12
- class="typo-body-regular input text-ellipsis"
13
- :type
14
- :disabled
15
- v-bind="attrs"
16
- />
17
- <VtsIcon
18
- v-if="!attrs.disabled && modelValue && clearable"
19
- :icon="faXmark"
20
- class="after"
21
- accent="brand"
22
- @click="modelValue = ''"
23
- />
24
- </div>
25
- <UiInfo v-if="slots.info || info" :accent="accent === 'brand' ? 'info' : accent">
26
- <slot name="info">{{ info }}</slot>
27
- </UiInfo>
3
+ <div :class="toVariants({ accent, disabled })" class="ui-input" @click.self="focus()">
4
+ <VtsIcon :icon accent="current" class="left-icon" />
5
+ <input
6
+ :id="wrapperController?.id ?? id"
7
+ ref="inputRef"
8
+ v-model.trim="modelValue"
9
+ :disabled
10
+ :required
11
+ :type
12
+ class="typo-body-regular input text-ellipsis"
13
+ v-bind="attrs"
14
+ />
15
+ <UiButtonIcon
16
+ v-if="!disabled && modelValue && clearable"
17
+ :icon="faXmark"
18
+ :target-scale="1.6"
19
+ accent="brand"
20
+ size="small"
21
+ @click="clear()"
22
+ />
23
+ <slot v-if="slots['right-icon'] || rightIcon" name="right-icon">
24
+ <VtsIcon :icon="rightIcon" accent="current" class="right-icon" />
25
+ </slot>
28
26
  </div>
29
27
  </template>
30
28
 
31
29
  <script lang="ts" setup>
32
30
  import VtsIcon from '@core/components/icon/VtsIcon.vue'
33
- import UiInfo from '@core/components/ui/info/UiInfo.vue'
34
- import UiLabel from '@core/components/ui/label/UiLabel.vue'
31
+ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
32
+ import type { LabelAccent } from '@core/components/ui/label/UiLabel.vue'
33
+ import { useMapper } from '@core/composables/mapper.composable.ts'
34
+ import { IK_INPUT_WRAPPER_CONTROLLER } from '@core/utils/injection-keys.util'
35
35
  import { toVariants } from '@core/utils/to-variants.util'
36
36
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
37
37
  import { faXmark } from '@fortawesome/free-solid-svg-icons'
38
- import { computed, useAttrs, useId } from 'vue'
38
+ import { inject, ref, useAttrs, watchEffect } from 'vue'
39
39
 
40
40
  type InputAccent = 'brand' | 'warning' | 'danger'
41
41
  type InputType = 'text' | 'number' | 'password' | 'search'
@@ -44,135 +44,152 @@ defineOptions({
44
44
  inheritAttrs: false,
45
45
  })
46
46
 
47
- const { accent, id = useId() } = defineProps<{
47
+ const {
48
+ accent: _accent,
49
+ required,
50
+ disabled,
51
+ id,
52
+ } = defineProps<{
48
53
  accent: InputAccent
49
- label?: string
50
- info?: string
51
54
  id?: string
52
- disabled?: boolean
53
- clearable?: boolean
54
- href?: string
55
55
  required?: boolean
56
- labelIcon?: IconDefinition
57
- icon?: IconDefinition
56
+ disabled?: boolean
58
57
  type?: InputType
58
+ icon?: IconDefinition
59
+ rightIcon?: IconDefinition
60
+ clearable?: boolean
59
61
  }>()
60
62
 
61
63
  const modelValue = defineModel<string | number>({ required: true })
62
64
 
63
65
  const slots = defineSlots<{
64
- default?(): any
65
- info?(): any
66
+ 'right-icon'?(): any
66
67
  }>()
67
68
 
68
69
  const attrs = useAttrs()
69
70
 
70
- const labelAccent = computed(() => (accent === 'brand' ? 'neutral' : accent))
71
+ const inputRef = ref<HTMLInputElement>()
72
+
73
+ const wrapperController = inject(IK_INPUT_WRAPPER_CONTROLLER, undefined)
74
+
75
+ const accent = useMapper<LabelAccent, InputAccent>(
76
+ () => wrapperController?.labelAccent,
77
+ {
78
+ neutral: _accent,
79
+ warning: 'warning',
80
+ danger: 'danger',
81
+ },
82
+ () => _accent
83
+ )
84
+
85
+ if (wrapperController) {
86
+ watchEffect(() => {
87
+ wrapperController.required = required
88
+ })
89
+ }
90
+
91
+ function focus() {
92
+ inputRef.value?.focus()
93
+ }
94
+
95
+ function clear() {
96
+ modelValue.value = ''
97
+ focus()
98
+ }
99
+
100
+ defineExpose({ focus })
71
101
  </script>
72
102
 
73
103
  <style lang="postcss" scoped>
74
- /* IMPLEMENTATION */
75
104
  .ui-input {
76
- position: relative;
77
105
  display: flex;
78
- flex-direction: column;
79
- gap: 0.4rem;
80
- flex: 1;
106
+ align-items: center;
107
+ gap: 1.6rem;
108
+ border-radius: 0.4rem;
109
+ border-width: 0.1rem;
110
+ border-style: solid;
111
+ background-color: var(--color-neutral-background-primary);
112
+ color: var(--color-neutral-txt-primary);
113
+ height: 4rem;
114
+ width: 100%;
115
+ padding-inline: 1.6rem;
116
+
117
+ .left-icon,
118
+ .right-icon {
119
+ pointer-events: none;
120
+ color: var(--color-neutral-txt-secondary);
121
+ }
122
+
123
+ &:not(.disabled) .right-icon {
124
+ color: var(--color-brand-item-base);
125
+ }
81
126
 
82
127
  .input {
83
- border-radius: 0.4rem;
84
- border-width: 0.1rem;
85
- border-style: solid;
86
- background-color: var(--color-neutral-background-primary);
87
- color: var(--color-neutral-txt-primary);
88
- height: 4rem;
128
+ background-color: transparent;
129
+ border: none;
89
130
  outline: none;
90
- width: 100%;
91
- padding-block: 0.8rem;
92
- padding-inline: 1.6rem;
131
+ flex: 1;
93
132
 
94
133
  &::placeholder {
95
134
  color: var(--color-neutral-txt-secondary);
96
135
  }
97
-
98
- &:focus {
99
- border-width: 0.2rem;
100
- }
101
-
102
- &:has(+ .after) {
103
- padding-inline-end: 4.8rem;
104
- }
105
136
  }
106
137
 
107
138
  /* VARIANT */
108
139
 
109
140
  &.accent--brand {
110
- .input {
111
- border-color: var(--color-neutral-border);
141
+ border-color: var(--color-neutral-border);
112
142
 
113
- &:hover {
114
- border-color: var(--color-brand-item-hover);
115
- }
143
+ &:focus-within {
144
+ border-color: var(--color-brand-item-base);
145
+ }
116
146
 
117
- &:focus {
118
- border-color: var(--color-brand-item-base);
119
- }
147
+ &:hover {
148
+ border-color: var(--color-brand-item-hover);
149
+ }
120
150
 
121
- &:active {
122
- border-color: var(--color-brand-item-active);
123
- }
151
+ &:has(input:active) {
152
+ border-color: var(--color-brand-item-active);
153
+ }
124
154
 
125
- &:disabled {
126
- border-color: var(--color-neutral-border);
127
- background-color: var(--color-neutral-background-disabled);
128
- }
155
+ &:has(input:disabled) {
156
+ border-color: var(--color-neutral-border);
157
+ background-color: var(--color-neutral-background-disabled);
129
158
  }
130
159
  }
131
160
 
132
161
  &.accent--warning {
133
- .input {
134
- border-color: var(--color-warning-item-base);
162
+ border-color: var(--color-warning-item-base);
135
163
 
136
- &:disabled {
137
- border-color: var(--color-neutral-border);
138
- background-color: var(--color-neutral-background-disabled);
139
- }
164
+ &:hover {
165
+ border-color: var(--color-warning-item-hover);
140
166
  }
141
- }
142
167
 
143
- &.accent--danger {
144
- .input {
145
- border-color: var(--color-danger-item-base);
168
+ &:has(input:active) {
169
+ border-color: var(--color-warning-item-active);
170
+ }
146
171
 
147
- &:disabled {
148
- border-color: var(--color-neutral-border);
149
- background-color: var(--color-neutral-background-disabled);
150
- }
172
+ &:has(input:disabled) {
173
+ border-color: var(--color-neutral-border);
174
+ background-color: var(--color-neutral-background-disabled);
151
175
  }
152
176
  }
153
177
 
154
- /* ICON POSITION */
155
-
156
- .before + .input {
157
- padding-inline-start: 4.8rem;
158
- }
178
+ &.accent--danger {
179
+ border-color: var(--color-danger-item-base);
159
180
 
160
- .before,
161
- .after {
162
- position: absolute;
163
- inset-block: 1.2rem;
164
- }
181
+ &:hover {
182
+ border-color: var(--color-danger-item-hover);
183
+ }
165
184
 
166
- .before {
167
- color: var(--color-neutral-txt-secondary);
168
- inset-inline-start: 1.6rem;
169
- pointer-events: none;
170
- z-index: 1;
171
- }
185
+ &:has(input:active) {
186
+ border-color: var(--color-danger-item-active);
187
+ }
172
188
 
173
- .after {
174
- inset-inline-end: 1.6rem;
175
- cursor: pointer;
189
+ &:has(input:disabled) {
190
+ border-color: var(--color-neutral-border);
191
+ background-color: var(--color-neutral-background-disabled);
192
+ }
176
193
  }
177
194
  }
178
195
  </style>
@@ -1,10 +1,10 @@
1
1
  import { useUiStore } from '@core/stores/ui.store'
2
- import { storeToRefs } from 'pinia'
2
+ import { storeToRefs, type Pinia } from 'pinia'
3
3
  import { computed, provide, ref, watch } from 'vue'
4
4
  import { THEME_KEY } from 'vue-echarts'
5
5
 
6
- export const useChartTheme = () => {
7
- const { colorMode } = storeToRefs(useUiStore())
6
+ export const useChartTheme = (pinia?: Pinia) => {
7
+ const { colorMode } = storeToRefs(useUiStore(pinia))
8
8
 
9
9
  const style = window.getComputedStyle(window.document.documentElement)
10
10
 
@@ -54,7 +54,7 @@ export default function useRelativeTime(fromDate: MaybeRef<Date>, toDate: MaybeR
54
54
  parts.push(t('relative-time.minute', { n: minutes }))
55
55
  }
56
56
 
57
- if (days === 0 && seconds > 0) {
57
+ if (minutes === 0 && seconds > 0) {
58
58
  parts.push(t('relative-time.second', { n: seconds }))
59
59
  }
60
60
  }
package/lib/i18n.ts CHANGED
@@ -49,6 +49,10 @@ export const locales: Locales = {
49
49
  code: 'uk',
50
50
  name: 'Українська',
51
51
  },
52
+ nl: {
53
+ code: 'nl',
54
+ name: 'Nederlands',
55
+ },
52
56
  }
53
57
 
54
58
  export default createI18n({