@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
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Text, View } 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
  // ---------------------------------------------------------------------------
@@ -57,6 +56,12 @@ function fmtPct(n: number | undefined | null): string {
57
56
  return `${(n * 100).toFixed(1)}%`;
58
57
  }
59
58
 
59
+ // A cell's own text is centred/right/left via flex, not text-align: the host
60
+ // is a <td> (unstyled, so it keeps native table-cell layout) and the content
61
+ // is a gui Text, which has no cascading effect from a parent's CSS text-align.
62
+ const justify = (align: 'left' | 'right' | 'center'): 'flex-start' | 'flex-end' | 'center' =>
63
+ align === 'right' ? 'flex-end' : align === 'left' ? 'flex-start' : 'center';
64
+
60
65
  // ---------------------------------------------------------------------------
61
66
  // QuoteCell — clickable cell in the option chain
62
67
  // ---------------------------------------------------------------------------
@@ -71,19 +76,64 @@ interface QuoteCellProps {
71
76
 
72
77
  function QuoteCell({ value, highlight, align = 'right', onClick, className }: QuoteCellProps) {
73
78
  return (
74
- <td
75
- className={cn(
76
- 'px-1.5 py-1 font-mono tabular-nums text-xs whitespace-nowrap',
77
- align === 'right' ? 'text-right' : 'text-left',
78
- highlight && 'text-zinc-100',
79
- !highlight && 'text-zinc-400',
80
- onClick && 'cursor-pointer hover:bg-zinc-700/50 transition-colors',
81
- className,
82
- )}
83
- onClick={onClick}
79
+ <View
80
+ unstyled
81
+ render={ <td/> }
82
+ onClick={ onClick }
83
+ paddingHorizontal={ 6 }
84
+ paddingVertical={ 4 }
85
+ cursor={ onClick ? 'pointer' : undefined }
86
+ transition="quick"
87
+ hoverStyle={ onClick ? ({ backgroundColor: 'var(--color-popover-item-hover)' } as never) : undefined }
88
+ style={ { display: 'flex', alignItems: 'center', justifyContent: justify(align) } as never }
89
+ className={ className }
84
90
  >
85
- {value}
86
- </td>
91
+ <Text
92
+ fontSize={ 12 }
93
+ style={ { fontFamily: 'monospace', whiteSpace: 'nowrap' } as never }
94
+ fontVariant={ ['tabular-nums'] as never }
95
+ color={ (highlight ? 'var(--color-text-primary)' : 'var(--color-text-secondary)') as never }
96
+ >
97
+ { value }
98
+ </Text>
99
+ </View>
100
+ );
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // HeadCell — <th> in either header row
105
+ // ---------------------------------------------------------------------------
106
+
107
+ interface HeadCellProps {
108
+ readonly children?: React.ReactNode;
109
+ readonly colSpan?: number;
110
+ readonly align?: 'left' | 'right' | 'center';
111
+ readonly size?: 'group' | 'column';
112
+ readonly color?: string;
113
+ }
114
+
115
+ function HeadCell({ children, colSpan, align = 'center', size = 'column', color }: HeadCellProps) {
116
+ return (
117
+ <View
118
+ unstyled
119
+ render={ colSpan ? <th colSpan={ colSpan }/> : <th/> }
120
+ paddingHorizontal={ size === 'group' ? 8 : 6 }
121
+ paddingVertical={ size === 'group' ? 6 : 4 }
122
+ style={ { display: 'flex', alignItems: 'center', justifyContent: justify(align) } as never }
123
+ >
124
+ { children != null && (
125
+ <Text
126
+ fontSize={ 10 }
127
+ fontWeight={ size === 'group' ? '600' : '500' }
128
+ textTransform="uppercase"
129
+ letterSpacing={ 0.5 }
130
+ style={ { whiteSpace: 'nowrap' } as never }
131
+ color={ (color ?? 'var(--color-text-secondary)') as never }
132
+ >
133
+ { children }
134
+ </Text>
135
+ ) }
136
+ </View>
87
137
  );
88
138
  }
89
139
 
@@ -133,58 +183,41 @@ export const OptionChain = React.forwardRef<HTMLDivElement, OptionChainProps>(
133
183
  );
134
184
 
135
185
  return (
136
- <div
137
- ref={ref}
138
- className={cn('w-full overflow-x-auto', className)}
139
- {...rest}
186
+ <View
187
+ ref={ ref as never }
188
+ unstyled
189
+ width="100%"
190
+ overflowX="auto"
191
+ className={ className }
192
+ { ...(rest as object) }
140
193
  >
141
- <table className="w-full border-collapse text-xs">
142
- <thead>
143
- <tr className="border-b border-zinc-800">
194
+ <View unstyled render={ <table/> } width="100%" style={ { borderCollapse: 'collapse' } as never }>
195
+ <View unstyled render={ <thead/> }>
196
+ <View unstyled render={ <tr/> } borderBottomWidth={ 1 } borderColor={ 'var(--color-border-divider)' as never }>
144
197
  {/* Call headers */}
145
- <th
146
- colSpan={CALL_HEADERS.length}
147
- className="px-2 py-1.5 text-center text-[10px] font-semibold uppercase tracking-wider text-emerald-400"
148
- >
149
- Calls
150
- </th>
198
+ <HeadCell colSpan={ CALL_HEADERS.length } size="group" color="var(--color-status-good)">Calls</HeadCell>
151
199
  {/* Strike header */}
152
- <th className="px-2 py-1.5 text-center text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
153
- Strike
154
- </th>
200
+ <HeadCell size="group">Strike</HeadCell>
155
201
  {/* Put headers */}
156
- <th
157
- colSpan={PUT_HEADERS.length}
158
- className="px-2 py-1.5 text-center text-[10px] font-semibold uppercase tracking-wider text-red-400"
159
- >
160
- Puts
161
- </th>
162
- </tr>
163
- <tr className="border-b border-zinc-800/60">
164
- {CALL_HEADERS.map((h) => (
165
- <th
166
- key={`call-${h}`}
167
- className="px-1.5 py-1 text-right text-[10px] font-medium uppercase tracking-wider text-zinc-500"
168
- >
169
- {h}
170
- </th>
171
- ))}
172
- <th className="px-2 py-1 text-center text-[10px] font-medium uppercase tracking-wider text-zinc-500" />
173
- {PUT_HEADERS.map((h) => (
174
- <th
175
- key={`put-${h}`}
176
- className={cn(
177
- 'px-1.5 py-1 text-[10px] font-medium uppercase tracking-wider text-zinc-500',
178
- h === 'Bid' || h === 'Ask' || h === 'Last' ? 'text-left' : 'text-right',
179
- )}
202
+ <HeadCell colSpan={ PUT_HEADERS.length } size="group" color="var(--color-status-bad)">Puts</HeadCell>
203
+ </View>
204
+ <View unstyled render={ <tr/> } borderBottomWidth={ 1 } borderColor={ 'var(--color-popover-item-hover)' as never }>
205
+ { CALL_HEADERS.map((h) => (
206
+ <HeadCell key={ `call-${ h }` } align="right">{ h }</HeadCell>
207
+ )) }
208
+ <HeadCell/>
209
+ { PUT_HEADERS.map((h) => (
210
+ <HeadCell
211
+ key={ `put-${ h }` }
212
+ align={ h === 'Bid' || h === 'Ask' || h === 'Last' ? 'left' : 'right' }
180
213
  >
181
- {h}
182
- </th>
183
- ))}
184
- </tr>
185
- </thead>
186
- <tbody>
187
- {strikes.map((row) => {
214
+ { h }
215
+ </HeadCell>
216
+ )) }
217
+ </View>
218
+ </View>
219
+ <View unstyled render={ <tbody/> }>
220
+ { strikes.map((row) => {
188
221
  const isATM = Math.abs(row.strike - spotPrice) <=
189
222
  (strikes.length > 1
190
223
  ? Math.abs((strikes[1]?.strike ?? 0) - (strikes[0]?.strike ?? 0)) / 2
@@ -194,68 +227,85 @@ export const OptionChain = React.forwardRef<HTMLDivElement, OptionChainProps>(
194
227
  const c = row.call;
195
228
  const p = row.put;
196
229
 
230
+ const rowBg = isATM
231
+ ? 'var(--secondary)'
232
+ : callITM
233
+ ? 'color-mix(in srgb, var(--color-status-good) 12%, transparent)'
234
+ : putITM
235
+ ? 'color-mix(in srgb, var(--color-status-bad) 12%, transparent)'
236
+ : 'transparent';
237
+
197
238
  return (
198
- <tr
199
- key={row.strike}
200
- className={cn(
201
- 'border-b border-zinc-800/30 transition-colors',
202
- isATM && 'bg-zinc-800/60',
203
- !isATM && callITM && 'bg-emerald-950/20',
204
- !isATM && putITM && 'bg-red-950/20',
205
- !isATM && !callITM && !putITM && 'hover:bg-zinc-800/30',
206
- )}
239
+ <View
240
+ key={ row.strike }
241
+ unstyled
242
+ render={ <tr/> }
243
+ borderBottomWidth={ 1 }
244
+ borderColor={ 'color-mix(in srgb, var(--color-border-divider) 50%, transparent)' as never }
245
+ transition="quick"
246
+ backgroundColor={ rowBg as never }
247
+ hoverStyle={ isATM || callITM || putITM ? undefined : ({ backgroundColor: 'var(--color-popover-item-hover)' } as never) }
207
248
  >
208
249
  {/* Call side */}
209
250
  <QuoteCell
210
- value={fmt(c?.bid)}
211
- highlight={!!c}
212
- onClick={() => handleClick(c, 'call', row.strike)}
251
+ value={ fmt(c?.bid) }
252
+ highlight={ !!c }
253
+ onClick={ () => handleClick(c, 'call', row.strike) }
213
254
  />
214
255
  <QuoteCell
215
- value={fmt(c?.ask)}
216
- highlight={!!c}
217
- onClick={() => handleClick(c, 'call', row.strike)}
256
+ value={ fmt(c?.ask) }
257
+ highlight={ !!c }
258
+ onClick={ () => handleClick(c, 'call', row.strike) }
218
259
  />
219
- <QuoteCell value={fmt(c?.last)} highlight={!!c} />
220
- <QuoteCell value={fmtInt(c?.volume)} />
221
- <QuoteCell value={fmtInt(c?.openInterest)} />
222
- <QuoteCell value={fmtPct(c?.iv)} />
223
- <QuoteCell value={fmt(c?.delta, 3)} />
260
+ <QuoteCell value={ fmt(c?.last) } highlight={ !!c }/>
261
+ <QuoteCell value={ fmtInt(c?.volume) }/>
262
+ <QuoteCell value={ fmtInt(c?.openInterest) }/>
263
+ <QuoteCell value={ fmtPct(c?.iv) }/>
264
+ <QuoteCell value={ fmt(c?.delta, 3) }/>
224
265
 
225
266
  {/* Strike */}
226
- <td
227
- className={cn(
228
- 'px-2 py-1 text-center font-mono tabular-nums text-xs font-semibold',
229
- isATM ? 'text-[var(--foreground)] bg-[var(--secondary)]' : 'text-[var(--muted-foreground)]',
230
- )}
267
+ <View
268
+ unstyled
269
+ render={ <td/> }
270
+ paddingHorizontal={ 8 }
271
+ paddingVertical={ 4 }
272
+ style={ { display: 'flex', alignItems: 'center', justifyContent: 'center' } as never }
231
273
  >
232
- {fmt(row.strike)}
233
- </td>
274
+ <Text
275
+ fontSize={ 12 }
276
+ fontWeight="600"
277
+ style={ { fontFamily: 'monospace' } as never }
278
+ fontVariant={ ['tabular-nums'] as never }
279
+ color={ (isATM ? 'var(--foreground)' : 'var(--muted-foreground)') as never }
280
+ >
281
+ { fmt(row.strike) }
282
+ </Text>
283
+ </View>
234
284
 
235
285
  {/* Put side (mirrored order) */}
236
- <QuoteCell value={fmt(p?.delta, 3)} align="right" />
237
- <QuoteCell value={fmtPct(p?.iv)} align="right" />
238
- <QuoteCell value={fmtInt(p?.openInterest)} align="right" />
239
- <QuoteCell value={fmtInt(p?.volume)} align="right" />
240
- <QuoteCell value={fmt(p?.last)} align="left" highlight={!!p} />
286
+ <QuoteCell value={ fmt(p?.delta, 3) } align="right"/>
287
+ <QuoteCell value={ fmtPct(p?.iv) } align="right"/>
288
+ <QuoteCell value={ fmtInt(p?.openInterest) } align="right"/>
289
+ <QuoteCell value={ fmtInt(p?.volume) } align="right"/>
290
+ <QuoteCell value={ fmt(p?.last) } align="left" highlight={ !!p }/>
241
291
  <QuoteCell
242
- value={fmt(p?.ask)}
292
+ value={ fmt(p?.ask) }
243
293
  align="left"
244
- highlight={!!p}
245
- onClick={() => handleClick(p, 'put', row.strike)}
294
+ highlight={ !!p }
295
+ onClick={ () => handleClick(p, 'put', row.strike) }
246
296
  />
247
297
  <QuoteCell
248
- value={fmt(p?.bid)}
298
+ value={ fmt(p?.bid) }
249
299
  align="left"
250
- highlight={!!p}
251
- onClick={() => handleClick(p, 'put', row.strike)}
300
+ highlight={ !!p }
301
+ onClick={ () => handleClick(p, 'put', row.strike) }
252
302
  />
253
- </tr>
303
+ </View>
254
304
  );
255
- })}
256
- </tbody>
257
- </table>
258
- </div>
305
+ }) }
306
+ </View>
307
+ </View>
308
+ </View>
259
309
  );
260
310
  },
261
311
  );
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Text, 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
  // ---------------------------------------------------------------------------
@@ -43,6 +42,8 @@ function formatDate(date: string): string {
43
42
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
44
43
  }
45
44
 
45
+ const MONO = { fontFamily: 'monospace' } as never;
46
+
46
47
  // ---------------------------------------------------------------------------
47
48
  // OptionPositionCard
48
49
  // ---------------------------------------------------------------------------
@@ -61,119 +62,149 @@ export const OptionPositionCard = React.forwardRef<HTMLDivElement, OptionPositio
61
62
  const isLong = position.quantity > 0;
62
63
  const isCall = position.type === 'call';
63
64
  const isProfitable = position.pnl >= 0;
65
+ const pnlColor = (isProfitable ? 'var(--color-status-good)' : 'var(--color-status-bad)') as never;
64
66
 
65
67
  return (
66
- <div
67
- ref={ref}
68
- className={cn(
69
- 'flex flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3',
70
- className,
71
- )}
72
- {...rest}
68
+ <YStack
69
+ ref={ ref as never }
70
+ gap={ 8 }
71
+ borderRadius={ 8 }
72
+ borderWidth={ 1 }
73
+ borderColor={ 'var(--color-border-divider)' as never }
74
+ backgroundColor={ 'var(--card)' as never }
75
+ padding={ 12 }
76
+ className={ className }
77
+ { ...(rest as object) }
73
78
  >
74
79
  {/* Header row: symbol badge + quantity */}
75
- <div className="flex items-center justify-between">
76
- <div className="flex items-center gap-2">
77
- {/* Symbol badge */}
78
- <span className={cn(
79
- 'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold',
80
- // Call vs put is a rank difference, not a hue difference.
81
- isCall ? 'bg-[var(--secondary)] text-[var(--foreground)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]',
82
- )}>
83
- {position.underlying}
84
- <span className="font-mono tabular-nums">{position.strike}</span>
85
- <span className="uppercase">{position.type === 'call' ? 'C' : 'P'}</span>
86
- <span className="text-zinc-500 font-normal">{formatDate(position.expiration)}</span>
87
- </span>
88
- </div>
80
+ <XStack alignItems="center" justifyContent="space-between">
81
+ <XStack alignItems="center" gap={ 8 }>
82
+ {/* Symbol badge. Call vs put is a rank difference, not a hue difference. */}
83
+ <XStack
84
+ alignItems="center"
85
+ gap={ 4 }
86
+ borderRadius={ 4 }
87
+ paddingHorizontal={ 6 }
88
+ paddingVertical={ 2 }
89
+ backgroundColor={ (isCall ? 'var(--secondary)' : 'var(--muted)') as never }
90
+ >
91
+ <Text fontSize={ 12 } fontWeight="600" color={ (isCall ? 'var(--foreground)' : 'var(--muted-foreground)') as never }>
92
+ { position.underlying }
93
+ </Text>
94
+ <Text fontSize={ 12 } fontWeight="600" style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ (isCall ? 'var(--foreground)' : 'var(--muted-foreground)') as never }>
95
+ { position.strike }
96
+ </Text>
97
+ <Text fontSize={ 12 } fontWeight="600" textTransform="uppercase" color={ (isCall ? 'var(--foreground)' : 'var(--muted-foreground)') as never }>
98
+ { position.type === 'call' ? 'C' : 'P' }
99
+ </Text>
100
+ <Text fontSize={ 12 } fontWeight="400" color={ 'var(--color-text-secondary)' as never }>
101
+ { formatDate(position.expiration) }
102
+ </Text>
103
+ </XStack>
104
+ </XStack>
89
105
 
90
106
  {/* Quantity */}
91
- <span className={cn(
92
- 'text-xs font-semibold font-mono tabular-nums',
93
- isLong ? 'text-emerald-400' : 'text-red-400',
94
- )}>
95
- {isLong ? '+' : ''}{position.quantity}
96
- </span>
97
- </div>
98
-
99
- {/* Values row */}
100
- <div className="grid grid-cols-3 gap-3">
101
- <div className="flex flex-col">
102
- <span className="text-[10px] uppercase tracking-wider text-zinc-500">Avg Cost</span>
103
- <span className="font-mono tabular-nums text-xs text-zinc-300">
104
- ${position.avgCost.toFixed(2)}
105
- </span>
106
- </div>
107
- <div className="flex flex-col">
108
- <span className="text-[10px] uppercase tracking-wider text-zinc-500">Value</span>
109
- <span className="font-mono tabular-nums text-xs text-zinc-300">
110
- ${position.currentValue.toFixed(2)}
111
- </span>
112
- </div>
113
- <div className="flex flex-col items-end">
114
- <span className="text-[10px] uppercase tracking-wider text-zinc-500">P/L</span>
115
- <div className="flex items-center gap-1">
116
- <span className={cn(
117
- 'font-mono tabular-nums text-xs font-semibold',
118
- isProfitable ? 'text-emerald-400' : 'text-red-400',
119
- )}>
120
- {isProfitable ? '+' : ''}{position.pnl.toFixed(2)}
121
- </span>
122
- <span className={cn(
123
- 'font-mono tabular-nums text-[10px]',
124
- isProfitable ? 'text-emerald-500/70' : 'text-red-500/70',
125
- )}>
126
- ({isProfitable ? '+' : ''}{position.pnlPercent.toFixed(1)}%)
127
- </span>
128
- </div>
129
- </div>
130
- </div>
131
-
132
- {/* Greeks row */}
133
- <div className="flex items-center gap-3 border-t border-zinc-800/60 pt-2">
134
- <GreekPill label={'\u0394'} value={position.greeks.delta} color="text-blue-400" />
135
- <GreekPill label={'\u0393'} value={position.greeks.gamma} color="text-purple-400" />
136
- <GreekPill label={'\u0398'} value={position.greeks.theta} color="text-red-400" />
137
- <GreekPill label={'\u03BD'} value={position.greeks.vega} color="text-emerald-400" />
138
- </div>
107
+ <Text
108
+ fontSize={ 12 }
109
+ fontWeight="600"
110
+ style={ MONO }
111
+ fontVariant={ ['tabular-nums'] as never }
112
+ color={ (isLong ? 'var(--color-status-good)' : 'var(--color-status-bad)') as never }
113
+ >
114
+ { isLong ? '+' : '' }{ position.quantity }
115
+ </Text>
116
+ </XStack>
117
+
118
+ {/* Values row (3 equal columns) */}
119
+ <XStack gap={ 12 }>
120
+ <YStack flex={ 1 } gap={ 2 }>
121
+ <Text fontSize={ 10 } textTransform="uppercase" letterSpacing={ 0.5 } color={ 'var(--color-text-secondary)' as never }>Avg Cost</Text>
122
+ <Text fontSize={ 12 } style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ 'var(--color-text-primary)' as never }>
123
+ ${ position.avgCost.toFixed(2) }
124
+ </Text>
125
+ </YStack>
126
+ <YStack flex={ 1 } gap={ 2 }>
127
+ <Text fontSize={ 10 } textTransform="uppercase" letterSpacing={ 0.5 } color={ 'var(--color-text-secondary)' as never }>Value</Text>
128
+ <Text fontSize={ 12 } style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ 'var(--color-text-primary)' as never }>
129
+ ${ position.currentValue.toFixed(2) }
130
+ </Text>
131
+ </YStack>
132
+ <YStack flex={ 1 } gap={ 2 } alignItems="flex-end">
133
+ <Text fontSize={ 10 } textTransform="uppercase" letterSpacing={ 0.5 } color={ 'var(--color-text-secondary)' as never }>P/L</Text>
134
+ <XStack alignItems="center" gap={ 4 }>
135
+ <Text fontSize={ 12 } fontWeight="600" style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ pnlColor }>
136
+ { isProfitable ? '+' : '' }{ position.pnl.toFixed(2) }
137
+ </Text>
138
+ <Text fontSize={ 10 } style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ pnlColor } opacity={ 0.7 }>
139
+ ({ isProfitable ? '+' : '' }{ position.pnlPercent.toFixed(1) }%)
140
+ </Text>
141
+ </XStack>
142
+ </YStack>
143
+ </XStack>
144
+
145
+ {/* Greeks row — same achromatic ramp as <GreeksDisplay>: a greek is
146
+ identified by its symbol, never by a hue. */}
147
+ <XStack alignItems="center" gap={ 12 } borderTopWidth={ 1 } borderColor={ 'var(--color-border-divider)' as never } paddingTop={ 8 }>
148
+ <GreekPill label="Δ" value={ position.greeks.delta } color="var(--foreground)"/>
149
+ <GreekPill label="Γ" value={ position.greeks.gamma } color="var(--chart-1)"/>
150
+ <GreekPill label="Θ" value={ position.greeks.theta } color="var(--chart-4)"/>
151
+ <GreekPill label="ν" value={ position.greeks.vega } color="var(--chart-2)"/>
152
+ </XStack>
139
153
 
140
154
  {/* Action buttons */}
141
- {(onClose || onExercise || onRoll) && (
142
- <div className="flex items-center gap-2 border-t border-zinc-800/60 pt-2">
143
- {onClose && (
144
- <button
145
- type="button"
146
- onClick={() => onClose(position)}
147
- className="flex-1 rounded-md bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-300 hover:bg-zinc-700 hover:text-zinc-100 transition-colors"
148
- >
149
- Close
150
- </button>
151
- )}
152
- {onExercise && (
153
- <button
154
- type="button"
155
- onClick={() => onExercise(position)}
156
- className="flex-1 rounded-md bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-300 hover:bg-zinc-700 hover:text-zinc-100 transition-colors"
157
- >
158
- Exercise
159
- </button>
160
- )}
161
- {onRoll && (
162
- <button
163
- type="button"
164
- onClick={() => onRoll(position)}
165
- className="flex-1 rounded-md bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-300 hover:bg-zinc-700 hover:text-zinc-100 transition-colors"
166
- >
167
- Roll
168
- </button>
169
- )}
170
- </div>
171
- )}
172
- </div>
155
+ { (onClose || onExercise || onRoll) && (
156
+ <XStack alignItems="center" gap={ 8 } borderTopWidth={ 1 } borderColor={ 'var(--color-border-divider)' as never } paddingTop={ 8 }>
157
+ { onClose && <ActionButton onClick={ () => onClose(position) }>Close</ActionButton> }
158
+ { onExercise && <ActionButton onClick={ () => onExercise(position) }>Exercise</ActionButton> }
159
+ { onRoll && <ActionButton onClick={ () => onRoll(position) }>Roll</ActionButton> }
160
+ </XStack>
161
+ ) }
162
+ </YStack>
173
163
  );
174
164
  },
175
165
  );
176
166
 
167
+ // ---------------------------------------------------------------------------
168
+ // ActionButton (internal) — Close / Exercise / Roll. Same resting/hover pair
169
+ // button.tsx's "subtle" variant uses: hover repaints the text, not the fill.
170
+ // ---------------------------------------------------------------------------
171
+
172
+ interface ActionButtonProps {
173
+ readonly onClick: () => void;
174
+ readonly children: React.ReactNode;
175
+ }
176
+
177
+ function ActionButton({ onClick, children }: ActionButtonProps) {
178
+ return (
179
+ <XStack
180
+ unstyled
181
+ render={ <button type="button"/> }
182
+ onClick={ onClick }
183
+ flex={ 1 }
184
+ alignItems="center"
185
+ justifyContent="center"
186
+ borderRadius={ 6 }
187
+ borderWidth={ 0 }
188
+ paddingHorizontal={ 8 }
189
+ paddingVertical={ 4 }
190
+ backgroundColor={ 'var(--color-button-subtle-bg)' as never }
191
+ cursor="pointer"
192
+ >
193
+ {/* `hoverStyle` lives on the Text, not the button frame — Stacks carry
194
+ no `color` (same reason they carry no `fontSize`), so the hover
195
+ re-tint has to be the text's own state, not the button's. */}
196
+ <Text
197
+ fontSize={ 12 }
198
+ fontWeight="500"
199
+ color={ 'var(--color-button-subtle-fg)' as never }
200
+ hoverStyle={ { color: 'var(--color-hover)' } as never }
201
+ >
202
+ { children }
203
+ </Text>
204
+ </XStack>
205
+ );
206
+ }
207
+
177
208
  // ---------------------------------------------------------------------------
178
209
  // GreekPill (internal)
179
210
  // ---------------------------------------------------------------------------
@@ -181,21 +212,19 @@ export const OptionPositionCard = React.forwardRef<HTMLDivElement, OptionPositio
181
212
  interface GreekPillProps {
182
213
  readonly label: string;
183
214
  readonly value: number;
215
+ /** A resolved `var(--…)` token — this component is package-private. */
184
216
  readonly color: string;
185
217
  }
186
218
 
187
219
  function GreekPill({ label, value, color }: GreekPillProps) {
188
220
  return (
189
- <div className="flex items-center gap-1">
190
- <span className={cn('text-[10px] font-semibold', color)}>
191
- {label}
192
- </span>
193
- <span className={cn(
194
- 'font-mono tabular-nums text-[11px]',
195
- value < 0 ? 'text-red-400' : 'text-zinc-400',
196
- )}>
197
- {value.toFixed(4)}
198
- </span>
199
- </div>
221
+ <XStack alignItems="center" gap={ 4 }>
222
+ <Text fontSize={ 10 } fontWeight="600" color={ color as never }>
223
+ { label }
224
+ </Text>
225
+ <Text fontSize={ 11 } style={ MONO } fontVariant={ ['tabular-nums'] as never } color={ (value < 0 ? 'var(--color-status-bad)' : 'var(--color-text-secondary)') as never }>
226
+ { value.toFixed(4) }
227
+ </Text>
228
+ </XStack>
200
229
  );
201
230
  }