@luxfi/ui 6.1.3 → 7.1.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.
- package/dist/accordion.cjs +213 -0
- package/dist/accordion.d.cts +74 -0
- package/dist/accordion.d.ts +74 -0
- package/dist/accordion.js +186 -0
- package/dist/alert.cjs +430 -0
- package/dist/alert.d.cts +30 -0
- package/dist/alert.d.ts +30 -0
- package/dist/alert.js +408 -0
- package/dist/avatar.cjs +149 -0
- package/dist/avatar.d.cts +25 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +125 -0
- package/dist/badge.cjs +452 -0
- package/dist/badge.d.cts +19 -0
- package/dist/badge.d.ts +19 -0
- package/dist/badge.js +431 -0
- package/dist/bank.cjs +272 -0
- package/dist/bank.d.cts +76 -0
- package/dist/bank.d.ts +76 -0
- package/dist/bank.js +258 -0
- package/dist/button.cjs +578 -0
- package/dist/button.d.cts +55 -0
- package/dist/button.d.ts +55 -0
- package/dist/button.js +553 -0
- package/dist/checkbox.cjs +265 -0
- package/dist/checkbox.d.cts +31 -0
- package/dist/checkbox.d.ts +31 -0
- package/dist/checkbox.js +241 -0
- package/dist/close-button.cjs +92 -0
- package/dist/close-button.d.cts +9 -0
- package/dist/close-button.d.ts +9 -0
- package/dist/close-button.js +70 -0
- package/dist/collapsible.cjs +555 -0
- package/dist/collapsible.d.cts +23 -0
- package/dist/collapsible.d.ts +23 -0
- package/dist/collapsible.js +532 -0
- package/dist/color-mode.cjs +96 -0
- package/dist/color-mode.d.cts +25 -0
- package/dist/color-mode.d.ts +25 -0
- package/dist/color-mode.js +72 -0
- package/dist/dialog.cjs +295 -0
- package/dist/dialog.d.cts +65 -0
- package/dist/dialog.d.ts +65 -0
- package/dist/dialog.js +263 -0
- package/dist/drawer.cjs +231 -0
- package/dist/drawer.d.cts +36 -0
- package/dist/drawer.d.ts +36 -0
- package/dist/drawer.js +199 -0
- package/dist/empty-state.cjs +93 -0
- package/dist/empty-state.d.cts +13 -0
- package/dist/empty-state.d.ts +13 -0
- package/dist/empty-state.js +71 -0
- package/dist/expiration-selector.cjs +138 -0
- package/dist/expiration-selector.d.cts +16 -0
- package/dist/expiration-selector.d.ts +16 -0
- package/dist/expiration-selector.js +116 -0
- package/dist/field.cjs +183 -0
- package/dist/field.d.cts +24 -0
- package/dist/field.d.ts +24 -0
- package/dist/field.js +160 -0
- package/dist/greeks-display.cjs +114 -0
- package/dist/greeks-display.d.cts +14 -0
- package/dist/greeks-display.d.ts +14 -0
- package/dist/greeks-display.js +92 -0
- package/dist/heading.cjs +41 -0
- package/dist/heading.d.cts +10 -0
- package/dist/heading.d.ts +10 -0
- package/dist/heading.js +35 -0
- package/dist/icon-button.cjs +367 -0
- package/dist/icon-button.d.cts +33 -0
- package/dist/icon-button.d.ts +33 -0
- package/dist/icon-button.js +345 -0
- package/dist/image.cjs +425 -0
- package/dist/image.d.cts +57 -0
- package/dist/image.d.ts +57 -0
- package/dist/image.js +404 -0
- package/dist/index.cjs +7220 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +7043 -0
- package/dist/input-group.cjs +155 -0
- package/dist/input-group.d.cts +17 -0
- package/dist/input-group.d.ts +17 -0
- package/dist/input-group.js +133 -0
- package/dist/input.cjs +65 -0
- package/dist/input.d.cts +11 -0
- package/dist/input.d.ts +11 -0
- package/dist/input.js +59 -0
- package/dist/link.cjs +483 -0
- package/dist/link.d.cts +37 -0
- package/dist/link.d.ts +37 -0
- package/dist/link.js +459 -0
- package/dist/menu.cjs +305 -0
- package/dist/menu.d.cts +84 -0
- package/dist/menu.d.ts +84 -0
- package/dist/menu.js +269 -0
- package/dist/option-chain.cjs +221 -0
- package/dist/option-chain.d.cts +37 -0
- package/dist/option-chain.d.ts +37 -0
- package/dist/option-chain.js +199 -0
- package/dist/option-position.cjs +164 -0
- package/dist/option-position.d.cts +29 -0
- package/dist/option-position.d.ts +29 -0
- package/dist/option-position.js +142 -0
- package/dist/pin-input.cjs +182 -0
- package/dist/pin-input.d.cts +25 -0
- package/dist/pin-input.d.ts +25 -0
- package/dist/pin-input.js +160 -0
- package/dist/pnl-diagram.cjs +453 -0
- package/dist/pnl-diagram.d.cts +11 -0
- package/dist/pnl-diagram.d.ts +11 -0
- package/dist/pnl-diagram.js +431 -0
- package/dist/popover.cjs +342 -0
- package/dist/popover.d.cts +73 -0
- package/dist/popover.d.ts +73 -0
- package/dist/popover.js +310 -0
- package/dist/progress-circle.cjs +152 -0
- package/dist/progress-circle.d.cts +47 -0
- package/dist/progress-circle.d.ts +47 -0
- package/dist/progress-circle.js +128 -0
- package/dist/progress.cjs +117 -0
- package/dist/progress.d.cts +28 -0
- package/dist/progress.d.ts +28 -0
- package/dist/progress.js +94 -0
- package/dist/provider.cjs +62 -0
- package/dist/provider.d.cts +7 -0
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +40 -0
- package/dist/radio.cjs +177 -0
- package/dist/radio.d.cts +53 -0
- package/dist/radio.d.ts +53 -0
- package/dist/radio.js +153 -0
- package/dist/rating.cjs +80 -0
- package/dist/rating.d.cts +14 -0
- package/dist/rating.d.ts +14 -0
- package/dist/rating.js +58 -0
- package/dist/select.cjs +644 -0
- package/dist/select.d.cts +130 -0
- package/dist/select.d.ts +130 -0
- package/dist/select.js +610 -0
- package/dist/separator.cjs +57 -0
- package/dist/separator.d.cts +13 -0
- package/dist/separator.d.ts +13 -0
- package/dist/separator.js +51 -0
- package/dist/skeleton.cjs +223 -0
- package/dist/skeleton.d.cts +60 -0
- package/dist/skeleton.d.ts +60 -0
- package/dist/skeleton.js +199 -0
- package/dist/slider.cjs +138 -0
- package/dist/slider.d.cts +24 -0
- package/dist/slider.d.ts +24 -0
- package/dist/slider.js +115 -0
- package/dist/strategy-builder.cjs +373 -0
- package/dist/strategy-builder.d.cts +26 -0
- package/dist/strategy-builder.d.ts +26 -0
- package/dist/strategy-builder.js +351 -0
- package/dist/switch.cjs +163 -0
- package/dist/switch.d.cts +27 -0
- package/dist/switch.d.ts +27 -0
- package/dist/switch.js +140 -0
- package/dist/table.cjs +897 -0
- package/dist/table.d.cts +107 -0
- package/dist/table.d.ts +107 -0
- package/dist/table.js +866 -0
- package/dist/tabs.cjs +240 -0
- package/dist/tabs.d.cts +78 -0
- package/dist/tabs.d.ts +78 -0
- package/dist/tabs.js +213 -0
- package/dist/tag.cjs +529 -0
- package/dist/tag.d.cts +21 -0
- package/dist/tag.d.ts +21 -0
- package/dist/tag.js +507 -0
- package/dist/textarea.cjs +65 -0
- package/dist/textarea.d.cts +11 -0
- package/dist/textarea.d.ts +11 -0
- package/dist/textarea.js +59 -0
- package/dist/toaster.cjs +99 -0
- package/dist/toaster.d.cts +29 -0
- package/dist/toaster.d.ts +29 -0
- package/dist/toaster.js +96 -0
- package/dist/tooltip.cjs +172 -0
- package/dist/tooltip.d.cts +38 -0
- package/dist/tooltip.d.ts +38 -0
- package/dist/tooltip.js +150 -0
- package/dist/utils.cjs +11 -0
- package/dist/utils.d.cts +5 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +9 -0
- package/package.json +284 -89
- package/src/accordion.tsx +285 -0
- package/src/alert.tsx +221 -0
- package/src/avatar.tsx +174 -0
- package/src/badge.tsx +147 -0
- package/src/bank.tsx +325 -0
- package/src/button.tsx +477 -0
- package/src/checkbox.tsx +307 -0
- package/src/close-button.tsx +77 -0
- package/src/collapsible.tsx +126 -0
- package/src/color-mode.tsx +125 -0
- package/src/dialog.tsx +346 -0
- package/src/drawer.tsx +186 -0
- package/src/empty-state.tsx +97 -0
- package/src/expiration-selector.tsx +163 -0
- package/src/field.tsx +202 -0
- package/src/greeks-display.tsx +138 -0
- package/src/heading.tsx +44 -0
- package/src/icon-button.tsx +217 -0
- package/src/image.tsx +280 -0
- package/src/index.ts +267 -138
- package/src/input-group.tsx +159 -0
- package/src/input.tsx +60 -0
- package/src/link.tsx +326 -0
- package/src/menu.tsx +471 -0
- package/src/option-chain.tsx +261 -0
- package/src/option-position.tsx +200 -0
- package/src/pin-input.tsx +187 -0
- package/src/pnl-diagram.tsx +458 -0
- package/src/popover.tsx +386 -0
- package/src/progress-circle.tsx +180 -0
- package/src/progress.tsx +109 -0
- package/src/provider.tsx +12 -0
- package/src/radio.tsx +175 -0
- package/src/rating.tsx +79 -0
- package/src/select.tsx +696 -0
- package/src/separator.tsx +59 -0
- package/src/skeleton.tsx +254 -0
- package/src/slider.tsx +152 -0
- package/src/strategy-builder.tsx +466 -0
- package/src/switch.tsx +158 -0
- package/src/table.tsx +621 -0
- package/src/tabs.tsx +354 -0
- package/src/tag.tsx +159 -0
- package/src/textarea.tsx +60 -0
- package/src/toaster.tsx +117 -0
- package/src/tokens.css +438 -0
- package/src/tooltip.tsx +191 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils.ts +6 -0
- package/tokens.css +438 -0
- package/LICENSE +0 -122
- 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
|
+
);
|