@bagelink/vue 1.4.141 → 1.4.147
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/dist/components/Btn.vue.d.ts.map +1 -1
- package/dist/components/Carousel.vue.d.ts +1 -1
- package/dist/components/Modal.vue.d.ts +3 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/Slider.vue.d.ts +1 -1
- package/dist/components/Slider.vue.d.ts.map +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +11 -3
- package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/LineChart.vue.d.ts +9 -0
- package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/PieChart.vue.d.ts +30 -2
- package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +15 -15
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
- package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/index.cjs +123 -22
- package/dist/index.mjs +123 -22
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Btn.vue +50 -42
- package/src/components/Modal.vue +49 -50
- package/src/components/analytics/BarChart.vue +118 -7
- package/src/components/analytics/KpiCard.vue +2 -2
- package/src/components/analytics/LineChart.vue +189 -105
- package/src/components/analytics/PieChart.vue +392 -49
- package/src/components/form/inputs/RichText/CheckList.md +23 -0
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -38
- package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
- package/src/components/form/inputs/RichText/composables/useCommands.ts +4 -1
- package/src/components/form/inputs/RichText/composables/useEditor.ts +6 -6
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +1 -0
- package/src/components/form/inputs/RichText/config.ts +23 -11
- package/src/components/form/inputs/RichText/editor.css +300 -33
- package/src/components/form/inputs/RichText/index.vue +3014 -75
- package/src/components/form/inputs/RichText/richTextTypes.ts +2 -3
- package/src/components/form/inputs/RichText/utils/commands.ts +279 -50
- package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
- package/src/components/form/inputs/RichText/utils/media.ts +133 -67
- package/src/components/form/inputs/RichText/utils/selection.ts +10 -2
- package/src/components/form/inputs/RichText/utils/table.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppContent.vue +26 -26
- package/src/components/layout/AppLayout.vue +21 -3
- package/src/components/layout/AppSidebar.vue +5 -2
- package/src/styles/layout.css +267 -0
- package/src/styles/mobilLayout.css +266 -0
- package/src/styles/modal.css +3 -17
package/package.json
CHANGED
package/src/components/Btn.vue
CHANGED
|
@@ -57,8 +57,6 @@ const emit = defineEmits<{
|
|
|
57
57
|
confirmed: [event: MouseEvent]
|
|
58
58
|
}>()
|
|
59
59
|
|
|
60
|
-
const { confirmModal } = useModal()
|
|
61
|
-
|
|
62
60
|
const isMobileScreen = ref(false)
|
|
63
61
|
|
|
64
62
|
function checkMobile() {
|
|
@@ -76,6 +74,7 @@ onUnmounted(() => {
|
|
|
76
74
|
|
|
77
75
|
async function handleClick(event: MouseEvent) {
|
|
78
76
|
if (props.confirm) {
|
|
77
|
+
const { confirmModal } = useModal()
|
|
79
78
|
const message = typeof props.confirm === 'string' ? props.confirm : 'Are you sure?'
|
|
80
79
|
const confirmed = await confirmModal({
|
|
81
80
|
title: 'Confirm',
|
|
@@ -96,15 +95,15 @@ const iconSizeComputed = $computed(() => {
|
|
|
96
95
|
|
|
97
96
|
// Default icon sizes based on button size
|
|
98
97
|
const sizeMap = {
|
|
99
|
-
xs: 0.7,
|
|
98
|
+
'xs': 0.7,
|
|
100
99
|
'extra-small': 0.7,
|
|
101
|
-
s: 0.9,
|
|
102
|
-
small: 0.9,
|
|
103
|
-
m: 1,
|
|
104
|
-
medium: 1,
|
|
105
|
-
l: 1.3,
|
|
106
|
-
large: 1.3,
|
|
107
|
-
xl: 1.6,
|
|
100
|
+
's': 0.9,
|
|
101
|
+
'small': 0.9,
|
|
102
|
+
'm': 1,
|
|
103
|
+
'medium': 1,
|
|
104
|
+
'l': 1.3,
|
|
105
|
+
'large': 1.3,
|
|
106
|
+
'xl': 1.6,
|
|
108
107
|
'extra-large': 1.6
|
|
109
108
|
}
|
|
110
109
|
|
|
@@ -138,36 +137,44 @@ const slots: SetupContext['slots'] = useSlots()
|
|
|
138
137
|
</script>
|
|
139
138
|
|
|
140
139
|
<template>
|
|
141
|
-
<component
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
140
|
+
<component
|
|
141
|
+
:is="isComponent" v-ripple="ripple" v-bind="bind" :disabled="disabled" class="bgl_btn" :class="{
|
|
142
|
+
'bgl_btn-icon': icon && !slots.default && !value,
|
|
143
|
+
thin,
|
|
144
|
+
'bgl_btn_xsSize': size === 'xs' || size === 'extra-small',
|
|
145
|
+
'bgl_btn_sSize': size === 's' || size === 'small',
|
|
146
|
+
'bgl_btn_mSize': size === 'm' || size === 'medium',
|
|
147
|
+
'bgl_btn_lSize': size === 'l' || size === 'large',
|
|
148
|
+
'bgl_btn_xlSize': size === 'xl' || size === 'extra-large',
|
|
149
|
+
'bgl_btn_fullWidth': fullWidth,
|
|
150
|
+
'bgl_btn_fullWidthMobile': fullWidthMobile,
|
|
151
|
+
'bgl_btn_alignStart': alignTxt === 'start',
|
|
152
|
+
'bgl_btn_alignEnd': alignTxt === 'end',
|
|
153
|
+
'bgl_btn_alignStartMobile': alignTxtMobile === 'start',
|
|
154
|
+
'bgl_btn_alignEndMobile': alignTxtMobile === 'end',
|
|
155
|
+
round,
|
|
156
|
+
'bgl_btn_flat': flat,
|
|
157
|
+
'bgl_btn-border': border || outline,
|
|
158
|
+
[`bgl_btn-${color}`]: color,
|
|
159
|
+
[`bgl_btn-${theme}`]: theme,
|
|
160
|
+
}" :tabindex="disabled ? -1 : 0" @click.stop="handleClick" @keydown.enter="handleClick" @keydown.space="handleClick"
|
|
161
|
+
>
|
|
162
|
+
<Loading v-if="loading" class="h-100p" size="15" />
|
|
163
|
+
<div v-else class="bgl_btn-flex">
|
|
164
|
+
<Icon
|
|
165
|
+
v-if="icon" :icon="icon" class="transition-400" :size="iconSizeComputed"
|
|
166
|
+
:mobile-size="iconMobileSize"
|
|
167
|
+
/>
|
|
168
|
+
<slot />
|
|
169
|
+
<template v-if="!slots.default && value">
|
|
170
|
+
{{ value }}
|
|
171
|
+
</template>
|
|
172
|
+
<Icon
|
|
173
|
+
v-if="iconEnd" :icon="iconEnd" class="transition-400" :size="iconSizeComputed"
|
|
174
|
+
:mobile-size="iconMobileSize"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
</component>
|
|
171
178
|
</template>
|
|
172
179
|
|
|
173
180
|
<style scoped>
|
|
@@ -232,6 +239,7 @@ a {
|
|
|
232
239
|
.bgl_btn-icon.bgl_btn_flat:active:not(:disabled) {
|
|
233
240
|
background: var(--bgl-gray-tint-dark);
|
|
234
241
|
}
|
|
242
|
+
|
|
235
243
|
.bgl_btn.round {
|
|
236
244
|
border-radius: 1000px !important;
|
|
237
245
|
}
|
|
@@ -265,6 +273,7 @@ a {
|
|
|
265
273
|
filter: grayscale(0.3);
|
|
266
274
|
cursor: not-allowed;
|
|
267
275
|
}
|
|
276
|
+
|
|
268
277
|
.bgl_btn-icon .bgl_btn-flex {
|
|
269
278
|
height: 100%;
|
|
270
279
|
}
|
|
@@ -282,7 +291,6 @@ a {
|
|
|
282
291
|
line-height: normal;
|
|
283
292
|
}
|
|
284
293
|
|
|
285
|
-
|
|
286
294
|
.bgl_btn_xsSize {
|
|
287
295
|
height: calc(var(--btn-height) / 2);
|
|
288
296
|
line-height: calc(var(--btn-height) / 2);
|
|
@@ -350,6 +358,7 @@ a {
|
|
|
350
358
|
height: calc(var(--btn-height) * 1.5);
|
|
351
359
|
width: calc(var(--btn-height) * 1.5);
|
|
352
360
|
}
|
|
361
|
+
|
|
353
362
|
.bgl_btn_fullWidth {
|
|
354
363
|
width: 100%;
|
|
355
364
|
}
|
|
@@ -358,7 +367,6 @@ a {
|
|
|
358
367
|
width: auto;
|
|
359
368
|
}
|
|
360
369
|
|
|
361
|
-
|
|
362
370
|
/* Content alignment styles */
|
|
363
371
|
.bgl_btn_alignStart .bgl_btn-flex {
|
|
364
372
|
justify-content: flex-start;
|
package/src/components/Modal.vue
CHANGED
|
@@ -4,8 +4,7 @@ import type { SetupContext } from 'vue'
|
|
|
4
4
|
import {
|
|
5
5
|
Btn,
|
|
6
6
|
Card,
|
|
7
|
-
Title
|
|
8
|
-
useEscape
|
|
7
|
+
Title
|
|
9
8
|
} from '@bagelink/vue'
|
|
10
9
|
import {
|
|
11
10
|
onMounted,
|
|
@@ -24,6 +23,7 @@ interface ModalProps {
|
|
|
24
23
|
actions?: BtnOptions[]
|
|
25
24
|
visible?: boolean
|
|
26
25
|
zIndex?: number
|
|
26
|
+
closePlacement?: 'header' | 'header-end' | 'overlay' | 'overlay-end' | 'none' | 'footer'
|
|
27
27
|
}
|
|
28
28
|
const props = withDefaults(defineProps<ModalProps>(), {
|
|
29
29
|
thin: false,
|
|
@@ -31,6 +31,7 @@ const props = withDefaults(defineProps<ModalProps>(), {
|
|
|
31
31
|
dismissable: true,
|
|
32
32
|
visible: false,
|
|
33
33
|
zIndex: 99,
|
|
34
|
+
closePlacement: 'header'
|
|
34
35
|
})
|
|
35
36
|
|
|
36
37
|
const emit = defineEmits(['update:visible'])
|
|
@@ -56,6 +57,17 @@ const maxWidth = $computed(() => {
|
|
|
56
57
|
return { 'max-width': '720px' }
|
|
57
58
|
})
|
|
58
59
|
|
|
60
|
+
// Computed properties for close button placement
|
|
61
|
+
const isOverlay = $computed(() => props.closePlacement === 'overlay' || props.closePlacement === 'overlay-end')
|
|
62
|
+
const isHeader = $computed(() => props.closePlacement === 'header' || props.closePlacement === 'header-end')
|
|
63
|
+
const isFooter = $computed(() => props.closePlacement === 'footer')
|
|
64
|
+
const isNone = $computed(() => props.closePlacement === 'none')
|
|
65
|
+
|
|
66
|
+
const overlayCloseClass = $computed(() => {
|
|
67
|
+
if (props.closePlacement === 'overlay-end') return 'top-1 end-1'
|
|
68
|
+
return 'top-1 start-1'
|
|
69
|
+
})
|
|
70
|
+
|
|
59
71
|
function closeModal() {
|
|
60
72
|
isVisible = false
|
|
61
73
|
setTimeout(() => { emit('update:visible', false) }, 200)
|
|
@@ -63,7 +75,11 @@ function closeModal() {
|
|
|
63
75
|
|
|
64
76
|
defineExpose({ closeModal })
|
|
65
77
|
|
|
66
|
-
const escapeKeyClose = (e: KeyboardEvent) =>
|
|
78
|
+
const escapeKeyClose = (e: KeyboardEvent) => {
|
|
79
|
+
if (props.dismissable && e.key === 'Escape') {
|
|
80
|
+
closeModal()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
67
83
|
|
|
68
84
|
function openModal() {
|
|
69
85
|
setTimeout(() => (isVisible = true), 1)
|
|
@@ -77,64 +93,43 @@ onUnmounted(() => {
|
|
|
77
93
|
</script>
|
|
78
94
|
|
|
79
95
|
<template>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
<div class="bg-dark" :style="{ zIndex }" :class="{ 'is-side': side, 'is-active': isVisible, 'bg-lignt': false }" @click="() => (dismissable ? closeModal() : '')" @keydown.esc="closeModal">
|
|
97
|
+
<!-- Overlay close button -->
|
|
98
|
+
<Btn v-if="dismissable && isOverlay" icon="close" icon-mobile-size="1.4" class="fixed" :class="overlayCloseClass" @click="closeModal" />
|
|
99
|
+
|
|
100
|
+
<Card class="modal m_pt-0" :style="{ ...maxWidth }" :thin="thin" @click.stop :class="{ 'pt-0': thin, 'pt-1': !thin, 'm_mt-5': isOverlay, 'display-flex column': side }">
|
|
101
|
+
<header v-if="slots.toolbar || title" class="tool-bar w-100p flex space-between sticky z-3 py-1">
|
|
102
|
+
<!-- Header close button -->
|
|
103
|
+
<Btn v-if="dismissable && isHeader && closePlacement === 'header'" :style="{ float: side ? '' : '' }" flat icon="close" thin icon-mobile-size="1.4" @click="closeModal" />
|
|
104
|
+
<slot name="toolbar" />
|
|
105
|
+
<Title v-if="title" class="modal-title txt-center txt20 medium my-0 w-100p ellipsis-1" tag="h3" :label="title"
|
|
106
|
+
:class="{ 'me-1-5': isHeader && dismissable && closePlacement === 'header', 'ms-1-5': isHeader && dismissable && closePlacement === 'header-end' }" />
|
|
107
|
+
<!-- Header-end close button -->
|
|
108
|
+
<Btn v-if="dismissable && isHeader && closePlacement === 'header-end'" :style="{ float: side ? '' : '' }" flat icon="close" thin icon-mobile-size="1.4" @click="closeModal" />
|
|
109
|
+
</header>
|
|
110
|
+
|
|
111
|
+
<header v-else class="tool-bar w-100p flex space-between sticky z-3" :class="{ 'py-1': !isOverlay, 'pt-1': isOverlay }">
|
|
112
|
+
<!-- Header close button (no title) -->
|
|
113
|
+
<Btn v-if="dismissable && isHeader && closePlacement === 'header'" :style="{ float: side ? '' : '' }" flat icon="close" thin icon-mobile-size="1.4" @click="closeModal" />
|
|
86
114
|
<slot name="toolbar" />
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@click="closeModal"
|
|
90
|
-
/>
|
|
91
|
-
<Title v-if="title" class="modal-title" tag="h3" :label="title" />
|
|
115
|
+
<!-- Header-end close button (no title) -->
|
|
116
|
+
<Btn v-if="dismissable && isHeader && closePlacement === 'header-end'" :style="{ float: side ? '' : '' }" flat icon="close" thin icon-mobile-size="1.4" class="ms-auto" @click="closeModal" />
|
|
92
117
|
</header>
|
|
93
|
-
|
|
94
|
-
<div v-else class="sticky z-index-999 -mt-1 -ms-1 px-025 h-30px pt-025 modal-no-title">
|
|
95
|
-
<Btn
|
|
96
|
-
v-if="dismissable" class="position-start" icon="close" thin round color="white"
|
|
97
|
-
icon-mobile-size="1.4" @click="closeModal"
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
118
|
+
|
|
100
119
|
<slot />
|
|
101
|
-
<footer v-if="slots.footer || actions?.length" class="modal-footer mt-1">
|
|
120
|
+
<footer v-if="slots.footer || actions?.length" class="modal-footer gap-1 flex space-between" :class="{ 'mt-1': !side, 'mt-auto': side }">
|
|
102
121
|
<Btn v-for="(action, i) in actions" :key="i" color="gray" v-bind="action" @click="closeModal" />
|
|
103
122
|
<slot name="footer" />
|
|
123
|
+
<!-- Footer close button -->
|
|
104
124
|
</footer>
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
<Btn v-if="dismissable && isFooter" icon="close" label="Close" class="mx-auto absolute start-0 end-0 modalFooterBtn" @click="closeModal" />
|
|
126
|
+
</Card>
|
|
127
|
+
</div>
|
|
107
128
|
</template>
|
|
108
129
|
|
|
109
130
|
<style>
|
|
110
131
|
.modal {
|
|
111
132
|
color: var(--bgl-popup-text);
|
|
112
|
-
/* display: flex;
|
|
113
|
-
flex-direction: column; */
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.modal-title {
|
|
117
|
-
text-align: center;
|
|
118
|
-
font-weight: 600;
|
|
119
|
-
font-size: 20px;
|
|
120
|
-
margin-inline-end: 2rem;
|
|
121
|
-
margin-top: 0.5rem;
|
|
122
|
-
margin-bottom: 0 !important;
|
|
123
|
-
width: 100%;
|
|
124
|
-
line-height: 2;
|
|
125
|
-
display: -webkit-box;
|
|
126
|
-
max-width: 100%;
|
|
127
|
-
-webkit-line-clamp: 1;
|
|
128
|
-
-webkit-box-orient: vertical;
|
|
129
|
-
overflow: hidden;
|
|
130
|
-
text-overflow: ellipsis;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.modal-footer {
|
|
134
|
-
gap: 1rem;
|
|
135
|
-
display: flex;
|
|
136
|
-
justify-content: space-between;
|
|
137
|
-
align-items: center;
|
|
138
133
|
}
|
|
139
134
|
|
|
140
135
|
.modal-footer>div {
|
|
@@ -147,6 +142,10 @@ onUnmounted(() => {
|
|
|
147
142
|
.modal-no-title {
|
|
148
143
|
width: calc(100% + 2rem);
|
|
149
144
|
border-radius: var(--card-border-radius);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.modalFooterBtn {
|
|
148
|
+
bottom: calc(var(--btn-height) / 2 * -1);
|
|
150
149
|
|
|
151
150
|
}
|
|
152
151
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { Icon, Loading } from '@bagelink/vue'
|
|
3
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
4
4
|
|
|
5
5
|
interface SecondaryValue {
|
|
6
6
|
label: string
|
|
@@ -29,6 +29,9 @@ interface Props {
|
|
|
29
29
|
maxBars?: number
|
|
30
30
|
loading?: boolean
|
|
31
31
|
rtl?: boolean
|
|
32
|
+
animated?: boolean
|
|
33
|
+
animationDuration?: number
|
|
34
|
+
animationStartDelay?: number
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -41,8 +44,18 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
41
44
|
maxBars: 30,
|
|
42
45
|
loading: false,
|
|
43
46
|
rtl: false,
|
|
47
|
+
animated: true,
|
|
48
|
+
animationDuration: 1500,
|
|
49
|
+
animationStartDelay: 0,
|
|
44
50
|
})
|
|
45
51
|
|
|
52
|
+
// Animation state
|
|
53
|
+
const animatedProgress = ref(0)
|
|
54
|
+
const isAnimating = ref(false)
|
|
55
|
+
const isInView = ref(false)
|
|
56
|
+
const observer = ref<IntersectionObserver | null>(null)
|
|
57
|
+
const chartRef = ref<HTMLElement | null>(null)
|
|
58
|
+
|
|
46
59
|
const chartData = computed(() => {
|
|
47
60
|
if (!props.data || props.data.length === 0) return []
|
|
48
61
|
|
|
@@ -56,6 +69,94 @@ const chartData = computed(() => {
|
|
|
56
69
|
}))
|
|
57
70
|
})
|
|
58
71
|
|
|
72
|
+
// Animation computed properties
|
|
73
|
+
const getBarOpacity = computed(() => {
|
|
74
|
+
return (index: number) => {
|
|
75
|
+
if (!props.animated) return 1
|
|
76
|
+
if (!isInView.value) return 0
|
|
77
|
+
|
|
78
|
+
const totalBars = chartData.value.length
|
|
79
|
+
|
|
80
|
+
// Each bar appears with a delay based on its index
|
|
81
|
+
const barDelay = index / totalBars
|
|
82
|
+
const progress = Math.max(0, Math.min(1, (animatedProgress.value - barDelay) * totalBars))
|
|
83
|
+
|
|
84
|
+
return progress
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Animation functions
|
|
89
|
+
function easeOutCubic(t: number): number {
|
|
90
|
+
return 1 - Math.pow(1 - t, 3)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function startAnimation() {
|
|
94
|
+
if (isAnimating.value || !props.animated) return
|
|
95
|
+
|
|
96
|
+
console.log(`🎯 TrendChart: Starting animation with ${props.animationDuration}ms duration`)
|
|
97
|
+
isAnimating.value = true
|
|
98
|
+
animatedProgress.value = 0
|
|
99
|
+
|
|
100
|
+
const startTime = performance.now()
|
|
101
|
+
|
|
102
|
+
function animate(currentTime: number) {
|
|
103
|
+
const elapsed = currentTime - startTime
|
|
104
|
+
const progress = Math.min(elapsed / props.animationDuration, 1)
|
|
105
|
+
const easedProgress = easeOutCubic(progress)
|
|
106
|
+
|
|
107
|
+
animatedProgress.value = easedProgress
|
|
108
|
+
|
|
109
|
+
if (progress < 1) {
|
|
110
|
+
requestAnimationFrame(animate)
|
|
111
|
+
} else {
|
|
112
|
+
isAnimating.value = false
|
|
113
|
+
console.log(`✅ TrendChart: Animation completed`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
requestAnimationFrame(animate)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setupIntersectionObserver() {
|
|
121
|
+
if (!chartRef.value || observer.value) return
|
|
122
|
+
|
|
123
|
+
observer.value = new IntersectionObserver(
|
|
124
|
+
(entries) => {
|
|
125
|
+
entries.forEach(entry => {
|
|
126
|
+
if (entry.isIntersecting && !isInView.value) {
|
|
127
|
+
console.log(`👀 TrendChart: Entered viewport, starting animation in ${props.animationStartDelay}ms`)
|
|
128
|
+
isInView.value = true
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
startAnimation()
|
|
131
|
+
}, props.animationStartDelay)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
threshold: 0.3,
|
|
137
|
+
rootMargin: '50px'
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
observer.value.observe(chartRef.value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onMounted(() => {
|
|
145
|
+
if (props.animated) {
|
|
146
|
+
setupIntersectionObserver()
|
|
147
|
+
} else {
|
|
148
|
+
// If not animated, show all bars immediately
|
|
149
|
+
isInView.value = true
|
|
150
|
+
animatedProgress.value = 1
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
onUnmounted(() => {
|
|
155
|
+
if (observer.value) {
|
|
156
|
+
observer.value.disconnect()
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
59
160
|
function formatDate(dateStr: string): string {
|
|
60
161
|
const date = new Date(dateStr)
|
|
61
162
|
return date.toLocaleDateString('he-IL', {
|
|
@@ -79,7 +180,7 @@ function formatTooltip(item: any): string {
|
|
|
79
180
|
const primaryValue = formatValue(item.value, props.currency)
|
|
80
181
|
const primaryText = `${props.prefix}${primaryValue}${props.suffix}`
|
|
81
182
|
|
|
82
|
-
let tooltipLines = [`${item.displayLabel}`,
|
|
183
|
+
let tooltipLines = [`${item.displayLabel}`, `<b>${primaryText}</b>`]
|
|
83
184
|
|
|
84
185
|
if (item.secondaryValues && Array.isArray(item.secondaryValues)) {
|
|
85
186
|
item.secondaryValues.forEach((secondary: SecondaryValue) => {
|
|
@@ -94,14 +195,20 @@ function formatTooltip(item: any): string {
|
|
|
94
195
|
</script>
|
|
95
196
|
|
|
96
197
|
<template>
|
|
97
|
-
<div class="h-100p flex column flex-stretch">
|
|
98
|
-
<div class="flex space-between">
|
|
99
|
-
<div class="flex align-center gap-05
|
|
100
|
-
<Icon :name="icon" size="1.2" :color="color" class="line-height-
|
|
198
|
+
<div ref="chartRef" class="h-100p flex column flex-stretch">
|
|
199
|
+
<div class="flex space-between pb-1">
|
|
200
|
+
<div class="flex align-center gap-05">
|
|
201
|
+
<Icon :name="icon" size="1.2" :color="color" class="line-height-0" />
|
|
101
202
|
<p class="white-space light m_txt14">
|
|
102
203
|
{{ title }}
|
|
103
204
|
</p>
|
|
104
205
|
</div>
|
|
206
|
+
<div v-if="percentageChange !== 0" class="flex align-center gap-025">
|
|
207
|
+
<Icon :name="percentageChange > 0 ? 'trending_up' : 'trending_down'" size="1" :class="percentageChange > 0 ? 'color-success' : 'color-danger'" />
|
|
208
|
+
<span class="txt12 bold" :class="percentageChange > 0 ? 'color-success' : 'color-danger'">
|
|
209
|
+
{{ Math.abs(percentageChange) }}%
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
105
212
|
</div>
|
|
106
213
|
<div class="flex w-100p align-items-end mt-auto gap-075 ltr overflow justify-content-start">
|
|
107
214
|
<div
|
|
@@ -109,7 +216,11 @@ function formatTooltip(item: any): string {
|
|
|
109
216
|
:key="index"
|
|
110
217
|
v-tooltip="{ content: formatTooltip(bar), html: true }"
|
|
111
218
|
class="flex-grow txt-center hover transition-400 relative barWrap mb-1"
|
|
112
|
-
:style="{
|
|
219
|
+
:style="{
|
|
220
|
+
width: `max(2rem, ${100 / chartData.length}%)`,
|
|
221
|
+
opacity: getBarOpacity(index),
|
|
222
|
+
transition: animated ? 'opacity 0.3s ease-out' : 'none'
|
|
223
|
+
}"
|
|
113
224
|
>
|
|
114
225
|
<div
|
|
115
226
|
class="bar radius-05 transition-400 "
|
|
@@ -50,8 +50,8 @@ const trendColor = computed(() => isIncreasing.value ? 'var(--bgl-green)' : 'var
|
|
|
50
50
|
<template>
|
|
51
51
|
<Card class=" flex column space-between align-items-start py-1 px-1-5 m_p-1 relative ">
|
|
52
52
|
<div class="mb-1 flex space-between align-items-start m_mb-05 w-100p">
|
|
53
|
-
<div class="flex gap-025
|
|
54
|
-
<Icon :name="icon" size="1" :color="color" class="line-height-
|
|
53
|
+
<div class="flex gap-025">
|
|
54
|
+
<Icon :name="icon" size="1" :color="color" class="line-height-0" weight="300" />
|
|
55
55
|
<div>
|
|
56
56
|
<h3 class="txt14 m-0 line-height-12 light opacity-6">
|
|
57
57
|
{{ title }}
|