@castui/cast-ui 4.7.0 → 4.8.0
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/README.md +6 -0
- package/dist/components/Accordion/Accordion.d.ts +80 -0
- package/dist/components/Accordion/Accordion.js +157 -0
- package/dist/components/Accordion/index.d.ts +1 -0
- package/dist/components/Accordion/index.js +6 -0
- package/dist/components/AppBar/AppBar.d.ts +47 -0
- package/dist/components/AppBar/AppBar.js +47 -0
- package/dist/components/AppBar/index.d.ts +1 -0
- package/dist/components/AppBar/index.js +5 -0
- package/dist/components/Autocomplete/Autocomplete.d.ts +70 -0
- package/dist/components/Autocomplete/Autocomplete.js +249 -0
- package/dist/components/Autocomplete/index.d.ts +1 -0
- package/dist/components/Autocomplete/index.js +5 -0
- package/dist/components/Backdrop/Backdrop.d.ts +32 -0
- package/dist/components/Backdrop/Backdrop.js +74 -0
- package/dist/components/Backdrop/index.d.ts +1 -0
- package/dist/components/Backdrop/index.js +5 -0
- package/dist/components/BottomSheet/BottomSheet.d.ts +50 -0
- package/dist/components/BottomSheet/BottomSheet.js +159 -0
- package/dist/components/BottomSheet/index.d.ts +1 -0
- package/dist/components/BottomSheet/index.js +6 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +63 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.js +143 -0
- package/dist/components/Breadcrumbs/index.d.ts +1 -0
- package/dist/components/Breadcrumbs/index.js +6 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +42 -0
- package/dist/components/CodeBlock/CodeBlock.js +110 -0
- package/dist/components/CodeBlock/index.d.ts +1 -0
- package/dist/components/CodeBlock/index.js +5 -0
- package/dist/components/Drawer/Drawer.d.ts +51 -0
- package/dist/components/Drawer/Drawer.js +168 -0
- package/dist/components/Drawer/index.d.ts +1 -0
- package/dist/components/Drawer/index.js +6 -0
- package/dist/components/Link/Link.d.ts +51 -0
- package/dist/components/Link/Link.js +73 -0
- package/dist/components/Link/index.d.ts +1 -0
- package/dist/components/Link/index.js +5 -0
- package/dist/components/Menu/Menu.d.ts +91 -0
- package/dist/components/Menu/Menu.js +211 -0
- package/dist/components/Menu/index.d.ts +1 -0
- package/dist/components/Menu/index.js +9 -0
- package/dist/components/Slider/Slider.d.ts +47 -0
- package/dist/components/Slider/Slider.js +132 -0
- package/dist/components/Slider/index.d.ts +1 -0
- package/dist/components/Slider/index.js +5 -0
- package/dist/components/SpeedDial/SpeedDial.d.ts +72 -0
- package/dist/components/SpeedDial/SpeedDial.js +189 -0
- package/dist/components/SpeedDial/index.d.ts +1 -0
- package/dist/components/SpeedDial/index.js +6 -0
- package/dist/components/Table/Table.d.ts +74 -0
- package/dist/components/Table/Table.js +176 -0
- package/dist/components/Table/index.d.ts +1 -0
- package/dist/components/Table/index.js +9 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.ts +69 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.js +158 -0
- package/dist/components/ToggleButtonGroup/index.d.ts +1 -0
- package/dist/components/ToggleButtonGroup/index.js +6 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +49 -2
- package/dist/theme/ThemeContext.d.ts +8 -1
- package/dist/theme/ThemeContext.js +7 -4
- package/dist/theme/applyCastTheme.d.ts +75 -0
- package/dist/theme/applyCastTheme.js +95 -0
- package/dist/theme/index.d.ts +2 -1
- package/dist/theme/index.js +3 -1
- package/dist/theme/themes.js +177 -0
- package/dist/theme/types.d.ts +177 -0
- package/dist/tokens/colors.d.ts +44 -0
- package/dist/tokens/colors.js +47 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +4 -1
- package/package.json +2 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Autocomplete = Autocomplete;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
/**
|
|
6
|
+
* Autocomplete — a text field that filters a list of options as you type.
|
|
7
|
+
*
|
|
8
|
+
* Maps 1:1 to the Figma <Autocomplete> component:
|
|
9
|
+
* size → small | default | large
|
|
10
|
+
* state → default | hover | focus | error | disabled
|
|
11
|
+
* option state → default | hover | selected | disabled
|
|
12
|
+
*
|
|
13
|
+
* Autocomplete is the Select combobox specialised for client-side filtering. Its
|
|
14
|
+
* field is an Input, so it reuses the input tokens; its options are
|
|
15
|
+
* value-selection rows, so it reuses the select tokens and the
|
|
16
|
+
* scheme.select.option colours. It introduces no new tokens. Type to filter the
|
|
17
|
+
* options by label, press one to select it, or clear the field.
|
|
18
|
+
*
|
|
19
|
+
* value is controlled with value/onValueChange (null = nothing selected) or
|
|
20
|
+
* uncontrolled with defaultValue. Pass a filterOptions function to change how
|
|
21
|
+
* matching works. Fonts are consumer-loaded.
|
|
22
|
+
*/
|
|
23
|
+
const react_1 = require("react");
|
|
24
|
+
const react_native_1 = require("react-native");
|
|
25
|
+
const theme_1 = require("../../theme");
|
|
26
|
+
const tokens_1 = require("../../tokens");
|
|
27
|
+
const Icon_1 = require("../Icon");
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Constants
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const CONTENT_MAX_HEIGHT = 240;
|
|
32
|
+
const LABEL_SCALE = {
|
|
33
|
+
small: 'lg',
|
|
34
|
+
default: 'md',
|
|
35
|
+
large: 'lg',
|
|
36
|
+
};
|
|
37
|
+
const BODY_SCALE = {
|
|
38
|
+
small: 'sm',
|
|
39
|
+
default: 'md',
|
|
40
|
+
large: 'lg',
|
|
41
|
+
};
|
|
42
|
+
const SHADOW_WEB = {
|
|
43
|
+
boxShadow: '0px 2px 4px -2px rgba(0,0,0,0.05), 0px 4px 6px -1px rgba(0,0,0,0.07)',
|
|
44
|
+
};
|
|
45
|
+
const SHADOW_NATIVE = {
|
|
46
|
+
shadowColor: '#000000',
|
|
47
|
+
shadowOffset: { width: 0, height: 4 },
|
|
48
|
+
shadowOpacity: 0.07,
|
|
49
|
+
shadowRadius: 6,
|
|
50
|
+
elevation: 4,
|
|
51
|
+
};
|
|
52
|
+
const defaultFilter = (options, query) => options.filter((o) => o.label.toLowerCase().includes(query));
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Option row
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function OptionRow({ option, size, selected, onSelect, }) {
|
|
57
|
+
const { components, scheme } = (0, theme_1.useTheme)();
|
|
58
|
+
const tokens = components.select.option;
|
|
59
|
+
const opt = scheme.select.option;
|
|
60
|
+
const [isHovered, setIsHovered] = (0, react_1.useState)(false);
|
|
61
|
+
const disabled = Boolean(option.disabled);
|
|
62
|
+
const labelTokens = tokens_1.label[BODY_SCALE[size]];
|
|
63
|
+
const bodyTokens = tokens_1.body[BODY_SCALE[size]];
|
|
64
|
+
const colors = disabled
|
|
65
|
+
? opt.disabled
|
|
66
|
+
: selected && isHovered
|
|
67
|
+
? opt.selectedHover
|
|
68
|
+
: selected
|
|
69
|
+
? opt.selected
|
|
70
|
+
: isHovered
|
|
71
|
+
? opt.hover
|
|
72
|
+
: opt.default;
|
|
73
|
+
const resolvedIcon = typeof option.icon === 'string' ? ((0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: option.icon, size: size, color: colors.fg })) : (option.icon);
|
|
74
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => {
|
|
75
|
+
if (!disabled)
|
|
76
|
+
onSelect(option.value);
|
|
77
|
+
}, onHoverIn: () => setIsHovered(true), onHoverOut: () => setIsHovered(false), disabled: disabled, accessibilityRole: "menuitem", accessibilityState: { selected, disabled }, style: {
|
|
78
|
+
flexDirection: 'row',
|
|
79
|
+
alignItems: 'flex-start',
|
|
80
|
+
gap: tokens.gap,
|
|
81
|
+
paddingHorizontal: tokens.paddingX,
|
|
82
|
+
paddingVertical: tokens.paddingY,
|
|
83
|
+
borderRadius: tokens.borderRadius,
|
|
84
|
+
backgroundColor: colors.bg,
|
|
85
|
+
}, children: [resolvedIcon ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { accessibilityElementsHidden: true, importantForAccessibility: "no", style: { width: tokens_1.iconSize[size], height: tokens_1.iconSize[size] }, children: resolvedIcon })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flex: 1 }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { selectable: false, style: {
|
|
86
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
87
|
+
fontWeight: tokens_1.fontWeight.medium,
|
|
88
|
+
fontSize: labelTokens.fontSize,
|
|
89
|
+
lineHeight: labelTokens.lineHeight,
|
|
90
|
+
letterSpacing: labelTokens.letterSpacing,
|
|
91
|
+
color: colors.fg,
|
|
92
|
+
}, children: option.label }), option.description ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, selectable: false, style: {
|
|
93
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
94
|
+
fontWeight: tokens_1.fontWeight.regular,
|
|
95
|
+
fontSize: bodyTokens.fontSize,
|
|
96
|
+
lineHeight: bodyTokens.lineHeight,
|
|
97
|
+
letterSpacing: bodyTokens.letterSpacing,
|
|
98
|
+
color: disabled ? colors.fg : scheme.text.description,
|
|
99
|
+
}, children: option.description })) : null] }), selected && !disabled ? ((0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "check", size: size, color: colors.fg })) : null] }));
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Autocomplete
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
function Autocomplete({ options, value: controlledValue, defaultValue = null, onValueChange, onInputChange, label: fieldLabel, helperText, placeholder = 'Search…', leadingIcon, size = 'default', disabled = false, error = false, clearable = true, noOptionsText = 'No options', filterOptions = defaultFilter, style, accessibilityLabel, }) {
|
|
105
|
+
const { components, scheme, colors } = (0, theme_1.useTheme)();
|
|
106
|
+
const inputTokens = components.input[size];
|
|
107
|
+
const neutral = colors.neutral.default;
|
|
108
|
+
const isControlled = controlledValue !== undefined;
|
|
109
|
+
const [internalValue, setInternalValue] = (0, react_1.useState)(defaultValue);
|
|
110
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
111
|
+
const selectedLabel = (0, react_1.useMemo)(() => options.find((o) => o.value === value)?.label ?? '', [options, value]);
|
|
112
|
+
const [search, setSearch] = (0, react_1.useState)(selectedLabel);
|
|
113
|
+
const [isOpen, setIsOpen] = (0, react_1.useState)(false);
|
|
114
|
+
const [isFocused, setIsFocused] = (0, react_1.useState)(false);
|
|
115
|
+
// Keep the field text in sync when the selected value changes externally.
|
|
116
|
+
(0, react_1.useEffect)(() => {
|
|
117
|
+
setSearch(selectedLabel);
|
|
118
|
+
}, [selectedLabel]);
|
|
119
|
+
const query = search.trim().toLowerCase();
|
|
120
|
+
const showAll = query.length === 0 || (value != null && search === selectedLabel);
|
|
121
|
+
const filtered = showAll ? options : filterOptions(options, query);
|
|
122
|
+
const labelTypo = tokens_1.label[LABEL_SCALE[size]];
|
|
123
|
+
const bodyTypo = tokens_1.body[BODY_SCALE[size]];
|
|
124
|
+
const setValue = (next) => {
|
|
125
|
+
if (!isControlled)
|
|
126
|
+
setInternalValue(next);
|
|
127
|
+
onValueChange?.(next);
|
|
128
|
+
};
|
|
129
|
+
const handleSelect = (optionValue) => {
|
|
130
|
+
setValue(optionValue);
|
|
131
|
+
const lbl = options.find((o) => o.value === optionValue)?.label ?? '';
|
|
132
|
+
setSearch(lbl);
|
|
133
|
+
onInputChange?.(lbl);
|
|
134
|
+
setIsOpen(false);
|
|
135
|
+
};
|
|
136
|
+
const handleClear = () => {
|
|
137
|
+
setValue(null);
|
|
138
|
+
setSearch('');
|
|
139
|
+
onInputChange?.('');
|
|
140
|
+
setIsOpen(true);
|
|
141
|
+
};
|
|
142
|
+
// Escape closes the dropdown on web.
|
|
143
|
+
(0, react_1.useEffect)(() => {
|
|
144
|
+
if (!isOpen || react_native_1.Platform.OS !== 'web')
|
|
145
|
+
return;
|
|
146
|
+
const onKey = (e) => {
|
|
147
|
+
if (e.key === 'Escape') {
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
setIsOpen(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
document.addEventListener('keydown', onKey);
|
|
153
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
154
|
+
}, [isOpen]);
|
|
155
|
+
const borderColor = disabled
|
|
156
|
+
? scheme.disabled.border
|
|
157
|
+
: error
|
|
158
|
+
? scheme.error.border
|
|
159
|
+
: isFocused
|
|
160
|
+
? neutral.hover.border
|
|
161
|
+
: neutral.default.border;
|
|
162
|
+
const bgColor = disabled ? scheme.disabled.bg : neutral.default.bg;
|
|
163
|
+
const fgColor = disabled ? scheme.disabled.fg : neutral.default.fg;
|
|
164
|
+
const resolvedLeading = typeof leadingIcon === 'string' ? ((0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: leadingIcon, size: size, color: fgColor })) : (leadingIcon);
|
|
165
|
+
const hasContent = search.length > 0 || value != null;
|
|
166
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [{ alignSelf: 'stretch', gap: components.input.fieldGap, zIndex: isOpen ? 1000 : 0 }, style], children: [fieldLabel ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { selectable: false, style: {
|
|
167
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
168
|
+
fontWeight: tokens_1.fontWeight.medium,
|
|
169
|
+
fontSize: labelTypo.fontSize,
|
|
170
|
+
lineHeight: labelTypo.lineHeight,
|
|
171
|
+
letterSpacing: labelTypo.letterSpacing,
|
|
172
|
+
color: fgColor,
|
|
173
|
+
}, children: fieldLabel })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { position: 'relative' }, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
|
|
174
|
+
flexDirection: 'row',
|
|
175
|
+
alignItems: 'center',
|
|
176
|
+
gap: inputTokens.gap,
|
|
177
|
+
paddingHorizontal: inputTokens.paddingX,
|
|
178
|
+
paddingVertical: inputTokens.paddingY,
|
|
179
|
+
borderRadius: inputTokens.borderRadius,
|
|
180
|
+
borderWidth: tokens_1.controlTokens.borderWidth,
|
|
181
|
+
borderColor,
|
|
182
|
+
backgroundColor: bgColor,
|
|
183
|
+
}, children: [resolvedLeading ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { accessibilityElementsHidden: true, importantForAccessibility: "no", style: { width: tokens_1.iconSize[size], height: tokens_1.iconSize[size] }, children: resolvedLeading })) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { value: search, onChangeText: (text) => {
|
|
184
|
+
setSearch(text);
|
|
185
|
+
onInputChange?.(text);
|
|
186
|
+
if (!isOpen)
|
|
187
|
+
setIsOpen(true);
|
|
188
|
+
}, onFocus: () => {
|
|
189
|
+
setIsFocused(true);
|
|
190
|
+
setIsOpen(true);
|
|
191
|
+
}, onBlur: () => setIsFocused(false), editable: !disabled, placeholder: placeholder, placeholderTextColor: scheme.text.description, accessibilityLabel: accessibilityLabel || fieldLabel || 'Search', style: {
|
|
192
|
+
flex: 1,
|
|
193
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
194
|
+
fontWeight: tokens_1.fontWeight.regular,
|
|
195
|
+
fontSize: bodyTypo.fontSize,
|
|
196
|
+
lineHeight: bodyTypo.lineHeight,
|
|
197
|
+
letterSpacing: bodyTypo.letterSpacing,
|
|
198
|
+
color: fgColor,
|
|
199
|
+
padding: 0,
|
|
200
|
+
...(react_native_1.Platform.OS === 'web' ? { outlineWidth: 0 } : {}),
|
|
201
|
+
} }), clearable && hasContent && !disabled ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: handleClear, hitSlop: 8, accessibilityRole: "button", accessibilityLabel: "Clear", children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "close", size: size, color: fgColor }) })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { accessibilityElementsHidden: true, importantForAccessibility: "no", style: isOpen ? { transform: [{ rotate: '180deg' }] } : undefined, children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "arrow_drop_down", size: size, color: fgColor }) }))] }), isOpen ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => setIsOpen(false), accessibilityRole: "button", accessibilityLabel: "Close options", style: react_native_1.Platform.select({
|
|
202
|
+
web: {
|
|
203
|
+
position: 'fixed',
|
|
204
|
+
top: 0,
|
|
205
|
+
left: 0,
|
|
206
|
+
right: 0,
|
|
207
|
+
bottom: 0,
|
|
208
|
+
zIndex: 0,
|
|
209
|
+
},
|
|
210
|
+
default: {
|
|
211
|
+
position: 'absolute',
|
|
212
|
+
top: -9999,
|
|
213
|
+
left: -9999,
|
|
214
|
+
width: 99999,
|
|
215
|
+
height: 99999,
|
|
216
|
+
},
|
|
217
|
+
}) })) : null, isOpen ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
|
|
218
|
+
position: 'absolute',
|
|
219
|
+
top: '100%',
|
|
220
|
+
left: 0,
|
|
221
|
+
right: 0,
|
|
222
|
+
paddingTop: 4,
|
|
223
|
+
zIndex: 1,
|
|
224
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
|
|
225
|
+
backgroundColor: scheme.surface.overlay.bg,
|
|
226
|
+
borderWidth: tokens_1.controlTokens.borderWidth,
|
|
227
|
+
borderColor: scheme.surface.overlay.border,
|
|
228
|
+
borderRadius: scheme.surface.overlay.borderRadius,
|
|
229
|
+
paddingVertical: components.select.content.paddingY,
|
|
230
|
+
maxHeight: CONTENT_MAX_HEIGHT,
|
|
231
|
+
...(react_native_1.Platform.OS === 'web' ? SHADOW_WEB : SHADOW_NATIVE),
|
|
232
|
+
}, children: filtered.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { nestedScrollEnabled: true, keyboardShouldPersistTaps: "handled", children: filtered.map((option) => ((0, jsx_runtime_1.jsx)(OptionRow, { option: option, size: size, selected: option.value === value, onSelect: handleSelect }, option.value))) })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { selectable: false, style: {
|
|
233
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
234
|
+
fontWeight: tokens_1.fontWeight.regular,
|
|
235
|
+
fontSize: bodyTypo.fontSize,
|
|
236
|
+
lineHeight: bodyTypo.lineHeight,
|
|
237
|
+
letterSpacing: bodyTypo.letterSpacing,
|
|
238
|
+
color: scheme.text.description,
|
|
239
|
+
paddingHorizontal: components.select.option.paddingX,
|
|
240
|
+
paddingVertical: components.select.option.paddingY,
|
|
241
|
+
}, children: noOptionsText })) }) })) : null] }), helperText ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { selectable: false, style: {
|
|
242
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
243
|
+
fontWeight: tokens_1.fontWeight.regular,
|
|
244
|
+
fontSize: tokens_1.caption.fontSize,
|
|
245
|
+
lineHeight: tokens_1.caption.lineHeight,
|
|
246
|
+
letterSpacing: tokens_1.caption.letterSpacing,
|
|
247
|
+
color: error ? scheme.error.fg : scheme.text.description,
|
|
248
|
+
}, children: helperText })) : null] }));
|
|
249
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Autocomplete, type AutocompleteProps, type AutocompleteOption, type AutocompleteSize, } from './Autocomplete';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Autocomplete = void 0;
|
|
4
|
+
var Autocomplete_1 = require("./Autocomplete");
|
|
5
|
+
Object.defineProperty(exports, "Autocomplete", { enumerable: true, get: function () { return Autocomplete_1.Autocomplete; } });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backdrop — a full-screen scrim that dims everything behind it.
|
|
3
|
+
*
|
|
4
|
+
* Maps to the Figma <Backdrop> component. Use it on its own (a loading veil) or
|
|
5
|
+
* as the dimming layer behind a modal surface. It fills its parent, so place it
|
|
6
|
+
* inside a full-screen container or a Modal. It fades in and out with `open` and
|
|
7
|
+
* can centre a child (e.g. a Spinner).
|
|
8
|
+
*
|
|
9
|
+
* Colour: a black scrim at scheme.overlay.scrimOpacity, so it follows the active
|
|
10
|
+
* colour mode. With `invisible`, the scrim is transparent but still catches
|
|
11
|
+
* presses (matching a click-away layer). Backdrop introduces no new tokens — it
|
|
12
|
+
* reuses the shared overlay scrim opacity.
|
|
13
|
+
*
|
|
14
|
+
* Press the scrim to dismiss when `onPress` is supplied.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { type StyleProp, type ViewStyle, type GestureResponderEvent } from 'react-native';
|
|
18
|
+
export type BackdropProps = {
|
|
19
|
+
/** Controls visibility. */
|
|
20
|
+
open: boolean;
|
|
21
|
+
/** Called when the scrim is pressed. */
|
|
22
|
+
onPress?: (e: GestureResponderEvent) => void;
|
|
23
|
+
/** Transparent scrim that still catches presses. Defaults to false. */
|
|
24
|
+
invisible?: boolean;
|
|
25
|
+
/** Centred content (e.g. a Spinner). */
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
/** Style override for the scrim layer. */
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
/** Accessibility label for the dismiss layer. */
|
|
30
|
+
accessibilityLabel?: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function Backdrop({ open, onPress, invisible, children, style, accessibilityLabel, }: BackdropProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Backdrop = Backdrop;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
/**
|
|
6
|
+
* Backdrop — a full-screen scrim that dims everything behind it.
|
|
7
|
+
*
|
|
8
|
+
* Maps to the Figma <Backdrop> component. Use it on its own (a loading veil) or
|
|
9
|
+
* as the dimming layer behind a modal surface. It fills its parent, so place it
|
|
10
|
+
* inside a full-screen container or a Modal. It fades in and out with `open` and
|
|
11
|
+
* can centre a child (e.g. a Spinner).
|
|
12
|
+
*
|
|
13
|
+
* Colour: a black scrim at scheme.overlay.scrimOpacity, so it follows the active
|
|
14
|
+
* colour mode. With `invisible`, the scrim is transparent but still catches
|
|
15
|
+
* presses (matching a click-away layer). Backdrop introduces no new tokens — it
|
|
16
|
+
* reuses the shared overlay scrim opacity.
|
|
17
|
+
*
|
|
18
|
+
* Press the scrim to dismiss when `onPress` is supplied.
|
|
19
|
+
*/
|
|
20
|
+
const react_1 = require("react");
|
|
21
|
+
const react_native_1 = require("react-native");
|
|
22
|
+
const theme_1 = require("../../theme");
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Fade timing. */
|
|
27
|
+
const DURATION = 220;
|
|
28
|
+
/** react-native-web does not support the native animation driver. */
|
|
29
|
+
const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Component
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
function Backdrop({ open, onPress, invisible = false, children, style, accessibilityLabel, }) {
|
|
34
|
+
const { scheme } = (0, theme_1.useTheme)();
|
|
35
|
+
const targetOpacity = invisible ? 0 : scheme.overlay.scrimOpacity;
|
|
36
|
+
const fade = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
37
|
+
const [mounted, setMounted] = (0, react_1.useState)(open);
|
|
38
|
+
(0, react_1.useEffect)(() => {
|
|
39
|
+
if (open) {
|
|
40
|
+
setMounted(true);
|
|
41
|
+
react_native_1.Animated.timing(fade, {
|
|
42
|
+
toValue: 1,
|
|
43
|
+
duration: DURATION,
|
|
44
|
+
useNativeDriver: USE_NATIVE_DRIVER,
|
|
45
|
+
}).start();
|
|
46
|
+
}
|
|
47
|
+
else if (mounted) {
|
|
48
|
+
react_native_1.Animated.timing(fade, {
|
|
49
|
+
toValue: 0,
|
|
50
|
+
duration: DURATION,
|
|
51
|
+
useNativeDriver: USE_NATIVE_DRIVER,
|
|
52
|
+
}).start(({ finished }) => {
|
|
53
|
+
if (finished)
|
|
54
|
+
setMounted(false);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
+
}, [open]);
|
|
59
|
+
if (!mounted)
|
|
60
|
+
return null;
|
|
61
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { pointerEvents: "box-none", style: [
|
|
62
|
+
react_native_1.StyleSheet.absoluteFillObject,
|
|
63
|
+
{
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
justifyContent: 'center',
|
|
66
|
+
backgroundColor: '#000000',
|
|
67
|
+
opacity: fade.interpolate({
|
|
68
|
+
inputRange: [0, 1],
|
|
69
|
+
outputRange: [0, targetOpacity],
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
style,
|
|
73
|
+
], children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: react_native_1.StyleSheet.absoluteFillObject, onPress: onPress, accessibilityRole: "button", accessibilityLabel: accessibilityLabel || 'Dismiss' }), children] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Backdrop, type BackdropProps } from './Backdrop';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BottomSheet — modal surface that slides up from the bottom edge.
|
|
3
|
+
*
|
|
4
|
+
* Maps to the Figma <BottomSheet> component. The sheet hugs its content up to a
|
|
5
|
+
* max height (~90% of the screen), then the content scrolls. There are no size
|
|
6
|
+
* variants. Spacing (padding, gap) comes from the density theme. The top corner
|
|
7
|
+
* radius and the drag handle dimensions are constant across density.
|
|
8
|
+
*
|
|
9
|
+
* Interaction model: the sheet slides up on open and down on close, the scrim
|
|
10
|
+
* fades with it. There is no finger-dragging, so it behaves the same on web and
|
|
11
|
+
* native. Dismiss by pressing the scrim (when enabled) or by calling onClose.
|
|
12
|
+
*
|
|
13
|
+
* Structure: scrim backdrop -> sheet (drag handle + optional title + content).
|
|
14
|
+
* Surface styling reuses the shared overlay tokens (bg, border). The drag handle
|
|
15
|
+
* is the one bespoke colour, scheme.bottomSheet.handle.
|
|
16
|
+
*
|
|
17
|
+
* Fonts are consumer-loaded (Inter via the typography tokens).
|
|
18
|
+
*
|
|
19
|
+
* Exports:
|
|
20
|
+
* BottomSheet — full modal (scrim + animated sheet)
|
|
21
|
+
* BottomSheetContent — just the sheet card, for inline use or static stories
|
|
22
|
+
*/
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { type ViewStyle, type StyleProp } from 'react-native';
|
|
25
|
+
export type BottomSheetContentProps = {
|
|
26
|
+
/** Heading shown above the content. Optional. */
|
|
27
|
+
title?: string;
|
|
28
|
+
/** Show the drag handle pill at the top of the sheet. Defaults to true. */
|
|
29
|
+
showHandle?: boolean;
|
|
30
|
+
/** Sheet content. */
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
/** Style override for the sheet card. */
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
/** Accessibility label. Falls back to the title. */
|
|
35
|
+
accessibilityLabel?: string;
|
|
36
|
+
};
|
|
37
|
+
export type BottomSheetProps = BottomSheetContentProps & {
|
|
38
|
+
/** Controls visibility. */
|
|
39
|
+
open: boolean;
|
|
40
|
+
/** Called when the scrim is pressed or the sheet requests close. */
|
|
41
|
+
onClose?: () => void;
|
|
42
|
+
/** Dismiss when the scrim is pressed. Defaults to true. */
|
|
43
|
+
closeOnBackdropPress?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The sheet card rendered inline. No modal, no scrim, no animation. Use this for
|
|
47
|
+
* static display (Storybook visual stories) or custom overlay implementations.
|
|
48
|
+
*/
|
|
49
|
+
export declare function BottomSheetContent({ title: titleText, showHandle, children, style, accessibilityLabel, }: BottomSheetContentProps): import("react/jsx-runtime").JSX.Element;
|
|
50
|
+
export declare function BottomSheet({ open, onClose, closeOnBackdropPress, ...contentProps }: BottomSheetProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BottomSheetContent = BottomSheetContent;
|
|
4
|
+
exports.BottomSheet = BottomSheet;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
/**
|
|
7
|
+
* BottomSheet — modal surface that slides up from the bottom edge.
|
|
8
|
+
*
|
|
9
|
+
* Maps to the Figma <BottomSheet> component. The sheet hugs its content up to a
|
|
10
|
+
* max height (~90% of the screen), then the content scrolls. There are no size
|
|
11
|
+
* variants. Spacing (padding, gap) comes from the density theme. The top corner
|
|
12
|
+
* radius and the drag handle dimensions are constant across density.
|
|
13
|
+
*
|
|
14
|
+
* Interaction model: the sheet slides up on open and down on close, the scrim
|
|
15
|
+
* fades with it. There is no finger-dragging, so it behaves the same on web and
|
|
16
|
+
* native. Dismiss by pressing the scrim (when enabled) or by calling onClose.
|
|
17
|
+
*
|
|
18
|
+
* Structure: scrim backdrop -> sheet (drag handle + optional title + content).
|
|
19
|
+
* Surface styling reuses the shared overlay tokens (bg, border). The drag handle
|
|
20
|
+
* is the one bespoke colour, scheme.bottomSheet.handle.
|
|
21
|
+
*
|
|
22
|
+
* Fonts are consumer-loaded (Inter via the typography tokens).
|
|
23
|
+
*
|
|
24
|
+
* Exports:
|
|
25
|
+
* BottomSheet — full modal (scrim + animated sheet)
|
|
26
|
+
* BottomSheetContent — just the sheet card, for inline use or static stories
|
|
27
|
+
*/
|
|
28
|
+
const react_1 = require("react");
|
|
29
|
+
const react_native_1 = require("react-native");
|
|
30
|
+
const theme_1 = require("../../theme");
|
|
31
|
+
const tokens_1 = require("../../tokens");
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Constants
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/** The sheet never grows past this share of the screen height. */
|
|
36
|
+
const MAX_HEIGHT_RATIO = 0.9;
|
|
37
|
+
/** Animation timing. */
|
|
38
|
+
const DURATION = 220;
|
|
39
|
+
/** react-native-web does not support the native animation driver. */
|
|
40
|
+
const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
|
|
41
|
+
/** Upward shadow for web (matches Figma shadow/lg, cast above the sheet). */
|
|
42
|
+
const SHADOW_WEB = {
|
|
43
|
+
boxShadow: '0px -4px 6px rgba(0,0,0,0.04), 0px -10px 15px rgba(0,0,0,0.08)',
|
|
44
|
+
};
|
|
45
|
+
/** Upward shadow for native. */
|
|
46
|
+
const SHADOW_NATIVE = {
|
|
47
|
+
shadowColor: '#000000',
|
|
48
|
+
shadowOffset: { width: 0, height: -8 },
|
|
49
|
+
shadowOpacity: 0.08,
|
|
50
|
+
shadowRadius: 15,
|
|
51
|
+
elevation: 16,
|
|
52
|
+
};
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// BottomSheetContent — the sheet card, without modal/scrim/animation
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* The sheet card rendered inline. No modal, no scrim, no animation. Use this for
|
|
58
|
+
* static display (Storybook visual stories) or custom overlay implementations.
|
|
59
|
+
*/
|
|
60
|
+
function BottomSheetContent({ title: titleText, showHandle = true, children, style, accessibilityLabel, }) {
|
|
61
|
+
const { components, scheme } = (0, theme_1.useTheme)();
|
|
62
|
+
const tokens = components.bottomSheet;
|
|
63
|
+
const surface = scheme.surface;
|
|
64
|
+
const textTokens = scheme.text;
|
|
65
|
+
const titleTokens = tokens_1.title.md;
|
|
66
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { accessibilityViewIsModal: true, accessibilityLabel: accessibilityLabel || titleText, style: [
|
|
67
|
+
{
|
|
68
|
+
width: '100%',
|
|
69
|
+
maxHeight: '100%',
|
|
70
|
+
backgroundColor: surface.overlay.bg,
|
|
71
|
+
borderTopLeftRadius: tokens.borderRadius,
|
|
72
|
+
borderTopRightRadius: tokens.borderRadius,
|
|
73
|
+
borderWidth: tokens_1.controlTokens.borderWidth,
|
|
74
|
+
borderColor: surface.overlay.border,
|
|
75
|
+
paddingHorizontal: tokens.padding,
|
|
76
|
+
paddingBottom: tokens.padding,
|
|
77
|
+
paddingTop: tokens.handleGap,
|
|
78
|
+
...(react_native_1.Platform.OS === 'web' ? SHADOW_WEB : SHADOW_NATIVE),
|
|
79
|
+
},
|
|
80
|
+
style,
|
|
81
|
+
], children: [showHandle ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { alignItems: 'center', marginBottom: tokens.handleGap }, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
|
|
82
|
+
width: tokens.handleWidth,
|
|
83
|
+
height: tokens.handleHeight,
|
|
84
|
+
borderRadius: tokens.handleHeight / 2,
|
|
85
|
+
backgroundColor: scheme.bottomSheet.handle,
|
|
86
|
+
} }) })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.ScrollView, { bounces: false, showsVerticalScrollIndicator: false, contentContainerStyle: { gap: tokens.gap }, style: { flexGrow: 0, flexShrink: 1 }, children: [titleText ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { accessibilityRole: "header", style: {
|
|
87
|
+
fontFamily: tokens_1.fontFamily.sans,
|
|
88
|
+
fontWeight: tokens_1.fontWeight.medium,
|
|
89
|
+
fontSize: titleTokens.fontSize,
|
|
90
|
+
lineHeight: titleTokens.lineHeight,
|
|
91
|
+
letterSpacing: titleTokens.letterSpacing,
|
|
92
|
+
color: textTokens.primary,
|
|
93
|
+
}, children: titleText })) : null, children] })] }));
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// BottomSheet — full modal with scrim and slide animation
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
function BottomSheet({ open, onClose, closeOnBackdropPress = true, ...contentProps }) {
|
|
99
|
+
const { scheme } = (0, theme_1.useTheme)();
|
|
100
|
+
const scrimOpacity = scheme.overlay.scrimOpacity;
|
|
101
|
+
const screenHeight = react_native_1.Dimensions.get('window').height;
|
|
102
|
+
const translateY = (0, react_1.useRef)(new react_native_1.Animated.Value(screenHeight)).current;
|
|
103
|
+
const backdrop = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
104
|
+
const [mounted, setMounted] = (0, react_1.useState)(open);
|
|
105
|
+
(0, react_1.useEffect)(() => {
|
|
106
|
+
if (open) {
|
|
107
|
+
setMounted(true);
|
|
108
|
+
react_native_1.Animated.parallel([
|
|
109
|
+
react_native_1.Animated.timing(backdrop, {
|
|
110
|
+
toValue: 1,
|
|
111
|
+
duration: DURATION,
|
|
112
|
+
useNativeDriver: USE_NATIVE_DRIVER,
|
|
113
|
+
}),
|
|
114
|
+
react_native_1.Animated.spring(translateY, {
|
|
115
|
+
toValue: 0,
|
|
116
|
+
damping: 22,
|
|
117
|
+
stiffness: 220,
|
|
118
|
+
mass: 0.9,
|
|
119
|
+
useNativeDriver: USE_NATIVE_DRIVER,
|
|
120
|
+
}),
|
|
121
|
+
]).start();
|
|
122
|
+
}
|
|
123
|
+
else if (mounted) {
|
|
124
|
+
react_native_1.Animated.parallel([
|
|
125
|
+
react_native_1.Animated.timing(backdrop, {
|
|
126
|
+
toValue: 0,
|
|
127
|
+
duration: DURATION,
|
|
128
|
+
useNativeDriver: USE_NATIVE_DRIVER,
|
|
129
|
+
}),
|
|
130
|
+
react_native_1.Animated.timing(translateY, {
|
|
131
|
+
toValue: screenHeight,
|
|
132
|
+
duration: DURATION,
|
|
133
|
+
useNativeDriver: USE_NATIVE_DRIVER,
|
|
134
|
+
}),
|
|
135
|
+
]).start(({ finished }) => {
|
|
136
|
+
if (finished)
|
|
137
|
+
setMounted(false);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
}, [open]);
|
|
142
|
+
if (!mounted)
|
|
143
|
+
return null;
|
|
144
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: mounted, transparent: true, animationType: "none", onRequestClose: onClose, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flex: 1, justifyContent: 'flex-end' }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: {
|
|
145
|
+
position: 'absolute',
|
|
146
|
+
top: 0,
|
|
147
|
+
left: 0,
|
|
148
|
+
right: 0,
|
|
149
|
+
bottom: 0,
|
|
150
|
+
backgroundColor: '#000000',
|
|
151
|
+
opacity: backdrop.interpolate({
|
|
152
|
+
inputRange: [0, 1],
|
|
153
|
+
outputRange: [0, scrimOpacity],
|
|
154
|
+
}),
|
|
155
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: { flex: 1 }, disabled: !closeOnBackdropPress, onPress: closeOnBackdropPress ? onClose : undefined, accessibilityRole: "button", accessibilityLabel: "Close sheet" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: {
|
|
156
|
+
maxHeight: `${MAX_HEIGHT_RATIO * 100}%`,
|
|
157
|
+
transform: [{ translateY }],
|
|
158
|
+
}, children: (0, jsx_runtime_1.jsx)(BottomSheetContent, { ...contentProps }) })] }) }));
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BottomSheet, BottomSheetContent, type BottomSheetProps, type BottomSheetContentProps, } from './BottomSheet';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BottomSheetContent = exports.BottomSheet = void 0;
|
|
4
|
+
var BottomSheet_1 = require("./BottomSheet");
|
|
5
|
+
Object.defineProperty(exports, "BottomSheet", { enumerable: true, get: function () { return BottomSheet_1.BottomSheet; } });
|
|
6
|
+
Object.defineProperty(exports, "BottomSheetContent", { enumerable: true, get: function () { return BottomSheet_1.BottomSheetContent; } });
|