@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.
Files changed (200) hide show
  1. package/dist/ThemeToggleNext-7727UR5W.js +4 -0
  2. package/dist/ThemeToggleNext-7727UR5W.js.map +1 -0
  3. package/dist/ThemeToggleNext-ICB5KL5J.cjs +19 -0
  4. package/dist/ThemeToggleNext-ICB5KL5J.cjs.map +1 -0
  5. package/dist/billing.cjs +15 -0
  6. package/dist/billing.cjs.map +1 -0
  7. package/{types → dist}/billing.d.ts +0 -1
  8. package/dist/billing.js +4 -0
  9. package/dist/billing.js.map +1 -0
  10. package/dist/canvas.cjs +15 -0
  11. package/dist/canvas.cjs.map +1 -0
  12. package/dist/canvas.d.ts +1 -0
  13. package/dist/canvas.js +4 -0
  14. package/dist/canvas.js.map +1 -0
  15. package/{types → dist}/cd/layout.d.ts +0 -1
  16. package/{types → dist}/cd/status.d.ts +0 -1
  17. package/{types → dist}/cd/time.d.ts +0 -1
  18. package/{types → dist}/cd/types.d.ts +0 -1
  19. package/dist/chunk-42CU5GSF.js +98 -0
  20. package/dist/chunk-42CU5GSF.js.map +1 -0
  21. package/dist/chunk-GDGVXU7W.js +1999 -0
  22. package/dist/chunk-GDGVXU7W.js.map +1 -0
  23. package/dist/chunk-HSPMQQVJ.cjs +2066 -0
  24. package/dist/chunk-HSPMQQVJ.cjs.map +1 -0
  25. package/dist/chunk-YBXVUV7P.cjs +103 -0
  26. package/dist/chunk-YBXVUV7P.cjs.map +1 -0
  27. package/dist/dashboard.cjs +15 -0
  28. package/dist/dashboard.cjs.map +1 -0
  29. package/dist/dashboard.d.ts +1 -0
  30. package/dist/dashboard.js +4 -0
  31. package/dist/dashboard.js.map +1 -0
  32. package/dist/data.cjs +15 -0
  33. package/dist/data.cjs.map +1 -0
  34. package/dist/data.d.ts +1 -0
  35. package/dist/data.js +4 -0
  36. package/dist/data.js.map +1 -0
  37. package/dist/gitops.cjs +15 -0
  38. package/dist/gitops.cjs.map +1 -0
  39. package/dist/gitops.d.ts +1 -0
  40. package/dist/gitops.js +4 -0
  41. package/dist/gitops.js.map +1 -0
  42. package/dist/index.cjs +291 -0
  43. package/dist/index.cjs.map +1 -0
  44. package/{types → dist}/index.d.ts +0 -1
  45. package/dist/index.js +6 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/network.cjs +15 -0
  48. package/dist/network.cjs.map +1 -0
  49. package/{types → dist}/network.d.ts +0 -1
  50. package/dist/network.js +4 -0
  51. package/dist/network.js.map +1 -0
  52. package/dist/primitives/bases/data/index.cjs +15 -0
  53. package/dist/primitives/bases/data/index.cjs.map +1 -0
  54. package/dist/primitives/bases/data/index.d.ts +1 -0
  55. package/dist/primitives/bases/data/index.js +4 -0
  56. package/dist/primitives/bases/data/index.js.map +1 -0
  57. package/{types → dist}/product/AppHeader.d.ts +0 -1
  58. package/{types → dist}/product/BrandMark.d.ts +0 -1
  59. package/{types → dist}/product/Charts.d.ts +0 -1
  60. package/{types → dist}/product/ComboBox.d.ts +0 -1
  61. package/{types → dist}/product/DataTable.d.ts +0 -1
  62. package/{types → dist}/product/Donut.d.ts +0 -1
  63. package/{types → dist}/product/EmptyState.d.ts +0 -1
  64. package/{types → dist}/product/FadeIn.d.ts +0 -1
  65. package/{types → dist}/product/Field.d.ts +0 -1
  66. package/{types → dist}/product/HanzoMark.d.ts +0 -1
  67. package/{types → dist}/product/Metric.d.ts +0 -1
  68. package/{types → dist}/product/OrgSwitcher.d.ts +0 -1
  69. package/{types → dist}/product/PageHeader.d.ts +0 -1
  70. package/{types → dist}/product/PrimaryButton.d.ts +0 -1
  71. package/{types → dist}/product/ProductIcon.d.ts +0 -1
  72. package/{types → dist}/product/ProviderLogo.d.ts +0 -1
  73. package/{types → dist}/product/Reorder.d.ts +0 -1
  74. package/{types → dist}/product/SelectMenu.d.ts +7 -6
  75. package/{types → dist}/product/SlideOver.d.ts +0 -1
  76. package/{types → dist}/product/StatusTag.d.ts +0 -1
  77. package/dist/product/ThemeToggle.d.ts +21 -0
  78. package/dist/product/ThemeToggleNext.d.ts +4 -0
  79. package/{types → dist}/product/Toast.d.ts +0 -1
  80. package/{types → dist}/product/color.d.ts +0 -1
  81. package/{types → dist}/product/combobox/filter.d.ts +0 -1
  82. package/dist/product/index.cjs +278 -0
  83. package/dist/product/index.cjs.map +1 -0
  84. package/{types → dist}/product/index.d.ts +2 -1
  85. package/dist/product/index.js +5 -0
  86. package/dist/product/index.js.map +1 -0
  87. package/dist/product/menu/ContextMenu.d.ts +21 -0
  88. package/dist/product/menu/DropdownMenu.d.ts +29 -0
  89. package/dist/product/menu/FloatingMenu.d.ts +36 -0
  90. package/dist/product/menu/index.d.ts +5 -0
  91. package/dist/product/menu/items.d.ts +68 -0
  92. package/dist/product/menu/portal-theme.d.ts +28 -0
  93. package/dist/product/menu/roving.d.ts +8 -0
  94. package/{types → dist}/product/scope.d.ts +0 -1
  95. package/{types → dist}/product/surfaces.data.d.ts +0 -1
  96. package/dist/usage.cjs +15 -0
  97. package/dist/usage.cjs.map +1 -0
  98. package/dist/usage.d.ts +1 -0
  99. package/dist/usage.js +4 -0
  100. package/dist/usage.js.map +1 -0
  101. package/dist/wallet.cjs +15 -0
  102. package/dist/wallet.cjs.map +1 -0
  103. package/{types → dist}/wallet.d.ts +0 -1
  104. package/dist/wallet.js +4 -0
  105. package/dist/wallet.js.map +1 -0
  106. package/package.json +58 -32
  107. package/src/billing.ts +0 -9
  108. package/src/canvas.ts +0 -11
  109. package/src/cd/layout.ts +0 -184
  110. package/src/cd/status.ts +0 -158
  111. package/src/cd/time.ts +0 -52
  112. package/src/cd/types.ts +0 -318
  113. package/src/dashboard.ts +0 -8
  114. package/src/data.ts +0 -12
  115. package/src/gitops.ts +0 -5
  116. package/src/gui-env.d.ts +0 -27
  117. package/src/index.ts +0 -22
  118. package/src/network.ts +0 -11
  119. package/src/primitives/bases/data/index.ts +0 -13
  120. package/src/product/AppHeader.tsx +0 -183
  121. package/src/product/BrandMark.tsx +0 -27
  122. package/src/product/Charts.tsx +0 -471
  123. package/src/product/ComboBox.tsx +0 -147
  124. package/src/product/DataTable.tsx +0 -99
  125. package/src/product/Donut.tsx +0 -83
  126. package/src/product/EmptyState.tsx +0 -129
  127. package/src/product/FadeIn.tsx +0 -38
  128. package/src/product/Field.tsx +0 -160
  129. package/src/product/HanzoMark.tsx +0 -31
  130. package/src/product/Metric.tsx +0 -253
  131. package/src/product/OrgSwitcher.tsx +0 -332
  132. package/src/product/PageHeader.tsx +0 -31
  133. package/src/product/PrimaryButton.tsx +0 -17
  134. package/src/product/ProductIcon.tsx +0 -61
  135. package/src/product/ProviderLogo.tsx +0 -137
  136. package/src/product/Reorder.test.ts +0 -41
  137. package/src/product/Reorder.tsx +0 -110
  138. package/src/product/SelectMenu.tsx +0 -94
  139. package/src/product/SlideOver.tsx +0 -197
  140. package/src/product/StatusTag.tsx +0 -49
  141. package/src/product/ThemeToggle.tsx +0 -25
  142. package/src/product/Toast.tsx +0 -166
  143. package/src/product/color.ts +0 -17
  144. package/src/product/combobox/filter.test.ts +0 -51
  145. package/src/product/combobox/filter.ts +0 -39
  146. package/src/product/index.ts +0 -70
  147. package/src/product/scope.test.ts +0 -92
  148. package/src/product/scope.ts +0 -123
  149. package/src/product/surfaces.data.ts +0 -42
  150. package/src/usage.ts +0 -8
  151. package/src/wallet.ts +0 -11
  152. package/types/billing.d.ts.map +0 -1
  153. package/types/canvas.d.ts +0 -2
  154. package/types/canvas.d.ts.map +0 -1
  155. package/types/cd/layout.d.ts.map +0 -1
  156. package/types/cd/status.d.ts.map +0 -1
  157. package/types/cd/time.d.ts.map +0 -1
  158. package/types/cd/types.d.ts.map +0 -1
  159. package/types/dashboard.d.ts +0 -2
  160. package/types/dashboard.d.ts.map +0 -1
  161. package/types/data.d.ts +0 -2
  162. package/types/data.d.ts.map +0 -1
  163. package/types/gitops.d.ts +0 -2
  164. package/types/gitops.d.ts.map +0 -1
  165. package/types/index.d.ts.map +0 -1
  166. package/types/network.d.ts.map +0 -1
  167. package/types/primitives/bases/data/index.d.ts +0 -2
  168. package/types/primitives/bases/data/index.d.ts.map +0 -1
  169. package/types/product/AppHeader.d.ts.map +0 -1
  170. package/types/product/BrandMark.d.ts.map +0 -1
  171. package/types/product/Charts.d.ts.map +0 -1
  172. package/types/product/ComboBox.d.ts.map +0 -1
  173. package/types/product/DataTable.d.ts.map +0 -1
  174. package/types/product/Donut.d.ts.map +0 -1
  175. package/types/product/EmptyState.d.ts.map +0 -1
  176. package/types/product/FadeIn.d.ts.map +0 -1
  177. package/types/product/Field.d.ts.map +0 -1
  178. package/types/product/HanzoMark.d.ts.map +0 -1
  179. package/types/product/Metric.d.ts.map +0 -1
  180. package/types/product/OrgSwitcher.d.ts.map +0 -1
  181. package/types/product/PageHeader.d.ts.map +0 -1
  182. package/types/product/PrimaryButton.d.ts.map +0 -1
  183. package/types/product/ProductIcon.d.ts.map +0 -1
  184. package/types/product/ProviderLogo.d.ts.map +0 -1
  185. package/types/product/Reorder.d.ts.map +0 -1
  186. package/types/product/SelectMenu.d.ts.map +0 -1
  187. package/types/product/SlideOver.d.ts.map +0 -1
  188. package/types/product/StatusTag.d.ts.map +0 -1
  189. package/types/product/ThemeToggle.d.ts +0 -2
  190. package/types/product/ThemeToggle.d.ts.map +0 -1
  191. package/types/product/Toast.d.ts.map +0 -1
  192. package/types/product/color.d.ts.map +0 -1
  193. package/types/product/combobox/filter.d.ts.map +0 -1
  194. package/types/product/index.d.ts.map +0 -1
  195. package/types/product/scope.d.ts.map +0 -1
  196. package/types/product/surfaces.data.d.ts.map +0 -1
  197. package/types/usage.d.ts +0 -2
  198. package/types/usage.d.ts.map +0 -1
  199. package/types/wallet.d.ts.map +0 -1
  200. /package/{src → dist}/styles/hanzo-motion.css +0 -0
@@ -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
- }
@@ -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
- }