@lotics/ui 5.8.0 → 5.10.0
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/AGENTS.md +184 -56
- package/examples/tpl_approvals.tsx +3 -3
- package/examples/tpl_convert.tsx +11 -23
- package/examples/tpl_dossier.tsx +5 -4
- package/examples/tpl_order.tsx +1 -1
- package/examples/tpl_pipeline.tsx +400 -0
- package/examples/tpl_ratedesk.tsx +1 -1
- package/examples/tpl_record.tsx +6 -5
- package/examples/tpl_shifts.tsx +7 -5
- package/examples/tpl_task_board.tsx +473 -0
- package/examples/tpl_tasks.tsx +440 -0
- package/package.json +19 -6
- package/src/accordion.tsx +1 -1
- package/src/agent_progress.tsx +1 -1
- package/src/agent_run.tsx +1 -1
- package/src/breakdown.tsx +2 -2
- package/src/button.tsx +12 -3
- package/src/calendar/calendar_view.tsx +10 -10
- package/src/calendar/month_view.tsx +30 -11
- package/src/calendar/time_grid_view.tsx +25 -7
- package/src/card_select_item.tsx +11 -13
- package/src/check_circle.tsx +88 -0
- package/src/checkbox_input.tsx +5 -1
- package/src/chip.tsx +1 -1
- package/src/chip_group.tsx +1 -1
- package/src/column_filter.tsx +3 -2
- package/src/combobox.tsx +84 -219
- package/src/confidence.tsx +14 -3
- package/src/control_surface.ts +22 -7
- package/src/data_grid.tsx +159 -0
- package/src/date_calendar.tsx +46 -23
- package/src/date_field.tsx +10 -2
- package/src/date_filter.tsx +5 -9
- package/src/date_picker.tsx +78 -26
- package/src/date_picker_value.ts +11 -0
- package/src/date_range_filter_field.tsx +6 -4
- package/src/drawer.tsx +1 -1
- package/src/file_dropzone.tsx +5 -0
- package/src/file_gallery_modal.tsx +227 -139
- package/src/file_grid.tsx +184 -0
- package/src/file_preview.web.tsx +349 -12
- package/src/file_preview_types.ts +49 -0
- package/src/file_row.tsx +118 -0
- package/src/file_rows.tsx +109 -0
- package/src/file_thumbnail.tsx +132 -81
- package/src/file_thumbnail_grid.tsx +135 -55
- package/src/files_editor.tsx +232 -0
- package/src/filter_chip.tsx +25 -12
- package/src/focus_ring_pressable.tsx +34 -0
- package/src/form_picker.tsx +3 -3
- package/src/gantt/gantt_view.tsx +25 -7
- package/src/heatmap.tsx +5 -1
- package/src/icon_button.tsx +14 -3
- package/src/image_gallery.tsx +8 -14
- package/src/index.css +8 -12
- package/src/inline_date_picker.tsx +11 -11
- package/src/inline_edit.tsx +22 -6
- package/src/inline_member_select.tsx +1 -1
- package/src/inline_select.tsx +9 -8
- package/src/inline_text_input.tsx +4 -1
- package/src/link.tsx +11 -1
- package/src/link_button.tsx +1 -1
- package/src/list_item.tsx +2 -2
- package/src/member_select.tsx +9 -4
- package/src/menu_button.tsx +2 -2
- package/src/menu_list_item.tsx +2 -2
- package/src/number_input.tsx +11 -4
- package/src/option_list.tsx +211 -0
- package/src/pdfjs_worker.d.ts +5 -0
- package/src/peek.tsx +1 -1
- package/src/picker.tsx +24 -213
- package/src/pressable_highlight.tsx +42 -23
- package/src/radio_picker.tsx +2 -2
- package/src/range_slider.tsx +35 -7
- package/src/react_native.d.ts +3 -0
- package/src/scroll_to_bottom.tsx +1 -1
- package/src/section.tsx +7 -2
- package/src/segmented_control.tsx +2 -2
- package/src/select.tsx +299 -0
- package/src/sort_header.tsx +1 -1
- package/src/sources.tsx +1 -1
- package/src/spreadsheet_view.tsx +43 -17
- package/src/status_grid.tsx +4 -3
- package/src/stepper.tsx +1 -1
- package/src/switch.tsx +5 -1
- package/src/switch_button.tsx +1 -1
- package/src/switcher.tsx +1 -1
- package/src/table.tsx +5 -1
- package/src/tabs.tsx +2 -2
- package/src/text_input_field.tsx +15 -4
- package/src/time_picker.tsx +11 -4
- package/src/timeline.tsx +1 -1
- package/src/uploading_thumbnail.tsx +212 -0
- package/src/use_focus_ring.ts +80 -0
- package/src/use_hover.ts +26 -0
- package/src/use_list_keyboard_nav.test.ts +71 -0
- package/src/use_list_keyboard_nav.ts +52 -5
- package/src/use_option_list.test.ts +193 -0
- package/src/use_option_list.ts +354 -0
- package/src/command_menu.tsx +0 -205
- package/src/hover_action.tsx +0 -54
- package/src/picker_menu.tsx +0 -355
- package/src/tag_input.tsx +0 -203
- package/src/time_field.tsx +0 -297
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// FileRows — the common "show a list of files" surface, batteries included: each
|
|
2
|
+
// file is a FileRow whose whole-row press opens the built-in full-screen
|
|
3
|
+
// FileGalleryModal, with a ⋯ menu (Download · Open in new tab · Remove) on the
|
|
4
|
+
// trailing edge. Pure kit composition (FileRow + ActionMenu + FileGalleryModal) —
|
|
5
|
+
// reach for the pieces directly only when you need a different shape.
|
|
6
|
+
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { View } from "react-native";
|
|
9
|
+
import { Alert } from "./alert";
|
|
10
|
+
import { FileRow } from "./file_row";
|
|
11
|
+
import { ActionMenu, type ActionMenuItem } from "./action_menu";
|
|
12
|
+
import { FileGalleryModal } from "./file_gallery_modal";
|
|
13
|
+
import { resolveMime } from "./file_badge";
|
|
14
|
+
import { downloadFileFromUrl } from "./download";
|
|
15
|
+
import { resolveGalleryLabels, type GalleryLabels } from "./file_preview_types";
|
|
16
|
+
import type { DisplayFile } from "./file_thumbnail";
|
|
17
|
+
|
|
18
|
+
export interface FileRowsProps {
|
|
19
|
+
files: DisplayFile[];
|
|
20
|
+
/** Secondary line per file. Default: the file-type label (e.g. "PDF"). */
|
|
21
|
+
meta?: (file: DisplayFile) => string | undefined;
|
|
22
|
+
/** Adds a (confirmed) "Remove" to each ⋯ menu and the gallery toolbar. The host
|
|
23
|
+
* drops the file from its own state. */
|
|
24
|
+
onRemove?: (file: DisplayFile) => void;
|
|
25
|
+
/** Open a file outside the app (a new tab on the frontend, the SDK's
|
|
26
|
+
* `openExternal` in a sandboxed app). Adds "Open in new tab". */
|
|
27
|
+
onOpenExternal?: (file: DisplayFile) => void;
|
|
28
|
+
/** Override the Download action (default: fetch + save the URL). The host
|
|
29
|
+
* frontend passes its own to send `credentials` for auth-gated proxy URLs. */
|
|
30
|
+
onDownload?: (file: DisplayFile) => void;
|
|
31
|
+
/** Reported when a previewed file fails to render (host wires its logger). */
|
|
32
|
+
onError?: (error: unknown, meta: { fileId: string; mimeType: string }) => void;
|
|
33
|
+
/** Translated chrome — Download / Remove / Open-external / gallery + confirm. */
|
|
34
|
+
labels?: Partial<GalleryLabels>;
|
|
35
|
+
/** Gap between rows. Default 8. */
|
|
36
|
+
gap?: number;
|
|
37
|
+
/** Credentials mode for the gallery's preview fetches — `"include"` for the
|
|
38
|
+
* host's auth-gated proxy URLs, omitted for an app's presigned URLs. */
|
|
39
|
+
credentials?: RequestCredentials;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A file LIST that just works: tap a row to preview it full-screen, ⋯ for quick
|
|
44
|
+
* actions. The default surface for "here are some files" — saved attachments, a
|
|
45
|
+
* record's documents, a message's files. For an ADD/upload surface use `FileGrid`;
|
|
46
|
+
* for bare square tiles use `FileThumbnailGrid`.
|
|
47
|
+
*/
|
|
48
|
+
export function FileRows({ files, meta, onRemove, onOpenExternal, onDownload, onError, labels, gap = 8, credentials }: FileRowsProps) {
|
|
49
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
50
|
+
const l = resolveGalleryLabels(labels);
|
|
51
|
+
|
|
52
|
+
if (files.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
const download = (file: DisplayFile) => {
|
|
55
|
+
if (onDownload) {
|
|
56
|
+
onDownload(file);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void downloadFileFromUrl(file.url, file.filename, { credentials });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const confirmRemove = (file: DisplayFile) => {
|
|
63
|
+
if (!onRemove) return;
|
|
64
|
+
Alert.alert(l.removeConfirmTitle, l.removeConfirmMessage(file.filename), [
|
|
65
|
+
{ text: l.cancel, style: "cancel" },
|
|
66
|
+
{ text: l.remove, style: "destructive", onPress: () => onRemove(file) },
|
|
67
|
+
]);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View style={{ gap }}>
|
|
72
|
+
{files.map((file, index) => {
|
|
73
|
+
const items: ActionMenuItem[] = [
|
|
74
|
+
{ key: "download", label: l.download, icon: "download", onPress: () => download(file) },
|
|
75
|
+
];
|
|
76
|
+
if (onOpenExternal) {
|
|
77
|
+
items.push({ key: "open", label: l.openExternal, icon: "external-link", onPress: () => onOpenExternal(file) });
|
|
78
|
+
}
|
|
79
|
+
if (onRemove) {
|
|
80
|
+
items.push({ key: "remove", label: l.remove, icon: "trash", danger: true, onPress: () => confirmRemove(file) });
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<FileRow
|
|
84
|
+
key={file.id}
|
|
85
|
+
name={file.filename}
|
|
86
|
+
meta={meta ? meta(file) : resolveMime(file.mimeType).label}
|
|
87
|
+
mimeType={file.mimeType}
|
|
88
|
+
onPress={() => setActiveIndex(index)}
|
|
89
|
+
trailing={<ActionMenu accessibilityLabel={`${l.actions}: ${file.filename}`} items={items} />}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
<FileGalleryModal
|
|
94
|
+
files={files}
|
|
95
|
+
activeIndex={activeIndex}
|
|
96
|
+
onIndexChange={setActiveIndex}
|
|
97
|
+
onDownload={onDownload}
|
|
98
|
+
onOpenExternal={onOpenExternal}
|
|
99
|
+
credentials={credentials}
|
|
100
|
+
onRemove={onRemove ? (fileId) => {
|
|
101
|
+
const file = files.find((f) => f.id === fileId);
|
|
102
|
+
if (file) onRemove(file);
|
|
103
|
+
} : undefined}
|
|
104
|
+
onError={onError}
|
|
105
|
+
labels={labels}
|
|
106
|
+
/>
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
package/src/file_thumbnail.tsx
CHANGED
|
@@ -2,9 +2,12 @@ import { FileBadge, resolveMime } from "./file_badge";
|
|
|
2
2
|
import { fontFamilySemiBold } from "./text_utils";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { Icon, type IconName } from "./icon";
|
|
5
|
+
import { IconButton } from "./icon_button";
|
|
5
6
|
import { Checkbox } from "./checkbox";
|
|
6
7
|
import { ActivityIndicator } from "./activity_indicator";
|
|
7
8
|
import { colors } from "./colors";
|
|
9
|
+
import { FOCUS_RING } from "./control_surface";
|
|
10
|
+
import { useFocusRing } from "./use_focus_ring";
|
|
8
11
|
import { useCallback, useState } from "react";
|
|
9
12
|
import {
|
|
10
13
|
GestureResponderEvent,
|
|
@@ -70,6 +73,8 @@ interface FileThumbnailProps {
|
|
|
70
73
|
/** While true, dims the thumbnail with a spinner — the file is still uploading
|
|
71
74
|
* (the local preview shows immediately; this marks it as in-flight). */
|
|
72
75
|
uploading?: boolean;
|
|
76
|
+
/** Marks the file as a document TEMPLATE — adds a "TMPL" badge (any file type). */
|
|
77
|
+
isTemplate?: boolean;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/**
|
|
@@ -77,7 +82,7 @@ interface FileThumbnailProps {
|
|
|
77
82
|
* Works with any file source via DisplayFile interface.
|
|
78
83
|
*/
|
|
79
84
|
export function FileThumbnail(props: FileThumbnailProps) {
|
|
80
|
-
const { file, size, onPress, onLongPress, onRemove, selected, uploading } = props;
|
|
85
|
+
const { file, size, onPress, onLongPress, onRemove, selected, uploading, isTemplate } = props;
|
|
81
86
|
|
|
82
87
|
const rootStyle =
|
|
83
88
|
size !== undefined ? { width: size, height: size } : { width: "100%" as const, aspectRatio: 1 };
|
|
@@ -101,6 +106,7 @@ export function FileThumbnail(props: FileThumbnailProps) {
|
|
|
101
106
|
<DocumentBadge
|
|
102
107
|
mimeType={file.mimeType}
|
|
103
108
|
size={size}
|
|
109
|
+
isTemplate={isTemplate}
|
|
104
110
|
onPress={onPress ?? (() => Linking.openURL(file.url))}
|
|
105
111
|
onLongPress={onLongPress}
|
|
106
112
|
/>
|
|
@@ -126,6 +132,7 @@ export function FileThumbnail(props: FileThumbnailProps) {
|
|
|
126
132
|
mimeType={file.mimeType}
|
|
127
133
|
filename={file.filename}
|
|
128
134
|
size={size}
|
|
135
|
+
isTemplate={isTemplate}
|
|
129
136
|
onPress={onPress ?? (() => Linking.openURL(file.url))}
|
|
130
137
|
onLongPress={onLongPress}
|
|
131
138
|
/>
|
|
@@ -157,7 +164,7 @@ const uploadingOverlayStyles = StyleSheet.create({
|
|
|
157
164
|
alignItems: "center",
|
|
158
165
|
justifyContent: "center",
|
|
159
166
|
backgroundColor: "rgba(24,24,27,0.42)",
|
|
160
|
-
borderRadius:
|
|
167
|
+
borderRadius: 10,
|
|
161
168
|
},
|
|
162
169
|
});
|
|
163
170
|
|
|
@@ -178,9 +185,10 @@ interface DocumentBadgeProps {
|
|
|
178
185
|
*/
|
|
179
186
|
export function DocumentBadge(props: DocumentBadgeProps) {
|
|
180
187
|
const { mimeType, isTemplate, onPress, onLongPress } = props;
|
|
188
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
181
189
|
|
|
182
190
|
return (
|
|
183
|
-
<Pressable onPress={onPress} onLongPress={onLongPress}>
|
|
191
|
+
<Pressable onPress={onPress} onLongPress={onLongPress} {...focusProps} style={focusVisible ? { boxShadow: FOCUS_RING, borderRadius: 6 } : undefined}>
|
|
184
192
|
<FileBadge mimeType={mimeType} isTemplate={isTemplate} />
|
|
185
193
|
</Pressable>
|
|
186
194
|
);
|
|
@@ -190,42 +198,47 @@ interface DocumentCardProps {
|
|
|
190
198
|
mimeType: string;
|
|
191
199
|
filename: string;
|
|
192
200
|
size?: number;
|
|
201
|
+
isTemplate?: boolean;
|
|
193
202
|
onPress?: () => void;
|
|
194
203
|
onLongPress?: () => void;
|
|
195
204
|
overlay?: React.ReactNode;
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
/**
|
|
199
|
-
* Document
|
|
208
|
+
* Document tile: the file-type badge centered with the filename directly below
|
|
209
|
+
* it (no divider, no footer box). The badge scales with the tile and carries the
|
|
210
|
+
* type, color, and the TMPL marker. For readable filenames in a list, reach for
|
|
211
|
+
* `FileRow` (horizontal) instead.
|
|
200
212
|
*/
|
|
201
213
|
export function DocumentCard(props: DocumentCardProps) {
|
|
202
|
-
const { mimeType, filename, size, onPress, onLongPress, overlay } = props;
|
|
203
|
-
const { label, color } = resolveMime(mimeType);
|
|
214
|
+
const { mimeType, filename, size, isTemplate, onPress, onLongPress, overlay } = props;
|
|
204
215
|
|
|
205
216
|
const sizeStyle =
|
|
206
217
|
size !== undefined
|
|
207
218
|
? { width: size, height: size }
|
|
208
219
|
: { width: "100%" as const, height: "100%" as const };
|
|
209
220
|
|
|
221
|
+
// Badge takes ~40% of the tile, clamped so tiny/huge tiles stay tasteful.
|
|
222
|
+
const badgeSize = size !== undefined ? Math.max(30, Math.min(60, Math.round(size * 0.4))) : 40;
|
|
223
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
224
|
+
|
|
210
225
|
return (
|
|
211
|
-
<Pressable
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
</View>
|
|
228
|
-
</View>
|
|
226
|
+
<Pressable
|
|
227
|
+
style={(state) => [
|
|
228
|
+
styles.documentTile,
|
|
229
|
+
sizeStyle,
|
|
230
|
+
(state as { hovered?: boolean }).hovered && styles.tileHovered,
|
|
231
|
+
state.pressed && styles.tilePressed,
|
|
232
|
+
focusVisible && { boxShadow: FOCUS_RING },
|
|
233
|
+
]}
|
|
234
|
+
onPress={onPress}
|
|
235
|
+
onLongPress={onLongPress}
|
|
236
|
+
{...focusProps}
|
|
237
|
+
>
|
|
238
|
+
<FileBadge mimeType={mimeType} size={badgeSize} isTemplate={isTemplate} />
|
|
239
|
+
<Text size="xs" numberOfLines={1} color="zinc-500" style={styles.documentTileName} userSelect="none">
|
|
240
|
+
{filename}
|
|
241
|
+
</Text>
|
|
229
242
|
{overlay}
|
|
230
243
|
</Pressable>
|
|
231
244
|
);
|
|
@@ -257,14 +270,16 @@ export function MediaCard(props: MediaCardProps) {
|
|
|
257
270
|
: { width: "100%" as const, height: "100%" as const };
|
|
258
271
|
|
|
259
272
|
const isCompact = size !== undefined && size <= COMPACT_THUMBNAIL_SIZE;
|
|
273
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
260
274
|
|
|
261
275
|
if (isCompact) {
|
|
262
276
|
const iconSize = Math.max(12, Math.round(size * 0.55));
|
|
263
277
|
return (
|
|
264
278
|
<Pressable
|
|
265
|
-
style={[styles.mediaCardCompact, sizeStyle, { backgroundColor: color }]}
|
|
279
|
+
style={({ pressed }) => [styles.mediaCardCompact, sizeStyle, { backgroundColor: color }, pressed && styles.pressedDim, focusVisible && { boxShadow: FOCUS_RING }]}
|
|
266
280
|
onPress={onPress}
|
|
267
281
|
onLongPress={onLongPress}
|
|
282
|
+
{...focusProps}
|
|
268
283
|
>
|
|
269
284
|
<Icon name={icon} size={iconSize} color={colors.white} />
|
|
270
285
|
{overlay}
|
|
@@ -276,21 +291,26 @@ export function MediaCard(props: MediaCardProps) {
|
|
|
276
291
|
const backdrop = iconSize + 20;
|
|
277
292
|
|
|
278
293
|
return (
|
|
279
|
-
<Pressable style={[styles.documentCard, sizeStyle]} onPress={onPress} onLongPress={onLongPress}>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
{
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
+
<Pressable style={[styles.documentCard, sizeStyle, focusVisible && { boxShadow: FOCUS_RING }]} onPress={onPress} onLongPress={onLongPress} {...focusProps}>
|
|
295
|
+
{(state) => (
|
|
296
|
+
<>
|
|
297
|
+
<View style={[styles.mediaCardBody, { backgroundColor: color }]}>
|
|
298
|
+
<View style={[styles.mediaIconBackdrop, { width: backdrop, height: backdrop, borderRadius: backdrop / 2 }]}>
|
|
299
|
+
<Icon name={icon} size={iconSize} color={colors.white} />
|
|
300
|
+
</View>
|
|
301
|
+
</View>
|
|
302
|
+
<View style={styles.documentCardFooter}>
|
|
303
|
+
<Text size="xs" numberOfLines={1} color="zinc-500" userSelect="none">
|
|
304
|
+
{filename}
|
|
305
|
+
</Text>
|
|
306
|
+
<View style={[styles.typeLabel, { backgroundColor: color }]}>
|
|
307
|
+
<RNText style={styles.typeLabelText}>{label}</RNText>
|
|
308
|
+
</View>
|
|
309
|
+
</View>
|
|
310
|
+
{overlay}
|
|
311
|
+
<TileScrim hovered={(state as { hovered?: boolean }).hovered} pressed={state.pressed} />
|
|
312
|
+
</>
|
|
313
|
+
)}
|
|
294
314
|
</Pressable>
|
|
295
315
|
);
|
|
296
316
|
}
|
|
@@ -311,6 +331,7 @@ interface ImageThumbnailProps {
|
|
|
311
331
|
function ImageThumbnail(props: ImageThumbnailProps) {
|
|
312
332
|
const { file, size, onPress, onLongPress } = props;
|
|
313
333
|
const [useFallback, setUseFallback] = useState(false);
|
|
334
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
314
335
|
|
|
315
336
|
// Use thumbnail if available, fallback to full URL
|
|
316
337
|
const url = !useFallback && file.thumbnailUrl ? file.thumbnailUrl : file.url;
|
|
@@ -321,22 +342,31 @@ function ImageThumbnail(props: ImageThumbnailProps) {
|
|
|
321
342
|
: { width: "100%" as const, height: "100%" as const };
|
|
322
343
|
|
|
323
344
|
return (
|
|
324
|
-
<Pressable
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
345
|
+
<Pressable onPress={onPress} onLongPress={onLongPress} {...focusProps} style={[styles.imageThumbnail, sizeStyle, focusVisible && { boxShadow: FOCUS_RING }]}>
|
|
346
|
+
{(state) => (
|
|
347
|
+
<>
|
|
348
|
+
<Image
|
|
349
|
+
source={{ uri: url }}
|
|
350
|
+
style={styles.image}
|
|
351
|
+
resizeMode="cover"
|
|
352
|
+
onError={() => setUseFallback(true)}
|
|
353
|
+
/>
|
|
354
|
+
<TileScrim hovered={(state as { hovered?: boolean }).hovered} pressed={state.pressed} />
|
|
355
|
+
</>
|
|
356
|
+
)}
|
|
335
357
|
</Pressable>
|
|
336
358
|
);
|
|
337
359
|
}
|
|
338
360
|
|
|
339
|
-
|
|
361
|
+
/** Hover/press feedback for image and media tiles: a subtle darkening scrim
|
|
362
|
+
* ON TOP of the image/colored body (a bg wash would sit under it). Renders
|
|
363
|
+
* nothing at rest, so it never tints the tile until pointed at. */
|
|
364
|
+
function TileScrim({ hovered, pressed }: { hovered?: boolean; pressed: boolean }) {
|
|
365
|
+
if (!hovered && !pressed) return null;
|
|
366
|
+
return <View style={[styles.tileScrim, pressed && styles.tileScrimPressed]} pointerEvents="none" />;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function RemoveButton({ onPress, label = "Remove" }: { onPress: () => void; label?: string }) {
|
|
340
370
|
const handlePress = useCallback(
|
|
341
371
|
(e: GestureResponderEvent) => {
|
|
342
372
|
e.stopPropagation();
|
|
@@ -345,9 +375,9 @@ export function RemoveButton({ onPress }: { onPress: () => void }) {
|
|
|
345
375
|
[onPress],
|
|
346
376
|
);
|
|
347
377
|
return (
|
|
348
|
-
<
|
|
349
|
-
<
|
|
350
|
-
</
|
|
378
|
+
<View style={styles.removeButtonAnchor}>
|
|
379
|
+
<IconButton icon="x" size="sm" elevated accessibilityLabel={label} onPress={handlePress} />
|
|
380
|
+
</View>
|
|
351
381
|
);
|
|
352
382
|
}
|
|
353
383
|
|
|
@@ -370,16 +400,53 @@ function SelectionOverlay({ selected }: { selected: boolean }) {
|
|
|
370
400
|
|
|
371
401
|
const styles = StyleSheet.create({
|
|
372
402
|
documentCard: {
|
|
373
|
-
borderRadius:
|
|
403
|
+
borderRadius: 10,
|
|
374
404
|
backgroundColor: colors.white,
|
|
375
405
|
borderWidth: 1,
|
|
376
406
|
borderColor: colors.zinc["200"],
|
|
377
407
|
overflow: "hidden",
|
|
378
408
|
},
|
|
379
|
-
|
|
380
|
-
|
|
409
|
+
documentTile: {
|
|
410
|
+
borderRadius: 10,
|
|
411
|
+
backgroundColor: colors.white,
|
|
412
|
+
borderWidth: 1,
|
|
413
|
+
borderColor: colors.zinc["200"],
|
|
414
|
+
overflow: "hidden",
|
|
415
|
+
alignItems: "center",
|
|
381
416
|
justifyContent: "center",
|
|
382
|
-
|
|
417
|
+
gap: 8,
|
|
418
|
+
paddingHorizontal: 8,
|
|
419
|
+
paddingVertical: 10,
|
|
420
|
+
...({ transitionDuration: "0.1s", transitionProperty: "background-color" } as object),
|
|
421
|
+
},
|
|
422
|
+
documentTileName: {
|
|
423
|
+
alignSelf: "stretch",
|
|
424
|
+
textAlign: "center",
|
|
425
|
+
},
|
|
426
|
+
// Hover/press feedback: doc tiles darken their white bg (wash); image/media
|
|
427
|
+
// tiles get a `TileScrim` ON TOP (a bg wash would sit under the image/body).
|
|
428
|
+
tileHovered: {
|
|
429
|
+
backgroundColor: colors.zinc["50"],
|
|
430
|
+
},
|
|
431
|
+
tilePressed: {
|
|
432
|
+
backgroundColor: colors.zinc["100"],
|
|
433
|
+
},
|
|
434
|
+
tileScrim: {
|
|
435
|
+
position: "absolute",
|
|
436
|
+
top: 0,
|
|
437
|
+
left: 0,
|
|
438
|
+
right: 0,
|
|
439
|
+
bottom: 0,
|
|
440
|
+
borderRadius: 10,
|
|
441
|
+
backgroundColor: "rgba(24, 24, 27, 0.06)",
|
|
442
|
+
...({ transitionDuration: "0.1s", transitionProperty: "background-color" } as object),
|
|
443
|
+
},
|
|
444
|
+
tileScrimPressed: {
|
|
445
|
+
backgroundColor: "rgba(24, 24, 27, 0.12)",
|
|
446
|
+
},
|
|
447
|
+
// Compact media badges (≤32px) are too small for a scrim — a quick opacity dim reads.
|
|
448
|
+
pressedDim: {
|
|
449
|
+
opacity: 0.85,
|
|
383
450
|
},
|
|
384
451
|
mediaCardBody: {
|
|
385
452
|
flex: 1,
|
|
@@ -387,7 +454,7 @@ const styles = StyleSheet.create({
|
|
|
387
454
|
alignItems: "center",
|
|
388
455
|
},
|
|
389
456
|
mediaCardCompact: {
|
|
390
|
-
borderRadius:
|
|
457
|
+
borderRadius: 7,
|
|
391
458
|
alignItems: "center",
|
|
392
459
|
justifyContent: "center",
|
|
393
460
|
overflow: "hidden",
|
|
@@ -397,14 +464,6 @@ const styles = StyleSheet.create({
|
|
|
397
464
|
justifyContent: "center",
|
|
398
465
|
backgroundColor: "rgba(255, 255, 255, 0.22)",
|
|
399
466
|
},
|
|
400
|
-
placeholderLines: {
|
|
401
|
-
gap: 4,
|
|
402
|
-
},
|
|
403
|
-
placeholderLine: {
|
|
404
|
-
height: 2,
|
|
405
|
-
borderRadius: 1,
|
|
406
|
-
backgroundColor: colors.zinc["200"],
|
|
407
|
-
},
|
|
408
467
|
documentCardFooter: {
|
|
409
468
|
gap: 2,
|
|
410
469
|
paddingHorizontal: 8,
|
|
@@ -427,25 +486,17 @@ const styles = StyleSheet.create({
|
|
|
427
486
|
letterSpacing: 0.3,
|
|
428
487
|
},
|
|
429
488
|
imageThumbnail: {
|
|
430
|
-
borderRadius:
|
|
489
|
+
borderRadius: 10,
|
|
431
490
|
overflow: "hidden",
|
|
432
491
|
},
|
|
433
492
|
image: {
|
|
434
493
|
width: "100%",
|
|
435
494
|
height: "100%",
|
|
436
495
|
},
|
|
437
|
-
|
|
496
|
+
removeButtonAnchor: {
|
|
438
497
|
position: "absolute",
|
|
439
|
-
top: -
|
|
440
|
-
right: -
|
|
441
|
-
width: 22,
|
|
442
|
-
height: 22,
|
|
443
|
-
alignItems: "center",
|
|
444
|
-
justifyContent: "center",
|
|
445
|
-
borderRadius: 999,
|
|
446
|
-
borderWidth: 1,
|
|
447
|
-
borderColor: colors.border,
|
|
448
|
-
backgroundColor: colors.zinc["50"],
|
|
498
|
+
top: -8,
|
|
499
|
+
right: -8,
|
|
449
500
|
zIndex: 1,
|
|
450
501
|
},
|
|
451
502
|
selectionOverlay: {
|
|
@@ -454,7 +505,7 @@ const styles = StyleSheet.create({
|
|
|
454
505
|
left: 0,
|
|
455
506
|
right: 0,
|
|
456
507
|
bottom: 0,
|
|
457
|
-
borderRadius:
|
|
508
|
+
borderRadius: 10,
|
|
458
509
|
zIndex: 1,
|
|
459
510
|
},
|
|
460
511
|
selectionOverlaySelected: {
|