@mrmeg/expo-ui 0.11.0 → 0.13.0-rsd.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 (85) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LLM_USAGE.md +1 -0
  3. package/README.md +2 -1
  4. package/dist/components/Accordion.d.ts +38 -18
  5. package/dist/components/Accordion.js +137 -121
  6. package/dist/components/AnimatedView.d.ts +13 -24
  7. package/dist/components/AnimatedView.js +70 -29
  8. package/dist/components/Badge.d.ts +2 -2
  9. package/dist/components/Badge.js +45 -47
  10. package/dist/components/BottomSheet.d.ts +30 -73
  11. package/dist/components/BottomSheet.js +240 -188
  12. package/dist/components/Button.d.ts +43 -56
  13. package/dist/components/Button.js +189 -203
  14. package/dist/components/Card.d.ts +7 -10
  15. package/dist/components/Card.js +90 -105
  16. package/dist/components/Checkbox.d.ts +20 -8
  17. package/dist/components/Checkbox.js +77 -80
  18. package/dist/components/Collapsible.d.ts +47 -47
  19. package/dist/components/Collapsible.js +108 -29
  20. package/dist/components/Dialog.d.ts +80 -121
  21. package/dist/components/Dialog.js +225 -132
  22. package/dist/components/DismissKeyboard.d.ts +1 -1
  23. package/dist/components/DismissKeyboard.js +73 -22
  24. package/dist/components/Drawer.d.ts +37 -76
  25. package/dist/components/Drawer.js +252 -486
  26. package/dist/components/DropdownMenu.d.ts +106 -113
  27. package/dist/components/DropdownMenu.js +350 -204
  28. package/dist/components/EmptyState.d.ts +2 -2
  29. package/dist/components/EmptyState.js +41 -34
  30. package/dist/components/InputOTP.d.ts +19 -53
  31. package/dist/components/InputOTP.js +81 -102
  32. package/dist/components/KeyboardAvoidingView.d.ts +23 -0
  33. package/dist/components/KeyboardAvoidingView.js +33 -0
  34. package/dist/components/Label.d.ts +12 -17
  35. package/dist/components/Label.js +38 -51
  36. package/dist/components/MaxWidthContainer.d.ts +5 -16
  37. package/dist/components/MaxWidthContainer.js +28 -29
  38. package/dist/components/Notification.d.ts +5 -9
  39. package/dist/components/Notification.js +190 -187
  40. package/dist/components/Popover.d.ts +38 -66
  41. package/dist/components/Popover.js +158 -69
  42. package/dist/components/Progress.d.ts +5 -4
  43. package/dist/components/Progress.js +65 -77
  44. package/dist/components/RadioGroup.d.ts +30 -24
  45. package/dist/components/RadioGroup.js +90 -94
  46. package/dist/components/Select.d.ts +62 -74
  47. package/dist/components/Select.js +241 -154
  48. package/dist/components/Separator.d.ts +13 -23
  49. package/dist/components/Separator.js +29 -36
  50. package/dist/components/Skeleton.d.ts +8 -7
  51. package/dist/components/Skeleton.js +74 -61
  52. package/dist/components/StyledText.context.d.ts +6 -2
  53. package/dist/components/StyledText.context.js +3 -0
  54. package/dist/components/StyledText.d.ts +29 -7
  55. package/dist/components/StyledText.js +92 -29
  56. package/dist/components/Switch.d.ts +18 -6
  57. package/dist/components/Switch.js +112 -106
  58. package/dist/components/Tabs.d.ts +26 -16
  59. package/dist/components/Tabs.js +189 -91
  60. package/dist/components/TextInput.d.ts +6 -19
  61. package/dist/components/TextInput.js +261 -195
  62. package/dist/components/Toggle.d.ts +23 -41
  63. package/dist/components/Toggle.js +84 -98
  64. package/dist/components/ToggleGroup.d.ts +37 -29
  65. package/dist/components/ToggleGroup.js +113 -108
  66. package/dist/components/Tooltip.d.ts +41 -111
  67. package/dist/components/Tooltip.js +156 -118
  68. package/dist/components/UIProvider.d.ts +13 -2
  69. package/dist/components/UIProvider.js +7 -3
  70. package/dist/components/index.d.ts +1 -0
  71. package/dist/components/index.js +1 -0
  72. package/dist/components/keyboardFocusRegistry.d.ts +15 -0
  73. package/dist/components/keyboardFocusRegistry.js +27 -0
  74. package/dist/hooks/useTheme.d.ts +34 -10
  75. package/dist/hooks/useTheme.js +20 -8
  76. package/dist/lib/index.d.ts +2 -0
  77. package/dist/lib/index.js +2 -0
  78. package/dist/lib/portal.d.ts +16 -0
  79. package/dist/lib/portal.js +84 -0
  80. package/dist/lib/styles.d.ts +32 -0
  81. package/dist/lib/styles.js +91 -0
  82. package/dist/lib/useAnchoredPosition.d.ts +57 -0
  83. package/dist/lib/useAnchoredPosition.js +120 -0
  84. package/llms-full.md +9 -4
  85. package/package.json +4 -22
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
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.12.0]
7
+
8
+ ### Added
9
+
10
+ - **`KeyboardAvoidingView` is now a public package component.** Native uses
11
+ `react-native-keyboard-controller` with `automaticOffset` enabled by default,
12
+ while web renders a plain `View`.
13
+ - **`UIProvider` now owns app-wide native keyboard avoidance by default.** Apps
14
+ that mount `KeyboardProvider` above `UIProvider` get root-level keyboard
15
+ avoiding behavior without adding per-screen `KeyboardAvoidingView` wrappers.
16
+ Pass `keyboardAvoiding={false}` to opt out, or `keyboardAvoidingProps` to tune
17
+ the root wrapper. Web skips the root keyboard wrapper unless explicitly
18
+ enabled.
19
+
20
+ ### Fixed
21
+
22
+ - **`DismissKeyboard` no longer nests keyboard-avoiding wrappers when the root
23
+ provider already owns keyboard avoidance.**
24
+
6
25
  ## [0.11.0]
7
26
 
8
27
  ### Added
package/LLM_USAGE.md CHANGED
@@ -177,6 +177,7 @@ Use this table before creating a new app-local primitive.
177
177
  | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text |
178
178
  | `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user |
179
179
  | `DismissKeyboard` | Tap-away keyboard dismissal | Screen-level keyboard handling | Forms, search screens, sign-in screens |
180
+ | `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers | Screen roots, composer footers, form-heavy subtrees |
180
181
  | `Drawer` | Side panels and drawer navigation | Custom sliding panels | Filter drawer, app navigation drawer, inspector panel |
181
182
  | `DropdownMenu` | Menus and command lists | Homemade popover menus | Row actions, account menu, sort menu |
182
183
  | `EmptyState` | No-data or recoverable error regions | One-off empty placeholders | Empty inbox, no search results, failed list load |
package/README.md CHANGED
@@ -222,6 +222,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
222
222
  | `ErrorBoundary` | React render error fallback | Unhandled screen crashes | Route-level fallback, feature boundary, recoverable widget crashes |
223
223
  | `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors | Button accessories, empty-state icons, menu icons, status glyphs |
224
224
  | `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
225
+ | `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers | Screen roots, composer footers, form-heavy subtrees |
225
226
  | `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
226
227
  | `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms, auth panels |
227
228
  | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, loading toast, bottom-position alerts |
@@ -272,7 +273,7 @@ Use `Button.preset`, not `variant`. `default` is the neutral primary action, `se
272
273
 
273
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.
274
275
 
275
- Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `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`.
276
+ 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`.
276
277
 
277
278
  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.
278
279
 
@@ -1,28 +1,33 @@
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> & {
1
+ import * as React from "react";
2
+ import { type LooseStyle } from "../lib/styles";
3
+ type BaseAccordionRootProps = {
4
4
  disabled?: boolean;
5
+ /** Single-type only: allow closing the open item by re-pressing it. */
5
6
  collapsible?: boolean;
7
+ /** Platform: WEB ONLY (kept for API compat; unused). */
6
8
  dir?: "ltr" | "rtl";
9
+ /** Platform: WEB ONLY (kept for API compat; unused). */
7
10
  orientation?: "vertical" | "horizontal";
8
- style?: ViewStyle;
11
+ style?: LooseStyle;
12
+ children?: React.ReactNode;
9
13
  };
10
- type WebSingleAccordionRootProps = BaseAccordionRootProps & {
14
+ type SingleAccordionRootProps = BaseAccordionRootProps & {
11
15
  type: "single";
12
16
  defaultValue?: string;
13
17
  value?: string;
14
18
  onValueChange?: (value: string | undefined) => void;
15
19
  };
16
- type WebMultipleAccordionRootProps = BaseAccordionRootProps & {
20
+ type MultipleAccordionRootProps = BaseAccordionRootProps & {
17
21
  type: "multiple";
18
22
  defaultValue?: string[];
19
23
  value?: string[];
20
24
  onValueChange?: (value: string[]) => void;
21
25
  };
22
- type AccordionRootProps = WebSingleAccordionRootProps | WebMultipleAccordionRootProps;
26
+ type AccordionRootProps = SingleAccordionRootProps | MultipleAccordionRootProps;
23
27
  /**
24
- * Accordion Root Component
25
- * Container for accordion items with support for single or multiple open items
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.
26
31
  *
27
32
  * Usage:
28
33
  * <Accordion type="single" collapsible>
@@ -32,23 +37,38 @@ type AccordionRootProps = WebSingleAccordionRootProps | WebMultipleAccordionRoot
32
37
  * </AccordionItem>
33
38
  * </Accordion>
34
39
  */
35
- declare function Accordion({ children, style, ...props }: AccordionRootProps): import("react").JSX.Element;
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
+ }
36
47
  /**
37
48
  * Accordion Item Component
38
- * Individual accordion item with border styling
49
+ * Individual accordion item with a bottom border. Shares its expanded state
50
+ * with the Trigger (chevron) and Content via context.
39
51
  */
40
- declare function AccordionItem({ children, value, style: styleOverride, ...props }: AccordionPrimitive.ItemProps & React.RefAttributes<AccordionPrimitive.ItemRef>): import("react").JSX.Element;
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
+ }
41
57
  /**
42
58
  * Accordion Trigger Component
43
- * Clickable header that expands/collapses the content
44
- * Includes animated chevron icon
59
+ * Clickable header that expands/collapses the item. Includes an animated
60
+ * chevron that rotates with the expanded state.
45
61
  */
46
- declare function AccordionTrigger({ children, style: styleOverride, ...props }: AccordionPrimitive.TriggerProps & {
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;
47
66
  children?: React.ReactNode;
48
- } & React.RefAttributes<AccordionPrimitive.TriggerRef>): import("react").JSX.Element;
67
+ style?: LooseStyle;
68
+ }
49
69
  /**
50
70
  * Accordion Content Component
51
- * Expandable content area with animations
71
+ * Expandable content area. Unmounts when collapsed unless `forceMount` is set.
52
72
  */
53
- declare function AccordionContent({ children, style: styleOverride, ...props }: AccordionPrimitive.ContentProps & React.RefAttributes<AccordionPrimitive.ContentRef>): import("react").JSX.Element;
73
+ declare function AccordionContent({ forceMount, children, style }: AccordionContentProps): React.JSX.Element | null;
54
74
  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -1,62 +1,31 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
3
- import { Animated, Platform, Pressable, View } from "react-native";
2
+ import { createContext, use, useCallback, useMemo, useState } from "react";
3
+ import { css, html } from "react-strict-dom";
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 { 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 }) }));
34
- }
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 }) }));
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;
50
16
  }
51
- function WebAccordionRoot(props) {
52
- if (props.type === "multiple") {
53
- return _jsx(WebMultipleAccordionRoot, { ...props });
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>");
54
22
  }
55
- return _jsx(WebSingleAccordionRoot, { ...props });
23
+ return ctx;
56
24
  }
57
25
  /**
58
- * Accordion Root Component
59
- * Container for accordion items with support for single or multiple open items
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.
60
29
  *
61
30
  * Usage:
62
31
  * <Accordion type="single" collapsible>
@@ -66,92 +35,139 @@ function WebAccordionRoot(props) {
66
35
  * </AccordionItem>
67
36
  * </Accordion>
68
37
  */
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 }) }));
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 }) }));
74
78
  }
79
+ const itemStyles = css.create({
80
+ base: (borderBottomColor) => ({
81
+ borderBottomWidth: 1,
82
+ borderBottomStyle: "solid",
83
+ borderBottomColor,
84
+ overflow: "hidden",
85
+ }),
86
+ });
75
87
  /**
76
88
  * Accordion Item Component
77
- * Individual accordion item with border styling
89
+ * Individual accordion item with a bottom border. Shares its expanded state
90
+ * with the Trigger (chevron) and Content via context.
78
91
  */
79
- function AccordionItem({ children, value, style: styleOverride, ...props }) {
92
+ function AccordionItem({ value, disabled: itemDisabled, children, style }) {
80
93
  const { theme } = useTheme();
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
- : []),
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),
91
103
  ], children: children }) }));
92
104
  }
93
- const Trigger = Platform.OS === "web" ? View : Pressable;
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
+ });
94
135
  /**
95
136
  * Accordion Trigger Component
96
- * Clickable header that expands/collapses the content
97
- * Includes animated chevron icon
137
+ * Clickable header that expands/collapses the item. Includes an animated
138
+ * chevron that rotates with the expanded state.
98
139
  */
99
- function AccordionTrigger({ children, style: styleOverride, ...props }) {
140
+ function AccordionTrigger({ children, style }) {
100
141
  const { theme } = useTheme();
101
142
  const reduceMotion = useReducedMotion();
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 }) })] }) }) }) }) }));
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 }) })] }) }) }));
140
150
  }
151
+ const contentStyles = css.create({
152
+ // paddingBottom (8) = spacing.sm.
153
+ base: {
154
+ paddingBottom: 8,
155
+ overflow: "hidden",
156
+ },
157
+ hidden: { display: "none" },
158
+ });
141
159
  /**
142
160
  * Accordion Content Component
143
- * Expandable content area with animations
161
+ * Expandable content area. Unmounts when collapsed unless `forceMount` is set.
144
162
  */
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 }) }) }));
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 }) }));
156
172
  }
157
173
  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
- import { ViewProps } from "react-native";
2
+ import { type LooseStyle } from "../lib/styles";
3
3
  /**
4
4
  * Animation type options
5
5
  */
6
6
  export type AnimationType = "fade" | "fadeSlideUp" | "fadeSlideDown" | "scale";
7
- interface AnimatedViewProps extends ViewProps {
7
+ export interface AnimatedViewProps {
8
8
  children: React.ReactNode;
9
9
  /**
10
10
  * Type of animation to use
@@ -22,34 +22,23 @@ interface AnimatedViewProps extends ViewProps {
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;
25
29
  }
26
30
  /**
27
31
  * Cross-Platform Animated View Component
28
- * Uses React Native Animated for lightweight cross-platform animations
29
32
  *
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
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.
35
37
  *
36
38
  * Usage:
37
39
  * ```tsx
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>
40
+ * <AnimatedView>{children}</AnimatedView>
41
+ * <AnimatedView type="fadeSlideUp" delay={100}>{children}</AnimatedView>
52
42
  * ```
53
43
  */
54
- export declare function AnimatedView({ children, type, enterDuration, delay, style, ...props }: AnimatedViewProps): React.JSX.Element;
55
- export {};
44
+ export declare function AnimatedView({ children, type, enterDuration, delay, style, testID, }: AnimatedViewProps): React.JSX.Element;
@@ -1,39 +1,80 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Animated } from "react-native";
3
- import { useStaggeredEntrance } from "../hooks/useStaggeredEntrance.js";
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
+ });
4
60
  /**
5
61
  * Cross-Platform Animated View Component
6
- * Uses React Native Animated for lightweight cross-platform animations
7
62
  *
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
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.
13
67
  *
14
68
  * Usage:
15
69
  * ```tsx
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>
70
+ * <AnimatedView>{children}</AnimatedView>
71
+ * <AnimatedView type="fadeSlideUp" delay={100}>{children}</AnimatedView>
30
72
  * ```
31
73
  */
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 }));
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 }));
39
80
  }
@@ -1,11 +1,11 @@
1
1
  import React from "react";
2
- import { StyleProp, ViewStyle } from "react-native";
2
+ import { type LooseStyle } from "../lib/styles";
3
3
  export type BadgeVariant = "default" | "secondary" | "outline" | "destructive";
4
4
  export interface BadgeProps {
5
5
  children?: React.ReactNode;
6
6
  text?: string;
7
7
  variant?: BadgeVariant;
8
- style?: StyleProp<ViewStyle>;
8
+ style?: LooseStyle;
9
9
  }
10
10
  /**
11
11
  * Badge Component