@luxfi/ui 7.4.10 → 7.4.12
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/accordion.cjs +107 -166
- package/dist/accordion.d.cts +18 -51
- package/dist/accordion.d.ts +18 -51
- package/dist/accordion.js +109 -166
- package/dist/alert.cjs +68 -46
- package/dist/alert.js +69 -46
- package/dist/avatar.cjs +49 -46
- package/dist/avatar.js +52 -48
- package/dist/badge.cjs +82 -49
- package/dist/badge.js +83 -50
- package/dist/checkbox.cjs +42 -75
- package/dist/checkbox.js +43 -76
- package/dist/chrome.cjs +196 -386
- package/dist/chrome.js +198 -386
- package/dist/close-button.cjs +3 -7
- package/dist/close-button.js +3 -7
- package/dist/collapsible.cjs +35 -26
- package/dist/collapsible.d.cts +2 -1
- package/dist/collapsible.d.ts +2 -1
- package/dist/collapsible.js +34 -24
- package/dist/dialog.cjs +3 -7
- package/dist/dialog.d.cts +1 -1
- package/dist/dialog.d.ts +1 -1
- package/dist/dialog.js +3 -7
- package/dist/drawer.cjs +95 -122
- package/dist/drawer.d.cts +43 -10
- package/dist/drawer.d.ts +43 -10
- package/dist/drawer.js +97 -122
- package/dist/empty-state.cjs +13 -13
- package/dist/empty-state.js +13 -13
- package/dist/expiration-selector.cjs +58 -63
- package/dist/expiration-selector.js +58 -63
- package/dist/field.cjs +60 -109
- package/dist/field.d.cts +0 -1
- package/dist/field.d.ts +0 -1
- package/dist/field.js +61 -109
- package/dist/greeks-display.cjs +48 -39
- package/dist/greeks-display.js +48 -39
- package/dist/heading.cjs +10 -17
- package/dist/heading.d.cts +0 -2
- package/dist/heading.d.ts +0 -2
- package/dist/heading.js +10 -17
- package/dist/icon-button.cjs +82 -82
- package/dist/icon-button.d.cts +16 -12
- package/dist/icon-button.d.ts +16 -12
- package/dist/icon-button.js +82 -82
- package/dist/index.cjs +1812 -2105
- package/dist/index.d.cts +1 -5
- package/dist/index.d.ts +1 -5
- package/dist/index.js +1814 -2100
- package/dist/input-group.cjs +11 -12
- package/dist/input-group.js +11 -12
- package/dist/input.cjs +27 -27
- package/dist/input.d.cts +21 -1
- package/dist/input.d.ts +21 -1
- package/dist/input.js +25 -28
- package/dist/menu.cjs +160 -271
- package/dist/menu.d.cts +41 -57
- package/dist/menu.d.ts +41 -57
- package/dist/menu.js +161 -251
- package/dist/option-chain.cjs +89 -67
- package/dist/option-chain.js +89 -67
- package/dist/option-position.cjs +88 -85
- package/dist/option-position.js +88 -85
- package/dist/pnl-diagram.cjs +20 -20
- package/dist/pnl-diagram.js +20 -20
- package/dist/popover.cjs +4 -8
- package/dist/popover.js +4 -8
- package/dist/progress.cjs +15 -22
- package/dist/progress.d.cts +6 -6
- package/dist/progress.d.ts +6 -6
- package/dist/progress.js +15 -22
- package/dist/radio.cjs +35 -67
- package/dist/radio.d.cts +18 -18
- package/dist/radio.d.ts +18 -18
- package/dist/radio.js +36 -68
- package/dist/select.cjs +40 -39
- package/dist/select.d.cts +35 -4
- package/dist/select.d.ts +35 -4
- package/dist/select.js +40 -38
- package/dist/separator.cjs +8 -33
- package/dist/separator.js +8 -33
- package/dist/slider.cjs +33 -49
- package/dist/slider.js +34 -50
- package/dist/switch.cjs +48 -55
- package/dist/switch.js +49 -56
- package/dist/tabs.cjs +108 -197
- package/dist/tabs.d.cts +16 -60
- package/dist/tabs.d.ts +16 -60
- package/dist/tabs.js +109 -196
- package/dist/tag.cjs +112 -69
- package/dist/tag.js +113 -69
- package/dist/textarea.cjs +56 -27
- package/dist/textarea.js +57 -28
- package/dist/toaster.cjs +97 -79
- package/dist/toaster.d.cts +7 -8
- package/dist/toaster.d.ts +7 -8
- package/dist/toaster.js +78 -80
- package/dist/tooltip.cjs +28 -17
- package/dist/tooltip.js +30 -18
- package/package.json +2 -16
- package/src/accordion.tsx +173 -227
- package/src/alert.tsx +78 -45
- package/src/avatar.tsx +54 -37
- package/src/badge.tsx +65 -44
- package/src/checkbox.tsx +70 -89
- package/src/close-button.tsx +3 -8
- package/src/collapsible.tsx +55 -52
- package/src/drawer.tsx +123 -115
- package/src/empty-state.tsx +21 -17
- package/src/expiration-selector.tsx +84 -66
- package/src/field.tsx +68 -124
- package/src/greeks-display.tsx +59 -51
- package/src/heading.tsx +22 -26
- package/src/icon-button.tsx +134 -141
- package/src/ink.tsx +37 -0
- package/src/input-group.tsx +59 -36
- package/src/input.tsx +41 -32
- package/src/menu.tsx +224 -397
- package/src/option-chain.tsx +154 -104
- package/src/option-position.tsx +136 -114
- package/src/pin-input.tsx +37 -29
- package/src/pnl-diagram.tsx +36 -28
- package/src/popover.tsx +1 -1
- package/src/progress.tsx +17 -21
- package/src/radio.tsx +56 -75
- package/src/select.tsx +80 -59
- package/src/separator.tsx +13 -34
- package/src/slider.tsx +43 -44
- package/src/strategy-builder.tsx +245 -156
- package/src/switch.tsx +63 -64
- package/src/tabs.tsx +166 -296
- package/src/tag.tsx +89 -51
- package/src/textarea.tsx +19 -29
- package/src/toaster.tsx +118 -86
- package/src/tooltip.tsx +21 -13
- package/tokens.css +79 -0
package/src/collapsible.tsx
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Collapsible, YStack } from '@hanzo/gui';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
-
import { cn } from './utils';
|
|
5
|
-
|
|
6
4
|
import type { LinkProps } from './link';
|
|
7
5
|
import { Link } from './link';
|
|
8
6
|
|
|
9
|
-
//
|
|
7
|
+
// Two disclosure shapes on gui's Collapsible: one that hides a block of detail
|
|
8
|
+
// behind a "View details" link, one that cuts a long list down to its first few
|
|
9
|
+
// rows.
|
|
10
|
+
//
|
|
11
|
+
// The trigger is the `Link` itself rather than a wrapping `Collapsible.Trigger`.
|
|
12
|
+
// gui's Trigger renders its OWN element — it has no `asChild` — so wrapping
|
|
13
|
+
// would nest a button around an anchor, which is invalid HTML and swallows the
|
|
14
|
+
// link's own keyboard behaviour. The state is already controlled here, so the
|
|
15
|
+
// only thing the wrapper contributed was `aria-expanded`, and that is a prop.
|
|
16
|
+
|
|
17
|
+
/** An effect that skips the first mount. */
|
|
10
18
|
function useUpdateEffect(effect: React.EffectCallback, deps: React.DependencyList): void {
|
|
11
19
|
const isFirstMount = React.useRef(true);
|
|
12
20
|
React.useEffect(() => {
|
|
@@ -19,12 +27,8 @@ function useUpdateEffect(effect: React.EffectCallback, deps: React.DependencyLis
|
|
|
19
27
|
}, deps);
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
// Inline scroll-to helper (replaces react-scroll dependency)
|
|
23
30
|
function scrollToElement(id: string): void {
|
|
24
|
-
|
|
25
|
-
if (el) {
|
|
26
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
27
|
-
}
|
|
31
|
+
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export interface CollapsibleDetailsProps extends LinkProps {
|
|
@@ -38,48 +42,49 @@ export interface CollapsibleDetailsProps extends LinkProps {
|
|
|
38
42
|
const CUT_ID = 'CollapsibleDetails';
|
|
39
43
|
|
|
40
44
|
export const CollapsibleDetails = (props: CollapsibleDetailsProps) => {
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const {
|
|
46
|
+
children, id = CUT_ID, onClick, isExpanded: isExpandedProp = false,
|
|
47
|
+
text: textProp, loading, noScroll, ...rest
|
|
48
|
+
} = props;
|
|
43
49
|
|
|
44
50
|
const [ isExpanded, setIsExpanded ] = React.useState(isExpandedProp);
|
|
45
51
|
|
|
46
52
|
const handleClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
47
53
|
setIsExpanded((flag) => !flag);
|
|
48
|
-
if (!noScroll)
|
|
49
|
-
scrollToElement(id);
|
|
50
|
-
}
|
|
54
|
+
if (!noScroll) scrollToElement(id);
|
|
51
55
|
onClick?.(event);
|
|
52
56
|
}, [ id, noScroll, onClick ]);
|
|
53
57
|
|
|
54
58
|
useUpdateEffect(() => {
|
|
55
59
|
setIsExpanded(isExpandedProp);
|
|
56
|
-
if (isExpandedProp && !noScroll)
|
|
57
|
-
scrollToElement(id);
|
|
58
|
-
}
|
|
60
|
+
if (isExpandedProp && !noScroll) scrollToElement(id);
|
|
59
61
|
}, [ isExpandedProp, id, noScroll ]);
|
|
60
62
|
|
|
61
63
|
const text = isExpanded ? (textProp?.[1] ?? 'Hide details') : (textProp?.[0] ?? 'View details');
|
|
62
64
|
|
|
63
65
|
return (
|
|
64
|
-
<Collapsible
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</Collapsible.Trigger>
|
|
66
|
+
<Collapsible open={ isExpanded } onOpenChange={ setIsExpanded }>
|
|
67
|
+
<Link
|
|
68
|
+
className="text-sm underline decoration-dashed w-fit"
|
|
69
|
+
aria-expanded={ isExpanded }
|
|
70
|
+
onClick={ handleClick }
|
|
71
|
+
loading={ loading }
|
|
72
|
+
{ ...rest }
|
|
73
|
+
>
|
|
74
|
+
<span id={ id }>{ text }</span>
|
|
75
|
+
</Link>
|
|
75
76
|
<Collapsible.Content>
|
|
76
77
|
{ children }
|
|
77
78
|
</Collapsible.Content>
|
|
78
|
-
</Collapsible
|
|
79
|
+
</Collapsible>
|
|
79
80
|
);
|
|
80
81
|
};
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
// `items` and `text` are this component's own nouns — the rows, and the pair of
|
|
84
|
+
// trigger labels. On gui they are the `alignItems` / `textAlign` shorthands.
|
|
85
|
+
// Ours win; the shorthands stay reachable under their full names.
|
|
86
|
+
interface CollapsibleListProps<T>
|
|
87
|
+
extends Omit<React.ComponentProps<typeof YStack>, 'children' | 'text' | 'items'> {
|
|
83
88
|
items: Array<T>;
|
|
84
89
|
renderItem: (item: T, index: number) => React.ReactNode;
|
|
85
90
|
triggerProps?: LinkProps;
|
|
@@ -88,10 +93,13 @@ interface CollapsibleListProps<T> extends React.ComponentPropsWithoutRef<'div'>
|
|
|
88
93
|
defaultExpanded?: boolean;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
const CUT_LENGTH = 3;
|
|
96
|
+
const CUT_LENGTH = 3;
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
export const CollapsibleList = <T,>(props: CollapsibleListProps<T>) => {
|
|
99
|
+
const {
|
|
100
|
+
items, renderItem, triggerProps, cutLength = CUT_LENGTH,
|
|
101
|
+
text: textProp, defaultExpanded = false, ...rest
|
|
102
|
+
} = props;
|
|
95
103
|
|
|
96
104
|
const [ isExpanded, setIsExpanded ] = React.useState(defaultExpanded);
|
|
97
105
|
|
|
@@ -99,28 +107,23 @@ export const CollapsibleList = <T,>(props: CollapsibleListProps<T>) => {
|
|
|
99
107
|
setIsExpanded(defaultExpanded);
|
|
100
108
|
}, [ defaultExpanded ]);
|
|
101
109
|
|
|
102
|
-
const handleToggle = React.useCallback(() =>
|
|
103
|
-
setIsExpanded((flag) => !flag);
|
|
104
|
-
}, []);
|
|
110
|
+
const handleToggle = React.useCallback(() => setIsExpanded((flag) => !flag), []);
|
|
105
111
|
|
|
106
112
|
const text = isExpanded ? (textProp?.[1] ?? 'Hide') : (textProp?.[0] ?? 'Show all');
|
|
107
113
|
|
|
108
114
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
) }
|
|
123
|
-
</div>
|
|
124
|
-
</Collapsible.Root>
|
|
115
|
+
<YStack width="100%" { ...rest }>
|
|
116
|
+
{ items.slice(0, isExpanded ? undefined : cutLength).map(renderItem) }
|
|
117
|
+
{ items.length > cutLength && (
|
|
118
|
+
<Link
|
|
119
|
+
className="text-sm underline decoration-dashed w-fit min-w-0"
|
|
120
|
+
aria-expanded={ isExpanded }
|
|
121
|
+
onClick={ handleToggle }
|
|
122
|
+
{ ...triggerProps }
|
|
123
|
+
>
|
|
124
|
+
{ text }
|
|
125
|
+
</Link>
|
|
126
|
+
) }
|
|
127
|
+
</YStack>
|
|
125
128
|
);
|
|
126
129
|
};
|
package/src/drawer.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { Sheet as GuiSheet } from '@hanzo/gui';
|
|
2
|
-
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
1
|
+
import { Dialog, Sheet as GuiSheet, View, XStack, YStack } from '@hanzo/gui';
|
|
3
2
|
import * as React from 'react';
|
|
4
3
|
|
|
5
|
-
import { useIsNarrow } from './use-narrow';
|
|
6
|
-
import { cn } from './utils';
|
|
7
|
-
|
|
8
4
|
import { CloseButton } from './close-button';
|
|
5
|
+
import { ink } from './ink';
|
|
6
|
+
import { useIsNarrow } from './use-narrow';
|
|
9
7
|
|
|
10
8
|
// A drawer is a side panel on a wide screen and a bottom sheet on a phone. Below
|
|
11
9
|
// `NARROW_PX` the content is presented by gui's Sheet — draggable, with
|
|
@@ -13,6 +11,16 @@ import { CloseButton } from './close-button';
|
|
|
13
11
|
// `position: fixed` panel with a slide-in keyframe cannot fake. Same components,
|
|
14
12
|
// same props, at every call site. The breakpoint itself lives in ./use-narrow,
|
|
15
13
|
// so "is this phone-sized?" has one answer across the package.
|
|
14
|
+
//
|
|
15
|
+
// Both presentations now come from the same engine: the wide panel is gui's
|
|
16
|
+
// Dialog where it used to be Radix's. That is the whole reason the root has to
|
|
17
|
+
// own its open state — it did already, because the Sheet lives outside the
|
|
18
|
+
// dialog tree and needs the same state, and now both sides read it.
|
|
19
|
+
//
|
|
20
|
+
// The slide is `enterStyle` / `exitStyle` on the content, driven by the CSS
|
|
21
|
+
// animation driver that lux.config feeds the engine. It is not decoration: the
|
|
22
|
+
// panel is `position: fixed` at the edge it flies in from, so without the
|
|
23
|
+
// transform it appears instantly, fully formed, which reads as a repaint bug.
|
|
16
24
|
|
|
17
25
|
// ─── DrawerRoot ─────────────────────────────────────────────────
|
|
18
26
|
|
|
@@ -21,46 +29,44 @@ export interface DrawerRootProps {
|
|
|
21
29
|
open?: boolean;
|
|
22
30
|
defaultOpen?: boolean;
|
|
23
31
|
onOpenChange?: (details: { open: boolean }) => void;
|
|
24
|
-
placement?:
|
|
32
|
+
placement?: Placement;
|
|
25
33
|
initialFocusEl?: (() => HTMLElement | null) | React.RefObject<HTMLElement>;
|
|
26
34
|
lazyMount?: boolean;
|
|
27
35
|
unmountOnExit?: boolean;
|
|
28
36
|
modal?: boolean;
|
|
29
|
-
size?:
|
|
37
|
+
size?: Size;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
type Placement = 'left' | 'right' | 'top' | 'bottom';
|
|
41
|
+
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
42
|
+
|
|
43
|
+
const DrawerPlacementContext = React.createContext<Placement>('right');
|
|
44
|
+
const DrawerSizeContext = React.createContext<Size>('md');
|
|
34
45
|
|
|
35
46
|
interface DrawerStateValue {
|
|
36
47
|
readonly open: boolean;
|
|
37
48
|
readonly setOpen: (next: boolean) => void;
|
|
38
49
|
readonly modal: boolean;
|
|
50
|
+
readonly narrow: boolean;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
const DrawerStateContext = React.createContext<DrawerStateValue>({
|
|
42
54
|
open: false,
|
|
43
55
|
setOpen: () => { /* noop */ },
|
|
44
56
|
modal: true,
|
|
57
|
+
narrow: false,
|
|
45
58
|
});
|
|
46
59
|
|
|
47
60
|
export const DrawerRoot = (props: DrawerRootProps) => {
|
|
48
61
|
const {
|
|
49
|
-
children,
|
|
50
|
-
|
|
51
|
-
defaultOpen,
|
|
52
|
-
onOpenChange,
|
|
53
|
-
placement = 'right',
|
|
54
|
-
modal = true,
|
|
55
|
-
size = 'md',
|
|
62
|
+
children, open, defaultOpen, onOpenChange,
|
|
63
|
+
placement = 'right', modal = true, size = 'md',
|
|
56
64
|
} = props;
|
|
57
65
|
|
|
58
|
-
// The root owns the open state (controllable) rather than leaving it inside
|
|
59
|
-
// Radix, because the phone presentation is a gui Sheet that lives outside the
|
|
60
|
-
// dialog tree and needs the same state.
|
|
61
66
|
const [ uncontrolled, setUncontrolled ] = React.useState(defaultOpen ?? false);
|
|
62
67
|
const isControlled = open !== undefined;
|
|
63
68
|
const isOpen = isControlled ? open : uncontrolled;
|
|
69
|
+
const narrow = useIsNarrow();
|
|
64
70
|
|
|
65
71
|
const setOpen = React.useCallback((nextOpen: boolean) => {
|
|
66
72
|
if (!isControlled) setUncontrolled(nextOpen);
|
|
@@ -68,21 +74,26 @@ export const DrawerRoot = (props: DrawerRootProps) => {
|
|
|
68
74
|
}, [ isControlled, onOpenChange ]);
|
|
69
75
|
|
|
70
76
|
const state = React.useMemo(
|
|
71
|
-
() => ({ open: isOpen, setOpen, modal }),
|
|
72
|
-
[ isOpen, setOpen, modal ],
|
|
77
|
+
() => ({ open: isOpen, setOpen, modal, narrow }),
|
|
78
|
+
[ isOpen, setOpen, modal, narrow ],
|
|
73
79
|
);
|
|
74
80
|
|
|
75
81
|
return (
|
|
76
82
|
<DrawerPlacementContext.Provider value={ placement }>
|
|
77
83
|
<DrawerSizeContext.Provider value={ size }>
|
|
78
84
|
<DrawerStateContext.Provider value={ state }>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
{ /* WHICH presentation is one decision, and it is made here.
|
|
86
|
+
A gui Sheet inside a gui Dialog is not a sheet in a dialog: the
|
|
87
|
+
Dialog publishes a sheet controller and adopts any Sheet beneath
|
|
88
|
+
it, so the phone panel stopped rendering entirely the moment the
|
|
89
|
+
wide path became a gui Dialog. On a phone there is no dialog —
|
|
90
|
+
the Sheet IS the presentation — so no Dialog is mounted at all,
|
|
91
|
+
and the trigger below reads the same flag. */ }
|
|
92
|
+
{ narrow ? children : (
|
|
93
|
+
<Dialog open={ isOpen } onOpenChange={ setOpen } modal={ modal }>
|
|
94
|
+
{ children }
|
|
95
|
+
</Dialog>
|
|
96
|
+
) }
|
|
86
97
|
</DrawerStateContext.Provider>
|
|
87
98
|
</DrawerSizeContext.Provider>
|
|
88
99
|
</DrawerPlacementContext.Provider>
|
|
@@ -98,45 +109,35 @@ export interface DrawerContentProps extends React.ComponentPropsWithoutRef<'div'
|
|
|
98
109
|
backdrop?: boolean;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
left: [
|
|
108
|
-
'inset-y-0 left-0',
|
|
109
|
-
'data-[state=open]:animate-in data-[state=open]:slide-in-from-left',
|
|
110
|
-
'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-left',
|
|
111
|
-
].join(' '),
|
|
112
|
-
top: [
|
|
113
|
-
'inset-x-0 top-0',
|
|
114
|
-
'data-[state=open]:animate-in data-[state=open]:slide-in-from-top',
|
|
115
|
-
'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-top',
|
|
116
|
-
].join(' '),
|
|
117
|
-
bottom: [
|
|
118
|
-
'inset-x-0 bottom-0',
|
|
119
|
-
'data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom',
|
|
120
|
-
'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom',
|
|
121
|
-
].join(' '),
|
|
112
|
+
/** Where the panel is pinned, and which way it flies in from. */
|
|
113
|
+
const ANCHOR: Record<Placement, Record<string, unknown>> = {
|
|
114
|
+
right: { top: 0, bottom: 0, right: 0, enterStyle: { x: 24, opacity: 0 }, exitStyle: { x: 24, opacity: 0 } },
|
|
115
|
+
left: { top: 0, bottom: 0, left: 0, enterStyle: { x: -24, opacity: 0 }, exitStyle: { x: -24, opacity: 0 } },
|
|
116
|
+
top: { left: 0, right: 0, top: 0, enterStyle: { y: -24, opacity: 0 }, exitStyle: { y: -24, opacity: 0 } },
|
|
117
|
+
bottom: { left: 0, right: 0, bottom: 0, enterStyle: { y: 24, opacity: 0 }, exitStyle: { y: 24, opacity: 0 } },
|
|
122
118
|
};
|
|
123
119
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
left: { xs: 'w-60', sm: 'w-80', md: 'w-96', lg: 'w-[480px]', xl: 'w-[640px]', full: 'w-screen' },
|
|
127
|
-
top: { xs: 'h-40', sm: 'h-60', md: 'h-80', lg: 'h-96', xl: 'h-[480px]', full: 'h-screen' },
|
|
128
|
-
bottom: { xs: 'h-40', sm: 'h-60', md: 'h-80', lg: 'h-96', xl: 'h-[480px]', full: 'h-screen' },
|
|
120
|
+
const ACROSS: Record<Size, number | string> = {
|
|
121
|
+
xs: 240, sm: 320, md: 384, lg: 480, xl: 640, full: '100%',
|
|
129
122
|
};
|
|
130
123
|
|
|
124
|
+
/** The panel's measured axis: width for a side panel, height for a top/bottom one. */
|
|
125
|
+
const extent = (placement: Placement, size: Size): Record<string, unknown> => (
|
|
126
|
+
placement === 'left' || placement === 'right' ?
|
|
127
|
+
{ width: ACROSS[size], maxWidth: '100%' } :
|
|
128
|
+
{ height: ACROSS[size], maxHeight: '100%' }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const SCRIM = 'rgba(0,0,0,0.5)';
|
|
132
|
+
|
|
131
133
|
export const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
|
|
132
134
|
function DrawerContent(props, ref) {
|
|
133
|
-
const { children, portalled = true, portalRef, offset: _offset, backdrop = true,
|
|
135
|
+
const { children, portalled = true, portalRef: _portalRef, offset: _offset, backdrop = true, ...rest } = props;
|
|
134
136
|
const placement = React.useContext(DrawerPlacementContext);
|
|
135
137
|
const size = React.useContext(DrawerSizeContext);
|
|
136
|
-
const { open, setOpen, modal } = React.useContext(DrawerStateContext);
|
|
137
|
-
const isNarrow = useIsNarrow();
|
|
138
|
+
const { open, setOpen, modal, narrow } = React.useContext(DrawerStateContext);
|
|
138
139
|
|
|
139
|
-
if (
|
|
140
|
+
if (narrow) {
|
|
140
141
|
return (
|
|
141
142
|
<GuiSheet
|
|
142
143
|
open={ open }
|
|
@@ -152,7 +153,7 @@ export const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps
|
|
|
152
153
|
transition="lazy"
|
|
153
154
|
enterStyle={{ opacity: 0 }}
|
|
154
155
|
exitStyle={{ opacity: 0 }}
|
|
155
|
-
|
|
156
|
+
backgroundColor={ SCRIM as never }
|
|
156
157
|
/>
|
|
157
158
|
) }
|
|
158
159
|
<GuiSheet.Handle/>
|
|
@@ -160,10 +161,11 @@ export const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps
|
|
|
160
161
|
Chakra contract hands us has nothing to attach to here. */ }
|
|
161
162
|
<GuiSheet.Frame
|
|
162
163
|
unstyled
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
) }
|
|
164
|
+
flexDirection="column"
|
|
165
|
+
borderTopLeftRadius={ 16 }
|
|
166
|
+
borderTopRightRadius={ 16 }
|
|
167
|
+
backgroundColor={ 'var(--color-drawer-bg)' as never }
|
|
168
|
+
shadowColor={ 'var(--shadow-drawer)' as never }
|
|
167
169
|
{ ...(rest as object) }
|
|
168
170
|
>
|
|
169
171
|
{ children }
|
|
@@ -175,85 +177,91 @@ export const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps
|
|
|
175
177
|
const content = (
|
|
176
178
|
<>
|
|
177
179
|
{ backdrop && (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
<Dialog.Overlay
|
|
181
|
+
key="overlay"
|
|
182
|
+
position="fixed"
|
|
183
|
+
top={ 0 }
|
|
184
|
+
right={ 0 }
|
|
185
|
+
bottom={ 0 }
|
|
186
|
+
left={ 0 }
|
|
187
|
+
zIndex={ 50 }
|
|
188
|
+
backgroundColor={ SCRIM as never }
|
|
189
|
+
transition="lazy"
|
|
190
|
+
enterStyle={{ opacity: 0 }}
|
|
191
|
+
exitStyle={{ opacity: 0 }}
|
|
184
192
|
/>
|
|
185
193
|
) }
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
) }
|
|
194
|
-
|
|
194
|
+
<Dialog.Content
|
|
195
|
+
key="content"
|
|
196
|
+
ref={ ref as never }
|
|
197
|
+
unstyled
|
|
198
|
+
position="fixed"
|
|
199
|
+
zIndex={ 50 }
|
|
200
|
+
flexDirection="column"
|
|
201
|
+
backgroundColor={ 'var(--color-drawer-bg)' as never }
|
|
202
|
+
transition="medium"
|
|
203
|
+
{ ...ANCHOR[placement] }
|
|
204
|
+
{ ...extent(placement, size) }
|
|
205
|
+
{ ...(rest as object) }
|
|
195
206
|
>
|
|
196
207
|
{ children }
|
|
197
|
-
</
|
|
208
|
+
</Dialog.Content>
|
|
198
209
|
</>
|
|
199
210
|
);
|
|
200
211
|
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<RadixDialog.Portal container={ portalRef?.current ?? undefined }>
|
|
204
|
-
{ content }
|
|
205
|
-
</RadixDialog.Portal>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return content;
|
|
212
|
+
return portalled ? <Dialog.Portal>{ content }</Dialog.Portal> : content;
|
|
210
213
|
},
|
|
211
214
|
);
|
|
212
215
|
|
|
213
|
-
// ───
|
|
216
|
+
// ─── Triggers ───────────────────────────────────────────────────
|
|
214
217
|
|
|
215
218
|
export const DrawerCloseTrigger = React.forwardRef<
|
|
216
219
|
HTMLButtonElement,
|
|
217
220
|
React.ComponentPropsWithoutRef<'button'>
|
|
218
221
|
>(function DrawerCloseTrigger(props, ref) {
|
|
219
222
|
return (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{ ...props }
|
|
225
|
-
|
|
226
|
-
</
|
|
223
|
+
// Placement belongs to the surrounding box, not to the button: CloseButton
|
|
224
|
+
// is a fixed 20px control and has no opinion about where it sits.
|
|
225
|
+
<View position="absolute" top={ 28 } right={ 20 } zIndex={ 1 }>
|
|
226
|
+
<Dialog.Close asChild>
|
|
227
|
+
<CloseButton ref={ ref } { ...props }/>
|
|
228
|
+
</Dialog.Close>
|
|
229
|
+
</View>
|
|
227
230
|
);
|
|
228
231
|
});
|
|
229
232
|
|
|
230
|
-
|
|
233
|
+
export const DrawerTrigger = ({ asChild = true, children, ...rest }: React.ComponentProps<typeof Dialog.Trigger>) => {
|
|
234
|
+
const { setOpen, narrow } = React.useContext(DrawerStateContext);
|
|
235
|
+
|
|
236
|
+
// On a phone there is no Dialog above this, so there is no Dialog.Trigger to
|
|
237
|
+
// be: the child opens the Sheet directly.
|
|
238
|
+
if (narrow) {
|
|
239
|
+
return React.isValidElement(children) ?
|
|
240
|
+
React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, { onClick: () => setOpen(true) }) :
|
|
241
|
+
<>{ children }</>;
|
|
242
|
+
}
|
|
231
243
|
|
|
232
|
-
|
|
233
|
-
const { asChild = true, ...rest } = props;
|
|
234
|
-
return <RadixDialog.Trigger asChild={ asChild } { ...rest }/>;
|
|
244
|
+
return <Dialog.Trigger asChild={ asChild } { ...rest }>{ children }</Dialog.Trigger>;
|
|
235
245
|
};
|
|
236
246
|
|
|
237
247
|
// ─── Sub-components ─────────────────────────────────────────────
|
|
238
248
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
249
|
+
// These three take caller-authored content, so a bare string is the normal
|
|
250
|
+
// case and has to be given a host — a View cannot lay one out.
|
|
251
|
+
export const DrawerHeader = ({ children, ...props }: React.ComponentProps<typeof YStack>) => (
|
|
252
|
+
<YStack gap={ 6 } padding={ 24 } paddingBottom={ 0 } { ...props }>{ ink(children) }</YStack>
|
|
243
253
|
);
|
|
244
254
|
|
|
245
|
-
export const DrawerBody =
|
|
246
|
-
|
|
247
|
-
return <div ref={ ref } className={ cn('flex-1 overflow-auto p-6', className) } { ...props }/>;
|
|
248
|
-
},
|
|
255
|
+
export const DrawerBody = ({ children, ...props }: React.ComponentProps<typeof YStack>) => (
|
|
256
|
+
<YStack flex={ 1 } overflow="scroll" padding={ 24 } { ...props }>{ ink(children) }</YStack>
|
|
249
257
|
);
|
|
250
258
|
|
|
251
|
-
export const DrawerFooter =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
export const DrawerFooter = ({ children, ...props }: React.ComponentProps<typeof XStack>) => (
|
|
260
|
+
<XStack alignItems="center" justifyContent="flex-end" gap={ 8 } padding={ 24 } paddingTop={ 0 } { ...props }>
|
|
261
|
+
{ ink(children) }
|
|
262
|
+
</XStack>
|
|
255
263
|
);
|
|
256
264
|
|
|
257
|
-
export const DrawerTitle =
|
|
258
|
-
export const DrawerDescription =
|
|
259
|
-
export const DrawerActionTrigger =
|
|
265
|
+
export const DrawerTitle = Dialog.Title;
|
|
266
|
+
export const DrawerDescription = Dialog.Description;
|
|
267
|
+
export const DrawerActionTrigger = Dialog.Close;
|
package/src/empty-state.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
1
2
|
import * as React from 'react';
|
|
2
3
|
|
|
3
|
-
import { cn } from './utils';
|
|
4
|
-
|
|
5
4
|
// Inline constants
|
|
6
|
-
const apos = '
|
|
5
|
+
const apos = '’'; // right single quotation mark (typographic apostrophe)
|
|
7
6
|
|
|
8
7
|
function upperFirst(str: string): string {
|
|
9
8
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
@@ -23,6 +22,8 @@ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
23
22
|
icon?: React.ReactNode;
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
const MUTED = 'var(--color-text-secondary)' as never;
|
|
26
|
+
|
|
26
27
|
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
27
28
|
function EmptyState(props, ref) {
|
|
28
29
|
const { title, description, term, type = 'query', icon, children, className, ...rest } = props;
|
|
@@ -70,28 +71,31 @@ export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
70
71
|
})();
|
|
71
72
|
|
|
72
73
|
return (
|
|
73
|
-
<
|
|
74
|
-
ref={ ref }
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
<XStack
|
|
75
|
+
ref={ ref as never }
|
|
76
|
+
alignItems="center"
|
|
77
|
+
justifyContent="center"
|
|
78
|
+
paddingVertical={ 40 }
|
|
79
|
+
className={ className }
|
|
80
|
+
{ ...(rest as object) }
|
|
77
81
|
>
|
|
78
|
-
<
|
|
82
|
+
<YStack alignItems="center" gap={ 20 }>
|
|
79
83
|
{ iconContent && (
|
|
80
|
-
<
|
|
84
|
+
<XStack alignItems="center" justifyContent="center">{ iconContent }</XStack>
|
|
81
85
|
) }
|
|
82
86
|
{ descriptionContent ? (
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
<
|
|
87
|
+
<YStack alignItems="center" gap={ 8 }>
|
|
88
|
+
<Text fontSize={ 18 } fontWeight="600" textAlign="center" color={ MUTED }>{ titleContent }</Text>
|
|
89
|
+
<Text fontSize={ 14 } textAlign="center" color={ MUTED }>
|
|
86
90
|
{ descriptionContent }
|
|
87
|
-
</
|
|
88
|
-
</
|
|
91
|
+
</Text>
|
|
92
|
+
</YStack>
|
|
89
93
|
) : (
|
|
90
|
-
<
|
|
94
|
+
<Text fontSize={ 18 } fontWeight="600" textAlign="center" color={ MUTED }>{ titleContent }</Text>
|
|
91
95
|
) }
|
|
92
96
|
{ children }
|
|
93
|
-
</
|
|
94
|
-
</
|
|
97
|
+
</YStack>
|
|
98
|
+
</XStack>
|
|
95
99
|
);
|
|
96
100
|
},
|
|
97
101
|
);
|