@energie360/ui-library 0.1.9 → 0.1.11
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/base/abstracts/_variables.scss +1 -0
- package/components/card/u-card.vue +10 -3
- package/components/card-cta-header/u-card-cta-header.vue +87 -0
- package/components/circular-progress/circular-progress.scss +34 -0
- package/components/circular-progress/u-circular-progress.vue +25 -0
- package/components/context-menu/context-menu.scss +28 -0
- package/components/context-menu/u-context-menu.vue +113 -0
- package/components/context-menu-divider/context-menu-divider.scss +6 -0
- package/components/context-menu-divider/u-context-menu-divider.vue +5 -0
- package/components/context-menu-link/context-menu-link.scss +26 -0
- package/components/context-menu-link/u-context-menu-link.vue +27 -0
- package/components/index.js +8 -0
- package/components/navigation-toolbar-link/navigation-toolbar-link.scss +177 -0
- package/components/navigation-toolbar-link/u-navigation-toolbar-link.vue +108 -0
- package/components/progress-avatar/progress-avatar.scss +27 -0
- package/components/progress-avatar/u-progress-avatar.vue +25 -0
- package/components/text-block/text-block.scss +58 -0
- package/components/text-block/u-text-block.vue +26 -0
- package/components/tooltip/popover.ts +74 -12
- package/dist/base-style.css +1 -0
- package/dist/base-style.css.map +1 -1
- package/dist/elements/text-link.css +1 -0
- package/dist/elements/text-link.css.map +1 -1
- package/dist/layout/split.css +1 -0
- package/dist/layout/split.css.map +1 -1
- package/elements/button/u-button.vue +12 -5
- package/elements/button-chip/button-chip.scss +2 -2
- package/elements/form/form.scss +174 -0
- package/elements/text-field/u-text-field.vue +9 -1
- package/i18n/i18n.ts +8 -0
- package/layout/form-grid/form-grid.scss +42 -0
- package/modules/index.js +2 -0
- package/modules/navigation-toolbar-side/navigation-toolbar-side.scss +89 -0
- package/modules/navigation-toolbar-side/u-navigation-toolbar-side.vue +93 -0
- package/modules/navigation-toolbar-top/navigation-toolbar-top.scss +89 -0
- package/modules/navigation-toolbar-top/u-navigation-toolbar-top.vue +130 -0
- package/package.json +4 -2
- package/utils/a11y/focus-trap.js +128 -0
|
@@ -35,6 +35,7 @@ $grid-gutter-s: math.div(dt.$space-5, $container-inner-s) * 100%;
|
|
|
35
35
|
|
|
36
36
|
// Transition
|
|
37
37
|
$trs-default: var(--e-trs-duration-faster) var(--e-trs-easing-default);
|
|
38
|
+
$trs-ease-out: var(--e-trs-duration-faster) cubic-bezier(0.215, 0.61, 0.355, 1); /* easeOutCubic */
|
|
38
39
|
|
|
39
40
|
// CMS-Section
|
|
40
41
|
$section-default-padding-2xl: dt.$space-28;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { provide, ref } from 'vue'
|
|
2
|
+
import { provide, ref, watch } from 'vue'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
active?: boolean
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
defineProps<Props>()
|
|
8
|
+
const { active = false } = defineProps<Props>()
|
|
9
9
|
|
|
10
|
-
const isActive = ref(
|
|
10
|
+
const isActive = ref(active)
|
|
11
11
|
const toggleActiveCard = (v: boolean) => {
|
|
12
12
|
isActive.value = v
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
provide('card', { toggleActiveCard })
|
|
16
|
+
|
|
17
|
+
watch(
|
|
18
|
+
() => active,
|
|
19
|
+
(newV) => {
|
|
20
|
+
isActive.value = newV
|
|
21
|
+
},
|
|
22
|
+
)
|
|
16
23
|
</script>
|
|
17
24
|
|
|
18
25
|
<template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string
|
|
4
|
+
subtitle: string
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { disabled = false } = defineProps<Props>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="card-cta-header" :class="{ disabled }">
|
|
13
|
+
<slot></slot>
|
|
14
|
+
<div class="card-cta-header__header">
|
|
15
|
+
<div class="card-cta-header__image">
|
|
16
|
+
<slot name="image"></slot>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div>
|
|
20
|
+
<h2 class="card-cta-header__title">{{ title }}</h2>
|
|
21
|
+
<p class="card-cta-header__subtitle">{{ subtitle }}</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="card-cta-header__cta">
|
|
26
|
+
<slot name="cta"></slot>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped lang="scss">
|
|
32
|
+
// We add the styles directly in the component
|
|
33
|
+
// because the :slotted selector won't work when styles are added via `src`.
|
|
34
|
+
|
|
35
|
+
@use '../../base/abstracts/' as a;
|
|
36
|
+
|
|
37
|
+
.card-cta-header {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: var(--e-space-4);
|
|
41
|
+
|
|
42
|
+
@include a.bp(m) {
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.card-cta-header__header {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
column-gap: var(--e-space-6);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.card-cta-header__title {
|
|
55
|
+
@include a.type(300, strong);
|
|
56
|
+
|
|
57
|
+
color: var(--e-c-mono-900);
|
|
58
|
+
|
|
59
|
+
.disabled & {
|
|
60
|
+
color: var(--e-c-mono-500);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.card-cta-header__subtitle {
|
|
65
|
+
@include a.type(300);
|
|
66
|
+
|
|
67
|
+
color: var(--e-c-mono-700);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.card-cta-header__cta {
|
|
71
|
+
margin-left: auto;
|
|
72
|
+
|
|
73
|
+
@include a.bp(m) {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
width: 100%;
|
|
77
|
+
margin-left: 0;
|
|
78
|
+
|
|
79
|
+
// This is used mostly because <nuxt-link> is probably used to wrap a button component.
|
|
80
|
+
// And <nuxt-link> always renders to <a>.
|
|
81
|
+
:slotted(a) {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// From https://www.30secondsofcode.org/css/s/circular-progress-bar/
|
|
2
|
+
.circular-progress {
|
|
3
|
+
--stroke-width: 4px;
|
|
4
|
+
--half-size: calc(var(--size) / 2);
|
|
5
|
+
--radius: calc((var(--size) - var(--stroke-width)) / 2);
|
|
6
|
+
--circumference: calc(var(--radius) * pi * 2);
|
|
7
|
+
--dash: calc((var(--progress) * var(--circumference)) / 100);
|
|
8
|
+
|
|
9
|
+
width: var(--size);
|
|
10
|
+
height: var(--size);
|
|
11
|
+
pointer-events: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.circular-progress__background,
|
|
15
|
+
.circular-progress__foreground {
|
|
16
|
+
cx: var(--half-size);
|
|
17
|
+
cy: var(--half-size);
|
|
18
|
+
r: var(--radius);
|
|
19
|
+
stroke-width: var(--stroke-width);
|
|
20
|
+
fill: none;
|
|
21
|
+
stroke-linecap: round;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.circular-progress__background {
|
|
25
|
+
stroke: var(--e-c-primary-01-100);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.circular-progress__foreground {
|
|
29
|
+
transform: rotate(-90deg);
|
|
30
|
+
transform-origin: var(--half-size) var(--half-size);
|
|
31
|
+
stroke-dasharray: var(--dash) calc(var(--circumference) - var(--dash));
|
|
32
|
+
transition: stroke-dasharray var(--e-trs-duration-fast) ease-out 0.1s;
|
|
33
|
+
stroke: var(--e-c-primary-01-500);
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
size?: number
|
|
6
|
+
progress: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { size = 100 } = defineProps<Props>()
|
|
10
|
+
|
|
11
|
+
const viewBox = computed(() => `0 0 ${size} ${size}`)
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<svg
|
|
16
|
+
class="circular-progress"
|
|
17
|
+
:viewBox
|
|
18
|
+
:style="{ '--size': `${size}px`, '--progress': progress }"
|
|
19
|
+
>
|
|
20
|
+
<circle class="circular-progress__background"></circle>
|
|
21
|
+
<circle class="circular-progress__foreground"></circle>
|
|
22
|
+
</svg>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<style scoped lang="scss" src="./circular-progress.scss"></style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@use '../../base/abstracts/' as a;
|
|
2
|
+
|
|
3
|
+
// .context-menu {
|
|
4
|
+
// // position: relative;
|
|
5
|
+
// }
|
|
6
|
+
|
|
7
|
+
.context-menu__menu {
|
|
8
|
+
position: absolute;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
row-gap: var(--e-space-2);
|
|
12
|
+
padding: var(--e-space-2);
|
|
13
|
+
border-radius: var(--e-brd-radius-2);
|
|
14
|
+
border: 1px solid var(--e-c-mono-200);
|
|
15
|
+
box-shadow: var(--e-elevation-md);
|
|
16
|
+
background-color: var(--e-c-mono-00);
|
|
17
|
+
min-width: a.rem(240);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.menu-enter-active,
|
|
21
|
+
.menu-leave-active {
|
|
22
|
+
transition: opacity a.$trs-default;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.menu-enter-from,
|
|
26
|
+
.menu-leave-to {
|
|
27
|
+
opacity: 0;
|
|
28
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, useTemplateRef, watch } from 'vue'
|
|
3
|
+
import { PopoverPositionParams, getPopoverPosition } from '../tooltip/popover'
|
|
4
|
+
import { debounceRaf } from '../../utils/functions/debounce'
|
|
5
|
+
import { focusTrap } from '../../utils/a11y/focus-trap'
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
placement = 'top-center',
|
|
9
|
+
offset = 8,
|
|
10
|
+
viewportPadding = 20,
|
|
11
|
+
} = defineProps<PopoverPositionParams>()
|
|
12
|
+
|
|
13
|
+
// TODO: Key codes should be defined globally
|
|
14
|
+
const ESC = 'Escape'
|
|
15
|
+
|
|
16
|
+
const triggerEl = useTemplateRef('trigger')
|
|
17
|
+
const menuEl = useTemplateRef('menu')
|
|
18
|
+
const isOpen = ref(false)
|
|
19
|
+
let focusTrapInstance
|
|
20
|
+
|
|
21
|
+
const onToggleMenu = (e: Event) => {
|
|
22
|
+
e.stopPropagation()
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
|
|
25
|
+
isOpen.value = !isOpen.value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const onResize = () => {
|
|
29
|
+
// Clear right position value, so we can get actual width of popover.
|
|
30
|
+
menuEl.value!.style.right = ''
|
|
31
|
+
|
|
32
|
+
const popoverPosition = getPopoverPosition(
|
|
33
|
+
triggerEl.value as HTMLElement,
|
|
34
|
+
menuEl.value as HTMLElement,
|
|
35
|
+
{
|
|
36
|
+
placement,
|
|
37
|
+
offset,
|
|
38
|
+
viewportPadding,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
menuEl.value!.style.top = `${popoverPosition.top}px`
|
|
43
|
+
menuEl.value!.style.left = `${popoverPosition.left}px`
|
|
44
|
+
if (popoverPosition.right) {
|
|
45
|
+
menuEl.value!.style.right = `${popoverPosition.right}px`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onResizeDebounced = debounceRaf(onResize)
|
|
50
|
+
|
|
51
|
+
const onEnter = () => {
|
|
52
|
+
// 1. Position context-menu
|
|
53
|
+
onResize()
|
|
54
|
+
|
|
55
|
+
// 2. Set focus to first focusable element in menu
|
|
56
|
+
focusTrapInstance = focusTrap(menuEl.value, {
|
|
57
|
+
focusFirstElement: true,
|
|
58
|
+
allowArrowUpDown: true,
|
|
59
|
+
allowArrowLeftRight: true,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// 3. Bind events
|
|
63
|
+
menuEl.value.addEventListener('keydown', onEsc)
|
|
64
|
+
document.addEventListener('click', onDocumentClick)
|
|
65
|
+
window.addEventListener('resize', onResizeDebounced)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onAfterLeave = () => {
|
|
69
|
+
focusTrapInstance.release()
|
|
70
|
+
window.removeEventListener('resize', onResizeDebounced)
|
|
71
|
+
document.removeEventListener('click', onDocumentClick)
|
|
72
|
+
menuEl.value?.removeEventListener('keydown', onEsc)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const onEsc = (e) => {
|
|
76
|
+
if (e.code === ESC) {
|
|
77
|
+
isOpen.value = false
|
|
78
|
+
|
|
79
|
+
// Set focus back to the trigger element
|
|
80
|
+
triggerEl.value.focus()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const onDocumentClick = (e: Event) => {
|
|
85
|
+
e.stopPropagation()
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
|
|
88
|
+
// Close context menu when any click happens.
|
|
89
|
+
isOpen.value = false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
watch([() => placement, () => offset, () => viewportPadding], () => {
|
|
93
|
+
if (isOpen.value) {
|
|
94
|
+
onResize()
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<div class="context-menu" :class="{ 'is-open': isOpen, 'is-closed': !isOpen }">
|
|
101
|
+
<div ref="trigger" class="context-menu__trigger" @click="onToggleMenu">
|
|
102
|
+
<slot name="trigger"></slot>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<Transition name="menu" @enter="onEnter" @after-leave="onAfterLeave">
|
|
106
|
+
<div v-if="isOpen" ref="menu" class="context-menu__menu">
|
|
107
|
+
<slot></slot>
|
|
108
|
+
</div>
|
|
109
|
+
</Transition>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<style lang="scss" scoped src="./context-menu.scss"></style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@use '../../base/abstracts/' as a;
|
|
2
|
+
|
|
3
|
+
.context-menu-link {
|
|
4
|
+
@include a.type(200, strong);
|
|
5
|
+
|
|
6
|
+
display: flex;
|
|
7
|
+
column-gap: var(--e-space-2);
|
|
8
|
+
padding: var(--e-space-1_5) var(--e-space-3);
|
|
9
|
+
color: var(--e-c-mono-700);
|
|
10
|
+
border-radius: var(--e-brd-radius-2);
|
|
11
|
+
text-wrap: nowrap;
|
|
12
|
+
transition:
|
|
13
|
+
background-color a.$trs-ease-out,
|
|
14
|
+
color a.$trs-ease-out;
|
|
15
|
+
|
|
16
|
+
&:hover,
|
|
17
|
+
&:active {
|
|
18
|
+
background-color: var(--e-c-primary-01-50);
|
|
19
|
+
color: var(--e-c-primary-01-700);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.active {
|
|
23
|
+
background-color: var(--e-c-primary-01-100);
|
|
24
|
+
color: var(--e-c-primary-01-900);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { UIcon } from '../../elements'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
icon: string
|
|
7
|
+
label?: string
|
|
8
|
+
href?: string
|
|
9
|
+
target?: string
|
|
10
|
+
active?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { href, active = false } = defineProps<Props>()
|
|
14
|
+
|
|
15
|
+
const tag = computed(() => {
|
|
16
|
+
return href ? 'a' : 'span'
|
|
17
|
+
})
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<component :is="tag" class="context-menu-link" :class="{ active }">
|
|
22
|
+
<UIcon :name="icon" />
|
|
23
|
+
<slot>{{ label }}</slot>
|
|
24
|
+
</component>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped lang="scss" src="./context-menu-link.scss"></style>
|
package/components/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { default as UCardPrice } from './card-price/u-card-price.vue'
|
|
|
14
14
|
export { default as UCardSection } from './card-section/u-card-section.vue'
|
|
15
15
|
export { default as UCardTable } from './card-table/u-card-table.vue'
|
|
16
16
|
export { default as UCardToggleSwitches } from './card-toggle-switches/u-card-toggle-switches.vue'
|
|
17
|
+
export { default as UCardCtaHeader } from './card-cta-header/u-card-cta-header.vue'
|
|
17
18
|
|
|
18
19
|
// Collapsible
|
|
19
20
|
export { default as UCollapsible } from './collapsible/u-collapsible.vue'
|
|
@@ -39,4 +40,11 @@ export { default as UTableRow } from './table/u-table-row.vue'
|
|
|
39
40
|
export { default as UTable } from './table/u-table.vue'
|
|
40
41
|
|
|
41
42
|
export { default as UTabs } from './tabs/u-tabs.vue'
|
|
43
|
+
export { default as UTextBlock } from './text-block/u-text-block.vue'
|
|
42
44
|
export { default as UTooltip } from './tooltip/u-tooltip.vue'
|
|
45
|
+
export { default as UNavigationToolbarLink } from './navigation-toolbar-link/u-navigation-toolbar-link.vue'
|
|
46
|
+
export { default as UContextMenu } from './context-menu/u-context-menu.vue'
|
|
47
|
+
export { default as UContextMenuLink } from './context-menu-link/u-context-menu-link.vue'
|
|
48
|
+
export { default as UContextMenuDivider } from './context-menu-divider/u-context-menu-divider.vue'
|
|
49
|
+
export { default as UCircularProgress } from './circular-progress/u-circular-progress.vue'
|
|
50
|
+
export { default as UProgressAvatar } from './progress-avatar/u-progress-avatar.vue'
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// TODO: How to handle text overflow?
|
|
2
|
+
|
|
3
|
+
@use '../../base/abstracts/' as a;
|
|
4
|
+
|
|
5
|
+
@keyframes fadeInLabel {
|
|
6
|
+
0% {
|
|
7
|
+
opacity: 0;
|
|
8
|
+
transform: translateX(calc(100% + #{a.rem(8)}));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
100% {
|
|
12
|
+
opacity: 1;
|
|
13
|
+
transform: translateX(100%);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keyframes fadeOutLabel {
|
|
18
|
+
0% {
|
|
19
|
+
opacity: 1;
|
|
20
|
+
transform: translateX(100%);
|
|
21
|
+
width: auto;
|
|
22
|
+
height: auto;
|
|
23
|
+
padding: var(--e-space-1_5) var(--e-space-2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
100% {
|
|
27
|
+
opacity: 0;
|
|
28
|
+
transform: translateX(calc(100% + #{a.rem(8)}));
|
|
29
|
+
width: auto;
|
|
30
|
+
height: auto;
|
|
31
|
+
padding: var(--e-space-1_5) var(--e-space-2);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@keyframes collapse {
|
|
36
|
+
0% {
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
padding: var(--e-space-1_5) var(--e-space-3);
|
|
39
|
+
width: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
100% {
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
padding: var(--e-space-1_5);
|
|
45
|
+
width: a.rem(36);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@keyframes expand {
|
|
50
|
+
0% {
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
padding: var(--e-space-1_5);
|
|
53
|
+
width: a.rem(36);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
100% {
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
padding: var(--e-space-1_5) var(--e-space-3);
|
|
59
|
+
width: 100%;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.navigation-toolbar-link {
|
|
64
|
+
--transition-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1); /* easeOutCubic */
|
|
65
|
+
|
|
66
|
+
position: relative;
|
|
67
|
+
padding: var(--e-space-1_5) var(--e-space-3);
|
|
68
|
+
height: a.rem(36);
|
|
69
|
+
display: flex;
|
|
70
|
+
justify-content: flex-start;
|
|
71
|
+
align-items: center;
|
|
72
|
+
flex-wrap: nowrap;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
width: 100%;
|
|
75
|
+
column-gap: var(--e-space-2);
|
|
76
|
+
background-color: var(--e-c-secondary-01-1000);
|
|
77
|
+
color: var(--e-c-mono-00);
|
|
78
|
+
border-radius: var(--e-brd-radius-2);
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
transition:
|
|
81
|
+
background-color a.$trs-default,
|
|
82
|
+
color a.$trs-default;
|
|
83
|
+
|
|
84
|
+
@include a.type(200, strong);
|
|
85
|
+
|
|
86
|
+
> * {
|
|
87
|
+
flex: 0 0 auto;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&:active,
|
|
91
|
+
&:hover:not(.is-collapsing, .is-expanding) {
|
|
92
|
+
background-color: var(--e-c-secondary-01-950);
|
|
93
|
+
color: var(--e-c-primary-01-100);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Active Page
|
|
97
|
+
&.active {
|
|
98
|
+
background-color: var(--e-c-secondary-01-900);
|
|
99
|
+
color: var(--e-c-primary-01-50);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Collapsed
|
|
103
|
+
&.collapsed {
|
|
104
|
+
padding: var(--e-space-1_5);
|
|
105
|
+
width: a.rem(36);
|
|
106
|
+
|
|
107
|
+
.navigation-toolbar-link__label {
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: 0;
|
|
110
|
+
right: calc(var(--e-space-2) * -1);
|
|
111
|
+
transform: translateX(calc(100% + #{a.rem(8)}));
|
|
112
|
+
border-radius: var(--e-brd-radius-2);
|
|
113
|
+
background-color: var(--e-c-mono-900);
|
|
114
|
+
color: var(--e-c-mono-00);
|
|
115
|
+
|
|
116
|
+
// Hide visually
|
|
117
|
+
width: 0;
|
|
118
|
+
height: 0;
|
|
119
|
+
opacity: 0;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&.hover-out:not(.is-expanding, .is-collapsing, .label-hidden) {
|
|
125
|
+
.navigation-toolbar-link__label {
|
|
126
|
+
// TODO: Maybe just use transitions instead of keyframe animations
|
|
127
|
+
animation-name: fadeOutLabel;
|
|
128
|
+
animation-duration: var(--e-trs-duration-faster);
|
|
129
|
+
animation-timing-function: ease-out;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&:hover:not(.is-expanding, .is-collapsing, .label-hidden) {
|
|
134
|
+
.navigation-toolbar-link__label {
|
|
135
|
+
animation-name: fadeInLabel;
|
|
136
|
+
animation-duration: var(--e-trs-duration-faster);
|
|
137
|
+
animation-timing-function: ease-out;
|
|
138
|
+
|
|
139
|
+
padding: var(--e-space-1_5) var(--e-space-2);
|
|
140
|
+
transform: translateX(100%);
|
|
141
|
+
width: auto;
|
|
142
|
+
height: auto;
|
|
143
|
+
opacity: 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Animation
|
|
150
|
+
.navigation-toolbar-link {
|
|
151
|
+
&.is-collapsing,
|
|
152
|
+
&.is-expanding {
|
|
153
|
+
animation-duration: var(--e-trs-duration-faster);
|
|
154
|
+
|
|
155
|
+
.navigation-toolbar-link__label {
|
|
156
|
+
animation: none;
|
|
157
|
+
position: static;
|
|
158
|
+
width: auto;
|
|
159
|
+
height: auto;
|
|
160
|
+
opacity: 1;
|
|
161
|
+
background-color: transparent;
|
|
162
|
+
transform: none;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&.is-collapsing {
|
|
167
|
+
// TODO: Maybe just use transitions instead of keyframe animations
|
|
168
|
+
animation-name: collapse;
|
|
169
|
+
animation-timing-function: var(--transition-ease-out);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&.is-expanding {
|
|
173
|
+
// TODO: Maybe just use transitions instead of keyframe animations
|
|
174
|
+
animation-name: expand;
|
|
175
|
+
animation-timing-function: var(--transition-ease-out);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, computed, useTemplateRef } from 'vue'
|
|
3
|
+
import { UIcon } from '../../elements'
|
|
4
|
+
|
|
5
|
+
// TODO: Label animation when collapsed is a mess. Refactor it as soon as possbile!
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
label?: string
|
|
9
|
+
icon: string
|
|
10
|
+
href?: string
|
|
11
|
+
target?: string
|
|
12
|
+
active?: boolean
|
|
13
|
+
collapsed?: boolean
|
|
14
|
+
labelHidden?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
active = false,
|
|
19
|
+
collapsed = false,
|
|
20
|
+
labelHidden = false,
|
|
21
|
+
href = '',
|
|
22
|
+
target = '_self',
|
|
23
|
+
} = defineProps<Props>()
|
|
24
|
+
|
|
25
|
+
const isCollapsed = ref(collapsed)
|
|
26
|
+
const isCollapsing = ref(false)
|
|
27
|
+
const isExpanding = ref(false)
|
|
28
|
+
const isHovering = ref(false)
|
|
29
|
+
const isHoverout = ref(false)
|
|
30
|
+
const labelEl = useTemplateRef('label')
|
|
31
|
+
|
|
32
|
+
const onWrapperAnimationEnd = (e: Event) => {
|
|
33
|
+
if (e.target === labelEl.value) {
|
|
34
|
+
isHoverout.value = false
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
isCollapsing.value = false
|
|
39
|
+
isExpanding.value = false
|
|
40
|
+
isHoverout.value = false
|
|
41
|
+
|
|
42
|
+
isCollapsed.value = !isCollapsed.value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onHover = () => {
|
|
46
|
+
isHovering.value = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onHoverOut = () => {
|
|
50
|
+
isHovering.value = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
watch(
|
|
54
|
+
() => collapsed,
|
|
55
|
+
(newV) => {
|
|
56
|
+
if (newV) {
|
|
57
|
+
// Start collapsing animation
|
|
58
|
+
isCollapsing.value = true
|
|
59
|
+
} else {
|
|
60
|
+
// Start expanding animation
|
|
61
|
+
isExpanding.value = true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
watch(isHovering, (newV) => {
|
|
67
|
+
if (!newV) {
|
|
68
|
+
isHoverout.value = true
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const tag = computed(() => {
|
|
73
|
+
if (href) {
|
|
74
|
+
return 'a'
|
|
75
|
+
} else {
|
|
76
|
+
return 'span'
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<component
|
|
83
|
+
:is="tag"
|
|
84
|
+
:href="tag === 'a' ? href : null"
|
|
85
|
+
:target="tag === 'a' ? target : null"
|
|
86
|
+
:class="[
|
|
87
|
+
'navigation-toolbar-link',
|
|
88
|
+
{
|
|
89
|
+
active,
|
|
90
|
+
collapsed: isCollapsed,
|
|
91
|
+
'hover-out': isHoverout,
|
|
92
|
+
'is-collapsing': isCollapsing,
|
|
93
|
+
'is-expanding': isExpanding,
|
|
94
|
+
'label-hidden': labelHidden,
|
|
95
|
+
},
|
|
96
|
+
]"
|
|
97
|
+
@animationend="onWrapperAnimationEnd"
|
|
98
|
+
@mouseenter="onHover"
|
|
99
|
+
@mouseleave="onHoverOut"
|
|
100
|
+
>
|
|
101
|
+
<UIcon :name="icon" />
|
|
102
|
+
<span ref="label" class="navigation-toolbar-link__label">
|
|
103
|
+
<slot>{{ label }}</slot>
|
|
104
|
+
</span>
|
|
105
|
+
</component>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<style lang="scss" src="./navigation-toolbar-link.scss" scoped></style>
|