@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.
Files changed (69) hide show
  1. package/dist/components/Btn.vue.d.ts +8 -0
  2. package/dist/components/Btn.vue.d.ts.map +1 -1
  3. package/dist/components/ListItem.vue.d.ts +6 -1
  4. package/dist/components/ListItem.vue.d.ts.map +1 -1
  5. package/dist/components/analytics/BarChart.vue.d.ts +39 -0
  6. package/dist/components/analytics/BarChart.vue.d.ts.map +1 -0
  7. package/dist/components/analytics/KpiCard.vue.d.ts +24 -0
  8. package/dist/components/analytics/KpiCard.vue.d.ts.map +1 -0
  9. package/dist/components/analytics/LineChart.vue.d.ts +26 -0
  10. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -0
  11. package/dist/components/analytics/PieChart.vue.d.ts +24 -0
  12. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -0
  13. package/dist/components/analytics/index.d.ts +5 -0
  14. package/dist/components/analytics/index.d.ts.map +1 -0
  15. package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
  16. package/dist/components/form/inputs/DatePicker.vue.d.ts +1 -0
  17. package/dist/components/form/inputs/DatePicker.vue.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RadioGroup.vue.d.ts +6 -10
  19. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  20. package/dist/components/form/inputs/SelectInput.vue.d.ts +2 -2
  21. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  22. package/dist/components/layout/AppContent.vue.d.ts +34 -0
  23. package/dist/components/layout/AppContent.vue.d.ts.map +1 -0
  24. package/dist/components/layout/AppLayout.vue.d.ts +27 -0
  25. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -0
  26. package/dist/components/layout/AppSidebar.vue.d.ts +44 -0
  27. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -0
  28. package/dist/components/layout/index.d.ts +3 -0
  29. package/dist/components/layout/index.d.ts.map +1 -1
  30. package/dist/composables/useFormField.d.ts.map +1 -1
  31. package/dist/composables/useSchemaField.d.ts.map +1 -1
  32. package/dist/index.cjs +19 -19
  33. package/dist/index.mjs +10 -10
  34. package/dist/style.css +1 -1
  35. package/dist/types/BagelForm.d.ts +2 -2
  36. package/dist/types/BagelForm.d.ts.map +1 -1
  37. package/dist/utils/BagelFormUtils.d.ts +1 -2
  38. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  39. package/dist/utils/calendar/dateUtils.d.ts +21 -0
  40. package/dist/utils/calendar/dateUtils.d.ts.map +1 -1
  41. package/dist/utils/elementUtils.d.ts +5 -0
  42. package/dist/utils/elementUtils.d.ts.map +1 -1
  43. package/dist/utils/useSearch.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/Btn.vue +53 -0
  46. package/src/components/ListItem.vue +32 -24
  47. package/src/components/analytics/BarChart.vue +153 -0
  48. package/src/components/analytics/KpiCard.vue +84 -0
  49. package/src/components/analytics/LineChart.vue +267 -0
  50. package/src/components/analytics/PieChart.vue +196 -0
  51. package/src/components/analytics/index.ts +4 -0
  52. package/src/components/form/BagelForm.vue +24 -0
  53. package/src/components/form/inputs/DatePicker.vue +3 -2
  54. package/src/components/form/inputs/RadioGroup.vue +60 -35
  55. package/src/components/form/inputs/SelectInput.vue +94 -101
  56. package/src/components/form/inputs/Upload/upload.css +135 -138
  57. package/src/components/layout/AppContent.vue +105 -0
  58. package/src/components/layout/AppLayout.vue +124 -0
  59. package/src/components/layout/AppSidebar.vue +271 -0
  60. package/src/components/layout/index.ts +5 -0
  61. package/src/composables/useFormField.ts +6 -0
  62. package/src/composables/useSchemaField.ts +31 -3
  63. package/src/styles/inputs.css +9 -0
  64. package/src/styles/theme.css +2 -2
  65. package/src/types/BagelForm.ts +3 -2
  66. package/src/utils/BagelFormUtils.ts +1 -3
  67. package/src/utils/calendar/dateUtils.ts +71 -17
  68. package/src/utils/elementUtils.ts +22 -0
  69. 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>
@@ -0,0 +1,4 @@
1
+ export { default as BarChart } from './BarChart.vue'
2
+ export { default as LineChart } from './LineChart.vue'
3
+ export { default as KpiCard } from './KpiCard.vue'
4
+ export { default as PieChart } from './PieChart.vue'
@@ -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 flex gap-075 m_flex-wrap calendar-container justify-content-center h-100p">
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
- const props = defineProps<{
15
- id?: string
16
- groupName?: string
17
- options: RadioOption<ContextObjType>[]
18
- deletable?: boolean
19
- required?: boolean
20
- error?: string
21
- disabled?: boolean
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 options"
61
- :key="opt.id || `${name}-${index}`"
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
- v-model="selectedOption"
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
- .active-list-item:has(:checked){
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;