@luxfi/ui 6.1.2 → 7.0.0

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 (240) hide show
  1. package/dist/accordion.cjs +213 -0
  2. package/dist/accordion.d.cts +74 -0
  3. package/dist/accordion.d.ts +74 -0
  4. package/dist/accordion.js +186 -0
  5. package/dist/alert.cjs +553 -0
  6. package/dist/alert.d.cts +30 -0
  7. package/dist/alert.d.ts +30 -0
  8. package/dist/alert.js +531 -0
  9. package/dist/avatar.cjs +149 -0
  10. package/dist/avatar.d.cts +25 -0
  11. package/dist/avatar.d.ts +25 -0
  12. package/dist/avatar.js +125 -0
  13. package/dist/badge.cjs +611 -0
  14. package/dist/badge.d.cts +23 -0
  15. package/dist/badge.d.ts +23 -0
  16. package/dist/badge.js +589 -0
  17. package/dist/bank.cjs +272 -0
  18. package/dist/bank.d.cts +76 -0
  19. package/dist/bank.d.ts +76 -0
  20. package/dist/bank.js +258 -0
  21. package/dist/button.cjs +689 -0
  22. package/dist/button.d.cts +52 -0
  23. package/dist/button.d.ts +52 -0
  24. package/dist/button.js +664 -0
  25. package/dist/checkbox.cjs +265 -0
  26. package/dist/checkbox.d.cts +31 -0
  27. package/dist/checkbox.d.ts +31 -0
  28. package/dist/checkbox.js +241 -0
  29. package/dist/close-button.cjs +73 -0
  30. package/dist/close-button.d.cts +9 -0
  31. package/dist/close-button.d.ts +9 -0
  32. package/dist/close-button.js +51 -0
  33. package/dist/collapsible.cjs +702 -0
  34. package/dist/collapsible.d.cts +23 -0
  35. package/dist/collapsible.d.ts +23 -0
  36. package/dist/collapsible.js +679 -0
  37. package/dist/color-mode.cjs +96 -0
  38. package/dist/color-mode.d.cts +25 -0
  39. package/dist/color-mode.d.ts +25 -0
  40. package/dist/color-mode.js +72 -0
  41. package/dist/dialog.cjs +279 -0
  42. package/dist/dialog.d.cts +66 -0
  43. package/dist/dialog.d.ts +66 -0
  44. package/dist/dialog.js +246 -0
  45. package/dist/drawer.cjs +207 -0
  46. package/dist/drawer.d.cts +36 -0
  47. package/dist/drawer.d.ts +36 -0
  48. package/dist/drawer.js +175 -0
  49. package/dist/empty-state.cjs +93 -0
  50. package/dist/empty-state.d.cts +13 -0
  51. package/dist/empty-state.d.ts +13 -0
  52. package/dist/empty-state.js +71 -0
  53. package/dist/expiration-selector.cjs +138 -0
  54. package/dist/expiration-selector.d.cts +16 -0
  55. package/dist/expiration-selector.d.ts +16 -0
  56. package/dist/expiration-selector.js +116 -0
  57. package/dist/field.cjs +183 -0
  58. package/dist/field.d.cts +24 -0
  59. package/dist/field.d.ts +24 -0
  60. package/dist/field.js +160 -0
  61. package/dist/greeks-display.cjs +114 -0
  62. package/dist/greeks-display.d.cts +14 -0
  63. package/dist/greeks-display.d.ts +14 -0
  64. package/dist/greeks-display.js +92 -0
  65. package/dist/heading.cjs +46 -0
  66. package/dist/heading.d.cts +10 -0
  67. package/dist/heading.d.ts +10 -0
  68. package/dist/heading.js +40 -0
  69. package/dist/icon-button.cjs +491 -0
  70. package/dist/icon-button.d.cts +33 -0
  71. package/dist/icon-button.d.ts +33 -0
  72. package/dist/icon-button.js +470 -0
  73. package/dist/image.cjs +572 -0
  74. package/dist/image.d.cts +57 -0
  75. package/dist/image.d.ts +57 -0
  76. package/dist/image.js +551 -0
  77. package/dist/index.cjs +7316 -0
  78. package/dist/index.d.cts +58 -0
  79. package/dist/index.d.ts +58 -0
  80. package/dist/index.js +7137 -0
  81. package/dist/input-group.cjs +155 -0
  82. package/dist/input-group.d.cts +17 -0
  83. package/dist/input-group.d.ts +17 -0
  84. package/dist/input-group.js +133 -0
  85. package/dist/input.cjs +65 -0
  86. package/dist/input.d.cts +11 -0
  87. package/dist/input.d.ts +11 -0
  88. package/dist/input.js +59 -0
  89. package/dist/link.cjs +630 -0
  90. package/dist/link.d.cts +37 -0
  91. package/dist/link.d.ts +37 -0
  92. package/dist/link.js +606 -0
  93. package/dist/menu.cjs +305 -0
  94. package/dist/menu.d.cts +84 -0
  95. package/dist/menu.d.ts +84 -0
  96. package/dist/menu.js +269 -0
  97. package/dist/option-chain.cjs +221 -0
  98. package/dist/option-chain.d.cts +37 -0
  99. package/dist/option-chain.d.ts +37 -0
  100. package/dist/option-chain.js +199 -0
  101. package/dist/option-position.cjs +164 -0
  102. package/dist/option-position.d.cts +29 -0
  103. package/dist/option-position.d.ts +29 -0
  104. package/dist/option-position.js +142 -0
  105. package/dist/pin-input.cjs +182 -0
  106. package/dist/pin-input.d.cts +25 -0
  107. package/dist/pin-input.d.ts +25 -0
  108. package/dist/pin-input.js +160 -0
  109. package/dist/pnl-diagram.cjs +453 -0
  110. package/dist/pnl-diagram.d.cts +11 -0
  111. package/dist/pnl-diagram.d.ts +11 -0
  112. package/dist/pnl-diagram.js +431 -0
  113. package/dist/popover.cjs +327 -0
  114. package/dist/popover.d.cts +73 -0
  115. package/dist/popover.d.ts +73 -0
  116. package/dist/popover.js +294 -0
  117. package/dist/progress-circle.cjs +152 -0
  118. package/dist/progress-circle.d.cts +47 -0
  119. package/dist/progress-circle.d.ts +47 -0
  120. package/dist/progress-circle.js +128 -0
  121. package/dist/progress.cjs +117 -0
  122. package/dist/progress.d.cts +28 -0
  123. package/dist/progress.d.ts +28 -0
  124. package/dist/progress.js +94 -0
  125. package/dist/provider.cjs +62 -0
  126. package/dist/provider.d.cts +7 -0
  127. package/dist/provider.d.ts +7 -0
  128. package/dist/provider.js +40 -0
  129. package/dist/radio.cjs +177 -0
  130. package/dist/radio.d.cts +53 -0
  131. package/dist/radio.d.ts +53 -0
  132. package/dist/radio.js +153 -0
  133. package/dist/rating.cjs +80 -0
  134. package/dist/rating.d.cts +14 -0
  135. package/dist/rating.d.ts +14 -0
  136. package/dist/rating.js +58 -0
  137. package/dist/select.cjs +791 -0
  138. package/dist/select.d.cts +130 -0
  139. package/dist/select.d.ts +130 -0
  140. package/dist/select.js +757 -0
  141. package/dist/separator.cjs +57 -0
  142. package/dist/separator.d.cts +13 -0
  143. package/dist/separator.d.ts +13 -0
  144. package/dist/separator.js +51 -0
  145. package/dist/skeleton.cjs +370 -0
  146. package/dist/skeleton.d.cts +60 -0
  147. package/dist/skeleton.d.ts +60 -0
  148. package/dist/skeleton.js +346 -0
  149. package/dist/slider.cjs +138 -0
  150. package/dist/slider.d.cts +24 -0
  151. package/dist/slider.d.ts +24 -0
  152. package/dist/slider.js +115 -0
  153. package/dist/strategy-builder.cjs +373 -0
  154. package/dist/strategy-builder.d.cts +26 -0
  155. package/dist/strategy-builder.d.ts +26 -0
  156. package/dist/strategy-builder.js +351 -0
  157. package/dist/switch.cjs +163 -0
  158. package/dist/switch.d.cts +27 -0
  159. package/dist/switch.d.ts +27 -0
  160. package/dist/switch.js +140 -0
  161. package/dist/table.cjs +1044 -0
  162. package/dist/table.d.cts +107 -0
  163. package/dist/table.d.ts +107 -0
  164. package/dist/table.js +1013 -0
  165. package/dist/tabs.cjs +240 -0
  166. package/dist/tabs.d.cts +78 -0
  167. package/dist/tabs.d.ts +78 -0
  168. package/dist/tabs.js +213 -0
  169. package/dist/tag.cjs +651 -0
  170. package/dist/tag.d.cts +21 -0
  171. package/dist/tag.d.ts +21 -0
  172. package/dist/tag.js +628 -0
  173. package/dist/textarea.cjs +65 -0
  174. package/dist/textarea.d.cts +11 -0
  175. package/dist/textarea.d.ts +11 -0
  176. package/dist/textarea.js +59 -0
  177. package/dist/toaster.cjs +99 -0
  178. package/dist/toaster.d.cts +29 -0
  179. package/dist/toaster.d.ts +29 -0
  180. package/dist/toaster.js +96 -0
  181. package/dist/tooltip.cjs +171 -0
  182. package/dist/tooltip.d.cts +39 -0
  183. package/dist/tooltip.d.ts +39 -0
  184. package/dist/tooltip.js +148 -0
  185. package/dist/utils.cjs +11 -0
  186. package/dist/utils.d.cts +5 -0
  187. package/dist/utils.d.ts +5 -0
  188. package/dist/utils.js +9 -0
  189. package/package.json +284 -85
  190. package/src/accordion.tsx +285 -0
  191. package/src/alert.tsx +221 -0
  192. package/src/avatar.tsx +174 -0
  193. package/src/badge.tsx +158 -0
  194. package/src/bank.tsx +325 -0
  195. package/src/button.tsx +411 -0
  196. package/src/checkbox.tsx +307 -0
  197. package/src/close-button.tsx +51 -0
  198. package/src/collapsible.tsx +126 -0
  199. package/src/color-mode.tsx +125 -0
  200. package/src/dialog.tsx +356 -0
  201. package/src/drawer.tsx +186 -0
  202. package/src/empty-state.tsx +97 -0
  203. package/src/expiration-selector.tsx +163 -0
  204. package/src/field.tsx +202 -0
  205. package/src/greeks-display.tsx +138 -0
  206. package/src/heading.tsx +55 -0
  207. package/src/icon-button.tsx +192 -0
  208. package/src/image.tsx +280 -0
  209. package/src/index.ts +268 -0
  210. package/src/input-group.tsx +159 -0
  211. package/src/input.tsx +60 -0
  212. package/src/link.tsx +326 -0
  213. package/src/menu.tsx +471 -0
  214. package/src/option-chain.tsx +261 -0
  215. package/src/option-position.tsx +200 -0
  216. package/src/pin-input.tsx +187 -0
  217. package/src/pnl-diagram.tsx +458 -0
  218. package/src/popover.tsx +400 -0
  219. package/src/progress-circle.tsx +180 -0
  220. package/src/progress.tsx +109 -0
  221. package/src/provider.tsx +12 -0
  222. package/src/radio.tsx +175 -0
  223. package/src/rating.tsx +79 -0
  224. package/src/select.tsx +696 -0
  225. package/src/separator.tsx +59 -0
  226. package/src/skeleton.tsx +302 -0
  227. package/src/slider.tsx +152 -0
  228. package/src/strategy-builder.tsx +466 -0
  229. package/src/switch.tsx +158 -0
  230. package/src/table.tsx +621 -0
  231. package/src/tabs.tsx +354 -0
  232. package/src/tag.tsx +159 -0
  233. package/src/textarea.tsx +60 -0
  234. package/src/toaster.tsx +117 -0
  235. package/src/tokens.css +438 -0
  236. package/src/tooltip.tsx +184 -0
  237. package/src/utils/cn.ts +7 -0
  238. package/src/utils.ts +6 -0
  239. package/tokens.css +438 -0
  240. package/README.md +0 -109
@@ -0,0 +1,261 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ import { cn } from './utils';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface OptionQuote {
12
+ symbol: string;
13
+ bid: number;
14
+ ask: number;
15
+ last: number;
16
+ volume: number;
17
+ openInterest: number;
18
+ iv: number;
19
+ delta: number;
20
+ gamma: number;
21
+ theta: number;
22
+ vega: number;
23
+ }
24
+
25
+ export interface OptionContract {
26
+ symbol: string;
27
+ type: 'call' | 'put';
28
+ strike: number;
29
+ bid: number;
30
+ ask: number;
31
+ }
32
+
33
+ export interface Strike {
34
+ strike: number;
35
+ call: OptionQuote | null;
36
+ put: OptionQuote | null;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function fmt(n: number | undefined | null, decimals = 2): string {
44
+ if (n === undefined || n === null) return '-';
45
+ return n.toFixed(decimals);
46
+ }
47
+
48
+ function fmtInt(n: number | undefined | null): string {
49
+ if (n === undefined || n === null) return '-';
50
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
51
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
52
+ return n.toLocaleString();
53
+ }
54
+
55
+ function fmtPct(n: number | undefined | null): string {
56
+ if (n === undefined || n === null) return '-';
57
+ return `${(n * 100).toFixed(1)}%`;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // QuoteCell — clickable cell in the option chain
62
+ // ---------------------------------------------------------------------------
63
+
64
+ interface QuoteCellProps {
65
+ readonly value: string;
66
+ readonly highlight?: boolean;
67
+ readonly align?: 'left' | 'right';
68
+ readonly onClick?: () => void;
69
+ readonly className?: string;
70
+ }
71
+
72
+ function QuoteCell({ value, highlight, align = 'right', onClick, className }: QuoteCellProps) {
73
+ 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}
84
+ >
85
+ {value}
86
+ </td>
87
+ );
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Column headers
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const CALL_HEADERS = ['Bid', 'Ask', 'Last', 'Vol', 'OI', 'IV', 'Delta'] as const;
95
+ const PUT_HEADERS = ['Delta', 'IV', 'OI', 'Vol', 'Last', 'Ask', 'Bid'] as const;
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // OptionChain
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export interface OptionChainProps extends React.HTMLAttributes<HTMLDivElement> {
102
+ readonly symbol: string;
103
+ readonly expiration: string;
104
+ readonly strikes: Strike[];
105
+ readonly spotPrice: number;
106
+ readonly onSelectContract?: (contract: OptionContract) => void;
107
+ }
108
+
109
+ export const OptionChain = React.forwardRef<HTMLDivElement, OptionChainProps>(
110
+ function OptionChain(props, ref) {
111
+ const {
112
+ symbol: _symbol,
113
+ expiration: _expiration,
114
+ strikes,
115
+ spotPrice,
116
+ onSelectContract,
117
+ className,
118
+ ...rest
119
+ } = props;
120
+
121
+ const handleClick = React.useCallback(
122
+ (quote: OptionQuote | null, type: 'call' | 'put', strike: number) => {
123
+ if (!quote || !onSelectContract) return;
124
+ onSelectContract({
125
+ symbol: quote.symbol,
126
+ type,
127
+ strike,
128
+ bid: quote.bid,
129
+ ask: quote.ask,
130
+ });
131
+ },
132
+ [onSelectContract],
133
+ );
134
+
135
+ return (
136
+ <div
137
+ ref={ref}
138
+ className={cn('w-full overflow-x-auto', className)}
139
+ {...rest}
140
+ >
141
+ <table className="w-full border-collapse text-xs">
142
+ <thead>
143
+ <tr className="border-b border-zinc-800">
144
+ {/* 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>
151
+ {/* 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>
155
+ {/* 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
+ )}
180
+ >
181
+ {h}
182
+ </th>
183
+ ))}
184
+ </tr>
185
+ </thead>
186
+ <tbody>
187
+ {strikes.map((row) => {
188
+ const isATM = Math.abs(row.strike - spotPrice) <=
189
+ (strikes.length > 1
190
+ ? Math.abs((strikes[1]?.strike ?? 0) - (strikes[0]?.strike ?? 0)) / 2
191
+ : 0.5);
192
+ const callITM = row.strike < spotPrice;
193
+ const putITM = row.strike > spotPrice;
194
+ const c = row.call;
195
+ const p = row.put;
196
+
197
+ 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
+ )}
207
+ >
208
+ {/* Call side */}
209
+ <QuoteCell
210
+ value={fmt(c?.bid)}
211
+ highlight={!!c}
212
+ onClick={() => handleClick(c, 'call', row.strike)}
213
+ />
214
+ <QuoteCell
215
+ value={fmt(c?.ask)}
216
+ highlight={!!c}
217
+ onClick={() => handleClick(c, 'call', row.strike)}
218
+ />
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)} />
224
+
225
+ {/* Strike */}
226
+ <td
227
+ className={cn(
228
+ 'px-2 py-1 text-center font-mono tabular-nums text-xs font-semibold',
229
+ isATM ? 'text-amber-400 bg-zinc-800/80' : 'text-zinc-300',
230
+ )}
231
+ >
232
+ {fmt(row.strike)}
233
+ </td>
234
+
235
+ {/* 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} />
241
+ <QuoteCell
242
+ value={fmt(p?.ask)}
243
+ align="left"
244
+ highlight={!!p}
245
+ onClick={() => handleClick(p, 'put', row.strike)}
246
+ />
247
+ <QuoteCell
248
+ value={fmt(p?.bid)}
249
+ align="left"
250
+ highlight={!!p}
251
+ onClick={() => handleClick(p, 'put', row.strike)}
252
+ />
253
+ </tr>
254
+ );
255
+ })}
256
+ </tbody>
257
+ </table>
258
+ </div>
259
+ );
260
+ },
261
+ );
@@ -0,0 +1,200 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ import { cn } from './utils';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface OptionPosition {
12
+ symbol: string;
13
+ underlying: string;
14
+ type: 'call' | 'put';
15
+ strike: number;
16
+ expiration: string;
17
+ quantity: number;
18
+ avgCost: number;
19
+ currentValue: number;
20
+ pnl: number;
21
+ pnlPercent: number;
22
+ greeks: {
23
+ delta: number;
24
+ gamma: number;
25
+ theta: number;
26
+ vega: number;
27
+ };
28
+ }
29
+
30
+ export interface OptionPositionCardProps extends React.HTMLAttributes<HTMLDivElement> {
31
+ readonly position: OptionPosition;
32
+ readonly onClose?: (position: OptionPosition) => void;
33
+ readonly onExercise?: (position: OptionPosition) => void;
34
+ readonly onRoll?: (position: OptionPosition) => void;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function formatDate(date: string): string {
42
+ const d = new Date(date);
43
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // OptionPositionCard
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export const OptionPositionCard = React.forwardRef<HTMLDivElement, OptionPositionCardProps>(
51
+ function OptionPositionCard(props, ref) {
52
+ const {
53
+ position,
54
+ onClose,
55
+ onExercise,
56
+ onRoll,
57
+ className,
58
+ ...rest
59
+ } = props;
60
+
61
+ const isLong = position.quantity > 0;
62
+ const isCall = position.type === 'call';
63
+ const isProfitable = position.pnl >= 0;
64
+
65
+ 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}
73
+ >
74
+ {/* 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
+ isCall ? 'bg-blue-900/40 text-blue-400' : 'bg-orange-900/40 text-orange-400',
81
+ )}>
82
+ {position.underlying}
83
+ <span className="font-mono tabular-nums">{position.strike}</span>
84
+ <span className="uppercase">{position.type === 'call' ? 'C' : 'P'}</span>
85
+ <span className="text-zinc-500 font-normal">{formatDate(position.expiration)}</span>
86
+ </span>
87
+ </div>
88
+
89
+ {/* Quantity */}
90
+ <span className={cn(
91
+ 'text-xs font-semibold font-mono tabular-nums',
92
+ isLong ? 'text-emerald-400' : 'text-red-400',
93
+ )}>
94
+ {isLong ? '+' : ''}{position.quantity}
95
+ </span>
96
+ </div>
97
+
98
+ {/* Values row */}
99
+ <div className="grid grid-cols-3 gap-3">
100
+ <div className="flex flex-col">
101
+ <span className="text-[10px] uppercase tracking-wider text-zinc-500">Avg Cost</span>
102
+ <span className="font-mono tabular-nums text-xs text-zinc-300">
103
+ ${position.avgCost.toFixed(2)}
104
+ </span>
105
+ </div>
106
+ <div className="flex flex-col">
107
+ <span className="text-[10px] uppercase tracking-wider text-zinc-500">Value</span>
108
+ <span className="font-mono tabular-nums text-xs text-zinc-300">
109
+ ${position.currentValue.toFixed(2)}
110
+ </span>
111
+ </div>
112
+ <div className="flex flex-col items-end">
113
+ <span className="text-[10px] uppercase tracking-wider text-zinc-500">P/L</span>
114
+ <div className="flex items-center gap-1">
115
+ <span className={cn(
116
+ 'font-mono tabular-nums text-xs font-semibold',
117
+ isProfitable ? 'text-emerald-400' : 'text-red-400',
118
+ )}>
119
+ {isProfitable ? '+' : ''}{position.pnl.toFixed(2)}
120
+ </span>
121
+ <span className={cn(
122
+ 'font-mono tabular-nums text-[10px]',
123
+ isProfitable ? 'text-emerald-500/70' : 'text-red-500/70',
124
+ )}>
125
+ ({isProfitable ? '+' : ''}{position.pnlPercent.toFixed(1)}%)
126
+ </span>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ {/* Greeks row */}
132
+ <div className="flex items-center gap-3 border-t border-zinc-800/60 pt-2">
133
+ <GreekPill label={'\u0394'} value={position.greeks.delta} color="text-blue-400" />
134
+ <GreekPill label={'\u0393'} value={position.greeks.gamma} color="text-purple-400" />
135
+ <GreekPill label={'\u0398'} value={position.greeks.theta} color="text-red-400" />
136
+ <GreekPill label={'\u03BD'} value={position.greeks.vega} color="text-emerald-400" />
137
+ </div>
138
+
139
+ {/* Action buttons */}
140
+ {(onClose || onExercise || onRoll) && (
141
+ <div className="flex items-center gap-2 border-t border-zinc-800/60 pt-2">
142
+ {onClose && (
143
+ <button
144
+ type="button"
145
+ onClick={() => onClose(position)}
146
+ 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"
147
+ >
148
+ Close
149
+ </button>
150
+ )}
151
+ {onExercise && (
152
+ <button
153
+ type="button"
154
+ onClick={() => onExercise(position)}
155
+ 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"
156
+ >
157
+ Exercise
158
+ </button>
159
+ )}
160
+ {onRoll && (
161
+ <button
162
+ type="button"
163
+ onClick={() => onRoll(position)}
164
+ 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"
165
+ >
166
+ Roll
167
+ </button>
168
+ )}
169
+ </div>
170
+ )}
171
+ </div>
172
+ );
173
+ },
174
+ );
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // GreekPill (internal)
178
+ // ---------------------------------------------------------------------------
179
+
180
+ interface GreekPillProps {
181
+ readonly label: string;
182
+ readonly value: number;
183
+ readonly color: string;
184
+ }
185
+
186
+ function GreekPill({ label, value, color }: GreekPillProps) {
187
+ return (
188
+ <div className="flex items-center gap-1">
189
+ <span className={cn('text-[10px] font-semibold', color)}>
190
+ {label}
191
+ </span>
192
+ <span className={cn(
193
+ 'font-mono tabular-nums text-[11px]',
194
+ value < 0 ? 'text-red-400' : 'text-zinc-400',
195
+ )}>
196
+ {value.toFixed(4)}
197
+ </span>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,187 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from './utils';
4
+
5
+ export interface PinInputProps {
6
+ readonly rootRef?: React.Ref<HTMLDivElement>;
7
+ readonly count?: number;
8
+ readonly inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
9
+ readonly attached?: boolean;
10
+ readonly placeholder?: string;
11
+ readonly value?: Array<string>;
12
+ readonly onValueChange?: (details: { value: Array<string> }) => void;
13
+ readonly onValueComplete?: (details: { value: Array<string> }) => void;
14
+ readonly disabled?: boolean;
15
+ readonly invalid?: boolean;
16
+ readonly otp?: boolean;
17
+ readonly name?: string;
18
+ readonly bgColor?: string;
19
+ readonly className?: string;
20
+ }
21
+
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)]';
37
+
38
+ export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
39
+ function PinInput(props, ref) {
40
+ const {
41
+ count = 6,
42
+ inputProps,
43
+ rootRef,
44
+ attached,
45
+ placeholder = ' ',
46
+ value: controlledValue,
47
+ onValueChange,
48
+ onValueComplete,
49
+ disabled,
50
+ invalid,
51
+ otp,
52
+ name,
53
+ bgColor,
54
+ className,
55
+ } = props;
56
+
57
+ const inputRefs = React.useRef<Array<HTMLInputElement | null>>([]);
58
+
59
+ const values = React.useMemo(
60
+ () => controlledValue ?? Array.from<string>({ length: count }).fill(''),
61
+ [ controlledValue, count ],
62
+ );
63
+
64
+ const updateValue = React.useCallback((index: number, char: string): void => {
65
+ const next = [ ...values ];
66
+ next[index] = char;
67
+ onValueChange?.({ value: next });
68
+
69
+ if (next.every((v) => v.length > 0)) {
70
+ onValueComplete?.({ value: next });
71
+ }
72
+ }, [ values, onValueChange, onValueComplete ]);
73
+
74
+ const focusInput = React.useCallback((index: number): void => {
75
+ const clamped = Math.max(0, Math.min(index, count - 1));
76
+ inputRefs.current[clamped]?.focus();
77
+ }, [ count ]);
78
+
79
+ const handleInput = React.useCallback((index: number, e: React.FormEvent<HTMLInputElement>): void => {
80
+ const target = e.currentTarget;
81
+ const char = target.value.slice(-1);
82
+ updateValue(index, char);
83
+
84
+ if (char && index < count - 1) {
85
+ focusInput(index + 1);
86
+ }
87
+ }, [ count, updateValue, focusInput ]);
88
+
89
+ const handleKeyDown = React.useCallback((index: number, e: React.KeyboardEvent<HTMLInputElement>): void => {
90
+ if (e.key === 'Backspace') {
91
+ if (values[index]) {
92
+ updateValue(index, '');
93
+ } else if (index > 0) {
94
+ updateValue(index - 1, '');
95
+ focusInput(index - 1);
96
+ }
97
+ e.preventDefault();
98
+ } else if (e.key === 'ArrowLeft' && index > 0) {
99
+ focusInput(index - 1);
100
+ e.preventDefault();
101
+ } else if (e.key === 'ArrowRight' && index < count - 1) {
102
+ focusInput(index + 1);
103
+ e.preventDefault();
104
+ }
105
+ }, [ count, values, updateValue, focusInput ]);
106
+
107
+ const handlePaste = React.useCallback((e: React.ClipboardEvent<HTMLInputElement>): void => {
108
+ e.preventDefault();
109
+ const pasted = e.clipboardData.getData('text/plain').trim();
110
+ if (!pasted) {
111
+ return;
112
+ }
113
+ const chars = pasted.slice(0, count).split('');
114
+ const next = [ ...values ];
115
+ chars.forEach((char, i) => {
116
+ next[i] = char;
117
+ });
118
+ onValueChange?.({ value: next });
119
+
120
+ if (next.every((v) => v.length > 0)) {
121
+ onValueComplete?.({ value: next });
122
+ }
123
+
124
+ focusInput(Math.min(chars.length, count - 1));
125
+ }, [ count, values, onValueChange, onValueComplete, focusInput ]);
126
+
127
+ const handleFocus = React.useCallback((e: React.FocusEvent<HTMLInputElement>): void => {
128
+ e.currentTarget.select();
129
+ }, []);
130
+
131
+ const handleNoop = React.useCallback((): void => { /* noop */ }, []);
132
+
133
+ const setInputRef = React.useCallback((index: number) => (el: HTMLInputElement | null): void => {
134
+ inputRefs.current[index] = el;
135
+ }, []);
136
+
137
+ const onInputAtIndex = React.useCallback((index: number) => (e: React.FormEvent<HTMLInputElement>): void => {
138
+ handleInput(index, e);
139
+ }, [ handleInput ]);
140
+
141
+ const onKeyDownAtIndex = React.useCallback((index: number) => (e: React.KeyboardEvent<HTMLInputElement>): void => {
142
+ handleKeyDown(index, e);
143
+ }, [ handleKeyDown ]);
144
+
145
+ const bgStyle = bgColor ? { backgroundColor: `var(--color-${ bgColor.replace(/\./g, '-') })` } : undefined;
146
+
147
+ return (
148
+ <div ref={ rootRef } className={ cn('inline-flex items-center', attached ? 'gap-0' : 'gap-2', className) }>
149
+ { /* Hidden input for form submission */ }
150
+ <input
151
+ ref={ ref }
152
+ type="hidden"
153
+ name={ name }
154
+ value={ values.join('') }
155
+ { ...inputProps }
156
+ />
157
+ { Array.from({ length: count }).map((_, index) => (
158
+ <input
159
+ key={ index }
160
+ ref={ setInputRef(index) }
161
+ type="text"
162
+ inputMode="numeric"
163
+ autoComplete={ otp ? 'one-time-code' : 'off' }
164
+ pattern="[0-9]*"
165
+ maxLength={ 1 }
166
+ placeholder={ placeholder }
167
+ disabled={ disabled }
168
+ aria-invalid={ invalid || undefined }
169
+ 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 }
177
+ onInput={ onInputAtIndex(index) }
178
+ onKeyDown={ onKeyDownAtIndex(index) }
179
+ onPaste={ handlePaste }
180
+ onFocus={ handleFocus }
181
+ onChange={ handleNoop }
182
+ />
183
+ )) }
184
+ </div>
185
+ );
186
+ },
187
+ );