@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
@@ -3,9 +3,18 @@ import * as React from 'react';
3
3
 
4
4
  import { Skeleton } from './skeleton';
5
5
 
6
- // ---------------------------------------------------------------------------
7
- // IconButtonFrame — styled base
8
- // ---------------------------------------------------------------------------
6
+ // A button whose whole content is an icon.
7
+ //
8
+ // `expanded`, `selected` and `highlighted` are React props, so the look they
9
+ // select is chosen HERE, by merging style objects, rather than by writing
10
+ // `data-[selected]:` classes and letting a stylesheet re-derive the state that
11
+ // this component already knows. The `data-*` attributes are still emitted —
12
+ // they are a DOM contract consumers target — but nothing in this file styles
13
+ // through them.
14
+ //
15
+ // Two things stay CSS because no prop reaches them: the spinner's keyframe (a
16
+ // gui transition interpolates between two states, it does not loop) and the
17
+ // size of a descendant `svg`.
9
18
 
10
19
  const IconButtonFrame = styled(View, {
11
20
  name: 'LuxIconButton',
@@ -16,7 +25,7 @@ const IconButtonFrame = styled(View, {
16
25
  alignItems: 'center',
17
26
  justifyContent: 'center',
18
27
  padding: 0,
19
- minWidth: 'auto' as any,
28
+ minWidth: 'auto' as never,
20
29
  flexShrink: 1,
21
30
  backgroundColor: 'transparent',
22
31
  borderWidth: 0,
@@ -25,189 +34,173 @@ const IconButtonFrame = styled(View, {
25
34
  size: {
26
35
  '2xs': { width: 20, height: 20, borderRadius: 4 },
27
36
  '2xs_alt': { width: 20, height: 20, borderRadius: 4 },
28
- md: { width: 32, height: 32, borderRadius: 'var(--radius-base, 8px)' as any },
37
+ md: { width: 32, height: 32, borderRadius: 8 },
29
38
  },
30
39
 
31
40
  disabled: {
32
- true: {
33
- opacity: 0.4,
34
- cursor: 'not-allowed',
35
- pointerEvents: 'none',
36
- },
41
+ true: { opacity: 0.4, cursor: 'not-allowed', pointerEvents: 'none' },
37
42
  },
38
43
  } as const,
39
44
  });
40
45
 
41
- // ---------------------------------------------------------------------------
42
- // Variant class maps (CSS custom properties via Tailwind)
43
- // ---------------------------------------------------------------------------
44
-
45
- const VARIANT_CLASSES: Record<string, string> = {
46
- plain: 'bg-transparent text-inherit border-none hover:bg-transparent',
47
-
48
- icon_secondary: [
49
- 'bg-transparent text-icon-secondary border-none',
50
- 'hover:text-hover',
51
- 'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text',
52
- 'data-[selected]:hover:text-hover',
53
- 'data-[expanded]:text-hover',
54
- ].join(' '),
55
-
56
- icon_background: [
57
- 'bg-[var(--color-button-icon-background-bg)] text-icon-secondary border-none',
58
- 'hover:text-hover',
59
- 'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text',
60
- 'data-[selected]:hover:text-hover',
61
- 'data-[expanded]:text-hover',
62
- ].join(' '),
63
-
64
- link: [
65
- 'bg-transparent text-link-primary border-none font-normal px-0 h-auto',
66
- 'hover:bg-transparent hover:text-link-primary-hover',
67
- 'disabled:text-text-secondary',
68
- ].join(' '),
69
-
70
- dropdown: [
71
- 'border-2 border-solid bg-transparent',
72
- 'text-[var(--color-button-dropdown-fg)] border-[var(--color-button-dropdown-border)]',
73
- 'hover:bg-transparent hover:text-hover hover:border-hover',
74
- 'data-[expanded]:bg-transparent data-[expanded]:text-hover data-[expanded]:border-hover',
75
- 'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text data-[selected]:border-transparent',
76
- 'data-[selected]:hover:text-hover',
77
- ].join(' '),
78
-
79
- pagination: [
80
- 'border-2 border-solid bg-transparent',
81
- 'text-[var(--color-button-pagination-fg)] border-[var(--color-button-pagination-border)]',
82
- 'hover:bg-transparent hover:text-hover hover:border-hover',
83
- 'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text data-[selected]:border-transparent',
84
- ].join(' '),
85
- };
46
+ type Style = Record<string, unknown>;
47
+
48
+ const HOVER = 'var(--color-hover)';
49
+ const SELECTED_BG = 'var(--color-selected-control-bg)';
50
+ const SELECTED_FG = 'var(--color-selected-control-text)';
51
+
52
+ /**
53
+ * Resting look, and the looks that replace it while selected or expanded.
54
+ * Merged in that order, so a key set in two of them resolves to the later —
55
+ * which is why `selected` and `expanded` are objects rather than `hoverStyle`
56
+ * entries that would fight the resting value.
57
+ */
58
+ const VARIANT: Record<string, { rest: Style; selected?: Style; expanded?: Style }> = {
59
+ plain: {
60
+ rest: { backgroundColor: 'transparent', hoverStyle: { backgroundColor: 'transparent' } },
61
+ },
62
+
63
+ icon_secondary: {
64
+ rest: {
65
+ backgroundColor: 'transparent',
66
+ color: 'var(--color-icon-secondary)',
67
+ hoverStyle: { color: HOVER },
68
+ },
69
+ selected: { backgroundColor: SELECTED_BG, color: SELECTED_FG, hoverStyle: { color: HOVER } },
70
+ expanded: { color: HOVER },
71
+ },
72
+
73
+ icon_background: {
74
+ rest: {
75
+ backgroundColor: 'var(--color-button-icon-background-bg)',
76
+ color: 'var(--color-icon-secondary)',
77
+ hoverStyle: { color: HOVER },
78
+ },
79
+ selected: { backgroundColor: SELECTED_BG, color: SELECTED_FG, hoverStyle: { color: HOVER } },
80
+ expanded: { color: HOVER },
81
+ },
86
82
 
87
- // SVG icon size classes per size variant
88
- const ICON_SIZE_CLASSES: Record<string, string> = {
89
- '2xs': '[&_svg]:size-5',
90
- '2xs_alt': '[&_svg]:size-3',
91
- md: '[&_svg]:size-5',
83
+ link: {
84
+ rest: {
85
+ backgroundColor: 'transparent',
86
+ color: 'var(--color-link-primary)',
87
+ fontWeight: '400',
88
+ paddingHorizontal: 0,
89
+ height: 'auto',
90
+ hoverStyle: { backgroundColor: 'transparent', color: 'var(--color-link-primary-hover)' },
91
+ disabledStyle: { color: 'var(--color-text-secondary)' },
92
+ },
93
+ },
94
+
95
+ dropdown: {
96
+ rest: {
97
+ borderWidth: 2,
98
+ borderStyle: 'solid',
99
+ backgroundColor: 'transparent',
100
+ color: 'var(--color-button-dropdown-fg)',
101
+ borderColor: 'var(--color-button-dropdown-border)',
102
+ hoverStyle: { backgroundColor: 'transparent', color: HOVER, borderColor: HOVER },
103
+ },
104
+ selected: { backgroundColor: SELECTED_BG, color: SELECTED_FG, borderColor: 'transparent', hoverStyle: { color: HOVER } },
105
+ expanded: { backgroundColor: 'transparent', color: HOVER, borderColor: HOVER },
106
+ },
107
+
108
+ pagination: {
109
+ rest: {
110
+ borderWidth: 2,
111
+ borderStyle: 'solid',
112
+ backgroundColor: 'transparent',
113
+ color: 'var(--color-button-pagination-fg)',
114
+ borderColor: 'var(--color-button-pagination-border)',
115
+ hoverStyle: { backgroundColor: 'transparent', color: HOVER, borderColor: HOVER },
116
+ },
117
+ selected: { backgroundColor: SELECTED_BG, color: SELECTED_FG, borderColor: 'transparent' },
118
+ },
92
119
  };
93
120
 
94
- // ---------------------------------------------------------------------------
95
- // Types
96
- // ---------------------------------------------------------------------------
121
+ /** Descendant `svg` size per control size, published as a custom property. */
122
+ const ICON_SIZE: Record<string, number> = { '2xs': 20, '2xs_alt': 12, md: 20 };
97
123
 
98
- type Variant = 'plain' | 'icon_secondary' | 'icon_background' | 'link' | 'dropdown' | 'pagination';
124
+ type Variant = keyof typeof VARIANT;
99
125
  type Size = '2xs' | '2xs_alt' | 'md';
100
126
 
101
127
  export interface IconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
128
+
102
129
  /** Visual variant */
103
130
  variant?: Variant;
131
+
104
132
  /** Icon-button size */
105
133
  size?: Size;
134
+
106
135
  /** Show a loading spinner (disables button) */
107
136
  loading?: boolean;
137
+
108
138
  /** Render a skeleton placeholder instead of the button */
109
139
  loadingSkeleton?: boolean;
110
- /** Maps to data-expanded for popover trigger styling */
140
+
141
+ /** Also emitted as data-expanded for consumer styling */
111
142
  expanded?: boolean;
112
- /** Maps to data-selected for toggle styling */
143
+
144
+ /** Also emitted as data-selected for consumer styling */
113
145
  selected?: boolean;
114
- /** Maps to data-highlighted */
146
+
147
+ /** Also emitted as data-highlighted */
115
148
  highlighted?: boolean;
149
+
116
150
  /** Polymorphic element type (e.g. "div") */
117
151
  as?: React.ElementType;
118
- // Legacy Chakra style-prop shims
119
- boxSize?: number | string;
120
- color?: string;
121
- px?: string | number;
122
- borderRadius?: string;
123
- ml?: number | string;
124
- mr?: number | string;
125
- _hover?: Record<string, string>;
126
- _expanded?: Record<string, string>;
127
152
  }
128
153
 
129
- // ---------------------------------------------------------------------------
130
- // Component
131
- // ---------------------------------------------------------------------------
132
-
133
- const SP = 4;
134
-
135
154
  export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
136
155
  function IconButton(props, ref) {
137
156
  const {
138
- size,
139
- variant = 'plain',
140
- loading,
141
- loadingSkeleton = false,
142
- expanded,
143
- selected,
144
- highlighted,
145
- disabled,
146
- className,
147
- children,
148
- as: _as,
149
- style: styleProp,
150
- // Strip Chakra style props
151
- boxSize: _boxSize,
152
- color: _color,
153
- px: _px,
154
- borderRadius: _borderRadius,
155
- ml: _ml,
156
- mr: _mr,
157
- _hover,
158
- _expanded,
159
- ...rest
157
+ size, variant = 'plain', loading, loadingSkeleton = false,
158
+ expanded, selected, highlighted, disabled, className, children,
159
+ as: asProp, style: styleProp, ...rest
160
160
  } = props;
161
161
 
162
- const shimStyle: React.CSSProperties = { ...styleProp };
163
- if (_boxSize !== undefined) {
164
- const bs = typeof _boxSize === 'number' ? `${_boxSize * SP}px` : _boxSize;
165
- shimStyle.width = bs;
166
- shimStyle.height = bs;
167
- }
168
- if (_color) shimStyle.color = _color;
169
- if (_px !== undefined) {
170
- const v = typeof _px === 'number' ? `${_px * SP}px` : _px;
171
- shimStyle.paddingLeft = v;
172
- shimStyle.paddingRight = v;
173
- }
174
- if (_borderRadius) shimStyle.borderRadius = _borderRadius;
175
- if (_ml !== undefined) shimStyle.marginLeft = typeof _ml === 'number' ? `${_ml * SP}px` : _ml;
176
- if (_mr !== undefined) shimStyle.marginRight = typeof _mr === 'number' ? `${_mr * SP}px` : _mr;
177
- const mergedStyle = Object.keys(shimStyle).length > 0 ? shimStyle : undefined;
178
-
179
- const variantClass = VARIANT_CLASSES[variant] ?? VARIANT_CLASSES.plain;
180
- const iconSizeClass = size ? (ICON_SIZE_CLASSES[size] ?? '') : '';
181
- const combinedClassName = [variantClass, iconSizeClass, className].filter(Boolean).join(' ');
162
+ const look = VARIANT[variant] ?? VARIANT.plain;
163
+ const iconSize = size ? ICON_SIZE[size] : undefined;
182
164
 
183
165
  const button = (
184
166
  <IconButtonFrame
185
- ref={ref as any}
186
- size={size as any}
187
- disabled={(!loadingSkeleton && (loading || disabled)) || undefined}
188
- render={_as ? (React.createElement(_as) as React.ReactElement) : undefined}
189
- className={combinedClassName}
190
- style={mergedStyle}
191
- {...(expanded ? { 'data-expanded': true } : {})}
192
- {...(selected ? { 'data-selected': true } : {})}
193
- {...(highlighted ? { 'data-highlighted': true } : {})}
194
- {...(loadingSkeleton ? { 'data-loading-skeleton': true } : {})}
195
- {...(rest as any)}
167
+ ref={ ref as never }
168
+ size={ size as never }
169
+ disabled={ (!loadingSkeleton && (loading || disabled)) || undefined }
170
+ render={ asProp ? (React.createElement(asProp) as React.ReactElement) : undefined }
171
+ className={ [ 'lux-icon', className ].filter(Boolean).join(' ') }
172
+ style={ { ...styleProp, ...(iconSize ? { '--icon-size': `${ iconSize }px` } : null) } as React.CSSProperties }
173
+ { ...look.rest }
174
+ { ...(selected ? look.selected : null) }
175
+ { ...(expanded ? look.expanded : null) }
176
+ { ...(expanded ? { 'data-expanded': true } : {}) }
177
+ { ...(selected ? { 'data-selected': true } : {}) }
178
+ { ...(highlighted ? { 'data-highlighted': true } : {}) }
179
+ { ...(loadingSkeleton ? { 'data-loading-skeleton': true } : {}) }
180
+ { ...(rest as object) }
196
181
  >
197
- {loading ? (
198
- <span
199
- className="inline-block size-5 animate-spin rounded-full border-2 border-current border-b-transparent border-l-transparent"
182
+ { loading ? (
183
+ <View
184
+ className="lux-spin"
185
+ render={ <span /> }
200
186
  role="status"
201
187
  aria-label="Loading"
188
+ width={ 20 }
189
+ height={ 20 }
190
+ borderRadius={ 1000 }
191
+ borderWidth={ 2 }
192
+ borderColor={ 'currentColor' as never }
193
+ borderBottomColor={ 'transparent' as never }
194
+ borderLeftColor={ 'transparent' as never }
202
195
  />
203
- ) : children}
196
+ ) : children }
204
197
  </IconButtonFrame>
205
198
  );
206
199
 
207
200
  if (loadingSkeleton) {
208
201
  return (
209
- <Skeleton loading={loadingSkeleton} asChild ref={ref as React.ForwardedRef<HTMLDivElement>}>
210
- {button}
202
+ <Skeleton loading={ loadingSkeleton } asChild ref={ ref as React.ForwardedRef<HTMLDivElement> }>
203
+ { button }
211
204
  </Skeleton>
212
205
  );
213
206
  }
package/src/ink.tsx ADDED
@@ -0,0 +1,37 @@
1
+ import { Text } from '@hanzo/gui';
2
+ import { Children, createElement, type ComponentType, type ReactNode } from 'react';
3
+
4
+ /**
5
+ * Give bare text a Text host; pass elements through untouched.
6
+ *
7
+ * gui's primitives are react-native primitives, and a react-native View cannot
8
+ * lay out a raw string. On native it throws; on web it is worse than that,
9
+ * because it renders — as a zero-height box with no error and no type failure.
10
+ * That is the whole failure mode: a build stays green and a label is invisible.
11
+ *
12
+ * So every component here that takes free-form children runs them through this,
13
+ * and a string child then behaves the same on web and on native.
14
+ */
15
+ export const ink = (
16
+ children: ReactNode,
17
+ Wrap: ComponentType<Record<string, unknown>> = Text as never,
18
+ props: Record<string, unknown> = {},
19
+ ): ReactNode => {
20
+ // Nothing to host: hand the children BACK, the same ones. Not a shortcut —
21
+ // `Children.map` rebuilds the list and re-keys it, so an only child comes out
22
+ // as a one-element array. `asChild` resolves its target with
23
+ // `Children.only`, which then finds a list instead of an element and renders
24
+ // nothing at all: every row of the account menu vanished this way while the
25
+ // menu itself opened correctly and no error was logged.
26
+ let hasText = false;
27
+ Children.forEach(children, (child) => {
28
+ if (typeof child === 'string' || typeof child === 'number') hasText = true;
29
+ });
30
+ if (!hasText) return children;
31
+
32
+ return Children.map(children, (child) => (
33
+ typeof child === 'string' || typeof child === 'number' ?
34
+ createElement(Wrap, props, child) :
35
+ child
36
+ ));
37
+ };
@@ -1,15 +1,16 @@
1
+ import { XStack } from '@hanzo/gui';
1
2
  import { debounce } from 'es-toolkit';
2
3
  import * as React from 'react';
3
4
 
4
- import { cn } from './utils';
5
+ import { ink } from './ink';
5
6
 
6
7
  import type { InputProps } from './input';
7
8
 
8
- function getComponentDisplayName(type: string | React.JSXElementConstructor<unknown>): string | undefined {
9
- if (typeof type === 'string') return undefined;
10
- if ('displayName' in type) return type.displayName as string;
11
- return undefined;
12
- }
9
+ // The adornment widths must be applied before the browser paints, or the field
10
+ // renders one frame with default padding and the text visibly jumps out from
11
+ // under the icon. useLayoutEffect does that but has no meaning while rendering
12
+ // on the server, where it only warns.
13
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
13
14
 
14
15
  export interface InputElementProps extends React.HTMLAttributes<HTMLDivElement> {
15
16
  readonly placement?: 'start' | 'end';
@@ -81,7 +82,7 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
81
82
  };
82
83
  }, [ calculateInlinePaddings ]);
83
84
 
84
- React.useEffect(() => {
85
+ useIsomorphicLayoutEffect(() => {
85
86
  calculateInlinePaddings();
86
87
 
87
88
  const resizeHandler = debounce(calculateInlinePaddings, 300);
@@ -110,48 +111,70 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
110
111
  const { className: endClassName, ...endRest } = endElementProps ?? {};
111
112
 
112
113
  return (
113
- <div
114
- ref={ combinedRef }
115
- className={ cn('relative flex w-full items-center', className) }
116
- { ...rest }
114
+ <XStack
115
+ ref={ combinedRef as never }
116
+ position="relative"
117
+ width="100%"
118
+ alignItems="center"
119
+ className={ className }
120
+ { ...(rest as object) }
117
121
  >
118
122
  { startElement && (
119
- <div
120
- ref={ startElementRef }
121
- className={ cn(
122
- 'pointer-events-none absolute inset-y-0 left-0 z-[1] flex items-center text-[var(--chakra-colors-input-element)]',
123
- startClassName,
124
- ) }
125
- { ...startRest }
123
+ <XStack
124
+ ref={ startElementRef as never }
125
+ position="absolute"
126
+ top={ 0 }
127
+ bottom={ 0 }
128
+ left={ 0 }
129
+ zIndex={ 1 }
130
+ alignItems="center"
131
+ pointerEvents="none"
132
+ color={ 'var(--color-icon-secondary)' as never }
133
+ className={ startClassName }
134
+ { ...(startRest as object) }
126
135
  >
127
- { startElement }
128
- </div>
136
+ { ink(startElement) }
137
+ </XStack>
129
138
  ) }
130
139
  { React.Children.map(children, (child: React.ReactElement<InputProps>) => {
131
- if (getComponentDisplayName(child.type) !== 'FieldInput') {
140
+ if (!React.isValidElement(child)) {
132
141
  return child;
133
142
  }
143
+ // The adornments are absolutely positioned over the field, so the
144
+ // control has to be inset by their measured width or the text starts
145
+ // underneath them. Written as inline padding rather than the `ps`/`pe`
146
+ // style props this once carried: those are Chakra's, and Input is now
147
+ // a Tailwind/gui component that spreads unknown props onto the DOM,
148
+ // so they applied nothing and the icon sat on the placeholder.
149
+ const start = startElement ? startOffset ?? inlinePaddings?.start : undefined;
150
+ const end = endElement ? endOffset ?? inlinePaddings?.end : undefined;
151
+ const px = (v: string | number) => typeof v === 'number' ? `${ v }px` : v;
134
152
  return React.cloneElement(child, {
135
- ...(startElement && { ps: startOffset ?? (inlinePaddings?.start ? `${ inlinePaddings.start }px` : undefined) }),
136
- ...(endElement && { pe: endOffset ?? (inlinePaddings?.end ? `${ inlinePaddings.end }px` : undefined) }),
137
- // hide input value and placeholder for the first render
138
- value: inlinePaddings ? child.props.value : undefined,
139
- placeholder: inlinePaddings ? child.props.placeholder : undefined,
153
+ style: {
154
+ ...child.props.style,
155
+ ...(start !== undefined && { paddingInlineStart: px(start) }),
156
+ ...(end !== undefined && { paddingInlineEnd: px(end) }),
157
+ },
140
158
  });
141
159
  }) }
142
160
  { endElement && (
143
- <div
144
- ref={ endElementRef }
145
- className={ cn(
146
- 'pointer-events-none absolute inset-y-0 right-0 z-[1] flex items-center text-[var(--chakra-colors-input-element)]',
147
- endClassName,
148
- ) }
149
- { ...endRest }
161
+ <XStack
162
+ ref={ endElementRef as never }
163
+ position="absolute"
164
+ top={ 0 }
165
+ bottom={ 0 }
166
+ right={ 0 }
167
+ zIndex={ 1 }
168
+ alignItems="center"
169
+ pointerEvents="none"
170
+ color={ 'var(--color-icon-secondary)' as never }
171
+ className={ endClassName }
172
+ { ...(endRest as object) }
150
173
  >
151
- { endElement }
152
- </div>
174
+ { ink(endElement) }
175
+ </XStack>
153
176
  ) }
154
- </div>
177
+ </XStack>
155
178
  );
156
179
  },
157
180
  );
package/src/input.tsx CHANGED
@@ -1,33 +1,15 @@
1
1
  import { Input as GuiInput } from '@hanzo/gui';
2
2
  import React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
- const INPUT_BASE = [
7
- 'w-full appearance-none outline-none transition-colors',
8
- 'bg-[var(--color-input-bg)] text-[var(--color-input-fg)] border-[var(--color-input-border)]',
9
- 'placeholder:text-[var(--color-input-placeholder,theme(colors.gray.400))]',
10
- 'hover:border-[var(--color-input-border-hover,theme(colors.gray.400))]',
11
- 'focus:border-[var(--color-input-border-focus,theme(colors.blue.500))]',
12
- 'focus:placeholder:text-[var(--color-input-placeholder-focus,theme(colors.gray.300))]',
13
- 'disabled:opacity-40 disabled:cursor-not-allowed',
14
- 'read-only:opacity-70',
15
- 'data-[invalid]:border-[var(--color-input-border-invalid,theme(colors.red.500))]',
16
- ].join(' ');
17
-
18
- const INPUT_SIZE_CLASSES: Record<string, string> = {
19
- xs: 'h-6 px-2 text-xs rounded',
20
- sm: 'h-8 px-3 text-sm rounded-md',
21
- md: 'h-10 px-4 text-base rounded-md',
22
- lg: 'h-12 px-4 text-lg rounded-lg',
23
- '2xl': 'h-14 px-4 text-xl rounded-lg',
24
- };
25
-
26
- const INPUT_VARIANT_CLASSES: Record<string, string> = {
27
- outline: 'border border-solid',
28
- filled: 'border-0 bg-[var(--color-input-filled-bg,theme(colors.gray.100))]',
29
- unstyled: 'border-0 bg-transparent p-0 h-auto',
30
- };
4
+ // gui's Input a real `<input>` on web, a real TextInput on native (soft
5
+ // keyboard, `returnKeyType`, autofill, IME) — wearing the Lux size/variant
6
+ // scale. `size` stays the Lux scale and never reaches the primitive, whose
7
+ // `size` is a gui SizeToken.
8
+ //
9
+ // Everything the element itself can express is a prop. The two states that only
10
+ // a selector reaches — `::placeholder`, and `:read-only`, neither of which is a
11
+ // style of the input so much as of something inside or about it — are one CSS
12
+ // rule in tokens.css, keyed off `.lux-control`.
31
13
 
32
14
  type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '2xl';
33
15
  type InputVariant = 'outline' | 'filled' | 'unstyled';
@@ -35,6 +17,7 @@ type InputVariant = 'outline' | 'filled' | 'unstyled';
35
17
  export interface InputProps extends Omit<React.ComponentPropsWithRef<'input'>, 'size'> {
36
18
  readonly size?: InputSize;
37
19
  readonly variant?: InputVariant;
20
+
38
21
  /**
39
22
  * React-Native convention: called with the text value on every change. The gui
40
23
  * Input raises it natively; on web we bridge it off `onChange`, so callers
@@ -43,10 +26,32 @@ export interface InputProps extends Omit<React.ComponentPropsWithRef<'input'>, '
43
26
  readonly onChangeText?: (text: string) => void;
44
27
  }
45
28
 
46
- // gui's Input a real `<input>` on web, a real TextInput on native (soft
47
- // keyboard, `returnKeyType`, autofill, IME) wearing the Lux size/variant
48
- // classes. `size` stays the Lux scale and never reaches the primitive, whose
49
- // `size` is a gui SizeToken.
29
+ export const SIZE: Record<InputSize, Record<string, number>> = {
30
+ xs: { height: 24, paddingHorizontal: 8, fontSize: 12, borderRadius: 4 },
31
+ sm: { height: 32, paddingHorizontal: 12, fontSize: 14, borderRadius: 6 },
32
+ md: { height: 40, paddingHorizontal: 16, fontSize: 16, borderRadius: 6 },
33
+ lg: { height: 48, paddingHorizontal: 16, fontSize: 18, borderRadius: 8 },
34
+ '2xl': { height: 56, paddingHorizontal: 16, fontSize: 20, borderRadius: 8 },
35
+ };
36
+
37
+ export const VARIANT: Record<InputVariant, Record<string, unknown>> = {
38
+ outline: { borderWidth: 1, borderStyle: 'solid' },
39
+ filled: { borderWidth: 0, backgroundColor: 'var(--color-input-filled-bg)' },
40
+ unstyled: { borderWidth: 0, backgroundColor: 'transparent', padding: 0, height: 'auto' },
41
+ };
42
+
43
+ /** The frame every text control shares. */
44
+ export const CONTROL = {
45
+ width: '100%',
46
+ outlineWidth: 0,
47
+ backgroundColor: 'var(--color-input-bg)',
48
+ color: 'var(--color-input-fg)',
49
+ borderColor: 'var(--color-input-border)',
50
+ hoverStyle: { borderColor: 'var(--color-input-border-hover)' },
51
+ focusStyle: { borderColor: 'var(--color-input-border-focus)' },
52
+ disabledStyle: { opacity: 0.4, cursor: 'not-allowed' },
53
+ } as const;
54
+
50
55
  export const Input = React.forwardRef<HTMLInputElement, InputProps>(
51
56
  ({ size = 'md', variant = 'outline', className, onChange, onChangeText, ...rest }, ref) => {
52
57
  const handleChange = React.useCallback(
@@ -56,11 +61,15 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
56
61
  },
57
62
  [ onChange, onChangeText ],
58
63
  );
64
+
59
65
  return (
60
66
  <GuiInput
61
67
  ref={ ref as never }
62
68
  unstyled
63
- className={ cn(INPUT_BASE, INPUT_SIZE_CLASSES[size], INPUT_VARIANT_CLASSES[variant], className) }
69
+ className={ className ? `lux-control ${ className }` : 'lux-control' }
70
+ { ...CONTROL }
71
+ { ...SIZE[size] }
72
+ { ...VARIANT[variant] }
64
73
  { ...(rest as object) }
65
74
  onChange={ handleChange as never }
66
75
  />