@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.
- package/lib/assets/css/_colors.pcss +125 -0
- package/lib/assets/css/_context.pcss +99 -0
- package/lib/assets/css/_fonts.pcss +6 -0
- package/lib/assets/css/_reset.pcss +42 -0
- package/lib/assets/css/_shadows.pcss +36 -0
- package/lib/assets/css/_typography.pcss +6 -0
- package/lib/assets/css/base.pcss +91 -0
- package/lib/assets/css/typography/_legacy.pcss +123 -0
- package/lib/assets/css/typography/_letter-spacing.pcss +27 -0
- package/lib/assets/css/typography/_line-height.pcss +19 -0
- package/lib/assets/css/typography/_size.pcss +95 -0
- package/lib/assets/css/typography/_style.pcss +34 -0
- package/lib/assets/css/typography/_weight.pcss +57 -0
- package/lib/assets/user.png +0 -0
- package/lib/components/PowerStateIcon.vue +46 -0
- package/lib/components/StatusPill.vue +32 -0
- package/lib/components/UiCounter.vue +89 -0
- package/lib/components/UiSpinner.vue +48 -0
- package/lib/components/UiTag.vue +97 -0
- package/lib/components/button/ButtonGroup.vue +33 -0
- package/lib/components/button/ButtonIcon.vue +199 -0
- package/lib/components/button/UiButton.vue +207 -0
- package/lib/components/chip/ChipIcon.vue +29 -0
- package/lib/components/chip/ChipRemoveIcon.vue +13 -0
- package/lib/components/chip/UiChip.vue +138 -0
- package/lib/components/dropdown/DropdownItem.vue +192 -0
- package/lib/components/dropdown/DropdownList.vue +31 -0
- package/lib/components/dropdown/DropdownTitle.vue +65 -0
- package/lib/components/icon/ComplexIcon.vue +51 -0
- package/lib/components/icon/ObjectIcon.vue +243 -0
- package/lib/components/icon/UiIcon.vue +47 -0
- package/lib/components/icon/VmIcon.vue +30 -0
- package/lib/components/layout/LayoutSidebar.vue +100 -0
- package/lib/components/menu/MenuItem.vue +82 -0
- package/lib/components/menu/MenuList.vue +104 -0
- package/lib/components/menu/MenuSeparator.vue +27 -0
- package/lib/components/menu/MenuTrigger.vue +52 -0
- package/lib/components/tab/TabItem.vue +100 -0
- package/lib/components/tab/TabList.vue +32 -0
- package/lib/components/tooltip/TooltipItem.vue +80 -0
- package/lib/components/tooltip/TooltipList.vue +15 -0
- package/lib/components/tree/TreeItem.vue +34 -0
- package/lib/components/tree/TreeItemError.vue +13 -0
- package/lib/components/tree/TreeItemLabel.vue +128 -0
- package/lib/components/tree/TreeLine.vue +51 -0
- package/lib/components/tree/TreeList.vue +14 -0
- package/lib/components/tree/TreeLoadingItem.vue +64 -0
- package/lib/components/user/UserLink.vue +72 -0
- package/lib/components/user/UserLogo.vue +57 -0
- package/lib/composables/context.composable.ts +34 -0
- package/lib/composables/tree/branch-definition.ts +17 -0
- package/lib/composables/tree/branch.ts +143 -0
- package/lib/composables/tree/build-nodes.ts +20 -0
- package/lib/composables/tree/define-branch.ts +23 -0
- package/lib/composables/tree/define-leaf.ts +16 -0
- package/lib/composables/tree/define-tree.ts +65 -0
- package/lib/composables/tree/leaf-definition.ts +8 -0
- package/lib/composables/tree/leaf.ts +34 -0
- package/lib/composables/tree/tree-node-base.ts +103 -0
- package/lib/composables/tree/tree-node-definition-base.ts +12 -0
- package/lib/composables/tree/types.ts +92 -0
- package/lib/composables/tree-filter.composable.ts +12 -0
- package/lib/composables/tree.composable.ts +85 -0
- package/lib/context.ts +10 -0
- package/lib/directives/tooltip.directive.md +117 -0
- package/lib/directives/tooltip.directive.ts +52 -0
- package/lib/i18n.ts +158 -0
- package/lib/layouts/CoreLayout.vue +182 -0
- package/lib/locales/de.json +6 -0
- package/lib/locales/en.json +15 -0
- package/lib/locales/fr.json +15 -0
- package/lib/stores/panel.store.ts +12 -0
- package/lib/stores/sidebar.store.ts +63 -0
- package/lib/stores/tooltip.store.ts +74 -0
- package/lib/stores/ui.store.ts +34 -0
- package/lib/types/button.type.ts +3 -0
- package/lib/types/color.type.ts +5 -0
- package/lib/types/object-icon.type.ts +43 -0
- package/lib/types/power-state.type.ts +1 -0
- package/lib/types/size.type.ts +3 -0
- package/lib/types/subscribable-store.type.ts +21 -0
- package/lib/types/utility.type.ts +1 -0
- package/lib/utils/create-subscribable-store-context.util.ts +66 -0
- package/lib/utils/has-ellipsis.util.ts +11 -0
- package/lib/utils/if-else.utils.ts +27 -0
- package/lib/utils/injection-keys.util.ts +17 -0
- package/lib/utils/sort-by-name-label.util.ts +6 -0
- package/lib/utils/to-array.utils.ts +9 -0
- package/lib/utils/unique-id.util.ts +8 -0
- package/package.json +45 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { buildNodes } from '@core/composables/tree/build-nodes'
|
|
2
|
+
import type {
|
|
3
|
+
DefinitionToTreeNode,
|
|
4
|
+
TreeContext,
|
|
5
|
+
TreeNode,
|
|
6
|
+
TreeNodeDefinition,
|
|
7
|
+
TreeNodeId,
|
|
8
|
+
UseTreeOptions,
|
|
9
|
+
} from '@core/composables/tree/types'
|
|
10
|
+
import { computed, type MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
|
|
11
|
+
|
|
12
|
+
export function useTree<
|
|
13
|
+
TDefinition extends TreeNodeDefinition,
|
|
14
|
+
TTreeNode extends DefinitionToTreeNode<TDefinition> = DefinitionToTreeNode<TDefinition>,
|
|
15
|
+
>(definitions: MaybeRefOrGetter<TDefinition[]>, options: UseTreeOptions = {}) {
|
|
16
|
+
const selectedIds = ref(new Set<TreeNodeId>())
|
|
17
|
+
const expandedIds = ref(new Set<TreeNodeId>())
|
|
18
|
+
const activeId = ref<TreeNodeId>()
|
|
19
|
+
|
|
20
|
+
const context = reactive({
|
|
21
|
+
allowMultiSelect: options.allowMultiSelect ?? false,
|
|
22
|
+
selectedIds,
|
|
23
|
+
expandedIds,
|
|
24
|
+
activeId,
|
|
25
|
+
}) as TreeContext
|
|
26
|
+
|
|
27
|
+
const nodes = computed(() => {
|
|
28
|
+
const nodes = buildNodes<TDefinition, TTreeNode>(toValue(definitions), context)
|
|
29
|
+
|
|
30
|
+
if (options.expand !== false) {
|
|
31
|
+
nodes.forEach(node => node.isBranch && node.toggleExpand(true, true))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return nodes
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const nodesMap = computed(() => {
|
|
38
|
+
const nodeMap = new Map<TreeNodeId, TreeNode>()
|
|
39
|
+
|
|
40
|
+
function traverse(node: TreeNode) {
|
|
41
|
+
nodeMap.set(node.id, node)
|
|
42
|
+
|
|
43
|
+
if (node.isBranch) {
|
|
44
|
+
node.rawChildren.forEach(traverse)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
nodes.value.forEach(traverse)
|
|
49
|
+
|
|
50
|
+
return nodeMap
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const visibleNodes = computed(() => nodes.value.filter(node => !node.isExcluded))
|
|
54
|
+
|
|
55
|
+
const getNode = (id: TreeNodeId | undefined) => (id !== undefined ? nodesMap.value.get(id) : undefined)
|
|
56
|
+
const getNodes = (ids: TreeNodeId[]) => ids.map(getNode).filter(node => node !== undefined) as TreeNode[]
|
|
57
|
+
|
|
58
|
+
const selectedNodes = computed(() => getNodes(Array.from(selectedIds.value.values())))
|
|
59
|
+
const expandedNodes = computed(() => getNodes(Array.from(expandedIds.value.values())))
|
|
60
|
+
const activeNode = computed(() => getNode(activeId.value))
|
|
61
|
+
|
|
62
|
+
const selectedLabel = computed(() => {
|
|
63
|
+
if (typeof options.selectedLabel === 'function') {
|
|
64
|
+
return options.selectedLabel(selectedNodes.value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof options.selectedLabel === 'object' && selectedNodes.value.length > options.selectedLabel.max) {
|
|
68
|
+
return options.selectedLabel.fn(selectedNodes.value.length)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return selectedNodes.value.map(node => node.label).join(', ')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
nodes: visibleNodes,
|
|
76
|
+
activeId,
|
|
77
|
+
activeNode,
|
|
78
|
+
selectedIds,
|
|
79
|
+
selectedNodes,
|
|
80
|
+
selectedLabel,
|
|
81
|
+
expandedIds,
|
|
82
|
+
expandedNodes,
|
|
83
|
+
options,
|
|
84
|
+
}
|
|
85
|
+
}
|
package/lib/context.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext } from '@core/composables/context.composable'
|
|
2
|
+
import type { Color } from '@core/types/color.type'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
export const DisabledContext = createContext(false)
|
|
6
|
+
|
|
7
|
+
export const ColorContext = createContext('info' as Color, (color, previousColor) => ({
|
|
8
|
+
name: color,
|
|
9
|
+
colorContextClass: computed(() => (previousColor.value === color.value ? undefined : `color-context-${color.value}`)),
|
|
10
|
+
}))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Tooltip Directive
|
|
2
|
+
|
|
3
|
+
By default, the tooltip will appear centered above the target element.
|
|
4
|
+
|
|
5
|
+
## Directive argument
|
|
6
|
+
|
|
7
|
+
The directive argument is **optional** and can be either:
|
|
8
|
+
|
|
9
|
+
- The tooltip [content](#tooltip-content)
|
|
10
|
+
- An object containing:
|
|
11
|
+
- `content`: The tooltip [content](#tooltip-content)
|
|
12
|
+
- `placement`: The tooltip [placement](#tooltip-placement)
|
|
13
|
+
- `selector`: A descendant [selector](#tooltip-selector)
|
|
14
|
+
- `vertical`: A boolean to enable [vertical mode](#tooltip-vertical)
|
|
15
|
+
|
|
16
|
+
## Automatic mode
|
|
17
|
+
|
|
18
|
+
When the tooltip content is `true` or `undefined`, the directive will check the target element to see if it has text
|
|
19
|
+
overflow.
|
|
20
|
+
|
|
21
|
+
If so, the directive will automatically create a tooltip with the target element's text content.
|
|
22
|
+
|
|
23
|
+
By default, the target element is the one the directive is attached to.
|
|
24
|
+
|
|
25
|
+
This can be changed by using the [`selector`](#tooltip-selector) option.
|
|
26
|
+
|
|
27
|
+
## Tooltip `content`
|
|
28
|
+
|
|
29
|
+
The tooltip content can be either:
|
|
30
|
+
|
|
31
|
+
- `true` or `undefined` to enable the [automatic mode](#automatic-mode).
|
|
32
|
+
- `false` or an empty-string to disable the tooltip
|
|
33
|
+
- Non-empty string to enable the tooltip and use the string as content.
|
|
34
|
+
|
|
35
|
+
## Tooltip `placement`
|
|
36
|
+
|
|
37
|
+
Tooltip can be placed in the following positions:
|
|
38
|
+
|
|
39
|
+
- `top`
|
|
40
|
+
- `top-start`
|
|
41
|
+
- `top-end`
|
|
42
|
+
- `bottom`
|
|
43
|
+
- `bottom-start`
|
|
44
|
+
- `bottom-end`
|
|
45
|
+
- `left`
|
|
46
|
+
- `left-start`
|
|
47
|
+
- `left-end`
|
|
48
|
+
- `right`
|
|
49
|
+
- `right-start`
|
|
50
|
+
- `right-end`
|
|
51
|
+
|
|
52
|
+
## Tooltip `selector`
|
|
53
|
+
|
|
54
|
+
When in automatic mode, by default, the directive will check if the element on which is attached the directive has text
|
|
55
|
+
overflow.
|
|
56
|
+
|
|
57
|
+
If you want to check the overflow of a descendant element, you can use the `selector` option.
|
|
58
|
+
|
|
59
|
+
## Tooltip `vertical`
|
|
60
|
+
|
|
61
|
+
By default, the overflow check is done horizontally.
|
|
62
|
+
|
|
63
|
+
If you want to check the vertical overflow, you can set the `vertical` option to `true`.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<template>
|
|
69
|
+
<!-- True -->
|
|
70
|
+
<div v-tooltip="true" class="label">This content will be ellipsized by CSS but displayed entirely in the tooltip</div>
|
|
71
|
+
|
|
72
|
+
<!-- Undefined / Unset -->
|
|
73
|
+
<div v-tooltip class="label">This content will be ellipsized by CSS but displayed entirely in the tooltip</div>
|
|
74
|
+
|
|
75
|
+
<!-- String -->
|
|
76
|
+
<div v-tooltip="'Tooltip content'" class="label">This item will have "Tooltip content" as tooltip</div>
|
|
77
|
+
|
|
78
|
+
<!-- Object -->
|
|
79
|
+
<div v-tooltip="{ content: 'Foobar', placement: 'left-end' }" class="label">
|
|
80
|
+
This item will have "Foobar" as tooltip and the tooltip will be placed at the bottom left of the item
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Dynamic -->
|
|
84
|
+
<div v-tooltip="myTooltip" class="label">This item will have the content of `myTooltip` as tooltip</div>
|
|
85
|
+
|
|
86
|
+
<!-- Conditional -->
|
|
87
|
+
<div v-tooltip="isTooltipEnabled && 'Foobar'" class="label">
|
|
88
|
+
This item will have "Foobar" as tooltip if `isTooltipEnabled` is true
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Selector -->
|
|
92
|
+
<div v-tooltip="{ selector: '.label' }">
|
|
93
|
+
Before
|
|
94
|
+
<div class="label">
|
|
95
|
+
This content will be ellipsized by CSS but displayed entirely in the tooltip attached to the parent element
|
|
96
|
+
</div>
|
|
97
|
+
After
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<script setup>
|
|
102
|
+
import { ref } from 'vue'
|
|
103
|
+
import { vTooltip } from '@core/directives/tooltip.directive'
|
|
104
|
+
|
|
105
|
+
const myTooltip = ref('Content') // or ref({ content: "Content", placement: "left-end" })
|
|
106
|
+
const isTooltipEnabled = ref(true)
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<style scoped>
|
|
110
|
+
div {
|
|
111
|
+
max-width: 100px;
|
|
112
|
+
white-space: nowrap;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
text-overflow: ellipsis;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TooltipEvents, TooltipOptions } from '@core/stores/tooltip.store'
|
|
2
|
+
import { useTooltipStore } from '@core/stores/tooltip.store'
|
|
3
|
+
import { isObject } from 'lodash-es'
|
|
4
|
+
import type { Options } from 'placement.js'
|
|
5
|
+
import type { Directive } from 'vue'
|
|
6
|
+
|
|
7
|
+
export type TooltipDirectiveContent = undefined | boolean | string
|
|
8
|
+
|
|
9
|
+
type TooltipDirectiveOptions =
|
|
10
|
+
| TooltipDirectiveContent
|
|
11
|
+
| {
|
|
12
|
+
content?: TooltipDirectiveContent
|
|
13
|
+
placement?: Options['placement']
|
|
14
|
+
selector?: string
|
|
15
|
+
vertical?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parseOptions = (options: TooltipDirectiveOptions): TooltipOptions => {
|
|
19
|
+
const {
|
|
20
|
+
placement,
|
|
21
|
+
content,
|
|
22
|
+
selector,
|
|
23
|
+
vertical = false,
|
|
24
|
+
} = isObject(options) ? options : { placement: undefined, content: options, selector: undefined, vertical: false }
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
placement,
|
|
28
|
+
content,
|
|
29
|
+
selector,
|
|
30
|
+
vertical,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const vTooltip: Directive<HTMLElement, TooltipDirectiveOptions> = {
|
|
35
|
+
mounted(target, binding) {
|
|
36
|
+
const store = useTooltipStore()
|
|
37
|
+
|
|
38
|
+
const events: TooltipEvents = binding.modifiers.focus
|
|
39
|
+
? { on: 'focusin', off: 'focusout' }
|
|
40
|
+
: { on: 'mouseenter', off: 'mouseleave' }
|
|
41
|
+
|
|
42
|
+
store.register(target, parseOptions(binding.value), events)
|
|
43
|
+
},
|
|
44
|
+
updated(target, binding) {
|
|
45
|
+
const store = useTooltipStore()
|
|
46
|
+
store.updateOptions(target, parseOptions(binding.value))
|
|
47
|
+
},
|
|
48
|
+
beforeUnmount(target) {
|
|
49
|
+
const store = useTooltipStore()
|
|
50
|
+
store.unregister(target)
|
|
51
|
+
},
|
|
52
|
+
}
|
package/lib/i18n.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createI18n } from 'vue-i18n'
|
|
2
|
+
import messages from '@intlify/unplugin-vue-i18n/messages'
|
|
3
|
+
|
|
4
|
+
interface Locales {
|
|
5
|
+
[key: string]: {
|
|
6
|
+
code: string
|
|
7
|
+
name: string
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const locales: Locales = {
|
|
12
|
+
en: {
|
|
13
|
+
code: 'en',
|
|
14
|
+
name: 'English',
|
|
15
|
+
},
|
|
16
|
+
fr: {
|
|
17
|
+
code: 'fr',
|
|
18
|
+
name: 'Français',
|
|
19
|
+
},
|
|
20
|
+
de: {
|
|
21
|
+
code: 'de',
|
|
22
|
+
name: 'Deutsch',
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default createI18n({
|
|
27
|
+
locale: localStorage.getItem('lang') ?? 'en',
|
|
28
|
+
fallbackLocale: 'en',
|
|
29
|
+
messages,
|
|
30
|
+
datetimeFormats: {
|
|
31
|
+
en: {
|
|
32
|
+
date_short: {
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
month: 'numeric',
|
|
35
|
+
day: 'numeric',
|
|
36
|
+
},
|
|
37
|
+
date_medium: {
|
|
38
|
+
year: 'numeric',
|
|
39
|
+
month: 'short',
|
|
40
|
+
day: 'numeric',
|
|
41
|
+
},
|
|
42
|
+
date_long: {
|
|
43
|
+
year: 'numeric',
|
|
44
|
+
month: 'long',
|
|
45
|
+
day: 'numeric',
|
|
46
|
+
},
|
|
47
|
+
datetime_short: {
|
|
48
|
+
year: 'numeric',
|
|
49
|
+
month: 'numeric',
|
|
50
|
+
day: 'numeric',
|
|
51
|
+
hour: '2-digit',
|
|
52
|
+
minute: '2-digit',
|
|
53
|
+
},
|
|
54
|
+
datetime_medium: {
|
|
55
|
+
year: 'numeric',
|
|
56
|
+
month: 'short',
|
|
57
|
+
day: 'numeric',
|
|
58
|
+
hour: '2-digit',
|
|
59
|
+
minute: '2-digit',
|
|
60
|
+
},
|
|
61
|
+
datetime_long: {
|
|
62
|
+
year: 'numeric',
|
|
63
|
+
month: 'long',
|
|
64
|
+
day: 'numeric',
|
|
65
|
+
hour: '2-digit',
|
|
66
|
+
minute: '2-digit',
|
|
67
|
+
},
|
|
68
|
+
time: {
|
|
69
|
+
hour: '2-digit',
|
|
70
|
+
minute: '2-digit',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
fr: {
|
|
74
|
+
date_short: {
|
|
75
|
+
year: 'numeric',
|
|
76
|
+
month: 'numeric',
|
|
77
|
+
day: 'numeric',
|
|
78
|
+
},
|
|
79
|
+
date_medium: {
|
|
80
|
+
year: 'numeric',
|
|
81
|
+
month: 'short',
|
|
82
|
+
day: 'numeric',
|
|
83
|
+
},
|
|
84
|
+
date_long: {
|
|
85
|
+
year: 'numeric',
|
|
86
|
+
month: 'long',
|
|
87
|
+
day: 'numeric',
|
|
88
|
+
},
|
|
89
|
+
datetime_short: {
|
|
90
|
+
year: 'numeric',
|
|
91
|
+
month: 'numeric',
|
|
92
|
+
day: 'numeric',
|
|
93
|
+
hour: '2-digit',
|
|
94
|
+
minute: '2-digit',
|
|
95
|
+
},
|
|
96
|
+
datetime_medium: {
|
|
97
|
+
year: 'numeric',
|
|
98
|
+
month: 'short',
|
|
99
|
+
day: 'numeric',
|
|
100
|
+
hour: '2-digit',
|
|
101
|
+
minute: '2-digit',
|
|
102
|
+
},
|
|
103
|
+
datetime_long: {
|
|
104
|
+
year: 'numeric',
|
|
105
|
+
month: 'long',
|
|
106
|
+
day: 'numeric',
|
|
107
|
+
hour: '2-digit',
|
|
108
|
+
minute: '2-digit',
|
|
109
|
+
},
|
|
110
|
+
time: {
|
|
111
|
+
hour: '2-digit',
|
|
112
|
+
minute: '2-digit',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
de: {
|
|
116
|
+
date_short: {
|
|
117
|
+
year: 'numeric',
|
|
118
|
+
month: 'numeric',
|
|
119
|
+
day: 'numeric',
|
|
120
|
+
},
|
|
121
|
+
date_medium: {
|
|
122
|
+
year: 'numeric',
|
|
123
|
+
month: 'short',
|
|
124
|
+
day: 'numeric',
|
|
125
|
+
},
|
|
126
|
+
date_long: {
|
|
127
|
+
year: 'numeric',
|
|
128
|
+
month: 'long',
|
|
129
|
+
day: 'numeric',
|
|
130
|
+
},
|
|
131
|
+
datetime_short: {
|
|
132
|
+
year: 'numeric',
|
|
133
|
+
month: 'numeric',
|
|
134
|
+
day: 'numeric',
|
|
135
|
+
hour: '2-digit',
|
|
136
|
+
minute: '2-digit',
|
|
137
|
+
},
|
|
138
|
+
datetime_medium: {
|
|
139
|
+
year: 'numeric',
|
|
140
|
+
month: 'short',
|
|
141
|
+
day: 'numeric',
|
|
142
|
+
hour: '2-digit',
|
|
143
|
+
minute: '2-digit',
|
|
144
|
+
},
|
|
145
|
+
datetime_long: {
|
|
146
|
+
year: 'numeric',
|
|
147
|
+
month: 'long',
|
|
148
|
+
day: 'numeric',
|
|
149
|
+
hour: '2-digit',
|
|
150
|
+
minute: '2-digit',
|
|
151
|
+
},
|
|
152
|
+
time: {
|
|
153
|
+
hour: '2-digit',
|
|
154
|
+
minute: '2-digit',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="core-layout">
|
|
3
|
+
<header class="header">
|
|
4
|
+
<slot name="app-logo" />
|
|
5
|
+
<UiButtonIcon
|
|
6
|
+
v-tooltip="{
|
|
7
|
+
content: sidebarStore.isExpanded ? $t('core.sidebar.close') : $t('core.sidebar.open'),
|
|
8
|
+
placement: 'right',
|
|
9
|
+
}"
|
|
10
|
+
:icon="sidebarStore.isExpanded ? faAngleDoubleLeft : faBars"
|
|
11
|
+
class="sidebar-toggle"
|
|
12
|
+
@click="sidebarStore.toggleExpand()"
|
|
13
|
+
/>
|
|
14
|
+
<slot name="app-header" />
|
|
15
|
+
</header>
|
|
16
|
+
<div class="container">
|
|
17
|
+
<div
|
|
18
|
+
v-if="sidebarStore.isExpanded && !sidebarStore.isLocked"
|
|
19
|
+
class="sidebar-overlay"
|
|
20
|
+
@click="sidebarStore.toggleExpand(false)"
|
|
21
|
+
/>
|
|
22
|
+
<LayoutSidebar class="sidebar">
|
|
23
|
+
<template #header>
|
|
24
|
+
<slot name="sidebar-header" />
|
|
25
|
+
</template>
|
|
26
|
+
<template #default>
|
|
27
|
+
<slot name="sidebar-content" />
|
|
28
|
+
</template>
|
|
29
|
+
<template #footer>
|
|
30
|
+
<slot name="sidebar-footer" />
|
|
31
|
+
</template>
|
|
32
|
+
</LayoutSidebar>
|
|
33
|
+
<div class="main-container">
|
|
34
|
+
<header>
|
|
35
|
+
<slot name="content-header" />
|
|
36
|
+
</header>
|
|
37
|
+
<main class="main">
|
|
38
|
+
<div class="content">
|
|
39
|
+
<slot name="content" />
|
|
40
|
+
</div>
|
|
41
|
+
<div v-if="isPanelVisible" :class="{ mobile: uiStore.isMobile }" class="panel">
|
|
42
|
+
<header v-if="$slots['panel-header'] || uiStore.isMobile" class="panel-header">
|
|
43
|
+
<UiButtonIcon
|
|
44
|
+
v-if="uiStore.isMobile"
|
|
45
|
+
:icon="faAngleLeft"
|
|
46
|
+
class="panel-close-icon"
|
|
47
|
+
@click="panelStore.close()"
|
|
48
|
+
/>
|
|
49
|
+
<slot name="panel-header" />
|
|
50
|
+
</header>
|
|
51
|
+
<div v-if="$slots['panel-content']" class="panel-content">
|
|
52
|
+
<slot name="panel-content" />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</main>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script lang="ts" setup>
|
|
62
|
+
import UiButtonIcon from '@core/components/button/ButtonIcon.vue'
|
|
63
|
+
import LayoutSidebar from '@core/components/layout/LayoutSidebar.vue'
|
|
64
|
+
import { vTooltip } from '@core/directives/tooltip.directive'
|
|
65
|
+
import { usePanelStore } from '@core/stores/panel.store'
|
|
66
|
+
import { useSidebarStore } from '@core/stores/sidebar.store'
|
|
67
|
+
import { useUiStore } from '@core/stores/ui.store'
|
|
68
|
+
import { faAngleDoubleLeft, faAngleLeft, faBars } from '@fortawesome/free-solid-svg-icons'
|
|
69
|
+
import { computed } from 'vue'
|
|
70
|
+
|
|
71
|
+
const sidebarStore = useSidebarStore()
|
|
72
|
+
const panelStore = usePanelStore()
|
|
73
|
+
const uiStore = useUiStore()
|
|
74
|
+
|
|
75
|
+
const slots = defineSlots<{
|
|
76
|
+
'app-logo'(): any
|
|
77
|
+
'app-header'(): any
|
|
78
|
+
'sidebar-header'(): any
|
|
79
|
+
'sidebar-content'(): any
|
|
80
|
+
'sidebar-footer'(): any
|
|
81
|
+
'content-header'(): any
|
|
82
|
+
content(): any
|
|
83
|
+
'panel-header'(): any
|
|
84
|
+
'panel-content'(): any
|
|
85
|
+
}>()
|
|
86
|
+
|
|
87
|
+
const isPanelVisible = computed(() => {
|
|
88
|
+
if (!slots['panel-header'] && !slots['panel-content']) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return panelStore.isExpanded
|
|
93
|
+
})
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<style lang="postcss" scoped>
|
|
97
|
+
.sidebar-overlay {
|
|
98
|
+
position: fixed;
|
|
99
|
+
inset: 0;
|
|
100
|
+
z-index: 1000;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.core-layout {
|
|
104
|
+
display: flex;
|
|
105
|
+
height: 100dvh;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.container {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex: 1;
|
|
112
|
+
min-height: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.header {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
height: 5.6rem;
|
|
119
|
+
background-color: var(--background-color-secondary);
|
|
120
|
+
border-bottom: 0.1rem solid var(--color-grey-500);
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
gap: 1.6rem;
|
|
123
|
+
padding: 0 1.6rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.sidebar-toggle {
|
|
127
|
+
margin-right: auto;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.main-container {
|
|
131
|
+
flex: 1;
|
|
132
|
+
overflow: auto;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.main {
|
|
138
|
+
background-color: var(--background-color-secondary);
|
|
139
|
+
display: flex;
|
|
140
|
+
flex: 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.content {
|
|
144
|
+
padding: 0.8rem;
|
|
145
|
+
flex: 1;
|
|
146
|
+
border-right: 0.1rem solid var(--color-grey-500);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.panel {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
width: 40rem;
|
|
153
|
+
background-color: var(--background-color-secondary);
|
|
154
|
+
|
|
155
|
+
&.mobile {
|
|
156
|
+
width: 100%;
|
|
157
|
+
position: fixed;
|
|
158
|
+
inset: 0 0 0 auto;
|
|
159
|
+
z-index: 1000;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.panel-header {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
padding: 0.4rem 1.6rem;
|
|
167
|
+
background-color: var(--background-color-primary);
|
|
168
|
+
border-bottom: 0.1rem solid var(--color-grey-500);
|
|
169
|
+
min-height: 4.8rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.panel-close-icon {
|
|
173
|
+
margin-right: auto;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.panel-content {
|
|
177
|
+
flex: 1;
|
|
178
|
+
padding: 0.8rem;
|
|
179
|
+
overflow: auto;
|
|
180
|
+
min-height: 0;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"core": {
|
|
3
|
+
"close": "Close",
|
|
4
|
+
"log-out": "Log out",
|
|
5
|
+
"master": "Primary host",
|
|
6
|
+
"open": "Open",
|
|
7
|
+
"quick-actions": "Quick actions",
|
|
8
|
+
"sidebar": {
|
|
9
|
+
"close": "Close sidebar",
|
|
10
|
+
"lock": "Lock sidebar open",
|
|
11
|
+
"open": "Open sidebar",
|
|
12
|
+
"unlock": "Unlock sidebar"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"core": {
|
|
3
|
+
"close": "Fermer",
|
|
4
|
+
"log-out": "Se déconnecter",
|
|
5
|
+
"master": "Hôte primaire",
|
|
6
|
+
"open": "Ouvrir",
|
|
7
|
+
"quick-actions": "Actions rapides",
|
|
8
|
+
"sidebar": {
|
|
9
|
+
"close": "Fermer la barre latérale",
|
|
10
|
+
"lock": "Verrouiller la barre latérale",
|
|
11
|
+
"open": "Ouvrir la barre latérale",
|
|
12
|
+
"unlock": "Déverrouiller la barre latérale"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useUiStore } from '@core/stores/ui.store'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
export const usePanelStore = defineStore('panel', () => {
|
|
6
|
+
const uiStore = useUiStore()
|
|
7
|
+
const isExpanded = ref(false)
|
|
8
|
+
const open = () => (isExpanded.value = true)
|
|
9
|
+
const close = () => (isExpanded.value = false)
|
|
10
|
+
|
|
11
|
+
return { open, close, isExpanded: computed(() => uiStore.isDesktop || isExpanded.value) }
|
|
12
|
+
})
|