@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
package/src/pin-input.tsx CHANGED
@@ -1,7 +1,6 @@
1
+ import { Input as GuiInput, XStack } from '@hanzo/gui';
1
2
  import * as React from 'react';
2
3
 
3
- import { cn } from './utils';
4
-
5
4
  export interface PinInputProps {
6
5
  readonly rootRef?: React.Ref<HTMLDivElement>;
7
6
  readonly count?: number;
@@ -19,21 +18,31 @@ export interface PinInputProps {
19
18
  readonly className?: string;
20
19
  }
21
20
 
22
- const INPUT_BASE = [
23
- 'w-10 h-10',
24
- 'text-center text-sm font-medium',
25
- 'border-2 rounded-md',
26
- 'outline-none appearance-none',
27
- 'bg-[var(--color-input-bg)] text-[var(--color-input-fg)]',
28
- 'border-[var(--color-input-border)]',
29
- 'placeholder:text-[var(--color-input-placeholder)]',
30
- 'hover:border-[var(--color-input-border-hover)]',
31
- 'focus:border-[var(--color-input-border-focus)] focus:shadow-md',
32
- 'disabled:opacity-40',
33
- ].join(' ');
34
-
35
- const INPUT_FILLED = 'border-[var(--color-input-border)]';
36
- const INPUT_INVALID = 'border-[var(--color-border-error)] hover:border-[var(--color-border-error)]';
21
+ // One box's frame. Colour and focus/hover/disabled states are the same
22
+ // `--color-input-*` tokens `Input` itself uses — a pin box is a text control,
23
+ // just a square one. `::placeholder` and the invalid border are the existing
24
+ // `.lux-control` rule in tokens.css (a pseudo-element and a selector, neither
25
+ // of which a prop reaches); `data-invalid` is what wires this box into it.
26
+ const BOX = {
27
+ width: 40,
28
+ height: 40,
29
+ textAlign: 'center' as const,
30
+ fontSize: 14,
31
+ fontWeight: 500 as const,
32
+ borderRadius: 6,
33
+ borderWidth: 2,
34
+ borderStyle: 'solid' as const,
35
+ outlineWidth: 0,
36
+ backgroundColor: 'var(--color-input-bg)' as never,
37
+ color: 'var(--color-input-fg)' as never,
38
+ borderColor: 'var(--color-input-border)' as never,
39
+ hoverStyle: { borderColor: 'var(--color-input-border-hover)' as never },
40
+ focusStyle: {
41
+ borderColor: 'var(--color-input-border-focus)' as never,
42
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
43
+ },
44
+ disabledStyle: { opacity: 0.4 },
45
+ };
37
46
 
38
47
  export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
39
48
  function PinInput(props, ref) {
@@ -142,10 +151,10 @@ export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
142
151
  handleKeyDown(index, e);
143
152
  }, [ handleKeyDown ]);
144
153
 
145
- const bgStyle = bgColor ? { backgroundColor: `var(--color-${ bgColor.replace(/\./g, '-') })` } : undefined;
154
+ const boxBackground = bgColor ? `var(--color-${ bgColor.replace(/\./g, '-') })` as never : BOX.backgroundColor;
146
155
 
147
156
  return (
148
- <div ref={ rootRef } className={ cn('inline-flex items-center', attached ? 'gap-0' : 'gap-2', className) }>
157
+ <XStack ref={ rootRef as never } alignItems="center" gap={ attached ? 0 : 8 } className={ className }>
149
158
  { /* Hidden input for form submission */ }
150
159
  <input
151
160
  ref={ ref }
@@ -155,9 +164,10 @@ export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
155
164
  { ...inputProps }
156
165
  />
157
166
  { Array.from({ length: count }).map((_, index) => (
158
- <input
167
+ <GuiInput
159
168
  key={ index }
160
- ref={ setInputRef(index) }
169
+ ref={ setInputRef(index) as never }
170
+ unstyled
161
171
  type="text"
162
172
  inputMode="numeric"
163
173
  autoComplete={ otp ? 'one-time-code' : 'off' }
@@ -166,14 +176,12 @@ export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
166
176
  placeholder={ placeholder }
167
177
  disabled={ disabled }
168
178
  aria-invalid={ invalid || undefined }
179
+ data-invalid={ invalid || undefined }
169
180
  value={ values[index] || '' }
170
- className={ cn(
171
- INPUT_BASE,
172
- values[index] && INPUT_FILLED,
173
- invalid && INPUT_INVALID,
174
- attached && index > 0 && '-ml-0.5',
175
- ) }
176
- style={ bgStyle }
181
+ className="lux-control"
182
+ { ...BOX }
183
+ backgroundColor={ boxBackground }
184
+ marginLeft={ attached && index > 0 ? -2 : undefined }
177
185
  onInput={ onInputAtIndex(index) }
178
186
  onKeyDown={ onKeyDownAtIndex(index) }
179
187
  onPaste={ handlePaste }
@@ -181,7 +189,7 @@ export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
181
189
  onChange={ handleNoop }
182
190
  />
183
191
  )) }
184
- </div>
192
+ </XStack>
185
193
  );
186
194
  },
187
195
  );
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Text, View, XStack } from '@hanzo/gui';
3
4
  import * as React from 'react';
4
5
 
5
- import { cn } from './utils';
6
-
7
6
  import type { StrategyLeg } from './strategy-builder';
8
7
 
9
8
  // ---------------------------------------------------------------------------
@@ -207,17 +206,24 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
207
206
  );
208
207
 
209
208
  return (
210
- <div
211
- ref={ref}
212
- className={cn('w-full', className)}
213
- {...rest}
209
+ <View
210
+ ref={ref as never}
211
+ unstyled
212
+ width="100%"
213
+ className={className}
214
+ {...(rest as object)}
214
215
  >
215
- <div ref={containerRef} className="w-full h-full min-h-[200px]">
216
+ <View unstyled ref={containerRef as never} width="100%" height="100%" minHeight={200}>
217
+ {/* This <svg> and everything inside it is the chart itself — left exactly as
218
+ it was: every fill/stroke/d/viewBox below is an SVG presentation attribute,
219
+ not Tailwind, and out of scope for a className→prop conversion. The one
220
+ change is `select-none`, a real Tailwind utility (not a presentation
221
+ attribute), moved to an equivalent inline style with no visual effect. */}
216
222
  <svg
217
223
  width={width}
218
224
  height={height}
219
225
  viewBox={`0 0 ${width} ${height}`}
220
- className="select-none"
226
+ style={{ userSelect: 'none' }}
221
227
  onMouseMove={handleMouseMove}
222
228
  onMouseLeave={handleMouseLeave}
223
229
  >
@@ -425,39 +431,41 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
425
431
  );
426
432
  })}
427
433
  </svg>
428
- </div>
434
+ </View>
429
435
 
430
436
  {/* Legend */}
431
- <div className="flex flex-wrap items-center gap-4 px-2 pt-1">
432
- <div className="flex items-center gap-1.5">
433
- <div className="h-0.5 w-4 bg-zinc-200 rounded" />
434
- <span className="text-[10px] text-zinc-400">Combined</span>
435
- </div>
437
+ <XStack flexWrap="wrap" alignItems="center" gap={16} paddingHorizontal={8} paddingTop={4}>
438
+ <XStack alignItems="center" gap={6}>
439
+ <View height={2} width={16} borderRadius={4} backgroundColor={'var(--color-gray-200)' as never} />
440
+ <Text fontSize={10} color={'var(--color-text-secondary)' as never}>Combined</Text>
441
+ </XStack>
436
442
  {legs.map((leg, i) => (
437
- <div key={i} className="flex items-center gap-1.5">
438
- <div
439
- className="h-0.5 w-4 rounded"
443
+ <XStack key={i} alignItems="center" gap={6}>
444
+ <View
445
+ height={2}
446
+ width={16}
447
+ borderRadius={4}
440
448
  style={{
441
449
  backgroundColor: LEG_COLORS[i % LEG_COLORS.length],
442
450
  opacity: 0.6,
443
451
  backgroundImage: `repeating-linear-gradient(90deg, transparent, transparent 2px, ${LEG_COLORS[i % LEG_COLORS.length]}00 2px, ${LEG_COLORS[i % LEG_COLORS.length]}00 5px)`,
444
- }}
452
+ } as never}
445
453
  />
446
- <span className="text-[10px] text-zinc-500">
454
+ <Text fontSize={10} color={'var(--color-text-secondary)' as never}>
447
455
  {leg.action === 'buy' ? 'B' : 'S'} {leg.quantity} {leg.strike} {leg.type === 'call' ? 'C' : 'P'}
448
- </span>
449
- </div>
456
+ </Text>
457
+ </XStack>
450
458
  ))}
451
459
  {breakevens.length > 0 && (
452
- <div className="flex items-center gap-1.5">
453
- <div className="h-1.5 w-1.5 rounded-full" style={{ background: BREAKEVEN_COLOR }} />
454
- <span className="text-[10px] text-zinc-500">
460
+ <XStack alignItems="center" gap={6}>
461
+ <View width={6} height={6} borderRadius={9999} style={{ background: BREAKEVEN_COLOR } as never} />
462
+ <Text fontSize={10} color={'var(--color-text-secondary)' as never}>
455
463
  BE: {breakevens.map((b) => b.toFixed(1)).join(', ')}
456
- </span>
457
- </div>
464
+ </Text>
465
+ </XStack>
458
466
  )}
459
- </div>
460
- </div>
467
+ </XStack>
468
+ </View>
461
469
  );
462
470
  },
463
471
  );
package/src/popover.tsx CHANGED
@@ -195,7 +195,7 @@ export const PopoverContent = React.forwardRef<
195
195
  className={ cn(
196
196
  'z-50 rounded-lg border border-[var(--color-popover-border,var(--color-border-divider))]',
197
197
  'bg-[var(--color-popover-bg,var(--color-dialog-bg))]',
198
- 'shadow-[var(--shadow-popover,var(--shadow-lg))]',
198
+ 'shadow-[var(--shadow-popover)]',
199
199
  'outline-none',
200
200
  'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
201
201
  'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
package/src/progress.tsx CHANGED
@@ -1,16 +1,14 @@
1
1
  import { Progress as GuiProgress } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
- const SIZE_CLASSES = {
7
- sm: 'h-1',
8
- md: 'h-2',
9
- lg: 'h-3',
10
- xl: 'h-4',
4
+ const SIZE = {
5
+ sm: 4,
6
+ md: 8,
7
+ lg: 12,
8
+ xl: 16,
11
9
  } as const;
12
10
 
13
- type Size = keyof typeof SIZE_CLASSES;
11
+ type Size = keyof typeof SIZE;
14
12
 
15
13
  interface TrackProps {
16
14
  readonly borderStartRadius?: number | string;
@@ -47,7 +45,6 @@ export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
47
45
  size = 'md',
48
46
  color,
49
47
  trackProps,
50
- className,
51
48
  style,
52
49
  w,
53
50
  flex,
@@ -69,6 +66,7 @@ export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
69
66
  };
70
67
 
71
68
  const rootStyle: React.CSSProperties = {
69
+ position: 'relative',
72
70
  ...style,
73
71
  ...(w !== undefined && { width: w === 'full' ? '100%' : w }),
74
72
  ...(flex !== undefined && { flex }),
@@ -77,7 +75,6 @@ export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
77
75
  return (
78
76
  <div
79
77
  ref={ ref }
80
- className={ cn('relative', className) }
81
78
  style={ rootStyle }
82
79
  { ...rest }
83
80
  >
@@ -85,26 +82,25 @@ export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
85
82
  value={ percent }
86
83
  max={ 100 }
87
84
  unstyled
88
- className={ cn(
89
- 'w-full overflow-hidden rounded-full',
90
- 'bg-[var(--color-progress-track)]',
91
- SIZE_CLASSES[size],
92
- trackProps?.className,
93
- ) }
85
+ className={ trackProps?.className }
86
+ width="100%"
87
+ overflow="hidden"
88
+ borderRadius={ 9999 }
89
+ height={ SIZE[size] }
90
+ backgroundColor={ 'var(--color-progress-track)' as never }
94
91
  style={ trackStyle as never }
95
92
  >
96
93
  { /* gui drives the fill off `value` — it sizes the indicator to 200%
97
94
  and translates it — and eases it with the config's transition
98
- driver. Geometry from the primitive, paint from the Lux classes,
95
+ driver. Geometry from the primitive, paint from these props,
99
96
  so the bar is a consequence of state rather than a hand-written
100
97
  width + CSS transition. */ }
101
98
  <GuiProgress.Indicator
102
99
  unstyled
103
100
  transition="slow"
104
- className={ cn(
105
- 'h-full rounded-full',
106
- !color && 'bg-[var(--color-progress-indicator)]',
107
- ) }
101
+ height="100%"
102
+ borderRadius={ 9999 }
103
+ backgroundColor={ (color ? undefined : 'var(--color-progress-indicator)') as never }
108
104
  style={{
109
105
  ...(color && { backgroundColor: `var(--color-${ color.replace('.', '-') }, ${ color })` }),
110
106
  } as never}
package/src/radio.tsx CHANGED
@@ -1,37 +1,19 @@
1
- import { RadioGroup as GuiRadioGroup } from '@hanzo/gui';
1
+ import { Label, RadioGroup as GuiRadioGroup, Text, VisuallyHidden } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
- // ─── Size classes ───────────────────────────────────────────────────
7
- const SIZE_CLASSES = {
8
- xs: {
9
- root: 'gap-1.5',
10
- control: 'h-3 w-3',
11
- indicator: 'h-1.5 w-1.5',
12
- label: 'text-xs',
13
- },
14
- sm: {
15
- root: 'gap-1.5',
16
- control: 'h-3.5 w-3.5',
17
- indicator: 'h-1.5 w-1.5',
18
- label: 'text-xs',
19
- },
20
- md: {
21
- root: 'gap-2',
22
- control: 'h-4 w-4',
23
- indicator: 'h-2 w-2',
24
- label: 'text-sm',
25
- },
26
- lg: {
27
- root: 'gap-2.5',
28
- control: 'h-5 w-5',
29
- indicator: 'h-2.5 w-2.5',
30
- label: 'text-base',
31
- },
4
+ // ─── Size mappings ──────────────────────────────────────────────────
5
+ const SIZE = {
6
+ xs: { rootGap: 6, control: 12, indicator: 6, label: 12 },
7
+ sm: { rootGap: 6, control: 14, indicator: 6, label: 12 },
8
+ md: { rootGap: 8, control: 16, indicator: 8, label: 14 },
9
+ lg: { rootGap: 10, control: 20, indicator: 10, label: 16 },
32
10
  } as const;
33
11
 
34
- type RadioSize = keyof typeof SIZE_CLASSES;
12
+ type RadioSize = keyof typeof SIZE;
13
+
14
+ const INK = 'var(--color-text-primary)' as never;
15
+ const RESTING_BORDER = 'var(--color-input-border)' as never;
16
+ const OUTLINE = 'var(--color-gray-500)' as never;
35
17
 
36
18
  // ─── Size context ───────────────────────────────────────────────────
37
19
  const RadioSizeContext = React.createContext<RadioSize>('md');
@@ -65,7 +47,6 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
65
47
  value,
66
48
  onValueChange,
67
49
  size = 'md',
68
- className,
69
50
  ...rest
70
51
  } = props;
71
52
 
@@ -91,11 +72,9 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
91
72
  defaultValue={ defaultValue }
92
73
  value={ value ?? undefined }
93
74
  onValueChange={ handleValueChange }
94
- className={ cn(
95
- 'flex',
96
- orientation === 'vertical' ? 'flex-col gap-3' : 'flex-row gap-6',
97
- className,
98
- ) }
75
+ display="flex"
76
+ flexDirection={ orientation === 'vertical' ? 'column' : 'row' }
77
+ gap={ orientation === 'vertical' ? 12 : 24 }
99
78
  { ...(rest as object) }
100
79
  >
101
80
  { children }
@@ -119,59 +98,61 @@ export interface RadioProps extends Omit<React.ComponentPropsWithoutRef<'label'>
119
98
 
120
99
  const RadioBase = React.forwardRef<HTMLInputElement, RadioProps>(
121
100
  function Radio(props, ref) {
122
- const { children, inputProps, rootRef, value, disabled, className, ...rest } = props;
101
+ const { children, inputProps, rootRef, value, disabled, ...rest } = props;
123
102
  const size = React.useContext(RadioSizeContext);
124
- const sizeClasses = SIZE_CLASSES[size];
103
+ const s = SIZE[size];
125
104
 
126
105
  return (
127
- <label
128
- ref={ rootRef }
129
- className={ cn(
130
- 'inline-flex items-center cursor-pointer select-none',
131
- sizeClasses.root,
132
- disabled && 'opacity-50 cursor-not-allowed',
133
- className,
134
- ) }
106
+ <Label
107
+ ref={ rootRef as never }
108
+ unstyled
109
+ flexDirection="row"
110
+ alignItems="center"
111
+ gap={ s.rootGap }
112
+ cursor={ disabled ? 'not-allowed' : 'pointer' }
113
+ userSelect="none"
114
+ opacity={ disabled ? 0.5 : 1 }
135
115
  data-disabled={ disabled || undefined }
136
- { ...rest }
116
+ { ...(rest as object) }
137
117
  >
138
118
  <GuiRadioGroup.Item
139
119
  unstyled
140
120
  value={ value }
141
121
  disabled={ disabled }
142
- className={ cn(
143
- 'inline-flex items-center justify-center shrink-0 rounded-full',
144
- 'border-2 border-current/30',
145
- 'data-[state=checked]:border-gray-800 dark:border-white',
146
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500',
147
- 'transition-colors duration-150',
148
- sizeClasses.control,
149
- ) }
122
+ flexShrink={ 0 }
123
+ alignItems="center"
124
+ justifyContent="center"
125
+ width={ s.control }
126
+ height={ s.control }
127
+ borderRadius={ 9999 }
128
+ borderWidth={ 2 }
129
+ borderColor={ RESTING_BORDER }
130
+ transition="quicker"
131
+ focusStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: OUTLINE, outlineOffset: 2 }}
150
132
  >
151
- <GuiRadioGroup.Indicator
152
- unstyled
153
- className={ cn(
154
- 'block rounded-full bg-gray-800 dark:bg-white',
155
- sizeClasses.indicator,
156
- ) }
157
- />
133
+ { /* gui only mounts this when the item is the group's selected
134
+ value — the primitive's own headless state, not a prop this
135
+ file tracks — so the dot is the correct+sufficient "selected"
136
+ signal without needing the item's checked state here too. */ }
137
+ <GuiRadioGroup.Indicator unstyled borderRadius={ 9999 } width={ s.indicator } height={ s.indicator } backgroundColor={ INK }/>
158
138
  </GuiRadioGroup.Item>
159
139
  { /* Hidden native input for form compatibility and ref forwarding */ }
160
- <input
161
- ref={ ref }
162
- type="radio"
163
- className="sr-only"
164
- value={ value }
165
- disabled={ disabled }
166
- tabIndex={ -1 }
167
- aria-hidden
168
- onChange={ NOOP }
169
- { ...inputProps }
170
- />
140
+ <VisuallyHidden>
141
+ <input
142
+ ref={ ref }
143
+ type="radio"
144
+ value={ value }
145
+ disabled={ disabled }
146
+ tabIndex={ -1 }
147
+ aria-hidden
148
+ onChange={ NOOP }
149
+ { ...inputProps }
150
+ />
151
+ </VisuallyHidden>
171
152
  { children != null && (
172
- <span className={ sizeClasses.label }>{ children }</span>
153
+ <Text fontSize={ s.label }>{ children }</Text>
173
154
  ) }
174
- </label>
155
+ </Label>
175
156
  );
176
157
  },
177
158
  );