@webority-technologies/mobile 0.0.20 → 0.0.22

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 (27) hide show
  1. package/lib/commonjs/components/BottomSheet/BottomSheet.js +61 -4
  2. package/lib/commonjs/components/BottomSheet/index.js +6 -0
  3. package/lib/commonjs/components/FieldBase/FieldBase.js +212 -24
  4. package/lib/commonjs/components/Input/Input.js +1 -1
  5. package/lib/commonjs/components/SearchBar/SearchBar.js +8 -2
  6. package/lib/commonjs/components/Select/Select.js +1 -1
  7. package/lib/commonjs/components/index.js +6 -0
  8. package/lib/module/components/BottomSheet/BottomSheet.js +60 -4
  9. package/lib/module/components/BottomSheet/index.js +1 -1
  10. package/lib/module/components/FieldBase/FieldBase.js +211 -23
  11. package/lib/module/components/Input/Input.js +1 -1
  12. package/lib/module/components/SearchBar/SearchBar.js +8 -2
  13. package/lib/module/components/Select/Select.js +1 -1
  14. package/lib/module/components/index.js +1 -1
  15. package/lib/typescript/commonjs/components/BottomSheet/BottomSheet.d.ts +41 -0
  16. package/lib/typescript/commonjs/components/BottomSheet/index.d.ts +2 -2
  17. package/lib/typescript/commonjs/components/FieldBase/FieldBase.d.ts +43 -12
  18. package/lib/typescript/commonjs/components/Input/Input.d.ts +1 -1
  19. package/lib/typescript/commonjs/components/index.d.ts +2 -2
  20. package/lib/typescript/commonjs/theme/types.d.ts +31 -7
  21. package/lib/typescript/module/components/BottomSheet/BottomSheet.d.ts +41 -0
  22. package/lib/typescript/module/components/BottomSheet/index.d.ts +2 -2
  23. package/lib/typescript/module/components/FieldBase/FieldBase.d.ts +43 -12
  24. package/lib/typescript/module/components/Input/Input.d.ts +1 -1
  25. package/lib/typescript/module/components/index.d.ts +2 -2
  26. package/lib/typescript/module/theme/types.d.ts +31 -7
  27. package/package.json +1 -1
@@ -27,6 +27,66 @@
27
27
  import React, { useEffect, useMemo, useRef } from 'react';
28
28
  import { Animated, Easing, Pressable, StyleSheet, View } from 'react-native';
29
29
  import { useTheme, createAnimatedValue, fontFor } from "../../theme/index.js";
30
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
31
+ const SIDES = ['top', 'right', 'bottom', 'left'];
32
+
33
+ /** Map a single colour string to all four sides (shorthand expansion). */
34
+ const expandColorSides = value => {
35
+ if (typeof value === 'string') {
36
+ return {
37
+ top: value,
38
+ right: value,
39
+ bottom: value,
40
+ left: value
41
+ };
42
+ }
43
+ // Object form: any side the consumer omitted falls through to 'transparent'.
44
+ // Rationale: if a consumer says `{ bottom: '#000' }`, they want only the
45
+ // bottom drawn — the other sides should be invisible, not inherit some
46
+ // unrelated colour.
47
+ return {
48
+ top: value.top ?? 'transparent',
49
+ right: value.right ?? 'transparent',
50
+ bottom: value.bottom ?? 'transparent',
51
+ left: value.left ?? 'transparent'
52
+ };
53
+ };
54
+
55
+ /** Map a single width number to all four sides (shorthand expansion). */
56
+ const expandWidthSides = value => {
57
+ if (typeof value === 'number') {
58
+ return {
59
+ top: value,
60
+ right: value,
61
+ bottom: value,
62
+ left: value
63
+ };
64
+ }
65
+ return {
66
+ top: value.top ?? 0,
67
+ right: value.right ?? 0,
68
+ bottom: value.bottom ?? 0,
69
+ left: value.left ?? 0
70
+ };
71
+ };
72
+
73
+ /** Are all four sides of a per-side record equal? Used to collapse to shorthand. */
74
+ const allSidesEqual = sides => sides.top === sides.right && sides.right === sides.bottom && sides.bottom === sides.left;
75
+
76
+ /**
77
+ * Pick the most "representative" side colour from a 4-side record. Used by
78
+ * consumers (Input's selectionColor, focus rings, etc.) that need a single
79
+ * colour to mirror the visible focus indicator. Priority: bottom → top →
80
+ * left → right, skipping `'transparent'`. The bottom-first order means
81
+ * underline variants pick their underline colour automatically.
82
+ */
83
+ export const pickRepresentativeBorderColor = sides => {
84
+ for (const side of ['bottom', 'top', 'left', 'right']) {
85
+ const c = sides[side];
86
+ if (c && c !== 'transparent') return c;
87
+ }
88
+ return sides.bottom;
89
+ };
30
90
 
31
91
  /**
32
92
  * Resolved text styling for the editable / displayed content inside a field.
@@ -36,7 +96,7 @@ import { useTheme, createAnimatedValue, fontFor } from "../../theme/index.js";
36
96
  * same place. OTPInput intentionally overrides this with its own semibold
37
97
  * display weight.
38
98
  */
39
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
99
+
40
100
  /**
41
101
  * Resolve the canonical text style chunk for a field's value. Reads from
42
102
  * `theme.components.field.{textColor,disabledTextColor,placeholderColor,fontWeight}`
@@ -115,31 +175,70 @@ const DEFAULT_SIZES = {
115
175
 
116
176
  /**
117
177
  * Strict, fully-resolved colour set used internally. Mirrors `FieldVariantTokens`
118
- * but every field is non-optional every state has a concrete colour by the
119
- * time the box renders, so downstream code can rely on string-typed fills.
178
+ * but every border colour / width is expanded to a 4-side record by the time
179
+ * the box renders, so the animation pipeline can treat per-side and shorthand
180
+ * the same way. `borderFocusedRepresentative` is a single colour pulled from
181
+ * the focused sides — exposed for consumers (Input's selectionColor) that
182
+ * need to mirror the visible focus indicator without re-resolving per side.
120
183
  */
121
184
 
122
185
  /**
123
186
  * Resolve the full set of state-aware fill + border colours for a given
124
- * variant. Order: explicit override → theme token → library default.
187
+ * variant. Order: explicit override → theme token → library default. Border
188
+ * colour + width are normalized to per-side records here; callers that need
189
+ * a single colour read `borderFocusedRepresentative`.
125
190
  */
126
191
  export const resolveVariantColors = (theme, variant, override) => {
127
192
  const tokenSet = theme.components.field?.[variant];
128
- const isOutlined = variant === 'outlined';
129
- const idleEmpty = isOutlined ? theme.colors.background.primary : theme.colors.background.secondary;
130
- const borderIdle = isOutlined ? theme.colors.border.primary : 'transparent';
131
- const borderWidth = isOutlined ? theme.colors.border.width : 0;
193
+
194
+ // Variant-specific defaults for resting fill + border treatment. `outlined`
195
+ // draws all four sides; `filled` draws nothing (fill-only); `underline`
196
+ // draws only the bottom edge Material-style single-line inputs.
197
+ let idleEmpty;
198
+ let borderIdleDefault;
199
+ let borderWidthDefault;
200
+ if (variant === 'outlined') {
201
+ idleEmpty = theme.colors.background.primary;
202
+ borderIdleDefault = theme.colors.border.primary;
203
+ borderWidthDefault = theme.colors.border.width;
204
+ } else if (variant === 'underline') {
205
+ idleEmpty = 'transparent';
206
+ borderIdleDefault = {
207
+ bottom: theme.colors.border.primary
208
+ };
209
+ borderWidthDefault = {
210
+ bottom: theme.colors.border.width
211
+ };
212
+ } else {
213
+ idleEmpty = theme.colors.background.secondary;
214
+ borderIdleDefault = 'transparent';
215
+ borderWidthDefault = 0;
216
+ }
217
+
218
+ // For focused/error/disabled we expand using the same per-side shape as
219
+ // idle. If `borderIdle` ended up bottom-only (underline), an unset
220
+ // `borderFocused` defaults to `{ bottom: colors.border.focus }` so the
221
+ // focus indicator stays on the same edge. Same logic for error.
222
+ const wrapToIdleShape = colour => typeof borderIdleDefault === 'string' ? colour : {
223
+ bottom: colour
224
+ };
225
+ const borderIdle = expandColorSides(override?.borderIdle ?? tokenSet?.borderIdle ?? borderIdleDefault);
226
+ const borderFocused = expandColorSides(override?.borderFocused ?? tokenSet?.borderFocused ?? wrapToIdleShape(theme.colors.border.focus));
227
+ const borderError = expandColorSides(override?.borderError ?? tokenSet?.borderError ?? wrapToIdleShape(theme.colors.border.error));
228
+ const borderDisabled = expandColorSides(override?.borderDisabled ?? tokenSet?.borderDisabled ?? borderIdleDefault);
229
+ const borderWidth = expandWidthSides(override?.borderWidth ?? tokenSet?.borderWidth ?? borderWidthDefault);
132
230
  return {
133
231
  backgroundIdleEmpty: override?.backgroundIdleEmpty ?? tokenSet?.backgroundIdleEmpty ?? idleEmpty,
134
232
  backgroundIdleFilled: override?.backgroundIdleFilled ?? tokenSet?.backgroundIdleFilled ?? override?.backgroundIdleEmpty ?? tokenSet?.backgroundIdleEmpty ?? idleEmpty,
135
233
  backgroundFocused: override?.backgroundFocused ?? tokenSet?.backgroundFocused ?? undefined,
136
234
  backgroundError: override?.backgroundError ?? tokenSet?.backgroundError ?? undefined,
137
235
  backgroundDisabled: override?.backgroundDisabled ?? tokenSet?.backgroundDisabled ?? theme.colors.surface.disabled,
138
- borderIdle: override?.borderIdle ?? tokenSet?.borderIdle ?? borderIdle,
139
- borderFocused: override?.borderFocused ?? tokenSet?.borderFocused ?? theme.colors.border.focus,
140
- borderError: override?.borderError ?? tokenSet?.borderError ?? theme.colors.border.error,
141
- borderDisabled: override?.borderDisabled ?? tokenSet?.borderDisabled ?? borderIdle,
142
- borderWidth: override?.borderWidth ?? tokenSet?.borderWidth ?? borderWidth
236
+ borderIdle,
237
+ borderFocused,
238
+ borderError,
239
+ borderDisabled,
240
+ borderWidth,
241
+ borderFocusedRepresentative: pickRepresentativeBorderColor(borderFocused)
143
242
  };
144
243
  };
145
244
  export const FieldBase = props => {
@@ -161,7 +260,15 @@ export const FieldBase = props => {
161
260
  paddingHorizontal: paddingHorizontalProp,
162
261
  paddingVertical: paddingVerticalProp,
163
262
  borderRadius: borderRadiusProp,
263
+ borderTopLeftRadius: borderTopLeftRadiusProp,
264
+ borderTopRightRadius: borderTopRightRadiusProp,
265
+ borderBottomLeftRadius: borderBottomLeftRadiusProp,
266
+ borderBottomRightRadius: borderBottomRightRadiusProp,
164
267
  borderWidth: borderWidthProp,
268
+ borderTopWidth: borderTopWidthProp,
269
+ borderRightWidth: borderRightWidthProp,
270
+ borderBottomWidth: borderBottomWidthProp,
271
+ borderLeftWidth: borderLeftWidthProp,
165
272
  gap: gapProp,
166
273
  fillOverrides,
167
274
  style,
@@ -200,13 +307,6 @@ export const FieldBase = props => {
200
307
  const idleFill = filled ? colors.backgroundIdleFilled : colors.backgroundIdleEmpty;
201
308
  const focusedFill = colors.backgroundFocused ?? idleFill;
202
309
  const errorFill = colors.backgroundError ?? idleFill;
203
- const animatedBorderColor = disabled ? colors.borderDisabled : error ? errorAnim.interpolate({
204
- inputRange: [0, 1],
205
- outputRange: [colors.borderIdle, colors.borderError]
206
- }) : focusAnim.interpolate({
207
- inputRange: [0, 1],
208
- outputRange: [colors.borderIdle, colors.borderFocused]
209
- });
210
310
  const animatedBackgroundColor = disabled ? colors.backgroundDisabled : error ? errorAnim.interpolate({
211
311
  inputRange: [0, 1],
212
312
  outputRange: [idleFill, errorFill]
@@ -214,17 +314,105 @@ export const FieldBase = props => {
214
314
  inputRange: [0, 1],
215
315
  outputRange: [idleFill, focusedFill]
216
316
  });
317
+
318
+ // Per-side border colour. For each side we resolve the "from" (idle) and
319
+ // "to" (focused or error, depending on state) colour, then drive a single
320
+ // animated interpolation. When all four sides share the same from+to pair
321
+ // we collapse to one `borderColor` key — the common case stays cheap.
322
+ const activeColors = error ? colors.borderError : colors.borderFocused;
323
+ const activeAnim = error ? errorAnim : focusAnim;
324
+ const sideFrom = disabled ? colors.borderDisabled : colors.borderIdle;
325
+ const sideTo = disabled ? colors.borderDisabled : activeColors;
326
+
327
+ // Per-side widths: longhand prop wins over shorthand prop wins over
328
+ // resolved variant token. Each side independently.
329
+ const propWidth = {
330
+ top: borderTopWidthProp,
331
+ right: borderRightWidthProp,
332
+ bottom: borderBottomWidthProp,
333
+ left: borderLeftWidthProp
334
+ };
335
+ const resolvedWidths = {
336
+ top: propWidth.top ?? borderWidthProp ?? colors.borderWidth.top,
337
+ right: propWidth.right ?? borderWidthProp ?? colors.borderWidth.right,
338
+ bottom: propWidth.bottom ?? borderWidthProp ?? colors.borderWidth.bottom,
339
+ left: propWidth.left ?? borderWidthProp ?? colors.borderWidth.left
340
+ };
217
341
  const boxStyle = {
218
342
  minHeight: height ?? minHeightProp ?? sizeTokens.minHeight,
219
343
  maxHeight,
220
344
  paddingHorizontal: paddingHorizontalProp ?? sizeTokens.paddingHorizontal,
221
345
  paddingVertical: paddingVerticalProp ?? sizeTokens.paddingVertical,
222
- borderRadius: borderRadiusProp ?? sizeTokens.borderRadius,
223
- borderWidth: borderWidthProp ?? colors.borderWidth,
224
- borderColor: animatedBorderColor,
225
346
  backgroundColor: animatedBackgroundColor,
226
347
  columnGap: gapProp ?? theme.spacing.sm
227
348
  };
349
+
350
+ // Radius: per-corner prop wins over shorthand prop wins over size token.
351
+ // Collapse to single `borderRadius` when all four corners match.
352
+ const radiusShorthand = borderRadiusProp ?? sizeTokens.borderRadius;
353
+ const corners = {
354
+ tl: borderTopLeftRadiusProp ?? radiusShorthand,
355
+ tr: borderTopRightRadiusProp ?? radiusShorthand,
356
+ bl: borderBottomLeftRadiusProp ?? radiusShorthand,
357
+ br: borderBottomRightRadiusProp ?? radiusShorthand
358
+ };
359
+ if (corners.tl === corners.tr && corners.tr === corners.bl && corners.bl === corners.br) {
360
+ boxStyle.borderRadius = corners.tl;
361
+ } else {
362
+ boxStyle.borderTopLeftRadius = corners.tl;
363
+ boxStyle.borderTopRightRadius = corners.tr;
364
+ boxStyle.borderBottomLeftRadius = corners.bl;
365
+ boxStyle.borderBottomRightRadius = corners.br;
366
+ }
367
+
368
+ // Width: collapse to single `borderWidth` when all four sides match.
369
+ if (resolvedWidths.top === resolvedWidths.right && resolvedWidths.right === resolvedWidths.bottom && resolvedWidths.bottom === resolvedWidths.left) {
370
+ boxStyle.borderWidth = resolvedWidths.top;
371
+ } else {
372
+ boxStyle.borderTopWidth = resolvedWidths.top;
373
+ boxStyle.borderRightWidth = resolvedWidths.right;
374
+ boxStyle.borderBottomWidth = resolvedWidths.bottom;
375
+ boxStyle.borderLeftWidth = resolvedWidths.left;
376
+ }
377
+
378
+ // Colour: collapse to single `borderColor` only when both endpoints are
379
+ // identical across all sides. Otherwise emit four `borderXColor`
380
+ // interpolations. Disabled state is static (no animation needed).
381
+ const fromAllEqual = allSidesEqual(sideFrom);
382
+ const toAllEqual = allSidesEqual(sideTo);
383
+ if (disabled) {
384
+ if (fromAllEqual) {
385
+ boxStyle.borderColor = sideFrom.top;
386
+ } else {
387
+ boxStyle.borderTopColor = sideFrom.top;
388
+ boxStyle.borderRightColor = sideFrom.right;
389
+ boxStyle.borderBottomColor = sideFrom.bottom;
390
+ boxStyle.borderLeftColor = sideFrom.left;
391
+ }
392
+ } else if (fromAllEqual && toAllEqual && sideFrom.top === sideFrom.right && sideTo.top === sideTo.right) {
393
+ // Common case: shorthand on both ends → single interpolation.
394
+ boxStyle.borderColor = activeAnim.interpolate({
395
+ inputRange: [0, 1],
396
+ outputRange: [sideFrom.top, sideTo.top]
397
+ });
398
+ } else {
399
+ boxStyle.borderTopColor = activeAnim.interpolate({
400
+ inputRange: [0, 1],
401
+ outputRange: [sideFrom.top, sideTo.top]
402
+ });
403
+ boxStyle.borderRightColor = activeAnim.interpolate({
404
+ inputRange: [0, 1],
405
+ outputRange: [sideFrom.right, sideTo.right]
406
+ });
407
+ boxStyle.borderBottomColor = activeAnim.interpolate({
408
+ inputRange: [0, 1],
409
+ outputRange: [sideFrom.bottom, sideTo.bottom]
410
+ });
411
+ boxStyle.borderLeftColor = activeAnim.interpolate({
412
+ inputRange: [0, 1],
413
+ outputRange: [sideFrom.left, sideTo.left]
414
+ });
415
+ }
228
416
  if (width !== undefined) boxStyle.width = width;
229
417
  if (height !== undefined) boxStyle.height = height;
230
418
  const a11yState = {
@@ -163,7 +163,7 @@ const Input = /*#__PURE__*/forwardRef((props, ref) => {
163
163
  // Selection / caret / handle colour walks the same resolution chain as the
164
164
  // focused border so brands that override `components.field.<variant>.borderFocused`
165
165
  // get a matching selection tint without also having to update root `border.focus`.
166
- const selectionColor = useMemo(() => resolveVariantColors(theme, variant, fillOverrides).borderFocused, [theme, variant, fillOverrides]);
166
+ const selectionColor = useMemo(() => resolveVariantColors(theme, variant, fillOverrides).borderFocusedRepresentative, [theme, variant, fillOverrides]);
167
167
  const borderRadiusOverride = borderRadiusProp ?? theme.components.input?.borderRadius;
168
168
 
169
169
  // Floating label styles
@@ -35,7 +35,13 @@ const SearchBar = /*#__PURE__*/forwardRef((props, ref) => {
35
35
  // field-family token (`components.field.defaultVariant`) → library default.
36
36
  // Library default stays `'filled'` so SearchBar reads as the traditional
37
37
  // pill-shaped, borderless search box when no theme opinion is expressed.
38
- const variant = variantProp ?? theme.components.searchBar?.defaultVariant ?? theme.components.field?.defaultVariant ?? 'filled';
38
+ // SearchBar deliberately only supports 'filled' and 'outlined' an
39
+ // underline-only pill makes no visual sense. If the shared field default is
40
+ // 'underline', fall through to 'filled' rather than carry an unsupported
41
+ // value down to the variant token lookup.
42
+ const sharedDefault = theme.components.field?.defaultVariant;
43
+ const sharedFallback = sharedDefault === 'outlined' || sharedDefault === 'filled' ? sharedDefault : 'filled';
44
+ const variant = variantProp ?? theme.components.searchBar?.defaultVariant ?? sharedFallback;
39
45
  const fieldTokens = resolveFieldSize(theme, size);
40
46
  const sizeStyles = {
41
47
  ...fieldTokens,
@@ -179,7 +185,7 @@ const SearchBar = /*#__PURE__*/forwardRef((props, ref) => {
179
185
  autoFocus: autoFocus,
180
186
  editable: !disabled,
181
187
  returnKeyType: "search",
182
- selectionColor: resolveVariantColors(theme, variant).borderFocused,
188
+ selectionColor: resolveVariantColors(theme, variant).borderFocusedRepresentative,
183
189
  accessibilityLabel: accessibilityLabel ?? placeholder,
184
190
  accessibilityState: {
185
191
  disabled
@@ -321,7 +321,7 @@ const Select = /*#__PURE__*/forwardRef((props, ref) => {
321
321
  onChangeText: setQuery,
322
322
  placeholder: "Search\u2026",
323
323
  placeholderTextColor: theme.colors.text.tertiary,
324
- selectionColor: resolveVariantColors(theme, theme.components.field?.defaultVariant ?? 'outlined').borderFocused,
324
+ selectionColor: resolveVariantColors(theme, theme.components.field?.defaultVariant ?? 'outlined').borderFocusedRepresentative,
325
325
  accessibilityLabel: "Search options",
326
326
  style: [styles.searchInput, {
327
327
  color: theme.colors.text.primary,
@@ -5,7 +5,7 @@ export { Avatar, AvatarGroup } from "./Avatar/index.js";
5
5
  export { Badge } from "./Badge/index.js";
6
6
  export { Banner } from "./Banner/index.js";
7
7
  export { BottomNavigation } from "./BottomNavigation/index.js";
8
- export { BottomSheet } from "./BottomSheet/index.js";
8
+ export { BottomSheet, useBottomSheet } from "./BottomSheet/index.js";
9
9
  export { Button } from "./Button/index.js";
10
10
  export { Card } from "./Card/index.js";
11
11
  export { Carousel } from "./Carousel/index.js";
@@ -56,6 +56,23 @@ export interface BottomSheetProps {
56
56
  mode?: BottomSheetMode;
57
57
  handleIndicatorStyle?: StyleProp<ViewStyle>;
58
58
  containerStyle?: StyleProp<ViewStyle>;
59
+ /**
60
+ * Sticky region rendered between the drag handle and the scrollable content.
61
+ * Does not scroll with `children`. Can call `useBottomSheet()` to read sheet
62
+ * state.
63
+ */
64
+ header?: React.ReactNode;
65
+ /**
66
+ * Sticky region pinned to the bottom of the sheet, above the safe-area
67
+ * inset. Does not scroll with `children`, and rides up with the keyboard
68
+ * when `keyboardBehavior='shift'` (no extra wiring needed — the whole sheet
69
+ * shifts). Typical use: action button rows.
70
+ */
71
+ footer?: React.ReactNode;
72
+ /** Style applied to the header region wrapper. */
73
+ headerStyle?: StyleProp<ViewStyle>;
74
+ /** Style applied to the footer region wrapper. */
75
+ footerStyle?: StyleProp<ViewStyle>;
59
76
  children?: React.ReactNode;
60
77
  accessibilityLabel?: string;
61
78
  accessibilityViewIsModal?: boolean;
@@ -66,6 +83,30 @@ export interface BottomSheetRef {
66
83
  collapse: () => void;
67
84
  close: () => void;
68
85
  }
86
+ /**
87
+ * State + actions exposed to anything rendered inside a `<BottomSheet>` —
88
+ * including the `header` and `footer` slots. Read via `useBottomSheet()`.
89
+ *
90
+ * `snapIndex` is the JS-thread mirror of the current snap point. -1 means the
91
+ * sheet is closed (mid-close-animation included). Use it to drive footer
92
+ * button enable state, conditional headers, etc. If you need per-frame
93
+ * progress (e.g. fade a header in as the sheet expands), reach for the
94
+ * animated value via a future enhancement — not exposed today to keep the
95
+ * surface minimal.
96
+ */
97
+ export interface BottomSheetContextValue {
98
+ snapIndex: number;
99
+ snapPoints: number[];
100
+ expand: (index?: number) => void;
101
+ collapse: () => void;
102
+ close: () => void;
103
+ }
104
+ /**
105
+ * Access the enclosing `<BottomSheet>`'s state and imperative actions.
106
+ * Must be called from a component rendered inside a `<BottomSheet>` (as
107
+ * `children`, `header`, or `footer`).
108
+ */
109
+ export declare const useBottomSheet: () => BottomSheetContextValue;
69
110
  declare const BottomSheet: React.ForwardRefExoticComponent<BottomSheetProps & React.RefAttributes<BottomSheetRef>>;
70
111
  export { BottomSheet };
71
112
  export default BottomSheet;
@@ -1,3 +1,3 @@
1
- export { BottomSheet, default } from './BottomSheet';
2
- export type { BottomSheetProps, BottomSheetRef, SnapPoint } from './BottomSheet';
1
+ export { BottomSheet, default, useBottomSheet } from './BottomSheet';
2
+ export type { BottomSheetContextValue, BottomSheetProps, BottomSheetRef, SnapPoint } from './BottomSheet';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -23,9 +23,17 @@
23
23
  */
24
24
  import React from 'react';
25
25
  import type { AccessibilityRole, AccessibilityState, StyleProp, TextStyle, ViewStyle } from 'react-native';
26
- import type { FieldSizeTokens, FieldVariantTokens, Theme } from '../../theme/types';
26
+ import type { FieldSide, FieldSizeTokens, FieldVariantTokens, Theme } from '../../theme/types';
27
27
  export type FieldBaseSize = 'sm' | 'md' | 'lg';
28
- export type FieldBaseVariant = 'outlined' | 'filled';
28
+ export type FieldBaseVariant = 'outlined' | 'filled' | 'underline';
29
+ /**
30
+ * Pick the most "representative" side colour from a 4-side record. Used by
31
+ * consumers (Input's selectionColor, focus rings, etc.) that need a single
32
+ * colour to mirror the visible focus indicator. Priority: bottom → top →
33
+ * left → right, skipping `'transparent'`. The bottom-first order means
34
+ * underline variants pick their underline colour automatically.
35
+ */
36
+ export declare const pickRepresentativeBorderColor: (sides: Record<FieldSide, string>) => string;
29
37
  export interface FieldBaseProps {
30
38
  /** Resolves to dimension tokens at `theme.components.field[size]`. Default `'md'`. */
31
39
  size?: FieldBaseSize;
@@ -65,10 +73,26 @@ export interface FieldBaseProps {
65
73
  paddingHorizontal?: number;
66
74
  /** Override vertical padding (single-line). Multiline parents usually leave this default. */
67
75
  paddingVertical?: number;
68
- /** Override corner radius. SearchBar pill uses this to apply `radius.full`. */
76
+ /** Override corner radius (shorthand — all four corners). SearchBar pill uses this to apply `radius.full`. */
69
77
  borderRadius?: number;
70
- /** Override border width. OTP uses 1.5; everything else inherits `colors.border.width`. */
78
+ /** Top-left corner radius. Wins over `borderRadius`. */
79
+ borderTopLeftRadius?: number;
80
+ /** Top-right corner radius. Wins over `borderRadius`. */
81
+ borderTopRightRadius?: number;
82
+ /** Bottom-left corner radius. Wins over `borderRadius`. */
83
+ borderBottomLeftRadius?: number;
84
+ /** Bottom-right corner radius. Wins over `borderRadius`. */
85
+ borderBottomRightRadius?: number;
86
+ /** Override border width (shorthand — all four sides). OTP uses 1.5; everything else inherits `colors.border.width`. */
71
87
  borderWidth?: number;
88
+ /** Top border width. Wins over `borderWidth`. */
89
+ borderTopWidth?: number;
90
+ /** Right border width. Wins over `borderWidth`. */
91
+ borderRightWidth?: number;
92
+ /** Bottom border width. Wins over `borderWidth`. */
93
+ borderBottomWidth?: number;
94
+ /** Left border width. Wins over `borderWidth`. */
95
+ borderLeftWidth?: number;
72
96
  /** Gap between leading / children / trailing. Defaults to `theme.spacing.sm`. */
73
97
  gap?: number;
74
98
  fillOverrides?: Partial<FieldVariantTokens>;
@@ -114,8 +138,11 @@ export declare const resolveFieldTextStyle: (theme: Theme, options?: FieldTextSt
114
138
  export declare const resolveFieldSize: (theme: Theme, size: FieldBaseSize) => FieldSizeTokens;
115
139
  /**
116
140
  * Strict, fully-resolved colour set used internally. Mirrors `FieldVariantTokens`
117
- * but every field is non-optional every state has a concrete colour by the
118
- * time the box renders, so downstream code can rely on string-typed fills.
141
+ * but every border colour / width is expanded to a 4-side record by the time
142
+ * the box renders, so the animation pipeline can treat per-side and shorthand
143
+ * the same way. `borderFocusedRepresentative` is a single colour pulled from
144
+ * the focused sides — exposed for consumers (Input's selectionColor) that
145
+ * need to mirror the visible focus indicator without re-resolving per side.
119
146
  */
120
147
  interface ResolvedFieldColors {
121
148
  backgroundIdleEmpty: string;
@@ -125,15 +152,19 @@ interface ResolvedFieldColors {
125
152
  /** Optional override fill while in error — when undefined, error animates from idle to idle. */
126
153
  backgroundError: string | undefined;
127
154
  backgroundDisabled: string;
128
- borderIdle: string;
129
- borderFocused: string;
130
- borderError: string;
131
- borderDisabled: string;
132
- borderWidth: number;
155
+ borderIdle: Record<FieldSide, string>;
156
+ borderFocused: Record<FieldSide, string>;
157
+ borderError: Record<FieldSide, string>;
158
+ borderDisabled: Record<FieldSide, string>;
159
+ borderWidth: Record<FieldSide, number>;
160
+ /** Single representative colour from `borderFocused` — first non-transparent side, bottom-priority. */
161
+ borderFocusedRepresentative: string;
133
162
  }
134
163
  /**
135
164
  * Resolve the full set of state-aware fill + border colours for a given
136
- * variant. Order: explicit override → theme token → library default.
165
+ * variant. Order: explicit override → theme token → library default. Border
166
+ * colour + width are normalized to per-side records here; callers that need
167
+ * a single colour read `borderFocusedRepresentative`.
137
168
  */
138
169
  export declare const resolveVariantColors: (theme: Theme, variant: FieldBaseVariant, override?: Partial<FieldVariantTokens>) => ResolvedFieldColors;
139
170
  export declare const FieldBase: React.FC<FieldBaseProps>;
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { TextInput } from 'react-native';
3
3
  import type { BlurEvent, FocusEvent, StyleProp, TextInputProps, TextStyle, ViewStyle } from 'react-native';
4
4
  export type InputSize = 'sm' | 'md' | 'lg';
5
- export type InputVariant = 'outlined' | 'filled';
5
+ export type InputVariant = 'outlined' | 'filled' | 'underline';
6
6
  export type InputLabelMode = 'float' | 'top';
7
7
  export interface InputProps extends Omit<TextInputProps, 'style'> {
8
8
  label?: string;
@@ -8,8 +8,8 @@ export { Banner } from './Banner';
8
8
  export type { BannerProps, BannerVariant, BannerAction } from './Banner';
9
9
  export { BottomNavigation } from './BottomNavigation';
10
10
  export type { BottomNavigationProps, TabConfig } from './BottomNavigation';
11
- export { BottomSheet } from './BottomSheet';
12
- export type { BottomSheetProps, BottomSheetRef } from './BottomSheet';
11
+ export { BottomSheet, useBottomSheet } from './BottomSheet';
12
+ export type { BottomSheetContextValue, BottomSheetProps, BottomSheetRef } from './BottomSheet';
13
13
  export { Button } from './Button';
14
14
  export type { ButtonProps, ButtonVariant, ButtonTone, ButtonSize } from './Button';
15
15
  export { Card } from './Card';
@@ -391,12 +391,33 @@ export interface FieldSizeTokens {
391
391
  borderRadius: number;
392
392
  iconSize: number;
393
393
  }
394
+ /**
395
+ * One of the four field-box sides. Used everywhere the library accepts
396
+ * per-side overrides for border width / colour.
397
+ */
398
+ export type FieldSide = 'top' | 'right' | 'bottom' | 'left';
399
+ /**
400
+ * Shorthand-or-longhand value. A bare `T` means "apply to all four sides";
401
+ * an object form lets a consumer set specific sides while the others fall
402
+ * through to whatever the resolver decided. Mirrors how React Native's
403
+ * `borderWidth` + `borderXWidth` (and CSS `border` + `border-X`) compose.
404
+ */
405
+ export type FieldPerSide<T> = T | Partial<Record<FieldSide, T>>;
406
+ /** Border colour value: a single colour string or a per-side colour map. */
407
+ export type FieldBorderColor = FieldPerSide<string>;
408
+ /** Border width value: a single width or a per-side width map. */
409
+ export type FieldBorderWidth = FieldPerSide<number>;
394
410
  /**
395
411
  * State-aware fill + border colours for a field variant. Resolution order
396
412
  * inside FieldBase: variant token → library default. Idle distinguishes
397
413
  * empty vs filled so consumers can tint the resting field differently once
398
414
  * the user has typed. When focused/error/disabled fills are omitted, the
399
415
  * box animates from the current idle fill to itself (no fill change).
416
+ *
417
+ * Border colour + width are `FieldPerSide` values — pass a bare string /
418
+ * number to set all four sides (the common case), or an object like
419
+ * `{ bottom: '#1A73E8' }` to draw only one side. Sides not specified in the
420
+ * object fall through to the shorthand default.
400
421
  */
401
422
  export interface FieldVariantTokens {
402
423
  backgroundIdleEmpty: string;
@@ -404,18 +425,20 @@ export interface FieldVariantTokens {
404
425
  backgroundFocused?: string;
405
426
  backgroundError?: string;
406
427
  backgroundDisabled: string;
407
- borderIdle: string;
408
- borderFocused: string;
409
- borderError: string;
410
- borderDisabled: string;
411
- borderWidth: number;
428
+ borderIdle: FieldBorderColor;
429
+ borderFocused: FieldBorderColor;
430
+ borderError: FieldBorderColor;
431
+ borderDisabled: FieldBorderColor;
432
+ borderWidth: FieldBorderWidth;
412
433
  }
413
434
  export interface FieldTokens extends Partial<Record<'sm' | 'md' | 'lg', FieldSizeTokens>> {
414
435
  /** Default `variant` when caller doesn't pass one. Library default: 'outlined'. */
415
- defaultVariant?: 'outlined' | 'filled';
436
+ defaultVariant?: 'outlined' | 'filled' | 'underline';
416
437
  /** State-aware fill + border colours per variant. */
417
438
  outlined?: Partial<FieldVariantTokens>;
418
439
  filled?: Partial<FieldVariantTokens>;
440
+ /** Bottom-border-only variant — Material-style underline inputs. */
441
+ underline?: Partial<FieldVariantTokens>;
419
442
  /** Text colour for the editable / displayed value. Defaults to `colors.text.primary`. */
420
443
  textColor?: string;
421
444
  /** Text colour when the field is disabled. Defaults to `colors.text.disabled`. */
@@ -439,7 +462,7 @@ export interface InputFillTokens {
439
462
  }
440
463
  export interface InputComponentTokens extends Partial<Record<ComponentSizeKey, InputSizeTokens>> {
441
464
  /** Default `variant` when caller doesn't pass one. Library default: 'outlined'. */
442
- defaultVariant?: 'outlined' | 'filled';
465
+ defaultVariant?: 'outlined' | 'filled' | 'underline';
443
466
  /** Default `labelMode` when caller doesn't pass one. Library default: 'float'. */
444
467
  defaultLabelMode?: 'float' | 'top';
445
468
  /** Corner radius in pixels for the field wrapper. Library default: size-driven (sm=10, md=12, lg=14). */
@@ -468,6 +491,7 @@ export interface InputComponentTokens extends Partial<Record<ComponentSizeKey, I
468
491
  fillColors?: {
469
492
  outlined?: InputFillTokens;
470
493
  filled?: InputFillTokens;
494
+ underline?: InputFillTokens;
471
495
  };
472
496
  }
473
497
  export interface BottomSheetTokens {
@@ -56,6 +56,23 @@ export interface BottomSheetProps {
56
56
  mode?: BottomSheetMode;
57
57
  handleIndicatorStyle?: StyleProp<ViewStyle>;
58
58
  containerStyle?: StyleProp<ViewStyle>;
59
+ /**
60
+ * Sticky region rendered between the drag handle and the scrollable content.
61
+ * Does not scroll with `children`. Can call `useBottomSheet()` to read sheet
62
+ * state.
63
+ */
64
+ header?: React.ReactNode;
65
+ /**
66
+ * Sticky region pinned to the bottom of the sheet, above the safe-area
67
+ * inset. Does not scroll with `children`, and rides up with the keyboard
68
+ * when `keyboardBehavior='shift'` (no extra wiring needed — the whole sheet
69
+ * shifts). Typical use: action button rows.
70
+ */
71
+ footer?: React.ReactNode;
72
+ /** Style applied to the header region wrapper. */
73
+ headerStyle?: StyleProp<ViewStyle>;
74
+ /** Style applied to the footer region wrapper. */
75
+ footerStyle?: StyleProp<ViewStyle>;
59
76
  children?: React.ReactNode;
60
77
  accessibilityLabel?: string;
61
78
  accessibilityViewIsModal?: boolean;
@@ -66,6 +83,30 @@ export interface BottomSheetRef {
66
83
  collapse: () => void;
67
84
  close: () => void;
68
85
  }
86
+ /**
87
+ * State + actions exposed to anything rendered inside a `<BottomSheet>` —
88
+ * including the `header` and `footer` slots. Read via `useBottomSheet()`.
89
+ *
90
+ * `snapIndex` is the JS-thread mirror of the current snap point. -1 means the
91
+ * sheet is closed (mid-close-animation included). Use it to drive footer
92
+ * button enable state, conditional headers, etc. If you need per-frame
93
+ * progress (e.g. fade a header in as the sheet expands), reach for the
94
+ * animated value via a future enhancement — not exposed today to keep the
95
+ * surface minimal.
96
+ */
97
+ export interface BottomSheetContextValue {
98
+ snapIndex: number;
99
+ snapPoints: number[];
100
+ expand: (index?: number) => void;
101
+ collapse: () => void;
102
+ close: () => void;
103
+ }
104
+ /**
105
+ * Access the enclosing `<BottomSheet>`'s state and imperative actions.
106
+ * Must be called from a component rendered inside a `<BottomSheet>` (as
107
+ * `children`, `header`, or `footer`).
108
+ */
109
+ export declare const useBottomSheet: () => BottomSheetContextValue;
69
110
  declare const BottomSheet: React.ForwardRefExoticComponent<BottomSheetProps & React.RefAttributes<BottomSheetRef>>;
70
111
  export { BottomSheet };
71
112
  export default BottomSheet;