@luxfi/ui 7.4.10 → 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/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/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/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/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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,
|
|
32
|
-
const Tag = level ?
|
|
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
|
-
|
|
43
|
-
|
|
37
|
+
fontFamily="$heading"
|
|
38
|
+
color={ 'var(--color-heading)' as never }
|
|
39
|
+
{ ...(level ? RAMP[level] : null) }
|
|
44
40
|
{ ...(rest as object) }
|
|
45
41
|
/>
|
|
46
42
|
);
|
package/src/icon-button.tsx
CHANGED
|
@@ -3,9 +3,18 @@ import * as React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { Skeleton } from './skeleton';
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
140
|
+
|
|
141
|
+
/** Also emitted as data-expanded for consumer styling */
|
|
111
142
|
expanded?: boolean;
|
|
112
|
-
|
|
143
|
+
|
|
144
|
+
/** Also emitted as data-selected for consumer styling */
|
|
113
145
|
selected?: boolean;
|
|
114
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
163
|
-
|
|
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
|
|
186
|
-
size={size as
|
|
187
|
-
disabled={(!loadingSkeleton && (loading || disabled)) || undefined}
|
|
188
|
-
render={
|
|
189
|
-
className={
|
|
190
|
-
style={
|
|
191
|
-
{...
|
|
192
|
-
{...(selected ?
|
|
193
|
-
{...(
|
|
194
|
-
{...(
|
|
195
|
-
{...(
|
|
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
|
-
<
|
|
199
|
-
className="
|
|
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
|
+
};
|
package/src/input-group.tsx
CHANGED
|
@@ -5,11 +5,11 @@ import { cn } from './utils';
|
|
|
5
5
|
|
|
6
6
|
import type { InputProps } from './input';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// The adornment widths must be applied before the browser paints, or the field
|
|
9
|
+
// renders one frame with default padding and the text visibly jumps out from
|
|
10
|
+
// under the icon. useLayoutEffect does that but has no meaning while rendering
|
|
11
|
+
// on the server, where it only warns.
|
|
12
|
+
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
|
13
13
|
|
|
14
14
|
export interface InputElementProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
15
15
|
readonly placement?: 'start' | 'end';
|
|
@@ -81,7 +81,7 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
|
|
81
81
|
};
|
|
82
82
|
}, [ calculateInlinePaddings ]);
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
useIsomorphicLayoutEffect(() => {
|
|
85
85
|
calculateInlinePaddings();
|
|
86
86
|
|
|
87
87
|
const resizeHandler = debounce(calculateInlinePaddings, 300);
|
|
@@ -128,15 +128,24 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
|
|
128
128
|
</div>
|
|
129
129
|
) }
|
|
130
130
|
{ React.Children.map(children, (child: React.ReactElement<InputProps>) => {
|
|
131
|
-
if (
|
|
131
|
+
if (!React.isValidElement(child)) {
|
|
132
132
|
return child;
|
|
133
133
|
}
|
|
134
|
+
// The adornments are absolutely positioned over the field, so the
|
|
135
|
+
// control has to be inset by their measured width or the text starts
|
|
136
|
+
// underneath them. Written as inline padding rather than the `ps`/`pe`
|
|
137
|
+
// style props this once carried: those are Chakra's, and Input is now
|
|
138
|
+
// a Tailwind/gui component that spreads unknown props onto the DOM,
|
|
139
|
+
// so they applied nothing and the icon sat on the placeholder.
|
|
140
|
+
const start = startElement ? startOffset ?? inlinePaddings?.start : undefined;
|
|
141
|
+
const end = endElement ? endOffset ?? inlinePaddings?.end : undefined;
|
|
142
|
+
const px = (v: string | number) => typeof v === 'number' ? `${ v }px` : v;
|
|
134
143
|
return React.cloneElement(child, {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
style: {
|
|
145
|
+
...child.props.style,
|
|
146
|
+
...(start !== undefined && { paddingInlineStart: px(start) }),
|
|
147
|
+
...(end !== undefined && { paddingInlineEnd: px(end) }),
|
|
148
|
+
},
|
|
140
149
|
});
|
|
141
150
|
}) }
|
|
142
151
|
{ endElement && (
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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={
|
|
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
|
/>
|