@mrmeg/expo-ui 0.13.0-rsd.0 → 0.13.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 (81) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LLM_USAGE.md +15 -0
  3. package/README.md +15 -1
  4. package/dist/components/Accordion.d.ts +18 -38
  5. package/dist/components/Accordion.js +121 -137
  6. package/dist/components/AnimatedView.d.ts +24 -13
  7. package/dist/components/AnimatedView.js +29 -70
  8. package/dist/components/Badge.d.ts +2 -2
  9. package/dist/components/Badge.js +47 -45
  10. package/dist/components/BottomSheet.d.ts +73 -30
  11. package/dist/components/BottomSheet.js +225 -191
  12. package/dist/components/Button.d.ts +56 -43
  13. package/dist/components/Button.js +203 -189
  14. package/dist/components/Card.d.ts +10 -7
  15. package/dist/components/Card.js +105 -90
  16. package/dist/components/Checkbox.d.ts +8 -20
  17. package/dist/components/Checkbox.js +80 -77
  18. package/dist/components/Collapsible.d.ts +47 -47
  19. package/dist/components/Collapsible.js +29 -108
  20. package/dist/components/Dialog.d.ts +121 -80
  21. package/dist/components/Dialog.js +132 -225
  22. package/dist/components/DismissKeyboard.js +2 -33
  23. package/dist/components/Drawer.d.ts +83 -37
  24. package/dist/components/Drawer.js +512 -252
  25. package/dist/components/DropdownMenu.d.ts +113 -106
  26. package/dist/components/DropdownMenu.js +204 -350
  27. package/dist/components/EmptyState.d.ts +2 -2
  28. package/dist/components/EmptyState.js +34 -41
  29. package/dist/components/InputOTP.d.ts +53 -19
  30. package/dist/components/InputOTP.js +102 -81
  31. package/dist/components/KeyboardAvoidingView.d.ts +3 -6
  32. package/dist/components/KeyboardAvoidingView.js +5 -18
  33. package/dist/components/Label.d.ts +17 -12
  34. package/dist/components/Label.js +51 -38
  35. package/dist/components/MaxWidthContainer.d.ts +16 -5
  36. package/dist/components/MaxWidthContainer.js +29 -28
  37. package/dist/components/Notification.d.ts +9 -5
  38. package/dist/components/Notification.js +187 -190
  39. package/dist/components/Popover.d.ts +66 -38
  40. package/dist/components/Popover.js +69 -158
  41. package/dist/components/Progress.d.ts +4 -5
  42. package/dist/components/Progress.js +77 -65
  43. package/dist/components/RadioGroup.d.ts +24 -30
  44. package/dist/components/RadioGroup.js +94 -90
  45. package/dist/components/Select.d.ts +74 -62
  46. package/dist/components/Select.js +154 -241
  47. package/dist/components/Separator.d.ts +23 -13
  48. package/dist/components/Separator.js +36 -29
  49. package/dist/components/Skeleton.d.ts +7 -8
  50. package/dist/components/Skeleton.js +61 -74
  51. package/dist/components/StyledText.context.d.ts +2 -6
  52. package/dist/components/StyledText.context.js +0 -3
  53. package/dist/components/StyledText.d.ts +7 -29
  54. package/dist/components/StyledText.js +29 -92
  55. package/dist/components/Switch.d.ts +6 -18
  56. package/dist/components/Switch.js +106 -112
  57. package/dist/components/Tabs.d.ts +16 -26
  58. package/dist/components/Tabs.js +91 -189
  59. package/dist/components/TextInput.d.ts +19 -6
  60. package/dist/components/TextInput.js +198 -226
  61. package/dist/components/Toggle.d.ts +41 -23
  62. package/dist/components/Toggle.js +98 -84
  63. package/dist/components/ToggleGroup.d.ts +29 -37
  64. package/dist/components/ToggleGroup.js +108 -113
  65. package/dist/components/Tooltip.d.ts +111 -41
  66. package/dist/components/Tooltip.js +118 -156
  67. package/dist/components/UIProvider.d.ts +1 -1
  68. package/dist/components/UIProvider.js +1 -1
  69. package/dist/components/keyboardFocusRegistry.d.ts +15 -1
  70. package/dist/components/keyboardFocusRegistry.js +39 -2
  71. package/dist/hooks/useTheme.d.ts +10 -34
  72. package/dist/hooks/useTheme.js +8 -20
  73. package/dist/lib/index.d.ts +0 -2
  74. package/dist/lib/index.js +0 -2
  75. package/package.json +29 -11
  76. package/dist/lib/portal.d.ts +0 -16
  77. package/dist/lib/portal.js +0 -84
  78. package/dist/lib/styles.d.ts +0 -32
  79. package/dist/lib/styles.js +0 -91
  80. package/dist/lib/useAnchoredPosition.d.ts +0 -57
  81. package/dist/lib/useAnchoredPosition.js +0 -120
package/CHANGELOG.md CHANGED
@@ -3,6 +3,37 @@
3
3
  All notable changes to `@mrmeg/expo-ui` are documented here. This project
4
4
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.13.0]
7
+
8
+ ### Added
9
+
10
+ - **`Drawer.Header` now supports a compact app-brand layout.** Use the new
11
+ `icon`, `title`, and `action` slots for a leading brand icon and title with a
12
+ trailing close or `Drawer.ToggleCollapse` control. Existing child-based
13
+ headers remain supported.
14
+
15
+ ### Changed
16
+
17
+ - Expanded peer compatibility through Expo 57, React Native 0.86, and their
18
+ matching `@expo/ui` and gesture-handler releases.
19
+
20
+ ## [0.12.1]
21
+
22
+ ### Fixed
23
+
24
+ - **`BottomSheet` now dismisses the keyboard when tapping outside a focused
25
+ field on Android.** The native sheet hosts its content in a separate window
26
+ (via `@expo/ui`'s `RNHostView`), outside the app's `KeyboardProvider` — so the
27
+ app-wide tap-away never reached sheet content, and `useKeyboardState()` /
28
+ `KeyboardController.dismiss()` are blind to that window. iOS papered over this
29
+ (SwiftUI resigns first-responder on outside taps for free); Compose did not,
30
+ leaving a focused field stuck open. `BottomSheet.Content` now mounts a
31
+ transparent dismiss overlay while a field is focused, detecting focus via the
32
+ window-independent `keyboardFocusRegistry` and resigning the field through its
33
+ own native `blur()` ref. The registry gained `subscribeKeyboardFocus`,
34
+ `hasKeyboardFocusedInput`, and `dismissKeyboardFocusedInput`, and `TextInput`
35
+ now registers a `blur` handle on focus.
36
+
6
37
  ## [0.12.0]
7
38
 
8
39
  ### Added
package/LLM_USAGE.md CHANGED
@@ -212,6 +212,7 @@ Use this table before creating a new app-local primitive.
212
212
  - Use `EmptyState` for no-data or recoverable error regions.
213
213
  - Use `Skeleton` for loading content with stable layout.
214
214
  - Use `Progress` for real progress or indeterminate long-running work.
215
+ - Use `Drawer.Header` with `icon`, `title`, and `action` for a compact app-brand row; place `Drawer.ToggleCollapse` in `action` for a trailing rail control.
215
216
 
216
217
  ## Minimal Examples
217
218
 
@@ -249,6 +250,20 @@ import { Button, Switch, TextInput } from "@mrmeg/expo-ui/components";
249
250
  </Button>
250
251
  ```
251
252
 
253
+ ```tsx
254
+ import { Drawer, Icon } from "@mrmeg/expo-ui/components";
255
+
256
+ <Drawer.Header
257
+ icon={<Icon name="hexagon" color="accent" />}
258
+ title="Acme"
259
+ action={
260
+ <Drawer.ToggleCollapse>
261
+ <Icon name="sidebar" decorative />
262
+ </Drawer.ToggleCollapse>
263
+ }
264
+ />
265
+ ```
266
+
252
267
  ```tsx
253
268
  import { EmptyState, Progress, SkeletonCard } from "@mrmeg/expo-ui/components";
254
269
  import { notify } from "@mrmeg/expo-ui/state";
package/README.md CHANGED
@@ -254,7 +254,7 @@ Most compound components support both direct named imports and dot notation on t
254
254
  | `Card` | `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` |
255
255
  | `Collapsible` | `CollapsibleTrigger`, `CollapsibleContent` |
256
256
  | `Dialog` | `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter`, `DialogTitle`, `DialogDescription`, `DialogClose` |
257
- | `Drawer` | `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerBody`, `DrawerFooter`, `DrawerClose` |
257
+ | `Drawer` | `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerBody`, `DrawerFooter`, `DrawerClose`, `DrawerToggleCollapse` |
258
258
  | `DropdownMenu` | `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuLabel`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuPortal`, `DropdownMenuSub`, `DropdownMenuSubTrigger`, `DropdownMenuSubContent` |
259
259
  | `Popover` | `PopoverTrigger`, `PopoverContent`, `PopoverHeader`, `PopoverBody`, `PopoverFooter` |
260
260
  | `RadioGroup` | `RadioGroupItem` |
@@ -273,6 +273,20 @@ Use `Button.preset`, not `variant`. `default` is the neutral primary action, `se
273
273
 
274
274
  Use `StyledText` or its aliases instead of raw `Text` whenever the text is part of app UI. Use `TextInput` for labeled fields because it already owns label, helper text, error text, clear buttons, password visibility, numeric filtering, and left/right elements.
275
275
 
276
+ `Drawer.Header` accepts `icon`, `title`, and `action` slots for a compact app-brand row. Pass a string title to use the package typography, and place `Drawer.ToggleCollapse` in `action` for a trailing rail control:
277
+
278
+ ```tsx
279
+ <Drawer.Header
280
+ icon={<Icon name="hexagon" color="accent" />}
281
+ title="Acme"
282
+ action={
283
+ <Drawer.ToggleCollapse>
284
+ <Icon name="sidebar" decorative />
285
+ </Drawer.ToggleCollapse>
286
+ }
287
+ />
288
+ ```
289
+
276
290
  Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `UIProvider` also wraps app content in the package keyboard-avoiding root by default, so ordinary screens and fixed footers stay above the soft keyboard without repeated app-local wrappers; pass `keyboardAvoiding={false}` to opt out, or use `KeyboardAvoidingView` directly for a subtree with custom behavior. Web skips the root keyboard wrapper unless `keyboardAvoiding` is explicitly enabled. `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. Trigger transient feedback with `notify`.
277
291
 
278
292
  Use `Skeleton` components for loading content with stable dimensions, `EmptyState` for no-data/recoverable errors, `Alert` for blocking confirm/alert dialogs, and `Notification` for transient global feedback.
@@ -1,33 +1,28 @@
1
- import * as React from "react";
2
- import { type LooseStyle } from "../lib/styles";
3
- type BaseAccordionRootProps = {
1
+ import { View, ViewStyle } from "react-native";
2
+ import * as AccordionPrimitive from "@rn-primitives/accordion";
3
+ type BaseAccordionRootProps = Omit<React.ComponentProps<typeof View>, "style"> & React.RefAttributes<AccordionPrimitive.RootRef> & {
4
4
  disabled?: boolean;
5
- /** Single-type only: allow closing the open item by re-pressing it. */
6
5
  collapsible?: boolean;
7
- /** Platform: WEB ONLY (kept for API compat; unused). */
8
6
  dir?: "ltr" | "rtl";
9
- /** Platform: WEB ONLY (kept for API compat; unused). */
10
7
  orientation?: "vertical" | "horizontal";
11
- style?: LooseStyle;
12
- children?: React.ReactNode;
8
+ style?: ViewStyle;
13
9
  };
14
- type SingleAccordionRootProps = BaseAccordionRootProps & {
10
+ type WebSingleAccordionRootProps = BaseAccordionRootProps & {
15
11
  type: "single";
16
12
  defaultValue?: string;
17
13
  value?: string;
18
14
  onValueChange?: (value: string | undefined) => void;
19
15
  };
20
- type MultipleAccordionRootProps = BaseAccordionRootProps & {
16
+ type WebMultipleAccordionRootProps = BaseAccordionRootProps & {
21
17
  type: "multiple";
22
18
  defaultValue?: string[];
23
19
  value?: string[];
24
20
  onValueChange?: (value: string[]) => void;
25
21
  };
26
- type AccordionRootProps = SingleAccordionRootProps | MultipleAccordionRootProps;
22
+ type AccordionRootProps = WebSingleAccordionRootProps | WebMultipleAccordionRootProps;
27
23
  /**
28
- * Accordion Root Component (react-strict-dom)
29
- * Container for accordion items with single or multiple open items. Supports
30
- * controlled (`value`) and uncontrolled (`defaultValue`) usage.
24
+ * Accordion Root Component
25
+ * Container for accordion items with support for single or multiple open items
31
26
  *
32
27
  * Usage:
33
28
  * <Accordion type="single" collapsible>
@@ -37,38 +32,23 @@ type AccordionRootProps = SingleAccordionRootProps | MultipleAccordionRootProps;
37
32
  * </AccordionItem>
38
33
  * </Accordion>
39
34
  */
40
- declare function Accordion(props: AccordionRootProps): React.JSX.Element;
41
- export interface AccordionItemProps {
42
- value: string;
43
- disabled?: boolean;
44
- children?: React.ReactNode;
45
- style?: LooseStyle;
46
- }
35
+ declare function Accordion({ children, style, ...props }: AccordionRootProps): import("react").JSX.Element;
47
36
  /**
48
37
  * Accordion Item Component
49
- * Individual accordion item with a bottom border. Shares its expanded state
50
- * with the Trigger (chevron) and Content via context.
38
+ * Individual accordion item with border styling
51
39
  */
52
- declare function AccordionItem({ value, disabled: itemDisabled, children, style }: AccordionItemProps): React.JSX.Element;
53
- export interface AccordionTriggerProps {
54
- children?: React.ReactNode;
55
- style?: LooseStyle;
56
- }
40
+ declare function AccordionItem({ children, value, style: styleOverride, ...props }: AccordionPrimitive.ItemProps & React.RefAttributes<AccordionPrimitive.ItemRef>): import("react").JSX.Element;
57
41
  /**
58
42
  * Accordion Trigger Component
59
- * Clickable header that expands/collapses the item. Includes an animated
60
- * chevron that rotates with the expanded state.
43
+ * Clickable header that expands/collapses the content
44
+ * Includes animated chevron icon
61
45
  */
62
- declare function AccordionTrigger({ children, style }: AccordionTriggerProps): React.JSX.Element;
63
- export interface AccordionContentProps {
64
- /** Keep content mounted (hidden) when collapsed instead of unmounting. */
65
- forceMount?: boolean;
46
+ declare function AccordionTrigger({ children, style: styleOverride, ...props }: AccordionPrimitive.TriggerProps & {
66
47
  children?: React.ReactNode;
67
- style?: LooseStyle;
68
- }
48
+ } & React.RefAttributes<AccordionPrimitive.TriggerRef>): import("react").JSX.Element;
69
49
  /**
70
50
  * Accordion Content Component
71
- * Expandable content area. Unmounts when collapsed unless `forceMount` is set.
51
+ * Expandable content area with animations
72
52
  */
73
- declare function AccordionContent({ forceMount, children, style }: AccordionContentProps): React.JSX.Element | null;
53
+ declare function AccordionContent({ children, style: styleOverride, ...props }: AccordionPrimitive.ContentProps & React.RefAttributes<AccordionPrimitive.ContentRef>): import("react").JSX.Element;
74
54
  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -1,31 +1,62 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, use, useCallback, useMemo, useState } from "react";
3
- import { css, html } from "react-strict-dom";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Animated, Platform, Pressable, View } from "react-native";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
6
6
  import { useTheme } from "../hooks/useTheme.js";
7
7
  import { useReducedMotion } from "../hooks/useReduceMotion.js";
8
- import { sanitizeWebStyle } from "../lib/styles.js";
9
- const AccordionContext = createContext(null);
10
- function useAccordionContext() {
11
- const ctx = use(AccordionContext);
12
- if (!ctx) {
13
- throw new Error("Accordion compound components must be used within an <Accordion>");
14
- }
15
- return ctx;
8
+ import { spacing } from "../constants/spacing.js";
9
+ import * as AccordionPrimitive from "@rn-primitives/accordion";
10
+ function normalizeSingleValue(value) {
11
+ return value ?? "";
12
+ }
13
+ function denormalizeSingleValue(value) {
14
+ return value === "" ? undefined : value;
15
+ }
16
+ function normalizeMultipleValue(value) {
17
+ return value ?? [];
18
+ }
19
+ function WebSingleAccordionRoot(props) {
20
+ const isControlled = Object.prototype.hasOwnProperty.call(props, "value");
21
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => normalizeSingleValue(props.defaultValue));
22
+ const normalizedValue = isControlled
23
+ ? normalizeSingleValue(props.value)
24
+ : uncontrolledValue;
25
+ const handleValueChange = (nextValue) => {
26
+ const normalizedNextValue = normalizeSingleValue(nextValue);
27
+ if (!isControlled) {
28
+ setUncontrolledValue(normalizedNextValue);
29
+ }
30
+ props.onValueChange?.(denormalizeSingleValue(normalizedNextValue));
31
+ };
32
+ const { children, style, type: _type, defaultValue: _defaultValue, onValueChange: _onValueChange, value: _value, ...rootProps } = props;
33
+ return (_jsx(AccordionPrimitive.Root, { ...rootProps, type: "single", value: normalizedValue, onValueChange: handleValueChange, asChild: false, children: _jsx(View, { style: style, children: children }) }));
16
34
  }
17
- const AccordionItemContext = createContext(null);
18
- function useAccordionItemContext() {
19
- const ctx = use(AccordionItemContext);
20
- if (!ctx) {
21
- throw new Error("AccordionTrigger/AccordionContent must be used within an <AccordionItem>");
35
+ function WebMultipleAccordionRoot(props) {
36
+ const isControlled = Object.prototype.hasOwnProperty.call(props, "value");
37
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => normalizeMultipleValue(props.defaultValue));
38
+ const normalizedValue = isControlled
39
+ ? normalizeMultipleValue(props.value)
40
+ : uncontrolledValue;
41
+ const handleValueChange = (nextValue) => {
42
+ const normalizedNextValue = normalizeMultipleValue(nextValue);
43
+ if (!isControlled) {
44
+ setUncontrolledValue(normalizedNextValue);
45
+ }
46
+ props.onValueChange?.(normalizedNextValue);
47
+ };
48
+ const { children, style, type: _type, defaultValue: _defaultValue, onValueChange: _onValueChange, value: _value, ...rootProps } = props;
49
+ return (_jsx(AccordionPrimitive.Root, { ...rootProps, type: "multiple", value: normalizedValue, onValueChange: handleValueChange, asChild: false, children: _jsx(View, { style: style, children: children }) }));
50
+ }
51
+ function WebAccordionRoot(props) {
52
+ if (props.type === "multiple") {
53
+ return _jsx(WebMultipleAccordionRoot, { ...props });
22
54
  }
23
- return ctx;
55
+ return _jsx(WebSingleAccordionRoot, { ...props });
24
56
  }
25
57
  /**
26
- * Accordion Root Component (react-strict-dom)
27
- * Container for accordion items with single or multiple open items. Supports
28
- * controlled (`value`) and uncontrolled (`defaultValue`) usage.
58
+ * Accordion Root Component
59
+ * Container for accordion items with support for single or multiple open items
29
60
  *
30
61
  * Usage:
31
62
  * <Accordion type="single" collapsible>
@@ -35,139 +66,92 @@ function useAccordionItemContext() {
35
66
  * </AccordionItem>
36
67
  * </Accordion>
37
68
  */
38
- function Accordion(props) {
39
- const { children, style, disabled = false } = props;
40
- const isControlled = props.value !== undefined;
41
- // Uncontrolled state — always stored as a string[] internally, projected to
42
- // single/multiple at the boundary.
43
- const [uncontrolled, setUncontrolled] = useState(() => {
44
- if (props.type === "multiple")
45
- return props.defaultValue ?? [];
46
- return props.defaultValue ? [props.defaultValue] : [];
47
- });
48
- const expandedValues = useMemo(() => {
49
- if (!isControlled)
50
- return uncontrolled;
51
- if (props.type === "multiple")
52
- return props.value ?? [];
53
- return props.value ? [props.value] : [];
54
- }, [isControlled, uncontrolled, props]);
55
- const isExpanded = useCallback((value) => expandedValues.includes(value), [expandedValues]);
56
- const toggle = useCallback((value) => {
57
- if (disabled)
58
- return;
59
- if (props.type === "multiple") {
60
- const next = expandedValues.includes(value)
61
- ? expandedValues.filter((v) => v !== value)
62
- : [...expandedValues, value];
63
- if (!isControlled)
64
- setUncontrolled(next);
65
- props.onValueChange?.(next);
66
- return;
67
- }
68
- // Single: open the pressed item; collapsible allows re-press to close.
69
- const isOpen = expandedValues.includes(value);
70
- const collapsible = props.collapsible ?? false;
71
- const nextValue = isOpen && collapsible ? undefined : value;
72
- if (!isControlled)
73
- setUncontrolled(nextValue ? [nextValue] : []);
74
- props.onValueChange?.(nextValue);
75
- }, [disabled, expandedValues, isControlled, props]);
76
- const contextValue = useMemo(() => ({ isExpanded, toggle, disabled }), [isExpanded, toggle, disabled]);
77
- return (_jsx(AccordionContext.Provider, { value: contextValue, children: _jsx(html.div, { style: sanitizeWebStyle(style), children: children }) }));
69
+ function Accordion({ children, style, ...props }) {
70
+ if (Platform.OS === "web") {
71
+ return (_jsx(WebAccordionRoot, { ...props, style: style, children: children }));
72
+ }
73
+ return (_jsx(AccordionPrimitive.Root, { ...props, asChild: true, children: _jsx(View, { style: style, children: children }) }));
78
74
  }
79
- const itemStyles = css.create({
80
- base: (borderBottomColor) => ({
81
- borderBottomWidth: 1,
82
- borderBottomStyle: "solid",
83
- borderBottomColor,
84
- overflow: "hidden",
85
- }),
86
- });
87
75
  /**
88
76
  * Accordion Item Component
89
- * Individual accordion item with a bottom border. Shares its expanded state
90
- * with the Trigger (chevron) and Content via context.
77
+ * Individual accordion item with border styling
91
78
  */
92
- function AccordionItem({ value, disabled: itemDisabled, children, style }) {
79
+ function AccordionItem({ children, value, style: styleOverride, ...props }) {
93
80
  const { theme } = useTheme();
94
- const { isExpanded, disabled: rootDisabled } = useAccordionContext();
95
- const itemContext = useMemo(() => ({
96
- value,
97
- isExpanded: isExpanded(value),
98
- disabled: !!itemDisabled || rootDisabled,
99
- }), [value, isExpanded, itemDisabled, rootDisabled]);
100
- return (_jsx(AccordionItemContext.Provider, { value: itemContext, children: _jsx(html.div, { style: [
101
- itemStyles.base(theme.colors.border),
102
- sanitizeWebStyle(style),
81
+ return (_jsx(AccordionPrimitive.Item, { value: value, asChild: true, ...props, children: _jsx(View, { style: [
82
+ {
83
+ borderBottomWidth: 1,
84
+ borderBottomColor: theme.colors.border,
85
+ overflow: "hidden",
86
+ },
87
+ // Spread array styles from primitives to prevent nested arrays on web
88
+ ...(styleOverride && typeof styleOverride !== "function"
89
+ ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
90
+ : []),
103
91
  ], children: children }) }));
104
92
  }
105
- const triggerStyles = css.create({
106
- // gap (16) = spacing.md, borderRadius (6) = spacing.radiusMd, paddingVertical (16) = spacing.md.
107
- base: {
108
- display: "flex",
109
- flexDirection: "row",
110
- alignItems: "center",
111
- justifyContent: "space-between",
112
- gap: 16,
113
- borderRadius: 6,
114
- paddingTop: 16,
115
- paddingBottom: 16,
116
- margin: 0,
117
- borderWidth: 0,
118
- backgroundColor: "transparent",
119
- appearance: "none",
120
- cursor: "pointer",
121
- userSelect: "none",
122
- textAlign: "inherit",
123
- width: "100%",
124
- },
125
- disabled: { cursor: "not-allowed" },
126
- // Chevron rotates 0deg (closed) → 180deg (open). Transition gated on motion.
127
- chevron: (rotate, animated) => ({
128
- display: "flex",
129
- transform: rotate,
130
- transitionProperty: animated ? "transform" : "none",
131
- transitionDuration: animated ? "0.2s" : "0s",
132
- transitionTimingFunction: "ease",
133
- }),
134
- });
93
+ const Trigger = Platform.OS === "web" ? View : Pressable;
135
94
  /**
136
95
  * Accordion Trigger Component
137
- * Clickable header that expands/collapses the item. Includes an animated
138
- * chevron that rotates with the expanded state.
96
+ * Clickable header that expands/collapses the content
97
+ * Includes animated chevron icon
139
98
  */
140
- function AccordionTrigger({ children, style }) {
99
+ function AccordionTrigger({ children, style: styleOverride, ...props }) {
141
100
  const { theme } = useTheme();
142
101
  const reduceMotion = useReducedMotion();
143
- const { toggle } = useAccordionContext();
144
- const { value, isExpanded, disabled } = useAccordionItemContext();
145
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(html.button, { type: "button", "aria-expanded": isExpanded, "aria-disabled": disabled, disabled: disabled, onClick: () => !disabled && toggle(value), style: [
146
- triggerStyles.base,
147
- disabled ? triggerStyles.disabled : null,
148
- sanitizeWebStyle(style),
149
- ], children: [_jsx(_Fragment, { children: children }), _jsx(html.span, { style: triggerStyles.chevron(isExpanded ? "rotate(180deg)" : "rotate(0deg)", !reduceMotion), children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }));
102
+ const { isExpanded } = AccordionPrimitive.useItemContext();
103
+ const rotation = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
104
+ useEffect(() => {
105
+ const target = isExpanded ? 1 : 0;
106
+ Animated.timing(rotation, {
107
+ toValue: target,
108
+ duration: reduceMotion ? 0 : isExpanded ? 200 : 150,
109
+ useNativeDriver: true,
110
+ }).start();
111
+ }, [isExpanded, reduceMotion, rotation]);
112
+ const chevronStyle = {
113
+ transform: [
114
+ {
115
+ rotate: rotation.interpolate({
116
+ inputRange: [0, 1],
117
+ outputRange: ["0deg", "180deg"],
118
+ }),
119
+ },
120
+ ],
121
+ };
122
+ return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(AccordionPrimitive.Trigger, { ...props, asChild: true, children: _jsxs(Trigger, { style: [
123
+ {
124
+ flexDirection: "row",
125
+ alignItems: "center",
126
+ justifyContent: "space-between",
127
+ gap: spacing.md,
128
+ borderRadius: spacing.radiusMd,
129
+ paddingVertical: spacing.md,
130
+ ...(Platform.OS === "web" && {
131
+ cursor: "pointer",
132
+ userSelect: "none",
133
+ }),
134
+ },
135
+ // Spread array styles from primitives to prevent nested arrays on web
136
+ ...(styleOverride && typeof styleOverride !== "function"
137
+ ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
138
+ : []),
139
+ ], children: [_jsx(_Fragment, { children: children }), _jsx(Animated.View, { style: chevronStyle, children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }) }) }));
150
140
  }
151
- const contentStyles = css.create({
152
- // paddingBottom (8) = spacing.sm.
153
- base: {
154
- paddingBottom: 8,
155
- overflow: "hidden",
156
- },
157
- hidden: { display: "none" },
158
- });
159
141
  /**
160
142
  * Accordion Content Component
161
- * Expandable content area. Unmounts when collapsed unless `forceMount` is set.
143
+ * Expandable content area with animations
162
144
  */
163
- function AccordionContent({ forceMount, children, style }) {
164
- const { isExpanded } = useAccordionItemContext();
165
- if (!isExpanded && !forceMount)
166
- return null;
167
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(html.div, { style: [
168
- contentStyles.base,
169
- !isExpanded ? contentStyles.hidden : null,
170
- sanitizeWebStyle(style),
171
- ], children: children }) }));
145
+ function AccordionContent({ children, style: styleOverride, ...props }) {
146
+ return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(AccordionPrimitive.Content, { ...props, children: _jsx(View, { style: [
147
+ {
148
+ paddingBottom: spacing.sm,
149
+ overflow: "hidden",
150
+ },
151
+ // Spread array styles from primitives to prevent nested arrays on web
152
+ ...(styleOverride && typeof styleOverride !== "function"
153
+ ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
154
+ : []),
155
+ ], children: children }) }) }));
172
156
  }
173
157
  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
- import { type LooseStyle } from "../lib/styles";
2
+ import { ViewProps } from "react-native";
3
3
  /**
4
4
  * Animation type options
5
5
  */
6
6
  export type AnimationType = "fade" | "fadeSlideUp" | "fadeSlideDown" | "scale";
7
- export interface AnimatedViewProps {
7
+ interface AnimatedViewProps extends ViewProps {
8
8
  children: React.ReactNode;
9
9
  /**
10
10
  * Type of animation to use
@@ -22,23 +22,34 @@ export interface AnimatedViewProps {
22
22
  * @default 0
23
23
  */
24
24
  delay?: number;
25
- /** Custom style override */
26
- style?: LooseStyle;
27
- /** Test identifier (forwarded as data-testid on web) */
28
- testID?: string;
29
25
  }
30
26
  /**
31
27
  * Cross-Platform Animated View Component
28
+ * Uses React Native Animated for lightweight cross-platform animations
32
29
  *
33
- * Plays a one-shot entrance animation via CSS keyframes on web. Respects the
34
- * reduced-motion accessibility preference (renders statically when set). On
35
- * native the keyframes are a no-op, so children appear immediately — the
36
- * package ships no Reanimated, a documented animation gap.
30
+ * Features:
31
+ * - Multiple animation types (fade, fadeSlideUp, fadeSlideDown, scale)
32
+ * - Configurable enter duration
33
+ * - Optional delay for staggered animations
34
+ * - Respects reduced motion accessibility preference
37
35
  *
38
36
  * Usage:
39
37
  * ```tsx
40
- * <AnimatedView>{children}</AnimatedView>
41
- * <AnimatedView type="fadeSlideUp" delay={100}>{children}</AnimatedView>
38
+ * // Simple fade
39
+ * <AnimatedView>
40
+ * {children}
41
+ * </AnimatedView>
42
+ *
43
+ * // Fade with slide up
44
+ * <AnimatedView type="fadeSlideUp">
45
+ * {children}
46
+ * </AnimatedView>
47
+ *
48
+ * // With delay (for staggered lists)
49
+ * <AnimatedView type="fadeSlideUp" delay={100}>
50
+ * {children}
51
+ * </AnimatedView>
42
52
  * ```
43
53
  */
44
- export declare function AnimatedView({ children, type, enterDuration, delay, style, testID, }: AnimatedViewProps): React.JSX.Element;
54
+ export declare function AnimatedView({ children, type, enterDuration, delay, style, ...props }: AnimatedViewProps): React.JSX.Element;
55
+ export {};
@@ -1,80 +1,39 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { css, html } from "react-strict-dom";
3
- import { useReducedMotion } from "../hooks/useReduceMotion.js";
4
- import { sanitizeWebStyle } from "../lib/styles.js";
5
- // Entrance keyframes. Slide distance (8px) and initial scale (0.95) mirror the
6
- // previous useStaggeredEntrance defaults. css.keyframes must be defined inline
7
- // and unconditionally in the same file as the css.create that references them
8
- // (StyleX can't fold a cross-module or ternary-gated keyframe name). On native
9
- // css.keyframes is a no-op and animationName has no effect, so views appear
10
- // instantly — documented animation fidelity gap (no Reanimated in the package).
11
- const fadeFrames = css.keyframes({
12
- "0%": { opacity: 0 },
13
- "100%": { opacity: 1 },
14
- });
15
- const fadeSlideUpFrames = css.keyframes({
16
- "0%": { opacity: 0, transform: "translateY(8px)" },
17
- "100%": { opacity: 1, transform: "translateY(0)" },
18
- });
19
- const fadeSlideDownFrames = css.keyframes({
20
- "0%": { opacity: 0, transform: "translateY(-8px)" },
21
- "100%": { opacity: 1, transform: "translateY(0)" },
22
- });
23
- const scaleFrames = css.keyframes({
24
- "0%": { opacity: 0, transform: "scale(0.95)" },
25
- "100%": { opacity: 1, transform: "scale(1)" },
26
- });
27
- // Duration/delay are runtime props, so they flow in as function-rule args.
28
- // animationFillMode:"both" holds the 0% state during the delay (so staggered
29
- // items stay hidden until their turn) and the 100% state after completion.
30
- const styles = css.create({
31
- fade: (duration, delay) => ({
32
- animationName: fadeFrames,
33
- animationDuration: duration,
34
- animationDelay: delay,
35
- animationTimingFunction: "ease-out",
36
- animationFillMode: "both",
37
- }),
38
- fadeSlideUp: (duration, delay) => ({
39
- animationName: fadeSlideUpFrames,
40
- animationDuration: duration,
41
- animationDelay: delay,
42
- animationTimingFunction: "ease-out",
43
- animationFillMode: "both",
44
- }),
45
- fadeSlideDown: (duration, delay) => ({
46
- animationName: fadeSlideDownFrames,
47
- animationDuration: duration,
48
- animationDelay: delay,
49
- animationTimingFunction: "ease-out",
50
- animationFillMode: "both",
51
- }),
52
- scale: (duration, delay) => ({
53
- animationName: scaleFrames,
54
- animationDuration: duration,
55
- animationDelay: delay,
56
- animationTimingFunction: "ease-out",
57
- animationFillMode: "both",
58
- }),
59
- });
2
+ import { Animated } from "react-native";
3
+ import { useStaggeredEntrance } from "../hooks/useStaggeredEntrance.js";
60
4
  /**
61
5
  * Cross-Platform Animated View Component
6
+ * Uses React Native Animated for lightweight cross-platform animations
62
7
  *
63
- * Plays a one-shot entrance animation via CSS keyframes on web. Respects the
64
- * reduced-motion accessibility preference (renders statically when set). On
65
- * native the keyframes are a no-op, so children appear immediately — the
66
- * package ships no Reanimated, a documented animation gap.
8
+ * Features:
9
+ * - Multiple animation types (fade, fadeSlideUp, fadeSlideDown, scale)
10
+ * - Configurable enter duration
11
+ * - Optional delay for staggered animations
12
+ * - Respects reduced motion accessibility preference
67
13
  *
68
14
  * Usage:
69
15
  * ```tsx
70
- * <AnimatedView>{children}</AnimatedView>
71
- * <AnimatedView type="fadeSlideUp" delay={100}>{children}</AnimatedView>
16
+ * // Simple fade
17
+ * <AnimatedView>
18
+ * {children}
19
+ * </AnimatedView>
20
+ *
21
+ * // Fade with slide up
22
+ * <AnimatedView type="fadeSlideUp">
23
+ * {children}
24
+ * </AnimatedView>
25
+ *
26
+ * // With delay (for staggered lists)
27
+ * <AnimatedView type="fadeSlideUp" delay={100}>
28
+ * {children}
29
+ * </AnimatedView>
72
30
  * ```
73
31
  */
74
- export function AnimatedView({ children, type = "fade", enterDuration = 200, delay = 0, style, testID, }) {
75
- const reduceMotion = useReducedMotion();
76
- const duration = `${enterDuration}ms`;
77
- const delayMs = `${delay}ms`;
78
- const animation = reduceMotion ? null : styles[type](duration, delayMs);
79
- return (_jsx(html.div, { "data-testid": testID, style: [animation, sanitizeWebStyle(style)], children: children }));
32
+ export function AnimatedView({ children, type = "fade", enterDuration = 200, delay = 0, style, ...props }) {
33
+ const entranceStyle = useStaggeredEntrance({
34
+ type,
35
+ delay,
36
+ duration: enterDuration,
37
+ });
38
+ return (_jsx(Animated.View, { style: [style, entranceStyle], ...props, children: children }));
80
39
  }