@luxfi/ui 7.4.10 → 7.4.12

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 (137) hide show
  1. package/dist/accordion.cjs +107 -166
  2. package/dist/accordion.d.cts +18 -51
  3. package/dist/accordion.d.ts +18 -51
  4. package/dist/accordion.js +109 -166
  5. package/dist/alert.cjs +68 -46
  6. package/dist/alert.js +69 -46
  7. package/dist/avatar.cjs +49 -46
  8. package/dist/avatar.js +52 -48
  9. package/dist/badge.cjs +82 -49
  10. package/dist/badge.js +83 -50
  11. package/dist/checkbox.cjs +42 -75
  12. package/dist/checkbox.js +43 -76
  13. package/dist/chrome.cjs +196 -386
  14. package/dist/chrome.js +198 -386
  15. package/dist/close-button.cjs +3 -7
  16. package/dist/close-button.js +3 -7
  17. package/dist/collapsible.cjs +35 -26
  18. package/dist/collapsible.d.cts +2 -1
  19. package/dist/collapsible.d.ts +2 -1
  20. package/dist/collapsible.js +34 -24
  21. package/dist/dialog.cjs +3 -7
  22. package/dist/dialog.d.cts +1 -1
  23. package/dist/dialog.d.ts +1 -1
  24. package/dist/dialog.js +3 -7
  25. package/dist/drawer.cjs +95 -122
  26. package/dist/drawer.d.cts +43 -10
  27. package/dist/drawer.d.ts +43 -10
  28. package/dist/drawer.js +97 -122
  29. package/dist/empty-state.cjs +13 -13
  30. package/dist/empty-state.js +13 -13
  31. package/dist/expiration-selector.cjs +58 -63
  32. package/dist/expiration-selector.js +58 -63
  33. package/dist/field.cjs +60 -109
  34. package/dist/field.d.cts +0 -1
  35. package/dist/field.d.ts +0 -1
  36. package/dist/field.js +61 -109
  37. package/dist/greeks-display.cjs +48 -39
  38. package/dist/greeks-display.js +48 -39
  39. package/dist/heading.cjs +10 -17
  40. package/dist/heading.d.cts +0 -2
  41. package/dist/heading.d.ts +0 -2
  42. package/dist/heading.js +10 -17
  43. package/dist/icon-button.cjs +82 -82
  44. package/dist/icon-button.d.cts +16 -12
  45. package/dist/icon-button.d.ts +16 -12
  46. package/dist/icon-button.js +82 -82
  47. package/dist/index.cjs +1812 -2105
  48. package/dist/index.d.cts +1 -5
  49. package/dist/index.d.ts +1 -5
  50. package/dist/index.js +1814 -2100
  51. package/dist/input-group.cjs +11 -12
  52. package/dist/input-group.js +11 -12
  53. package/dist/input.cjs +27 -27
  54. package/dist/input.d.cts +21 -1
  55. package/dist/input.d.ts +21 -1
  56. package/dist/input.js +25 -28
  57. package/dist/menu.cjs +160 -271
  58. package/dist/menu.d.cts +41 -57
  59. package/dist/menu.d.ts +41 -57
  60. package/dist/menu.js +161 -251
  61. package/dist/option-chain.cjs +89 -67
  62. package/dist/option-chain.js +89 -67
  63. package/dist/option-position.cjs +88 -85
  64. package/dist/option-position.js +88 -85
  65. package/dist/pnl-diagram.cjs +20 -20
  66. package/dist/pnl-diagram.js +20 -20
  67. package/dist/popover.cjs +4 -8
  68. package/dist/popover.js +4 -8
  69. package/dist/progress.cjs +15 -22
  70. package/dist/progress.d.cts +6 -6
  71. package/dist/progress.d.ts +6 -6
  72. package/dist/progress.js +15 -22
  73. package/dist/radio.cjs +35 -67
  74. package/dist/radio.d.cts +18 -18
  75. package/dist/radio.d.ts +18 -18
  76. package/dist/radio.js +36 -68
  77. package/dist/select.cjs +40 -39
  78. package/dist/select.d.cts +35 -4
  79. package/dist/select.d.ts +35 -4
  80. package/dist/select.js +40 -38
  81. package/dist/separator.cjs +8 -33
  82. package/dist/separator.js +8 -33
  83. package/dist/slider.cjs +33 -49
  84. package/dist/slider.js +34 -50
  85. package/dist/switch.cjs +48 -55
  86. package/dist/switch.js +49 -56
  87. package/dist/tabs.cjs +108 -197
  88. package/dist/tabs.d.cts +16 -60
  89. package/dist/tabs.d.ts +16 -60
  90. package/dist/tabs.js +109 -196
  91. package/dist/tag.cjs +112 -69
  92. package/dist/tag.js +113 -69
  93. package/dist/textarea.cjs +56 -27
  94. package/dist/textarea.js +57 -28
  95. package/dist/toaster.cjs +97 -79
  96. package/dist/toaster.d.cts +7 -8
  97. package/dist/toaster.d.ts +7 -8
  98. package/dist/toaster.js +78 -80
  99. package/dist/tooltip.cjs +28 -17
  100. package/dist/tooltip.js +30 -18
  101. package/package.json +2 -16
  102. package/src/accordion.tsx +173 -227
  103. package/src/alert.tsx +78 -45
  104. package/src/avatar.tsx +54 -37
  105. package/src/badge.tsx +65 -44
  106. package/src/checkbox.tsx +70 -89
  107. package/src/close-button.tsx +3 -8
  108. package/src/collapsible.tsx +55 -52
  109. package/src/drawer.tsx +123 -115
  110. package/src/empty-state.tsx +21 -17
  111. package/src/expiration-selector.tsx +84 -66
  112. package/src/field.tsx +68 -124
  113. package/src/greeks-display.tsx +59 -51
  114. package/src/heading.tsx +22 -26
  115. package/src/icon-button.tsx +134 -141
  116. package/src/ink.tsx +37 -0
  117. package/src/input-group.tsx +59 -36
  118. package/src/input.tsx +41 -32
  119. package/src/menu.tsx +224 -397
  120. package/src/option-chain.tsx +154 -104
  121. package/src/option-position.tsx +136 -114
  122. package/src/pin-input.tsx +37 -29
  123. package/src/pnl-diagram.tsx +36 -28
  124. package/src/popover.tsx +1 -1
  125. package/src/progress.tsx +17 -21
  126. package/src/radio.tsx +56 -75
  127. package/src/select.tsx +80 -59
  128. package/src/separator.tsx +13 -34
  129. package/src/slider.tsx +43 -44
  130. package/src/strategy-builder.tsx +245 -156
  131. package/src/switch.tsx +63 -64
  132. package/src/tabs.tsx +166 -296
  133. package/src/tag.tsx +89 -51
  134. package/src/textarea.tsx +19 -29
  135. package/src/toaster.tsx +118 -86
  136. package/src/tooltip.tsx +21 -13
  137. package/tokens.css +79 -0
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Text, XStack, YStack } from '@hanzo/gui';
3
4
  import * as React from 'react';
4
5
 
5
- import { cn } from './utils';
6
-
7
6
  // ---------------------------------------------------------------------------
8
7
  // Types
9
8
  // ---------------------------------------------------------------------------
@@ -48,31 +47,62 @@ interface ExpirationPillProps {
48
47
  readonly onClick: () => void;
49
48
  }
50
49
 
50
+ // Resting and selected are the same two states every selectable pill in this
51
+ // package uses (tabs, menu rows): a solid fill when selected, a bordered card
52
+ // otherwise. The date label sets no colour of its own — it inherits the
53
+ // button's, so `hoverStyle` here is what brightens it on hover, in one place.
54
+ const SHAPE = {
55
+ selected: {
56
+ borderColor: 'transparent',
57
+ backgroundColor: 'var(--color-selected-control-bg)',
58
+ color: 'var(--color-selected-control-text)',
59
+ },
60
+ resting: {
61
+ borderColor: 'var(--color-border-divider)',
62
+ backgroundColor: 'var(--card)',
63
+ color: 'var(--color-text-secondary)',
64
+ hoverStyle: { borderColor: 'var(--color-hover)', color: 'var(--color-hover)' },
65
+ },
66
+ } as const;
67
+
51
68
  function ExpirationPill({ exp, isSelected, onClick }: ExpirationPillProps) {
69
+ const shape = isSelected ? SHAPE.selected : SHAPE.resting;
70
+
71
+ // Near-expiry is a notice, and a notice in Lux is the loudest neutral — it
72
+ // only shows while resting; a selected pill's own fill already carries the
73
+ // emphasis.
74
+ const dteColor = exp.dte <= 7 && !isSelected
75
+ ? 'var(--color-status-warn)'
76
+ : isSelected ? 'var(--color-selected-control-text)' : 'var(--muted-foreground)';
77
+
52
78
  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
- )}
79
+ <YStack
80
+ unstyled
81
+ render={ <button type="button"/> }
82
+ onClick={ onClick }
83
+ alignItems="center"
84
+ gap={ 2 }
85
+ borderRadius={ 6 }
86
+ paddingHorizontal={ 12 }
87
+ paddingVertical={ 6 }
88
+ flexShrink={ 0 }
89
+ borderWidth={ 1 }
90
+ borderStyle="solid"
91
+ cursor="pointer"
92
+ { ...(shape as object) }
63
93
  >
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-[var(--foreground)]' : 'text-[var(--muted-foreground)]',
70
- // Near expiry is a notice, and a notice in Lux is the loudest neutral.
71
- exp.dte <= 7 && !isSelected && 'text-[var(--color-status-warn)]',
72
- )}>
73
- {exp.dte}d
74
- </span>
75
- </button>
94
+ <Text unstyled fontSize={ 12 } fontWeight="500" style={ { whiteSpace: 'nowrap' } as never }>
95
+ { formatExpDate(exp.date) }
96
+ </Text>
97
+ <Text
98
+ fontVariant={ ['tabular-nums'] as never }
99
+ style={ { fontFamily: 'monospace' } as never }
100
+ fontSize={ 10 }
101
+ color={ dteColor as never }
102
+ >
103
+ { exp.dte }d
104
+ </Text>
105
+ </YStack>
76
106
  );
77
107
  }
78
108
 
@@ -111,54 +141,42 @@ export const ExpirationSelector = React.forwardRef<HTMLDivElement, ExpirationSel
111
141
  })).filter((g) => g.items.length > 0);
112
142
 
113
143
  return (
114
- <div
115
- ref={ref}
116
- className={cn('flex flex-col gap-2', className)}
117
- {...rest}
118
- >
119
- {grouped.map((group) => (
120
- <div key={group.type} className="flex flex-col gap-1">
121
- <span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-600 px-1">
122
- {group.label}
123
- </span>
124
- <div className="flex gap-1.5 overflow-x-auto no-scrollbar">
125
- {group.items.map((exp) => (
126
- <div key={exp.date} data-selected={exp.date === selected || undefined}>
127
- <ExpirationPill
128
- exp={exp}
129
- isSelected={exp.date === selected}
130
- onClick={() => onSelect(exp.date)}
131
- />
144
+ <YStack ref={ ref as never } gap={ 8 } className={ className } { ...(rest as object) }>
145
+ { grouped.map((group) => (
146
+ <YStack key={ group.type } gap={ 4 }>
147
+ <Text
148
+ fontSize={ 10 }
149
+ fontWeight="600"
150
+ textTransform="uppercase"
151
+ letterSpacing={ 0.5 }
152
+ paddingHorizontal={ 4 }
153
+ color={ 'var(--color-text-secondary)' as never }
154
+ >
155
+ { group.label }
156
+ </Text>
157
+ <XStack gap={ 6 } overflowX="auto" overflowY="hidden" className="lux-no-scrollbar">
158
+ { group.items.map((exp) => (
159
+ <div key={ exp.date } data-selected={ exp.date === selected || undefined }>
160
+ <ExpirationPill exp={ exp } isSelected={ exp.date === selected } onClick={ () => onSelect(exp.date) }/>
132
161
  </div>
133
- ))}
134
- </div>
135
- </div>
136
- ))}
137
- </div>
162
+ )) }
163
+ </XStack>
164
+ </YStack>
165
+ )) }
166
+ </YStack>
138
167
  );
139
168
  }
140
169
 
141
170
  return (
142
- <div
143
- ref={ref}
144
- className={cn('flex items-center gap-1', className)}
145
- {...rest}
146
- >
147
- <div
148
- ref={scrollRef}
149
- className="flex gap-1.5 overflow-x-auto no-scrollbar"
150
- >
151
- {expirations.map((exp) => (
152
- <div key={exp.date} data-selected={exp.date === selected || undefined}>
153
- <ExpirationPill
154
- exp={exp}
155
- isSelected={exp.date === selected}
156
- onClick={() => onSelect(exp.date)}
157
- />
171
+ <XStack ref={ ref as never } alignItems="center" gap={ 4 } className={ className } { ...(rest as object) }>
172
+ <XStack ref={ scrollRef as never } gap={ 6 } overflowX="auto" overflowY="hidden" className="lux-no-scrollbar">
173
+ { expirations.map((exp) => (
174
+ <div key={ exp.date } data-selected={ exp.date === selected || undefined }>
175
+ <ExpirationPill exp={ exp } isSelected={ exp.date === selected } onClick={ () => onSelect(exp.date) }/>
158
176
  </div>
159
- ))}
160
- </div>
161
- </div>
177
+ )) }
178
+ </XStack>
179
+ </XStack>
162
180
  );
163
181
  },
164
182
  );
package/src/field.tsx CHANGED
@@ -1,12 +1,20 @@
1
- import * as LabelPrimitive from '@radix-ui/react-label';
1
+ import { Label, Text, YStack } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
4
  import type { InputProps } from './input';
7
5
  import type { InputGroupProps } from './input-group';
8
6
 
9
- // Inline constants
7
+ // A labelled form control: label, the control itself, and whichever of helper /
8
+ // error / optional text applies.
9
+ //
10
+ // The label is gui's `Label` — a real `<label htmlFor>`, which is the entire
11
+ // reason Radix's was imported. Everything else was Tailwind, and a good part of
12
+ // it was dead: the colours were written `text-[var(--color-x,theme(colors.gray.400))]`,
13
+ // and `theme()` is a Tailwind 3 function that v4 does not resolve inside an
14
+ // arbitrary value. So the fallback half of every one of those declarations was
15
+ // discarded and the class contributed nothing whenever the custom property was
16
+ // unset. The tokens are referenced directly now, and they are all defined.
17
+
10
18
  const space = String.fromCharCode(32);
11
19
 
12
20
  function getComponentDisplayName(type: string | React.JSXElementConstructor<unknown>): string | undefined {
@@ -27,75 +35,62 @@ export interface FieldProps {
27
35
  readonly readOnly?: boolean;
28
36
  readonly invalid?: boolean;
29
37
  readonly floating?: boolean;
30
- readonly bgColor?: string | Record<string, string>;
31
38
  readonly id?: string;
32
39
  readonly className?: string;
33
40
  readonly style?: React.CSSProperties;
34
41
  }
35
42
 
43
+ const ERROR = 'var(--color-text-error)' as never;
44
+ const MUTED = 'var(--color-text-secondary)' as never;
45
+
46
+ /** The `*`, the "(optional)", and the inline error that ride alongside a label. */
47
+ const Marks = ({ required, optionalText, errorText }: Pick<FieldProps, 'required' | 'optionalText' | 'errorText'>) => (
48
+ <>
49
+ { required && <Text color={ ERROR } aria-hidden>*</Text> }
50
+ { !required && optionalText && <Text fontSize={ 14 } color={ MUTED }>{ optionalText }</Text> }
51
+ { errorText && <Text fontSize={ 14 } color={ ERROR }>-{ space }{ errorText }</Text> }
52
+ </>
53
+ );
54
+
36
55
  export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
37
56
  function Field(props, ref) {
38
57
  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,
58
+ label, children, helperText, errorText, optionalText, size,
59
+ required, disabled, readOnly, invalid, floating, id, className, style,
53
60
  } = props;
54
61
 
62
+ const flags = {
63
+ 'data-disabled': disabled || undefined,
64
+ 'data-readonly': readOnly || undefined,
65
+ 'data-invalid': invalid || undefined,
66
+ };
67
+
55
68
  // A floating field cannot be without a label.
56
69
  if (floating && label) {
57
- const injectedProps = {
58
- className: 'peer',
59
- placeholder: ' ',
60
- size,
61
- floating,
62
- disabled,
63
- readOnly,
64
- };
70
+ const injectedProps = { className: 'lux-field-control', placeholder: ' ', size, floating, disabled, readOnly };
65
71
 
66
72
  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
- ) }
73
+ <Label
76
74
  htmlFor={ id }
75
+ position="absolute"
76
+ left={ 12 }
77
+ top="50%"
78
+ pointerEvents="none"
79
+ userSelect="none"
80
+ flexDirection="row"
81
+ gap={ 4 }
82
+ color={ 'var(--color-input-placeholder)' as never }
83
+ // The float itself stays in CSS: it keys off `:placeholder-shown` and
84
+ // `:focus` of a SIBLING, which is a selector, not a prop.
85
+ className="lux-field-float"
77
86
  >
78
87
  { 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>
88
+ <Marks required={ required } optionalText={ optionalText } errorText={ errorText }/>
89
+ </Label>
93
90
  );
94
91
 
95
92
  const helperTextElement = helperText ? (
96
- <p className="mt-1.5 text-xs text-[var(--color-text-secondary,theme(colors.gray.400))]">
97
- { helperText }
98
- </p>
93
+ <Text marginTop={ 6 } fontSize={ 12 } color={ MUTED }>{ helperText }</Text>
99
94
  ) : null;
100
95
 
101
96
  const child = React.Children.only<React.ReactElement<InputProps | InputGroupProps>>(children);
@@ -107,96 +102,45 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
107
102
  injectedProps,
108
103
  );
109
104
 
110
- const groupInputElement = React.cloneElement(child,
111
- {},
112
- inputElement,
113
- labelElement,
114
- );
115
-
116
105
  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 }
106
+ <YStack ref={ ref as never } id={ id } position="relative" width="100%" className={ className } style={ style } { ...flags }>
107
+ { React.cloneElement(child, {}, inputElement, labelElement) }
127
108
  { helperTextElement }
128
- </div>
109
+ </YStack>
129
110
  );
130
111
  }
131
112
 
132
- const inputElement = React.cloneElement(child, injectedProps);
133
-
134
113
  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 }
114
+ <YStack ref={ ref as never } id={ id } position="relative" width="100%" className={ className } style={ style } { ...flags }>
115
+ { React.cloneElement(child, injectedProps) }
145
116
  { labelElement }
146
117
  { helperTextElement }
147
- </div>
118
+ </YStack>
148
119
  );
149
120
  }
150
121
 
151
- // Non-floating: standard field layout
152
- const injectedProps = {
153
- size,
154
- };
155
122
  const child = React.Children.only<React.ReactElement<InputProps | InputGroupProps>>(children);
156
- const clonedChild = React.cloneElement(child, injectedProps);
157
123
 
158
124
  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
- >
125
+ <YStack ref={ ref as never } id={ id } gap={ 4 } className={ className } style={ style } { ...flags }>
168
126
  { 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
- ) }
127
+ <Label
175
128
  htmlFor={ id }
129
+ flexDirection="row"
130
+ gap={ 4 }
131
+ fontSize={ 14 }
132
+ fontWeight="500"
133
+ color={ 'var(--color-text-primary)' as never }
134
+ opacity={ disabled ? 0.4 : 1 }
176
135
  >
177
136
  { 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>
137
+ <Marks required={ required } optionalText={ optionalText }/>
138
+ </Label>
198
139
  ) }
199
- </div>
140
+ { React.cloneElement(child, { size }) }
141
+ { helperText && <Text fontSize={ 12 } color={ MUTED }>{ helperText }</Text> }
142
+ { errorText && <Text fontSize={ 12 } color={ ERROR }>{ errorText }</Text> }
143
+ </YStack>
200
144
  );
201
145
  },
202
146
  );
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Text, View, XStack } from '@hanzo/gui';
3
4
  import * as React from 'react';
4
5
 
5
- import { cn } from './utils';
6
-
7
6
  // ---------------------------------------------------------------------------
8
7
  // Types
9
8
  // ---------------------------------------------------------------------------
@@ -36,12 +35,12 @@ interface GreekDef {
36
35
  // on one achromatic ramp (tokens.css --chart-*, plus --foreground for the sixth),
37
36
  // so the panel obeys the brand atom and reskins with the host.
38
37
  const GREEKS: GreekDef[] = [
39
- { key: 'delta', label: 'Delta', symbol: '\u0394', color: 'text-[var(--foreground)]', barColor: 'bg-[var(--foreground)]', decimals: 4, maxMagnitude: 1 },
40
- { key: 'gamma', label: 'Gamma', symbol: '\u0393', color: 'text-[var(--chart-1)]', barColor: 'bg-[var(--chart-1)]', decimals: 4, maxMagnitude: 0.1 },
41
- { key: 'theta', label: 'Theta', symbol: '\u0398', color: 'text-[var(--chart-4)]', barColor: 'bg-[var(--chart-4)]', decimals: 4, maxMagnitude: 1 },
42
- { key: 'vega', label: 'Vega', symbol: '\u03BD', color: 'text-[var(--chart-2)]', barColor: 'bg-[var(--chart-2)]', decimals: 4, maxMagnitude: 1 },
43
- { key: 'rho', label: 'Rho', symbol: '\u03C1', color: 'text-[var(--chart-3)]', barColor: 'bg-[var(--chart-3)]', decimals: 4, maxMagnitude: 1 },
44
- { key: 'iv', label: 'IV', symbol: '\u03C3', color: 'text-[var(--chart-5)]', barColor: 'bg-[var(--chart-5)]', decimals: 1, maxMagnitude: 100 },
38
+ { key: 'delta', label: 'Delta', symbol: 'Δ', color: 'var(--foreground)', barColor: 'var(--foreground)', decimals: 4, maxMagnitude: 1 },
39
+ { key: 'gamma', label: 'Gamma', symbol: 'Γ', color: 'var(--chart-1)', barColor: 'var(--chart-1)', decimals: 4, maxMagnitude: 0.1 },
40
+ { key: 'theta', label: 'Theta', symbol: 'Θ', color: 'var(--chart-4)', barColor: 'var(--chart-4)', decimals: 4, maxMagnitude: 1 },
41
+ { key: 'vega', label: 'Vega', symbol: 'ν', color: 'var(--chart-2)', barColor: 'var(--chart-2)', decimals: 4, maxMagnitude: 1 },
42
+ { key: 'rho', label: 'Rho', symbol: 'ρ', color: 'var(--chart-3)', barColor: 'var(--chart-3)', decimals: 4, maxMagnitude: 1 },
43
+ { key: 'iv', label: 'IV', symbol: 'σ', color: 'var(--chart-5)', barColor: 'var(--chart-5)', decimals: 1, maxMagnitude: 100 },
45
44
  ];
46
45
 
47
46
  // ---------------------------------------------------------------------------
@@ -61,35 +60,45 @@ function GreekItem({ def, value, compact }: GreekItemProps) {
61
60
  : value.toFixed(def.decimals);
62
61
 
63
62
  return (
64
- <div
65
- className={cn(
66
- 'flex items-center gap-1.5',
67
- compact ? 'min-w-0' : 'min-w-[100px]',
68
- )}
69
- >
70
- <span className={cn('text-[10px] font-semibold shrink-0', def.color)}>
71
- {def.symbol}
72
- </span>
73
- {!compact && (
74
- <span className="text-[10px] text-zinc-500 shrink-0 w-[34px]">
75
- {def.label}
76
- </span>
77
- )}
78
- <span className={cn(
79
- 'font-mono tabular-nums text-xs text-zinc-200 shrink-0',
80
- value < 0 && 'text-red-400',
81
- )}>
82
- {displayValue}
83
- </span>
84
- {!compact && (
85
- <div className="flex-1 h-1 rounded-full bg-zinc-800 min-w-[24px] max-w-[48px] overflow-hidden">
86
- <div
87
- className={cn('h-full rounded-full transition-all', def.barColor)}
88
- style={{ width: `${magnitude * 100}%`, opacity: 0.7 }}
63
+ <XStack alignItems="center" gap={ 6 } minWidth={ compact ? 0 : 100 }>
64
+ <Text fontSize={ 10 } fontWeight="600" flexShrink={ 0 } color={ def.color as never }>
65
+ { def.symbol }
66
+ </Text>
67
+ { !compact && (
68
+ <Text fontSize={ 10 } flexShrink={ 0 } width={ 34 } color={ 'var(--color-text-secondary)' as never }>
69
+ { def.label }
70
+ </Text>
71
+ ) }
72
+ <Text
73
+ fontSize={ 12 }
74
+ flexShrink={ 0 }
75
+ style={ { fontFamily: 'monospace' } as never }
76
+ fontVariant={ ['tabular-nums'] as never }
77
+ color={ (value < 0 ? 'var(--color-status-bad)' : 'var(--color-text-primary)') as never }
78
+ >
79
+ { displayValue }
80
+ </Text>
81
+ { !compact && (
82
+ <View
83
+ flex={ 1 }
84
+ height={ 4 }
85
+ borderRadius={ 9999 }
86
+ minWidth={ 24 }
87
+ maxWidth={ 48 }
88
+ overflow="hidden"
89
+ backgroundColor={ 'var(--color-progress-track)' as never }
90
+ >
91
+ <View
92
+ height="100%"
93
+ borderRadius={ 9999 }
94
+ transition="quick"
95
+ width={ `${ magnitude * 100 }%` as never }
96
+ opacity={ 0.7 }
97
+ backgroundColor={ def.barColor as never }
89
98
  />
90
- </div>
91
- )}
92
- </div>
99
+ </View>
100
+ ) }
101
+ </XStack>
93
102
  );
94
103
  }
95
104
 
@@ -118,24 +127,23 @@ export const GreeksDisplay = React.forwardRef<HTMLDivElement, GreeksDisplayProps
118
127
  const activeGreeks = GREEKS.filter((g) => values[g.key] !== undefined);
119
128
 
120
129
  return (
121
- <div
122
- ref={ref}
123
- className={cn(
124
- 'flex flex-wrap items-center',
125
- compact ? 'gap-3' : 'gap-4',
126
- className,
127
- )}
128
- {...rest}
130
+ <XStack
131
+ ref={ ref as never }
132
+ flexWrap="wrap"
133
+ alignItems="center"
134
+ gap={ compact ? 12 : 16 }
135
+ className={ className }
136
+ { ...(rest as object) }
129
137
  >
130
- {activeGreeks.map((g) => (
138
+ { activeGreeks.map((g) => (
131
139
  <GreekItem
132
- key={g.key}
133
- def={g}
134
- value={values[g.key] as number}
135
- compact={compact}
140
+ key={ g.key }
141
+ def={ g }
142
+ value={ values[g.key] as number }
143
+ compact={ compact }
136
144
  />
137
- ))}
138
- </div>
145
+ )) }
146
+ </XStack>
139
147
  );
140
148
  },
141
149
  );
package/src/heading.tsx CHANGED
@@ -1,46 +1,42 @@
1
1
  import { H1, H2, H3 } from '@hanzo/gui';
2
2
  import React from 'react';
3
3
 
4
- import { cn } from './utils';
4
+ // gui's heading primitives real h1/h2/h3 on web, sized Text on native, both
5
+ // inheriting the theme's heading font.
6
+ //
7
+ // The type ramp is props rather than classes, so it survives on native, where a
8
+ // className means nothing. `$lg` is min-width 1024 in the v5 media config — the
9
+ // same breakpoint Tailwind's `lg:` means, so the ramp is unchanged.
10
+ //
11
+ // It is `$lg`, not `$gtLg`. The `gt*` names belong to the v4 media config and
12
+ // this package builds on v5, whose keys are plain mobile-first min-widths. gui
13
+ // does not consume a media prop it has no key for; it forwards it, and React
14
+ // reports it as an unknown DOM attribute. A wrong breakpoint name is not a
15
+ // smaller heading — it is no responsive rule at all.
5
16
 
6
17
  export interface HeadingProps extends React.ComponentPropsWithoutRef<'h1'> {
7
18
  level?: '1' | '2' | '3';
8
- // Legacy Chakra style-prop shims
9
- mb?: number | string;
10
- size?: string;
11
19
  }
12
20
 
13
- // gui's heading primitives real h1/h2/h3 on web, sized Text on native, both
14
- // inheriting the theme's heading font. The Lux type ramp stays in the classes.
15
- const LEVEL_TAG = { '1': H1, '2': H2, '3': H3 } as const;
21
+ const TAG = { '1': H1, '2': H2, '3': H3 } as const;
16
22
 
17
- // base (mobile) + lg (desktop) responsive text styles per level
18
- //
19
- // Level 1: 18px/24px font-500 -> 32px/40px font-500 tracking=-0.5px
20
- // Level 2: 16px/24px font-500 -> 24px/32px font-500
21
- // Level 3: 14px/20px font-600 -> 18px/24px font-500
22
- const LEVEL_CLASSES = {
23
- '1': 'text-[18px] leading-[24px] font-medium lg:text-[32px] lg:leading-[40px] lg:tracking-[-0.5px]',
24
- '2': 'text-[16px] leading-[24px] font-medium lg:text-[24px] lg:leading-[32px]',
25
- '3': 'text-[14px] leading-[20px] font-semibold lg:text-[18px] lg:leading-[24px] lg:font-medium',
23
+ const RAMP = {
24
+ '1': { fontSize: 18, lineHeight: 24, fontWeight: '500', $lg: { fontSize: 32, lineHeight: 40, letterSpacing: -0.5 } },
25
+ '2': { fontSize: 16, lineHeight: 24, fontWeight: '500', $lg: { fontSize: 24, lineHeight: 32 } },
26
+ '3': { fontSize: 14, lineHeight: 20, fontWeight: '600', $lg: { fontSize: 18, lineHeight: 24, fontWeight: '500' } },
26
27
  } as const;
27
28
 
28
- const BASE_CLASSES = 'font-heading text-heading';
29
-
30
29
  export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
31
- function Heading({ level, className, mb, size: _size, style: styleProp, ...rest }, ref) {
32
- const Tag = level ? LEVEL_TAG[level] : H2;
33
- const levelClasses = level ? LEVEL_CLASSES[level] : undefined;
34
- const mergedStyle = mb !== undefined ?
35
- { ...styleProp, marginBottom: typeof mb === 'number' ? `${ mb * 4 }px` : mb } :
36
- styleProp;
30
+ function Heading({ level, ...rest }, ref) {
31
+ const Tag = level ? TAG[level] : H2;
37
32
 
38
33
  return (
39
34
  <Tag
40
35
  ref={ ref as never }
41
36
  unstyled
42
- className={ cn(BASE_CLASSES, levelClasses, className) }
43
- style={ mergedStyle as never }
37
+ fontFamily="$heading"
38
+ color={ 'var(--color-heading)' as never }
39
+ { ...(level ? RAMP[level] : null) }
44
40
  { ...(rest as object) }
45
41
  />
46
42
  );