@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.
- package/CHANGELOG.md +598 -0
- package/LLM_USAGE.md +233 -35
- package/README.md +504 -103
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +87 -16
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +5 -2
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +73 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +20 -30
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +71 -26
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.js +12 -4
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -5
- package/dist/lib/index.js +1 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- 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
|
+
}
|
|
@@ -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
|
}));
|
package/dist/state/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/state/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/state/notify.d.ts
CHANGED
|
@@ -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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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 (
|
|
76
|
-
* Same contract as the other override slots: empty by default,
|
|
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 (
|
|
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;
|