panelui-native 0.6.0 → 0.9.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 (181) hide show
  1. package/README.md +1 -1
  2. package/lib/module/components/accordion/index.js +7 -5
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/alert/index.js +3 -6
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/attachment/index.js +219 -0
  7. package/lib/module/components/attachment/index.js.map +1 -0
  8. package/lib/module/components/bottom-sheet/index.js +82 -4
  9. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  10. package/lib/module/components/button/index.js +36 -1
  11. package/lib/module/components/button/index.js.map +1 -1
  12. package/lib/module/components/dialog/index.js +6 -2
  13. package/lib/module/components/dialog/index.js.map +1 -1
  14. package/lib/module/components/empty-state/index.js +98 -22
  15. package/lib/module/components/empty-state/index.js.map +1 -1
  16. package/lib/module/components/inline-select/index.js +171 -46
  17. package/lib/module/components/inline-select/index.js.map +1 -1
  18. package/lib/module/components/input/index.js +28 -5
  19. package/lib/module/components/input/index.js.map +1 -1
  20. package/lib/module/components/input-group/index.js +0 -1
  21. package/lib/module/components/input-group/index.js.map +1 -1
  22. package/lib/module/components/item/index.js +359 -0
  23. package/lib/module/components/item/index.js.map +1 -0
  24. package/lib/module/components/label/index.js +5 -3
  25. package/lib/module/components/label/index.js.map +1 -1
  26. package/lib/module/components/line-chart/index.js +1024 -0
  27. package/lib/module/components/line-chart/index.js.map +1 -0
  28. package/lib/module/components/marker/index.js +190 -0
  29. package/lib/module/components/marker/index.js.map +1 -0
  30. package/lib/module/components/message/index.js +334 -0
  31. package/lib/module/components/message/index.js.map +1 -0
  32. package/lib/module/components/message-scroller/index.js +451 -0
  33. package/lib/module/components/message-scroller/index.js.map +1 -0
  34. package/lib/module/components/popover/index.js +500 -0
  35. package/lib/module/components/popover/index.js.map +1 -0
  36. package/lib/module/components/radio-group/index.js +104 -16
  37. package/lib/module/components/radio-group/index.js.map +1 -1
  38. package/lib/module/components/scroll-fade/index.js +119 -71
  39. package/lib/module/components/scroll-fade/index.js.map +1 -1
  40. package/lib/module/components/select/index.js +35 -6
  41. package/lib/module/components/select/index.js.map +1 -1
  42. package/lib/module/components/separator/index.js +91 -0
  43. package/lib/module/components/separator/index.js.map +1 -0
  44. package/lib/module/components/shimmer/index.js +111 -64
  45. package/lib/module/components/shimmer/index.js.map +1 -1
  46. package/lib/module/components/surface/index.js +24 -4
  47. package/lib/module/components/surface/index.js.map +1 -1
  48. package/lib/module/components/switch/index.js +20 -1
  49. package/lib/module/components/switch/index.js.map +1 -1
  50. package/lib/module/components/toast/index.js +7 -8
  51. package/lib/module/components/toast/index.js.map +1 -1
  52. package/lib/module/components/typography/index.js +3 -3
  53. package/lib/module/hooks/index.js +1 -0
  54. package/lib/module/hooks/index.js.map +1 -1
  55. package/lib/module/hooks/use-breakpoint.js +2 -2
  56. package/lib/module/hooks/use-copy-to-clipboard.js +0 -2
  57. package/lib/module/hooks/use-copy-to-clipboard.js.map +1 -1
  58. package/lib/module/hooks/use-keyboard-avoidance.js +120 -0
  59. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -0
  60. package/lib/module/icons/index.js +73 -4
  61. package/lib/module/icons/index.js.map +1 -1
  62. package/lib/module/index.js +13 -1
  63. package/lib/module/index.js.map +1 -1
  64. package/lib/module/native/index.js +53 -0
  65. package/lib/module/native/index.js.map +1 -0
  66. package/lib/module/primitives/keyboard-avoider.js +40 -0
  67. package/lib/module/primitives/keyboard-avoider.js.map +1 -0
  68. package/lib/module/primitives/scrim.js +72 -0
  69. package/lib/module/primitives/scrim.js.map +1 -0
  70. package/lib/module/providers/panel-ui-provider.js +24 -4
  71. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  72. package/lib/module/theme/use-theme.js +1 -1
  73. package/lib/typescript/src/components/accordion/index.d.ts +7 -5
  74. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  75. package/lib/typescript/src/components/alert/index.d.ts +3 -6
  76. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  77. package/lib/typescript/src/components/attachment/index.d.ts +93 -0
  78. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -0
  79. package/lib/typescript/src/components/bottom-sheet/index.d.ts +27 -2
  80. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/button/index.d.ts +11 -0
  82. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/dialog/index.d.ts +7 -1
  84. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/empty-state/index.d.ts +156 -7
  86. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/inline-select/index.d.ts +29 -6
  88. package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/input/index.d.ts +13 -0
  90. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/input-group/index.d.ts +0 -1
  92. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/item/index.d.ts +286 -0
  94. package/lib/typescript/src/components/item/index.d.ts.map +1 -0
  95. package/lib/typescript/src/components/label/index.d.ts +5 -3
  96. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/line-chart/index.d.ts +247 -0
  98. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -0
  99. package/lib/typescript/src/components/marker/index.d.ts +137 -0
  100. package/lib/typescript/src/components/marker/index.d.ts.map +1 -0
  101. package/lib/typescript/src/components/message/index.d.ts +177 -0
  102. package/lib/typescript/src/components/message/index.d.ts.map +1 -0
  103. package/lib/typescript/src/components/message-scroller/index.d.ts +150 -0
  104. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -0
  105. package/lib/typescript/src/components/popover/index.d.ts +130 -0
  106. package/lib/typescript/src/components/popover/index.d.ts.map +1 -0
  107. package/lib/typescript/src/components/radio-group/index.d.ts +32 -0
  108. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  109. package/lib/typescript/src/components/scroll-fade/index.d.ts +18 -9
  110. package/lib/typescript/src/components/scroll-fade/index.d.ts.map +1 -1
  111. package/lib/typescript/src/components/select/index.d.ts +16 -1
  112. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/separator/index.d.ts +47 -0
  114. package/lib/typescript/src/components/separator/index.d.ts.map +1 -0
  115. package/lib/typescript/src/components/shimmer/index.d.ts +38 -20
  116. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
  117. package/lib/typescript/src/components/surface/index.d.ts +51 -2
  118. package/lib/typescript/src/components/surface/index.d.ts.map +1 -1
  119. package/lib/typescript/src/components/switch/index.d.ts +10 -0
  120. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  121. package/lib/typescript/src/components/toast/index.d.ts +4 -4
  122. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  123. package/lib/typescript/src/hooks/index.d.ts +1 -0
  124. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  125. package/lib/typescript/src/hooks/use-breakpoint.d.ts +2 -2
  126. package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts +0 -2
  127. package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts.map +1 -1
  128. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +20 -0
  129. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -0
  130. package/lib/typescript/src/icons/index.d.ts +10 -4
  131. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  132. package/lib/typescript/src/index.d.ts +11 -1
  133. package/lib/typescript/src/index.d.ts.map +1 -1
  134. package/lib/typescript/src/native/index.d.ts +35 -0
  135. package/lib/typescript/src/native/index.d.ts.map +1 -0
  136. package/lib/typescript/src/primitives/keyboard-avoider.d.ts +22 -0
  137. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -0
  138. package/lib/typescript/src/primitives/scrim.d.ts +21 -0
  139. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -0
  140. package/lib/typescript/src/providers/panel-ui-provider.d.ts +1 -1
  141. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  142. package/lib/typescript/src/theme/use-theme.d.ts +1 -1
  143. package/package.json +16 -3
  144. package/src/components/accordion/index.tsx +7 -5
  145. package/src/components/alert/index.tsx +3 -6
  146. package/src/components/attachment/index.tsx +279 -0
  147. package/src/components/bottom-sheet/index.tsx +102 -2
  148. package/src/components/button/index.tsx +52 -0
  149. package/src/components/dialog/index.tsx +10 -1
  150. package/src/components/empty-state/index.tsx +64 -19
  151. package/src/components/inline-select/index.tsx +191 -44
  152. package/src/components/input/index.tsx +39 -3
  153. package/src/components/input-group/index.tsx +0 -1
  154. package/src/components/item/index.tsx +387 -0
  155. package/src/components/label/index.tsx +5 -3
  156. package/src/components/line-chart/index.tsx +1224 -0
  157. package/src/components/marker/index.tsx +194 -0
  158. package/src/components/message/index.tsx +336 -0
  159. package/src/components/message-scroller/index.tsx +574 -0
  160. package/src/components/popover/index.tsx +599 -0
  161. package/src/components/radio-group/index.tsx +108 -17
  162. package/src/components/scroll-fade/index.tsx +163 -51
  163. package/src/components/select/index.tsx +54 -9
  164. package/src/components/separator/index.tsx +97 -0
  165. package/src/components/shimmer/index.tsx +151 -58
  166. package/src/components/surface/index.tsx +32 -5
  167. package/src/components/switch/index.tsx +27 -1
  168. package/src/components/toast/index.tsx +7 -8
  169. package/src/components/typography/index.tsx +3 -3
  170. package/src/hooks/index.ts +5 -0
  171. package/src/hooks/use-breakpoint.ts +2 -2
  172. package/src/hooks/use-copy-to-clipboard.ts +0 -2
  173. package/src/hooks/use-keyboard-avoidance.ts +140 -0
  174. package/src/icons/index.tsx +52 -4
  175. package/src/index.ts +92 -0
  176. package/src/native/index.ts +79 -0
  177. package/src/primitives/keyboard-avoider.tsx +49 -0
  178. package/src/primitives/scrim.tsx +95 -0
  179. package/src/providers/panel-ui-provider.tsx +30 -9
  180. package/src/theme/use-theme.ts +1 -1
  181. package/theme.css +63 -3
@@ -0,0 +1,21 @@
1
+ import { type ViewProps } from 'react-native';
2
+ /** True when a real blur can be drawn — for a caller that wants to know. */
3
+ export declare const hasBlur: boolean;
4
+ export interface ScrimProps extends Omit<ViewProps, 'children'> {
5
+ /** Frost the backdrop instead of dimming it. Falls back to a dim if it can't. */
6
+ blur?: boolean;
7
+ /** Blur strength, 0–100. */
8
+ intensity?: number;
9
+ /** Which way the blur tints. */
10
+ tint?: 'light' | 'dark' | 'default' | 'systemMaterial';
11
+ /**
12
+ * The dim used when not blurring — and when blurring is asked for but
13
+ * unavailable. A popover passes a lighter one than a dialog.
14
+ */
15
+ dimClassName?: string;
16
+ }
17
+ export declare function Scrim({ blur, intensity, tint, dimClassName, style, ...props }: ScrimProps): import("react").JSX.Element;
18
+ export declare namespace Scrim {
19
+ var displayName: string;
20
+ }
21
+ //# sourceMappingURL=scrim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrim.d.ts","sourceRoot":"","sources":["../../../../src/primitives/scrim.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAyBhE,4EAA4E;AAC5E,eAAO,MAAM,OAAO,SAAoB,CAAC;AAEzC,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;IAC7D,iFAAiF;IACjF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACvD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,KAAK,CAAC,EACpB,IAAY,EACZ,SAAc,EACd,IAAgB,EAChB,YAA4B,EAC5B,KAAK,EACL,GAAG,KAAK,EACT,EAAE,UAAU,+BA0BZ;yBAjCe,KAAK"}
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  export interface PanelUIProviderProps {
3
3
  children: ReactNode;
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"panel-ui-provider.d.ts","sourceRoot":"","sources":["../../../../src/providers/panel-ui-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOvC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,UAAiB,GAClB,EAAE,oBAAoB,+BAatB"}
1
+ {"version":3,"file":"panel-ui-provider.d.ts","sourceRoot":"","sources":["../../../../src/providers/panel-ui-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAwBrE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,UAAiB,GAClB,EAAE,oBAAoB,+BAiBtB"}
@@ -17,7 +17,7 @@ export interface PanelThemeFamily {
17
17
  /**
18
18
  * The theme families PanelUI ships. Each sets its own palette *and* its own
19
19
  * radius scale, so switching family changes the shape of the UI as well as
20
- * its colour — Panel is the Coss default, Moon is sharp and monochrome,
20
+ * its colour — Panel is the default, Moon is sharp and monochrome,
21
21
  * Grass is soft and green.
22
22
  *
23
23
  * Everything beyond `panel` must be registered in your metro config, or
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "panelui-native",
3
- "version": "0.6.0",
4
- "description": "High-performance React Native UI components for Expo. Coss UI design language, powered by Uniwind (Tailwind v4) and Reanimated.",
3
+ "version": "0.9.0",
4
+ "description": "High-performance React Native UI components for Expo. Semantic design tokens, powered by Uniwind (Tailwind v4) and Reanimated.",
5
5
  "main": "./lib/module/index.js",
6
6
  "module": "./lib/module/index.js",
7
7
  "types": "./lib/typescript/src/index.d.ts",
@@ -43,7 +43,6 @@
43
43
  "nativewind",
44
44
  "reanimated",
45
45
  "design-system",
46
- "shadcn",
47
46
  "dark-mode",
48
47
  "theming",
49
48
  "accessibility",
@@ -64,10 +63,15 @@
64
63
  "license": "MIT",
65
64
  "homepage": "https://github.com/panel-ui/PanelUI#readme",
66
65
  "peerDependencies": {
66
+ "@expo/ui": "*",
67
+ "@react-native-masked-view/masked-view": ">=0.3.0",
68
+ "expo-blur": ">=13.0.0",
67
69
  "expo-clipboard": ">=7.0.0",
70
+ "expo-linear-gradient": ">=13.0.0",
68
71
  "react": "*",
69
72
  "react-native": "*",
70
73
  "react-native-gesture-handler": ">=2.0.0",
74
+ "react-native-keyboard-controller": ">=1.12.0",
71
75
  "react-native-reanimated": ">=3.0.0",
72
76
  "react-native-safe-area-context": ">=4.0.0",
73
77
  "react-native-svg": ">=15.0.0",
@@ -96,8 +100,17 @@
96
100
  ]
97
101
  },
98
102
  "peerDependenciesMeta": {
103
+ "@expo/ui": {
104
+ "optional": true
105
+ },
106
+ "expo-blur": {
107
+ "optional": true
108
+ },
99
109
  "expo-clipboard": {
100
110
  "optional": true
111
+ },
112
+ "react-native-keyboard-controller": {
113
+ "optional": true
101
114
  }
102
115
  }
103
116
  }
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Accordion — collapsible sections.
3
3
  *
4
- * Adapted from: heroui-inc/heroui-native src/components/accordion/
5
- * (the Item / Trigger / Indicator / Content anatomy, single vs multiple
6
- * selection, the rotating chevron indicator, and layout-transition expand).
4
+ * Split into Item / Trigger / Indicator / Content so a section header can hold
5
+ * anything — a badge, a price, a second line — instead of only a string. The
6
+ * chevron rotates rather than swapping glyphs, and the panel expands through a
7
+ * layout transition, both on the UI thread.
7
8
  *
8
- * Upstream ships `default` and `surface`; the extra variants here follow the
9
- * same slot structure.
9
+ * `selectionMode` decides whether opening one section closes the others, which
10
+ * also changes the shape of `value`: a string when single, an array when
11
+ * multiple.
10
12
  */
11
13
  import {
12
14
  Children,
@@ -1,12 +1,9 @@
1
1
  /**
2
2
  * Alert — a status message with an optional leading status icon.
3
3
  *
4
- * Adapted from: heroui-inc/heroui-native src/components/alert/alert.tsx
5
- * (Indicator / Content / Title / Description anatomy, status-driven icon and
6
- * title colour), restyled with Coss UI tokens.
7
- *
8
- * The `variant` prop name is kept from PanelUI v0.2 rather than renamed to
9
- * HeroUI's `status`, so existing code keeps working.
4
+ * The indicator derives its icon and colour from the variant, so a success
5
+ * alert cannot end up with a warning icon. Pass `icon` on the root to override
6
+ * it everywhere, or give `Alert.Indicator` children to override one.
10
7
  */
11
8
  import { createContext, forwardRef, useContext, type ReactNode } from 'react';
12
9
  import { View, type Text as RNText, type ViewProps } from 'react-native';
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Attachment — a file, with its upload life.
3
+ *
4
+ * A file in a chat composer or an upload list is the same row shape `Item`
5
+ * already draws — media, a name, a size, a remove button — so this is that row,
6
+ * not a second one. What it adds is the part `Item` has no opinion on: the
7
+ * file's *state*. A file is picked, then uploading, then processing, then done
8
+ * (or failed), and each of those has to look different or the user cannot tell
9
+ * a stuck upload from a finished one.
10
+ *
11
+ * ```tsx
12
+ * <Attachment state="uploading">
13
+ * <Attachment.Media><FileIcon /></Attachment.Media>
14
+ * <Attachment.Content>
15
+ * <Attachment.Title>report.pdf</Attachment.Title>
16
+ * <Attachment.Description>PDF · 2.4 MB</Attachment.Description>
17
+ * </Attachment.Content>
18
+ * <Attachment.Actions>
19
+ * <Attachment.Action aria-label="Cancel"><XIcon size={14} /></Attachment.Action>
20
+ * </Attachment.Actions>
21
+ * </Attachment>
22
+ * ```
23
+ *
24
+ * While uploading or processing the title shimmers — the same sweep the rest of
25
+ * the library uses for "in progress" — and an optional bar tracks `progress`.
26
+ * A failed file tints its name and description in the destructive colour, which
27
+ * carries the state without relying on an icon nobody reads.
28
+ */
29
+ import { createContext, forwardRef, useContext, type ReactNode } from 'react';
30
+ import { View, type ViewProps } from 'react-native';
31
+ import { tv } from 'tailwind-variants';
32
+ import { useCSSVariable } from 'uniwind';
33
+ import { AnimatedPressable, type AnimatedPressableProps } from '../../primitives/animated-pressable';
34
+ import { Text, type TextProps } from '../../primitives/text';
35
+ import { cn } from '../../utils/cn';
36
+ import { Item } from '../item';
37
+ import { Shimmer } from '../shimmer';
38
+
39
+ export type AttachmentState = 'idle' | 'uploading' | 'processing' | 'error' | 'done';
40
+ type AttachmentSize = 'default' | 'sm' | 'xs';
41
+
42
+ interface AttachmentContextValue {
43
+ state: AttachmentState;
44
+ size: AttachmentSize;
45
+ }
46
+
47
+ const AttachmentContext = createContext<AttachmentContextValue>({
48
+ state: 'done',
49
+ size: 'default',
50
+ });
51
+
52
+ /** True while the file is still moving — the two states that shimmer. */
53
+ function isBusy(state: AttachmentState): boolean {
54
+ return state === 'uploading' || state === 'processing';
55
+ }
56
+
57
+ const rootVariants = tv({
58
+ base: '',
59
+ variants: {
60
+ state: {
61
+ idle: '',
62
+ uploading: '',
63
+ processing: '',
64
+ // The border carries the failure, so it reads at a glance in a list.
65
+ error: 'border border-destructive',
66
+ done: '',
67
+ },
68
+ },
69
+ });
70
+
71
+ export interface AttachmentProps
72
+ extends Omit<AnimatedPressableProps, 'children' | 'disabled'> {
73
+ className?: string;
74
+ /**
75
+ * Where the file is in its life. `uploading`/`processing` shimmer the name;
76
+ * `error` tints the row destructive. Default `done`.
77
+ */
78
+ state?: AttachmentState;
79
+ /** Row density, passed through to the underlying Item. */
80
+ size?: AttachmentSize;
81
+ /** Stack the row into a card — for a horizontal group of thumbnails. */
82
+ orientation?: 'horizontal' | 'vertical';
83
+ /** Upload progress 0–1. Draws a thin bar along the bottom while busy. */
84
+ progress?: number;
85
+ disabled?: boolean;
86
+ children?: ReactNode;
87
+ }
88
+
89
+ const AttachmentRoot = forwardRef<View, AttachmentProps>(
90
+ (
91
+ { className, state = 'done', size = 'default', orientation, progress, children, ...props },
92
+ ref
93
+ ) => {
94
+ const track = useCSSVariable('--color-primary');
95
+
96
+ return (
97
+ <AttachmentContext.Provider value={{ state, size }}>
98
+ <Item
99
+ ref={ref}
100
+ size={size}
101
+ orientation={orientation}
102
+ variant="outline"
103
+ className={cn('overflow-hidden', rootVariants({ state }), className)}
104
+ {...props}
105
+ >
106
+ {children}
107
+
108
+ {/* A determinate bar while busy, if progress is known. Absolute so it
109
+ rides the row's bottom edge without shifting the layout. */}
110
+ {isBusy(state) && progress !== undefined ? (
111
+ <View className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted">
112
+ <View
113
+ style={{
114
+ width: `${Math.min(100, Math.max(0, progress * 100))}%`,
115
+ backgroundColor: typeof track === 'string' ? track : undefined,
116
+ }}
117
+ className="h-full"
118
+ />
119
+ </View>
120
+ ) : null}
121
+ </Item>
122
+ </AttachmentContext.Provider>
123
+ );
124
+ }
125
+ );
126
+ AttachmentRoot.displayName = 'Attachment';
127
+
128
+ export interface AttachmentGroupProps extends ViewProps {
129
+ className?: string;
130
+ orientation?: 'horizontal' | 'vertical';
131
+ children?: ReactNode;
132
+ }
133
+
134
+ /** A row (or column) of attachments — the same stack `Item.Group` provides. */
135
+ const AttachmentGroup = forwardRef<View, AttachmentGroupProps>(
136
+ ({ className, orientation = 'vertical', children, ...props }, ref) => (
137
+ <Item.Group ref={ref} orientation={orientation} className={className} {...props}>
138
+ {children}
139
+ </Item.Group>
140
+ )
141
+ );
142
+ AttachmentGroup.displayName = 'Attachment.Group';
143
+
144
+ export interface AttachmentMediaProps extends ViewProps {
145
+ className?: string;
146
+ /** `icon` frames it in a tile; `image` clips it for a thumbnail. */
147
+ variant?: 'default' | 'icon' | 'image';
148
+ children?: ReactNode;
149
+ }
150
+
151
+ /** Leading slot — a file-type icon tile, or an image thumbnail. */
152
+ const AttachmentMedia = forwardRef<View, AttachmentMediaProps>(
153
+ ({ className, variant = 'icon', children, ...props }, ref) => (
154
+ <Item.Media ref={ref} variant={variant} className={className} {...props}>
155
+ {children}
156
+ </Item.Media>
157
+ )
158
+ );
159
+ AttachmentMedia.displayName = 'Attachment.Media';
160
+
161
+ export interface AttachmentContentProps extends ViewProps {
162
+ className?: string;
163
+ children?: ReactNode;
164
+ }
165
+
166
+ const AttachmentContent = forwardRef<View, AttachmentContentProps>(
167
+ ({ className, children, ...props }, ref) => (
168
+ <Item.Content ref={ref} className={className} {...props}>
169
+ {children}
170
+ </Item.Content>
171
+ )
172
+ );
173
+ AttachmentContent.displayName = 'Attachment.Content';
174
+
175
+ export interface AttachmentTitleProps extends TextProps {
176
+ className?: string;
177
+ children?: ReactNode;
178
+ }
179
+
180
+ /** The file name. Shimmers while the file is uploading or processing. */
181
+ const AttachmentTitle = forwardRef<React.ElementRef<typeof Text>, AttachmentTitleProps>(
182
+ ({ className, children, ...props }, ref) => {
183
+ const { state } = useContext(AttachmentContext);
184
+
185
+ if (isBusy(state)) {
186
+ // Shimmer owns the text so it can mask the sweep to the glyphs.
187
+ return <Shimmer textClassName={cn('text-base font-medium', className)}>{children}</Shimmer>;
188
+ }
189
+
190
+ return (
191
+ <Item.Title
192
+ ref={ref}
193
+ className={cn(state === 'error' && 'text-destructive', className)}
194
+ {...props}
195
+ >
196
+ {children}
197
+ </Item.Title>
198
+ );
199
+ }
200
+ );
201
+ AttachmentTitle.displayName = 'Attachment.Title';
202
+
203
+ export interface AttachmentDescriptionProps extends TextProps {
204
+ className?: string;
205
+ children?: ReactNode;
206
+ }
207
+
208
+ /** File type, size or status. Tints destructive on `error`. */
209
+ const AttachmentDescription = forwardRef<
210
+ React.ElementRef<typeof Text>,
211
+ AttachmentDescriptionProps
212
+ >(({ className, children, ...props }, ref) => {
213
+ const { state } = useContext(AttachmentContext);
214
+
215
+ return (
216
+ <Item.Description
217
+ ref={ref}
218
+ className={cn(state === 'error' && 'text-destructive', className)}
219
+ {...props}
220
+ >
221
+ {children}
222
+ </Item.Description>
223
+ );
224
+ });
225
+ AttachmentDescription.displayName = 'Attachment.Description';
226
+
227
+ export interface AttachmentActionsProps extends ViewProps {
228
+ className?: string;
229
+ children?: ReactNode;
230
+ }
231
+
232
+ const AttachmentActions = forwardRef<View, AttachmentActionsProps>(
233
+ ({ className, children, ...props }, ref) => (
234
+ <Item.Actions ref={ref} className={className} {...props}>
235
+ {children}
236
+ </Item.Actions>
237
+ )
238
+ );
239
+ AttachmentActions.displayName = 'Attachment.Actions';
240
+
241
+ export interface AttachmentActionProps
242
+ extends Omit<AnimatedPressableProps, 'children'> {
243
+ className?: string;
244
+ accessibilityLabel: string;
245
+ children?: ReactNode;
246
+ }
247
+
248
+ /**
249
+ * A single icon button in the actions slot — remove, retry, download. An icon
250
+ * on its own says nothing to a screen reader, so the label is required.
251
+ */
252
+ const AttachmentAction = forwardRef<View, AttachmentActionProps>(
253
+ ({ className, accessibilityLabel, children, ...props }, ref) => (
254
+ <AnimatedPressable
255
+ ref={ref}
256
+ accessibilityRole="button"
257
+ accessibilityLabel={accessibilityLabel}
258
+ hitSlop={8}
259
+ className={cn(
260
+ 'h-8 w-8 items-center justify-center rounded-full active:bg-muted',
261
+ className
262
+ )}
263
+ {...props}
264
+ >
265
+ {children}
266
+ </AnimatedPressable>
267
+ )
268
+ );
269
+ AttachmentAction.displayName = 'Attachment.Action';
270
+
271
+ export const Attachment = Object.assign(AttachmentRoot, {
272
+ Group: AttachmentGroup,
273
+ Media: AttachmentMedia,
274
+ Content: AttachmentContent,
275
+ Title: AttachmentTitle,
276
+ Description: AttachmentDescription,
277
+ Actions: AttachmentActions,
278
+ Action: AttachmentAction,
279
+ });
@@ -10,6 +10,7 @@ import {
10
10
  type ReactNode,
11
11
  } from 'react';
12
12
  import { Pressable, useWindowDimensions, View, type ViewProps } from 'react-native';
13
+ import { useCSSVariable } from 'uniwind';
13
14
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
14
15
  import Animated, {
15
16
  FadeIn,
@@ -23,6 +24,8 @@ import Animated, {
23
24
  withTiming,
24
25
  } from 'react-native-reanimated';
25
26
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
27
+ import { getNativeUI } from '../../native';
28
+ import { XIcon } from '../../icons';
26
29
  import { Portal } from '../../primitives/portal';
27
30
  import { cn } from '../../utils/cn';
28
31
 
@@ -50,13 +53,40 @@ export interface BottomSheetProps {
50
53
  open?: boolean;
51
54
  onOpenChange?: (open: boolean) => void;
52
55
  defaultOpen?: boolean;
56
+ /**
57
+ * Present the platform's own sheet instead of this one, so it gets the
58
+ * system's detents, scroll interaction and dismiss gesture. Requires the
59
+ * optional `@expo/ui` package; without it this prop does nothing.
60
+ *
61
+ * **Theme tokens do not apply to the sheet chrome** — the platform draws the
62
+ * container, so `BottomSheet.Content`'s `className` and its drag handle are
63
+ * ignored. The content inside is still yours.
64
+ */
65
+ native?: boolean;
66
+ /**
67
+ * Heights the native sheet can rest at. Omit to size to the content.
68
+ * `{ fraction }` and `{ height }` are iOS-only; Android snaps them to the
69
+ * nearest of `half` / `full`.
70
+ */
71
+ snapPoints?: ('half' | 'full' | { fraction: number } | { height: number })[];
53
72
  }
54
73
 
74
+ /**
75
+ * Set by the root so Content knows the platform is drawing the sheet, and with
76
+ * which detents. Null means the styled sheet renders.
77
+ */
78
+ const NativeSheetContext = createContext<{
79
+ nativeUI: NonNullable<ReturnType<typeof getNativeUI>>;
80
+ snapPoints: BottomSheetProps['snapPoints'];
81
+ } | null>(null);
82
+
55
83
  function BottomSheetRoot({
56
84
  children,
57
85
  open,
58
86
  onOpenChange,
59
87
  defaultOpen = false,
88
+ native,
89
+ snapPoints,
60
90
  }: BottomSheetProps) {
61
91
  const [internalOpen, setInternalOpen] = useState(defaultOpen);
62
92
  const isControlled = open !== undefined;
@@ -75,9 +105,17 @@ function BottomSheetRoot({
75
105
  [resolvedOpen, setOpen]
76
106
  );
77
107
 
108
+ const nativeUI = native ? getNativeUI() : null;
109
+ const nativeSheet = useMemo(
110
+ () => (nativeUI ? { nativeUI, snapPoints } : null),
111
+ [nativeUI, snapPoints]
112
+ );
113
+
78
114
  return (
79
115
  <BottomSheetContext.Provider value={context}>
80
- {children}
116
+ <NativeSheetContext.Provider value={nativeSheet}>
117
+ {children}
118
+ </NativeSheetContext.Provider>
81
119
  </BottomSheetContext.Provider>
82
120
  );
83
121
  }
@@ -102,20 +140,28 @@ export interface BottomSheetContentProps extends ViewProps {
102
140
  className?: string;
103
141
  /** Tap on the backdrop closes the sheet. Default true. */
104
142
  dismissible?: boolean;
143
+ /**
144
+ * Show a close button in the top-right corner. On by default for the styled
145
+ * sheet; ignored by the native sheet, which has its own dismiss affordances.
146
+ */
147
+ showClose?: boolean;
105
148
  children?: ReactNode;
106
149
  }
107
150
 
108
151
  function BottomSheetContent({
109
152
  className,
110
153
  dismissible = true,
154
+ showClose = true,
111
155
  children,
112
156
  ...props
113
157
  }: BottomSheetContentProps) {
114
158
  const context = useBottomSheet('BottomSheet.Content');
115
159
  const { open, setOpen } = context;
116
- const { height: screenHeight } = useWindowDimensions();
160
+ const nativeSheet = useContext(NativeSheetContext);
161
+ const { height: screenHeight, width: screenWidth } = useWindowDimensions();
117
162
  const insets = useSafeAreaInsets();
118
163
  const translateY = useSharedValue(0);
164
+ const closeTint = useCSSVariable('--color-muted-foreground');
119
165
 
120
166
  const close = useCallback(() => setOpen(false), [setOpen]);
121
167
 
@@ -142,6 +188,49 @@ function BottomSheetContent({
142
188
  transform: [{ translateY: translateY.value }],
143
189
  }));
144
190
 
191
+ if (nativeSheet) {
192
+ const { Host, BottomSheet: NativeBottomSheet, RNHostView } = nativeSheet.nativeUI;
193
+ // The platform owns presentation, so this stays mounted and toggles
194
+ // isPresented rather than unmounting on close.
195
+ //
196
+ // RNHostView is not optional: our content is React Native, and the native
197
+ // sheet cannot measure RN views directly. Without it the sheet sizes to
198
+ // nothing and the content spills outside its container.
199
+ return (
200
+ <Host matchContents style={{ position: 'absolute' }}>
201
+ <NativeBottomSheet
202
+ isPresented={open}
203
+ onDismiss={dismissible ? close : () => {}}
204
+ snapPoints={nativeSheet.snapPoints}
205
+ >
206
+ <RNHostView matchContents>
207
+ <BottomSheetContext.Provider value={context}>
208
+ <View
209
+ {...props}
210
+ className={cn(
211
+ // The platform draws the container, but it hands us a bare
212
+ // box — padding and safe-area are still ours.
213
+ 'gap-2 px-5 pb-2 pt-1',
214
+ className
215
+ )}
216
+ style={{
217
+ // An explicit width, not `w-full`. Inside the native host
218
+ // there is no parent width for a percentage to resolve
219
+ // against, so `100%` measures against nothing and the
220
+ // content lays out wider than the sheet it sits in.
221
+ width: screenWidth,
222
+ paddingBottom: Math.max(insets.bottom, 16),
223
+ }}
224
+ >
225
+ {children}
226
+ </View>
227
+ </BottomSheetContext.Provider>
228
+ </RNHostView>
229
+ </NativeBottomSheet>
230
+ </Host>
231
+ );
232
+ }
233
+
145
234
  if (!open) return null;
146
235
 
147
236
  return (
@@ -176,6 +265,17 @@ function BottomSheetContent({
176
265
  <View className="mb-3 self-center">
177
266
  <View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
178
267
  </View>
268
+ {showClose ? (
269
+ <Pressable
270
+ accessibilityRole="button"
271
+ accessibilityLabel="Close"
272
+ onPress={close}
273
+ hitSlop={8}
274
+ className="absolute right-4 top-3 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70"
275
+ >
276
+ <XIcon size={16} color={typeof closeTint === 'string' ? closeTint : undefined} />
277
+ </Pressable>
278
+ ) : null}
179
279
  {children}
180
280
  </Animated.View>
181
281
  </GestureDetector>
@@ -8,6 +8,7 @@ import {
8
8
  } from '../../primitives/animated-pressable';
9
9
  import { Text } from '../../primitives/text';
10
10
  import { IconColorProvider } from '../../icons';
11
+ import { getNativeUI } from '../../native';
11
12
  import { Spinner } from '../spinner';
12
13
 
13
14
  const buttonVariants = tv({
@@ -104,8 +105,32 @@ export interface ButtonProps
104
105
  endContent?: ReactNode;
105
106
  /** Extra classes for the label when children is a string. */
106
107
  labelClassName?: string;
108
+ /**
109
+ * Render the platform's own button instead of this one. Requires the
110
+ * optional `@expo/ui` package; without it this prop does nothing.
111
+ *
112
+ * **Theme tokens do not apply** — the platform draws the button, so
113
+ * `className`, `size`, `fullWidth`, `startContent`, `endContent` and
114
+ * `loading` are all ignored. `variant` maps onto the nearest platform
115
+ * style: `primary`/`destructive` → filled, `outline` → outlined, everything
116
+ * else → text.
117
+ */
118
+ native?: boolean;
107
119
  }
108
120
 
121
+ /** PanelUI variants mapped onto the platform button styles. */
122
+ const NATIVE_VARIANT: Record<
123
+ NonNullable<ButtonVariantProps['variant']>,
124
+ 'filled' | 'outlined' | 'text'
125
+ > = {
126
+ primary: 'filled',
127
+ destructive: 'filled',
128
+ secondary: 'filled',
129
+ outline: 'outlined',
130
+ ghost: 'text',
131
+ social: 'outlined',
132
+ };
133
+
109
134
  export const Button = forwardRef<View, ButtonProps>(
110
135
  (
111
136
  {
@@ -119,11 +144,13 @@ export const Button = forwardRef<View, ButtonProps>(
119
144
  loading = false,
120
145
  startContent,
121
146
  endContent,
147
+ native,
122
148
  ...props
123
149
  },
124
150
  ref
125
151
  ) => {
126
152
  const isDisabled = disabled || loading;
153
+ const nativeUI = native ? getNativeUI() : null;
127
154
  const { root, label, spinner } = buttonVariants({
128
155
  variant,
129
156
  size,
@@ -146,6 +173,31 @@ export const Button = forwardRef<View, ButtonProps>(
146
173
  ? themedColor
147
174
  : undefined;
148
175
 
176
+ if (nativeUI) {
177
+ const { Host, Button: NativeButton, RNHostView } = nativeUI;
178
+ const isStringLabel = typeof children === 'string';
179
+
180
+ return (
181
+ <Host matchContents>
182
+ <NativeButton
183
+ label={isStringLabel ? children : undefined}
184
+ variant={NATIVE_VARIANT[variant ?? 'primary']}
185
+ disabled={isDisabled}
186
+ onPress={props.onPress}
187
+ >
188
+ {/* Non-string children are React Native views, and the native
189
+ button cannot measure those directly — they have to be hosted
190
+ or they render outside the button's bounds. */}
191
+ {isStringLabel ? undefined : (
192
+ <RNHostView matchContents>
193
+ <>{children}</>
194
+ </RNHostView>
195
+ )}
196
+ </NativeButton>
197
+ </Host>
198
+ );
199
+ }
200
+
149
201
  return (
150
202
  <IconColorProvider color={contentColor}>
151
203
  <AnimatedPressable