@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,63 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import type { SduiBlock, SduiBlockWire } from '@proulr/sdui-contract';
|
|
3
|
+
import { renderBlockTree } from './sdui-component-registry';
|
|
4
|
+
import { materializeRowTemplate, type SduiRowTemplateSource } from './lib/sdui-row-template';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
/** Props do bloco hospedeiro (`{ row_template, item_key, section }`) ou array nu de blocos do template. */
|
|
8
|
+
template: SduiRowTemplateSource | null | undefined;
|
|
9
|
+
/** Item vivo do feed (`screen.data[...]`). */
|
|
10
|
+
item: unknown;
|
|
11
|
+
/** Índice do item — fallback do sufixo de id quando `item_key` não resolve. */
|
|
12
|
+
index?: number;
|
|
13
|
+
/** `screen.data` repassado ao renderer de blocos. */
|
|
14
|
+
data?: Record<string, unknown>;
|
|
15
|
+
onAction: (actionId: string, vars?: Record<string, string>) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function toDomainBlock(block: SduiBlockWire): SduiBlock {
|
|
19
|
+
return {
|
|
20
|
+
id: block.id,
|
|
21
|
+
type: block.type,
|
|
22
|
+
props: block.props ?? {},
|
|
23
|
+
children: (block.children ?? []).map(toDomainBlock),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Renderiza **uma** linha de um row template wire-driven: materializa o subtree
|
|
29
|
+
* do bloco template para o item (bindings `{{item.*}}` + ids estáveis) e monta
|
|
30
|
+
* via registry de blocos do SDUI (`renderBlockTree`) — componentes genéricos,
|
|
31
|
+
* sem layout de linha hardcoded no híbrido.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* {items.map((post, i) => (
|
|
35
|
+
* <SduiTemplateRow
|
|
36
|
+
* key={post.id}
|
|
37
|
+
* template={readSduiRowTemplate(block.props)}
|
|
38
|
+
* item={post}
|
|
39
|
+
* index={i}
|
|
40
|
+
* data={data}
|
|
41
|
+
* onAction={onAction}
|
|
42
|
+
* />
|
|
43
|
+
* ))}
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function SduiTemplateRow({ template, item, index = 0, data = {}, onAction }: Props) {
|
|
47
|
+
const blocks = useMemo(
|
|
48
|
+
() => (template ? materializeRowTemplate(template, item, index) : []),
|
|
49
|
+
[template, item, index],
|
|
50
|
+
);
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{blocks.map((block) => {
|
|
54
|
+
const domainBlock = toDomainBlock(block);
|
|
55
|
+
return (
|
|
56
|
+
<React.Fragment key={domainBlock.id}>
|
|
57
|
+
{renderBlockTree(domainBlock, data, onAction)}
|
|
58
|
+
</React.Fragment>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
|
|
3
|
+
import { AppIcon } from '../ui/app-icon';
|
|
4
|
+
import { isAppIconName, type AppIconName } from '../../catalog/app-icon-names';
|
|
5
|
+
import { resolveDataRef } from '../lib/resolve-data-ref';
|
|
6
|
+
import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
|
|
7
|
+
|
|
8
|
+
function itemsFrom(
|
|
9
|
+
block: SduiBlockRenderProps['block'],
|
|
10
|
+
data: Record<string, unknown>,
|
|
11
|
+
fallback = '',
|
|
12
|
+
): unknown[] {
|
|
13
|
+
const refProp = block.props.items_ref;
|
|
14
|
+
const ref = typeof refProp === 'string' && refProp ? refProp : fallback;
|
|
15
|
+
if (!ref) return [];
|
|
16
|
+
const raw = resolveDataRef(data, ref);
|
|
17
|
+
return Array.isArray(raw) ? raw : [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function asRecord(item: unknown): Record<string, unknown> | null {
|
|
21
|
+
return item && typeof item === 'object' ? (item as Record<string, unknown>) : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toText(value: unknown): string {
|
|
25
|
+
if (typeof value === 'string') return value;
|
|
26
|
+
if (value != null) return String(value);
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toIcon(value: unknown): AppIconName | undefined {
|
|
31
|
+
return typeof value === 'string' && isAppIconName(value) ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── stat_tile_row ─────────────────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
type StatTile = {
|
|
37
|
+
icon?: AppIconName;
|
|
38
|
+
value: string;
|
|
39
|
+
label: string;
|
|
40
|
+
actionId?: string;
|
|
41
|
+
actionParams?: Record<string, string>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function chunk<T>(items: T[], size: number): T[][] {
|
|
45
|
+
const rows: T[][] = [];
|
|
46
|
+
for (let i = 0; i < items.length; i += size) rows.push(items.slice(i, i + size));
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function StatTileView({
|
|
51
|
+
tile,
|
|
52
|
+
card,
|
|
53
|
+
onAction,
|
|
54
|
+
}: {
|
|
55
|
+
tile: StatTile;
|
|
56
|
+
card: boolean;
|
|
57
|
+
onAction: SduiBlockRenderProps['onAction'];
|
|
58
|
+
}) {
|
|
59
|
+
const content = card ? (
|
|
60
|
+
<>
|
|
61
|
+
<View style={styles.cardHead}>
|
|
62
|
+
{tile.icon ? (
|
|
63
|
+
<View style={styles.cardIcon}>
|
|
64
|
+
<AppIcon name={tile.icon} size={16} color={m3Colors.primary} />
|
|
65
|
+
</View>
|
|
66
|
+
) : null}
|
|
67
|
+
{tile.actionId ? <AppIcon name="arrow-up-right" size={16} color={m3Colors.onSurfaceVariant} /> : null}
|
|
68
|
+
</View>
|
|
69
|
+
<Text style={styles.cardValue} numberOfLines={2}>{tile.value}</Text>
|
|
70
|
+
<Text style={styles.label} numberOfLines={1}>{tile.label}</Text>
|
|
71
|
+
</>
|
|
72
|
+
) : (
|
|
73
|
+
<>
|
|
74
|
+
{tile.icon ? <AppIcon name={tile.icon} size={17} color={m3Colors.onSurfaceVariant} /> : null}
|
|
75
|
+
<Text style={styles.value} numberOfLines={1}>{tile.value}</Text>
|
|
76
|
+
<Text style={styles.label} numberOfLines={1}>{tile.label}</Text>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
const tileStyle = card ? styles.card : styles.tile;
|
|
80
|
+
const actionId = tile.actionId;
|
|
81
|
+
if (!actionId) return <View style={tileStyle}>{content}</View>;
|
|
82
|
+
return (
|
|
83
|
+
<Pressable
|
|
84
|
+
accessibilityRole="link"
|
|
85
|
+
accessibilityLabel={`${tile.label} ${tile.value}`.trim()}
|
|
86
|
+
onPress={() => onAction(actionId, tile.actionParams)}
|
|
87
|
+
style={({ pressed }) => [tileStyle, card && styles.cardLink, pressed && styles.pressed]}
|
|
88
|
+
>
|
|
89
|
+
{content}
|
|
90
|
+
</Pressable>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tiles genéricos de métrica (ícone + valor + rótulo) — layout vem do wire, regra do BFF. */
|
|
95
|
+
export function SduiStatTileRowBlock({ block, data, onAction }: SduiBlockRenderProps) {
|
|
96
|
+
const tiles: StatTile[] = itemsFrom(block, data, 'stats')
|
|
97
|
+
.map(asRecord)
|
|
98
|
+
.filter((row): row is Record<string, unknown> => row !== null)
|
|
99
|
+
.map((row) => ({
|
|
100
|
+
icon: toIcon(row.icon),
|
|
101
|
+
value: toText(row.value),
|
|
102
|
+
label: toText(row.label),
|
|
103
|
+
actionId: typeof row.action_id === 'string' && row.action_id ? row.action_id : undefined,
|
|
104
|
+
actionParams:
|
|
105
|
+
row.action_params && typeof row.action_params === 'object'
|
|
106
|
+
? (row.action_params as Record<string, string>)
|
|
107
|
+
: undefined,
|
|
108
|
+
}))
|
|
109
|
+
.filter((tile) => tile.value || tile.label);
|
|
110
|
+
if (tiles.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
const columnsProp = block.props.columns;
|
|
113
|
+
const columns =
|
|
114
|
+
typeof columnsProp === 'number' && Number.isInteger(columnsProp) && columnsProp > 0
|
|
115
|
+
? columnsProp
|
|
116
|
+
: tiles.length;
|
|
117
|
+
const card = block.props.align === 'start';
|
|
118
|
+
const rows = chunk(tiles, columns);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View style={styles.grid} accessibilityRole="list">
|
|
122
|
+
{rows.map((row, rIndex) => (
|
|
123
|
+
<View key={`row-${rIndex}`} style={styles.row}>
|
|
124
|
+
{row.map((tile, index) => (
|
|
125
|
+
<StatTileView key={`${tile.label}-${index}`} tile={tile} card={card} onAction={onAction} />
|
|
126
|
+
))}
|
|
127
|
+
{Array.from({ length: columns - row.length }, (_, pad) => (
|
|
128
|
+
<View key={`pad-${pad}`} style={styles.pad} />
|
|
129
|
+
))}
|
|
130
|
+
</View>
|
|
131
|
+
))}
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── data_group_list ──────────────────────────────────────────────────────── */
|
|
137
|
+
|
|
138
|
+
/** Grupos de detalhes (cabeçalho com ícone + linhas rótulo→valor). Reutilizável em qualquer tela. */
|
|
139
|
+
export function SduiDataGroupListBlock({ block, data }: SduiBlockRenderProps) {
|
|
140
|
+
const groups = itemsFrom(block, data)
|
|
141
|
+
.map(asRecord)
|
|
142
|
+
.filter((g): g is Record<string, unknown> => g !== null)
|
|
143
|
+
.map((group) => {
|
|
144
|
+
const rowsRaw = Array.isArray(group.rows) ? group.rows : [];
|
|
145
|
+
const rows = rowsRaw
|
|
146
|
+
.map(asRecord)
|
|
147
|
+
.filter((r): r is Record<string, unknown> => r !== null)
|
|
148
|
+
.map((row) => ({ label: toText(row.label), value: toText(row.value) }))
|
|
149
|
+
.filter((row) => row.label || row.value);
|
|
150
|
+
return {
|
|
151
|
+
icon: toIcon(group.icon),
|
|
152
|
+
title: toText(group.title),
|
|
153
|
+
rows,
|
|
154
|
+
};
|
|
155
|
+
})
|
|
156
|
+
.filter((group) => group.rows.length > 0);
|
|
157
|
+
if (groups.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<View style={styles.groups}>
|
|
161
|
+
{groups.map((group, gIndex) => (
|
|
162
|
+
<View key={`${group.title}-${gIndex}`} style={styles.group}>
|
|
163
|
+
<View style={styles.groupHead} accessibilityRole="header">
|
|
164
|
+
{group.icon ? <AppIcon name={group.icon} size={16} color={m3Colors.onSurface} /> : null}
|
|
165
|
+
<Text style={styles.groupTitle}>{group.title}</Text>
|
|
166
|
+
</View>
|
|
167
|
+
{group.rows.map((row, rIndex) => (
|
|
168
|
+
<View key={`${row.label}-${rIndex}`} style={styles.dataRow}>
|
|
169
|
+
<Text style={styles.dataLabel} numberOfLines={1}>{row.label}</Text>
|
|
170
|
+
<Text style={styles.dataValue}>{row.value}</Text>
|
|
171
|
+
</View>
|
|
172
|
+
))}
|
|
173
|
+
</View>
|
|
174
|
+
))}
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ── vouch_list ───────────────────────────────────────────────────────────── */
|
|
180
|
+
|
|
181
|
+
/** Cartões de citação/recomendação (cabeçalho + citações com autor). Reutilizável (depoimentos, reviews). */
|
|
182
|
+
export function SduiVouchListBlock({ block, data }: SduiBlockRenderProps) {
|
|
183
|
+
const itemsRefProp = block.props.items_ref;
|
|
184
|
+
const itemsRef = typeof itemsRefProp === 'string' && itemsRefProp ? itemsRefProp : 'vouches';
|
|
185
|
+
const container = asRecord(resolveDataRef(data, itemsRef));
|
|
186
|
+
const header = container ? toText(container.header) : '';
|
|
187
|
+
const items = (container && Array.isArray(container.items) ? container.items : [])
|
|
188
|
+
.map(asRecord)
|
|
189
|
+
.filter((i): i is Record<string, unknown> => i !== null)
|
|
190
|
+
.map((item) => ({
|
|
191
|
+
quote: toText(item.quote),
|
|
192
|
+
author: toText(item.author_name),
|
|
193
|
+
meta: toText(item.meta),
|
|
194
|
+
}))
|
|
195
|
+
.filter((item) => item.quote);
|
|
196
|
+
if (!header && items.length === 0) return null;
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<View style={styles.vouchWrap}>
|
|
200
|
+
{header ? <Text style={styles.vouchHeader}>{header}</Text> : null}
|
|
201
|
+
{items.map((item, index) => (
|
|
202
|
+
<View key={`vouch-${index}`} style={styles.vouchCard}>
|
|
203
|
+
<Text style={styles.vouchQuote}>“{item.quote}”</Text>
|
|
204
|
+
<View style={styles.vouchFooter}>
|
|
205
|
+
<Text style={styles.vouchAuthor}>{item.author}</Text>
|
|
206
|
+
{item.meta ? <Text style={styles.vouchMeta}>{item.meta}</Text> : null}
|
|
207
|
+
</View>
|
|
208
|
+
</View>
|
|
209
|
+
))}
|
|
210
|
+
</View>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const styles = StyleSheet.create({
|
|
215
|
+
grid: { gap: spacing.sm },
|
|
216
|
+
row: { flexDirection: 'row', gap: spacing.sm },
|
|
217
|
+
pad: { flex: 1 },
|
|
218
|
+
tile: {
|
|
219
|
+
flex: 1,
|
|
220
|
+
minHeight: 64,
|
|
221
|
+
borderRadius: m3Shape.cornerMedium,
|
|
222
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
223
|
+
borderWidth: 1,
|
|
224
|
+
borderColor: m3Colors.outlineVariant,
|
|
225
|
+
alignItems: 'center',
|
|
226
|
+
justifyContent: 'center',
|
|
227
|
+
gap: 1,
|
|
228
|
+
paddingVertical: spacing.sm,
|
|
229
|
+
paddingHorizontal: spacing.xs,
|
|
230
|
+
},
|
|
231
|
+
value: { ...m3Type.title, color: m3Colors.onSurface, fontFamily: fonts.extrabold },
|
|
232
|
+
label: { ...m3Type.labelSmall, color: m3Colors.onSurfaceVariant },
|
|
233
|
+
card: {
|
|
234
|
+
flex: 1,
|
|
235
|
+
minWidth: 0,
|
|
236
|
+
minHeight: 92,
|
|
237
|
+
borderRadius: m3Shape.cornerMedium,
|
|
238
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
239
|
+
borderWidth: 1,
|
|
240
|
+
borderColor: m3Colors.outlineVariant,
|
|
241
|
+
gap: spacing.xs,
|
|
242
|
+
padding: spacing.sm + 4,
|
|
243
|
+
},
|
|
244
|
+
cardLink: { borderColor: m3Colors.outlineInteractive },
|
|
245
|
+
cardHead: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
246
|
+
cardIcon: {
|
|
247
|
+
width: 28,
|
|
248
|
+
height: 28,
|
|
249
|
+
borderRadius: m3Shape.cornerSmall,
|
|
250
|
+
backgroundColor: m3Colors.surfaceContainerHighest,
|
|
251
|
+
alignItems: 'center',
|
|
252
|
+
justifyContent: 'center',
|
|
253
|
+
},
|
|
254
|
+
cardValue: { ...m3Type.body, color: m3Colors.onSurface, fontFamily: fonts.bold },
|
|
255
|
+
pressed: { opacity: 0.85 },
|
|
256
|
+
|
|
257
|
+
groups: { gap: spacing.md },
|
|
258
|
+
group: {
|
|
259
|
+
borderRadius: m3Shape.cornerMedium,
|
|
260
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
261
|
+
borderWidth: 1,
|
|
262
|
+
borderColor: m3Colors.outlineVariant,
|
|
263
|
+
overflow: 'hidden',
|
|
264
|
+
},
|
|
265
|
+
groupHead: {
|
|
266
|
+
flexDirection: 'row',
|
|
267
|
+
alignItems: 'center',
|
|
268
|
+
gap: spacing.sm,
|
|
269
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
270
|
+
paddingHorizontal: spacing.md,
|
|
271
|
+
paddingVertical: spacing.sm,
|
|
272
|
+
},
|
|
273
|
+
groupTitle: { ...m3Type.label, color: m3Colors.onSurface, fontFamily: fonts.bold },
|
|
274
|
+
dataRow: {
|
|
275
|
+
flexDirection: 'row',
|
|
276
|
+
justifyContent: 'space-between',
|
|
277
|
+
alignItems: 'flex-start',
|
|
278
|
+
gap: spacing.md,
|
|
279
|
+
paddingHorizontal: spacing.md,
|
|
280
|
+
paddingVertical: spacing.sm,
|
|
281
|
+
},
|
|
282
|
+
dataLabel: { ...m3Type.bodySmall, color: m3Colors.onSurfaceVariant, flexShrink: 0 },
|
|
283
|
+
dataValue: { ...m3Type.bodySmall, color: m3Colors.onSurface, fontFamily: fonts.semibold, flex: 1, textAlign: 'right' },
|
|
284
|
+
|
|
285
|
+
vouchWrap: { gap: spacing.sm },
|
|
286
|
+
vouchHeader: { ...m3Type.bodySmall, color: m3Colors.onSurfaceVariant },
|
|
287
|
+
vouchCard: {
|
|
288
|
+
borderRadius: m3Shape.cornerMedium,
|
|
289
|
+
backgroundColor: m3Colors.surfaceContainerHigh,
|
|
290
|
+
borderWidth: 1,
|
|
291
|
+
borderColor: m3Colors.outlineVariant,
|
|
292
|
+
padding: spacing.md,
|
|
293
|
+
gap: spacing.sm,
|
|
294
|
+
},
|
|
295
|
+
vouchQuote: { ...m3Type.body, color: m3Colors.onSurface },
|
|
296
|
+
vouchFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: spacing.sm },
|
|
297
|
+
vouchAuthor: { ...m3Type.labelSmall, color: m3Colors.secondary, fontFamily: fonts.bold },
|
|
298
|
+
vouchMeta: { ...m3Type.labelSmall, color: m3Colors.onSurfaceVariant },
|
|
299
|
+
});
|
|
@@ -13,7 +13,7 @@ import type { SduiBlock } from '@proulr/sdui-contract';
|
|
|
13
13
|
import { TextField as UiTextField } from '../ui/TextField';
|
|
14
14
|
import { Chip } from '../ui/Chip';
|
|
15
15
|
import { ChipRow } from '../ui/ChipRow';
|
|
16
|
-
import { AppIcon } from '../ui/app-icon';
|
|
16
|
+
import { AppIcon, isAppIconName } from '../ui/app-icon';
|
|
17
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';
|
|
@@ -280,6 +280,7 @@ export function SduiDiscoverGridBlock({ block, data, onAction }: SduiBlockRender
|
|
|
280
280
|
export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
281
281
|
const label = readStringProp(block, 'label') ?? '';
|
|
282
282
|
const glyph = readStringProp(block, 'glyph');
|
|
283
|
+
const tone = readStringProp(block, 'tone');
|
|
283
284
|
const actionId = readStringProp(block, 'action_id');
|
|
284
285
|
const actionParams = readStringRecordProp(block, 'action_params');
|
|
285
286
|
const active = readBooleanProp(block, 'active');
|
|
@@ -288,6 +289,7 @@ export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
|
288
289
|
<Chip
|
|
289
290
|
label={label}
|
|
290
291
|
icon={glyph}
|
|
292
|
+
tone={tone === 'secondary' || tone === 'primary' ? tone : undefined}
|
|
291
293
|
active={active}
|
|
292
294
|
disabled={!actionId}
|
|
293
295
|
onPress={() => actionId && onAction(actionId, actionParams)}
|
|
@@ -295,8 +297,42 @@ export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) {
|
|
|
295
297
|
);
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
export function SduiChipRowBlock({ children }: SduiBlockRenderProps) {
|
|
299
|
-
|
|
300
|
+
export function SduiChipRowBlock({ block, data, children, onAction }: SduiBlockRenderProps) {
|
|
301
|
+
const itemsRef = readStringProp(block, 'items_ref');
|
|
302
|
+
const wrap = readStringProp(block, 'layout') === 'wrap';
|
|
303
|
+
if (itemsRef) {
|
|
304
|
+
const raw = resolveDataRef(data, itemsRef);
|
|
305
|
+
const items = Array.isArray(raw) ? raw : [];
|
|
306
|
+
return (
|
|
307
|
+
<ChipRow wrap={wrap}>
|
|
308
|
+
{items.map((item, index) => {
|
|
309
|
+
if (!item || typeof item !== 'object') return null;
|
|
310
|
+
const row = item as Record<string, unknown>;
|
|
311
|
+
const label = typeof row.label === 'string' ? row.label : '';
|
|
312
|
+
if (!label) return null;
|
|
313
|
+
const glyph = typeof row.glyph === 'string' && isAppIconName(row.glyph) ? row.glyph : undefined;
|
|
314
|
+
const toneRaw = row.tone;
|
|
315
|
+
const tone = toneRaw === 'secondary' || toneRaw === 'primary' ? toneRaw : undefined;
|
|
316
|
+
const actionId = typeof row.action_id === 'string' ? row.action_id : undefined;
|
|
317
|
+
const actionParams =
|
|
318
|
+
row.action_params && typeof row.action_params === 'object'
|
|
319
|
+
? (row.action_params as Record<string, string>)
|
|
320
|
+
: undefined;
|
|
321
|
+
return (
|
|
322
|
+
<Chip
|
|
323
|
+
key={`${label}-${index}`}
|
|
324
|
+
label={label}
|
|
325
|
+
icon={glyph}
|
|
326
|
+
tone={tone}
|
|
327
|
+
readOnly={!actionId}
|
|
328
|
+
onPress={() => actionId && onAction(actionId, actionParams)}
|
|
329
|
+
/>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</ChipRow>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return <ChipRow wrap={wrap}>{children}</ChipRow>;
|
|
300
336
|
}
|
|
301
337
|
|
|
302
338
|
export function SduiTextFieldBlock({ block, data, onAction }: SduiBlockRenderProps) {
|