@lotics/ui 5.7.0 → 5.9.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.
Files changed (80) hide show
  1. package/AGENTS.md +102 -42
  2. package/examples/tpl_approvals.tsx +10 -10
  3. package/examples/tpl_convert.tsx +11 -23
  4. package/examples/tpl_crm_desk.tsx +3 -3
  5. package/examples/tpl_dieline.tsx +7 -6
  6. package/examples/tpl_directory.tsx +3 -3
  7. package/examples/tpl_dossier.tsx +7 -7
  8. package/examples/tpl_draft.tsx +4 -4
  9. package/examples/tpl_extract.tsx +7 -7
  10. package/examples/tpl_inventory.tsx +3 -3
  11. package/examples/tpl_lookup.tsx +4 -3
  12. package/examples/tpl_match.tsx +1 -1
  13. package/examples/tpl_order.tsx +1 -1
  14. package/examples/tpl_ratedesk.tsx +12 -12
  15. package/examples/tpl_record.tsx +1 -1
  16. package/examples/tpl_report.tsx +3 -3
  17. package/examples/tpl_stock.tsx +3 -3
  18. package/examples/tpl_triage.tsx +1 -1
  19. package/package.json +12 -4
  20. package/src/agent_progress.tsx +14 -12
  21. package/src/agent_run.tsx +253 -66
  22. package/src/back_button.tsx +3 -2
  23. package/src/badge.tsx +2 -1
  24. package/src/button.tsx +11 -32
  25. package/src/button_colors.ts +23 -0
  26. package/src/calendar/calendar_view.tsx +2 -2
  27. package/src/card_select_item.tsx +2 -1
  28. package/src/change_review.tsx +5 -6
  29. package/src/{pill_button.tsx → chip.tsx} +7 -7
  30. package/src/chip_group.tsx +2 -2
  31. package/src/column_filter.tsx +4 -4
  32. package/src/combobox.tsx +2 -9
  33. package/src/composer.tsx +4 -3
  34. package/src/control_surface.ts +15 -15
  35. package/src/date_field.tsx +2 -1
  36. package/src/date_filter.tsx +2 -1
  37. package/src/date_range_filter_field.tsx +2 -1
  38. package/src/dialog.tsx +2 -2
  39. package/src/discrepancy.tsx +1 -1
  40. package/src/drawer.tsx +1 -2
  41. package/src/file_gallery_modal.tsx +220 -133
  42. package/src/file_grid.tsx +184 -0
  43. package/src/file_preview.web.tsx +329 -6
  44. package/src/file_preview_types.ts +40 -0
  45. package/src/file_row.tsx +114 -0
  46. package/src/file_rows.tsx +105 -0
  47. package/src/file_thumbnail.tsx +121 -79
  48. package/src/file_thumbnail_grid.tsx +130 -55
  49. package/src/{filter_pill.tsx → filter_chip.tsx} +8 -8
  50. package/src/finding.tsx +1 -1
  51. package/src/gantt/gantt_view.tsx +2 -2
  52. package/src/icon_button.tsx +49 -14
  53. package/src/image_gallery.tsx +0 -4
  54. package/src/inline_edit.tsx +2 -2
  55. package/src/link_button.tsx +2 -1
  56. package/src/menu_button.tsx +2 -1
  57. package/src/menu_list_item.tsx +2 -1
  58. package/src/number_input.tsx +2 -1
  59. package/src/pdfjs_worker.d.ts +5 -0
  60. package/src/picker.tsx +3 -3
  61. package/src/popover_header.tsx +2 -2
  62. package/src/radio_picker.tsx +2 -1
  63. package/src/range_slider.tsx +1 -1
  64. package/src/review_card.tsx +2 -2
  65. package/src/scan_field.tsx +2 -1
  66. package/src/scored_option.tsx +0 -1
  67. package/src/search_input.tsx +3 -2
  68. package/src/segmented_control.tsx +2 -1
  69. package/src/status_badge.tsx +2 -1
  70. package/src/stepper.tsx +3 -2
  71. package/src/switch_button.tsx +2 -1
  72. package/src/switcher.tsx +3 -2
  73. package/src/tabs.tsx +2 -1
  74. package/src/tag_input.tsx +2 -1
  75. package/src/text_input_field.tsx +2 -1
  76. package/src/time_field.tsx +2 -1
  77. package/src/time_picker.tsx +2 -1
  78. package/src/triage_row.tsx +3 -3
  79. package/src/uploading_thumbnail.tsx +212 -0
  80. package/src/hover_action.tsx +0 -54
@@ -0,0 +1,212 @@
1
+ // UploadingThumbnail — the in-flight upload tile (loading / paused / retrying /
2
+ // error states). Pure: reuses the file display primitives (DocumentCard,
3
+ // MediaCard, RemoveButton) and is i18n-free (status text comes in via `labels`).
4
+ // Pairs with FileGrid, which interleaves these with completed FileThumbnails.
5
+
6
+ import { Icon } from "./icon";
7
+ import { Text } from "./text";
8
+ import { IconButton } from "./icon_button";
9
+ import { colors } from "./colors";
10
+ import { DocumentCard, MediaCard, RemoveButton, getMediaIcon } from "./file_thumbnail";
11
+ import { ActivityIndicator, Image, StyleSheet, View } from "react-native";
12
+ import { isImageMimeType } from "./mime";
13
+
14
+ /** In-flight upload states (everything except a completed file). */
15
+ export type UploadStatus = "preparing" | "queued" | "uploading" | "paused_offline" | "retrying" | "error";
16
+
17
+ /** Status text for the non-happy-path states. i18n-free defaults (English); the
18
+ * Lotics host injects translations. The common `uploading` state stays text-free
19
+ * (a spinner is self-evident) — only states that need attention get a word. */
20
+ export interface UploadStatusLabels {
21
+ /** Upload paused because the device is offline. Default "Paused". */
22
+ paused?: string;
23
+ /** Upload is being retried. Default "Retrying". */
24
+ retrying?: string;
25
+ /** Upload failed but can be retried. Default "Upload failed". */
26
+ failed?: string;
27
+ /** Upload failed and retry is futile (dead file ref / empty bytes — re-pick). Default "Can't upload". */
28
+ unavailable?: string;
29
+ /** Accessible label on the retry control. Default "Retry upload". */
30
+ retry?: string;
31
+ }
32
+
33
+ interface UploadingThumbnailProps {
34
+ filename: string;
35
+ mimeType: string;
36
+ previewUrl?: string;
37
+ status: UploadStatus;
38
+ /** True when retry would hit the same failure on the same input: the picker
39
+ * File reference is dead (Android SAF eviction) or the file has zero bytes
40
+ * (empty clipboard paste). The user must re-pick or re-copy the file. */
41
+ staleFile?: boolean;
42
+ /** Explicit pixel size. When omitted, fills container. */
43
+ size?: number;
44
+ onRemove?: () => void;
45
+ onRetry?: () => void;
46
+ labels?: UploadStatusLabels;
47
+ }
48
+
49
+ /** Below this tile size the status word can't fit — show the icon/spinner alone. */
50
+ const LABEL_MIN_SIZE = 72;
51
+
52
+ /**
53
+ * Displays a file during upload with loading/error states.
54
+ */
55
+ export function UploadingThumbnail(props: UploadingThumbnailProps) {
56
+ const { filename, mimeType, previewUrl, status, staleFile, size, onRemove, onRetry, labels } = props;
57
+ const compact = size !== undefined && size < LABEL_MIN_SIZE;
58
+
59
+ const rootStyle =
60
+ size !== undefined ? { width: size, height: size } : { width: "100%" as const, aspectRatio: 1 };
61
+
62
+ const overlay = (
63
+ <UploadOverlay
64
+ status={status}
65
+ staleFile={staleFile === true}
66
+ compact={compact}
67
+ labels={labels}
68
+ onRetry={onRetry}
69
+ />
70
+ );
71
+
72
+ if (isImageMimeType(mimeType) && previewUrl) {
73
+ return (
74
+ <View style={[styles.uploadingContainer, rootStyle]}>
75
+ <Image source={{ uri: previewUrl }} style={styles.uploadingImage} resizeMode="cover" />
76
+ {overlay}
77
+ {onRemove && <RemoveButton onPress={onRemove} />}
78
+ </View>
79
+ );
80
+ }
81
+
82
+ const mediaIcon = getMediaIcon(mimeType);
83
+
84
+ return (
85
+ <View style={rootStyle}>
86
+ {mediaIcon !== undefined ? (
87
+ <MediaCard mimeType={mimeType} filename={filename} icon={mediaIcon} overlay={overlay} />
88
+ ) : (
89
+ <DocumentCard mimeType={mimeType} filename={filename} overlay={overlay} />
90
+ )}
91
+ {onRemove && <RemoveButton onPress={onRemove} />}
92
+ </View>
93
+ );
94
+ }
95
+
96
+ function UploadOverlay(props: {
97
+ status: UploadStatus;
98
+ staleFile: boolean;
99
+ compact: boolean;
100
+ labels?: UploadStatusLabels;
101
+ onRetry?: () => void;
102
+ }) {
103
+ const { status, staleFile, compact, labels, onRetry } = props;
104
+
105
+ if (status === "preparing" || status === "queued" || status === "uploading") {
106
+ return (
107
+ <View style={styles.overlay} pointerEvents="none">
108
+ <ActivityIndicator size="small" color={colors.white} />
109
+ </View>
110
+ );
111
+ }
112
+
113
+ if (status === "paused_offline") {
114
+ return (
115
+ <OverlayStack compact={compact} label={labels?.paused ?? "Paused"}>
116
+ <Icon name="pause" size={20} color={colors.white} />
117
+ </OverlayStack>
118
+ );
119
+ }
120
+
121
+ if (status === "retrying") {
122
+ return (
123
+ <OverlayStack compact={compact} label={labels?.retrying ?? "Retrying"}>
124
+ <ActivityIndicator size="small" color={colors.white} />
125
+ </OverlayStack>
126
+ );
127
+ }
128
+
129
+ // error, retry is futile — the user must re-pick the file
130
+ if (staleFile) {
131
+ return (
132
+ <OverlayStack tone="error" compact={compact} label={labels?.unavailable ?? "Can't upload"}>
133
+ <Icon name="circle-alert" size={20} color={colors.white} />
134
+ </OverlayStack>
135
+ );
136
+ }
137
+
138
+ // error, retryable
139
+ return (
140
+ <View style={[styles.overlay, styles.overlayError]} pointerEvents="box-none">
141
+ <View style={styles.stack}>
142
+ {onRetry && (
143
+ <IconButton
144
+ icon="rotate-cw"
145
+ size={compact ? "sm" : "md"}
146
+ elevated
147
+ accessibilityLabel={labels?.retry ?? "Retry upload"}
148
+ onPress={(e) => {
149
+ e.stopPropagation();
150
+ onRetry();
151
+ }}
152
+ />
153
+ )}
154
+ {!compact && <OverlayLabel>{labels?.failed ?? "Upload failed"}</OverlayLabel>}
155
+ </View>
156
+ </View>
157
+ );
158
+ }
159
+
160
+ function OverlayStack(props: { tone?: "error"; compact: boolean; label: string; children: React.ReactNode }) {
161
+ return (
162
+ <View style={[styles.overlay, props.tone === "error" && styles.overlayError]} pointerEvents="none">
163
+ <View style={styles.stack}>
164
+ {props.children}
165
+ {!props.compact && <OverlayLabel>{props.label}</OverlayLabel>}
166
+ </View>
167
+ </View>
168
+ );
169
+ }
170
+
171
+ function OverlayLabel({ children }: { children: string }) {
172
+ return (
173
+ <Text size="xs" weight="medium" color="inverted" numberOfLines={2} style={styles.overlayLabel} userSelect="none">
174
+ {children}
175
+ </Text>
176
+ );
177
+ }
178
+
179
+ const styles = StyleSheet.create({
180
+ uploadingContainer: {
181
+ position: "relative",
182
+ },
183
+ uploadingImage: {
184
+ width: "100%",
185
+ height: "100%",
186
+ borderRadius: 10,
187
+ },
188
+ overlay: {
189
+ position: "absolute",
190
+ top: 0,
191
+ left: 0,
192
+ right: 0,
193
+ bottom: 0,
194
+ borderRadius: 10,
195
+ backgroundColor: "rgba(24, 24, 27, 0.55)",
196
+ justifyContent: "center",
197
+ alignItems: "center",
198
+ padding: 8,
199
+ },
200
+ // Error states get a red wash — the at-a-glance "this didn't work" signal —
201
+ // dark enough that the white label + elevated retry button stay legible.
202
+ overlayError: {
203
+ backgroundColor: "rgba(153, 27, 27, 0.66)",
204
+ },
205
+ stack: {
206
+ alignItems: "center",
207
+ gap: 6,
208
+ },
209
+ overlayLabel: {
210
+ textAlign: "center",
211
+ },
212
+ });
@@ -1,54 +0,0 @@
1
- import { View, ViewStyle } from "react-native";
2
- import { ReactNode, useState, useCallback } from "react";
3
- import { useScreenSize } from "./use_screen_size";
4
-
5
- interface HoverActionProps {
6
- children: ReactNode;
7
- /** Content to show on hover (e.g., IconButton with Popover) */
8
- action: ReactNode;
9
- /** Additional condition for showing the action (combined with hover state) */
10
- visible?: boolean;
11
- hover?: boolean;
12
-
13
- /** Style for the outer wrapper */
14
- style?: ViewStyle;
15
- }
16
-
17
- export function HoverAction(props: HoverActionProps) {
18
- const { children, action, visible = true, hover = true, style } = props;
19
- const [hovered, setHovered] = useState(false);
20
- const screenSize = useScreenSize();
21
-
22
- // Disable hover on small screens (mobile) to prevent tap issues
23
- const enableHover = hover && !screenSize.small;
24
-
25
- const handleMouseEnter = useCallback(() => setHovered(true), []);
26
- const handleMouseLeave = useCallback(() => setHovered(false), []);
27
-
28
- return (
29
- <View
30
- style={[{ position: "relative" }, style]}
31
- {...(enableHover
32
- ? {
33
- onMouseEnter: handleMouseEnter,
34
- onMouseLeave: handleMouseLeave,
35
- }
36
- : {})}
37
- >
38
- {children}
39
- {(hovered || !enableHover) && visible && (
40
- <View
41
- style={{
42
- position: "absolute",
43
- zIndex: 1,
44
- top: "50%",
45
- transform: [{ translateY: "-50%" }],
46
- right: 8,
47
- }}
48
- >
49
- {action}
50
- </View>
51
- )}
52
- </View>
53
- );
54
- }