@luxfi/ui 7.4.10 → 7.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/accordion.cjs +107 -166
  2. package/dist/accordion.d.cts +18 -51
  3. package/dist/accordion.d.ts +18 -51
  4. package/dist/accordion.js +109 -166
  5. package/dist/alert.cjs +68 -46
  6. package/dist/alert.js +69 -46
  7. package/dist/avatar.cjs +49 -46
  8. package/dist/avatar.js +52 -48
  9. package/dist/badge.cjs +82 -49
  10. package/dist/badge.js +83 -50
  11. package/dist/checkbox.cjs +42 -75
  12. package/dist/checkbox.js +43 -76
  13. package/dist/chrome.cjs +196 -386
  14. package/dist/chrome.js +198 -386
  15. package/dist/close-button.cjs +3 -7
  16. package/dist/close-button.js +3 -7
  17. package/dist/collapsible.cjs +35 -26
  18. package/dist/collapsible.d.cts +2 -1
  19. package/dist/collapsible.d.ts +2 -1
  20. package/dist/collapsible.js +34 -24
  21. package/dist/dialog.cjs +3 -7
  22. package/dist/dialog.d.cts +1 -1
  23. package/dist/dialog.d.ts +1 -1
  24. package/dist/dialog.js +3 -7
  25. package/dist/drawer.cjs +95 -122
  26. package/dist/drawer.d.cts +43 -10
  27. package/dist/drawer.d.ts +43 -10
  28. package/dist/drawer.js +97 -122
  29. package/dist/empty-state.cjs +13 -13
  30. package/dist/empty-state.js +13 -13
  31. package/dist/expiration-selector.cjs +58 -63
  32. package/dist/expiration-selector.js +58 -63
  33. package/dist/field.cjs +60 -109
  34. package/dist/field.d.cts +0 -1
  35. package/dist/field.d.ts +0 -1
  36. package/dist/field.js +61 -109
  37. package/dist/greeks-display.cjs +48 -39
  38. package/dist/greeks-display.js +48 -39
  39. package/dist/heading.cjs +10 -17
  40. package/dist/heading.d.cts +0 -2
  41. package/dist/heading.d.ts +0 -2
  42. package/dist/heading.js +10 -17
  43. package/dist/icon-button.cjs +82 -82
  44. package/dist/icon-button.d.cts +16 -12
  45. package/dist/icon-button.d.ts +16 -12
  46. package/dist/icon-button.js +82 -82
  47. package/dist/index.cjs +1812 -2105
  48. package/dist/index.d.cts +1 -5
  49. package/dist/index.d.ts +1 -5
  50. package/dist/index.js +1814 -2100
  51. package/dist/input-group.cjs +11 -12
  52. package/dist/input-group.js +11 -12
  53. package/dist/input.cjs +27 -27
  54. package/dist/input.d.cts +21 -1
  55. package/dist/input.d.ts +21 -1
  56. package/dist/input.js +25 -28
  57. package/dist/menu.cjs +160 -271
  58. package/dist/menu.d.cts +41 -57
  59. package/dist/menu.d.ts +41 -57
  60. package/dist/menu.js +161 -251
  61. package/dist/option-chain.cjs +89 -67
  62. package/dist/option-chain.js +89 -67
  63. package/dist/option-position.cjs +88 -85
  64. package/dist/option-position.js +88 -85
  65. package/dist/pnl-diagram.cjs +20 -20
  66. package/dist/pnl-diagram.js +20 -20
  67. package/dist/popover.cjs +4 -8
  68. package/dist/popover.js +4 -8
  69. package/dist/progress.cjs +15 -22
  70. package/dist/progress.d.cts +6 -6
  71. package/dist/progress.d.ts +6 -6
  72. package/dist/progress.js +15 -22
  73. package/dist/radio.cjs +35 -67
  74. package/dist/radio.d.cts +18 -18
  75. package/dist/radio.d.ts +18 -18
  76. package/dist/radio.js +36 -68
  77. package/dist/select.cjs +40 -39
  78. package/dist/select.d.cts +35 -4
  79. package/dist/select.d.ts +35 -4
  80. package/dist/select.js +40 -38
  81. package/dist/separator.cjs +8 -33
  82. package/dist/separator.js +8 -33
  83. package/dist/slider.cjs +33 -49
  84. package/dist/slider.js +34 -50
  85. package/dist/switch.cjs +48 -55
  86. package/dist/switch.js +49 -56
  87. package/dist/tabs.cjs +108 -197
  88. package/dist/tabs.d.cts +16 -60
  89. package/dist/tabs.d.ts +16 -60
  90. package/dist/tabs.js +109 -196
  91. package/dist/tag.cjs +112 -69
  92. package/dist/tag.js +113 -69
  93. package/dist/textarea.cjs +56 -27
  94. package/dist/textarea.js +57 -28
  95. package/dist/toaster.cjs +97 -79
  96. package/dist/toaster.d.cts +7 -8
  97. package/dist/toaster.d.ts +7 -8
  98. package/dist/toaster.js +78 -80
  99. package/dist/tooltip.cjs +28 -17
  100. package/dist/tooltip.js +30 -18
  101. package/package.json +2 -16
  102. package/src/accordion.tsx +173 -227
  103. package/src/alert.tsx +78 -45
  104. package/src/avatar.tsx +54 -37
  105. package/src/badge.tsx +65 -44
  106. package/src/checkbox.tsx +70 -89
  107. package/src/close-button.tsx +3 -8
  108. package/src/collapsible.tsx +55 -52
  109. package/src/drawer.tsx +123 -115
  110. package/src/empty-state.tsx +21 -17
  111. package/src/expiration-selector.tsx +84 -66
  112. package/src/field.tsx +68 -124
  113. package/src/greeks-display.tsx +59 -51
  114. package/src/heading.tsx +22 -26
  115. package/src/icon-button.tsx +134 -141
  116. package/src/ink.tsx +37 -0
  117. package/src/input-group.tsx +59 -36
  118. package/src/input.tsx +41 -32
  119. package/src/menu.tsx +224 -397
  120. package/src/option-chain.tsx +154 -104
  121. package/src/option-position.tsx +136 -114
  122. package/src/pin-input.tsx +37 -29
  123. package/src/pnl-diagram.tsx +36 -28
  124. package/src/popover.tsx +1 -1
  125. package/src/progress.tsx +17 -21
  126. package/src/radio.tsx +56 -75
  127. package/src/select.tsx +80 -59
  128. package/src/separator.tsx +13 -34
  129. package/src/slider.tsx +43 -44
  130. package/src/strategy-builder.tsx +245 -156
  131. package/src/switch.tsx +63 -64
  132. package/src/tabs.tsx +166 -296
  133. package/src/tag.tsx +89 -51
  134. package/src/textarea.tsx +19 -29
  135. package/src/toaster.tsx +118 -86
  136. package/src/tooltip.tsx +21 -13
  137. package/tokens.css +79 -0
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Text, View, XStack, YStack } from '@hanzo/gui';
3
4
  import * as React from 'react';
4
5
 
5
- import { cn } from './utils';
6
-
7
6
  // ---------------------------------------------------------------------------
8
7
  // Types
9
8
  // ---------------------------------------------------------------------------
@@ -163,6 +162,21 @@ const STRATEGY_PRESETS: StrategyPreset[] = [
163
162
  },
164
163
  ];
165
164
 
165
+ // ---------------------------------------------------------------------------
166
+ // Shared control shape — same tokens input.tsx's native text control uses.
167
+ // ---------------------------------------------------------------------------
168
+
169
+ const MONO = { fontFamily: 'monospace' } as never;
170
+
171
+ const CONTROL = {
172
+ backgroundColor: 'var(--color-input-bg)',
173
+ borderWidth: 1,
174
+ borderColor: 'var(--color-input-border)',
175
+ color: 'var(--color-input-fg)',
176
+ outlineWidth: 0,
177
+ focusStyle: { borderColor: 'var(--color-input-border-focus)' },
178
+ } as const;
179
+
166
180
  // ---------------------------------------------------------------------------
167
181
  // LegRow
168
182
  // ---------------------------------------------------------------------------
@@ -183,100 +197,148 @@ function LegRow({ leg, index, strikes, expirations, removable, onChange, onRemov
183
197
  };
184
198
 
185
199
  return (
186
- <div className="flex items-center gap-2 rounded-md bg-zinc-800/50 px-2 py-1.5">
187
- <span className="text-[10px] font-mono text-zinc-500 w-4 shrink-0">
188
- {index + 1}
189
- </span>
200
+ <XStack
201
+ alignItems="center"
202
+ gap={ 8 }
203
+ borderRadius={ 6 }
204
+ backgroundColor={ 'var(--color-tag-subtle-bg)' as never }
205
+ paddingHorizontal={ 8 }
206
+ paddingVertical={ 6 }
207
+ >
208
+ <Text fontSize={ 10 } style={ MONO } flexShrink={ 0 } width={ 16 } color={ 'var(--color-text-secondary)' as never }>
209
+ { index + 1 }
210
+ </Text>
190
211
 
191
212
  {/* Action */}
192
- <button
193
- type="button"
194
- className={cn(
195
- 'px-2 py-0.5 rounded text-xs font-semibold transition-colors',
196
- leg.action === 'buy'
197
- ? 'bg-emerald-900/60 text-emerald-400'
198
- : 'bg-red-900/60 text-red-400',
199
- )}
200
- onClick={() => update({ action: leg.action === 'buy' ? 'sell' : 'buy' })}
213
+ <XStack
214
+ unstyled
215
+ render={ <button type="button"/> }
216
+ onClick={ () => update({ action: leg.action === 'buy' ? 'sell' : 'buy' }) }
217
+ paddingHorizontal={ 8 }
218
+ paddingVertical={ 2 }
219
+ borderRadius={ 4 }
220
+ borderWidth={ 0 }
221
+ cursor="pointer"
222
+ transition="quick"
223
+ backgroundColor={ (leg.action === 'buy'
224
+ ? 'color-mix(in srgb, var(--color-status-good) 20%, transparent)'
225
+ : 'color-mix(in srgb, var(--color-status-bad) 20%, transparent)') as never }
201
226
  >
202
- {leg.action === 'buy' ? 'BUY' : 'SELL'}
203
- </button>
204
-
205
- {/* Type */}
206
- <button
207
- type="button"
208
- className={cn(
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.
211
- leg.type === 'call'
212
- ? 'bg-[var(--secondary)] text-[var(--foreground)]'
213
- : 'bg-[var(--muted)] text-[var(--muted-foreground)]',
214
- )}
215
- onClick={() => update({ type: leg.type === 'call' ? 'put' : 'call' })}
227
+ <Text fontSize={ 12 } fontWeight="600" color={ (leg.action === 'buy' ? 'var(--color-status-good)' : 'var(--color-status-bad)') as never }>
228
+ { leg.action === 'buy' ? 'BUY' : 'SELL' }
229
+ </Text>
230
+ </XStack>
231
+
232
+ {/* Type — call vs put is a rank difference, not a hue difference. */}
233
+ <XStack
234
+ unstyled
235
+ render={ <button type="button"/> }
236
+ onClick={ () => update({ type: leg.type === 'call' ? 'put' : 'call' }) }
237
+ paddingHorizontal={ 8 }
238
+ paddingVertical={ 2 }
239
+ borderRadius={ 4 }
240
+ borderWidth={ 0 }
241
+ cursor="pointer"
242
+ transition="quick"
243
+ backgroundColor={ (leg.type === 'call' ? 'var(--secondary)' : 'var(--muted)') as never }
216
244
  >
217
- {leg.type === 'call' ? 'CALL' : 'PUT'}
218
- </button>
245
+ <Text fontSize={ 12 } fontWeight="500" color={ (leg.type === 'call' ? 'var(--foreground)' : 'var(--muted-foreground)') as never }>
246
+ { leg.type === 'call' ? 'CALL' : 'PUT' }
247
+ </Text>
248
+ </XStack>
219
249
 
220
250
  {/* Strike */}
221
- <select
222
- value={leg.strike}
223
- onChange={(e) => update({ strike: Number(e.target.value) })}
224
- className="bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 text-xs font-mono text-zinc-200 outline-none focus:border-zinc-500 min-w-[72px]"
251
+ <View
252
+ unstyled
253
+ render={ <select/> }
254
+ value={ leg.strike }
255
+ onChange={ (e: React.ChangeEvent<HTMLSelectElement>) => update({ strike: Number(e.target.value) }) }
256
+ { ...CONTROL }
257
+ style={ MONO }
258
+ fontSize={ 12 }
259
+ borderRadius={ 4 }
260
+ paddingHorizontal={ 6 }
261
+ paddingVertical={ 2 }
262
+ minWidth={ 72 }
225
263
  >
226
- {strikes.map((s) => (
227
- <option key={s} value={s}>
228
- {s.toFixed(2)}
264
+ { strikes.map((s) => (
265
+ <option key={ s } value={ s }>
266
+ { s.toFixed(2) }
229
267
  </option>
230
- ))}
231
- </select>
268
+ )) }
269
+ </View>
232
270
 
233
271
  {/* Expiration */}
234
- <select
235
- value={leg.expiration}
236
- onChange={(e) => update({ expiration: e.target.value })}
237
- className="bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 text-xs text-zinc-200 outline-none focus:border-zinc-500 min-w-[80px]"
272
+ <View
273
+ unstyled
274
+ render={ <select/> }
275
+ value={ leg.expiration }
276
+ onChange={ (e: React.ChangeEvent<HTMLSelectElement>) => update({ expiration: e.target.value }) }
277
+ { ...CONTROL }
278
+ fontSize={ 12 }
279
+ borderRadius={ 4 }
280
+ paddingHorizontal={ 6 }
281
+ paddingVertical={ 2 }
282
+ minWidth={ 80 }
238
283
  >
239
- {expirations.map((exp) => (
240
- <option key={exp} value={exp}>
241
- {exp}
284
+ { expirations.map((exp) => (
285
+ <option key={ exp } value={ exp }>
286
+ { exp }
242
287
  </option>
243
- ))}
244
- </select>
288
+ )) }
289
+ </View>
245
290
 
246
291
  {/* Quantity */}
247
- <input
248
- type="number"
249
- min={1}
250
- max={999}
251
- value={leg.quantity}
252
- onChange={(e) => update({ quantity: Math.max(1, parseInt(e.target.value, 10) || 1) })}
253
- className="bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 text-xs font-mono text-zinc-200 outline-none focus:border-zinc-500 w-[48px] text-center"
292
+ <View
293
+ unstyled
294
+ render={ <input type="number"/> }
295
+ min={ 1 }
296
+ max={ 999 }
297
+ value={ leg.quantity }
298
+ onChange={ (e: React.ChangeEvent<HTMLInputElement>) => update({ quantity: Math.max(1, parseInt(e.target.value, 10) || 1) }) }
299
+ { ...CONTROL }
300
+ style={ MONO }
301
+ fontSize={ 12 }
302
+ borderRadius={ 4 }
303
+ paddingHorizontal={ 6 }
304
+ paddingVertical={ 2 }
305
+ width={ 48 }
306
+ textAlign="center"
254
307
  />
255
308
 
256
309
  {/* Premium (read-only if provided) */}
257
- {leg.premium !== undefined && (
258
- <span className={cn(
259
- 'font-mono tabular-nums text-xs ml-auto',
260
- leg.premium >= 0 ? 'text-emerald-400' : 'text-red-400',
261
- )}>
262
- {leg.premium >= 0 ? '+' : ''}{leg.premium.toFixed(2)}
263
- </span>
264
- )}
310
+ { leg.premium !== undefined && (
311
+ <Text
312
+ fontSize={ 12 }
313
+ style={ MONO }
314
+ fontVariant={ ['tabular-nums'] as never }
315
+ marginLeft="auto"
316
+ color={ (leg.premium >= 0 ? 'var(--color-status-good)' : 'var(--color-status-bad)') as never }
317
+ >
318
+ { leg.premium >= 0 ? '+' : '' }{ leg.premium.toFixed(2) }
319
+ </Text>
320
+ ) }
265
321
 
266
322
  {/* Remove */}
267
- {removable && (
268
- <button
269
- type="button"
270
- onClick={() => onRemove(index)}
271
- className="ml-auto p-0.5 text-zinc-500 hover:text-red-400 transition-colors"
272
- aria-label="Remove leg"
323
+ { removable && (
324
+ <XStack
325
+ unstyled
326
+ render={ <button type="button" aria-label="Remove leg"/> }
327
+ onClick={ () => onRemove(index) }
328
+ marginLeft="auto"
329
+ padding={ 2 }
330
+ borderWidth={ 0 }
331
+ cursor="pointer"
332
+ transition="quick"
333
+ color={ 'var(--color-text-secondary)' as never }
334
+ hoverStyle={ { color: 'var(--color-status-bad)' } as never }
273
335
  >
274
- <svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none">
275
- <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
336
+ <svg width={ 14 } height={ 14 } viewBox="0 0 24 24" fill="none">
337
+ <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
276
338
  </svg>
277
- </button>
278
- )}
279
- </div>
339
+ </XStack>
340
+ ) }
341
+ </XStack>
280
342
  );
281
343
  }
282
344
 
@@ -360,108 +422,135 @@ export const StrategyBuilder = React.forwardRef<HTMLDivElement, StrategyBuilderP
360
422
  }, [onSubmit, strategyType, legs, netPremium]);
361
423
 
362
424
  const currentPreset = STRATEGY_PRESETS.find((p) => p.value === strategyType);
425
+ const netColor = netPremium > 0 ? 'var(--color-status-good)' : netPremium < 0 ? 'var(--color-status-bad)' : 'var(--color-text-secondary)';
363
426
 
364
427
  return (
365
- <div
366
- ref={ref}
367
- className={cn(
368
- 'flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3',
369
- className,
370
- )}
371
- {...rest}
428
+ <YStack
429
+ ref={ ref as never }
430
+ gap={ 12 }
431
+ borderRadius={ 8 }
432
+ borderWidth={ 1 }
433
+ borderColor={ 'var(--color-border-divider)' as never }
434
+ backgroundColor={ 'var(--card)' as never }
435
+ padding={ 12 }
436
+ className={ className }
437
+ { ...(rest as object) }
372
438
  >
373
439
  {/* Header */}
374
- <div className="flex items-center justify-between gap-3">
375
- <div className="flex items-center gap-2">
376
- <span className="text-xs font-semibold uppercase tracking-wider text-zinc-500">
440
+ <XStack alignItems="center" justifyContent="space-between" gap={ 12 }>
441
+ <XStack alignItems="center" gap={ 8 }>
442
+ <Text fontSize={ 12 } fontWeight="600" textTransform="uppercase" letterSpacing={ 0.6 } color={ 'var(--color-text-secondary)' as never }>
377
443
  Strategy
378
- </span>
379
- <span className="text-sm font-medium text-zinc-200">
380
- {underlying}
381
- </span>
382
- </div>
383
- <span className="text-[10px] text-zinc-500">
384
- Spot: <span className="font-mono tabular-nums text-zinc-300">{spotPrice.toFixed(2)}</span>
385
- </span>
386
- </div>
444
+ </Text>
445
+ <Text fontSize={ 14 } fontWeight="500" color={ 'var(--color-text-primary)' as never }>
446
+ { underlying }
447
+ </Text>
448
+ </XStack>
449
+ <Text fontSize={ 10 } color={ 'var(--color-text-secondary)' as never }>
450
+ Spot: <Text style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ 'var(--color-text-primary)' as never }>{ spotPrice.toFixed(2) }</Text>
451
+ </Text>
452
+ </XStack>
387
453
 
388
454
  {/* Strategy selector */}
389
- <div className="flex items-center gap-2">
390
- <select
391
- value={strategyType}
392
- onChange={handleStrategyChange}
393
- className="flex-1 bg-zinc-900 border border-zinc-700 rounded-md px-2.5 py-1.5 text-sm text-zinc-200 outline-none focus:border-zinc-500"
455
+ <XStack alignItems="center" gap={ 8 }>
456
+ <View
457
+ unstyled
458
+ render={ <select/> }
459
+ value={ strategyType }
460
+ onChange={ handleStrategyChange }
461
+ flex={ 1 }
462
+ { ...CONTROL }
463
+ fontSize={ 14 }
464
+ borderRadius={ 6 }
465
+ paddingHorizontal={ 10 }
466
+ paddingVertical={ 6 }
394
467
  >
395
- {STRATEGY_PRESETS.map((preset) => (
396
- <option key={preset.value} value={preset.value}>
397
- {preset.label}
468
+ { STRATEGY_PRESETS.map((preset) => (
469
+ <option key={ preset.value } value={ preset.value }>
470
+ { preset.label }
398
471
  </option>
399
- ))}
400
- </select>
401
- </div>
472
+ )) }
473
+ </View>
474
+ </XStack>
402
475
 
403
- {currentPreset && (
404
- <p className="text-[11px] text-zinc-500 -mt-1">
405
- {currentPreset.description}
406
- </p>
407
- )}
476
+ { currentPreset && (
477
+ <Text fontSize={ 11 } marginTop={ -4 } color={ 'var(--color-text-secondary)' as never }>
478
+ { currentPreset.description }
479
+ </Text>
480
+ ) }
408
481
 
409
482
  {/* Legs */}
410
- <div className="flex flex-col gap-1.5">
411
- {legs.map((leg, i) => (
483
+ <YStack gap={ 6 }>
484
+ { legs.map((leg, i) => (
412
485
  <LegRow
413
- key={i}
414
- leg={leg}
415
- index={i}
416
- strikes={strikes}
417
- expirations={expirations}
418
- removable={strategyType === 'custom' && legs.length > 1}
419
- onChange={handleLegChange}
420
- onRemove={handleLegRemove}
486
+ key={ i }
487
+ leg={ leg }
488
+ index={ i }
489
+ strikes={ strikes }
490
+ expirations={ expirations }
491
+ removable={ strategyType === 'custom' && legs.length > 1 }
492
+ onChange={ handleLegChange }
493
+ onRemove={ handleLegRemove }
421
494
  />
422
- ))}
423
- </div>
495
+ )) }
496
+ </YStack>
424
497
 
425
498
  {/* Add leg button (custom only) */}
426
- {strategyType === 'custom' && legs.length < 4 && (
427
- <button
428
- type="button"
429
- onClick={handleAddLeg}
430
- className="flex items-center justify-center gap-1 rounded-md border border-dashed border-zinc-700 py-1 text-xs text-zinc-500 hover:text-zinc-300 hover:border-zinc-500 transition-colors"
499
+ { strategyType === 'custom' && legs.length < 4 && (
500
+ <XStack
501
+ unstyled
502
+ render={ <button type="button"/> }
503
+ onClick={ handleAddLeg }
504
+ alignItems="center"
505
+ justifyContent="center"
506
+ gap={ 4 }
507
+ borderRadius={ 6 }
508
+ borderWidth={ 1 }
509
+ paddingVertical={ 4 }
510
+ cursor="pointer"
511
+ transition="quick"
512
+ style={ { borderStyle: 'dashed' } as never }
513
+ borderColor={ 'var(--color-border-divider)' as never }
514
+ color={ 'var(--color-text-secondary)' as never }
515
+ hoverStyle={ { borderColor: 'var(--color-hover)', color: 'var(--color-hover)' } as never }
431
516
  >
432
- <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none">
433
- <path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
517
+ <svg width={ 12 } height={ 12 } viewBox="0 0 24 24" fill="none">
518
+ <path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
434
519
  </svg>
435
- Add Leg
436
- </button>
437
- )}
520
+ <Text unstyled fontSize={ 12 }>Add Leg</Text>
521
+ </XStack>
522
+ ) }
438
523
 
439
524
  {/* Net premium + submit */}
440
- <div className="flex items-center justify-between pt-1 border-t border-zinc-800">
441
- <div className="flex items-center gap-2">
442
- <span className="text-xs text-zinc-500">Net:</span>
443
- <span className={cn(
444
- 'font-mono tabular-nums text-sm font-semibold',
445
- netPremium > 0 ? 'text-emerald-400' : netPremium < 0 ? 'text-red-400' : 'text-zinc-400',
446
- )}>
447
- {netPremium > 0 ? 'Credit ' : netPremium < 0 ? 'Debit ' : ''}
448
- {netPremium !== 0 ? `$${Math.abs(netPremium).toFixed(2)}` : '-'}
449
- </span>
450
- </div>
451
- <button
452
- type="button"
453
- onClick={handleSubmit}
454
- disabled={legs.length === 0}
455
- className={cn(
456
- 'px-4 py-1.5 rounded-md text-xs font-semibold transition-colors',
457
- 'bg-blue-600 text-white hover:bg-blue-500',
458
- 'disabled:opacity-40 disabled:cursor-not-allowed',
459
- )}
525
+ <XStack alignItems="center" justifyContent="space-between" paddingTop={ 4 } borderTopWidth={ 1 } borderColor={ 'var(--color-border-divider)' as never }>
526
+ <XStack alignItems="center" gap={ 8 }>
527
+ <Text fontSize={ 12 } color={ 'var(--color-text-secondary)' as never }>Net:</Text>
528
+ <Text fontSize={ 14 } fontWeight="600" style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ netColor as never }>
529
+ { netPremium > 0 ? 'Credit ' : netPremium < 0 ? 'Debit ' : '' }
530
+ { netPremium !== 0 ? `$${ Math.abs(netPremium).toFixed(2) }` : '-' }
531
+ </Text>
532
+ </XStack>
533
+ <XStack
534
+ unstyled
535
+ render={ <button type="button"/> }
536
+ onClick={ handleSubmit }
537
+ disabled={ legs.length === 0 }
538
+ paddingHorizontal={ 16 }
539
+ paddingVertical={ 6 }
540
+ borderRadius={ 6 }
541
+ borderWidth={ 0 }
542
+ cursor="pointer"
543
+ transition="quick"
544
+ backgroundColor={ 'var(--color-button-solid-bg)' as never }
545
+ hoverStyle={ { backgroundColor: 'var(--color-hover)' } as never }
546
+ disabledStyle={ { opacity: 0.4, cursor: 'not-allowed' } as never }
460
547
  >
461
- Review Order
462
- </button>
463
- </div>
464
- </div>
548
+ <Text fontSize={ 12 } fontWeight="600" color={ 'var(--color-button-solid-text)' as never }>
549
+ Review Order
550
+ </Text>
551
+ </XStack>
552
+ </XStack>
553
+ </YStack>
465
554
  );
466
555
  },
467
556
  );
package/src/switch.tsx CHANGED
@@ -1,32 +1,23 @@
1
- import { Switch as GuiSwitch } from '@hanzo/gui';
1
+ import { Label, Switch as GuiSwitch, Text, VisuallyHidden } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
4
  const NOOP = () => { /* noop */ };
7
5
 
8
6
  // ─── Size mappings ────────────────────────────────────────────────────
9
7
  // Paint and box size only. Thumb travel is NOT a hardcoded translate any more:
10
8
  // gui measures the track and the knob and moves the knob by the difference, so
11
9
  // the switch stays correct at any size and on native.
12
- const SIZE_CLASSES = {
13
- sm: {
14
- root: 'h-4 w-7',
15
- thumb: 'h-3 w-3',
16
- label: 'text-xs',
17
- },
18
- md: {
19
- root: 'h-5 w-9',
20
- thumb: 'h-4 w-4',
21
- label: 'text-sm',
22
- },
23
- lg: {
24
- root: 'h-6 w-11',
25
- thumb: 'h-5 w-5',
26
- label: 'text-base',
27
- },
10
+ const SIZE = {
11
+ sm: { width: 28, height: 16, thumb: 12, label: 12 },
12
+ md: { width: 36, height: 20, thumb: 16, label: 14 },
13
+ lg: { width: 44, height: 24, thumb: 20, label: 16 },
28
14
  } as const;
29
15
 
16
+ const TRACK_ON = 'var(--color-text-primary)' as never;
17
+ const TRACK_OFF = 'var(--color-switch-bg)' as never;
18
+ const OUTLINE = 'var(--color-gray-500)' as never;
19
+ const WHITE = 'var(--color-white)' as never;
20
+
30
21
  // ─── Props ────────────────────────────────────────────────────────────
31
22
  export interface SwitchProps extends Omit<React.ComponentPropsWithoutRef<'label'>, 'onChange' | 'dir'> {
32
23
  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
@@ -59,7 +50,6 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
59
50
  disabled = false,
60
51
  size = 'md',
61
52
  direction,
62
- className,
63
53
  ...rest
64
54
  } = props;
65
55
 
@@ -80,78 +70,87 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
80
70
  [ isControlled, onCheckedChange ],
81
71
  );
82
72
 
83
- const sizeClasses = SIZE_CLASSES[size];
73
+ const s = SIZE[size];
84
74
  const isRtl = direction === 'rtl';
85
75
 
86
76
  return (
87
- <label
88
- ref={ rootRef }
89
- className={ cn(
90
- 'inline-flex items-center cursor-pointer select-none gap-2',
91
- isRtl && 'flex-row-reverse',
92
- disabled && 'opacity-50 cursor-not-allowed',
93
- className,
94
- ) }
95
- onChange={ onChange }
77
+ <Label
78
+ ref={ rootRef as never }
79
+ unstyled
80
+ flexDirection={ isRtl ? 'row-reverse' : 'row' }
81
+ alignItems="center"
82
+ gap={ 8 }
83
+ cursor={ disabled ? 'not-allowed' : 'pointer' }
84
+ userSelect="none"
85
+ opacity={ disabled ? 0.5 : 1 }
86
+ onChange={ onChange as never }
96
87
  data-disabled={ disabled || undefined }
97
- { ...rest }
88
+ { ...(rest as object) }
98
89
  >
99
90
  <GuiSwitch
100
91
  unstyled
101
92
  checked={ checkedState }
102
93
  onCheckedChange={ handleCheckedChange }
103
94
  disabled={ disabled }
104
- className={ cn(
105
- 'relative inline-flex shrink-0 items-center rounded-full',
106
- 'bg-gray-300 dark:bg-gray-600',
107
- 'data-[state=checked]:bg-gray-800 dark:bg-white',
108
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500',
109
- 'transition-colors duration-200',
110
- sizeClasses.root,
111
- ) }
95
+ position="relative"
96
+ flexShrink={ 0 }
97
+ alignItems="center"
98
+ width={ s.width }
99
+ height={ s.height }
100
+ borderRadius={ 9999 }
101
+ backgroundColor={ checkedState ? TRACK_ON : TRACK_OFF }
102
+ transition="quick"
103
+ focusStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: OUTLINE, outlineOffset: 2 }}
112
104
  >
113
105
  { trackLabel && (
114
- <span className="absolute inset-0 flex items-center justify-center text-[8px] font-bold text-white pointer-events-none">
106
+ <Text
107
+ position="absolute"
108
+ width="100%"
109
+ textAlign="center"
110
+ fontSize={ 8 }
111
+ fontWeight="700"
112
+ color={ WHITE }
113
+ pointerEvents="none"
114
+ >
115
115
  { checkedState ? trackLabel.on : trackLabel.off }
116
- </span>
116
+ </Text>
117
117
  ) }
118
118
  <GuiSwitch.Thumb
119
119
  unstyled
120
120
  data-thumb
121
121
  transition="quicker"
122
- className={ cn(
123
- 'block rounded-full bg-white shadow-sm',
124
- sizeClasses.thumb,
125
- ) }
122
+ width={ s.thumb }
123
+ height={ s.thumb }
124
+ borderRadius={ 9999 }
125
+ backgroundColor={ WHITE }
126
+ boxShadow="0 1px 2px 0 rgba(0,0,0,0.05)"
126
127
  >
127
128
  { thumbLabel && (
128
- <span className="flex items-center justify-center h-full w-full text-[8px]">
129
+ <Text width="100%" height="100%" textAlign="center" fontSize={ 8 }>
129
130
  { checkedState ? thumbLabel.on : thumbLabel.off }
130
- </span>
131
+ </Text>
131
132
  ) }
132
133
  </GuiSwitch.Thumb>
133
134
  </GuiSwitch>
134
135
  { /* Hidden native input for form compat and ref forwarding */ }
135
- <input
136
- ref={ ref }
137
- type="checkbox"
138
- className="sr-only"
139
- checked={ checkedState }
140
- disabled={ disabled }
141
- tabIndex={ -1 }
142
- aria-hidden
143
- onChange={ NOOP }
144
- { ...inputProps }
145
- />
136
+ <VisuallyHidden>
137
+ <input
138
+ ref={ ref }
139
+ type="checkbox"
140
+ checked={ checkedState }
141
+ disabled={ disabled }
142
+ tabIndex={ -1 }
143
+ aria-hidden
144
+ onChange={ NOOP }
145
+ { ...inputProps }
146
+ />
147
+ </VisuallyHidden>
146
148
  { children != null && (
147
- <span
148
- { ...labelProps }
149
- className={ cn(sizeClasses.label, labelProps?.className) }
150
- >
149
+ <Text { ...(labelProps as object) } fontSize={ s.label }>
151
150
  { children }
152
- </span>
151
+ </Text>
153
152
  ) }
154
- </label>
153
+ </Label>
155
154
  );
156
155
  },
157
156
  );