@proulr/sdui-components 0.1.4 → 0.1.6
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 +13 -2
- package/src/renderer/blocks/SduiDiscoverBlocks.tsx +18 -14
- package/src/renderer/index.ts +8 -1
- package/src/renderer/theme/proulr-theme.ts +4 -0
- package/src/renderer/ui/app-glyphs.tsx +73 -0
- package/src/renderer/ui/app-icon.tsx +7 -1
- package/src/renderer/ui/compose-glyphs.tsx +0 -1
- package/src/renderer/ui/compose-pill-input.tsx +557 -0
- package/src/renderer/ui/floating-create-menu.tsx +1 -1
- package/src/renderer/ui/index.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proulr/sdui-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"main": "./src/renderer/index.ts",
|
|
5
5
|
"types": "./src/renderer/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
"url": "git+https://github.com/saviorbp/proulr-sdui-components.git"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
+
"prepare": "husky",
|
|
19
20
|
"typecheck": "tsc --noEmit",
|
|
20
|
-
"prepublishOnly": "npm run typecheck"
|
|
21
|
+
"prepublishOnly": "npm run typecheck",
|
|
22
|
+
"lint": "eslint .",
|
|
23
|
+
"lint:fix": "eslint . --fix"
|
|
21
24
|
},
|
|
22
25
|
"publishConfig": {
|
|
23
26
|
"access": "public"
|
|
@@ -52,11 +55,19 @@
|
|
|
52
55
|
"devDependencies": {
|
|
53
56
|
"@types/node": "^22.20.0",
|
|
54
57
|
"@types/react": "~19.2.2",
|
|
58
|
+
"eslint": "^9.0.0",
|
|
59
|
+
"eslint-config-expo": "~56.0.4",
|
|
55
60
|
"expo-linear-gradient": "~56.0.4",
|
|
61
|
+
"globals": "^16.0.0",
|
|
62
|
+
"husky": "^9.1.7",
|
|
63
|
+
"lint-staged": "^16.4.0",
|
|
56
64
|
"react": "19.2.3",
|
|
57
65
|
"react-native": "0.85.3",
|
|
58
66
|
"react-native-google-mobile-ads": "^16.3.4",
|
|
59
67
|
"react-native-safe-area-context": "~5.7.0",
|
|
60
68
|
"typescript": "~6.0.3"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=22"
|
|
61
72
|
}
|
|
62
73
|
}
|
|
@@ -188,20 +188,7 @@ function DiscoverGridCell({
|
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
const hybrid = useSduiHybridRenderer();
|
|
193
|
-
if (hybrid.renderDiscoverGrid) {
|
|
194
|
-
return (
|
|
195
|
-
<>
|
|
196
|
-
{hybrid.renderDiscoverGrid({
|
|
197
|
-
block,
|
|
198
|
-
data,
|
|
199
|
-
onAction,
|
|
200
|
-
})}
|
|
201
|
-
</>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
191
|
+
function SduiDiscoverGridBlockFallback({ block, data, onAction }: SduiBlockRenderProps) {
|
|
205
192
|
const itemsRef = readStringProp(block, 'items_ref') ?? 'grid_items';
|
|
206
193
|
const columns = readNumberProp(block, 'columns', GRID_COLUMNS);
|
|
207
194
|
const profileActionId = readStringProp(block, 'profile_action_id');
|
|
@@ -252,6 +239,23 @@ export function SduiDiscoverGridBlock({ block, data, onAction }: SduiBlockRender
|
|
|
252
239
|
);
|
|
253
240
|
}
|
|
254
241
|
|
|
242
|
+
export function SduiDiscoverGridBlock({ block, data, onAction }: SduiBlockRenderProps) {
|
|
243
|
+
const hybrid = useSduiHybridRenderer();
|
|
244
|
+
if (hybrid.renderDiscoverGrid) {
|
|
245
|
+
return (
|
|
246
|
+
<>
|
|
247
|
+
{hybrid.renderDiscoverGrid({
|
|
248
|
+
block,
|
|
249
|
+
data,
|
|
250
|
+
onAction,
|
|
251
|
+
})}
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return <SduiDiscoverGridBlockFallback block={block} data={data} onAction={onAction} />;
|
|
257
|
+
}
|
|
258
|
+
|
|
255
259
|
export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
256
260
|
const label = readStringProp(block, 'label') ?? '';
|
|
257
261
|
const actionId = readStringProp(block, 'action_id');
|
package/src/renderer/index.ts
CHANGED
|
@@ -32,8 +32,15 @@ export {
|
|
|
32
32
|
formatDistanceLabel,
|
|
33
33
|
getPresenceStatus,
|
|
34
34
|
profileAgeFromBirthDate,
|
|
35
|
+
readDiscoverGridItems,
|
|
36
|
+
isDiscoverGridItem,
|
|
37
|
+
} from './lib/discover-profile-formatters';
|
|
38
|
+
export type {
|
|
39
|
+
DiscoverNearbyProfileWire,
|
|
40
|
+
DiscoverGridItemWire,
|
|
41
|
+
DiscoverGridProfileItemWire,
|
|
42
|
+
DiscoverGridAdItemWire,
|
|
35
43
|
} from './lib/discover-profile-formatters';
|
|
36
|
-
export type { DiscoverNearbyProfileWire } from './lib/discover-profile-formatters';
|
|
37
44
|
export {
|
|
38
45
|
configureNativeAdCatalog,
|
|
39
46
|
getNativeAdCatalog,
|
|
@@ -31,6 +31,8 @@ export const m3Colors = {
|
|
|
31
31
|
inverseOnSurface: '#000000',
|
|
32
32
|
scrim: 'rgba(0,0,0,0.78)',
|
|
33
33
|
online: '#34FF88',
|
|
34
|
+
chromePillBackground: 'rgba(28, 28, 30, 0.82)',
|
|
35
|
+
chromePillBorder: 'rgba(255, 255, 255, 0.06)',
|
|
34
36
|
} as const;
|
|
35
37
|
|
|
36
38
|
export const m3Shape = {
|
|
@@ -77,6 +79,8 @@ export const colors = {
|
|
|
77
79
|
dangerSoft: 'rgba(255,68,68,0.22)',
|
|
78
80
|
overlay: 'rgba(0,0,0,0.78)',
|
|
79
81
|
surfaceGlass: 'rgba(255,255,255,0.06)',
|
|
82
|
+
chromePillBackground: 'rgba(28, 28, 30, 0.82)',
|
|
83
|
+
chromePillBorder: 'rgba(255, 255, 255, 0.06)',
|
|
80
84
|
primary: m3Colors.primary,
|
|
81
85
|
onSurface: m3Colors.onSurface,
|
|
82
86
|
onSurfaceVariant: m3Colors.onSurfaceVariant,
|
|
@@ -1234,6 +1234,79 @@ export function AppChromeSignOutGlyph({ color = m3Colors.primary, size = 24 }: A
|
|
|
1234
1234
|
);
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
|
+
/** Olho aberto — mostrar senha. */
|
|
1238
|
+
export function AppChromeVisibilityGlyph({ color = m3Colors.onSurfaceVariant, size = 24 }: AppChromeGlyphProps) {
|
|
1239
|
+
const stroke = strokeFor(size);
|
|
1240
|
+
const eyeW = size * 0.82;
|
|
1241
|
+
const eyeH = size * 0.46;
|
|
1242
|
+
const pupil = size * 0.18;
|
|
1243
|
+
return (
|
|
1244
|
+
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
|
|
1245
|
+
<View
|
|
1246
|
+
style={{
|
|
1247
|
+
width: eyeW,
|
|
1248
|
+
height: eyeH,
|
|
1249
|
+
borderRadius: eyeH / 2,
|
|
1250
|
+
borderWidth: stroke,
|
|
1251
|
+
borderColor: color,
|
|
1252
|
+
}}
|
|
1253
|
+
/>
|
|
1254
|
+
<View
|
|
1255
|
+
style={{
|
|
1256
|
+
position: 'absolute',
|
|
1257
|
+
width: pupil,
|
|
1258
|
+
height: pupil,
|
|
1259
|
+
borderRadius: pupil / 2,
|
|
1260
|
+
backgroundColor: color,
|
|
1261
|
+
}}
|
|
1262
|
+
/>
|
|
1263
|
+
</View>
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** Olho riscado — ocultar senha. */
|
|
1268
|
+
export function AppChromeVisibilityOffGlyph({ color = m3Colors.onSurfaceVariant, size = 24 }: AppChromeGlyphProps) {
|
|
1269
|
+
const stroke = strokeFor(size);
|
|
1270
|
+
const eyeW = size * 0.82;
|
|
1271
|
+
const eyeH = size * 0.46;
|
|
1272
|
+
const pupil = size * 0.18;
|
|
1273
|
+
const slash = size * 0.88;
|
|
1274
|
+
return (
|
|
1275
|
+
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
|
|
1276
|
+
<View
|
|
1277
|
+
style={{
|
|
1278
|
+
width: eyeW,
|
|
1279
|
+
height: eyeH,
|
|
1280
|
+
borderRadius: eyeH / 2,
|
|
1281
|
+
borderWidth: stroke,
|
|
1282
|
+
borderColor: color,
|
|
1283
|
+
opacity: 0.72,
|
|
1284
|
+
}}
|
|
1285
|
+
/>
|
|
1286
|
+
<View
|
|
1287
|
+
style={{
|
|
1288
|
+
position: 'absolute',
|
|
1289
|
+
width: pupil,
|
|
1290
|
+
height: pupil,
|
|
1291
|
+
borderRadius: pupil / 2,
|
|
1292
|
+
backgroundColor: color,
|
|
1293
|
+
opacity: 0.72,
|
|
1294
|
+
}}
|
|
1295
|
+
/>
|
|
1296
|
+
<View
|
|
1297
|
+
style={{
|
|
1298
|
+
position: 'absolute',
|
|
1299
|
+
width: slash,
|
|
1300
|
+
height: stroke,
|
|
1301
|
+
backgroundColor: color,
|
|
1302
|
+
borderRadius: 1,
|
|
1303
|
+
transform: [{ rotate: '-35deg' }],
|
|
1304
|
+
}}
|
|
1305
|
+
/>
|
|
1306
|
+
</View>
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1237
1310
|
const styles = StyleSheet.create({
|
|
1238
1311
|
crosshair: {
|
|
1239
1312
|
position: 'absolute',
|
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
AppChromeTabDiscoverGlyph,
|
|
47
47
|
AppChromeTabFlashGlyph,
|
|
48
48
|
AppChromeTabVibesGlyph,
|
|
49
|
+
AppChromeVisibilityGlyph,
|
|
50
|
+
AppChromeVisibilityOffGlyph,
|
|
49
51
|
type AppChromeGlyphProps,
|
|
50
52
|
} from './app-glyphs';
|
|
51
53
|
import {
|
|
@@ -110,7 +112,9 @@ export type AppIconName =
|
|
|
110
112
|
| 'pause'
|
|
111
113
|
| 'plus'
|
|
112
114
|
| 'followers'
|
|
113
|
-
| 'mention'
|
|
115
|
+
| 'mention'
|
|
116
|
+
| 'visibility'
|
|
117
|
+
| 'visibility-off';
|
|
114
118
|
|
|
115
119
|
const ICON_REGISTRY: Record<AppIconName, ComponentType<AppChromeGlyphProps>> = {
|
|
116
120
|
search: AppChromeSearchGlyph,
|
|
@@ -165,6 +169,8 @@ const ICON_REGISTRY: Record<AppIconName, ComponentType<AppChromeGlyphProps>> = {
|
|
|
165
169
|
plus: AppChromePlusGlyph,
|
|
166
170
|
followers: ComposeFollowersGlyph,
|
|
167
171
|
mention: ComposeMentionGlyph,
|
|
172
|
+
visibility: AppChromeVisibilityGlyph,
|
|
173
|
+
'visibility-off': AppChromeVisibilityOffGlyph,
|
|
168
174
|
};
|
|
169
175
|
|
|
170
176
|
export type AppIconProps = {
|
|
@@ -7,7 +7,6 @@ type GlyphProps = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export function ComposeCloseGlyph({ color = m3Colors.onSurface, size = 18 }: GlyphProps) {
|
|
10
|
-
const stroke = Math.max(1.5, size * 0.1);
|
|
11
10
|
return (
|
|
12
11
|
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
|
|
13
12
|
<View style={[styles.diag, { width: size, backgroundColor: color, transform: [{ rotate: '45deg' }] }]} />
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
NativeSyntheticEvent,
|
|
5
|
+
Platform,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
TextInput,
|
|
10
|
+
TextInputContentSizeChangeEventData,
|
|
11
|
+
View,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
type TextInputProps,
|
|
14
|
+
type ViewStyle,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import { AppIcon } from './app-icon';
|
|
17
|
+
import { colors, fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
|
|
18
|
+
|
|
19
|
+
const ACTION_BUTTON_SIZE = 48;
|
|
20
|
+
const FONT_SIZE = 16;
|
|
21
|
+
const LINE_HEIGHT = 25;
|
|
22
|
+
const MAX_LINES_DEFAULT = 4;
|
|
23
|
+
const PILL_MIN_HEIGHT = ACTION_BUTTON_SIZE;
|
|
24
|
+
const VERTICAL_PAD = (PILL_MIN_HEIGHT - LINE_HEIGHT) / 2;
|
|
25
|
+
|
|
26
|
+
export type ComposePillVariant = 'surface' | 'chrome' | 'overlay';
|
|
27
|
+
|
|
28
|
+
export type ComposePillReplyPreview = {
|
|
29
|
+
title: string;
|
|
30
|
+
body: string;
|
|
31
|
+
onDismiss: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ComposePillLineLayout = {
|
|
35
|
+
lineCount: number;
|
|
36
|
+
isMultiline: boolean;
|
|
37
|
+
pillHeight: number;
|
|
38
|
+
inputMaxHeight: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ComposePillLineLayoutState = ComposePillLineLayout & {
|
|
42
|
+
handleContentSizeChange: (event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => void;
|
|
43
|
+
resetLineCount: () => void;
|
|
44
|
+
setLineCount: (count: number) => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ComposePillLayoutOptions = {
|
|
48
|
+
maxLines?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function singleLineMaxContentHeight(): number {
|
|
52
|
+
return LINE_HEIGHT * 2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function maxPillHeight(maxLines: number): number {
|
|
56
|
+
return LINE_HEIGHT * maxLines + VERTICAL_PAD * 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function countComposePillLines(
|
|
60
|
+
text: string,
|
|
61
|
+
contentHeight: number,
|
|
62
|
+
maxLines: number = MAX_LINES_DEFAULT,
|
|
63
|
+
): number {
|
|
64
|
+
if (!text) return 1;
|
|
65
|
+
|
|
66
|
+
const explicitLines = text.split('\n').length;
|
|
67
|
+
if (explicitLines > 1) {
|
|
68
|
+
return Math.min(maxLines, explicitLines);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (contentHeight <= singleLineMaxContentHeight()) {
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const wrappedLines = Math.round(contentHeight / LINE_HEIGHT);
|
|
76
|
+
return Math.min(maxLines, Math.max(1, wrappedLines));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function pillHeightForLineCount(lineCount: number, maxLines: number = MAX_LINES_DEFAULT): number {
|
|
80
|
+
if (lineCount <= 1) return PILL_MIN_HEIGHT;
|
|
81
|
+
const textBlockHeight = lineCount * LINE_HEIGHT;
|
|
82
|
+
return Math.min(maxPillHeight(maxLines), textBlockHeight);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useComposePillLineLayout(
|
|
86
|
+
value: string,
|
|
87
|
+
{ maxLines = MAX_LINES_DEFAULT }: ComposePillLayoutOptions = {},
|
|
88
|
+
): ComposePillLineLayoutState {
|
|
89
|
+
const valueRef = useRef(value);
|
|
90
|
+
valueRef.current = value;
|
|
91
|
+
const [lineCount, setLineCount] = useState(1);
|
|
92
|
+
|
|
93
|
+
const updateLineLayout = useCallback(
|
|
94
|
+
(text: string, contentHeight: number) => {
|
|
95
|
+
const nextLineCount = text ? countComposePillLines(text, contentHeight, maxLines) : 1;
|
|
96
|
+
setLineCount(nextLineCount);
|
|
97
|
+
},
|
|
98
|
+
[maxLines],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const handleContentSizeChange = useCallback(
|
|
102
|
+
(event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
103
|
+
const text = valueRef.current;
|
|
104
|
+
if (!text) {
|
|
105
|
+
setLineCount(1);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
updateLineLayout(text, event.nativeEvent.contentSize.height);
|
|
109
|
+
},
|
|
110
|
+
[updateLineLayout],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const resetLineCount = useCallback(() => {
|
|
114
|
+
setLineCount(1);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const isMultiline = lineCount > 1;
|
|
118
|
+
const pillHeight = pillHeightForLineCount(lineCount, maxLines);
|
|
119
|
+
const inputMaxHeight = maxPillHeight(maxLines) - VERTICAL_PAD * 2;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
lineCount,
|
|
123
|
+
isMultiline,
|
|
124
|
+
pillHeight,
|
|
125
|
+
inputMaxHeight,
|
|
126
|
+
handleContentSizeChange,
|
|
127
|
+
resetLineCount,
|
|
128
|
+
setLineCount,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type ComposePillTextInputRef = React.Ref<TextInput>;
|
|
133
|
+
|
|
134
|
+
export type ComposePillTextFieldProps = Omit<TextInputProps, 'multiline' | 'style'> & {
|
|
135
|
+
value: string;
|
|
136
|
+
onChangeText: (text: string) => void;
|
|
137
|
+
variant?: ComposePillVariant;
|
|
138
|
+
maxLines?: number;
|
|
139
|
+
inputRef?: ComposePillTextInputRef;
|
|
140
|
+
layout?: ComposePillLineLayoutState;
|
|
141
|
+
style?: StyleProp<ViewStyle>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function ComposePillTextField({
|
|
145
|
+
value,
|
|
146
|
+
onChangeText,
|
|
147
|
+
variant = 'surface',
|
|
148
|
+
maxLines = MAX_LINES_DEFAULT,
|
|
149
|
+
inputRef,
|
|
150
|
+
layout,
|
|
151
|
+
placeholder,
|
|
152
|
+
placeholderTextColor,
|
|
153
|
+
onFocus,
|
|
154
|
+
onBlur,
|
|
155
|
+
editable,
|
|
156
|
+
style,
|
|
157
|
+
...rest
|
|
158
|
+
}: ComposePillTextFieldProps) {
|
|
159
|
+
const internalLayout = useComposePillLineLayout(value, { maxLines });
|
|
160
|
+
const {
|
|
161
|
+
lineCount,
|
|
162
|
+
isMultiline,
|
|
163
|
+
inputMaxHeight,
|
|
164
|
+
handleContentSizeChange,
|
|
165
|
+
setLineCount,
|
|
166
|
+
} = layout ?? internalLayout;
|
|
167
|
+
|
|
168
|
+
const handleChange = useCallback(
|
|
169
|
+
(text: string) => {
|
|
170
|
+
onChangeText(text);
|
|
171
|
+
if (!text) {
|
|
172
|
+
setLineCount(1);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
[onChangeText, setLineCount],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const variantColors = composePillVariantColors(variant);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<TextInput
|
|
182
|
+
ref={inputRef}
|
|
183
|
+
value={value}
|
|
184
|
+
onChangeText={handleChange}
|
|
185
|
+
onContentSizeChange={handleContentSizeChange}
|
|
186
|
+
onFocus={onFocus}
|
|
187
|
+
onBlur={onBlur}
|
|
188
|
+
placeholder={placeholder}
|
|
189
|
+
placeholderTextColor={placeholderTextColor ?? variantColors.placeholder}
|
|
190
|
+
multiline
|
|
191
|
+
scrollEnabled={lineCount >= maxLines}
|
|
192
|
+
blurOnSubmit={false}
|
|
193
|
+
returnKeyType="default"
|
|
194
|
+
editable={editable}
|
|
195
|
+
style={[
|
|
196
|
+
styles.input,
|
|
197
|
+
{
|
|
198
|
+
color: variantColors.text,
|
|
199
|
+
maxHeight: inputMaxHeight,
|
|
200
|
+
textAlignVertical: isMultiline ? 'top' : 'center',
|
|
201
|
+
},
|
|
202
|
+
style,
|
|
203
|
+
]}
|
|
204
|
+
{...(Platform.OS === 'android' ? { includeFontPadding: false } : {})}
|
|
205
|
+
{...(Platform.OS === 'web'
|
|
206
|
+
? ({
|
|
207
|
+
outlineStyle: 'none',
|
|
208
|
+
resize: 'none',
|
|
209
|
+
overflow: 'hidden',
|
|
210
|
+
} as object)
|
|
211
|
+
: {})}
|
|
212
|
+
{...rest}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type ComposePillInputProps = {
|
|
218
|
+
value: string;
|
|
219
|
+
onChangeText: (text: string) => void;
|
|
220
|
+
placeholder?: string;
|
|
221
|
+
disabled?: boolean;
|
|
222
|
+
maxLines?: number;
|
|
223
|
+
variant?: ComposePillVariant;
|
|
224
|
+
onFocus?: () => void;
|
|
225
|
+
onBlur?: () => void;
|
|
226
|
+
inputRef?: ComposePillTextInputRef;
|
|
227
|
+
leading?: ReactNode;
|
|
228
|
+
trailing?: ReactNode;
|
|
229
|
+
endAction?: ReactNode;
|
|
230
|
+
replyPreview?: ComposePillReplyPreview | null;
|
|
231
|
+
style?: StyleProp<ViewStyle>;
|
|
232
|
+
transparentWrap?: boolean;
|
|
233
|
+
/** Bare row without pill chrome — text field only grows inline (ex.: comentários). */
|
|
234
|
+
bare?: boolean;
|
|
235
|
+
rowAlignItems?: 'center' | 'flex-end';
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
function composePillVariantColors(variant: ComposePillVariant) {
|
|
239
|
+
if (variant === 'overlay') {
|
|
240
|
+
return {
|
|
241
|
+
text: colors.white,
|
|
242
|
+
placeholder: 'rgba(255,255,255,0.55)',
|
|
243
|
+
pillBackground: 'rgba(28,28,30,0.72)',
|
|
244
|
+
pillBorder: 'rgba(255,255,255,0.08)',
|
|
245
|
+
replyBackground: 'rgba(28,28,30,0.72)',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (variant === 'chrome') {
|
|
250
|
+
return {
|
|
251
|
+
text: m3Colors.onSurface,
|
|
252
|
+
placeholder: m3Colors.onSurfaceVariant,
|
|
253
|
+
pillBackground: colors.chromePillBackground,
|
|
254
|
+
pillBorder: colors.chromePillBorder,
|
|
255
|
+
replyBackground: colors.chromePillBackground,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
text: m3Colors.onSurface,
|
|
261
|
+
placeholder: m3Colors.onSurfaceVariant,
|
|
262
|
+
pillBackground: m3Colors.surfaceContainerHigh,
|
|
263
|
+
pillBorder: 'transparent',
|
|
264
|
+
replyBackground: m3Colors.surfaceContainerHigh,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function ComposePillReplyBar({
|
|
269
|
+
preview,
|
|
270
|
+
variant = 'surface',
|
|
271
|
+
}: {
|
|
272
|
+
preview: ComposePillReplyPreview;
|
|
273
|
+
variant?: ComposePillVariant;
|
|
274
|
+
}) {
|
|
275
|
+
const variantColors = composePillVariantColors(variant);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<View style={[styles.replyBar, { backgroundColor: variantColors.replyBackground }]}>
|
|
279
|
+
<View style={styles.replyBarAccent} />
|
|
280
|
+
<View style={styles.replyBarContent}>
|
|
281
|
+
<Text style={styles.replyBarName} numberOfLines={1}>
|
|
282
|
+
{preview.title}
|
|
283
|
+
</Text>
|
|
284
|
+
<Text style={styles.replyBarPreview} numberOfLines={1}>
|
|
285
|
+
{preview.body}
|
|
286
|
+
</Text>
|
|
287
|
+
</View>
|
|
288
|
+
<Pressable
|
|
289
|
+
onPress={preview.onDismiss}
|
|
290
|
+
hitSlop={8}
|
|
291
|
+
style={styles.replyBarClose}
|
|
292
|
+
accessibilityRole="button"
|
|
293
|
+
accessibilityLabel="Cancelar resposta"
|
|
294
|
+
>
|
|
295
|
+
<AppIcon name="close" size={14} color={m3Colors.onSurfaceVariant} />
|
|
296
|
+
</Pressable>
|
|
297
|
+
</View>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function ComposePillIconButton({
|
|
302
|
+
label,
|
|
303
|
+
children,
|
|
304
|
+
onPress,
|
|
305
|
+
disabled,
|
|
306
|
+
}: {
|
|
307
|
+
label: string;
|
|
308
|
+
children: ReactNode;
|
|
309
|
+
onPress?: () => void;
|
|
310
|
+
disabled?: boolean;
|
|
311
|
+
}) {
|
|
312
|
+
return (
|
|
313
|
+
<Pressable
|
|
314
|
+
style={[styles.iconButton, disabled && styles.iconButtonDisabled]}
|
|
315
|
+
onPress={onPress}
|
|
316
|
+
disabled={disabled || !onPress}
|
|
317
|
+
hitSlop={6}
|
|
318
|
+
accessibilityRole="button"
|
|
319
|
+
accessibilityLabel={label}
|
|
320
|
+
>
|
|
321
|
+
{children}
|
|
322
|
+
</Pressable>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function ComposePillActionButton({
|
|
327
|
+
label,
|
|
328
|
+
children,
|
|
329
|
+
onPress,
|
|
330
|
+
disabled,
|
|
331
|
+
}: {
|
|
332
|
+
label: string;
|
|
333
|
+
children: ReactNode;
|
|
334
|
+
onPress?: () => void;
|
|
335
|
+
disabled?: boolean;
|
|
336
|
+
}) {
|
|
337
|
+
return (
|
|
338
|
+
<Pressable
|
|
339
|
+
style={[styles.actionButton, disabled && styles.actionButtonDisabled]}
|
|
340
|
+
onPress={onPress}
|
|
341
|
+
disabled={disabled || !onPress}
|
|
342
|
+
accessibilityRole="button"
|
|
343
|
+
accessibilityLabel={label}
|
|
344
|
+
>
|
|
345
|
+
{children}
|
|
346
|
+
</Pressable>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function ComposePillInput({
|
|
351
|
+
value,
|
|
352
|
+
onChangeText,
|
|
353
|
+
placeholder = 'Mensagem',
|
|
354
|
+
disabled,
|
|
355
|
+
maxLines = MAX_LINES_DEFAULT,
|
|
356
|
+
variant = 'surface',
|
|
357
|
+
onFocus,
|
|
358
|
+
onBlur,
|
|
359
|
+
inputRef,
|
|
360
|
+
leading,
|
|
361
|
+
trailing,
|
|
362
|
+
endAction,
|
|
363
|
+
replyPreview,
|
|
364
|
+
style,
|
|
365
|
+
transparentWrap = false,
|
|
366
|
+
bare = false,
|
|
367
|
+
rowAlignItems = bare ? 'center' : 'flex-end',
|
|
368
|
+
}: ComposePillInputProps) {
|
|
369
|
+
const layout = useComposePillLineLayout(value, { maxLines });
|
|
370
|
+
const { isMultiline, pillHeight, resetLineCount } = layout;
|
|
371
|
+
const variantColors = composePillVariantColors(variant);
|
|
372
|
+
|
|
373
|
+
const handleChange = useCallback(
|
|
374
|
+
(text: string) => {
|
|
375
|
+
onChangeText(text);
|
|
376
|
+
if (!text) {
|
|
377
|
+
resetLineCount();
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
[onChangeText, resetLineCount],
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const textField = (
|
|
384
|
+
<ComposePillTextField
|
|
385
|
+
value={value}
|
|
386
|
+
onChangeText={handleChange}
|
|
387
|
+
variant={variant}
|
|
388
|
+
maxLines={maxLines}
|
|
389
|
+
inputRef={inputRef}
|
|
390
|
+
layout={layout}
|
|
391
|
+
placeholder={placeholder}
|
|
392
|
+
onFocus={onFocus}
|
|
393
|
+
onBlur={onBlur}
|
|
394
|
+
editable={!disabled}
|
|
395
|
+
style={bare ? styles.bareInput : undefined}
|
|
396
|
+
/>
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<View
|
|
401
|
+
style={[
|
|
402
|
+
styles.wrap,
|
|
403
|
+
transparentWrap && styles.wrapTransparent,
|
|
404
|
+
variant === 'surface' && !transparentWrap && styles.wrapSurface,
|
|
405
|
+
style,
|
|
406
|
+
]}
|
|
407
|
+
>
|
|
408
|
+
{replyPreview ? <ComposePillReplyBar preview={replyPreview} variant={variant} /> : null}
|
|
409
|
+
|
|
410
|
+
<View style={[styles.composerRow, { alignItems: rowAlignItems }]}>
|
|
411
|
+
{bare ? (
|
|
412
|
+
<>
|
|
413
|
+
{leading}
|
|
414
|
+
{textField}
|
|
415
|
+
{trailing}
|
|
416
|
+
</>
|
|
417
|
+
) : (
|
|
418
|
+
<View
|
|
419
|
+
style={[
|
|
420
|
+
styles.inputPill,
|
|
421
|
+
isMultiline ? styles.inputPillExpanded : styles.inputPillSingle,
|
|
422
|
+
{
|
|
423
|
+
minHeight: pillHeight,
|
|
424
|
+
alignItems: isMultiline ? 'flex-end' : 'center',
|
|
425
|
+
backgroundColor: variantColors.pillBackground,
|
|
426
|
+
borderColor: variantColors.pillBorder,
|
|
427
|
+
borderWidth: variant === 'overlay' || variant === 'chrome' ? 1 : 0,
|
|
428
|
+
},
|
|
429
|
+
]}
|
|
430
|
+
>
|
|
431
|
+
{leading}
|
|
432
|
+
{textField}
|
|
433
|
+
{trailing ? (
|
|
434
|
+
<View style={[styles.trailingIcons, isMultiline && styles.trailingIconsExpanded]}>
|
|
435
|
+
{trailing}
|
|
436
|
+
</View>
|
|
437
|
+
) : null}
|
|
438
|
+
</View>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{endAction}
|
|
442
|
+
</View>
|
|
443
|
+
</View>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const styles = StyleSheet.create({
|
|
448
|
+
wrap: {
|
|
449
|
+
backgroundColor: 'transparent',
|
|
450
|
+
},
|
|
451
|
+
wrapTransparent: {
|
|
452
|
+
backgroundColor: 'transparent',
|
|
453
|
+
},
|
|
454
|
+
wrapSurface: {
|
|
455
|
+
backgroundColor: m3Colors.surface,
|
|
456
|
+
},
|
|
457
|
+
replyBar: {
|
|
458
|
+
flexDirection: 'row',
|
|
459
|
+
alignItems: 'center',
|
|
460
|
+
marginHorizontal: spacing.sm,
|
|
461
|
+
marginTop: spacing.sm,
|
|
462
|
+
marginBottom: spacing.xs,
|
|
463
|
+
paddingVertical: spacing.sm,
|
|
464
|
+
paddingHorizontal: spacing.md,
|
|
465
|
+
borderRadius: m3Shape.cornerMedium,
|
|
466
|
+
gap: spacing.sm,
|
|
467
|
+
},
|
|
468
|
+
replyBarAccent: {
|
|
469
|
+
width: 4,
|
|
470
|
+
alignSelf: 'stretch',
|
|
471
|
+
borderRadius: m3Shape.cornerExtraSmall,
|
|
472
|
+
backgroundColor: m3Colors.primary,
|
|
473
|
+
},
|
|
474
|
+
replyBarContent: {
|
|
475
|
+
flex: 1,
|
|
476
|
+
gap: 2,
|
|
477
|
+
},
|
|
478
|
+
replyBarName: {
|
|
479
|
+
color: m3Colors.primary,
|
|
480
|
+
fontSize: 13,
|
|
481
|
+
fontFamily: fonts.semibold,
|
|
482
|
+
},
|
|
483
|
+
replyBarPreview: {
|
|
484
|
+
color: m3Colors.onSurfaceVariant,
|
|
485
|
+
fontSize: 13,
|
|
486
|
+
fontFamily: fonts.regular,
|
|
487
|
+
},
|
|
488
|
+
replyBarClose: {
|
|
489
|
+
width: 32,
|
|
490
|
+
height: 32,
|
|
491
|
+
alignItems: 'center',
|
|
492
|
+
justifyContent: 'center',
|
|
493
|
+
},
|
|
494
|
+
composerRow: {
|
|
495
|
+
flexDirection: 'row',
|
|
496
|
+
alignItems: 'flex-end',
|
|
497
|
+
gap: spacing.sm,
|
|
498
|
+
paddingHorizontal: spacing.sm,
|
|
499
|
+
paddingTop: spacing.sm,
|
|
500
|
+
},
|
|
501
|
+
inputPill: {
|
|
502
|
+
flex: 1,
|
|
503
|
+
flexDirection: 'row',
|
|
504
|
+
paddingLeft: spacing.xs,
|
|
505
|
+
paddingRight: spacing.xs,
|
|
506
|
+
overflow: 'hidden',
|
|
507
|
+
},
|
|
508
|
+
inputPillSingle: {
|
|
509
|
+
borderRadius: m3Shape.cornerFull,
|
|
510
|
+
},
|
|
511
|
+
inputPillExpanded: {
|
|
512
|
+
borderRadius: m3Shape.cornerLarge,
|
|
513
|
+
},
|
|
514
|
+
iconButton: {
|
|
515
|
+
width: 40,
|
|
516
|
+
height: 40,
|
|
517
|
+
alignItems: 'center',
|
|
518
|
+
justifyContent: 'center',
|
|
519
|
+
},
|
|
520
|
+
iconButtonDisabled: {
|
|
521
|
+
opacity: 0.4,
|
|
522
|
+
},
|
|
523
|
+
trailingIcons: {
|
|
524
|
+
flexDirection: 'row',
|
|
525
|
+
alignItems: 'center',
|
|
526
|
+
},
|
|
527
|
+
trailingIconsExpanded: {
|
|
528
|
+
paddingBottom: spacing.xs,
|
|
529
|
+
},
|
|
530
|
+
input: {
|
|
531
|
+
flex: 1,
|
|
532
|
+
fontSize: FONT_SIZE,
|
|
533
|
+
lineHeight: LINE_HEIGHT,
|
|
534
|
+
fontFamily: fonts.regular,
|
|
535
|
+
paddingHorizontal: spacing.xs,
|
|
536
|
+
paddingTop: VERTICAL_PAD,
|
|
537
|
+
paddingBottom: VERTICAL_PAD,
|
|
538
|
+
},
|
|
539
|
+
bareInput: {
|
|
540
|
+
minHeight: 40,
|
|
541
|
+
paddingTop: spacing.xs,
|
|
542
|
+
paddingBottom: spacing.xs,
|
|
543
|
+
},
|
|
544
|
+
actionButton: {
|
|
545
|
+
width: ACTION_BUTTON_SIZE,
|
|
546
|
+
height: ACTION_BUTTON_SIZE,
|
|
547
|
+
borderRadius: ACTION_BUTTON_SIZE / 2,
|
|
548
|
+
backgroundColor: m3Colors.primary,
|
|
549
|
+
alignItems: 'center',
|
|
550
|
+
justifyContent: 'center',
|
|
551
|
+
},
|
|
552
|
+
actionButtonDisabled: {
|
|
553
|
+
opacity: 0.45,
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
export const COMPOSE_PILL_ACTION_BUTTON_SIZE = ACTION_BUTTON_SIZE;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useCallback, useState } from 'react';
|
|
3
3
|
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
4
|
-
import { fonts, m3Colors, m3Elevation,
|
|
4
|
+
import { fonts, m3Colors, m3Elevation, spacing } from '../theme/proulr-theme';
|
|
5
5
|
|
|
6
6
|
export type FloatingCreateMenuAction = {
|
|
7
7
|
id: string;
|
package/src/renderer/ui/index.ts
CHANGED
|
@@ -71,6 +71,23 @@ export { ComposeMediaShortcut, COMPOSE_MEDIA_TILE_SIZE } from './compose-media-s
|
|
|
71
71
|
export { ComposeRecentMediaGrid, type ComposeRecentMediaItem } from './compose-recent-media-grid';
|
|
72
72
|
export { ComposeScreenHeader } from './compose-screen-header';
|
|
73
73
|
export { ComposeTextArea, ComposeMediaPreviewFrame } from './compose-text-area';
|
|
74
|
+
export {
|
|
75
|
+
ComposePillActionButton,
|
|
76
|
+
ComposePillIconButton,
|
|
77
|
+
ComposePillInput,
|
|
78
|
+
ComposePillReplyBar,
|
|
79
|
+
ComposePillTextField,
|
|
80
|
+
COMPOSE_PILL_ACTION_BUTTON_SIZE,
|
|
81
|
+
countComposePillLines,
|
|
82
|
+
pillHeightForLineCount,
|
|
83
|
+
useComposePillLineLayout,
|
|
84
|
+
type ComposePillInputProps,
|
|
85
|
+
type ComposePillLineLayout,
|
|
86
|
+
type ComposePillLineLayoutState,
|
|
87
|
+
type ComposePillReplyPreview,
|
|
88
|
+
type ComposePillTextFieldProps,
|
|
89
|
+
type ComposePillVariant,
|
|
90
|
+
} from './compose-pill-input';
|
|
74
91
|
export { ComposeToolbar, ComposeToolbarIcon, COMPOSE_TOOLBAR_ICON_SIZE } from './compose-toolbar';
|
|
75
92
|
export { ComposeReplyAudienceSheet, type ComposeReplyAudienceOption, type ComposeReplyAudienceValue } from './compose-reply-audience-sheet';
|
|
76
93
|
export { ComposeVisibilityRow } from './compose-visibility-row';
|