@proulr/sdui-components 0.1.88 → 0.1.91
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 +2 -2
- package/src/bdc/compile-bdc-row-template.test.ts +179 -0
- package/src/bdc/compile-bdc-to-wire.test.ts +11 -0
- package/src/bdc/compile-bdc-to-wire.ts +52 -1
- package/src/bdc/components/bdc-components.test.ts +9 -0
- package/src/bdc/components/discover.tsx +7 -1
- package/src/bdc/components/list.test.ts +36 -0
- package/src/bdc/components/list.tsx +62 -0
- package/src/bdc/components/media.test.ts +268 -0
- package/src/bdc/components/media.tsx +275 -0
- package/src/bdc/components/native-blocks.tsx +7 -1
- package/src/bdc/components/ui.tsx +33 -0
- package/src/bdc/index.ts +5 -0
- package/src/catalog/block-types.ts +5 -0
- package/src/renderer/SduiTemplateRow.tsx +63 -0
- package/src/renderer/blocks/SduiContentBlocks.tsx +299 -0
- package/src/renderer/blocks/SduiDiscoverBlocks.tsx +39 -3
- package/src/renderer/blocks/SduiMediaBlocks.tsx +348 -0
- package/src/renderer/blocks/SduiNativeDomainBlocks.tsx +2 -0
- package/src/renderer/blocks/SduiPrimitiveBlocks.tsx +22 -3
- package/src/renderer/index.ts +48 -0
- package/src/renderer/lib/sdui-media-block-props.ts +282 -0
- package/src/renderer/lib/sdui-media-blocks.test.ts +279 -0
- package/src/renderer/lib/sdui-media-uri.ts +39 -0
- package/src/renderer/lib/sdui-row-template.test.ts +190 -0
- package/src/renderer/lib/sdui-row-template.ts +153 -0
- package/src/renderer/sdui-component-registry.tsx +16 -0
- package/src/renderer/sdui-hybrid-renderer-context.tsx +2 -0
- package/src/renderer/sdui-media-resolver-context.tsx +84 -0
- package/src/renderer/sdui-video-player-context.tsx +55 -0
- package/src/renderer/theme/resolve-style-props.ts +2 -0
- package/src/renderer/ui/Chip.tsx +25 -8
- package/src/renderer/ui/ChipRow.tsx +14 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Image, Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
computeSduiMediaFrame,
|
|
5
|
+
deriveSduiInitials,
|
|
6
|
+
parseSduiAvatarProps,
|
|
7
|
+
parseSduiContentImageProps,
|
|
8
|
+
parseSduiIconButtonProps,
|
|
9
|
+
parseSduiRowOverflowMenuProps,
|
|
10
|
+
parseSduiVideoProps,
|
|
11
|
+
} from '../lib/sdui-media-block-props';
|
|
12
|
+
import { useSduiMediaResolver, useSduiMediaUri } from '../sdui-media-resolver-context';
|
|
13
|
+
import { useSduiVideoPlayer } from '../sdui-video-player-context';
|
|
14
|
+
import { AppIcon } from '../ui/app-icon';
|
|
15
|
+
import { APP_ICON_SIZE } from '../ui/app-icon-size-catalog';
|
|
16
|
+
import { colors, fonts, m3Colors, m3Elevation, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
|
|
17
|
+
import { resolveViewStyleProps } from '../theme/resolve-style-props';
|
|
18
|
+
import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Blocos genéricos de mídia/estado para row templates e telas BFF-authored:
|
|
22
|
+
* `content_image`, `video`, `avatar`, `icon_button`, `row_overflow_menu`.
|
|
23
|
+
* Layout 100% no wire; path de mídia
|
|
24
|
+
* 100% em dados (`{{item.*}}`); resolução path → URL é capability do app via
|
|
25
|
+
* `SduiMediaResolver`. Zero hex aqui — só tokens do tema.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
function cornerPx(token: keyof typeof m3Shape | undefined): number | undefined {
|
|
29
|
+
return token ? m3Shape[token] : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function m3Color(token: string | undefined, fallback: string): string {
|
|
33
|
+
const value = token ? m3Colors[token as keyof typeof m3Colors] : undefined;
|
|
34
|
+
return value ?? fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SduiContentImageBlock({ block }: SduiBlockRenderProps) {
|
|
38
|
+
const parsed = parseSduiContentImageProps(block.props);
|
|
39
|
+
const resolver = useSduiMediaResolver();
|
|
40
|
+
const { uri, settled } = useSduiMediaUri(parsed.path);
|
|
41
|
+
|
|
42
|
+
// Capability ausente ou path vazio (binding não resolvido) → renderizar nada.
|
|
43
|
+
// Nunca cair em path/URL de produto hardcoded.
|
|
44
|
+
if (!resolver || !parsed.path) return null;
|
|
45
|
+
if (settled && !uri) return null;
|
|
46
|
+
|
|
47
|
+
const frame = computeSduiMediaFrame(parsed.width, parsed.height, parsed.aspectRatio);
|
|
48
|
+
const containerStyle = [
|
|
49
|
+
styles.mediaShell,
|
|
50
|
+
{
|
|
51
|
+
width: frame.width,
|
|
52
|
+
height: frame.height,
|
|
53
|
+
aspectRatio: frame.aspectRatio,
|
|
54
|
+
...(frame.stretch ? { alignSelf: 'stretch' as const } : null),
|
|
55
|
+
borderRadius: cornerPx(parsed.corner ?? 'cornerMedium'),
|
|
56
|
+
},
|
|
57
|
+
resolveViewStyleProps(block.props.style as Record<string, unknown>),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (!uri) {
|
|
61
|
+
// Placeholder de tamanho estável (mesmo frame do futuro image) — anti-flicker.
|
|
62
|
+
return <View style={containerStyle} testID={parsed.testId} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fillBehind = parsed.blurFill && parsed.fit === 'contain';
|
|
66
|
+
return (
|
|
67
|
+
<View style={containerStyle} testID={parsed.testId}>
|
|
68
|
+
{fillBehind ? (
|
|
69
|
+
<>
|
|
70
|
+
<Image source={{ uri }} resizeMode="cover" style={StyleSheet.absoluteFill} />
|
|
71
|
+
<View style={[StyleSheet.absoluteFill, styles.blurVeil]} pointerEvents="none" />
|
|
72
|
+
</>
|
|
73
|
+
) : null}
|
|
74
|
+
<Image
|
|
75
|
+
source={{ uri }}
|
|
76
|
+
resizeMode={parsed.fit === 'contain' ? 'contain' : 'cover'}
|
|
77
|
+
style={StyleSheet.absoluteFill}
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function SduiVideoBlock({ block, data, onAction }: SduiBlockRenderProps) {
|
|
84
|
+
const parsed = parseSduiVideoProps(block.props);
|
|
85
|
+
const resolver = useSduiMediaResolver();
|
|
86
|
+
const player = useSduiVideoPlayer();
|
|
87
|
+
const { uri, pending, settled } = useSduiMediaUri(parsed.path);
|
|
88
|
+
const { uri: posterUri } = useSduiMediaUri(parsed.posterPath ?? '');
|
|
89
|
+
|
|
90
|
+
// Capability ausente ou path vazio (binding não resolvido) → renderizar nada.
|
|
91
|
+
if (!resolver || !parsed.path) return null;
|
|
92
|
+
|
|
93
|
+
// Slot do app (expo-video etc.): a lib só entrega URI resolvida + frame do wire.
|
|
94
|
+
if (player) {
|
|
95
|
+
return <>{player({ block, data, onAction, parsed, uri, pending, settled, posterUri })}</>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Default sem player: FRAME com poster/still (ou placeholder estável com
|
|
99
|
+
// glyph `play`). Anti-flicker: mesmo frame do wire em todos os estados.
|
|
100
|
+
const frame = computeSduiMediaFrame(parsed.width, parsed.height, parsed.aspectRatio);
|
|
101
|
+
const containerStyle = [
|
|
102
|
+
styles.mediaShell,
|
|
103
|
+
{
|
|
104
|
+
width: frame.width,
|
|
105
|
+
height: frame.height,
|
|
106
|
+
aspectRatio: frame.aspectRatio,
|
|
107
|
+
...(frame.stretch ? { alignSelf: 'stretch' as const } : null),
|
|
108
|
+
borderRadius: cornerPx(parsed.corner ?? 'cornerMedium'),
|
|
109
|
+
},
|
|
110
|
+
resolveViewStyleProps(block.props.style as Record<string, unknown>),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
if (settled && !uri && !posterUri) return null;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<View style={containerStyle} testID={parsed.testId}>
|
|
117
|
+
{posterUri ? (
|
|
118
|
+
<Image
|
|
119
|
+
source={{ uri: posterUri }}
|
|
120
|
+
resizeMode={parsed.fit === 'contain' ? 'contain' : 'cover'}
|
|
121
|
+
style={StyleSheet.absoluteFill}
|
|
122
|
+
/>
|
|
123
|
+
) : null}
|
|
124
|
+
<View style={[StyleSheet.absoluteFill, styles.videoStillCenter]} pointerEvents="none">
|
|
125
|
+
<AppIcon name="play" size={APP_ICON_SIZE.fab} color={m3Colors.onSurface} />
|
|
126
|
+
</View>
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function SduiRowOverflowMenuBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
132
|
+
const parsed = parseSduiRowOverflowMenuProps(block.props);
|
|
133
|
+
const [open, setOpen] = useState(false);
|
|
134
|
+
|
|
135
|
+
// Sem entradas válidas → o trigger não abriria nada; não renderiza.
|
|
136
|
+
if (parsed.actions.length === 0) return null;
|
|
137
|
+
|
|
138
|
+
const pressAction = (actionId: string) => {
|
|
139
|
+
setOpen(false);
|
|
140
|
+
onAction(actionId);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<Pressable
|
|
146
|
+
onPress={() => setOpen(true)}
|
|
147
|
+
accessibilityRole="button"
|
|
148
|
+
testID={parsed.testId}
|
|
149
|
+
style={({ pressed }) => [
|
|
150
|
+
styles.iconButton,
|
|
151
|
+
pressed && styles.iconButtonPressed,
|
|
152
|
+
resolveViewStyleProps(block.props.style as Record<string, unknown>),
|
|
153
|
+
]}
|
|
154
|
+
>
|
|
155
|
+
<AppIcon
|
|
156
|
+
name={parsed.triggerIcon}
|
|
157
|
+
size={APP_ICON_SIZE.feedAction}
|
|
158
|
+
color={m3Colors.onSurfaceVariant}
|
|
159
|
+
/>
|
|
160
|
+
</Pressable>
|
|
161
|
+
|
|
162
|
+
<Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
|
|
163
|
+
<View style={styles.overflowOverlay}>
|
|
164
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={() => setOpen(false)} />
|
|
165
|
+
<View style={styles.overflowMenu}>
|
|
166
|
+
{parsed.actions.map((action) => (
|
|
167
|
+
<Pressable
|
|
168
|
+
key={action.actionId}
|
|
169
|
+
onPress={() => pressAction(action.actionId)}
|
|
170
|
+
accessibilityRole="button"
|
|
171
|
+
accessibilityLabel={action.label}
|
|
172
|
+
style={({ pressed }) => [
|
|
173
|
+
styles.overflowMenuItem,
|
|
174
|
+
pressed && styles.overflowMenuItemPressed,
|
|
175
|
+
]}
|
|
176
|
+
>
|
|
177
|
+
{action.icon ? (
|
|
178
|
+
<AppIcon
|
|
179
|
+
name={action.icon}
|
|
180
|
+
size={APP_ICON_SIZE.feedAction}
|
|
181
|
+
color={action.destructive ? m3Colors.error : m3Colors.onSurfaceVariant}
|
|
182
|
+
/>
|
|
183
|
+
) : null}
|
|
184
|
+
<Text
|
|
185
|
+
style={[
|
|
186
|
+
styles.overflowMenuLabel,
|
|
187
|
+
action.destructive && { color: m3Colors.error },
|
|
188
|
+
]}
|
|
189
|
+
>
|
|
190
|
+
{action.label}
|
|
191
|
+
</Text>
|
|
192
|
+
</Pressable>
|
|
193
|
+
))}
|
|
194
|
+
</View>
|
|
195
|
+
</View>
|
|
196
|
+
</Modal>
|
|
197
|
+
</>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function SduiAvatarBlock({ block }: SduiBlockRenderProps) {
|
|
202
|
+
const parsed = parseSduiAvatarProps(block.props);
|
|
203
|
+
const { uri } = useSduiMediaUri(parsed.path);
|
|
204
|
+
const initials = (parsed.initials?.trim() || deriveSduiInitials(parsed.name)).slice(0, 2);
|
|
205
|
+
const sizeStyle = {
|
|
206
|
+
width: parsed.sizePx,
|
|
207
|
+
height: parsed.sizePx,
|
|
208
|
+
borderRadius: cornerPx(parsed.corner),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (parsed.path && uri) {
|
|
212
|
+
return (
|
|
213
|
+
<View style={[styles.avatarShell, sizeStyle]} testID={parsed.testId}>
|
|
214
|
+
<Image source={{ uri }} style={StyleSheet.absoluteFill} resizeMode="cover" />
|
|
215
|
+
</View>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Sem foto resolvível (path vazio, sem resolver, pendente sem iniciais?) →
|
|
220
|
+
// placeholder genérico: iniciais derivadas de dados ou glyph do catalog.
|
|
221
|
+
// Nunca um path de produto fallback.
|
|
222
|
+
if (initials) {
|
|
223
|
+
return (
|
|
224
|
+
<View
|
|
225
|
+
style={[styles.avatarShell, styles.avatarInitials, sizeStyle]}
|
|
226
|
+
testID={parsed.testId}
|
|
227
|
+
accessibilityLabel={parsed.name}
|
|
228
|
+
>
|
|
229
|
+
<Text
|
|
230
|
+
style={[
|
|
231
|
+
styles.avatarInitialsText,
|
|
232
|
+
{ fontSize: Math.max(10, Math.round(parsed.sizePx * 0.36)) },
|
|
233
|
+
]}
|
|
234
|
+
>
|
|
235
|
+
{initials}
|
|
236
|
+
</Text>
|
|
237
|
+
</View>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<View style={[styles.avatarShell, styles.avatarFallback, sizeStyle]} testID={parsed.testId}>
|
|
243
|
+
<AppIcon name="profile" size={Math.round(parsed.sizePx * 0.55)} color={m3Colors.onSurfaceVariant} />
|
|
244
|
+
</View>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function SduiIconButtonBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
249
|
+
const parsed = parseSduiIconButtonProps(block.props);
|
|
250
|
+
if (!parsed.icon) return null;
|
|
251
|
+
|
|
252
|
+
const glyphSize = parsed.size ?? APP_ICON_SIZE.feedAction;
|
|
253
|
+
const name = parsed.selected && parsed.selectedIcon ? parsed.selectedIcon : parsed.icon;
|
|
254
|
+
const tint = parsed.selected
|
|
255
|
+
? m3Color(parsed.selectedColor, m3Colors.primary)
|
|
256
|
+
: m3Color(parsed.color, m3Colors.onSurfaceVariant);
|
|
257
|
+
const pressableSize = glyphSize + spacing.xs * 2;
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Pressable
|
|
261
|
+
onPress={() => parsed.actionId && onAction(parsed.actionId)}
|
|
262
|
+
disabled={parsed.disabled || !parsed.actionId}
|
|
263
|
+
accessibilityRole="button"
|
|
264
|
+
accessibilityLabel={parsed.label}
|
|
265
|
+
accessibilityState={{ disabled: parsed.disabled, selected: parsed.selected }}
|
|
266
|
+
testID={parsed.testId}
|
|
267
|
+
style={({ pressed }) => [
|
|
268
|
+
styles.iconButton,
|
|
269
|
+
{ width: pressableSize, height: pressableSize },
|
|
270
|
+
pressed && styles.iconButtonPressed,
|
|
271
|
+
parsed.disabled && styles.iconButtonDisabled,
|
|
272
|
+
resolveViewStyleProps(block.props.style as Record<string, unknown>),
|
|
273
|
+
]}
|
|
274
|
+
>
|
|
275
|
+
<AppIcon name={name} size={glyphSize} color={tint} />
|
|
276
|
+
</Pressable>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const styles = StyleSheet.create({
|
|
281
|
+
mediaShell: {
|
|
282
|
+
overflow: 'hidden',
|
|
283
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
284
|
+
},
|
|
285
|
+
/** Véu OLED sobre o cover de trás no blur-fill (o app pode trocar pelo
|
|
286
|
+
* MediaBlurFillImage canônico com blur real; a estrutura é a mesma). */
|
|
287
|
+
blurVeil: {
|
|
288
|
+
backgroundColor: colors.blackAlpha62,
|
|
289
|
+
},
|
|
290
|
+
avatarShell: {
|
|
291
|
+
overflow: 'hidden',
|
|
292
|
+
alignItems: 'center',
|
|
293
|
+
justifyContent: 'center',
|
|
294
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
295
|
+
},
|
|
296
|
+
avatarInitials: {
|
|
297
|
+
backgroundColor: m3Colors.secondaryContainer,
|
|
298
|
+
},
|
|
299
|
+
avatarInitialsText: {
|
|
300
|
+
...m3Type.labelSmall,
|
|
301
|
+
fontFamily: fonts.bold,
|
|
302
|
+
color: m3Colors.onSecondaryContainer,
|
|
303
|
+
},
|
|
304
|
+
avatarFallback: {
|
|
305
|
+
backgroundColor: m3Colors.surfaceContainerHighest,
|
|
306
|
+
},
|
|
307
|
+
iconButton: {
|
|
308
|
+
alignItems: 'center',
|
|
309
|
+
justifyContent: 'center',
|
|
310
|
+
},
|
|
311
|
+
iconButtonPressed: {
|
|
312
|
+
backgroundColor: colors.whiteAlpha12,
|
|
313
|
+
borderRadius: m3Shape.cornerFull,
|
|
314
|
+
},
|
|
315
|
+
iconButtonDisabled: {
|
|
316
|
+
opacity: 0.4,
|
|
317
|
+
},
|
|
318
|
+
videoStillCenter: {
|
|
319
|
+
alignItems: 'center',
|
|
320
|
+
justifyContent: 'center',
|
|
321
|
+
},
|
|
322
|
+
overflowOverlay: {
|
|
323
|
+
flex: 1,
|
|
324
|
+
backgroundColor: colors.blackAlpha62,
|
|
325
|
+
justifyContent: 'flex-end',
|
|
326
|
+
},
|
|
327
|
+
overflowMenu: {
|
|
328
|
+
margin: spacing.md,
|
|
329
|
+
paddingVertical: spacing.xs,
|
|
330
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
331
|
+
borderRadius: m3Shape.cornerLarge,
|
|
332
|
+
...m3Elevation.level3,
|
|
333
|
+
},
|
|
334
|
+
overflowMenuItem: {
|
|
335
|
+
flexDirection: 'row',
|
|
336
|
+
alignItems: 'center',
|
|
337
|
+
gap: spacing.sm,
|
|
338
|
+
paddingHorizontal: spacing.md,
|
|
339
|
+
paddingVertical: spacing.sm,
|
|
340
|
+
},
|
|
341
|
+
overflowMenuItemPressed: {
|
|
342
|
+
backgroundColor: colors.whiteAlpha12,
|
|
343
|
+
},
|
|
344
|
+
overflowMenuLabel: {
|
|
345
|
+
...m3Type.body,
|
|
346
|
+
color: m3Colors.onSurface,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
@@ -14,6 +14,7 @@ function NativeBlockShell({
|
|
|
14
14
|
block,
|
|
15
15
|
data,
|
|
16
16
|
onAction,
|
|
17
|
+
children,
|
|
17
18
|
rendererKey,
|
|
18
19
|
fallbackLabel,
|
|
19
20
|
}: SduiBlockRenderProps & {
|
|
@@ -53,6 +54,7 @@ function NativeBlockShell({
|
|
|
53
54
|
block,
|
|
54
55
|
data,
|
|
55
56
|
onAction,
|
|
57
|
+
children,
|
|
56
58
|
dataRef: readStringProp(block, 'data_ref'),
|
|
57
59
|
conversationIdRef: readStringProp(block, 'conversation_id_ref'),
|
|
58
60
|
peerIdRef: readStringProp(block, 'peer_id_ref'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Platform, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
import type { SduiBlock } from '@proulr/sdui-contract';
|
|
4
4
|
import { AppButton } from '../ui/AppButton';
|
|
5
5
|
import { AppIcon, isAppIconName } from '../ui/app-icon';
|
|
@@ -22,16 +22,18 @@ export type SduiBlockRenderProps = {
|
|
|
22
22
|
|
|
23
23
|
const textVariants = ['headline', 'title', 'body', 'label'] as const;
|
|
24
24
|
|
|
25
|
-
export function SduiTextBlock({ block }: SduiBlockRenderProps) {
|
|
25
|
+
export function SduiTextBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
26
26
|
const text = typeof block.props.text === 'string' ? block.props.text : '';
|
|
27
27
|
const variant =
|
|
28
28
|
typeof block.props.variant === 'string' &&
|
|
29
29
|
(textVariants as readonly string[]).includes(block.props.variant)
|
|
30
30
|
? block.props.variant
|
|
31
31
|
: 'body';
|
|
32
|
+
const actionId = typeof block.props.action_id === 'string' ? block.props.action_id : '';
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
const label = (
|
|
34
35
|
<Text
|
|
36
|
+
numberOfLines={actionId ? 1 : undefined}
|
|
35
37
|
style={[
|
|
36
38
|
{ color: colors.onSurface },
|
|
37
39
|
resolveTextStyleProps(block.props.style as Record<string, unknown>, variant),
|
|
@@ -40,6 +42,19 @@ export function SduiTextBlock({ block }: SduiBlockRenderProps) {
|
|
|
40
42
|
{text}
|
|
41
43
|
</Text>
|
|
42
44
|
);
|
|
45
|
+
if (!actionId) return label;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Pressable
|
|
49
|
+
accessibilityRole="link"
|
|
50
|
+
accessibilityLabel={text}
|
|
51
|
+
hitSlop={spacing.xs}
|
|
52
|
+
onPress={() => onAction(actionId)}
|
|
53
|
+
style={styles.textLink}
|
|
54
|
+
>
|
|
55
|
+
{label}
|
|
56
|
+
</Pressable>
|
|
57
|
+
);
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
export function SduiColumnBlock({ block, children }: SduiBlockRenderProps) {
|
|
@@ -164,6 +179,9 @@ export function SduiIconBlock({ block }: SduiBlockRenderProps) {
|
|
|
164
179
|
}
|
|
165
180
|
|
|
166
181
|
export function SduiSpacerBlock({ block }: SduiBlockRenderProps) {
|
|
182
|
+
if (typeof block.props.flex === 'number') {
|
|
183
|
+
return <View style={{ flex: block.props.flex, minWidth: 0 }} />;
|
|
184
|
+
}
|
|
167
185
|
const height = typeof block.props.height === 'number' ? block.props.height : spacing.md;
|
|
168
186
|
return <View style={{ height }} />;
|
|
169
187
|
}
|
|
@@ -184,6 +202,7 @@ export function SduiUnknownBlock({ block }: SduiBlockRenderProps) {
|
|
|
184
202
|
|
|
185
203
|
const styles = StyleSheet.create({
|
|
186
204
|
scroll: { flex: 1, minHeight: 0 },
|
|
205
|
+
textLink: { flexShrink: 1, minWidth: 0 },
|
|
187
206
|
banner: {
|
|
188
207
|
padding: spacing.md,
|
|
189
208
|
backgroundColor: colors.surface,
|
package/src/renderer/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export { SduiScreenRenderer } from './SduiScreenRenderer';
|
|
2
|
+
export { SduiTemplateRow } from './SduiTemplateRow';
|
|
3
|
+
export {
|
|
4
|
+
materializeRowTemplate,
|
|
5
|
+
readSduiRowTemplate,
|
|
6
|
+
readSduiRowTemplates,
|
|
7
|
+
resolveRowTemplateBindings,
|
|
8
|
+
} from './lib/sdui-row-template';
|
|
9
|
+
export type { SduiRowTemplateSource } from './lib/sdui-row-template';
|
|
2
10
|
export {
|
|
3
11
|
resolveSduiBlockComponent,
|
|
4
12
|
registerSduiBlockComponent,
|
|
@@ -9,6 +17,46 @@ export {
|
|
|
9
17
|
SduiHybridRendererProvider,
|
|
10
18
|
useSduiHybridRenderer,
|
|
11
19
|
} from './sdui-hybrid-renderer-context';
|
|
20
|
+
export {
|
|
21
|
+
SduiMediaResolverProvider,
|
|
22
|
+
useSduiMediaResolver,
|
|
23
|
+
useSduiMediaUri,
|
|
24
|
+
} from './sdui-media-resolver-context';
|
|
25
|
+
export { invokeSduiMediaResolver } from './lib/sdui-media-uri';
|
|
26
|
+
export type { SduiMediaResolver } from './lib/sdui-media-uri';
|
|
27
|
+
export {
|
|
28
|
+
SDUI_MEDIA_ASPECT_RATIOS,
|
|
29
|
+
SDUI_AVATAR_SIZES,
|
|
30
|
+
SDUI_ROW_OVERFLOW_DEFAULT_ICON,
|
|
31
|
+
computeSduiMediaFrame,
|
|
32
|
+
deriveSduiInitials,
|
|
33
|
+
parseSduiAvatarProps,
|
|
34
|
+
parseSduiContentImageProps,
|
|
35
|
+
parseSduiIconButtonProps,
|
|
36
|
+
parseSduiRowOverflowMenuProps,
|
|
37
|
+
parseSduiVideoProps,
|
|
38
|
+
} from './lib/sdui-media-block-props';
|
|
39
|
+
export type {
|
|
40
|
+
SduiAvatarParsed,
|
|
41
|
+
SduiContentImageParsed,
|
|
42
|
+
SduiIconButtonParsed,
|
|
43
|
+
SduiMediaFrame,
|
|
44
|
+
SduiRowOverflowMenuActionParsed,
|
|
45
|
+
SduiRowOverflowMenuParsed,
|
|
46
|
+
SduiVideoParsed,
|
|
47
|
+
} from './lib/sdui-media-block-props';
|
|
48
|
+
export {
|
|
49
|
+
SduiAvatarBlock,
|
|
50
|
+
SduiContentImageBlock,
|
|
51
|
+
SduiIconButtonBlock,
|
|
52
|
+
SduiRowOverflowMenuBlock,
|
|
53
|
+
SduiVideoBlock,
|
|
54
|
+
} from './blocks/SduiMediaBlocks';
|
|
55
|
+
export {
|
|
56
|
+
SduiVideoPlayerProvider,
|
|
57
|
+
useSduiVideoPlayer,
|
|
58
|
+
} from './sdui-video-player-context';
|
|
59
|
+
export type { SduiVideoPlayer, SduiVideoPlayerProps } from './sdui-video-player-context';
|
|
12
60
|
export {
|
|
13
61
|
SduiChromeScrollProvider,
|
|
14
62
|
useSduiChromeScroll,
|