@bagelink/vue 1.4.107 → 1.4.111
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 +8 -0
- package/dist/components/Btn.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/form/BagelForm.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/DatePicker.vue.d.ts +1 -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/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.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 +2 -2
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/dist/utils/BagelFormUtils.d.ts +1 -2
- 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/Btn.vue +53 -0
- 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 +196 -0
- package/src/components/analytics/index.ts +4 -0
- package/src/components/form/BagelForm.vue +24 -0
- package/src/components/form/inputs/DatePicker.vue +3 -2
- package/src/components/form/inputs/RadioGroup.vue +60 -35
- 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 +105 -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 +31 -3
- package/src/styles/inputs.css +9 -0
- package/src/styles/theme.css +2 -2
- package/src/types/BagelForm.ts +3 -2
- package/src/utils/BagelFormUtils.ts +1 -3
- package/src/utils/calendar/dateUtils.ts +71 -17
- package/src/utils/elementUtils.ts +22 -0
- package/src/utils/useSearch.ts +14 -7
|
@@ -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,196 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { Icon } from '@bagelink/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 = parseInt(hex.substr(0, 2), 16)
|
|
39
|
+
const g = parseInt(hex.substr(2, 2), 16)
|
|
40
|
+
const b = 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(() =>
|
|
55
|
+
props.data.reduce((sum, item) => sum + item.value, 0)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const pieSegments = computed(() => {
|
|
59
|
+
let cumulativeAngle = 0
|
|
60
|
+
return props.data.map((item, index) => {
|
|
61
|
+
const percentage = item.value / totalValue.value
|
|
62
|
+
const angle = percentage * 360
|
|
63
|
+
const startAngle = cumulativeAngle
|
|
64
|
+
const endAngle = cumulativeAngle + angle
|
|
65
|
+
cumulativeAngle += angle
|
|
66
|
+
|
|
67
|
+
const startAngleRad = (startAngle * Math.PI) / 180
|
|
68
|
+
const endAngleRad = (endAngle * Math.PI) / 180
|
|
69
|
+
|
|
70
|
+
const radius = props.size / 2 - 10
|
|
71
|
+
const innerRadius = props.donut ? radius * 0.6 : 0
|
|
72
|
+
|
|
73
|
+
const largeArcFlag = angle > 180 ? 1 : 0
|
|
74
|
+
|
|
75
|
+
const pathData = props.donut
|
|
76
|
+
? createDonutPath(startAngleRad, endAngleRad, radius, innerRadius, largeArcFlag)
|
|
77
|
+
: createPiePath(startAngleRad, endAngleRad, radius, largeArcFlag)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
...item,
|
|
81
|
+
percentage,
|
|
82
|
+
angle,
|
|
83
|
+
startAngle,
|
|
84
|
+
endAngle,
|
|
85
|
+
pathData,
|
|
86
|
+
color: item.color || colors.value[index % colors.value.length]
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
function createPiePath(startAngle: number, endAngle: number, radius: number, largeArcFlag: number): string {
|
|
92
|
+
const x1 = Math.cos(startAngle) * radius
|
|
93
|
+
const y1 = Math.sin(startAngle) * radius
|
|
94
|
+
const x2 = Math.cos(endAngle) * radius
|
|
95
|
+
const y2 = Math.sin(endAngle) * radius
|
|
96
|
+
|
|
97
|
+
return `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createDonutPath(startAngle: number, endAngle: number, outerRadius: number, innerRadius: number, largeArcFlag: number): string {
|
|
101
|
+
const x1Outer = Math.cos(startAngle) * outerRadius
|
|
102
|
+
const y1Outer = Math.sin(startAngle) * outerRadius
|
|
103
|
+
const x2Outer = Math.cos(endAngle) * outerRadius
|
|
104
|
+
const y2Outer = Math.sin(endAngle) * outerRadius
|
|
105
|
+
|
|
106
|
+
const x1Inner = Math.cos(startAngle) * innerRadius
|
|
107
|
+
const y1Inner = Math.sin(startAngle) * innerRadius
|
|
108
|
+
const x2Inner = Math.cos(endAngle) * innerRadius
|
|
109
|
+
const y2Inner = Math.sin(endAngle) * innerRadius
|
|
110
|
+
|
|
111
|
+
return `M ${x1Outer} ${y1Outer} A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2Outer} ${y2Outer} L ${x2Inner} ${y2Inner} A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x1Inner} ${y1Inner} Z`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatValue(value: number): string {
|
|
115
|
+
return value.toLocaleString()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatPercentage(percentage: number): string {
|
|
119
|
+
return `${(percentage * 100).toFixed(1)}%`
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<div class="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 space-between gap-1 flex-wrap" :class="{ 'with-legend': showLegend }">
|
|
134
|
+
<svg
|
|
135
|
+
:width="size"
|
|
136
|
+
:height="size"
|
|
137
|
+
class="chart-svg mx-auto flex-shrink-0"
|
|
138
|
+
>
|
|
139
|
+
<g :transform="`translate(${size / 2}, ${size / 2})`">
|
|
140
|
+
<path
|
|
141
|
+
v-for="(segment, index) in pieSegments"
|
|
142
|
+
:key="index"
|
|
143
|
+
:d="segment.pathData"
|
|
144
|
+
:fill="segment.color"
|
|
145
|
+
class="pie-segment"
|
|
146
|
+
stroke="white"
|
|
147
|
+
stroke-width="2"
|
|
148
|
+
>
|
|
149
|
+
<title>{{ segment.label }}: {{ formatValue(segment.value) }} ({{ formatPercentage(segment.percentage) }})</title>
|
|
150
|
+
</path>
|
|
151
|
+
</g>
|
|
152
|
+
</svg>
|
|
153
|
+
|
|
154
|
+
<div v-if="showLegend" class="legend mx-auto display-flex column gap-05 min-w-100px">
|
|
155
|
+
<div
|
|
156
|
+
v-for="(segment, index) in pieSegments"
|
|
157
|
+
:key="index"
|
|
158
|
+
class="gap-1 flex align-items-center txt14 justify-content-start"
|
|
159
|
+
>
|
|
160
|
+
<div
|
|
161
|
+
class="legend-color flex-shrink-0 radius-05"
|
|
162
|
+
:style="{ backgroundColor: segment.color }"
|
|
163
|
+
/>
|
|
164
|
+
<span v-if="segment.label" class="flex-shrink-1 flex-grow-1">{{ segment.label }}</span>
|
|
165
|
+
<span class="bold">{{ formatValue(segment.value) }}</span>
|
|
166
|
+
<span class="opacity-6 txt12">({{ formatPercentage(segment.percentage) }})</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</template>
|
|
172
|
+
|
|
173
|
+
<style scoped>
|
|
174
|
+
[dir="rtl"] .chart-container.with-legend {
|
|
175
|
+
flex-direction: row-reverse;
|
|
176
|
+
}
|
|
177
|
+
.pie-segment {
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
transition: opacity 0.2s ease;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.pie-segment:hover {
|
|
183
|
+
opacity: 0.8;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* RTL support for legend */
|
|
187
|
+
[dir="rtl"] .legend-item {
|
|
188
|
+
flex-direction: row-reverse;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.legend-color {
|
|
192
|
+
width: 12px;
|
|
193
|
+
height: 12px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
</style>
|
|
@@ -43,6 +43,7 @@ const isDirty = computed(() => {
|
|
|
43
43
|
// Initialize on mount
|
|
44
44
|
onMounted(() => {
|
|
45
45
|
if (props.modelValue) initialFormData.value = clone(props.modelValue)
|
|
46
|
+
applyDefaultValues()
|
|
46
47
|
})
|
|
47
48
|
|
|
48
49
|
// Watch for model changes
|
|
@@ -50,6 +51,29 @@ watch(() => props.modelValue, (val) => {
|
|
|
50
51
|
if (val !== undefined) formData.value = clone(val)
|
|
51
52
|
}, { immediate: true, deep: true })
|
|
52
53
|
|
|
54
|
+
function applyDefaultValues() {
|
|
55
|
+
const schema = resolvedSchema.value as unknown as Array<BaseBagelField<T, any>> | undefined
|
|
56
|
+
if (!schema) return
|
|
57
|
+
|
|
58
|
+
const walk = (nodes: any[]) => {
|
|
59
|
+
for (const node of nodes) {
|
|
60
|
+
if (!node || typeof node !== 'object') continue
|
|
61
|
+
const hasId = typeof node.id === 'string' && node.id.length > 0
|
|
62
|
+
const hasDefault = Object.prototype.hasOwnProperty.call(node, 'defaultValue') && node.defaultValue !== undefined
|
|
63
|
+
if (hasId && hasDefault) {
|
|
64
|
+
const current = getNestedValue(formData.value as any, node.id as string, undefined)
|
|
65
|
+
if (current === undefined || current === '') {
|
|
66
|
+
updateFormData({ fieldId: node.id, value: node.defaultValue })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(node.children) && node.children.length) walk(node.children)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
walk(schema as any[])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
watch(resolvedSchema, () => { applyDefaultValues() }, { deep: true })
|
|
76
|
+
|
|
53
77
|
// Update form data
|
|
54
78
|
function updateFormData({ fieldId, value }: { fieldId?: string, value: any }) {
|
|
55
79
|
const keys = fieldId?.split('.') || []
|
|
@@ -15,12 +15,13 @@ const props = withDefaults(
|
|
|
15
15
|
locale?: string
|
|
16
16
|
enableTime?: boolean
|
|
17
17
|
highlightedDates?: MaybeRefOrGetter<(string | Date)[]>
|
|
18
|
+
autoSize?: boolean
|
|
18
19
|
}>(),
|
|
19
20
|
{
|
|
20
21
|
mode: () => ({ mode: 'day' }),
|
|
21
22
|
firstDayOfWeek: WEEK_START_DAY.SUNDAY,
|
|
22
23
|
locale: '',
|
|
23
|
-
enableTime: false
|
|
24
|
+
enableTime: false,
|
|
24
25
|
},
|
|
25
26
|
)
|
|
26
27
|
|
|
@@ -309,7 +310,7 @@ function selectDate(date: Date | null) {
|
|
|
309
310
|
</script>
|
|
310
311
|
|
|
311
312
|
<template>
|
|
312
|
-
<div class="ltr
|
|
313
|
+
<div class="ltr gap-075 m_flex-wrap calendar-container justify-content-center h-100p" :class="{ 'flex': !autoSize }">
|
|
313
314
|
<div class="calendar-section m_border-none px-05 m_p-0">
|
|
314
315
|
<div class="flex space-between pb-1">
|
|
315
316
|
<template v-if="currentView === 'days'">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts" generic="ContextObjType extends { [key: string]: any }">
|
|
2
2
|
import { Btn } from '@bagelink/vue'
|
|
3
|
+
import { watch } from 'vue'
|
|
3
4
|
|
|
4
5
|
export interface RadioOption<T> {
|
|
5
6
|
imgAlt?: string
|
|
@@ -11,15 +12,26 @@ export interface RadioOption<T> {
|
|
|
11
12
|
value: any
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
type RadioOptionsSource<T> = RadioOption<T>[] | (() => Promise<RadioOption<T>[]>)
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
id?: string
|
|
20
|
+
groupName?: string
|
|
21
|
+
options: RadioOptionsSource<ContextObjType>
|
|
22
|
+
deletable?: boolean
|
|
23
|
+
required?: boolean
|
|
24
|
+
error?: string
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
thin?: boolean
|
|
27
|
+
flat?: boolean
|
|
28
|
+
invertedActive?: boolean
|
|
29
|
+
align?: 'start' | 'center' | 'end' | 'top' | 'bottom'
|
|
30
|
+
}>(),
|
|
31
|
+
{
|
|
32
|
+
align: 'center'
|
|
33
|
+
}
|
|
34
|
+
)
|
|
23
35
|
|
|
24
36
|
const emit = defineEmits(['delete', 'focus', 'blur', 'change'])
|
|
25
37
|
|
|
@@ -31,6 +43,23 @@ const name = $computed(
|
|
|
31
43
|
)
|
|
32
44
|
)
|
|
33
45
|
const selectedOption = defineModel('modelValue')
|
|
46
|
+
let loadedOptions = $ref<any[]>([])
|
|
47
|
+
const visibleOptions = $computed(() => Array.isArray(props.options) ? (props.options as any[]) : (loadedOptions as any[]))
|
|
48
|
+
|
|
49
|
+
async function loadOptionsIfNeeded() {
|
|
50
|
+
if (typeof props.options === 'function') {
|
|
51
|
+
try {
|
|
52
|
+
loadedOptions = await props.options()
|
|
53
|
+
} catch {
|
|
54
|
+
loadedOptions = []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
watch(() => props.options, () => {
|
|
60
|
+
loadedOptions = []
|
|
61
|
+
loadOptionsIfNeeded()
|
|
62
|
+
}, { immediate: true, deep: true })
|
|
34
63
|
let isFocused = $ref(false)
|
|
35
64
|
|
|
36
65
|
const containerClasses = $computed(() => ({
|
|
@@ -57,60 +86,56 @@ function handleChange() {
|
|
|
57
86
|
<template>
|
|
58
87
|
<div :class="containerClasses">
|
|
59
88
|
<label
|
|
60
|
-
v-for="(opt, index) in
|
|
61
|
-
|
|
62
|
-
class="border rounded p-1 flex bg-gray-light mb-05 gap-075 active-list-item hover"
|
|
89
|
+
v-for="(opt, index) in visibleOptions" :key="opt.id || `${name}-${index}`"
|
|
90
|
+
class="border rounded flex mb-05 active-list-item hover"
|
|
63
91
|
:for="opt.id || `${name}-${index}`"
|
|
92
|
+
:class="{ 'p-05 gap-025': thin, 'py-1 gap-075': !thin, 'bg-gray-light': !flat, 'align-items-start': align === 'start' || align === 'top', 'align-items-center': align === 'center', 'align-items-end': align === 'end' || align === 'bottom', invertedActive }"
|
|
64
93
|
>
|
|
65
94
|
<input
|
|
66
|
-
:id="opt.id || `${name}-${index}`"
|
|
67
|
-
|
|
68
|
-
:disabled
|
|
69
|
-
class="radio-input-list"
|
|
70
|
-
type="radio"
|
|
71
|
-
:name
|
|
72
|
-
:value="opt.value"
|
|
73
|
-
:required="required"
|
|
74
|
-
@focus="handleFocus"
|
|
75
|
-
@blur="handleBlur"
|
|
95
|
+
:id="opt.id || `${name}-${index}`" v-model="selectedOption" :disabled class="radio-input-list"
|
|
96
|
+
type="radio" :name :value="opt.value" :required="required" @focus="handleFocus" @blur="handleBlur"
|
|
76
97
|
@change="handleChange"
|
|
98
|
+
:class="{
|
|
99
|
+
'mt-025': align === 'start' || align === 'top',
|
|
100
|
+
'mb-025': align === 'end' || align === 'bottom'
|
|
101
|
+
}"
|
|
77
102
|
>
|
|
78
103
|
<div class="flex w-100 gap-1 flex-wrap m_gap-05 m_gap-row-025">
|
|
79
104
|
<img
|
|
80
|
-
v-if="opt.imgSrc"
|
|
81
|
-
class="bg-popup shadow-light py-025 radius-05 m_w40"
|
|
82
|
-
width="60"
|
|
83
|
-
:src="opt.imgSrc"
|
|
105
|
+
v-if="opt.imgSrc" class="bg-popup shadow-light py-025 radius-05 m_w40" height="40" :src="opt.imgSrc"
|
|
84
106
|
:alt="opt.imgAlt"
|
|
85
107
|
>
|
|
86
108
|
<div class="">
|
|
87
|
-
<p v-if="opt.label" class="m-0 m_txt-14">{{ opt.label }}</p>
|
|
109
|
+
<p v-if="opt.label" class="m-0 m_txt-14 line-height-14 pb-025">{{ opt.label }}</p>
|
|
88
110
|
<p v-if="opt.subLabel" class="txt-gray txt-12 m-0">{{ opt.subLabel }}</p>
|
|
89
111
|
</div>
|
|
90
112
|
<slot name="radioItem" v-bind="opt" />
|
|
91
113
|
</div>
|
|
92
|
-
<Btn
|
|
93
|
-
v-if="deletable"
|
|
94
|
-
class="ms-auto"
|
|
95
|
-
flat thin icon="delete"
|
|
96
|
-
@click="$emit('delete', opt)"
|
|
97
|
-
/>
|
|
114
|
+
<Btn v-if="deletable" class="ms-auto" flat thin icon="delete" @click="$emit('delete', opt)" />
|
|
98
115
|
</label>
|
|
99
116
|
</div>
|
|
100
117
|
</template>
|
|
101
118
|
|
|
102
119
|
<style scoped>
|
|
103
|
-
.radio-input-list{
|
|
120
|
+
.radio-input-list {
|
|
104
121
|
width: auto;
|
|
105
122
|
transform: scale(1.4);
|
|
106
123
|
margin-inline-end: 0.6rem;
|
|
107
124
|
|
|
108
125
|
}
|
|
109
|
-
|
|
126
|
+
|
|
127
|
+
.active-list-item:has(:checked) {
|
|
110
128
|
background: var(--bgl-primary-light) !important;
|
|
111
129
|
border-color: var(--bgl-primary) !important;
|
|
112
130
|
accent-color: var(--bgl-accent-color);
|
|
113
131
|
}
|
|
132
|
+
.invertedActive:has(:checked) {
|
|
133
|
+
background: var(--bgl-primary) !important;
|
|
134
|
+
border-color: var(--bgl-primary) !important;
|
|
135
|
+
color: var(--bgl-white);
|
|
136
|
+
accent-color: var(--bgl-white);
|
|
137
|
+
|
|
138
|
+
}
|
|
114
139
|
|
|
115
140
|
.has-error .active-list-item {
|
|
116
141
|
border-color: var(--bgl-red) !important;
|