@luxfi/ui 7.4.11 → 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 (70) hide show
  1. package/dist/alert.cjs +68 -46
  2. package/dist/alert.js +69 -46
  3. package/dist/avatar.cjs +49 -46
  4. package/dist/avatar.js +52 -48
  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 +756 -703
  24. package/dist/index.js +757 -704
  25. package/dist/option-chain.cjs +89 -67
  26. package/dist/option-chain.js +89 -67
  27. package/dist/option-position.cjs +88 -85
  28. package/dist/option-position.js +88 -85
  29. package/dist/pnl-diagram.cjs +20 -20
  30. package/dist/pnl-diagram.js +20 -20
  31. package/dist/popover.cjs +3 -7
  32. package/dist/popover.js +3 -7
  33. package/dist/progress.cjs +15 -22
  34. package/dist/progress.d.cts +6 -6
  35. package/dist/progress.d.ts +6 -6
  36. package/dist/progress.js +15 -22
  37. package/dist/radio.cjs +35 -67
  38. package/dist/radio.d.cts +18 -18
  39. package/dist/radio.d.ts +18 -18
  40. package/dist/radio.js +36 -68
  41. package/dist/slider.cjs +33 -49
  42. package/dist/slider.js +34 -50
  43. package/dist/switch.cjs +48 -55
  44. package/dist/switch.js +49 -56
  45. package/dist/tag.cjs +112 -69
  46. package/dist/tag.js +113 -69
  47. package/dist/tooltip.cjs +28 -17
  48. package/dist/tooltip.js +30 -18
  49. package/package.json +1 -1
  50. package/src/alert.tsx +78 -45
  51. package/src/avatar.tsx +54 -37
  52. package/src/badge.tsx +65 -44
  53. package/src/checkbox.tsx +70 -89
  54. package/src/close-button.tsx +3 -8
  55. package/src/empty-state.tsx +21 -17
  56. package/src/expiration-selector.tsx +84 -66
  57. package/src/greeks-display.tsx +59 -51
  58. package/src/input-group.tsx +38 -24
  59. package/src/option-chain.tsx +154 -104
  60. package/src/option-position.tsx +136 -114
  61. package/src/pin-input.tsx +37 -29
  62. package/src/pnl-diagram.tsx +36 -28
  63. package/src/progress.tsx +17 -21
  64. package/src/radio.tsx +56 -75
  65. package/src/slider.tsx +43 -44
  66. package/src/strategy-builder.tsx +245 -156
  67. package/src/switch.tsx +63 -64
  68. package/src/tag.tsx +89 -51
  69. package/src/tooltip.tsx +21 -13
  70. 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
  );
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
  );
package/src/slider.tsx CHANGED
@@ -1,8 +1,6 @@
1
- import { Slider as GuiSlider } from '@hanzo/gui';
1
+ import { Slider as GuiSlider, Text, View, XStack, YStack } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
4
  export interface SliderProps {
7
5
  readonly marks?: Array<number | { readonly value: number; readonly label: React.ReactNode }>;
8
6
  readonly label?: React.ReactNode;
@@ -20,6 +18,12 @@ export interface SliderProps {
20
18
  readonly className?: string;
21
19
  }
22
20
 
21
+ const MUTED = 'var(--color-text-secondary)' as never;
22
+ const TRACK = 'var(--color-border-divider)' as never;
23
+ const ACTIVE = 'var(--color-link-primary)' as never;
24
+ const ACTIVE_HOVER = 'var(--color-link-primary-hover)' as never;
25
+ const WHITE = 'var(--color-white)' as never;
26
+
23
27
  // gui's Slider: pointer, touch and keyboard from one implementation, with the
24
28
  // drag handled as a pan gesture by the engine rather than by mouse events — so it
25
29
  // works under a finger, not only under a cursor. `role=slider` and the arrow-key
@@ -64,30 +68,32 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
64
68
  }, [ onValueCommit ]);
65
69
 
66
70
  return (
67
- <div className={ cn('flex flex-col gap-1', className) }>
71
+ <YStack className={ className } gap={ 4 }>
68
72
  { label && !showValue && (
69
- <label className="text-sm font-medium text-text-secondary">
73
+ <Text fontSize={ 14 } fontWeight="500" color={ MUTED }>
70
74
  { label }
71
- </label>
75
+ </Text>
72
76
  ) }
73
77
  { label && showValue && (
74
- <div className="flex items-center justify-between">
75
- <label className="text-sm font-medium text-text-secondary">
78
+ <XStack alignItems="center" justifyContent="space-between">
79
+ <Text fontSize={ 14 } fontWeight="500" color={ MUTED }>
76
80
  { label }
77
- </label>
78
- <span className="text-sm text-text-secondary">
81
+ </Text>
82
+ <Text fontSize={ 14 } color={ MUTED }>
79
83
  { (value ?? defaultValue ?? [])?.join(', ') }
80
- </span>
81
- </div>
84
+ </Text>
85
+ </XStack>
82
86
  ) }
83
87
 
84
88
  <GuiSlider
85
89
  ref={ ref as never }
86
90
  unstyled
87
- className={ cn(
88
- 'relative flex w-full touch-none select-none items-center',
89
- hasMarkLabel && 'mb-6',
90
- ) }
91
+ position="relative"
92
+ flexDirection="row"
93
+ width="100%"
94
+ userSelect="none"
95
+ alignItems="center"
96
+ marginBottom={ hasMarkLabel ? 24 : 0 }
91
97
  value={ value }
92
98
  defaultValue={ defaultValue }
93
99
  min={ min }
@@ -99,14 +105,8 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
99
105
  onValueChange={ handleValueChange }
100
106
  onSlideEnd={ handleSlideEnd }
101
107
  >
102
- <GuiSlider.Track
103
- unstyled
104
- className="relative grow h-1.5 rounded-full bg-border-divider"
105
- >
106
- <GuiSlider.TrackActive
107
- unstyled
108
- className="absolute h-full rounded-full bg-link-primary"
109
- />
108
+ <GuiSlider.Track unstyled position="relative" flexGrow={ 1 } height={ 6 } borderRadius={ 9999 } backgroundColor={ TRACK }>
109
+ <GuiSlider.TrackActive unstyled position="absolute" height="100%" borderRadius={ 9999 } backgroundColor={ ACTIVE }/>
110
110
  </GuiSlider.Track>
111
111
 
112
112
  { resolvedValue?.map((_, index) => (
@@ -114,20 +114,23 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
114
114
  key={ index }
115
115
  index={ index }
116
116
  unstyled
117
- className={ cn(
118
- 'block h-5 w-5',
119
- 'rounded-full border-2 border-link-primary bg-white',
120
- 'shadow-sm transition-colors',
121
- 'hover:border-link-primary-hover',
122
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-link-primary focus-visible:ring-offset-2',
123
- 'disabled:pointer-events-none disabled:opacity-50',
124
- ) }
117
+ width={ 20 }
118
+ height={ 20 }
119
+ borderRadius={ 9999 }
120
+ borderWidth={ 2 }
121
+ borderColor={ ACTIVE }
122
+ backgroundColor={ WHITE }
123
+ boxShadow="0 1px 2px 0 rgba(0,0,0,0.05)"
124
+ transition="quick"
125
+ hoverStyle={{ borderColor: ACTIVE_HOVER }}
126
+ focusStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: ACTIVE, outlineOffset: 2 }}
127
+ disabledStyle={{ pointerEvents: 'none', opacity: 0.5 }}
125
128
  />
126
129
  )) }
127
130
  </GuiSlider>
128
131
 
129
132
  <SliderMarks marks={ marks } min={ min } max={ max }/>
130
- </div>
133
+ </YStack>
131
134
  );
132
135
  },
133
136
  );
@@ -145,24 +148,20 @@ function SliderMarks(props: SliderMarksProps): React.ReactNode {
145
148
  const range = max - min;
146
149
 
147
150
  return (
148
- <div className="relative w-full h-4">
151
+ <View position="relative" width="100%" height={ 16 }>
149
152
  { marks.map((mark, index) => {
150
153
  const percent = ((mark.value - min) / range) * 100;
151
154
  return (
152
- <div
153
- key={ index }
154
- className="absolute flex flex-col items-center -translate-x-1/2"
155
- style={{ left: `${ percent }%` }}
156
- >
157
- <div className="h-1.5 w-0.5 bg-border-divider"/>
155
+ <View key={ index } position="absolute" left={ `${ percent }%` as never } alignItems="center">
156
+ <View width={ 2 } height={ 6 } backgroundColor={ TRACK }/>
158
157
  { mark.label != null && (
159
- <span className="mt-0.5 text-xs text-text-secondary whitespace-nowrap">
158
+ <Text marginTop={ 2 } fontSize={ 12 } color={ MUTED }>
160
159
  { mark.label }
161
- </span>
160
+ </Text>
162
161
  ) }
163
- </div>
162
+ </View>
164
163
  );
165
164
  }) }
166
- </div>
165
+ </View>
167
166
  );
168
167
  }