panelui-native 0.84.0 → 0.85.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 (49) hide show
  1. package/README.md +17 -8
  2. package/lib/module/components/bottom-sheet/index.js +3 -2
  3. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  4. package/lib/module/components/button/index.js +37 -16
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/flip-card/index.js +338 -0
  7. package/lib/module/components/flip-card/index.js.map +1 -0
  8. package/lib/module/components/popover/index.js +3 -2
  9. package/lib/module/components/popover/index.js.map +1 -1
  10. package/lib/module/components/select/index.js +3 -2
  11. package/lib/module/components/select/index.js.map +1 -1
  12. package/lib/module/components/slider/index.js +16 -5
  13. package/lib/module/components/slider/index.js.map +1 -1
  14. package/lib/module/components/switch/index.js +28 -7
  15. package/lib/module/components/switch/index.js.map +1 -1
  16. package/lib/module/index.js +1 -0
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/native/index.js +7 -0
  19. package/lib/module/native/index.js.map +1 -1
  20. package/lib/module/native/native-host.js +62 -0
  21. package/lib/module/native/native-host.js.map +1 -0
  22. package/lib/module/primitives/glass.js +14 -0
  23. package/lib/module/primitives/glass.js.map +1 -1
  24. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  25. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  26. package/lib/typescript/src/components/flip-card/index.d.ts +116 -0
  27. package/lib/typescript/src/components/flip-card/index.d.ts.map +1 -0
  28. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/switch/index.d.ts +0 -4
  30. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  31. package/lib/typescript/src/index.d.ts +1 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -1
  33. package/lib/typescript/src/native/index.d.ts +28 -11
  34. package/lib/typescript/src/native/index.d.ts.map +1 -1
  35. package/lib/typescript/src/native/native-host.d.ts +71 -0
  36. package/lib/typescript/src/native/native-host.d.ts.map +1 -0
  37. package/lib/typescript/src/primitives/glass.d.ts.map +1 -1
  38. package/package.json +11 -1
  39. package/src/components/bottom-sheet/index.tsx +8 -3
  40. package/src/components/button/index.tsx +36 -15
  41. package/src/components/flip-card/index.tsx +419 -0
  42. package/src/components/popover/index.tsx +3 -3
  43. package/src/components/select/index.tsx +3 -3
  44. package/src/components/slider/index.tsx +28 -6
  45. package/src/components/switch/index.tsx +28 -7
  46. package/src/index.ts +9 -0
  47. package/src/native/index.ts +35 -10
  48. package/src/native/native-host.tsx +75 -0
  49. package/src/primitives/glass.tsx +12 -0
@@ -8,7 +8,7 @@ import Animated, {
8
8
  } from 'react-native-reanimated';
9
9
  import { tv, type VariantProps } from 'tailwind-variants';
10
10
  import { useDirectionSign } from '../../hooks/use-direction';
11
- import { getNativeUI } from '../../native';
11
+ import { NativeHost, getNativeUI } from '../../native';
12
12
  import { selectionTick } from '../../utils/haptics';
13
13
  import { useFieldLabelledBy } from '../field';
14
14
 
@@ -79,6 +79,15 @@ export interface SwitchProps
79
79
  * Animated switch. Thumb position and active-track opacity are driven on the
80
80
  * UI thread; toggling never re-renders beyond the value change itself.
81
81
  */
82
+ /**
83
+ * The box the platform toggle is given, in points.
84
+ *
85
+ * Taller than either platform's switch — 31 on iOS, 32 on Android — so the
86
+ * control is never clipped by the box it is centred in, and short enough that
87
+ * a row built around it is still a row.
88
+ */
89
+ const NATIVE_TOGGLE_HEIGHT = 32;
90
+
82
91
  export const Switch = forwardRef<View, SwitchProps>(
83
92
  (
84
93
  {
@@ -124,18 +133,30 @@ export const Switch = forwardRef<View, SwitchProps>(
124
133
  if (nativeUI) {
125
134
  const { Host, Switch: NativeSwitch } = nativeUI;
126
135
  return (
127
- // A platform toggle has a definite intrinsic size on both platforms,
128
- // so the host is left to follow it. Pinning the host to a number
129
- // instead is what leaves the control laid out against a box it never
130
- // agreed to, and settling into it a frame later.
131
- <Host matchContents ignoreSafeArea="keyboard">
136
+ /*
137
+ * The height is stated; only the width is matched.
138
+ *
139
+ * `matchContents` hands an axis to the platform for good — the host
140
+ * writes the measured size back into the layout every time the
141
+ * platform's geometry changes, not once on mount — and the vertical
142
+ * axis is the one that moves everything below it when it does. A
143
+ * toggle's height is the one number here that does not vary, so it is
144
+ * given rather than asked for; its width is the platform's and stays
145
+ * the platform's.
146
+ */
147
+ <NativeHost
148
+ host={Host}
149
+ matchContents={{ horizontal: true }}
150
+ ignoreSafeArea="keyboard"
151
+ style={{ height: NATIVE_TOGGLE_HEIGHT }}
152
+ >
132
153
  <NativeSwitch
133
154
  value={value}
134
155
  onValueChange={(next: boolean) => onValueChange?.(next)}
135
156
  label={label ?? accessibilityLabel}
136
157
  disabled={disabled}
137
158
  />
138
- </Host>
159
+ </NativeHost>
139
160
  );
140
161
  }
141
162
 
package/src/index.ts CHANGED
@@ -222,6 +222,15 @@ export {
222
222
  type FlowRect,
223
223
  type FlowPoint,
224
224
  } from './components/flow';
225
+ export {
226
+ FlipCard,
227
+ useFlipCard,
228
+ type FlipCardProps,
229
+ type FlipCardFaceProps,
230
+ type FlipCardDirection,
231
+ type FlipCardRotation,
232
+ type FlipCardTrigger,
233
+ } from './components/flip-card';
225
234
  export {
226
235
  FunnelChart,
227
236
  useFunnelChart,
@@ -18,25 +18,40 @@
18
18
  * **Theme tokens do not apply in native mode.** The platform draws the control
19
19
  * with its own colours, metrics and typography — that is the entire point, and
20
20
  * it means `className` and the variant props are ignored on those components.
21
+ *
22
+ * The one exception is which appearance it draws: `colorScheme` is the single
23
+ * theme signal the toolkit accepts, and `NativeHost` is what passes it. Mount
24
+ * every host through that rather than reaching for `Host` directly, or the
25
+ * control resolves its own appearance from the system and stops tracking the
26
+ * app's theme.
21
27
  */
22
28
  import { Platform } from 'react-native';
23
29
  import type { ComponentType, ReactNode } from 'react';
24
30
 
31
+ export { NativeHost, type NativeHostProps } from './native-host';
32
+
25
33
  interface NativeUIModule {
26
34
  Host: ComponentType<{
27
35
  children?: ReactNode;
28
36
  /**
29
- * Whether the host resizes itself to the platform content.
37
+ * Which axes the platform is allowed to size.
30
38
  *
31
- * This is on for every control here, and it is the whole answer to the
32
- * jump. Sizing the *host* and leaving the control unsized inside it hands
33
- * the platform a box it never agreed to: it lays out against its own
34
- * intrinsic size, and settles into the box on the first thing that forces
35
- * a second pass — which for a button is the first press.
39
+ * **It is not a one-off measurement.** An axis given to `matchContents` is
40
+ * given for good: the host writes the platform's measured size straight
41
+ * into the layout every time the platform's geometry changes, and dirties
42
+ * the layout when it does. So a control that lays itself out again under a
43
+ * press drags its box with it, and everything below it moves — which is
44
+ * the defect this spent three attempts on, twice reasoning about the first
45
+ * measurement when the problem was every one after it.
36
46
  *
37
- * The per-axis form is for a control with no intrinsic width, like a
38
- * slider or a picker: the width comes from ordinary layout and only the
39
- * height is reported back.
47
+ * The rule that comes out of that: **never hand over an axis whose size
48
+ * you already know.** State it in `style` instead and match only what is
49
+ * genuinely the platform's — a labelled button's width, a toggle's width.
50
+ * An axis left out is never written to, so an explicit size on it is safe.
51
+ *
52
+ * Where nothing can be stated the axis has to stay matched. A picker is
53
+ * the honest example: a menu is a compact button and a wheel is a rotor,
54
+ * and only the platform knows which it drew.
40
55
  */
41
56
  matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
42
57
  /**
@@ -56,6 +71,14 @@ interface NativeUIModule {
56
71
  * platform's business and stay its business.
57
72
  */
58
73
  ignoreSafeArea?: 'all' | 'container' | 'keyboard';
74
+ /**
75
+ * The appearance the platform draws the hosted control in.
76
+ *
77
+ * Passed by `NativeHost` from the app's own theme, because the host would
78
+ * otherwise resolve it from the system — which is a different question,
79
+ * and one whose answer does not change when the theme does.
80
+ */
81
+ colorScheme?: 'light' | 'dark';
59
82
  style?: unknown;
60
83
  [key: string]: unknown;
61
84
  }>;
@@ -175,7 +198,9 @@ interface SwiftUIComponents {
175
198
  Host: ComponentType<{
176
199
  children?: ReactNode;
177
200
  matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
178
- ignoreSafeArea?: unknown;
201
+ ignoreSafeArea?: 'all' | 'container' | 'keyboard';
202
+ /** As on the portable host above — see `NativeHost`. */
203
+ colorScheme?: 'light' | 'dark';
179
204
  style?: unknown;
180
205
  }>;
181
206
  RNHostView: ComponentType<{ children?: ReactNode; matchContents?: boolean }>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The host every native control is mounted in, told which appearance to draw.
3
+ *
4
+ * A hosting controller resolves its colour scheme from the trait environment
5
+ * it is placed in, which is the system appearance — not the theme the app is
6
+ * running. Those are the same thing only by coincidence: an app in a dark
7
+ * theme on a phone set to light gets a light platform control beside dark
8
+ * content, and a theme changed at runtime leaves the control where it was,
9
+ * because nothing in the trait environment moved.
10
+ *
11
+ * So the appearance is passed rather than inferred. `colorScheme` is the one
12
+ * theme signal the platform toolkit accepts, and this is the single place it
13
+ * is given.
14
+ *
15
+ * ```tsx
16
+ * const { Host, Switch: NativeSwitch } = nativeUI;
17
+ * <NativeHost host={Host} matchContents ignoreSafeArea="keyboard">
18
+ * <NativeSwitch value={on} onValueChange={setOn} />
19
+ * </NativeHost>
20
+ * ```
21
+ *
22
+ * ## Why the host arrives as a prop
23
+ *
24
+ * There are two of them. The universal `Host` comes from `getNativeUI()` and
25
+ * the SwiftUI-only one from `getSwiftUI()`, and a caller has already resolved
26
+ * whichever it needs before it renders. Taking the component rather than
27
+ * resolving it again keeps this file free of the module bridge, which is what
28
+ * lets the bridge re-export it without the two importing each other.
29
+ *
30
+ * ## Why this is a component rather than a hook at each call site
31
+ *
32
+ * `useThemeMode` subscribes to theme changes, and a hook cannot be called
33
+ * conditionally — so reading it inside `Button` would put a subscription on
34
+ * every button in a list for a branch almost none of them take. Here the
35
+ * subscription exists only where a native host is actually mounted.
36
+ */
37
+ import type { ComponentType, ReactNode } from 'react';
38
+ import { useThemeMode } from '../theme/use-theme';
39
+
40
+ /**
41
+ * What this renders the host with. A type alias rather than an interface, and
42
+ * that is load-bearing: only an alias gets an implicit index signature, which
43
+ * is what makes it assignable to the bridge's own `Host` prop type.
44
+ */
45
+ type NativeHostRenderProps = {
46
+ children?: ReactNode;
47
+ colorScheme?: 'light' | 'dark';
48
+ matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
49
+ ignoreSafeArea?: 'all' | 'container' | 'keyboard';
50
+ style?: unknown;
51
+ };
52
+
53
+ export interface NativeHostProps {
54
+ /**
55
+ * The host component to render — `Host` from `getNativeUI()` for a portable
56
+ * control, or from `getSwiftUI()` for an iOS-only one.
57
+ */
58
+ host: ComponentType<NativeHostRenderProps>;
59
+ children?: ReactNode;
60
+ /** Whether the host resizes itself to the platform content. */
61
+ matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
62
+ /** Which safe areas the host lets the platform inset its content for. */
63
+ ignoreSafeArea?: 'all' | 'container' | 'keyboard';
64
+ style?: unknown;
65
+ }
66
+
67
+ export function NativeHost({ host: Host, children, ...props }: NativeHostProps) {
68
+ const { mode } = useThemeMode();
69
+
70
+ return (
71
+ <Host colorScheme={mode} {...props}>
72
+ {children}
73
+ </Host>
74
+ );
75
+ }
@@ -44,6 +44,7 @@
44
44
  */
45
45
  import type { ComponentType, ReactNode } from 'react';
46
46
  import { Platform, StyleSheet, View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';
47
+ import { useThemeMode } from '../theme/use-theme';
47
48
  import { cn } from '../utils/cn';
48
49
  import { useReduceTransparency } from './scrim';
49
50
 
@@ -144,6 +145,16 @@ export function Glass({
144
145
  ...props
145
146
  }: GlassProps) {
146
147
  const reduceTransparency = useReduceTransparency();
148
+ /*
149
+ * Which appearance the material is drawn in, from the app's theme rather
150
+ * than the phone's.
151
+ *
152
+ * The material's own default follows the system, so an app running a dark
153
+ * theme on a phone set to light draws light glass over dark content — and a
154
+ * theme changed at runtime leaves it where it was, because the system
155
+ * appearance never moved.
156
+ */
157
+ const { mode } = useThemeMode();
147
158
  // Not knowing yet counts as "do not draw it": the material arriving a frame
148
159
  // late is invisible, and one flashing at somebody who opted out is not.
149
160
  const material = GlassView !== null && reduceTransparency === false;
@@ -159,6 +170,7 @@ export function Glass({
159
170
  <GlassView
160
171
  glassEffectStyle={variant}
161
172
  tintColor={tint}
173
+ colorScheme={mode}
162
174
  pointerEvents="none"
163
175
  style={[StyleSheet.absoluteFill, shape]}
164
176
  />