@luxfi/ui 7.4.11 → 7.4.13

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 (84) hide show
  1. package/dist/alert.cjs +68 -46
  2. package/dist/alert.js +69 -46
  3. package/dist/avatar.cjs +49 -47
  4. package/dist/avatar.js +52 -49
  5. package/dist/badge.cjs +82 -49
  6. package/dist/badge.js +83 -50
  7. package/dist/checkbox.cjs +42 -75
  8. package/dist/checkbox.js +43 -76
  9. package/dist/chrome.cjs +3 -7
  10. package/dist/chrome.js +3 -7
  11. package/dist/close-button.cjs +3 -7
  12. package/dist/close-button.js +3 -7
  13. package/dist/dialog.cjs +3 -7
  14. package/dist/dialog.js +3 -7
  15. package/dist/drawer.cjs +3 -7
  16. package/dist/drawer.js +3 -7
  17. package/dist/empty-state.cjs +13 -13
  18. package/dist/empty-state.js +13 -13
  19. package/dist/expiration-selector.cjs +58 -63
  20. package/dist/expiration-selector.js +58 -63
  21. package/dist/greeks-display.cjs +48 -39
  22. package/dist/greeks-display.js +48 -39
  23. package/dist/index.cjs +1053 -888
  24. package/dist/index.js +1054 -889
  25. package/dist/input-group.cjs +36 -19
  26. package/dist/input-group.js +37 -19
  27. package/dist/option-chain.cjs +89 -67
  28. package/dist/option-chain.js +89 -67
  29. package/dist/option-position.cjs +88 -85
  30. package/dist/option-position.js +88 -85
  31. package/dist/pin-input.cjs +30 -29
  32. package/dist/pin-input.js +30 -29
  33. package/dist/pnl-diagram.cjs +20 -20
  34. package/dist/pnl-diagram.js +20 -20
  35. package/dist/popover.cjs +3 -7
  36. package/dist/popover.js +3 -7
  37. package/dist/progress-circle.cjs +40 -22
  38. package/dist/progress-circle.d.cts +5 -5
  39. package/dist/progress-circle.d.ts +5 -5
  40. package/dist/progress-circle.js +41 -22
  41. package/dist/progress.cjs +15 -22
  42. package/dist/progress.d.cts +6 -6
  43. package/dist/progress.d.ts +6 -6
  44. package/dist/progress.js +15 -22
  45. package/dist/radio.cjs +35 -68
  46. package/dist/radio.d.cts +18 -18
  47. package/dist/radio.d.ts +18 -18
  48. package/dist/radio.js +36 -69
  49. package/dist/rating.cjs +15 -17
  50. package/dist/rating.js +15 -17
  51. package/dist/slider.cjs +33 -50
  52. package/dist/slider.js +34 -51
  53. package/dist/strategy-builder.cjs +194 -115
  54. package/dist/strategy-builder.js +194 -115
  55. package/dist/switch.cjs +48 -55
  56. package/dist/switch.js +49 -56
  57. package/dist/tag.cjs +115 -69
  58. package/dist/tag.js +116 -69
  59. package/dist/tooltip.cjs +28 -17
  60. package/dist/tooltip.js +30 -18
  61. package/package.json +1 -1
  62. package/src/alert.tsx +78 -45
  63. package/src/avatar.tsx +54 -38
  64. package/src/badge.tsx +65 -44
  65. package/src/checkbox.tsx +70 -89
  66. package/src/close-button.tsx +3 -8
  67. package/src/empty-state.tsx +21 -17
  68. package/src/expiration-selector.tsx +84 -66
  69. package/src/greeks-display.tsx +59 -51
  70. package/src/input-group.tsx +38 -24
  71. package/src/option-chain.tsx +154 -104
  72. package/src/option-position.tsx +143 -114
  73. package/src/pin-input.tsx +37 -29
  74. package/src/pnl-diagram.tsx +36 -28
  75. package/src/progress-circle.tsx +52 -28
  76. package/src/progress.tsx +17 -21
  77. package/src/radio.tsx +56 -76
  78. package/src/rating.tsx +22 -20
  79. package/src/slider.tsx +43 -45
  80. package/src/strategy-builder.tsx +260 -162
  81. package/src/switch.tsx +63 -64
  82. package/src/tag.tsx +89 -51
  83. package/src/tooltip.tsx +21 -13
  84. package/tokens.css +18 -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
  );
@@ -1,5 +1,7 @@
1
+ import { View } from '@hanzo/gui';
1
2
  import * as React from 'react';
2
3
 
4
+ import { ink } from './ink';
3
5
  import { cn } from './utils';
4
6
 
5
7
  // ---------------------------------------------------------------------------
@@ -7,11 +9,11 @@ import { cn } from './utils';
7
9
  // ---------------------------------------------------------------------------
8
10
 
9
11
  const SIZE_MAP = {
10
- xs: { size: 24, thickness: 4, textClass: 'text-[10px]' },
11
- sm: { size: 32, thickness: 5, textClass: 'text-[10px]' },
12
- md: { size: 40, thickness: 6, textClass: 'text-xs' },
13
- lg: { size: 48, thickness: 7, textClass: 'text-sm' },
14
- xl: { size: 64, thickness: 8, textClass: 'text-sm' },
12
+ xs: { size: 24, thickness: 4, fontSize: 10 },
13
+ sm: { size: 32, thickness: 5, fontSize: 10 },
14
+ md: { size: 40, thickness: 6, fontSize: 12 },
15
+ lg: { size: 48, thickness: 7, fontSize: 14 },
16
+ xl: { size: 64, thickness: 8, fontSize: 14 },
15
17
  } as const;
16
18
 
17
19
  type Size = keyof typeof SIZE_MAP;
@@ -63,17 +65,23 @@ export const ProgressCircleRoot = React.forwardRef<
63
65
 
64
66
  return (
65
67
  <Ctx.Provider value={ ctx }>
66
- <div
67
- ref={ ref }
68
+ <View
69
+ ref={ ref as never }
68
70
  role="progressbar"
69
71
  aria-valuenow={ value ?? undefined }
70
72
  aria-valuemin={ 0 }
71
73
  aria-valuemax={ 100 }
72
- className={ cn('relative inline-flex text-sm', className) }
73
- { ...rest }
74
+ position="relative"
75
+ // `inline-flex` and `fontSize` are not in a Stack's style base — a
76
+ // Stack has no text props at all, and its `display` enum is narrower
77
+ // than CSS's (no `inline-*` variants) — so both travel as plain style
78
+ // here rather than gui props.
79
+ style={{ display: 'inline-flex', fontSize: 14 }}
80
+ className={ className }
81
+ { ...(rest as object) }
74
82
  >
75
- { children }
76
- </div>
83
+ { ink(children) }
84
+ </View>
77
85
  </Ctx.Provider>
78
86
  );
79
87
  });
@@ -108,10 +116,7 @@ export const ProgressCircleRing = React.forwardRef<
108
116
  width={ size }
109
117
  height={ size }
110
118
  viewBox={ `0 0 ${ size } ${ size }` }
111
- className={ cn(
112
- value === null && 'animate-spin',
113
- className,
114
- ) }
119
+ className={ value === null ? cn('lux-spin', className) : className }
115
120
  { ...rest }
116
121
  >
117
122
  { /* Track */ }
@@ -122,7 +127,7 @@ export const ProgressCircleRing = React.forwardRef<
122
127
  fill="transparent"
123
128
  strokeWidth={ thickness }
124
129
  stroke={ trackColor ?? 'currentColor' }
125
- className={ !trackColor ? 'opacity-20' : undefined }
130
+ opacity={ !trackColor ? 0.2 : undefined }
126
131
  />
127
132
  { /* Range */ }
128
133
  <circle
@@ -160,21 +165,40 @@ export const ProgressCircleValueText = React.forwardRef<
160
165
  const { className, children, ...rest } = props;
161
166
  const { value, size: sizeKey } = React.useContext(Ctx);
162
167
 
163
- const { textClass } = SIZE_MAP[sizeKey];
168
+ const { fontSize } = SIZE_MAP[sizeKey];
164
169
 
165
170
  return (
166
- <div
167
- ref={ ref }
171
+ <View
172
+ ref={ ref as never }
168
173
  aria-live="polite"
169
- className={ cn(
170
- 'absolute inset-0 flex items-center justify-center',
171
- 'font-medium tracking-tight tabular-nums leading-none',
172
- textClass,
173
- className,
174
- ) }
175
- { ...rest }
174
+ position="absolute"
175
+ top={ 0 }
176
+ left={ 0 }
177
+ right={ 0 }
178
+ bottom={ 0 }
179
+ display="flex"
180
+ alignItems="center"
181
+ justifyContent="center"
182
+ // A Stack has no text props (fontSize/fontWeight/letterSpacing/lineHeight
183
+ // /fontVariantNumeric) — a View cannot style text any more than a native
184
+ // <View> can, only the <Text> inside it. `ink()` below hosts the string
185
+ // in a real Text, but callers may also pass their own element, so this
186
+ // stays a style object rather than gui text props: it composes either
187
+ // way, exactly like the Tailwind classes it replaces did through
188
+ // inheritance. Tailwind's `leading-none` (line-height: 1, unitless) and
189
+ // `tracking-tight` (-0.025em) are both relative to font size, so they are
190
+ // computed per size tier here instead of copied as one constant.
191
+ style={{
192
+ fontWeight: 500,
193
+ fontSize,
194
+ lineHeight: `${ fontSize }px`,
195
+ letterSpacing: fontSize * -0.025,
196
+ fontVariantNumeric: 'tabular-nums',
197
+ }}
198
+ className={ className }
199
+ { ...(rest as object) }
176
200
  >
177
- { children ?? `${ Math.round(value ?? 0) }%` }
178
- </div>
201
+ { ink(children ?? `${ Math.round(value ?? 0) }%`) }
202
+ </View>
179
203
  );
180
204
  });
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
 
@@ -83,7 +64,6 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
83
64
  from its headless core, real Pressables on native. */ }
84
65
  <GuiRadioGroup
85
66
  ref={ ref as never }
86
- unstyled
87
67
  name={ name }
88
68
  required={ required }
89
69
  disabled={ disabled || readOnly }
@@ -91,11 +71,9 @@ const RadioGroupBase = React.forwardRef<HTMLDivElement, RadioGroupProps>(
91
71
  defaultValue={ defaultValue }
92
72
  value={ value ?? undefined }
93
73
  onValueChange={ handleValueChange }
94
- className={ cn(
95
- 'flex',
96
- orientation === 'vertical' ? 'flex-col gap-3' : 'flex-row gap-6',
97
- className,
98
- ) }
74
+ display="flex"
75
+ flexDirection={ orientation === 'vertical' ? 'column' : 'row' }
76
+ gap={ orientation === 'vertical' ? 12 : 24 }
99
77
  { ...(rest as object) }
100
78
  >
101
79
  { children }
@@ -119,59 +97,61 @@ export interface RadioProps extends Omit<React.ComponentPropsWithoutRef<'label'>
119
97
 
120
98
  const RadioBase = React.forwardRef<HTMLInputElement, RadioProps>(
121
99
  function Radio(props, ref) {
122
- const { children, inputProps, rootRef, value, disabled, className, ...rest } = props;
100
+ const { children, inputProps, rootRef, value, disabled, ...rest } = props;
123
101
  const size = React.useContext(RadioSizeContext);
124
- const sizeClasses = SIZE_CLASSES[size];
102
+ const s = SIZE[size];
125
103
 
126
104
  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
- ) }
105
+ <Label
106
+ ref={ rootRef as never }
107
+ unstyled
108
+ flexDirection="row"
109
+ alignItems="center"
110
+ gap={ s.rootGap }
111
+ cursor={ disabled ? 'not-allowed' : 'pointer' }
112
+ userSelect="none"
113
+ opacity={ disabled ? 0.5 : 1 }
135
114
  data-disabled={ disabled || undefined }
136
- { ...rest }
115
+ { ...(rest as object) }
137
116
  >
138
117
  <GuiRadioGroup.Item
139
118
  unstyled
140
119
  value={ value }
141
120
  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
- ) }
121
+ flexShrink={ 0 }
122
+ alignItems="center"
123
+ justifyContent="center"
124
+ width={ s.control }
125
+ height={ s.control }
126
+ borderRadius={ 9999 }
127
+ borderWidth={ 2 }
128
+ borderColor={ RESTING_BORDER }
129
+ transition="quicker"
130
+ focusStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: OUTLINE, outlineOffset: 2 }}
150
131
  >
151
- <GuiRadioGroup.Indicator
152
- unstyled
153
- className={ cn(
154
- 'block rounded-full bg-gray-800 dark:bg-white',
155
- sizeClasses.indicator,
156
- ) }
157
- />
132
+ { /* gui only mounts this when the item is the group's selected
133
+ value — the primitive's own headless state, not a prop this
134
+ file tracks — so the dot is the correct+sufficient "selected"
135
+ signal without needing the item's checked state here too. */ }
136
+ <GuiRadioGroup.Indicator unstyled borderRadius={ 9999 } width={ s.indicator } height={ s.indicator } backgroundColor={ INK }/>
158
137
  </GuiRadioGroup.Item>
159
138
  { /* 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
- />
139
+ <VisuallyHidden>
140
+ <input
141
+ ref={ ref }
142
+ type="radio"
143
+ value={ value }
144
+ disabled={ disabled }
145
+ tabIndex={ -1 }
146
+ aria-hidden
147
+ onChange={ NOOP }
148
+ { ...inputProps }
149
+ />
150
+ </VisuallyHidden>
171
151
  { children != null && (
172
- <span className={ sizeClasses.label }>{ children }</span>
152
+ <Text fontSize={ s.label }>{ children }</Text>
173
153
  ) }
174
- </label>
154
+ </Label>
175
155
  );
176
156
  },
177
157
  );