@proulr/sdui-components 0.1.55 → 0.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useCallback, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import {
|
|
4
|
+
LayoutAnimation,
|
|
4
5
|
NativeSyntheticEvent,
|
|
5
6
|
Platform,
|
|
6
7
|
Pressable,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
Text,
|
|
9
10
|
TextInput,
|
|
10
11
|
TextInputContentSizeChangeEventData,
|
|
12
|
+
UIManager,
|
|
11
13
|
View,
|
|
12
14
|
type StyleProp,
|
|
13
15
|
type TextInputProps,
|
|
@@ -18,7 +20,13 @@ import { AppIcon } from './app-icon';
|
|
|
18
20
|
import { ChromeGlassLayer } from './ChromeGlass';
|
|
19
21
|
import { colors, fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
|
|
20
22
|
|
|
23
|
+
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
24
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const ACTION_BUTTON_SIZE = 48;
|
|
28
|
+
/** Filled action circle when nested inside the pill (Instagram-style composer). */
|
|
29
|
+
const ACTION_BUTTON_SIZE_INSET = 36;
|
|
22
30
|
const FONT_SIZE = 16;
|
|
23
31
|
const LINE_HEIGHT = 25;
|
|
24
32
|
const MAX_LINES_DEFAULT = 4;
|
|
@@ -196,10 +204,12 @@ export function ComposePillTextField({
|
|
|
196
204
|
editable={editable}
|
|
197
205
|
style={[
|
|
198
206
|
styles.input,
|
|
207
|
+
isMultiline ? styles.inputMultiline : styles.inputSingleLine,
|
|
199
208
|
{
|
|
200
209
|
color: variantColors.text,
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
...(isMultiline
|
|
211
|
+
? { maxHeight: inputMaxHeight, textAlignVertical: 'top' as const }
|
|
212
|
+
: { textAlignVertical: 'center' as const }),
|
|
203
213
|
},
|
|
204
214
|
style,
|
|
205
215
|
]}
|
|
@@ -216,6 +226,8 @@ export function ComposePillTextField({
|
|
|
216
226
|
);
|
|
217
227
|
}
|
|
218
228
|
|
|
229
|
+
export type ComposePillEndActionPlacement = 'outside' | 'inside';
|
|
230
|
+
|
|
219
231
|
export type ComposePillInputProps = {
|
|
220
232
|
value: string;
|
|
221
233
|
onChangeText: (text: string) => void;
|
|
@@ -226,14 +238,33 @@ export type ComposePillInputProps = {
|
|
|
226
238
|
onFocus?: () => void;
|
|
227
239
|
onBlur?: () => void;
|
|
228
240
|
inputRef?: ComposePillTextInputRef;
|
|
241
|
+
/** Inside the pill (camera / location circle, etc.). */
|
|
229
242
|
leading?: ReactNode;
|
|
243
|
+
/** Before the pill (avatar in comments). */
|
|
244
|
+
leadingOutside?: ReactNode;
|
|
230
245
|
trailing?: ReactNode;
|
|
231
246
|
endAction?: ReactNode;
|
|
247
|
+
/**
|
|
248
|
+
* `outside` — send circle beside the pill.
|
|
249
|
+
* `inside` — action nested in the pill.
|
|
250
|
+
* Implied `inside` when `actionSwap` is on.
|
|
251
|
+
*/
|
|
252
|
+
endActionPlacement?: ComposePillEndActionPlacement;
|
|
253
|
+
/**
|
|
254
|
+
* System-wide Instagram composer: hide `trailing` when the field has text and
|
|
255
|
+
* show `endAction` in its place inside the pill (with a short layout animation).
|
|
256
|
+
*/
|
|
257
|
+
actionSwap?: boolean;
|
|
232
258
|
replyPreview?: ComposePillReplyPreview | null;
|
|
233
259
|
style?: StyleProp<ViewStyle>;
|
|
234
260
|
transparentWrap?: boolean;
|
|
235
|
-
/** Bare row without pill chrome — text field only grows inline
|
|
261
|
+
/** Bare row without pill chrome — text field only grows inline. */
|
|
236
262
|
bare?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Drop composer row insets — use when the pill sits inside an already-padded
|
|
265
|
+
* dock (profile quick message beside FABs).
|
|
266
|
+
*/
|
|
267
|
+
flush?: boolean;
|
|
237
268
|
rowAlignItems?: 'center' | 'flex-end';
|
|
238
269
|
textInputProps?: Omit<
|
|
239
270
|
ComposePillTextFieldProps,
|
|
@@ -345,15 +376,23 @@ export function ComposePillActionButton({
|
|
|
345
376
|
children,
|
|
346
377
|
onPress,
|
|
347
378
|
disabled,
|
|
379
|
+
inset = false,
|
|
348
380
|
}: {
|
|
349
381
|
label: string;
|
|
350
382
|
children: ReactNode;
|
|
351
383
|
onPress?: () => void;
|
|
352
384
|
disabled?: boolean;
|
|
385
|
+
/** Smaller filled circle for use inside the composer pill. */
|
|
386
|
+
inset?: boolean;
|
|
353
387
|
}) {
|
|
388
|
+
const size = inset ? ACTION_BUTTON_SIZE_INSET : ACTION_BUTTON_SIZE;
|
|
354
389
|
return (
|
|
355
390
|
<Pressable
|
|
356
|
-
style={[
|
|
391
|
+
style={[
|
|
392
|
+
styles.actionButton,
|
|
393
|
+
{ width: size, height: size, borderRadius: size / 2 },
|
|
394
|
+
disabled && styles.actionButtonDisabled,
|
|
395
|
+
]}
|
|
357
396
|
onPress={onPress}
|
|
358
397
|
disabled={disabled || !onPress}
|
|
359
398
|
accessibilityRole="button"
|
|
@@ -375,18 +414,37 @@ export function ComposePillInput({
|
|
|
375
414
|
onBlur,
|
|
376
415
|
inputRef,
|
|
377
416
|
leading,
|
|
417
|
+
leadingOutside,
|
|
378
418
|
trailing,
|
|
379
419
|
endAction,
|
|
420
|
+
endActionPlacement = 'outside',
|
|
421
|
+
actionSwap = false,
|
|
380
422
|
replyPreview,
|
|
381
423
|
style,
|
|
382
424
|
transparentWrap = false,
|
|
383
425
|
bare = false,
|
|
426
|
+
flush = false,
|
|
384
427
|
rowAlignItems = bare ? 'center' : 'flex-end',
|
|
385
428
|
textInputProps,
|
|
386
429
|
}: ComposePillInputProps) {
|
|
387
430
|
const layout = useComposePillLineLayout(value, { maxLines });
|
|
388
431
|
const { isMultiline, pillHeight, resetLineCount } = layout;
|
|
389
432
|
const variantColors = composePillVariantColors(variant);
|
|
433
|
+
const hasText = Boolean(value.trim());
|
|
434
|
+
const endActionInside = (actionSwap || endActionPlacement === 'inside') && !bare;
|
|
435
|
+
const visibleTrailing = actionSwap && hasText ? null : trailing;
|
|
436
|
+
const visibleEndAction = actionSwap && !hasText ? null : endAction;
|
|
437
|
+
const hadTextRef = useRef(hasText);
|
|
438
|
+
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!actionSwap) {
|
|
441
|
+
hadTextRef.current = hasText;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (hadTextRef.current === hasText) return;
|
|
445
|
+
hadTextRef.current = hasText;
|
|
446
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
447
|
+
}, [actionSwap, hasText]);
|
|
390
448
|
|
|
391
449
|
const handleChange = useCallback(
|
|
392
450
|
(text: string) => {
|
|
@@ -415,6 +473,14 @@ export function ComposePillInput({
|
|
|
415
473
|
/>
|
|
416
474
|
);
|
|
417
475
|
|
|
476
|
+
const trailingSlot =
|
|
477
|
+
visibleTrailing || (endActionInside && visibleEndAction) ? (
|
|
478
|
+
<View style={[styles.trailingIcons, isMultiline && styles.trailingIconsExpanded]}>
|
|
479
|
+
{visibleTrailing}
|
|
480
|
+
{endActionInside ? visibleEndAction : null}
|
|
481
|
+
</View>
|
|
482
|
+
) : null;
|
|
483
|
+
|
|
418
484
|
return (
|
|
419
485
|
<View
|
|
420
486
|
style={[
|
|
@@ -426,12 +492,20 @@ export function ComposePillInput({
|
|
|
426
492
|
>
|
|
427
493
|
{replyPreview ? <ComposePillReplyBar preview={replyPreview} variant={variant} /> : null}
|
|
428
494
|
|
|
429
|
-
<View
|
|
495
|
+
<View
|
|
496
|
+
style={[
|
|
497
|
+
styles.composerRow,
|
|
498
|
+
flush && styles.composerRowFlush,
|
|
499
|
+
{ alignItems: rowAlignItems },
|
|
500
|
+
]}
|
|
501
|
+
>
|
|
502
|
+
{leadingOutside}
|
|
430
503
|
{bare ? (
|
|
431
504
|
<>
|
|
432
505
|
{leading}
|
|
433
506
|
{textField}
|
|
434
|
-
{
|
|
507
|
+
{visibleTrailing}
|
|
508
|
+
{!endActionInside ? visibleEndAction : null}
|
|
435
509
|
</>
|
|
436
510
|
) : (
|
|
437
511
|
<View
|
|
@@ -446,20 +520,17 @@ export function ComposePillInput({
|
|
|
446
520
|
borderWidth: variant === 'overlay' || variant === 'chrome' ? 1 : 0,
|
|
447
521
|
},
|
|
448
522
|
variantColors.useChromeGlass && styles.inputPillGlass,
|
|
523
|
+
endActionInside && styles.inputPillWithInsetAction,
|
|
449
524
|
]}
|
|
450
525
|
>
|
|
451
526
|
{variantColors.useChromeGlass ? <ChromeGlassLayer /> : null}
|
|
452
527
|
{leading}
|
|
453
528
|
{textField}
|
|
454
|
-
{
|
|
455
|
-
<View style={[styles.trailingIcons, isMultiline && styles.trailingIconsExpanded]}>
|
|
456
|
-
{trailing}
|
|
457
|
-
</View>
|
|
458
|
-
) : null}
|
|
529
|
+
{trailingSlot}
|
|
459
530
|
</View>
|
|
460
531
|
)}
|
|
461
532
|
|
|
462
|
-
{
|
|
533
|
+
{!endActionInside ? visibleEndAction : null}
|
|
463
534
|
</View>
|
|
464
535
|
</View>
|
|
465
536
|
);
|
|
@@ -525,6 +596,11 @@ const styles = StyleSheet.create({
|
|
|
525
596
|
paddingHorizontal: spacing.sm,
|
|
526
597
|
paddingTop: spacing.sm,
|
|
527
598
|
},
|
|
599
|
+
composerRowFlush: {
|
|
600
|
+
paddingHorizontal: 0,
|
|
601
|
+
paddingTop: 0,
|
|
602
|
+
gap: 0,
|
|
603
|
+
},
|
|
528
604
|
inputPill: {
|
|
529
605
|
flex: 1,
|
|
530
606
|
flexDirection: 'row',
|
|
@@ -532,6 +608,11 @@ const styles = StyleSheet.create({
|
|
|
532
608
|
paddingRight: spacing.xs,
|
|
533
609
|
overflow: 'hidden',
|
|
534
610
|
},
|
|
611
|
+
inputPillWithInsetAction: {
|
|
612
|
+
paddingLeft: spacing.xs,
|
|
613
|
+
paddingRight: spacing.xs,
|
|
614
|
+
gap: spacing.xs,
|
|
615
|
+
},
|
|
535
616
|
inputPillGlass: {},
|
|
536
617
|
inputPillSingle: {
|
|
537
618
|
borderRadius: m3Shape.cornerFull,
|
|
@@ -563,9 +644,33 @@ const styles = StyleSheet.create({
|
|
|
563
644
|
lineHeight: LINE_HEIGHT,
|
|
564
645
|
fontFamily: fonts.regular,
|
|
565
646
|
paddingHorizontal: spacing.xs,
|
|
647
|
+
zIndex: 1,
|
|
648
|
+
},
|
|
649
|
+
/** Single-line: match pill height so placeholder/caret sit optically centered with inset actions. */
|
|
650
|
+
inputSingleLine: {
|
|
651
|
+
height: PILL_MIN_HEIGHT,
|
|
652
|
+
minHeight: PILL_MIN_HEIGHT,
|
|
653
|
+
maxHeight: PILL_MIN_HEIGHT,
|
|
654
|
+
paddingTop: 0,
|
|
655
|
+
paddingBottom: 0,
|
|
656
|
+
...Platform.select({
|
|
657
|
+
ios: {
|
|
658
|
+
// Multiline TextInput on iOS ignores textAlignVertical — pad to center the glyph.
|
|
659
|
+
paddingTop: Math.max(0, (PILL_MIN_HEIGHT - LINE_HEIGHT) / 2 - 1),
|
|
660
|
+
paddingBottom: Math.max(0, (PILL_MIN_HEIGHT - LINE_HEIGHT) / 2 + 1),
|
|
661
|
+
},
|
|
662
|
+
android: {
|
|
663
|
+
textAlignVertical: 'center',
|
|
664
|
+
},
|
|
665
|
+
default: {
|
|
666
|
+
paddingTop: VERTICAL_PAD,
|
|
667
|
+
paddingBottom: VERTICAL_PAD,
|
|
668
|
+
},
|
|
669
|
+
}),
|
|
670
|
+
},
|
|
671
|
+
inputMultiline: {
|
|
566
672
|
paddingTop: VERTICAL_PAD,
|
|
567
673
|
paddingBottom: VERTICAL_PAD,
|
|
568
|
-
zIndex: 1,
|
|
569
674
|
},
|
|
570
675
|
bareInput: {
|
|
571
676
|
minHeight: 40,
|
|
@@ -573,12 +678,10 @@ const styles = StyleSheet.create({
|
|
|
573
678
|
paddingBottom: spacing.xs,
|
|
574
679
|
},
|
|
575
680
|
actionButton: {
|
|
576
|
-
width: ACTION_BUTTON_SIZE,
|
|
577
|
-
height: ACTION_BUTTON_SIZE,
|
|
578
|
-
borderRadius: ACTION_BUTTON_SIZE / 2,
|
|
579
681
|
backgroundColor: m3Colors.primary,
|
|
580
682
|
alignItems: 'center',
|
|
581
683
|
justifyContent: 'center',
|
|
684
|
+
zIndex: 1,
|
|
582
685
|
},
|
|
583
686
|
actionButtonDisabled: {
|
|
584
687
|
opacity: 0.45,
|
|
@@ -586,3 +689,4 @@ const styles = StyleSheet.create({
|
|
|
586
689
|
});
|
|
587
690
|
|
|
588
691
|
export const COMPOSE_PILL_ACTION_BUTTON_SIZE = ACTION_BUTTON_SIZE;
|
|
692
|
+
export const COMPOSE_PILL_ACTION_BUTTON_SIZE_INSET = ACTION_BUTTON_SIZE_INSET;
|
package/src/renderer/ui/index.ts
CHANGED
|
@@ -116,9 +116,11 @@ export {
|
|
|
116
116
|
ComposePillReplyBar,
|
|
117
117
|
ComposePillTextField,
|
|
118
118
|
COMPOSE_PILL_ACTION_BUTTON_SIZE,
|
|
119
|
+
COMPOSE_PILL_ACTION_BUTTON_SIZE_INSET,
|
|
119
120
|
countComposePillLines,
|
|
120
121
|
pillHeightForLineCount,
|
|
121
122
|
useComposePillLineLayout,
|
|
123
|
+
type ComposePillEndActionPlacement,
|
|
122
124
|
type ComposePillInputProps,
|
|
123
125
|
type ComposePillLineLayout,
|
|
124
126
|
type ComposePillLineLayoutState,
|