@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.
- 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-M2ILMK44.cjs +2104 -0
- package/dist/chunk-M2ILMK44.cjs.map +1 -0
- package/dist/chunk-YBXVUV7P.cjs +103 -0
- package/dist/chunk-YBXVUV7P.cjs.map +1 -0
- package/dist/chunk-YSURJUOA.js +2035 -0
- package/dist/chunk-YSURJUOA.js.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 +299 -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/oss/blueprint.d.ts +37 -0
- package/dist/oss/catalog.d.ts +67 -0
- package/dist/oss.cjs +190 -0
- package/dist/oss.cjs.map +1 -0
- package/dist/oss.d.ts +2 -0
- package/dist/oss.js +180 -0
- package/dist/oss.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/dist/product/OrgMark.d.ts +21 -0
- package/{types → dist}/product/OrgSwitcher.d.ts +8 -2
- 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 +286 -0
- package/dist/product/index.cjs.map +1 -0
- package/{types → dist}/product/index.d.ts +1 -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/telemetry.cjs +15 -0
- package/dist/telemetry.cjs.map +1 -0
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.js +4 -0
- package/dist/telemetry.js.map +1 -0
- 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 +69 -34
- 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
package/src/product/Toast.tsx
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Toast — the ONE feedback primitive. Every mutation reports through `useToast()`
|
|
5
|
-
* so success/failure looks and behaves the same everywhere. A single provider
|
|
6
|
-
* holds the queue and renders the viewport (portalled top-right); there is no
|
|
7
|
-
* per-call markup. Built from GUI primitives only.
|
|
8
|
-
*
|
|
9
|
-
* Mount `<ToastProvider>` once (the dashboard layout does). Call:
|
|
10
|
-
* const toast = useToast()
|
|
11
|
-
* toast.success('Created my-db')
|
|
12
|
-
* toast.error(err instanceof ApiError ? err.message : 'Failed to create')
|
|
13
|
-
*/
|
|
14
|
-
import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
15
|
-
import { createPortal } from 'react-dom'
|
|
16
|
-
import { Button, Card, Text, XStack, YStack } from '@hanzo/gui'
|
|
17
|
-
import { CircleCheck, CircleX, Info, X } from '@hanzogui/lucide-icons-2'
|
|
18
|
-
|
|
19
|
-
export type ToastKind = 'success' | 'error' | 'info'
|
|
20
|
-
|
|
21
|
-
export type ToastInput = {
|
|
22
|
-
title: string
|
|
23
|
-
description?: string
|
|
24
|
-
kind?: ToastKind
|
|
25
|
-
/** Auto-dismiss after this many ms; 0 keeps it until dismissed. */
|
|
26
|
-
durationMs?: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type Toast = Required<Omit<ToastInput, 'description'>> & { id: number; description?: string }
|
|
30
|
-
|
|
31
|
-
type ToastApi = {
|
|
32
|
-
toast: (t: ToastInput) => void
|
|
33
|
-
success: (title: string, description?: string) => void
|
|
34
|
-
error: (title: string, description?: string) => void
|
|
35
|
-
info: (title: string, description?: string) => void
|
|
36
|
-
dismiss: (id: number) => void
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const ToastContext = createContext<ToastApi | null>(null)
|
|
40
|
-
|
|
41
|
-
/** Theme-aware accent per kind (adapts to light/dark). */
|
|
42
|
-
const ACCENT = {
|
|
43
|
-
success: '$green10',
|
|
44
|
-
error: '$red10',
|
|
45
|
-
info: '$color11',
|
|
46
|
-
} as const
|
|
47
|
-
|
|
48
|
-
const ICON: Record<ToastKind, typeof Info> = {
|
|
49
|
-
success: CircleCheck,
|
|
50
|
-
error: CircleX,
|
|
51
|
-
info: Info,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function ToastCard({ t, onClose }: { t: Toast; onClose: () => void }) {
|
|
55
|
-
const Icon = ICON[t.kind]
|
|
56
|
-
const accent = ACCENT[t.kind]
|
|
57
|
-
return (
|
|
58
|
-
<Card
|
|
59
|
-
width={360}
|
|
60
|
-
maxWidth="90%"
|
|
61
|
-
p="$3"
|
|
62
|
-
gap="$2"
|
|
63
|
-
bg="$color2"
|
|
64
|
-
borderWidth={1}
|
|
65
|
-
borderColor="$borderColor"
|
|
66
|
-
borderLeftWidth={3}
|
|
67
|
-
borderLeftColor={accent}
|
|
68
|
-
rounded="$4"
|
|
69
|
-
elevation="$2"
|
|
70
|
-
>
|
|
71
|
-
<XStack gap="$2.5" items="flex-start">
|
|
72
|
-
<YStack pt={1}>
|
|
73
|
-
<Icon size={18} color={accent} />
|
|
74
|
-
</YStack>
|
|
75
|
-
<YStack flex={1} gap="$1">
|
|
76
|
-
<Text fontSize="$3" fontWeight="700" color="$color12">
|
|
77
|
-
{t.title}
|
|
78
|
-
</Text>
|
|
79
|
-
{t.description ? (
|
|
80
|
-
<Text fontSize="$2" color="$color11">
|
|
81
|
-
{t.description}
|
|
82
|
-
</Text>
|
|
83
|
-
) : null}
|
|
84
|
-
</YStack>
|
|
85
|
-
<Button size="$1" chromeless icon={<X size={14} />} onPress={onClose} aria-label="Dismiss" />
|
|
86
|
-
</XStack>
|
|
87
|
-
</Card>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Portalled, fixed top-right stack. Rendered only after mount (SSR-safe). */
|
|
92
|
-
function ToastViewport({ toasts, dismiss }: { toasts: Toast[]; dismiss: (id: number) => void }) {
|
|
93
|
-
const [mounted, setMounted] = useState(false)
|
|
94
|
-
useEffect(() => setMounted(true), [])
|
|
95
|
-
if (!mounted || typeof document === 'undefined') return null
|
|
96
|
-
return createPortal(
|
|
97
|
-
<div style={{ position: 'fixed', top: 16, right: 16, zIndex: 100000, pointerEvents: 'none' }}>
|
|
98
|
-
<YStack gap="$2" items="flex-end">
|
|
99
|
-
{toasts.map((t) => (
|
|
100
|
-
<YStack key={t.id} pointerEvents="auto">
|
|
101
|
-
<ToastCard t={t} onClose={() => dismiss(t.id)} />
|
|
102
|
-
</YStack>
|
|
103
|
-
))}
|
|
104
|
-
</YStack>
|
|
105
|
-
</div>,
|
|
106
|
-
document.body,
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
111
|
-
const [toasts, setToasts] = useState<Toast[]>([])
|
|
112
|
-
const timers = useRef(new Map<number, ReturnType<typeof setTimeout>>())
|
|
113
|
-
const seq = useRef(0)
|
|
114
|
-
|
|
115
|
-
const dismiss = useCallback((id: number) => {
|
|
116
|
-
setToasts((ts) => ts.filter((t) => t.id !== id))
|
|
117
|
-
const timer = timers.current.get(id)
|
|
118
|
-
if (timer) {
|
|
119
|
-
clearTimeout(timer)
|
|
120
|
-
timers.current.delete(id)
|
|
121
|
-
}
|
|
122
|
-
}, [])
|
|
123
|
-
|
|
124
|
-
const toast = useCallback(
|
|
125
|
-
(input: ToastInput) => {
|
|
126
|
-
const id = ++seq.current
|
|
127
|
-
const kind = input.kind ?? 'info'
|
|
128
|
-
const durationMs = input.durationMs ?? (kind === 'error' ? 6000 : 3500)
|
|
129
|
-
setToasts((ts) => [...ts, { id, kind, title: input.title, description: input.description, durationMs }])
|
|
130
|
-
if (durationMs > 0) {
|
|
131
|
-
timers.current.set(
|
|
132
|
-
id,
|
|
133
|
-
setTimeout(() => dismiss(id), durationMs),
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
[dismiss],
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
// Clear any pending timers on unmount.
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
const map = timers.current
|
|
143
|
-
return () => map.forEach((t) => clearTimeout(t))
|
|
144
|
-
}, [])
|
|
145
|
-
|
|
146
|
-
const api: ToastApi = {
|
|
147
|
-
toast,
|
|
148
|
-
success: (title, description) => toast({ title, description, kind: 'success' }),
|
|
149
|
-
error: (title, description) => toast({ title, description, kind: 'error' }),
|
|
150
|
-
info: (title, description) => toast({ title, description, kind: 'info' }),
|
|
151
|
-
dismiss,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<ToastContext.Provider value={api}>
|
|
156
|
-
{children}
|
|
157
|
-
<ToastViewport toasts={toasts} dismiss={dismiss} />
|
|
158
|
-
</ToastContext.Provider>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function useToast(): ToastApi {
|
|
163
|
-
const ctx = useContext(ToastContext)
|
|
164
|
-
if (!ctx) throw new Error('useToast must be used within <ToastProvider>')
|
|
165
|
-
return ctx
|
|
166
|
-
}
|
package/src/product/color.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ComponentType } from 'react'
|
|
2
|
-
import type { GetThemeValueForKey } from '@hanzo/gui'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Bridge between raw CSS colors (product hex accents) and Gui's typed `color`
|
|
6
|
-
* prop. Gui types `color` as a theme-token template (`$color12`, …), but ANY CSS
|
|
7
|
-
* color is valid at runtime — so tinting an icon with a product hex needs one
|
|
8
|
-
* sanctioned cast, in ONE place, rather than `as any` scattered around.
|
|
9
|
-
*/
|
|
10
|
-
export type IconColor = GetThemeValueForKey<'color'>
|
|
11
|
-
|
|
12
|
-
/** Cast a raw CSS color (e.g. `#5E6AD2`) to Gui's `color` prop type. */
|
|
13
|
-
export const asColor = (hex: string): IconColor => hex as unknown as IconColor
|
|
14
|
-
|
|
15
|
-
/** A lucide/Gui icon component that accepts a size + a (token-or-cast) color —
|
|
16
|
-
* the shape the shell passes around for product/nav icons. */
|
|
17
|
-
export type IconLike = ComponentType<{ size?: number; color?: IconColor }>
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { filterOptions, isKnownOption, type ComboOption } from './filter'
|
|
4
|
-
|
|
5
|
-
const opts: ComboOption[] = [
|
|
6
|
-
{ value: 'zen-omni', label: 'Zen Omni', hint: 'hanzo' },
|
|
7
|
-
{ value: 'gpt-4o-mini', label: 'GPT-4o mini', hint: 'openai' },
|
|
8
|
-
{ value: 'claude-sonnet-4-5', hint: 'anthropic' },
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
describe('filterOptions', () => {
|
|
12
|
-
it('returns every option for an empty / whitespace query', () => {
|
|
13
|
-
expect(filterOptions(opts, '')).toHaveLength(3)
|
|
14
|
-
expect(filterOptions(opts, ' ')).toHaveLength(3)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('matches on value (case-insensitive substring)', () => {
|
|
18
|
-
expect(filterOptions(opts, 'ZEN').map((o) => o.value)).toEqual(['zen-omni'])
|
|
19
|
-
expect(filterOptions(opts, 'sonnet').map((o) => o.value)).toEqual(['claude-sonnet-4-5'])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('matches on label', () => {
|
|
23
|
-
expect(filterOptions(opts, 'GPT-4o').map((o) => o.value)).toEqual(['gpt-4o-mini'])
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('matches on hint (the provider)', () => {
|
|
27
|
-
expect(filterOptions(opts, 'anthropic').map((o) => o.value)).toEqual(['claude-sonnet-4-5'])
|
|
28
|
-
expect(filterOptions(opts, 'openai').map((o) => o.value)).toEqual(['gpt-4o-mini'])
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('returns nothing when nothing matches (the typed value is still usable by the caller)', () => {
|
|
32
|
-
expect(filterOptions(opts, 'llama')).toEqual([])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('treats the query as a LITERAL string, never a regex (no ReDoS / injection)', () => {
|
|
36
|
-
// A regex-special query must match literally (or not at all) — never be compiled.
|
|
37
|
-
expect(filterOptions(opts, '.*')).toEqual([]) // no option literally contains ".*"
|
|
38
|
-
expect(filterOptions(opts, '(')).toEqual([]) // an unbalanced paren would throw if compiled
|
|
39
|
-
expect(filterOptions(opts, '[a-z')).toEqual([]) // an invalid char class would throw if compiled
|
|
40
|
-
// A literal hyphen (regex-benign but proves substring semantics):
|
|
41
|
-
expect(filterOptions(opts, '4o-mini').map((o) => o.value)).toEqual(['gpt-4o-mini'])
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('isKnownOption', () => {
|
|
46
|
-
it('is true only for an exact value match (case-sensitive)', () => {
|
|
47
|
-
expect(isKnownOption(opts, 'zen-omni')).toBe(true)
|
|
48
|
-
expect(isKnownOption(opts, 'ZEN-OMNI')).toBe(false)
|
|
49
|
-
expect(isKnownOption(opts, 'custom-model')).toBe(false)
|
|
50
|
-
})
|
|
51
|
-
})
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Combobox option filtering — pure, unit-tested.
|
|
3
|
-
*
|
|
4
|
-
* The ONE matcher the ComboBox uses to narrow a live option list against typed
|
|
5
|
-
* text. Matching is a LITERAL, case-insensitive substring over the option's
|
|
6
|
-
* value + label + hint — NEVER a compiled RegExp of user input (that would be a
|
|
7
|
-
* ReDoS / injection surface; the whole codebase filters with literal `includes`,
|
|
8
|
-
* e.g. marketplace/logic + filterOrgs). Kept dependency-free so it lifts into
|
|
9
|
-
* `@hanzo/ui` alongside the ComboBox.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** One option in a combobox list. `value` is what gets committed; label/hint are display. */
|
|
13
|
-
export type ComboOption = {
|
|
14
|
-
/** The value committed to the field when this option is chosen. */
|
|
15
|
-
value: string
|
|
16
|
-
/** Display label (defaults to `value`). */
|
|
17
|
-
label?: string
|
|
18
|
-
/** Optional secondary text (e.g. a provider, a description) — also searched. */
|
|
19
|
-
hint?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The searchable haystack for one option (value + label + hint), lowercased. */
|
|
23
|
-
const hay = (o: ComboOption): string => `${o.value} ${o.label ?? ''} ${o.hint ?? ''}`.toLowerCase()
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Filter `options` by `query` — a literal, case-insensitive substring over
|
|
27
|
-
* value/label/hint. An empty/whitespace query returns every option (no filter).
|
|
28
|
-
* PURE. The input is treated as a plain string, never a pattern.
|
|
29
|
-
*/
|
|
30
|
-
export function filterOptions(options: ComboOption[], query: string): ComboOption[] {
|
|
31
|
-
const q = query.trim().toLowerCase()
|
|
32
|
-
if (!q) return options
|
|
33
|
-
return options.filter((o) => hay(o).includes(q))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** True iff `value` exactly matches an option's `value` (case-sensitive). */
|
|
37
|
-
export function isKnownOption(options: ComboOption[], value: string): boolean {
|
|
38
|
-
return options.some((o) => o.value === value)
|
|
39
|
-
}
|
package/src/product/index.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// @hanzo/ui/product — the product/app component layer on @hanzo/gui.
|
|
2
|
-
//
|
|
3
|
-
// Charts, metrics, page headers, status tags, rich empty states, combobox,
|
|
4
|
-
// slide-over, toasts, drag-reorder, labeled field rows, provider/product marks,
|
|
5
|
-
// theme toggle. Presentational + host-agnostic: data and effects are INJECTED as
|
|
6
|
-
// props — nothing here imports an app's `~/lib`. Cross-platform (web + native +
|
|
7
|
-
// desktop) because it builds only on @hanzo/gui primitives. Clean-room.
|
|
8
|
-
//
|
|
9
|
-
// Single collision-free namespace: where a name exists in two files the canonical
|
|
10
|
-
// (Charts) keeps the name and the variant is aliased, so NOTHING is dropped.
|
|
11
|
-
|
|
12
|
-
// Charts — the canonical Sparkline + Donut live here (dependency-free inline SVG:
|
|
13
|
-
// hover tooltips, axis ticks, honest null under 2 real points).
|
|
14
|
-
export * from './Charts'
|
|
15
|
-
|
|
16
|
-
// Metric — resource-console stat tiles/panels/bars. Its own near-identical
|
|
17
|
-
// Sparkline is preserved as `MetricSparkline` (zero loss; see CONSOLIDATION.md
|
|
18
|
-
// for the eventual single-Sparkline dedup).
|
|
19
|
-
export {
|
|
20
|
-
SERIES,
|
|
21
|
-
colorForIndex,
|
|
22
|
-
utilColor,
|
|
23
|
-
Sparkline as MetricSparkline,
|
|
24
|
-
MiniBars,
|
|
25
|
-
UtilBar,
|
|
26
|
-
LegendDot,
|
|
27
|
-
MetricCard,
|
|
28
|
-
HintButton,
|
|
29
|
-
Panel,
|
|
30
|
-
} from './Metric'
|
|
31
|
-
|
|
32
|
-
// Standalone dependency-free ring — aliased to avoid the Charts.Donut clash.
|
|
33
|
-
export { Donut as DonutRing, type DonutSegment } from './Donut'
|
|
34
|
-
|
|
35
|
-
// ComboBox — the typeable select. `ComboOption` comes from the ONE pure filter
|
|
36
|
-
// module (ReDoS-safe literal substring), so export the component here and the
|
|
37
|
-
// option type + matchers from there.
|
|
38
|
-
export { ComboBox } from './ComboBox'
|
|
39
|
-
export * from './combobox/filter'
|
|
40
|
-
|
|
41
|
-
// The shared shell — brand mark, org scope + switcher, app header (the
|
|
42
|
-
// console's org-scope contract + switcher hoisted here; hanzoai/ui#36).
|
|
43
|
-
// `surfaces.data` is the ONE canonical cross-surface list every launcher consumes.
|
|
44
|
-
export * from './surfaces.data'
|
|
45
|
-
export * from './AppHeader'
|
|
46
|
-
export * from './BrandMark'
|
|
47
|
-
export * from './OrgSwitcher'
|
|
48
|
-
export * from './scope'
|
|
49
|
-
|
|
50
|
-
// Menu — the ONE menu system: portal-theme-safe DropdownMenu (click) + ContextMenu
|
|
51
|
-
// (right-click), both rendering the SAME item spec (MenuPanel/MenuItemView) so every
|
|
52
|
-
// menu across the fleet is pixel-identical. SelectMenu/ComboBox share the same spec.
|
|
53
|
-
export * from './menu'
|
|
54
|
-
|
|
55
|
-
// The rest — every exported name below is unique across the layer.
|
|
56
|
-
export * from './DataTable'
|
|
57
|
-
export * from './EmptyState'
|
|
58
|
-
export * from './FadeIn'
|
|
59
|
-
export * from './Field'
|
|
60
|
-
export * from './HanzoMark'
|
|
61
|
-
export * from './PageHeader'
|
|
62
|
-
export * from './PrimaryButton'
|
|
63
|
-
export * from './ProductIcon'
|
|
64
|
-
export * from './ProviderLogo'
|
|
65
|
-
export * from './Reorder'
|
|
66
|
-
export * from './SelectMenu'
|
|
67
|
-
export * from './SlideOver'
|
|
68
|
-
export * from './StatusTag'
|
|
69
|
-
export * from './ThemeToggle'
|
|
70
|
-
export { ThemeToggleNext } from './ThemeToggleNext'
|
|
71
|
-
export * from './Toast'
|
|
72
|
-
export * from './color'
|
|
73
|
-
|
|
74
|
-
// Usage — the canonical AI-usage surface (totals, chart, spend-by-model, activity)
|
|
75
|
-
// now lives in ONE home: `@hanzo/usage/panel` (<UsagePanel>). The former meter/card/
|
|
76
|
-
// dashboard kit here was orphaned (zero consumers) and folded into it.
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ContextMenu — a right-click menu that renders the SAME shared item spec as
|
|
5
|
-
* DropdownMenu, so every menu in the fleet is identical. Built on the @hanzo/gui
|
|
6
|
-
* Portal with the SAME portal-theme fix as the dropdown: the menu is portaled to the
|
|
7
|
-
* root, escaping any nested `<Theme>`, so it re-applies the captured theme via
|
|
8
|
-
* <PortalTheme>. Web/desktop right-click (`onContextMenu`); on native — which has no
|
|
9
|
-
* right-click — the wrapped child renders untouched.
|
|
10
|
-
*
|
|
11
|
-
* <ContextMenu items={[{ key:'copy', label:'Copy', icon:<Copy size={16}/>, onSelect:copy }]}>
|
|
12
|
-
* <YStack>right-click me</YStack>
|
|
13
|
-
* </ContextMenu>
|
|
14
|
-
*/
|
|
15
|
-
import type { ReactElement, MouseEvent } from 'react'
|
|
16
|
-
import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
17
|
-
import { Portal } from '@hanzo/gui'
|
|
18
|
-
import { MenuPanel, renderMenuItems, type MenuItemSpec } from './items'
|
|
19
|
-
import { PortalTheme, useThemeName } from './portal-theme'
|
|
20
|
-
import { menuKeyDown } from './roving'
|
|
21
|
-
|
|
22
|
-
const EDGE = 8 // keep this far from the viewport edge
|
|
23
|
-
|
|
24
|
-
export type ContextMenuProps = {
|
|
25
|
-
/** The right-clickable target. Its `onContextMenu` is composed, not replaced. */
|
|
26
|
-
children: ReactElement
|
|
27
|
-
items: MenuItemSpec[]
|
|
28
|
-
disabled?: boolean
|
|
29
|
-
minWidth?: number
|
|
30
|
-
maxHeight?: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function ContextMenu({ children, items, disabled, minWidth = 200, maxHeight }: ContextMenuProps) {
|
|
34
|
-
const themeName = useThemeName()
|
|
35
|
-
const [state, setState] = useState<{ open: boolean; x: number; y: number }>({ open: false, x: 0, y: 0 })
|
|
36
|
-
const panelRef = useRef<HTMLElement | null>(null)
|
|
37
|
-
|
|
38
|
-
const close = useCallback(() => setState((s) => (s.open ? { ...s, open: false } : s)), [])
|
|
39
|
-
|
|
40
|
-
const openAt = (e: MouseEvent) => {
|
|
41
|
-
if (disabled) return
|
|
42
|
-
e.preventDefault()
|
|
43
|
-
e.stopPropagation()
|
|
44
|
-
setState({ open: true, x: e.clientX, y: e.clientY })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Edge-flip before paint: clamp so the panel stays fully in the viewport.
|
|
48
|
-
useLayoutEffect(() => {
|
|
49
|
-
if (!state.open || typeof window === 'undefined') return
|
|
50
|
-
const node = panelRef.current
|
|
51
|
-
if (!node) return
|
|
52
|
-
const r = node.getBoundingClientRect()
|
|
53
|
-
let x = state.x
|
|
54
|
-
let y = state.y
|
|
55
|
-
if (x + r.width > window.innerWidth - EDGE) x = Math.max(EDGE, window.innerWidth - r.width - EDGE)
|
|
56
|
-
if (y + r.height > window.innerHeight - EDGE) y = Math.max(EDGE, window.innerHeight - r.height - EDGE)
|
|
57
|
-
if (x !== state.x || y !== state.y) setState((s) => ({ ...s, x, y }))
|
|
58
|
-
// Focus the panel so keyboard nav works immediately.
|
|
59
|
-
node.focus?.()
|
|
60
|
-
}, [state.open, state.x, state.y])
|
|
61
|
-
|
|
62
|
-
// Dismiss on outside pointer, Escape, scroll, resize, blur.
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (!state.open || typeof document === 'undefined') return
|
|
65
|
-
const onPointerDown = (e: PointerEvent) => {
|
|
66
|
-
if (panelRef.current && !panelRef.current.contains(e.target as Node)) close()
|
|
67
|
-
}
|
|
68
|
-
const onKey = (e: KeyboardEvent) => {
|
|
69
|
-
if (e.key === 'Escape') close()
|
|
70
|
-
}
|
|
71
|
-
document.addEventListener('pointerdown', onPointerDown, true)
|
|
72
|
-
document.addEventListener('keydown', onKey, true)
|
|
73
|
-
window.addEventListener('scroll', close, true)
|
|
74
|
-
window.addEventListener('resize', close)
|
|
75
|
-
window.addEventListener('blur', close)
|
|
76
|
-
return () => {
|
|
77
|
-
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
78
|
-
document.removeEventListener('keydown', onKey, true)
|
|
79
|
-
window.removeEventListener('scroll', close, true)
|
|
80
|
-
window.removeEventListener('resize', close)
|
|
81
|
-
window.removeEventListener('blur', close)
|
|
82
|
-
}
|
|
83
|
-
}, [state.open, close])
|
|
84
|
-
|
|
85
|
-
const childOnContextMenu = (children.props as { onContextMenu?: (e: MouseEvent) => void }).onContextMenu
|
|
86
|
-
const target = cloneElement(children, {
|
|
87
|
-
onContextMenu: (e: MouseEvent) => {
|
|
88
|
-
childOnContextMenu?.(e)
|
|
89
|
-
openAt(e)
|
|
90
|
-
},
|
|
91
|
-
} as Partial<typeof children.props>)
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<>
|
|
95
|
-
{target}
|
|
96
|
-
{state.open ? (
|
|
97
|
-
<Portal>
|
|
98
|
-
<PortalTheme name={themeName}>
|
|
99
|
-
<MenuPanel
|
|
100
|
-
panelRef={(n) => (panelRef.current = n)}
|
|
101
|
-
minWidth={minWidth}
|
|
102
|
-
maxHeight={maxHeight}
|
|
103
|
-
tabIndex={-1}
|
|
104
|
-
onKeyDown={(e) => menuKeyDown(e, close)}
|
|
105
|
-
style={{ position: 'fixed', left: state.x, top: state.y, zIndex: 100000 }}
|
|
106
|
-
>
|
|
107
|
-
{renderMenuItems(items, close)}
|
|
108
|
-
</MenuPanel>
|
|
109
|
-
</PortalTheme>
|
|
110
|
-
</Portal>
|
|
111
|
-
) : null}
|
|
112
|
-
</>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* DropdownMenu — a portal-theme-safe dropdown on the @hanzo/gui Popover (the proven
|
|
5
|
-
* console idiom, same shell as SelectMenu/ComboBox). Declarative `items`; renders the
|
|
6
|
-
* ONE shared menu-item spec so it is pixel-identical to every other menu. Opens under
|
|
7
|
-
* a nested `<Theme>` correctly because the portaled content re-applies the captured
|
|
8
|
-
* theme (see PortalTheme).
|
|
9
|
-
*
|
|
10
|
-
* <DropdownMenu
|
|
11
|
-
* trigger={<Button>Actions</Button>}
|
|
12
|
-
* items={[
|
|
13
|
-
* { key: 'rename', label: 'Rename', icon: <Pencil size={16} />, onSelect: rename },
|
|
14
|
-
* { type: 'separator' },
|
|
15
|
-
* { key: 'del', label: 'Delete', destructive: true, onSelect: del },
|
|
16
|
-
* ]}
|
|
17
|
-
* />
|
|
18
|
-
*/
|
|
19
|
-
import type { ReactElement } from 'react'
|
|
20
|
-
import { Popover, useControllableState } from '@hanzo/gui'
|
|
21
|
-
import { MenuPanel, renderMenuItems, type MenuItemSpec } from './items'
|
|
22
|
-
import { PortalTheme, useThemeName } from './portal-theme'
|
|
23
|
-
import { menuKeyDown } from './roving'
|
|
24
|
-
|
|
25
|
-
export type DropdownMenuProps = {
|
|
26
|
-
/** The clickable element. Cloned as the Popover trigger (`asChild`). */
|
|
27
|
-
trigger: ReactElement
|
|
28
|
-
items: MenuItemSpec[]
|
|
29
|
-
/** Controlled open state. Omit for uncontrolled. */
|
|
30
|
-
open?: boolean
|
|
31
|
-
defaultOpen?: boolean
|
|
32
|
-
onOpenChange?: (open: boolean) => void
|
|
33
|
-
/** Popover placement (default `bottom-start`). */
|
|
34
|
-
placement?: React.ComponentProps<typeof Popover>['placement']
|
|
35
|
-
minWidth?: number
|
|
36
|
-
maxHeight?: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function DropdownMenu({
|
|
40
|
-
trigger,
|
|
41
|
-
items,
|
|
42
|
-
open,
|
|
43
|
-
defaultOpen = false,
|
|
44
|
-
onOpenChange,
|
|
45
|
-
placement = 'bottom-start',
|
|
46
|
-
minWidth = 200,
|
|
47
|
-
maxHeight,
|
|
48
|
-
}: DropdownMenuProps) {
|
|
49
|
-
const [isOpen, setOpen] = useControllableState({
|
|
50
|
-
prop: open,
|
|
51
|
-
defaultProp: defaultOpen,
|
|
52
|
-
onChange: onOpenChange,
|
|
53
|
-
})
|
|
54
|
-
// Capture the resolved theme HERE, where theme context is still available; the
|
|
55
|
-
// Popover.Content portals out of it and re-applies it via <PortalTheme>.
|
|
56
|
-
const themeName = useThemeName()
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<Popover open={isOpen} onOpenChange={setOpen} placement={placement} allowFlip>
|
|
60
|
-
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
|
|
61
|
-
<Popover.Content
|
|
62
|
-
// The visible surface is our MenuPanel — keep Content a transparent shell.
|
|
63
|
-
bg="transparent"
|
|
64
|
-
borderWidth={0}
|
|
65
|
-
p={0}
|
|
66
|
-
elevation={0}
|
|
67
|
-
>
|
|
68
|
-
<PortalTheme name={themeName}>
|
|
69
|
-
<MenuPanel minWidth={minWidth} maxHeight={maxHeight} onKeyDown={(e) => menuKeyDown(e, () => setOpen(false))}>
|
|
70
|
-
{renderMenuItems(items, () => setOpen(false))}
|
|
71
|
-
</MenuPanel>
|
|
72
|
-
</PortalTheme>
|
|
73
|
-
</Popover.Content>
|
|
74
|
-
</Popover>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// @hanzo/ui/product menu — ONE menu system. Portal-theme-safe DropdownMenu (click)
|
|
2
|
-
// and ContextMenu (right-click) both render the SAME item spec, so every menu across
|
|
3
|
-
// the fleet is pixel-identical. See items.tsx for the shared spec.
|
|
4
|
-
|
|
5
|
-
export { DropdownMenu, type DropdownMenuProps } from './DropdownMenu'
|
|
6
|
-
export { ContextMenu, type ContextMenuProps } from './ContextMenu'
|
|
7
|
-
export {
|
|
8
|
-
MenuPanel,
|
|
9
|
-
MenuItemView,
|
|
10
|
-
MenuSeparatorView,
|
|
11
|
-
MenuLabelView,
|
|
12
|
-
renderMenuItems,
|
|
13
|
-
type MenuItemSpec,
|
|
14
|
-
} from './items'
|
|
15
|
-
export { PortalTheme } from './portal-theme'
|