@proulr/sdui-components 0.1.56 → 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,6 @@
1
1
  {
2
2
  "name": "@proulr/sdui-components",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "main": "./src/renderer/index.ts",
6
6
  "types": "./src/renderer/index.ts",
@@ -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,6 +20,10 @@ 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;
22
28
  /** Filled action circle when nested inside the pill (Instagram-style composer). */
23
29
  const ACTION_BUTTON_SIZE_INSET = 36;
@@ -198,10 +204,12 @@ export function ComposePillTextField({
198
204
  editable={editable}
199
205
  style={[
200
206
  styles.input,
207
+ isMultiline ? styles.inputMultiline : styles.inputSingleLine,
201
208
  {
202
209
  color: variantColors.text,
203
- maxHeight: inputMaxHeight,
204
- textAlignVertical: isMultiline ? 'top' : 'center',
210
+ ...(isMultiline
211
+ ? { maxHeight: inputMaxHeight, textAlignVertical: 'top' as const }
212
+ : { textAlignVertical: 'center' as const }),
205
213
  },
206
214
  style,
207
215
  ]}
@@ -230,19 +238,33 @@ export type ComposePillInputProps = {
230
238
  onFocus?: () => void;
231
239
  onBlur?: () => void;
232
240
  inputRef?: ComposePillTextInputRef;
241
+ /** Inside the pill (camera / location circle, etc.). */
233
242
  leading?: ReactNode;
243
+ /** Before the pill (avatar in comments). */
244
+ leadingOutside?: ReactNode;
234
245
  trailing?: ReactNode;
235
246
  endAction?: ReactNode;
236
247
  /**
237
- * `outside` — send/mic circle beside the pill (legacy / story reply).
238
- * `inside` — action nested in the pill (Instagram-style chat composer).
248
+ * `outside` — send circle beside the pill.
249
+ * `inside` — action nested in the pill.
250
+ * Implied `inside` when `actionSwap` is on.
239
251
  */
240
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;
241
258
  replyPreview?: ComposePillReplyPreview | null;
242
259
  style?: StyleProp<ViewStyle>;
243
260
  transparentWrap?: boolean;
244
- /** Bare row without pill chrome — text field only grows inline (ex.: comentários). */
261
+ /** Bare row without pill chrome — text field only grows inline. */
245
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;
246
268
  rowAlignItems?: 'center' | 'flex-end';
247
269
  textInputProps?: Omit<
248
270
  ComposePillTextFieldProps,
@@ -392,20 +414,37 @@ export function ComposePillInput({
392
414
  onBlur,
393
415
  inputRef,
394
416
  leading,
417
+ leadingOutside,
395
418
  trailing,
396
419
  endAction,
397
420
  endActionPlacement = 'outside',
421
+ actionSwap = false,
398
422
  replyPreview,
399
423
  style,
400
424
  transparentWrap = false,
401
425
  bare = false,
426
+ flush = false,
402
427
  rowAlignItems = bare ? 'center' : 'flex-end',
403
428
  textInputProps,
404
429
  }: ComposePillInputProps) {
405
430
  const layout = useComposePillLineLayout(value, { maxLines });
406
431
  const { isMultiline, pillHeight, resetLineCount } = layout;
407
432
  const variantColors = composePillVariantColors(variant);
408
- const endActionInside = endActionPlacement === 'inside' && !bare;
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]);
409
448
 
410
449
  const handleChange = useCallback(
411
450
  (text: string) => {
@@ -435,10 +474,10 @@ export function ComposePillInput({
435
474
  );
436
475
 
437
476
  const trailingSlot =
438
- trailing || (endActionInside && endAction) ? (
477
+ visibleTrailing || (endActionInside && visibleEndAction) ? (
439
478
  <View style={[styles.trailingIcons, isMultiline && styles.trailingIconsExpanded]}>
440
- {trailing}
441
- {endActionInside ? endAction : null}
479
+ {visibleTrailing}
480
+ {endActionInside ? visibleEndAction : null}
442
481
  </View>
443
482
  ) : null;
444
483
 
@@ -453,12 +492,20 @@ export function ComposePillInput({
453
492
  >
454
493
  {replyPreview ? <ComposePillReplyBar preview={replyPreview} variant={variant} /> : null}
455
494
 
456
- <View style={[styles.composerRow, { alignItems: rowAlignItems }]}>
495
+ <View
496
+ style={[
497
+ styles.composerRow,
498
+ flush && styles.composerRowFlush,
499
+ { alignItems: rowAlignItems },
500
+ ]}
501
+ >
502
+ {leadingOutside}
457
503
  {bare ? (
458
504
  <>
459
505
  {leading}
460
506
  {textField}
461
- {trailing}
507
+ {visibleTrailing}
508
+ {!endActionInside ? visibleEndAction : null}
462
509
  </>
463
510
  ) : (
464
511
  <View
@@ -483,7 +530,7 @@ export function ComposePillInput({
483
530
  </View>
484
531
  )}
485
532
 
486
- {!endActionInside ? endAction : null}
533
+ {!endActionInside ? visibleEndAction : null}
487
534
  </View>
488
535
  </View>
489
536
  );
@@ -549,6 +596,11 @@ const styles = StyleSheet.create({
549
596
  paddingHorizontal: spacing.sm,
550
597
  paddingTop: spacing.sm,
551
598
  },
599
+ composerRowFlush: {
600
+ paddingHorizontal: 0,
601
+ paddingTop: 0,
602
+ gap: 0,
603
+ },
552
604
  inputPill: {
553
605
  flex: 1,
554
606
  flexDirection: 'row',
@@ -592,9 +644,33 @@ const styles = StyleSheet.create({
592
644
  lineHeight: LINE_HEIGHT,
593
645
  fontFamily: fonts.regular,
594
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: {
595
672
  paddingTop: VERTICAL_PAD,
596
673
  paddingBottom: VERTICAL_PAD,
597
- zIndex: 1,
598
674
  },
599
675
  bareInput: {
600
676
  minHeight: 40,