@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.
- 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 +553 -0
- package/dist/alert.d.cts +30 -0
- package/dist/alert.d.ts +30 -0
- package/dist/alert.js +531 -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 +611 -0
- package/dist/badge.d.cts +23 -0
- package/dist/badge.d.ts +23 -0
- package/dist/badge.js +589 -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 +689 -0
- package/dist/button.d.cts +52 -0
- package/dist/button.d.ts +52 -0
- package/dist/button.js +664 -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 +73 -0
- package/dist/close-button.d.cts +9 -0
- package/dist/close-button.d.ts +9 -0
- package/dist/close-button.js +51 -0
- package/dist/collapsible.cjs +702 -0
- package/dist/collapsible.d.cts +23 -0
- package/dist/collapsible.d.ts +23 -0
- package/dist/collapsible.js +679 -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 +279 -0
- package/dist/dialog.d.cts +66 -0
- package/dist/dialog.d.ts +66 -0
- package/dist/dialog.js +246 -0
- package/dist/drawer.cjs +207 -0
- package/dist/drawer.d.cts +36 -0
- package/dist/drawer.d.ts +36 -0
- package/dist/drawer.js +175 -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 +46 -0
- package/dist/heading.d.cts +10 -0
- package/dist/heading.d.ts +10 -0
- package/dist/heading.js +40 -0
- package/dist/icon-button.cjs +491 -0
- package/dist/icon-button.d.cts +33 -0
- package/dist/icon-button.d.ts +33 -0
- package/dist/icon-button.js +470 -0
- package/dist/image.cjs +572 -0
- package/dist/image.d.cts +57 -0
- package/dist/image.d.ts +57 -0
- package/dist/image.js +551 -0
- package/dist/index.cjs +7316 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +7137 -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 +630 -0
- package/dist/link.d.cts +37 -0
- package/dist/link.d.ts +37 -0
- package/dist/link.js +606 -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 +327 -0
- package/dist/popover.d.cts +73 -0
- package/dist/popover.d.ts +73 -0
- package/dist/popover.js +294 -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 +791 -0
- package/dist/select.d.cts +130 -0
- package/dist/select.d.ts +130 -0
- package/dist/select.js +757 -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 +370 -0
- package/dist/skeleton.d.cts +60 -0
- package/dist/skeleton.d.ts +60 -0
- package/dist/skeleton.js +346 -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 +1044 -0
- package/dist/table.d.cts +107 -0
- package/dist/table.d.ts +107 -0
- package/dist/table.js +1013 -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 +651 -0
- package/dist/tag.d.cts +21 -0
- package/dist/tag.d.ts +21 -0
- package/dist/tag.js +628 -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 +171 -0
- package/dist/tooltip.d.cts +39 -0
- package/dist/tooltip.d.ts +39 -0
- package/dist/tooltip.js +148 -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 -85
- package/src/accordion.tsx +285 -0
- package/src/alert.tsx +221 -0
- package/src/avatar.tsx +174 -0
- package/src/badge.tsx +158 -0
- package/src/bank.tsx +325 -0
- package/src/button.tsx +411 -0
- package/src/checkbox.tsx +307 -0
- package/src/close-button.tsx +51 -0
- package/src/collapsible.tsx +126 -0
- package/src/color-mode.tsx +125 -0
- package/src/dialog.tsx +356 -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 +55 -0
- package/src/icon-button.tsx +192 -0
- package/src/image.tsx +280 -0
- package/src/index.ts +268 -0
- 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 +400 -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 +302 -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 +184 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils.ts +6 -0
- package/tokens.css +438 -0
- package/README.md +0 -109
|
@@ -0,0 +1,466 @@
|
|
|
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 StrategyLeg {
|
|
12
|
+
action: 'buy' | 'sell';
|
|
13
|
+
type: 'call' | 'put';
|
|
14
|
+
strike: number;
|
|
15
|
+
expiration: string;
|
|
16
|
+
quantity: number;
|
|
17
|
+
premium?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type StrategyType =
|
|
21
|
+
| 'bull_call_spread'
|
|
22
|
+
| 'bear_put_spread'
|
|
23
|
+
| 'iron_condor'
|
|
24
|
+
| 'butterfly'
|
|
25
|
+
| 'straddle'
|
|
26
|
+
| 'strangle'
|
|
27
|
+
| 'collar'
|
|
28
|
+
| 'custom';
|
|
29
|
+
|
|
30
|
+
export interface StrategySubmission {
|
|
31
|
+
type: string;
|
|
32
|
+
legs: StrategyLeg[];
|
|
33
|
+
netPremium: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StrategyBuilderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSubmit'> {
|
|
37
|
+
readonly underlying: string;
|
|
38
|
+
readonly expirations: string[];
|
|
39
|
+
readonly strikes: number[];
|
|
40
|
+
readonly spotPrice: number;
|
|
41
|
+
readonly onSubmit?: (strategy: StrategySubmission) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Strategy presets
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
interface StrategyPreset {
|
|
49
|
+
label: string;
|
|
50
|
+
value: StrategyType;
|
|
51
|
+
description: string;
|
|
52
|
+
buildLegs: (strikes: number[], spotPrice: number, expiration: string) => StrategyLeg[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findATM(strikes: number[], spot: number): number {
|
|
56
|
+
if (strikes.length === 0) return spot;
|
|
57
|
+
return strikes.reduce((prev, curr) =>
|
|
58
|
+
Math.abs(curr - spot) < Math.abs(prev - spot) ? curr : prev,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findOTM(strikes: number[], spot: number, type: 'call' | 'put', offset: number): number {
|
|
63
|
+
const sorted = [...strikes].sort((a, b) => a - b);
|
|
64
|
+
const atm = findATM(strikes, spot);
|
|
65
|
+
const atIdx = sorted.indexOf(atm);
|
|
66
|
+
if (type === 'call') {
|
|
67
|
+
return sorted[Math.min(atIdx + offset, sorted.length - 1)] ?? atm;
|
|
68
|
+
}
|
|
69
|
+
return sorted[Math.max(atIdx - offset, 0)] ?? atm;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const STRATEGY_PRESETS: StrategyPreset[] = [
|
|
73
|
+
{
|
|
74
|
+
label: 'Bull Call Spread',
|
|
75
|
+
value: 'bull_call_spread',
|
|
76
|
+
description: 'Buy lower strike call, sell higher strike call',
|
|
77
|
+
buildLegs: (strikes, spot, exp) => {
|
|
78
|
+
const lower = findATM(strikes, spot);
|
|
79
|
+
const upper = findOTM(strikes, spot, 'call', 2);
|
|
80
|
+
return [
|
|
81
|
+
{ action: 'buy', type: 'call', strike: lower, expiration: exp, quantity: 1 },
|
|
82
|
+
{ action: 'sell', type: 'call', strike: upper, expiration: exp, quantity: 1 },
|
|
83
|
+
];
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: 'Bear Put Spread',
|
|
88
|
+
value: 'bear_put_spread',
|
|
89
|
+
description: 'Buy higher strike put, sell lower strike put',
|
|
90
|
+
buildLegs: (strikes, spot, exp) => {
|
|
91
|
+
const upper = findATM(strikes, spot);
|
|
92
|
+
const lower = findOTM(strikes, spot, 'put', 2);
|
|
93
|
+
return [
|
|
94
|
+
{ action: 'buy', type: 'put', strike: upper, expiration: exp, quantity: 1 },
|
|
95
|
+
{ action: 'sell', type: 'put', strike: lower, expiration: exp, quantity: 1 },
|
|
96
|
+
];
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: 'Iron Condor',
|
|
101
|
+
value: 'iron_condor',
|
|
102
|
+
description: 'Sell OTM put + call, buy further OTM put + call',
|
|
103
|
+
buildLegs: (strikes, spot, exp) => {
|
|
104
|
+
const sellPut = findOTM(strikes, spot, 'put', 1);
|
|
105
|
+
const buyPut = findOTM(strikes, spot, 'put', 3);
|
|
106
|
+
const sellCall = findOTM(strikes, spot, 'call', 1);
|
|
107
|
+
const buyCall = findOTM(strikes, spot, 'call', 3);
|
|
108
|
+
return [
|
|
109
|
+
{ action: 'buy', type: 'put', strike: buyPut, expiration: exp, quantity: 1 },
|
|
110
|
+
{ action: 'sell', type: 'put', strike: sellPut, expiration: exp, quantity: 1 },
|
|
111
|
+
{ action: 'sell', type: 'call', strike: sellCall, expiration: exp, quantity: 1 },
|
|
112
|
+
{ action: 'buy', type: 'call', strike: buyCall, expiration: exp, quantity: 1 },
|
|
113
|
+
];
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'Butterfly',
|
|
118
|
+
value: 'butterfly',
|
|
119
|
+
description: 'Buy 1 low, sell 2 middle, buy 1 high',
|
|
120
|
+
buildLegs: (strikes, spot, exp) => {
|
|
121
|
+
const mid = findATM(strikes, spot);
|
|
122
|
+
const low = findOTM(strikes, spot, 'put', 2);
|
|
123
|
+
const high = findOTM(strikes, spot, 'call', 2);
|
|
124
|
+
return [
|
|
125
|
+
{ action: 'buy', type: 'call', strike: low, expiration: exp, quantity: 1 },
|
|
126
|
+
{ action: 'sell', type: 'call', strike: mid, expiration: exp, quantity: 2 },
|
|
127
|
+
{ action: 'buy', type: 'call', strike: high, expiration: exp, quantity: 1 },
|
|
128
|
+
];
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: 'Straddle',
|
|
133
|
+
value: 'straddle',
|
|
134
|
+
description: 'Buy call + put at same strike',
|
|
135
|
+
buildLegs: (strikes, spot, exp) => {
|
|
136
|
+
const atm = findATM(strikes, spot);
|
|
137
|
+
return [
|
|
138
|
+
{ action: 'buy', type: 'call', strike: atm, expiration: exp, quantity: 1 },
|
|
139
|
+
{ action: 'buy', type: 'put', strike: atm, expiration: exp, quantity: 1 },
|
|
140
|
+
];
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: 'Strangle',
|
|
145
|
+
value: 'strangle',
|
|
146
|
+
description: 'Buy OTM call + OTM put',
|
|
147
|
+
buildLegs: (strikes, spot, exp) => {
|
|
148
|
+
const otmCall = findOTM(strikes, spot, 'call', 2);
|
|
149
|
+
const otmPut = findOTM(strikes, spot, 'put', 2);
|
|
150
|
+
return [
|
|
151
|
+
{ action: 'buy', type: 'put', strike: otmPut, expiration: exp, quantity: 1 },
|
|
152
|
+
{ action: 'buy', type: 'call', strike: otmCall, expiration: exp, quantity: 1 },
|
|
153
|
+
];
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: 'Custom',
|
|
158
|
+
value: 'custom',
|
|
159
|
+
description: 'Build your own multi-leg strategy',
|
|
160
|
+
buildLegs: (_strikes, spot, exp) => [
|
|
161
|
+
{ action: 'buy', type: 'call', strike: spot, expiration: exp, quantity: 1 },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// LegRow
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
interface LegRowProps {
|
|
171
|
+
readonly leg: StrategyLeg;
|
|
172
|
+
readonly index: number;
|
|
173
|
+
readonly strikes: number[];
|
|
174
|
+
readonly expirations: string[];
|
|
175
|
+
readonly removable: boolean;
|
|
176
|
+
readonly onChange: (index: number, leg: StrategyLeg) => void;
|
|
177
|
+
readonly onRemove: (index: number) => void;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function LegRow({ leg, index, strikes, expirations, removable, onChange, onRemove }: LegRowProps) {
|
|
181
|
+
const update = (patch: Partial<StrategyLeg>) => {
|
|
182
|
+
onChange(index, { ...leg, ...patch });
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
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>
|
|
190
|
+
|
|
191
|
+
{/* 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' })}
|
|
201
|
+
>
|
|
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
|
+
leg.type === 'call'
|
|
211
|
+
? 'bg-blue-900/40 text-blue-400'
|
|
212
|
+
: 'bg-orange-900/40 text-orange-400',
|
|
213
|
+
)}
|
|
214
|
+
onClick={() => update({ type: leg.type === 'call' ? 'put' : 'call' })}
|
|
215
|
+
>
|
|
216
|
+
{leg.type === 'call' ? 'CALL' : 'PUT'}
|
|
217
|
+
</button>
|
|
218
|
+
|
|
219
|
+
{/* Strike */}
|
|
220
|
+
<select
|
|
221
|
+
value={leg.strike}
|
|
222
|
+
onChange={(e) => update({ strike: Number(e.target.value) })}
|
|
223
|
+
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]"
|
|
224
|
+
>
|
|
225
|
+
{strikes.map((s) => (
|
|
226
|
+
<option key={s} value={s}>
|
|
227
|
+
{s.toFixed(2)}
|
|
228
|
+
</option>
|
|
229
|
+
))}
|
|
230
|
+
</select>
|
|
231
|
+
|
|
232
|
+
{/* Expiration */}
|
|
233
|
+
<select
|
|
234
|
+
value={leg.expiration}
|
|
235
|
+
onChange={(e) => update({ expiration: e.target.value })}
|
|
236
|
+
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]"
|
|
237
|
+
>
|
|
238
|
+
{expirations.map((exp) => (
|
|
239
|
+
<option key={exp} value={exp}>
|
|
240
|
+
{exp}
|
|
241
|
+
</option>
|
|
242
|
+
))}
|
|
243
|
+
</select>
|
|
244
|
+
|
|
245
|
+
{/* Quantity */}
|
|
246
|
+
<input
|
|
247
|
+
type="number"
|
|
248
|
+
min={1}
|
|
249
|
+
max={999}
|
|
250
|
+
value={leg.quantity}
|
|
251
|
+
onChange={(e) => update({ quantity: Math.max(1, parseInt(e.target.value, 10) || 1) })}
|
|
252
|
+
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"
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
{/* Premium (read-only if provided) */}
|
|
256
|
+
{leg.premium !== undefined && (
|
|
257
|
+
<span className={cn(
|
|
258
|
+
'font-mono tabular-nums text-xs ml-auto',
|
|
259
|
+
leg.premium >= 0 ? 'text-emerald-400' : 'text-red-400',
|
|
260
|
+
)}>
|
|
261
|
+
{leg.premium >= 0 ? '+' : ''}{leg.premium.toFixed(2)}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Remove */}
|
|
266
|
+
{removable && (
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={() => onRemove(index)}
|
|
270
|
+
className="ml-auto p-0.5 text-zinc-500 hover:text-red-400 transition-colors"
|
|
271
|
+
aria-label="Remove leg"
|
|
272
|
+
>
|
|
273
|
+
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none">
|
|
274
|
+
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
275
|
+
</svg>
|
|
276
|
+
</button>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// StrategyBuilder
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
export const StrategyBuilder = React.forwardRef<HTMLDivElement, StrategyBuilderProps>(
|
|
287
|
+
function StrategyBuilder(props, ref) {
|
|
288
|
+
const {
|
|
289
|
+
underlying,
|
|
290
|
+
expirations,
|
|
291
|
+
strikes,
|
|
292
|
+
spotPrice,
|
|
293
|
+
onSubmit,
|
|
294
|
+
className,
|
|
295
|
+
...rest
|
|
296
|
+
} = props;
|
|
297
|
+
|
|
298
|
+
const defaultExp = expirations[0] ?? '';
|
|
299
|
+
const [strategyType, setStrategyType] = React.useState<StrategyType>('bull_call_spread');
|
|
300
|
+
const [legs, setLegs] = React.useState<StrategyLeg[]>(() => {
|
|
301
|
+
const preset = STRATEGY_PRESETS.find((p) => p.value === 'bull_call_spread');
|
|
302
|
+
return preset ? preset.buildLegs(strikes, spotPrice, defaultExp) : [];
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const handleStrategyChange = React.useCallback(
|
|
306
|
+
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
307
|
+
const type = e.target.value as StrategyType;
|
|
308
|
+
setStrategyType(type);
|
|
309
|
+
const preset = STRATEGY_PRESETS.find((p) => p.value === type);
|
|
310
|
+
if (preset) {
|
|
311
|
+
setLegs(preset.buildLegs(strikes, spotPrice, defaultExp));
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
[strikes, spotPrice, defaultExp],
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const handleLegChange = React.useCallback(
|
|
318
|
+
(index: number, leg: StrategyLeg) => {
|
|
319
|
+
setLegs((prev) => {
|
|
320
|
+
const next = [...prev];
|
|
321
|
+
next[index] = leg;
|
|
322
|
+
return next;
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
[],
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const handleLegRemove = React.useCallback(
|
|
329
|
+
(index: number) => {
|
|
330
|
+
setLegs((prev) => prev.filter((_, i) => i !== index));
|
|
331
|
+
},
|
|
332
|
+
[],
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const handleAddLeg = React.useCallback(() => {
|
|
336
|
+
if (legs.length >= 4) return;
|
|
337
|
+
const atm = findATM(strikes, spotPrice);
|
|
338
|
+
setLegs((prev) => [
|
|
339
|
+
...prev,
|
|
340
|
+
{ action: 'buy', type: 'call', strike: atm, expiration: defaultExp, quantity: 1 },
|
|
341
|
+
]);
|
|
342
|
+
}, [legs.length, strikes, spotPrice, defaultExp]);
|
|
343
|
+
|
|
344
|
+
const netPremium = React.useMemo(() => {
|
|
345
|
+
return legs.reduce((sum, leg) => {
|
|
346
|
+
if (leg.premium === undefined) return sum;
|
|
347
|
+
const sign = leg.action === 'buy' ? -1 : 1;
|
|
348
|
+
return sum + sign * leg.premium * leg.quantity;
|
|
349
|
+
}, 0);
|
|
350
|
+
}, [legs]);
|
|
351
|
+
|
|
352
|
+
const handleSubmit = React.useCallback(() => {
|
|
353
|
+
if (!onSubmit || legs.length === 0) return;
|
|
354
|
+
onSubmit({
|
|
355
|
+
type: strategyType,
|
|
356
|
+
legs,
|
|
357
|
+
netPremium,
|
|
358
|
+
});
|
|
359
|
+
}, [onSubmit, strategyType, legs, netPremium]);
|
|
360
|
+
|
|
361
|
+
const currentPreset = STRATEGY_PRESETS.find((p) => p.value === strategyType);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
ref={ref}
|
|
366
|
+
className={cn(
|
|
367
|
+
'flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3',
|
|
368
|
+
className,
|
|
369
|
+
)}
|
|
370
|
+
{...rest}
|
|
371
|
+
>
|
|
372
|
+
{/* Header */}
|
|
373
|
+
<div className="flex items-center justify-between gap-3">
|
|
374
|
+
<div className="flex items-center gap-2">
|
|
375
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-zinc-500">
|
|
376
|
+
Strategy
|
|
377
|
+
</span>
|
|
378
|
+
<span className="text-sm font-medium text-zinc-200">
|
|
379
|
+
{underlying}
|
|
380
|
+
</span>
|
|
381
|
+
</div>
|
|
382
|
+
<span className="text-[10px] text-zinc-500">
|
|
383
|
+
Spot: <span className="font-mono tabular-nums text-zinc-300">{spotPrice.toFixed(2)}</span>
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{/* Strategy selector */}
|
|
388
|
+
<div className="flex items-center gap-2">
|
|
389
|
+
<select
|
|
390
|
+
value={strategyType}
|
|
391
|
+
onChange={handleStrategyChange}
|
|
392
|
+
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"
|
|
393
|
+
>
|
|
394
|
+
{STRATEGY_PRESETS.map((preset) => (
|
|
395
|
+
<option key={preset.value} value={preset.value}>
|
|
396
|
+
{preset.label}
|
|
397
|
+
</option>
|
|
398
|
+
))}
|
|
399
|
+
</select>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{currentPreset && (
|
|
403
|
+
<p className="text-[11px] text-zinc-500 -mt-1">
|
|
404
|
+
{currentPreset.description}
|
|
405
|
+
</p>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{/* Legs */}
|
|
409
|
+
<div className="flex flex-col gap-1.5">
|
|
410
|
+
{legs.map((leg, i) => (
|
|
411
|
+
<LegRow
|
|
412
|
+
key={i}
|
|
413
|
+
leg={leg}
|
|
414
|
+
index={i}
|
|
415
|
+
strikes={strikes}
|
|
416
|
+
expirations={expirations}
|
|
417
|
+
removable={strategyType === 'custom' && legs.length > 1}
|
|
418
|
+
onChange={handleLegChange}
|
|
419
|
+
onRemove={handleLegRemove}
|
|
420
|
+
/>
|
|
421
|
+
))}
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{/* Add leg button (custom only) */}
|
|
425
|
+
{strategyType === 'custom' && legs.length < 4 && (
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={handleAddLeg}
|
|
429
|
+
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"
|
|
430
|
+
>
|
|
431
|
+
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none">
|
|
432
|
+
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
433
|
+
</svg>
|
|
434
|
+
Add Leg
|
|
435
|
+
</button>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{/* Net premium + submit */}
|
|
439
|
+
<div className="flex items-center justify-between pt-1 border-t border-zinc-800">
|
|
440
|
+
<div className="flex items-center gap-2">
|
|
441
|
+
<span className="text-xs text-zinc-500">Net:</span>
|
|
442
|
+
<span className={cn(
|
|
443
|
+
'font-mono tabular-nums text-sm font-semibold',
|
|
444
|
+
netPremium > 0 ? 'text-emerald-400' : netPremium < 0 ? 'text-red-400' : 'text-zinc-400',
|
|
445
|
+
)}>
|
|
446
|
+
{netPremium > 0 ? 'Credit ' : netPremium < 0 ? 'Debit ' : ''}
|
|
447
|
+
{netPremium !== 0 ? `$${Math.abs(netPremium).toFixed(2)}` : '-'}
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
onClick={handleSubmit}
|
|
453
|
+
disabled={legs.length === 0}
|
|
454
|
+
className={cn(
|
|
455
|
+
'px-4 py-1.5 rounded-md text-xs font-semibold transition-colors',
|
|
456
|
+
'bg-blue-600 text-white hover:bg-blue-500',
|
|
457
|
+
'disabled:opacity-40 disabled:cursor-not-allowed',
|
|
458
|
+
)}
|
|
459
|
+
>
|
|
460
|
+
Review Order
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
},
|
|
466
|
+
);
|
package/src/switch.tsx
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as RadixSwitch from '@radix-ui/react-switch';
|
|
2
|
+
// chakra() HOC removed — pure Radix + Tailwind
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from './utils';
|
|
6
|
+
|
|
7
|
+
const NOOP = () => { /* noop */ };
|
|
8
|
+
|
|
9
|
+
// ─── Size mappings ────────────────────────────────────────────────────
|
|
10
|
+
const SIZE_CLASSES = {
|
|
11
|
+
sm: {
|
|
12
|
+
root: 'h-4 w-7',
|
|
13
|
+
thumb: 'h-3 w-3 data-[state=checked]:translate-x-3',
|
|
14
|
+
label: 'text-xs',
|
|
15
|
+
},
|
|
16
|
+
md: {
|
|
17
|
+
root: 'h-5 w-9',
|
|
18
|
+
thumb: 'h-4 w-4 data-[state=checked]:translate-x-4',
|
|
19
|
+
label: 'text-sm',
|
|
20
|
+
},
|
|
21
|
+
lg: {
|
|
22
|
+
root: 'h-6 w-11',
|
|
23
|
+
thumb: 'h-5 w-5 data-[state=checked]:translate-x-5',
|
|
24
|
+
label: 'text-base',
|
|
25
|
+
},
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
// ─── Props ────────────────────────────────────────────────────────────
|
|
29
|
+
export interface SwitchProps extends Omit<React.ComponentPropsWithoutRef<'label'>, 'onChange' | 'dir'> {
|
|
30
|
+
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
31
|
+
labelProps?: React.ComponentPropsWithoutRef<'span'>;
|
|
32
|
+
rootRef?: React.Ref<HTMLLabelElement>;
|
|
33
|
+
trackLabel?: { on: React.ReactNode; off: React.ReactNode };
|
|
34
|
+
thumbLabel?: { on: React.ReactNode; off: React.ReactNode };
|
|
35
|
+
checked?: boolean;
|
|
36
|
+
defaultChecked?: boolean;
|
|
37
|
+
onCheckedChange?: (details: { checked: boolean }) => void;
|
|
38
|
+
onChange?: React.FormEventHandler<HTMLLabelElement>;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
size?: 'sm' | 'md' | 'lg';
|
|
41
|
+
direction?: 'ltr' | 'rtl';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
45
|
+
function Switch(props, ref) {
|
|
46
|
+
const {
|
|
47
|
+
inputProps,
|
|
48
|
+
children,
|
|
49
|
+
rootRef,
|
|
50
|
+
labelProps,
|
|
51
|
+
trackLabel,
|
|
52
|
+
thumbLabel,
|
|
53
|
+
checked: checkedProp,
|
|
54
|
+
defaultChecked,
|
|
55
|
+
onCheckedChange,
|
|
56
|
+
onChange,
|
|
57
|
+
disabled = false,
|
|
58
|
+
size = 'md',
|
|
59
|
+
direction,
|
|
60
|
+
className,
|
|
61
|
+
...rest
|
|
62
|
+
} = props;
|
|
63
|
+
|
|
64
|
+
const [ internalChecked, setInternalChecked ] = React.useState(defaultChecked ?? false);
|
|
65
|
+
|
|
66
|
+
const isControlled = checkedProp !== undefined;
|
|
67
|
+
const checkedState = isControlled ? checkedProp : internalChecked;
|
|
68
|
+
|
|
69
|
+
const handleCheckedChange = React.useCallback(
|
|
70
|
+
(nextChecked: boolean) => {
|
|
71
|
+
if (!isControlled) {
|
|
72
|
+
setInternalChecked(nextChecked);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (onCheckedChange) {
|
|
76
|
+
// Support both Chakra-style ({ checked }) and Radix-style (checked) signatures.
|
|
77
|
+
// We call with a single object arg; if the consumer ignores it, that's fine.
|
|
78
|
+
(onCheckedChange as (details: { checked: boolean }) => void)({ checked: nextChecked });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[ isControlled, onCheckedChange ],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const sizeClasses = SIZE_CLASSES[size];
|
|
85
|
+
const isRtl = direction === 'rtl';
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<label
|
|
89
|
+
ref={ rootRef }
|
|
90
|
+
className={ cn(
|
|
91
|
+
'inline-flex items-center cursor-pointer select-none gap-2',
|
|
92
|
+
isRtl && 'flex-row-reverse',
|
|
93
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
94
|
+
className,
|
|
95
|
+
) }
|
|
96
|
+
onChange={ onChange }
|
|
97
|
+
data-disabled={ disabled || undefined }
|
|
98
|
+
{ ...rest }
|
|
99
|
+
>
|
|
100
|
+
<RadixSwitch.Root
|
|
101
|
+
checked={ checkedState }
|
|
102
|
+
onCheckedChange={ handleCheckedChange }
|
|
103
|
+
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
|
+
) }
|
|
112
|
+
>
|
|
113
|
+
{ trackLabel && (
|
|
114
|
+
<span className="absolute inset-0 flex items-center justify-center text-[8px] font-bold text-white pointer-events-none">
|
|
115
|
+
{ checkedState ? trackLabel.on : trackLabel.off }
|
|
116
|
+
</span>
|
|
117
|
+
) }
|
|
118
|
+
<RadixSwitch.Thumb
|
|
119
|
+
className={ cn(
|
|
120
|
+
'block rounded-full bg-white shadow-sm',
|
|
121
|
+
'transition-transform duration-200',
|
|
122
|
+
'translate-x-0.5',
|
|
123
|
+
sizeClasses.thumb,
|
|
124
|
+
) }
|
|
125
|
+
>
|
|
126
|
+
{ thumbLabel && (
|
|
127
|
+
<span className="flex items-center justify-center h-full w-full text-[8px]">
|
|
128
|
+
{ checkedState ? thumbLabel.on : thumbLabel.off }
|
|
129
|
+
</span>
|
|
130
|
+
) }
|
|
131
|
+
</RadixSwitch.Thumb>
|
|
132
|
+
</RadixSwitch.Root>
|
|
133
|
+
{ /* Hidden native input for form compat and ref forwarding */ }
|
|
134
|
+
<input
|
|
135
|
+
ref={ ref }
|
|
136
|
+
type="checkbox"
|
|
137
|
+
className="sr-only"
|
|
138
|
+
checked={ checkedState }
|
|
139
|
+
disabled={ disabled }
|
|
140
|
+
tabIndex={ -1 }
|
|
141
|
+
aria-hidden
|
|
142
|
+
onChange={ NOOP }
|
|
143
|
+
{ ...inputProps }
|
|
144
|
+
/>
|
|
145
|
+
{ children != null && (
|
|
146
|
+
<span
|
|
147
|
+
{ ...labelProps }
|
|
148
|
+
className={ cn(sizeClasses.label, labelProps?.className) }
|
|
149
|
+
>
|
|
150
|
+
{ children }
|
|
151
|
+
</span>
|
|
152
|
+
) }
|
|
153
|
+
</label>
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
export const Switch = SwitchBase;
|