@proulr/sdui-components 0.1.73 → 0.1.76

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.73",
3
+ "version": "0.1.76",
4
4
  "type": "module",
5
5
  "main": "./src/renderer/index.ts",
6
6
  "types": "./src/renderer/index.ts",
@@ -71,7 +71,7 @@ function readProfile(data: Record<string, unknown>, ref?: string): DiscoverNearb
71
71
 
72
72
  function PresenceDot({ status }: { status: ReturnType<typeof getPresenceStatus> }) {
73
73
  const dotColor =
74
- status === 'online' ? m3Colors.online : status === 'recent' ? m3Colors.secondary : m3Colors.outline;
74
+ status === 'online' ? m3Colors.online : status === 'recent' ? m3Colors.presenceRecent : m3Colors.outline;
75
75
  return <View style={[styles.presenceDot, { backgroundColor: dotColor }]} />;
76
76
  }
77
77
 
@@ -89,9 +89,15 @@ export function SduiScrollBlock({ block, children }: SduiBlockRenderProps) {
89
89
  ? { paddingHorizontal: padding, gap }
90
90
  : { padding, gap };
91
91
 
92
+ const scrollViewProps = chromeScroll?.scrollViewProps ?? {};
93
+ const { ref: scrollRef, ...restScrollViewProps } = scrollViewProps as {
94
+ ref?: React.Ref<ScrollView>;
95
+ } & Record<string, unknown>;
96
+
92
97
  return (
93
98
  <SduiLayoutDebugFrame layer="scroll" style={{ flex: 1, minHeight: 0, alignSelf: 'stretch' }}>
94
99
  <ScrollView
100
+ ref={scrollRef}
95
101
  style={[
96
102
  styles.scroll,
97
103
  resolveViewStyleProps(block.props.style as Record<string, unknown>),
@@ -105,7 +111,7 @@ export function SduiScrollBlock({ block, children }: SduiBlockRenderProps) {
105
111
  : blockContentStyle
106
112
  }
107
113
  {...(chromeScroll?.scrollInsetProps ?? {})}
108
- {...(chromeScroll?.scrollViewProps ?? {})}
114
+ {...restScrollViewProps}
109
115
  onScroll={chromeScroll?.onScroll}
110
116
  scrollEventThrottle={chromeScroll?.onScroll || chromeScroll?.scrollViewProps ? 16 : undefined}
111
117
  refreshControl={chromeScroll?.refreshControl}
@@ -32,6 +32,8 @@ export const m3Colors = {
32
32
  scrim: 'rgba(0,0,0,0.78)',
33
33
  storyInteractionScrim: 'rgba(0,0,0,0.48)',
34
34
  online: '#34FF88',
35
+ /** Saiu recentemente (≤24h) — laranja, distinto do online verde e do ciano de marca. */
36
+ presenceRecent: '#FF9F0A',
35
37
  } as const;
36
38
 
37
39
  /** Accent/soft por reação Flash (conteúdo) — fonte única, sem hex solto nos apps. */
@@ -96,6 +98,9 @@ export const colors = {
96
98
  headerEnd: '#D946EF',
97
99
  online: '#34FF88',
98
100
  onlineSoft: 'rgba(52,255,136,0.18)',
101
+ /** Presence: recently active (not online). */
102
+ presenceRecent: '#FF9F0A',
103
+ presenceRecentSoft: 'rgba(255,159,10,0.22)',
99
104
  danger: '#FF4444',
100
105
  dangerSoft: 'rgba(255,68,68,0.22)',
101
106
  dangerSoft18: 'rgba(239,68,68,0.18)',
@@ -0,0 +1,108 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type DimensionValue, type StyleProp, type ViewStyle } from 'react-native';
3
+ import Animated, {
4
+ Easing,
5
+ ReduceMotion,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withRepeat,
9
+ withTiming,
10
+ } from 'react-native-reanimated';
11
+ import { m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
12
+
13
+ const PULSE_DURATION_MS = 900;
14
+ const PULSE_MIN_OPACITY = 0.45;
15
+
16
+ /**
17
+ * Pulso de opacidade compartilhado por cada bloco de skeleton. Respeita o
18
+ * "reduzir movimento" do sistema (fica estatico). Como `react-native-reanimated`
19
+ * ja e usado nos primitivos do pacote (ex.: RangeSlider), importamos direto.
20
+ */
21
+ function usePulseStyle() {
22
+ const progress = useSharedValue(PULSE_MIN_OPACITY);
23
+
24
+ useEffect(() => {
25
+ progress.value = withRepeat(
26
+ withTiming(1, {
27
+ duration: PULSE_DURATION_MS,
28
+ easing: Easing.inOut(Easing.ease),
29
+ reduceMotion: ReduceMotion.System,
30
+ }),
31
+ -1,
32
+ true,
33
+ );
34
+ }, [progress]);
35
+
36
+ return useAnimatedStyle(() => ({ opacity: progress.value }));
37
+ }
38
+
39
+ export type SkeletonProps = {
40
+ width?: DimensionValue;
41
+ height?: DimensionValue;
42
+ radius?: number;
43
+ style?: StyleProp<ViewStyle>;
44
+ };
45
+
46
+ /** Bloco tonal com shimmer — base de qualquer skeleton. */
47
+ export function Skeleton({
48
+ width = '100%',
49
+ height = 16,
50
+ radius = m3Shape.cornerSmall,
51
+ style,
52
+ }: SkeletonProps) {
53
+ const pulse = usePulseStyle();
54
+ return (
55
+ <Animated.View
56
+ accessibilityRole="none"
57
+ style={[
58
+ { width, height, borderRadius: radius, backgroundColor: m3Colors.surfaceContainerHigh },
59
+ pulse,
60
+ style,
61
+ ]}
62
+ />
63
+ );
64
+ }
65
+
66
+ export type SkeletonCircleProps = {
67
+ size?: number;
68
+ style?: StyleProp<ViewStyle>;
69
+ };
70
+
71
+ /** Placeholder circular (avatar). */
72
+ export function SkeletonCircle({ size = 48, style }: SkeletonCircleProps) {
73
+ return <Skeleton width={size} height={size} radius={size / 2} style={style} />;
74
+ }
75
+
76
+ export type SkeletonTextProps = {
77
+ lines?: number;
78
+ lineHeight?: number;
79
+ gap?: number;
80
+ /** Largura da ultima linha (efeito paragrafo). */
81
+ lastLineWidth?: DimensionValue;
82
+ style?: StyleProp<ViewStyle>;
83
+ };
84
+
85
+ /** Bloco de linhas de texto (ultima linha mais curta). */
86
+ export function SkeletonText({
87
+ lines = 3,
88
+ lineHeight = 12,
89
+ gap = spacing.xs,
90
+ lastLineWidth = '60%',
91
+ style,
92
+ }: SkeletonTextProps) {
93
+ return (
94
+ <View style={[{ gap }, style]}>
95
+ {Array.from({ length: Math.max(1, lines) }).map((_, index) => {
96
+ const isLast = index === lines - 1;
97
+ return (
98
+ <Skeleton
99
+ key={index}
100
+ height={lineHeight}
101
+ width={isLast && lines > 1 ? lastLineWidth : '100%'}
102
+ radius={m3Shape.cornerFull}
103
+ />
104
+ );
105
+ })}
106
+ </View>
107
+ );
108
+ }
@@ -1,5 +1,6 @@
1
1
  import React, { useState } from 'react';
2
2
  import {
3
+ Platform,
3
4
  Pressable,
4
5
  StyleSheet,
5
6
  StyleProp,
@@ -11,6 +12,14 @@ import {
11
12
  import { AppIcon, type AppIconName } from './app-icon';
12
13
  import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
13
14
 
15
+ const webInputLock =
16
+ Platform.OS === 'web'
17
+ ? ({
18
+ WebkitTextFillColor: m3Colors.onSurface,
19
+ caretColor: m3Colors.onSurface,
20
+ } as object)
21
+ : null;
22
+
14
23
  export function TextField(
15
24
  props: TextInputProps & {
16
25
  containerStyle?: StyleProp<ViewStyle>;
@@ -33,7 +42,7 @@ export function TextField(
33
42
  placeholderTextColor={m3Colors.onSurfaceVariant}
34
43
  multiline={multiline}
35
44
  textAlignVertical={multiline ? 'top' : 'center'}
36
- style={[styles.fieldInput, multiline && styles.fieldInputMultiline, style]}
45
+ style={[styles.fieldInput, multiline && styles.fieldInputMultiline, webInputLock, style]}
37
46
  {...rest}
38
47
  />
39
48
  </View>
@@ -54,7 +63,7 @@ export function PasswordField(
54
63
  <TextInput
55
64
  ref={inputRef}
56
65
  placeholderTextColor={m3Colors.onSurfaceVariant}
57
- style={[styles.fieldInput, styles.passwordInput]}
66
+ style={[styles.fieldInput, styles.passwordInput, webInputLock]}
58
67
  secureTextEntry={!visible}
59
68
  {...rest}
60
69
  />
@@ -219,6 +219,8 @@ export function ComposePillTextField({
219
219
  outlineStyle: 'none',
220
220
  resize: 'none',
221
221
  overflow: 'hidden',
222
+ WebkitTextFillColor: variantColors.text,
223
+ caretColor: variantColors.text,
222
224
  } as object)
223
225
  : {})}
224
226
  {...rest}
@@ -8,7 +8,7 @@ export function ComposeTextArea(props: TextInputProps) {
8
8
  placeholder="O que está acontecendo?"
9
9
  placeholderTextColor={m3Colors.onSurfaceVariant}
10
10
  multiline
11
- style={styles.input}
11
+ style={[styles.input, Platform.OS === 'web' ? styles.inputWeb : null]}
12
12
  {...(Platform.OS === 'android' ? { includeFontPadding: false } : {})}
13
13
  {...props}
14
14
  />
@@ -56,6 +56,12 @@ const styles = StyleSheet.create({
56
56
  textAlignVertical: 'top',
57
57
  paddingTop: spacing.xs,
58
58
  },
59
+ // Samsung/Xiaomi force-dark can leave caret text black on OLED — lock fill color on web.
60
+ inputWeb: {
61
+ WebkitTextFillColor: m3Colors.onSurface,
62
+ caretColor: m3Colors.onSurface,
63
+ outlineStyle: 'none',
64
+ } as object,
59
65
  previewWrap: {
60
66
  position: 'relative',
61
67
  marginTop: spacing.xs,
@@ -59,6 +59,14 @@ export { ProgressBar } from './ProgressBar';
59
59
  export { RangeSlider, type RangeSliderProps } from './RangeSlider';
60
60
  export { SectionHeader } from './SectionHeader';
61
61
  export { SelectRow, MultiSelectRow } from './SelectRow';
62
+ export {
63
+ Skeleton,
64
+ SkeletonCircle,
65
+ SkeletonText,
66
+ type SkeletonProps,
67
+ type SkeletonCircleProps,
68
+ type SkeletonTextProps,
69
+ } from './Skeleton';
62
70
  export { StatRow } from './StatRow';
63
71
  export { ToggleRow } from './ToggleRow';
64
72
  export { PasswordField, TextField } from './TextField';