@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.
- 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 +7 -6
- package/{types → dist}/product/SlideOver.d.ts +0 -1
- package/{types → dist}/product/StatusTag.d.ts +0 -1
- package/dist/product/ThemeToggle.d.ts +21 -0
- package/dist/product/ThemeToggleNext.d.ts +4 -0
- 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 +2 -1
- package/dist/product/index.js +5 -0
- package/dist/product/index.js.map +1 -0
- package/dist/product/menu/ContextMenu.d.ts +21 -0
- package/dist/product/menu/DropdownMenu.d.ts +29 -0
- package/dist/product/menu/FloatingMenu.d.ts +36 -0
- package/dist/product/menu/index.d.ts +5 -0
- package/dist/product/menu/items.d.ts +68 -0
- package/dist/product/menu/portal-theme.d.ts +28 -0
- package/dist/product/menu/roving.d.ts +8 -0
- 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 +58 -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 -147
- 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 -94
- package/src/product/SlideOver.tsx +0 -197
- package/src/product/StatusTag.tsx +0 -49
- package/src/product/ThemeToggle.tsx +0 -25
- 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 -70
- 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 +0 -2
- package/types/product/ThemeToggle.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/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,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
|
-
})
|
package/src/product/Reorder.tsx
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Reorder — a tiny pointer-driven drag-to-reorder list. No dependency: it uses
|
|
5
|
-
* Pointer Events (so it works with mouse AND touch) and CSS transforms, matching
|
|
6
|
-
* the shell's transform-driven motion. Rows are a FIXED height (all pinned rows
|
|
7
|
-
* are), which keeps the "where does it drop?" math exact and pure (`targetIndex`)
|
|
8
|
-
* and the reflow buttery — the lifted row follows the pointer 1:1 while its
|
|
9
|
-
* displaced siblings ease into place.
|
|
10
|
-
*
|
|
11
|
-
* The list is controlled: `onReorder(from, to)` fires once on drop; the parent
|
|
12
|
-
* owns the data (here, the account-backed pins model). `renderItem` gets a
|
|
13
|
-
* `handle` to spread on the drag affordance so the rest of the row stays
|
|
14
|
-
* interactive (tap to open, etc.).
|
|
15
|
-
*/
|
|
16
|
-
import { useCallback, useRef, useState, type ReactNode } from 'react'
|
|
17
|
-
import { YStack } from '@hanzo/gui'
|
|
18
|
-
|
|
19
|
-
/** Final index for a drag: start + rounded rows travelled, clamped. PURE. */
|
|
20
|
-
export function targetIndex(from: number, dy: number, rowH: number, count: number): number {
|
|
21
|
-
if (rowH <= 0 || count <= 0) return from
|
|
22
|
-
const raw = from + Math.round(dy / rowH)
|
|
23
|
-
return Math.max(0, Math.min(raw, count - 1))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Vertical shift for row `i` during a drag from `from`→`to` (the dragged row
|
|
27
|
-
* follows the pointer; rows between them make room). PURE. */
|
|
28
|
-
export function rowShift(i: number, from: number, to: number, dy: number, rowH: number): number {
|
|
29
|
-
if (i === from) return dy
|
|
30
|
-
if (from < to && i > from && i <= to) return -rowH
|
|
31
|
-
if (from > to && i < from && i >= to) return rowH
|
|
32
|
-
return 0
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type DragHandle = { onPointerDown: (e: { clientY?: number; pointerId?: number; nativeEvent?: { clientY?: number } }) => void }
|
|
36
|
-
|
|
37
|
-
export function Reorder<T>({
|
|
38
|
-
items,
|
|
39
|
-
keyOf,
|
|
40
|
-
rowHeight = 44,
|
|
41
|
-
onReorder,
|
|
42
|
-
renderItem,
|
|
43
|
-
}: {
|
|
44
|
-
items: T[]
|
|
45
|
-
keyOf: (t: T) => string
|
|
46
|
-
rowHeight?: number
|
|
47
|
-
onReorder: (from: number, to: number) => void
|
|
48
|
-
renderItem: (item: T, handle: DragHandle, dragging: boolean) => ReactNode
|
|
49
|
-
}) {
|
|
50
|
-
const [drag, setDrag] = useState<{ from: number; dy: number } | null>(null)
|
|
51
|
-
const startRef = useRef<{ from: number; startY: number } | null>(null)
|
|
52
|
-
|
|
53
|
-
const to = drag ? targetIndex(drag.from, drag.dy, rowHeight, items.length) : -1
|
|
54
|
-
|
|
55
|
-
const begin = useCallback(
|
|
56
|
-
(index: number) =>
|
|
57
|
-
(e: { clientY?: number; pointerId?: number; nativeEvent?: { clientY?: number } }) => {
|
|
58
|
-
const startY = e.clientY ?? e.nativeEvent?.clientY ?? 0
|
|
59
|
-
startRef.current = { from: index, startY }
|
|
60
|
-
setDrag({ from: index, dy: 0 })
|
|
61
|
-
|
|
62
|
-
const move = (ev: PointerEvent) => {
|
|
63
|
-
if (!startRef.current) return
|
|
64
|
-
setDrag({ from: startRef.current.from, dy: ev.clientY - startRef.current.startY })
|
|
65
|
-
}
|
|
66
|
-
const end = () => {
|
|
67
|
-
const s = startRef.current
|
|
68
|
-
window.removeEventListener('pointermove', move)
|
|
69
|
-
window.removeEventListener('pointerup', end)
|
|
70
|
-
window.removeEventListener('pointercancel', end)
|
|
71
|
-
startRef.current = null
|
|
72
|
-
setDrag((d) => {
|
|
73
|
-
if (s && d) {
|
|
74
|
-
const t = targetIndex(s.from, d.dy, rowHeight, items.length)
|
|
75
|
-
if (t !== s.from) onReorder(s.from, t)
|
|
76
|
-
}
|
|
77
|
-
return null
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
window.addEventListener('pointermove', move)
|
|
81
|
-
window.addEventListener('pointerup', end)
|
|
82
|
-
window.addEventListener('pointercancel', end)
|
|
83
|
-
},
|
|
84
|
-
[items.length, rowHeight, onReorder],
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<YStack>
|
|
89
|
-
{items.map((item, i) => {
|
|
90
|
-
const dragging = drag?.from === i
|
|
91
|
-
const shift = drag && to >= 0 ? rowShift(i, drag.from, to, drag.dy, rowHeight) : 0
|
|
92
|
-
return (
|
|
93
|
-
<YStack
|
|
94
|
-
key={keyOf(item)}
|
|
95
|
-
height={rowHeight}
|
|
96
|
-
justify="center"
|
|
97
|
-
className="hz-drag-item"
|
|
98
|
-
style={{
|
|
99
|
-
transform: `translateY(${shift}px)`,
|
|
100
|
-
zIndex: dragging ? 2 : 1,
|
|
101
|
-
...(dragging ? { transition: 'none', boxShadow: '0 8px 24px rgba(0,0,0,0.28)' } : null),
|
|
102
|
-
}}
|
|
103
|
-
>
|
|
104
|
-
{renderItem(item, { onPointerDown: begin(i) }, dragging)}
|
|
105
|
-
</YStack>
|
|
106
|
-
)
|
|
107
|
-
})}
|
|
108
|
-
</YStack>
|
|
109
|
-
)
|
|
110
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SelectMenu — a compact, reusable dropdown select on @hanzo/gui Popover (the
|
|
5
|
-
* console's proven dropdown idiom, same as OrgSwitcher/ScopeSwitcher). Renders a
|
|
6
|
-
* labelled trigger ("All types", or the chosen option) and a popover list with a
|
|
7
|
-
* check on the active option. Prop-driven + self-contained so it lifts into
|
|
8
|
-
* `@hanzo/ui`. `value === null` is the "all"/unfiltered state.
|
|
9
|
-
*/
|
|
10
|
-
import type { ReactElement } from 'react'
|
|
11
|
-
import { useState } from 'react'
|
|
12
|
-
import { Button, Popover, Text, XStack, YStack } from '@hanzo/gui'
|
|
13
|
-
import { ChevronDown, Check } from '@hanzogui/lucide-icons-2'
|
|
14
|
-
|
|
15
|
-
export type SelectOption<T extends string> = { key: T; label: string }
|
|
16
|
-
|
|
17
|
-
export function SelectMenu<T extends string>({
|
|
18
|
-
options,
|
|
19
|
-
value,
|
|
20
|
-
onChange,
|
|
21
|
-
allLabel = 'All',
|
|
22
|
-
icon,
|
|
23
|
-
minWidth = 130,
|
|
24
|
-
}: {
|
|
25
|
-
options: SelectOption<T>[]
|
|
26
|
-
value: T | null
|
|
27
|
-
onChange: (v: T | null) => void
|
|
28
|
-
/** Label shown (and first menu row) for the unfiltered `null` state. */
|
|
29
|
-
allLabel?: string
|
|
30
|
-
/** Optional leading icon in the trigger. */
|
|
31
|
-
icon?: ReactElement
|
|
32
|
-
minWidth?: number
|
|
33
|
-
}) {
|
|
34
|
-
const [open, setOpen] = useState(false)
|
|
35
|
-
const active = value === null ? null : options.find((o) => o.key === value) ?? null
|
|
36
|
-
const triggerLabel = active ? active.label : allLabel
|
|
37
|
-
|
|
38
|
-
const pick = (v: T | null) => {
|
|
39
|
-
onChange(v)
|
|
40
|
-
setOpen(false)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<Popover open={open} onOpenChange={setOpen} placement="bottom-start">
|
|
45
|
-
<Popover.Trigger asChild>
|
|
46
|
-
<Button
|
|
47
|
-
size="$2"
|
|
48
|
-
minW={minWidth}
|
|
49
|
-
justify="space-between"
|
|
50
|
-
borderWidth={1}
|
|
51
|
-
borderColor="$borderColor"
|
|
52
|
-
bg={value !== null ? '$color5' : 'transparent'}
|
|
53
|
-
icon={icon}
|
|
54
|
-
iconAfter={<ChevronDown size={14} opacity={0.6} />}
|
|
55
|
-
>
|
|
56
|
-
<Text fontSize="$2" color="$color12" numberOfLines={1} flex={1}>
|
|
57
|
-
{triggerLabel}
|
|
58
|
-
</Text>
|
|
59
|
-
</Button>
|
|
60
|
-
</Popover.Trigger>
|
|
61
|
-
<Popover.Content bordered elevate p="$1.5" minW={minWidth} bg="$color2" borderColor="$borderColor">
|
|
62
|
-
<YStack gap="$0.5" minW={minWidth} maxH={320} overflow="scroll">
|
|
63
|
-
<Row label={allLabel} active={value === null} onPress={() => pick(null)} />
|
|
64
|
-
{options.map((o) => (
|
|
65
|
-
<Row key={o.key} label={o.label} active={value === o.key} onPress={() => pick(o.key)} />
|
|
66
|
-
))}
|
|
67
|
-
</YStack>
|
|
68
|
-
</Popover.Content>
|
|
69
|
-
</Popover>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function Row({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
|
|
74
|
-
return (
|
|
75
|
-
<XStack
|
|
76
|
-
items="center"
|
|
77
|
-
gap="$2"
|
|
78
|
-
px="$2.5"
|
|
79
|
-
py="$1.5"
|
|
80
|
-
rounded="$3"
|
|
81
|
-
cursor="pointer"
|
|
82
|
-
hoverStyle={{ bg: '$color4' }}
|
|
83
|
-
bg={active ? '$color4' : 'transparent'}
|
|
84
|
-
onPress={onPress}
|
|
85
|
-
>
|
|
86
|
-
<XStack width={14} items="center" justify="center">
|
|
87
|
-
{active ? <Check size={13} /> : null}
|
|
88
|
-
</XStack>
|
|
89
|
-
<Text fontSize="$2" color="$color12" flex={1} numberOfLines={1}>
|
|
90
|
-
{label}
|
|
91
|
-
</Text>
|
|
92
|
-
</XStack>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SlideOver — the ONE right-side (or left) slide-out overlay for the console.
|
|
5
|
-
*
|
|
6
|
-
* It is the single mechanism behind every off-canvas surface: the mobile nav
|
|
7
|
-
* drawer, the item DetailPane, and the account/settings menu. One definition, many
|
|
8
|
-
* mounts (DRY) — so every drawer slides, dims, traps, and closes identically.
|
|
9
|
-
*
|
|
10
|
-
* Smoothness is CSS-transform driven (never a JS layout branch): the panel is
|
|
11
|
-
* ALWAYS mounted and toggles `translateX(0) ↔ translateX(±100%)` via the shared
|
|
12
|
-
* `.hz-slide` class, and the backdrop cross-fades via `.hz-fade`. Because it never
|
|
13
|
-
* unmounts, the EXIT animates too (a `Dialog` that unmounts on close cannot). Both
|
|
14
|
-
* classes are reduced-motion-aware in `globals.css`, so the whole thing snaps
|
|
15
|
-
* instantly for users who ask for that — no bespoke logic here.
|
|
16
|
-
*
|
|
17
|
-
* Responsive by construction: FULL-WIDTH (full-screen) below `lg`, a fixed-width
|
|
18
|
-
* pane at `lg+` (`size`). Uses `100dvh` so the mobile browser chrome never clips it.
|
|
19
|
-
*
|
|
20
|
-
* A11y: `role="dialog" aria-modal`, Escape to close, backdrop click to close, body
|
|
21
|
-
* scroll lock while open (ref-counted so stacked overlays don't fight), and focus
|
|
22
|
-
* moves into the panel on open and returns to the opener on close.
|
|
23
|
-
*/
|
|
24
|
-
import { useEffect, useRef, type ReactNode } from 'react'
|
|
25
|
-
import { Button, ScrollView, Text, XStack, YStack } from '@hanzo/gui'
|
|
26
|
-
import { X } from '@hanzogui/lucide-icons-2'
|
|
27
|
-
|
|
28
|
-
import { asColor, type IconLike } from './color'
|
|
29
|
-
|
|
30
|
-
/** The lg breakpoint (px) — matches the shell's one responsive threshold. */
|
|
31
|
-
const LG = 1024
|
|
32
|
-
|
|
33
|
-
// ── Body scroll lock, ref-counted so nested/stacked overlays don't clobber it ──
|
|
34
|
-
let lockCount = 0
|
|
35
|
-
let savedOverflow = ''
|
|
36
|
-
function lockScroll() {
|
|
37
|
-
if (typeof document === 'undefined') return
|
|
38
|
-
if (lockCount === 0) {
|
|
39
|
-
savedOverflow = document.body.style.overflow
|
|
40
|
-
document.body.style.overflow = 'hidden'
|
|
41
|
-
}
|
|
42
|
-
lockCount++
|
|
43
|
-
}
|
|
44
|
-
function unlockScroll() {
|
|
45
|
-
if (typeof document === 'undefined') return
|
|
46
|
-
lockCount = Math.max(0, lockCount - 1)
|
|
47
|
-
if (lockCount === 0) document.body.style.overflow = savedOverflow
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type SlideOverProps = {
|
|
51
|
-
open: boolean
|
|
52
|
-
onClose: () => void
|
|
53
|
-
/** Which edge the panel slides from. Default right. */
|
|
54
|
-
side?: 'right' | 'left'
|
|
55
|
-
/** Panel width at lg+ (px). Below lg it is always full-screen. Default 420. */
|
|
56
|
-
size?: number
|
|
57
|
-
/** Optional header title — when set, SlideOver renders a header + a scroll body. */
|
|
58
|
-
title?: ReactNode
|
|
59
|
-
/** Optional header icon (tinted with `iconColor`). */
|
|
60
|
-
icon?: IconLike
|
|
61
|
-
iconColor?: string
|
|
62
|
-
/** Extra header content, right-aligned before the close button. */
|
|
63
|
-
headerRight?: ReactNode
|
|
64
|
-
/** Accessible label when no visible title is given. */
|
|
65
|
-
ariaLabel?: string
|
|
66
|
-
/** z-index for stacking (a pane opened over a drawer passes a higher value). */
|
|
67
|
-
zIndex?: number
|
|
68
|
-
children: ReactNode
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function SlideOver({
|
|
72
|
-
open,
|
|
73
|
-
onClose,
|
|
74
|
-
side = 'right',
|
|
75
|
-
size = 420,
|
|
76
|
-
title,
|
|
77
|
-
icon: Icon,
|
|
78
|
-
iconColor,
|
|
79
|
-
headerRight,
|
|
80
|
-
ariaLabel,
|
|
81
|
-
zIndex = 1000,
|
|
82
|
-
children,
|
|
83
|
-
}: SlideOverProps) {
|
|
84
|
-
const panelRef = useRef<HTMLElement | null>(null)
|
|
85
|
-
const openerRef = useRef<Element | null>(null)
|
|
86
|
-
|
|
87
|
-
// Body scroll lock + focus management, driven by `open`.
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (!open) return
|
|
90
|
-
openerRef.current = typeof document !== 'undefined' ? document.activeElement : null
|
|
91
|
-
lockScroll()
|
|
92
|
-
// Move focus into the panel (next frame so it is laid out + on-screen).
|
|
93
|
-
const t = window.setTimeout(() => {
|
|
94
|
-
const el = panelRef.current
|
|
95
|
-
if (el && typeof el.focus === 'function') el.focus()
|
|
96
|
-
}, 0)
|
|
97
|
-
return () => {
|
|
98
|
-
window.clearTimeout(t)
|
|
99
|
-
unlockScroll()
|
|
100
|
-
const opener = openerRef.current
|
|
101
|
-
if (opener && opener instanceof HTMLElement && typeof opener.focus === 'function') opener.focus()
|
|
102
|
-
}
|
|
103
|
-
}, [open])
|
|
104
|
-
|
|
105
|
-
// Escape closes.
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (!open) return
|
|
108
|
-
const onKey = (e: KeyboardEvent) => {
|
|
109
|
-
if (e.key === 'Escape') {
|
|
110
|
-
e.stopPropagation()
|
|
111
|
-
onClose()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
window.addEventListener('keydown', onKey)
|
|
115
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
116
|
-
}, [open, onClose])
|
|
117
|
-
|
|
118
|
-
const offscreen = side === 'right' ? 'translateX(100%)' : 'translateX(-100%)'
|
|
119
|
-
const edge = side === 'right' ? { r: 0 } : { l: 0 }
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<YStack
|
|
123
|
-
// Fixed to the viewport; overflow hidden so the off-screen panel (absolute
|
|
124
|
-
// child) is CLIPPED and never adds a horizontal scrollbar.
|
|
125
|
-
overflow="hidden"
|
|
126
|
-
pointerEvents={open ? 'auto' : 'none'}
|
|
127
|
-
aria-hidden={!open}
|
|
128
|
-
style={{ position: 'fixed', inset: 0, zIndex }}
|
|
129
|
-
>
|
|
130
|
-
{/* Backdrop — cross-fades; click to dismiss. */}
|
|
131
|
-
<YStack
|
|
132
|
-
position="absolute"
|
|
133
|
-
t={0}
|
|
134
|
-
l={0}
|
|
135
|
-
r={0}
|
|
136
|
-
b={0}
|
|
137
|
-
bg="rgba(0,0,0,0.55)"
|
|
138
|
-
className="hz-fade"
|
|
139
|
-
style={{ opacity: open ? 1 : 0 }}
|
|
140
|
-
onPress={onClose}
|
|
141
|
-
/>
|
|
142
|
-
|
|
143
|
-
{/* Panel — slides in from the edge. Full-screen below lg, fixed width at lg+. */}
|
|
144
|
-
<YStack
|
|
145
|
-
// @ts-expect-error — web DOM ref; Gui forwards it to the underlying node.
|
|
146
|
-
ref={panelRef}
|
|
147
|
-
tabIndex={-1}
|
|
148
|
-
role="dialog"
|
|
149
|
-
aria-modal={open ? true : undefined}
|
|
150
|
-
aria-label={typeof title === 'string' ? title : ariaLabel}
|
|
151
|
-
position="absolute"
|
|
152
|
-
t={0}
|
|
153
|
-
b={0}
|
|
154
|
-
{...edge}
|
|
155
|
-
width="100%"
|
|
156
|
-
$lg={{ width: size, maxW: '100vw' }}
|
|
157
|
-
bg="$color1"
|
|
158
|
-
borderLeftWidth={side === 'right' ? 1 : 0}
|
|
159
|
-
borderRightWidth={side === 'left' ? 1 : 0}
|
|
160
|
-
borderColor="$borderColor"
|
|
161
|
-
className="hz-slide"
|
|
162
|
-
style={{ transform: open ? 'translateX(0)' : offscreen, height: '100dvh' }}
|
|
163
|
-
>
|
|
164
|
-
{title !== undefined ? (
|
|
165
|
-
<>
|
|
166
|
-
<XStack
|
|
167
|
-
items="center"
|
|
168
|
-
gap="$2.5"
|
|
169
|
-
px="$4"
|
|
170
|
-
height={56}
|
|
171
|
-
borderBottomWidth={1}
|
|
172
|
-
borderColor="$borderColor"
|
|
173
|
-
>
|
|
174
|
-
{Icon ? <Icon size={18} color={iconColor ? asColor(iconColor) : undefined} /> : null}
|
|
175
|
-
<Text flex={1} fontSize="$5" fontWeight="700" color="$color12" numberOfLines={1}>
|
|
176
|
-
{title}
|
|
177
|
-
</Text>
|
|
178
|
-
{headerRight}
|
|
179
|
-
<Button size="$2" chromeless icon={<X size={18} />} onPress={onClose} aria-label="Close" />
|
|
180
|
-
</XStack>
|
|
181
|
-
<ScrollView flex={1}>
|
|
182
|
-
<YStack flex={1} p="$4" gap="$3">
|
|
183
|
-
{children}
|
|
184
|
-
</YStack>
|
|
185
|
-
</ScrollView>
|
|
186
|
-
</>
|
|
187
|
-
) : (
|
|
188
|
-
// Bare mode — the caller owns the full panel layout (e.g. the nav drawer
|
|
189
|
-
// renders its own header + scroll + footer).
|
|
190
|
-
children
|
|
191
|
-
)}
|
|
192
|
-
</YStack>
|
|
193
|
-
</YStack>
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export { LG as SLIDEOVER_LG }
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Status pill — maps a resource/cluster status string to a tone, one place.
|
|
5
|
-
*
|
|
6
|
-
* Provisioned things (managed resources, clusters) report free-form lifecycle
|
|
7
|
-
* strings; this normalizes them to green/yellow/red/neutral so every list reads
|
|
8
|
-
* the same. Style props use the v5 shorthand set.
|
|
9
|
-
*/
|
|
10
|
-
import { Text } from '@hanzo/gui'
|
|
11
|
-
|
|
12
|
-
type Tone = 'green' | 'yellow' | 'red' | 'neutral'
|
|
13
|
-
|
|
14
|
-
const toneOf = (status: string): Tone => {
|
|
15
|
-
const s = status.toLowerCase()
|
|
16
|
-
// Platform health verdicts map straight to a tone (the apps inventory reports
|
|
17
|
-
// green/yellow/red directly).
|
|
18
|
-
if (s === 'green') return 'green'
|
|
19
|
-
if (s === 'yellow') return 'yellow'
|
|
20
|
-
if (s === 'red') return 'red'
|
|
21
|
-
if (['ready', 'active', 'running', 'available', 'ok'].includes(s)) return 'green'
|
|
22
|
-
if (['creating', 'provisioning', 'pending', 'updating', 'attaching'].includes(s)) return 'yellow'
|
|
23
|
-
if (['error', 'failed', 'degraded', 'down'].includes(s)) return 'red'
|
|
24
|
-
return 'neutral'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// `as const` keeps the literal token types (not widened to `string`) so they
|
|
28
|
-
// satisfy the GUI `bg`/`color` token unions.
|
|
29
|
-
const TONE_BG = {
|
|
30
|
-
green: '$color5',
|
|
31
|
-
yellow: '$color4',
|
|
32
|
-
red: '$color4',
|
|
33
|
-
neutral: '$color3',
|
|
34
|
-
} as const
|
|
35
|
-
const TONE_FG = {
|
|
36
|
-
green: '$color12',
|
|
37
|
-
yellow: '$color12',
|
|
38
|
-
red: '$color12',
|
|
39
|
-
neutral: '$color11',
|
|
40
|
-
} as const
|
|
41
|
-
|
|
42
|
-
export function StatusTag({ status }: { status?: string }) {
|
|
43
|
-
const tone = toneOf(status ?? '')
|
|
44
|
-
return (
|
|
45
|
-
<Text fontSize="$1" px="$2" py="$1" rounded="$2" bg={TONE_BG[tone]} color={TONE_FG[tone]}>
|
|
46
|
-
{status || 'unknown'}
|
|
47
|
-
</Text>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Theme toggle — flips the console between dark and light via next-theme. The
|
|
5
|
-
* provider already wires `NextThemeProvider` → `GuiProvider`, so setting the
|
|
6
|
-
* theme here re-themes the whole tree. Shows the action's target icon (a sun in
|
|
7
|
-
* dark mode, a moon in light mode).
|
|
8
|
-
*/
|
|
9
|
-
import { Button } from '@hanzo/gui'
|
|
10
|
-
import { useThemeSetting } from '@hanzogui/next-theme'
|
|
11
|
-
import { Moon, Sun } from '@hanzogui/lucide-icons-2'
|
|
12
|
-
|
|
13
|
-
export function ThemeToggle() {
|
|
14
|
-
const { current, resolvedTheme, set } = useThemeSetting()
|
|
15
|
-
const isDark = (resolvedTheme ?? current ?? 'dark') !== 'light'
|
|
16
|
-
return (
|
|
17
|
-
<Button
|
|
18
|
-
size="$2"
|
|
19
|
-
chromeless
|
|
20
|
-
icon={isDark ? <Sun size={16} /> : <Moon size={16} />}
|
|
21
|
-
onPress={() => set(isDark ? 'light' : 'dark')}
|
|
22
|
-
aria-label={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
23
|
-
/>
|
|
24
|
-
)
|
|
25
|
-
}
|