@hanzo/ui 8.0.6 → 8.0.8
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/ThemeToggleNext-7727UR5W.js +4 -0
- package/dist/ThemeToggleNext-7727UR5W.js.map +1 -0
- package/dist/ThemeToggleNext-ICB5KL5J.cjs +19 -0
- package/dist/ThemeToggleNext-ICB5KL5J.cjs.map +1 -0
- package/dist/billing.cjs +15 -0
- package/dist/billing.cjs.map +1 -0
- package/{types → dist}/billing.d.ts +0 -1
- package/dist/billing.js +4 -0
- package/dist/billing.js.map +1 -0
- package/dist/canvas.cjs +15 -0
- package/dist/canvas.cjs.map +1 -0
- package/dist/canvas.d.ts +1 -0
- package/dist/canvas.js +4 -0
- package/dist/canvas.js.map +1 -0
- package/{types → dist}/cd/layout.d.ts +0 -1
- package/{types → dist}/cd/status.d.ts +0 -1
- package/{types → dist}/cd/time.d.ts +0 -1
- package/{types → dist}/cd/types.d.ts +0 -1
- package/dist/chunk-42CU5GSF.js +98 -0
- package/dist/chunk-42CU5GSF.js.map +1 -0
- package/dist/chunk-GDGVXU7W.js +1999 -0
- package/dist/chunk-GDGVXU7W.js.map +1 -0
- package/dist/chunk-HSPMQQVJ.cjs +2066 -0
- package/dist/chunk-HSPMQQVJ.cjs.map +1 -0
- package/dist/chunk-YBXVUV7P.cjs +103 -0
- package/dist/chunk-YBXVUV7P.cjs.map +1 -0
- package/dist/dashboard.cjs +15 -0
- package/dist/dashboard.cjs.map +1 -0
- package/dist/dashboard.d.ts +1 -0
- package/dist/dashboard.js +4 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/data.cjs +15 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.ts +1 -0
- package/dist/data.js +4 -0
- package/dist/data.js.map +1 -0
- package/dist/gitops.cjs +15 -0
- package/dist/gitops.cjs.map +1 -0
- package/dist/gitops.d.ts +1 -0
- package/dist/gitops.js +4 -0
- package/dist/gitops.js.map +1 -0
- package/dist/index.cjs +291 -0
- package/dist/index.cjs.map +1 -0
- package/{types → dist}/index.d.ts +0 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/network.cjs +15 -0
- package/dist/network.cjs.map +1 -0
- package/{types → dist}/network.d.ts +0 -1
- package/dist/network.js +4 -0
- package/dist/network.js.map +1 -0
- package/dist/primitives/bases/data/index.cjs +15 -0
- package/dist/primitives/bases/data/index.cjs.map +1 -0
- package/dist/primitives/bases/data/index.d.ts +1 -0
- package/dist/primitives/bases/data/index.js +4 -0
- package/dist/primitives/bases/data/index.js.map +1 -0
- package/{types → dist}/product/AppHeader.d.ts +0 -1
- package/{types → dist}/product/BrandMark.d.ts +0 -1
- package/{types → dist}/product/Charts.d.ts +0 -1
- package/{types → dist}/product/ComboBox.d.ts +0 -1
- package/{types → dist}/product/DataTable.d.ts +0 -1
- package/{types → dist}/product/Donut.d.ts +0 -1
- package/{types → dist}/product/EmptyState.d.ts +0 -1
- package/{types → dist}/product/FadeIn.d.ts +0 -1
- package/{types → dist}/product/Field.d.ts +0 -1
- package/{types → dist}/product/HanzoMark.d.ts +0 -1
- package/{types → dist}/product/Metric.d.ts +0 -1
- package/{types → dist}/product/OrgSwitcher.d.ts +0 -1
- package/{types → dist}/product/PageHeader.d.ts +0 -1
- package/{types → dist}/product/PrimaryButton.d.ts +0 -1
- package/{types → dist}/product/ProductIcon.d.ts +0 -1
- package/{types → dist}/product/ProviderLogo.d.ts +0 -1
- package/{types → dist}/product/Reorder.d.ts +0 -1
- package/{types → dist}/product/SelectMenu.d.ts +7 -6
- package/{types → dist}/product/SlideOver.d.ts +0 -1
- package/{types → dist}/product/StatusTag.d.ts +0 -1
- package/dist/product/ThemeToggle.d.ts +21 -0
- package/dist/product/ThemeToggleNext.d.ts +4 -0
- package/{types → dist}/product/Toast.d.ts +0 -1
- package/{types → dist}/product/color.d.ts +0 -1
- package/{types → dist}/product/combobox/filter.d.ts +0 -1
- package/dist/product/index.cjs +278 -0
- package/dist/product/index.cjs.map +1 -0
- package/{types → dist}/product/index.d.ts +2 -1
- package/dist/product/index.js +5 -0
- package/dist/product/index.js.map +1 -0
- package/dist/product/menu/ContextMenu.d.ts +21 -0
- package/dist/product/menu/DropdownMenu.d.ts +29 -0
- package/dist/product/menu/FloatingMenu.d.ts +36 -0
- package/dist/product/menu/index.d.ts +5 -0
- package/dist/product/menu/items.d.ts +68 -0
- package/dist/product/menu/portal-theme.d.ts +28 -0
- package/dist/product/menu/roving.d.ts +8 -0
- package/{types → dist}/product/scope.d.ts +0 -1
- package/{types → dist}/product/surfaces.data.d.ts +0 -1
- package/dist/usage.cjs +15 -0
- package/dist/usage.cjs.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.js +4 -0
- package/dist/usage.js.map +1 -0
- package/dist/wallet.cjs +15 -0
- package/dist/wallet.cjs.map +1 -0
- package/{types → dist}/wallet.d.ts +0 -1
- package/dist/wallet.js +4 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +58 -32
- package/src/billing.ts +0 -9
- package/src/canvas.ts +0 -11
- package/src/cd/layout.ts +0 -184
- package/src/cd/status.ts +0 -158
- package/src/cd/time.ts +0 -52
- package/src/cd/types.ts +0 -318
- package/src/dashboard.ts +0 -8
- package/src/data.ts +0 -12
- package/src/gitops.ts +0 -5
- package/src/gui-env.d.ts +0 -27
- package/src/index.ts +0 -22
- package/src/network.ts +0 -11
- package/src/primitives/bases/data/index.ts +0 -13
- package/src/product/AppHeader.tsx +0 -183
- package/src/product/BrandMark.tsx +0 -27
- package/src/product/Charts.tsx +0 -471
- package/src/product/ComboBox.tsx +0 -147
- package/src/product/DataTable.tsx +0 -99
- package/src/product/Donut.tsx +0 -83
- package/src/product/EmptyState.tsx +0 -129
- package/src/product/FadeIn.tsx +0 -38
- package/src/product/Field.tsx +0 -160
- package/src/product/HanzoMark.tsx +0 -31
- package/src/product/Metric.tsx +0 -253
- package/src/product/OrgSwitcher.tsx +0 -332
- package/src/product/PageHeader.tsx +0 -31
- package/src/product/PrimaryButton.tsx +0 -17
- package/src/product/ProductIcon.tsx +0 -61
- package/src/product/ProviderLogo.tsx +0 -137
- package/src/product/Reorder.test.ts +0 -41
- package/src/product/Reorder.tsx +0 -110
- package/src/product/SelectMenu.tsx +0 -94
- package/src/product/SlideOver.tsx +0 -197
- package/src/product/StatusTag.tsx +0 -49
- package/src/product/ThemeToggle.tsx +0 -25
- package/src/product/Toast.tsx +0 -166
- package/src/product/color.ts +0 -17
- package/src/product/combobox/filter.test.ts +0 -51
- package/src/product/combobox/filter.ts +0 -39
- package/src/product/index.ts +0 -70
- package/src/product/scope.test.ts +0 -92
- package/src/product/scope.ts +0 -123
- package/src/product/surfaces.data.ts +0 -42
- package/src/usage.ts +0 -8
- package/src/wallet.ts +0 -11
- package/types/billing.d.ts.map +0 -1
- package/types/canvas.d.ts +0 -2
- package/types/canvas.d.ts.map +0 -1
- package/types/cd/layout.d.ts.map +0 -1
- package/types/cd/status.d.ts.map +0 -1
- package/types/cd/time.d.ts.map +0 -1
- package/types/cd/types.d.ts.map +0 -1
- package/types/dashboard.d.ts +0 -2
- package/types/dashboard.d.ts.map +0 -1
- package/types/data.d.ts +0 -2
- package/types/data.d.ts.map +0 -1
- package/types/gitops.d.ts +0 -2
- package/types/gitops.d.ts.map +0 -1
- package/types/index.d.ts.map +0 -1
- package/types/network.d.ts.map +0 -1
- package/types/primitives/bases/data/index.d.ts +0 -2
- package/types/primitives/bases/data/index.d.ts.map +0 -1
- package/types/product/AppHeader.d.ts.map +0 -1
- package/types/product/BrandMark.d.ts.map +0 -1
- package/types/product/Charts.d.ts.map +0 -1
- package/types/product/ComboBox.d.ts.map +0 -1
- package/types/product/DataTable.d.ts.map +0 -1
- package/types/product/Donut.d.ts.map +0 -1
- package/types/product/EmptyState.d.ts.map +0 -1
- package/types/product/FadeIn.d.ts.map +0 -1
- package/types/product/Field.d.ts.map +0 -1
- package/types/product/HanzoMark.d.ts.map +0 -1
- package/types/product/Metric.d.ts.map +0 -1
- package/types/product/OrgSwitcher.d.ts.map +0 -1
- package/types/product/PageHeader.d.ts.map +0 -1
- package/types/product/PrimaryButton.d.ts.map +0 -1
- package/types/product/ProductIcon.d.ts.map +0 -1
- package/types/product/ProviderLogo.d.ts.map +0 -1
- package/types/product/Reorder.d.ts.map +0 -1
- package/types/product/SelectMenu.d.ts.map +0 -1
- package/types/product/SlideOver.d.ts.map +0 -1
- package/types/product/StatusTag.d.ts.map +0 -1
- package/types/product/ThemeToggle.d.ts +0 -2
- package/types/product/ThemeToggle.d.ts.map +0 -1
- package/types/product/Toast.d.ts.map +0 -1
- package/types/product/color.d.ts.map +0 -1
- package/types/product/combobox/filter.d.ts.map +0 -1
- package/types/product/index.d.ts.map +0 -1
- package/types/product/scope.d.ts.map +0 -1
- package/types/product/surfaces.data.d.ts.map +0 -1
- package/types/usage.d.ts +0 -2
- package/types/usage.d.ts.map +0 -1
- package/types/wallet.d.ts.map +0 -1
- /package/{src → dist}/styles/hanzo-motion.css +0 -0
package/src/product/Charts.tsx
DELETED
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Charts — the ONE way the console draws a trend, a series, a share, or a
|
|
5
|
-
* distribution. Dependency-free: inline SVG for the data-dense temporal charts
|
|
6
|
-
* (Sparkline / LineChart / BarChart) and @hanzo/gui primitives for the
|
|
7
|
-
* token-native ones (BarRows, and the Donut's optional legend), so everything
|
|
8
|
-
* themes to the shell.
|
|
9
|
-
*
|
|
10
|
-
* Color is data-driven, not a mode flag: a `Slice` is drawn in its own `color`
|
|
11
|
-
* when it has one (categorical/polychrome — e.g. spend-by-model) and in a
|
|
12
|
-
* monochrome opacity ramp off the foreground token when it doesn't (on-brand —
|
|
13
|
-
* e.g. collections-by-model). One Donut serves both; a consumer that needs a
|
|
14
|
-
* richer legend (amounts, click targets) leaves `legend` off and composes its own.
|
|
15
|
-
*
|
|
16
|
-
* Honest by construction: a Sparkline with fewer than two points renders nothing
|
|
17
|
-
* (no invented trend); Donut / BarRows with no positive value render an em-dash.
|
|
18
|
-
* Callers pass REAL series — these never fabricate data.
|
|
19
|
-
*
|
|
20
|
-
* SVG marks can't read Tamagui tokens, so concrete colors for SVG come from the
|
|
21
|
-
* live theme (`useTheme().<token>.get()` → `var(--token)` on web, stays reactive).
|
|
22
|
-
*/
|
|
23
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
24
|
-
import { Text, XStack, YStack, useTheme } from '@hanzo/gui'
|
|
25
|
-
|
|
26
|
-
/** Distinct hues for categorical series (e.g. spend-by-model donut), dark-tuned. */
|
|
27
|
-
export const CHART_PALETTE = [
|
|
28
|
-
'#7c5cff',
|
|
29
|
-
'#23c562',
|
|
30
|
-
'#ff9f45',
|
|
31
|
-
'#3aa0ff',
|
|
32
|
-
'#ff5d8f',
|
|
33
|
-
'#16c0c8',
|
|
34
|
-
'#c084fc',
|
|
35
|
-
'#f4c245',
|
|
36
|
-
]
|
|
37
|
-
export const CHART_OTHER = '#64748b'
|
|
38
|
-
|
|
39
|
-
const ACCENT = CHART_PALETTE[0]
|
|
40
|
-
const GRID = 'rgba(148,163,184,0.16)'
|
|
41
|
-
const AXIS = 'rgba(148,163,184,0.85)'
|
|
42
|
-
|
|
43
|
-
/** A point on a temporal/sequential axis (LineChart / BarChart). */
|
|
44
|
-
export type ChartPoint = { label: string; value: number }
|
|
45
|
-
|
|
46
|
-
/** A labelled magnitude in a categorical chart (Donut / BarRows). */
|
|
47
|
-
export type Slice = { label: string; value: number; color?: string; hint?: string }
|
|
48
|
-
|
|
49
|
-
type Themed = ReturnType<typeof useTheme>
|
|
50
|
-
|
|
51
|
-
/** Read a concrete color for a theme token (for SVG), with a safe fallback. */
|
|
52
|
-
const tone = (theme: Themed, key: string, fallback: string): string => {
|
|
53
|
-
const v = (theme as unknown as Record<string, { get?: () => unknown } | undefined>)[key]
|
|
54
|
-
const got = v?.get?.()
|
|
55
|
-
return typeof got === 'string' ? got : fallback
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Opacity ramp so N monochrome slices/areas stay visually distinct. */
|
|
59
|
-
const rampOpacity = (i: number, n: number): number => (n <= 1 ? 0.9 : 0.95 - (i / (n - 1)) * 0.62)
|
|
60
|
-
|
|
61
|
-
function useContainerWidth() {
|
|
62
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
63
|
-
const [w, setW] = useState(0)
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const el = ref.current
|
|
66
|
-
if (!el) return
|
|
67
|
-
const measure = () => setW(el.clientWidth)
|
|
68
|
-
measure()
|
|
69
|
-
const ro = new ResizeObserver(measure)
|
|
70
|
-
ro.observe(el)
|
|
71
|
-
return () => ro.disconnect()
|
|
72
|
-
}, [])
|
|
73
|
-
return { ref, w }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── Sparkline (fixed size, for metric cards) ─────────────────────────────────
|
|
77
|
-
|
|
78
|
-
export function Sparkline({
|
|
79
|
-
values,
|
|
80
|
-
width = 120,
|
|
81
|
-
height = 36,
|
|
82
|
-
color,
|
|
83
|
-
}: {
|
|
84
|
-
values: number[]
|
|
85
|
-
width?: number
|
|
86
|
-
height?: number
|
|
87
|
-
color?: string
|
|
88
|
-
}) {
|
|
89
|
-
const theme = useTheme()
|
|
90
|
-
const clean = (values ?? []).filter((v) => Number.isFinite(v))
|
|
91
|
-
if (clean.length < 2) return null
|
|
92
|
-
const stroke = color ?? tone(theme, 'color11', 'currentColor')
|
|
93
|
-
const max = Math.max(...clean)
|
|
94
|
-
const min = Math.min(...clean)
|
|
95
|
-
const span = max - min || 1
|
|
96
|
-
const n = clean.length
|
|
97
|
-
const px = (i: number) => (i / (n - 1)) * (width - 2) + 1
|
|
98
|
-
const py = (v: number) => height - 2 - ((v - min) / span) * (height - 4)
|
|
99
|
-
const line = clean.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' ')
|
|
100
|
-
const area = `${line} L${(width - 1).toFixed(1)},${height} L1,${height} Z`
|
|
101
|
-
return (
|
|
102
|
-
<svg width={width} height={height} aria-hidden style={{ display: 'block' }}>
|
|
103
|
-
<path d={area} fill={stroke} fillOpacity={0.12} />
|
|
104
|
-
<path d={line} fill="none" stroke={stroke} strokeWidth={1.5} strokeLinejoin="round" strokeLinecap="round" />
|
|
105
|
-
</svg>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Shared hover read-out (LineChart / BarChart) ─────────────────────────────
|
|
110
|
-
|
|
111
|
-
function Tooltip({ x, height, label, value }: { x: number; height: number; label: string; value: string }) {
|
|
112
|
-
// Keep the box on-screen near the guide line.
|
|
113
|
-
const left = Math.max(0, x - 60)
|
|
114
|
-
return (
|
|
115
|
-
<div
|
|
116
|
-
style={{
|
|
117
|
-
position: 'absolute',
|
|
118
|
-
left,
|
|
119
|
-
top: 0,
|
|
120
|
-
height,
|
|
121
|
-
pointerEvents: 'none',
|
|
122
|
-
display: 'flex',
|
|
123
|
-
alignItems: 'flex-start',
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
<div
|
|
127
|
-
style={{
|
|
128
|
-
background: 'rgba(15,17,21,0.94)',
|
|
129
|
-
border: '1px solid rgba(148,163,184,0.25)',
|
|
130
|
-
borderRadius: 8,
|
|
131
|
-
padding: '6px 9px',
|
|
132
|
-
fontSize: 11,
|
|
133
|
-
lineHeight: 1.35,
|
|
134
|
-
color: '#e2e8f0',
|
|
135
|
-
whiteSpace: 'nowrap',
|
|
136
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
|
|
137
|
-
}}
|
|
138
|
-
>
|
|
139
|
-
<div style={{ color: AXIS }}>{label}</div>
|
|
140
|
-
<div style={{ fontWeight: 700 }}>{value}</div>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function nearestIndex(clientX: number, rect: DOMRect, padL: number, innerW: number, n: number): number {
|
|
147
|
-
if (n <= 1) return 0
|
|
148
|
-
const mx = clientX - rect.left - padL
|
|
149
|
-
const i = Math.round((mx / (innerW || 1)) * (n - 1))
|
|
150
|
-
return Math.max(0, Math.min(n - 1, i))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Pick a few evenly-spaced x labels (first/mid/last) to avoid crowding. */
|
|
154
|
-
function axisTicks(data: ChartPoint[]): { i: number; label: string; anchor: 'start' | 'middle' | 'end' }[] {
|
|
155
|
-
const n = data.length
|
|
156
|
-
if (n === 0) return []
|
|
157
|
-
if (n === 1) return [{ i: 0, label: data[0].label, anchor: 'middle' }]
|
|
158
|
-
const mid = Math.floor((n - 1) / 2)
|
|
159
|
-
const out: { i: number; label: string; anchor: 'start' | 'middle' | 'end' }[] = [
|
|
160
|
-
{ i: 0, label: data[0].label, anchor: 'start' },
|
|
161
|
-
{ i: n - 1, label: data[n - 1].label, anchor: 'end' },
|
|
162
|
-
]
|
|
163
|
-
if (n >= 5) out.splice(1, 0, { i: mid, label: data[mid].label, anchor: 'middle' })
|
|
164
|
-
return out
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── Line chart (a value over time) ───────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
export function LineChart({
|
|
170
|
-
data,
|
|
171
|
-
height = 210,
|
|
172
|
-
color = ACCENT,
|
|
173
|
-
formatValue = (v) => String(v),
|
|
174
|
-
}: {
|
|
175
|
-
data: ChartPoint[]
|
|
176
|
-
height?: number
|
|
177
|
-
color?: string
|
|
178
|
-
formatValue?: (v: number) => string
|
|
179
|
-
}) {
|
|
180
|
-
const { ref, w } = useContainerWidth()
|
|
181
|
-
const [hover, setHover] = useState<number | null>(null)
|
|
182
|
-
const padL = 10
|
|
183
|
-
const padR = 10
|
|
184
|
-
const padT = 12
|
|
185
|
-
const padB = 24
|
|
186
|
-
const n = data.length
|
|
187
|
-
const innerW = Math.max(1, w - padL - padR)
|
|
188
|
-
const innerH = Math.max(1, height - padT - padB)
|
|
189
|
-
const max = Math.max(...data.map((d) => d.value), 1)
|
|
190
|
-
const x = (i: number) => padL + (n <= 1 ? innerW / 2 : (i / (n - 1)) * innerW)
|
|
191
|
-
const y = (v: number) => padT + innerH - (v / max) * innerH
|
|
192
|
-
|
|
193
|
-
const line = data.map((d, i) => `${i === 0 ? 'M' : 'L'}${x(i).toFixed(1)},${y(d.value).toFixed(1)}`).join(' ')
|
|
194
|
-
const area = n >= 2 ? `${line} L${x(n - 1).toFixed(1)},${(padT + innerH).toFixed(1)} L${x(0).toFixed(1)},${(padT + innerH).toFixed(1)} Z` : ''
|
|
195
|
-
const ticks = axisTicks(data)
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<div ref={ref} style={{ position: 'relative', width: '100%' }}>
|
|
199
|
-
{w > 0 ? (
|
|
200
|
-
<svg
|
|
201
|
-
width={w}
|
|
202
|
-
height={height}
|
|
203
|
-
style={{ display: 'block' }}
|
|
204
|
-
onMouseMove={(e) => setHover(nearestIndex(e.clientX, e.currentTarget.getBoundingClientRect(), padL, innerW, n))}
|
|
205
|
-
onMouseLeave={() => setHover(null)}
|
|
206
|
-
>
|
|
207
|
-
{[0, 0.5, 1].map((g) => (
|
|
208
|
-
<line key={g} x1={padL} x2={w - padR} y1={padT + innerH * g} y2={padT + innerH * g} stroke={GRID} strokeWidth={1} />
|
|
209
|
-
))}
|
|
210
|
-
{area ? <path d={area} fill={color} fillOpacity={0.1} /> : null}
|
|
211
|
-
{n >= 2 ? <path d={line} fill="none" stroke={color} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" /> : null}
|
|
212
|
-
{hover != null && data[hover] ? (
|
|
213
|
-
<>
|
|
214
|
-
<line x1={x(hover)} x2={x(hover)} y1={padT} y2={padT + innerH} stroke={AXIS} strokeWidth={1} strokeDasharray="3 3" />
|
|
215
|
-
<circle cx={x(hover)} cy={y(data[hover].value)} r={4} fill={color} stroke="#0f1115" strokeWidth={2} />
|
|
216
|
-
</>
|
|
217
|
-
) : null}
|
|
218
|
-
{ticks.map((t) => (
|
|
219
|
-
<text key={t.i} x={x(t.i)} y={height - 7} fontSize={10} fill={AXIS} textAnchor={t.anchor}>
|
|
220
|
-
{t.label}
|
|
221
|
-
</text>
|
|
222
|
-
))}
|
|
223
|
-
</svg>
|
|
224
|
-
) : null}
|
|
225
|
-
{hover != null && data[hover] ? (
|
|
226
|
-
<Tooltip x={x(hover)} height={height} label={data[hover].label} value={formatValue(data[hover].value)} />
|
|
227
|
-
) : null}
|
|
228
|
-
</div>
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ── Bar chart (a value over time, vertical bars) ─────────────────────────────
|
|
233
|
-
|
|
234
|
-
export function BarChart({
|
|
235
|
-
data,
|
|
236
|
-
height = 210,
|
|
237
|
-
color = ACCENT,
|
|
238
|
-
formatValue = (v) => String(v),
|
|
239
|
-
}: {
|
|
240
|
-
data: ChartPoint[]
|
|
241
|
-
height?: number
|
|
242
|
-
color?: string
|
|
243
|
-
formatValue?: (v: number) => string
|
|
244
|
-
}) {
|
|
245
|
-
const { ref, w } = useContainerWidth()
|
|
246
|
-
const [hover, setHover] = useState<number | null>(null)
|
|
247
|
-
const padL = 10
|
|
248
|
-
const padR = 10
|
|
249
|
-
const padT = 12
|
|
250
|
-
const padB = 24
|
|
251
|
-
const n = data.length
|
|
252
|
-
const innerW = Math.max(1, w - padL - padR)
|
|
253
|
-
const innerH = Math.max(1, height - padT - padB)
|
|
254
|
-
const max = Math.max(...data.map((d) => d.value), 1)
|
|
255
|
-
const slot = innerW / Math.max(1, n)
|
|
256
|
-
const barW = Math.max(1, Math.min(slot * 0.62, 36))
|
|
257
|
-
const cx = (i: number) => padL + slot * i + slot / 2
|
|
258
|
-
const ticks = axisTicks(data)
|
|
259
|
-
|
|
260
|
-
return (
|
|
261
|
-
<div ref={ref} style={{ position: 'relative', width: '100%' }}>
|
|
262
|
-
{w > 0 ? (
|
|
263
|
-
<svg
|
|
264
|
-
width={w}
|
|
265
|
-
height={height}
|
|
266
|
-
style={{ display: 'block' }}
|
|
267
|
-
onMouseMove={(e) => {
|
|
268
|
-
const rect = e.currentTarget.getBoundingClientRect()
|
|
269
|
-
const i = Math.floor((e.clientX - rect.left - padL) / (slot || 1))
|
|
270
|
-
setHover(Math.max(0, Math.min(n - 1, i)))
|
|
271
|
-
}}
|
|
272
|
-
onMouseLeave={() => setHover(null)}
|
|
273
|
-
>
|
|
274
|
-
{[0, 0.5, 1].map((g) => (
|
|
275
|
-
<line key={g} x1={padL} x2={w - padR} y1={padT + innerH * g} y2={padT + innerH * g} stroke={GRID} strokeWidth={1} />
|
|
276
|
-
))}
|
|
277
|
-
{data.map((d, i) => {
|
|
278
|
-
const h = (d.value / max) * innerH
|
|
279
|
-
return (
|
|
280
|
-
<rect
|
|
281
|
-
key={i}
|
|
282
|
-
x={cx(i) - barW / 2}
|
|
283
|
-
y={padT + innerH - h}
|
|
284
|
-
width={barW}
|
|
285
|
-
height={Math.max(0, h)}
|
|
286
|
-
rx={Math.min(3, barW / 2)}
|
|
287
|
-
fill={color}
|
|
288
|
-
fillOpacity={hover === i ? 1 : 0.78}
|
|
289
|
-
/>
|
|
290
|
-
)
|
|
291
|
-
})}
|
|
292
|
-
{ticks.map((t) => (
|
|
293
|
-
<text key={t.i} x={cx(t.i)} y={height - 7} fontSize={10} fill={AXIS} textAnchor="middle">
|
|
294
|
-
{t.label}
|
|
295
|
-
</text>
|
|
296
|
-
))}
|
|
297
|
-
</svg>
|
|
298
|
-
) : null}
|
|
299
|
-
{hover != null && data[hover] ? (
|
|
300
|
-
<Tooltip x={cx(hover)} height={height} label={data[hover].label} value={formatValue(data[hover].value)} />
|
|
301
|
-
) : null}
|
|
302
|
-
</div>
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ── Donut (categorical share) ────────────────────────────────────────────────
|
|
307
|
-
|
|
308
|
-
export function Donut({
|
|
309
|
-
slices,
|
|
310
|
-
size = 160,
|
|
311
|
-
thickness = 22,
|
|
312
|
-
center,
|
|
313
|
-
legend = false,
|
|
314
|
-
}: {
|
|
315
|
-
slices: Slice[]
|
|
316
|
-
size?: number
|
|
317
|
-
thickness?: number
|
|
318
|
-
/** Rendered, centered, inside the ring (a number, a label+sub, anything). */
|
|
319
|
-
center?: ReactNode
|
|
320
|
-
/**
|
|
321
|
-
* Render a built-in label/percentage legend beside the ring. Off by default so
|
|
322
|
-
* a consumer can compose a richer legend (amounts, click targets) itself.
|
|
323
|
-
*/
|
|
324
|
-
legend?: boolean
|
|
325
|
-
}) {
|
|
326
|
-
const theme = useTheme()
|
|
327
|
-
const fg = tone(theme, 'color12', 'currentColor')
|
|
328
|
-
const positive = slices.filter((s) => s.value > 0)
|
|
329
|
-
const total = positive.reduce((sum, s) => sum + s.value, 0)
|
|
330
|
-
const r = size / 2
|
|
331
|
-
const rInner = r - thickness
|
|
332
|
-
const mid = (r + rInner) / 2
|
|
333
|
-
|
|
334
|
-
if (total <= 0) {
|
|
335
|
-
return (
|
|
336
|
-
<XStack items="center" justify="center" height={size}>
|
|
337
|
-
<Text fontSize="$6" color="$color10">
|
|
338
|
-
—
|
|
339
|
-
</Text>
|
|
340
|
-
</XStack>
|
|
341
|
-
)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const ring = (
|
|
345
|
-
<div style={{ position: 'relative', width: size, height: size }}>
|
|
346
|
-
<svg width={size} height={size} style={{ display: 'block' }}>
|
|
347
|
-
{positive.length === 1 ? (
|
|
348
|
-
<circle
|
|
349
|
-
cx={r}
|
|
350
|
-
cy={r}
|
|
351
|
-
r={mid}
|
|
352
|
-
fill="none"
|
|
353
|
-
stroke={positive[0].color ?? fg}
|
|
354
|
-
strokeOpacity={positive[0].color ? 1 : 0.9}
|
|
355
|
-
strokeWidth={thickness}
|
|
356
|
-
/>
|
|
357
|
-
) : (
|
|
358
|
-
renderArcs(positive, total, r, rInner, fg)
|
|
359
|
-
)}
|
|
360
|
-
</svg>
|
|
361
|
-
{center ? (
|
|
362
|
-
<div
|
|
363
|
-
style={{
|
|
364
|
-
position: 'absolute',
|
|
365
|
-
inset: 0,
|
|
366
|
-
display: 'flex',
|
|
367
|
-
flexDirection: 'column',
|
|
368
|
-
alignItems: 'center',
|
|
369
|
-
justifyContent: 'center',
|
|
370
|
-
textAlign: 'center',
|
|
371
|
-
pointerEvents: 'none',
|
|
372
|
-
}}
|
|
373
|
-
>
|
|
374
|
-
{center}
|
|
375
|
-
</div>
|
|
376
|
-
) : null}
|
|
377
|
-
</div>
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
if (!legend) return ring
|
|
381
|
-
|
|
382
|
-
return (
|
|
383
|
-
<XStack gap="$4" items="center" flexWrap="wrap">
|
|
384
|
-
{ring}
|
|
385
|
-
<YStack gap="$1.5" flex={1} minW={150}>
|
|
386
|
-
{positive.map((s, i) => (
|
|
387
|
-
<XStack key={s.label} gap="$2" items="center">
|
|
388
|
-
<div
|
|
389
|
-
style={{
|
|
390
|
-
width: 10,
|
|
391
|
-
height: 10,
|
|
392
|
-
borderRadius: 2,
|
|
393
|
-
backgroundColor: s.color ?? AXIS,
|
|
394
|
-
opacity: s.color ? 1 : rampOpacity(i, positive.length),
|
|
395
|
-
}}
|
|
396
|
-
/>
|
|
397
|
-
<Text fontSize="$2" color="$color11" flex={1} numberOfLines={1}>
|
|
398
|
-
{s.label}
|
|
399
|
-
</Text>
|
|
400
|
-
<Text fontSize="$2" color="$color12" fontWeight="600">
|
|
401
|
-
{Math.round((s.value / total) * 100)}%
|
|
402
|
-
</Text>
|
|
403
|
-
</XStack>
|
|
404
|
-
))}
|
|
405
|
-
</YStack>
|
|
406
|
-
</XStack>
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function renderArcs(slices: Slice[], total: number, rOuter: number, rInner: number, fg: string): ReactNode {
|
|
411
|
-
let a0 = -Math.PI / 2
|
|
412
|
-
const paths: ReactNode[] = []
|
|
413
|
-
slices.forEach((s, idx) => {
|
|
414
|
-
const frac = Math.max(0, s.value) / total
|
|
415
|
-
if (frac <= 0) return
|
|
416
|
-
const a1 = a0 + frac * Math.PI * 2
|
|
417
|
-
paths.push(
|
|
418
|
-
<path
|
|
419
|
-
key={s.label || idx}
|
|
420
|
-
d={annularArc(rOuter, rOuter, rOuter, rInner, a0, Math.min(a1, a0 + Math.PI * 1.9999))}
|
|
421
|
-
fill={s.color ?? fg}
|
|
422
|
-
fillOpacity={s.color ? 1 : rampOpacity(idx, slices.length)}
|
|
423
|
-
/>,
|
|
424
|
-
)
|
|
425
|
-
a0 = a1
|
|
426
|
-
})
|
|
427
|
-
return <>{paths}</>
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function polar(cx: number, cy: number, rad: number, a: number): [number, number] {
|
|
431
|
-
return [cx + rad * Math.cos(a), cy + rad * Math.sin(a)]
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function annularArc(cx: number, cy: number, rOuter: number, rInner: number, a0: number, a1: number): string {
|
|
435
|
-
const large = a1 - a0 > Math.PI ? 1 : 0
|
|
436
|
-
const [x0, y0] = polar(cx, cy, rOuter, a0)
|
|
437
|
-
const [x1, y1] = polar(cx, cy, rOuter, a1)
|
|
438
|
-
const [x2, y2] = polar(cx, cy, rInner, a1)
|
|
439
|
-
const [x3, y3] = polar(cx, cy, rInner, a0)
|
|
440
|
-
return `M${x0},${y0} A${rOuter},${rOuter} 0 ${large} 1 ${x1},${y1} L${x2},${y2} A${rInner},${rInner} 0 ${large} 0 ${x3},${y3} Z`
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ── Bar rows (a ranked distribution, horizontal, token-native) ───────────────
|
|
444
|
-
|
|
445
|
-
export function BarRows({ bars }: { bars: Slice[] }) {
|
|
446
|
-
const max = Math.max(0, ...bars.map((b) => b.value))
|
|
447
|
-
if (max <= 0) {
|
|
448
|
-
return (
|
|
449
|
-
<Text fontSize="$3" color="$color10">
|
|
450
|
-
—
|
|
451
|
-
</Text>
|
|
452
|
-
)
|
|
453
|
-
}
|
|
454
|
-
return (
|
|
455
|
-
<YStack gap="$2.5">
|
|
456
|
-
{bars.map((b) => (
|
|
457
|
-
<XStack key={b.label} gap="$3" items="center">
|
|
458
|
-
<Text width={64} fontSize="$2" color="$color11" text="right">
|
|
459
|
-
{b.label}
|
|
460
|
-
</Text>
|
|
461
|
-
<YStack flex={1} height={10} bg="$color3" rounded="$2" overflow="hidden">
|
|
462
|
-
<YStack height={10} width={`${Math.max(2, (b.value / max) * 100)}%`} bg="$color9" rounded="$2" />
|
|
463
|
-
</YStack>
|
|
464
|
-
<Text width={56} fontSize="$2" color="$color12" fontWeight="600">
|
|
465
|
-
{b.value.toLocaleString()}
|
|
466
|
-
</Text>
|
|
467
|
-
</XStack>
|
|
468
|
-
))}
|
|
469
|
-
</YStack>
|
|
470
|
-
)
|
|
471
|
-
}
|
package/src/product/ComboBox.tsx
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ComboBox — a typeable select: a text input the user can type any value into,
|
|
5
|
-
* PLUS a popover of LIVE options (filtered by what's typed) they can pick from.
|
|
6
|
-
* The value is always exactly the input text, so a custom id is inherently
|
|
7
|
-
* supported — selecting an option just fills the input. This is the ONE way the
|
|
8
|
-
* console offers "pick from a live list OR type your own" (the model field, the
|
|
9
|
-
* tool field). Prop-driven + self-contained (options/loading/error injected by the
|
|
10
|
-
* caller), so it is orthogonal to the data source and lifts into `@hanzo/ui`.
|
|
11
|
-
*
|
|
12
|
-
* Idiom: the same @hanzo/gui Popover shell as OrgSwitcher/SelectMenu (bordered,
|
|
13
|
-
* elevate, `$color2`). Options render in a portal above the DetailPane SlideOver.
|
|
14
|
-
*/
|
|
15
|
-
import { useMemo, useState } from 'react'
|
|
16
|
-
import { Button, Input, Popover, Spinner, Text, XStack, YStack } from '@hanzo/gui'
|
|
17
|
-
import { Check, ChevronDown, RefreshCw } from '@hanzogui/lucide-icons-2'
|
|
18
|
-
|
|
19
|
-
import { filterOptions, type ComboOption } from './combobox/filter'
|
|
20
|
-
|
|
21
|
-
export type { ComboOption } from './combobox/filter'
|
|
22
|
-
|
|
23
|
-
export function ComboBox({
|
|
24
|
-
value,
|
|
25
|
-
onChange,
|
|
26
|
-
options,
|
|
27
|
-
loading = false,
|
|
28
|
-
error = null,
|
|
29
|
-
onRetry,
|
|
30
|
-
placeholder,
|
|
31
|
-
disabled,
|
|
32
|
-
emptyText = 'No matches — press to use what you typed.',
|
|
33
|
-
minWidth = 240,
|
|
34
|
-
}: {
|
|
35
|
-
value: string
|
|
36
|
-
onChange: (v: string) => void
|
|
37
|
-
/** The live option list. Filtered against the typed text. */
|
|
38
|
-
options: ComboOption[]
|
|
39
|
-
/** True while the caller is loading the option list (shows a spinner row). */
|
|
40
|
-
loading?: boolean
|
|
41
|
-
/** A human message when the option list failed to load (list still typeable). */
|
|
42
|
-
error?: string | null
|
|
43
|
-
/** Retry the option load (shown next to the error). */
|
|
44
|
-
onRetry?: () => void
|
|
45
|
-
placeholder?: string
|
|
46
|
-
disabled?: boolean
|
|
47
|
-
/** Row shown when the filter matches nothing (the typed value is still usable). */
|
|
48
|
-
emptyText?: string
|
|
49
|
-
minWidth?: number
|
|
50
|
-
}) {
|
|
51
|
-
const [open, setOpen] = useState(false)
|
|
52
|
-
const filtered = useMemo(() => filterOptions(options, value), [options, value])
|
|
53
|
-
|
|
54
|
-
const pick = (v: string) => {
|
|
55
|
-
onChange(v)
|
|
56
|
-
setOpen(false)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<Popover open={open} onOpenChange={setOpen} placement="bottom-start" allowFlip>
|
|
61
|
-
<XStack items="center" gap="$2" minW={minWidth}>
|
|
62
|
-
<Input
|
|
63
|
-
flex={1}
|
|
64
|
-
value={value}
|
|
65
|
-
onChangeText={(v) => {
|
|
66
|
-
onChange(v)
|
|
67
|
-
if (!open) setOpen(true)
|
|
68
|
-
}}
|
|
69
|
-
onFocus={() => setOpen(true)}
|
|
70
|
-
disabled={disabled}
|
|
71
|
-
placeholder={placeholder}
|
|
72
|
-
autoCapitalize="none"
|
|
73
|
-
/>
|
|
74
|
-
<Popover.Trigger asChild>
|
|
75
|
-
<Button
|
|
76
|
-
size="$2"
|
|
77
|
-
chromeless
|
|
78
|
-
disabled={disabled}
|
|
79
|
-
icon={<ChevronDown size={16} opacity={0.7} />}
|
|
80
|
-
onPress={() => setOpen((o) => !o)}
|
|
81
|
-
aria-label="Show options"
|
|
82
|
-
/>
|
|
83
|
-
</Popover.Trigger>
|
|
84
|
-
</XStack>
|
|
85
|
-
|
|
86
|
-
<Popover.Content bordered elevate p="$1.5" minW={minWidth} bg="$color2" borderColor="$borderColor">
|
|
87
|
-
<YStack gap="$0.5" minW={minWidth} maxH={300} overflow="scroll">
|
|
88
|
-
{loading ? (
|
|
89
|
-
<XStack items="center" gap="$2" px="$2.5" py="$2">
|
|
90
|
-
<Spinner size="small" color="$color11" />
|
|
91
|
-
<Text fontSize="$2" color="$color10">
|
|
92
|
-
Loading options…
|
|
93
|
-
</Text>
|
|
94
|
-
</XStack>
|
|
95
|
-
) : error ? (
|
|
96
|
-
<XStack items="center" gap="$2" px="$2.5" py="$2">
|
|
97
|
-
<Text fontSize="$2" color="$color10" flex={1} numberOfLines={2}>
|
|
98
|
-
{error}
|
|
99
|
-
</Text>
|
|
100
|
-
{onRetry ? (
|
|
101
|
-
<Button size="$1" chromeless icon={<RefreshCw size={12} />} onPress={onRetry} aria-label="Retry" />
|
|
102
|
-
) : null}
|
|
103
|
-
</XStack>
|
|
104
|
-
) : filtered.length === 0 ? (
|
|
105
|
-
<Text fontSize="$2" color="$color10" px="$2.5" py="$2">
|
|
106
|
-
{emptyText}
|
|
107
|
-
</Text>
|
|
108
|
-
) : (
|
|
109
|
-
filtered.map((o) => (
|
|
110
|
-
<Row key={o.value} option={o} active={o.value === value} onPress={() => pick(o.value)} />
|
|
111
|
-
))
|
|
112
|
-
)}
|
|
113
|
-
</YStack>
|
|
114
|
-
</Popover.Content>
|
|
115
|
-
</Popover>
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function Row({ option, active, onPress }: { option: ComboOption; active: boolean; onPress: () => void }) {
|
|
120
|
-
return (
|
|
121
|
-
<XStack
|
|
122
|
-
items="center"
|
|
123
|
-
gap="$2"
|
|
124
|
-
px="$2.5"
|
|
125
|
-
py="$1.5"
|
|
126
|
-
rounded="$3"
|
|
127
|
-
cursor="pointer"
|
|
128
|
-
hoverStyle={{ bg: '$color4' }}
|
|
129
|
-
bg={active ? '$color4' : 'transparent'}
|
|
130
|
-
onPress={onPress}
|
|
131
|
-
>
|
|
132
|
-
<XStack width={14} items="center" justify="center">
|
|
133
|
-
{active ? <Check size={13} /> : null}
|
|
134
|
-
</XStack>
|
|
135
|
-
<YStack flex={1} minW={0}>
|
|
136
|
-
<Text fontSize="$2" color="$color12" numberOfLines={1}>
|
|
137
|
-
{option.label ?? option.value}
|
|
138
|
-
</Text>
|
|
139
|
-
{option.hint ? (
|
|
140
|
-
<Text fontSize="$1" color="$color10" numberOfLines={1}>
|
|
141
|
-
{option.hint}
|
|
142
|
-
</Text>
|
|
143
|
-
) : null}
|
|
144
|
-
</YStack>
|
|
145
|
-
</XStack>
|
|
146
|
-
)
|
|
147
|
-
}
|