@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ergonomic data normalization for the chart family.
|
|
3
|
+
*
|
|
4
|
+
* Accepts the simplest thing that works and progressively enhances:
|
|
5
|
+
* :data="[5, 8, 6, 12]" → one series, x = index
|
|
6
|
+
* :data="[{ x: 'Jan', y: 5 }, …]" → one series, labelled
|
|
7
|
+
* :series="[{ name, color, data: number[] }]" → many series
|
|
8
|
+
* :labels="['Jan','Feb',…]" → shared x labels
|
|
9
|
+
*
|
|
10
|
+
* A server can return `{ labels, series }` and drop it straight in.
|
|
11
|
+
*/
|
|
12
|
+
import type { ThemeType } from '../../../types'
|
|
13
|
+
|
|
14
|
+
/** A single value the way callers pass it. */
|
|
15
|
+
export type RawPoint = number | { x?: string | number; y: number; label?: string }
|
|
16
|
+
|
|
17
|
+
/** A caller-supplied series. */
|
|
18
|
+
export interface RawSeries {
|
|
19
|
+
name?: string
|
|
20
|
+
/** Tone name (`primary`, `blue`, …) or any CSS color. */
|
|
21
|
+
color?: ThemeType | string
|
|
22
|
+
data: RawPoint[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Normalized point used internally by every chart. */
|
|
26
|
+
export interface Point {
|
|
27
|
+
/** Numeric x for scaling (index if labels are categorical). */
|
|
28
|
+
x: number
|
|
29
|
+
y: number
|
|
30
|
+
/** Display label for the x value. */
|
|
31
|
+
label: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Normalized series used internally by every chart. */
|
|
35
|
+
export interface Series {
|
|
36
|
+
name: string
|
|
37
|
+
/** Resolved CSS color (already `var(--bgl-…)` or a raw color). */
|
|
38
|
+
color?: ThemeType | string
|
|
39
|
+
points: Point[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NormalizeOptions {
|
|
43
|
+
/** Shared x labels; overrides per-point labels when provided. */
|
|
44
|
+
labels?: (string | number)[]
|
|
45
|
+
/** Fallback color (tone name or CSS) for series that don't set their own.
|
|
46
|
+
Lets a single-series chart honor a top-level `color` prop. */
|
|
47
|
+
defaultColor?: ThemeType | string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toPoints(data: RawPoint[], labels?: (string | number)[]): Point[] {
|
|
51
|
+
return data.map((d, i) => {
|
|
52
|
+
if (typeof d === 'number') {
|
|
53
|
+
return { x: i, y: d, label: String(labels?.[i] ?? i) }
|
|
54
|
+
}
|
|
55
|
+
const label = d.label ?? labels?.[i] ?? d.x ?? i
|
|
56
|
+
return { x: typeof d.x === 'number' ? d.x : i, y: d.y, label: String(label) }
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve `data` / `series` props into a uniform `Series[]`.
|
|
62
|
+
* Exactly one of `data` or `series` is expected; `series` wins if both given.
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeSeries(
|
|
65
|
+
data: RawPoint[] | undefined,
|
|
66
|
+
series: RawSeries[] | undefined,
|
|
67
|
+
opts: NormalizeOptions = {},
|
|
68
|
+
): Series[] {
|
|
69
|
+
if (series?.length) {
|
|
70
|
+
// Only apply the chart-level default to a lone series; multi-series keep
|
|
71
|
+
// their automatic per-index tone sequence unless each sets its own color.
|
|
72
|
+
const useDefault = series.length === 1
|
|
73
|
+
return series.map((s, i) => ({
|
|
74
|
+
name: s.name ?? `Series ${i + 1}`,
|
|
75
|
+
color: s.color ?? (useDefault ? opts.defaultColor : undefined),
|
|
76
|
+
points: toPoints(s.data ?? [], opts.labels),
|
|
77
|
+
}))
|
|
78
|
+
}
|
|
79
|
+
if (data?.length) {
|
|
80
|
+
return [{ name: 'Series 1', color: opts.defaultColor, points: toPoints(data, opts.labels) }]
|
|
81
|
+
}
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Flatten all y-values across series (for domain calc). */
|
|
86
|
+
export function allValues(series: Series[]): number[] {
|
|
87
|
+
return series.flatMap(s => s.points.map(p => p.y))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The x labels of the first/longest series (charts share an x axis). */
|
|
91
|
+
export function sharedLabels(series: Series[]): string[] {
|
|
92
|
+
let longest: Point[] = []
|
|
93
|
+
for (const s of series) if (s.points.length > longest.length) longest = s.points
|
|
94
|
+
return longest.map(p => p.label)
|
|
95
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware value formatting for charts. Uses the app i18n locale (via
|
|
3
|
+
* `resolveI18n`) instead of a hardcoded locale, with graceful fallbacks.
|
|
4
|
+
*/
|
|
5
|
+
import { getI18n } from '../../../i18n'
|
|
6
|
+
|
|
7
|
+
function currentLocale(): string {
|
|
8
|
+
try {
|
|
9
|
+
const loc = getI18n().global.locale as unknown
|
|
10
|
+
const v = (loc as { value?: string })?.value ?? loc
|
|
11
|
+
if (typeof v === 'string' && v) return v
|
|
12
|
+
} catch { /* i18n not initialized — fall through */ }
|
|
13
|
+
if (typeof document !== 'undefined') {
|
|
14
|
+
return document.documentElement.lang || navigator?.language || 'en'
|
|
15
|
+
}
|
|
16
|
+
return 'en'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ValueFormat {
|
|
20
|
+
/** ISO currency code (e.g. 'USD', 'ILS'). When set, formats as currency. */
|
|
21
|
+
currency?: string
|
|
22
|
+
prefix?: string
|
|
23
|
+
suffix?: string
|
|
24
|
+
/** Compact large numbers (1.2K, 3.4M). Default true for axis ticks. */
|
|
25
|
+
compact?: boolean
|
|
26
|
+
maximumFractionDigits?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatValue(value: number, fmt: ValueFormat = {}): string {
|
|
30
|
+
const locale = currentLocale()
|
|
31
|
+
let out: string
|
|
32
|
+
try {
|
|
33
|
+
if (fmt.currency) {
|
|
34
|
+
out = new Intl.NumberFormat(locale, {
|
|
35
|
+
style: 'currency',
|
|
36
|
+
currency: fmt.currency,
|
|
37
|
+
notation: fmt.compact ? 'compact' : 'standard',
|
|
38
|
+
maximumFractionDigits: fmt.maximumFractionDigits ?? (fmt.compact ? 1 : 0),
|
|
39
|
+
}).format(value)
|
|
40
|
+
} else {
|
|
41
|
+
out = new Intl.NumberFormat(locale, {
|
|
42
|
+
notation: fmt.compact ? 'compact' : 'standard',
|
|
43
|
+
maximumFractionDigits: fmt.maximumFractionDigits ?? (fmt.compact ? 1 : 2),
|
|
44
|
+
}).format(value)
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
out = String(value)
|
|
48
|
+
}
|
|
49
|
+
return `${fmt.prefix ?? ''}${out}${fmt.suffix ?? ''}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Format an x label that may be an ISO date; otherwise pass through. */
|
|
53
|
+
export function formatLabel(label: string): string {
|
|
54
|
+
// Only treat as a date if it parses AND looks date-ish (has - or /).
|
|
55
|
+
if (/[-/]/.test(label)) {
|
|
56
|
+
const d = new Date(label)
|
|
57
|
+
if (!Number.isNaN(d.getTime())) {
|
|
58
|
+
try {
|
|
59
|
+
return d.toLocaleDateString(currentLocale(), { month: 'short', day: 'numeric' })
|
|
60
|
+
} catch { /* noop */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return label
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color resolution for charts. Tone names map to theme CSS vars so charts are
|
|
3
|
+
* automatically theme + dark-mode aware; any raw CSS color passes through.
|
|
4
|
+
*/
|
|
5
|
+
import type { ThemeType } from '../../../types'
|
|
6
|
+
|
|
7
|
+
const TONES = [
|
|
8
|
+
'blue', 'green', 'red', 'yellow', 'purple', 'brown',
|
|
9
|
+
'orange', 'turquoise', 'gray', 'black', 'pink', 'primary', 'white',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
/** A pleasant default multi-series sequence (theme tokens). */
|
|
13
|
+
export const SERIES_TONES: ThemeType[] = [
|
|
14
|
+
'primary', 'purple', 'turquoise', 'orange', 'pink', 'green', 'blue', 'yellow', 'red',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
/** Is this string one of the known theme tones (incl. -light/-tint suffixes)? */
|
|
18
|
+
function isTone(c: string): boolean {
|
|
19
|
+
const base = c.replace(/-(light|tint|dark|\d+)$/, '')
|
|
20
|
+
return TONES.includes(base)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a caller color into a paintable CSS value.
|
|
25
|
+
* - tone name → `var(--bgl-<tone>)`
|
|
26
|
+
* - anything else (hex/rgb/var(...)) → returned as-is
|
|
27
|
+
* - undefined → falls back to the series-index tone
|
|
28
|
+
*/
|
|
29
|
+
export function resolveColor(color: ThemeType | string | undefined, index = 0): string {
|
|
30
|
+
if (!color) return `var(--bgl-${SERIES_TONES[index % SERIES_TONES.length]})`
|
|
31
|
+
if (typeof color === 'string' && isTone(color)) return `var(--bgl-${color})`
|
|
32
|
+
return color
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A translucent version of a resolved color (for area fills / hovers). */
|
|
36
|
+
export function alpha(cssColor: string, pct: number): string {
|
|
37
|
+
return `color-mix(in srgb, ${cssColor} ${pct}%, transparent)`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A monochrome ramp: `count` shades of one base color, from full strength to a
|
|
42
|
+
* lighter tint. Reads premium for donut/pie slices and stacked categories.
|
|
43
|
+
* `index 0` is the strongest; later slices fade toward white.
|
|
44
|
+
*/
|
|
45
|
+
export function ramp(base: ThemeType | string | undefined, count: number, index = 0): string {
|
|
46
|
+
const color = resolveColor(base ?? 'primary')
|
|
47
|
+
if (count <= 1) return color
|
|
48
|
+
// Spread mix from 100% (strongest) down to ~40% (lightest) of the base.
|
|
49
|
+
const minPct = 42
|
|
50
|
+
const pct = 100 - (index / (count - 1)) * (100 - minPct)
|
|
51
|
+
return `color-mix(in srgb, ${color} ${pct}%, var(--bgl-box-bg))`
|
|
52
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reveal animation: eases 0→1 once the chart scrolls into view. Respects
|
|
3
|
+
* `prefers-reduced-motion` (snaps to 1) and cleans up after itself.
|
|
4
|
+
*/
|
|
5
|
+
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
export interface AnimOptions {
|
|
8
|
+
el: Ref<HTMLElement | undefined>
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
duration?: number
|
|
11
|
+
delay?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function easeOutCubic(t: number): number {
|
|
15
|
+
return 1 - (1 - t) ** 3
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function prefersReduced(): boolean {
|
|
19
|
+
return typeof window !== 'undefined'
|
|
20
|
+
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useChartAnim(opts: AnimOptions) {
|
|
24
|
+
const progress = ref(opts.enabled === false || prefersReduced() ? 1 : 0)
|
|
25
|
+
let observer: IntersectionObserver | null = null
|
|
26
|
+
let raf = 0
|
|
27
|
+
let started = false
|
|
28
|
+
|
|
29
|
+
function run() {
|
|
30
|
+
if (started) return
|
|
31
|
+
started = true
|
|
32
|
+
const duration = opts.duration ?? 700
|
|
33
|
+
const start = performance.now() + (opts.delay ?? 0)
|
|
34
|
+
const tick = (now: number) => {
|
|
35
|
+
const elapsed = now - start
|
|
36
|
+
if (elapsed < 0) { raf = requestAnimationFrame(tick); return }
|
|
37
|
+
const t = Math.min(elapsed / duration, 1)
|
|
38
|
+
progress.value = easeOutCubic(t)
|
|
39
|
+
if (t < 1) raf = requestAnimationFrame(tick)
|
|
40
|
+
}
|
|
41
|
+
raf = requestAnimationFrame(tick)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onMounted(() => {
|
|
45
|
+
if (progress.value === 1) return
|
|
46
|
+
if (typeof IntersectionObserver === 'undefined') { run(); return }
|
|
47
|
+
observer = new IntersectionObserver((entries) => {
|
|
48
|
+
for (const e of entries) if (e.isIntersecting) { run(); observer?.disconnect() }
|
|
49
|
+
}, { threshold: 0.25 })
|
|
50
|
+
if (opts.el.value) observer.observe(opts.el.value)
|
|
51
|
+
else run()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
onUnmounted(() => {
|
|
55
|
+
observer?.disconnect()
|
|
56
|
+
cancelAnimationFrame(raf)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return { progress }
|
|
60
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive chart frame: tracks the container's width via ResizeObserver,
|
|
3
|
+
* exposes inner plot dimensions, and detects RTL once (charts flip x in RTL).
|
|
4
|
+
*/
|
|
5
|
+
import { computed, onMounted, onUnmounted, ref, type Ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
export interface Padding { top: number; right: number; bottom: number; left: number }
|
|
8
|
+
|
|
9
|
+
export interface FrameOptions {
|
|
10
|
+
height: Ref<number> | number
|
|
11
|
+
padding?: Partial<Padding>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PAD: Padding = { top: 8, right: 8, bottom: 22, left: 36 }
|
|
15
|
+
|
|
16
|
+
export function useChartFrame(el: Ref<HTMLElement | undefined>, opts: FrameOptions) {
|
|
17
|
+
const width = ref(0)
|
|
18
|
+
const height = computed(() => (typeof opts.height === 'number' ? opts.height : opts.height.value))
|
|
19
|
+
const pad = computed<Padding>(() => ({ ...DEFAULT_PAD, ...opts.padding }))
|
|
20
|
+
|
|
21
|
+
const innerWidth = computed(() => Math.max(0, width.value - pad.value.left - pad.value.right))
|
|
22
|
+
const innerHeight = computed(() => Math.max(0, height.value - pad.value.top - pad.value.bottom))
|
|
23
|
+
|
|
24
|
+
const isRTL = ref(false)
|
|
25
|
+
function detectRTL() {
|
|
26
|
+
if (typeof document === 'undefined') return
|
|
27
|
+
const node = el.value
|
|
28
|
+
const dir = node?.closest('[dir]')?.getAttribute('dir') || document.documentElement.dir
|
|
29
|
+
isRTL.value = dir === 'rtl' || document.documentElement.lang === 'he'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let ro: ResizeObserver | null = null
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
detectRTL()
|
|
35
|
+
if (el.value) {
|
|
36
|
+
width.value = el.value.clientWidth || 0
|
|
37
|
+
ro = new ResizeObserver((entries) => {
|
|
38
|
+
for (const e of entries) width.value = e.contentRect.width
|
|
39
|
+
})
|
|
40
|
+
ro.observe(el.value)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
onUnmounted(() => ro?.disconnect())
|
|
44
|
+
|
|
45
|
+
/** Map a plot-local x (0..innerWidth) to the painted x, flipping in RTL. */
|
|
46
|
+
const flipX = (x: number) => (isRTL.value ? innerWidth.value - x : x)
|
|
47
|
+
|
|
48
|
+
return { width, height, pad, innerWidth, innerHeight, isRTL, flipX }
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal scale + tick helpers. No d3 — just the bits dashboards need.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** "Nice" rounded domain max so axis ticks land on clean numbers. */
|
|
6
|
+
export function niceMax(max: number, ticks = 4): number {
|
|
7
|
+
if (max <= 0) return ticks
|
|
8
|
+
const rough = max / ticks
|
|
9
|
+
const mag = 10 ** Math.floor(Math.log10(rough))
|
|
10
|
+
const norm = rough / mag
|
|
11
|
+
const step = norm >= 5 ? 10 : norm >= 2 ? 5 : norm >= 1 ? 2 : 1
|
|
12
|
+
const niceStep = step * mag
|
|
13
|
+
return Math.ceil(max / niceStep) * niceStep
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Evenly spaced tick values from min..max (inclusive). */
|
|
17
|
+
export function ticks(min: number, max: number, count = 4): number[] {
|
|
18
|
+
const out: number[] = []
|
|
19
|
+
const step = (max - min) / count
|
|
20
|
+
for (let i = 0; i <= count; i++) out.push(min + step * i)
|
|
21
|
+
return out
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Linear scale factory: maps a value in [d0,d1] → pixel in [r0,r1]. */
|
|
25
|
+
export function linear(d0: number, d1: number, r0: number, r1: number) {
|
|
26
|
+
const span = d1 - d0 || 1
|
|
27
|
+
return (v: number) => r0 + ((v - d0) / span) * (r1 - r0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Band scale for categorical x: returns center x + band width. */
|
|
31
|
+
export function band(count: number, range: number, padding = 0.2) {
|
|
32
|
+
const step = range / Math.max(1, count)
|
|
33
|
+
const bandWidth = step * (1 - padding)
|
|
34
|
+
return {
|
|
35
|
+
bandWidth,
|
|
36
|
+
center: (i: number) => step * i + step / 2,
|
|
37
|
+
start: (i: number) => step * i + (step - bandWidth) / 2,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default as Sparkline } from './Sparkline.vue'
|
|
2
|
+
export { default as LineChart } from './LineChart.vue'
|
|
3
|
+
export { default as BarChart } from './BarChart.vue'
|
|
4
|
+
export { default as Donut } from './Donut.vue'
|
|
5
|
+
export { default as Funnel } from './Funnel.vue'
|
|
6
|
+
export { default as Gauge } from './Gauge.vue'
|
|
7
|
+
export { default as RadialBars } from './RadialBars.vue'
|
|
8
|
+
export { default as RankBars } from './RankBars.vue'
|
|
9
|
+
export { default as StatCard } from './StatCard.vue'
|
|
10
|
+
|
|
11
|
+
export type { RawPoint, RawSeries, NormalizeOptions } from './core/data'
|
|
12
|
+
export type { ValueFormat } from './core/format'
|
|
@@ -42,6 +42,7 @@ const props = withDefaults(
|
|
|
42
42
|
borderColor?: string
|
|
43
43
|
textColor?: string
|
|
44
44
|
textAlign?: 'left' | 'center' | 'right'
|
|
45
|
+
wrapperClass?: string
|
|
45
46
|
} & BagelInputShellProps>(),
|
|
46
47
|
{
|
|
47
48
|
align: 'center'
|
|
@@ -112,7 +113,7 @@ function handleChange() {
|
|
|
112
113
|
<p v-if="label" class="group-label">
|
|
113
114
|
{{ resolveI18n(label) }} <span v-if="required">*</span>
|
|
114
115
|
</p>
|
|
115
|
-
<div class="radio-group-wrap">
|
|
116
|
+
<div class="radio-group-wrap" :class="wrapperClass">
|
|
116
117
|
<label v-for="(opt, index) in visibleOptions" :key="opt.id || `${name}-${index}`" class="border rounded flex active-list-item hover mb-05" :for="opt.id || `${name}-${index}`"
|
|
117
118
|
:class="{ 'p-05 gap-025': thin, 'py-1 gap-075': !thin, 'ps-05': !hideRadio, 'bg-gray-light': !bgColor && !flat, 'align-items-start': align === 'start' || align === 'top', 'align-items-center': align === 'center', 'align-items-end': align === 'end' || align === 'bottom', invertedActive }"
|
|
118
119
|
:style="{ backgroundColor: bgColor, borderColor }">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { resolveI18n } from '@bagelink/vue'
|
|
3
|
-
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
4
4
|
import type { BagelInputShellProps } from './bagelInputShell'
|
|
5
5
|
|
|
6
6
|
export interface RangeInputProps extends BagelInputShellProps {
|
|
@@ -12,13 +12,18 @@ export interface RangeInputProps extends BagelInputShellProps {
|
|
|
12
12
|
label?: string
|
|
13
13
|
disabled?: boolean
|
|
14
14
|
id?: string
|
|
15
|
+
/** Force direction. Omit to inherit the page/parent `dir` (recommended). */
|
|
15
16
|
rtl?: boolean
|
|
16
17
|
multiRange?: boolean
|
|
17
18
|
formatValue?: (value: number) => string
|
|
18
|
-
/**
|
|
19
|
+
/** Track (background) color. */
|
|
19
20
|
trackColor?: string
|
|
20
|
-
/**
|
|
21
|
+
/** Active range + thumb color — defaults to labelActiveColor or primary. */
|
|
21
22
|
activeColor?: string
|
|
23
|
+
/** Slim variant for bare sliders / media scrubbers (smaller thumb + track). */
|
|
24
|
+
size?: 'sm' | 'md'
|
|
25
|
+
/** Hide the min/max footer labels (e.g. for a media scrubber). */
|
|
26
|
+
hideMinMax?: boolean
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
const props = defineProps<RangeInputProps>()
|
|
@@ -32,6 +37,17 @@ const {
|
|
|
32
37
|
formatValue = (value: number) => value.toString()
|
|
33
38
|
} = props
|
|
34
39
|
|
|
40
|
+
// Resolve direction: explicit `rtl` prop wins; otherwise inherit the element's
|
|
41
|
+
// computed `dir` so a slider in an RTL page fills from the start edge natively.
|
|
42
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
43
|
+
const inheritedRtl = ref(false)
|
|
44
|
+
onMounted(() => {
|
|
45
|
+
if (props.rtl === undefined && rootEl.value) {
|
|
46
|
+
inheritedRtl.value = getComputedStyle(rootEl.value).direction === 'rtl'
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
const isRtl = computed(() => props.rtl ?? inheritedRtl.value)
|
|
50
|
+
|
|
35
51
|
const from = ref<number>(Array.isArray(props.modelValue) ? props.modelValue[0] : (props.modelValue ?? min))
|
|
36
52
|
const to = ref<number>(Array.isArray(props.modelValue) ? props.modelValue[1] : max)
|
|
37
53
|
|
|
@@ -60,16 +76,16 @@ function handleInput(value: number, isFromInput: boolean) {
|
|
|
60
76
|
emit('update:modelValue', multiRange ? [validFrom.value, validTo.value] : validFrom.value)
|
|
61
77
|
}
|
|
62
78
|
|
|
79
|
+
// Position the active range with logical properties so the browser handles RTL.
|
|
63
80
|
const rangeStyle = computed(() => {
|
|
64
81
|
if (multiRange) {
|
|
65
82
|
return {
|
|
66
|
-
|
|
83
|
+
insetInlineStart: `${fromPercentage.value}%`,
|
|
67
84
|
width: `${toPercentage.value - fromPercentage.value}%`
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
: { left: '0', width: `${fromPercentage.value}%` }
|
|
87
|
+
// Single value: always fills from the inline-start edge (start = min).
|
|
88
|
+
return { insetInlineStart: '0', width: `${fromPercentage.value}%` }
|
|
73
89
|
})
|
|
74
90
|
|
|
75
91
|
const displayFrom = computed(() => formatValue(validFrom.value))
|
|
@@ -78,11 +94,14 @@ const displayTo = computed(() => formatValue(validTo.value))
|
|
|
78
94
|
|
|
79
95
|
<template>
|
|
80
96
|
<div
|
|
81
|
-
|
|
97
|
+
ref="rootEl"
|
|
98
|
+
:dir="rtl === undefined ? undefined : (rtl ? 'rtl' : 'ltr')"
|
|
99
|
+
:class="{ 'range-sm': size === 'sm' }"
|
|
82
100
|
:style="[
|
|
83
101
|
minWidth ? { minWidth } : {},
|
|
84
102
|
maxWidth ? { maxWidth } : {},
|
|
85
103
|
activeColor ? { '--bgl-range-thumb-color': activeColor } : {},
|
|
104
|
+
activeColor ? { '--bgl-input-label-active-color': activeColor } : {},
|
|
86
105
|
labelActiveColor ? { '--bgl-input-label-active-color': labelActiveColor } : {},
|
|
87
106
|
]"
|
|
88
107
|
>
|
|
@@ -145,7 +164,7 @@ const displayTo = computed(() => formatValue(validTo.value))
|
|
|
145
164
|
</slot>
|
|
146
165
|
</p>
|
|
147
166
|
</div>
|
|
148
|
-
<p class="txt-center txt-14 user-select-none range-slider-txt flex space-between opacity-4 mx-05">
|
|
167
|
+
<p v-if="!hideMinMax" class="txt-center txt-14 user-select-none range-slider-txt flex space-between opacity-4 mx-05">
|
|
149
168
|
<slot name="min" :max="formatValue(max)" :min="formatValue(min)" :from="displayFrom" :to="displayTo" :progress="displayFrom">
|
|
150
169
|
<span>{{ formatValue(min) }}</span>
|
|
151
170
|
</slot>
|
|
@@ -160,23 +179,33 @@ const displayTo = computed(() => formatValue(validTo.value))
|
|
|
160
179
|
|
|
161
180
|
.range-slider-position-txt{
|
|
162
181
|
margin-inline-start: calc((var(--progress) * 1%) - (var(--bgl-range-thumb-size) * var(--progress) / 100));
|
|
163
|
-
|
|
164
|
-
|
|
182
|
+
/* Float the value bubble ABOVE the thumb so the thumb never covers it. */
|
|
183
|
+
bottom: calc(100% + 0.25rem);
|
|
184
|
+
transition: transform 0.1s, opacity 0.3s, bottom 0.3s;
|
|
165
185
|
transform: scale(0.8);
|
|
186
|
+
transform-origin: bottom center;
|
|
187
|
+
opacity: 0;
|
|
166
188
|
width: var(--bgl-range-thumb-size);
|
|
167
189
|
}
|
|
168
|
-
.range-slider:hover .range-slider-position-txt
|
|
190
|
+
.range-slider:hover .range-slider-position-txt,
|
|
191
|
+
.range-slider:focus-within .range-slider-position-txt{
|
|
169
192
|
opacity: 1;
|
|
170
193
|
transform: scale(1);
|
|
171
|
-
top: calc(var(--bgl-range-thumb-size) / 2);
|
|
172
194
|
}
|
|
173
195
|
|
|
174
196
|
.range-slider {
|
|
175
|
-
height: var(--bgl-range-
|
|
197
|
+
height: var(--bgl-range-thumb-size);
|
|
176
198
|
display: flex;
|
|
177
199
|
align-items: center;
|
|
178
200
|
margin-top: calc(var(--bgl-range-thumb-size) / 2 + 0.5rem) ;
|
|
179
201
|
}
|
|
202
|
+
|
|
203
|
+
/* Slim variant — smaller thumb + thinner track for bare sliders / scrubbers. */
|
|
204
|
+
.range-sm {
|
|
205
|
+
--bgl-range-thumb-size: 14px;
|
|
206
|
+
--bgl-range-track-height: 4px;
|
|
207
|
+
}
|
|
208
|
+
.range-sm .range-slider { margin-top: 0; }
|
|
180
209
|
.range-slider-txt{
|
|
181
210
|
margin-top: calc(var(--bgl-range-thumb-size) / 2) !important;
|
|
182
211
|
}
|
|
@@ -230,7 +259,6 @@ cursor: grabbing;
|
|
|
230
259
|
.range-slider-position-txt{
|
|
231
260
|
opacity: 1;
|
|
232
261
|
transform: scale(1);
|
|
233
|
-
top: calc(var(--bgl-range-thumb-size) / 1.8) ;
|
|
234
262
|
}
|
|
235
263
|
}
|
|
236
264
|
|
|
@@ -303,10 +303,7 @@ onMounted(() => {
|
|
|
303
303
|
<div v-if="clearable && selectedItemCount > 0" class="ms-auto ps-05 me-05">
|
|
304
304
|
<Btn flat thin icon="clear" class="color-gray" @click="selectedItems = []; emitUpdate()" />
|
|
305
305
|
</div>
|
|
306
|
-
<
|
|
307
|
-
<Icon v-if="open" key="open" thin :icon="underlined ? 'expand_less' : 'unfold_less'" />
|
|
308
|
-
<Icon v-else key="closed" thin :icon="underlined ? 'expand_more' : 'unfold_more'" />
|
|
309
|
-
</Transition>
|
|
306
|
+
<Icon v-if="!disabled" thin transition="rotate" :icon="open ? (underlined ? 'expand_less' : 'unfold_less') : (underlined ? 'expand_more' : 'unfold_more')" />
|
|
310
307
|
</button>
|
|
311
308
|
<input
|
|
312
309
|
v-if="required && !underlined" tabindex="-1"
|
|
@@ -393,21 +390,6 @@ background: var(--bgl-gray-tint);
|
|
|
393
390
|
opacity: 0.3;
|
|
394
391
|
}
|
|
395
392
|
|
|
396
|
-
.icon-swap-enter-active,
|
|
397
|
-
.icon-swap-leave-active {
|
|
398
|
-
transition: opacity 0.12s ease, transform 0.12s ease;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.icon-swap-enter-from {
|
|
402
|
-
opacity: 0;
|
|
403
|
-
transform: translateY(-4px);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
.icon-swap-leave-to {
|
|
407
|
-
opacity: 0;
|
|
408
|
-
transform: translateY(4px);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
393
|
.selected {
|
|
412
394
|
color: var(--bgl-input-label-active-color, var(--bgl-primary));
|
|
413
395
|
background: var(--bgl-selected);
|
package/src/components/index.ts
CHANGED
|
@@ -3,7 +3,8 @@ export { default as AccordionItem } from './AccordionItem.vue'
|
|
|
3
3
|
export { default as AddressSearch } from './AddressSearch.vue'
|
|
4
4
|
export { default as AddToCalendar } from './AddToCalendar.vue'
|
|
5
5
|
export { default as Alert } from './Alert.vue'
|
|
6
|
-
export
|
|
6
|
+
export { BarChart, Donut, Funnel, Gauge, LineChart, RadialBars, RankBars, Sparkline, StatCard } from './charts'
|
|
7
|
+
export type { NormalizeOptions, RawPoint, RawSeries, ValueFormat } from './charts'
|
|
7
8
|
export { default as Avatar } from './Avatar.vue'
|
|
8
9
|
export { default as Badge } from './Badge.vue'
|
|
9
10
|
/** @deprecated Renamed to Badge. Pill is an alias that will be removed in a future version. */
|
|
@@ -19,6 +20,7 @@ export { default as TableSchema } from './dataTable/DataTable.vue'
|
|
|
19
20
|
export { Draggable, useDraggable, vDraggable } from './draggable'
|
|
20
21
|
export { default as DragOver } from './DragOver.vue'
|
|
21
22
|
export { default as Dropdown } from './Dropdown.vue'
|
|
23
|
+
export { default as EmptyState } from './EmptyState.vue'
|
|
22
24
|
export { default as FieldSetVue } from './FieldSetVue.vue'
|
|
23
25
|
export { default as FilterQuery } from './FilterQuery.vue'
|
|
24
26
|
export type { FilterField, QueryOption } from './FilterQuery.types'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
defineOptions({ name: 'BglTimeline' })
|
|
3
|
+
import { useSlots } from 'vue'
|
|
4
|
+
import type { TimelineEntry } from './Timeline.types'
|
|
5
|
+
import TimelineItem from './TimelineItem.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vertical activity / history feed. Two ways to use it:
|
|
9
|
+
*
|
|
10
|
+
* 1. Data-driven — pass `items`; each maps to a <TimelineItem>. Use the default
|
|
11
|
+
* scoped slot to render custom body content per entry:
|
|
12
|
+
* <Timeline :items="activity">
|
|
13
|
+
* <template #default="{ item }">{{ item.title }}</template>
|
|
14
|
+
* </Timeline>
|
|
15
|
+
*
|
|
16
|
+
* 2. Composition — drop <TimelineItem> children directly (omit `items`). Mark the
|
|
17
|
+
* last one with `last` to drop its connector, or let data-driven mode do it.
|
|
18
|
+
*/
|
|
19
|
+
const { items = [] } = defineProps<{
|
|
20
|
+
items?: TimelineEntry[]
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const slots = useSlots()
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div class="bgl-timeline grid">
|
|
28
|
+
<template v-if="items.length">
|
|
29
|
+
<TimelineItem
|
|
30
|
+
v-for="(item, i) in items"
|
|
31
|
+
:key="i"
|
|
32
|
+
:title="item.title"
|
|
33
|
+
:meta="item.meta"
|
|
34
|
+
:icon="item.icon"
|
|
35
|
+
:color="item.color"
|
|
36
|
+
:last="i === items.length - 1"
|
|
37
|
+
>
|
|
38
|
+
<template v-if="slots.default" #default>
|
|
39
|
+
<slot :item="item" :index="i" />
|
|
40
|
+
</template>
|
|
41
|
+
<template v-if="slots.meta" #meta>
|
|
42
|
+
<slot name="meta" :item="item" :index="i" />
|
|
43
|
+
</template>
|
|
44
|
+
</TimelineItem>
|
|
45
|
+
</template>
|
|
46
|
+
<slot v-else name="items" />
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
.bgl-timeline {
|
|
52
|
+
align-content: start;
|
|
53
|
+
}
|
|
54
|
+
</style>
|