@mrmeg/expo-ui 0.27.1 → 0.29.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 (155) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LLM_USAGE.md +241 -37
  3. package/README.md +591 -136
  4. package/bin/expo-ui-icons.mjs +509 -0
  5. package/dist/components/Accordion.js +12 -6
  6. package/dist/components/AnimatedView.d.ts +1 -1
  7. package/dist/components/AnimatedView.js +5 -2
  8. package/dist/components/Avatar.d.ts +1 -1
  9. package/dist/components/Avatar.js +2 -2
  10. package/dist/components/Badge.js +5 -1
  11. package/dist/components/BottomSheet.d.ts +29 -1
  12. package/dist/components/BottomSheet.js +107 -30
  13. package/dist/components/Button.d.ts +20 -4
  14. package/dist/components/Button.js +60 -43
  15. package/dist/components/Card.d.ts +1 -1
  16. package/dist/components/Card.js +24 -4
  17. package/dist/components/Carousel.js +2 -2
  18. package/dist/components/Checkbox.js +51 -44
  19. package/dist/components/Collapsible.js +7 -3
  20. package/dist/components/Dialog.js +87 -16
  21. package/dist/components/Drawer.d.ts +1 -1
  22. package/dist/components/Drawer.js +65 -55
  23. package/dist/components/DropdownMenu.js +11 -7
  24. package/dist/components/EmptyState.d.ts +2 -2
  25. package/dist/components/EmptyState.js +5 -2
  26. package/dist/components/Hydrated.d.ts +31 -0
  27. package/dist/components/Hydrated.js +30 -0
  28. package/dist/components/Icon.d.ts +71 -28
  29. package/dist/components/Icon.js +70 -28
  30. package/dist/components/IconGlyph.d.ts +15 -0
  31. package/dist/components/IconGlyph.js +32 -0
  32. package/dist/components/IconGlyph.native.d.ts +10 -0
  33. package/dist/components/IconGlyph.native.js +30 -0
  34. package/dist/components/InputOTP.js +5 -2
  35. package/dist/components/Item.d.ts +103 -4
  36. package/dist/components/Item.js +168 -15
  37. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  38. package/dist/components/KeyboardAvoidingView.js +4 -1
  39. package/dist/components/Label.js +3 -2
  40. package/dist/components/MaxWidthContainer.js +1 -1
  41. package/dist/components/Notification.js +45 -27
  42. package/dist/components/Popover.d.ts +18 -5
  43. package/dist/components/Popover.js +72 -8
  44. package/dist/components/Progress.js +8 -6
  45. package/dist/components/RadioGroup.js +52 -49
  46. package/dist/components/Screen.d.ts +61 -0
  47. package/dist/components/Screen.js +62 -0
  48. package/dist/components/SectionHeader.d.ts +1 -1
  49. package/dist/components/SegmentedControl.d.ts +12 -4
  50. package/dist/components/SegmentedControl.js +94 -4
  51. package/dist/components/Select.js +21 -26
  52. package/dist/components/Skeleton.js +24 -10
  53. package/dist/components/Slider.d.ts +11 -3
  54. package/dist/components/Slider.js +73 -6
  55. package/dist/components/StatCard.d.ts +4 -2
  56. package/dist/components/StatCard.js +3 -1
  57. package/dist/components/StyledText.d.ts +7 -1
  58. package/dist/components/StyledText.js +5 -3
  59. package/dist/components/Switch.d.ts +1 -1
  60. package/dist/components/Switch.js +20 -30
  61. package/dist/components/Tabs.d.ts +2 -2
  62. package/dist/components/Tabs.js +13 -24
  63. package/dist/components/TextInput.d.ts +1 -1
  64. package/dist/components/TextInput.js +71 -26
  65. package/dist/components/ThemeSelector.d.ts +52 -0
  66. package/dist/components/ThemeSelector.js +90 -0
  67. package/dist/components/Toggle.d.ts +5 -49
  68. package/dist/components/Toggle.js +53 -29
  69. package/dist/components/ToggleGroup.d.ts +4 -8
  70. package/dist/components/ToggleGroup.js +52 -34
  71. package/dist/components/Tooltip.d.ts +1 -1
  72. package/dist/components/Tooltip.js +9 -3
  73. package/dist/components/UIProvider.d.ts +22 -2
  74. package/dist/components/UIProvider.js +15 -3
  75. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  76. package/dist/components/bottomSheetDismiss.js +44 -0
  77. package/dist/components/iconBuiltins.generated.d.ts +143 -0
  78. package/dist/components/iconBuiltins.generated.js +25 -0
  79. package/dist/components/iconTypes.d.ts +37 -0
  80. package/dist/components/iconTypes.js +1 -0
  81. package/dist/components/index.d.ts +47 -44
  82. package/dist/components/index.js +3 -0
  83. package/dist/components/keyboardDismiss.js +12 -4
  84. package/dist/components/nativeTextField.android.js +13 -9
  85. package/dist/constants/colors.d.ts +40 -5
  86. package/dist/constants/colors.js +26 -8
  87. package/dist/constants/fonts.d.ts +41 -2
  88. package/dist/constants/fonts.js +46 -7
  89. package/dist/constants/index.d.ts +5 -4
  90. package/dist/constants/index.js +1 -0
  91. package/dist/constants/interaction.d.ts +18 -0
  92. package/dist/constants/interaction.js +18 -0
  93. package/dist/constants/spacing.d.ts +2 -1
  94. package/dist/constants/spacing.js +2 -1
  95. package/dist/design-system.json +429 -132
  96. package/dist/hooks/index.d.ts +12 -7
  97. package/dist/hooks/index.js +5 -0
  98. package/dist/hooks/useDimensions.d.ts +13 -8
  99. package/dist/hooks/useDimensions.js +123 -48
  100. package/dist/hooks/useFocusVisible.d.ts +30 -0
  101. package/dist/hooks/useFocusVisible.js +45 -0
  102. package/dist/hooks/useFontStyle.d.ts +2 -2
  103. package/dist/hooks/useFontStyle.js +3 -2
  104. package/dist/hooks/useHydrated.d.ts +13 -0
  105. package/dist/hooks/useHydrated.js +19 -0
  106. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  107. package/dist/hooks/useNotificationOffset.js +34 -0
  108. package/dist/hooks/useResources.d.ts +21 -20
  109. package/dist/hooks/useResources.js +72 -68
  110. package/dist/hooks/useScalePress.d.ts +8 -5
  111. package/dist/hooks/useScalePress.js +16 -9
  112. package/dist/hooks/useShape.d.ts +24 -0
  113. package/dist/hooks/useShape.js +24 -0
  114. package/dist/hooks/useStaggeredEntrance.js +9 -7
  115. package/dist/hooks/useTheme.d.ts +3 -3
  116. package/dist/hooks/useTheme.js +177 -66
  117. package/dist/hooks/useWindowInsets.d.ts +13 -0
  118. package/dist/hooks/useWindowInsets.js +23 -0
  119. package/dist/icons/LICENSE.lucide +43 -0
  120. package/dist/icons/lucide.json +2144 -0
  121. package/dist/index.d.ts +5 -5
  122. package/dist/lib/composeRefs.d.ts +8 -0
  123. package/dist/lib/composeRefs.js +20 -0
  124. package/dist/lib/haptics.d.ts +12 -0
  125. package/dist/lib/haptics.js +21 -0
  126. package/dist/lib/index.d.ts +6 -5
  127. package/dist/lib/index.js +1 -0
  128. package/dist/lib/interFonts.d.ts +15 -0
  129. package/dist/lib/interFonts.js +1 -0
  130. package/dist/lib/interFonts.native.d.ts +3 -0
  131. package/dist/lib/interFonts.native.js +20 -0
  132. package/dist/lib/popoverPlacement.d.ts +36 -0
  133. package/dist/lib/popoverPlacement.js +29 -0
  134. package/dist/lib/themedStyles.d.ts +1 -1
  135. package/dist/lib/useAnimatedValue.d.ts +15 -0
  136. package/dist/lib/useAnimatedValue.js +19 -0
  137. package/dist/lib/webHitTarget.d.ts +40 -0
  138. package/dist/lib/webHitTarget.js +37 -0
  139. package/dist/state/feedbackStore.d.ts +24 -0
  140. package/dist/state/feedbackStore.js +16 -0
  141. package/dist/state/globalUIStore.d.ts +27 -0
  142. package/dist/state/globalUIStore.js +23 -0
  143. package/dist/state/index.d.ts +7 -5
  144. package/dist/state/index.js +2 -0
  145. package/dist/state/initialScheme.d.ts +27 -0
  146. package/dist/state/initialScheme.js +29 -0
  147. package/dist/state/notify.d.ts +1 -1
  148. package/dist/state/themeColorScope.d.ts +1 -1
  149. package/dist/state/themeStore.d.ts +69 -16
  150. package/dist/state/themeStore.js +65 -38
  151. package/llms-full.md +59 -10
  152. package/llms.txt +10 -1
  153. package/package.json +79 -4
  154. package/dist/components/iconRegistry.generated.d.ts +0 -148
  155. package/dist/components/iconRegistry.generated.js +0 -296
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export * from "./components";
2
- export * from "./constants";
3
- export * from "./hooks";
4
- export * from "./state";
5
- export * from "./lib";
1
+ export * from "./components/index.js";
2
+ export * from "./constants/index.js";
3
+ export * from "./hooks/index.js";
4
+ export * from "./state/index.js";
5
+ export * from "./lib/index.js";
@@ -0,0 +1,8 @@
1
+ import type { Ref, RefCallback } from "react";
2
+ /**
3
+ * One ref callback that feeds every ref given — object refs (`.current`) and
4
+ * callback refs alike, `null`/`undefined` skipped. For a component that needs
5
+ * the host node itself (e.g. to set a DOM attribute) while its consumer, or an
6
+ * `asChild` slot, also passes a `ref`.
7
+ */
8
+ export declare function composeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * One ref callback that feeds every ref given — object refs (`.current`) and
3
+ * callback refs alike, `null`/`undefined` skipped. For a component that needs
4
+ * the host node itself (e.g. to set a DOM attribute) while its consumer, or an
5
+ * `asChild` slot, also passes a `ref`.
6
+ */
7
+ export function composeRefs(...refs) {
8
+ return (node) => {
9
+ for (const ref of refs) {
10
+ if (!ref)
11
+ continue;
12
+ if (typeof ref === "function") {
13
+ ref(node);
14
+ }
15
+ else {
16
+ ref.current = node;
17
+ }
18
+ }
19
+ };
20
+ }
@@ -1,3 +1,15 @@
1
+ /** A light tap, unconditionally. Prefer `hapticPress` / `hapticSelection`. */
1
2
  export declare function hapticLight(): void;
2
3
  export declare function hapticMedium(): void;
3
4
  export declare function hapticSuccess(): void;
5
+ /**
6
+ * The tap a control fires when its state changes (Switch, Checkbox, Toggle,
7
+ * ToggleGroup, SegmentedControl). Fires under the `"selection"` (default) and
8
+ * `"all"` settings; silent under `"off"`.
9
+ */
10
+ export declare function hapticSelection(): void;
11
+ /**
12
+ * The tap a pressable fires on press-in (Button, pressable Card and Item,
13
+ * `useScalePress` consumers). Fires only under the `"all"` setting.
14
+ */
15
+ export declare function hapticPress(): void;
@@ -1,4 +1,5 @@
1
1
  import { Platform } from "react-native";
2
+ import { useFeedbackStore } from "../state/feedbackStore.js";
2
3
  function runHaptic(fn) {
3
4
  if (Platform.OS === "web")
4
5
  return;
@@ -9,6 +10,7 @@ function runHaptic(fn) {
9
10
  // Haptics not available
10
11
  }
11
12
  }
13
+ /** A light tap, unconditionally. Prefer `hapticPress` / `hapticSelection`. */
12
14
  export function hapticLight() {
13
15
  runHaptic(() => {
14
16
  const H = require("expo-haptics");
@@ -27,3 +29,22 @@ export function hapticSuccess() {
27
29
  H.notificationAsync(H.NotificationFeedbackType.Success);
28
30
  });
29
31
  }
32
+ /**
33
+ * The tap a control fires when its state changes (Switch, Checkbox, Toggle,
34
+ * ToggleGroup, SegmentedControl). Fires under the `"selection"` (default) and
35
+ * `"all"` settings; silent under `"off"`.
36
+ */
37
+ export function hapticSelection() {
38
+ if (useFeedbackStore.getState().haptics === "off")
39
+ return;
40
+ hapticLight();
41
+ }
42
+ /**
43
+ * The tap a pressable fires on press-in (Button, pressable Card and Item,
44
+ * `useScalePress` consumers). Fires only under the `"all"` setting.
45
+ */
46
+ export function hapticPress() {
47
+ if (useFeedbackStore.getState().haptics !== "all")
48
+ return;
49
+ hapticLight();
50
+ }
@@ -1,5 +1,6 @@
1
- export * from "./animations";
2
- export * from "./haptics";
3
- export * from "./i18n";
4
- export * from "./stateSurface";
5
- export * from "./themedStyles";
1
+ export * from "./animations.js";
2
+ export * from "./composeRefs.js";
3
+ export * from "./haptics.js";
4
+ export * from "./i18n.js";
5
+ export * from "./stateSurface.js";
6
+ export * from "./themedStyles.js";
package/dist/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./animations.js";
2
+ export * from "./composeRefs.js";
2
3
  export * from "./haptics.js";
3
4
  export * from "./i18n.js";
4
5
  export * from "./stateSurface.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The packaged Inter faces `useResources` registers through `expo-font` —
3
+ * web variant: none.
4
+ *
5
+ * Web loads Inter as one multi-weight CSS family from Google Fonts (see
6
+ * `useResources`), and `constants/fonts.ts` points every web weight at that
7
+ * family, so no static `.ttf` would ever be referenced. Importing one here
8
+ * would still make the bundler emit it as a web asset, so this variant imports
9
+ * nothing. Metro resolves `interFonts.native.ts` on iOS and Android instead.
10
+ *
11
+ * Private module: not re-exported from `lib/index.ts`, and `lib/*` is not an
12
+ * exported subpath, so consumers can't reach it.
13
+ */
14
+ export type InterFontMap = Readonly<Record<string, number>>;
15
+ export declare const interFontMap: InterFontMap | null;
@@ -0,0 +1 @@
1
+ export const interFontMap = null;
@@ -0,0 +1,3 @@
1
+ import type { InterFontMap } from "./interFonts.js";
2
+ export type { InterFontMap };
3
+ export declare const interFontMap: InterFontMap | null;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The packaged Inter faces `useResources` registers through `expo-font` —
3
+ * native variant.
4
+ *
5
+ * Exactly the four static weights the native `sansSerif` family keys point at
6
+ * (see `constants/fonts.ts`), each imported from its own per-weight subpath.
7
+ * The `@expo-google-fonts/inter` root entry `require`s all 18 files (every
8
+ * weight, plus italics), and Metro ships every file a bundle requires, so
9
+ * importing it added 14 unused faces (~4.8 MB) to every native app.
10
+ */
11
+ import { Inter_400Regular } from "@expo-google-fonts/inter/400Regular";
12
+ import { Inter_500Medium } from "@expo-google-fonts/inter/500Medium";
13
+ import { Inter_600SemiBold } from "@expo-google-fonts/inter/600SemiBold";
14
+ import { Inter_700Bold } from "@expo-google-fonts/inter/700Bold";
15
+ export const interFontMap = {
16
+ Inter_400Regular,
17
+ Inter_500Medium,
18
+ Inter_600SemiBold,
19
+ Inter_700Bold,
20
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Side and height cap for a native popover.
3
+ *
4
+ * `@rn-primitives/popover` places native content on the requested side and,
5
+ * when it does not fit, clamps it to the screen edge instead of flipping, so an
6
+ * oversized popover covers its own trigger or sits under the system bars.
7
+ * `PopoverContent` resolves the side and a height cap here first; the content
8
+ * then scrolls inside the cap. Web never calls this: Radix flips on its own.
9
+ */
10
+ export type PopoverSide = "top" | "bottom";
11
+ export interface PopoverPlacementInput {
12
+ /** Requested side; undefined means the primitive's default, `"bottom"`. */
13
+ side: PopoverSide | undefined;
14
+ sideOffset: number;
15
+ /** The trigger's measured screen frame, or null until the primitive measures it. */
16
+ trigger: {
17
+ pageY: number;
18
+ height: number;
19
+ } | null;
20
+ screenHeight: number;
21
+ insets: {
22
+ top?: number;
23
+ bottom?: number;
24
+ };
25
+ /** The content's uncapped height, card chrome included; undefined while unknown. */
26
+ naturalHeight: number | undefined;
27
+ avoidCollisions: boolean;
28
+ }
29
+ export interface PopoverPlacement {
30
+ side: PopoverSide;
31
+ /** Undefined when the popover is not capped. */
32
+ maxHeight: number | undefined;
33
+ }
34
+ /** Smallest cap; with less room than this the primitive's inset clamp places the card. */
35
+ export declare const POPOVER_MIN_CAP = 120;
36
+ export declare function resolvePopoverPlacement({ side, sideOffset, trigger, screenHeight, insets, naturalHeight, avoidCollisions, }: PopoverPlacementInput): PopoverPlacement;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Side and height cap for a native popover.
3
+ *
4
+ * `@rn-primitives/popover` places native content on the requested side and,
5
+ * when it does not fit, clamps it to the screen edge instead of flipping, so an
6
+ * oversized popover covers its own trigger or sits under the system bars.
7
+ * `PopoverContent` resolves the side and a height cap here first; the content
8
+ * then scrolls inside the cap. Web never calls this: Radix flips on its own.
9
+ */
10
+ /** Smallest cap; with less room than this the primitive's inset clamp places the card. */
11
+ export const POPOVER_MIN_CAP = 120;
12
+ export function resolvePopoverPlacement({ side, sideOffset, trigger, screenHeight, insets, naturalHeight, avoidCollisions, }) {
13
+ const requested = side ?? "bottom";
14
+ if (!avoidCollisions || !trigger) {
15
+ return { side: requested, maxHeight: undefined };
16
+ }
17
+ const room = {
18
+ top: trigger.pageY - sideOffset - (insets.top ?? 0),
19
+ bottom: screenHeight - (insets.bottom ?? 0) - (trigger.pageY + trigger.height + sideOffset),
20
+ };
21
+ const opposite = requested === "top" ? "bottom" : "top";
22
+ // Without the natural height (not measured yet, or content that scrolls
23
+ // itself) there is nothing to compare, so only cap the requested side.
24
+ const flips = naturalHeight !== undefined &&
25
+ naturalHeight > room[requested] &&
26
+ room[opposite] > room[requested];
27
+ const chosen = flips ? opposite : requested;
28
+ return { side: chosen, maxHeight: Math.max(room[chosen], POPOVER_MIN_CAP) };
29
+ }
@@ -1,4 +1,4 @@
1
- import { type Theme } from "../constants/colors";
1
+ import { type Theme } from "../constants/colors.js";
2
2
  /**
3
3
  * Registers theme-dependent styles at module scope so react-native-web
4
4
  * inserts their CSS rules into the sheet before the prerendered document
@@ -0,0 +1,15 @@
1
+ import { Animated } from "react-native";
2
+ /**
3
+ * An `Animated.Value` created once, on the first render, and stable for the
4
+ * component's lifetime — React Native's `useAnimatedValue`, which
5
+ * react-native-web 0.21 does not export.
6
+ *
7
+ * Replaces `useRef(new Animated.Value(x)).current`, which built and discarded
8
+ * a value on every render and read a ref during render, so the React Compiler
9
+ * skipped the whole component. A lazy state initializer is allocated once and
10
+ * returns a plain value the compiler can reason about. Like the ref version,
11
+ * only the first render's `initialValue` is used.
12
+ *
13
+ * Private to the package: not re-exported from `lib/index.ts`.
14
+ */
15
+ export declare function useAnimatedValue(initialValue: number): Animated.Value;
@@ -0,0 +1,19 @@
1
+ import { useState } from "react";
2
+ import { Animated } from "react-native";
3
+ /**
4
+ * An `Animated.Value` created once, on the first render, and stable for the
5
+ * component's lifetime — React Native's `useAnimatedValue`, which
6
+ * react-native-web 0.21 does not export.
7
+ *
8
+ * Replaces `useRef(new Animated.Value(x)).current`, which built and discarded
9
+ * a value on every render and read a ref during render, so the React Compiler
10
+ * skipped the whole component. A lazy state initializer is allocated once and
11
+ * returns a plain value the compiler can reason about. Like the ref version,
12
+ * only the first render's `initialValue` is used.
13
+ *
14
+ * Private to the package: not re-exported from `lib/index.ts`.
15
+ */
16
+ export function useAnimatedValue(initialValue) {
17
+ const [value] = useState(() => new Animated.Value(initialValue));
18
+ return value;
19
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Web pointer targets. react-native-web ignores `hitSlop`, so a control whose
3
+ * drawn box is shorter than `spacing.touchTarget` (44) grows its target on web
4
+ * with a transparent, absolutely positioned child inside the drawn box. The
5
+ * Pressable root keeps its rect (popover and tooltip anchors do not move),
6
+ * layout does not change, and a click on the child bubbles to the root.
7
+ *
8
+ * The default extends vertically only: two controls in an 8pt-gap row must
9
+ * never share a hit region. A caller `hitSlop` is used as given, like native.
10
+ * Used by `Button`, `Toggle` and `ToggleGroup` items.
11
+ */
12
+ import React from "react";
13
+ import { type PressableProps } from "react-native";
14
+ export type HitInsets = {
15
+ top: number;
16
+ bottom: number;
17
+ left: number;
18
+ right: number;
19
+ };
20
+ /** Slop per side that brings `height` up to `spacing.touchTarget`; 0 when it already is. */
21
+ export declare const verticalSlopFor: (height: number) => number;
22
+ /**
23
+ * Web insets for the hit extender: the default vertical slop for `height`, or
24
+ * a caller `hitSlop` (a number on all four sides, insets as given, missing
25
+ * sides 0). `null` when nothing extends (`hitSlop={0}` opts out).
26
+ */
27
+ export declare function getWebHitInsets(hitSlop: PressableProps["hitSlop"], height: number): HitInsets | null;
28
+ interface WebHitTargetProps {
29
+ insets: HitInsets | null;
30
+ /** The drawn box's border width: an absolute child sits against the padding box, inside the border. */
31
+ border?: number;
32
+ testID?: string;
33
+ }
34
+ /**
35
+ * The transparent hit extender. Render it as the last child of the drawn box
36
+ * (so it paints above the content); nothing on native or when `insets` is null.
37
+ * Hidden from assistive tech: the control itself is the accessible element.
38
+ */
39
+ export declare function WebHitTarget({ insets, border, testID }: WebHitTargetProps): React.JSX.Element | null;
40
+ export {};
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Platform, View } from "react-native";
3
+ import { spacing } from "../constants/spacing.js";
4
+ /** Slop per side that brings `height` up to `spacing.touchTarget`; 0 when it already is. */
5
+ export const verticalSlopFor = (height) => Math.ceil(Math.max(0, spacing.touchTarget - height) / 2);
6
+ /**
7
+ * Web insets for the hit extender: the default vertical slop for `height`, or
8
+ * a caller `hitSlop` (a number on all four sides, insets as given, missing
9
+ * sides 0). `null` when nothing extends (`hitSlop={0}` opts out).
10
+ */
11
+ export function getWebHitInsets(hitSlop, height) {
12
+ if (hitSlop === undefined || hitSlop === null) {
13
+ const slop = verticalSlopFor(height);
14
+ return slop > 0 ? { top: slop, bottom: slop, left: 0, right: 0 } : null;
15
+ }
16
+ const insets = typeof hitSlop === "number"
17
+ ? { top: hitSlop, bottom: hitSlop, left: hitSlop, right: hitSlop }
18
+ : { top: hitSlop.top ?? 0, bottom: hitSlop.bottom ?? 0, left: hitSlop.left ?? 0, right: hitSlop.right ?? 0 };
19
+ return insets.top || insets.bottom || insets.left || insets.right ? insets : null;
20
+ }
21
+ /**
22
+ * The transparent hit extender. Render it as the last child of the drawn box
23
+ * (so it paints above the content); nothing on native or when `insets` is null.
24
+ * Hidden from assistive tech: the control itself is the accessible element.
25
+ */
26
+ export function WebHitTarget({ insets, border = 0, testID = "hit-target" }) {
27
+ if (Platform.OS !== "web" || !insets)
28
+ return null;
29
+ return (_jsx(View, { testID: testID, "aria-hidden": true, importantForAccessibility: "no-hide-descendants", focusable: false, style: {
30
+ position: "absolute",
31
+ // `0 - n` keeps a 0 inset at +0 (a unary minus would give -0).
32
+ top: 0 - (insets.top + border),
33
+ bottom: 0 - (insets.bottom + border),
34
+ left: 0 - (insets.left + border),
35
+ right: 0 - (insets.right + border),
36
+ } }));
37
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Which interactions may vibrate.
3
+ *
4
+ * - `"off"`: never.
5
+ * - `"selection"` (default): state changes — Switch, Checkbox, Toggle,
6
+ * ToggleGroup, SegmentedControl.
7
+ * - `"all"`: selection plus a light tap on press for Button, pressable Card
8
+ * and Item, and every `useScalePress` consumer that has not opted out.
9
+ *
10
+ * Web never vibrates regardless of the setting.
11
+ */
12
+ export type HapticsSetting = "off" | "selection" | "all";
13
+ export type FeedbackStore = {
14
+ haptics: HapticsSetting;
15
+ setHaptics: (setting: HapticsSetting) => void;
16
+ };
17
+ /**
18
+ * Feedback settings the package's controls consult at event time. Set it
19
+ * through `UIProvider`'s `haptics` prop or `setHaptics()`; read it in a
20
+ * component with `useFeedbackStore((s) => s.haptics)`.
21
+ */
22
+ export declare const useFeedbackStore: import("zustand").UseBoundStore<import("zustand").StoreApi<FeedbackStore>>;
23
+ /** Imperative form of `UIProvider`'s `haptics` prop, for app startup code. */
24
+ export declare function setHaptics(setting: HapticsSetting): void;
@@ -0,0 +1,16 @@
1
+ import { create } from "zustand";
2
+ /**
3
+ * Feedback settings the package's controls consult at event time. Set it
4
+ * through `UIProvider`'s `haptics` prop or `setHaptics()`; read it in a
5
+ * component with `useFeedbackStore((s) => s.haptics)`.
6
+ */
7
+ export const useFeedbackStore = create((set) => ({
8
+ haptics: "selection",
9
+ setHaptics: (setting) => {
10
+ set({ haptics: setting });
11
+ },
12
+ }));
13
+ /** Imperative form of `UIProvider`'s `haptics` prop, for app startup code. */
14
+ export function setHaptics(setting) {
15
+ useFeedbackStore.getState().setHaptics(setting);
16
+ }
@@ -7,6 +7,8 @@
7
7
  * Methods:
8
8
  * - show({ type, title, messages, duration, loading, action }): displays a notification
9
9
  * - hide(): hides the current notification
10
+ * - setNotificationOffset(key, { top, bottom }) / clearNotificationOffset(key):
11
+ * keep toasts clear of a tab bar or floating header (use `useNotificationOffset`)
10
12
  *
11
13
  * Notifications auto-dismiss after `DEFAULT_NOTIFICATION_DURATION` unless a
12
14
  * `duration` is given. Pass `duration: 0` to keep one up until dismissed;
@@ -35,11 +37,36 @@ export type GlobalNotificationAlert = {
35
37
  position?: GlobalNotificationPosition;
36
38
  action?: GlobalNotificationAction;
37
39
  };
40
+ /**
41
+ * Extra distance a toast keeps from a window edge, measured from that edge and
42
+ * including any safe-area inset there — the number `useBottomTabBarHeight()`
43
+ * returns for a JS tab bar, or a native tab bar's height plus the bottom
44
+ * inset. `Notification` sits `spacing.sm` past the largest of the inset, the
45
+ * registered offsets and its 12 pt floor, so a bottom toast clears a tab bar
46
+ * and a top one clears a floating header. Register with
47
+ * `useNotificationOffset()` (`@mrmeg/expo-ui/hooks`) from the layout that
48
+ * owns the bar; the offset lasts while that layout is mounted.
49
+ */
50
+ export type NotificationOffset = {
51
+ top?: number;
52
+ bottom?: number;
53
+ };
38
54
  export type GlobalUIState = {
39
55
  alert: GlobalNotificationAlert | null;
56
+ /** Registered offsets by registrant key; see {@link NotificationOffset}. */
57
+ notificationOffsets: Record<string, NotificationOffset>;
40
58
  };
41
59
  export type GlobalUIActions = {
42
60
  show: (alert: Omit<GlobalNotificationAlert, "show">) => void;
43
61
  hide: () => void;
62
+ /** Register (or update) the offset for one registrant. */
63
+ setNotificationOffset: (key: string, offset: NotificationOffset) => void;
64
+ /** Remove one registrant's offset. */
65
+ clearNotificationOffset: (key: string) => void;
44
66
  };
67
+ /**
68
+ * The effective offset per edge: the largest registered value, 0 with none.
69
+ * Read it with `useStore(globalUIStore, selectNotificationOffset)`.
70
+ */
71
+ export declare const selectNotificationOffset: (state: Pick<GlobalUIState, "notificationOffsets">) => Required<NotificationOffset>;
45
72
  export declare const globalUIStore: import("zustand").UseBoundStore<import("zustand").StoreApi<GlobalUIState & GlobalUIActions>>;
@@ -1,8 +1,24 @@
1
1
  import { create } from "zustand";
2
2
  /** Auto-dismiss delay applied when `show()` is called without a `duration`. */
3
3
  export const DEFAULT_NOTIFICATION_DURATION = 4000;
4
+ /**
5
+ * The effective offset per edge: the largest registered value, 0 with none.
6
+ * Read it with `useStore(globalUIStore, selectNotificationOffset)`.
7
+ */
8
+ export const selectNotificationOffset = (state) => {
9
+ let top = 0;
10
+ let bottom = 0;
11
+ for (const offset of Object.values(state.notificationOffsets)) {
12
+ if (offset.top !== undefined && offset.top > top)
13
+ top = offset.top;
14
+ if (offset.bottom !== undefined && offset.bottom > bottom)
15
+ bottom = offset.bottom;
16
+ }
17
+ return { top, bottom };
18
+ };
4
19
  export const globalUIStore = create((set) => ({
5
20
  alert: null,
21
+ notificationOffsets: {},
6
22
  show: (alert) => set({
7
23
  alert: {
8
24
  ...alert,
@@ -13,4 +29,11 @@ export const globalUIStore = create((set) => ({
13
29
  },
14
30
  }),
15
31
  hide: () => set({ alert: null }),
32
+ setNotificationOffset: (key, offset) => set((state) => ({ notificationOffsets: { ...state.notificationOffsets, [key]: offset } })),
33
+ clearNotificationOffset: (key) => set((state) => {
34
+ if (!(key in state.notificationOffsets))
35
+ return state;
36
+ const { [key]: _removed, ...rest } = state.notificationOffsets;
37
+ return { notificationOffsets: rest };
38
+ }),
16
39
  }));
@@ -1,5 +1,7 @@
1
- export * from "./globalUIStore";
2
- export * from "./notify";
3
- export * from "./SsrViewportContext";
4
- export * from "./themeStore";
5
- export * from "./themeColorScope";
1
+ export * from "./globalUIStore.js";
2
+ export * from "./notify.js";
3
+ export * from "./SsrViewportContext.js";
4
+ export * from "./themeStore.js";
5
+ export * from "./themeColorScope.js";
6
+ export * from "./initialScheme.js";
7
+ export * from "./feedbackStore.js";
@@ -3,3 +3,5 @@ export * from "./notify.js";
3
3
  export * from "./SsrViewportContext.js";
4
4
  export * from "./themeStore.js";
5
5
  export * from "./themeColorScope.js";
6
+ export * from "./initialScheme.js";
7
+ export * from "./feedbackStore.js";
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ResolvedTheme } from "./themeStore.js";
3
+ /**
4
+ * The scheme the FIRST render should use, before the persisted preference has
5
+ * been read — a server-render hint.
6
+ *
7
+ * On web the theme store boots `system` + `light` and only reads the visitor's
8
+ * preference in `syncThemeFromEnvironment()` after the first commit, so the
9
+ * server and the hydrating client both paint light and a dark-mode visitor
10
+ * gets a flash (and any `useId`-dependent markup that differs by scheme
11
+ * mismatches). An app that knows the scheme on both sides — the template
12
+ * mirrors it into a `color-scheme` cookie the server and `document.cookie`
13
+ * both read — wraps its root in this provider; `useTheme()` uses the hint
14
+ * until `hasLoadedTheme` is true, then the store as always. Mount it ABOVE the
15
+ * first `useTheme()` caller (the root layout itself usually reads the theme
16
+ * for the navigation `ThemeProvider`). Native never hydrates and ignores it.
17
+ *
18
+ * Context, not `setState`: the store is a module singleton shared by every
19
+ * concurrent server request, so a per-request value has to travel down the
20
+ * tree.
21
+ */
22
+ export declare function InitialSchemeProvider({ scheme, children, }: {
23
+ scheme: ResolvedTheme | undefined;
24
+ children: ReactNode;
25
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<ResolvedTheme | undefined>>;
26
+ /** The first-render scheme hint, or `undefined` outside an `InitialSchemeProvider`. */
27
+ export declare function useInitialScheme(): ResolvedTheme | undefined;
@@ -0,0 +1,29 @@
1
+ import { createContext, createElement, use } from "react";
2
+ const InitialSchemeContext = createContext(undefined);
3
+ /**
4
+ * The scheme the FIRST render should use, before the persisted preference has
5
+ * been read — a server-render hint.
6
+ *
7
+ * On web the theme store boots `system` + `light` and only reads the visitor's
8
+ * preference in `syncThemeFromEnvironment()` after the first commit, so the
9
+ * server and the hydrating client both paint light and a dark-mode visitor
10
+ * gets a flash (and any `useId`-dependent markup that differs by scheme
11
+ * mismatches). An app that knows the scheme on both sides — the template
12
+ * mirrors it into a `color-scheme` cookie the server and `document.cookie`
13
+ * both read — wraps its root in this provider; `useTheme()` uses the hint
14
+ * until `hasLoadedTheme` is true, then the store as always. Mount it ABOVE the
15
+ * first `useTheme()` caller (the root layout itself usually reads the theme
16
+ * for the navigation `ThemeProvider`). Native never hydrates and ignores it.
17
+ *
18
+ * Context, not `setState`: the store is a module singleton shared by every
19
+ * concurrent server request, so a per-request value has to travel down the
20
+ * tree.
21
+ */
22
+ export function InitialSchemeProvider({ scheme, children, }) {
23
+ // `state/*` modules are `.ts` (the package's source export pattern), hence no JSX.
24
+ return createElement(InitialSchemeContext.Provider, { value: scheme }, children);
25
+ }
26
+ /** The first-render scheme hint, or `undefined` outside an `InitialSchemeProvider`. */
27
+ export function useInitialScheme() {
28
+ return use(InitialSchemeContext);
29
+ }
@@ -1,4 +1,4 @@
1
- import type { GlobalNotificationAlert } from "./globalUIStore";
1
+ import type { GlobalNotificationAlert } from "./globalUIStore.js";
2
2
  /**
3
3
  * notify
4
4
  *
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import type { ColorOverrides } from "./themeStore";
2
+ import type { ColorOverrides } from "./themeStore.js";
3
3
  /** Read the active scoped override (null when not inside a scope). */
4
4
  export declare function useThemeColorScope(): ColorOverrides | null;
5
5
  export declare function ThemeColorScope({ colors, children, }: {