@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,253 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * Console metric primitives — the ONE shared set of stat tiles, panels, legend
5
- * rows, util bars, and "real action or honest-disabled" buttons that every
6
- * resource console (GPUs, Kubernetes, Containers, Training, Tasks) renders.
7
- *
8
- * These were first written for the GPU surface; promoted here so a sibling
9
- * product never reaches into `gpus/` for shell chrome (separation of concerns).
10
- * `gpus/charts.tsx` now re-exports them, so there is exactly one definition.
11
- *
12
- * Honest by construction: a `MetricCard` shows the pre-formatted `value` the
13
- * caller passes (an absent metric is an em-dash, never a fabricated number); a
14
- * `Sparkline`/`MiniBars` with no real series renders nothing; a `HintButton`
15
- * with no `onPress` is visibly disabled with a tooltip saying WHY. Inline SVG is
16
- * the house pattern (see ui/Loader, ui/HanzoMark); style props use the v5
17
- * shorthand set.
18
- */
19
- import type { ReactElement, ReactNode } from 'react'
20
- import { Button, Card, Text, XStack, YStack } from '@hanzo/gui'
21
-
22
- /** Distinct, theme-neutral series colors (mid-saturation reads on dark + light). */
23
- export const SERIES = ['#6ea8fe', '#7ee787', '#f0a868', '#c792ea', '#56d4c4', '#e879a6', '#d6c15a', '#8b9bb4'] as const
24
- export const colorForIndex = (i: number): string => SERIES[i % SERIES.length]
25
- const TRACK = 'rgba(128,128,128,0.18)'
26
-
27
- /** Tone for a utilization/temperature value (green calm → red hot). */
28
- export function utilColor(pct: number): string {
29
- if (pct >= 90) return '#e5534b'
30
- if (pct >= 70) return '#f0a868'
31
- return '#7ee787'
32
- }
33
-
34
- /**
35
- * A single-series sparkline over real points. Renders nothing meaningful for <2
36
- * points (the caller shows `—` instead). The y-range is the data's own min/max.
37
- */
38
- export function Sparkline({
39
- points,
40
- width = 104,
41
- height = 30,
42
- color = SERIES[0],
43
- }: {
44
- points: number[]
45
- width?: number
46
- height?: number
47
- color?: string
48
- }): ReactElement | null {
49
- if (!points || points.length < 2) return null
50
- const min = Math.min(...points)
51
- const max = Math.max(...points)
52
- const span = max - min || 1
53
- const stepX = width / (points.length - 1)
54
- const y = (v: number) => height - 2 - ((v - min) / span) * (height - 4)
55
- const d = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${(i * stepX).toFixed(1)},${y(p).toFixed(1)}`).join(' ')
56
- return (
57
- <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} role="img" aria-label="trend">
58
- <path d={d} fill="none" stroke={color} strokeWidth={1.5} strokeLinejoin="round" strokeLinecap="round" />
59
- </svg>
60
- )
61
- }
62
-
63
- /** A small vertical-bar chart (e.g. cost per day). One bar per real datum. */
64
- export function MiniBars({
65
- bars,
66
- width = 320,
67
- height = 96,
68
- color = SERIES[0],
69
- }: {
70
- bars: { label: string; value: number }[]
71
- width?: number
72
- height?: number
73
- color?: string
74
- }): ReactElement | null {
75
- if (!bars.length) return null
76
- const max = Math.max(...bars.map((b) => b.value), 1)
77
- const gap = 4
78
- const bw = (width - gap * (bars.length - 1)) / bars.length
79
- return (
80
- <svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none" role="img" aria-label="bars">
81
- {bars.map((b, i) => {
82
- const h = Math.max(1, (b.value / max) * (height - 2))
83
- return (
84
- <rect key={b.label + i} x={i * (bw + gap)} y={height - h} width={bw} height={h} rx={2} fill={color} opacity={0.85}>
85
- <title>{`${b.label}: ${b.value}`}</title>
86
- </rect>
87
- )
88
- })}
89
- </svg>
90
- )
91
- }
92
-
93
- /**
94
- * A thin horizontal bar (0–100). By default the fill tones by value (green→red,
95
- * for utilization/temperature). Pass a fixed `color` for a neutral meter like
96
- * training PROGRESS, where a high value is good, not "hot".
97
- */
98
- export function UtilBar({ value, width = 120, color }: { value: number; width?: number; color?: string }): ReactElement {
99
- const v = Math.max(0, Math.min(100, value))
100
- return (
101
- <svg width={width} height={8} viewBox={`0 0 ${width} 8`} role="img" aria-label={`${Math.round(v)}%`}>
102
- <rect x={0} y={1} width={width} height={6} rx={3} fill={TRACK} />
103
- <rect x={0} y={1} width={(v / 100) * width} height={6} rx={3} fill={color ?? utilColor(v)} />
104
- </svg>
105
- )
106
- }
107
-
108
- /** A legend row: color swatch + label + value. */
109
- export function LegendDot({ color, label, value }: { color: string; label: string; value?: ReactNode }) {
110
- return (
111
- <XStack items="center" gap="$2" justify="space-between">
112
- <XStack items="center" gap="$2">
113
- <YStack width={10} height={10} rounded="$1" bg={color as never} />
114
- <Text fontSize="$2" color="$color11">
115
- {label}
116
- </Text>
117
- </XStack>
118
- {value != null ? (
119
- <Text fontSize="$2" color="$color12" fontWeight="600">
120
- {value}
121
- </Text>
122
- ) : null}
123
- </XStack>
124
- )
125
- }
126
-
127
- /**
128
- * One overview metric tile. `value` is pre-formatted by the caller (so an absent
129
- * metric is an honest `—`, never a fabricated number). The sparkline only appears
130
- * when the caller has a REAL series; the delta only when a real comparison exists.
131
- */
132
- export function MetricCard({
133
- icon,
134
- label,
135
- value,
136
- caption,
137
- spark,
138
- sparkColor,
139
- delta,
140
- }: {
141
- icon: ReactElement
142
- label: string
143
- value: string
144
- caption?: string
145
- spark?: number[]
146
- sparkColor?: string
147
- delta?: { pct: number }
148
- }) {
149
- const up = delta ? delta.pct >= 0 : false
150
- return (
151
- <Card p="$3.5" gap="$2" borderWidth={1} borderColor="$borderColor" flex={1} minW={172}>
152
- <XStack items="center" gap="$2" justify="space-between">
153
- <XStack items="center" gap="$2">
154
- {icon}
155
- <Text fontSize="$2" color="$color11" fontWeight="600">
156
- {label}
157
- </Text>
158
- </XStack>
159
- {delta ? (
160
- <Text fontSize="$1" color={up ? '#7ee787' : '#e5534b'}>
161
- {up ? '▲' : '▼'} {Math.abs(delta.pct)}%
162
- </Text>
163
- ) : null}
164
- </XStack>
165
- <XStack items="flex-end" justify="space-between" gap="$2">
166
- <Text fontSize="$8" fontWeight="900" color="$color12" numberOfLines={1}>
167
- {value}
168
- </Text>
169
- {spark && spark.length >= 2 ? <Sparkline points={spark} color={sparkColor ?? SERIES[0]} /> : null}
170
- </XStack>
171
- {caption ? (
172
- <Text fontSize="$1" color="$color10" numberOfLines={1}>
173
- {caption}
174
- </Text>
175
- ) : null}
176
- </Card>
177
- )
178
- }
179
-
180
- /**
181
- * A button that is either a real action (onPress) or HONESTLY disabled with a
182
- * native tooltip explaining why (the action has no backend yet). One way to
183
- * render the "real route or honest-disabled" affordance the consoles use.
184
- */
185
- export function HintButton({
186
- icon,
187
- iconAfter,
188
- children,
189
- onPress,
190
- disabled,
191
- hint,
192
- theme,
193
- }: {
194
- icon?: ReactElement
195
- iconAfter?: ReactElement
196
- children: ReactNode
197
- onPress?: () => void
198
- disabled?: boolean
199
- /** Native tooltip text (shown on hover) — say WHY it is disabled. */
200
- hint?: string
201
- theme?: 'light'
202
- }) {
203
- const btn = (
204
- <Button
205
- size="$2"
206
- theme={theme}
207
- icon={icon}
208
- iconAfter={iconAfter}
209
- disabled={disabled}
210
- onPress={disabled ? undefined : onPress}
211
- >
212
- {children}
213
- </Button>
214
- )
215
- if (!hint) return btn
216
- return (
217
- <span title={hint} style={{ display: 'inline-flex' }}>
218
- {btn}
219
- </span>
220
- )
221
- }
222
-
223
- /**
224
- * A titled panel wrapper used across the consoles (chart + list cards). `grow`
225
- * (default true) sets `flex={1}` so panels SHARE width in a ROW; pass `grow={false}`
226
- * inside a vertical rail (a column YStack) so each panel sizes to its content and
227
- * stacks — flex-grow in a column with no fixed height collapses them onto each other.
228
- */
229
- export function Panel({
230
- title,
231
- right,
232
- children,
233
- minW = 280,
234
- grow = true,
235
- }: {
236
- title: string
237
- right?: ReactNode
238
- children: ReactNode
239
- minW?: number
240
- grow?: boolean
241
- }) {
242
- return (
243
- <Card p="$4" gap="$3" borderWidth={1} borderColor="$borderColor" flex={grow ? 1 : undefined} minW={minW} width={grow ? undefined : '100%'}>
244
- <XStack items="center" justify="space-between" gap="$2">
245
- <Text fontSize="$4" fontWeight="800" color="$color12">
246
- {title}
247
- </Text>
248
- {right}
249
- </XStack>
250
- {children}
251
- </Card>
252
- )
253
- }
@@ -1,332 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * OrgSwitcher — the Vercel-style org/team switcher, hoisted from the console
5
- * (hanzoai/ui#36) onto @hanzo/gui primitives. Shows the org the surface is
6
- * scoped to, switches between the orgs the caller can see, and creates one.
7
- *
8
- * Host-agnostic: the LIST is a lazy, paged loader the host injects (IAM
9
- * `get-organizations` behind its own proxy) — one page at a time, the search
10
- * term pushed to the server, more on demand (scroll + "Load more"), so it
11
- * scales to thousands of orgs. No loader → the current org alone, synthesized
12
- * from the scope (never fabricated). Selecting re-scopes IN PLACE via
13
- * `scope.switchOrg` (persist + reload → every module refetches under the new
14
- * `X-Org-Id`). Create posts through the injected hook, then scopes into the
15
- * new org.
16
- */
17
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
18
- import { Button, Input, Popover, Spinner, Text, XStack, YStack } from '@hanzo/gui'
19
- import { Check, ChevronsUpDown, LayoutGrid, Plus, Search } from '@hanzogui/lucide-icons-2'
20
-
21
- import { filterOrgs, type Org, type OrgScope } from './scope'
22
-
23
- const titleCase = (s: string) => (s ? s[0].toUpperCase() + s.slice(1) : s)
24
- const initialsOf = (o: Org) =>
25
- (o.displayName || o.name)
26
- .split(/\s+/)
27
- .map((w) => w[0])
28
- .join('')
29
- .slice(0, 2)
30
- .toUpperCase()
31
-
32
- /** An org's avatar — its logo when set, else a monogram tile. */
33
- function OrgAvatar({ org, size = 22 }: { org: Org; size?: number }) {
34
- if (org.logo) {
35
- // eslint-disable-next-line @next/next/no-img-element
36
- return <img src={org.logo} alt="" style={{ height: size, width: size, objectFit: 'contain', display: 'block', borderRadius: 6 }} />
37
- }
38
- return (
39
- <YStack width={size} height={size} rounded="$3" bg="$color4" items="center" justify="center">
40
- <Text fontSize="$1" fontWeight="800" color="$color12">
41
- {initialsOf(org)}
42
- </Text>
43
- </YStack>
44
- )
45
- }
46
-
47
- export type OrgSwitcherProps = {
48
- /** The active-org contract (see `orgScope`). */
49
- scope: OrgScope
50
- /**
51
- * Lazy paged org list: 0-based page + server-pushed search → one page of
52
- * rows. A full page (=== pageSize) implies another. Omit when the caller
53
- * can only see its own org.
54
- */
55
- orgs?: (page: number, query: string) => Promise<Org[]>
56
- /** Rows per page the loader returns. Default 20. */
57
- pageSize?: number
58
- /** Create-org hook → the created org's id; omit to hide the affordance. */
59
- create?: (name: string) => Promise<string>
60
- /** Show the "All organizations" de-scope row (`scope.leaveOrg`). */
61
- picker?: boolean
62
- }
63
-
64
- export function OrgSwitcher({ scope, orgs, pageSize = 20, create, picker = false }: OrgSwitcherProps) {
65
- const currentId = scope.currentOrg()
66
-
67
- const [open, setOpen] = useState(false)
68
- const [query, setQuery] = useState('')
69
- const [debounced, setDebounced] = useState('')
70
- const [rows, setRows] = useState<Org[]>([])
71
- const [loading, setLoading] = useState(false)
72
- const [loadingMore, setLoadingMore] = useState(false)
73
- const [hasMore, setHasMore] = useState(false)
74
- const [creating, setCreating] = useState(false)
75
- const [newName, setNewName] = useState('')
76
- const [busy, setBusy] = useState(false)
77
- const [err, setErr] = useState<string | null>(null)
78
- const pageRef = useRef(0)
79
- const reqRef = useRef(0) // race token — a newer request supersedes older ones
80
-
81
- // Debounce the search so type-as-you-search re-hits the server at most ~4×/s.
82
- useEffect(() => {
83
- const id = setTimeout(() => setDebounced(query.trim()), 250)
84
- return () => clearTimeout(id)
85
- }, [query])
86
-
87
- const fetchPage = useCallback(
88
- async (page: number, q: string, append: boolean) => {
89
- if (!orgs) return
90
- const token = ++reqRef.current
91
- append ? setLoadingMore(true) : setLoading(true)
92
- try {
93
- const incoming = await orgs(page, q)
94
- if (token !== reqRef.current) return
95
- pageRef.current = page
96
- setRows((prev) => {
97
- const merged = append ? [...prev] : []
98
- const seen = new Set(merged.map((o) => o.name))
99
- for (const o of incoming) if (!seen.has(o.name)) merged.push(o)
100
- return merged
101
- })
102
- setHasMore(incoming.length >= pageSize)
103
- } catch {
104
- // Honest empty / keep what's shown — never a fabricated list.
105
- if (token !== reqRef.current) return
106
- if (!append) setRows([])
107
- setHasMore(false)
108
- } finally {
109
- if (token === reqRef.current) {
110
- setLoading(false)
111
- setLoadingMore(false)
112
- }
113
- }
114
- },
115
- [orgs, pageSize],
116
- )
117
-
118
- // (Re)load page 0 when the popover opens or the debounced query changes.
119
- useEffect(() => {
120
- if (!open || !orgs) return
121
- void fetchPage(0, debounced, false)
122
- }, [open, debounced, fetchPage, orgs])
123
-
124
- const loadMore = useCallback(() => {
125
- if (loading || loadingMore || !hasMore) return
126
- void fetchPage(pageRef.current + 1, debounced, true)
127
- }, [loading, loadingMore, hasMore, debounced, fetchPage])
128
-
129
- // Rows to render — the loaded list (client-filtered too), else the current
130
- // org synthesized from the scope.
131
- const visible: Org[] = useMemo(() => {
132
- if (orgs) return filterOrgs(rows, query)
133
- return [{ name: currentId, displayName: titleCase(currentId) }]
134
- }, [orgs, rows, query, currentId])
135
-
136
- const current: Org = useMemo(
137
- () => rows.find((o) => o.name === currentId) ?? { name: currentId, displayName: titleCase(currentId) },
138
- [rows, currentId],
139
- )
140
-
141
- const select = useCallback(
142
- (org: string) => {
143
- setOpen(false)
144
- scope.switchOrg(org)
145
- },
146
- [scope],
147
- )
148
-
149
- const submitCreate = async () => {
150
- const name = newName.trim()
151
- if (!name || !create) return
152
- setBusy(true)
153
- setErr(null)
154
- try {
155
- const org = await create(name)
156
- scope.switchOrg(org)
157
- } catch (e) {
158
- setErr(e instanceof Error ? e.message : 'Could not create the organization.')
159
- setBusy(false)
160
- }
161
- }
162
-
163
- return (
164
- <Popover open={open} onOpenChange={setOpen} placement="bottom-start">
165
- <Popover.Trigger asChild>
166
- <Button size="$2" chromeless icon={<OrgAvatar org={current} size={18} />} iconAfter={<ChevronsUpDown size={13} />}>
167
- {current.displayName || titleCase(current.name)}
168
- </Button>
169
- </Popover.Trigger>
170
- <Popover.Content bordered elevate p="$2" width={300} bg="$color2" borderColor="$borderColor">
171
- {creating ? (
172
- <YStack gap="$2">
173
- <Text fontSize="$2" color="$color12" fontWeight="700">
174
- Create organization
175
- </Text>
176
- <Input
177
- size="$3"
178
- placeholder="Organization name"
179
- value={newName}
180
- onChangeText={setNewName}
181
- autoCapitalize="words"
182
- onSubmitEditing={() => void submitCreate()}
183
- />
184
- {err ? (
185
- <Text fontSize="$1" color="$red10">
186
- {err}
187
- </Text>
188
- ) : null}
189
- <XStack gap="$2" justify="flex-end">
190
- <Button size="$2" chromeless onPress={() => setCreating(false)} disabled={busy}>
191
- Cancel
192
- </Button>
193
- <Button
194
- size="$2"
195
- onPress={() => void submitCreate()}
196
- disabled={busy || !newName.trim()}
197
- icon={busy ? <Spinner size="small" /> : <Plus size={14} />}
198
- >
199
- Create
200
- </Button>
201
- </XStack>
202
- </YStack>
203
- ) : (
204
- <YStack gap="$1">
205
- {orgs ? (
206
- <XStack items="center" gap="$2" px="$2" py="$1" rounded="$3" borderWidth={1} borderColor="$borderColor">
207
- <Search size={13} opacity={0.6} />
208
- <Input
209
- flex={1}
210
- size="$2"
211
- borderWidth={0}
212
- bg="transparent"
213
- placeholder="Find organization…"
214
- value={query}
215
- onChangeText={setQuery}
216
- autoCapitalize="none"
217
- autoCorrect={false}
218
- />
219
- </XStack>
220
- ) : null}
221
-
222
- <div
223
- style={{ maxHeight: 300, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}
224
- onScroll={(e) => {
225
- const el = e.currentTarget
226
- if (el.scrollHeight - el.scrollTop - el.clientHeight < 48) loadMore()
227
- }}
228
- >
229
- {loading ? (
230
- <XStack items="center" gap="$2" px="$2" py="$3">
231
- <Spinner size="small" color="$color11" />
232
- <Text fontSize="$2" color="$color10">
233
- Loading organizations…
234
- </Text>
235
- </XStack>
236
- ) : visible.length === 0 ? (
237
- <Text px="$2" py="$2" fontSize="$2" color="$color10">
238
- {query.trim() ? `No organizations match “${query.trim()}”.` : 'No organizations yet.'}
239
- </Text>
240
- ) : (
241
- visible.map((org) => (
242
- <XStack
243
- key={org.name}
244
- onPress={() => select(org.name)}
245
- cursor="pointer"
246
- items="center"
247
- gap="$2.5"
248
- px="$2"
249
- py="$2"
250
- rounded="$3"
251
- bg={org.name === currentId ? '$color4' : 'transparent'}
252
- hoverStyle={{ bg: '$color5' }}
253
- >
254
- <OrgAvatar org={org} />
255
- <Text flex={1} fontSize="$2" color="$color12" numberOfLines={1}>
256
- {org.displayName || org.name}
257
- </Text>
258
- {org.name === currentId ? <Check size={15} /> : null}
259
- </XStack>
260
- ))
261
- )}
262
-
263
- {loadingMore ? (
264
- <XStack items="center" gap="$2" px="$2" py="$2">
265
- <Spinner size="small" color="$color11" />
266
- <Text fontSize="$1" color="$color10">
267
- Loading more…
268
- </Text>
269
- </XStack>
270
- ) : hasMore ? (
271
- <XStack onPress={loadMore} cursor="pointer" items="center" justify="center" px="$2" py="$1.5" rounded="$3" hoverStyle={{ bg: '$color4' }}>
272
- <Text fontSize="$1" color="$color11" fontWeight="600">
273
- Load more
274
- </Text>
275
- </XStack>
276
- ) : null}
277
- </div>
278
-
279
- {create ? (
280
- <XStack
281
- onPress={() => {
282
- setCreating(true)
283
- setErr(null)
284
- }}
285
- cursor="pointer"
286
- items="center"
287
- gap="$2.5"
288
- px="$2"
289
- py="$2"
290
- mt="$1"
291
- rounded="$3"
292
- borderTopWidth={1}
293
- borderColor="$borderColor"
294
- hoverStyle={{ bg: '$color5' }}
295
- >
296
- <YStack width={22} height={22} rounded="$3" bg="$color3" items="center" justify="center">
297
- <Plus size={14} />
298
- </YStack>
299
- <Text fontSize="$2" color="$color12">
300
- Create organization
301
- </Text>
302
- </XStack>
303
- ) : null}
304
-
305
- {picker ? (
306
- <XStack
307
- onPress={() => {
308
- setOpen(false)
309
- scope.leaveOrg()
310
- }}
311
- cursor="pointer"
312
- items="center"
313
- gap="$2.5"
314
- px="$2"
315
- py="$2"
316
- rounded="$3"
317
- hoverStyle={{ bg: '$color5' }}
318
- >
319
- <YStack width={22} height={22} rounded="$3" bg="$color3" items="center" justify="center">
320
- <LayoutGrid size={14} />
321
- </YStack>
322
- <Text fontSize="$2" color="$color12">
323
- All organizations
324
- </Text>
325
- </XStack>
326
- ) : null}
327
- </YStack>
328
- )}
329
- </Popover.Content>
330
- </Popover>
331
- )
332
- }
@@ -1,31 +0,0 @@
1
- 'use client'
2
-
3
- import type { ReactNode } from 'react'
4
- import { Text, XStack, YStack } from '@hanzo/gui'
5
-
6
- /** Section title + optional subtitle and right-aligned actions. */
7
- export function PageHeader({
8
- title,
9
- subtitle,
10
- actions,
11
- }: {
12
- title: string
13
- subtitle?: string
14
- actions?: ReactNode
15
- }) {
16
- return (
17
- <XStack justify="space-between" items="flex-start" gap="$4">
18
- <YStack gap="$1">
19
- <Text fontSize="$7" fontWeight="800">
20
- {title}
21
- </Text>
22
- {subtitle ? (
23
- <Text fontSize="$3" color="$color11">
24
- {subtitle}
25
- </Text>
26
- ) : null}
27
- </YStack>
28
- {actions ? <XStack gap="$2">{actions}</XStack> : null}
29
- </XStack>
30
- )
31
- }
@@ -1,17 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * Primary button — the one white, high-emphasis action for the console.
5
- *
6
- * Monochrome brand: `theme="light"` flips the button to the light theme inside
7
- * the dark console, giving a white fill with a near-black label and icon — no
8
- * hue accent. Use it for the single primary action in a view (sign in, save,
9
- * get started). Secondary and destructive actions use the default neutral
10
- * `Button`.
11
- */
12
- import type { ComponentProps } from 'react'
13
- import { Button } from '@hanzo/gui'
14
-
15
- export function PrimaryButton(props: ComponentProps<typeof Button>) {
16
- return <Button theme="light" {...props} />
17
- }
@@ -1,61 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * ProductIcon — the Linear-style product tile: a rounded-square block filled with
5
- * the product's accent color, with the product glyph knocked out in near-white so
6
- * it reads "cut out" of the color. This is the ONE way a product icon renders in
7
- * the console chrome (sidebar nav, command palette, product overview header) — a
8
- * bare tinted glyph is replaced everywhere by this tile.
9
- *
10
- * Prop-driven and pure: the accent hex is resolved by the caller (registry-curated
11
- * → per-user override → stable hash, via `useProductColors().colorOf`) and passed
12
- * in as `color`, so this component owns only the look, never the palette. With no
13
- * color it falls back to a tasteful neutral chip ($color12 fill + $color1 glyph) —
14
- * never a random hue — matching the first-party `ProviderLogo` mark, so first-party
15
- * marks and product tiles read as one family.
16
- *
17
- * The knock-out is the same technique the color-swatch button uses (a raw-hex
18
- * `backgroundColor` fill + a `#ffffff` glyph) so an arbitrary product accent tints
19
- * cleanly outside Gui's token palette, in the ONE sanctioned place.
20
- */
21
- import { XStack } from '@hanzo/gui'
22
-
23
- import { asColor, type IconLike } from './color'
24
-
25
- export function ProductIcon({
26
- icon: Icon,
27
- color,
28
- size = 24,
29
- }: {
30
- icon: IconLike
31
- /** The product's resolved accent hex (e.g. `#5E6AD2`). Omit for the neutral chip. */
32
- color?: string
33
- size?: number
34
- }) {
35
- const radius = Math.round(size * 0.28)
36
- const glyph = Math.round(size * 0.58)
37
- const tinted = typeof color === 'string' && color.trim() !== ''
38
-
39
- // Accent tile — product color fill + near-white glyph (the Linear "cut out").
40
- if (tinted) {
41
- return (
42
- <XStack
43
- width={size}
44
- height={size}
45
- items="center"
46
- justify="center"
47
- rounded={radius}
48
- style={{ backgroundColor: color }}
49
- >
50
- <Icon size={glyph} color={asColor('#ffffff')} />
51
- </XStack>
52
- )
53
- }
54
-
55
- // No accent — a tasteful neutral chip (consistent with ProviderLogo's mark).
56
- return (
57
- <XStack width={size} height={size} items="center" justify="center" rounded={radius} bg="$color12">
58
- <Icon size={glyph} color="$color1" />
59
- </XStack>
60
- )
61
- }