@luxfi/ui 7.4.0 → 7.4.2

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 (130) hide show
  1. package/dist/avatar.cjs +7 -6
  2. package/dist/avatar.js +7 -5
  3. package/dist/badge.cjs +30 -32
  4. package/dist/badge.js +30 -32
  5. package/dist/bank.d.cts +3 -3
  6. package/dist/bank.d.ts +3 -3
  7. package/dist/checkbox.cjs +4 -4
  8. package/dist/checkbox.d.cts +3 -2
  9. package/dist/checkbox.d.ts +3 -2
  10. package/dist/checkbox.js +5 -4
  11. package/dist/chrome.cjs +950 -0
  12. package/dist/chrome.d.cts +93 -0
  13. package/dist/chrome.d.ts +93 -0
  14. package/dist/chrome.js +921 -0
  15. package/dist/drawer.cjs +86 -20
  16. package/dist/drawer.js +85 -19
  17. package/dist/expiration-selector.cjs +3 -2
  18. package/dist/expiration-selector.js +3 -2
  19. package/dist/greeks-display.cjs +6 -6
  20. package/dist/greeks-display.js +6 -6
  21. package/dist/gui.cjs +13 -0
  22. package/dist/gui.d.cts +2 -0
  23. package/dist/gui.d.ts +2 -0
  24. package/dist/gui.js +2 -0
  25. package/dist/heading.cjs +4 -2
  26. package/dist/heading.js +4 -2
  27. package/dist/iam.cjs +206 -0
  28. package/dist/iam.d.cts +65 -0
  29. package/dist/iam.d.ts +65 -0
  30. package/dist/iam.js +201 -0
  31. package/dist/icons.cjs +13 -0
  32. package/dist/icons.d.cts +1 -0
  33. package/dist/icons.d.ts +1 -0
  34. package/dist/icons.js +2 -0
  35. package/dist/identity.cjs +420 -0
  36. package/dist/identity.d.cts +64 -0
  37. package/dist/identity.d.ts +64 -0
  38. package/dist/identity.js +398 -0
  39. package/dist/index.cjs +1597 -833
  40. package/dist/index.d.cts +7 -3
  41. package/dist/index.d.ts +7 -3
  42. package/dist/index.js +1574 -828
  43. package/dist/input.cjs +3 -1
  44. package/dist/input.d.cts +3 -5
  45. package/dist/input.d.ts +3 -5
  46. package/dist/input.js +3 -1
  47. package/dist/lux.config.cjs +23 -2
  48. package/dist/lux.config.d.cts +28 -10
  49. package/dist/lux.config.d.ts +28 -10
  50. package/dist/lux.config.js +23 -2
  51. package/dist/menu.cjs +4 -4
  52. package/dist/menu.js +4 -4
  53. package/dist/next.cjs +99 -0
  54. package/dist/next.d.cts +29 -0
  55. package/dist/next.d.ts +29 -0
  56. package/dist/next.js +96 -0
  57. package/dist/option-chain.cjs +1 -1
  58. package/dist/option-chain.js +1 -1
  59. package/dist/option-position.cjs +2 -1
  60. package/dist/option-position.js +2 -1
  61. package/dist/pnl-diagram.cjs +8 -6
  62. package/dist/pnl-diagram.js +8 -6
  63. package/dist/progress.cjs +7 -6
  64. package/dist/progress.js +7 -5
  65. package/dist/provider.cjs +136 -4
  66. package/dist/provider.d.cts +15 -3
  67. package/dist/provider.d.ts +15 -3
  68. package/dist/provider.js +136 -5
  69. package/dist/radio.cjs +9 -7
  70. package/dist/radio.js +10 -7
  71. package/dist/separator.cjs +3 -1
  72. package/dist/separator.js +3 -1
  73. package/dist/slider.cjs +28 -16
  74. package/dist/slider.js +28 -15
  75. package/dist/strategy-builder.cjs +2 -1
  76. package/dist/strategy-builder.js +2 -1
  77. package/dist/switch.cjs +11 -12
  78. package/dist/switch.js +12 -12
  79. package/dist/tag.cjs +31 -33
  80. package/dist/tag.js +30 -32
  81. package/dist/textarea.cjs +3 -1
  82. package/dist/textarea.js +3 -1
  83. package/dist/tooltip.cjs +25 -27
  84. package/dist/tooltip.js +24 -26
  85. package/dist/use-narrow.cjs +46 -0
  86. package/dist/use-narrow.d.cts +12 -0
  87. package/dist/use-narrow.d.ts +12 -0
  88. package/dist/use-narrow.js +20 -0
  89. package/dist/vite.cjs +40 -0
  90. package/dist/vite.d.cts +29 -0
  91. package/dist/vite.d.ts +29 -0
  92. package/dist/vite.js +38 -0
  93. package/dist/white-label.cjs +132 -0
  94. package/dist/white-label.d.cts +59 -0
  95. package/dist/white-label.d.ts +59 -0
  96. package/dist/white-label.js +124 -0
  97. package/package.json +85 -19
  98. package/src/avatar.tsx +22 -16
  99. package/src/checkbox.tsx +15 -10
  100. package/src/chrome.tsx +459 -0
  101. package/src/drawer.tsx +83 -10
  102. package/src/expiration-selector.tsx +3 -2
  103. package/src/greeks-display.tsx +9 -6
  104. package/src/gui.ts +17 -0
  105. package/src/heading.tsx +12 -8
  106. package/src/iam.ts +170 -0
  107. package/src/icons.ts +18 -0
  108. package/src/identity.tsx +318 -0
  109. package/src/index.ts +44 -1
  110. package/src/input.tsx +13 -9
  111. package/src/lux.config.ts +16 -15
  112. package/src/menu.tsx +4 -4
  113. package/src/next.ts +142 -0
  114. package/src/option-chain.tsx +1 -1
  115. package/src/option-position.tsx +2 -1
  116. package/src/pnl-diagram.tsx +11 -6
  117. package/src/progress.tsx +15 -8
  118. package/src/provider.tsx +48 -7
  119. package/src/radio.tsx +15 -11
  120. package/src/separator.tsx +8 -3
  121. package/src/slider.tsx +35 -19
  122. package/src/strategy-builder.tsx +3 -2
  123. package/src/switch.tsx +17 -16
  124. package/src/textarea.tsx +8 -4
  125. package/src/tooltip.tsx +2 -21
  126. package/src/use-narrow.ts +40 -0
  127. package/src/vite.ts +78 -0
  128. package/src/white-label.ts +240 -0
  129. package/tokens.css +544 -319
  130. package/src/tokens.css +0 -438
@@ -66,7 +66,12 @@ function buildPath(points: Array<{ x: number; y: number }>, vb: ViewBox, width:
66
66
  // Leg colors
67
67
  // ---------------------------------------------------------------------------
68
68
 
69
- const LEG_COLORS = ['#6366f1', '#f59e0b', '#ec4899', '#06b6d4'];
69
+ // Legs are told apart by position on the achromatic chart ramp, not by hue —
70
+ // and they resolve through tokens.css, so a host reskin moves them too.
71
+ const LEG_COLORS = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)'];
72
+ // Breakeven markers ride the same ramp; the ring is the page ground.
73
+ const BREAKEVEN_COLOR = 'var(--chart-1)';
74
+ const BREAKEVEN_RING = 'var(--background)';
70
75
 
71
76
  // ---------------------------------------------------------------------------
72
77
  // PnlDiagram
@@ -323,10 +328,10 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
323
328
  {/* Breakeven markers */}
324
329
  {breakevens.map((be, i) => (
325
330
  <g key={`be-${i}`}>
326
- <circle cx={scaleX(be)} cy={zeroY} r="3" fill="#fbbf24" stroke="#18181b" strokeWidth="1" />
331
+ <circle cx={scaleX(be)} cy={zeroY} r="3" fill={BREAKEVEN_COLOR} stroke={BREAKEVEN_RING} strokeWidth="1" />
327
332
  <text
328
333
  x={scaleX(be)} y={PADDING.top + chartH + 14}
329
- fill="#fbbf24" fontSize="8" fontFamily="monospace" textAnchor="middle"
334
+ fill={BREAKEVEN_COLOR} fontSize="8" fontFamily="monospace" textAnchor="middle"
330
335
  >
331
336
  {be.toFixed(1)}
332
337
  </text>
@@ -366,7 +371,7 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
366
371
  cy={scaleY(hoverPnl)}
367
372
  r="3"
368
373
  fill={hoverPnl >= 0 ? '#22c55e' : '#ef4444'}
369
- stroke="#18181b"
374
+ stroke={BREAKEVEN_RING}
370
375
  strokeWidth="1"
371
376
  />
372
377
  {/* Tooltip */}
@@ -376,7 +381,7 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
376
381
  width="90"
377
382
  height="28"
378
383
  rx="3"
379
- fill="#18181b"
384
+ fill={BREAKEVEN_RING}
380
385
  stroke="#3f3f46"
381
386
  strokeWidth="1"
382
387
  />
@@ -445,7 +450,7 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
445
450
  ))}
446
451
  {breakevens.length > 0 && (
447
452
  <div className="flex items-center gap-1.5">
448
- <div className="h-1.5 w-1.5 rounded-full bg-amber-400" />
453
+ <div className="h-1.5 w-1.5 rounded-full" style={{ background: BREAKEVEN_COLOR }} />
449
454
  <span className="text-[10px] text-zinc-500">
450
455
  BE: {breakevens.map((b) => b.toFixed(1)).join(', ')}
451
456
  </span>
package/src/progress.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import * as RadixProgress from '@radix-ui/react-progress';
1
+ import { Progress as GuiProgress } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
4
  import { cn } from './utils';
@@ -81,28 +81,35 @@ export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
81
81
  style={ rootStyle }
82
82
  { ...rest }
83
83
  >
84
- <RadixProgress.Root
84
+ <GuiProgress
85
85
  value={ percent }
86
86
  max={ 100 }
87
+ unstyled
87
88
  className={ cn(
88
89
  'w-full overflow-hidden rounded-full',
89
90
  'bg-[var(--color-progress-track)]',
90
91
  SIZE_CLASSES[size],
91
92
  trackProps?.className,
92
93
  ) }
93
- style={ trackStyle }
94
+ style={ trackStyle as never }
94
95
  >
95
- <RadixProgress.Indicator
96
+ { /* gui drives the fill off `value` — it sizes the indicator to 200%
97
+ and translates it — and eases it with the config's transition
98
+ driver. Geometry from the primitive, paint from the Lux classes,
99
+ so the bar is a consequence of state rather than a hand-written
100
+ width + CSS transition. */ }
101
+ <GuiProgress.Indicator
102
+ unstyled
103
+ transition="slow"
96
104
  className={ cn(
97
- 'h-full rounded-full transition-transform duration-500 ease-out',
105
+ 'h-full rounded-full',
98
106
  !color && 'bg-[var(--color-progress-indicator)]',
99
107
  ) }
100
108
  style={{
101
- width: `${ percent }%`,
102
109
  ...(color && { backgroundColor: `var(--color-${ color.replace('.', '-') }, ${ color })` }),
103
- }}
110
+ } as never}
104
111
  />
105
- </RadixProgress.Root>
112
+ </GuiProgress>
106
113
  </div>
107
114
  );
108
115
  },
package/src/provider.tsx CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import React from 'react';
13
13
 
14
14
  import config from './lux.config';
15
+ import { currentHost, resolveWhiteLabel, type WhiteLabel } from './white-label';
15
16
 
16
17
  const defaultQueryClient = new QueryClient({
17
18
  defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, staleTime: 30_000 } },
@@ -43,10 +44,45 @@ const useFontBodyReset = (): void => {
43
44
  }, []);
44
45
  };
45
46
 
47
+ // --- White-label context ---------------------------------------------------
48
+ // Resolved once at the root and read anywhere below. Brand, theme, IAM client
49
+ // and issuer all come from the Host header — see ./white-label.
50
+ const WhiteLabelContext = React.createContext<WhiteLabel>(resolveWhiteLabel(''));
51
+
52
+ /** The white-label identity for the host this page is being served under. */
53
+ export const useWhiteLabel = (): WhiteLabel => React.useContext(WhiteLabelContext);
54
+
55
+ /**
56
+ * Host used for the FIRST render, then upgraded to the browser's real host.
57
+ *
58
+ * Server-rendered surfaces pass `req.headers.host` in, so first paint is already
59
+ * correct and hydration matches. Surfaces that do not pass one render the
60
+ * default row on the server and swap to the host-resolved row in an effect —
61
+ * deterministic on both sides, so there is no hydration mismatch either way.
62
+ */
63
+ const useHostWhiteLabel = (host: string | undefined): WhiteLabel => {
64
+ const [ resolvedHost, setResolvedHost ] = React.useState(host ?? '');
65
+ React.useEffect(() => {
66
+ if (host !== undefined) return;
67
+ const browserHost = currentHost();
68
+ if (browserHost) setResolvedHost(browserHost);
69
+ }, [ host ]);
70
+ return React.useMemo(() => resolveWhiteLabel(resolvedHost), [ resolvedHost ]);
71
+ };
72
+
46
73
  interface AppProviderProps {
47
74
  children: React.ReactNode;
48
75
  queryClient?: QueryClient;
49
- /** Lux brand child-theme row: dark_lux | dark_zoo | dark_hanzo | dark_pars */
76
+ /**
77
+ * Host header value, for server rendering. Omit in the browser and the host
78
+ * is read from `location`. Never bake a brand into a deployment — pass the
79
+ * request's host and the same image serves every org.
80
+ */
81
+ host?: string;
82
+ /**
83
+ * Escape hatch: pin the gui child-theme row (`dark_lux` | `dark_zoo` |
84
+ * `dark_hanzo` | `dark_pars`). Leave unset so the theme follows the host.
85
+ */
50
86
  defaultTheme?: string;
51
87
  }
52
88
 
@@ -55,15 +91,20 @@ interface AppProviderProps {
55
91
  export const AppProvider = ({
56
92
  children,
57
93
  queryClient,
58
- defaultTheme = 'dark_lux',
94
+ host,
95
+ defaultTheme,
59
96
  }: AppProviderProps): React.ReactElement => {
60
97
  useFontBodyReset();
98
+ const whiteLabel = useHostWhiteLabel(host);
99
+ const theme = defaultTheme ?? whiteLabel.theme;
61
100
  return (
62
- <GuiProvider config={config} defaultTheme={defaultTheme as never}>
63
- <QueryClientProvider client={queryClient ?? defaultQueryClient}>
64
- {children}
65
- </QueryClientProvider>
66
- </GuiProvider>
101
+ <WhiteLabelContext.Provider value={whiteLabel}>
102
+ <GuiProvider config={config} defaultTheme={theme as never}>
103
+ <QueryClientProvider client={queryClient ?? defaultQueryClient}>
104
+ {children}
105
+ </QueryClientProvider>
106
+ </GuiProvider>
107
+ </WhiteLabelContext.Provider>
67
108
  );
68
109
  };
69
110
 
package/src/radio.tsx CHANGED
@@ -1,5 +1,4 @@
1
- import * as RadixRadioGroup from '@radix-ui/react-radio-group';
2
- // chakra() HOC removed — pure Radix + Tailwind
1
+ import { RadioGroup as GuiRadioGroup } from '@hanzo/gui';
3
2
  import * as React from 'react';
4
3
 
5
4
  import { cn } from './utils';
@@ -60,7 +59,8 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
60
59
  disabled = false,
61
60
  readOnly = false,
62
61
  orientation = 'vertical',
63
- loop = true,
62
+ // `loop` was a Radix-only knob; gui's roving focus always wraps.
63
+ loop: _loop = true,
64
64
  defaultValue,
65
65
  value,
66
66
  onValueChange,
@@ -79,13 +79,15 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
79
79
 
80
80
  return (
81
81
  <RadioSizeContext.Provider value={ size }>
82
- <RadixRadioGroup.Root
83
- ref={ ref }
82
+ { /* gui's RadioGroup — role=radiogroup / role=radio and roving focus
83
+ from its headless core, real Pressables on native. */ }
84
+ <GuiRadioGroup
85
+ ref={ ref as never }
86
+ unstyled
84
87
  name={ name }
85
88
  required={ required }
86
89
  disabled={ disabled || readOnly }
87
90
  orientation={ orientation }
88
- loop={ loop }
89
91
  defaultValue={ defaultValue }
90
92
  value={ value ?? undefined }
91
93
  onValueChange={ handleValueChange }
@@ -94,10 +96,10 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
94
96
  orientation === 'vertical' ? 'flex-col gap-3' : 'flex-row gap-6',
95
97
  className,
96
98
  ) }
97
- { ...rest }
99
+ { ...(rest as object) }
98
100
  >
99
101
  { children }
100
- </RadixRadioGroup.Root>
102
+ </GuiRadioGroup>
101
103
  </RadioSizeContext.Provider>
102
104
  );
103
105
  },
@@ -133,7 +135,8 @@ const RadioBase = React.forwardRef<HTMLInputElement, RadioProps>(
133
135
  data-disabled={ disabled || undefined }
134
136
  { ...rest }
135
137
  >
136
- <RadixRadioGroup.Item
138
+ <GuiRadioGroup.Item
139
+ unstyled
137
140
  value={ value }
138
141
  disabled={ disabled }
139
142
  className={ cn(
@@ -145,13 +148,14 @@ const RadioBase = React.forwardRef<HTMLInputElement, RadioProps>(
145
148
  sizeClasses.control,
146
149
  ) }
147
150
  >
148
- <RadixRadioGroup.Indicator
151
+ <GuiRadioGroup.Indicator
152
+ unstyled
149
153
  className={ cn(
150
154
  'block rounded-full bg-gray-800 dark:bg-white',
151
155
  sizeClasses.indicator,
152
156
  ) }
153
157
  />
154
- </RadixRadioGroup.Item>
158
+ </GuiRadioGroup.Item>
155
159
  { /* Hidden native input for form compatibility and ref forwarding */ }
156
160
  <input
157
161
  ref={ ref }
package/src/separator.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { Separator as GuiSeparator } from '@hanzo/gui';
1
2
  import React from 'react';
2
3
 
3
4
  import { cn } from './utils';
@@ -32,6 +33,9 @@ const VERTICAL_SIZE_CLASSES: Record<Size, string> = {
32
33
  lg: 'border-l-[3px]',
33
34
  };
34
35
 
36
+ // gui's Separator wearing the Lux border scale. `vertical` is the primitive's
37
+ // own orientation variant, so it lays out correctly on native too; the Tailwind
38
+ // classes only carry border weight and dash pattern.
35
39
  export const Separator = React.forwardRef<HTMLHRElement, SeparatorProps>(
36
40
  function Separator(
37
41
  { orientation = 'horizontal', variant = 'solid', size = 'sm', className, ...rest },
@@ -40,8 +44,9 @@ export const Separator = React.forwardRef<HTMLHRElement, SeparatorProps>(
40
44
  const isVertical = orientation === 'vertical';
41
45
 
42
46
  return (
43
- <hr
44
- ref={ ref }
47
+ <GuiSeparator
48
+ ref={ ref as never }
49
+ vertical={ isVertical }
45
50
  role="separator"
46
51
  aria-orientation={ orientation }
47
52
  className={ cn(
@@ -52,7 +57,7 @@ export const Separator = React.forwardRef<HTMLHRElement, SeparatorProps>(
52
57
  isVertical ? 'self-stretch h-auto w-0' : 'w-full',
53
58
  className,
54
59
  ) }
55
- { ...rest }
60
+ { ...(rest as object) }
56
61
  />
57
62
  );
58
63
  },
package/src/slider.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import * as RadixSlider from '@radix-ui/react-slider';
1
+ import { Slider as GuiSlider } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
4
  import { cn } from './utils';
@@ -20,9 +20,10 @@ export interface SliderProps {
20
20
  readonly className?: string;
21
21
  }
22
22
 
23
- const THUMB_SIZE = 20;
24
- const TRACK_HEIGHT = 6;
25
-
23
+ // gui's Slider: pointer, touch and keyboard from one implementation, with the
24
+ // drag handled as a pan gesture by the engine rather than by mouse events — so it
25
+ // works under a finger, not only under a cursor. `role=slider` and the arrow-key
26
+ // contract come from the primitive.
26
27
  export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
27
28
  function Slider(props, ref) {
28
29
  const {
@@ -35,7 +36,7 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
35
36
  max = 100,
36
37
  step,
37
38
  disabled,
38
- orientation,
39
+ orientation = 'horizontal',
39
40
  name,
40
41
  onValueChange,
41
42
  onValueCommit,
@@ -51,6 +52,17 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
51
52
 
52
53
  const hasMarkLabel = Boolean(marks?.some((mark) => mark.label));
53
54
 
55
+ // gui signals the end of a drag without echoing the value, so keep the last
56
+ // one to satisfy the Chakra-shaped `onValueCommit(value)` contract.
57
+ const latest = React.useRef<Array<number>>(value ?? defaultValue ?? []);
58
+ const handleValueChange = React.useCallback((next: Array<number>) => {
59
+ latest.current = next;
60
+ onValueChange?.(next);
61
+ }, [ onValueChange ]);
62
+ const handleSlideEnd = React.useCallback(() => {
63
+ onValueCommit?.(latest.current);
64
+ }, [ onValueCommit ]);
65
+
54
66
  return (
55
67
  <div className={ cn('flex flex-col gap-1', className) }>
56
68
  { label && !showValue && (
@@ -69,8 +81,9 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
69
81
  </div>
70
82
  ) }
71
83
 
72
- <RadixSlider.Root
73
- ref={ ref }
84
+ <GuiSlider
85
+ ref={ ref as never }
86
+ unstyled
74
87
  className={ cn(
75
88
  'relative flex w-full touch-none select-none items-center',
76
89
  hasMarkLabel && 'mb-6',
@@ -83,23 +96,26 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
83
96
  disabled={ disabled }
84
97
  orientation={ orientation }
85
98
  name={ name }
86
- onValueChange={ onValueChange }
87
- onValueCommit={ onValueCommit }
99
+ onValueChange={ handleValueChange }
100
+ onSlideEnd={ handleSlideEnd }
88
101
  >
89
- <RadixSlider.Track
90
- className={ cn(
91
- 'relative grow rounded-full bg-border-divider',
92
- `h-[${ TRACK_HEIGHT }px]`,
93
- ) }
102
+ <GuiSlider.Track
103
+ unstyled
104
+ className="relative grow h-1.5 rounded-full bg-border-divider"
94
105
  >
95
- <RadixSlider.Range className="absolute h-full rounded-full bg-link-primary"/>
96
- </RadixSlider.Track>
106
+ <GuiSlider.TrackActive
107
+ unstyled
108
+ className="absolute h-full rounded-full bg-link-primary"
109
+ />
110
+ </GuiSlider.Track>
97
111
 
98
112
  { resolvedValue?.map((_, index) => (
99
- <RadixSlider.Thumb
113
+ <GuiSlider.Thumb
100
114
  key={ index }
115
+ index={ index }
116
+ unstyled
101
117
  className={ cn(
102
- `block h-[${ THUMB_SIZE }px] w-[${ THUMB_SIZE }px]`,
118
+ 'block h-5 w-5',
103
119
  'rounded-full border-2 border-link-primary bg-white',
104
120
  'shadow-sm transition-colors',
105
121
  'hover:border-link-primary-hover',
@@ -108,7 +124,7 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
108
124
  ) }
109
125
  />
110
126
  )) }
111
- </RadixSlider.Root>
127
+ </GuiSlider>
112
128
 
113
129
  <SliderMarks marks={ marks } min={ min } max={ max }/>
114
130
  </div>
@@ -207,9 +207,10 @@ function LegRow({ leg, index, strikes, expirations, removable, onChange, onRemov
207
207
  type="button"
208
208
  className={cn(
209
209
  'px-2 py-0.5 rounded text-xs font-medium transition-colors',
210
+ // Call vs put is a rank difference, not a hue difference.
210
211
  leg.type === 'call'
211
- ? 'bg-blue-900/40 text-blue-400'
212
- : 'bg-orange-900/40 text-orange-400',
212
+ ? 'bg-[var(--secondary)] text-[var(--foreground)]'
213
+ : 'bg-[var(--muted)] text-[var(--muted-foreground)]',
213
214
  )}
214
215
  onClick={() => update({ type: leg.type === 'call' ? 'put' : 'call' })}
215
216
  >
package/src/switch.tsx CHANGED
@@ -1,5 +1,4 @@
1
- import * as RadixSwitch from '@radix-ui/react-switch';
2
- // chakra() HOC removed — pure Radix + Tailwind
1
+ import { Switch as GuiSwitch } from '@hanzo/gui';
3
2
  import * as React from 'react';
4
3
 
5
4
  import { cn } from './utils';
@@ -7,20 +6,23 @@ import { cn } from './utils';
7
6
  const NOOP = () => { /* noop */ };
8
7
 
9
8
  // ─── Size mappings ────────────────────────────────────────────────────
9
+ // Paint and box size only. Thumb travel is NOT a hardcoded translate any more:
10
+ // gui measures the track and the knob and moves the knob by the difference, so
11
+ // the switch stays correct at any size and on native.
10
12
  const SIZE_CLASSES = {
11
13
  sm: {
12
14
  root: 'h-4 w-7',
13
- thumb: 'h-3 w-3 data-[state=checked]:translate-x-3',
15
+ thumb: 'h-3 w-3',
14
16
  label: 'text-xs',
15
17
  },
16
18
  md: {
17
19
  root: 'h-5 w-9',
18
- thumb: 'h-4 w-4 data-[state=checked]:translate-x-4',
20
+ thumb: 'h-4 w-4',
19
21
  label: 'text-sm',
20
22
  },
21
23
  lg: {
22
24
  root: 'h-6 w-11',
23
- thumb: 'h-5 w-5 data-[state=checked]:translate-x-5',
25
+ thumb: 'h-5 w-5',
24
26
  label: 'text-base',
25
27
  },
26
28
  } as const;
@@ -72,11 +74,8 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
72
74
  setInternalChecked(nextChecked);
73
75
  }
74
76
 
75
- if (onCheckedChange) {
76
- // Support both Chakra-style ({ checked }) and Radix-style (checked) signatures.
77
- // We call with a single object arg; if the consumer ignores it, that's fine.
78
- (onCheckedChange as (details: { checked: boolean }) => void)({ checked: nextChecked });
79
- }
77
+ // Chakra-shaped callback ({ checked }) — the contract consumers already use.
78
+ onCheckedChange?.({ checked: nextChecked });
80
79
  },
81
80
  [ isControlled, onCheckedChange ],
82
81
  );
@@ -97,7 +96,8 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
97
96
  data-disabled={ disabled || undefined }
98
97
  { ...rest }
99
98
  >
100
- <RadixSwitch.Root
99
+ <GuiSwitch
100
+ unstyled
101
101
  checked={ checkedState }
102
102
  onCheckedChange={ handleCheckedChange }
103
103
  disabled={ disabled }
@@ -115,11 +115,12 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
115
115
  { checkedState ? trackLabel.on : trackLabel.off }
116
116
  </span>
117
117
  ) }
118
- <RadixSwitch.Thumb
118
+ <GuiSwitch.Thumb
119
+ unstyled
120
+ data-thumb
121
+ transition="quicker"
119
122
  className={ cn(
120
123
  'block rounded-full bg-white shadow-sm',
121
- 'transition-transform duration-200',
122
- 'translate-x-0.5',
123
124
  sizeClasses.thumb,
124
125
  ) }
125
126
  >
@@ -128,8 +129,8 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
128
129
  { checkedState ? thumbLabel.on : thumbLabel.off }
129
130
  </span>
130
131
  ) }
131
- </RadixSwitch.Thumb>
132
- </RadixSwitch.Root>
132
+ </GuiSwitch.Thumb>
133
+ </GuiSwitch>
133
134
  { /* Hidden native input for form compat and ref forwarding */ }
134
135
  <input
135
136
  ref={ ref }
package/src/textarea.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { TextArea as GuiTextArea } from '@hanzo/gui';
1
2
  import React from 'react';
2
3
 
3
4
  import { cn } from './utils';
@@ -38,6 +39,8 @@ export interface TextareaProps extends Omit<React.ComponentPropsWithRef<'textare
38
39
  readonly onChangeText?: (text: string) => void;
39
40
  }
40
41
 
42
+ // gui's TextArea — multiline TextInput on native, `<textarea>` on web — wearing
43
+ // the Lux size/variant classes.
41
44
  export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
42
45
  ({ size = 'md', variant = 'outline', className, onChange, onChangeText, ...rest }, ref) => {
43
46
  const handleChange = React.useCallback(
@@ -48,11 +51,12 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
48
51
  [ onChange, onChangeText ],
49
52
  );
50
53
  return (
51
- <textarea
52
- ref={ ref }
54
+ <GuiTextArea
55
+ ref={ ref as never }
56
+ unstyled
53
57
  className={ cn(TEXTAREA_BASE, TEXTAREA_SIZE_CLASSES[size], TEXTAREA_VARIANT_CLASSES[variant], className) }
54
- { ...rest }
55
- onChange={ handleChange }
58
+ { ...(rest as object) }
59
+ onChange={ handleChange as never }
56
60
  />
57
61
  );
58
62
  },
package/src/tooltip.tsx CHANGED
@@ -5,28 +5,9 @@ import {
5
5
  import { useClickAway } from '@uidotdev/usehooks';
6
6
  import * as React from 'react';
7
7
 
8
+ import { useIsTouch } from './use-narrow';
8
9
  import { cn } from './utils';
9
10
 
10
- // Inline useIsMobile hook (matches lg breakpoint = 1024px)
11
- const LG_BREAKPOINT = 1024;
12
-
13
- function useIsMobile(): boolean {
14
- const [ isMobile, setIsMobile ] = React.useState(() => {
15
- if (typeof window === 'undefined') return false;
16
- return window.innerWidth < LG_BREAKPOINT;
17
- });
18
-
19
- React.useEffect(() => {
20
- const mql = window.matchMedia(`(max-width: ${ LG_BREAKPOINT - 1 }px)`);
21
- const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
22
- setIsMobile(mql.matches);
23
- mql.addEventListener('change', handler);
24
- return () => mql.removeEventListener('change', handler);
25
- }, []);
26
-
27
- return isMobile;
28
- }
29
-
30
11
  /** Map placement string to @hanzogui Popper placement prop. */
31
12
  function mapPlacement(p: string | undefined): string | undefined {
32
13
  if (!p) return undefined;
@@ -86,7 +67,7 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
86
67
  const [ open, setOpen ] = React.useState<boolean>(defaultOpen);
87
68
  const timeoutRef = React.useRef<number | null>(null);
88
69
 
89
- const isMobile = useIsMobile();
70
+ const isMobile = useIsTouch();
90
71
 
91
72
  const handleOpenChange = React.useCallback((nextOpen: boolean) => {
92
73
  setOpen(nextOpen);
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ // ONE viewport hook for the whole design system.
6
+ //
7
+ // Three components used to carry their own copy of the same `matchMedia`
8
+ // dance at three different breakpoints, so "is this phone-sized?" had three
9
+ // answers. It has one now, and the breakpoints are named values rather than
10
+ // magic numbers buried in a component.
11
+ //
12
+ // SSR-safe by construction: the first render is always `false` on both the
13
+ // server and the client (there is no viewport during prerender, and guessing
14
+ // one produces a hydration mismatch), then the real answer arrives in an
15
+ // effect before paint.
16
+
17
+ /** Below this the UI is a phone: menus become sheets, drawers drag. */
18
+ export const NARROW_PX = 640;
19
+
20
+ /** Below this the pointer is a finger: hover-only affordances are suppressed. */
21
+ export const TOUCH_PX = 1024;
22
+
23
+ /** True while the viewport is narrower than `px`. */
24
+ export function useBelow(px: number): boolean {
25
+ const [ below, setBelow ] = React.useState(false);
26
+ React.useEffect(() => {
27
+ const mql = window.matchMedia(`(max-width: ${ px - 1 }px)`);
28
+ const apply = () => setBelow(mql.matches);
29
+ apply();
30
+ mql.addEventListener('change', apply);
31
+ return () => mql.removeEventListener('change', apply);
32
+ }, [ px ]);
33
+ return below;
34
+ }
35
+
36
+ /** Phone-sized: present menus as sheets. */
37
+ export const useIsNarrow = (): boolean => useBelow(NARROW_PX);
38
+
39
+ /** Touch-sized: no hover, so hover-only affordances must not be the only path. */
40
+ export const useIsTouch = (): boolean => useBelow(TOUCH_PX);