@bagelink/vue 1.4.109 → 1.4.115
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/bin/generateFormSchema.ts +12 -12
- package/dist/components/Card.vue.d.ts.map +1 -1
- package/dist/components/ImportData.vue.d.ts.map +1 -1
- package/dist/components/ListItem.vue.d.ts +6 -1
- package/dist/components/ListItem.vue.d.ts.map +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +39 -0
- package/dist/components/analytics/BarChart.vue.d.ts.map +1 -0
- package/dist/components/analytics/KpiCard.vue.d.ts +24 -0
- package/dist/components/analytics/KpiCard.vue.d.ts.map +1 -0
- package/dist/components/analytics/LineChart.vue.d.ts +26 -0
- package/dist/components/analytics/LineChart.vue.d.ts.map +1 -0
- package/dist/components/analytics/PieChart.vue.d.ts +24 -0
- package/dist/components/analytics/PieChart.vue.d.ts.map +1 -0
- package/dist/components/analytics/index.d.ts +5 -0
- package/dist/components/analytics/index.d.ts.map +1 -0
- package/dist/components/calendar/Index.vue.d.ts.map +1 -1
- package/dist/components/calendar/index.d.ts +2 -0
- package/dist/components/calendar/index.d.ts.map +1 -0
- package/dist/components/calendar/views/MonthView.vue.d.ts.map +1 -1
- package/dist/components/calendar/views/WeekView.vue.d.ts.map +1 -1
- package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/DatePicker.vue.d.ts +2 -0
- package/dist/components/form/inputs/DatePicker.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RadioGroup.vue.d.ts +6 -10
- package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts +34 -0
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -0
- package/dist/components/layout/AppLayout.vue.d.ts +27 -0
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -0
- package/dist/components/layout/AppSidebar.vue.d.ts +44 -0
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/composables/useFormField.d.ts.map +1 -1
- package/dist/composables/useSchemaField.d.ts +2 -2
- package/dist/composables/useSchemaField.d.ts.map +1 -1
- package/dist/index.cjs +19 -19
- package/dist/index.mjs +10 -10
- package/dist/style.css +1 -1
- package/dist/types/BagelForm.d.ts +25 -13
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/dist/utils/BagelFormUtils.d.ts +11 -8
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/dist/utils/calendar/dateUtils.d.ts +21 -0
- package/dist/utils/calendar/dateUtils.d.ts.map +1 -1
- package/dist/utils/elementUtils.d.ts +5 -0
- package/dist/utils/elementUtils.d.ts.map +1 -1
- package/dist/utils/useSearch.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Card.vue +1 -2
- package/src/components/DataPreview.vue +1 -1
- package/src/components/ImportData.vue +94 -88
- package/src/components/ListItem.vue +32 -24
- package/src/components/analytics/BarChart.vue +153 -0
- package/src/components/analytics/KpiCard.vue +84 -0
- package/src/components/analytics/LineChart.vue +267 -0
- package/src/components/analytics/PieChart.vue +183 -0
- package/src/components/analytics/index.ts +4 -0
- package/src/components/calendar/Index.vue +15 -35
- package/src/components/calendar/views/MonthView.vue +84 -88
- package/src/components/calendar/views/WeekView.vue +143 -89
- package/src/components/dataTable/DataTable.vue +2 -3
- package/src/components/form/BagelForm.vue +27 -6
- package/src/components/form/inputs/DateInput.vue +2 -2
- package/src/components/form/inputs/DatePicker.vue +42 -48
- package/src/components/form/inputs/RadioGroup.vue +60 -35
- package/src/components/form/inputs/RichText/utils/media.ts +1 -2
- package/src/components/form/inputs/SelectInput.vue +94 -101
- package/src/components/form/inputs/Upload/upload.css +135 -138
- package/src/components/layout/AppContent.vue +125 -0
- package/src/components/layout/AppLayout.vue +124 -0
- package/src/components/layout/AppSidebar.vue +271 -0
- package/src/components/layout/index.ts +5 -0
- package/src/composables/useFormField.ts +6 -0
- package/src/composables/useSchemaField.ts +38 -10
- package/src/styles/inputs.css +9 -0
- package/src/styles/theme.css +2 -2
- package/src/types/BagelForm.ts +68 -13
- package/src/utils/BagelFormUtils.ts +49 -52
- package/src/utils/calendar/dateUtils.ts +71 -17
- package/src/utils/elementUtils.ts +23 -4
- package/src/utils/useSearch.ts +14 -7
- /package/src/components/{dialog → calendar}/index.ts +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Card, Icon } from '@bagelink/vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string
|
|
7
|
+
value: number | string
|
|
8
|
+
icon?: string
|
|
9
|
+
color?: string
|
|
10
|
+
percentageChange?: number
|
|
11
|
+
prefix?: string
|
|
12
|
+
suffix?: string
|
|
13
|
+
currency?: Currency
|
|
14
|
+
loading?: boolean
|
|
15
|
+
subtitle?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Currency = 'ILS' | 'USD' | 'EUR'
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
icon: 'trending_up',
|
|
22
|
+
color: 'var(--bgl-primary)',
|
|
23
|
+
percentageChange: 0,
|
|
24
|
+
prefix: '',
|
|
25
|
+
suffix: '',
|
|
26
|
+
loading: false,
|
|
27
|
+
subtitle: ''
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const isIncreasing = computed(() => props.percentageChange >= 0)
|
|
31
|
+
|
|
32
|
+
const formattedValue = computed(() => {
|
|
33
|
+
if (typeof props.value === 'string') return props.value
|
|
34
|
+
|
|
35
|
+
if (props.currency) {
|
|
36
|
+
return new Intl.NumberFormat('he-IL', {
|
|
37
|
+
style: 'currency',
|
|
38
|
+
currency: props.currency,
|
|
39
|
+
minimumFractionDigits: 0
|
|
40
|
+
}).format(props.value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return props.value.toLocaleString()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const trendColor = computed(() => isIncreasing.value ? 'var(--bgl-green)' : 'var(--bgl-red)'
|
|
47
|
+
)
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<Card class=" flex column space-between align-items-start py-1 px-1-5 m_p-1 relative ">
|
|
52
|
+
<div class="mb-1 flex space-between align-items-start m_mb-05 w-100p">
|
|
53
|
+
<div class="flex gap-025 align-items-start">
|
|
54
|
+
<Icon :name="icon" size="1" :color="color" class="line-height-08" weight="300" />
|
|
55
|
+
<div>
|
|
56
|
+
<h3 class="txt14 m-0 line-height-12 light opacity-6">
|
|
57
|
+
{{ title }}
|
|
58
|
+
</h3>
|
|
59
|
+
<p v-if="subtitle" class="txt12 color-gray">
|
|
60
|
+
{{ subtitle }}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div v-if="percentageChange !== 0" class="kpi-trend flex gap-025 txt12 bold ms-auto" :style="{ color: trendColor }">
|
|
65
|
+
<Icon :name="isIncreasing ? 'trending_up' : 'trending_down'" />
|
|
66
|
+
<span>{{ Math.abs(percentageChange).toFixed(1) }}%</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="flex">
|
|
71
|
+
<div class="flex align-items-baseline gap-025 w100p" :class="{ loading }">
|
|
72
|
+
<span v-if="prefix" class="kpi-prefix txt16 semi color-gray">{{ prefix }}</span>
|
|
73
|
+
<span class="kpi-main-value bold txt28 m_txt24 line-height-1">{{ loading ? '...' : formattedValue }}</span>
|
|
74
|
+
<span v-if="suffix" class="kpi-suffix txt16 semi color-gray">{{ suffix }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</Card>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
.loading .kpi-main-value {
|
|
82
|
+
color: var(--bgl-gray);
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
|
|
3
|
+
import { computed, onMounted, ref } from 'vue'
|
|
4
|
+
import { Icon } from '@bagelink/vue'
|
|
5
|
+
|
|
6
|
+
interface DataPoint {
|
|
7
|
+
date: string
|
|
8
|
+
value: number
|
|
9
|
+
label?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data: DataPoint[]
|
|
14
|
+
title?: string
|
|
15
|
+
icon?: string
|
|
16
|
+
color?: string
|
|
17
|
+
height?: number
|
|
18
|
+
showPoints?: boolean
|
|
19
|
+
currency?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
title: 'Line Chart',
|
|
24
|
+
icon: 'trending_up',
|
|
25
|
+
color: 'var(--bgl-primary)',
|
|
26
|
+
height: 200,
|
|
27
|
+
showPoints: true,
|
|
28
|
+
currency: false
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const svgRef = ref<SVGElement>()
|
|
32
|
+
const width = ref(600)
|
|
33
|
+
const height = ref(props.height)
|
|
34
|
+
|
|
35
|
+
const padding = { top: 20, inline_end: 30, bottom: 25, inline_start: 60 }
|
|
36
|
+
const chartWidth = computed(() => width.value - padding.inline_start - padding.inline_end)
|
|
37
|
+
const chartHeight = computed(() => height.value - padding.top - padding.bottom)
|
|
38
|
+
|
|
39
|
+
// RTL-aware padding calculations
|
|
40
|
+
const paddingLeft = computed(() => {
|
|
41
|
+
const isRTL = document.documentElement.dir === 'rtl' ||
|
|
42
|
+
document.documentElement.getAttribute('lang') === 'he'
|
|
43
|
+
return isRTL ? padding.inline_end : padding.inline_start
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// RTL-aware label positioning
|
|
47
|
+
const labelXPosition = computed(() => {
|
|
48
|
+
const isRTL = document.documentElement.dir === 'rtl' ||
|
|
49
|
+
document.documentElement.getAttribute('lang') === 'he'
|
|
50
|
+
return isRTL ? paddingLeft.value + chartWidth.value + 10 : paddingLeft.value - 10
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const labelTextAnchor = computed(() => {
|
|
54
|
+
const isRTL = document.documentElement.dir === 'rtl' ||
|
|
55
|
+
document.documentElement.getAttribute('lang') === 'he'
|
|
56
|
+
return isRTL ? 'start' : 'end'
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const maxValue = computed(() => Math.max(...props.data.map(d => d.value), 0))
|
|
60
|
+
const minValue = computed(() => Math.min(...props.data.map(d => d.value), 0))
|
|
61
|
+
|
|
62
|
+
const xScale = computed(() => {
|
|
63
|
+
const isRTL = document.documentElement.dir === 'rtl' ||
|
|
64
|
+
document.documentElement.getAttribute('lang') === 'he'
|
|
65
|
+
const domain = props.data.length - 1
|
|
66
|
+
|
|
67
|
+
return (index: number) => {
|
|
68
|
+
const position = (index / domain) * chartWidth.value
|
|
69
|
+
// In RTL, reverse the x position so the chart flows from right to left
|
|
70
|
+
return isRTL ? chartWidth.value - position : position
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const yScale = computed(() => {
|
|
75
|
+
const range = maxValue.value - minValue.value || 1
|
|
76
|
+
return (value: number) => chartHeight.value - ((value - minValue.value) / range) * chartHeight.value
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const pathData = computed(() => {
|
|
80
|
+
if (props.data.length === 0) return ''
|
|
81
|
+
|
|
82
|
+
const points = props.data.map((d, i) =>
|
|
83
|
+
`${i === 0 ? 'M' : 'L'} ${paddingLeft.value + xScale.value(i)} ${padding.top + yScale.value(d.value)}`
|
|
84
|
+
).join(' ')
|
|
85
|
+
|
|
86
|
+
return points
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const gridLines = computed(() => {
|
|
90
|
+
const lines = []
|
|
91
|
+
const step = chartHeight.value / 4
|
|
92
|
+
for (let i = 0; i <= 4; i++) {
|
|
93
|
+
const y = i * step
|
|
94
|
+
const value = maxValue.value - (i / 4) * (maxValue.value - minValue.value)
|
|
95
|
+
lines.push({ y, value })
|
|
96
|
+
}
|
|
97
|
+
return lines
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
function formatValue(value: number): string {
|
|
101
|
+
if (props.currency) {
|
|
102
|
+
return new Intl.NumberFormat('he-IL', {
|
|
103
|
+
style: 'currency',
|
|
104
|
+
currency: 'ILS',
|
|
105
|
+
minimumFractionDigits: 0
|
|
106
|
+
}).format(value)
|
|
107
|
+
}
|
|
108
|
+
return value.toLocaleString()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatDate(dateStr: string): string {
|
|
112
|
+
const date = new Date(dateStr)
|
|
113
|
+
return date.toLocaleDateString('he-IL', { month: 'short', day: 'numeric' })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onMounted(() => {
|
|
117
|
+
if (svgRef.value) {
|
|
118
|
+
width.value = svgRef.value.clientWidth || 600
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<div class="line-chart h-100p flex column flex-stretch">
|
|
125
|
+
<div class="flex space-between">
|
|
126
|
+
<div class="flex align-center gap-05 pb-1">
|
|
127
|
+
<Icon :name="icon" size="1.2" :color="color" class="line-height-08" />
|
|
128
|
+
<p class="white-space light m_txt14">
|
|
129
|
+
{{ title }}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="chart-container flex-grow w-100p relative">
|
|
134
|
+
<svg
|
|
135
|
+
ref="svgRef"
|
|
136
|
+
:width="width"
|
|
137
|
+
:height="height"
|
|
138
|
+
class="chart-svg h-100p w-100p"
|
|
139
|
+
>
|
|
140
|
+
<!-- Grid lines -->
|
|
141
|
+
<g class="grid">
|
|
142
|
+
<line
|
|
143
|
+
v-for="line in gridLines"
|
|
144
|
+
:key="line.y"
|
|
145
|
+
:x1="paddingLeft"
|
|
146
|
+
:y1="padding.top + line.y"
|
|
147
|
+
:x2="paddingLeft + chartWidth"
|
|
148
|
+
:y2="padding.top + line.y"
|
|
149
|
+
stroke="#e0e0e0"
|
|
150
|
+
stroke-width="1"
|
|
151
|
+
/>
|
|
152
|
+
<!-- Y-axis labels -->
|
|
153
|
+
<text
|
|
154
|
+
v-for="line in gridLines"
|
|
155
|
+
:key="`label-${line.y}`"
|
|
156
|
+
:x="labelXPosition"
|
|
157
|
+
:y="padding.top + line.y + 4"
|
|
158
|
+
class="grid-label"
|
|
159
|
+
:text-anchor="labelTextAnchor"
|
|
160
|
+
>
|
|
161
|
+
{{ formatValue(line.value) }}
|
|
162
|
+
</text>
|
|
163
|
+
</g>
|
|
164
|
+
|
|
165
|
+
<!-- X-axis -->
|
|
166
|
+
<line
|
|
167
|
+
:x1="paddingLeft"
|
|
168
|
+
:y1="padding.top + chartHeight"
|
|
169
|
+
:x2="paddingLeft + chartWidth"
|
|
170
|
+
:y2="padding.top + chartHeight"
|
|
171
|
+
stroke="#ccc"
|
|
172
|
+
stroke-width="2"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<!-- Y-axis -->
|
|
176
|
+
<line
|
|
177
|
+
:x1="paddingLeft"
|
|
178
|
+
:y1="padding.top"
|
|
179
|
+
:x2="paddingLeft"
|
|
180
|
+
:y2="padding.top + chartHeight"
|
|
181
|
+
stroke="#ccc"
|
|
182
|
+
stroke-width="2"
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<!-- Line path -->
|
|
186
|
+
<path
|
|
187
|
+
:d="pathData"
|
|
188
|
+
:stroke="color"
|
|
189
|
+
stroke-width="3"
|
|
190
|
+
fill="none"
|
|
191
|
+
stroke-linecap="round"
|
|
192
|
+
stroke-linejoin="round"
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
<!-- Data points -->
|
|
196
|
+
<g v-if="showPoints">
|
|
197
|
+
<circle
|
|
198
|
+
v-for="(point, index) in data"
|
|
199
|
+
:key="index"
|
|
200
|
+
:cx="paddingLeft + xScale(index)"
|
|
201
|
+
:cy="padding.top + yScale(point.value)"
|
|
202
|
+
r="4"
|
|
203
|
+
:fill="color"
|
|
204
|
+
class="data-point"
|
|
205
|
+
>
|
|
206
|
+
<title>{{ formatDate(point.date) }}: {{ formatValue(point.value) }}</title>
|
|
207
|
+
</circle>
|
|
208
|
+
</g>
|
|
209
|
+
|
|
210
|
+
<!-- X-axis labels -->
|
|
211
|
+
<g class="x-labels">
|
|
212
|
+
<text
|
|
213
|
+
v-for="(point, index) in data.filter((_, i) => i % Math.ceil(data.length / 6) === 0)"
|
|
214
|
+
:key="index"
|
|
215
|
+
:x="paddingLeft + xScale(data.findIndex(d => d === point))"
|
|
216
|
+
:y="padding.top + chartHeight + 15"
|
|
217
|
+
class="axis-label"
|
|
218
|
+
text-anchor="middle"
|
|
219
|
+
>
|
|
220
|
+
{{ formatDate(point.date) }}
|
|
221
|
+
</text>
|
|
222
|
+
</g>
|
|
223
|
+
</svg>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</template>
|
|
227
|
+
|
|
228
|
+
<style scoped>
|
|
229
|
+
.line-chart, .chart-container {
|
|
230
|
+
direction: inherit;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.chart-svg {
|
|
234
|
+
transform-origin: center;
|
|
235
|
+
display: block; /* Remove any inline spacing */
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.chart-container {
|
|
239
|
+
overflow: hidden; /* Prevent any overflow spacing */
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.grid-label,
|
|
243
|
+
.axis-label {
|
|
244
|
+
fill: var(--bgl-gray) !important;;
|
|
245
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
246
|
+
font-size: 12px;
|
|
247
|
+
margin: 1rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.axis-label {
|
|
251
|
+
text-anchor: middle;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.data-point {
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
transition: r 0.2s ease;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.data-point:hover {
|
|
260
|
+
r: 6;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
[dir="rtl"] .line-chart {
|
|
264
|
+
text-align: right;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
</style>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Icon } from '@bagelink/vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface PieData {
|
|
6
|
+
label: string
|
|
7
|
+
value: number
|
|
8
|
+
color?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
data: PieData[]
|
|
13
|
+
title?: string
|
|
14
|
+
icon?: string
|
|
15
|
+
color?: string
|
|
16
|
+
size?: number
|
|
17
|
+
showLegend?: boolean
|
|
18
|
+
donut?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
22
|
+
title: 'Pie Chart',
|
|
23
|
+
icon: 'bx-pie-chart-alt',
|
|
24
|
+
color: '#3B82F6',
|
|
25
|
+
size: 200,
|
|
26
|
+
showLegend: true,
|
|
27
|
+
donut: false
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Generate colors with different opacity based on the main color
|
|
31
|
+
const colors = computed(() => {
|
|
32
|
+
const baseColor = props.color
|
|
33
|
+
const opacities = [1, 0.8, 0.6, 0.4, 0.85, 0.7, 0.5, 0.3, 0.9, 0.75]
|
|
34
|
+
|
|
35
|
+
return opacities.map((opacity) => {
|
|
36
|
+
// Convert hex to rgba with opacity
|
|
37
|
+
const hex = baseColor.replace('#', '')
|
|
38
|
+
const r = Number.parseInt(hex.substr(0, 2), 16)
|
|
39
|
+
const g = Number.parseInt(hex.substr(2, 2), 16)
|
|
40
|
+
const b = Number.parseInt(hex.substr(4, 2), 16)
|
|
41
|
+
|
|
42
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// // RTL detection
|
|
47
|
+
// const isRtl = computed(() => {
|
|
48
|
+
// if (typeof document !== 'undefined') {
|
|
49
|
+
// return document.documentElement.dir === 'rtl' || document.body.dir === 'rtl'
|
|
50
|
+
// }
|
|
51
|
+
// return false
|
|
52
|
+
// })
|
|
53
|
+
|
|
54
|
+
const totalValue = computed(() => props.data.reduce((sum, item) => sum + item.value, 0)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const pieSegments = computed(() => {
|
|
58
|
+
let cumulativeAngle = 0
|
|
59
|
+
return props.data.map((item, index) => {
|
|
60
|
+
const percentage = item.value / totalValue.value
|
|
61
|
+
const angle = percentage * 360
|
|
62
|
+
const startAngle = cumulativeAngle
|
|
63
|
+
const endAngle = cumulativeAngle + angle
|
|
64
|
+
cumulativeAngle += angle
|
|
65
|
+
|
|
66
|
+
const startAngleRad = (startAngle * Math.PI) / 180
|
|
67
|
+
const endAngleRad = (endAngle * Math.PI) / 180
|
|
68
|
+
|
|
69
|
+
const radius = props.size / 2 - 10
|
|
70
|
+
const innerRadius = props.donut ? radius * 0.6 : 0
|
|
71
|
+
|
|
72
|
+
const largeArcFlag = angle > 180 ? 1 : 0
|
|
73
|
+
|
|
74
|
+
const pathData = props.donut
|
|
75
|
+
? createDonutPath(startAngleRad, endAngleRad, radius, innerRadius, largeArcFlag)
|
|
76
|
+
: createPiePath(startAngleRad, endAngleRad, radius, largeArcFlag)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...item,
|
|
80
|
+
percentage,
|
|
81
|
+
angle,
|
|
82
|
+
startAngle,
|
|
83
|
+
endAngle,
|
|
84
|
+
pathData,
|
|
85
|
+
color: item.color || colors.value[index % colors.value.length]
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function createPiePath(startAngle: number, endAngle: number, radius: number, largeArcFlag: number): string {
|
|
91
|
+
const x1 = Math.cos(startAngle) * radius
|
|
92
|
+
const y1 = Math.sin(startAngle) * radius
|
|
93
|
+
const x2 = Math.cos(endAngle) * radius
|
|
94
|
+
const y2 = Math.sin(endAngle) * radius
|
|
95
|
+
|
|
96
|
+
return `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createDonutPath(startAngle: number, endAngle: number, outerRadius: number, innerRadius: number, largeArcFlag: number): string {
|
|
100
|
+
const x1Outer = Math.cos(startAngle) * outerRadius
|
|
101
|
+
const y1Outer = Math.sin(startAngle) * outerRadius
|
|
102
|
+
const x2Outer = Math.cos(endAngle) * outerRadius
|
|
103
|
+
const y2Outer = Math.sin(endAngle) * outerRadius
|
|
104
|
+
|
|
105
|
+
const x1Inner = Math.cos(startAngle) * innerRadius
|
|
106
|
+
const y1Inner = Math.sin(startAngle) * innerRadius
|
|
107
|
+
const x2Inner = Math.cos(endAngle) * innerRadius
|
|
108
|
+
const y2Inner = Math.sin(endAngle) * innerRadius
|
|
109
|
+
|
|
110
|
+
return `M ${x1Outer} ${y1Outer} A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2Outer} ${y2Outer} L ${x2Inner} ${y2Inner} A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x1Inner} ${y1Inner} Z`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatValue(value: number): string {
|
|
114
|
+
return value.toLocaleString()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatPercentage(percentage: number): string {
|
|
118
|
+
return `${(percentage * 100).toFixed(1)}%`
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
<div class="h-100p flex column flex-stretch">
|
|
124
|
+
<div class="flex space-between">
|
|
125
|
+
<div class="flex align-center gap-05 pb-1">
|
|
126
|
+
<Icon :name="icon" size="1.2" :color="color" class="line-height-08" />
|
|
127
|
+
<p class="white-space light m_txt14">
|
|
128
|
+
{{ title }}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="chart-container flex space-between gap-1 flex-wrap" :class="{ 'with-legend': showLegend }">
|
|
133
|
+
<svg :width="size" :height="size" class="chart-svg mx-auto flex-shrink-0">
|
|
134
|
+
<g :transform="`translate(${size / 2}, ${size / 2})`">
|
|
135
|
+
<path
|
|
136
|
+
v-for="(segment, index) in pieSegments" :key="index" :d="segment.pathData"
|
|
137
|
+
:fill="segment.color" class="pie-segment" stroke="white" stroke-width="2"
|
|
138
|
+
>
|
|
139
|
+
<title>{{ segment.label }}: {{ formatValue(segment.value) }} ({{
|
|
140
|
+
formatPercentage(segment.percentage) }})</title>
|
|
141
|
+
</path>
|
|
142
|
+
</g>
|
|
143
|
+
</svg>
|
|
144
|
+
|
|
145
|
+
<div v-if="showLegend" class="legend mx-auto display-flex column gap-05 min-w-100px">
|
|
146
|
+
<div
|
|
147
|
+
v-for="(segment, index) in pieSegments" :key="index"
|
|
148
|
+
class="gap-1 flex align-items-center txt14 justify-content-start"
|
|
149
|
+
>
|
|
150
|
+
<div class="legend-color flex-shrink-0 radius-05" :style="{ backgroundColor: segment.color }" />
|
|
151
|
+
<span v-if="segment.label" class="flex-shrink-1 flex-grow-1">{{ segment.label }}</span>
|
|
152
|
+
<span class="bold">{{ formatValue(segment.value) }}</span>
|
|
153
|
+
<span class="opacity-6 txt12">({{ formatPercentage(segment.percentage) }})</span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
|
|
160
|
+
<style scoped>
|
|
161
|
+
[dir="rtl"] .chart-container.with-legend {
|
|
162
|
+
flex-direction: row-reverse;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.pie-segment {
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
transition: opacity 0.2s ease;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.pie-segment:hover {
|
|
171
|
+
opacity: 0.8;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* RTL support for legend */
|
|
175
|
+
[dir="rtl"] .legend-item {
|
|
176
|
+
flex-direction: row-reverse;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.legend-color {
|
|
180
|
+
width: 12px;
|
|
181
|
+
height: 12px;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Component } from 'vue'
|
|
3
3
|
import type { CalendarEvent, CalendarProps, CalendarView, CalendarViewState } from './CalendarTypes'
|
|
4
|
-
import { timeDelta, Btn, ListItem, Dropdown,
|
|
4
|
+
import { timeDelta, Btn, ListItem, Dropdown, formatDate } from '@bagelink/vue'
|
|
5
5
|
import { ref, computed, onMounted } from 'vue'
|
|
6
6
|
import CalendarPopover from './CalendarPopover.vue'
|
|
7
7
|
import AgendaView from './views/AgendaView.vue'
|
|
@@ -124,44 +124,28 @@ onMounted(() => { emit('ready', state.value) })
|
|
|
124
124
|
<div class="calendar">
|
|
125
125
|
<div class="flex m_block m_pb-1">
|
|
126
126
|
<h3 class="txt-light my-0">
|
|
127
|
-
<b>{{
|
|
128
|
-
{{
|
|
127
|
+
<b>{{ formatDate(currentDate, 'MMMM') }}</b>
|
|
128
|
+
{{ formatDate(currentDate, 'YYYY') }}
|
|
129
129
|
</h3>
|
|
130
130
|
<div class="ms-auto flex gap-025">
|
|
131
131
|
<Dropdown thin :value="currentView" iconEnd="keyboard_arrow_down" color="gray">
|
|
132
|
-
<ListItem
|
|
133
|
-
v-for="(_, key) in views"
|
|
134
|
-
:key="key"
|
|
135
|
-
:title="key"
|
|
136
|
-
@click="handleViewChange(key)"
|
|
137
|
-
/>
|
|
132
|
+
<ListItem v-for="(_, key) in views" :key="key" thin :title="key" @click="handleViewChange(key)" />
|
|
138
133
|
</Dropdown>
|
|
139
134
|
<Btn icon="calendar" thin color="gray" value="Today" @click="handleDateChange(new Date())" />
|
|
140
135
|
<Btn
|
|
141
|
-
icon="chevron_left"
|
|
142
|
-
color="gray"
|
|
143
|
-
thin
|
|
136
|
+
icon="chevron_left" color="gray" thin
|
|
144
137
|
@click="handleDateChange(timeDelta(currentDate, { [currentView]: -1 }))"
|
|
145
138
|
/>
|
|
146
139
|
<Btn
|
|
147
|
-
icon="chevron_right"
|
|
148
|
-
color="gray"
|
|
149
|
-
thin
|
|
140
|
+
icon="chevron_right" color="gray" thin
|
|
150
141
|
@click="handleDateChange(timeDelta(currentDate, { [currentView]: 1 }))"
|
|
151
142
|
/>
|
|
152
143
|
</div>
|
|
153
144
|
</div>
|
|
154
145
|
<component
|
|
155
|
-
:is="views[currentView]"
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
:week-start="weekStart"
|
|
159
|
-
@event-click="handleEventClick"
|
|
160
|
-
@event-create="handleEventCreate"
|
|
161
|
-
@event-update="handleEventUpdate"
|
|
162
|
-
@event-delete="handleEventDelete"
|
|
163
|
-
@date-change="handleDateChange"
|
|
164
|
-
@open-popover="openPopover"
|
|
146
|
+
:is="views[currentView]" :events="events" :start-date="currentDate" :week-start="weekStart"
|
|
147
|
+
@event-click="handleEventClick" @event-create="handleEventCreate" @event-update="handleEventUpdate"
|
|
148
|
+
@event-delete="handleEventDelete" @date-change="handleDateChange" @open-popover="openPopover"
|
|
165
149
|
>
|
|
166
150
|
<template #eventContent="{ event }">
|
|
167
151
|
<slot name="eventContent" :event="event" />
|
|
@@ -170,12 +154,8 @@ onMounted(() => { emit('ready', state.value) })
|
|
|
170
154
|
|
|
171
155
|
<!-- Global Popover -->
|
|
172
156
|
<CalendarPopover
|
|
173
|
-
:event="activeEvent"
|
|
174
|
-
:
|
|
175
|
-
:show="showPopover"
|
|
176
|
-
:has-event-content-slot="!!$slots.eventContent"
|
|
177
|
-
@close="closePopover"
|
|
178
|
-
@event-click="handleEventClick"
|
|
157
|
+
:event="activeEvent" :position="popoverPosition" :show="showPopover"
|
|
158
|
+
:has-event-content-slot="!!$slots.eventContent" @close="closePopover" @event-click="handleEventClick"
|
|
179
159
|
>
|
|
180
160
|
<template #eventContent="{ event }">
|
|
181
161
|
<slot name="eventContent" :event="event" />
|
|
@@ -186,9 +166,9 @@ onMounted(() => { emit('ready', state.value) })
|
|
|
186
166
|
|
|
187
167
|
<style scoped>
|
|
188
168
|
.calendar {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
height: 100%;
|
|
172
|
+
width: 100%;
|
|
193
173
|
}
|
|
194
174
|
</style>
|