finsignal-feed-explore 1.0.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.
- package/LICENSE +22 -0
- package/README.md +107 -0
- package/dist/NewsFeed.d.ts +49 -0
- package/dist/NewsFeed.js +229 -0
- package/dist/SegmentControl.d.ts +13 -0
- package/dist/SegmentControl.js +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/newsfeed.css +205 -0
- package/dist/snippets/NewsSnippet.d.ts +31 -0
- package/dist/snippets/NewsSnippet.js +121 -0
- package/dist/styles.css +33 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 FinSignal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# FinSignal UI Components
|
|
2
|
+
|
|
3
|
+
Набор UI компонентов для React веб-приложений: segment control и новостной фид.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install finsignal-segment-control
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Компоненты
|
|
12
|
+
|
|
13
|
+
### SegmentControl
|
|
14
|
+
|
|
15
|
+
Переключатель вкладок с плавной анимацией.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { SegmentControl } from 'finsignal-segment-control';
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
const [selectedValue, setSelectedValue] = React.useState('option1');
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<SegmentControl
|
|
25
|
+
options={[
|
|
26
|
+
{ value: 'option1', label: 'Опция 1' },
|
|
27
|
+
{ value: 'option2', label: 'Опция 2' },
|
|
28
|
+
{ value: 'option3', label: 'Опция 3' },
|
|
29
|
+
]}
|
|
30
|
+
value={selectedValue}
|
|
31
|
+
onValueChange={setSelectedValue}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### NewsFeed
|
|
38
|
+
|
|
39
|
+
Компонент новостной ленты с красивыми снипеттами.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { NewsFeed } from 'finsignal-segment-control';
|
|
43
|
+
|
|
44
|
+
function App() {
|
|
45
|
+
return (
|
|
46
|
+
<NewsFeed
|
|
47
|
+
onItemClick={(item) => console.log('Clicked:', item)}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
С кастомными новостями:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const customNews = [
|
|
57
|
+
{
|
|
58
|
+
id: '1',
|
|
59
|
+
title: 'Заголовок новости',
|
|
60
|
+
snippet: 'Краткое описание новости...',
|
|
61
|
+
source: 'Источник',
|
|
62
|
+
time: '5 мин назад',
|
|
63
|
+
category: 'Категория'
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
<NewsFeed
|
|
68
|
+
items={customNews}
|
|
69
|
+
onItemClick={(item) => console.log(item)}
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
### SegmentControlProps
|
|
76
|
+
|
|
77
|
+
| Prop | Тип | Обязательно | Описание |
|
|
78
|
+
|------|-----|-------------|----------|
|
|
79
|
+
| `options` | `SegmentOption[]` | Да | Массив опций для отображения |
|
|
80
|
+
| `value` | `string` | Да | Текущее выбранное значение |
|
|
81
|
+
| `onValueChange` | `(value: string) => void` | Да | Callback при изменении значения |
|
|
82
|
+
| `className` | `string` | Нет | Дополнительные CSS классы |
|
|
83
|
+
|
|
84
|
+
### SegmentOption
|
|
85
|
+
|
|
86
|
+
| Свойство | Тип | Описание |
|
|
87
|
+
|----------|-----|----------|
|
|
88
|
+
| `value` | `string` | Уникальное значение опции |
|
|
89
|
+
| `label` | `string` | Отображаемый текст |
|
|
90
|
+
|
|
91
|
+
## Стилизация
|
|
92
|
+
|
|
93
|
+
Компонент использует CSS классы:
|
|
94
|
+
- `.segment-control` - контейнер
|
|
95
|
+
- `.segment-option` - кнопка опции
|
|
96
|
+
- `.segment-option.active` - активная опция
|
|
97
|
+
|
|
98
|
+
Можно переопределить стили или добавить свой className.
|
|
99
|
+
|
|
100
|
+
## Требования
|
|
101
|
+
|
|
102
|
+
- React >= 18.0.0
|
|
103
|
+
|
|
104
|
+
## Лицензия
|
|
105
|
+
|
|
106
|
+
MIT
|
|
107
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SegmentOption } from './SegmentControl';
|
|
2
|
+
import './newsfeed.css';
|
|
3
|
+
export interface FeedItem {
|
|
4
|
+
content_id: string;
|
|
5
|
+
title?: string | null;
|
|
6
|
+
body?: string | null;
|
|
7
|
+
type: 'post' | 'signal' | 'news' | 'strategies' | 'trigger' | 'market_vibe' | 'guides';
|
|
8
|
+
source: 'user' | 'provider' | 'api';
|
|
9
|
+
created_at: string;
|
|
10
|
+
score: number;
|
|
11
|
+
metadata: {
|
|
12
|
+
news?: {
|
|
13
|
+
recommendation?: {
|
|
14
|
+
text: string;
|
|
15
|
+
priceRange: string;
|
|
16
|
+
};
|
|
17
|
+
stocks?: Array<{
|
|
18
|
+
symbol: string;
|
|
19
|
+
price: string;
|
|
20
|
+
change: string;
|
|
21
|
+
changeType: 'positive' | 'negative';
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
signal?: {
|
|
25
|
+
symbol: string;
|
|
26
|
+
companyName: string;
|
|
27
|
+
metrics: {
|
|
28
|
+
buy: string;
|
|
29
|
+
entry: string;
|
|
30
|
+
takeProfit: string;
|
|
31
|
+
stopLoss: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
ai_categorization?: {
|
|
35
|
+
primaryCategory: string;
|
|
36
|
+
tags: string[];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface FeedListProps {
|
|
41
|
+
feedType?: 'trending' | 'personal' | 'hot';
|
|
42
|
+
items?: FeedItem[];
|
|
43
|
+
onItemClick?: (item: FeedItem) => void;
|
|
44
|
+
className?: string;
|
|
45
|
+
segments?: SegmentOption[];
|
|
46
|
+
showSegmentControl?: boolean;
|
|
47
|
+
onSegmentChange?: (value: string) => void;
|
|
48
|
+
}
|
|
49
|
+
export declare function FeedList({ feedType, items, onItemClick, className, segments, showSegmentControl, onSegmentChange }: FeedListProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/NewsFeed.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { NewsSnippet } from './snippets/NewsSnippet';
|
|
4
|
+
import { SegmentControl } from './SegmentControl';
|
|
5
|
+
import './newsfeed.css';
|
|
6
|
+
// Моковые данные в формате FinSignal Feed
|
|
7
|
+
const mockFeedItems = [
|
|
8
|
+
{
|
|
9
|
+
content_id: '1',
|
|
10
|
+
title: 'Tesla планирует открыть новый завод в Европе',
|
|
11
|
+
body: 'Компания Tesla Motors объявила о планах строительства нового завода по производству электромобилей в Берлине. Ожидается создание более 10,000 рабочих мест.',
|
|
12
|
+
type: 'news',
|
|
13
|
+
source: 'provider',
|
|
14
|
+
created_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
|
15
|
+
score: 95,
|
|
16
|
+
metadata: {
|
|
17
|
+
news: {
|
|
18
|
+
recommendation: {
|
|
19
|
+
text: 'A breakout is expected at the resistance level of',
|
|
20
|
+
priceRange: '$250-$260'
|
|
21
|
+
},
|
|
22
|
+
stocks: [
|
|
23
|
+
{
|
|
24
|
+
symbol: 'TSLA',
|
|
25
|
+
price: '$242.50',
|
|
26
|
+
change: '+5.2%',
|
|
27
|
+
changeType: 'positive'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
ai_categorization: {
|
|
32
|
+
primaryCategory: 'Automotive',
|
|
33
|
+
tags: ['Tesla', 'Electric Vehicles', 'Europe']
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
content_id: '2',
|
|
39
|
+
title: 'NVIDIA представила новую линейку графических процессоров',
|
|
40
|
+
body: 'Новая серия RTX 5000 обещает на 40% больше производительности в задачах искусственного интеллекта и машинного обучения.',
|
|
41
|
+
type: 'news',
|
|
42
|
+
source: 'api',
|
|
43
|
+
created_at: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
|
|
44
|
+
score: 92,
|
|
45
|
+
metadata: {
|
|
46
|
+
news: {
|
|
47
|
+
stocks: [
|
|
48
|
+
{
|
|
49
|
+
symbol: 'NVDA',
|
|
50
|
+
price: '$495.80',
|
|
51
|
+
change: '+12.3%',
|
|
52
|
+
changeType: 'positive'
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
ai_categorization: {
|
|
57
|
+
primaryCategory: 'Technology',
|
|
58
|
+
tags: ['NVIDIA', 'AI', 'Hardware']
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
content_id: '3',
|
|
64
|
+
title: 'Microsoft расширяет сотрудничество с OpenAI',
|
|
65
|
+
body: 'Microsoft объявила о дополнительных $10 млрд инвестиций в OpenAI для развития технологий искусственного интеллекта и интеграции GPT-4 в продукты.',
|
|
66
|
+
type: 'news',
|
|
67
|
+
source: 'provider',
|
|
68
|
+
created_at: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
|
|
69
|
+
score: 90,
|
|
70
|
+
metadata: {
|
|
71
|
+
news: {
|
|
72
|
+
recommendation: {
|
|
73
|
+
text: 'A breakout is expected at the resistance level of',
|
|
74
|
+
priceRange: '$385-$395'
|
|
75
|
+
},
|
|
76
|
+
stocks: [
|
|
77
|
+
{
|
|
78
|
+
symbol: 'MSFT',
|
|
79
|
+
price: '$378.90',
|
|
80
|
+
change: '+3.5%',
|
|
81
|
+
changeType: 'positive'
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
ai_categorization: {
|
|
86
|
+
primaryCategory: 'Technology',
|
|
87
|
+
tags: ['Microsoft', 'OpenAI', 'AI']
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
content_id: '4',
|
|
93
|
+
title: 'Amazon планирует IPO своего подразделения AWS',
|
|
94
|
+
body: 'По данным инсайдеров, Amazon рассматривает возможность выделения Amazon Web Services в отдельную публичную компанию.',
|
|
95
|
+
type: 'news',
|
|
96
|
+
source: 'api',
|
|
97
|
+
created_at: new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString(),
|
|
98
|
+
score: 88,
|
|
99
|
+
metadata: {
|
|
100
|
+
news: {
|
|
101
|
+
stocks: [
|
|
102
|
+
{
|
|
103
|
+
symbol: 'AMZN',
|
|
104
|
+
price: '$142.60',
|
|
105
|
+
change: '-1.2%',
|
|
106
|
+
changeType: 'negative'
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
ai_categorization: {
|
|
111
|
+
primaryCategory: 'Cloud Computing',
|
|
112
|
+
tags: ['Amazon', 'AWS', 'IPO']
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
content_id: '5',
|
|
118
|
+
title: 'Meta показала рекордный рост пользователей',
|
|
119
|
+
body: 'Количество активных пользователей социальных сетей Meta выросло на 12% за квартал, достигнув нового исторического максимума.',
|
|
120
|
+
type: 'news',
|
|
121
|
+
source: 'provider',
|
|
122
|
+
created_at: new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString(),
|
|
123
|
+
score: 87,
|
|
124
|
+
metadata: {
|
|
125
|
+
news: {
|
|
126
|
+
stocks: [
|
|
127
|
+
{
|
|
128
|
+
symbol: 'META',
|
|
129
|
+
price: '$312.40',
|
|
130
|
+
change: '+8.7%',
|
|
131
|
+
changeType: 'positive'
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
ai_categorization: {
|
|
136
|
+
primaryCategory: 'Social Media',
|
|
137
|
+
tags: ['Meta', 'Facebook', 'Growth']
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
content_id: '6',
|
|
143
|
+
title: 'Apple выпустит новую линейку MacBook Pro',
|
|
144
|
+
body: 'Apple готовится представить обновленные MacBook Pro с новыми процессорами M3 и улучшенными дисплеями ProMotion.',
|
|
145
|
+
type: 'news',
|
|
146
|
+
source: 'api',
|
|
147
|
+
created_at: new Date(Date.now() - 18 * 60 * 60 * 1000).toISOString(),
|
|
148
|
+
score: 85,
|
|
149
|
+
metadata: {
|
|
150
|
+
news: {
|
|
151
|
+
recommendation: {
|
|
152
|
+
text: 'A breakout is expected at the resistance level of',
|
|
153
|
+
priceRange: '$185-$190'
|
|
154
|
+
},
|
|
155
|
+
stocks: [
|
|
156
|
+
{
|
|
157
|
+
symbol: 'AAPL',
|
|
158
|
+
price: '$178.25',
|
|
159
|
+
change: '+2.1%',
|
|
160
|
+
changeType: 'positive'
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
ai_categorization: {
|
|
165
|
+
primaryCategory: 'Technology',
|
|
166
|
+
tags: ['Apple', 'MacBook', 'Hardware']
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
function formatTimeAgo(dateString) {
|
|
172
|
+
const now = new Date();
|
|
173
|
+
const date = new Date(dateString);
|
|
174
|
+
const diff = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
175
|
+
if (diff < 60)
|
|
176
|
+
return 'только что';
|
|
177
|
+
if (diff < 3600)
|
|
178
|
+
return `${Math.floor(diff / 60)} мин назад`;
|
|
179
|
+
if (diff < 86400)
|
|
180
|
+
return `${Math.floor(diff / 3600)} ч назад`;
|
|
181
|
+
return `${Math.floor(diff / 86400)} дн назад`;
|
|
182
|
+
}
|
|
183
|
+
export function FeedList({ feedType = 'trending', items = mockFeedItems, onItemClick, className = '', segments = [
|
|
184
|
+
{ value: 'all', label: 'Все новости' },
|
|
185
|
+
{ value: 'markets', label: 'Рынки' },
|
|
186
|
+
{ value: 'crypto', label: 'Крипто' }
|
|
187
|
+
], showSegmentControl = true, onSegmentChange }) {
|
|
188
|
+
var _a;
|
|
189
|
+
const [selectedSegment, setSelectedSegment] = React.useState(((_a = segments[0]) === null || _a === void 0 ? void 0 : _a.value) || 'all');
|
|
190
|
+
const handleSegmentChange = (value) => {
|
|
191
|
+
setSelectedSegment(value);
|
|
192
|
+
onSegmentChange === null || onSegmentChange === void 0 ? void 0 : onSegmentChange(value);
|
|
193
|
+
};
|
|
194
|
+
const getTypeLabel = (type) => {
|
|
195
|
+
const labels = {
|
|
196
|
+
signal: 'Сигнал',
|
|
197
|
+
news: 'Новости',
|
|
198
|
+
trigger: 'Триггер',
|
|
199
|
+
market_vibe: 'Настроение',
|
|
200
|
+
strategies: 'Стратегия',
|
|
201
|
+
guides: 'Гайд',
|
|
202
|
+
post: 'Пост'
|
|
203
|
+
};
|
|
204
|
+
return labels[type] || type;
|
|
205
|
+
};
|
|
206
|
+
const getTypeColor = (type) => {
|
|
207
|
+
const colors = {
|
|
208
|
+
signal: '#10b981',
|
|
209
|
+
news: '#3b82f6',
|
|
210
|
+
trigger: '#f59e0b',
|
|
211
|
+
market_vibe: '#8b5cf6',
|
|
212
|
+
strategies: '#ec4899',
|
|
213
|
+
guides: '#06b6d4',
|
|
214
|
+
post: '#6b7280'
|
|
215
|
+
};
|
|
216
|
+
return colors[type] || colors.post;
|
|
217
|
+
};
|
|
218
|
+
return (_jsxs(_Fragment, { children: [showSegmentControl && (_jsx("div", { style: { marginBottom: '16px' }, children: _jsx(SegmentControl, { options: segments, value: selectedSegment, onValueChange: handleSegmentChange }) })), _jsx("div", { className: `feed-list ${className}`, children: items.map((item) => {
|
|
219
|
+
var _a, _b, _c, _d;
|
|
220
|
+
// Рендерим news items через NewsSnippet
|
|
221
|
+
if (item.type === 'news' && item.title && item.body) {
|
|
222
|
+
return (_jsx(NewsSnippet, { id: item.content_id, title: item.title, content: item.body, recommendation: (_a = item.metadata.news) === null || _a === void 0 ? void 0 : _a.recommendation, stocks: (_b = item.metadata.news) === null || _b === void 0 ? void 0 : _b.stocks, onPress: () => onItemClick === null || onItemClick === void 0 ? void 0 : onItemClick(item), draggable: true, onDragStart: () => {
|
|
223
|
+
console.log('Dragging news:', item.title);
|
|
224
|
+
} }, item.content_id));
|
|
225
|
+
}
|
|
226
|
+
// Для остальных типов оставляем обычный рендеринг
|
|
227
|
+
return (_jsxs("div", { className: "feed-item", onClick: () => onItemClick === null || onItemClick === void 0 ? void 0 : onItemClick(item), children: [_jsxs("div", { className: "feed-item-header", children: [_jsx("span", { className: "feed-type-badge", style: { backgroundColor: getTypeColor(item.type) }, children: getTypeLabel(item.type) }), _jsx("span", { className: "feed-time", children: formatTimeAgo(item.created_at) })] }), item.title && _jsx("h3", { className: "feed-title", children: item.title }), item.body && _jsx("p", { className: "feed-body", children: item.body }), item.metadata.signal && (_jsxs("div", { className: "feed-signal-metrics", children: [_jsxs("div", { className: "signal-stock", children: [_jsx("span", { className: "signal-symbol", children: item.metadata.signal.symbol }), _jsx("span", { className: "signal-company", children: item.metadata.signal.companyName })] }), _jsxs("div", { className: "signal-prices", children: [_jsxs("div", { className: "signal-price-item", children: [_jsx("span", { className: "label", children: "Entry:" }), _jsx("span", { className: "value", children: item.metadata.signal.metrics.entry })] }), _jsxs("div", { className: "signal-price-item", children: [_jsx("span", { className: "label", children: "TP:" }), _jsx("span", { className: "value green", children: item.metadata.signal.metrics.takeProfit })] }), _jsxs("div", { className: "signal-price-item", children: [_jsx("span", { className: "label", children: "SL:" }), _jsx("span", { className: "value red", children: item.metadata.signal.metrics.stopLoss })] })] })] })), ((_c = item.metadata.news) === null || _c === void 0 ? void 0 : _c.stocks) && (_jsx("div", { className: "feed-stocks", children: item.metadata.news.stocks.map((stock, idx) => (_jsxs("div", { className: "stock-card", children: [_jsx("span", { className: "stock-symbol", children: stock.symbol }), _jsx("span", { className: "stock-price", children: stock.price }), _jsx("span", { className: `stock-change ${stock.changeType}`, children: stock.change })] }, idx))) })), ((_d = item.metadata.ai_categorization) === null || _d === void 0 ? void 0 : _d.tags) && (_jsx("div", { className: "feed-tags", children: item.metadata.ai_categorization.tags.map((tag, idx) => (_jsxs("span", { className: "feed-tag", children: ["#", tag] }, idx))) }))] }, item.content_id));
|
|
228
|
+
}) })] }));
|
|
229
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import './styles.css';
|
|
2
|
+
interface SegmentOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
interface SegmentControlProps {
|
|
7
|
+
options: SegmentOption[];
|
|
8
|
+
value: string;
|
|
9
|
+
onValueChange: (value: string) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function SegmentControl({ options, value, onValueChange, className }: SegmentControlProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export type { SegmentControlProps, SegmentOption };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import './styles.css';
|
|
3
|
+
export function SegmentControl({ options, value, onValueChange, className = '' }) {
|
|
4
|
+
return (_jsx("div", { className: `segment-control ${className}`, children: options.map((option) => (_jsx("button", { onClick: () => onValueChange(option.value), className: `segment-option ${value === option.value ? 'active' : ''}`, type: "button", children: option.label }, option.value))) }));
|
|
5
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SegmentControl } from './SegmentControl';
|
|
2
|
+
export type { SegmentControlProps, SegmentOption } from './SegmentControl';
|
|
3
|
+
export { FeedList } from './NewsFeed';
|
|
4
|
+
export type { FeedListProps, FeedItem } from './NewsFeed';
|
|
5
|
+
export { NewsSnippet } from './snippets/NewsSnippet';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
.feed-list {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
max-height: 600px;
|
|
6
|
+
overflow-y: auto;
|
|
7
|
+
padding: 4px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Кастомный скроллбар */
|
|
11
|
+
.feed-list::-webkit-scrollbar {
|
|
12
|
+
width: 6px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.feed-list::-webkit-scrollbar-track {
|
|
16
|
+
background: transparent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.feed-list::-webkit-scrollbar-thumb {
|
|
20
|
+
background: #d1d5db;
|
|
21
|
+
border-radius: 3px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.feed-list::-webkit-scrollbar-thumb:hover {
|
|
25
|
+
background: #9ca3af;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Feed Item */
|
|
29
|
+
.feed-item {
|
|
30
|
+
padding: 16px;
|
|
31
|
+
background: #ffffff;
|
|
32
|
+
border-radius: 12px;
|
|
33
|
+
border: 1px solid #e5e7eb;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
transition: all 200ms ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.feed-item:hover {
|
|
39
|
+
border-color: #d1d5db;
|
|
40
|
+
transform: translateY(-1px);
|
|
41
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.feed-item-header {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
margin-bottom: 10px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.feed-type-badge {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
padding: 4px 10px;
|
|
54
|
+
color: white;
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
letter-spacing: 0.5px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.feed-time {
|
|
63
|
+
font-size: 11px;
|
|
64
|
+
color: #9ca3af;
|
|
65
|
+
font-weight: 500;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.feed-title {
|
|
69
|
+
margin: 0 0 8px 0;
|
|
70
|
+
font-size: 15px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
color: #111827;
|
|
73
|
+
line-height: 1.4;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.feed-body {
|
|
77
|
+
margin: 0 0 12px 0;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
color: #4b5563;
|
|
80
|
+
line-height: 1.5;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Signal Metrics */
|
|
84
|
+
.feed-signal-metrics {
|
|
85
|
+
margin-top: 12px;
|
|
86
|
+
padding: 12px;
|
|
87
|
+
background: #f9fafb;
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.signal-stock {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 8px;
|
|
95
|
+
margin-bottom: 8px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.signal-symbol {
|
|
99
|
+
font-size: 15px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
color: #111827;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.signal-company {
|
|
105
|
+
font-size: 12px;
|
|
106
|
+
color: #6b7280;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.signal-prices {
|
|
110
|
+
display: flex;
|
|
111
|
+
gap: 16px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.signal-price-item {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: 2px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.signal-price-item .label {
|
|
121
|
+
font-size: 10px;
|
|
122
|
+
color: #9ca3af;
|
|
123
|
+
text-transform: uppercase;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.signal-price-item .value {
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
color: #111827;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.signal-price-item .value.green {
|
|
134
|
+
color: #10b981;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.signal-price-item .value.red {
|
|
138
|
+
color: #ef4444;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* News Stocks */
|
|
142
|
+
.feed-stocks {
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
margin-top: 12px;
|
|
146
|
+
flex-wrap: wrap;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.stock-card {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
padding: 8px 12px;
|
|
153
|
+
background: #f9fafb;
|
|
154
|
+
border-radius: 8px;
|
|
155
|
+
border: 1px solid #e5e7eb;
|
|
156
|
+
gap: 4px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.stock-symbol {
|
|
160
|
+
font-size: 12px;
|
|
161
|
+
font-weight: 700;
|
|
162
|
+
color: #111827;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.stock-price {
|
|
166
|
+
font-size: 14px;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
color: #111827;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.stock-change {
|
|
172
|
+
font-size: 11px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.stock-change.positive {
|
|
177
|
+
color: #10b981;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.stock-change.negative {
|
|
181
|
+
color: #ef4444;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Tags */
|
|
185
|
+
.feed-tags {
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 6px;
|
|
188
|
+
flex-wrap: wrap;
|
|
189
|
+
margin-top: 12px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.feed-tag {
|
|
193
|
+
font-size: 11px;
|
|
194
|
+
color: #6b7280;
|
|
195
|
+
background: #f3f4f6;
|
|
196
|
+
padding: 4px 8px;
|
|
197
|
+
border-radius: 6px;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.feed-tag:hover {
|
|
202
|
+
background: #e5e7eb;
|
|
203
|
+
color: #374151;
|
|
204
|
+
}
|
|
205
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
interface NewsSnippetProps {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
content: string;
|
|
6
|
+
recommendation?: {
|
|
7
|
+
text: string;
|
|
8
|
+
priceRange: string;
|
|
9
|
+
};
|
|
10
|
+
stocks?: Array<{
|
|
11
|
+
symbol: string;
|
|
12
|
+
price: string;
|
|
13
|
+
change: string;
|
|
14
|
+
changeType: 'positive' | 'negative';
|
|
15
|
+
logo?: string;
|
|
16
|
+
}>;
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
onShare?: () => void;
|
|
19
|
+
onBookmark?: () => void;
|
|
20
|
+
onAI?: () => void;
|
|
21
|
+
onDislike?: () => void;
|
|
22
|
+
onLike?: () => void;
|
|
23
|
+
isLiked?: boolean;
|
|
24
|
+
isDisliked?: boolean;
|
|
25
|
+
isBookmarked?: boolean;
|
|
26
|
+
draggable?: boolean;
|
|
27
|
+
onDragStart?: (e: React.DragEvent) => void;
|
|
28
|
+
onDragEnd?: (e: React.DragEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare const NewsSnippet: React.FC<NewsSnippetProps>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { View, Text, Pressable, Platform, ScrollView } from 'react-native';
|
|
5
|
+
import { Share2, Bookmark, MessageCircle, ThumbsDown, ThumbsUp, Sparkles } from 'lucide-react';
|
|
6
|
+
// Простая замена useThemeStyles для npm пакета
|
|
7
|
+
function useThemeStyles() {
|
|
8
|
+
return {
|
|
9
|
+
colors: {
|
|
10
|
+
card: '#ffffff',
|
|
11
|
+
text: '#111827',
|
|
12
|
+
textSecondary: '#6b7280'
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export const NewsSnippet = ({ id, title, content, recommendation, stocks = [], onPress, onShare, onBookmark, onAI, onDislike, onLike, isLiked = false, isDisliked = false, isBookmarked = false, draggable = true, onDragStart, onDragEnd }) => {
|
|
17
|
+
const { colors } = useThemeStyles();
|
|
18
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
19
|
+
const handleDragStart = (e) => {
|
|
20
|
+
console.log('🚀 NewsSnippet: Drag started', title);
|
|
21
|
+
setIsDragging(true);
|
|
22
|
+
// Сохраняем данные новости для drop
|
|
23
|
+
const newsData = {
|
|
24
|
+
id,
|
|
25
|
+
title,
|
|
26
|
+
content,
|
|
27
|
+
recommendation,
|
|
28
|
+
stocks
|
|
29
|
+
};
|
|
30
|
+
console.log('📦 NewsSnippet: Setting data', newsData);
|
|
31
|
+
e.dataTransfer.setData('application/news-data', JSON.stringify(newsData));
|
|
32
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
33
|
+
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(e);
|
|
34
|
+
};
|
|
35
|
+
const handleDragEnd = (e) => {
|
|
36
|
+
console.log('🏁 NewsSnippet: Drag ended', title);
|
|
37
|
+
setIsDragging(false);
|
|
38
|
+
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(e);
|
|
39
|
+
};
|
|
40
|
+
const containerStyle = Object.assign({ backgroundColor: colors.card, borderRadius: 16, padding: 16, marginBottom: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, opacity: isDragging ? 0.5 : 1, transition: 'opacity 0.2s ease' }, (Platform.OS === 'web' && {
|
|
41
|
+
cursor: isDragging ? 'grabbing' : (draggable ? 'grab' : 'pointer')
|
|
42
|
+
}));
|
|
43
|
+
const stockItemStyle = (changeType) => ({
|
|
44
|
+
backgroundColor: changeType === 'positive' ? '#F0FDF4' : '#FEF2F2',
|
|
45
|
+
paddingHorizontal: 12,
|
|
46
|
+
paddingVertical: 8,
|
|
47
|
+
borderRadius: 12,
|
|
48
|
+
flexDirection: 'row',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
alignSelf: 'flex-start'
|
|
51
|
+
});
|
|
52
|
+
// Для web используем нативный div для поддержки HTML5 drag & drop
|
|
53
|
+
const ContainerComponent = Platform.OS === 'web' ? 'div' : Pressable;
|
|
54
|
+
return (_jsxs(ContainerComponent, Object.assign({}, (Platform.OS === 'web'
|
|
55
|
+
? {
|
|
56
|
+
draggable: draggable,
|
|
57
|
+
onDragStart: handleDragStart,
|
|
58
|
+
onDragEnd: handleDragEnd,
|
|
59
|
+
onClick: onPress,
|
|
60
|
+
style: containerStyle
|
|
61
|
+
}
|
|
62
|
+
: {
|
|
63
|
+
onPress: onPress,
|
|
64
|
+
style: containerStyle
|
|
65
|
+
}), { children: [_jsx(Text, { style: {
|
|
66
|
+
fontSize: 18,
|
|
67
|
+
fontWeight: '700',
|
|
68
|
+
color: colors.text,
|
|
69
|
+
marginBottom: 12,
|
|
70
|
+
lineHeight: 24
|
|
71
|
+
}, children: title }), _jsx(Text, { style: {
|
|
72
|
+
fontSize: 14,
|
|
73
|
+
color: colors.textSecondary,
|
|
74
|
+
lineHeight: 20,
|
|
75
|
+
marginBottom: 16
|
|
76
|
+
}, children: content }), recommendation && (_jsxs(View, { style: {
|
|
77
|
+
backgroundColor: '#F3E8FF',
|
|
78
|
+
padding: 12,
|
|
79
|
+
borderRadius: 16,
|
|
80
|
+
marginBottom: 16,
|
|
81
|
+
flexDirection: 'row',
|
|
82
|
+
alignItems: 'flex-start'
|
|
83
|
+
}, children: [_jsx(Sparkles, { size: 16, color: "#8B5CF6", style: { marginRight: 8 } }), _jsxs(Text, { style: {
|
|
84
|
+
fontSize: 14,
|
|
85
|
+
color: '#111827',
|
|
86
|
+
flex: 1,
|
|
87
|
+
lineHeight: 20
|
|
88
|
+
}, children: ["A breakout is expected at the resistance level of", ' ', _jsx(Text, { style: { fontWeight: '700' }, children: recommendation.priceRange }), ". It is recommended to consider long positions"] })] })), stocks.length > 0 && (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: { marginBottom: 16, borderRadius: 16 }, contentContainerStyle: { paddingRight: 16 }, children: _jsx(View, { style: { flexDirection: 'row', gap: 8 }, children: stocks.map((stock, index) => (_jsxs(View, { style: stockItemStyle(stock.changeType), children: [_jsx(View, { style: {
|
|
89
|
+
width: 24,
|
|
90
|
+
height: 24,
|
|
91
|
+
borderRadius: 16,
|
|
92
|
+
backgroundColor: stock.changeType === 'positive' ? '#10B981' : '#EF4444',
|
|
93
|
+
marginRight: 8,
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
alignItems: 'center'
|
|
96
|
+
}, children: _jsx(Text, { style: {
|
|
97
|
+
color: 'white',
|
|
98
|
+
fontSize: 10,
|
|
99
|
+
fontWeight: '600'
|
|
100
|
+
}, children: stock.symbol.charAt(0) }) }), _jsx(Text, { style: {
|
|
101
|
+
fontSize: 12,
|
|
102
|
+
fontWeight: '600',
|
|
103
|
+
color: '#111827',
|
|
104
|
+
marginRight: 8
|
|
105
|
+
}, children: stock.symbol }), _jsx(Text, { style: {
|
|
106
|
+
fontSize: 14,
|
|
107
|
+
fontWeight: '700',
|
|
108
|
+
color: '#111827',
|
|
109
|
+
marginRight: 8
|
|
110
|
+
}, children: stock.price }), _jsx(Text, { style: {
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
fontWeight: '600',
|
|
113
|
+
color: stock.changeType === 'positive' ? '#10B981' : '#EF4444',
|
|
114
|
+
marginRight: 2
|
|
115
|
+
}, children: stock.change }), stock.changeType === 'positive' ? (_jsx(Text, { style: { color: '#10B981', fontSize: 10 }, children: "\u2197" })) : (_jsx(Text, { style: { color: '#EF4444', fontSize: 10 }, children: "\u2198" }))] }, index))) }) })), _jsxs(View, { style: {
|
|
116
|
+
flexDirection: 'row',
|
|
117
|
+
justifyContent: 'space-around',
|
|
118
|
+
alignItems: 'center',
|
|
119
|
+
paddingTop: 12
|
|
120
|
+
}, children: [_jsx(Pressable, { onPress: onShare, style: { padding: 8 }, children: _jsx(Share2, { size: 20, color: "#6B7280" }) }), _jsx(Pressable, { onPress: onBookmark, style: { padding: 8 }, children: _jsx(Bookmark, { size: 20, color: isBookmarked ? "#0B69FF" : "#6B7280", fill: isBookmarked ? "#0B69FF" : "none" }) }), _jsx(Pressable, { onPress: onAI, style: { padding: 8 }, children: _jsx(MessageCircle, { size: 20, color: "#6B7280" }) }), _jsx(Pressable, { onPress: onDislike, style: { padding: 8 }, children: _jsx(ThumbsDown, { size: 20, color: isDisliked ? "#EF4444" : "#6B7280", fill: isDisliked ? "#EF4444" : "none" }) }), _jsx(Pressable, { onPress: onLike, style: { padding: 8 }, children: _jsx(ThumbsUp, { size: 20, color: isLiked ? "#10B981" : "#6B7280", fill: isLiked ? "#10B981" : "none" }) })] })] })));
|
|
121
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.segment-control {
|
|
2
|
+
display: flex;
|
|
3
|
+
background-color: #f3f4f6;
|
|
4
|
+
border-radius: 9999px;
|
|
5
|
+
padding: 4px;
|
|
6
|
+
gap: 4px;
|
|
7
|
+
width: fit-content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.segment-option {
|
|
11
|
+
flex: 1;
|
|
12
|
+
padding: 12px 24px;
|
|
13
|
+
border-radius: 9999px;
|
|
14
|
+
border: none;
|
|
15
|
+
background-color: transparent;
|
|
16
|
+
color: #6b7280;
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
font-weight: 500;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
transition: all 200ms ease;
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.segment-option:hover:not(.active) {
|
|
25
|
+
background-color: #e5e7eb;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.segment-option.active {
|
|
29
|
+
background-color: #ffffff;
|
|
30
|
+
color: #111827;
|
|
31
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
|
32
|
+
}
|
|
33
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "finsignal-feed-explore",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "News feed and segment control components for React web applications",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && cp src/styles.css dist/styles.css && cp src/newsfeed.css dist/newsfeed.css",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react",
|
|
18
|
+
"segment-control",
|
|
19
|
+
"tabs",
|
|
20
|
+
"ui",
|
|
21
|
+
"component",
|
|
22
|
+
"web"
|
|
23
|
+
],
|
|
24
|
+
"author": "FinSignal",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/finsignal/segment-control.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/finsignal/segment-control#readme",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18.0.0",
|
|
33
|
+
"react-native-web": ">=0.19.0",
|
|
34
|
+
"lucide-react": ">=0.294.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^18.0.0",
|
|
38
|
+
"@types/react-native": "^0.72.0",
|
|
39
|
+
"typescript": "^5.0.0",
|
|
40
|
+
"react-native-web": "^0.20.0",
|
|
41
|
+
"lucide-react": "^0.544.0"
|
|
42
|
+
}
|
|
43
|
+
}
|