@proulr/sdui-components 0.1.5 → 0.1.7

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,12 +1,15 @@
1
1
  {
2
2
  "name": "@proulr/sdui-components",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "main": "./src/renderer/index.ts",
5
5
  "types": "./src/renderer/index.ts",
6
6
  "exports": {
7
7
  "./bdc": "./src/bdc/index.ts",
8
8
  "./bdc/jsx-runtime": "./src/bdc/jsx-runtime.ts",
9
- "./renderer": "./src/renderer/index.ts"
9
+ "./renderer": "./src/renderer/index.ts",
10
+ "./ui": "./src/renderer/ui/index.ts",
11
+ "./primitives": "./src/renderer/ui/primitives.ts",
12
+ "./theme": "./src/renderer/theme/proulr-theme.ts"
10
13
  },
11
14
  "files": [
12
15
  "src"
@@ -16,6 +19,7 @@
16
19
  "url": "git+https://github.com/saviorbp/proulr-sdui-components.git"
17
20
  },
18
21
  "scripts": {
22
+ "prepare": "husky",
19
23
  "typecheck": "tsc --noEmit",
20
24
  "prepublishOnly": "npm run typecheck",
21
25
  "lint": "eslint .",
@@ -57,10 +61,16 @@
57
61
  "eslint": "^9.0.0",
58
62
  "eslint-config-expo": "~56.0.4",
59
63
  "expo-linear-gradient": "~56.0.4",
64
+ "globals": "^16.0.0",
65
+ "husky": "^9.1.7",
66
+ "lint-staged": "^16.4.0",
60
67
  "react": "19.2.3",
61
68
  "react-native": "0.85.3",
62
69
  "react-native-google-mobile-ads": "^16.3.4",
63
70
  "react-native-safe-area-context": "~5.7.0",
64
71
  "typescript": "~6.0.3"
72
+ },
73
+ "engines": {
74
+ "node": ">=22"
65
75
  }
66
76
  }
@@ -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,
@@ -0,0 +1,62 @@
1
+ import { Pressable, StyleSheet } from 'react-native';
2
+ import { AppIcon, type AppIconName, APP_ICON_SIZE } from './app-icon';
3
+ import { m3Colors, spacing } from '../theme/proulr-theme';
4
+
5
+ type AppChromeIconButtonProps = {
6
+ label: string;
7
+ icon: AppIconName;
8
+ onPress?: () => void;
9
+ disabled?: boolean;
10
+ color?: string;
11
+ size?: number;
12
+ /** Header chrome pill — circular bordered target. */
13
+ variant?: 'header' | 'plain';
14
+ };
15
+
16
+ export function AppChromeIconButton({
17
+ label,
18
+ icon,
19
+ onPress,
20
+ disabled,
21
+ color = m3Colors.primary,
22
+ size = APP_ICON_SIZE.chromeHeader,
23
+ variant = 'header',
24
+ }: AppChromeIconButtonProps) {
25
+ return (
26
+ <Pressable
27
+ onPress={onPress}
28
+ disabled={disabled || !onPress}
29
+ hitSlop={8}
30
+ accessibilityRole="button"
31
+ accessibilityLabel={label}
32
+ style={[
33
+ variant === 'header' ? styles.headerBtn : styles.plainBtn,
34
+ disabled && styles.disabled,
35
+ ]}
36
+ >
37
+ <AppIcon name={icon} color={color} size={size} />
38
+ </Pressable>
39
+ );
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ headerBtn: {
44
+ width: 45,
45
+ height: 45,
46
+ borderRadius: 22.5,
47
+ borderWidth: 2,
48
+ borderColor: 'rgba(255,255,255,0.22)',
49
+ margin: spacing.xs,
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ },
53
+ plainBtn: {
54
+ width: 40,
55
+ height: 40,
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ },
59
+ disabled: {
60
+ opacity: 0.4,
61
+ },
62
+ });
@@ -0,0 +1,60 @@
1
+ import { Pressable, StyleSheet, Text } from 'react-native';
2
+ import { colors, fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
3
+
4
+ export function Chip({
5
+ label,
6
+ active,
7
+ onPress,
8
+ variant = 'default',
9
+ }: {
10
+ label: string;
11
+ active?: boolean;
12
+ onPress?: () => void;
13
+ variant?: 'default' | 'onHeader';
14
+ }) {
15
+ const isHeader = variant === 'onHeader';
16
+
17
+ const containerStyle = active
18
+ ? isHeader
19
+ ? { backgroundColor: m3Colors.onPrimary, borderColor: m3Colors.onPrimary }
20
+ : { backgroundColor: m3Colors.secondaryContainer, borderColor: m3Colors.outline }
21
+ : isHeader
22
+ ? { backgroundColor: colors.surfaceGlass, borderColor: 'rgba(255,255,255,0.28)' }
23
+ : { backgroundColor: m3Colors.surfaceContainerHigh, borderColor: m3Colors.outline };
24
+
25
+ const textColor = active
26
+ ? isHeader
27
+ ? colors.headerStart
28
+ : m3Colors.onSecondaryContainer
29
+ : isHeader
30
+ ? colors.textOnAccent
31
+ : m3Colors.onSurfaceVariant;
32
+
33
+ return (
34
+ <Pressable onPress={onPress} style={[styles.chip, isHeader && styles.chipOnHeader, containerStyle]}>
35
+ <Text
36
+ style={{
37
+ color: textColor,
38
+ fontSize: isHeader ? 12 : 13,
39
+ fontFamily: fonts.semibold,
40
+ }}
41
+ >
42
+ {label}
43
+ </Text>
44
+ </Pressable>
45
+ );
46
+ }
47
+
48
+ const styles = StyleSheet.create({
49
+ chip: {
50
+ paddingHorizontal: spacing.sm,
51
+ paddingVertical: spacing.sm,
52
+ borderRadius: m3Shape.cornerSmall,
53
+ borderWidth: 1,
54
+ },
55
+ chipOnHeader: {
56
+ borderRadius: m3Shape.cornerFull,
57
+ paddingHorizontal: spacing.sm + 2,
58
+ paddingVertical: spacing.xs,
59
+ },
60
+ });
@@ -0,0 +1,22 @@
1
+ import { StyleSheet, Text, type TextProps } from 'react-native';
2
+ import { fonts, m3Colors, spacing } from '../theme/proulr-theme';
3
+
4
+ type HintTextProps = TextProps & {
5
+ spaced?: boolean;
6
+ };
7
+
8
+ export function HintText({ spaced, style, ...props }: HintTextProps) {
9
+ return <Text style={[styles.hint, spaced && styles.spaced, style]} {...props} />;
10
+ }
11
+
12
+ const styles = StyleSheet.create({
13
+ hint: {
14
+ color: m3Colors.onSurfaceVariant,
15
+ fontSize: 12,
16
+ lineHeight: 17,
17
+ fontFamily: fonts.regular,
18
+ },
19
+ spaced: {
20
+ marginTop: spacing.sm,
21
+ },
22
+ });
@@ -1,27 +1,33 @@
1
- import React from 'react';
2
1
  import { Pressable, StyleSheet, Text, View } from 'react-native';
2
+ import { AppIcon, type AppIconName, APP_ICON_SIZE } from './app-icon';
3
3
  import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
4
4
 
5
5
  export function InfoBanner({
6
6
  message,
7
7
  icon = 'ℹ',
8
+ appIcon,
8
9
  onDismiss,
9
10
  }: {
10
11
  message: string;
11
12
  icon?: string;
13
+ appIcon?: AppIconName;
12
14
  onDismiss?: () => void;
13
15
  }) {
14
16
  return (
15
17
  <View style={styles.infoBanner}>
16
18
  <View style={styles.infoIconWrap}>
17
- <Text style={styles.infoIcon}>{icon}</Text>
19
+ {appIcon ? (
20
+ <AppIcon name={appIcon} size={APP_ICON_SIZE.banner} color={colors.accentText} />
21
+ ) : (
22
+ <Text style={styles.infoIcon}>{icon}</Text>
23
+ )}
18
24
  </View>
19
25
  <Text style={styles.infoText}>{message}</Text>
20
- {onDismiss && (
26
+ {onDismiss ? (
21
27
  <Pressable onPress={onDismiss} hitSlop={8}>
22
- <Text style={styles.infoDismiss}>✕</Text>
28
+ <AppIcon name="close" size={14} color={colors.textMuted} />
23
29
  </Pressable>
24
- )}
30
+ ) : null}
25
31
  </View>
26
32
  );
27
33
  }
@@ -47,5 +53,4 @@ const styles = StyleSheet.create({
47
53
  },
48
54
  infoIcon: { fontSize: 16, color: colors.accentText },
49
55
  infoText: { flex: 1, color: colors.textSecondary, fontSize: 13, lineHeight: 18, fontFamily: fonts.medium },
50
- infoDismiss: { color: colors.textMuted, fontSize: 16 },
51
56
  });
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+ import { StyleSheet, Text } from 'react-native';
3
+ import { colors, fonts, spacing } from '../theme/proulr-theme';
4
+
5
+ export function Label({ children }: { children: ReactNode }) {
6
+ return <Text style={styles.label}>{children}</Text>;
7
+ }
8
+
9
+ const styles = StyleSheet.create({
10
+ label: {
11
+ color: colors.textSecondary,
12
+ fontSize: 12,
13
+ fontFamily: fonts.bold,
14
+ letterSpacing: 0.8,
15
+ marginTop: spacing.sm,
16
+ marginBottom: spacing.sm,
17
+ },
18
+ });
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+ import { colors, fonts, m3Colors, spacing } from '../theme/proulr-theme';
4
+
5
+ export function ListItem({
6
+ avatar,
7
+ title,
8
+ subtitle,
9
+ trailing,
10
+ onPress,
11
+ showDivider,
12
+ showChevron,
13
+ }: {
14
+ avatar?: ReactNode;
15
+ title: string;
16
+ subtitle?: string;
17
+ trailing?: ReactNode;
18
+ onPress?: () => void;
19
+ showDivider?: boolean;
20
+ showChevron?: boolean;
21
+ }) {
22
+ const navChevron = onPress && showChevron !== false && !trailing;
23
+
24
+ const inner = (
25
+ <>
26
+ {avatar ? <View style={styles.listAvatar}>{avatar}</View> : null}
27
+ <View style={styles.listContent}>
28
+ <Text style={styles.listTitle} numberOfLines={1}>
29
+ {title}
30
+ </Text>
31
+ {subtitle ? (
32
+ <Text style={styles.listSubtitle} numberOfLines={1}>
33
+ {subtitle}
34
+ </Text>
35
+ ) : null}
36
+ </View>
37
+ {navChevron ? <Text style={styles.chevron}>{'\u203a'}</Text> : null}
38
+ {trailing ? <View style={styles.listTrailing}>{trailing}</View> : null}
39
+ </>
40
+ );
41
+
42
+ if (onPress) {
43
+ return (
44
+ <Pressable style={[styles.listItem, showDivider && styles.listItemDivider]} onPress={onPress}>
45
+ {inner}
46
+ </Pressable>
47
+ );
48
+ }
49
+ return <View style={[styles.listItem, showDivider && styles.listItemDivider]}>{inner}</View>;
50
+ }
51
+
52
+ const styles = StyleSheet.create({
53
+ listItem: {
54
+ flexDirection: 'row',
55
+ alignItems: 'center',
56
+ gap: spacing.md,
57
+ paddingVertical: spacing.md,
58
+ },
59
+ listItemDivider: {
60
+ borderBottomWidth: StyleSheet.hairlineWidth,
61
+ borderBottomColor: colors.border,
62
+ },
63
+ listAvatar: {},
64
+ listContent: { flex: 1 },
65
+ listTitle: { color: colors.textPrimary, fontSize: 15, fontFamily: fonts.semibold },
66
+ listSubtitle: { color: colors.textSecondary, fontSize: 13, marginTop: 2 },
67
+ listTrailing: { alignItems: 'flex-end' },
68
+ chevron: { color: m3Colors.onSurfaceVariant, fontSize: 22 },
69
+ });
@@ -0,0 +1,21 @@
1
+ import { StyleSheet, View } from 'react-native';
2
+ import { colors, radius } from '../theme/proulr-theme';
3
+
4
+ export function ProgressBar({ progress, color = colors.accent }: { progress: number; color?: string }) {
5
+ const pct = Math.min(1, Math.max(0, progress));
6
+ return (
7
+ <View style={styles.progressTrack}>
8
+ <View style={[styles.progressFill, { width: `${pct * 100}%`, backgroundColor: color }]} />
9
+ </View>
10
+ );
11
+ }
12
+
13
+ const styles = StyleSheet.create({
14
+ progressTrack: {
15
+ height: 8,
16
+ borderRadius: radius.pill,
17
+ backgroundColor: colors.surface2,
18
+ overflow: 'hidden',
19
+ },
20
+ progressFill: { height: '100%', borderRadius: radius.pill },
21
+ });
@@ -0,0 +1,43 @@
1
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
2
+ import { colors, fonts, spacing } from '../theme/proulr-theme';
3
+
4
+ export function SectionHeader({
5
+ title,
6
+ action,
7
+ onAction,
8
+ }: {
9
+ title: string;
10
+ action?: string;
11
+ onAction?: () => void;
12
+ }) {
13
+ return (
14
+ <View style={styles.sectionHeaderRow}>
15
+ <Text style={styles.sectionHeader}>{title}</Text>
16
+ {action && onAction ? (
17
+ <Pressable onPress={onAction} hitSlop={8}>
18
+ <Text style={styles.sectionAction}>{action}</Text>
19
+ </Pressable>
20
+ ) : null}
21
+ </View>
22
+ );
23
+ }
24
+
25
+ const styles = StyleSheet.create({
26
+ sectionHeaderRow: {
27
+ flexDirection: 'row',
28
+ alignItems: 'center',
29
+ justifyContent: 'space-between',
30
+ marginBottom: spacing.sm,
31
+ marginTop: spacing.xl,
32
+ },
33
+ sectionHeader: {
34
+ color: colors.textSecondary,
35
+ fontSize: 14,
36
+ fontFamily: fonts.semibold,
37
+ },
38
+ sectionAction: {
39
+ color: colors.accentText,
40
+ fontSize: 13,
41
+ fontFamily: fonts.semibold,
42
+ },
43
+ });
@@ -0,0 +1,156 @@
1
+ import { useState } from 'react';
2
+ import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
+ import { AppButton } from './AppButton';
4
+ import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
5
+
6
+ function OptionModal({
7
+ title,
8
+ visible,
9
+ options,
10
+ selected,
11
+ multi,
12
+ onClose,
13
+ onSelect,
14
+ }: {
15
+ title: string;
16
+ visible: boolean;
17
+ options: string[];
18
+ selected: string[];
19
+ multi?: boolean;
20
+ onClose: () => void;
21
+ onSelect: (v: string) => void;
22
+ }) {
23
+ return (
24
+ <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
25
+ <Pressable style={styles.modalBackdrop} onPress={onClose}>
26
+ <Pressable style={styles.modalSheet}>
27
+ <Text style={styles.modalTitle}>{title}</Text>
28
+ <ScrollView style={{ maxHeight: 360 }}>
29
+ {options.map((opt) => {
30
+ const isSel = selected.includes(opt);
31
+ return (
32
+ <Pressable key={opt} style={styles.optionRow} onPress={() => onSelect(opt)}>
33
+ <Text style={{ color: isSel ? colors.accentText : colors.white, fontSize: 16 }}>{opt}</Text>
34
+ {isSel ? <Text style={{ color: colors.accent, fontSize: 16 }}>{'\u2713'}</Text> : null}
35
+ </Pressable>
36
+ );
37
+ })}
38
+ </ScrollView>
39
+ {multi ? (
40
+ <View style={{ marginTop: spacing.md }}>
41
+ <AppButton label="Pronto" onPress={onClose} />
42
+ </View>
43
+ ) : null}
44
+ </Pressable>
45
+ </Pressable>
46
+ </Modal>
47
+ );
48
+ }
49
+
50
+ export function SelectRow({
51
+ label,
52
+ value,
53
+ options,
54
+ onChange,
55
+ }: {
56
+ label: string;
57
+ value?: string | null;
58
+ options: string[];
59
+ onChange: (v: string) => void;
60
+ }) {
61
+ const [open, setOpen] = useState(false);
62
+ return (
63
+ <>
64
+ <Pressable style={styles.selectRow} onPress={() => setOpen(true)}>
65
+ <Text style={styles.selectLabel}>{label}</Text>
66
+ <Text style={styles.selectValue} numberOfLines={1}>
67
+ {value || 'Selecionar'}
68
+ </Text>
69
+ <Text style={styles.chevron}>{'\u203a'}</Text>
70
+ </Pressable>
71
+ <OptionModal
72
+ title={label}
73
+ visible={open}
74
+ options={options}
75
+ selected={value ? [value] : []}
76
+ onClose={() => setOpen(false)}
77
+ onSelect={(v) => {
78
+ onChange(v);
79
+ setOpen(false);
80
+ }}
81
+ />
82
+ </>
83
+ );
84
+ }
85
+
86
+ export function MultiSelectRow({
87
+ label,
88
+ values,
89
+ options,
90
+ onChange,
91
+ }: {
92
+ label: string;
93
+ values: string[];
94
+ options: string[];
95
+ onChange: (v: string[]) => void;
96
+ }) {
97
+ const [open, setOpen] = useState(false);
98
+ return (
99
+ <>
100
+ <Pressable style={styles.selectRow} onPress={() => setOpen(true)}>
101
+ <Text style={styles.selectLabel}>{label}</Text>
102
+ <Text style={styles.selectValue} numberOfLines={1}>
103
+ {values.length ? values.join(', ') : 'Selecionar'}
104
+ </Text>
105
+ <Text style={styles.chevron}>{'\u203a'}</Text>
106
+ </Pressable>
107
+ <OptionModal
108
+ title={label}
109
+ visible={open}
110
+ options={options}
111
+ selected={values}
112
+ multi
113
+ onClose={() => setOpen(false)}
114
+ onSelect={(v) => {
115
+ const set = new Set(values);
116
+ if (set.has(v)) set.delete(v);
117
+ else set.add(v);
118
+ onChange(Array.from(set));
119
+ }}
120
+ />
121
+ </>
122
+ );
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ selectRow: {
127
+ flexDirection: 'row',
128
+ alignItems: 'center',
129
+ gap: spacing.sm,
130
+ paddingHorizontal: spacing.sm,
131
+ paddingVertical: 15,
132
+ borderRadius: radius.lg,
133
+ backgroundColor: colors.surface2,
134
+ },
135
+ selectLabel: { flex: 1, color: colors.white, fontSize: 15, fontFamily: fonts.medium },
136
+ selectValue: { color: colors.textSecondary, fontSize: 14, maxWidth: 160 },
137
+ chevron: { color: colors.textSecondary, fontSize: 18 },
138
+ modalBackdrop: { flex: 1, backgroundColor: colors.overlay, justifyContent: 'flex-end' },
139
+ modalSheet: {
140
+ backgroundColor: colors.surface,
141
+ borderTopLeftRadius: radius.xl,
142
+ borderTopRightRadius: radius.xl,
143
+ padding: spacing.xl,
144
+ borderTopWidth: 1,
145
+ borderColor: colors.border,
146
+ },
147
+ modalTitle: { color: colors.white, fontSize: 18, fontFamily: fonts.bold, marginBottom: spacing.md },
148
+ optionRow: {
149
+ flexDirection: 'row',
150
+ justifyContent: 'space-between',
151
+ alignItems: 'center',
152
+ paddingVertical: 14,
153
+ borderBottomWidth: StyleSheet.hairlineWidth,
154
+ borderBottomColor: colors.border,
155
+ },
156
+ });
@@ -0,0 +1,33 @@
1
+ import { Fragment } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
4
+
5
+ export function StatRow({ stats }: { stats: { value: string; label: string; highlight?: boolean }[] }) {
6
+ return (
7
+ <View style={styles.statRow}>
8
+ {stats.map((s, i) => (
9
+ <Fragment key={s.label}>
10
+ {i > 0 ? <View style={styles.statDivider} /> : null}
11
+ <View style={styles.statItem}>
12
+ <Text style={[styles.statValue, s.highlight && { color: colors.accentText }]}>{s.value}</Text>
13
+ <Text style={styles.statLabel}>{s.label}</Text>
14
+ </View>
15
+ </Fragment>
16
+ ))}
17
+ </View>
18
+ );
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ statRow: {
23
+ flexDirection: 'row',
24
+ alignItems: 'center',
25
+ backgroundColor: colors.surface2,
26
+ borderRadius: radius.lg,
27
+ paddingVertical: spacing.sm,
28
+ },
29
+ statItem: { flex: 1, alignItems: 'center' },
30
+ statDivider: { width: 1, height: 36, backgroundColor: colors.border },
31
+ statValue: { color: colors.white, fontSize: 18, fontFamily: fonts.bold },
32
+ statLabel: { color: colors.textSecondary, fontSize: 12, marginTop: 2, fontFamily: fonts.medium },
33
+ });
@@ -1,10 +1,19 @@
1
1
  import React, { useState } from 'react';
2
- import { Pressable, StyleSheet, Text, TextInput, TextInputProps, View, ViewStyle } from 'react-native';
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ StyleProp,
6
+ TextInput,
7
+ TextInputProps,
8
+ View,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+ import { AppIcon, type AppIconName } from './app-icon';
3
12
  import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
4
13
 
5
14
  export function TextField(
6
15
  props: TextInputProps & {
7
- containerStyle?: ViewStyle;
16
+ containerStyle?: StyleProp<ViewStyle>;
8
17
  focused?: boolean;
9
18
  inputRef?: React.Ref<TextInput>;
10
19
  },
@@ -30,7 +39,7 @@ export function TextField(
30
39
 
31
40
  export function PasswordField(
32
41
  props: TextInputProps & {
33
- containerStyle?: ViewStyle;
42
+ containerStyle?: StyleProp<ViewStyle>;
34
43
  inputRef?: React.Ref<TextInput>;
35
44
  },
36
45
  ) {
@@ -51,8 +60,13 @@ export function PasswordField(
51
60
  hitSlop={8}
52
61
  style={styles.passwordToggle}
53
62
  accessibilityLabel={visible ? 'Ocultar senha' : 'Mostrar senha'}
63
+ accessibilityRole="button"
54
64
  >
55
- <Text style={styles.passwordToggleIcon}>{visible ? '🙈' : '👁'}</Text>
65
+ <AppIcon
66
+ name={(visible ? 'visibility-off' : 'visibility') as AppIconName}
67
+ size={20}
68
+ color={m3Colors.onSurfaceVariant}
69
+ />
56
70
  </Pressable>
57
71
  </View>
58
72
  );
@@ -77,5 +91,4 @@ const styles = StyleSheet.create({
77
91
  alignItems: 'center',
78
92
  justifyContent: 'center',
79
93
  },
80
- passwordToggleIcon: { fontSize: 18 },
81
94
  });
@@ -0,0 +1,56 @@
1
+ import { StyleSheet, Switch, Text, View } from 'react-native';
2
+ import { fonts, m3Colors, spacing } from '../theme/proulr-theme';
3
+
4
+ export function ToggleRow({
5
+ label,
6
+ hint,
7
+ value,
8
+ onChange,
9
+ disabled,
10
+ }: {
11
+ label: string;
12
+ hint?: string;
13
+ value: boolean;
14
+ onChange: (value: boolean) => void;
15
+ disabled?: boolean;
16
+ }) {
17
+ return (
18
+ <View style={styles.row}>
19
+ <View style={styles.copy}>
20
+ <Text style={styles.label}>{label}</Text>
21
+ {hint ? <Text style={styles.hint}>{hint}</Text> : null}
22
+ </View>
23
+ <Switch
24
+ value={value}
25
+ onValueChange={onChange}
26
+ disabled={disabled}
27
+ trackColor={{ false: m3Colors.surfaceContainerHighest, true: m3Colors.primary }}
28
+ thumbColor={m3Colors.onPrimary}
29
+ />
30
+ </View>
31
+ );
32
+ }
33
+
34
+ const styles = StyleSheet.create({
35
+ row: {
36
+ flexDirection: 'row',
37
+ alignItems: 'center',
38
+ gap: spacing.md,
39
+ paddingVertical: spacing.xs,
40
+ },
41
+ copy: {
42
+ flex: 1,
43
+ gap: spacing.xs,
44
+ },
45
+ label: {
46
+ color: m3Colors.onSurface,
47
+ fontSize: 15,
48
+ fontFamily: fonts.semibold,
49
+ },
50
+ hint: {
51
+ color: m3Colors.onSurfaceVariant,
52
+ fontSize: 12,
53
+ lineHeight: 17,
54
+ fontFamily: fonts.regular,
55
+ },
56
+ });
@@ -168,21 +168,26 @@ export function AppChromeMicGlyph({ color = m3Colors.onPrimary, size = 24 }: App
168
168
  }
169
169
 
170
170
  export function AppChromeSendGlyph({ color = m3Colors.onPrimary, size = 24 }: AppChromeGlyphProps) {
171
+ const wingSpan = size * 0.17;
172
+ const bodyLength = size * 0.5;
173
+
171
174
  return (
172
175
  <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
173
- <View
174
- style={{
175
- width: 0,
176
- height: 0,
177
- borderTopWidth: size * 0.24,
178
- borderBottomWidth: size * 0.24,
179
- borderLeftWidth: size * 0.38,
180
- borderTopColor: 'transparent',
181
- borderBottomColor: 'transparent',
182
- borderLeftColor: color,
183
- marginLeft: size * 0.1,
184
- }}
185
- />
176
+ <View style={{ transform: [{ rotate: '-42deg' }] }}>
177
+ <View
178
+ style={{
179
+ width: 0,
180
+ height: 0,
181
+ borderTopWidth: wingSpan,
182
+ borderBottomWidth: wingSpan,
183
+ borderLeftWidth: bodyLength,
184
+ borderTopColor: 'transparent',
185
+ borderBottomColor: 'transparent',
186
+ borderLeftColor: color,
187
+ marginLeft: size * 0.04,
188
+ }}
189
+ />
190
+ </View>
186
191
  </View>
187
192
  );
188
193
  }
@@ -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,9 @@
1
1
  export { AppButton } from './AppButton';
2
+ export { AppChromeIconButton } from './AppChromeIconButton';
2
3
  export { AppLoadingSpinner } from './AppLoadingSpinner';
3
4
  export { AuthStepShell } from './AuthStepShell';
4
5
  export { Card } from './Card';
6
+ export { Chip } from './Chip';
5
7
  export { ComposeAudiencePill } from './compose-audience-pill';
6
8
  export {
7
9
  ComposeCameraGlyph,
@@ -71,12 +73,37 @@ export { ComposeMediaShortcut, COMPOSE_MEDIA_TILE_SIZE } from './compose-media-s
71
73
  export { ComposeRecentMediaGrid, type ComposeRecentMediaItem } from './compose-recent-media-grid';
72
74
  export { ComposeScreenHeader } from './compose-screen-header';
73
75
  export { ComposeTextArea, ComposeMediaPreviewFrame } from './compose-text-area';
76
+ export {
77
+ ComposePillActionButton,
78
+ ComposePillIconButton,
79
+ ComposePillInput,
80
+ ComposePillReplyBar,
81
+ ComposePillTextField,
82
+ COMPOSE_PILL_ACTION_BUTTON_SIZE,
83
+ countComposePillLines,
84
+ pillHeightForLineCount,
85
+ useComposePillLineLayout,
86
+ type ComposePillInputProps,
87
+ type ComposePillLineLayout,
88
+ type ComposePillLineLayoutState,
89
+ type ComposePillReplyPreview,
90
+ type ComposePillTextFieldProps,
91
+ type ComposePillVariant,
92
+ } from './compose-pill-input';
74
93
  export { ComposeToolbar, ComposeToolbarIcon, COMPOSE_TOOLBAR_ICON_SIZE } from './compose-toolbar';
75
94
  export { ComposeReplyAudienceSheet, type ComposeReplyAudienceOption, type ComposeReplyAudienceValue } from './compose-reply-audience-sheet';
76
95
  export { ComposeVisibilityRow } from './compose-visibility-row';
77
96
  export { DateField } from './DateField';
97
+ export { HintText } from './HintText';
78
98
  export { FloatingCreateMenu, type FloatingCreateMenuAction } from './floating-create-menu';
79
99
  export { InfoBanner } from './InfoBanner';
100
+ export { Label } from './Label';
101
+ export { ListItem } from './ListItem';
102
+ export { ProgressBar } from './ProgressBar';
103
+ export { SectionHeader } from './SectionHeader';
104
+ export { SelectRow, MultiSelectRow } from './SelectRow';
105
+ export { StatRow } from './StatRow';
106
+ export { ToggleRow } from './ToggleRow';
80
107
  export { PasswordField, TextField } from './TextField';
81
108
  export {
82
109
  StoryCollageGlyph,
@@ -0,0 +1,16 @@
1
+ /** Barrel leve — só primitivos de formulário/lista (sem glyphs/compose/story). */
2
+ export { AppButton } from './AppButton';
3
+ export { AppLoadingSpinner } from './AppLoadingSpinner';
4
+ export { Card } from './Card';
5
+ export { Chip } from './Chip';
6
+ export { DateField } from './DateField';
7
+ export { HintText } from './HintText';
8
+ export { InfoBanner } from './InfoBanner';
9
+ export { Label } from './Label';
10
+ export { ListItem } from './ListItem';
11
+ export { ProgressBar } from './ProgressBar';
12
+ export { SectionHeader } from './SectionHeader';
13
+ export { SelectRow, MultiSelectRow } from './SelectRow';
14
+ export { StatRow } from './StatRow';
15
+ export { TextField, PasswordField } from './TextField';
16
+ export { ToggleRow } from './ToggleRow';