panelui-native 0.5.1 → 0.6.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 (62) hide show
  1. package/README.md +10 -5
  2. package/lib/module/components/scroll-fade/index.js +156 -0
  3. package/lib/module/components/scroll-fade/index.js.map +1 -0
  4. package/lib/module/components/shimmer/index.js +120 -0
  5. package/lib/module/components/shimmer/index.js.map +1 -0
  6. package/lib/module/components/surface/index.js +64 -0
  7. package/lib/module/components/surface/index.js.map +1 -0
  8. package/lib/module/components/timeline/index.js +55 -30
  9. package/lib/module/components/timeline/index.js.map +1 -1
  10. package/lib/module/hooks/index.js +9 -0
  11. package/lib/module/hooks/index.js.map +1 -0
  12. package/lib/module/hooks/use-breakpoint.js +39 -0
  13. package/lib/module/hooks/use-breakpoint.js.map +1 -0
  14. package/lib/module/hooks/use-copy-to-clipboard.js +62 -0
  15. package/lib/module/hooks/use-copy-to-clipboard.js.map +1 -0
  16. package/lib/module/hooks/use-debounced-value.js +24 -0
  17. package/lib/module/hooks/use-debounced-value.js.map +1 -0
  18. package/lib/module/hooks/use-disclosure.js +43 -0
  19. package/lib/module/hooks/use-disclosure.js.map +1 -0
  20. package/lib/module/hooks/use-keyboard.js +36 -0
  21. package/lib/module/hooks/use-keyboard.js.map +1 -0
  22. package/lib/module/hooks/use-previous.js +25 -0
  23. package/lib/module/hooks/use-previous.js.map +1 -0
  24. package/lib/module/index.js +6 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/typescript/src/components/scroll-fade/index.d.ts +38 -0
  27. package/lib/typescript/src/components/scroll-fade/index.d.ts.map +1 -0
  28. package/lib/typescript/src/components/shimmer/index.d.ts +42 -0
  29. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -0
  30. package/lib/typescript/src/components/surface/index.d.ts +30 -0
  31. package/lib/typescript/src/components/surface/index.d.ts.map +1 -0
  32. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  33. package/lib/typescript/src/hooks/index.d.ts +7 -0
  34. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  35. package/lib/typescript/src/hooks/use-breakpoint.d.ts +32 -0
  36. package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -0
  37. package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts +30 -0
  38. package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts.map +1 -0
  39. package/lib/typescript/src/hooks/use-debounced-value.d.ts +13 -0
  40. package/lib/typescript/src/hooks/use-debounced-value.d.ts.map +1 -0
  41. package/lib/typescript/src/hooks/use-disclosure.d.ts +32 -0
  42. package/lib/typescript/src/hooks/use-disclosure.d.ts.map +1 -0
  43. package/lib/typescript/src/hooks/use-keyboard.d.ts +20 -0
  44. package/lib/typescript/src/hooks/use-keyboard.d.ts.map +1 -0
  45. package/lib/typescript/src/hooks/use-previous.d.ts +15 -0
  46. package/lib/typescript/src/hooks/use-previous.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +4 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -1
  49. package/package.json +8 -1
  50. package/src/components/scroll-fade/index.tsx +168 -0
  51. package/src/components/shimmer/index.tsx +135 -0
  52. package/src/components/surface/index.tsx +63 -0
  53. package/src/components/timeline/index.tsx +64 -38
  54. package/src/hooks/index.ts +19 -0
  55. package/src/hooks/use-breakpoint.ts +52 -0
  56. package/src/hooks/use-copy-to-clipboard.ts +81 -0
  57. package/src/hooks/use-debounced-value.ts +23 -0
  58. package/src/hooks/use-disclosure.ts +57 -0
  59. package/src/hooks/use-keyboard.ts +43 -0
  60. package/src/hooks/use-previous.ts +24 -0
  61. package/src/index.ts +9 -0
  62. package/theme.css +18 -0
@@ -22,13 +22,17 @@ export { InputGroup, type InputGroupProps, type InputGroupInputProps, type Input
22
22
  export { Label, type LabelProps, type LabelTextProps } from './components/label/index.js';
23
23
  export { Progress, type ProgressProps } from './components/progress/index.js';
24
24
  export { RadioGroup, type RadioGroupProps, type RadioGroupItemProps, } from './components/radio-group/index.js';
25
+ export { ScrollFade, type ScrollFadeProps, } from './components/scroll-fade/index.js';
26
+ export { Shimmer, type ShimmerProps } from './components/shimmer/index.js';
25
27
  export { Skeleton, type SkeletonProps } from './components/skeleton/index.js';
26
28
  export { Spinner, type SpinnerProps } from './components/spinner/index.js';
27
29
  export { Steps, type StepsProps, type StepsItemProps, type StepsTriggerProps, type StepsIndicatorProps, type StepsSeparatorProps, type StepState, type StepsOrientation, } from './components/steps/index.js';
30
+ export { Surface, type SurfaceProps } from './components/surface/index.js';
28
31
  export { Switch, type SwitchProps } from './components/switch/index.js';
29
32
  export { Timeline, type TimelineProps, type TimelineItemProps, type TimelineIndicatorProps, type TimelineStatProps, type TimelineVariant, type TimelineTone, } from './components/timeline/index.js';
30
33
  export { Toast, ToastViewport, toast, useToast, type ToastProps, type ToastOptions, type ToastItem, type ToastVariant, type ToastPlacement, type ToastHandle, type ToastIndicatorProps, type ToastCloseProps, } from './components/toast/index.js';
31
34
  export { Typography, type TypographyProps, type TypographyType, type TypographyHeadingProps, type TypographyParagraphProps, type TypographyCodeProps, } from './components/typography/index.js';
32
35
  export { AlertTriangleIcon, AppleIcon, ArrowUpRightIcon, BellIcon, CardIcon, CheckCircleIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, FacebookIcon, GoogleIcon, IconColorProvider, InfoIcon, MoonIcon, PackageIcon, PlusSquareIcon, ReceiptIcon, SearchIcon, SendIcon, ShareNodesIcon, ShieldAlertIcon, ShieldCheckIcon, SunIcon, XIcon, useIconColor, type IconProps, } from './icons/index.js';
36
+ export * from './hooks/index.js';
33
37
  export { cn } from './utils/cn.js';
34
38
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,kCAA+B,CAAC;AAG3F,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,sBAAmB,CAAC;AAG3B,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,wBAAqB,CAAC;AACzE,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAmB,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,GAC5B,MAAM,oCAAiC,CAAC;AAGzC,OAAO,EACL,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,GAC5B,MAAM,iCAAwB,CAAC;AAChC,OAAO,EACL,KAAK,EACL,KAAK,UAAU,EACf,KAAK,mBAAmB,GACzB,MAAM,6BAAoB,CAAC;AAC5B,OAAO,EACL,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,8BAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,6BAAoB,CAAC;AAC5D,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,oCAA2B,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,8BAAqB,CAAC;AAC/D,OAAO,EACL,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,8BAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,6BAAoB,CAAC;AAC5D,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,qCAA4B,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,8BAAqB,CAAC;AACrF,OAAO,EACL,IAAI,EACJ,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACtB,MAAM,4BAAmB,CAAC;AAC3B,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,4BAAmB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAuB,CAAC;AACrE,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,mCAA0B,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,6BAAoB,CAAC;AAC5D,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,GAC9B,MAAM,mCAA0B,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,6BAAoB,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAuB,CAAC;AACrE,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,mBAAmB,GACzB,MAAM,mCAA0B,CAAC;AAClC,OAAO,EACL,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,mCAA0B,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,+BAAsB,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAuB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,+BAAsB,CAAC;AAClE,OAAO,EACL,KAAK,EACL,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,6BAAoB,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,+BAAsB,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,8BAAqB,CAAC;AAC/D,OAAO,EACL,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,gCAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,EACL,aAAa,EACb,KAAK,EACL,QAAQ,EACR,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,6BAAoB,CAAC;AAC5B,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,kCAAyB,CAAC;AAGjC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,SAAS,EACT,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,cAAc,EACd,WAAW,EACX,UAAU,EACV,QAAQ,EACR,cAAc,EACd,eAAe,EACf,eAAe,EACf,OAAO,EACP,KAAK,EACL,YAAY,EACZ,KAAK,SAAS,GACf,MAAM,kBAAS,CAAC;AAGjB,cAAc,kBAAS,CAAC;AAGxB,OAAO,EAAE,EAAE,EAAE,MAAM,eAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panelui-native",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "High-performance React Native UI components for Expo. Coss UI design language, powered by Uniwind (Tailwind v4) and Reanimated.",
5
5
  "main": "./lib/module/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -64,11 +64,13 @@
64
64
  "license": "MIT",
65
65
  "homepage": "https://github.com/panel-ui/PanelUI#readme",
66
66
  "peerDependencies": {
67
+ "expo-clipboard": ">=7.0.0",
67
68
  "react": "*",
68
69
  "react-native": "*",
69
70
  "react-native-gesture-handler": ">=2.0.0",
70
71
  "react-native-reanimated": ">=3.0.0",
71
72
  "react-native-safe-area-context": ">=4.0.0",
73
+ "react-native-svg": ">=15.0.0",
72
74
  "tailwindcss": ">=4.0.0",
73
75
  "uniwind": ">=1.0.0"
74
76
  },
@@ -92,5 +94,10 @@
92
94
  ],
93
95
  "typescript"
94
96
  ]
97
+ },
98
+ "peerDependenciesMeta": {
99
+ "expo-clipboard": {
100
+ "optional": true
101
+ }
95
102
  }
96
103
  }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * ScrollFade — fades the edges of a scroll container.
3
+ *
4
+ * The React Native equivalent of shadcn-ui/ui's `scroll-fade` utility
5
+ * (https://ui.shadcn.com/docs/utils/scroll-fade), which is CSS `mask-image`
6
+ * driven by a scroll timeline. React Native has neither masks nor scroll
7
+ * timelines, so the fades are SVG gradients overlaid on each edge, with their
8
+ * opacity following the scroll position.
9
+ *
10
+ * Composition follows heroui-native's ScrollShadow: wrap the scrollable and
11
+ * let the orientation be inferred from its `horizontal` prop.
12
+ *
13
+ * ```tsx
14
+ * <ScrollFade>
15
+ * <ScrollView horizontal>…</ScrollView>
16
+ * </ScrollFade>
17
+ * ```
18
+ */
19
+ import {
20
+ Children,
21
+ cloneElement,
22
+ isValidElement,
23
+ useState,
24
+ type ReactElement,
25
+ type ReactNode,
26
+ } from 'react';
27
+ import {
28
+ View,
29
+ type NativeScrollEvent,
30
+ type NativeSyntheticEvent,
31
+ type ViewProps,
32
+ } from 'react-native';
33
+ import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
34
+ import { useCSSVariable } from 'uniwind';
35
+
36
+ /** Past this many pixels from an edge, that edge's fade is fully shown. */
37
+ const FADE_IN_DISTANCE = 24;
38
+
39
+ export interface ScrollFadeProps extends ViewProps {
40
+ className?: string;
41
+ /** Depth of the fade in pixels. */
42
+ size?: number;
43
+ /** Which edges fade. */
44
+ edges?: 'both' | 'start' | 'end';
45
+ /**
46
+ * Colour the fade resolves to — normally whatever sits behind the
47
+ * scrollable. Defaults to the theme's background.
48
+ */
49
+ color?: string;
50
+ children?: ReactNode;
51
+ }
52
+
53
+ interface ScrollableProps {
54
+ horizontal?: boolean;
55
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
56
+ scrollEventThrottle?: number;
57
+ }
58
+
59
+ export function ScrollFade({
60
+ className,
61
+ size = 48,
62
+ edges = 'both',
63
+ color,
64
+ children,
65
+ ...props
66
+ }: ScrollFadeProps) {
67
+ const [startOpacity, setStartOpacity] = useState(0);
68
+ const [endOpacity, setEndOpacity] = useState(0);
69
+
70
+ const themeBackground = useCSSVariable('--color-background');
71
+ const fadeColor =
72
+ color ?? (typeof themeBackground === 'string' ? themeBackground : '#000000');
73
+
74
+ const child = Children.only(children) as ReactElement<ScrollableProps>;
75
+ const horizontal = isValidElement(child) ? !!child.props.horizontal : false;
76
+
77
+ const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
78
+ const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
79
+ const offset = horizontal ? contentOffset.x : contentOffset.y;
80
+ const total = horizontal ? contentSize.width : contentSize.height;
81
+ const visible = horizontal ? layoutMeasurement.width : layoutMeasurement.height;
82
+ const remaining = total - visible - offset;
83
+
84
+ // Each edge fades in over the first FADE_IN_DISTANCE px of travel, so an
85
+ // edge with nothing past it shows nothing.
86
+ setStartOpacity(Math.min(offset / FADE_IN_DISTANCE, 1));
87
+ setEndOpacity(Math.min(Math.max(remaining, 0) / FADE_IN_DISTANCE, 1));
88
+
89
+ child.props.onScroll?.(event);
90
+ };
91
+
92
+ const scrollable = isValidElement(child)
93
+ ? cloneElement(child, {
94
+ onScroll: handleScroll,
95
+ scrollEventThrottle: child.props.scrollEventThrottle ?? 16,
96
+ })
97
+ : child;
98
+
99
+ const showStart = edges !== 'end';
100
+ const showEnd = edges !== 'start';
101
+
102
+ return (
103
+ <View {...props} className={className}>
104
+ {scrollable}
105
+
106
+ {showStart ? (
107
+ <Fade
108
+ color={fadeColor}
109
+ size={size}
110
+ horizontal={horizontal}
111
+ opacity={startOpacity}
112
+ edge="start"
113
+ />
114
+ ) : null}
115
+ {showEnd ? (
116
+ <Fade
117
+ color={fadeColor}
118
+ size={size}
119
+ horizontal={horizontal}
120
+ opacity={endOpacity}
121
+ edge="end"
122
+ />
123
+ ) : null}
124
+ </View>
125
+ );
126
+ }
127
+
128
+ ScrollFade.displayName = 'ScrollFade';
129
+
130
+ /** One edge's gradient, opaque at the edge and clear inwards. */
131
+ function Fade({
132
+ color,
133
+ size,
134
+ horizontal,
135
+ opacity,
136
+ edge,
137
+ }: {
138
+ color: string;
139
+ size: number;
140
+ horizontal: boolean;
141
+ opacity: number;
142
+ edge: 'start' | 'end';
143
+ }) {
144
+ const isStart = edge === 'start';
145
+ const id = `scroll-fade-${edge}-${horizontal ? 'x' : 'y'}`;
146
+
147
+ const position = horizontal
148
+ ? { top: 0, bottom: 0, width: size, ...(isStart ? { left: 0 } : { right: 0 }) }
149
+ : { left: 0, right: 0, height: size, ...(isStart ? { top: 0 } : { bottom: 0 }) };
150
+
151
+ // The gradient runs from the screen edge inwards, so it is reversed on the
152
+ // end edge.
153
+ const [x2, y2] = horizontal ? [1, 0] : [0, 1];
154
+
155
+ return (
156
+ <View pointerEvents="none" style={{ position: 'absolute', opacity, ...position }}>
157
+ <Svg width="100%" height="100%">
158
+ <Defs>
159
+ <LinearGradient id={id} x1="0" y1="0" x2={String(x2)} y2={String(y2)}>
160
+ <Stop offset="0" stopColor={color} stopOpacity={isStart ? 1 : 0} />
161
+ <Stop offset="1" stopColor={color} stopOpacity={isStart ? 0 : 1} />
162
+ </LinearGradient>
163
+ </Defs>
164
+ <Rect x="0" y="0" width="100%" height="100%" fill={`url(#${id})`} />
165
+ </Svg>
166
+ </View>
167
+ );
168
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Shimmer — an animated highlight sweeping across content.
3
+ *
4
+ * The React Native equivalent of shadcn-ui/ui's `shimmer` utility
5
+ * (https://ui.shadcn.com/docs/utils/shimmer). That one is CSS:
6
+ * `background-clip: text` with a `color-mix()` highlight. React Native has
7
+ * neither, so the highlight is drawn as an SVG gradient band swept across an
8
+ * overlay by Reanimated, on the UI thread.
9
+ *
10
+ * The visible difference: the band passes *over* the content rather than
11
+ * being clipped to glyph shapes. On body-sized text the two read the same; on
12
+ * very large text the web version is crisper.
13
+ *
14
+ * ```tsx
15
+ * <Shimmer>
16
+ * <Text muted>Generating response…</Text>
17
+ * </Shimmer>
18
+ * ```
19
+ */
20
+ import { useEffect, useState, type ReactNode } from 'react';
21
+ import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
22
+ import Animated, {
23
+ cancelAnimation,
24
+ Easing,
25
+ useAnimatedStyle,
26
+ useReducedMotion,
27
+ useSharedValue,
28
+ withRepeat,
29
+ withTiming,
30
+ } from 'react-native-reanimated';
31
+ import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
32
+ import { useCSSVariable } from 'uniwind';
33
+ import { cn } from '../../utils/cn';
34
+
35
+ export interface ShimmerProps extends ViewProps {
36
+ className?: string;
37
+ /** Milliseconds for one sweep. */
38
+ duration?: number;
39
+ /** Sweep once instead of looping. */
40
+ once?: boolean;
41
+ /** Sweep right-to-left. */
42
+ reverse?: boolean;
43
+ /** Width of the highlight band, as a fraction of the content width. */
44
+ spread?: number;
45
+ /** Highlight colour. Defaults to the theme's foreground. */
46
+ color?: string;
47
+ /** Peak opacity of the highlight. */
48
+ intensity?: number;
49
+ children?: ReactNode;
50
+ }
51
+
52
+ export function Shimmer({
53
+ className,
54
+ duration = 2000,
55
+ once = false,
56
+ reverse = false,
57
+ spread = 0.4,
58
+ color,
59
+ intensity = 0.45,
60
+ children,
61
+ ...props
62
+ }: ShimmerProps) {
63
+ const [width, setWidth] = useState(0);
64
+ const progress = useSharedValue(0);
65
+ const reducedMotion = useReducedMotion();
66
+
67
+ const themeForeground = useCSSVariable('--color-foreground');
68
+ const highlight =
69
+ color ?? (typeof themeForeground === 'string' ? themeForeground : '#ffffff');
70
+
71
+ useEffect(() => {
72
+ // Respect the OS setting rather than animating regardless — the same
73
+ // thing shadcn's utility does with prefers-reduced-motion.
74
+ if (reducedMotion || width === 0) return;
75
+
76
+ progress.value = 0;
77
+ progress.value = withRepeat(
78
+ withTiming(1, { duration, easing: Easing.linear }),
79
+ once ? 1 : -1,
80
+ false
81
+ );
82
+
83
+ return () => cancelAnimation(progress);
84
+ }, [duration, once, reducedMotion, width, progress]);
85
+
86
+ const bandWidth = Math.max(width * spread, 1);
87
+
88
+ const bandStyle = useAnimatedStyle(() => {
89
+ // Travel from fully off one edge to fully off the other.
90
+ const from = -bandWidth;
91
+ const to = width;
92
+ const x = reverse
93
+ ? to - progress.value * (to - from)
94
+ : from + progress.value * (to - from);
95
+ return { transform: [{ translateX: x }] };
96
+ });
97
+
98
+ const onLayout = (event: LayoutChangeEvent) => {
99
+ setWidth(event.nativeEvent.layout.width);
100
+ props.onLayout?.(event);
101
+ };
102
+
103
+ return (
104
+ <View
105
+ {...props}
106
+ onLayout={onLayout}
107
+ className={cn('overflow-hidden', className)}
108
+ >
109
+ {children}
110
+
111
+ {reducedMotion || width === 0 ? null : (
112
+ <Animated.View
113
+ pointerEvents="none"
114
+ style={[
115
+ { position: 'absolute', top: 0, bottom: 0, width: bandWidth },
116
+ bandStyle,
117
+ ]}
118
+ >
119
+ <Svg width="100%" height="100%">
120
+ <Defs>
121
+ <LinearGradient id="shimmer" x1="0" y1="0" x2="1" y2="0">
122
+ <Stop offset="0" stopColor={highlight} stopOpacity={0} />
123
+ <Stop offset="0.5" stopColor={highlight} stopOpacity={intensity} />
124
+ <Stop offset="1" stopColor={highlight} stopOpacity={0} />
125
+ </LinearGradient>
126
+ </Defs>
127
+ <Rect x="0" y="0" width="100%" height="100%" fill="url(#shimmer)" />
128
+ </Svg>
129
+ </Animated.View>
130
+ )}
131
+ </View>
132
+ );
133
+ }
134
+
135
+ Shimmer.displayName = 'Shimmer';
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Surface — an elevated container.
3
+ *
4
+ * Adapted from: heroui-inc/heroui-native src/components/surface/
5
+ * (the variant ladder and the nesting model for building hierarchy).
6
+ *
7
+ * Both the fill and the corner radius follow the active theme: the variants
8
+ * resolve to themed `--color-surface-*` tokens, and `rounded-3xl` resolves
9
+ * through the per-theme radius scale — so Moon's surfaces are tight and
10
+ * Grass's are soft, without the component knowing anything about either.
11
+ *
12
+ * ```tsx
13
+ * <Surface>
14
+ * <Surface variant="secondary">
15
+ * <Surface variant="tertiary">…</Surface>
16
+ * </Surface>
17
+ * </Surface>
18
+ * ```
19
+ */
20
+ import { forwardRef } from 'react';
21
+ import { StyleSheet, View, type ViewProps } from 'react-native';
22
+ import { tv, type VariantProps } from 'tailwind-variants';
23
+
24
+ const surfaceVariants = tv({
25
+ base: 'overflow-hidden rounded-3xl p-4',
26
+ variants: {
27
+ variant: {
28
+ default: 'bg-surface',
29
+ secondary: 'bg-surface-secondary',
30
+ tertiary: 'bg-surface-tertiary',
31
+ transparent: 'bg-transparent',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: 'default',
36
+ },
37
+ });
38
+
39
+ export interface SurfaceProps
40
+ extends ViewProps,
41
+ VariantProps<typeof surfaceVariants> {
42
+ className?: string;
43
+ }
44
+
45
+ export const Surface = forwardRef<View, SurfaceProps>(
46
+ ({ className, variant, style, ...props }, ref) => (
47
+ <View
48
+ ref={ref}
49
+ // `borderCurve` has no Tailwind equivalent. On iOS it gives the
50
+ // continuous (squircle) corner Apple uses, which is visibly smoother
51
+ // than a circular arc at this radius; Android ignores it.
52
+ style={[styles.root, style]}
53
+ className={surfaceVariants({ variant, className })}
54
+ {...props}
55
+ />
56
+ )
57
+ );
58
+
59
+ Surface.displayName = 'Surface';
60
+
61
+ const styles = StyleSheet.create({
62
+ root: { borderCurve: 'continuous' },
63
+ });
@@ -58,7 +58,9 @@ const timelineVariants = tv({
58
58
  item: 'w-full flex-row gap-3',
59
59
  aside: 'w-20 items-end gap-0.5 pt-0.5',
60
60
  rail: 'items-center',
61
- indicator: 'items-center justify-center rounded-full border-2',
61
+ // Flat discs, no ring — the same restraint Steps uses. Rings on every node
62
+ // were most of what made the timeline read as noisy.
63
+ indicator: 'items-center justify-center rounded-full',
62
64
  indicatorLabel: 'text-xs font-medium',
63
65
  separator: 'w-0.5 flex-1 rounded-full',
64
66
  body: 'flex-1 pb-6',
@@ -112,22 +114,48 @@ const timelineVariants = tv({
112
114
  },
113
115
  });
114
116
 
115
- /** Node fill and border per tone, split by whether the node is solid. */
116
- const TONE_NODE: Record<TimelineTone, { solid: string; outline: string }> = {
117
- default: { solid: 'border-primary bg-primary', outline: 'border-primary' },
118
- info: { solid: 'border-info bg-info', outline: 'border-info' },
119
- success: { solid: 'border-success bg-success', outline: 'border-success' },
120
- warning: { solid: 'border-warning bg-warning', outline: 'border-warning' },
121
- danger: { solid: 'border-destructive bg-destructive', outline: 'border-destructive' },
117
+ /*
118
+ * Colour rule, matching the Steps component: progress is solid, event kind is
119
+ * tinted.
120
+ *
121
+ * - untoned pending → bg-muted (Steps' inactive)
122
+ * - untoned completed → bg-primary (Steps' completed)
123
+ * - toned → bg-{tone}-soft, content in the tone's foreground
124
+ *
125
+ * The `-soft` fills are the same tinted tokens Alert uses, so a toned node sits
126
+ * in the same family as the rest of the library instead of shouting in raw
127
+ * brand colour.
128
+ *
129
+ * Nodes that hold something — an icon or a number — get a hairline so they read
130
+ * as a container. The `dot` and `card` discs stay bare: at 16px an outline is
131
+ * most of what made the original look busy. The border therefore lives on the
132
+ * tone class rather than the shared `indicator` slot, so a solid disc cannot
133
+ * pick one up by accident.
134
+ */
135
+ const TONE_NODE: Record<TimelineTone, { solid: string; tinted: string }> = {
136
+ default: { solid: 'bg-primary', tinted: 'border border-border bg-muted' },
137
+ info: { solid: 'bg-info', tinted: 'border border-info/32 bg-info-soft' },
138
+ success: { solid: 'bg-success', tinted: 'border border-success/32 bg-success-soft' },
139
+ warning: { solid: 'bg-warning', tinted: 'border border-warning/32 bg-warning-soft' },
140
+ danger: {
141
+ solid: 'bg-destructive',
142
+ tinted: 'border border-destructive/32 bg-destructive-soft',
143
+ },
122
144
  };
123
145
 
124
- /** The node an incomplete, untoned step gets. */
125
- const PENDING_NODE = 'border-muted bg-background';
146
+ /**
147
+ * The node an incomplete, untoned step gets — bordered when it holds an icon
148
+ * or a number, bare when it is a status disc.
149
+ */
150
+ const PENDING_NODE = {
151
+ solid: 'bg-muted',
152
+ tinted: 'border border-border bg-muted',
153
+ } as const;
126
154
 
127
155
  /**
128
- * CSS variable an outlined node's icon takes its colour from. The
129
- * `-foreground` tokens are the ones tuned to read against the page in both
130
- * light and dark, which is what an outlined node's contents sit on.
156
+ * CSS variable a tinted node's contents take their colour from. The
157
+ * `-foreground` tokens are the ones tuned to read in both light and dark,
158
+ * which is what sits on a soft tint.
131
159
  */
132
160
  const TONE_ICON_VAR: Record<TimelineTone, string> = {
133
161
  default: '--color-primary',
@@ -264,33 +292,31 @@ const TimelineIndicator = forwardRef<View, TimelineIndicatorProps>(
264
292
  completed,
265
293
  });
266
294
 
267
- const isSolid = SOLID_VARIANTS.includes(variant);
295
+ // dot/card nodes are small discs with nothing inside, so they keep full
296
+ // saturation — a soft tint at 16px would disappear. Nodes that hold an
297
+ // icon or a number take the tinted fill instead.
298
+ const isDisc = SOLID_VARIANTS.includes(variant);
268
299
  const toned = tone !== 'default';
269
- // An untoned, unfinished step stays neutral; everything else takes its
270
- // tone (or primary, once complete).
271
- const nodeClass =
272
- toned || completed
273
- ? TONE_NODE[tone][isSolid ? 'solid' : 'outline']
274
- : PENDING_NODE;
275
-
276
- const themedIcon = useCSSVariable(
277
- completed || toned ? TONE_ICON_VAR[tone] : '--color-muted-foreground'
300
+
301
+ const fill = isDisc ? 'solid' : 'tinted';
302
+ const nodeClass = toned
303
+ ? TONE_NODE[tone][fill]
304
+ : completed
305
+ ? TONE_NODE.default.solid
306
+ : PENDING_NODE[fill];
307
+
308
+ // Contents sit on the tint (or on primary once complete), so they take the
309
+ // colour tuned to read against it.
310
+ const tintedContent = useCSSVariable(
311
+ toned ? TONE_ICON_VAR[tone] : '--color-muted-foreground'
278
312
  );
279
- const outlineColor = typeof themedIcon === 'string' ? themedIcon : undefined;
280
-
281
- // On a solid node the contents sit on the fill, so they need the colour
282
- // that reads against it: the theme's own foreground for `default`, and
283
- // white for the saturated status fills, which stay saturated in both
284
- // themes.
285
- const primaryForeground = useCSSVariable('--color-primary-foreground');
286
- const solidColor =
287
- tone === 'default'
288
- ? typeof primaryForeground === 'string'
289
- ? primaryForeground
290
- : undefined
291
- : '#ffffff';
292
-
293
- const contentColor = isSolid && (completed || toned) ? solidColor : outlineColor;
313
+ const solidContent = useCSSVariable('--color-primary-foreground');
314
+
315
+ const resolved = (value: unknown) =>
316
+ typeof value === 'string' ? value : undefined;
317
+
318
+ const contentColor =
319
+ !toned && completed ? resolved(solidContent) : resolved(tintedContent);
294
320
 
295
321
  return (
296
322
  <View className={rail()}>
@@ -0,0 +1,19 @@
1
+ export {
2
+ useCopyToClipboard,
3
+ type UseCopyToClipboardOptions,
4
+ type UseCopyToClipboardResult,
5
+ } from './use-copy-to-clipboard';
6
+ export {
7
+ useDisclosure,
8
+ type UseDisclosureOptions,
9
+ type UseDisclosureResult,
10
+ } from './use-disclosure';
11
+ export {
12
+ useBreakpoint,
13
+ BREAKPOINTS,
14
+ type Breakpoint,
15
+ type UseBreakpointResult,
16
+ } from './use-breakpoint';
17
+ export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
18
+ export { useDebouncedValue } from './use-debounced-value';
19
+ export { usePrevious } from './use-previous';
@@ -0,0 +1,52 @@
1
+ import { useWindowDimensions } from 'react-native';
2
+
3
+ /** Tailwind's breakpoints, minus the ones no phone or tablet reaches. */
4
+ export const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;
5
+
6
+ export type Breakpoint = keyof typeof BREAKPOINTS;
7
+
8
+ export interface UseBreakpointResult {
9
+ /** Largest breakpoint the window currently satisfies, or `base`. */
10
+ current: Breakpoint | 'base';
11
+ /** True when the window is at least this wide. */
12
+ isAtLeast: (breakpoint: Breakpoint) => boolean;
13
+ width: number;
14
+ height: number;
15
+ isLandscape: boolean;
16
+ }
17
+
18
+ /**
19
+ * Responsive state from the window size — the React Native answer to
20
+ * coss.com/ui's `useMediaQuery`, which has no equivalent here.
21
+ *
22
+ * For styling, prefer Uniwind's responsive class prefixes (`md:flex-row`).
23
+ * Reach for this when the *behaviour* changes, not just the look — rendering
24
+ * a sheet on a phone and a dialog on a tablet, say.
25
+ *
26
+ * ```tsx
27
+ * const { isAtLeast } = useBreakpoint();
28
+ * return isAtLeast('md') ? <Dialog …/> : <BottomSheet …/>;
29
+ * ```
30
+ */
31
+ export function useBreakpoint(): UseBreakpointResult {
32
+ const { width, height } = useWindowDimensions();
33
+
34
+ const current: Breakpoint | 'base' =
35
+ width >= BREAKPOINTS.xl
36
+ ? 'xl'
37
+ : width >= BREAKPOINTS.lg
38
+ ? 'lg'
39
+ : width >= BREAKPOINTS.md
40
+ ? 'md'
41
+ : width >= BREAKPOINTS.sm
42
+ ? 'sm'
43
+ : 'base';
44
+
45
+ return {
46
+ current,
47
+ isAtLeast: (breakpoint) => width >= BREAKPOINTS[breakpoint],
48
+ width,
49
+ height,
50
+ isLandscape: width > height,
51
+ };
52
+ }