@castui/cast-ui 4.7.0 → 4.9.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.
Files changed (78) hide show
  1. package/README.md +61 -0
  2. package/dist/components/Accordion/Accordion.d.ts +80 -0
  3. package/dist/components/Accordion/Accordion.js +157 -0
  4. package/dist/components/Accordion/index.d.ts +1 -0
  5. package/dist/components/Accordion/index.js +6 -0
  6. package/dist/components/AppBar/AppBar.d.ts +47 -0
  7. package/dist/components/AppBar/AppBar.js +47 -0
  8. package/dist/components/AppBar/index.d.ts +1 -0
  9. package/dist/components/AppBar/index.js +5 -0
  10. package/dist/components/Autocomplete/Autocomplete.d.ts +70 -0
  11. package/dist/components/Autocomplete/Autocomplete.js +249 -0
  12. package/dist/components/Autocomplete/index.d.ts +1 -0
  13. package/dist/components/Autocomplete/index.js +5 -0
  14. package/dist/components/Backdrop/Backdrop.d.ts +32 -0
  15. package/dist/components/Backdrop/Backdrop.js +74 -0
  16. package/dist/components/Backdrop/index.d.ts +1 -0
  17. package/dist/components/Backdrop/index.js +5 -0
  18. package/dist/components/BottomSheet/BottomSheet.d.ts +50 -0
  19. package/dist/components/BottomSheet/BottomSheet.js +159 -0
  20. package/dist/components/BottomSheet/index.d.ts +1 -0
  21. package/dist/components/BottomSheet/index.js +6 -0
  22. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +63 -0
  23. package/dist/components/Breadcrumbs/Breadcrumbs.js +143 -0
  24. package/dist/components/Breadcrumbs/index.d.ts +1 -0
  25. package/dist/components/Breadcrumbs/index.js +6 -0
  26. package/dist/components/CodeBlock/CodeBlock.d.ts +42 -0
  27. package/dist/components/CodeBlock/CodeBlock.js +110 -0
  28. package/dist/components/CodeBlock/index.d.ts +1 -0
  29. package/dist/components/CodeBlock/index.js +5 -0
  30. package/dist/components/Drawer/Drawer.d.ts +51 -0
  31. package/dist/components/Drawer/Drawer.js +168 -0
  32. package/dist/components/Drawer/index.d.ts +1 -0
  33. package/dist/components/Drawer/index.js +6 -0
  34. package/dist/components/Link/Link.d.ts +51 -0
  35. package/dist/components/Link/Link.js +73 -0
  36. package/dist/components/Link/index.d.ts +1 -0
  37. package/dist/components/Link/index.js +5 -0
  38. package/dist/components/Menu/Menu.d.ts +91 -0
  39. package/dist/components/Menu/Menu.js +211 -0
  40. package/dist/components/Menu/index.d.ts +1 -0
  41. package/dist/components/Menu/index.js +9 -0
  42. package/dist/components/Slider/Slider.d.ts +47 -0
  43. package/dist/components/Slider/Slider.js +132 -0
  44. package/dist/components/Slider/index.d.ts +1 -0
  45. package/dist/components/Slider/index.js +5 -0
  46. package/dist/components/SpeedDial/SpeedDial.d.ts +72 -0
  47. package/dist/components/SpeedDial/SpeedDial.js +189 -0
  48. package/dist/components/SpeedDial/index.d.ts +1 -0
  49. package/dist/components/SpeedDial/index.js +6 -0
  50. package/dist/components/Table/Table.d.ts +74 -0
  51. package/dist/components/Table/Table.js +176 -0
  52. package/dist/components/Table/index.d.ts +1 -0
  53. package/dist/components/Table/index.js +9 -0
  54. package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.ts +69 -0
  55. package/dist/components/ToggleButtonGroup/ToggleButtonGroup.js +158 -0
  56. package/dist/components/ToggleButtonGroup/index.d.ts +1 -0
  57. package/dist/components/ToggleButtonGroup/index.js +6 -0
  58. package/dist/hooks/index.d.ts +1 -0
  59. package/dist/hooks/index.js +7 -0
  60. package/dist/hooks/useBreakpoint.d.ts +22 -0
  61. package/dist/hooks/useBreakpoint.js +45 -0
  62. package/dist/index.d.ts +17 -2
  63. package/dist/index.js +59 -2
  64. package/dist/theme/ThemeContext.d.ts +8 -1
  65. package/dist/theme/ThemeContext.js +7 -4
  66. package/dist/theme/applyCastTheme.d.ts +75 -0
  67. package/dist/theme/applyCastTheme.js +95 -0
  68. package/dist/theme/index.d.ts +2 -1
  69. package/dist/theme/index.js +3 -1
  70. package/dist/theme/themes.js +177 -0
  71. package/dist/theme/types.d.ts +177 -0
  72. package/dist/tokens/breakpoints.d.ts +57 -0
  73. package/dist/tokens/breakpoints.js +92 -0
  74. package/dist/tokens/colors.d.ts +44 -0
  75. package/dist/tokens/colors.js +47 -1
  76. package/dist/tokens/index.d.ts +2 -1
  77. package/dist/tokens/index.js +9 -1
  78. 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,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Backdrop = void 0;
4
+ var Backdrop_1 = require("./Backdrop");
5
+ Object.defineProperty(exports, "Backdrop", { enumerable: true, get: function () { return Backdrop_1.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; } });