rn-vs-lb 1.0.61 → 1.0.63

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.
@@ -22,6 +22,7 @@ const meta: Meta<Props> = {
22
22
  argTypes: {
23
23
  onAttachPress: { control: false },
24
24
  onMaxImagesExceeded: { control: false },
25
+ enableImageAttachment: { control: 'boolean' },
25
26
  },
26
27
  };
27
28
  export default meta;
@@ -89,6 +90,12 @@ Default.args = {
89
90
  maxImages: 1,
90
91
  };
91
92
 
93
+ export const AttachmentsDisabled = Template.bind({});
94
+ AttachmentsDisabled.args = {
95
+ placeholder: 'No attachments allowed',
96
+ enableImageAttachment: false,
97
+ };
98
+
92
99
  export const WithAttachments = Template.bind({});
93
100
  WithAttachments.args = {
94
101
  placeholder: 'Attach up to 2 images',
@@ -1,4 +1,4 @@
1
- import React, { FC, useCallback, useRef, useState } from 'react';
1
+ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { View, StyleSheet, TextInput, TouchableOpacity, Text, Image, ScrollView, ActivityIndicator } from 'react-native';
3
3
  import { Ionicons } from '@expo/vector-icons';
4
4
  import { ThemeType, useTheme } from '../../theme';
@@ -22,6 +22,7 @@ interface InputMessageProps {
22
22
  maxImages?: number; // по умолчанию 1
23
23
  onAttachPress?: () => Promise<ImageAsset[] | void>; // родитель сам открывает пикер и вернёт выбранные
24
24
  onMaxImagesExceeded?: (max: number) => void;
25
+ enableImageAttachment?: boolean;
25
26
 
26
27
  // Индикатор отправки (можно не передавать — тогда управляем внутри)
27
28
  sendingControlled?: boolean;
@@ -47,6 +48,7 @@ export const InputMessage: FC<InputMessageProps> = ({
47
48
  maxImages = 1,
48
49
  onAttachPress,
49
50
  onMaxImagesExceeded,
51
+ enableImageAttachment = true,
50
52
 
51
53
  sendingControlled,
52
54
  isSending: isSendingProp,
@@ -64,6 +66,13 @@ export const InputMessage: FC<InputMessageProps> = ({
64
66
  const [isSendingLocal, setIsSendingLocal] = useState(false);
65
67
 
66
68
  const isSending = sendingControlled ? !!isSendingProp : isSendingLocal;
69
+ const attachmentsAllowed = enableImageAttachment;
70
+
71
+ useEffect(() => {
72
+ if (!attachmentsAllowed && images.length > 0) {
73
+ setImages([]);
74
+ }
75
+ }, [attachmentsAllowed, images.length]);
67
76
 
68
77
  // Собственный дебаунс без lodash
69
78
  const stopTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -74,7 +83,7 @@ export const InputMessage: FC<InputMessageProps> = ({
74
83
  }, [onTyping, onStopTyping]);
75
84
 
76
85
  const handleAttachPress = useCallback(async () => {
77
- if (!onAttachPress) return;
86
+ if (!attachmentsAllowed || !onAttachPress) return;
78
87
  const selected = (await onAttachPress()) || [];
79
88
  if (!selected.length) return;
80
89
 
@@ -165,7 +174,12 @@ export const InputMessage: FC<InputMessageProps> = ({
165
174
  )}
166
175
 
167
176
  {/* Поле ввода + кнопки */}
168
- <View style={styles.inputContainer}>
177
+ <View
178
+ style={[
179
+ styles.inputContainer,
180
+ attachmentsAllowed ? styles.inputContainerWithAttach : styles.inputContainerWithoutAttach,
181
+ ]}
182
+ >
169
183
  <TextInput
170
184
  multiline
171
185
  placeholder={placeholder}
@@ -183,9 +197,11 @@ export const InputMessage: FC<InputMessageProps> = ({
183
197
  onContentSizeChange={(e) => setInputHeight(e.nativeEvent.contentSize.height)}
184
198
  />
185
199
 
186
- <TouchableOpacity style={styles.attachButton} onPress={handleAttachPress}>
187
- <Ionicons name="image-outline" size={22} color={theme.primary} />
188
- </TouchableOpacity>
200
+ {attachmentsAllowed && (
201
+ <TouchableOpacity style={styles.attachButton} onPress={handleAttachPress}>
202
+ <Ionicons name="image-outline" size={22} color={theme.primary} />
203
+ </TouchableOpacity>
204
+ )}
189
205
 
190
206
  <TouchableOpacity style={styles.sendButton} onPress={handleSubmit} disabled={isSending}>
191
207
  {isSending ? (
@@ -208,8 +224,13 @@ export const getStyles = (theme: ThemeType) =>
208
224
  borderTopWidth: 1,
209
225
  borderTopColor: theme.border,
210
226
  paddingRight: 45,
227
+ },
228
+ inputContainerWithAttach: {
211
229
  paddingLeft: 45,
212
230
  },
231
+ inputContainerWithoutAttach: {
232
+ paddingLeft: 12,
233
+ },
213
234
  replyContainer: {
214
235
  backgroundColor: theme.background,
215
236
  padding: 6,
@@ -0,0 +1,138 @@
1
+ import React, { memo, useMemo } from "react";
2
+ import { ActivityIndicator, Image, Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { useTheme, ThemeType, SizesType, TypographytType } from "../../theme";
4
+ import { GalleryModal } from "../Modals";
5
+
6
+ export type AiAgentGalleryViewProps = {
7
+ isLoading: boolean;
8
+ photos: string[];
9
+ galleryColumns: number;
10
+ galleryItemSize: number;
11
+
12
+ visible: boolean;
13
+ initialIndex: number;
14
+ onOpenAt: (index: number) => void;
15
+ onClose: () => void;
16
+
17
+ emptyTest: string;
18
+ };
19
+
20
+ export const AiAgentGalleryView = memo(
21
+ ({
22
+ isLoading,
23
+ photos,
24
+ galleryColumns,
25
+ galleryItemSize,
26
+ visible,
27
+ initialIndex,
28
+ onOpenAt,
29
+ onClose,
30
+ emptyTest,
31
+ }: AiAgentGalleryViewProps) => {
32
+ const { theme, sizes, typography } = useTheme();
33
+ const isDark = (theme as any)?.isDark ?? false;
34
+
35
+ const styles = useMemo(() => getStyles(theme, sizes, typography, isDark), [
36
+ theme,
37
+ sizes,
38
+ typography,
39
+ isDark,
40
+ ]);
41
+
42
+ if (isLoading) {
43
+ return (
44
+ <View style={styles.galleryWrapper}>
45
+ <ActivityIndicator />
46
+ </View>
47
+ );
48
+ }
49
+
50
+ if (!photos.length) {
51
+ return (
52
+ <View style={styles.galleryWrapper}>
53
+ <View style={styles.emptyState}>
54
+ <Text style={styles.emptyText}>{emptyTest}</Text>
55
+ </View>
56
+ </View>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <View style={styles.galleryWrapper}>
62
+ <View style={styles.galleryGrid}>
63
+ {photos.map((photo, i) => {
64
+ const isLastInRow = (i + 1) % galleryColumns === 0;
65
+ return (
66
+ <Pressable
67
+ key={`${photo}-${i}`}
68
+ onPress={() => onOpenAt(i)}
69
+ android_ripple={{ color: "#00000022" }}
70
+ style={[
71
+ styles.galleryImage,
72
+ isLastInRow && styles.galleryImageLast,
73
+ { width: galleryItemSize, height: galleryItemSize },
74
+ ]}
75
+ >
76
+ <Image
77
+ source={{ uri: photo }}
78
+ style={{
79
+ width: "100%",
80
+ height: "100%",
81
+ borderRadius: styles.galleryImage.borderRadius,
82
+ }}
83
+ />
84
+ </Pressable>
85
+ );
86
+ })}
87
+ </View>
88
+
89
+ <GalleryModal
90
+ visible={visible}
91
+ images={photos}
92
+ initialIndex={initialIndex}
93
+ onRequestClose={onClose}
94
+ />
95
+ </View>
96
+ );
97
+ }
98
+ );
99
+
100
+ AiAgentGalleryView.displayName = "AiAgentGalleryView";
101
+
102
+ const getStyles = (
103
+ theme: ThemeType,
104
+ sizes: SizesType,
105
+ typography: TypographytType,
106
+ isDark: boolean
107
+ ) =>
108
+ StyleSheet.create({
109
+ galleryWrapper: {
110
+ paddingHorizontal: sizes.md as number,
111
+ paddingVertical: sizes.xl as number,
112
+ },
113
+ galleryGrid: {
114
+ flexDirection: "row",
115
+ flexWrap: "wrap",
116
+ },
117
+ galleryImage: {
118
+ borderRadius: 18,
119
+ backgroundColor: theme.backgroundSecond,
120
+ marginRight: sizes.sm as number,
121
+ marginBottom: sizes.sm as number,
122
+ },
123
+ galleryImageLast: {
124
+ marginRight: 0,
125
+ },
126
+ emptyState: {
127
+ alignItems: "center",
128
+ justifyContent: "center",
129
+ paddingVertical: sizes.lg as number,
130
+ },
131
+ emptyText: {
132
+ ...(typography.bodySm as object),
133
+ color: theme.greyText,
134
+ textAlign: "center",
135
+ },
136
+ });
137
+
138
+ export default AiAgentGalleryView;
@@ -0,0 +1,75 @@
1
+ import React, { useState } from 'react';
2
+ import { Meta, StoryFn } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { ThemeProvider } from '../../theme';
5
+ import { AiAgentGalleryView } from './AiAgentGallery.pure';
6
+
7
+ type Props = React.ComponentProps<typeof AiAgentGalleryView>;
8
+
9
+ const meta: Meta<Props> = {
10
+ title: 'Gallery/AiAgentGallery',
11
+ component: AiAgentGalleryView,
12
+ decorators: [
13
+ (Story) => (
14
+ <ThemeProvider>
15
+ <View style={{ paddingVertical: 24 }}>
16
+ <Story />
17
+ </View>
18
+ </ThemeProvider>
19
+ ),
20
+ ],
21
+ args: {
22
+ galleryColumns: 3,
23
+ galleryItemSize: 108,
24
+ emptyTest: 'No generated images yet. Try creating one with your AI agent!',
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+
30
+ const Template: StoryFn<Props> = ({
31
+ onOpenAt: _onOpenAt,
32
+ onClose: _onClose,
33
+ visible: _visible,
34
+ initialIndex: _initialIndex,
35
+ ...rest
36
+ }) => {
37
+ const [visible, setVisible] = useState(false);
38
+ const [initialIndex, setInitialIndex] = useState(0);
39
+
40
+ return (
41
+ <AiAgentGalleryView
42
+ {...rest}
43
+ visible={visible}
44
+ initialIndex={initialIndex}
45
+ onOpenAt={(index) => {
46
+ setInitialIndex(index);
47
+ setVisible(true);
48
+ }}
49
+ onClose={() => setVisible(false)}
50
+ />
51
+ );
52
+ };
53
+
54
+ export const Default = Template.bind({});
55
+ Default.args = {
56
+ isLoading: false,
57
+ photos: [
58
+ 'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=600&q=60',
59
+ 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=600&q=60',
60
+ 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?auto=format&fit=crop&w=600&q=60',
61
+ 'https://images.unsplash.com/photo-1545239351-1141bd82e8a6?auto=format&fit=crop&w=600&q=60',
62
+ ],
63
+ };
64
+
65
+ export const Loading = Template.bind({});
66
+ Loading.args = {
67
+ isLoading: true,
68
+ photos: [],
69
+ };
70
+
71
+ export const Empty = Template.bind({});
72
+ Empty.args = {
73
+ isLoading: false,
74
+ photos: [],
75
+ };
@@ -0,0 +1 @@
1
+ export * from "./AiAgentGallery.pure"
@@ -0,0 +1,61 @@
1
+ import React, { useState } from 'react';
2
+ import { Meta, StoryFn } from '@storybook/react';
3
+ import { View, Alert } from 'react-native';
4
+ import { ThemeProvider } from '../../theme';
5
+ import { StepProgress } from './StepProgress';
6
+
7
+ type Props = React.ComponentProps<typeof StepProgress>;
8
+
9
+ const meta: Meta<Props> = {
10
+ title: 'UI/StepProgress',
11
+ component: StepProgress,
12
+ decorators: [
13
+ (Story) => (
14
+ <ThemeProvider>
15
+ <View style={{ padding: 24 }}>
16
+ <Story />
17
+ </View>
18
+ </ThemeProvider>
19
+ ),
20
+ ],
21
+ args: {
22
+ steps: [
23
+ { title: 'Briefing', description: 'Share context and goals for your agent.' },
24
+ { title: 'Generation', description: 'The agent drafts a tailored solution.' },
25
+ { title: 'Review', description: 'Assess the output and give feedback.' },
26
+ { title: 'Launch', description: 'Publish or deploy your agent deliverable.' },
27
+ ],
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+
33
+ const Template: StoryFn<Props> = (args) => <StepProgress {...args} />;
34
+
35
+ export const Default = Template.bind({});
36
+ Default.args = {
37
+ activeStep: 1,
38
+ };
39
+
40
+ export const FirstStep = Template.bind({});
41
+ FirstStep.args = {
42
+ activeStep: 0,
43
+ };
44
+
45
+ export const WithClickableSteps: StoryFn<Props> = (args) => {
46
+ const [activeStep, setActiveStep] = useState(args.activeStep ?? 2);
47
+
48
+ return (
49
+ <StepProgress
50
+ {...args}
51
+ activeStep={activeStep}
52
+ onStepPress={(index) => {
53
+ Alert.alert('Step selected', args.steps?.[index]?.title ?? `Step ${index + 1}`);
54
+ setActiveStep(index);
55
+ }}
56
+ />
57
+ );
58
+ };
59
+ WithClickableSteps.args = {
60
+ activeStep: 2,
61
+ };
@@ -0,0 +1,153 @@
1
+ import React, { useMemo } from "react";
2
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
3
+ import { ThemeType, TypographytType, SizesType, useTheme } from "rn-vs-lb/theme";
4
+
5
+ interface StepProgressProps {
6
+ steps: { title: string; description?: string }[];
7
+ activeStep: number;
8
+ onStepPress?: (index: number) => void;
9
+ }
10
+
11
+ export const StepProgress: React.FC<StepProgressProps> = ({ steps, activeStep, onStepPress }) => {
12
+ const { theme, typography, sizes } = useTheme();
13
+
14
+ const styles = useMemo(
15
+ () => createStyles({ theme, typography, sizes }),
16
+ [theme, typography, sizes],
17
+ );
18
+
19
+ const currentDescription = steps[activeStep]?.description ?? "";
20
+
21
+ return (
22
+ <View style={styles.container}>
23
+ <View style={styles.stepsRow}>
24
+ {steps.map((step, index) => {
25
+ const isActive = index === activeStep;
26
+ const isCompleted = index < activeStep;
27
+ const isPressable = typeof onStepPress === "function" && index < activeStep;
28
+ return (
29
+ <TouchableOpacity
30
+ key={step.title ?? index}
31
+ style={[styles.stepItem, isPressable && styles.stepItemPressable]}
32
+ activeOpacity={0.7}
33
+ onPress={isPressable ? () => onStepPress(index) : undefined}
34
+ disabled={!isPressable}
35
+ >
36
+ <View style={styles.stepHeader}>
37
+ <View
38
+ style={[
39
+ styles.circle,
40
+ (isActive || isCompleted) && styles.circleActive,
41
+ ]}
42
+ >
43
+ <Text
44
+ style={[
45
+ styles.circleText,
46
+ (isActive || isCompleted) && styles.circleTextActive,
47
+ ]}
48
+ >
49
+ {index + 1}
50
+ </Text>
51
+ </View>
52
+ {index < steps.length - 1 ? (
53
+ <View
54
+ style={[
55
+ styles.connector,
56
+ (isCompleted || (isActive && activeStep === index)) && styles.connectorActive,
57
+ ]}
58
+ />
59
+ ) : null}
60
+ </View>
61
+ <Text
62
+ numberOfLines={2}
63
+ style={[styles.stepTitle, isActive && styles.stepTitleActive]}
64
+ >
65
+ {step.title}
66
+ </Text>
67
+ </TouchableOpacity>
68
+ );
69
+ })}
70
+ </View>
71
+ {currentDescription ? (
72
+ <Text style={styles.description}>{currentDescription}</Text>
73
+ ) : null}
74
+ </View>
75
+ );
76
+ };
77
+
78
+ const createStyles = ({
79
+ theme,
80
+ typography,
81
+ sizes,
82
+ }: {
83
+ theme: ThemeType;
84
+ typography: TypographytType;
85
+ sizes: SizesType;
86
+ }) =>
87
+ StyleSheet.create({
88
+ container: {
89
+ marginBottom: sizes.xl as number,
90
+ },
91
+ stepsRow: {
92
+ flexDirection: "row",
93
+ alignItems: "center",
94
+ justifyContent: "space-between",
95
+ },
96
+ stepItem: {
97
+ flex: 1,
98
+ marginRight: sizes.sm as number,
99
+ },
100
+ stepItemPressable: {
101
+ opacity: 0.9,
102
+ },
103
+ stepHeader: {
104
+ flexDirection: "row",
105
+ alignItems: "center",
106
+ marginBottom: sizes.xs as number,
107
+ },
108
+ circle: {
109
+ width: 36,
110
+ height: 36,
111
+ borderRadius: 18,
112
+ borderWidth: 2,
113
+ borderColor: theme.border,
114
+ alignItems: "center",
115
+ justifyContent: "center",
116
+ backgroundColor: theme.white,
117
+ },
118
+ circleActive: {
119
+ borderColor: theme.primary,
120
+ backgroundColor: theme.primary,
121
+ },
122
+ circleText: {
123
+ ...typography.bodyXs,
124
+ color: theme.text,
125
+ fontWeight: "600",
126
+ },
127
+ circleTextActive: {
128
+ color: theme.white,
129
+ },
130
+ connector: {
131
+ flex: 1,
132
+ height: 2,
133
+ marginLeft: sizes.xs as number,
134
+ backgroundColor: theme.border,
135
+ },
136
+ connectorActive: {
137
+ backgroundColor: theme.primary,
138
+ },
139
+ stepTitle: {
140
+ ...typography.bodyXs,
141
+ color: theme.greyText,
142
+ },
143
+ stepTitleActive: {
144
+ color: theme.text,
145
+ fontWeight: "600",
146
+ },
147
+ description: {
148
+ marginTop: sizes.sm as number,
149
+ ...typography.bodySm,
150
+ color: theme.greyText,
151
+ },
152
+ });
153
+
@@ -28,3 +28,4 @@ export { ThreeDotsMenu } from './ThreeDotsMenu';
28
28
  export { default as TripleSwitch } from './TripleSwitch';
29
29
  export type { TripleSwitchValue } from './TripleSwitch';
30
30
  export { UpdateRequiredView } from './UpdateRequiredView';
31
+ export * from "./StepProgress";
@@ -10,3 +10,4 @@ export * from './Specialist';
10
10
  export * from './Tooltip';
11
11
  export * from './UI';
12
12
  export * from './UserCards';
13
+ export * from './Gallery';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-vs-lb",
3
- "version": "1.0.61",
3
+ "version": "1.0.63",
4
4
  "description": "Expo Router + Storybook template ready for npm distribution.",
5
5
  "keywords": [
6
6
  "expo",