@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,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,6 @@
1
+ {
2
+ "core": {
3
+ "close": "Schließen",
4
+ "log-out": "Abmelden"
5
+ }
6
+ }
@@ -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
+ })