@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.
- 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/alert.cjs +68 -46
- package/dist/alert.js +69 -46
- package/dist/avatar.cjs +49 -46
- package/dist/avatar.js +52 -48
- package/dist/badge.cjs +82 -49
- package/dist/badge.js +83 -50
- package/dist/checkbox.cjs +42 -75
- package/dist/checkbox.js +43 -76
- package/dist/chrome.cjs +196 -386
- package/dist/chrome.js +198 -386
- package/dist/close-button.cjs +3 -7
- package/dist/close-button.js +3 -7
- 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.cjs +3 -7
- package/dist/dialog.d.cts +1 -1
- package/dist/dialog.d.ts +1 -1
- package/dist/dialog.js +3 -7
- package/dist/drawer.cjs +95 -122
- package/dist/drawer.d.cts +43 -10
- package/dist/drawer.d.ts +43 -10
- package/dist/drawer.js +97 -122
- package/dist/empty-state.cjs +13 -13
- package/dist/empty-state.js +13 -13
- package/dist/expiration-selector.cjs +58 -63
- package/dist/expiration-selector.js +58 -63
- 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/greeks-display.cjs +48 -39
- package/dist/greeks-display.js +48 -39
- 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 +1812 -2105
- package/dist/index.d.cts +1 -5
- package/dist/index.d.ts +1 -5
- package/dist/index.js +1814 -2100
- 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/option-chain.cjs +89 -67
- package/dist/option-chain.js +89 -67
- package/dist/option-position.cjs +88 -85
- package/dist/option-position.js +88 -85
- package/dist/pnl-diagram.cjs +20 -20
- package/dist/pnl-diagram.js +20 -20
- package/dist/popover.cjs +4 -8
- package/dist/popover.js +4 -8
- package/dist/progress.cjs +15 -22
- package/dist/progress.d.cts +6 -6
- package/dist/progress.d.ts +6 -6
- package/dist/progress.js +15 -22
- package/dist/radio.cjs +35 -67
- package/dist/radio.d.cts +18 -18
- package/dist/radio.d.ts +18 -18
- package/dist/radio.js +36 -68
- 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/slider.cjs +33 -49
- package/dist/slider.js +34 -50
- package/dist/switch.cjs +48 -55
- package/dist/switch.js +49 -56
- 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/tag.cjs +112 -69
- package/dist/tag.js +113 -69
- 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/dist/tooltip.cjs +28 -17
- package/dist/tooltip.js +30 -18
- package/package.json +2 -16
- package/src/accordion.tsx +173 -227
- package/src/alert.tsx +78 -45
- package/src/avatar.tsx +54 -37
- package/src/badge.tsx +65 -44
- package/src/checkbox.tsx +70 -89
- package/src/close-button.tsx +3 -8
- package/src/collapsible.tsx +55 -52
- package/src/drawer.tsx +123 -115
- package/src/empty-state.tsx +21 -17
- package/src/expiration-selector.tsx +84 -66
- package/src/field.tsx +68 -124
- package/src/greeks-display.tsx +59 -51
- package/src/heading.tsx +22 -26
- package/src/icon-button.tsx +134 -141
- package/src/ink.tsx +37 -0
- package/src/input-group.tsx +59 -36
- package/src/input.tsx +41 -32
- package/src/menu.tsx +224 -397
- package/src/option-chain.tsx +154 -104
- package/src/option-position.tsx +136 -114
- package/src/pin-input.tsx +37 -29
- package/src/pnl-diagram.tsx +36 -28
- package/src/popover.tsx +1 -1
- package/src/progress.tsx +17 -21
- package/src/radio.tsx +56 -75
- package/src/select.tsx +80 -59
- package/src/separator.tsx +13 -34
- package/src/slider.tsx +43 -44
- package/src/strategy-builder.tsx +245 -156
- package/src/switch.tsx +63 -64
- package/src/tabs.tsx +166 -296
- package/src/tag.tsx +89 -51
- package/src/textarea.tsx +19 -29
- package/src/toaster.tsx +118 -86
- package/src/tooltip.tsx +21 -13
- package/tokens.css +79 -0
package/src/tag.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { Text, XStack } from '@hanzo/gui';
|
|
1
2
|
import * as React from 'react';
|
|
2
3
|
|
|
3
|
-
import { cn } from './utils';
|
|
4
|
-
|
|
5
4
|
import { CloseButton } from './close-button';
|
|
6
5
|
import { Skeleton } from './skeleton';
|
|
7
6
|
import { Tooltip } from './tooltip';
|
|
@@ -21,29 +20,29 @@ function TruncatedTextTooltip({ label, children }: { readonly label: React.React
|
|
|
21
20
|
type TagVariant = 'subtle' | 'clickable' | 'filter' | 'select';
|
|
22
21
|
type TagSize = 'sm' | 'md' | 'lg';
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
select: 'cursor-pointer bg-[var(--color-tag-select-bg)] text-[var(--color-tag-select-fg)] hover:text-[var(--color-hover)] hover:opacity-76',
|
|
23
|
+
/** The chip — background, cursor, and the hover opacity `clickable`/`select` get. */
|
|
24
|
+
const TAG_VARIANT: Record<TagVariant, { backgroundColor: string; cursor?: string; hoverStyle?: Record<string, unknown> }> = {
|
|
25
|
+
subtle: { backgroundColor: 'var(--color-tag-subtle-bg)' },
|
|
26
|
+
clickable: { backgroundColor: 'var(--color-tag-clickable-bg)', cursor: 'pointer', hoverStyle: { opacity: 0.76 } },
|
|
27
|
+
filter: { backgroundColor: 'var(--color-tag-filter-bg)' },
|
|
28
|
+
select: { backgroundColor: 'var(--color-tag-select-bg)', cursor: 'pointer', hoverStyle: { opacity: 0.76 } },
|
|
31
29
|
};
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/** The label inside it — same four variants, the colour half (`filter` has none). */
|
|
32
|
+
const TAG_VARIANT_FG: Record<TagVariant, string | undefined> = {
|
|
33
|
+
subtle: 'var(--color-tag-subtle-fg)',
|
|
34
|
+
clickable: 'var(--color-tag-clickable-fg)',
|
|
35
|
+
filter: undefined,
|
|
36
|
+
select: 'var(--color-tag-select-fg)',
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
].join(' ');
|
|
39
|
+
const TAG_SIZE: Record<TagSize, Record<string, number>> = {
|
|
40
|
+
sm: { paddingHorizontal: 4, paddingVertical: 2, minHeight: 20, gap: 4 },
|
|
41
|
+
md: { paddingHorizontal: 4, paddingVertical: 2, minHeight: 24, gap: 4 },
|
|
42
|
+
lg: { paddingHorizontal: 6, paddingVertical: 6, minHeight: 32, minWidth: 32, gap: 4 },
|
|
43
|
+
};
|
|
45
44
|
|
|
46
|
-
const
|
|
45
|
+
const TAG_SIZE_FONT: Record<TagSize, number> = { sm: 12, md: 14, lg: 14 };
|
|
47
46
|
|
|
48
47
|
export interface TagProps extends Omit<React.ComponentPropsWithRef<'span'>, 'size'> {
|
|
49
48
|
readonly variant?: TagVariant;
|
|
@@ -80,14 +79,39 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
|
|
80
79
|
...rest
|
|
81
80
|
} = props;
|
|
82
81
|
|
|
82
|
+
const resolvedVariant = variant ?? 'subtle';
|
|
83
|
+
const resolvedSize = size ?? 'md';
|
|
84
|
+
const isSelected = Boolean(selected) && !loading;
|
|
85
|
+
|
|
86
|
+
// `selected` overrides the variant's own colours; `disabled` only dims —
|
|
87
|
+
// neither is known until render, so resolve the final tokens here rather
|
|
88
|
+
// than lean on a class-merge order that no longer exists.
|
|
89
|
+
const backgroundColor = isSelected ? 'var(--color-tag-select-selected-bg)' : TAG_VARIANT[resolvedVariant].backgroundColor;
|
|
90
|
+
const color = isSelected ? 'var(--color-tag-select-selected-fg)' : TAG_VARIANT_FG[resolvedVariant];
|
|
91
|
+
const hoverStyle = isSelected ? { opacity: 0.76 } : TAG_VARIANT[resolvedVariant].hoverStyle;
|
|
92
|
+
// Unselected `select` rows preview the selected look on hover; once
|
|
93
|
+
// selected the hover colour equals the resting colour, i.e. no change.
|
|
94
|
+
const textHoverColor = !isSelected && resolvedVariant === 'select' ? 'var(--color-hover)' : undefined;
|
|
95
|
+
|
|
83
96
|
const labelElement = label ? (
|
|
84
|
-
<
|
|
97
|
+
<Text fontSize={ TAG_SIZE_FONT[resolvedSize] } fontWeight="500" color={ 'var(--color-text-secondary)' as never }>
|
|
98
|
+
{ label }:{ nbsp }
|
|
99
|
+
</Text>
|
|
85
100
|
) : null;
|
|
86
101
|
|
|
87
102
|
const labelSpan = (
|
|
88
|
-
|
|
103
|
+
// `ellipsis` is Text's own built-in single-line-truncation variant —
|
|
104
|
+
// the same rule Tailwind's `line-clamp-1`/`text-ellipsis` drew, through
|
|
105
|
+
// the component's variants rather than three separate style props.
|
|
106
|
+
<Text
|
|
107
|
+
ellipsis
|
|
108
|
+
fontWeight="500"
|
|
109
|
+
fontSize={ TAG_SIZE_FONT[resolvedSize] }
|
|
110
|
+
color={ color as never }
|
|
111
|
+
hoverStyle={ textHoverColor ? { color: textHoverColor as never } : undefined }
|
|
112
|
+
>
|
|
89
113
|
{ labelElement }{ children }
|
|
90
|
-
</
|
|
114
|
+
</Text>
|
|
91
115
|
);
|
|
92
116
|
|
|
93
117
|
const contentElement = truncated ? (
|
|
@@ -98,43 +122,57 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
|
|
98
122
|
|
|
99
123
|
return (
|
|
100
124
|
<Skeleton loading={ loading } asChild>
|
|
101
|
-
<
|
|
102
|
-
ref={ ref }
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{
|
|
125
|
+
<XStack
|
|
126
|
+
ref={ ref as never }
|
|
127
|
+
render={ <span/> }
|
|
128
|
+
display="inline-flex"
|
|
129
|
+
alignItems="center"
|
|
130
|
+
verticalAlign="top"
|
|
131
|
+
maxWidth="100%"
|
|
132
|
+
userSelect="text"
|
|
133
|
+
{ ...TAG_SIZE[resolvedSize] }
|
|
134
|
+
backgroundColor={ backgroundColor as never }
|
|
135
|
+
hoverStyle={ hoverStyle }
|
|
136
|
+
focusVisibleStyle={{
|
|
137
|
+
outlineWidth: 2,
|
|
138
|
+
outlineOffset: 2,
|
|
139
|
+
outlineColor: 'var(--color-gray-500)' as never,
|
|
140
|
+
outlineStyle: 'solid',
|
|
141
|
+
}}
|
|
142
|
+
// One cursor, decided once: disabled outranks the variant's.
|
|
143
|
+
cursor={ disabled ? 'not-allowed' : TAG_VARIANT[resolvedVariant].cursor }
|
|
144
|
+
{ ...(disabled ? { opacity: 0.4, pointerEvents: 'none' } : {}) }
|
|
145
|
+
{ ...(isSelected && { 'data-selected': true }) }
|
|
112
146
|
{ ...(disabled && { 'data-disabled': true }) }
|
|
113
|
-
{
|
|
147
|
+
className={ className }
|
|
148
|
+
{ ...(rest as object) }
|
|
114
149
|
>
|
|
115
150
|
{ startElement && (
|
|
116
|
-
<
|
|
151
|
+
<XStack flexShrink={ 0 } alignItems="center" justifyContent="center">
|
|
117
152
|
{ startElement }
|
|
118
|
-
</
|
|
153
|
+
</XStack>
|
|
119
154
|
) }
|
|
120
155
|
{ contentElement }
|
|
121
|
-
{ endElement && (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
{ endElement && (() => {
|
|
157
|
+
const { className: endClassName, ...endElementRest } = endElementProps ?? {};
|
|
158
|
+
return (
|
|
159
|
+
<XStack
|
|
160
|
+
{ ...(endElementRest as object) }
|
|
161
|
+
flexShrink={ 0 }
|
|
162
|
+
alignItems="center"
|
|
163
|
+
justifyContent="center"
|
|
164
|
+
className={ endClassName }
|
|
165
|
+
>
|
|
166
|
+
{ endElement }
|
|
167
|
+
</XStack>
|
|
168
|
+
);
|
|
169
|
+
})() }
|
|
132
170
|
{ closable && (
|
|
133
|
-
<
|
|
171
|
+
<XStack flexShrink={ 0 } alignItems="center" justifyContent="center">
|
|
134
172
|
<CloseButton onClick={ onClose }/>
|
|
135
|
-
</
|
|
173
|
+
</XStack>
|
|
136
174
|
) }
|
|
137
|
-
</
|
|
175
|
+
</XStack>
|
|
138
176
|
</Skeleton>
|
|
139
177
|
);
|
|
140
178
|
},
|
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
|
/>
|
package/src/toaster.tsx
CHANGED
|
@@ -1,89 +1,73 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import { Text, Toast, ToastProvider, ToastViewport, XStack, YStack } from '@hanzo/gui';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
// Toasts, on the gui engine.
|
|
7
|
+
//
|
|
8
|
+
// The published surface is imperative — `toaster.error({ title })` from
|
|
9
|
+
// anywhere, including code that is not a component — while gui's toast is a
|
|
10
|
+
// hook (`useToastController`) that only a descendant of a provider can call.
|
|
11
|
+
// The bridge is a module-level store: callers push onto it, `<Toaster/>`
|
|
12
|
+
// subscribes with `useSyncExternalStore` and renders one gui `Toast` per entry.
|
|
13
|
+
// That is the whole adaptation, and it is why `sonner` is gone rather than
|
|
14
|
+
// wrapped — a second toast implementation would have been a second way to do
|
|
15
|
+
// the same thing.
|
|
5
16
|
|
|
6
17
|
const SECOND = 1_000;
|
|
7
18
|
const DURATION = 10 * SECOND;
|
|
8
19
|
|
|
20
|
+
type ToastType = 'info' | 'warning' | 'success' | 'error' | 'loading';
|
|
21
|
+
|
|
9
22
|
interface ToastOptions {
|
|
10
23
|
readonly id?: string | number;
|
|
11
24
|
readonly title?: string;
|
|
12
25
|
readonly description?: string;
|
|
13
|
-
readonly type?:
|
|
26
|
+
readonly type?: ToastType;
|
|
14
27
|
readonly duration?: number;
|
|
15
28
|
readonly action?: { readonly label: string; readonly onClick: () => void };
|
|
16
29
|
readonly onStatusChange?: (details: { readonly status: string }) => void;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
id: opts.id,
|
|
22
|
-
description: opts.description,
|
|
23
|
-
duration: opts.duration,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
if (opts.action) {
|
|
27
|
-
external.action = {
|
|
28
|
-
label: opts.action.label,
|
|
29
|
-
onClick: opts.action.onClick,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (opts.onStatusChange) {
|
|
34
|
-
const cb = opts.onStatusChange;
|
|
35
|
-
external.onDismiss = () => cb({ status: 'unmounted' });
|
|
36
|
-
external.onAutoClose = () => cb({ status: 'unmounted' });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return external;
|
|
32
|
+
interface Entry extends ToastOptions {
|
|
33
|
+
readonly id: string | number;
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
const external = mapOptions(opts);
|
|
44
|
-
const title = opts.title ?? '';
|
|
45
|
-
|
|
46
|
-
switch (opts.type) {
|
|
47
|
-
case 'success':
|
|
48
|
-
return toast.success(title, external);
|
|
49
|
-
case 'error':
|
|
50
|
-
return toast.error(title, external);
|
|
51
|
-
case 'warning':
|
|
52
|
-
return toast.warning(title, external);
|
|
53
|
-
case 'loading':
|
|
54
|
-
return toast.loading(title, external);
|
|
55
|
-
case 'info':
|
|
56
|
-
default:
|
|
57
|
-
return toast.info(title, external);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
36
|
+
// ── the store ──────────────────────────────────────────────────────────────
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
38
|
+
let entries: ReadonlyArray<Entry> = [];
|
|
39
|
+
const listeners = new Set<() => void>();
|
|
40
|
+
let counter = 0;
|
|
41
|
+
|
|
42
|
+
const emit = () => listeners.forEach((l) => l());
|
|
43
|
+
|
|
44
|
+
const subscribe = (l: () => void) => {
|
|
45
|
+
listeners.add(l);
|
|
46
|
+
return () => { listeners.delete(l); };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** `useSyncExternalStore` compares by identity, so the array is replaced, never mutated. */
|
|
50
|
+
const snapshot = () => entries;
|
|
51
|
+
|
|
52
|
+
const remove = (id: string | number): void => {
|
|
53
|
+
const found = entries.find((e) => e.id === id);
|
|
54
|
+
entries = entries.filter((e) => e.id !== id);
|
|
55
|
+
emit();
|
|
56
|
+
found?.onStatusChange?.({ status: 'unmounted' });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const create = (opts: ToastOptions): string | number => {
|
|
60
|
+
const id = opts.id ?? `toast-${ ++counter }`;
|
|
61
|
+
const entry: Entry = { ...opts, id };
|
|
62
|
+
entries = [ ...entries.filter((e) => e.id !== id), entry ];
|
|
63
|
+
emit();
|
|
64
|
+
return id;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const update = (id: string | number, opts: ToastOptions): void => {
|
|
68
|
+
entries = entries.map((e) => (e.id === id ? { ...e, ...opts, id } : e));
|
|
69
|
+
emit();
|
|
70
|
+
};
|
|
87
71
|
|
|
88
72
|
export const toaster = {
|
|
89
73
|
create,
|
|
@@ -91,27 +75,75 @@ export const toaster = {
|
|
|
91
75
|
error: (opts: ToastOptions) => create({ ...opts, type: 'error' }),
|
|
92
76
|
loading: (opts: ToastOptions) => create({ ...opts, type: 'loading' }),
|
|
93
77
|
update,
|
|
94
|
-
remove
|
|
78
|
+
remove,
|
|
95
79
|
} as const;
|
|
96
80
|
|
|
81
|
+
// ── presentation ───────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/** Border colour carries the severity; the surface stays the popover surface. */
|
|
84
|
+
const EDGE: Record<ToastType, string> = {
|
|
85
|
+
success: 'var(--color-status-good)',
|
|
86
|
+
error: 'var(--color-status-bad)',
|
|
87
|
+
warning: 'var(--color-status-warn)',
|
|
88
|
+
loading: 'var(--color-border-divider)',
|
|
89
|
+
info: 'var(--color-border-divider)',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const One = ({ entry }: { entry: Entry }) => (
|
|
93
|
+
<Toast
|
|
94
|
+
key={ entry.id }
|
|
95
|
+
duration={ entry.duration ?? DURATION }
|
|
96
|
+
onOpenChange={ (open: boolean) => { if (!open) remove(entry.id); } }
|
|
97
|
+
unstyled
|
|
98
|
+
flexDirection="column"
|
|
99
|
+
gap={ 4 }
|
|
100
|
+
padding={ 12 }
|
|
101
|
+
borderRadius={ 8 }
|
|
102
|
+
borderWidth={ 1 }
|
|
103
|
+
borderColor={ EDGE[entry.type ?? 'info'] as never }
|
|
104
|
+
backgroundColor={ 'var(--color-popover-bg)' as never }
|
|
105
|
+
transition="quick"
|
|
106
|
+
enterStyle={{ opacity: 0, y: -8 }}
|
|
107
|
+
exitStyle={{ opacity: 0, y: -8 }}
|
|
108
|
+
>
|
|
109
|
+
{ entry.title ? (
|
|
110
|
+
<Toast.Title unstyled fontSize={ 14 } fontWeight="600" color={ 'var(--color-text-primary)' as never }>
|
|
111
|
+
{ entry.title }
|
|
112
|
+
</Toast.Title>
|
|
113
|
+
) : null }
|
|
114
|
+
{ entry.description ? (
|
|
115
|
+
<Toast.Description unstyled fontSize={ 13 } color={ 'var(--color-text-secondary)' as never }>
|
|
116
|
+
{ entry.description }
|
|
117
|
+
</Toast.Description>
|
|
118
|
+
) : null }
|
|
119
|
+
{ /* Toast.Action is the accessible affordance and takes no styling of its
|
|
120
|
+
own; the look belongs to the Text it wraps. */ }
|
|
121
|
+
{ entry.action ? (
|
|
122
|
+
<XStack marginTop={ 4 }>
|
|
123
|
+
<Toast.Action altText={ entry.action.label } onPress={ entry.action.onClick }>
|
|
124
|
+
<Text
|
|
125
|
+
cursor="pointer"
|
|
126
|
+
fontSize={ 13 }
|
|
127
|
+
fontWeight="500"
|
|
128
|
+
color={ 'var(--color-link-primary)' as never }
|
|
129
|
+
>
|
|
130
|
+
{ entry.action.label }
|
|
131
|
+
</Text>
|
|
132
|
+
</Toast.Action>
|
|
133
|
+
</XStack>
|
|
134
|
+
) : null }
|
|
135
|
+
</Toast>
|
|
136
|
+
);
|
|
137
|
+
|
|
97
138
|
export const Toaster: React.FC = () => {
|
|
139
|
+
const list = React.useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
140
|
+
|
|
98
141
|
return (
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
mobileOffset={{ top: 16, right: 16, bottom: 16, left: 16 }}
|
|
106
|
-
toastOptions={{
|
|
107
|
-
className: 'font-sans',
|
|
108
|
-
style: {
|
|
109
|
-
fontFamily: 'var(--font-sans, "Geist", sans-serif)',
|
|
110
|
-
color: 'var(--color-text-primary)',
|
|
111
|
-
backgroundColor: 'var(--color-bg-primary)',
|
|
112
|
-
borderColor: 'var(--color-border-divider)',
|
|
113
|
-
},
|
|
114
|
-
}}
|
|
115
|
-
/>
|
|
142
|
+
<ToastProvider>
|
|
143
|
+
<YStack>
|
|
144
|
+
{ list.map((entry) => <One key={ entry.id } entry={ entry }/>) }
|
|
145
|
+
</YStack>
|
|
146
|
+
<ToastViewport position="absolute" top={ 12 } right={ 12 }/>
|
|
147
|
+
</ToastProvider>
|
|
116
148
|
);
|
|
117
149
|
};
|
package/src/tooltip.tsx
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
import { useClickAway } from '@uidotdev/usehooks';
|
|
6
6
|
import * as React from 'react';
|
|
7
7
|
|
|
8
|
+
import { ink } from './ink';
|
|
8
9
|
import { useIsTouch } from './use-narrow';
|
|
9
|
-
import { cn } from './utils';
|
|
10
10
|
|
|
11
11
|
/** Map placement string to @hanzogui Popper placement prop. */
|
|
12
12
|
function mapPlacement(p: string | undefined): string | undefined {
|
|
@@ -161,14 +161,21 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
|
|
161
161
|
<GuiTooltip.Content
|
|
162
162
|
ref={ ref }
|
|
163
163
|
unstyled
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
zIndex={ 9999 }
|
|
165
|
+
overflow="hidden"
|
|
166
|
+
borderRadius={ 8 }
|
|
167
|
+
paddingHorizontal={ 12 }
|
|
168
|
+
paddingVertical={ 8 }
|
|
169
|
+
maxWidth={ isPopover ? 384 : 320 }
|
|
170
|
+
backgroundColor={ (isPopover ? 'var(--color-popover-bg)' : 'var(--color-tooltip-bg)') as never }
|
|
171
|
+
transition="quick"
|
|
172
|
+
enterStyle={{ opacity: 0, scale: 0.95 }}
|
|
173
|
+
exitStyle={{ opacity: 0, scale: 0.95 }}
|
|
174
|
+
{ ...(isPopover ? {
|
|
175
|
+
borderWidth: 1,
|
|
176
|
+
borderColor: 'var(--color-border-divider)' as never,
|
|
177
|
+
shadowColor: 'var(--shadow-popover)' as never,
|
|
178
|
+
} : {}) }
|
|
172
179
|
onClick={ interactive ? handleContentClick : undefined }
|
|
173
180
|
{ ...(selected ? { 'data-selected': true } : {}) }
|
|
174
181
|
{ ...contentProps as any }
|
|
@@ -176,12 +183,13 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
|
|
176
183
|
{ showArrow && (
|
|
177
184
|
<GuiTooltip.Arrow
|
|
178
185
|
unstyled
|
|
179
|
-
|
|
180
|
-
isPopover ? 'fill-[var(--color-popover-bg)]' : 'fill-[var(--color-tooltip-bg)]',
|
|
181
|
-
) }
|
|
186
|
+
backgroundColor={ (isPopover ? 'var(--color-popover-bg)' : 'var(--color-tooltip-bg)') as never }
|
|
182
187
|
/>
|
|
183
188
|
) }
|
|
184
|
-
{ content
|
|
189
|
+
{ ink(content, undefined, {
|
|
190
|
+
fontSize: 14,
|
|
191
|
+
color: (isPopover ? 'var(--color-text-primary)' : 'var(--color-tooltip-fg)') as never,
|
|
192
|
+
}) }
|
|
185
193
|
</GuiTooltip.Content>
|
|
186
194
|
</GuiTooltip>
|
|
187
195
|
</GuiTooltipGroup>
|