@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.
- package/dist/components/AccordionItem.vue.d.ts.map +1 -1
- package/dist/components/Avatar.vue.d.ts +6 -1
- package/dist/components/Avatar.vue.d.ts.map +1 -1
- package/dist/components/Badge.vue.d.ts.map +1 -1
- package/dist/components/Card.vue.d.ts +7 -0
- package/dist/components/Card.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/EmptyState.vue.d.ts +43 -0
- package/dist/components/EmptyState.vue.d.ts.map +1 -0
- package/dist/components/Icon/Icon.vue.d.ts +13 -0
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +26 -1
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/ListItem.vue.d.ts +9 -9
- package/dist/components/ListItem.vue.d.ts.map +1 -1
- package/dist/components/Menu.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +3 -3
- package/dist/components/calendar/CalendarPopover.vue.d.ts +10 -0
- package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/charts/BarChart.vue.d.ts +34 -0
- package/dist/components/charts/BarChart.vue.d.ts.map +1 -0
- package/dist/components/charts/ChartTooltip.vue.d.ts +33 -0
- package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -0
- package/dist/components/charts/Donut.vue.d.ts +53 -0
- package/dist/components/charts/Donut.vue.d.ts.map +1 -0
- package/dist/components/charts/Funnel.vue.d.ts +53 -0
- package/dist/components/charts/Funnel.vue.d.ts.map +1 -0
- package/dist/components/charts/Gauge.vue.d.ts +28 -0
- package/dist/components/charts/Gauge.vue.d.ts.map +1 -0
- package/dist/components/charts/LineChart.vue.d.ts +37 -0
- package/dist/components/charts/LineChart.vue.d.ts.map +1 -0
- package/dist/components/charts/RadialBars.vue.d.ts +34 -0
- package/dist/components/charts/RadialBars.vue.d.ts.map +1 -0
- package/dist/components/charts/RankBars.vue.d.ts +27 -0
- package/dist/components/charts/RankBars.vue.d.ts.map +1 -0
- package/dist/components/charts/Sparkline.vue.d.ts +25 -0
- package/dist/components/charts/Sparkline.vue.d.ts.map +1 -0
- package/dist/components/charts/StatCard.vue.d.ts +28 -0
- package/dist/components/charts/StatCard.vue.d.ts.map +1 -0
- package/dist/components/charts/core/data.d.ts +46 -0
- package/dist/components/charts/core/data.d.ts.map +1 -0
- package/dist/components/charts/core/format.d.ts +13 -0
- package/dist/components/charts/core/format.d.ts.map +1 -0
- package/dist/components/charts/core/palette.d.ts +19 -0
- package/dist/components/charts/core/palette.d.ts.map +1 -0
- package/dist/components/charts/core/uid.d.ts +2 -0
- package/dist/components/charts/core/uid.d.ts.map +1 -0
- package/dist/components/charts/core/useChartAnim.d.ts +11 -0
- package/dist/components/charts/core/useChartAnim.d.ts.map +1 -0
- package/dist/components/charts/core/useChartFrame.d.ts +21 -0
- package/dist/components/charts/core/useChartFrame.d.ts.map +1 -0
- package/dist/components/charts/core/useScale.d.ts +16 -0
- package/dist/components/charts/core/useScale.d.ts.map +1 -0
- package/dist/components/charts/index.d.ts +12 -0
- package/dist/components/charts/index.d.ts.map +1 -0
- package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -0
- package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RangeInput.vue.d.ts +13 -4
- package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/Layout.vue.d.ts +1 -1
- package/dist/components/layout/Layout.vue.d.ts.map +1 -1
- package/dist/components/layout/Panel.vue.d.ts +1 -1
- package/dist/components/layout/Panel.vue.d.ts.map +1 -1
- package/dist/components/layout/Timeline.types.d.ts +9 -0
- package/dist/components/layout/Timeline.types.d.ts.map +1 -0
- package/dist/components/layout/Timeline.vue.d.ts +42 -0
- package/dist/components/layout/Timeline.vue.d.ts.map +1 -0
- package/dist/components/layout/TimelineItem.vue.d.ts +37 -0
- package/dist/components/layout/TimelineItem.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/dialog/Dialog.vue.d.ts +4 -0
- package/dist/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/index.cjs +110 -116
- package/dist/index.mjs +38059 -37009
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/AccordionItem.vue +24 -22
- package/src/components/Avatar.vue +49 -11
- package/src/components/Badge.vue +4 -7
- package/src/components/Card.vue +32 -2
- package/src/components/Dropdown.vue +14 -3
- package/src/components/EmptyState.vue +91 -0
- package/src/components/Icon/Icon.vue +118 -25
- package/src/components/Image.vue +70 -3
- package/src/components/ListItem.vue +43 -22
- package/src/components/Menu.vue +10 -2
- package/src/components/charts/BarChart.vue +197 -0
- package/src/components/charts/ChartTooltip.vue +74 -0
- package/src/components/charts/Donut.vue +219 -0
- package/src/components/charts/Funnel.vue +377 -0
- package/src/components/charts/Gauge.vue +90 -0
- package/src/components/charts/LineChart.vue +255 -0
- package/src/components/charts/RadialBars.vue +99 -0
- package/src/components/charts/RankBars.vue +72 -0
- package/src/components/charts/Sparkline.vue +90 -0
- package/src/components/charts/StatCard.vue +84 -0
- package/src/components/charts/core/data.ts +95 -0
- package/src/components/charts/core/format.ts +64 -0
- package/src/components/charts/core/palette.ts +52 -0
- package/src/components/charts/core/uid.ts +6 -0
- package/src/components/charts/core/useChartAnim.ts +60 -0
- package/src/components/charts/core/useChartFrame.ts +49 -0
- package/src/components/charts/core/useScale.ts +39 -0
- package/src/components/charts/index.ts +12 -0
- package/src/components/form/inputs/RadioGroup.vue +2 -1
- package/src/components/form/inputs/RangeInput.vue +43 -15
- package/src/components/form/inputs/SelectInput.vue +1 -19
- package/src/components/index.ts +3 -1
- package/src/components/layout/Timeline.types.ts +9 -0
- package/src/components/layout/Timeline.vue +54 -0
- package/src/components/layout/TimelineItem.vue +93 -0
- package/src/components/layout/index.ts +3 -0
- package/src/dialog/Dialog.vue +29 -1
- package/src/styles/bagel.css +1 -0
- package/src/styles/gradients.css +181 -0
- package/src/styles/layout.css +9 -0
- package/src/styles/theme.css +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +0 -47
- package/dist/components/analytics/BarChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/KpiCard.vue.d.ts +0 -24
- package/dist/components/analytics/KpiCard.vue.d.ts.map +0 -1
- package/dist/components/analytics/LineChart.vue.d.ts +0 -35
- package/dist/components/analytics/LineChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/PieChart.vue.d.ts +0 -53
- package/dist/components/analytics/PieChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/index.d.ts +0 -5
- package/dist/components/analytics/index.d.ts.map +0 -1
- package/src/components/analytics/BarChart.vue +0 -262
- package/src/components/analytics/KpiCard.vue +0 -84
- package/src/components/analytics/LineChart.vue +0 -357
- package/src/components/analytics/PieChart.vue +0 -544
- package/src/components/analytics/index.ts +0 -4
package/src/components/Image.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
defineOptions({ name: 'BglImage' })
|
|
2
|
+
defineOptions({ name: 'BglImage', inheritAttrs: true })
|
|
3
3
|
import { Skeleton, normalizeDimension, appendScript, awaitGlobal, normalizeURL, Icon, pathKeyToURL } from '@bagelink/vue'
|
|
4
|
-
import { ref, watch } from 'vue'
|
|
4
|
+
import { computed, ref, useSlots, watch } from 'vue'
|
|
5
5
|
|
|
6
6
|
interface ImageProps {
|
|
7
7
|
src?: string
|
|
@@ -11,6 +11,16 @@ interface ImageProps {
|
|
|
11
11
|
height?: string | number
|
|
12
12
|
caption?: string
|
|
13
13
|
modelValue?: string
|
|
14
|
+
/** Aspect ratio, e.g. "1", "2-3", "16-9" — maps to the `ratio-*` utility (keeps a stable box while loading). */
|
|
15
|
+
ratio?: string
|
|
16
|
+
/** Gradient shown behind/instead of the image — a tone (`purple`), a CSS gradient string, or `[from, to]`. Doubles as the fallback when the image fails or is absent. */
|
|
17
|
+
gradient?: string | [string, string]
|
|
18
|
+
/** object-fit for the image. @default 'cover' when `ratio`/`gradient` is set. */
|
|
19
|
+
fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
|
|
20
|
+
/** mix-blend-mode for blending the photo into the gradient (e.g. 'luminosity'). */
|
|
21
|
+
blend?: string
|
|
22
|
+
/** Rounded corners — true (default radius) or a px radius. */
|
|
23
|
+
rounded?: boolean | number
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
declare global {
|
|
@@ -92,10 +102,64 @@ async function loadImage() {
|
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
watch(() => [props.src, props.pathKey, props.modelValue], loadImage, { immediate: true })
|
|
105
|
+
|
|
106
|
+
// ── Framed mode (ratio / gradient / blend / rounded / overlay slot) ──────────
|
|
107
|
+
const slots = useSlots()
|
|
108
|
+
const framed = computed(() =>
|
|
109
|
+
props.ratio !== undefined || props.gradient !== undefined || props.blend !== undefined
|
|
110
|
+
|| props.rounded !== undefined || !!slots.default)
|
|
111
|
+
|
|
112
|
+
// Resolve `gradient` into a CSS background. Accepts a tone name, a [from,to]
|
|
113
|
+
// pair (tone or color), or a raw CSS gradient/color string.
|
|
114
|
+
function toneToVar(t: string) {
|
|
115
|
+
return /^(--|#|rgb|hsl|var\(|linear|radial|conic)/.test(t) ? t : `var(--bgl-${t})`
|
|
116
|
+
}
|
|
117
|
+
const gradientCss = computed(() => {
|
|
118
|
+
const g = props.gradient
|
|
119
|
+
if (!g) return undefined
|
|
120
|
+
if (Array.isArray(g)) return `linear-gradient(150deg, ${toneToVar(g[0])}, ${toneToVar(g[1])})`
|
|
121
|
+
if (/^(linear|radial|conic)-gradient/.test(g)) return g
|
|
122
|
+
// single tone → subtle two-stop using the tone and a darker mix
|
|
123
|
+
const c = toneToVar(g)
|
|
124
|
+
return `linear-gradient(150deg, ${c}, color-mix(in srgb, ${c} 55%, #000))`
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const ratioClass = computed(() => (props.ratio ? `ratio-${props.ratio}` : ''))
|
|
128
|
+
const radiusStyle = computed(() => {
|
|
129
|
+
if (props.rounded === undefined || props.rounded === false) return undefined
|
|
130
|
+
return typeof props.rounded === 'number' ? `${props.rounded}px` : 'var(--bgl-card-radius, 12px)'
|
|
131
|
+
})
|
|
132
|
+
const imgFit = computed(() => props.fit ?? ((props.ratio || props.gradient) ? 'cover' : undefined))
|
|
95
133
|
</script>
|
|
96
134
|
|
|
97
135
|
<template>
|
|
98
|
-
|
|
136
|
+
<!-- Framed mode: ratio box with gradient base, blended photo, and overlay slot. -->
|
|
137
|
+
<div
|
|
138
|
+
v-if="framed"
|
|
139
|
+
class="bgl-image-frame relative overflow-hidden"
|
|
140
|
+
:class="ratioClass"
|
|
141
|
+
:style="{
|
|
142
|
+
width: normalizeDimension(width),
|
|
143
|
+
height: ratio ? undefined : normalizeDimension(height),
|
|
144
|
+
background: gradientCss,
|
|
145
|
+
borderRadius: radiusStyle,
|
|
146
|
+
}"
|
|
147
|
+
>
|
|
148
|
+
<img
|
|
149
|
+
v-if="imageSrc"
|
|
150
|
+
:src="imageSrc"
|
|
151
|
+
:alt="alt"
|
|
152
|
+
class="bgl-image-frame-img absolute-fill"
|
|
153
|
+
:style="{ objectFit: imgFit, mixBlendMode: blend }"
|
|
154
|
+
>
|
|
155
|
+
<Skeleton v-else-if="!gradientCss && !loadingError" class="absolute-fill" />
|
|
156
|
+
<div v-else-if="loadingError && !gradientCss" class="flex-center absolute-fill error-image">
|
|
157
|
+
<Icon name="broken_image" />
|
|
158
|
+
</div>
|
|
159
|
+
<slot />
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<figcaption v-else-if="caption">
|
|
99
163
|
<img
|
|
100
164
|
v-if="imageSrc"
|
|
101
165
|
:src="imageSrc"
|
|
@@ -150,4 +214,7 @@ width: 100%;
|
|
|
150
214
|
background-color: var(--bgl-skeleton-bg);
|
|
151
215
|
}
|
|
152
216
|
|
|
217
|
+
.bgl-image-frame { display: block; }
|
|
218
|
+
.bgl-image-frame-img { width: 100%; height: 100%; }
|
|
219
|
+
|
|
153
220
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import type { IconType } from '@bagelink/vue'
|
|
2
|
+
import type { IconType, ThemeType } from '@bagelink/vue'
|
|
3
3
|
import { Avatar, Icon } from '@bagelink/vue'
|
|
4
4
|
import { computed } from 'vue'
|
|
5
5
|
|
|
@@ -22,13 +22,14 @@ const props = withDefaults(
|
|
|
22
22
|
fullWidth?: boolean
|
|
23
23
|
ellipsis?: boolean
|
|
24
24
|
ripple?: boolean
|
|
25
|
-
/**
|
|
26
|
-
to the
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/** Theme accent for the leading icon-chip and hover/active tint.
|
|
26
|
+
Defaults to the surface's primary. e.g. `color="purple"`. */
|
|
27
|
+
color?: ThemeType
|
|
28
|
+
/** Card-style row: rounded, no divider, tinted hover. Use for standalone
|
|
29
|
+
selectable lists (vs. the default flush divided rows in a panel). */
|
|
30
|
+
rounded?: boolean
|
|
31
|
+
/** Visually mark as selected (for non-router selection lists). */
|
|
32
|
+
active?: boolean
|
|
32
33
|
/** Render as an interactive row (cursor + hover) without needing a handler.
|
|
33
34
|
Implied when `to`, `href`, or `onClick` is set. */
|
|
34
35
|
clickable?: boolean
|
|
@@ -37,7 +38,6 @@ const props = withDefaults(
|
|
|
37
38
|
{
|
|
38
39
|
ellipsis: true,
|
|
39
40
|
ripple: false,
|
|
40
|
-
fullRow: false,
|
|
41
41
|
}
|
|
42
42
|
)
|
|
43
43
|
|
|
@@ -70,7 +70,8 @@ const bind = computed(() => {
|
|
|
70
70
|
<template>
|
|
71
71
|
<div
|
|
72
72
|
class="flex space-between list-item-row"
|
|
73
|
-
:class="{ 'no-border-list': props.flat, 'list-item-flush': props.fullWidth, 'list-item-fullrow':
|
|
73
|
+
:class="{ 'no-border-list': props.flat || rounded, 'list-item-flush': props.fullWidth, 'list-item-fullrow': isClickable, 'list-item-rounded': rounded, 'list-item-active': active }"
|
|
74
|
+
:style="color ? { '--bgl-list-item-accent': `var(--bgl-${color})` } : undefined"
|
|
74
75
|
>
|
|
75
76
|
<!-- Content rendered before the clickable area (e.g. a drag handle, avatar
|
|
76
77
|
or leading visual). Lives outside the clickable component so interacting
|
|
@@ -111,9 +112,9 @@ const bind = computed(() => {
|
|
|
111
112
|
</p>
|
|
112
113
|
</div>
|
|
113
114
|
</component>
|
|
114
|
-
<!-- Trailing meta.
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
<!-- Trailing meta. For clickable rows it floats over the end of the click
|
|
116
|
+
button so the button spans the whole row; the end content stays on top
|
|
117
|
+
and independently clickable. Tune room with --bgl-list-item-end-space. -->
|
|
117
118
|
<div class="list-item-end flex align-items-center gap-05 flex-shrink-0">
|
|
118
119
|
<slot name="end">
|
|
119
120
|
<span v-if="end" class="list-item-endtext" :class="{ 'ellipsis-1': ellipsis }" v-text="end" />
|
|
@@ -140,11 +141,14 @@ gap: 0.5rem;
|
|
|
140
141
|
independently clickable. The button reserves matching room so text never sits
|
|
141
142
|
under the meta. Tune the reserved width with --bgl-list-item-end-space. */
|
|
142
143
|
.list-item-fullrow {
|
|
143
|
-
--bgl-list-item-end-space:
|
|
144
|
+
--bgl-list-item-end-space: 3.5rem;
|
|
145
|
+
/* Keep the trailing meta aligned with the row's own inline padding (1rem) so it
|
|
146
|
+
doesn't crowd the edge. Override per-list with --bgl-list-item-end-inset. */
|
|
147
|
+
--bgl-list-item-end-inset: 1rem;
|
|
144
148
|
}
|
|
145
149
|
.list-item-fullrow .list-item-end {
|
|
146
150
|
position: absolute;
|
|
147
|
-
inset-inline-end:
|
|
151
|
+
inset-inline-end: var(--bgl-list-item-end-inset);
|
|
148
152
|
top: 50%;
|
|
149
153
|
transform: translateY(-50%);
|
|
150
154
|
z-index: 1;
|
|
@@ -156,7 +160,7 @@ pointer-events: none;
|
|
|
156
160
|
pointer-events: auto;
|
|
157
161
|
}
|
|
158
162
|
.list-item-fullrow .list-item:not(.px-0) { padding-inline-end: var(--bgl-list-item-end-space); }
|
|
159
|
-
.list-item-fullrow.list-item-flush .list-item-end {
|
|
163
|
+
.list-item-fullrow.list-item-flush .list-item-end { --bgl-list-item-end-inset: 0; }
|
|
160
164
|
.list-item-row::after {
|
|
161
165
|
content: '';
|
|
162
166
|
position: absolute;
|
|
@@ -194,6 +198,12 @@ opacity: 0.5;
|
|
|
194
198
|
pointer-events: none;
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
/* Accent used by the icon-chip and (for rounded/active) the tint. Defaults to
|
|
202
|
+
primary; set per-item via `color` prop → --bgl-list-item-accent. */
|
|
203
|
+
.list-item-row {
|
|
204
|
+
--bgl-list-item-accent: var(--bgl-primary);
|
|
205
|
+
}
|
|
206
|
+
|
|
197
207
|
/* Hover/active use Bagelink's translucent gray tint, which already flips from
|
|
198
208
|
translucent-dark (light mode) to translucent-light (.bgl-dark-mode) — so it
|
|
199
209
|
reads correctly on any background. Override per-list with --bgl-list-item-hover. */
|
|
@@ -201,8 +211,18 @@ pointer-events: none;
|
|
|
201
211
|
.list-item.router-link-exact-active {
|
|
202
212
|
background-color: var(--bgl-list-item-hover, var(--bgl-gray-tint));
|
|
203
213
|
}
|
|
204
|
-
.list-item.router-link-exact-active
|
|
205
|
-
|
|
214
|
+
.list-item.router-link-exact-active,
|
|
215
|
+
.list-item-active .list-item {
|
|
216
|
+
background-color: var(--bgl-list-item-active, color-mix(in srgb, var(--bgl-list-item-accent) 12%, transparent));
|
|
217
|
+
color: var(--bgl-list-item-accent);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Rounded card-style rows: pill-radius, tinted accent hover, no divider. */
|
|
221
|
+
.list-item-rounded .list-item {
|
|
222
|
+
border-radius: var(--bgl-card-border-radius);
|
|
223
|
+
}
|
|
224
|
+
.list-item-rounded .list-item:hover {
|
|
225
|
+
background-color: var(--bgl-list-item-hover, color-mix(in srgb, var(--bgl-list-item-accent) 8%, transparent));
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
.notClickable,
|
|
@@ -215,15 +235,15 @@ cursor: default;
|
|
|
215
235
|
filter: var(--bgl-hover-filter);
|
|
216
236
|
}
|
|
217
237
|
|
|
218
|
-
/* Leading icon chip */
|
|
238
|
+
/* Leading icon chip — tinted with the row accent (primary by default). */
|
|
219
239
|
.list-item-icon {
|
|
220
240
|
display: grid;
|
|
221
241
|
place-items: center;
|
|
222
242
|
width: 34px;
|
|
223
243
|
height: 34px;
|
|
224
244
|
border-radius: 9px;
|
|
225
|
-
background: color-mix(in srgb, var(--bgl-
|
|
226
|
-
color: var(--bgl-
|
|
245
|
+
background: color-mix(in srgb, var(--bgl-list-item-accent) 12%, transparent);
|
|
246
|
+
color: var(--bgl-list-item-accent);
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
/* Text block: tight, clear hierarchy */
|
|
@@ -238,13 +258,14 @@ line-height: 1.3;
|
|
|
238
258
|
}
|
|
239
259
|
.list-item-title {
|
|
240
260
|
font-size: 0.9375rem;
|
|
241
|
-
font-weight:
|
|
261
|
+
font-weight: 600;
|
|
242
262
|
line-height: 1.35;
|
|
243
263
|
}
|
|
244
264
|
.list-item-subtitle {
|
|
245
265
|
font-size: 0.8125rem;
|
|
246
266
|
opacity: 0.6;
|
|
247
267
|
line-height: 1.35;
|
|
268
|
+
font-weight: 300;
|
|
248
269
|
margin-top: 1px;
|
|
249
270
|
}
|
|
250
271
|
.list-item-endtext {
|
package/src/components/Menu.vue
CHANGED
|
@@ -11,13 +11,13 @@ defineProps<{ items: NavLink[] }>()
|
|
|
11
11
|
<div class="flex gap-025 m_gap-05">
|
|
12
12
|
<template v-for="item in items" :key="item.label">
|
|
13
13
|
<Dropdown v-if="item.children" :value="resolveI18n(item.label)" iconEnd="keyboard_arrow_down">
|
|
14
|
-
<template #trigger="{ show }">
|
|
14
|
+
<template #trigger="{ show, shown }">
|
|
15
15
|
<Btn thin flat :class="item.class" @click="show()">
|
|
16
16
|
<Icon :name="item.icon" size="0.8" style="margin-bottom: -0.1rem" class=" line-height-0" />
|
|
17
17
|
<p class="-ms-025">
|
|
18
18
|
{{ resolveI18n(item.label) }}
|
|
19
19
|
</p>
|
|
20
|
-
<Icon name="keyboard_arrow_down" size="0.8" style="margin-bottom: -0.1rem" class="
|
|
20
|
+
<Icon name="keyboard_arrow_down" size="0.8" style="margin-bottom: -0.1rem" class="line-height-0 menu-chevron" :class="{ 'menu-chevron-open': shown }" />
|
|
21
21
|
</Btn>
|
|
22
22
|
</template>
|
|
23
23
|
<Btn
|
|
@@ -53,6 +53,14 @@ background: var(--bgl-primary) !important;
|
|
|
53
53
|
color: var(--bgl-white) !important;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/* Chevron flips when its dropdown is open (mirror-pair → rotate, not swap). */
|
|
57
|
+
.menu-chevron {
|
|
58
|
+
transition: transform 0.2s ease;
|
|
59
|
+
}
|
|
60
|
+
.menu-chevron-open {
|
|
61
|
+
transform: rotate(180deg);
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
</style>
|
|
57
65
|
|
|
58
66
|
<style>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BarChart — vertical bars, single or multi-series (grouped or stacked).
|
|
4
|
+
* Renders just the plot; wrap in your own Card for a titled panel.
|
|
5
|
+
*
|
|
6
|
+
* <BarChart :data="[12,18,9,22]" :labels="['Q1','Q2','Q3','Q4']" />
|
|
7
|
+
* <BarChart :series="[{name:'A',data:[…]},{name:'B',data:[…]}]" stacked />
|
|
8
|
+
*/
|
|
9
|
+
import { computed, ref } from 'vue'
|
|
10
|
+
import { chartUid } from './core/uid'
|
|
11
|
+
import type { RawPoint, RawSeries } from './core/data'
|
|
12
|
+
import { allValues, normalizeSeries, sharedLabels } from './core/data'
|
|
13
|
+
import { resolveColor } from './core/palette'
|
|
14
|
+
import { formatLabel, formatValue, type ValueFormat } from './core/format'
|
|
15
|
+
import { band, linear, niceMax, ticks } from './core/useScale'
|
|
16
|
+
import { useChartFrame } from './core/useChartFrame'
|
|
17
|
+
import { useChartAnim } from './core/useChartAnim'
|
|
18
|
+
import ChartTooltip from './ChartTooltip.vue'
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<{
|
|
21
|
+
data?: RawPoint[]
|
|
22
|
+
series?: RawSeries[]
|
|
23
|
+
labels?: (string | number)[]
|
|
24
|
+
color?: string
|
|
25
|
+
stacked?: boolean
|
|
26
|
+
/** Soft top→bottom gradient on bars (opt-in; flat by default). */
|
|
27
|
+
gradient?: boolean
|
|
28
|
+
height?: number
|
|
29
|
+
grid?: boolean
|
|
30
|
+
radius?: number
|
|
31
|
+
currency?: string
|
|
32
|
+
prefix?: string
|
|
33
|
+
suffix?: string
|
|
34
|
+
maxLabels?: number
|
|
35
|
+
/** Minimum width per category (px). When the categories need more room than
|
|
36
|
+
the container has, the chart keeps this width per bar group and scrolls
|
|
37
|
+
horizontally instead of cramming — handy on mobile. Set 0 to disable. */
|
|
38
|
+
minBarWidth?: number
|
|
39
|
+
animated?: boolean
|
|
40
|
+
}>(), {
|
|
41
|
+
height: 220,
|
|
42
|
+
grid: true,
|
|
43
|
+
radius: 4,
|
|
44
|
+
maxLabels: 12,
|
|
45
|
+
minBarWidth: 28,
|
|
46
|
+
animated: true,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const el = ref<HTMLElement>()
|
|
50
|
+
const heightRef = computed(() => props.height)
|
|
51
|
+
const { width, height: plotH, pad, innerWidth, innerHeight, flipX } = useChartFrame(el, { height: heightRef })
|
|
52
|
+
const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
|
|
53
|
+
|
|
54
|
+
const series = computed(() => normalizeSeries(props.data, props.series, { labels: props.labels, defaultColor: props.color }))
|
|
55
|
+
const cats = computed(() => sharedLabels(series.value))
|
|
56
|
+
|
|
57
|
+
// Minimum plot width so dense data scrolls (mobile) instead of cramming.
|
|
58
|
+
const minWidth = computed(() =>
|
|
59
|
+
props.minBarWidth > 0 && cats.value.length
|
|
60
|
+
? Math.ceil(cats.value.length * props.minBarWidth + pad.value.left + pad.value.right)
|
|
61
|
+
: 0,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const valueFmt = computed<ValueFormat>(() => ({ currency: props.currency, prefix: props.prefix, suffix: props.suffix }))
|
|
65
|
+
const axisFmt = computed<ValueFormat>(() => ({ ...valueFmt.value, compact: true }))
|
|
66
|
+
|
|
67
|
+
const stackTotals = computed(() => cats.value.map((_, i) => series.value.reduce((sum, s) => sum + (s.points[i]?.y ?? 0), 0)))
|
|
68
|
+
const yMax = computed(() => niceMax(props.stacked ? Math.max(0, ...stackTotals.value) : Math.max(0, ...allValues(series.value))))
|
|
69
|
+
const sy = computed(() => linear(0, yMax.value, innerHeight.value, 0))
|
|
70
|
+
const yTicks = computed(() => (props.grid ? ticks(0, yMax.value, 4) : []))
|
|
71
|
+
|
|
72
|
+
const groups = computed(() => band(cats.value.length, innerWidth.value, 0.3))
|
|
73
|
+
const subBand = computed(() => {
|
|
74
|
+
const n = props.stacked ? 1 : series.value.length
|
|
75
|
+
const w = groups.value.bandWidth / n
|
|
76
|
+
return { width: w, at: (si: number) => si * w }
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
interface Bar { x: number; y: number; w: number; h: number; color: string; cat: number }
|
|
80
|
+
const bars = computed<Bar[]>(() => {
|
|
81
|
+
const out: Bar[] = []
|
|
82
|
+
cats.value.forEach((lbl, i) => {
|
|
83
|
+
const groupStart = groups.value.start(i)
|
|
84
|
+
let stackY = innerHeight.value
|
|
85
|
+
series.value.forEach((s, si) => {
|
|
86
|
+
const v = s.points[i]?.y ?? 0
|
|
87
|
+
const color = resolveColor(s.color, si)
|
|
88
|
+
const full = innerHeight.value - sy.value(v)
|
|
89
|
+
const h = full * progress.value
|
|
90
|
+
const w = subBand.value.width
|
|
91
|
+
const x = props.stacked ? groupStart : groupStart + subBand.value.at(si)
|
|
92
|
+
const y = props.stacked ? (stackY - h) : (innerHeight.value - h)
|
|
93
|
+
if (props.stacked) stackY -= full
|
|
94
|
+
out.push({ x: flipX(x + w / 2) - w / 2, y, w, h, color, cat: i })
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
return out
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const labelStride = computed(() => Math.max(1, Math.ceil(cats.value.length / props.maxLabels)))
|
|
101
|
+
|
|
102
|
+
// Gradient defs: one per distinct bar color.
|
|
103
|
+
const uid = chartUid('bar')
|
|
104
|
+
const gradColors = computed(() => [...new Set(bars.value.map(b => b.color))])
|
|
105
|
+
const gradIdFor = (color: string) => `${uid}-${gradColors.value.indexOf(color)}`
|
|
106
|
+
|
|
107
|
+
// ── Shared hover tooltip (mouse-follow, per category) ────────────────────────
|
|
108
|
+
const hoverIdx = ref<number | null>(null)
|
|
109
|
+
const mouseX = ref(0)
|
|
110
|
+
function onMove(e: MouseEvent) {
|
|
111
|
+
const host = el.value
|
|
112
|
+
if (!host || !cats.value.length) return
|
|
113
|
+
const rect = host.getBoundingClientRect()
|
|
114
|
+
let local = e.clientX - rect.left - pad.value.left
|
|
115
|
+
if (flipX(0) !== 0) local = innerWidth.value - local
|
|
116
|
+
const i = Math.floor((local / Math.max(1, innerWidth.value)) * cats.value.length)
|
|
117
|
+
hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, i))
|
|
118
|
+
mouseX.value = e.clientX - rect.left
|
|
119
|
+
}
|
|
120
|
+
function onLeave() { hoverIdx.value = null }
|
|
121
|
+
|
|
122
|
+
const hoverLabel = computed(() => (hoverIdx.value == null ? '' : formatLabel(cats.value[hoverIdx.value])))
|
|
123
|
+
const hoverRows = computed(() => {
|
|
124
|
+
if (hoverIdx.value == null) return []
|
|
125
|
+
return series.value.map((s, si) => ({
|
|
126
|
+
name: s.name,
|
|
127
|
+
color: resolveColor(s.color, si),
|
|
128
|
+
value: formatValue(s.points[hoverIdx.value as number]?.y ?? 0, valueFmt.value),
|
|
129
|
+
}))
|
|
130
|
+
})
|
|
131
|
+
const hoverTotal = computed(() => {
|
|
132
|
+
if (hoverIdx.value == null || !props.stacked) return null
|
|
133
|
+
return formatValue(stackTotals.value[hoverIdx.value] ?? 0, valueFmt.value)
|
|
134
|
+
})
|
|
135
|
+
const tooltipStyle = computed(() => {
|
|
136
|
+
const half = 70
|
|
137
|
+
const x = Math.min(Math.max(mouseX.value, half), Math.max(half, width.value - half))
|
|
138
|
+
return { left: `${x}px` }
|
|
139
|
+
})
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<template>
|
|
143
|
+
<div class="bgl-chart-scroll w-100p overflow-x">
|
|
144
|
+
<div
|
|
145
|
+
ref="el" class="bgl-chart relative" :style="{ height: `${plotH}px`, minWidth: minWidth ? `${minWidth}px` : undefined }"
|
|
146
|
+
@mousemove="onMove" @mouseleave="onLeave"
|
|
147
|
+
>
|
|
148
|
+
<svg v-if="width" :width="width" :height="plotH" class="display-block overflow-hidden">
|
|
149
|
+
<defs v-if="gradient">
|
|
150
|
+
<linearGradient v-for="c in gradColors" :id="gradIdFor(c)" :key="c" x1="0" y1="0" x2="0" y2="1">
|
|
151
|
+
<stop offset="0%" :stop-color="c" stop-opacity="1" />
|
|
152
|
+
<stop offset="100%" :stop-color="c" stop-opacity="0.55" />
|
|
153
|
+
</linearGradient>
|
|
154
|
+
</defs>
|
|
155
|
+
<g :transform="`translate(${pad.left}, ${pad.top})`">
|
|
156
|
+
<g v-if="grid" class="bgl-chart__grid">
|
|
157
|
+
<g v-for="(t, i) in yTicks" :key="i">
|
|
158
|
+
<line :x1="0" :x2="innerWidth" :y1="sy(t)" :y2="sy(t)" />
|
|
159
|
+
<text class="bgl-chart__ytick" :x="-8" :y="sy(t) + 3">{{ formatValue(t, axisFmt) }}</text>
|
|
160
|
+
</g>
|
|
161
|
+
</g>
|
|
162
|
+
|
|
163
|
+
<rect
|
|
164
|
+
v-for="(b, i) in bars" :key="i"
|
|
165
|
+
:x="b.x" :y="b.y" :width="Math.max(0, b.w)" :height="Math.max(0, b.h)"
|
|
166
|
+
:rx="Math.min(radius, b.w / 2)" :fill="gradient ? `url(#${gradIdFor(b.color)})` : b.color"
|
|
167
|
+
class="bgl-chart__bar" :class="{ 'is-dim opacity-5': hoverIdx != null && hoverIdx !== b.cat }"
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
<g class="bgl-chart__xlabels">
|
|
171
|
+
<text
|
|
172
|
+
v-for="(lbl, i) in cats" v-show="i % labelStride === 0" :key="i"
|
|
173
|
+
:x="flipX(groups.center(i))" :y="innerHeight + 16" text-anchor="middle"
|
|
174
|
+
>{{ formatLabel(lbl) }}</text>
|
|
175
|
+
</g>
|
|
176
|
+
</g>
|
|
177
|
+
</svg>
|
|
178
|
+
|
|
179
|
+
<ChartTooltip
|
|
180
|
+
v-if="hoverIdx != null" :style="tooltipStyle"
|
|
181
|
+
:label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</template>
|
|
186
|
+
|
|
187
|
+
<style scoped>
|
|
188
|
+
.bgl-chart { direction: inherit; }
|
|
189
|
+
.bgl-chart__grid line { stroke: var(--bgl-border-color); stroke-width: 1; }
|
|
190
|
+
.bgl-chart__ytick,
|
|
191
|
+
.bgl-chart__xlabels text { fill: var(--bgl-gray); font-size: 11px; }
|
|
192
|
+
.bgl-chart__ytick { text-anchor: end; }
|
|
193
|
+
.bgl-chart__bar { transition: opacity 0.15s ease; outline: none; }
|
|
194
|
+
.bgl-chart__bar:focus,
|
|
195
|
+
.bgl-chart__bar:focus-visible { outline: none; }
|
|
196
|
+
[dir="rtl"] .bgl-chart__ytick { text-anchor: start; }
|
|
197
|
+
</style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ChartTooltip — the single tooltip used by every chart (Line, Bar, Donut,
|
|
4
|
+
* Funnel). Renders a label header, optional hero value, per-series rows
|
|
5
|
+
* (dot · name · value), an optional total row, and an optional stats block.
|
|
6
|
+
*
|
|
7
|
+
* Positioned by the host via `style` (absolute left/top); the host owns
|
|
8
|
+
* mouse tracking. Non-interactive (pointer-events: none).
|
|
9
|
+
*/
|
|
10
|
+
export interface TipRow { name: string; color?: string; value: string }
|
|
11
|
+
export interface TipStat { label: string; value: string }
|
|
12
|
+
|
|
13
|
+
defineOptions({ name: 'BglChartTooltip' })
|
|
14
|
+
|
|
15
|
+
withDefaults(defineProps<{
|
|
16
|
+
label?: string
|
|
17
|
+
/** Big focal number (used by single-value charts like the funnel). */
|
|
18
|
+
hero?: string
|
|
19
|
+
rows?: TipRow[]
|
|
20
|
+
total?: string | null
|
|
21
|
+
stats?: TipStat[]
|
|
22
|
+
/** Float above the anchor point instead of below it. */
|
|
23
|
+
above?: boolean
|
|
24
|
+
}>(), {
|
|
25
|
+
rows: () => [],
|
|
26
|
+
stats: () => [],
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="bgl-tip absolute pointer-events-none nowrap z-5 color-white line-height-13" :class="{ 'bgl-tip--above': above }">
|
|
32
|
+
<p v-if="label" class="bgl-tip__label m-0">{{ label }}</p>
|
|
33
|
+
<p v-if="hero" class="bgl-tip__hero m-0 tabular-nums">{{ hero }}</p>
|
|
34
|
+
|
|
35
|
+
<p v-for="(r, i) in rows" :key="i" class="bgl-tip__row flex align-items-center m-0">
|
|
36
|
+
<span v-if="r.color" class="bgl-tip__dot flex-shrink-0" :style="{ background: r.color }" />
|
|
37
|
+
<span v-if="rows.length > 1" class="bgl-tip__name">{{ r.name }}</span>
|
|
38
|
+
<span class="bgl-tip__val tabular-nums">{{ r.value }}</span>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p v-if="total" class="bgl-tip__row bgl-tip__total flex align-items-center m-0">
|
|
42
|
+
<span class="bgl-tip__name">Total</span>
|
|
43
|
+
<span class="bgl-tip__val tabular-nums">{{ total }}</span>
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<div v-if="stats.length" class="bgl-tip__stats flex column">
|
|
47
|
+
<p v-for="(s, i) in stats" :key="i" class="bgl-tip__row flex align-items-center m-0">
|
|
48
|
+
<span class="bgl-tip__name">{{ s.label }}</span>
|
|
49
|
+
<span class="bgl-tip__val tabular-nums">{{ s.value }}</span>
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.bgl-tip {
|
|
57
|
+
top: 0; transform: translateX(-50%); transform-origin: center;
|
|
58
|
+
background: var(--bgl-black); border-radius: 0.5rem; padding: 0.4rem 0.6rem;
|
|
59
|
+
font-size: 0.75rem; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); min-width: 7rem;
|
|
60
|
+
}
|
|
61
|
+
.bgl-tip--above { transform: translate(-50%, calc(-100% - 0.6rem)); }
|
|
62
|
+
.bgl-tip__label { margin-bottom: 0.2rem; opacity: 0.7; font-size: 0.7rem; }
|
|
63
|
+
.bgl-tip__hero { margin-bottom: 0.2rem; font-size: 1.05rem; font-weight: 700; }
|
|
64
|
+
.bgl-tip__row { gap: 0.4rem; }
|
|
65
|
+
.bgl-tip__dot { width: 8px; height: 8px; border-radius: 2px; }
|
|
66
|
+
.bgl-tip__name { opacity: 0.7; }
|
|
67
|
+
.bgl-tip__val { margin-inline-start: auto; font-weight: 600; }
|
|
68
|
+
.bgl-tip__total,
|
|
69
|
+
.bgl-tip__stats {
|
|
70
|
+
margin-top: 0.28rem; padding-top: 0.28rem;
|
|
71
|
+
border-top: 1px solid color-mix(in srgb, var(--bgl-white) 18%, transparent);
|
|
72
|
+
}
|
|
73
|
+
.bgl-tip__total .bgl-tip__name { opacity: 1; font-weight: 500; }
|
|
74
|
+
</style>
|