@comergehq/studio 0.1.2 → 0.1.3

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 (172) hide show
  1. package/dist/index.js +255 -245
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +213 -203
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +8 -5
  6. package/src/components/chat/ChatComposer.tsx +277 -0
  7. package/src/components/chat/ChatHeader.tsx +31 -0
  8. package/src/components/chat/ChatMessageBubble.tsx +69 -0
  9. package/src/components/chat/ChatMessageList.tsx +137 -0
  10. package/src/components/chat/ChatPage.tsx +69 -0
  11. package/src/components/chat/ForkNoticeBanner.tsx +66 -0
  12. package/src/components/chat/MultilineTextInput.tsx +46 -0
  13. package/src/components/chat/ScrollToBottomButton.tsx +78 -0
  14. package/src/components/chat/TypingIndicator.tsx +54 -0
  15. package/src/components/chat/index.ts +28 -0
  16. package/src/components/comments/AppCommentsSheet.tsx +213 -0
  17. package/src/components/comments/CommentRow.tsx +63 -0
  18. package/src/components/comments/formatTimeAgo.ts +3 -0
  19. package/src/components/comments/index.ts +3 -0
  20. package/src/components/comments/useAppComments.ts +74 -0
  21. package/src/components/comments/useAppDetails.ts +35 -0
  22. package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
  23. package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
  24. package/src/components/dialogs/index.ts +4 -0
  25. package/src/components/draw/DrawColorPicker.tsx +77 -0
  26. package/src/components/draw/DrawModeOverlay.tsx +144 -0
  27. package/src/components/draw/DrawSurface.tsx +127 -0
  28. package/src/components/draw/DrawToolbar.tsx +253 -0
  29. package/src/components/draw/index.ts +15 -0
  30. package/src/components/draw/optionalHaptics.ts +15 -0
  31. package/src/components/draw/strokes.ts +21 -0
  32. package/src/components/draw/types.ts +9 -0
  33. package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
  34. package/src/components/floating-draggable-button/constants.ts +17 -0
  35. package/src/components/floating-draggable-button/index.ts +4 -0
  36. package/src/components/floating-draggable-button/types.ts +63 -0
  37. package/src/components/icons/MergeIcon.tsx +14 -0
  38. package/src/components/icons/StudioIcons.tsx +66 -0
  39. package/src/components/index.ts +17 -0
  40. package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
  41. package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
  42. package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
  43. package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
  44. package/src/components/merge-requests/index.ts +7 -0
  45. package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
  46. package/src/components/merge-requests/toIsoString.ts +9 -0
  47. package/src/components/merge-requests/useControlledExpansion.ts +16 -0
  48. package/src/components/models/index.ts +9 -0
  49. package/src/components/models/types.ts +43 -0
  50. package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
  51. package/src/components/overlays/index.ts +4 -0
  52. package/src/components/preview/PreviewHeroCard.tsx +58 -0
  53. package/src/components/preview/PreviewImage.tsx +22 -0
  54. package/src/components/preview/PreviewMetaRow.tsx +70 -0
  55. package/src/components/preview/PreviewPage.tsx +36 -0
  56. package/src/components/preview/PreviewPlaceholder.tsx +72 -0
  57. package/src/components/preview/PreviewStatusBadge.tsx +63 -0
  58. package/src/components/preview/StatsBar.tsx +109 -0
  59. package/src/components/preview/index.ts +22 -0
  60. package/src/components/primitives/Avatar.tsx +68 -0
  61. package/src/components/primitives/Button.tsx +102 -0
  62. package/src/components/primitives/Card.tsx +30 -0
  63. package/src/components/primitives/Divider.tsx +17 -0
  64. package/src/components/primitives/Icon.tsx +40 -0
  65. package/src/components/primitives/MarkdownText.tsx +72 -0
  66. package/src/components/primitives/Modal.tsx +53 -0
  67. package/src/components/primitives/Surface.tsx +42 -0
  68. package/src/components/primitives/Text.tsx +83 -0
  69. package/src/components/primitives/index.ts +35 -0
  70. package/src/components/primitives/types.ts +30 -0
  71. package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
  72. package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
  73. package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
  74. package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
  75. package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
  76. package/src/components/studio-sheet/index.ts +18 -0
  77. package/src/components/studio-sheet/types.ts +5 -0
  78. package/src/components/utils/color.ts +25 -0
  79. package/src/components/utils/formatTimeAgo.ts +19 -0
  80. package/src/core/logger.ts +42 -0
  81. package/src/core/services/http/baseUrl.ts +3 -0
  82. package/src/core/services/http/index.ts +128 -0
  83. package/src/core/services/http/public.ts +14 -0
  84. package/src/core/services/supabase/auth.ts +41 -0
  85. package/src/core/services/supabase/client.ts +43 -0
  86. package/src/core/services/supabase/index.ts +7 -0
  87. package/src/data/agent/remote.ts +30 -0
  88. package/src/data/agent/repository.ts +34 -0
  89. package/src/data/agent/types.ts +28 -0
  90. package/src/data/apps/bundles/remote.ts +47 -0
  91. package/src/data/apps/bundles/repository.ts +35 -0
  92. package/src/data/apps/bundles/types.ts +27 -0
  93. package/src/data/apps/images/remote.ts +61 -0
  94. package/src/data/apps/images/repository.ts +47 -0
  95. package/src/data/apps/remote.ts +97 -0
  96. package/src/data/apps/repository.ts +185 -0
  97. package/src/data/apps/types.ts +206 -0
  98. package/src/data/attachment/remote.ts +32 -0
  99. package/src/data/attachment/repository.ts +40 -0
  100. package/src/data/attachment/types.ts +42 -0
  101. package/src/data/base-remote.ts +3 -0
  102. package/src/data/base-repository.ts +11 -0
  103. package/src/data/comments/likes/remote.ts +87 -0
  104. package/src/data/comments/likes/repository.ts +61 -0
  105. package/src/data/comments/likes/types.ts +47 -0
  106. package/src/data/comments/remote.ts +71 -0
  107. package/src/data/comments/repository.ts +53 -0
  108. package/src/data/comments/types.ts +60 -0
  109. package/src/data/github/remote.ts +23 -0
  110. package/src/data/github/repository.ts +35 -0
  111. package/src/data/github/types.ts +23 -0
  112. package/src/data/home/remote.ts +24 -0
  113. package/src/data/home/repository.ts +28 -0
  114. package/src/data/home/types.ts +70 -0
  115. package/src/data/index.ts +3 -0
  116. package/src/data/likes/remote.ts +57 -0
  117. package/src/data/likes/repository.ts +47 -0
  118. package/src/data/likes/types.ts +46 -0
  119. package/src/data/me/remote.ts +28 -0
  120. package/src/data/me/repository.ts +30 -0
  121. package/src/data/me/types.ts +14 -0
  122. package/src/data/merge-requests/remote.ts +76 -0
  123. package/src/data/merge-requests/repository.ts +66 -0
  124. package/src/data/merge-requests/types.ts +33 -0
  125. package/src/data/messages/remote.ts +21 -0
  126. package/src/data/messages/repository.ts +104 -0
  127. package/src/data/messages/types.ts +20 -0
  128. package/src/data/public/studio-config/remote.ts +19 -0
  129. package/src/data/public/studio-config/repository.ts +23 -0
  130. package/src/data/public/studio-config/types.ts +6 -0
  131. package/src/data/ratings/remote.ts +76 -0
  132. package/src/data/ratings/repository.ts +63 -0
  133. package/src/data/ratings/types.ts +57 -0
  134. package/src/data/threads/remote.ts +40 -0
  135. package/src/data/threads/repository.ts +41 -0
  136. package/src/data/threads/types.ts +25 -0
  137. package/src/data/types.ts +8 -0
  138. package/src/data/users/remote.ts +31 -0
  139. package/src/data/users/repository.ts +45 -0
  140. package/src/data/users/types.ts +15 -0
  141. package/src/index.ts +6 -0
  142. package/src/studio/ComergeStudio.tsx +246 -0
  143. package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
  144. package/src/studio/bootstrap/useStudioBootstrap.ts +51 -0
  145. package/src/studio/hooks/useApp.ts +83 -0
  146. package/src/studio/hooks/useAppStats.ts +111 -0
  147. package/src/studio/hooks/useAttachmentUpload.ts +59 -0
  148. package/src/studio/hooks/useBundleManager.ts +389 -0
  149. package/src/studio/hooks/useMergeRequests.ts +173 -0
  150. package/src/studio/hooks/useStudioActions.ts +96 -0
  151. package/src/studio/hooks/useThreadMessages.ts +85 -0
  152. package/src/studio/lib/chat.ts +34 -0
  153. package/src/studio/ui/ChatPanel.tsx +154 -0
  154. package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
  155. package/src/studio/ui/PreviewPanel.tsx +131 -0
  156. package/src/studio/ui/RuntimeRenderer.tsx +40 -0
  157. package/src/studio/ui/StudioOverlay.tsx +257 -0
  158. package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
  159. package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
  160. package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
  161. package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
  162. package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
  163. package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
  164. package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
  165. package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
  166. package/src/studio/ui/preview-panel/utils.ts +29 -0
  167. package/src/theme/index.ts +5 -0
  168. package/src/theme/tokens.ts +118 -0
  169. package/src/theme/types.ts +90 -0
  170. package/src/theme/useTheme.ts +11 -0
  171. package/dist/assets/images/merge.svg +0 -3
  172. package/dist/merge-72UG27QV.svg +0 -3
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import { Pressable, View, type ViewStyle } from 'react-native';
3
+ import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
4
+ import { Heart, MessageCircle } from 'lucide-react-native';
5
+
6
+ import { useTheme } from '../../theme';
7
+ import { Text } from '../primitives/Text';
8
+ import { MergeIcon } from '../icons/MergeIcon';
9
+
10
+ export type StatsBarProps = {
11
+ likeCount: number;
12
+ commentCount: number;
13
+ forkCount: number;
14
+ isLiked?: boolean;
15
+ onPressLike?: () => void;
16
+ onPressComments?: () => void;
17
+ style?: ViewStyle;
18
+ centered?: boolean;
19
+ fixedWidth?: number;
20
+ };
21
+
22
+ export function StatsBar({
23
+ likeCount,
24
+ commentCount,
25
+ forkCount,
26
+ isLiked = false,
27
+ onPressLike,
28
+ onPressComments,
29
+ style,
30
+ centered = false,
31
+ fixedWidth,
32
+ }: StatsBarProps) {
33
+ const theme = useTheme();
34
+ const statsBgColor = theme.scheme === 'dark' ? 'rgba(24, 24, 27, 0.5)' : 'rgba(255, 255, 255, 0.5)';
35
+
36
+ return (
37
+ <View
38
+ style={[
39
+ { position: 'absolute', bottom: 12, width: '100%', paddingHorizontal: 12 },
40
+ centered && { alignItems: 'center' },
41
+ style,
42
+ ]}
43
+ >
44
+ <LiquidGlassView
45
+ style={[
46
+ { borderRadius: 100, overflow: 'hidden' },
47
+ fixedWidth ? { width: fixedWidth } : undefined,
48
+ !isLiquidGlassSupported && { backgroundColor: statsBgColor },
49
+ ]}
50
+ effect="clear"
51
+ >
52
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16 }}>
53
+ <Pressable
54
+ disabled={!onPressLike}
55
+ onPress={onPressLike}
56
+ hitSlop={8}
57
+ style={{ paddingVertical: 8 }}
58
+ >
59
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
60
+ <Heart
61
+ size={16}
62
+ strokeWidth={2.5}
63
+ color={isLiked ? theme.colors.danger : '#FFFFFF'}
64
+ fill={isLiked ? theme.colors.danger : 'transparent'}
65
+ />
66
+ <View style={{ width: 4 }} />
67
+ <Text
68
+ variant="caption"
69
+ style={{
70
+ color: isLiked ? theme.colors.danger : '#FFFFFF',
71
+ fontWeight: theme.typography.fontWeight.bold,
72
+ }}
73
+ >
74
+ {likeCount}
75
+ </Text>
76
+ </View>
77
+ </Pressable>
78
+
79
+ <Pressable
80
+ disabled={!onPressComments}
81
+ onPress={onPressComments}
82
+ hitSlop={8}
83
+ style={{ paddingVertical: 8 }}
84
+ >
85
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
86
+ <MessageCircle size={16} strokeWidth={2.5} color="#FFFFFF" />
87
+ <View style={{ width: 4 }} />
88
+ <Text variant="caption" style={{ color: '#FFFFFF', fontWeight: theme.typography.fontWeight.bold }}>
89
+ {commentCount}
90
+ </Text>
91
+ </View>
92
+ </Pressable>
93
+
94
+ <View style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8 }}>
95
+ <View style={{ transform: [{ scaleY: -1 }] }}>
96
+ <MergeIcon width={14} height={14} color="#FFFFFF" />
97
+ </View>
98
+ <View style={{ width: 4 }} />
99
+ <Text variant="caption" style={{ color: '#FFFFFF', fontWeight: theme.typography.fontWeight.bold }}>
100
+ {forkCount}
101
+ </Text>
102
+ </View>
103
+ </View>
104
+ </LiquidGlassView>
105
+ </View>
106
+ );
107
+ }
108
+
109
+
@@ -0,0 +1,22 @@
1
+ export { PreviewPage } from './PreviewPage';
2
+ export type { PreviewPageProps } from './PreviewPage';
3
+
4
+ export { PreviewHeroCard } from './PreviewHeroCard';
5
+ export type { PreviewHeroCardProps } from './PreviewHeroCard';
6
+
7
+ export { PreviewImage } from './PreviewImage';
8
+ export type { PreviewImageProps } from './PreviewImage';
9
+
10
+ export { PreviewPlaceholder } from './PreviewPlaceholder';
11
+ export type { PreviewPlaceholderProps } from './PreviewPlaceholder';
12
+
13
+ export { StatsBar } from './StatsBar';
14
+ export type { StatsBarProps } from './StatsBar';
15
+
16
+ export { PreviewMetaRow } from './PreviewMetaRow';
17
+ export type { PreviewMetaRowProps } from './PreviewMetaRow';
18
+
19
+ export { PreviewStatusBadge } from './PreviewStatusBadge';
20
+ export type { PreviewStatusBadgeProps } from './PreviewStatusBadge';
21
+
22
+
@@ -0,0 +1,68 @@
1
+ import * as React from 'react';
2
+ import { Image, View, type ImageStyle, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+ import { Text } from './Text';
6
+ import type { WithStyle } from './types';
7
+
8
+ export type AvatarProps = WithStyle<ViewStyle> & {
9
+ size?: number;
10
+ uri?: string | null;
11
+ name?: string | null;
12
+ /**
13
+ * Optional override for the fallback background.
14
+ * Prefer leaving this undefined so it uses theme.
15
+ */
16
+ fallbackBackgroundColor?: string;
17
+ imageStyle?: WithStyle<ImageStyle>['style'];
18
+ };
19
+
20
+ function initialsFrom(name?: string | null): string {
21
+ const trimmed = (name ?? '').trim();
22
+ if (!trimmed) return '?';
23
+ return trimmed[0]?.toUpperCase?.() ?? '?';
24
+ }
25
+
26
+ export function Avatar({
27
+ size = 32,
28
+ uri,
29
+ name,
30
+ fallbackBackgroundColor,
31
+ style,
32
+ imageStyle,
33
+ }: AvatarProps) {
34
+ const theme = useTheme();
35
+ const radius = size / 2;
36
+ const fallbackBg = fallbackBackgroundColor ?? theme.colors.neutral;
37
+
38
+ return (
39
+ <View
40
+ style={[
41
+ {
42
+ width: size,
43
+ height: size,
44
+ borderRadius: radius,
45
+ overflow: 'hidden',
46
+ backgroundColor: fallbackBg,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ },
50
+ style,
51
+ ]}
52
+ >
53
+ {uri ? (
54
+ <Image
55
+ source={{ uri }}
56
+ style={[{ width: size, height: size }, imageStyle]}
57
+ resizeMode="cover"
58
+ />
59
+ ) : (
60
+ <Text variant="caption" style={{ color: theme.colors.onNeutral }}>
61
+ {initialsFrom(name)}
62
+ </Text>
63
+ )}
64
+ </View>
65
+ );
66
+ }
67
+
68
+
@@ -0,0 +1,102 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Pressable,
4
+ type PressableProps,
5
+ type ViewStyle,
6
+ } from 'react-native';
7
+
8
+ import { useTheme } from '../../theme';
9
+ import type { ButtonSize, ButtonVariant, PressStateStyle, WithStyle } from './types';
10
+
11
+ export type ButtonProps = Omit<PressableProps, 'style'> &
12
+ WithStyle<ViewStyle> &
13
+ PressStateStyle<ViewStyle> & {
14
+ variant?: ButtonVariant;
15
+ size?: ButtonSize;
16
+ children?: React.ReactNode;
17
+ };
18
+
19
+ function backgroundFor(
20
+ variant: ButtonVariant,
21
+ theme: ReturnType<typeof useTheme>,
22
+ pressed: boolean,
23
+ disabled?: boolean
24
+ ): string {
25
+ const { colors } = theme;
26
+ if (variant === 'ghost') return 'transparent';
27
+
28
+ if (disabled) {
29
+ return colors.neutral;
30
+ }
31
+
32
+ const base =
33
+ variant === 'primary'
34
+ ? colors.primary
35
+ : variant === 'danger'
36
+ ? colors.danger
37
+ : colors.neutral;
38
+
39
+ // pressed state is a subtle overlay using borderStrong as tint source
40
+ if (!pressed) return base;
41
+ return base;
42
+ }
43
+
44
+ function borderFor(variant: ButtonVariant, theme: ReturnType<typeof useTheme>): { borderWidth?: number; borderColor?: string } {
45
+ if (variant !== 'ghost') return {};
46
+ return { borderWidth: 1, borderColor: theme.colors.border };
47
+ }
48
+
49
+ function paddingFor(size: ButtonSize, theme: ReturnType<typeof useTheme>): { paddingHorizontal: number; paddingVertical: number; minHeight: number; minWidth?: number } {
50
+ switch (size) {
51
+ case 'sm':
52
+ return { paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm, minHeight: 36 };
53
+ case 'icon':
54
+ return { paddingHorizontal: 0, paddingVertical: 0, minHeight: 44, minWidth: 44 };
55
+ case 'md':
56
+ default:
57
+ return { paddingHorizontal: theme.spacing.lg, paddingVertical: theme.spacing.md, minHeight: 44 };
58
+ }
59
+ }
60
+
61
+ export function Button({
62
+ variant = 'neutral',
63
+ size = 'md',
64
+ disabled,
65
+ style,
66
+ children,
67
+ ...props
68
+ }: ButtonProps) {
69
+ const theme = useTheme();
70
+ const isDisabled = disabled ?? undefined;
71
+
72
+ return (
73
+ <Pressable
74
+ {...props}
75
+ disabled={isDisabled}
76
+ style={(state) => {
77
+ const pressed = state.pressed;
78
+ const base: ViewStyle = {
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ flexDirection: 'row',
82
+ borderRadius: size === 'icon' ? theme.radii.pill : theme.radii.pill,
83
+ backgroundColor: backgroundFor(variant, theme, pressed, isDisabled),
84
+ opacity: pressed && !isDisabled ? 0.92 : 1,
85
+ ...paddingFor(size, theme),
86
+ ...(borderFor(variant, theme) as ViewStyle),
87
+ };
88
+
89
+ const resolved =
90
+ typeof style === 'function'
91
+ ? style({ pressed, disabled: isDisabled })
92
+ : style;
93
+
94
+ return [base, resolved] as any;
95
+ }}
96
+ >
97
+ {children}
98
+ </Pressable>
99
+ );
100
+ }
101
+
102
+
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ import { type ViewProps, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+ import { Surface } from './Surface';
6
+ import type { CardVariant, WithStyle } from './types';
7
+
8
+ export type CardProps = ViewProps &
9
+ WithStyle<ViewStyle> & {
10
+ variant?: CardVariant;
11
+ padded?: boolean;
12
+ border?: boolean;
13
+ };
14
+
15
+ export function Card({ variant = 'surface', padded = true, border = true, style, ...props }: CardProps) {
16
+ const theme = useTheme();
17
+ const radius = theme.radii.lg;
18
+ const padding = padded ? theme.spacing.lg : 0;
19
+
20
+ return (
21
+ <Surface
22
+ {...props}
23
+ variant={variant === 'surfaceRaised' ? 'surfaceRaised' : 'surface'}
24
+ border={border}
25
+ style={[{ borderRadius: radius, padding }, style]}
26
+ />
27
+ );
28
+ }
29
+
30
+
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+ import type { DividerVariant, WithStyle } from './types';
6
+
7
+ export type DividerProps = WithStyle<ViewStyle> & {
8
+ variant?: DividerVariant;
9
+ };
10
+
11
+ export function Divider({ variant = 'default', style }: DividerProps) {
12
+ const theme = useTheme();
13
+ const color = variant === 'subtle' ? theme.colors.border : theme.colors.borderStrong;
14
+ return <View style={[{ height: 1, backgroundColor: color }, style]} />;
15
+ }
16
+
17
+
@@ -0,0 +1,40 @@
1
+ import * as React from 'react';
2
+
3
+ import { useTheme } from '../../theme';
4
+ import type { IconColorRole } from './types';
5
+
6
+ export type IconProps = {
7
+ /**
8
+ * Any icon component that supports `color` and `size` props (e.g. lucide-react-native).
9
+ */
10
+ as: React.ComponentType<{ color?: string; size?: number }>;
11
+ size?: number;
12
+ role?: IconColorRole;
13
+ color?: string;
14
+ };
15
+
16
+ function colorFor(role: IconColorRole, theme: ReturnType<typeof useTheme>): string {
17
+ const { colors } = theme;
18
+ switch (role) {
19
+ case 'muted':
20
+ return colors.textMuted;
21
+ case 'primary':
22
+ return colors.primary;
23
+ case 'danger':
24
+ return colors.danger;
25
+ case 'success':
26
+ return colors.success;
27
+ case 'warning':
28
+ return colors.warning;
29
+ case 'default':
30
+ default:
31
+ return colors.text;
32
+ }
33
+ }
34
+
35
+ export function Icon({ as: Comp, size = 18, role = 'default', color }: IconProps) {
36
+ const theme = useTheme();
37
+ return <Comp size={size} color={color ?? colorFor(role, theme)} />;
38
+ }
39
+
40
+
@@ -0,0 +1,72 @@
1
+ import { Platform, View, type ViewStyle } from 'react-native';
2
+
3
+ import Markdown from 'react-native-markdown-display';
4
+
5
+ import { useTheme } from '../../theme';
6
+
7
+ export type MarkdownTextVariant = 'chat' | 'mergeRequest';
8
+
9
+ export type MarkdownTextProps = {
10
+ markdown: string;
11
+ variant?: MarkdownTextVariant;
12
+ /**
13
+ * Optional override for the base text color (e.g. success/error outcomes in chat).
14
+ */
15
+ bodyColor?: string;
16
+ style?: ViewStyle;
17
+ };
18
+
19
+ export function MarkdownText({ markdown, variant = 'chat', bodyColor, style }: MarkdownTextProps) {
20
+ const theme = useTheme();
21
+ const isDark = theme.scheme === 'dark';
22
+
23
+ const baseBodyColor = variant === 'mergeRequest' ? theme.colors.textMuted : theme.colors.text;
24
+ const linkColor =
25
+ variant === 'mergeRequest' ? (isDark ? theme.colors.primary : '#3700B3') : theme.colors.link;
26
+ const linkWeight = variant === 'mergeRequest' ? theme.typography.fontWeight.semibold : undefined;
27
+
28
+ const codeBgColor = isDark ? '#27272A' : '#E4E4E7';
29
+ const codeTextColor = isDark ? '#FFFFFF' : theme.colors.text;
30
+
31
+ const paragraphBottom = variant === 'mergeRequest' ? 8 : 6;
32
+ const baseLineHeight = variant === 'mergeRequest' ? 22 : 20;
33
+
34
+ return (
35
+ <View style={style}>
36
+ <Markdown
37
+ style={{
38
+ body: { color: bodyColor ?? baseBodyColor, fontSize: 14, lineHeight: baseLineHeight },
39
+ paragraph: { marginTop: 0, marginBottom: paragraphBottom },
40
+ link: { color: linkColor, fontWeight: linkWeight },
41
+ code_inline: {
42
+ backgroundColor: codeBgColor,
43
+ color: codeTextColor,
44
+ paddingHorizontal: variant === 'mergeRequest' ? 6 : 4,
45
+ paddingVertical: variant === 'mergeRequest' ? 2 : 0,
46
+ borderRadius: variant === 'mergeRequest' ? 6 : 4,
47
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
48
+ fontSize: 13,
49
+ },
50
+ code_block: {
51
+ backgroundColor: codeBgColor,
52
+ color: codeTextColor,
53
+ padding: variant === 'mergeRequest' ? 12 : 8,
54
+ borderRadius: variant === 'mergeRequest' ? 8 : 6,
55
+ marginVertical: variant === 'mergeRequest' ? 8 : 0,
56
+ },
57
+ fence: {
58
+ backgroundColor: codeBgColor,
59
+ color: codeTextColor,
60
+ padding: variant === 'mergeRequest' ? 12 : 8,
61
+ borderRadius: variant === 'mergeRequest' ? 8 : 6,
62
+ marginVertical: variant === 'mergeRequest' ? 8 : 0,
63
+ },
64
+ }}
65
+ >
66
+ {markdown}
67
+ </Markdown>
68
+ </View>
69
+ );
70
+ }
71
+
72
+
@@ -0,0 +1,53 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Modal as RNModal,
4
+ Pressable,
5
+ View,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+
9
+ import { useTheme } from '../../theme';
10
+ import { Card } from './Card';
11
+
12
+ export type ModalProps = {
13
+ visible: boolean;
14
+ onRequestClose: () => void;
15
+ children: React.ReactNode;
16
+ /**
17
+ * When true, tapping the backdrop closes the modal.
18
+ */
19
+ dismissOnBackdropPress?: boolean;
20
+ contentStyle?: ViewStyle;
21
+ };
22
+
23
+ export function Modal({
24
+ visible,
25
+ onRequestClose,
26
+ dismissOnBackdropPress = true,
27
+ children,
28
+ contentStyle,
29
+ }: ModalProps) {
30
+ const theme = useTheme();
31
+
32
+ return (
33
+ <RNModal
34
+ visible={visible}
35
+ transparent
36
+ animationType="fade"
37
+ onRequestClose={onRequestClose}
38
+ >
39
+ <View style={{ flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: 'center', padding: theme.spacing.lg }}>
40
+ <Pressable
41
+ accessibilityRole="button"
42
+ onPress={dismissOnBackdropPress ? onRequestClose : undefined}
43
+ style={{ position: 'absolute', inset: 0 }}
44
+ />
45
+ <Card variant="surfaceRaised" padded style={[{ borderRadius: theme.radii.xl }, contentStyle]}>
46
+ {children}
47
+ </Card>
48
+ </View>
49
+ </RNModal>
50
+ );
51
+ }
52
+
53
+
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ import { View, type ViewProps, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+ import type { SurfaceVariant, WithStyle } from './types';
6
+
7
+ export type SurfaceProps = ViewProps &
8
+ WithStyle<ViewStyle> & {
9
+ variant?: SurfaceVariant;
10
+ border?: boolean;
11
+ };
12
+
13
+ function backgroundFor(variant: SurfaceVariant, theme: ReturnType<typeof useTheme>): string {
14
+ const { colors } = theme;
15
+ switch (variant) {
16
+ case 'background':
17
+ return colors.background;
18
+ case 'surfaceRaised':
19
+ return colors.surfaceRaised;
20
+ case 'floating':
21
+ return colors.floatingSurface;
22
+ case 'surface':
23
+ default:
24
+ return colors.surface;
25
+ }
26
+ }
27
+
28
+ export function Surface({ variant = 'surface', border = false, style, ...props }: SurfaceProps) {
29
+ const theme = useTheme();
30
+ return (
31
+ <View
32
+ {...props}
33
+ style={[
34
+ { backgroundColor: backgroundFor(variant, theme) },
35
+ border ? { borderWidth: 1, borderColor: theme.colors.border } : null,
36
+ style,
37
+ ]}
38
+ />
39
+ );
40
+ }
41
+
42
+
@@ -0,0 +1,83 @@
1
+ import * as React from 'react';
2
+ import { Text as RNText, type TextProps as RNTextProps, type TextStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+ import type { TextAlign, TextVariant, WithStyle } from './types';
6
+
7
+ export type TextProps = RNTextProps &
8
+ WithStyle<TextStyle> & {
9
+ variant?: TextVariant;
10
+ align?: TextAlign;
11
+ /**
12
+ * Optional explicit color override. Prefer `variant` + theme.
13
+ * Use sparingly for semantic emphasis.
14
+ */
15
+ color?: string;
16
+ };
17
+
18
+ function getVariantStyle(variant: TextVariant, theme: ReturnType<typeof useTheme>): TextStyle {
19
+ const { colors, typography } = theme;
20
+ switch (variant) {
21
+ case 'title':
22
+ return {
23
+ color: colors.text,
24
+ fontSize: typography.fontSize.xl,
25
+ lineHeight: typography.lineHeight.xl,
26
+ fontWeight: typography.fontWeight.semibold,
27
+ };
28
+ case 'caption':
29
+ return {
30
+ color: colors.text,
31
+ fontSize: typography.fontSize.xs,
32
+ lineHeight: typography.lineHeight.xs,
33
+ fontWeight: typography.fontWeight.medium,
34
+ };
35
+ case 'captionMuted':
36
+ return {
37
+ color: colors.textSubtle,
38
+ fontSize: typography.fontSize.xs,
39
+ lineHeight: typography.lineHeight.xs,
40
+ fontWeight: typography.fontWeight.medium,
41
+ };
42
+ case 'bodyMuted':
43
+ return {
44
+ color: colors.textMuted,
45
+ fontSize: typography.fontSize.md,
46
+ lineHeight: typography.lineHeight.md,
47
+ fontWeight: typography.fontWeight.regular,
48
+ };
49
+ case 'body':
50
+ default:
51
+ return {
52
+ color: colors.text,
53
+ fontSize: typography.fontSize.md,
54
+ lineHeight: typography.lineHeight.md,
55
+ fontWeight: typography.fontWeight.regular,
56
+ };
57
+ }
58
+ }
59
+
60
+ export function Text({
61
+ variant = 'body',
62
+ align,
63
+ color,
64
+ style,
65
+ ...props
66
+ }: TextProps) {
67
+ const theme = useTheme();
68
+ const base = getVariantStyle(variant, theme);
69
+
70
+ return (
71
+ <RNText
72
+ {...props}
73
+ style={[
74
+ base,
75
+ align ? { textAlign: align } : null,
76
+ color ? { color } : null,
77
+ style,
78
+ ]}
79
+ />
80
+ );
81
+ }
82
+
83
+
@@ -0,0 +1,35 @@
1
+ export { Text } from './Text';
2
+ export type { TextProps } from './Text';
3
+
4
+ export { Button } from './Button';
5
+ export type { ButtonProps } from './Button';
6
+
7
+ export { Surface } from './Surface';
8
+ export type { SurfaceProps } from './Surface';
9
+
10
+ export { Card } from './Card';
11
+ export type { CardProps } from './Card';
12
+
13
+ export { Avatar } from './Avatar';
14
+ export type { AvatarProps } from './Avatar';
15
+
16
+ export { Divider } from './Divider';
17
+ export type { DividerProps } from './Divider';
18
+
19
+ export { Icon } from './Icon';
20
+ export type { IconProps } from './Icon';
21
+
22
+ export { Modal } from './Modal';
23
+ export type { ModalProps } from './Modal';
24
+
25
+ export type {
26
+ ButtonSize,
27
+ ButtonVariant,
28
+ CardVariant,
29
+ DividerVariant,
30
+ IconColorRole,
31
+ SurfaceVariant,
32
+ TextVariant,
33
+ } from './types';
34
+
35
+