@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.
Files changed (137) hide show
  1. package/dist/accordion.cjs +107 -166
  2. package/dist/accordion.d.cts +18 -51
  3. package/dist/accordion.d.ts +18 -51
  4. package/dist/accordion.js +109 -166
  5. package/dist/alert.cjs +68 -46
  6. package/dist/alert.js +69 -46
  7. package/dist/avatar.cjs +49 -46
  8. package/dist/avatar.js +52 -48
  9. package/dist/badge.cjs +82 -49
  10. package/dist/badge.js +83 -50
  11. package/dist/checkbox.cjs +42 -75
  12. package/dist/checkbox.js +43 -76
  13. package/dist/chrome.cjs +196 -386
  14. package/dist/chrome.js +198 -386
  15. package/dist/close-button.cjs +3 -7
  16. package/dist/close-button.js +3 -7
  17. package/dist/collapsible.cjs +35 -26
  18. package/dist/collapsible.d.cts +2 -1
  19. package/dist/collapsible.d.ts +2 -1
  20. package/dist/collapsible.js +34 -24
  21. package/dist/dialog.cjs +3 -7
  22. package/dist/dialog.d.cts +1 -1
  23. package/dist/dialog.d.ts +1 -1
  24. package/dist/dialog.js +3 -7
  25. package/dist/drawer.cjs +95 -122
  26. package/dist/drawer.d.cts +43 -10
  27. package/dist/drawer.d.ts +43 -10
  28. package/dist/drawer.js +97 -122
  29. package/dist/empty-state.cjs +13 -13
  30. package/dist/empty-state.js +13 -13
  31. package/dist/expiration-selector.cjs +58 -63
  32. package/dist/expiration-selector.js +58 -63
  33. package/dist/field.cjs +60 -109
  34. package/dist/field.d.cts +0 -1
  35. package/dist/field.d.ts +0 -1
  36. package/dist/field.js +61 -109
  37. package/dist/greeks-display.cjs +48 -39
  38. package/dist/greeks-display.js +48 -39
  39. package/dist/heading.cjs +10 -17
  40. package/dist/heading.d.cts +0 -2
  41. package/dist/heading.d.ts +0 -2
  42. package/dist/heading.js +10 -17
  43. package/dist/icon-button.cjs +82 -82
  44. package/dist/icon-button.d.cts +16 -12
  45. package/dist/icon-button.d.ts +16 -12
  46. package/dist/icon-button.js +82 -82
  47. package/dist/index.cjs +1812 -2105
  48. package/dist/index.d.cts +1 -5
  49. package/dist/index.d.ts +1 -5
  50. package/dist/index.js +1814 -2100
  51. package/dist/input-group.cjs +11 -12
  52. package/dist/input-group.js +11 -12
  53. package/dist/input.cjs +27 -27
  54. package/dist/input.d.cts +21 -1
  55. package/dist/input.d.ts +21 -1
  56. package/dist/input.js +25 -28
  57. package/dist/menu.cjs +160 -271
  58. package/dist/menu.d.cts +41 -57
  59. package/dist/menu.d.ts +41 -57
  60. package/dist/menu.js +161 -251
  61. package/dist/option-chain.cjs +89 -67
  62. package/dist/option-chain.js +89 -67
  63. package/dist/option-position.cjs +88 -85
  64. package/dist/option-position.js +88 -85
  65. package/dist/pnl-diagram.cjs +20 -20
  66. package/dist/pnl-diagram.js +20 -20
  67. package/dist/popover.cjs +4 -8
  68. package/dist/popover.js +4 -8
  69. package/dist/progress.cjs +15 -22
  70. package/dist/progress.d.cts +6 -6
  71. package/dist/progress.d.ts +6 -6
  72. package/dist/progress.js +15 -22
  73. package/dist/radio.cjs +35 -67
  74. package/dist/radio.d.cts +18 -18
  75. package/dist/radio.d.ts +18 -18
  76. package/dist/radio.js +36 -68
  77. package/dist/select.cjs +40 -39
  78. package/dist/select.d.cts +35 -4
  79. package/dist/select.d.ts +35 -4
  80. package/dist/select.js +40 -38
  81. package/dist/separator.cjs +8 -33
  82. package/dist/separator.js +8 -33
  83. package/dist/slider.cjs +33 -49
  84. package/dist/slider.js +34 -50
  85. package/dist/switch.cjs +48 -55
  86. package/dist/switch.js +49 -56
  87. package/dist/tabs.cjs +108 -197
  88. package/dist/tabs.d.cts +16 -60
  89. package/dist/tabs.d.ts +16 -60
  90. package/dist/tabs.js +109 -196
  91. package/dist/tag.cjs +112 -69
  92. package/dist/tag.js +113 -69
  93. package/dist/textarea.cjs +56 -27
  94. package/dist/textarea.js +57 -28
  95. package/dist/toaster.cjs +97 -79
  96. package/dist/toaster.d.cts +7 -8
  97. package/dist/toaster.d.ts +7 -8
  98. package/dist/toaster.js +78 -80
  99. package/dist/tooltip.cjs +28 -17
  100. package/dist/tooltip.js +30 -18
  101. package/package.json +2 -16
  102. package/src/accordion.tsx +173 -227
  103. package/src/alert.tsx +78 -45
  104. package/src/avatar.tsx +54 -37
  105. package/src/badge.tsx +65 -44
  106. package/src/checkbox.tsx +70 -89
  107. package/src/close-button.tsx +3 -8
  108. package/src/collapsible.tsx +55 -52
  109. package/src/drawer.tsx +123 -115
  110. package/src/empty-state.tsx +21 -17
  111. package/src/expiration-selector.tsx +84 -66
  112. package/src/field.tsx +68 -124
  113. package/src/greeks-display.tsx +59 -51
  114. package/src/heading.tsx +22 -26
  115. package/src/icon-button.tsx +134 -141
  116. package/src/ink.tsx +37 -0
  117. package/src/input-group.tsx +59 -36
  118. package/src/input.tsx +41 -32
  119. package/src/menu.tsx +224 -397
  120. package/src/option-chain.tsx +154 -104
  121. package/src/option-position.tsx +136 -114
  122. package/src/pin-input.tsx +37 -29
  123. package/src/pnl-diagram.tsx +36 -28
  124. package/src/popover.tsx +1 -1
  125. package/src/progress.tsx +17 -21
  126. package/src/radio.tsx +56 -75
  127. package/src/select.tsx +80 -59
  128. package/src/separator.tsx +13 -34
  129. package/src/slider.tsx +43 -44
  130. package/src/strategy-builder.tsx +245 -156
  131. package/src/switch.tsx +63 -64
  132. package/src/tabs.tsx +166 -296
  133. package/src/tag.tsx +89 -51
  134. package/src/textarea.tsx +19 -29
  135. package/src/toaster.tsx +118 -86
  136. package/src/tooltip.tsx +21 -13
  137. package/tokens.css +79 -0
@@ -1,12 +1,20 @@
1
- import * as Collapsible from '@radix-ui/react-collapsible';
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
- // Inline useUpdateEffect (effect that skips first mount)
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
- const el = document.getElementById(id);
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
- const { children, id = CUT_ID, onClick, isExpanded: isExpandedProp = false, text: textProp, loading, noScroll, ...rest } = props;
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.Root open={ isExpanded }>
65
- <Collapsible.Trigger asChild>
66
- <Link
67
- className="text-sm underline decoration-dashed w-fit"
68
- onClick={ handleClick }
69
- loading={ loading }
70
- { ...rest }
71
- >
72
- <span id={ id }>{ text }</span>
73
- </Link>
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.Root>
79
+ </Collapsible>
79
80
  );
80
81
  };
81
82
 
82
- interface CollapsibleListProps<T> extends React.ComponentPropsWithoutRef<'div'> {
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
- export const CollapsibleList = <T,>(props: CollapsibleListProps<T>) => {
92
- const CUT_LENGTH = 3;
96
+ const CUT_LENGTH = 3;
93
97
 
94
- const { items, renderItem, triggerProps, cutLength = CUT_LENGTH, text: textProp, defaultExpanded = false, className, ...rest } = props;
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
- <Collapsible.Root open={ isExpanded }>
110
- <div className={ cn('flex flex-col w-full', className) } { ...rest }>
111
- { items.slice(0, isExpanded ? undefined : cutLength).map(renderItem) }
112
- { items.length > cutLength && (
113
- <Collapsible.Trigger asChild>
114
- <Link
115
- className="text-sm underline decoration-dashed w-fit min-w-0"
116
- onClick={ handleToggle }
117
- { ...triggerProps }
118
- >
119
- { text }
120
- </Link>
121
- </Collapsible.Trigger>
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?: 'left' | 'right' | 'top' | 'bottom';
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?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
37
+ size?: Size;
30
38
  }
31
39
 
32
- const DrawerPlacementContext = React.createContext<string>('right');
33
- const DrawerSizeContext = React.createContext<string>('md');
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
- open,
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
- <RadixDialog.Root
80
- open={ isOpen }
81
- onOpenChange={ setOpen }
82
- modal={ modal }
83
- >
84
- { children }
85
- </RadixDialog.Root>
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
- const PLACEMENT_CLASSES: Record<string, string> = {
102
- right: [
103
- 'inset-y-0 right-0',
104
- 'data-[state=open]:animate-in data-[state=open]:slide-in-from-right',
105
- 'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right',
106
- ].join(' '),
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 SIZE_CLASSES: Record<string, Record<string, string>> = {
125
- right: { xs: 'w-60', sm: 'w-80', md: 'w-96', lg: 'w-[480px]', xl: 'w-[640px]', full: 'w-screen' },
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, className, ...rest } = props;
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 (isNarrow) {
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
- className="bg-black/50"
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
- className={ cn(
164
- 'flex flex-col rounded-t-2xl bg-[var(--color-drawer-bg)] shadow-[var(--shadow-drawer)]',
165
- className,
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
- <RadixDialog.Overlay
179
- className={ cn(
180
- 'fixed inset-0 z-50 bg-black/50',
181
- 'data-[state=open]:animate-in data-[state=open]:fade-in-0',
182
- 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
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
- <RadixDialog.Content
187
- ref={ ref }
188
- className={ cn(
189
- 'fixed z-50 flex flex-col bg-[var(--color-drawer-bg)] shadow-[var(--shadow-drawer)] duration-300',
190
- PLACEMENT_CLASSES[placement] ?? PLACEMENT_CLASSES.right,
191
- SIZE_CLASSES[placement]?.[size] ?? SIZE_CLASSES.right?.md,
192
- className,
193
- ) }
194
- { ...rest }
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
- </RadixDialog.Content>
208
+ </Dialog.Content>
198
209
  </>
199
210
  );
200
211
 
201
- if (portalled) {
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
- // ─── DrawerCloseTrigger ─────────────────────────────────────────
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
- <RadixDialog.Close asChild>
221
- <CloseButton
222
- ref={ ref }
223
- className={ cn('absolute top-7 right-5', props.className) }
224
- { ...props }
225
- />
226
- </RadixDialog.Close>
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
- // ─── DrawerTrigger ──────────────────────────────────────────────
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
- export const DrawerTrigger = (props: React.ComponentPropsWithoutRef<typeof RadixDialog.Trigger>) => {
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
- export const DrawerHeader = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
240
- function DrawerHeader({ className, ...props }, ref) {
241
- return <div ref={ ref } className={ cn('flex flex-col gap-1.5 p-6 pb-0', className) } { ...props }/>;
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 = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
246
- function DrawerBody({ className, ...props }, ref) {
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 = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
252
- function DrawerFooter({ className, ...props }, ref) {
253
- return <div ref={ ref } className={ cn('flex items-center justify-end gap-2 p-6 pt-0', className) } { ...props }/>;
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 = RadixDialog.Title;
258
- export const DrawerDescription = RadixDialog.Description;
259
- export const DrawerActionTrigger = RadixDialog.Close;
265
+ export const DrawerTitle = Dialog.Title;
266
+ export const DrawerDescription = Dialog.Description;
267
+ export const DrawerActionTrigger = Dialog.Close;
@@ -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 = '\u2019'; // right single quotation mark (typographic apostrophe)
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
- <div
74
- ref={ ref }
75
- className={ cn('flex items-center justify-center py-10', className) }
76
- { ...rest }
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
- <div className="flex flex-col items-center gap-5">
82
+ <YStack alignItems="center" gap={ 20 }>
79
83
  { iconContent && (
80
- <div className="flex items-center justify-center">{ iconContent }</div>
84
+ <XStack alignItems="center" justifyContent="center">{ iconContent }</XStack>
81
85
  ) }
82
86
  { descriptionContent ? (
83
- <div className="flex flex-col items-center gap-2 text-center">
84
- <span className="text-lg font-semibold text-text-secondary">{ titleContent }</span>
85
- <span className="text-sm text-text-secondary">
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
- </span>
88
- </div>
91
+ </Text>
92
+ </YStack>
89
93
  ) : (
90
- <span className="text-lg font-semibold text-text-secondary">{ titleContent }</span>
94
+ <Text fontSize={ 18 } fontWeight="600" textAlign="center" color={ MUTED }>{ titleContent }</Text>
91
95
  ) }
92
96
  { children }
93
- </div>
94
- </div>
97
+ </YStack>
98
+ </XStack>
95
99
  );
96
100
  },
97
101
  );