@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,6 @@
1
1
  {
2
2
  "name": "@proulr/sdui-components",
3
- "version": "0.1.55",
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,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
- maxHeight: inputMaxHeight,
202
- textAlignVertical: isMultiline ? 'top' : 'center',
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 (ex.: comentários). */
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={[styles.actionButton, disabled && styles.actionButtonDisabled]}
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 style={[styles.composerRow, { alignItems: rowAlignItems }]}>
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
- {trailing}
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
- {trailing ? (
455
- <View style={[styles.trailingIcons, isMultiline && styles.trailingIconsExpanded]}>
456
- {trailing}
457
- </View>
458
- ) : null}
529
+ {trailingSlot}
459
530
  </View>
460
531
  )}
461
532
 
462
- {endAction}
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;
@@ -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,