@hanzo/ui 8.0.7 → 8.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ThemeToggleNext-7727UR5W.js +4 -0
- package/dist/ThemeToggleNext-7727UR5W.js.map +1 -0
- package/dist/ThemeToggleNext-ICB5KL5J.cjs +19 -0
- package/dist/ThemeToggleNext-ICB5KL5J.cjs.map +1 -0
- package/dist/billing.cjs +15 -0
- package/dist/billing.cjs.map +1 -0
- package/{types → dist}/billing.d.ts +0 -1
- package/dist/billing.js +4 -0
- package/dist/billing.js.map +1 -0
- package/dist/canvas.cjs +15 -0
- package/dist/canvas.cjs.map +1 -0
- package/dist/canvas.d.ts +1 -0
- package/dist/canvas.js +4 -0
- package/dist/canvas.js.map +1 -0
- package/{types → dist}/cd/layout.d.ts +0 -1
- package/{types → dist}/cd/status.d.ts +0 -1
- package/{types → dist}/cd/time.d.ts +0 -1
- package/{types → dist}/cd/types.d.ts +0 -1
- package/dist/chunk-42CU5GSF.js +98 -0
- package/dist/chunk-42CU5GSF.js.map +1 -0
- package/dist/chunk-GDGVXU7W.js +1999 -0
- package/dist/chunk-GDGVXU7W.js.map +1 -0
- package/dist/chunk-HSPMQQVJ.cjs +2066 -0
- package/dist/chunk-HSPMQQVJ.cjs.map +1 -0
- package/dist/chunk-YBXVUV7P.cjs +103 -0
- package/dist/chunk-YBXVUV7P.cjs.map +1 -0
- package/dist/dashboard.cjs +15 -0
- package/dist/dashboard.cjs.map +1 -0
- package/dist/dashboard.d.ts +1 -0
- package/dist/dashboard.js +4 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/data.cjs +15 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.ts +1 -0
- package/dist/data.js +4 -0
- package/dist/data.js.map +1 -0
- package/dist/gitops.cjs +15 -0
- package/dist/gitops.cjs.map +1 -0
- package/dist/gitops.d.ts +1 -0
- package/dist/gitops.js +4 -0
- package/dist/gitops.js.map +1 -0
- package/dist/index.cjs +291 -0
- package/dist/index.cjs.map +1 -0
- package/{types → dist}/index.d.ts +0 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/network.cjs +15 -0
- package/dist/network.cjs.map +1 -0
- package/{types → dist}/network.d.ts +0 -1
- package/dist/network.js +4 -0
- package/dist/network.js.map +1 -0
- package/dist/primitives/bases/data/index.cjs +15 -0
- package/dist/primitives/bases/data/index.cjs.map +1 -0
- package/dist/primitives/bases/data/index.d.ts +1 -0
- package/dist/primitives/bases/data/index.js +4 -0
- package/dist/primitives/bases/data/index.js.map +1 -0
- package/{types → dist}/product/AppHeader.d.ts +0 -1
- package/{types → dist}/product/BrandMark.d.ts +0 -1
- package/{types → dist}/product/Charts.d.ts +0 -1
- package/{types → dist}/product/ComboBox.d.ts +0 -1
- package/{types → dist}/product/DataTable.d.ts +0 -1
- package/{types → dist}/product/Donut.d.ts +0 -1
- package/{types → dist}/product/EmptyState.d.ts +0 -1
- package/{types → dist}/product/FadeIn.d.ts +0 -1
- package/{types → dist}/product/Field.d.ts +0 -1
- package/{types → dist}/product/HanzoMark.d.ts +0 -1
- package/{types → dist}/product/Metric.d.ts +0 -1
- package/{types → dist}/product/OrgSwitcher.d.ts +0 -1
- package/{types → dist}/product/PageHeader.d.ts +0 -1
- package/{types → dist}/product/PrimaryButton.d.ts +0 -1
- package/{types → dist}/product/ProductIcon.d.ts +0 -1
- package/{types → dist}/product/ProviderLogo.d.ts +0 -1
- package/{types → dist}/product/Reorder.d.ts +0 -1
- package/{types → dist}/product/SelectMenu.d.ts +6 -8
- package/{types → dist}/product/SlideOver.d.ts +0 -1
- package/{types → dist}/product/StatusTag.d.ts +0 -1
- package/{types → dist}/product/ThemeToggle.d.ts +0 -1
- package/{types → dist}/product/ThemeToggleNext.d.ts +0 -1
- package/{types → dist}/product/Toast.d.ts +0 -1
- package/{types → dist}/product/color.d.ts +0 -1
- package/{types → dist}/product/combobox/filter.d.ts +0 -1
- package/dist/product/index.cjs +278 -0
- package/dist/product/index.cjs.map +1 -0
- package/{types → dist}/product/index.d.ts +0 -1
- package/dist/product/index.js +5 -0
- package/dist/product/index.js.map +1 -0
- package/{types → dist}/product/menu/ContextMenu.d.ts +3 -6
- package/dist/product/menu/DropdownMenu.d.ts +29 -0
- package/dist/product/menu/FloatingMenu.d.ts +36 -0
- package/{types → dist}/product/menu/index.d.ts +1 -1
- package/{types → dist}/product/menu/items.d.ts +0 -1
- package/{types → dist}/product/menu/portal-theme.d.ts +0 -1
- package/{types → dist}/product/menu/roving.d.ts +0 -1
- package/{types → dist}/product/scope.d.ts +0 -1
- package/{types → dist}/product/surfaces.data.d.ts +0 -1
- package/dist/usage.cjs +15 -0
- package/dist/usage.cjs.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.js +4 -0
- package/dist/usage.js.map +1 -0
- package/dist/wallet.cjs +15 -0
- package/dist/wallet.cjs.map +1 -0
- package/{types → dist}/wallet.d.ts +0 -1
- package/dist/wallet.js +4 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +54 -32
- package/src/billing.ts +0 -9
- package/src/canvas.ts +0 -11
- package/src/cd/layout.ts +0 -184
- package/src/cd/status.ts +0 -158
- package/src/cd/time.ts +0 -52
- package/src/cd/types.ts +0 -318
- package/src/dashboard.ts +0 -8
- package/src/data.ts +0 -12
- package/src/gitops.ts +0 -5
- package/src/gui-env.d.ts +0 -27
- package/src/index.ts +0 -22
- package/src/network.ts +0 -11
- package/src/primitives/bases/data/index.ts +0 -13
- package/src/product/AppHeader.tsx +0 -183
- package/src/product/BrandMark.tsx +0 -27
- package/src/product/Charts.tsx +0 -471
- package/src/product/ComboBox.tsx +0 -127
- package/src/product/DataTable.tsx +0 -99
- package/src/product/Donut.tsx +0 -83
- package/src/product/EmptyState.tsx +0 -129
- package/src/product/FadeIn.tsx +0 -38
- package/src/product/Field.tsx +0 -160
- package/src/product/HanzoMark.tsx +0 -31
- package/src/product/Metric.tsx +0 -253
- package/src/product/OrgSwitcher.tsx +0 -332
- package/src/product/PageHeader.tsx +0 -31
- package/src/product/PrimaryButton.tsx +0 -17
- package/src/product/ProductIcon.tsx +0 -61
- package/src/product/ProviderLogo.tsx +0 -137
- package/src/product/Reorder.test.ts +0 -41
- package/src/product/Reorder.tsx +0 -110
- package/src/product/SelectMenu.tsx +0 -80
- package/src/product/SlideOver.tsx +0 -197
- package/src/product/StatusTag.tsx +0 -49
- package/src/product/ThemeToggle.tsx +0 -135
- package/src/product/ThemeToggleNext.tsx +0 -27
- package/src/product/Toast.tsx +0 -166
- package/src/product/color.ts +0 -17
- package/src/product/combobox/filter.test.ts +0 -51
- package/src/product/combobox/filter.ts +0 -39
- package/src/product/index.ts +0 -76
- package/src/product/menu/ContextMenu.tsx +0 -114
- package/src/product/menu/DropdownMenu.tsx +0 -76
- package/src/product/menu/index.ts +0 -15
- package/src/product/menu/items.tsx +0 -230
- package/src/product/menu/portal-theme.tsx +0 -33
- package/src/product/menu/roving.ts +0 -34
- package/src/product/scope.test.ts +0 -92
- package/src/product/scope.ts +0 -123
- package/src/product/surfaces.data.ts +0 -42
- package/src/usage.ts +0 -8
- package/src/wallet.ts +0 -11
- package/types/billing.d.ts.map +0 -1
- package/types/canvas.d.ts +0 -2
- package/types/canvas.d.ts.map +0 -1
- package/types/cd/layout.d.ts.map +0 -1
- package/types/cd/status.d.ts.map +0 -1
- package/types/cd/time.d.ts.map +0 -1
- package/types/cd/types.d.ts.map +0 -1
- package/types/dashboard.d.ts +0 -2
- package/types/dashboard.d.ts.map +0 -1
- package/types/data.d.ts +0 -2
- package/types/data.d.ts.map +0 -1
- package/types/gitops.d.ts +0 -2
- package/types/gitops.d.ts.map +0 -1
- package/types/index.d.ts.map +0 -1
- package/types/network.d.ts.map +0 -1
- package/types/primitives/bases/data/index.d.ts +0 -2
- package/types/primitives/bases/data/index.d.ts.map +0 -1
- package/types/product/AppHeader.d.ts.map +0 -1
- package/types/product/BrandMark.d.ts.map +0 -1
- package/types/product/Charts.d.ts.map +0 -1
- package/types/product/ComboBox.d.ts.map +0 -1
- package/types/product/DataTable.d.ts.map +0 -1
- package/types/product/Donut.d.ts.map +0 -1
- package/types/product/EmptyState.d.ts.map +0 -1
- package/types/product/FadeIn.d.ts.map +0 -1
- package/types/product/Field.d.ts.map +0 -1
- package/types/product/HanzoMark.d.ts.map +0 -1
- package/types/product/Metric.d.ts.map +0 -1
- package/types/product/OrgSwitcher.d.ts.map +0 -1
- package/types/product/PageHeader.d.ts.map +0 -1
- package/types/product/PrimaryButton.d.ts.map +0 -1
- package/types/product/ProductIcon.d.ts.map +0 -1
- package/types/product/ProviderLogo.d.ts.map +0 -1
- package/types/product/Reorder.d.ts.map +0 -1
- package/types/product/SelectMenu.d.ts.map +0 -1
- package/types/product/SlideOver.d.ts.map +0 -1
- package/types/product/StatusTag.d.ts.map +0 -1
- package/types/product/ThemeToggle.d.ts.map +0 -1
- package/types/product/ThemeToggleNext.d.ts.map +0 -1
- package/types/product/Toast.d.ts.map +0 -1
- package/types/product/color.d.ts.map +0 -1
- package/types/product/combobox/filter.d.ts.map +0 -1
- package/types/product/index.d.ts.map +0 -1
- package/types/product/menu/ContextMenu.d.ts.map +0 -1
- package/types/product/menu/DropdownMenu.d.ts +0 -34
- package/types/product/menu/DropdownMenu.d.ts.map +0 -1
- package/types/product/menu/index.d.ts.map +0 -1
- package/types/product/menu/items.d.ts.map +0 -1
- package/types/product/menu/portal-theme.d.ts.map +0 -1
- package/types/product/menu/roving.d.ts.map +0 -1
- package/types/product/scope.d.ts.map +0 -1
- package/types/product/surfaces.data.d.ts.map +0 -1
- package/types/usage.d.ts +0 -2
- package/types/usage.d.ts.map +0 -1
- package/types/wallet.d.ts.map +0 -1
- /package/{src → dist}/styles/hanzo-motion.css +0 -0
|
@@ -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
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ProviderLogo — a small provider/model avatar resolved from a provider NAME
|
|
5
|
-
* alone (no external logo URLs, no network). Self-contained and prop-driven so
|
|
6
|
-
* it lifts cleanly into `@hanzo/ui` for hanzo.ai / @hanzo/dev / the desktop app.
|
|
7
|
-
*
|
|
8
|
-
* Resolution order:
|
|
9
|
-
* 1. First-party — Zen renders the real ensō mark, Hanzo the real block-H mark
|
|
10
|
-
* (same geometry as @zenlm/logo / @hanzo/logo), knocked out of a filled
|
|
11
|
-
* rounded tile so our own models read distinctly and on-brand.
|
|
12
|
-
* 2. A known provider → its mapped @hanzogui/lucide-icons-2 glyph.
|
|
13
|
-
* 3. Otherwise → a stable initials chip (1–2 letters from the name).
|
|
14
|
-
*
|
|
15
|
-
* Honest by construction: we never claim an official brand logo we don't ship —
|
|
16
|
-
* unknown providers get clean initials, not a guessed icon. Colors are @hanzo/gui
|
|
17
|
-
* theme tokens so the mark themes with the shell.
|
|
18
|
-
*/
|
|
19
|
-
import { Text, XStack, useTheme } from '@hanzo/gui'
|
|
20
|
-
import { Boxes, Server, Globe, Cpu } from '@hanzogui/lucide-icons-2'
|
|
21
|
-
|
|
22
|
-
type IconCmp = typeof Globe
|
|
23
|
-
|
|
24
|
-
/** Curated, extendable provider→glyph map (lowercased keys). Brand-neutral avatars. */
|
|
25
|
-
const KNOWN: Record<string, IconCmp> = {
|
|
26
|
-
openrouter: Globe,
|
|
27
|
-
nvidia: Cpu,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const isFirstParty = (provider: string): boolean => {
|
|
31
|
-
const p = provider.trim().toLowerCase()
|
|
32
|
-
return p === 'hanzo' || p === 'zen'
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The Zen ensō mark — identical geometry to @zenlm/logo (svg/zen-enso.svg). */
|
|
36
|
-
function EnsoMark({ size, color }: { size: number; color: string }) {
|
|
37
|
-
return (
|
|
38
|
-
<svg width={size} height={size} viewBox="0 0 100 100" aria-hidden="true">
|
|
39
|
-
<path d="M66.22 83.26 A37 37 0 1 1 85.57 60.20" fill="none" stroke={color} strokeWidth={11} strokeLinecap="round" />
|
|
40
|
-
</svg>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The Hanzo block-H mark — identical geometry to @hanzo/logo (app/icon.svg). */
|
|
45
|
-
const HANZO_PATHS = [
|
|
46
|
-
'M22.21 67V44.6369H0V67H22.21Z',
|
|
47
|
-
'M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z',
|
|
48
|
-
'M22.21 0H0V22.3184H22.21V0Z',
|
|
49
|
-
'M66.7198 0H44.5098V22.3184H66.7198V0Z',
|
|
50
|
-
'M66.7198 67V44.6369H44.5098V67H66.7198Z',
|
|
51
|
-
]
|
|
52
|
-
function HanzoHMark({ size, color }: { size: number; color: string }) {
|
|
53
|
-
return (
|
|
54
|
-
<svg width={size} height={size} viewBox="0 0 67 67" aria-hidden="true">
|
|
55
|
-
{HANZO_PATHS.map((d) => (
|
|
56
|
-
<path key={d} d={d} fill={color} />
|
|
57
|
-
))}
|
|
58
|
-
</svg>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** 1–2 uppercase initials from a provider name: words→first letters, else first 2 chars. */
|
|
63
|
-
export function providerInitials(provider: string): string {
|
|
64
|
-
const name = provider.trim()
|
|
65
|
-
if (!name) return '•'
|
|
66
|
-
const words = name.split(/[\s/_-]+/).filter(Boolean)
|
|
67
|
-
if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase()
|
|
68
|
-
return name.slice(0, 2).toUpperCase()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function ProviderLogo({ provider, size = 24 }: { provider: string; size?: number }) {
|
|
72
|
-
const theme = useTheme()
|
|
73
|
-
const radius = Math.round(size * 0.28)
|
|
74
|
-
const iconSize = Math.round(size * 0.56)
|
|
75
|
-
|
|
76
|
-
// First-party (Zen/Hanzo) — the real brand mark knocked out of a filled tile.
|
|
77
|
-
if (isFirstParty(provider)) {
|
|
78
|
-
const fg = theme.color1?.get() ?? '#000000' // cut-out mark: the tile's contrast color
|
|
79
|
-
const markSize = Math.round(size * 0.66)
|
|
80
|
-
const isZen = provider.trim().toLowerCase() === 'zen'
|
|
81
|
-
return (
|
|
82
|
-
<XStack width={size} height={size} items="center" justify="center" rounded={radius} bg="$color12">
|
|
83
|
-
{isZen ? <EnsoMark size={markSize} color={fg} /> : <HanzoHMark size={Math.round(size * 0.56)} color={fg} />}
|
|
84
|
-
</XStack>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// A known provider with a curated, brand-neutral glyph.
|
|
89
|
-
const Known = KNOWN[provider.trim().toLowerCase()]
|
|
90
|
-
if (Known) {
|
|
91
|
-
return (
|
|
92
|
-
<XStack
|
|
93
|
-
width={size}
|
|
94
|
-
height={size}
|
|
95
|
-
items="center"
|
|
96
|
-
justify="center"
|
|
97
|
-
rounded={radius}
|
|
98
|
-
bg="$color3"
|
|
99
|
-
borderWidth={1}
|
|
100
|
-
borderColor="$borderColor"
|
|
101
|
-
>
|
|
102
|
-
<Known size={iconSize} color="$color11" />
|
|
103
|
-
</XStack>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Otherwise — a clean, stable initials chip (no fabricated brand logo).
|
|
108
|
-
return (
|
|
109
|
-
<XStack
|
|
110
|
-
width={size}
|
|
111
|
-
height={size}
|
|
112
|
-
items="center"
|
|
113
|
-
justify="center"
|
|
114
|
-
rounded={radius}
|
|
115
|
-
bg="$color3"
|
|
116
|
-
borderWidth={1}
|
|
117
|
-
borderColor="$borderColor"
|
|
118
|
-
>
|
|
119
|
-
<Text fontSize={Math.round(size * 0.4)} fontWeight="800" color="$color11">
|
|
120
|
-
{providerInitials(provider)}
|
|
121
|
-
</Text>
|
|
122
|
-
</XStack>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** A generic fallback mark for an unspecified provider (used sparingly). */
|
|
127
|
-
export function GenericLogo({ size = 24 }: { size?: number }) {
|
|
128
|
-
return (
|
|
129
|
-
<XStack width={size} height={size} items="center" justify="center" rounded={Math.round(size * 0.28)} bg="$color3" borderWidth={1} borderColor="$borderColor">
|
|
130
|
-
<Server size={Math.round(size * 0.56)} color="$color11" />
|
|
131
|
-
</XStack>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Boxes is re-exported as the conventional "custom model" mark for callers that
|
|
136
|
-
// render a non-provider tile next to ProviderLogo (keeps the icon source single).
|
|
137
|
-
export { Boxes as CustomModelMark }
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { rowShift, targetIndex } from './Reorder'
|
|
4
|
-
|
|
5
|
-
describe('targetIndex', () => {
|
|
6
|
-
it('rounds travel to the nearest row and clamps to range', () => {
|
|
7
|
-
// rowH=44, from=2, drag down 100px → +2.27 → +2 → index 4
|
|
8
|
-
expect(targetIndex(2, 100, 44, 6)).toBe(4)
|
|
9
|
-
// drag up 100px → −2 → index 0
|
|
10
|
-
expect(targetIndex(2, -100, 44, 6)).toBe(0)
|
|
11
|
-
// clamp at the ends
|
|
12
|
-
expect(targetIndex(5, 999, 44, 6)).toBe(5)
|
|
13
|
-
expect(targetIndex(0, -999, 44, 6)).toBe(0)
|
|
14
|
-
// sub-half-row travel stays put
|
|
15
|
-
expect(targetIndex(3, 20, 44, 6)).toBe(3)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('is a safe no-op for degenerate inputs', () => {
|
|
19
|
-
expect(targetIndex(2, 100, 0, 6)).toBe(2)
|
|
20
|
-
expect(targetIndex(2, 100, 44, 0)).toBe(2)
|
|
21
|
-
})
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('rowShift — displacement of siblings during a drag', () => {
|
|
25
|
-
const rowH = 44
|
|
26
|
-
it('the dragged row follows the pointer', () => {
|
|
27
|
-
expect(rowShift(2, 2, 4, 90, rowH)).toBe(90)
|
|
28
|
-
})
|
|
29
|
-
it('rows between from and to (dragging down) move up by one row', () => {
|
|
30
|
-
// from=1 → to=3: rows 2,3 shift up
|
|
31
|
-
expect(rowShift(2, 1, 3, 90, rowH)).toBe(-rowH)
|
|
32
|
-
expect(rowShift(3, 1, 3, 90, rowH)).toBe(-rowH)
|
|
33
|
-
expect(rowShift(4, 1, 3, 90, rowH)).toBe(0)
|
|
34
|
-
})
|
|
35
|
-
it('rows between to and from (dragging up) move down by one row', () => {
|
|
36
|
-
// from=4 → to=2: rows 2,3 shift down
|
|
37
|
-
expect(rowShift(2, 4, 2, -90, rowH)).toBe(rowH)
|
|
38
|
-
expect(rowShift(3, 4, 2, -90, rowH)).toBe(rowH)
|
|
39
|
-
expect(rowShift(1, 4, 2, -90, rowH)).toBe(0)
|
|
40
|
-
})
|
|
41
|
-
})
|