@hanzo/ui 8.0.7 → 8.0.11

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 (227) 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-M2ILMK44.cjs +2104 -0
  22. package/dist/chunk-M2ILMK44.cjs.map +1 -0
  23. package/dist/chunk-YBXVUV7P.cjs +103 -0
  24. package/dist/chunk-YBXVUV7P.cjs.map +1 -0
  25. package/dist/chunk-YSURJUOA.js +2035 -0
  26. package/dist/chunk-YSURJUOA.js.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 +299 -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/oss/blueprint.d.ts +37 -0
  53. package/dist/oss/catalog.d.ts +67 -0
  54. package/dist/oss.cjs +190 -0
  55. package/dist/oss.cjs.map +1 -0
  56. package/dist/oss.d.ts +2 -0
  57. package/dist/oss.js +180 -0
  58. package/dist/oss.js.map +1 -0
  59. package/dist/primitives/bases/data/index.cjs +15 -0
  60. package/dist/primitives/bases/data/index.cjs.map +1 -0
  61. package/dist/primitives/bases/data/index.d.ts +1 -0
  62. package/dist/primitives/bases/data/index.js +4 -0
  63. package/dist/primitives/bases/data/index.js.map +1 -0
  64. package/{types → dist}/product/AppHeader.d.ts +0 -1
  65. package/{types → dist}/product/BrandMark.d.ts +0 -1
  66. package/{types → dist}/product/Charts.d.ts +0 -1
  67. package/{types → dist}/product/ComboBox.d.ts +0 -1
  68. package/{types → dist}/product/DataTable.d.ts +0 -1
  69. package/{types → dist}/product/Donut.d.ts +0 -1
  70. package/{types → dist}/product/EmptyState.d.ts +0 -1
  71. package/{types → dist}/product/FadeIn.d.ts +0 -1
  72. package/{types → dist}/product/Field.d.ts +0 -1
  73. package/{types → dist}/product/HanzoMark.d.ts +0 -1
  74. package/{types → dist}/product/Metric.d.ts +0 -1
  75. package/dist/product/OrgMark.d.ts +21 -0
  76. package/{types → dist}/product/OrgSwitcher.d.ts +8 -2
  77. package/{types → dist}/product/PageHeader.d.ts +0 -1
  78. package/{types → dist}/product/PrimaryButton.d.ts +0 -1
  79. package/{types → dist}/product/ProductIcon.d.ts +0 -1
  80. package/{types → dist}/product/ProviderLogo.d.ts +0 -1
  81. package/{types → dist}/product/Reorder.d.ts +0 -1
  82. package/{types → dist}/product/SelectMenu.d.ts +6 -8
  83. package/{types → dist}/product/SlideOver.d.ts +0 -1
  84. package/{types → dist}/product/StatusTag.d.ts +0 -1
  85. package/{types → dist}/product/ThemeToggle.d.ts +0 -1
  86. package/{types → dist}/product/ThemeToggleNext.d.ts +0 -1
  87. package/{types → dist}/product/Toast.d.ts +0 -1
  88. package/{types → dist}/product/color.d.ts +0 -1
  89. package/{types → dist}/product/combobox/filter.d.ts +0 -1
  90. package/dist/product/index.cjs +286 -0
  91. package/dist/product/index.cjs.map +1 -0
  92. package/{types → dist}/product/index.d.ts +1 -1
  93. package/dist/product/index.js +5 -0
  94. package/dist/product/index.js.map +1 -0
  95. package/{types → dist}/product/menu/ContextMenu.d.ts +3 -6
  96. package/dist/product/menu/DropdownMenu.d.ts +29 -0
  97. package/dist/product/menu/FloatingMenu.d.ts +36 -0
  98. package/{types → dist}/product/menu/index.d.ts +1 -1
  99. package/{types → dist}/product/menu/items.d.ts +0 -1
  100. package/{types → dist}/product/menu/portal-theme.d.ts +0 -1
  101. package/{types → dist}/product/menu/roving.d.ts +0 -1
  102. package/{types → dist}/product/scope.d.ts +0 -1
  103. package/{types → dist}/product/surfaces.data.d.ts +0 -1
  104. package/dist/telemetry.cjs +15 -0
  105. package/dist/telemetry.cjs.map +1 -0
  106. package/dist/telemetry.d.ts +1 -0
  107. package/dist/telemetry.js +4 -0
  108. package/dist/telemetry.js.map +1 -0
  109. package/dist/usage.cjs +15 -0
  110. package/dist/usage.cjs.map +1 -0
  111. package/dist/usage.d.ts +1 -0
  112. package/dist/usage.js +4 -0
  113. package/dist/usage.js.map +1 -0
  114. package/dist/wallet.cjs +15 -0
  115. package/dist/wallet.cjs.map +1 -0
  116. package/{types → dist}/wallet.d.ts +0 -1
  117. package/dist/wallet.js +4 -0
  118. package/dist/wallet.js.map +1 -0
  119. package/package.json +69 -34
  120. package/src/billing.ts +0 -9
  121. package/src/canvas.ts +0 -11
  122. package/src/cd/layout.ts +0 -184
  123. package/src/cd/status.ts +0 -158
  124. package/src/cd/time.ts +0 -52
  125. package/src/cd/types.ts +0 -318
  126. package/src/dashboard.ts +0 -8
  127. package/src/data.ts +0 -12
  128. package/src/gitops.ts +0 -5
  129. package/src/gui-env.d.ts +0 -27
  130. package/src/index.ts +0 -22
  131. package/src/network.ts +0 -11
  132. package/src/primitives/bases/data/index.ts +0 -13
  133. package/src/product/AppHeader.tsx +0 -183
  134. package/src/product/BrandMark.tsx +0 -27
  135. package/src/product/Charts.tsx +0 -471
  136. package/src/product/ComboBox.tsx +0 -127
  137. package/src/product/DataTable.tsx +0 -99
  138. package/src/product/Donut.tsx +0 -83
  139. package/src/product/EmptyState.tsx +0 -129
  140. package/src/product/FadeIn.tsx +0 -38
  141. package/src/product/Field.tsx +0 -160
  142. package/src/product/HanzoMark.tsx +0 -31
  143. package/src/product/Metric.tsx +0 -253
  144. package/src/product/OrgSwitcher.tsx +0 -332
  145. package/src/product/PageHeader.tsx +0 -31
  146. package/src/product/PrimaryButton.tsx +0 -17
  147. package/src/product/ProductIcon.tsx +0 -61
  148. package/src/product/ProviderLogo.tsx +0 -137
  149. package/src/product/Reorder.test.ts +0 -41
  150. package/src/product/Reorder.tsx +0 -110
  151. package/src/product/SelectMenu.tsx +0 -80
  152. package/src/product/SlideOver.tsx +0 -197
  153. package/src/product/StatusTag.tsx +0 -49
  154. package/src/product/ThemeToggle.tsx +0 -135
  155. package/src/product/ThemeToggleNext.tsx +0 -27
  156. package/src/product/Toast.tsx +0 -166
  157. package/src/product/color.ts +0 -17
  158. package/src/product/combobox/filter.test.ts +0 -51
  159. package/src/product/combobox/filter.ts +0 -39
  160. package/src/product/index.ts +0 -76
  161. package/src/product/menu/ContextMenu.tsx +0 -114
  162. package/src/product/menu/DropdownMenu.tsx +0 -76
  163. package/src/product/menu/index.ts +0 -15
  164. package/src/product/menu/items.tsx +0 -230
  165. package/src/product/menu/portal-theme.tsx +0 -33
  166. package/src/product/menu/roving.ts +0 -34
  167. package/src/product/scope.test.ts +0 -92
  168. package/src/product/scope.ts +0 -123
  169. package/src/product/surfaces.data.ts +0 -42
  170. package/src/usage.ts +0 -8
  171. package/src/wallet.ts +0 -11
  172. package/types/billing.d.ts.map +0 -1
  173. package/types/canvas.d.ts +0 -2
  174. package/types/canvas.d.ts.map +0 -1
  175. package/types/cd/layout.d.ts.map +0 -1
  176. package/types/cd/status.d.ts.map +0 -1
  177. package/types/cd/time.d.ts.map +0 -1
  178. package/types/cd/types.d.ts.map +0 -1
  179. package/types/dashboard.d.ts +0 -2
  180. package/types/dashboard.d.ts.map +0 -1
  181. package/types/data.d.ts +0 -2
  182. package/types/data.d.ts.map +0 -1
  183. package/types/gitops.d.ts +0 -2
  184. package/types/gitops.d.ts.map +0 -1
  185. package/types/index.d.ts.map +0 -1
  186. package/types/network.d.ts.map +0 -1
  187. package/types/primitives/bases/data/index.d.ts +0 -2
  188. package/types/primitives/bases/data/index.d.ts.map +0 -1
  189. package/types/product/AppHeader.d.ts.map +0 -1
  190. package/types/product/BrandMark.d.ts.map +0 -1
  191. package/types/product/Charts.d.ts.map +0 -1
  192. package/types/product/ComboBox.d.ts.map +0 -1
  193. package/types/product/DataTable.d.ts.map +0 -1
  194. package/types/product/Donut.d.ts.map +0 -1
  195. package/types/product/EmptyState.d.ts.map +0 -1
  196. package/types/product/FadeIn.d.ts.map +0 -1
  197. package/types/product/Field.d.ts.map +0 -1
  198. package/types/product/HanzoMark.d.ts.map +0 -1
  199. package/types/product/Metric.d.ts.map +0 -1
  200. package/types/product/OrgSwitcher.d.ts.map +0 -1
  201. package/types/product/PageHeader.d.ts.map +0 -1
  202. package/types/product/PrimaryButton.d.ts.map +0 -1
  203. package/types/product/ProductIcon.d.ts.map +0 -1
  204. package/types/product/ProviderLogo.d.ts.map +0 -1
  205. package/types/product/Reorder.d.ts.map +0 -1
  206. package/types/product/SelectMenu.d.ts.map +0 -1
  207. package/types/product/SlideOver.d.ts.map +0 -1
  208. package/types/product/StatusTag.d.ts.map +0 -1
  209. package/types/product/ThemeToggle.d.ts.map +0 -1
  210. package/types/product/ThemeToggleNext.d.ts.map +0 -1
  211. package/types/product/Toast.d.ts.map +0 -1
  212. package/types/product/color.d.ts.map +0 -1
  213. package/types/product/combobox/filter.d.ts.map +0 -1
  214. package/types/product/index.d.ts.map +0 -1
  215. package/types/product/menu/ContextMenu.d.ts.map +0 -1
  216. package/types/product/menu/DropdownMenu.d.ts +0 -34
  217. package/types/product/menu/DropdownMenu.d.ts.map +0 -1
  218. package/types/product/menu/index.d.ts.map +0 -1
  219. package/types/product/menu/items.d.ts.map +0 -1
  220. package/types/product/menu/portal-theme.d.ts.map +0 -1
  221. package/types/product/menu/roving.d.ts.map +0 -1
  222. package/types/product/scope.d.ts.map +0 -1
  223. package/types/product/surfaces.data.d.ts.map +0 -1
  224. package/types/usage.d.ts +0 -2
  225. package/types/usage.d.ts.map +0 -1
  226. package/types/wallet.d.ts.map +0 -1
  227. /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,127 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * ComboBox — a typeable select: a text input the user can type any value into, PLUS a
5
- * menu of LIVE options (filtered by what's typed) they can pick from. The value is
6
- * always exactly the input text, so a custom id is inherently supported. Prop-driven +
7
- * self-contained (options/loading/error injected by the caller).
8
- *
9
- * Uses the ONE shared menu spec (MenuPanel + MenuItemView) so options look identical to
10
- * every other menu, and the SAME portal-theme fix (PortalTheme) so the list renders
11
- * correctly through the portal above a SlideOver and under a nested `<Theme>`.
12
- */
13
- import { useMemo, useState } from 'react'
14
- import { Button, Input, Popover, Spinner, Text, XStack } from '@hanzo/gui'
15
- import { ChevronDown, RefreshCw } from '@hanzogui/lucide-icons-2'
16
-
17
- import { filterOptions, type ComboOption } from './combobox/filter'
18
- import { MenuItemView, MenuPanel } from './menu/items'
19
- import { PortalTheme, useThemeName } from './menu/portal-theme'
20
- import { menuKeyDown } from './menu/roving'
21
-
22
- export type { ComboOption } from './combobox/filter'
23
-
24
- export function ComboBox({
25
- value,
26
- onChange,
27
- options,
28
- loading = false,
29
- error = null,
30
- onRetry,
31
- placeholder,
32
- disabled,
33
- emptyText = 'No matches — press to use what you typed.',
34
- minWidth = 240,
35
- }: {
36
- value: string
37
- onChange: (v: string) => void
38
- /** The live option list. Filtered against the typed text. */
39
- options: ComboOption[]
40
- /** True while the caller is loading the option list (shows a spinner row). */
41
- loading?: boolean
42
- /** A human message when the option list failed to load (list still typeable). */
43
- error?: string | null
44
- /** Retry the option load (shown next to the error). */
45
- onRetry?: () => void
46
- placeholder?: string
47
- disabled?: boolean
48
- /** Row shown when the filter matches nothing (the typed value is still usable). */
49
- emptyText?: string
50
- minWidth?: number
51
- }) {
52
- const [open, setOpen] = useState(false)
53
- const themeName = useThemeName()
54
- const filtered = useMemo(() => filterOptions(options, value), [options, value])
55
-
56
- const pick = (v: string) => {
57
- onChange(v)
58
- setOpen(false)
59
- }
60
-
61
- return (
62
- <Popover open={open} onOpenChange={setOpen} placement="bottom-start" allowFlip>
63
- <XStack items="center" gap="$2" minW={minWidth}>
64
- <Input
65
- flex={1}
66
- value={value}
67
- onChangeText={(v: string) => {
68
- onChange(v)
69
- if (!open) setOpen(true)
70
- }}
71
- onFocus={() => setOpen(true)}
72
- disabled={disabled}
73
- placeholder={placeholder}
74
- autoCapitalize="none"
75
- />
76
- <Popover.Trigger asChild>
77
- <Button
78
- size="$2"
79
- chromeless
80
- disabled={disabled}
81
- icon={<ChevronDown size={16} opacity={0.7} />}
82
- onPress={() => setOpen((o) => !o)}
83
- aria-label="Show options"
84
- />
85
- </Popover.Trigger>
86
- </XStack>
87
-
88
- <Popover.Content bg="transparent" borderWidth={0} p={0} elevation={0}>
89
- <PortalTheme name={themeName}>
90
- <MenuPanel minWidth={minWidth} maxHeight={300} onKeyDown={(e) => menuKeyDown(e, () => setOpen(false))}>
91
- {loading ? (
92
- <XStack items="center" gap="$2" px="$2" py="$2">
93
- <Spinner size="small" color="$color11" />
94
- <Text fontSize="$2" color="$color10">
95
- Loading options…
96
- </Text>
97
- </XStack>
98
- ) : error ? (
99
- <XStack items="center" gap="$2" px="$2" py="$2">
100
- <Text fontSize="$2" color="$color10" flex={1} numberOfLines={2}>
101
- {error}
102
- </Text>
103
- {onRetry ? (
104
- <Button size="$1" chromeless icon={<RefreshCw size={12} />} onPress={onRetry} aria-label="Retry" />
105
- ) : null}
106
- </XStack>
107
- ) : filtered.length === 0 ? (
108
- <Text fontSize="$2" color="$color10" px="$2" py="$2">
109
- {emptyText}
110
- </Text>
111
- ) : (
112
- filtered.map((o) => (
113
- <MenuItemView
114
- key={o.value}
115
- label={o.label ?? o.value}
116
- description={o.hint}
117
- selected={o.value === value}
118
- onSelect={() => pick(o.value)}
119
- />
120
- ))
121
- )}
122
- </MenuPanel>
123
- </PortalTheme>
124
- </Popover.Content>
125
- </Popover>
126
- )
127
- }
@@ -1,99 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * Generic data table — the shared list primitive for every product module.
5
- *
6
- * Rows are typed `<T>`; columns declare a key, header, and optional cell
7
- * renderer. Built on Hanzo GUI Stacks (shorthand style props per the v5 config)
8
- * so it adapts across platforms. Loading and empty states are first-class.
9
- */
10
- import type { ReactNode } from 'react'
11
- import { Spinner, Text, XStack, YStack } from '@hanzo/gui'
12
-
13
- export type Column<T> = {
14
- key: string
15
- header: string
16
- /** Cell renderer; defaults to `String(row[key])`. */
17
- render?: (row: T) => ReactNode
18
- /** Fixed width (px) for the column; otherwise flexes. */
19
- width?: number
20
- }
21
-
22
- export function DataTable<T>({
23
- columns,
24
- rows,
25
- loading,
26
- empty = 'Nothing here yet.',
27
- rowKey,
28
- onRowPress,
29
- }: {
30
- columns: Column<T>[]
31
- rows: T[]
32
- loading?: boolean
33
- empty?: string
34
- rowKey: (row: T) => string
35
- onRowPress?: (row: T) => void
36
- }) {
37
- if (loading) {
38
- return (
39
- <XStack p="$6" justify="center">
40
- <Spinner size="large" color="$color11" />
41
- </XStack>
42
- )
43
- }
44
-
45
- if (rows.length === 0) {
46
- return (
47
- <YStack p="$6" items="center" borderWidth={1} borderColor="$borderColor" rounded="$4">
48
- <Text color="$color11">{empty}</Text>
49
- </YStack>
50
- )
51
- }
52
-
53
- return (
54
- <YStack borderWidth={1} borderColor="$borderColor" rounded="$4" overflow="hidden">
55
- <XStack bg="$color2" py="$2.5" px="$3" gap="$3">
56
- {columns.map((c) => (
57
- <Text
58
- key={c.key}
59
- width={c.width}
60
- flex={c.width ? undefined : 1}
61
- fontSize="$2"
62
- fontWeight="700"
63
- color="$color11"
64
- >
65
- {c.header}
66
- </Text>
67
- ))}
68
- </XStack>
69
- <YStack>
70
- {rows.map((row) => (
71
- <XStack
72
- key={rowKey(row)}
73
- py="$2.5"
74
- px="$3"
75
- gap="$3"
76
- borderTopWidth={1}
77
- borderColor="$borderColor"
78
- items="center"
79
- hoverStyle={onRowPress ? { bg: '$color2' } : undefined}
80
- cursor={onRowPress ? 'pointer' : undefined}
81
- onPress={onRowPress ? () => onRowPress(row) : undefined}
82
- >
83
- {columns.map((c) => (
84
- <YStack key={c.key} width={c.width} flex={c.width ? undefined : 1}>
85
- {c.render ? (
86
- c.render(row)
87
- ) : (
88
- <Text fontSize="$3" numberOfLines={1}>
89
- {String((row as Record<string, unknown>)[c.key] ?? '')}
90
- </Text>
91
- )}
92
- </YStack>
93
- ))}
94
- </XStack>
95
- ))}
96
- </YStack>
97
- </YStack>
98
- )
99
- }