@comergehq/studio 0.1.10 → 0.1.11

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comergehq/studio",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Comerge studio",
5
5
  "main": "src/index.ts",
6
6
  "module": "dist/index.mjs",
@@ -9,12 +9,14 @@ import {
9
9
  View,
10
10
  type ViewStyle,
11
11
  } from 'react-native';
12
- import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
12
+ import { isLiquidGlassSupported } from '@callstack/liquid-glass';
13
13
  import { Plus } from 'lucide-react-native';
14
14
 
15
15
  import { useTheme } from '../../theme';
16
16
  import { MultilineTextInput } from './MultilineTextInput';
17
17
  import { IconChevronRight, IconClose } from '../icons/StudioIcons';
18
+ import { withAlpha } from '../utils/color';
19
+ import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
18
20
 
19
21
  export type ChatComposerProps = {
20
22
  value?: string;
@@ -157,6 +159,7 @@ export function ChatComposer({
157
159
 
158
160
  const textareaBgColor = theme.scheme === 'dark' ? '#18181B' : '#F6F6F6';
159
161
  const placeholderTextColor = theme.scheme === 'dark' ? '#A1A1AA' : '#71717A';
162
+ const sendBg = withAlpha(theme.colors.primary, isButtonDisabled ? 0.6 : sendPressed ? 0.9 : 1);
160
163
 
161
164
  return (
162
165
  <View
@@ -165,7 +168,7 @@ export function ChatComposer({
165
168
  >
166
169
  <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 8 }}>
167
170
  <Animated.View style={{ flex: 1, transform: [{ translateX: shakeAnim }] }}>
168
- <LiquidGlassView
171
+ <ResettableLiquidGlassView
169
172
  style={[
170
173
  // LiquidGlassView doesn't reliably auto-size to children; ensure enough height for the
171
174
  // thumbnail strip when attachments are present.
@@ -234,10 +237,10 @@ export function ChatComposer({
234
237
  lineHeight: 20,
235
238
  }}
236
239
  />
237
- </LiquidGlassView>
240
+ </ResettableLiquidGlassView>
238
241
  </Animated.View>
239
242
 
240
- <LiquidGlassView
243
+ <ResettableLiquidGlassView
241
244
  style={[{ borderRadius: 100 }, !isLiquidGlassSupported && { backgroundColor: textareaBgColor }]}
242
245
  interactive
243
246
  effect="clear"
@@ -248,8 +251,7 @@ export function ChatComposer({
248
251
  height: 44,
249
252
  borderRadius: 22,
250
253
  overflow: 'hidden',
251
- backgroundColor: theme.colors.primary,
252
- opacity: isButtonDisabled ? 0.6 : sendPressed ? 0.9 : 1,
254
+ backgroundColor: sendBg,
253
255
  }}
254
256
  >
255
257
  <Pressable
@@ -270,7 +272,7 @@ export function ChatComposer({
270
272
  )}
271
273
  </Pressable>
272
274
  </View>
273
- </LiquidGlassView>
275
+ </ResettableLiquidGlassView>
274
276
  </View>
275
277
  </View>
276
278
  );
@@ -6,7 +6,7 @@ import {
6
6
  BottomSheetScrollView,
7
7
  } from '@gorhom/bottom-sheet';
8
8
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
- import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
9
+ import { isLiquidGlassSupported } from '@callstack/liquid-glass';
10
10
  import { Play } from 'lucide-react-native';
11
11
 
12
12
  import { useTheme } from '../../theme';
@@ -17,6 +17,7 @@ import { CommentRow } from './CommentRow';
17
17
  import { useAppComments } from './useAppComments';
18
18
  import { useAppDetails } from './useAppDetails';
19
19
  import { useIosKeyboardSnapFix } from './useIosKeyboardSnapFix';
20
+ import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
20
21
 
21
22
  export type AppCommentsSheetProps = {
22
23
  appId: string | null;
@@ -117,7 +118,7 @@ export function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }: A
117
118
  {loadingApp ? 'Loading...' : app?.name || 'Comments'}
118
119
  </Text>
119
120
 
120
- <LiquidGlassView
121
+ <ResettableLiquidGlassView
121
122
  style={[
122
123
  { borderRadius: 24 },
123
124
  !isLiquidGlassSupported && { backgroundColor: theme.scheme === 'dark' ? '#18181B' : '#F6F6F6' },
@@ -130,10 +131,9 @@ export function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }: A
130
131
  width: 32,
131
132
  height: 32,
132
133
  borderRadius: 999,
133
- backgroundColor: theme.colors.primary,
134
+ backgroundColor: withAlpha(theme.colors.primary, appId ? 1 : 0.5),
134
135
  alignItems: 'center',
135
136
  justifyContent: 'center',
136
- opacity: appId ? 1 : 0.5,
137
137
  }}
138
138
  >
139
139
  <Pressable
@@ -147,13 +147,13 @@ export function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }: A
147
147
  alignItems: 'center',
148
148
  justifyContent: 'center',
149
149
  },
150
- pressed ? { opacity: 0.85 } : null,
150
+ pressed ? { transform: [{ scale: 0.96 }] } : null,
151
151
  ]}
152
152
  >
153
153
  <Play size={16} color={theme.colors.onPrimary} />
154
154
  </Pressable>
155
155
  </View>
156
- </LiquidGlassView>
156
+ </ResettableLiquidGlassView>
157
157
  </View>
158
158
 
159
159
  <BottomSheetScrollView
@@ -20,11 +20,12 @@ import Animated, {
20
20
  withSpring,
21
21
  withTiming,
22
22
  } from 'react-native-reanimated';
23
- import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
23
+ import { isLiquidGlassSupported } from '@callstack/liquid-glass';
24
24
 
25
- import { DEFAULT_EDGE_PADDING, DEFAULT_OFFSET, DEFAULT_SIZE, ENTER_ROTATION_FROM_DEG, ENTER_SCALE_FROM, HIDDEN_OPACITY, PULSE_DURATION_MS } from './constants';
25
+ import { DEFAULT_EDGE_PADDING, DEFAULT_OFFSET, DEFAULT_SIZE, ENTER_ROTATION_FROM_DEG, ENTER_SCALE_FROM, PULSE_DURATION_MS } from './constants';
26
26
  import type { FloatingDraggableButtonProps } from './types';
27
27
  import { useTheme } from '../../theme';
28
+ import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
28
29
 
29
30
  const HIDDEN_OFFSET_X = 20;
30
31
 
@@ -35,9 +36,6 @@ const SPRING_ROTATION_IN = { damping: 15, stiffness: 80 } as const;
35
36
  const SPRING_ROTATION_GRAB = { damping: 20 } as const;
36
37
  const SPRING_SCALE_GRAB = { damping: 15, stiffness: 200 } as const;
37
38
 
38
- const TIMING_OPACITY_IN = { duration: 300, easing: Easing.out(Easing.ease) } as const;
39
- const TIMING_OPACITY_OUT = { duration: 250, easing: Easing.in(Easing.ease) } as const;
40
-
41
39
  function clamp(value: number, min: number, max: number) {
42
40
  'worklet';
43
41
  return Math.max(min, Math.min(max, value));
@@ -91,7 +89,6 @@ export function FloatingDraggableButton({
91
89
  const translateY = useSharedValue(getHiddenTranslateY(height));
92
90
  const scale = useSharedValue(ENTER_SCALE_FROM);
93
91
  const rotation = useSharedValue(ENTER_ROTATION_FROM_DEG);
94
- const opacity = useSharedValue(1);
95
92
  const borderPulse = useSharedValue(0);
96
93
  const startPos = useRef({ x: 0, y: 0 });
97
94
  const isAnimatingOut = useRef(false);
@@ -99,26 +96,16 @@ export function FloatingDraggableButton({
99
96
  const animateToHidden = useCallback(
100
97
  (options?: { onFinish?: () => void }) => {
101
98
  // Animate back to starting position (reverse of entrance)
102
- translateX.value = withSpring(getHiddenTranslateX(size), SPRING_POSITION);
99
+ const finish = options?.onFinish;
100
+
101
+ translateX.value = withSpring(getHiddenTranslateX(size), SPRING_POSITION, (finished?: boolean) => {
102
+ if (finished && finish) runOnJS(finish)();
103
+ });
103
104
  translateY.value = withSpring(getHiddenTranslateY(height), SPRING_POSITION);
104
105
  scale.value = withSpring(ENTER_SCALE_FROM, SPRING_SCALE_IN);
105
106
  rotation.value = withSpring(ENTER_ROTATION_FROM_DEG, SPRING_ROTATION_IN);
106
-
107
- const finish = options?.onFinish;
108
- if (!finish) {
109
- opacity.value = withTiming(HIDDEN_OPACITY, TIMING_OPACITY_OUT);
110
- return;
111
- }
112
-
113
- opacity.value = withTiming(
114
- HIDDEN_OPACITY as unknown as number,
115
- TIMING_OPACITY_OUT,
116
- (finished?: boolean) => {
117
- if (finished) runOnJS(finish)();
118
- }
119
- );
120
107
  },
121
- [height, opacity, rotation, scale, size, translateX, translateY]
108
+ [height, rotation, scale, size, translateX, translateY]
122
109
  );
123
110
 
124
111
  const animateOut = useCallback(() => {
@@ -163,8 +150,7 @@ export function FloatingDraggableButton({
163
150
  withSpring(1, SPRING_SCALE_OUT)
164
151
  );
165
152
  rotation.value = withSpring(0, SPRING_ROTATION_IN);
166
- opacity.value = withTiming(1, TIMING_OPACITY_IN);
167
- }, [height, offset.bottom, offset.left, opacity, rotation, scale, size, translateX, translateY]);
153
+ }, [height, offset.bottom, offset.left, rotation, scale, size, translateX, translateY]);
168
154
 
169
155
  // Initial animation on mount
170
156
  useEffect(() => {
@@ -228,7 +214,6 @@ export function FloatingDraggableButton({
228
214
  { scale: scale.value },
229
215
  { rotate: `${rotation.value}deg` },
230
216
  ],
231
- opacity: opacity.value,
232
217
  }));
233
218
 
234
219
  const borderAnimatedStyle = useAnimatedStyle(() => {
@@ -257,7 +242,7 @@ export function FloatingDraggableButton({
257
242
  accessibilityLabel={ariaLabel}
258
243
  >
259
244
  <Animated.View style={[{ width: size, height: size, borderRadius: size / 2 }, borderAnimatedStyle]}>
260
- <LiquidGlassView
245
+ <ResettableLiquidGlassView
261
246
  style={[{ flex: 1, borderRadius: size / 2 }, !isLiquidGlassSupported && { backgroundColor: fallbackBgColor }]}
262
247
  interactive
263
248
  effect="clear"
@@ -271,7 +256,7 @@ export function FloatingDraggableButton({
271
256
  >
272
257
  {children ?? <View />}
273
258
  </Pressable>
274
- </LiquidGlassView>
259
+ </ResettableLiquidGlassView>
275
260
  </Animated.View>
276
261
 
277
262
  {badgeCount > 0 && (
@@ -1,11 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import { Pressable, View, type ViewStyle } from 'react-native';
3
- import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
3
+ import { isLiquidGlassSupported } from '@callstack/liquid-glass';
4
4
  import { Heart, MessageCircle } from 'lucide-react-native';
5
5
 
6
6
  import { useTheme } from '../../theme';
7
7
  import { Text } from '../primitives/Text';
8
8
  import { MergeIcon } from '../icons/MergeIcon';
9
+ import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
9
10
 
10
11
  export type StatsBarProps = {
11
12
  likeCount: number;
@@ -41,7 +42,7 @@ export function StatsBar({
41
42
  style,
42
43
  ]}
43
44
  >
44
- <LiquidGlassView
45
+ <ResettableLiquidGlassView
45
46
  style={[
46
47
  { borderRadius: 100, overflow: 'hidden' },
47
48
  fixedWidth ? { width: fixedWidth } : undefined,
@@ -101,7 +102,7 @@ export function StatsBar({
101
102
  </Text>
102
103
  </View>
103
104
  </View>
104
- </LiquidGlassView>
105
+ </ResettableLiquidGlassView>
105
106
  </View>
106
107
  );
107
108
  }
@@ -1,9 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import { Platform, View, type ViewStyle } from 'react-native';
3
3
  import type { BottomSheetBackgroundProps } from '@gorhom/bottom-sheet';
4
- import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
4
+ import { isLiquidGlassSupported } from '@callstack/liquid-glass';
5
5
 
6
6
  import { useTheme } from '../../theme';
7
+ import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
7
8
 
8
9
  export type StudioSheetBackgroundProps = BottomSheetBackgroundProps & {
9
10
  /**
@@ -35,7 +36,7 @@ export function StudioSheetBackground({
35
36
 
36
37
  return (
37
38
  <>
38
- <LiquidGlassView
39
+ <ResettableLiquidGlassView
39
40
  style={[containerStyle, !isLiquidGlassSupported && { backgroundColor: fallbackBgColor }]}
40
41
  effect="regular"
41
42
  />
@@ -1,8 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import { Pressable, View, type ViewStyle } from 'react-native';
3
- import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
3
+ import { isLiquidGlassSupported } from '@callstack/liquid-glass';
4
4
 
5
5
  import { useTheme } from '../../theme';
6
+ import { withAlpha } from '../utils/color';
7
+ import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
6
8
 
7
9
  export type StudioSheetHeaderIconButtonProps = {
8
10
  onPress: () => void;
@@ -38,11 +40,12 @@ export function StudioSheetHeaderIconButton({
38
40
  const glassInnerBg = intent === 'danger' ? theme.colors.danger : theme.colors.primary;
39
41
 
40
42
  const resolvedOpacity = disabled ? 0.6 : pressed ? 0.9 : 1;
43
+ const glassBg = withAlpha(glassInnerBg, resolvedOpacity);
41
44
 
42
45
  return (
43
46
  <View style={style}>
44
47
  {appearance === 'glass' ? (
45
- <LiquidGlassView
48
+ <ResettableLiquidGlassView
46
49
  style={[{ borderRadius: 100 }, !isLiquidGlassSupported && { backgroundColor: glassFallbackBg }]}
47
50
  interactive
48
51
  effect="clear"
@@ -54,8 +57,7 @@ export function StudioSheetHeaderIconButton({
54
57
  borderRadius: 100,
55
58
  alignItems: 'center',
56
59
  justifyContent: 'center',
57
- backgroundColor: glassInnerBg,
58
- opacity: resolvedOpacity,
60
+ backgroundColor: glassBg,
59
61
  }}
60
62
  >
61
63
  <Pressable
@@ -73,7 +75,7 @@ export function StudioSheetHeaderIconButton({
73
75
  {children}
74
76
  </Pressable>
75
77
  </View>
76
- </LiquidGlassView>
78
+ </ResettableLiquidGlassView>
77
79
  ) : (
78
80
  <View
79
81
  style={{
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+ import { LiquidGlassView } from '@callstack/liquid-glass';
3
+
4
+ import { useLiquidGlassResetToken } from './liquidGlassReset';
5
+
6
+ export type ResettableLiquidGlassViewProps = React.ComponentProps<typeof LiquidGlassView>;
7
+
8
+ export function ResettableLiquidGlassView({ children, ...props }: ResettableLiquidGlassViewProps) {
9
+ const token = useLiquidGlassResetToken();
10
+ const [layoutBootKey, setLayoutBootKey] = React.useState(0);
11
+ const sawNonZeroLayoutRef = React.useRef(false);
12
+
13
+ const onLayout: NonNullable<ResettableLiquidGlassViewProps['onLayout']> = (e) => {
14
+ props.onLayout?.(e);
15
+ const { width, height } = e.nativeEvent.layout;
16
+ if (width > 0 && height > 0 && !sawNonZeroLayoutRef.current) {
17
+ sawNonZeroLayoutRef.current = true;
18
+ setLayoutBootKey((k) => k + 1);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <LiquidGlassView key={`liquid-glass-${token}-${layoutBootKey}`} {...props} onLayout={onLayout}>
24
+ {children}
25
+ </LiquidGlassView>
26
+ );
27
+ }
28
+
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import { AppState, type AppStateStatus, Platform } from 'react-native';
3
+
4
+ const LiquidGlassResetContext = React.createContext(0);
5
+
6
+ export function LiquidGlassResetProvider({
7
+ children,
8
+ resetTriggers = [],
9
+ }: {
10
+ children: React.ReactNode;
11
+ resetTriggers?: React.DependencyList;
12
+ }) {
13
+ const [token, setToken] = React.useState(0);
14
+
15
+ React.useEffect(() => {
16
+ if (Platform.OS !== 'ios') return;
17
+
18
+ const onChange = (state: AppStateStatus) => {
19
+ if (state === 'active') setToken((t) => t + 1);
20
+ };
21
+
22
+ const sub = AppState.addEventListener('change', onChange);
23
+ return () => sub.remove();
24
+ }, []);
25
+
26
+ React.useEffect(() => {
27
+ setToken((t) => t + 1);
28
+ }, resetTriggers);
29
+
30
+ return <LiquidGlassResetContext.Provider value={token}>{children}</LiquidGlassResetContext.Provider>;
31
+ }
32
+
33
+ export function useLiquidGlassResetToken() {
34
+ return React.useContext(LiquidGlassResetContext);
35
+ }
36
+
@@ -13,6 +13,7 @@ import { useStudioActions } from './hooks/useStudioActions';
13
13
  import { hasNoOutcomeAfterLastHuman } from './lib/chat';
14
14
  import { RuntimeRenderer } from './ui/RuntimeRenderer';
15
15
  import { StudioOverlay } from './ui/StudioOverlay';
16
+ import { LiquidGlassResetProvider } from '../components/utils/liquidGlassReset';
16
17
 
17
18
  export type ComergeStudioProps = {
18
19
  appId: string;
@@ -46,20 +47,22 @@ export function ComergeStudio({
46
47
  <StudioBootstrap apiKey={apiKey}>
47
48
  {({ userId }) => (
48
49
  <BottomSheetModalProvider>
49
- <ComergeStudioInner
50
- userId={userId}
51
- activeAppId={activeAppId}
52
- setActiveAppId={setActiveAppId}
53
- runtimeAppId={runtimeAppId}
54
- setRuntimeAppId={setRuntimeAppId}
55
- pendingRuntimeTargetAppId={pendingRuntimeTargetAppId}
56
- setPendingRuntimeTargetAppId={setPendingRuntimeTargetAppId}
57
- appKey={appKey}
58
- platform={platform}
59
- onNavigateHome={onNavigateHome}
60
- captureTargetRef={captureTargetRef}
61
- style={style}
62
- />
50
+ <LiquidGlassResetProvider resetTriggers={[appId, activeAppId, runtimeAppId]}>
51
+ <ComergeStudioInner
52
+ userId={userId}
53
+ activeAppId={activeAppId}
54
+ setActiveAppId={setActiveAppId}
55
+ runtimeAppId={runtimeAppId}
56
+ setRuntimeAppId={setRuntimeAppId}
57
+ pendingRuntimeTargetAppId={pendingRuntimeTargetAppId}
58
+ setPendingRuntimeTargetAppId={setPendingRuntimeTargetAppId}
59
+ appKey={appKey}
60
+ platform={platform}
61
+ onNavigateHome={onNavigateHome}
62
+ captureTargetRef={captureTargetRef}
63
+ style={style}
64
+ />
65
+ </LiquidGlassResetProvider>
63
66
  </BottomSheetModalProvider>
64
67
  )}
65
68
  </StudioBootstrap>