@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
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = exports.BottomSheet = void 0;
6
+ exports.useBottomSheet = exports.default = exports.BottomSheet = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _reactNativeGestureHandler = require("react-native-gesture-handler");
@@ -32,6 +32,33 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
32
32
  * ref.current?.close(); // dismiss
33
33
  */
34
34
 
35
+ /**
36
+ * State + actions exposed to anything rendered inside a `<BottomSheet>` —
37
+ * including the `header` and `footer` slots. Read via `useBottomSheet()`.
38
+ *
39
+ * `snapIndex` is the JS-thread mirror of the current snap point. -1 means the
40
+ * sheet is closed (mid-close-animation included). Use it to drive footer
41
+ * button enable state, conditional headers, etc. If you need per-frame
42
+ * progress (e.g. fade a header in as the sheet expands), reach for the
43
+ * animated value via a future enhancement — not exposed today to keep the
44
+ * surface minimal.
45
+ */
46
+
47
+ const BottomSheetContext = /*#__PURE__*/(0, _react.createContext)(null);
48
+
49
+ /**
50
+ * Access the enclosing `<BottomSheet>`'s state and imperative actions.
51
+ * Must be called from a component rendered inside a `<BottomSheet>` (as
52
+ * `children`, `header`, or `footer`).
53
+ */
54
+ const useBottomSheet = () => {
55
+ const ctx = (0, _react.useContext)(BottomSheetContext);
56
+ if (!ctx) {
57
+ throw new Error('useBottomSheet must be used inside a <BottomSheet>.');
58
+ }
59
+ return ctx;
60
+ };
61
+ exports.useBottomSheet = useBottomSheet;
35
62
  const SPRING_CONFIG = {
36
63
  damping: 20,
37
64
  stiffness: 240,
@@ -51,6 +78,10 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
51
78
  mode = 'modal',
52
79
  handleIndicatorStyle,
53
80
  containerStyle,
81
+ header,
82
+ footer,
83
+ headerStyle,
84
+ footerStyle,
54
85
  children,
55
86
  accessibilityLabel,
56
87
  accessibilityViewIsModal,
@@ -336,6 +367,13 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
336
367
  if (!success) return;
337
368
  (0, _reactNativeReanimated.runOnJS)(handleBackdropPress)();
338
369
  }), [handleBackdropPress]);
370
+ const contextValue = (0, _react.useMemo)(() => ({
371
+ snapIndex: currentIndex,
372
+ snapPoints: resolvedSnapPoints,
373
+ expand,
374
+ collapse,
375
+ close
376
+ }), [currentIndex, resolvedSnapPoints, expand, collapse, close]);
339
377
 
340
378
  // Don't render the backdrop / sheet tree when the sheet is fully closed.
341
379
  // Inline: gated by `inlineMounted` (set in expand, cleared in
@@ -389,13 +427,23 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
389
427
  backgroundColor: theme.components.bottomSheet?.handleColor ?? theme.colors.border.primary
390
428
  }, handleIndicatorStyle]
391
429
  })
430
+ }), header != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
431
+ style: [styles.header, headerStyle],
432
+ children: header
392
433
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
393
434
  style: styles.content,
394
435
  children: children
436
+ }), footer != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
437
+ style: [styles.footer, footerStyle],
438
+ children: footer
395
439
  })]
396
440
  })
397
441
  })]
398
442
  });
443
+ const wrappedTree = /*#__PURE__*/(0, _jsxRuntime.jsx)(BottomSheetContext.Provider, {
444
+ value: contextValue,
445
+ children: sheetTree
446
+ });
399
447
  if (mode === 'modal') {
400
448
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
401
449
  transparent: true,
@@ -410,11 +458,11 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
410
458
  supportedOrientations: ['portrait', 'landscape'],
411
459
  children: _reactNative.Platform.OS === 'android' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureHandlerRootView, {
412
460
  style: styles.modalRoot,
413
- children: sheetTree
414
- }) : sheetTree
461
+ children: wrappedTree
462
+ }) : wrappedTree
415
463
  });
416
464
  }
417
- return sheetTree;
465
+ return wrappedTree;
418
466
  });
419
467
  BottomSheet.displayName = 'BottomSheet';
420
468
 
@@ -459,6 +507,15 @@ const buildStyles = _theme => _reactNative.StyleSheet.create({
459
507
  paddingTop: 10,
460
508
  paddingBottom: 8
461
509
  },
510
+ header: {
511
+ // No opinionated padding — slot owns its own styling. Sits between the
512
+ // drag handle and the scrollable content; does not flex.
513
+ },
514
+ footer: {
515
+ // Pinned to the bottom of the sheet (above safe-area inset, which lives
516
+ // on the sheet's paddingBottom). Does not flex, does not scroll with
517
+ // content. Rides with the keyboard via the sheet's `top` animation.
518
+ },
462
519
  handle: {
463
520
  width: 36,
464
521
  height: 4,
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "default", {
15
15
  return _BottomSheet.default;
16
16
  }
17
17
  });
18
+ Object.defineProperty(exports, "useBottomSheet", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _BottomSheet.useBottomSheet;
22
+ }
23
+ });
18
24
  var _BottomSheet = _interopRequireWildcard(require("./BottomSheet.js"));
19
25
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
20
26
  //# sourceMappingURL=index.js.map
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.resolveVariantColors = exports.resolveFieldTextStyle = exports.resolveFieldSize = exports.default = exports.FieldBase = void 0;
6
+ exports.resolveVariantColors = exports.resolveFieldTextStyle = exports.resolveFieldSize = exports.pickRepresentativeBorderColor = exports.default = exports.FieldBase = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _index = require("../../theme/index.js");
@@ -33,6 +33,66 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
33
33
  * - Shake-on-error or any cross-field animation.
34
34
  */
35
35
 
36
+ const SIDES = ['top', 'right', 'bottom', 'left'];
37
+
38
+ /** Map a single colour string to all four sides (shorthand expansion). */
39
+ const expandColorSides = value => {
40
+ if (typeof value === 'string') {
41
+ return {
42
+ top: value,
43
+ right: value,
44
+ bottom: value,
45
+ left: value
46
+ };
47
+ }
48
+ // Object form: any side the consumer omitted falls through to 'transparent'.
49
+ // Rationale: if a consumer says `{ bottom: '#000' }`, they want only the
50
+ // bottom drawn — the other sides should be invisible, not inherit some
51
+ // unrelated colour.
52
+ return {
53
+ top: value.top ?? 'transparent',
54
+ right: value.right ?? 'transparent',
55
+ bottom: value.bottom ?? 'transparent',
56
+ left: value.left ?? 'transparent'
57
+ };
58
+ };
59
+
60
+ /** Map a single width number to all four sides (shorthand expansion). */
61
+ const expandWidthSides = value => {
62
+ if (typeof value === 'number') {
63
+ return {
64
+ top: value,
65
+ right: value,
66
+ bottom: value,
67
+ left: value
68
+ };
69
+ }
70
+ return {
71
+ top: value.top ?? 0,
72
+ right: value.right ?? 0,
73
+ bottom: value.bottom ?? 0,
74
+ left: value.left ?? 0
75
+ };
76
+ };
77
+
78
+ /** Are all four sides of a per-side record equal? Used to collapse to shorthand. */
79
+ const allSidesEqual = sides => sides.top === sides.right && sides.right === sides.bottom && sides.bottom === sides.left;
80
+
81
+ /**
82
+ * Pick the most "representative" side colour from a 4-side record. Used by
83
+ * consumers (Input's selectionColor, focus rings, etc.) that need a single
84
+ * colour to mirror the visible focus indicator. Priority: bottom → top →
85
+ * left → right, skipping `'transparent'`. The bottom-first order means
86
+ * underline variants pick their underline colour automatically.
87
+ */
88
+ const pickRepresentativeBorderColor = sides => {
89
+ for (const side of ['bottom', 'top', 'left', 'right']) {
90
+ const c = sides[side];
91
+ if (c && c !== 'transparent') return c;
92
+ }
93
+ return sides.bottom;
94
+ };
95
+
36
96
  /**
37
97
  * Resolved text styling for the editable / displayed content inside a field.
38
98
  * Single source of truth so every field component (Input, NumberInput,
@@ -41,7 +101,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
41
101
  * same place. OTPInput intentionally overrides this with its own semibold
42
102
  * display weight.
43
103
  */
44
-
104
+ exports.pickRepresentativeBorderColor = pickRepresentativeBorderColor;
45
105
  /**
46
106
  * Resolve the canonical text style chunk for a field's value. Reads from
47
107
  * `theme.components.field.{textColor,disabledTextColor,placeholderColor,fontWeight}`
@@ -122,31 +182,70 @@ const DEFAULT_SIZES = {
122
182
 
123
183
  /**
124
184
  * Strict, fully-resolved colour set used internally. Mirrors `FieldVariantTokens`
125
- * but every field is non-optional every state has a concrete colour by the
126
- * time the box renders, so downstream code can rely on string-typed fills.
185
+ * but every border colour / width is expanded to a 4-side record by the time
186
+ * the box renders, so the animation pipeline can treat per-side and shorthand
187
+ * the same way. `borderFocusedRepresentative` is a single colour pulled from
188
+ * the focused sides — exposed for consumers (Input's selectionColor) that
189
+ * need to mirror the visible focus indicator without re-resolving per side.
127
190
  */
128
191
 
129
192
  /**
130
193
  * Resolve the full set of state-aware fill + border colours for a given
131
- * variant. Order: explicit override → theme token → library default.
194
+ * variant. Order: explicit override → theme token → library default. Border
195
+ * colour + width are normalized to per-side records here; callers that need
196
+ * a single colour read `borderFocusedRepresentative`.
132
197
  */
133
198
  const resolveVariantColors = (theme, variant, override) => {
134
199
  const tokenSet = theme.components.field?.[variant];
135
- const isOutlined = variant === 'outlined';
136
- const idleEmpty = isOutlined ? theme.colors.background.primary : theme.colors.background.secondary;
137
- const borderIdle = isOutlined ? theme.colors.border.primary : 'transparent';
138
- const borderWidth = isOutlined ? theme.colors.border.width : 0;
200
+
201
+ // Variant-specific defaults for resting fill + border treatment. `outlined`
202
+ // draws all four sides; `filled` draws nothing (fill-only); `underline`
203
+ // draws only the bottom edge Material-style single-line inputs.
204
+ let idleEmpty;
205
+ let borderIdleDefault;
206
+ let borderWidthDefault;
207
+ if (variant === 'outlined') {
208
+ idleEmpty = theme.colors.background.primary;
209
+ borderIdleDefault = theme.colors.border.primary;
210
+ borderWidthDefault = theme.colors.border.width;
211
+ } else if (variant === 'underline') {
212
+ idleEmpty = 'transparent';
213
+ borderIdleDefault = {
214
+ bottom: theme.colors.border.primary
215
+ };
216
+ borderWidthDefault = {
217
+ bottom: theme.colors.border.width
218
+ };
219
+ } else {
220
+ idleEmpty = theme.colors.background.secondary;
221
+ borderIdleDefault = 'transparent';
222
+ borderWidthDefault = 0;
223
+ }
224
+
225
+ // For focused/error/disabled we expand using the same per-side shape as
226
+ // idle. If `borderIdle` ended up bottom-only (underline), an unset
227
+ // `borderFocused` defaults to `{ bottom: colors.border.focus }` so the
228
+ // focus indicator stays on the same edge. Same logic for error.
229
+ const wrapToIdleShape = colour => typeof borderIdleDefault === 'string' ? colour : {
230
+ bottom: colour
231
+ };
232
+ const borderIdle = expandColorSides(override?.borderIdle ?? tokenSet?.borderIdle ?? borderIdleDefault);
233
+ const borderFocused = expandColorSides(override?.borderFocused ?? tokenSet?.borderFocused ?? wrapToIdleShape(theme.colors.border.focus));
234
+ const borderError = expandColorSides(override?.borderError ?? tokenSet?.borderError ?? wrapToIdleShape(theme.colors.border.error));
235
+ const borderDisabled = expandColorSides(override?.borderDisabled ?? tokenSet?.borderDisabled ?? borderIdleDefault);
236
+ const borderWidth = expandWidthSides(override?.borderWidth ?? tokenSet?.borderWidth ?? borderWidthDefault);
139
237
  return {
140
238
  backgroundIdleEmpty: override?.backgroundIdleEmpty ?? tokenSet?.backgroundIdleEmpty ?? idleEmpty,
141
239
  backgroundIdleFilled: override?.backgroundIdleFilled ?? tokenSet?.backgroundIdleFilled ?? override?.backgroundIdleEmpty ?? tokenSet?.backgroundIdleEmpty ?? idleEmpty,
142
240
  backgroundFocused: override?.backgroundFocused ?? tokenSet?.backgroundFocused ?? undefined,
143
241
  backgroundError: override?.backgroundError ?? tokenSet?.backgroundError ?? undefined,
144
242
  backgroundDisabled: override?.backgroundDisabled ?? tokenSet?.backgroundDisabled ?? theme.colors.surface.disabled,
145
- borderIdle: override?.borderIdle ?? tokenSet?.borderIdle ?? borderIdle,
146
- borderFocused: override?.borderFocused ?? tokenSet?.borderFocused ?? theme.colors.border.focus,
147
- borderError: override?.borderError ?? tokenSet?.borderError ?? theme.colors.border.error,
148
- borderDisabled: override?.borderDisabled ?? tokenSet?.borderDisabled ?? borderIdle,
149
- borderWidth: override?.borderWidth ?? tokenSet?.borderWidth ?? borderWidth
243
+ borderIdle,
244
+ borderFocused,
245
+ borderError,
246
+ borderDisabled,
247
+ borderWidth,
248
+ borderFocusedRepresentative: pickRepresentativeBorderColor(borderFocused)
150
249
  };
151
250
  };
152
251
  exports.resolveVariantColors = resolveVariantColors;
@@ -169,7 +268,15 @@ const FieldBase = props => {
169
268
  paddingHorizontal: paddingHorizontalProp,
170
269
  paddingVertical: paddingVerticalProp,
171
270
  borderRadius: borderRadiusProp,
271
+ borderTopLeftRadius: borderTopLeftRadiusProp,
272
+ borderTopRightRadius: borderTopRightRadiusProp,
273
+ borderBottomLeftRadius: borderBottomLeftRadiusProp,
274
+ borderBottomRightRadius: borderBottomRightRadiusProp,
172
275
  borderWidth: borderWidthProp,
276
+ borderTopWidth: borderTopWidthProp,
277
+ borderRightWidth: borderRightWidthProp,
278
+ borderBottomWidth: borderBottomWidthProp,
279
+ borderLeftWidth: borderLeftWidthProp,
173
280
  gap: gapProp,
174
281
  fillOverrides,
175
282
  style,
@@ -208,13 +315,6 @@ const FieldBase = props => {
208
315
  const idleFill = filled ? colors.backgroundIdleFilled : colors.backgroundIdleEmpty;
209
316
  const focusedFill = colors.backgroundFocused ?? idleFill;
210
317
  const errorFill = colors.backgroundError ?? idleFill;
211
- const animatedBorderColor = disabled ? colors.borderDisabled : error ? errorAnim.interpolate({
212
- inputRange: [0, 1],
213
- outputRange: [colors.borderIdle, colors.borderError]
214
- }) : focusAnim.interpolate({
215
- inputRange: [0, 1],
216
- outputRange: [colors.borderIdle, colors.borderFocused]
217
- });
218
318
  const animatedBackgroundColor = disabled ? colors.backgroundDisabled : error ? errorAnim.interpolate({
219
319
  inputRange: [0, 1],
220
320
  outputRange: [idleFill, errorFill]
@@ -222,17 +322,105 @@ const FieldBase = props => {
222
322
  inputRange: [0, 1],
223
323
  outputRange: [idleFill, focusedFill]
224
324
  });
325
+
326
+ // Per-side border colour. For each side we resolve the "from" (idle) and
327
+ // "to" (focused or error, depending on state) colour, then drive a single
328
+ // animated interpolation. When all four sides share the same from+to pair
329
+ // we collapse to one `borderColor` key — the common case stays cheap.
330
+ const activeColors = error ? colors.borderError : colors.borderFocused;
331
+ const activeAnim = error ? errorAnim : focusAnim;
332
+ const sideFrom = disabled ? colors.borderDisabled : colors.borderIdle;
333
+ const sideTo = disabled ? colors.borderDisabled : activeColors;
334
+
335
+ // Per-side widths: longhand prop wins over shorthand prop wins over
336
+ // resolved variant token. Each side independently.
337
+ const propWidth = {
338
+ top: borderTopWidthProp,
339
+ right: borderRightWidthProp,
340
+ bottom: borderBottomWidthProp,
341
+ left: borderLeftWidthProp
342
+ };
343
+ const resolvedWidths = {
344
+ top: propWidth.top ?? borderWidthProp ?? colors.borderWidth.top,
345
+ right: propWidth.right ?? borderWidthProp ?? colors.borderWidth.right,
346
+ bottom: propWidth.bottom ?? borderWidthProp ?? colors.borderWidth.bottom,
347
+ left: propWidth.left ?? borderWidthProp ?? colors.borderWidth.left
348
+ };
225
349
  const boxStyle = {
226
350
  minHeight: height ?? minHeightProp ?? sizeTokens.minHeight,
227
351
  maxHeight,
228
352
  paddingHorizontal: paddingHorizontalProp ?? sizeTokens.paddingHorizontal,
229
353
  paddingVertical: paddingVerticalProp ?? sizeTokens.paddingVertical,
230
- borderRadius: borderRadiusProp ?? sizeTokens.borderRadius,
231
- borderWidth: borderWidthProp ?? colors.borderWidth,
232
- borderColor: animatedBorderColor,
233
354
  backgroundColor: animatedBackgroundColor,
234
355
  columnGap: gapProp ?? theme.spacing.sm
235
356
  };
357
+
358
+ // Radius: per-corner prop wins over shorthand prop wins over size token.
359
+ // Collapse to single `borderRadius` when all four corners match.
360
+ const radiusShorthand = borderRadiusProp ?? sizeTokens.borderRadius;
361
+ const corners = {
362
+ tl: borderTopLeftRadiusProp ?? radiusShorthand,
363
+ tr: borderTopRightRadiusProp ?? radiusShorthand,
364
+ bl: borderBottomLeftRadiusProp ?? radiusShorthand,
365
+ br: borderBottomRightRadiusProp ?? radiusShorthand
366
+ };
367
+ if (corners.tl === corners.tr && corners.tr === corners.bl && corners.bl === corners.br) {
368
+ boxStyle.borderRadius = corners.tl;
369
+ } else {
370
+ boxStyle.borderTopLeftRadius = corners.tl;
371
+ boxStyle.borderTopRightRadius = corners.tr;
372
+ boxStyle.borderBottomLeftRadius = corners.bl;
373
+ boxStyle.borderBottomRightRadius = corners.br;
374
+ }
375
+
376
+ // Width: collapse to single `borderWidth` when all four sides match.
377
+ if (resolvedWidths.top === resolvedWidths.right && resolvedWidths.right === resolvedWidths.bottom && resolvedWidths.bottom === resolvedWidths.left) {
378
+ boxStyle.borderWidth = resolvedWidths.top;
379
+ } else {
380
+ boxStyle.borderTopWidth = resolvedWidths.top;
381
+ boxStyle.borderRightWidth = resolvedWidths.right;
382
+ boxStyle.borderBottomWidth = resolvedWidths.bottom;
383
+ boxStyle.borderLeftWidth = resolvedWidths.left;
384
+ }
385
+
386
+ // Colour: collapse to single `borderColor` only when both endpoints are
387
+ // identical across all sides. Otherwise emit four `borderXColor`
388
+ // interpolations. Disabled state is static (no animation needed).
389
+ const fromAllEqual = allSidesEqual(sideFrom);
390
+ const toAllEqual = allSidesEqual(sideTo);
391
+ if (disabled) {
392
+ if (fromAllEqual) {
393
+ boxStyle.borderColor = sideFrom.top;
394
+ } else {
395
+ boxStyle.borderTopColor = sideFrom.top;
396
+ boxStyle.borderRightColor = sideFrom.right;
397
+ boxStyle.borderBottomColor = sideFrom.bottom;
398
+ boxStyle.borderLeftColor = sideFrom.left;
399
+ }
400
+ } else if (fromAllEqual && toAllEqual && sideFrom.top === sideFrom.right && sideTo.top === sideTo.right) {
401
+ // Common case: shorthand on both ends → single interpolation.
402
+ boxStyle.borderColor = activeAnim.interpolate({
403
+ inputRange: [0, 1],
404
+ outputRange: [sideFrom.top, sideTo.top]
405
+ });
406
+ } else {
407
+ boxStyle.borderTopColor = activeAnim.interpolate({
408
+ inputRange: [0, 1],
409
+ outputRange: [sideFrom.top, sideTo.top]
410
+ });
411
+ boxStyle.borderRightColor = activeAnim.interpolate({
412
+ inputRange: [0, 1],
413
+ outputRange: [sideFrom.right, sideTo.right]
414
+ });
415
+ boxStyle.borderBottomColor = activeAnim.interpolate({
416
+ inputRange: [0, 1],
417
+ outputRange: [sideFrom.bottom, sideTo.bottom]
418
+ });
419
+ boxStyle.borderLeftColor = activeAnim.interpolate({
420
+ inputRange: [0, 1],
421
+ outputRange: [sideFrom.left, sideTo.left]
422
+ });
423
+ }
236
424
  if (width !== undefined) boxStyle.width = width;
237
425
  if (height !== undefined) boxStyle.height = height;
238
426
  const a11yState = {
@@ -168,7 +168,7 @@ const Input = exports.Input = /*#__PURE__*/(0, _react.forwardRef)((props, ref) =
168
168
  // Selection / caret / handle colour walks the same resolution chain as the
169
169
  // focused border so brands that override `components.field.<variant>.borderFocused`
170
170
  // get a matching selection tint without also having to update root `border.focus`.
171
- const selectionColor = (0, _react.useMemo)(() => (0, _FieldBase.resolveVariantColors)(theme, variant, fillOverrides).borderFocused, [theme, variant, fillOverrides]);
171
+ const selectionColor = (0, _react.useMemo)(() => (0, _FieldBase.resolveVariantColors)(theme, variant, fillOverrides).borderFocusedRepresentative, [theme, variant, fillOverrides]);
172
172
  const borderRadiusOverride = borderRadiusProp ?? theme.components.input?.borderRadius;
173
173
 
174
174
  // Floating label styles
@@ -40,7 +40,13 @@ const SearchBar = exports.SearchBar = /*#__PURE__*/(0, _react.forwardRef)((props
40
40
  // field-family token (`components.field.defaultVariant`) → library default.
41
41
  // Library default stays `'filled'` so SearchBar reads as the traditional
42
42
  // pill-shaped, borderless search box when no theme opinion is expressed.
43
- const variant = variantProp ?? theme.components.searchBar?.defaultVariant ?? theme.components.field?.defaultVariant ?? 'filled';
43
+ // SearchBar deliberately only supports 'filled' and 'outlined' an
44
+ // underline-only pill makes no visual sense. If the shared field default is
45
+ // 'underline', fall through to 'filled' rather than carry an unsupported
46
+ // value down to the variant token lookup.
47
+ const sharedDefault = theme.components.field?.defaultVariant;
48
+ const sharedFallback = sharedDefault === 'outlined' || sharedDefault === 'filled' ? sharedDefault : 'filled';
49
+ const variant = variantProp ?? theme.components.searchBar?.defaultVariant ?? sharedFallback;
44
50
  const fieldTokens = (0, _FieldBase.resolveFieldSize)(theme, size);
45
51
  const sizeStyles = {
46
52
  ...fieldTokens,
@@ -184,7 +190,7 @@ const SearchBar = exports.SearchBar = /*#__PURE__*/(0, _react.forwardRef)((props
184
190
  autoFocus: autoFocus,
185
191
  editable: !disabled,
186
192
  returnKeyType: "search",
187
- selectionColor: (0, _FieldBase.resolveVariantColors)(theme, variant).borderFocused,
193
+ selectionColor: (0, _FieldBase.resolveVariantColors)(theme, variant).borderFocusedRepresentative,
188
194
  accessibilityLabel: accessibilityLabel ?? placeholder,
189
195
  accessibilityState: {
190
196
  disabled
@@ -326,7 +326,7 @@ const Select = exports.Select = /*#__PURE__*/(0, _react.forwardRef)((props, ref)
326
326
  onChangeText: setQuery,
327
327
  placeholder: "Search\u2026",
328
328
  placeholderTextColor: theme.colors.text.tertiary,
329
- selectionColor: (0, _FieldBase.resolveVariantColors)(theme, theme.components.field?.defaultVariant ?? 'outlined').borderFocused,
329
+ selectionColor: (0, _FieldBase.resolveVariantColors)(theme, theme.components.field?.defaultVariant ?? 'outlined').borderFocusedRepresentative,
330
330
  accessibilityLabel: "Search options",
331
331
  style: [styles.searchInput, {
332
332
  color: theme.colors.text.primary,
@@ -345,6 +345,12 @@ Object.defineProperty(exports, "toast", {
345
345
  return _index41.toast;
346
346
  }
347
347
  });
348
+ Object.defineProperty(exports, "useBottomSheet", {
349
+ enumerable: true,
350
+ get: function () {
351
+ return _index6.useBottomSheet;
352
+ }
353
+ });
348
354
  Object.defineProperty(exports, "useReduceMotion", {
349
355
  enumerable: true,
350
356
  get: function () {
@@ -19,14 +19,40 @@
19
19
  * ref.current?.close(); // dismiss
20
20
  */
21
21
 
22
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
22
+ import React, { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
23
23
  import { Dimensions, Keyboard, Modal, Platform, StyleSheet, View } from 'react-native';
24
24
  import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
25
25
  import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
26
26
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
27
27
  import { useTheme } from "../../theme/index.js";
28
28
  import { triggerHaptic } from "../../utils/index.js";
29
+
30
+ /**
31
+ * State + actions exposed to anything rendered inside a `<BottomSheet>` —
32
+ * including the `header` and `footer` slots. Read via `useBottomSheet()`.
33
+ *
34
+ * `snapIndex` is the JS-thread mirror of the current snap point. -1 means the
35
+ * sheet is closed (mid-close-animation included). Use it to drive footer
36
+ * button enable state, conditional headers, etc. If you need per-frame
37
+ * progress (e.g. fade a header in as the sheet expands), reach for the
38
+ * animated value via a future enhancement — not exposed today to keep the
39
+ * surface minimal.
40
+ */
29
41
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
42
+ const BottomSheetContext = /*#__PURE__*/createContext(null);
43
+
44
+ /**
45
+ * Access the enclosing `<BottomSheet>`'s state and imperative actions.
46
+ * Must be called from a component rendered inside a `<BottomSheet>` (as
47
+ * `children`, `header`, or `footer`).
48
+ */
49
+ export const useBottomSheet = () => {
50
+ const ctx = useContext(BottomSheetContext);
51
+ if (!ctx) {
52
+ throw new Error('useBottomSheet must be used inside a <BottomSheet>.');
53
+ }
54
+ return ctx;
55
+ };
30
56
  const SPRING_CONFIG = {
31
57
  damping: 20,
32
58
  stiffness: 240,
@@ -46,6 +72,10 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
46
72
  mode = 'modal',
47
73
  handleIndicatorStyle,
48
74
  containerStyle,
75
+ header,
76
+ footer,
77
+ headerStyle,
78
+ footerStyle,
49
79
  children,
50
80
  accessibilityLabel,
51
81
  accessibilityViewIsModal,
@@ -331,6 +361,13 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
331
361
  if (!success) return;
332
362
  runOnJS(handleBackdropPress)();
333
363
  }), [handleBackdropPress]);
364
+ const contextValue = useMemo(() => ({
365
+ snapIndex: currentIndex,
366
+ snapPoints: resolvedSnapPoints,
367
+ expand,
368
+ collapse,
369
+ close
370
+ }), [currentIndex, resolvedSnapPoints, expand, collapse, close]);
334
371
 
335
372
  // Don't render the backdrop / sheet tree when the sheet is fully closed.
336
373
  // Inline: gated by `inlineMounted` (set in expand, cleared in
@@ -384,13 +421,23 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
384
421
  backgroundColor: theme.components.bottomSheet?.handleColor ?? theme.colors.border.primary
385
422
  }, handleIndicatorStyle]
386
423
  })
424
+ }), header != null && /*#__PURE__*/_jsx(View, {
425
+ style: [styles.header, headerStyle],
426
+ children: header
387
427
  }), /*#__PURE__*/_jsx(View, {
388
428
  style: styles.content,
389
429
  children: children
430
+ }), footer != null && /*#__PURE__*/_jsx(View, {
431
+ style: [styles.footer, footerStyle],
432
+ children: footer
390
433
  })]
391
434
  })
392
435
  })]
393
436
  });
437
+ const wrappedTree = /*#__PURE__*/_jsx(BottomSheetContext.Provider, {
438
+ value: contextValue,
439
+ children: sheetTree
440
+ });
394
441
  if (mode === 'modal') {
395
442
  return /*#__PURE__*/_jsx(Modal, {
396
443
  transparent: true,
@@ -405,11 +452,11 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
405
452
  supportedOrientations: ['portrait', 'landscape'],
406
453
  children: Platform.OS === 'android' ? /*#__PURE__*/_jsx(GestureHandlerRootView, {
407
454
  style: styles.modalRoot,
408
- children: sheetTree
409
- }) : sheetTree
455
+ children: wrappedTree
456
+ }) : wrappedTree
410
457
  });
411
458
  }
412
- return sheetTree;
459
+ return wrappedTree;
413
460
  });
414
461
  BottomSheet.displayName = 'BottomSheet';
415
462
 
@@ -454,6 +501,15 @@ const buildStyles = _theme => StyleSheet.create({
454
501
  paddingTop: 10,
455
502
  paddingBottom: 8
456
503
  },
504
+ header: {
505
+ // No opinionated padding — slot owns its own styling. Sits between the
506
+ // drag handle and the scrollable content; does not flex.
507
+ },
508
+ footer: {
509
+ // Pinned to the bottom of the sheet (above safe-area inset, which lives
510
+ // on the sheet's paddingBottom). Does not flex, does not scroll with
511
+ // content. Rides with the keyboard via the sheet's `top` animation.
512
+ },
457
513
  handle: {
458
514
  width: 36,
459
515
  height: 4,
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export { BottomSheet, default } from "./BottomSheet.js";
3
+ export { BottomSheet, default, useBottomSheet } from "./BottomSheet.js";
4
4
  //# sourceMappingURL=index.js.map