@mrmeg/expo-ui 0.27.1 → 0.28.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 (140) hide show
  1. package/CHANGELOG.md +598 -0
  2. package/LLM_USAGE.md +233 -35
  3. package/README.md +504 -103
  4. package/dist/components/Accordion.js +12 -6
  5. package/dist/components/AnimatedView.d.ts +1 -1
  6. package/dist/components/AnimatedView.js +5 -2
  7. package/dist/components/Avatar.d.ts +1 -1
  8. package/dist/components/Avatar.js +2 -2
  9. package/dist/components/Badge.js +5 -1
  10. package/dist/components/BottomSheet.d.ts +29 -1
  11. package/dist/components/BottomSheet.js +107 -30
  12. package/dist/components/Button.d.ts +20 -4
  13. package/dist/components/Button.js +60 -43
  14. package/dist/components/Card.d.ts +1 -1
  15. package/dist/components/Card.js +24 -4
  16. package/dist/components/Carousel.js +2 -2
  17. package/dist/components/Checkbox.js +51 -44
  18. package/dist/components/Collapsible.js +7 -3
  19. package/dist/components/Dialog.js +87 -16
  20. package/dist/components/Drawer.d.ts +1 -1
  21. package/dist/components/Drawer.js +65 -55
  22. package/dist/components/DropdownMenu.js +11 -7
  23. package/dist/components/EmptyState.d.ts +2 -2
  24. package/dist/components/EmptyState.js +5 -2
  25. package/dist/components/Hydrated.d.ts +31 -0
  26. package/dist/components/Hydrated.js +30 -0
  27. package/dist/components/Icon.d.ts +17 -5
  28. package/dist/components/Icon.js +31 -10
  29. package/dist/components/InputOTP.js +5 -2
  30. package/dist/components/Item.d.ts +103 -4
  31. package/dist/components/Item.js +168 -15
  32. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  33. package/dist/components/KeyboardAvoidingView.js +4 -1
  34. package/dist/components/Label.js +3 -2
  35. package/dist/components/MaxWidthContainer.js +1 -1
  36. package/dist/components/Notification.js +45 -27
  37. package/dist/components/Popover.d.ts +18 -5
  38. package/dist/components/Popover.js +72 -8
  39. package/dist/components/Progress.js +8 -6
  40. package/dist/components/RadioGroup.js +52 -49
  41. package/dist/components/Screen.d.ts +61 -0
  42. package/dist/components/Screen.js +62 -0
  43. package/dist/components/SectionHeader.d.ts +1 -1
  44. package/dist/components/SegmentedControl.d.ts +12 -4
  45. package/dist/components/SegmentedControl.js +94 -4
  46. package/dist/components/Select.js +21 -26
  47. package/dist/components/Skeleton.js +24 -10
  48. package/dist/components/Slider.d.ts +11 -3
  49. package/dist/components/Slider.js +73 -6
  50. package/dist/components/StatCard.d.ts +4 -2
  51. package/dist/components/StatCard.js +3 -1
  52. package/dist/components/StyledText.d.ts +7 -1
  53. package/dist/components/StyledText.js +5 -3
  54. package/dist/components/Switch.d.ts +1 -1
  55. package/dist/components/Switch.js +20 -30
  56. package/dist/components/Tabs.d.ts +2 -2
  57. package/dist/components/Tabs.js +13 -24
  58. package/dist/components/TextInput.d.ts +1 -1
  59. package/dist/components/TextInput.js +71 -26
  60. package/dist/components/Toggle.d.ts +2 -42
  61. package/dist/components/Toggle.js +38 -27
  62. package/dist/components/ToggleGroup.d.ts +1 -1
  63. package/dist/components/ToggleGroup.js +37 -32
  64. package/dist/components/Tooltip.d.ts +1 -1
  65. package/dist/components/Tooltip.js +9 -3
  66. package/dist/components/UIProvider.d.ts +22 -2
  67. package/dist/components/UIProvider.js +15 -3
  68. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  69. package/dist/components/bottomSheetDismiss.js +44 -0
  70. package/dist/components/index.d.ts +46 -44
  71. package/dist/components/index.js +2 -0
  72. package/dist/components/keyboardDismiss.js +12 -4
  73. package/dist/components/nativeTextField.android.js +13 -9
  74. package/dist/constants/colors.d.ts +40 -5
  75. package/dist/constants/colors.js +26 -8
  76. package/dist/constants/fonts.d.ts +41 -2
  77. package/dist/constants/fonts.js +46 -7
  78. package/dist/constants/index.d.ts +5 -4
  79. package/dist/constants/index.js +1 -0
  80. package/dist/constants/interaction.d.ts +18 -0
  81. package/dist/constants/interaction.js +18 -0
  82. package/dist/constants/spacing.d.ts +2 -1
  83. package/dist/constants/spacing.js +2 -1
  84. package/dist/design-system.json +154 -2
  85. package/dist/hooks/index.d.ts +12 -7
  86. package/dist/hooks/index.js +5 -0
  87. package/dist/hooks/useDimensions.d.ts +13 -8
  88. package/dist/hooks/useDimensions.js +123 -48
  89. package/dist/hooks/useFocusVisible.d.ts +30 -0
  90. package/dist/hooks/useFocusVisible.js +45 -0
  91. package/dist/hooks/useFontStyle.d.ts +2 -2
  92. package/dist/hooks/useFontStyle.js +3 -2
  93. package/dist/hooks/useHydrated.d.ts +13 -0
  94. package/dist/hooks/useHydrated.js +19 -0
  95. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  96. package/dist/hooks/useNotificationOffset.js +34 -0
  97. package/dist/hooks/useResources.d.ts +21 -20
  98. package/dist/hooks/useResources.js +72 -68
  99. package/dist/hooks/useScalePress.d.ts +8 -5
  100. package/dist/hooks/useScalePress.js +16 -9
  101. package/dist/hooks/useShape.d.ts +24 -0
  102. package/dist/hooks/useShape.js +24 -0
  103. package/dist/hooks/useStaggeredEntrance.js +9 -7
  104. package/dist/hooks/useTheme.d.ts +3 -3
  105. package/dist/hooks/useTheme.js +177 -66
  106. package/dist/hooks/useWindowInsets.d.ts +13 -0
  107. package/dist/hooks/useWindowInsets.js +23 -0
  108. package/dist/index.d.ts +5 -5
  109. package/dist/lib/composeRefs.d.ts +8 -0
  110. package/dist/lib/composeRefs.js +20 -0
  111. package/dist/lib/haptics.d.ts +12 -0
  112. package/dist/lib/haptics.js +21 -0
  113. package/dist/lib/index.d.ts +6 -5
  114. package/dist/lib/index.js +1 -0
  115. package/dist/lib/interFonts.d.ts +15 -0
  116. package/dist/lib/interFonts.js +1 -0
  117. package/dist/lib/interFonts.native.d.ts +3 -0
  118. package/dist/lib/interFonts.native.js +20 -0
  119. package/dist/lib/popoverPlacement.d.ts +36 -0
  120. package/dist/lib/popoverPlacement.js +29 -0
  121. package/dist/lib/themedStyles.d.ts +1 -1
  122. package/dist/lib/useAnimatedValue.d.ts +15 -0
  123. package/dist/lib/useAnimatedValue.js +19 -0
  124. package/dist/lib/webHitTarget.d.ts +40 -0
  125. package/dist/lib/webHitTarget.js +37 -0
  126. package/dist/state/feedbackStore.d.ts +24 -0
  127. package/dist/state/feedbackStore.js +16 -0
  128. package/dist/state/globalUIStore.d.ts +27 -0
  129. package/dist/state/globalUIStore.js +23 -0
  130. package/dist/state/index.d.ts +7 -5
  131. package/dist/state/index.js +2 -0
  132. package/dist/state/initialScheme.d.ts +27 -0
  133. package/dist/state/initialScheme.js +29 -0
  134. package/dist/state/notify.d.ts +1 -1
  135. package/dist/state/themeColorScope.d.ts +1 -1
  136. package/dist/state/themeStore.d.ts +69 -16
  137. package/dist/state/themeStore.js +65 -38
  138. package/llms-full.md +56 -8
  139. package/llms.txt +10 -1
  140. package/package.json +69 -2
@@ -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, }: {
@@ -1,5 +1,5 @@
1
- import type { ThemeColors } from "../constants/colors";
2
- import type { FontOverrides } from "../constants/fonts";
1
+ import type { ThemeColors } from "../constants/colors.js";
2
+ import type { FontOverrides, SerifPreset } from "../constants/fonts.js";
3
3
  export type ThemePreference = "system" | "light" | "dark";
4
4
  export type ResolvedTheme = "light" | "dark";
5
5
  /**
@@ -27,19 +27,28 @@ export type ColorOverrides = {
27
27
  light?: Partial<ThemeColors>;
28
28
  dark?: Partial<ThemeColors>;
29
29
  };
30
+ /** A radius override for one shape slot. `0` is square corners; `9999` is a pill. */
31
+ export type ShapeSlotOverride = {
32
+ /** Border radius for every component in the slot. A caller `style` still wins. */
33
+ borderRadius?: number;
34
+ };
30
35
  /**
31
- * Shape overrides a host app can inject, grouped per component so future
32
- * shape knobs (card radius, input radius, …) slot in without reshaping the
33
- * API. Every field is optional; omitted fields keep the package default.
36
+ * Shape overrides a host app can inject, grouped per slot. Every field is
37
+ * optional; omitted fields keep the package default. Components read their
38
+ * slot through `useShape(slot)` and layer it after the static radius, before
39
+ * the caller's `style`.
40
+ *
41
+ * | Slot | Components | Package default |
42
+ * |---|---|---|
43
+ * | `button` | Button (every preset) | `spacing.radiusMd` (10) |
44
+ * | `input` | TextInput (not `underlined`), Select trigger, InputOTP cells | `spacing.radiusMd` (10) |
45
+ * | `card` | Card, StatCard, EmptyState (`bordered`), SkeletonCard | `spacing.radiusLg` (14) |
46
+ * | `sheet` | BottomSheet top corners (where the platform lets the sheet draw them: web, Android) | platform |
47
+ * | `badge` | Badge | `spacing.radiusFull` |
48
+ * | `dialog` | Dialog and AlertDialog content | `spacing.radiusLg` (14) |
34
49
  */
35
50
  export type ShapeOverrides = {
36
- button?: {
37
- /**
38
- * Border radius applied to every Button preset. Package default:
39
- * `spacing.radiusMd` (10). Use 9999 for pill buttons. A caller `style`
40
- * still wins over this, as it always has.
41
- */
42
- borderRadius?: number;
51
+ button?: ShapeSlotOverride & {
43
52
  /**
44
53
  * Whether the `default` preset renders its shadow. Package default: true.
45
54
  * Other presets stay flat regardless; the per-instance `withShadow` prop
@@ -47,7 +56,13 @@ export type ShapeOverrides = {
47
56
  */
48
57
  withShadow?: boolean;
49
58
  };
59
+ input?: ShapeSlotOverride;
60
+ card?: ShapeSlotOverride;
61
+ sheet?: ShapeSlotOverride;
62
+ badge?: ShapeSlotOverride;
63
+ dialog?: ShapeSlotOverride;
50
64
  };
65
+ export type ShapeSlot = keyof ShapeOverrides;
51
66
  export type ThemeStore = {
52
67
  userTheme: ThemePreference;
53
68
  systemTheme: ResolvedTheme;
@@ -72,11 +87,19 @@ export type ThemeStore = {
72
87
  */
73
88
  fontOverrides: FontOverrides;
74
89
  /**
75
- * App-injected shape overrides (button radius, default-preset shadow).
76
- * Same contract as the other override slots: empty by default, fully
77
- * backward compatible when unset.
90
+ * App-injected shape overrides (radii per slot, the default Button preset's
91
+ * shadow). Same contract as the other override slots: empty by default,
92
+ * fully backward compatible when unset.
78
93
  */
79
94
  shapeOverrides: ShapeOverrides;
95
+ /**
96
+ * Which serif the `serif` variant resolves to when no `setFonts` serif
97
+ * override exists: `"georgia"` (default, the system face) or
98
+ * `"newsreader"`, set by `useResources({ serif: "newsreader" })` once its
99
+ * files or stylesheet are in place. Read by `resolveFontStyle` through
100
+ * `StyledText` and `useFontStyle`.
101
+ */
102
+ serifPreset: SerifPreset;
80
103
  setTheme: (theme: ThemePreference) => void;
81
104
  setSystemTheme: (theme: ResolvedTheme) => void;
82
105
  /**
@@ -96,13 +119,43 @@ export type ThemeStore = {
96
119
  setFonts: (overrides: FontOverrides) => void;
97
120
  /**
98
121
  * Replace the active shape overrides. Pass `{}` to clear them and fall back
99
- * to the package defaults (button radius 12, default-preset shadow on).
122
+ * to the package defaults (see the slot table on `ShapeOverrides`: button
123
+ * and input radius 10 = `spacing.radiusMd`, card and dialog 14 =
124
+ * `spacing.radiusLg`, badge pill, default-preset shadow on).
100
125
  */
101
126
  setShape: (overrides: ShapeOverrides) => void;
127
+ /** Switch the serif preset. `useResources` calls this; apps rarely need to. */
128
+ setSerifPreset: (preset: SerifPreset) => void;
102
129
  loadTheme: () => void;
103
130
  };
104
131
  export declare function resolveThemePreference(userTheme: ThemePreference, systemTheme: ResolvedTheme): ResolvedTheme;
105
132
  export declare const useThemeStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ThemeStore>>;
106
133
  export declare function syncSystemTheme(): void;
134
+ /**
135
+ * Keep `systemTheme` following the OS color scheme (`prefers-color-scheme` on
136
+ * web, `Appearance` on native). The first holder reads the current scheme and
137
+ * attaches the one listener; later calls share it.
138
+ *
139
+ * Returns this caller's release: once every holder has released, the
140
+ * listener is removed. Calling a release again is a no-op, so an effect
141
+ * cleanup that runs twice can't drop another caller's hold. On native the
142
+ * package holds the listener for the app's lifetime (see the module-load
143
+ * init below), so an app's release never stops OS tracking there.
144
+ */
107
145
  export declare function startSystemThemeListener(): () => void;
146
+ /**
147
+ * Single entry point for host apps to populate the store from the
148
+ * environment: the OS color-scheme listener plus the persisted preference.
149
+ *
150
+ * Web apps call it once, from a top-level `useEffect` (never during render or
151
+ * at module scope — see below), and return its result as the cleanup:
152
+ * `useEffect(() => syncThemeFromEnvironment(), [])`. Safe to call more than
153
+ * once (StrictMode's double effects, two roots): every call shares the one
154
+ * OS listener, re-reads the persisted preference, and returns its own
155
+ * cleanup, which releases only that call's hold on the listener.
156
+ *
157
+ * The listener starts BEFORE `loadTheme()` on purpose: reading the real OS
158
+ * scheme first means a `system` user resolves straight from the boot-default
159
+ * "light" to their actual scheme in one commit.
160
+ */
108
161
  export declare function syncThemeFromEnvironment(): () => void;