@bagelink/vue 1.15.63 → 1.15.65

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 (136) hide show
  1. package/dist/components/AccordionItem.vue.d.ts.map +1 -1
  2. package/dist/components/Avatar.vue.d.ts +6 -1
  3. package/dist/components/Avatar.vue.d.ts.map +1 -1
  4. package/dist/components/Badge.vue.d.ts.map +1 -1
  5. package/dist/components/Card.vue.d.ts +7 -0
  6. package/dist/components/Card.vue.d.ts.map +1 -1
  7. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  8. package/dist/components/EmptyState.vue.d.ts +43 -0
  9. package/dist/components/EmptyState.vue.d.ts.map +1 -0
  10. package/dist/components/Icon/Icon.vue.d.ts +13 -0
  11. package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
  12. package/dist/components/Image.vue.d.ts +26 -1
  13. package/dist/components/Image.vue.d.ts.map +1 -1
  14. package/dist/components/ListItem.vue.d.ts +9 -9
  15. package/dist/components/ListItem.vue.d.ts.map +1 -1
  16. package/dist/components/Menu.vue.d.ts.map +1 -1
  17. package/dist/components/Swiper.vue.d.ts +3 -3
  18. package/dist/components/calendar/CalendarPopover.vue.d.ts +10 -0
  19. package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
  20. package/dist/components/charts/BarChart.vue.d.ts +34 -0
  21. package/dist/components/charts/BarChart.vue.d.ts.map +1 -0
  22. package/dist/components/charts/ChartTooltip.vue.d.ts +33 -0
  23. package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -0
  24. package/dist/components/charts/Donut.vue.d.ts +53 -0
  25. package/dist/components/charts/Donut.vue.d.ts.map +1 -0
  26. package/dist/components/charts/Funnel.vue.d.ts +53 -0
  27. package/dist/components/charts/Funnel.vue.d.ts.map +1 -0
  28. package/dist/components/charts/Gauge.vue.d.ts +28 -0
  29. package/dist/components/charts/Gauge.vue.d.ts.map +1 -0
  30. package/dist/components/charts/LineChart.vue.d.ts +37 -0
  31. package/dist/components/charts/LineChart.vue.d.ts.map +1 -0
  32. package/dist/components/charts/RadialBars.vue.d.ts +34 -0
  33. package/dist/components/charts/RadialBars.vue.d.ts.map +1 -0
  34. package/dist/components/charts/RankBars.vue.d.ts +27 -0
  35. package/dist/components/charts/RankBars.vue.d.ts.map +1 -0
  36. package/dist/components/charts/Sparkline.vue.d.ts +25 -0
  37. package/dist/components/charts/Sparkline.vue.d.ts.map +1 -0
  38. package/dist/components/charts/StatCard.vue.d.ts +28 -0
  39. package/dist/components/charts/StatCard.vue.d.ts.map +1 -0
  40. package/dist/components/charts/core/data.d.ts +46 -0
  41. package/dist/components/charts/core/data.d.ts.map +1 -0
  42. package/dist/components/charts/core/format.d.ts +13 -0
  43. package/dist/components/charts/core/format.d.ts.map +1 -0
  44. package/dist/components/charts/core/palette.d.ts +19 -0
  45. package/dist/components/charts/core/palette.d.ts.map +1 -0
  46. package/dist/components/charts/core/uid.d.ts +2 -0
  47. package/dist/components/charts/core/uid.d.ts.map +1 -0
  48. package/dist/components/charts/core/useChartAnim.d.ts +11 -0
  49. package/dist/components/charts/core/useChartAnim.d.ts.map +1 -0
  50. package/dist/components/charts/core/useChartFrame.d.ts +21 -0
  51. package/dist/components/charts/core/useChartFrame.d.ts.map +1 -0
  52. package/dist/components/charts/core/useScale.d.ts +16 -0
  53. package/dist/components/charts/core/useScale.d.ts.map +1 -0
  54. package/dist/components/charts/index.d.ts +12 -0
  55. package/dist/components/charts/index.d.ts.map +1 -0
  56. package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -0
  57. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  58. package/dist/components/form/inputs/RangeInput.vue.d.ts +13 -4
  59. package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
  60. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  61. package/dist/components/index.d.ts +3 -1
  62. package/dist/components/index.d.ts.map +1 -1
  63. package/dist/components/layout/Layout.vue.d.ts +1 -1
  64. package/dist/components/layout/Layout.vue.d.ts.map +1 -1
  65. package/dist/components/layout/Panel.vue.d.ts +1 -1
  66. package/dist/components/layout/Panel.vue.d.ts.map +1 -1
  67. package/dist/components/layout/Timeline.types.d.ts +9 -0
  68. package/dist/components/layout/Timeline.types.d.ts.map +1 -0
  69. package/dist/components/layout/Timeline.vue.d.ts +42 -0
  70. package/dist/components/layout/Timeline.vue.d.ts.map +1 -0
  71. package/dist/components/layout/TimelineItem.vue.d.ts +37 -0
  72. package/dist/components/layout/TimelineItem.vue.d.ts.map +1 -0
  73. package/dist/components/layout/index.d.ts +3 -0
  74. package/dist/components/layout/index.d.ts.map +1 -1
  75. package/dist/dialog/Dialog.vue.d.ts +4 -0
  76. package/dist/dialog/Dialog.vue.d.ts.map +1 -1
  77. package/dist/index.cjs +110 -116
  78. package/dist/index.mjs +38059 -37009
  79. package/dist/style.css +1 -1
  80. package/package.json +2 -1
  81. package/src/components/AccordionItem.vue +24 -22
  82. package/src/components/Avatar.vue +49 -11
  83. package/src/components/Badge.vue +4 -7
  84. package/src/components/Card.vue +32 -2
  85. package/src/components/Dropdown.vue +14 -3
  86. package/src/components/EmptyState.vue +91 -0
  87. package/src/components/Icon/Icon.vue +118 -25
  88. package/src/components/Image.vue +70 -3
  89. package/src/components/ListItem.vue +43 -22
  90. package/src/components/Menu.vue +10 -2
  91. package/src/components/charts/BarChart.vue +197 -0
  92. package/src/components/charts/ChartTooltip.vue +74 -0
  93. package/src/components/charts/Donut.vue +219 -0
  94. package/src/components/charts/Funnel.vue +377 -0
  95. package/src/components/charts/Gauge.vue +90 -0
  96. package/src/components/charts/LineChart.vue +255 -0
  97. package/src/components/charts/RadialBars.vue +99 -0
  98. package/src/components/charts/RankBars.vue +72 -0
  99. package/src/components/charts/Sparkline.vue +90 -0
  100. package/src/components/charts/StatCard.vue +84 -0
  101. package/src/components/charts/core/data.ts +95 -0
  102. package/src/components/charts/core/format.ts +64 -0
  103. package/src/components/charts/core/palette.ts +52 -0
  104. package/src/components/charts/core/uid.ts +6 -0
  105. package/src/components/charts/core/useChartAnim.ts +60 -0
  106. package/src/components/charts/core/useChartFrame.ts +49 -0
  107. package/src/components/charts/core/useScale.ts +39 -0
  108. package/src/components/charts/index.ts +12 -0
  109. package/src/components/form/inputs/RadioGroup.vue +2 -1
  110. package/src/components/form/inputs/RangeInput.vue +43 -15
  111. package/src/components/form/inputs/SelectInput.vue +1 -19
  112. package/src/components/index.ts +3 -1
  113. package/src/components/layout/Timeline.types.ts +9 -0
  114. package/src/components/layout/Timeline.vue +54 -0
  115. package/src/components/layout/TimelineItem.vue +93 -0
  116. package/src/components/layout/index.ts +3 -0
  117. package/src/dialog/Dialog.vue +29 -1
  118. package/src/styles/bagel.css +1 -0
  119. package/src/styles/gradients.css +181 -0
  120. package/src/styles/layout.css +9 -0
  121. package/src/styles/theme.css +1 -1
  122. package/dist/components/analytics/BarChart.vue.d.ts +0 -47
  123. package/dist/components/analytics/BarChart.vue.d.ts.map +0 -1
  124. package/dist/components/analytics/KpiCard.vue.d.ts +0 -24
  125. package/dist/components/analytics/KpiCard.vue.d.ts.map +0 -1
  126. package/dist/components/analytics/LineChart.vue.d.ts +0 -35
  127. package/dist/components/analytics/LineChart.vue.d.ts.map +0 -1
  128. package/dist/components/analytics/PieChart.vue.d.ts +0 -53
  129. package/dist/components/analytics/PieChart.vue.d.ts.map +0 -1
  130. package/dist/components/analytics/index.d.ts +0 -5
  131. package/dist/components/analytics/index.d.ts.map +0 -1
  132. package/src/components/analytics/BarChart.vue +0 -262
  133. package/src/components/analytics/KpiCard.vue +0 -84
  134. package/src/components/analytics/LineChart.vue +0 -357
  135. package/src/components/analytics/PieChart.vue +0 -544
  136. package/src/components/analytics/index.ts +0 -4
@@ -0,0 +1,255 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LineChart — line + optional area fill, single or multi-series, theme-driven.
4
+ * Renders JUST the plot (no title/card chrome) so you can drop it in any Card.
5
+ *
6
+ * <LineChart :data="[5,8,6,12]" />
7
+ * <LineChart :data="revenue" color="green" area :currency="'USD'" />
8
+ * <LineChart :series="[{ name:'2024', data:[…] }, { name:'2025', data:[…] }]" />
9
+ */
10
+ import { computed, ref } from 'vue'
11
+ import { chartUid } from './core/uid'
12
+ import type { RawPoint, RawSeries } from './core/data'
13
+ import { allValues, normalizeSeries, sharedLabels } from './core/data'
14
+ import { alpha, resolveColor } from './core/palette'
15
+ import { formatLabel, formatValue, type ValueFormat } from './core/format'
16
+ import { linear, niceMax, ticks } from './core/useScale'
17
+ import { useChartFrame } from './core/useChartFrame'
18
+ import { useChartAnim } from './core/useChartAnim'
19
+ import ChartTooltip from './ChartTooltip.vue'
20
+
21
+ const props = withDefaults(defineProps<{
22
+ data?: RawPoint[]
23
+ series?: RawSeries[]
24
+ labels?: (string | number)[]
25
+ color?: string
26
+ /** Fill area under each line. */
27
+ area?: boolean
28
+ /** Use a soft top→bottom gradient for area fills (premium look).
29
+ Implies `area`. On by default when `area`/`stacked` is set. */
30
+ gradient?: boolean
31
+ /** Stack multi-series areas (implies `area`). Composition over time. */
32
+ stacked?: boolean
33
+ height?: number
34
+ /** Show horizontal grid lines + y ticks. */
35
+ grid?: boolean
36
+ /** Show data-point dots. */
37
+ points?: boolean
38
+ /** Currency code → formats values as currency. */
39
+ currency?: string
40
+ prefix?: string
41
+ suffix?: string
42
+ /** Max x-axis labels to show (avoids crowding). */
43
+ maxLabels?: number
44
+ animated?: boolean
45
+ }>(), {
46
+ height: 220,
47
+ grid: true,
48
+ points: false,
49
+ maxLabels: 7,
50
+ animated: true,
51
+ })
52
+
53
+ const el = ref<HTMLElement>()
54
+ const heightRef = computed(() => props.height)
55
+ const { width, height: plotH, pad, innerWidth, innerHeight, flipX, isRTL } = useChartFrame(el, { height: heightRef })
56
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 800 })
57
+
58
+ const series = computed(() => normalizeSeries(props.data, props.series, { labels: props.labels, defaultColor: props.color }))
59
+ const cats = computed(() => sharedLabels(series.value))
60
+
61
+ const valueFmt = computed<ValueFormat>(() => ({ currency: props.currency, prefix: props.prefix, suffix: props.suffix }))
62
+ const axisFmt = computed<ValueFormat>(() => ({ ...valueFmt.value, compact: true }))
63
+
64
+ const isStacked = computed(() => props.stacked && series.value.length > 1)
65
+
66
+ // Per-label cumulative totals for the stacked domain.
67
+ const stackTotals = computed(() => cats.value.map((_, i) => series.value.reduce((sum, s) => sum + (s.points[i]?.y ?? 0), 0)))
68
+
69
+ const yMax = computed(() => niceMax(isStacked.value ? Math.max(0, ...stackTotals.value) : Math.max(0, ...allValues(series.value))))
70
+ const yMin = computed(() => (isStacked.value ? 0 : Math.min(0, ...allValues(series.value))))
71
+
72
+ const sx = computed(() => linear(0, Math.max(1, cats.value.length - 1), 0, innerWidth.value))
73
+ const sy = computed(() => linear(yMin.value, yMax.value, innerHeight.value, 0))
74
+
75
+ const yTicks = computed(() => (props.grid ? ticks(yMin.value, yMax.value, 4) : []))
76
+
77
+ const lines = computed(() => {
78
+ const base = innerHeight.value
79
+ // Running cumulative per label for stacking.
80
+ const cum = cats.value.map(() => 0)
81
+ return series.value.map((s, i) => {
82
+ const color = resolveColor(s.color, i)
83
+ if (isStacked.value) {
84
+ const lower = cats.value.map((_, idx) => cum[idx])
85
+ const upper = cats.value.map((_, idx) => cum[idx] + (s.points[idx]?.y ?? 0))
86
+ upper.forEach((v, idx) => { cum[idx] = v })
87
+ const topCoords = upper.map((v, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(v), p: s.points[idx] ?? { label: cats.value[idx], y: v } }))
88
+ const line = topCoords.map((c, idx) => `${idx === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
89
+ const lowerPts = lower.map((v, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(v) }))
90
+ const down = [...lowerPts].reverse().map(c => `L ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
91
+ const areaPath = topCoords.length ? `${line} ${down} Z` : ''
92
+ return { name: s.name, color, coords: topCoords, line, areaPath }
93
+ }
94
+ const coords = s.points.map((p, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(p.y), p }))
95
+ const line = coords.map((c, idx) => `${idx === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
96
+ const areaPath = coords.length
97
+ ? `${line} L ${coords[coords.length - 1].x.toFixed(1)} ${base} L ${coords[0].x.toFixed(1)} ${base} Z`
98
+ : ''
99
+ return { name: s.name, color, coords, line, areaPath }
100
+ })
101
+ })
102
+
103
+ // In stacked mode the area is always shown (it's the whole point).
104
+ const showArea = computed(() => props.area || isStacked.value)
105
+ // Gradient fills default ON whenever an area is shown, unless explicitly off.
106
+ const useGradient = computed(() => showArea.value && props.gradient !== false)
107
+
108
+ const uid = chartUid()
109
+ const gradId = (i: number) => `${uid}-grad-${i}`
110
+
111
+ const labelStride = computed(() => Math.max(1, Math.ceil(cats.value.length / props.maxLabels)))
112
+ const dashLength = computed(() => (innerWidth.value + innerHeight.value) * 2)
113
+ const dashOffset = computed(() => dashLength.value * (1 - progress.value))
114
+
115
+ // ── Hover crosshair + shared tooltip (follows the mouse across the plot) ──────
116
+ const hoverIdx = ref<number | null>(null)
117
+ const mouseX = ref(0)
118
+
119
+ function onMove(e: MouseEvent) {
120
+ const host = el.value
121
+ if (!host || !cats.value.length) return
122
+ const rect = host.getBoundingClientRect()
123
+ // Mouse x within the plot area (account for left padding + RTL flip).
124
+ let local = e.clientX - rect.left - pad.value.left
125
+ if (isRTL.value) local = innerWidth.value - local
126
+ const idx = Math.round((local / Math.max(1, innerWidth.value)) * (cats.value.length - 1))
127
+ hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, idx))
128
+ mouseX.value = e.clientX - rect.left
129
+ }
130
+ function onLeave() { hoverIdx.value = null }
131
+
132
+ const hoverX = computed(() => (hoverIdx.value == null ? 0 : flipX(sx.value(hoverIdx.value))))
133
+
134
+ // Markers (one per series) at the hovered index.
135
+ const hoverMarkers = computed(() => {
136
+ if (hoverIdx.value == null) return []
137
+ return lines.value
138
+ .map(l => l.coords[hoverIdx.value as number])
139
+ .filter(Boolean)
140
+ .map((c, i) => ({ x: c.x, y: c.y, color: lines.value[i].color }))
141
+ })
142
+
143
+ // Tooltip rows: every series' value at the hovered label.
144
+ const hoverRows = computed(() => {
145
+ if (hoverIdx.value == null) return []
146
+ return series.value.map((s, i) => ({
147
+ name: s.name,
148
+ color: resolveColor(s.color, i),
149
+ value: formatValue(s.points[hoverIdx.value as number]?.y ?? 0, valueFmt.value),
150
+ }))
151
+ })
152
+ const hoverLabel = computed(() => (hoverIdx.value == null ? '' : formatLabel(cats.value[hoverIdx.value])))
153
+
154
+ // Total row for stacked charts.
155
+ const hoverTotal = computed(() => {
156
+ if (hoverIdx.value == null || !isStacked.value) return null
157
+ const sum = series.value.reduce((s, srs) => s + (srs.points[hoverIdx.value as number]?.y ?? 0), 0)
158
+ return formatValue(sum, valueFmt.value)
159
+ })
160
+
161
+ // Keep the tooltip inside the chart horizontally.
162
+ const tooltipStyle = computed(() => {
163
+ const half = 70
164
+ const x = Math.min(Math.max(mouseX.value, half), Math.max(half, width.value - half))
165
+ return { left: `${x}px` }
166
+ })
167
+ </script>
168
+
169
+ <template>
170
+ <div ref="el" class="bgl-chart w-100p relative" :style="{ height: `${plotH}px` }" @mousemove="onMove" @mouseleave="onLeave">
171
+ <svg v-if="width" :width="width" :height="plotH" class="display-block overflow-hidden">
172
+ <defs v-if="useGradient">
173
+ <linearGradient
174
+ v-for="(l, i) in lines" :id="gradId(i)" :key="i"
175
+ gradientUnits="userSpaceOnUse" :x1="0" :y1="0" :x2="0" :y2="innerHeight"
176
+ >
177
+ <stop offset="0%" :stop-color="l.color" :stop-opacity="isStacked ? 0.9 : 0.32" />
178
+ <stop offset="100%" :stop-color="l.color" :stop-opacity="isStacked ? 0.15 : 0.02" />
179
+ </linearGradient>
180
+ </defs>
181
+ <g :transform="`translate(${pad.left}, ${pad.top})`">
182
+ <!-- grid + y ticks -->
183
+ <g v-if="grid" class="bgl-chart__grid">
184
+ <g v-for="(t, i) in yTicks" :key="i">
185
+ <line :x1="0" :x2="innerWidth" :y1="sy(t)" :y2="sy(t)" />
186
+ <text class="bgl-chart__ytick" :x="-8" :y="sy(t) + 3">{{ formatValue(t, axisFmt) }}</text>
187
+ </g>
188
+ </g>
189
+
190
+ <!-- area fills (gradient by default; stacked layers read as bands) -->
191
+ <template v-if="showArea">
192
+ <path
193
+ v-for="(l, i) in lines" :key="`a${i}`" :d="l.areaPath"
194
+ :fill="useGradient ? `url(#${gradId(i)})` : alpha(l.color, isStacked ? 78 : 14)"
195
+ stroke="none" :opacity="progress"
196
+ />
197
+ </template>
198
+
199
+ <!-- lines -->
200
+ <path
201
+ v-for="(l, i) in lines" :key="`l${i}`" :d="l.line" :stroke="l.color" stroke-width="2.5"
202
+ fill="none" stroke-linecap="round" stroke-linejoin="round"
203
+ :stroke-dasharray="dashLength" :stroke-dashoffset="dashOffset"
204
+ />
205
+
206
+ <!-- points (values shown via the shared hover tooltip) -->
207
+ <template v-if="points">
208
+ <g v-for="(l, i) in lines" :key="`p${i}`">
209
+ <circle
210
+ v-for="(c, idx) in l.coords" :key="idx"
211
+ :cx="c.x" :cy="c.y" r="3.5" :fill="l.color" class="bgl-chart__pt" :opacity="progress"
212
+ />
213
+ </g>
214
+ </template>
215
+
216
+ <!-- x labels -->
217
+ <g class="bgl-chart__xlabels">
218
+ <text
219
+ v-for="(lbl, i) in cats" v-show="i % labelStride === 0" :key="i"
220
+ :x="flipX(sx(i))" :y="innerHeight + 16" text-anchor="middle"
221
+ >{{ formatLabel(lbl) }}</text>
222
+ </g>
223
+
224
+ <!-- hover crosshair + per-series markers -->
225
+ <g v-if="hoverIdx != null" class="bgl-chart__hover" pointer-events="none">
226
+ <line :x1="hoverX" :x2="hoverX" :y1="0" :y2="innerHeight" class="bgl-chart__crosshair" />
227
+ <circle v-for="(m, i) in hoverMarkers" :key="i" :cx="m.x" :cy="m.y" r="4" :fill="m.color" class="bgl-chart__hoverpt" />
228
+ </g>
229
+ </g>
230
+ </svg>
231
+
232
+ <!-- shared tooltip following the mouse -->
233
+ <ChartTooltip
234
+ v-if="hoverIdx != null" :style="tooltipStyle"
235
+ :label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
236
+ />
237
+ </div>
238
+ </template>
239
+
240
+ <style scoped>
241
+ .bgl-chart { direction: inherit; }
242
+ .bgl-chart__grid line { stroke: var(--bgl-border-color); stroke-width: 1; }
243
+ .bgl-chart__ytick,
244
+ .bgl-chart__xlabels text { fill: var(--bgl-gray); font-size: 11px; }
245
+ .bgl-chart__ytick { text-anchor: end; }
246
+ .bgl-chart__pt { cursor: pointer; transition: r 0.15s ease; outline: none; }
247
+ .bgl-chart__pt:hover { r: 5; }
248
+ .bgl-chart__pt:focus,
249
+ .bgl-chart__pt:focus-visible { outline: none; }
250
+ [dir="rtl"] .bgl-chart__ytick { text-anchor: start; }
251
+
252
+ /* hover crosshair + markers */
253
+ .bgl-chart__crosshair { stroke: var(--bgl-border-color); stroke-width: 1; stroke-dasharray: 3 3; }
254
+ .bgl-chart__hoverpt { stroke: var(--bgl-box-bg); stroke-width: 2; }
255
+ </style>
@@ -0,0 +1,99 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * RadialBars — concentric progress rings (the "activity rings" look). Each item
4
+ * is a value 0–max rendered as a ring; great for multi-KPI completion tiles.
5
+ *
6
+ * <RadialBars :data="[{label:'Sales',value:82},{label:'Leads',value:64},{label:'Churn',value:23}]" />
7
+ * <RadialBars :data="rings" :max="100" :legend="false" />
8
+ */
9
+ import { computed, ref } from 'vue'
10
+ import { alpha, resolveColor } from './core/palette'
11
+ import { useChartAnim } from './core/useChartAnim'
12
+ import type { ThemeType } from '../../types'
13
+
14
+ interface Ring { label: string; value: number; color?: ThemeType | string; max?: number }
15
+
16
+ const props = withDefaults(defineProps<{
17
+ data: Ring[]
18
+ /** Shared max when a ring doesn't set its own. */
19
+ max?: number
20
+ size?: number
21
+ /** Ring thickness in px. */
22
+ thickness?: number
23
+ /** Gap between rings in px. */
24
+ gap?: number
25
+ legend?: boolean
26
+ /** Show the value of each ring in the legend. Default %. */
27
+ suffix?: string
28
+ animated?: boolean
29
+ }>(), {
30
+ max: 100,
31
+ size: 180,
32
+ thickness: 14,
33
+ gap: 6,
34
+ legend: true,
35
+ suffix: '%',
36
+ animated: true,
37
+ })
38
+
39
+ const el = ref<HTMLElement>()
40
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 900 })
41
+
42
+ const cx = computed(() => props.size / 2)
43
+
44
+ function ringPath(r: number): string {
45
+ const c = cx.value
46
+ // Full-circle path (two arcs) starting at 12 o'clock.
47
+ return `M ${c} ${c - r} A ${r} ${r} 0 1 1 ${c - 0.001} ${c - r} Z`
48
+ }
49
+
50
+ interface Arc { path: string; color: string; label: string; display: string; circ: number; filled: number }
51
+ const arcs = computed<Arc[]>(() =>
52
+ props.data.map((d, i) => {
53
+ const r = cx.value - props.thickness / 2 - i * (props.thickness + props.gap)
54
+ const max = d.max ?? props.max
55
+ const frac = Math.min(1, Math.max(0, d.value / (max || 1)))
56
+ const circ = 2 * Math.PI * r
57
+ return {
58
+ path: ringPath(r),
59
+ color: resolveColor(d.color, i),
60
+ label: d.label,
61
+ display: `${d.value}${props.suffix}`,
62
+ circ,
63
+ filled: circ * frac * progress.value,
64
+ }
65
+ }),
66
+ )
67
+ </script>
68
+
69
+ <template>
70
+ <div
71
+ ref="el" class="bgl-radial inline-flex gap-1"
72
+ :class="legend ? 'row' : 'column'"
73
+ >
74
+ <svg :width="size" :height="size" :viewBox="`0 0 ${size} ${size}`" class="bgl-radial__svg flex-shrink-0">
75
+ <g v-for="(a, i) in arcs" :key="i">
76
+ <path :d="a.path" :stroke="alpha(a.color, 14)" :stroke-width="thickness" fill="none" stroke-linecap="round" />
77
+ <path
78
+ :d="a.path" :stroke="a.color" :stroke-width="thickness" fill="none" stroke-linecap="round"
79
+ :stroke-dasharray="`${a.filled} ${a.circ}`"
80
+ :transform="`rotate(-90 ${cx} ${cx})`"
81
+ />
82
+ </g>
83
+ </svg>
84
+
85
+ <ul v-if="legend" class="bgl-radial__legend grid gap-05 m-0 min-w-0">
86
+ <li v-for="(a, i) in arcs" :key="i" class="flex align-items-center gap-05 txt13">
87
+ <span class="bgl-radial__dot flex-shrink-0" :style="{ background: a.color }" />
88
+ <span>{{ a.label }}</span>
89
+ <span class="bgl-radial__val semibold tabular-nums ms-auto">{{ a.display }}</span>
90
+ </li>
91
+ </ul>
92
+ </div>
93
+ </template>
94
+
95
+ <style scoped>
96
+ .bgl-radial__svg path { transition: stroke-dasharray 0.3s ease; }
97
+ .bgl-radial__legend { list-style: none; padding: 0; }
98
+ .bgl-radial__dot { width: 10px; height: 10px; border-radius: 3px; }
99
+ </style>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * RankBars — horizontal ranked bars for top-N lists (leaderboards, sources by
4
+ * revenue, most-viewed). Pure CSS bars: label + bar + value per row.
5
+ *
6
+ * <RankBars :data="[{label:'Google',value:4200},{label:'Direct',value:3100}]" />
7
+ * <RankBars :data="rows" color="purple" currency="USD" :max-rows="5" />
8
+ */
9
+ import { computed } from 'vue'
10
+ import { ramp, resolveColor } from './core/palette'
11
+ import { formatValue, type ValueFormat } from './core/format'
12
+ import type { ThemeType } from '../../types'
13
+
14
+ interface Row { label: string; value: number; color?: ThemeType | string }
15
+
16
+ const props = withDefaults(defineProps<{
17
+ data: Row[]
18
+ /** Base tone for bars (monochrome shade ramp by rank). */
19
+ color?: ThemeType | string
20
+ /** One tone per row instead of shades. Per-row `color` always wins. */
21
+ mode?: 'shades' | 'multi'
22
+ /** Cap rows shown (already-sorted input recommended). */
23
+ maxRows?: number
24
+ /** Sort descending by value before rendering. */
25
+ sort?: boolean
26
+ currency?: string
27
+ prefix?: string
28
+ suffix?: string
29
+ }>(), {
30
+ color: 'primary',
31
+ mode: 'shades',
32
+ sort: true,
33
+ })
34
+
35
+ const fmt = computed<ValueFormat>(() => ({ currency: props.currency, prefix: props.prefix, suffix: props.suffix }))
36
+
37
+ const rows = computed(() => {
38
+ let r = [...props.data]
39
+ if (props.sort) r.sort((a, b) => b.value - a.value)
40
+ if (props.maxRows) r = r.slice(0, props.maxRows)
41
+ const max = Math.max(1, ...r.map(d => d.value))
42
+ return r.map((d, i) => ({
43
+ label: d.label,
44
+ value: d.value,
45
+ display: formatValue(d.value, fmt.value),
46
+ pct: (d.value / max) * 100,
47
+ color: d.color
48
+ ? resolveColor(d.color)
49
+ : props.mode === 'multi'
50
+ ? resolveColor(undefined, i)
51
+ : ramp(props.color, r.length, i),
52
+ }))
53
+ })
54
+ </script>
55
+
56
+ <template>
57
+ <div class="bgl-rank grid w-100p gap-05">
58
+ <div v-for="(r, i) in rows" :key="i" class="bgl-rank__row align-items-center grid gap-075">
59
+ <span class="bgl-rank__label ellipsis-1 txt14" :title="r.label">{{ r.label }}</span>
60
+ <span class="bgl-rank__track overflow-hidden round">
61
+ <span class="bgl-rank__bar display-block round" :style="{ width: `${r.pct}%`, background: r.color }" />
62
+ </span>
63
+ <span class="txt13 tabular-nums semibold">{{ r.display }}</span>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <style scoped>
69
+ .bgl-rank__row { grid-template-columns: minmax(60px, 28%) 1fr auto; }
70
+ .bgl-rank__track { height: 10px; background: var(--bgl-border-color); }
71
+ .bgl-rank__bar { height: 100%; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
72
+ </style>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Sparkline — a tiny inline line/area chart with no axes or labels. Perfect for
4
+ * cards, table cells, and stat tiles.
5
+ *
6
+ * <Sparkline :data="[5,8,6,12,9,14]" />
7
+ * <Sparkline :data="trend" color="green" area />
8
+ */
9
+ import { computed, ref } from 'vue'
10
+ import type { RawPoint } from './core/data'
11
+ import { normalizeSeries } from './core/data'
12
+ import { alpha, resolveColor } from './core/palette'
13
+ import { linear } from './core/useScale'
14
+ import { useChartAnim } from './core/useChartAnim'
15
+ import { chartUid } from './core/uid'
16
+
17
+ defineOptions({ name: 'BglSparkline' })
18
+
19
+ const props = withDefaults(defineProps<{
20
+ data: RawPoint[]
21
+ color?: string
22
+ /** Fill the area under the line. */
23
+ area?: boolean
24
+ /** Use a soft gradient for the area fill (default on when `area`). */
25
+ gradient?: boolean
26
+ width?: number
27
+ height?: number
28
+ strokeWidth?: number
29
+ /** Draw a dot on the last point. */
30
+ marker?: boolean
31
+ animated?: boolean
32
+ }>(), {
33
+ width: 100,
34
+ height: 28,
35
+ strokeWidth: 2,
36
+ animated: true,
37
+ })
38
+
39
+ const el = ref<HTMLElement>()
40
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 600 })
41
+
42
+ const stroke = computed(() => resolveColor(props.color))
43
+ const points = computed(() => normalizeSeries(props.data, undefined)[0]?.points ?? [])
44
+ const useGradient = computed(() => props.area && props.gradient !== false)
45
+ const gradId = chartUid('spark') + '-grad'
46
+
47
+ const pad = computed(() => props.strokeWidth + (props.marker ? 2 : 0))
48
+ const geom = computed(() => {
49
+ const pts = points.value
50
+ if (pts.length < 2) return { line: '', area: '', last: null as null | { x: number; y: number } }
51
+ const ys = pts.map(p => p.y)
52
+ const min = Math.min(...ys)
53
+ const max = Math.max(...ys)
54
+ const sx = linear(0, pts.length - 1, pad.value, props.width - pad.value)
55
+ const sy = linear(min, max, props.height - pad.value, pad.value)
56
+ const coords = pts.map((p, i) => ({ x: sx(i), y: sy(p.y) }))
57
+ const line = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
58
+ const area = `${line} L ${coords[coords.length - 1].x.toFixed(1)} ${props.height - pad.value} L ${coords[0].x.toFixed(1)} ${props.height - pad.value} Z`
59
+ return { line, area, last: coords[coords.length - 1] }
60
+ })
61
+
62
+ // Animate by revealing the stroke via dash offset.
63
+ const dashLength = computed(() => props.width * 2)
64
+ const dashOffset = computed(() => dashLength.value * (1 - progress.value))
65
+ </script>
66
+
67
+ <template>
68
+ <span ref="el" class="bgl-sparkline inline-block line-height-0 vertical-align-middle" :style="{ width: `${width}px`, height: `${height}px` }">
69
+ <svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" preserveAspectRatio="none" class="bgl-sparkline__svg display-block">
70
+ <defs v-if="useGradient">
71
+ <linearGradient :id="gradId" x1="0" y1="0" x2="0" y2="1">
72
+ <stop offset="0%" :stop-color="stroke" stop-opacity="0.35" />
73
+ <stop offset="100%" :stop-color="stroke" stop-opacity="0.02" />
74
+ </linearGradient>
75
+ </defs>
76
+ <path v-if="area && geom.area" :d="geom.area" :fill="useGradient ? `url(#${gradId})` : alpha(stroke, 14)" stroke="none" :opacity="progress" />
77
+ <path
78
+ :d="geom.line" :stroke="stroke" :stroke-width="strokeWidth" fill="none"
79
+ stroke-linecap="round" stroke-linejoin="round"
80
+ :stroke-dasharray="dashLength" :stroke-dashoffset="dashOffset"
81
+ />
82
+ <circle v-if="marker && geom.last" :cx="geom.last.x" :cy="geom.last.y" :r="strokeWidth + 0.5" :fill="stroke" :opacity="progress" />
83
+ </svg>
84
+ </span>
85
+ </template>
86
+
87
+ <style scoped>
88
+ /* Only the SVG overflow has no utility equivalent. */
89
+ .bgl-sparkline__svg { overflow: visible; }
90
+ </style>
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * StatCard — a KPI tile: label, big value, delta chip, and an optional trend
4
+ * sparkline. Composes Card + Sparkline; all formatting is locale-aware.
5
+ *
6
+ * <StatCard label="Revenue" :value="48200" currency="USD" :change="12.4" :trend="[…]" />
7
+ * <StatCard label="Active users" :value="1280" :change="-3.1" icon="group" color="purple" />
8
+ */
9
+ import { computed } from 'vue'
10
+ import Card from '../Card.vue'
11
+ import Icon from '../Icon/Icon.vue'
12
+ import Sparkline from './Sparkline.vue'
13
+ import type { RawPoint } from './core/data'
14
+ import { formatValue } from './core/format'
15
+ import { resolveColor } from './core/palette'
16
+ import type { ThemeType } from '../../types'
17
+
18
+ const props = withDefaults(defineProps<{
19
+ label: string
20
+ value: number | string
21
+ icon?: string
22
+ color?: ThemeType | string
23
+ /** Percentage change; sign drives the up/down chip color. */
24
+ change?: number
25
+ /** Invert chip colors when down is good (e.g. churn, cost). */
26
+ invertChange?: boolean
27
+ /** Sparkline data for a trend line. */
28
+ trend?: RawPoint[]
29
+ currency?: string
30
+ prefix?: string
31
+ suffix?: string
32
+ subtitle?: string
33
+ loading?: boolean
34
+ /** Outlined card (transparent bg + border), like `<Card outline>`. */
35
+ outline?: boolean
36
+ /** Hairline framed card, like `<Card frame>`. */
37
+ frame?: boolean
38
+ }>(), {
39
+ color: 'primary',
40
+ })
41
+
42
+ const accent = computed(() => resolveColor(props.color))
43
+ const formatted = computed(() =>
44
+ typeof props.value === 'number'
45
+ ? formatValue(props.value, { currency: props.currency, prefix: props.prefix, suffix: props.suffix })
46
+ : `${props.prefix ?? ''}${props.value}${props.suffix ?? ''}`,
47
+ )
48
+ const hasChange = computed(() => props.change !== undefined && props.change !== 0)
49
+ const up = computed(() => (props.change ?? 0) > 0)
50
+ const good = computed(() => (props.invertChange ? !up.value : up.value))
51
+ </script>
52
+
53
+ <template>
54
+ <Card thin :outline="outline" :frame="frame" class="bgl-stat flex column gap-1">
55
+ <div class="flex space-between w-100p align-items-start gap-05">
56
+ <div class="flex gap-05 align-items-center min-w-0">
57
+ <span v-if="icon" class="bgl-stat__icon grid place-items-center flex-shrink-0" :style="{ color: accent, background: `color-mix(in srgb, ${accent} 12%, transparent)` }">
58
+ <Icon :name="icon" :size="1.1" />
59
+ </span>
60
+ <div class="min-w-0">
61
+ <p class="txt13 opacity-6 m-0 ellipsis-1">{{ label }}</p>
62
+ <p v-if="subtitle" class="txt12 color-gray m-0 ellipsis-1">{{ subtitle }}</p>
63
+ </div>
64
+ </div>
65
+ <span v-if="hasChange" class="bgl-stat__delta inline-flex align-items-center flex-shrink-0 semibold" :class="good ? 'bgl-stat__delta--up' : 'bgl-stat__delta--down'">
66
+ <Icon :name="up ? 'trending_up' : 'trending_down'" :size="0.9" />
67
+ {{ Math.abs(change!).toFixed(1) }}%
68
+ </span>
69
+ </div>
70
+
71
+ <div class="flex space-between w-100p align-items-end gap-1">
72
+ <span class="txt28 m_txt24 semibold line-height-1 tabular-nums" :class="{ 'opacity-4': loading }">{{ loading ? '…' : formatted }}</span>
73
+ <Sparkline v-if="trend?.length" :data="trend" :color="color" area class="flex-shrink-0 opacity-9" :width="96" :height="32" />
74
+ </div>
75
+ </Card>
76
+ </template>
77
+
78
+ <style scoped>
79
+ .bgl-stat__icon { width: 34px; height: 34px; border-radius: 9px; }
80
+ /* Delta chip: layout via utilities; pill shape + sizing stay here. */
81
+ .bgl-stat__delta { gap: 0.15rem; font-size: 0.78rem; padding: 0.1rem 0.4rem; border-radius: 999px; }
82
+ .bgl-stat__delta--up { color: var(--bgl-green); background: color-mix(in srgb, var(--bgl-green) 12%, transparent); }
83
+ .bgl-stat__delta--down { color: var(--bgl-red); background: color-mix(in srgb, var(--bgl-red) 12%, transparent); }
84
+ </style>