@luxfi/ui 6.1.3 → 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 (241) 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 +283 -90
  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 +267 -138
  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/LICENSE +0 -122
  241. package/README.md +0 -109
@@ -0,0 +1,163 @@
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 ExpirationOption {
12
+ date: string;
13
+ dte: number;
14
+ type: 'weekly' | 'monthly' | 'quarterly';
15
+ }
16
+
17
+ export interface ExpirationSelectorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
18
+ readonly expirations: ExpirationOption[];
19
+ readonly selected: string;
20
+ readonly onSelect: (date: string) => void;
21
+ readonly showGroups?: boolean;
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function formatExpDate(date: string): string {
29
+ const d = new Date(date);
30
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
31
+ }
32
+
33
+ const GROUP_ORDER: ExpirationOption['type'][] = ['weekly', 'monthly', 'quarterly'];
34
+
35
+ const GROUP_LABELS: Record<ExpirationOption['type'], string> = {
36
+ weekly: 'Weekly',
37
+ monthly: 'Monthly',
38
+ quarterly: 'Quarterly',
39
+ };
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // ExpirationPill
43
+ // ---------------------------------------------------------------------------
44
+
45
+ interface ExpirationPillProps {
46
+ readonly exp: ExpirationOption;
47
+ readonly isSelected: boolean;
48
+ readonly onClick: () => void;
49
+ }
50
+
51
+ function ExpirationPill({ exp, isSelected, onClick }: ExpirationPillProps) {
52
+ return (
53
+ <button
54
+ type="button"
55
+ onClick={onClick}
56
+ className={cn(
57
+ 'flex flex-col items-center gap-0.5 rounded-md px-3 py-1.5 transition-colors shrink-0',
58
+ 'border text-xs',
59
+ isSelected
60
+ ? 'border-blue-500/60 bg-blue-900/30 text-blue-300'
61
+ : 'border-zinc-800 bg-zinc-900/50 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200',
62
+ )}
63
+ >
64
+ <span className="font-medium whitespace-nowrap">
65
+ {formatExpDate(exp.date)}
66
+ </span>
67
+ <span className={cn(
68
+ 'font-mono tabular-nums text-[10px]',
69
+ isSelected ? 'text-blue-400/70' : 'text-zinc-600',
70
+ exp.dte <= 7 && !isSelected && 'text-amber-600',
71
+ )}>
72
+ {exp.dte}d
73
+ </span>
74
+ </button>
75
+ );
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // ExpirationSelector
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export const ExpirationSelector = React.forwardRef<HTMLDivElement, ExpirationSelectorProps>(
83
+ function ExpirationSelector(props, ref) {
84
+ const {
85
+ expirations,
86
+ selected,
87
+ onSelect,
88
+ showGroups = false,
89
+ className,
90
+ ...rest
91
+ } = props;
92
+
93
+ const scrollRef = React.useRef<HTMLDivElement>(null);
94
+
95
+ // Scroll selected pill into view on mount / change
96
+ React.useEffect(() => {
97
+ const container = scrollRef.current;
98
+ if (!container) return;
99
+ const selectedEl = container.querySelector('[data-selected="true"]');
100
+ if (selectedEl) {
101
+ selectedEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
102
+ }
103
+ }, [selected]);
104
+
105
+ if (showGroups) {
106
+ const grouped = GROUP_ORDER.map((type) => ({
107
+ type,
108
+ label: GROUP_LABELS[type],
109
+ items: expirations.filter((e) => e.type === type),
110
+ })).filter((g) => g.items.length > 0);
111
+
112
+ return (
113
+ <div
114
+ ref={ref}
115
+ className={cn('flex flex-col gap-2', className)}
116
+ {...rest}
117
+ >
118
+ {grouped.map((group) => (
119
+ <div key={group.type} className="flex flex-col gap-1">
120
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-600 px-1">
121
+ {group.label}
122
+ </span>
123
+ <div className="flex gap-1.5 overflow-x-auto no-scrollbar">
124
+ {group.items.map((exp) => (
125
+ <div key={exp.date} data-selected={exp.date === selected || undefined}>
126
+ <ExpirationPill
127
+ exp={exp}
128
+ isSelected={exp.date === selected}
129
+ onClick={() => onSelect(exp.date)}
130
+ />
131
+ </div>
132
+ ))}
133
+ </div>
134
+ </div>
135
+ ))}
136
+ </div>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <div
142
+ ref={ref}
143
+ className={cn('flex items-center gap-1', className)}
144
+ {...rest}
145
+ >
146
+ <div
147
+ ref={scrollRef}
148
+ className="flex gap-1.5 overflow-x-auto no-scrollbar"
149
+ >
150
+ {expirations.map((exp) => (
151
+ <div key={exp.date} data-selected={exp.date === selected || undefined}>
152
+ <ExpirationPill
153
+ exp={exp}
154
+ isSelected={exp.date === selected}
155
+ onClick={() => onSelect(exp.date)}
156
+ />
157
+ </div>
158
+ ))}
159
+ </div>
160
+ </div>
161
+ );
162
+ },
163
+ );
package/src/field.tsx ADDED
@@ -0,0 +1,202 @@
1
+ import * as LabelPrimitive from '@radix-ui/react-label';
2
+ import * as React from 'react';
3
+
4
+ import { cn } from './utils';
5
+
6
+ import type { InputProps } from './input';
7
+ import type { InputGroupProps } from './input-group';
8
+
9
+ // Inline constants
10
+ const space = String.fromCharCode(32);
11
+
12
+ function getComponentDisplayName(type: string | React.JSXElementConstructor<unknown>): string | undefined {
13
+ if (typeof type === 'string') return undefined;
14
+ if ('displayName' in type) return type.displayName as string;
15
+ return undefined;
16
+ }
17
+
18
+ export interface FieldProps {
19
+ readonly label?: React.ReactNode;
20
+ readonly helperText?: React.ReactNode;
21
+ readonly errorText?: React.ReactNode;
22
+ readonly optionalText?: React.ReactNode;
23
+ readonly children: React.ReactElement<InputProps> | React.ReactElement<InputGroupProps>;
24
+ readonly size?: 'sm' | 'md' | 'lg' | '2xl';
25
+ readonly required?: boolean;
26
+ readonly disabled?: boolean;
27
+ readonly readOnly?: boolean;
28
+ readonly invalid?: boolean;
29
+ readonly floating?: boolean;
30
+ readonly bgColor?: string | Record<string, string>;
31
+ readonly id?: string;
32
+ readonly className?: string;
33
+ readonly style?: React.CSSProperties;
34
+ }
35
+
36
+ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
37
+ function Field(props, ref) {
38
+ const {
39
+ label,
40
+ children,
41
+ helperText,
42
+ errorText,
43
+ optionalText,
44
+ size,
45
+ required,
46
+ disabled,
47
+ readOnly,
48
+ invalid,
49
+ floating,
50
+ id,
51
+ className,
52
+ style,
53
+ } = props;
54
+
55
+ // A floating field cannot be without a label.
56
+ if (floating && label) {
57
+ const injectedProps = {
58
+ className: 'peer',
59
+ placeholder: ' ',
60
+ size,
61
+ floating,
62
+ disabled,
63
+ readOnly,
64
+ };
65
+
66
+ const labelElement = (
67
+ <LabelPrimitive.Root
68
+ className={ cn(
69
+ 'absolute left-3 top-1/2 -translate-y-1/2',
70
+ 'text-[var(--color-input-placeholder,theme(colors.gray.400))]',
71
+ 'pointer-events-none select-none',
72
+ 'origin-top-left transition-all duration-150',
73
+ 'peer-focus:top-1 peer-focus:translate-y-0 peer-focus:scale-75',
74
+ 'peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:translate-y-0 peer-[:not(:placeholder-shown)]:scale-75',
75
+ ) }
76
+ htmlFor={ id }
77
+ >
78
+ { label }
79
+ { required && (
80
+ <span className="ml-0.5 text-[var(--color-fg-error,theme(colors.red.500))]" aria-hidden="true">*</span>
81
+ ) }
82
+ { !required && optionalText && (
83
+ <span className="ml-1 text-[var(--color-text-secondary,theme(colors.gray.400))] text-sm">
84
+ { optionalText }
85
+ </span>
86
+ ) }
87
+ { errorText && (
88
+ <span className="ml-0.5 text-[var(--color-fg-error,theme(colors.red.500))] text-sm">
89
+ -{ space }{ errorText }
90
+ </span>
91
+ ) }
92
+ </LabelPrimitive.Root>
93
+ );
94
+
95
+ const helperTextElement = helperText ? (
96
+ <p className="mt-1.5 text-xs text-[var(--color-text-secondary,theme(colors.gray.400))]">
97
+ { helperText }
98
+ </p>
99
+ ) : null;
100
+
101
+ const child = React.Children.only<React.ReactElement<InputProps | InputGroupProps>>(children);
102
+ const isInputGroup = getComponentDisplayName(child.type) === 'InputGroup';
103
+
104
+ if (isInputGroup) {
105
+ const inputElement = React.cloneElement(
106
+ React.Children.only<React.ReactElement<InputProps>>(child.props.children as React.ReactElement<InputProps>),
107
+ injectedProps,
108
+ );
109
+
110
+ const groupInputElement = React.cloneElement(child,
111
+ {},
112
+ inputElement,
113
+ labelElement,
114
+ );
115
+
116
+ return (
117
+ <div
118
+ ref={ ref }
119
+ id={ id }
120
+ className={ cn('relative w-full', className) }
121
+ style={ style }
122
+ data-disabled={ disabled || undefined }
123
+ data-readonly={ readOnly || undefined }
124
+ data-invalid={ invalid || undefined }
125
+ >
126
+ { groupInputElement }
127
+ { helperTextElement }
128
+ </div>
129
+ );
130
+ }
131
+
132
+ const inputElement = React.cloneElement(child, injectedProps);
133
+
134
+ return (
135
+ <div
136
+ ref={ ref }
137
+ id={ id }
138
+ className={ cn('relative w-full', className) }
139
+ style={ style }
140
+ data-disabled={ disabled || undefined }
141
+ data-readonly={ readOnly || undefined }
142
+ data-invalid={ invalid || undefined }
143
+ >
144
+ { inputElement }
145
+ { labelElement }
146
+ { helperTextElement }
147
+ </div>
148
+ );
149
+ }
150
+
151
+ // Non-floating: standard field layout
152
+ const injectedProps = {
153
+ size,
154
+ };
155
+ const child = React.Children.only<React.ReactElement<InputProps | InputGroupProps>>(children);
156
+ const clonedChild = React.cloneElement(child, injectedProps);
157
+
158
+ return (
159
+ <div
160
+ ref={ ref }
161
+ id={ id }
162
+ className={ cn('flex flex-col gap-1', className) }
163
+ style={ style }
164
+ data-disabled={ disabled || undefined }
165
+ data-readonly={ readOnly || undefined }
166
+ data-invalid={ invalid || undefined }
167
+ >
168
+ { label && (
169
+ <LabelPrimitive.Root
170
+ className={ cn(
171
+ 'text-sm font-medium text-[var(--color-field-label,theme(colors.gray.700))]',
172
+ 'dark:text-[var(--color-field-label,theme(colors.gray.300))]',
173
+ disabled && 'opacity-40',
174
+ ) }
175
+ htmlFor={ id }
176
+ >
177
+ { label }
178
+ { required && (
179
+ <span className="ml-0.5 text-[var(--color-fg-error,theme(colors.red.500))]" aria-hidden="true">*</span>
180
+ ) }
181
+ { !required && optionalText && (
182
+ <span className="ml-1 text-[var(--color-text-secondary,theme(colors.gray.400))] text-sm font-normal">
183
+ { optionalText }
184
+ </span>
185
+ ) }
186
+ </LabelPrimitive.Root>
187
+ ) }
188
+ { clonedChild }
189
+ { helperText && (
190
+ <p className="text-xs text-[var(--color-text-secondary,theme(colors.gray.400))]">
191
+ { helperText }
192
+ </p>
193
+ ) }
194
+ { errorText && (
195
+ <p className="text-xs text-[var(--color-fg-error,theme(colors.red.500))]">
196
+ { errorText }
197
+ </p>
198
+ ) }
199
+ </div>
200
+ );
201
+ },
202
+ );
@@ -0,0 +1,138 @@
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 GreeksDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ readonly delta: number;
13
+ readonly gamma: number;
14
+ readonly theta: number;
15
+ readonly vega: number;
16
+ readonly rho?: number;
17
+ readonly iv?: number;
18
+ readonly compact?: boolean;
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Greek definitions
23
+ // ---------------------------------------------------------------------------
24
+
25
+ interface GreekDef {
26
+ key: string;
27
+ label: string;
28
+ symbol: string;
29
+ color: string;
30
+ barColor: string;
31
+ decimals: number;
32
+ maxMagnitude: number;
33
+ }
34
+
35
+ const GREEKS: GreekDef[] = [
36
+ { key: 'delta', label: 'Delta', symbol: '\u0394', color: 'text-blue-400', barColor: 'bg-blue-500', decimals: 4, maxMagnitude: 1 },
37
+ { key: 'gamma', label: 'Gamma', symbol: '\u0393', color: 'text-purple-400', barColor: 'bg-purple-500', decimals: 4, maxMagnitude: 0.1 },
38
+ { key: 'theta', label: 'Theta', symbol: '\u0398', color: 'text-red-400', barColor: 'bg-red-500', decimals: 4, maxMagnitude: 1 },
39
+ { key: 'vega', label: 'Vega', symbol: '\u03BD', color: 'text-emerald-400', barColor: 'bg-emerald-500', decimals: 4, maxMagnitude: 1 },
40
+ { key: 'rho', label: 'Rho', symbol: '\u03C1', color: 'text-orange-400', barColor: 'bg-orange-500', decimals: 4, maxMagnitude: 1 },
41
+ { key: 'iv', label: 'IV', symbol: '\u03C3', color: 'text-amber-400', barColor: 'bg-amber-500', decimals: 1, maxMagnitude: 100 },
42
+ ];
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // GreekItem
46
+ // ---------------------------------------------------------------------------
47
+
48
+ interface GreekItemProps {
49
+ readonly def: GreekDef;
50
+ readonly value: number;
51
+ readonly compact?: boolean;
52
+ }
53
+
54
+ function GreekItem({ def, value, compact }: GreekItemProps) {
55
+ const magnitude = Math.min(Math.abs(value) / def.maxMagnitude, 1);
56
+ const displayValue = def.key === 'iv'
57
+ ? `${(value * 100).toFixed(def.decimals)}%`
58
+ : value.toFixed(def.decimals);
59
+
60
+ return (
61
+ <div
62
+ className={cn(
63
+ 'flex items-center gap-1.5',
64
+ compact ? 'min-w-0' : 'min-w-[100px]',
65
+ )}
66
+ >
67
+ <span className={cn('text-[10px] font-semibold shrink-0', def.color)}>
68
+ {def.symbol}
69
+ </span>
70
+ {!compact && (
71
+ <span className="text-[10px] text-zinc-500 shrink-0 w-[34px]">
72
+ {def.label}
73
+ </span>
74
+ )}
75
+ <span className={cn(
76
+ 'font-mono tabular-nums text-xs text-zinc-200 shrink-0',
77
+ value < 0 && 'text-red-400',
78
+ )}>
79
+ {displayValue}
80
+ </span>
81
+ {!compact && (
82
+ <div className="flex-1 h-1 rounded-full bg-zinc-800 min-w-[24px] max-w-[48px] overflow-hidden">
83
+ <div
84
+ className={cn('h-full rounded-full transition-all', def.barColor)}
85
+ style={{ width: `${magnitude * 100}%`, opacity: 0.7 }}
86
+ />
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // GreeksDisplay
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export const GreeksDisplay = React.forwardRef<HTMLDivElement, GreeksDisplayProps>(
98
+ function GreeksDisplay(props, ref) {
99
+ const {
100
+ delta,
101
+ gamma,
102
+ theta,
103
+ vega,
104
+ rho,
105
+ iv,
106
+ compact = false,
107
+ className,
108
+ ...rest
109
+ } = props;
110
+
111
+ const values: Record<string, number | undefined> = {
112
+ delta, gamma, theta, vega, rho, iv,
113
+ };
114
+
115
+ const activeGreeks = GREEKS.filter((g) => values[g.key] !== undefined);
116
+
117
+ return (
118
+ <div
119
+ ref={ref}
120
+ className={cn(
121
+ 'flex flex-wrap items-center',
122
+ compact ? 'gap-3' : 'gap-4',
123
+ className,
124
+ )}
125
+ {...rest}
126
+ >
127
+ {activeGreeks.map((g) => (
128
+ <GreekItem
129
+ key={g.key}
130
+ def={g}
131
+ value={values[g.key] as number}
132
+ compact={compact}
133
+ />
134
+ ))}
135
+ </div>
136
+ );
137
+ },
138
+ );
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+
3
+ import { cn } from './utils';
4
+
5
+ export interface HeadingProps extends React.ComponentPropsWithoutRef<'h1'> {
6
+ level?: '1' | '2' | '3';
7
+ // Legacy Chakra style-prop shims
8
+ mb?: number | string;
9
+ size?: string;
10
+ }
11
+
12
+ const LEVEL_TAG = {
13
+ '1': 'h1',
14
+ '2': 'h2',
15
+ '3': 'h3',
16
+ } as const;
17
+
18
+ // base (mobile) + lg (desktop) responsive text styles per level
19
+ //
20
+ // Level 1: heading.md -> heading.xl
21
+ // base: 18px/24px font-500
22
+ // lg: 32px/40px font-500 tracking=-0.5px
23
+ //
24
+ // Level 2: heading.sm -> heading.lg
25
+ // base: 16px/24px font-500
26
+ // lg: 24px/32px font-500
27
+ //
28
+ // Level 3: heading.xs -> heading.md
29
+ // base: 14px/20px font-600
30
+ // lg: 18px/24px font-500
31
+ const LEVEL_CLASSES = {
32
+ '1': 'text-[18px] leading-[24px] font-medium lg:text-[32px] lg:leading-[40px] lg:tracking-[-0.5px]',
33
+ '2': 'text-[16px] leading-[24px] font-medium lg:text-[24px] lg:leading-[32px]',
34
+ '3': 'text-[14px] leading-[20px] font-semibold lg:text-[18px] lg:leading-[24px] lg:font-medium',
35
+ } as const;
36
+
37
+ const BASE_CLASSES = 'font-heading text-heading';
38
+
39
+ export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
40
+ function Heading({ level, className, mb, size: _size, style: styleProp, ...rest }, ref) {
41
+ const Tag = level ? LEVEL_TAG[level] : 'h2';
42
+ const levelClasses = level ? LEVEL_CLASSES[level] : undefined;
43
+ const shimStyle: React.CSSProperties = { ...styleProp };
44
+ if (mb !== undefined) shimStyle.marginBottom = typeof mb === 'number' ? `${ mb * 4 }px` : mb;
45
+
46
+ return (
47
+ <Tag
48
+ ref={ ref }
49
+ className={ cn(BASE_CLASSES, levelClasses, className) }
50
+ style={ Object.keys(shimStyle).length > 0 ? shimStyle : undefined }
51
+ { ...rest }
52
+ />
53
+ );
54
+ },
55
+ );