@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
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,80 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SelectMenu — a compact, reusable dropdown select on @hanzo/gui Popover. Renders a
|
|
5
|
-
* labelled trigger ("All types", or the chosen option) and a menu list with a check on
|
|
6
|
-
* the active option. Prop-driven + self-contained. `value === null` is the
|
|
7
|
-
* "all"/unfiltered state.
|
|
8
|
-
*
|
|
9
|
-
* Uses the ONE shared menu spec (MenuPanel + MenuItemView) so it is pixel-identical to
|
|
10
|
-
* DropdownMenu/ContextMenu, and the SAME portal-theme fix (PortalTheme) so it renders
|
|
11
|
-
* correctly through the portal under a nested `<Theme>`, light or dark.
|
|
12
|
-
*/
|
|
13
|
-
import type { ReactElement } from 'react'
|
|
14
|
-
import { useState } from 'react'
|
|
15
|
-
import { Button, Popover, Text } from '@hanzo/gui'
|
|
16
|
-
import { ChevronDown } from '@hanzogui/lucide-icons-2'
|
|
17
|
-
import { MenuItemView, MenuPanel } from './menu/items'
|
|
18
|
-
import { PortalTheme, useThemeName } from './menu/portal-theme'
|
|
19
|
-
import { menuKeyDown } from './menu/roving'
|
|
20
|
-
|
|
21
|
-
export type SelectOption<T extends string> = { key: T; label: string }
|
|
22
|
-
|
|
23
|
-
export function SelectMenu<T extends string>({
|
|
24
|
-
options,
|
|
25
|
-
value,
|
|
26
|
-
onChange,
|
|
27
|
-
allLabel = 'All',
|
|
28
|
-
icon,
|
|
29
|
-
minWidth = 130,
|
|
30
|
-
}: {
|
|
31
|
-
options: SelectOption<T>[]
|
|
32
|
-
value: T | null
|
|
33
|
-
onChange: (v: T | null) => void
|
|
34
|
-
/** Label shown (and first menu row) for the unfiltered `null` state. */
|
|
35
|
-
allLabel?: string
|
|
36
|
-
/** Optional leading icon in the trigger. */
|
|
37
|
-
icon?: ReactElement
|
|
38
|
-
minWidth?: number
|
|
39
|
-
}) {
|
|
40
|
-
const [open, setOpen] = useState(false)
|
|
41
|
-
const themeName = useThemeName()
|
|
42
|
-
const active = value === null ? null : options.find((o) => o.key === value) ?? null
|
|
43
|
-
const triggerLabel = active ? active.label : allLabel
|
|
44
|
-
|
|
45
|
-
const pick = (v: T | null) => {
|
|
46
|
-
onChange(v)
|
|
47
|
-
setOpen(false)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<Popover open={open} onOpenChange={setOpen} placement="bottom-start">
|
|
52
|
-
<Popover.Trigger asChild>
|
|
53
|
-
<Button
|
|
54
|
-
size="$2"
|
|
55
|
-
minW={minWidth}
|
|
56
|
-
justify="space-between"
|
|
57
|
-
borderWidth={1}
|
|
58
|
-
borderColor="$borderColor"
|
|
59
|
-
bg={value !== null ? '$color5' : 'transparent'}
|
|
60
|
-
icon={icon}
|
|
61
|
-
iconAfter={<ChevronDown size={14} opacity={0.6} />}
|
|
62
|
-
>
|
|
63
|
-
<Text fontSize="$2" color="$color12" numberOfLines={1} flex={1}>
|
|
64
|
-
{triggerLabel}
|
|
65
|
-
</Text>
|
|
66
|
-
</Button>
|
|
67
|
-
</Popover.Trigger>
|
|
68
|
-
<Popover.Content bg="transparent" borderWidth={0} p={0} elevation={0}>
|
|
69
|
-
<PortalTheme name={themeName}>
|
|
70
|
-
<MenuPanel minWidth={minWidth} maxHeight={320} onKeyDown={(e) => menuKeyDown(e, () => setOpen(false))}>
|
|
71
|
-
<MenuItemView label={allLabel} selected={value === null} onSelect={() => pick(null)} />
|
|
72
|
-
{options.map((o) => (
|
|
73
|
-
<MenuItemView key={o.key} label={o.label} selected={value === o.key} onSelect={() => pick(o.key)} />
|
|
74
|
-
))}
|
|
75
|
-
</MenuPanel>
|
|
76
|
-
</PortalTheme>
|
|
77
|
-
</Popover.Content>
|
|
78
|
-
</Popover>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
@@ -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,135 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ThemeToggle — flips between dark and light. Framework-agnostic by default so
|
|
5
|
-
* Vite / Tauri / Express hosts can use it with NO Next dependency:
|
|
6
|
-
*
|
|
7
|
-
* • Controlled: <ThemeToggle theme={theme} onToggle={setTheme} /> (host owns theme)
|
|
8
|
-
* • Uncontrolled (agnostic): <ThemeToggle onThemeChange={fn} /> (toggles the DOM
|
|
9
|
-
* `.dark` class + persists to localStorage)
|
|
10
|
-
* • Uncontrolled (no props): <ThemeToggle /> (console/Next —
|
|
11
|
-
* falls back to the OPTIONAL @hanzogui/next-theme path;
|
|
12
|
-
* if it cannot load, degrades to the agnostic DOM toggle)
|
|
13
|
-
*
|
|
14
|
-
* Shows the action's target icon (a sun in dark mode, a moon in light mode).
|
|
15
|
-
*/
|
|
16
|
-
import type { ReactNode } from 'react'
|
|
17
|
-
import { Component, Suspense, lazy, useState } from 'react'
|
|
18
|
-
import { Button } from '@hanzo/gui'
|
|
19
|
-
import { Moon, Sun } from '@hanzogui/lucide-icons-2'
|
|
20
|
-
|
|
21
|
-
export type ThemeMode = 'light' | 'dark'
|
|
22
|
-
|
|
23
|
-
export type ThemeToggleProps = {
|
|
24
|
-
/** Controlled theme. Pair with `onToggle`/`onThemeChange` to fully control it. */
|
|
25
|
-
theme?: ThemeMode
|
|
26
|
-
/** Seed for uncontrolled mode (default: read from the DOM `.dark` class, else 'dark'). */
|
|
27
|
-
defaultTheme?: ThemeMode
|
|
28
|
-
/** Called with the next theme on toggle (alias of `onThemeChange`). */
|
|
29
|
-
onToggle?: (next: ThemeMode) => void
|
|
30
|
-
/** Called with the next theme on toggle. */
|
|
31
|
-
onThemeChange?: (next: ThemeMode) => void
|
|
32
|
-
/** @hanzo/gui Button size token (default "$2"). */
|
|
33
|
-
size?: string
|
|
34
|
-
/** aria-label override. */
|
|
35
|
-
label?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const STORAGE_KEY = 'theme'
|
|
39
|
-
|
|
40
|
-
function readDomTheme(): ThemeMode | undefined {
|
|
41
|
-
if (typeof document === 'undefined') return undefined
|
|
42
|
-
const el = document.documentElement
|
|
43
|
-
if (el.classList.contains('dark')) return 'dark'
|
|
44
|
-
if (el.classList.contains('light')) return 'light'
|
|
45
|
-
try {
|
|
46
|
-
const ls = window.localStorage.getItem(STORAGE_KEY)
|
|
47
|
-
if (ls === 'light' || ls === 'dark') return ls
|
|
48
|
-
} catch {
|
|
49
|
-
/* localStorage may be unavailable */
|
|
50
|
-
}
|
|
51
|
-
return undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function applyDomTheme(next: ThemeMode): void {
|
|
55
|
-
if (typeof document === 'undefined') return
|
|
56
|
-
const el = document.documentElement
|
|
57
|
-
el.classList.toggle('dark', next === 'dark')
|
|
58
|
-
el.classList.toggle('light', next === 'light')
|
|
59
|
-
el.style.colorScheme = next
|
|
60
|
-
try {
|
|
61
|
-
window.localStorage.setItem(STORAGE_KEY, next)
|
|
62
|
-
} catch {
|
|
63
|
-
/* ignore */
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The framework-agnostic toggle button. Controlled when `theme` is provided; otherwise
|
|
69
|
-
* self-managed via the DOM `.dark` class. No framework dependency — safe on any host.
|
|
70
|
-
*/
|
|
71
|
-
export function AgnosticThemeToggle({
|
|
72
|
-
theme,
|
|
73
|
-
defaultTheme,
|
|
74
|
-
onToggle,
|
|
75
|
-
onThemeChange,
|
|
76
|
-
size = '$2',
|
|
77
|
-
label,
|
|
78
|
-
}: ThemeToggleProps) {
|
|
79
|
-
const controlled = theme !== undefined
|
|
80
|
-
const [internal, setInternal] = useState<ThemeMode>(() => theme ?? defaultTheme ?? readDomTheme() ?? 'dark')
|
|
81
|
-
const current: ThemeMode = controlled ? (theme as ThemeMode) : internal
|
|
82
|
-
const isDark = current === 'dark'
|
|
83
|
-
|
|
84
|
-
const toggle = () => {
|
|
85
|
-
const next: ThemeMode = isDark ? 'light' : 'dark'
|
|
86
|
-
if (!controlled) {
|
|
87
|
-
setInternal(next)
|
|
88
|
-
applyDomTheme(next)
|
|
89
|
-
}
|
|
90
|
-
onToggle?.(next)
|
|
91
|
-
onThemeChange?.(next)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<Button
|
|
96
|
-
size={size as never}
|
|
97
|
-
chromeless
|
|
98
|
-
icon={isDark ? <Sun size={16} /> : <Moon size={16} />}
|
|
99
|
-
onPress={toggle}
|
|
100
|
-
aria-label={label ?? (isDark ? 'Switch to light theme' : 'Switch to dark theme')}
|
|
101
|
-
/>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// @hanzogui/next-theme is an OPTIONAL dependency — loaded only on the uncontrolled,
|
|
106
|
-
// no-props path (console/Next). Lazily code-split so non-Next hosts never need it at
|
|
107
|
-
// runtime; if it fails to load, the boundary below degrades to the agnostic toggle.
|
|
108
|
-
const NextThemeToggle = lazy(() => import('./ThemeToggleNext'))
|
|
109
|
-
|
|
110
|
-
class NextThemeBoundary extends Component<{ fallback: ReactNode; children: ReactNode }, { failed: boolean }> {
|
|
111
|
-
state = { failed: false }
|
|
112
|
-
static getDerivedStateFromError() {
|
|
113
|
-
return { failed: true }
|
|
114
|
-
}
|
|
115
|
-
render() {
|
|
116
|
-
return this.state.failed ? this.props.fallback : this.props.children
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function ThemeToggle(props: ThemeToggleProps) {
|
|
121
|
-
const injected =
|
|
122
|
-
props.theme !== undefined || props.onToggle !== undefined || props.onThemeChange !== undefined
|
|
123
|
-
if (injected) return <AgnosticThemeToggle {...props} />
|
|
124
|
-
|
|
125
|
-
// No props → keep the existing console/Next behaviour via the optional next-theme
|
|
126
|
-
// path, degrading to the agnostic DOM toggle if it is not installed.
|
|
127
|
-
const fallback = <AgnosticThemeToggle {...props} />
|
|
128
|
-
return (
|
|
129
|
-
<NextThemeBoundary fallback={fallback}>
|
|
130
|
-
<Suspense fallback={fallback}>
|
|
131
|
-
<NextThemeToggle size={props.size} label={props.label} />
|
|
132
|
-
</Suspense>
|
|
133
|
-
</NextThemeBoundary>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ThemeToggleNext — the @hanzogui/next-theme binding (console / Next). This is the ONLY
|
|
5
|
-
* module that imports @hanzogui/next-theme, so it stays an OPTIONAL dependency: the base
|
|
6
|
-
* ThemeToggle lazy-loads it (default export) and non-Next hosts never pull it in unless
|
|
7
|
-
* they import this component directly. UI is delegated to the framework-agnostic button
|
|
8
|
-
* in controlled mode, so every toggle looks identical.
|
|
9
|
-
*/
|
|
10
|
-
import { useThemeSetting } from '@hanzogui/next-theme'
|
|
11
|
-
import { AgnosticThemeToggle, type ThemeToggleProps } from './ThemeToggle'
|
|
12
|
-
|
|
13
|
-
function ThemeToggleNext({ size, label }: Pick<ThemeToggleProps, 'size' | 'label'>) {
|
|
14
|
-
const { current, resolvedTheme, set } = useThemeSetting()
|
|
15
|
-
const isDark = (resolvedTheme ?? current ?? 'dark') !== 'light'
|
|
16
|
-
return (
|
|
17
|
-
<AgnosticThemeToggle
|
|
18
|
-
theme={isDark ? 'dark' : 'light'}
|
|
19
|
-
onToggle={(next) => set(next)}
|
|
20
|
-
size={size}
|
|
21
|
-
label={label}
|
|
22
|
-
/>
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default ThemeToggleNext
|
|
27
|
-
export { ThemeToggleNext }
|