@luxfi/ui 7.4.9 → 7.4.11
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 +107 -166
- package/dist/accordion.d.cts +18 -51
- package/dist/accordion.d.ts +18 -51
- package/dist/accordion.js +109 -166
- package/dist/chrome.cjs +193 -379
- package/dist/chrome.js +195 -379
- package/dist/collapsible.cjs +35 -26
- package/dist/collapsible.d.cts +2 -1
- package/dist/collapsible.d.ts +2 -1
- package/dist/collapsible.js +34 -24
- package/dist/dialog.d.cts +1 -1
- package/dist/dialog.d.ts +1 -1
- package/dist/drawer.cjs +92 -115
- package/dist/drawer.d.cts +43 -10
- package/dist/drawer.d.ts +43 -10
- package/dist/drawer.js +94 -115
- package/dist/field.cjs +60 -109
- package/dist/field.d.cts +0 -1
- package/dist/field.d.ts +0 -1
- package/dist/field.js +61 -109
- package/dist/heading.cjs +10 -17
- package/dist/heading.d.cts +0 -2
- package/dist/heading.d.ts +0 -2
- package/dist/heading.js +10 -17
- package/dist/icon-button.cjs +82 -82
- package/dist/icon-button.d.cts +16 -12
- package/dist/icon-button.d.ts +16 -12
- package/dist/icon-button.js +82 -82
- package/dist/index.cjs +1073 -1419
- package/dist/index.d.cts +1 -5
- package/dist/index.d.ts +1 -5
- package/dist/index.js +1075 -1414
- package/dist/input-group.cjs +11 -12
- package/dist/input-group.js +11 -12
- package/dist/input.cjs +27 -27
- package/dist/input.d.cts +21 -1
- package/dist/input.d.ts +21 -1
- package/dist/input.js +25 -28
- package/dist/menu.cjs +160 -271
- package/dist/menu.d.cts +41 -57
- package/dist/menu.d.ts +41 -57
- package/dist/menu.js +161 -251
- package/dist/next.cjs +6 -10
- package/dist/next.js +5 -9
- package/dist/popover.cjs +1 -1
- package/dist/popover.js +1 -1
- package/dist/select.cjs +40 -39
- package/dist/select.d.cts +35 -4
- package/dist/select.d.ts +35 -4
- package/dist/select.js +40 -38
- package/dist/separator.cjs +8 -33
- package/dist/separator.js +8 -33
- package/dist/tabs.cjs +108 -197
- package/dist/tabs.d.cts +16 -60
- package/dist/tabs.d.ts +16 -60
- package/dist/tabs.js +109 -196
- package/dist/textarea.cjs +56 -27
- package/dist/textarea.js +57 -28
- package/dist/toaster.cjs +97 -79
- package/dist/toaster.d.cts +7 -8
- package/dist/toaster.d.ts +7 -8
- package/dist/toaster.js +78 -80
- package/package.json +2 -16
- package/src/accordion.tsx +173 -227
- package/src/collapsible.tsx +55 -52
- package/src/drawer.tsx +123 -115
- package/src/engine.ts +0 -25
- package/src/field.tsx +68 -124
- package/src/heading.tsx +22 -26
- package/src/icon-button.tsx +134 -141
- package/src/ink.tsx +37 -0
- package/src/input-group.tsx +21 -12
- package/src/input.tsx +41 -32
- package/src/menu.tsx +224 -397
- package/src/next.ts +32 -1
- package/src/popover.tsx +1 -1
- package/src/select.tsx +80 -59
- package/src/separator.tsx +13 -34
- package/src/tabs.tsx +166 -296
- package/src/textarea.tsx +19 -29
- package/src/toaster.tsx +118 -86
- package/tokens.css +61 -0
package/src/tabs.tsx
CHANGED
|
@@ -1,20 +1,103 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Tabs, Text, useTabsContext } from '@hanzo/gui';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
import { ink } from './ink';
|
|
5
|
+
|
|
6
|
+
// The Lux tab skin, on the gui engine.
|
|
7
|
+
//
|
|
8
|
+
// gui's Tabs already owns everything Radix was imported for: roving focus,
|
|
9
|
+
// `activationMode`, the `data-state="active" | "inactive"` contract, and an
|
|
10
|
+
// `activeStyle` prop that applies while a tab is selected. So this file is a
|
|
11
|
+
// SKIN — a variant table and three wrappers — and nothing else.
|
|
12
|
+
//
|
|
13
|
+
// The styles are inline props rather than `styled()` wrappers, because
|
|
14
|
+
// `Tabs.Tab` is `TabFrame.styleable(...)`: an HOC, not a frame. `styled()` over
|
|
15
|
+
// one does not compile style props to classes — they leak onto the element as
|
|
16
|
+
// lowercase HTML attributes (`backgroundcolor="…"`) and nothing is styled.
|
|
17
|
+
// Extending `Tabs.Tab` would look right, typecheck, and render unstyled.
|
|
18
|
+
//
|
|
19
|
+
// What went with Radix: a `useTabsContext` that read `variant` and `size` off a
|
|
20
|
+
// `data-*` attribute of the nearest ancestor DOM node, inside a `useEffect`. It
|
|
21
|
+
// could only answer AFTER paint, so every trigger rendered one frame in the
|
|
22
|
+
// wrong variant and then corrected itself, and during SSR it could not answer at
|
|
23
|
+
// all. Variant and size are props of the root, so they travel as React context.
|
|
9
24
|
|
|
10
25
|
type TabsVariant = 'solid' | 'secondary' | 'segmented' | 'unstyled';
|
|
11
26
|
type TabsSize = 'sm' | 'md' | 'free';
|
|
12
27
|
|
|
28
|
+
type Style = Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
/** Variant and size, declared on the root and read by every trigger. */
|
|
31
|
+
const Shape = React.createContext<{ variant: TabsVariant; size: TabsSize; fitted: boolean }>({
|
|
32
|
+
variant: 'solid',
|
|
33
|
+
size: 'md',
|
|
34
|
+
fitted: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const SIZE: Record<TabsSize, Style> = {
|
|
38
|
+
sm: { height: 32, minWidth: 32, paddingVertical: 4, paddingHorizontal: 12, fontSize: 14 },
|
|
39
|
+
md: { height: 40, minWidth: 40, paddingVertical: 8, paddingHorizontal: 16, fontSize: 16 },
|
|
40
|
+
free: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const SELECTED_BG = 'var(--color-selected-control-bg)';
|
|
44
|
+
const SELECTED_FG = 'var(--color-selected-control-text)';
|
|
45
|
+
|
|
46
|
+
// Resting look and selected look, per variant, as two plain objects that get
|
|
47
|
+
// MERGED here rather than handed to gui's `activeStyle`.
|
|
48
|
+
//
|
|
49
|
+
// `activeStyle` is gui's own prop for "while this tab is selected", and it does
|
|
50
|
+
// not survive contact with a variant that also styles the resting state: the
|
|
51
|
+
// trigger spreads `activeStyle` and only THEN spreads the caller's remaining
|
|
52
|
+
// props, so any key set in both — `backgroundColor` for solid, `borderColor`
|
|
53
|
+
// for secondary — resolves to the resting value even while selected. Measured:
|
|
54
|
+
// the selected tab's computed background stayed `rgba(0, 0, 0, 0)`, identical
|
|
55
|
+
// to an unselected one, with every behavioural test still passing.
|
|
56
|
+
//
|
|
57
|
+
// Selection is readable instead (`useTabsContext`), so the style is a function
|
|
58
|
+
// of state and one object reaches gui with nothing left to override it.
|
|
59
|
+
const VARIANT: Record<TabsVariant, { rest: Style; active: Style }> = {
|
|
60
|
+
solid: {
|
|
61
|
+
rest: {
|
|
62
|
+
fontWeight: '600',
|
|
63
|
+
borderRadius: 6,
|
|
64
|
+
color: 'var(--color-tabs-solid-fg)',
|
|
65
|
+
hoverStyle: { color: 'var(--color-hover)' },
|
|
66
|
+
},
|
|
67
|
+
active: { backgroundColor: SELECTED_BG, color: SELECTED_FG },
|
|
68
|
+
},
|
|
69
|
+
secondary: {
|
|
70
|
+
rest: {
|
|
71
|
+
fontWeight: '500',
|
|
72
|
+
borderRadius: 6,
|
|
73
|
+
borderWidth: 2,
|
|
74
|
+
borderColor: 'var(--color-tabs-secondary-border)',
|
|
75
|
+
color: 'var(--color-tabs-secondary-fg)',
|
|
76
|
+
hoverStyle: { color: 'var(--color-hover)', borderColor: 'var(--color-hover)' },
|
|
77
|
+
},
|
|
78
|
+
active: { backgroundColor: SELECTED_BG, color: SELECTED_FG, borderColor: 'transparent' },
|
|
79
|
+
},
|
|
80
|
+
segmented: {
|
|
81
|
+
rest: {
|
|
82
|
+
borderWidth: 2,
|
|
83
|
+
borderColor: SELECTED_BG,
|
|
84
|
+
color: 'var(--color-text-primary)',
|
|
85
|
+
hoverStyle: { color: 'var(--color-hover)' },
|
|
86
|
+
},
|
|
87
|
+
active: { backgroundColor: SELECTED_BG, borderColor: SELECTED_BG, color: SELECTED_FG },
|
|
88
|
+
},
|
|
89
|
+
unstyled: { rest: {}, active: {} },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const LIST_HEIGHT: Record<TabsSize, number | undefined> = { sm: 32, md: 40, free: undefined };
|
|
93
|
+
|
|
13
94
|
// ---------------------------------------------------------------------------
|
|
14
|
-
//
|
|
95
|
+
// Root
|
|
15
96
|
// ---------------------------------------------------------------------------
|
|
16
97
|
|
|
17
|
-
|
|
98
|
+
// `size` is narrowed away from gui's own token-valued `size` on purpose: a Lux
|
|
99
|
+
// tab bar has three sizes, not the whole size ramp.
|
|
100
|
+
export interface TabsProps extends Omit<React.ComponentProps<typeof Tabs>, 'onValueChange' | 'size'> {
|
|
18
101
|
|
|
19
102
|
/** Visual variant. Default: "solid". */
|
|
20
103
|
variant?: TabsVariant;
|
|
@@ -25,256 +108,99 @@ export interface TabsProps extends Omit<React.ComponentPropsWithoutRef<typeof Ra
|
|
|
25
108
|
/** Stretch triggers to fill the list width. */
|
|
26
109
|
fitted?: boolean;
|
|
27
110
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
* renders when active). Kept so call-sites don't break at compile time.
|
|
31
|
-
*/
|
|
32
|
-
lazyMount?: boolean;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @deprecated Chakra-compat. Radix unmounts inactive content by default.
|
|
36
|
-
* Kept so call-sites don't break at compile time.
|
|
37
|
-
*/
|
|
38
|
-
unmountOnExit?: boolean;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Chakra-compatible value-change handler.
|
|
42
|
-
* Accepts both `(details: { value: string }) => void` (Chakra convention)
|
|
43
|
-
* and `(value: string) => void` (Radix convention).
|
|
44
|
-
*/
|
|
45
|
-
onValueChange?: ((details: { value: string }) => void) | ((value: string) => void);
|
|
111
|
+
/** Fires with the newly selected value. */
|
|
112
|
+
onValueChange?: (details: { value: string }) => void;
|
|
46
113
|
}
|
|
47
114
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
export const TabsRoot = ({
|
|
116
|
+
variant = 'solid',
|
|
117
|
+
size = 'md',
|
|
118
|
+
fitted = false,
|
|
119
|
+
onValueChange,
|
|
120
|
+
...rest
|
|
121
|
+
}: TabsProps): React.ReactElement => {
|
|
122
|
+
const shape = React.useMemo(() => ({ variant, size, fitted }), [ variant, size, fitted ]);
|
|
53
123
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
unmountOnExit: _unmountOnExit,
|
|
62
|
-
onValueChange,
|
|
63
|
-
className,
|
|
64
|
-
...rest
|
|
65
|
-
} = props;
|
|
66
|
-
|
|
67
|
-
// Bridge Chakra-style `onValueChange({value})` to Radix `onValueChange(value)`
|
|
68
|
-
const handleValueChange = React.useCallback(
|
|
69
|
-
(value: string) => {
|
|
70
|
-
if (!onValueChange) return;
|
|
71
|
-
// Detect which signature the caller expects by trying the Chakra shape.
|
|
72
|
-
// Both signatures are valid TS, so we call with the object form since
|
|
73
|
-
// all existing consumers use `({ value })`.
|
|
74
|
-
(onValueChange as (details: { value: string }) => void)({ value });
|
|
75
|
-
},
|
|
76
|
-
[ onValueChange ],
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<RadixTabs.Root
|
|
81
|
-
ref={ ref }
|
|
82
|
-
data-variant={ variant }
|
|
83
|
-
data-size={ size }
|
|
84
|
-
data-fitted={ fitted ? '' : undefined }
|
|
85
|
-
className={ cn(
|
|
86
|
-
'relative',
|
|
87
|
-
ROOT_SIZE_CLASSES[size],
|
|
88
|
-
className,
|
|
89
|
-
) }
|
|
90
|
-
onValueChange={ onValueChange ? handleValueChange : undefined }
|
|
124
|
+
return (
|
|
125
|
+
<Shape.Provider value={ shape }>
|
|
126
|
+
<Tabs
|
|
127
|
+
unstyled
|
|
128
|
+
position="relative"
|
|
129
|
+
orientation="horizontal"
|
|
130
|
+
onValueChange={ onValueChange ? (value: string) => onValueChange({ value }) : undefined }
|
|
91
131
|
{ ...rest }
|
|
92
132
|
/>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
133
|
+
</Shape.Provider>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
96
136
|
|
|
97
137
|
// ---------------------------------------------------------------------------
|
|
98
|
-
//
|
|
138
|
+
// List
|
|
99
139
|
// ---------------------------------------------------------------------------
|
|
100
140
|
|
|
101
|
-
export
|
|
102
|
-
// Radix pass-through
|
|
103
|
-
asChild?: boolean;
|
|
104
|
-
loop?: boolean;
|
|
105
|
-
// Legacy Chakra style-prop shims
|
|
106
|
-
flexWrap?: string;
|
|
107
|
-
alignItems?: string;
|
|
108
|
-
whiteSpace?: string;
|
|
109
|
-
bgColor?: string;
|
|
110
|
-
marginBottom?: number | string;
|
|
111
|
-
mx?: string | Record<string, string>;
|
|
112
|
-
px?: string | Record<string, string>;
|
|
113
|
-
w?: string | Record<string, string>;
|
|
114
|
-
overflowX?: string | Record<string, string>;
|
|
115
|
-
overscrollBehaviorX?: string;
|
|
116
|
-
css?: Record<string, unknown>;
|
|
117
|
-
position?: string;
|
|
118
|
-
boxShadow?: string | Record<string, string>;
|
|
119
|
-
top?: number | string;
|
|
120
|
-
zIndex?: string | Record<string, string>;
|
|
121
|
-
}
|
|
141
|
+
export type TabsListProps = React.ComponentProps<typeof Tabs.List>;
|
|
122
142
|
|
|
123
|
-
export const TabsList = React.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<RadixTabs.List
|
|
140
|
-
ref={ ref }
|
|
141
|
-
asChild={ asChild }
|
|
142
|
-
loop={ loop }
|
|
143
|
-
className={ cn(
|
|
144
|
-
'inline-flex w-full relative isolate flex-row',
|
|
145
|
-
'min-h-[var(--tabs-height,2.5rem)]',
|
|
146
|
-
className,
|
|
147
|
-
) }
|
|
148
|
-
style={ styleProp }
|
|
149
|
-
{ ...rest }
|
|
150
|
-
/>
|
|
151
|
-
);
|
|
152
|
-
},
|
|
153
|
-
);
|
|
143
|
+
export const TabsList = (props: TabsListProps): React.ReactElement => {
|
|
144
|
+
const { size } = React.useContext(Shape);
|
|
145
|
+
return (
|
|
146
|
+
<Tabs.List
|
|
147
|
+
unstyled
|
|
148
|
+
width="100%"
|
|
149
|
+
position="relative"
|
|
150
|
+
flexDirection="row"
|
|
151
|
+
backgroundColor="transparent"
|
|
152
|
+
minHeight={ LIST_HEIGHT[size] }
|
|
153
|
+
{ ...props }
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
154
157
|
|
|
155
158
|
// ---------------------------------------------------------------------------
|
|
156
|
-
//
|
|
159
|
+
// Trigger
|
|
157
160
|
// ---------------------------------------------------------------------------
|
|
158
161
|
|
|
159
|
-
export
|
|
160
|
-
// Legacy Chakra style-prop shims
|
|
161
|
-
scrollSnapAlign?: string;
|
|
162
|
-
flexShrink?: number;
|
|
163
|
-
bgColor?: string | Record<string, string>;
|
|
164
|
-
w?: string;
|
|
165
|
-
py?: string;
|
|
166
|
-
borderRadius?: string;
|
|
167
|
-
fontWeight?: string;
|
|
168
|
-
color?: string;
|
|
169
|
-
_hover?: Record<string, string>;
|
|
170
|
-
position?: string;
|
|
171
|
-
top?: string;
|
|
172
|
-
left?: string;
|
|
173
|
-
visibility?: string;
|
|
174
|
-
}
|
|
162
|
+
export type TabsTriggerProps = React.ComponentProps<typeof Tabs.Tab>;
|
|
175
163
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
' disabled:cursor-not-allowed disabled:opacity-50';
|
|
164
|
+
export const TabsTrigger = ({ children, ...props }: TabsTriggerProps): React.ReactElement => {
|
|
165
|
+
const { variant, size, fitted } = React.useContext(Shape);
|
|
166
|
+
const { rest, active } = VARIANT[variant];
|
|
167
|
+
const selected = useTabsContext().value === props.value;
|
|
181
168
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
' hover:text-hover hover:border-hover',
|
|
202
|
-
segmented:
|
|
203
|
-
'bg-transparent' +
|
|
204
|
-
' text-text-primary' +
|
|
205
|
-
' border-2 border-solid border-selected-control-bg' +
|
|
206
|
-
' hover:text-hover' +
|
|
207
|
-
' data-[state=active]:text-selected-control-text data-[state=active]:bg-selected-control-bg' +
|
|
208
|
-
' data-[state=active]:border-selected-control-bg' +
|
|
209
|
-
' data-[state=active]:hover:text-selected-control-text',
|
|
210
|
-
unstyled: '',
|
|
169
|
+
return (
|
|
170
|
+
<Tabs.Tab
|
|
171
|
+
unstyled
|
|
172
|
+
flexDirection="row"
|
|
173
|
+
alignItems="center"
|
|
174
|
+
justifyContent="center"
|
|
175
|
+
position="relative"
|
|
176
|
+
cursor="pointer"
|
|
177
|
+
gap={ 8 }
|
|
178
|
+
borderWidth={ 0 }
|
|
179
|
+
{ ...SIZE[size] }
|
|
180
|
+
{ ...rest }
|
|
181
|
+
{ ...(selected ? active : null) }
|
|
182
|
+
{ ...(fitted ? { flex: 1 } : null) }
|
|
183
|
+
{ ...props }
|
|
184
|
+
>
|
|
185
|
+
{ ink(children, undefined, { fontSize: 'inherit', color: 'inherit', fontWeight: 'inherit' }) }
|
|
186
|
+
</Tabs.Tab>
|
|
187
|
+
);
|
|
211
188
|
};
|
|
212
189
|
|
|
213
|
-
export const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
|
|
214
|
-
function TabsTrigger(props, ref) {
|
|
215
|
-
const {
|
|
216
|
-
className,
|
|
217
|
-
// Strip Chakra style props
|
|
218
|
-
scrollSnapAlign: _scrollSnapAlign, flexShrink: _flexShrink, bgColor: _bgColor,
|
|
219
|
-
w: _w, py: _py, borderRadius: _borderRadius, fontWeight: _fontWeight,
|
|
220
|
-
color: _color, _hover, position: _position, top: _top, left: _left,
|
|
221
|
-
visibility: _visibility,
|
|
222
|
-
...rest
|
|
223
|
-
} = props;
|
|
224
|
-
|
|
225
|
-
// Read variant / size from the closest TabsRoot via DOM data attributes.
|
|
226
|
-
// We could use React context, but reading from data-* keeps the API surface
|
|
227
|
-
// identical to Chakra where recipe tokens cascade automatically.
|
|
228
|
-
const internalRef = React.useRef<HTMLButtonElement>(null);
|
|
229
|
-
const mergedRef = useMergedRef(ref, internalRef);
|
|
230
|
-
const { variant, size, fitted } = useTabsContext(internalRef);
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<RadixTabs.Trigger
|
|
234
|
-
ref={ mergedRef }
|
|
235
|
-
className={ cn(
|
|
236
|
-
'group',
|
|
237
|
-
TRIGGER_BASE,
|
|
238
|
-
TRIGGER_SIZE_CLASSES[size],
|
|
239
|
-
TRIGGER_VARIANT_CLASSES[variant],
|
|
240
|
-
fitted && 'flex-1 text-center justify-center',
|
|
241
|
-
className,
|
|
242
|
-
) }
|
|
243
|
-
{ ...rest }
|
|
244
|
-
/>
|
|
245
|
-
);
|
|
246
|
-
},
|
|
247
|
-
);
|
|
248
|
-
|
|
249
190
|
// ---------------------------------------------------------------------------
|
|
250
|
-
//
|
|
191
|
+
// Content
|
|
251
192
|
// ---------------------------------------------------------------------------
|
|
252
193
|
|
|
253
|
-
export
|
|
254
|
-
// Legacy Chakra style-prop shims
|
|
255
|
-
padding?: number | string;
|
|
256
|
-
}
|
|
194
|
+
export type TabsContentProps = React.ComponentProps<typeof Tabs.Content>;
|
|
257
195
|
|
|
258
|
-
export const TabsContent = React.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<RadixTabs.Content
|
|
264
|
-
ref={ ref }
|
|
265
|
-
className={ cn(
|
|
266
|
-
'w-full pt-[var(--tabs-content-padding,1.5rem)]',
|
|
267
|
-
'focus-visible:outline-2 focus-visible:outline-offset-[-2px]',
|
|
268
|
-
className,
|
|
269
|
-
) }
|
|
270
|
-
{ ...rest }
|
|
271
|
-
/>
|
|
272
|
-
);
|
|
273
|
-
},
|
|
196
|
+
export const TabsContent = ({ children, ...props }: TabsContentProps): React.ReactElement => (
|
|
197
|
+
<Tabs.Content unstyled width="100%" paddingTop={ 24 } backgroundColor="transparent" { ...props }>
|
|
198
|
+
{ ink(children) }
|
|
199
|
+
</Tabs.Content>
|
|
274
200
|
);
|
|
275
201
|
|
|
276
202
|
// ---------------------------------------------------------------------------
|
|
277
|
-
//
|
|
203
|
+
// Counter
|
|
278
204
|
// ---------------------------------------------------------------------------
|
|
279
205
|
|
|
280
206
|
export interface TabsCounterProps {
|
|
@@ -283,72 +209,16 @@ export interface TabsCounterProps {
|
|
|
283
209
|
|
|
284
210
|
const COUNTER_OVERLOAD = 50;
|
|
285
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The count beside a tab label. Zero is deliberately dimmer than a real count —
|
|
214
|
+
* "nothing here" should read quieter than "three things here".
|
|
215
|
+
*/
|
|
286
216
|
export const TabsCounter = ({ count }: TabsCounterProps): React.ReactElement | null => {
|
|
287
|
-
if (count === undefined || count === null)
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
217
|
+
if (count === undefined || count === null) return null;
|
|
290
218
|
|
|
291
219
|
return (
|
|
292
|
-
<
|
|
293
|
-
className={ cn(
|
|
294
|
-
'group-hover:text-inherit',
|
|
295
|
-
count > 0 ?
|
|
296
|
-
'text-text-secondary' :
|
|
297
|
-
'text-[rgba(16,17,18,0.24)] dark:text-[rgba(255,255,255,0.24)]',
|
|
298
|
-
) }
|
|
299
|
-
>
|
|
220
|
+
<Text color={ (count > 0 ? 'var(--color-text-secondary)' : 'var(--color-text-disabled)') as never }>
|
|
300
221
|
{ count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count }
|
|
301
|
-
</
|
|
222
|
+
</Text>
|
|
302
223
|
);
|
|
303
224
|
};
|
|
304
|
-
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
|
-
// Helpers
|
|
307
|
-
// ---------------------------------------------------------------------------
|
|
308
|
-
|
|
309
|
-
/** Read variant / size / fitted from the nearest ancestor [data-variant] element. */
|
|
310
|
-
function useTabsContext(triggerRef: React.RefObject<HTMLButtonElement | null>): {
|
|
311
|
-
variant: TabsVariant;
|
|
312
|
-
size: TabsSize;
|
|
313
|
-
fitted: boolean;
|
|
314
|
-
} {
|
|
315
|
-
const [ ctx, setCtx ] = React.useState<{
|
|
316
|
-
variant: TabsVariant;
|
|
317
|
-
size: TabsSize;
|
|
318
|
-
fitted: boolean;
|
|
319
|
-
}>({ variant: 'solid', size: 'md', fitted: false });
|
|
320
|
-
|
|
321
|
-
React.useEffect(() => {
|
|
322
|
-
const el = triggerRef.current;
|
|
323
|
-
if (!el) return;
|
|
324
|
-
const root = el.closest('[data-variant]');
|
|
325
|
-
if (!root) return;
|
|
326
|
-
setCtx({
|
|
327
|
-
variant: (root.getAttribute('data-variant') as TabsVariant) ?? 'solid',
|
|
328
|
-
size: (root.getAttribute('data-size') as TabsSize) ?? 'md',
|
|
329
|
-
fitted: root.hasAttribute('data-fitted'),
|
|
330
|
-
});
|
|
331
|
-
}, [ triggerRef ]);
|
|
332
|
-
|
|
333
|
-
return ctx;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** Merge multiple refs into one callback ref. */
|
|
337
|
-
function useMergedRef<T>(
|
|
338
|
-
...refs: Array<React.Ref<T> | undefined>
|
|
339
|
-
): React.RefCallback<T> {
|
|
340
|
-
return React.useCallback(
|
|
341
|
-
(instance: T | null) => {
|
|
342
|
-
for (const ref of refs) {
|
|
343
|
-
if (!ref) continue;
|
|
344
|
-
if (typeof ref === 'function') {
|
|
345
|
-
ref(instance);
|
|
346
|
-
} else {
|
|
347
|
-
(ref as React.MutableRefObject<T | null>).current = instance;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
},
|
|
351
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
352
|
-
refs,
|
|
353
|
-
);
|
|
354
|
-
}
|
package/src/textarea.tsx
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
import { TextArea as GuiTextArea } from '@hanzo/gui';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { CONTROL, VARIANT } from './input';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 TEXTAREA_SIZE_CLASSES: Record<string, string> = {
|
|
19
|
-
xs: 'px-2 py-1 text-xs rounded',
|
|
20
|
-
sm: 'px-3 py-2 text-sm rounded-md',
|
|
21
|
-
md: 'px-4 py-2 text-base rounded-md',
|
|
22
|
-
lg: 'px-4 py-3 text-lg rounded-lg',
|
|
23
|
-
'2xl': 'px-4 py-3 text-xl rounded-lg',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const TEXTAREA_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',
|
|
30
|
-
};
|
|
6
|
+
// gui's TextArea — multiline TextInput on native, `<textarea>` on web. The
|
|
7
|
+
// frame, the variants and the placeholder rule are the Input's: a textarea is a
|
|
8
|
+
// text control that wraps, not a second design.
|
|
31
9
|
|
|
32
10
|
type TextareaSize = 'xs' | 'sm' | 'md' | 'lg' | '2xl';
|
|
33
11
|
type TextareaVariant = 'outline' | 'filled' | 'unstyled';
|
|
@@ -35,12 +13,20 @@ type TextareaVariant = 'outline' | 'filled' | 'unstyled';
|
|
|
35
13
|
export interface TextareaProps extends Omit<React.ComponentPropsWithRef<'textarea'>, 'size'> {
|
|
36
14
|
readonly size?: TextareaSize;
|
|
37
15
|
readonly variant?: TextareaVariant;
|
|
16
|
+
|
|
38
17
|
/** Tamagui / React-Native convention — bridged to native `onChange` (see Input). */
|
|
39
18
|
readonly onChangeText?: (text: string) => void;
|
|
40
19
|
}
|
|
41
20
|
|
|
42
|
-
|
|
43
|
-
|
|
21
|
+
/** Same ramp as Input, but a textarea is sized by padding, not by height. */
|
|
22
|
+
const SIZE: Record<TextareaSize, Record<string, number>> = {
|
|
23
|
+
xs: { paddingHorizontal: 8, paddingVertical: 4, fontSize: 12, borderRadius: 4 },
|
|
24
|
+
sm: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 14, borderRadius: 6 },
|
|
25
|
+
md: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 16, borderRadius: 6 },
|
|
26
|
+
lg: { paddingHorizontal: 16, paddingVertical: 12, fontSize: 18, borderRadius: 8 },
|
|
27
|
+
'2xl': { paddingHorizontal: 16, paddingVertical: 12, fontSize: 20, borderRadius: 8 },
|
|
28
|
+
};
|
|
29
|
+
|
|
44
30
|
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
45
31
|
({ size = 'md', variant = 'outline', className, onChange, onChangeText, ...rest }, ref) => {
|
|
46
32
|
const handleChange = React.useCallback(
|
|
@@ -50,11 +36,15 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
50
36
|
},
|
|
51
37
|
[ onChange, onChangeText ],
|
|
52
38
|
);
|
|
39
|
+
|
|
53
40
|
return (
|
|
54
41
|
<GuiTextArea
|
|
55
42
|
ref={ ref as never }
|
|
56
43
|
unstyled
|
|
57
|
-
className={
|
|
44
|
+
className={ className ? `lux-control ${ className }` : 'lux-control' }
|
|
45
|
+
{ ...CONTROL }
|
|
46
|
+
{ ...SIZE[size] }
|
|
47
|
+
{ ...VARIANT[variant] }
|
|
58
48
|
{ ...(rest as object) }
|
|
59
49
|
onChange={ handleChange as never }
|
|
60
50
|
/>
|