@proulr/sdui-components 0.1.85 → 0.1.87
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 +1 -1
- package/src/bdc/components/discover.tsx +1 -4
- package/src/bdc/components/forms.tsx +4 -0
- package/src/bdc/components/inbox.tsx +4 -0
- package/src/renderer/SduiScreenRenderer.tsx +14 -2
- package/src/renderer/blocks/SduiDiscoverBlocks.tsx +50 -34
- package/src/renderer/blocks/SduiInboxBlocks.tsx +26 -11
- package/src/renderer/blocks/SduiLayoutBlocks.tsx +39 -46
- package/src/renderer/blocks/SduiNativeAdBlock.tsx +7 -8
- package/src/renderer/blocks/SduiSocialBlocks.tsx +4 -27
- package/src/renderer/lib/discover-profile-formatters.test.ts +7 -0
- package/src/renderer/lib/discover-profile-formatters.ts +6 -3
- package/src/renderer/sdui-hybrid-renderer-context.tsx +6 -1
- package/src/renderer/theme/proulr-theme.test.ts +107 -0
- package/src/renderer/theme/proulr-theme.ts +51 -9
- package/src/renderer/ui/AppButton.tsx +54 -28
- package/src/renderer/ui/AppChromeIconButton.tsx +19 -7
- package/src/renderer/ui/AuthStepShell.tsx +1 -2
- package/src/renderer/ui/Chip.tsx +41 -34
- package/src/renderer/ui/ChipRow.tsx +7 -13
- package/src/renderer/ui/HintText.tsx +2 -4
- package/src/renderer/ui/InfoBanner.tsx +20 -3
- package/src/renderer/ui/Label.tsx +2 -3
- package/src/renderer/ui/ListItem.tsx +28 -4
- package/src/renderer/ui/RangeSlider.tsx +1 -2
- package/src/renderer/ui/SectionHeader.tsx +3 -4
- package/src/renderer/ui/SelectRow.tsx +113 -22
- package/src/renderer/ui/StatRow.tsx +3 -3
- package/src/renderer/ui/TextField.tsx +15 -11
- package/src/renderer/ui/ToggleRow.tsx +38 -11
- package/src/renderer/ui/compose-audience-pill.tsx +2 -3
- package/src/renderer/ui/compose-media-carousel.tsx +20 -7
- package/src/renderer/ui/compose-media-shortcut.tsx +2 -3
- package/src/renderer/ui/compose-pill-input.tsx +28 -9
- package/src/renderer/ui/compose-recent-media-grid.tsx +5 -8
- package/src/renderer/ui/compose-reply-audience-sheet.tsx +19 -8
- package/src/renderer/ui/compose-screen-header.tsx +15 -10
- package/src/renderer/ui/compose-text-area.tsx +14 -8
- package/src/renderer/ui/compose-toolbar-icon.tsx +9 -5
- package/src/renderer/ui/compose-toolbar.tsx +0 -7
- package/src/renderer/ui/compose-visibility-row.tsx +2 -3
- package/src/renderer/ui/floating-create-menu.tsx +2 -3
- package/src/renderer/ui/focus-ring.ts +26 -0
- package/src/renderer/ui/index.ts +2 -1
- package/src/renderer/ui/story-compose-header.tsx +14 -9
- package/src/renderer/ui/story-create-shortcut-row.tsx +1 -1
- package/src/renderer/ui/story-editor-chrome.tsx +30 -16
- package/src/renderer/ui/story-surface-picker.tsx +1 -2
package/package.json
CHANGED
|
@@ -17,14 +17,11 @@ type ChipProps = LayoutProps & {
|
|
|
17
17
|
actionId?: string;
|
|
18
18
|
actionParams?: Record<string, string>;
|
|
19
19
|
active?: boolean;
|
|
20
|
-
variant?: 'default' | 'on_header';
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
export const Chip: BdcComponent = (props) => layoutBlock('chip', props as ChipProps);
|
|
24
23
|
|
|
25
|
-
type ChipRowProps = LayoutProps
|
|
26
|
-
variant?: 'default' | 'on_header';
|
|
27
|
-
};
|
|
24
|
+
type ChipRowProps = LayoutProps;
|
|
28
25
|
|
|
29
26
|
export const ChipRow: BdcComponent = (props) => layoutBlock('chip_row', props as ChipRowProps);
|
|
30
27
|
|
|
@@ -15,6 +15,7 @@ type TextFieldProps = LayoutProps & {
|
|
|
15
15
|
placeholder?: string;
|
|
16
16
|
dataRef?: string;
|
|
17
17
|
submitActionId?: string;
|
|
18
|
+
changeActionId?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export const TextField: BdcComponent = (props) => layoutBlock('text_field', props as TextFieldProps);
|
|
@@ -25,6 +26,9 @@ type SelectRowProps = LayoutProps & {
|
|
|
25
26
|
optionsRef?: string;
|
|
26
27
|
changeActionId?: string;
|
|
27
28
|
disabled?: boolean;
|
|
29
|
+
/** Copy traduzida no BFF (Rosetta); o renderer não inventa rótulo. */
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
confirmLabel?: string;
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
export const SelectRow: BdcComponent = (props) => layoutBlock('select_row', props as SelectRowProps);
|
|
@@ -26,6 +26,10 @@ type EmptyStateProps = LayoutProps & {
|
|
|
26
26
|
titleRef?: string;
|
|
27
27
|
hint?: string;
|
|
28
28
|
hintRef?: string;
|
|
29
|
+
/** CTA da tela vazia — sem ação o bloco é só aviso, e o usuário não tem para onde ir. */
|
|
30
|
+
actionId?: string;
|
|
31
|
+
actionLabel?: string;
|
|
32
|
+
actionLabelRef?: string;
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
export const EmptyState: BdcComponent = (props) => layoutBlock('empty_state', props as EmptyStateProps);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { memo } from 'react';
|
|
2
2
|
import { StyleSheet } from 'react-native';
|
|
3
3
|
import type { SduiScreen, SduiBlock } from '@proulr/sdui-contract';
|
|
4
|
-
import { colors } from './theme/proulr-theme';
|
|
4
|
+
import { SCREEN_CONTENT_MAX_WIDTH, colors } from './theme/proulr-theme';
|
|
5
5
|
import { renderBlockTree } from './sdui-component-registry';
|
|
6
6
|
import { SduiLayoutDebugFrame } from './sdui-layout-debug';
|
|
7
7
|
|
|
@@ -49,7 +49,19 @@ export const SduiScreenRenderer = memo(function SduiScreenRenderer({ screen, onA
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
const styles = StyleSheet.create({
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Teto de leitura também aqui: o shell aplica o mesmo token, mas o renderer
|
|
54
|
+
* pode ser montado direto (master-detail, wrappers futuros) — sem este clamp
|
|
55
|
+
* essas superfícies esticariam de ponta a ponta em desktop.
|
|
56
|
+
*/
|
|
57
|
+
screen: {
|
|
58
|
+
flex: 1,
|
|
59
|
+
minHeight: 0,
|
|
60
|
+
width: '100%',
|
|
61
|
+
maxWidth: SCREEN_CONTENT_MAX_WIDTH,
|
|
62
|
+
alignSelf: 'center',
|
|
63
|
+
backgroundColor: colors.bg,
|
|
64
|
+
},
|
|
53
65
|
block: { flex: 1, minHeight: 0 },
|
|
54
66
|
overlayBlock: {
|
|
55
67
|
...StyleSheet.absoluteFill,
|
|
@@ -14,7 +14,7 @@ import { TextField as UiTextField } from '../ui/TextField';
|
|
|
14
14
|
import { Chip } from '../ui/Chip';
|
|
15
15
|
import { ChipRow } from '../ui/ChipRow';
|
|
16
16
|
import { AppIcon } from '../ui/app-icon';
|
|
17
|
-
import { colors, fonts, gradients, m3Colors, m3Elevation, m3Shape, spacing } from '../theme/proulr-theme';
|
|
17
|
+
import { colors, fonts, gradients, m3Colors, m3Elevation, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
|
|
18
18
|
import { resolveDataRef, resolveDataRefString } from '../lib/resolve-data-ref';
|
|
19
19
|
import { useSduiHybridRenderer } from '../sdui-hybrid-renderer-context';
|
|
20
20
|
import {
|
|
@@ -102,6 +102,7 @@ export function SduiDiscoverProfileCardBlock({ block, data, onAction }: SduiBloc
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const age = profileAgeFromBirthDate(person.birth_date);
|
|
105
|
+
const distanceLabel = formatDistanceLabel(person.distance_m);
|
|
105
106
|
const photoUri = person.photo_url ?? person.primary_photo_path ?? null;
|
|
106
107
|
const presence = getPresenceStatus(person.last_seen);
|
|
107
108
|
const online = presence === 'online';
|
|
@@ -151,7 +152,9 @@ export function SduiDiscoverProfileCardBlock({ block, data, onAction }: SduiBloc
|
|
|
151
152
|
</Text>
|
|
152
153
|
<View style={styles.distRow}>
|
|
153
154
|
<PresenceDot status={presence} />
|
|
154
|
-
|
|
155
|
+
{distanceLabel ? (
|
|
156
|
+
<Text style={styles.distText}>{distanceLabel}</Text>
|
|
157
|
+
) : null}
|
|
155
158
|
</View>
|
|
156
159
|
</View>
|
|
157
160
|
</Pressable>
|
|
@@ -280,8 +283,6 @@ export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
|
280
283
|
const actionId = readStringProp(block, 'action_id');
|
|
281
284
|
const actionParams = readStringRecordProp(block, 'action_params');
|
|
282
285
|
const active = readBooleanProp(block, 'active');
|
|
283
|
-
const variant = readStringProp(block, 'variant') ?? 'default';
|
|
284
|
-
const isHeader = variant === 'on_header';
|
|
285
286
|
|
|
286
287
|
return (
|
|
287
288
|
<Chip
|
|
@@ -289,17 +290,13 @@ export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
|
289
290
|
icon={glyph}
|
|
290
291
|
active={active}
|
|
291
292
|
disabled={!actionId}
|
|
292
|
-
variant={isHeader ? 'onHeader' : 'default'}
|
|
293
293
|
onPress={() => actionId && onAction(actionId, actionParams)}
|
|
294
294
|
/>
|
|
295
295
|
);
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
export function SduiChipRowBlock({
|
|
299
|
-
|
|
300
|
-
const isHeader = variant === 'on_header';
|
|
301
|
-
|
|
302
|
-
return <ChipRow variant={isHeader ? 'onHeader' : 'default'}>{children}</ChipRow>;
|
|
298
|
+
export function SduiChipRowBlock({ children }: SduiBlockRenderProps) {
|
|
299
|
+
return <ChipRow>{children}</ChipRow>;
|
|
303
300
|
}
|
|
304
301
|
|
|
305
302
|
export function SduiTextFieldBlock({ block, data, onAction }: SduiBlockRenderProps) {
|
|
@@ -307,7 +304,9 @@ export function SduiTextFieldBlock({ block, data, onAction }: SduiBlockRenderPro
|
|
|
307
304
|
const placeholder = readStringProp(block, 'placeholder') ?? '';
|
|
308
305
|
const dataRef = readStringProp(block, 'data_ref');
|
|
309
306
|
const submitActionId = readStringProp(block, 'submit_action_id');
|
|
307
|
+
const changeActionId = readStringProp(block, 'change_action_id');
|
|
310
308
|
const value = dataRef ? resolveDataRefString(data, dataRef) : '';
|
|
309
|
+
const [draft, setDraft] = useState(value);
|
|
311
310
|
|
|
312
311
|
if (hybrid.renderTextField && dataRef) {
|
|
313
312
|
return (
|
|
@@ -323,14 +322,19 @@ export function SduiTextFieldBlock({ block, data, onAction }: SduiBlockRenderPro
|
|
|
323
322
|
);
|
|
324
323
|
}
|
|
325
324
|
|
|
325
|
+
// Com change_action_id o wire é a fonte do valor; sem ele o input fica
|
|
326
|
+
// não-controlado e o texto digitado segue no payload do submit.
|
|
327
|
+
const binding = changeActionId && dataRef ? { value } : { defaultValue: value };
|
|
328
|
+
|
|
326
329
|
return (
|
|
327
330
|
<UiTextField
|
|
331
|
+
{...binding}
|
|
328
332
|
placeholder={placeholder}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
333
|
+
onChangeText={(next) => {
|
|
334
|
+
setDraft(next);
|
|
335
|
+
if (changeActionId) onAction(changeActionId, { value: next });
|
|
332
336
|
}}
|
|
333
|
-
onSubmitEditing={() => submitActionId && onAction(submitActionId)}
|
|
337
|
+
onSubmitEditing={() => submitActionId && onAction(submitActionId, { value: draft })}
|
|
334
338
|
returnKeyType={submitActionId ? 'search' : 'done'}
|
|
335
339
|
autoCapitalize="words"
|
|
336
340
|
autoCorrect={false}
|
|
@@ -440,34 +444,42 @@ const styles = StyleSheet.create({
|
|
|
440
444
|
top: spacing.sm,
|
|
441
445
|
left: spacing.sm,
|
|
442
446
|
backgroundColor: m3Colors.primary,
|
|
443
|
-
paddingHorizontal:
|
|
444
|
-
paddingVertical:
|
|
447
|
+
paddingHorizontal: spacing.sm,
|
|
448
|
+
paddingVertical: spacing.xs,
|
|
445
449
|
borderRadius: m3Shape.cornerSmall,
|
|
446
450
|
},
|
|
447
|
-
newBadgeText: {
|
|
451
|
+
newBadgeText: {
|
|
452
|
+
...m3Type.labelSmall,
|
|
453
|
+
color: m3Colors.onPrimary,
|
|
454
|
+
fontFamily: fonts.bold,
|
|
455
|
+
},
|
|
448
456
|
travelBadge: {
|
|
449
457
|
position: 'absolute',
|
|
450
458
|
top: spacing.sm,
|
|
451
459
|
right: spacing.sm,
|
|
452
460
|
flexDirection: 'row',
|
|
453
461
|
alignItems: 'center',
|
|
454
|
-
gap:
|
|
462
|
+
gap: spacing.xs,
|
|
455
463
|
backgroundColor: m3Colors.secondaryContainer,
|
|
456
|
-
paddingHorizontal:
|
|
457
|
-
paddingVertical:
|
|
464
|
+
paddingHorizontal: spacing.sm,
|
|
465
|
+
paddingVertical: spacing.xs,
|
|
458
466
|
borderRadius: m3Shape.cornerSmall,
|
|
459
467
|
},
|
|
460
468
|
travelBadgeOffset: {
|
|
461
469
|
top: spacing.sm + 24,
|
|
462
470
|
},
|
|
463
|
-
travelBadgeText: {
|
|
471
|
+
travelBadgeText: {
|
|
472
|
+
...m3Type.labelSmall,
|
|
473
|
+
color: m3Colors.onSecondaryContainer,
|
|
474
|
+
fontFamily: fonts.bold,
|
|
475
|
+
},
|
|
464
476
|
rightNowBadge: {
|
|
465
477
|
position: 'absolute',
|
|
466
478
|
top: spacing.sm,
|
|
467
479
|
left: spacing.sm,
|
|
468
480
|
width: 26,
|
|
469
481
|
height: 26,
|
|
470
|
-
borderRadius:
|
|
482
|
+
borderRadius: m3Shape.cornerFull,
|
|
471
483
|
alignItems: 'center',
|
|
472
484
|
justifyContent: 'center',
|
|
473
485
|
backgroundColor: m3Colors.surfaceContainerHighest,
|
|
@@ -478,10 +490,17 @@ const styles = StyleSheet.create({
|
|
|
478
490
|
left: spacing.sm + 52,
|
|
479
491
|
},
|
|
480
492
|
profileInfo: { position: 'absolute', left: spacing.sm, bottom: spacing.sm, right: spacing.sm },
|
|
481
|
-
profileName: {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
493
|
+
profileName: {
|
|
494
|
+
...m3Type.label,
|
|
495
|
+
color: colors.white,
|
|
496
|
+
fontFamily: fonts.bold,
|
|
497
|
+
},
|
|
498
|
+
distRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginTop: spacing.xs },
|
|
499
|
+
distText: {
|
|
500
|
+
...m3Type.labelSmall,
|
|
501
|
+
color: colors.textSecondary,
|
|
502
|
+
},
|
|
503
|
+
presenceDot: { width: 8, height: 8, borderRadius: m3Shape.cornerFull },
|
|
485
504
|
gridContent: {
|
|
486
505
|
gap: GRID_GAP,
|
|
487
506
|
paddingHorizontal: spacing.sm,
|
|
@@ -493,9 +512,8 @@ const styles = StyleSheet.create({
|
|
|
493
512
|
marginHorizontal: spacing.sm,
|
|
494
513
|
},
|
|
495
514
|
loadMoreText: {
|
|
515
|
+
...m3Type.label,
|
|
496
516
|
color: m3Colors.primary,
|
|
497
|
-
fontSize: 14,
|
|
498
|
-
fontWeight: '600',
|
|
499
517
|
fontFamily: fonts.semibold,
|
|
500
518
|
},
|
|
501
519
|
textField: {
|
|
@@ -513,24 +531,22 @@ const styles = StyleSheet.create({
|
|
|
513
531
|
alignItems: 'center',
|
|
514
532
|
justifyContent: 'center',
|
|
515
533
|
padding: spacing.lg,
|
|
516
|
-
backgroundColor:
|
|
534
|
+
backgroundColor: colors.videoControlBackground,
|
|
517
535
|
gap: spacing.sm,
|
|
518
536
|
},
|
|
519
537
|
mapTitle: {
|
|
538
|
+
...m3Type.titleSmall,
|
|
520
539
|
color: colors.white,
|
|
521
|
-
fontSize: 16,
|
|
522
|
-
fontFamily: fonts.semibold,
|
|
523
540
|
textAlign: 'center',
|
|
524
541
|
},
|
|
525
542
|
mapCoords: {
|
|
543
|
+
...m3Type.bodySmall,
|
|
526
544
|
color: colors.textSecondary,
|
|
527
|
-
fontSize: 14,
|
|
528
|
-
fontFamily: fonts.regular,
|
|
529
545
|
textAlign: 'center',
|
|
530
546
|
},
|
|
531
547
|
mapHint: {
|
|
548
|
+
...m3Type.labelSmall,
|
|
532
549
|
color: colors.textSecondary,
|
|
533
|
-
fontSize: 12,
|
|
534
550
|
fontFamily: fonts.regular,
|
|
535
551
|
textAlign: 'center',
|
|
536
552
|
},
|
|
@@ -2,7 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
import type { SduiBlock } from '@proulr/sdui-contract';
|
|
4
4
|
import { Card } from '../ui/Card';
|
|
5
|
-
import {
|
|
5
|
+
import { AppButton } from '../ui/AppButton';
|
|
6
|
+
import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
|
|
6
7
|
import { resolveDataRef, resolveDataRefString } from '../lib/resolve-data-ref';
|
|
7
8
|
import { useSduiHybridRenderer } from '../sdui-hybrid-renderer-context';
|
|
8
9
|
import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
|
|
@@ -85,21 +86,37 @@ export function SduiSegmentedTabsBlock({ block, data, onAction }: SduiBlockRende
|
|
|
85
86
|
);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export function SduiEmptyStateBlock({ block, data }: SduiBlockRenderProps) {
|
|
89
|
+
export function SduiEmptyStateBlock({ block, data, onAction }: SduiBlockRenderProps) {
|
|
89
90
|
const hybrid = useSduiHybridRenderer();
|
|
90
91
|
const title =
|
|
91
92
|
readStringProp(block, 'title') || resolveDataRefString(data, readStringProp(block, 'title_ref'));
|
|
92
93
|
const hint =
|
|
93
94
|
readStringProp(block, 'hint') || resolveDataRefString(data, readStringProp(block, 'hint_ref'));
|
|
95
|
+
const actionLabel =
|
|
96
|
+
readStringProp(block, 'action_label') ||
|
|
97
|
+
resolveDataRefString(data, readStringProp(block, 'action_label_ref'));
|
|
98
|
+
const actionId = readStringProp(block, 'action_id');
|
|
94
99
|
|
|
95
100
|
if (hybrid.renderEmptyState) {
|
|
96
|
-
return
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
{hybrid.renderEmptyState({
|
|
104
|
+
title,
|
|
105
|
+
hint,
|
|
106
|
+
actionLabel,
|
|
107
|
+
onAction: actionId ? () => onAction(actionId) : undefined,
|
|
108
|
+
})}
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
return (
|
|
100
114
|
<Card style={styles.emptyCard}>
|
|
101
115
|
<Text style={styles.emptyTitle}>{title}</Text>
|
|
102
116
|
{hint ? <Text style={styles.emptyHint}>{hint}</Text> : null}
|
|
117
|
+
{actionId && actionLabel ? (
|
|
118
|
+
<AppButton label={actionLabel} variant="primary" onPress={() => onAction(actionId)} />
|
|
119
|
+
) : null}
|
|
103
120
|
</Card>
|
|
104
121
|
);
|
|
105
122
|
}
|
|
@@ -262,7 +279,7 @@ const styles = StyleSheet.create({
|
|
|
262
279
|
paddingVertical: spacing.sm,
|
|
263
280
|
paddingHorizontal: spacing.sm,
|
|
264
281
|
borderRadius: m3Shape.cornerFull,
|
|
265
|
-
minHeight:
|
|
282
|
+
minHeight: 44,
|
|
266
283
|
},
|
|
267
284
|
segmentActive: {
|
|
268
285
|
backgroundColor: m3Colors.primaryContainer,
|
|
@@ -271,9 +288,9 @@ const styles = StyleSheet.create({
|
|
|
271
288
|
fontSize: 14,
|
|
272
289
|
},
|
|
273
290
|
segmentLabel: {
|
|
291
|
+
...m3Type.label,
|
|
274
292
|
color: m3Colors.onSurfaceVariant,
|
|
275
293
|
fontFamily: fonts.semibold,
|
|
276
|
-
fontSize: 14,
|
|
277
294
|
},
|
|
278
295
|
segmentLabelActive: {
|
|
279
296
|
color: m3Colors.onPrimaryContainer,
|
|
@@ -285,14 +302,13 @@ const styles = StyleSheet.create({
|
|
|
285
302
|
marginHorizontal: spacing.sm,
|
|
286
303
|
},
|
|
287
304
|
emptyTitle: {
|
|
305
|
+
...m3Type.label,
|
|
288
306
|
color: m3Colors.onSurfaceVariant,
|
|
289
|
-
fontSize: 15,
|
|
290
|
-
fontFamily: fonts.medium,
|
|
291
307
|
textAlign: 'center',
|
|
292
308
|
},
|
|
293
309
|
emptyHint: {
|
|
310
|
+
...m3Type.bodySmall,
|
|
294
311
|
color: m3Colors.onSurfaceVariant,
|
|
295
|
-
fontSize: 13,
|
|
296
312
|
textAlign: 'center',
|
|
297
313
|
},
|
|
298
314
|
rowCard: {
|
|
@@ -300,13 +316,12 @@ const styles = StyleSheet.create({
|
|
|
300
316
|
marginHorizontal: spacing.sm,
|
|
301
317
|
},
|
|
302
318
|
rowTitle: {
|
|
319
|
+
...m3Type.titleSmall,
|
|
303
320
|
color: m3Colors.onSurface,
|
|
304
|
-
fontSize: 16,
|
|
305
|
-
fontFamily: fonts.semibold,
|
|
306
321
|
},
|
|
307
322
|
rowMeta: {
|
|
323
|
+
...m3Type.bodySmall,
|
|
308
324
|
color: m3Colors.onSurfaceVariant,
|
|
309
|
-
fontSize: 12,
|
|
310
325
|
marginTop: spacing.xs,
|
|
311
326
|
},
|
|
312
327
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Pressable, StyleSheet,
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
import type { SduiBlock } from '@proulr/sdui-contract';
|
|
4
4
|
import { Card } from '../ui/Card';
|
|
5
5
|
import { AppIcon } from '../ui/app-icon';
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveDataRefString,
|
|
11
11
|
} from '../lib/resolve-data-ref';
|
|
12
12
|
import { SelectRow as UiSelectRow } from '../ui/SelectRow';
|
|
13
|
+
import { ToggleRow as UiToggleRow } from '../ui/ToggleRow';
|
|
13
14
|
import { resolveViewStyleProps } from '../theme/resolve-style-props';
|
|
14
15
|
import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
|
|
15
16
|
|
|
@@ -39,7 +40,12 @@ export function SduiSectionHeaderBlock({ block, onAction }: SduiBlockRenderProps
|
|
|
39
40
|
<View style={styles.sectionHeaderRow}>
|
|
40
41
|
<Text style={styles.sectionHeader}>{title}</Text>
|
|
41
42
|
{actionLabel && actionId ? (
|
|
42
|
-
<Pressable
|
|
43
|
+
<Pressable
|
|
44
|
+
onPress={() => onAction(actionId)}
|
|
45
|
+
hitSlop={8}
|
|
46
|
+
accessibilityRole="button"
|
|
47
|
+
style={({ pressed }) => [styles.sectionActionWrap, pressed && styles.pressed]}
|
|
48
|
+
>
|
|
43
49
|
<Text style={styles.sectionAction}>{actionLabel}</Text>
|
|
44
50
|
</Pressable>
|
|
45
51
|
) : null}
|
|
@@ -80,7 +86,13 @@ export function SduiListItemBlock({ block, data, onAction }: SduiBlockRenderProp
|
|
|
80
86
|
if (actionId) {
|
|
81
87
|
return (
|
|
82
88
|
<Pressable
|
|
83
|
-
|
|
89
|
+
accessibilityRole="button"
|
|
90
|
+
testID={readStringProp(block, 'test_id')}
|
|
91
|
+
style={({ pressed }) => [
|
|
92
|
+
styles.listItem,
|
|
93
|
+
showDivider && styles.listItemDivider,
|
|
94
|
+
pressed && styles.pressed,
|
|
95
|
+
]}
|
|
84
96
|
onPress={() => onAction(actionId)}
|
|
85
97
|
>
|
|
86
98
|
{inner}
|
|
@@ -100,22 +112,16 @@ export function SduiToggleRowBlock({ block, data, onAction }: SduiBlockRenderPro
|
|
|
100
112
|
const value = resolveDataRefBoolean(data, dataRef);
|
|
101
113
|
|
|
102
114
|
return (
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
onAction(toggleActionId, { value: String(next) });
|
|
114
|
-
}}
|
|
115
|
-
trackColor={{ false: m3Colors.surfaceContainerHighest, true: m3Colors.primary }}
|
|
116
|
-
thumbColor={m3Colors.onPrimary}
|
|
117
|
-
/>
|
|
118
|
-
</View>
|
|
115
|
+
<UiToggleRow
|
|
116
|
+
label={label}
|
|
117
|
+
hint={hint}
|
|
118
|
+
value={value}
|
|
119
|
+
disabled={disabled || !toggleActionId}
|
|
120
|
+
onChange={(next) => {
|
|
121
|
+
if (!toggleActionId) return;
|
|
122
|
+
onAction(toggleActionId, { value: String(next) });
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -134,6 +140,8 @@ export function SduiSelectRowBlock({ block, data, onAction }: SduiBlockRenderPro
|
|
|
134
140
|
label={label}
|
|
135
141
|
value={value || null}
|
|
136
142
|
options={options}
|
|
143
|
+
placeholder={readStringProp(block, 'placeholder') ?? ''}
|
|
144
|
+
confirmLabel={readStringProp(block, 'confirm_label') ?? ''}
|
|
137
145
|
onChange={(next) => {
|
|
138
146
|
if (disabled || !changeActionId) return;
|
|
139
147
|
onAction(changeActionId, { value: next });
|
|
@@ -155,48 +163,33 @@ const styles = StyleSheet.create({
|
|
|
155
163
|
marginTop: spacing.xl,
|
|
156
164
|
},
|
|
157
165
|
sectionHeader: {
|
|
166
|
+
...m3Type.titleSmall,
|
|
158
167
|
color: colors.textSecondary,
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
},
|
|
169
|
+
sectionActionWrap: {
|
|
170
|
+
minHeight: 44,
|
|
171
|
+
justifyContent: 'center',
|
|
172
|
+
paddingLeft: spacing.md,
|
|
161
173
|
},
|
|
162
174
|
sectionAction: {
|
|
175
|
+
...m3Type.label,
|
|
163
176
|
color: colors.accentText,
|
|
164
|
-
fontSize: 13,
|
|
165
177
|
fontFamily: fonts.semibold,
|
|
166
178
|
},
|
|
179
|
+
pressed: { backgroundColor: m3Colors.surfaceContainerHigh },
|
|
167
180
|
listItem: {
|
|
168
181
|
flexDirection: 'row',
|
|
169
182
|
alignItems: 'center',
|
|
170
183
|
gap: spacing.md,
|
|
184
|
+
minHeight: 48,
|
|
171
185
|
paddingVertical: spacing.md,
|
|
172
186
|
},
|
|
173
187
|
listItemDivider: {
|
|
174
188
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
175
|
-
borderBottomColor:
|
|
189
|
+
borderBottomColor: m3Colors.divider,
|
|
176
190
|
},
|
|
177
191
|
listContent: { flex: 1 },
|
|
178
|
-
listTitle: { color: colors.textPrimary
|
|
179
|
-
listSubtitle: { color: colors.textSecondary,
|
|
192
|
+
listTitle: { ...m3Type.titleSmall, color: colors.textPrimary },
|
|
193
|
+
listSubtitle: { ...m3Type.bodySmall, color: colors.textSecondary, marginTop: 2 },
|
|
180
194
|
trailingText: { ...m3Type.bodySmall, color: colors.textSecondary },
|
|
181
|
-
toggleRow: {
|
|
182
|
-
flexDirection: 'row',
|
|
183
|
-
alignItems: 'center',
|
|
184
|
-
gap: spacing.md,
|
|
185
|
-
paddingVertical: spacing.xs,
|
|
186
|
-
},
|
|
187
|
-
toggleCopy: {
|
|
188
|
-
flex: 1,
|
|
189
|
-
gap: spacing.xs,
|
|
190
|
-
},
|
|
191
|
-
toggleLabel: {
|
|
192
|
-
color: m3Colors.onSurface,
|
|
193
|
-
fontSize: 15,
|
|
194
|
-
fontFamily: fonts.semibold,
|
|
195
|
-
},
|
|
196
|
-
toggleHint: {
|
|
197
|
-
color: m3Colors.onSurfaceVariant,
|
|
198
|
-
fontSize: 12,
|
|
199
|
-
lineHeight: 17,
|
|
200
|
-
fontFamily: fonts.regular,
|
|
201
|
-
},
|
|
202
195
|
});
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
DEFAULT_NATIVE_AD_PLACEMENT,
|
|
7
7
|
getNativeAdCatalog,
|
|
8
8
|
} from '../ads/native-ad-catalog';
|
|
9
|
-
import { colors, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
|
|
9
|
+
import { colors, fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
|
|
10
10
|
import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
|
|
11
11
|
|
|
12
12
|
type NativeAdModule = typeof import('react-native-google-mobile-ads');
|
|
@@ -220,10 +220,9 @@ const styles = StyleSheet.create({
|
|
|
220
220
|
borderRadius: m3Shape.cornerSmall,
|
|
221
221
|
},
|
|
222
222
|
adBadgeText: {
|
|
223
|
+
...m3Type.labelSmall,
|
|
223
224
|
color: colors.onSurfaceVariant,
|
|
224
|
-
|
|
225
|
-
fontWeight: '700',
|
|
226
|
-
letterSpacing: 0.4,
|
|
225
|
+
fontFamily: fonts.bold,
|
|
227
226
|
textTransform: 'uppercase',
|
|
228
227
|
},
|
|
229
228
|
headline: {
|
|
@@ -231,17 +230,17 @@ const styles = StyleSheet.create({
|
|
|
231
230
|
left: spacing.sm,
|
|
232
231
|
right: spacing.sm,
|
|
233
232
|
bottom: spacing.xl + spacing.sm,
|
|
233
|
+
...m3Type.labelSmall,
|
|
234
234
|
color: m3Colors.onSurface,
|
|
235
|
-
|
|
236
|
-
fontWeight: '700',
|
|
235
|
+
fontFamily: fonts.bold,
|
|
237
236
|
},
|
|
238
237
|
cta: {
|
|
239
238
|
position: 'absolute',
|
|
240
239
|
left: spacing.sm,
|
|
241
240
|
right: spacing.sm,
|
|
242
241
|
bottom: spacing.sm,
|
|
242
|
+
...m3Type.labelSmall,
|
|
243
243
|
color: m3Colors.secondary,
|
|
244
|
-
|
|
245
|
-
fontWeight: '600',
|
|
244
|
+
fontFamily: fonts.semibold,
|
|
246
245
|
},
|
|
247
246
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { StyleSheet, Text, View } from 'react-native';
|
|
3
2
|
import type { SduiBlock } from '@proulr/sdui-contract';
|
|
4
3
|
import { resolveDataRefString } from '../lib/resolve-data-ref';
|
|
5
4
|
import { useSduiHybridRenderer } from '../sdui-hybrid-renderer-context';
|
|
@@ -14,15 +13,8 @@ export function SduiStoryRailBlock({ block }: SduiBlockRenderProps) {
|
|
|
14
13
|
const hybrid = useSduiHybridRenderer();
|
|
15
14
|
const surface = readStringProp(block, 'surface') === 'vibes' ? 'vibes' : 'discover';
|
|
16
15
|
|
|
17
|
-
if (hybrid.renderStoryRail)
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<View style={styles.placeholder}>
|
|
23
|
-
<Text style={styles.placeholderText}>StoryRail ({surface})</Text>
|
|
24
|
-
</View>
|
|
25
|
-
);
|
|
16
|
+
if (!hybrid.renderStoryRail) return null;
|
|
17
|
+
return <>{hybrid.renderStoryRail({ surface })}</>;
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
export function SduiProfileCompletionBannerBlock({ block, data, onAction }: SduiBlockRenderProps) {
|
|
@@ -44,21 +36,6 @@ export function SduiProfileCompletionBannerBlock({ block, data, onAction }: Sdui
|
|
|
44
36
|
);
|
|
45
37
|
}
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<View style={styles.placeholder}>
|
|
51
|
-
<Text style={styles.placeholderText}>Complete seu perfil ({percent}%)</Text>
|
|
52
|
-
</View>
|
|
53
|
-
);
|
|
39
|
+
// Sem hybrid registrado o slot fica vazio: o renderer não inventa UI nem copy.
|
|
40
|
+
return null;
|
|
54
41
|
}
|
|
55
|
-
|
|
56
|
-
const styles = StyleSheet.create({
|
|
57
|
-
placeholder: {
|
|
58
|
-
padding: 12,
|
|
59
|
-
},
|
|
60
|
-
placeholderText: {
|
|
61
|
-
color: '#A3A3A3',
|
|
62
|
-
fontSize: 13,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
@@ -29,6 +29,13 @@ describe('formatDistanceLabel', () => {
|
|
|
29
29
|
expect(formatDistanceLabel(10, 'imperial')).toMatch(/ft$/);
|
|
30
30
|
expect(formatDistanceLabel(2000, 'imperial')).toMatch(/mi$/);
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
it('returns null when the visitor hides distance', () => {
|
|
34
|
+
expect(formatDistanceLabel(null)).toBeNull();
|
|
35
|
+
expect(formatDistanceLabel(undefined)).toBeNull();
|
|
36
|
+
expect(formatDistanceLabel(Number.NaN)).toBeNull();
|
|
37
|
+
expect(formatDistanceLabel(null, 'imperial')).toBeNull();
|
|
38
|
+
});
|
|
32
39
|
});
|
|
33
40
|
|
|
34
41
|
describe('getPresenceStatus', () => {
|
|
@@ -2,7 +2,8 @@ export type DiscoverNearbyProfileWire = {
|
|
|
2
2
|
id: string;
|
|
3
3
|
display_name: string | null;
|
|
4
4
|
birth_date: string | null;
|
|
5
|
-
|
|
5
|
+
/** `null` = visitante ativou "ocultar distância" — o rótulo some, o resto do card fica. */
|
|
6
|
+
distance_m: number | null;
|
|
6
7
|
primary_photo_path: string | null;
|
|
7
8
|
photo_url?: string | null;
|
|
8
9
|
is_online: boolean | null;
|
|
@@ -42,9 +43,11 @@ export function profileAgeFromBirthDate(birthDate: string | null | undefined): n
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function formatDistanceLabel(
|
|
45
|
-
distanceM: number,
|
|
46
|
+
distanceM: number | null | undefined,
|
|
46
47
|
unit: 'metric' | 'imperial' = 'metric',
|
|
47
|
-
): string {
|
|
48
|
+
): string | null {
|
|
49
|
+
if (distanceM === null || distanceM === undefined || !Number.isFinite(distanceM)) return null;
|
|
50
|
+
|
|
48
51
|
if (unit === 'imperial') {
|
|
49
52
|
const feet = distanceM * 3.28084;
|
|
50
53
|
if (feet < 528) return `${Math.round(feet)} ft`;
|
|
@@ -102,7 +102,12 @@ export type SduiHybridRenderers = {
|
|
|
102
102
|
activeRef?: string;
|
|
103
103
|
actionPrefix?: string;
|
|
104
104
|
}) => ReactNode;
|
|
105
|
-
renderEmptyState?: (props: {
|
|
105
|
+
renderEmptyState?: (props: {
|
|
106
|
+
title: string;
|
|
107
|
+
hint?: string;
|
|
108
|
+
actionLabel?: string;
|
|
109
|
+
onAction?: () => void;
|
|
110
|
+
}) => ReactNode;
|
|
106
111
|
renderInboxPersonRow?: (props: {
|
|
107
112
|
block: SduiBlock;
|
|
108
113
|
data: Record<string, unknown>;
|