@stevecastaneda/paseo-progress 1.4.0 → 1.5.1
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/README.md +18 -1
- package/client/attachments.tsx +9 -6
- package/client/deliverables.tsx +6 -4
- package/client/dialog-stack.tsx +3 -0
- package/client/history.tsx +23 -18
- package/client/i18n.ts +86 -0
- package/client/language.tsx +23 -0
- package/client/launcher.tsx +22 -17
- package/client/locales/en.ts +207 -0
- package/client/locales/zh-CN.ts +207 -0
- package/client/messages.ts +189 -0
- package/client/panel.tsx +48 -38
- package/client/pill-popover.tsx +8 -5
- package/client/pill-state.ts +4 -3
- package/client/pills.tsx +25 -11
- package/client/preview.tsx +18 -13
- package/client/questions.tsx +26 -18
- package/client/settings.tsx +46 -28
- package/client/show-more.tsx +4 -2
- package/client/spinner.tsx +7 -3
- package/client/status-badge.tsx +3 -9
- package/client/ticket-dialog.tsx +35 -29
- package/client/video.tsx +3 -1
- package/client/whats-new.tsx +11 -6
- package/client/when.tsx +5 -3
- package/client/write-up.tsx +5 -3
- package/images/5-settings.png +0 -0
- package/index.client.tsx +3 -2
- package/index.server.ts +2 -1
- package/package.json +1 -1
- package/shared/settings.ts +10 -0
- package/shared/ticket-story.ts +12 -10
package/client/panel.tsx
CHANGED
|
@@ -14,10 +14,10 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
14
14
|
import { ActivityIndicator, Animated, Platform, ScrollView, Text, View } from "react-native";
|
|
15
15
|
import { finishedToFold, isFinished, shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
|
|
16
16
|
import type { TicketStatus } from "../shared/events";
|
|
17
|
-
import {
|
|
17
|
+
import { minutesSince } from "../shared/format";
|
|
18
|
+
import { clientHeadline, formatClientMinutes, formatClientWorkDone } from "./i18n";
|
|
18
19
|
import { markPanelOpened } from "../shared/rpc";
|
|
19
20
|
import { useDashboard } from "./dashboard-query";
|
|
20
|
-
import { STATUS_LABEL } from "./status-badge";
|
|
21
21
|
import { TicketDialogs } from "./ticket-dialog";
|
|
22
22
|
import { PressableRow } from "./row";
|
|
23
23
|
import { TabStrip } from "./tabs";
|
|
@@ -25,6 +25,7 @@ import { ShowMoreRow, usePaged } from "./show-more";
|
|
|
25
25
|
import { notePanelOpened } from "./panel-opened";
|
|
26
26
|
import { NARROW_MAX, NarrowProvider, useNarrow } from "./narrow";
|
|
27
27
|
import { errorText } from "./errors";
|
|
28
|
+
import { useProgressLanguage } from "./language";
|
|
28
29
|
|
|
29
30
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
30
31
|
|
|
@@ -38,6 +39,7 @@ export function ProgressPanel(props: PanelProps) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function WorkspaceProgress({ theme, workspaceId, host, layout, navigation, openSettings }: PanelProps) {
|
|
42
|
+
const { messages: m } = useProgressLanguage();
|
|
41
43
|
const colors = theme.colors;
|
|
42
44
|
const directory = useWorkspace(workspaceId, (workspace) => workspace.directory);
|
|
43
45
|
// Paseo marks the workspace running while any of its agents is. Since 0.11 a Claude
|
|
@@ -53,10 +55,10 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation, openS
|
|
|
53
55
|
<NarrowProvider value={narrow}>
|
|
54
56
|
<ScrollView onLayout={(event) => setNarrow(event.nativeEvent.layout.width <= NARROW_MAX)}
|
|
55
57
|
style={{ flex: 1, backgroundColor: colors.surface0 }} contentContainerStyle={{ paddingBottom: 16 }}>
|
|
56
|
-
{query.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel=
|
|
58
|
+
{query.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingProgress} style={{ padding: 12 }} /> : null}
|
|
57
59
|
{query.error ? (
|
|
58
60
|
<Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18, padding: 12 }}>
|
|
59
|
-
|
|
61
|
+
{m.readProgressError(errorText(query.error, m.unknownError))}
|
|
60
62
|
</Text>
|
|
61
63
|
) : null}
|
|
62
64
|
<WhatsNewBanner colors={colors} host={host} />
|
|
@@ -90,8 +92,9 @@ function useMarkPanelOpened(workspaceId: string, directory: string | null, neede
|
|
|
90
92
|
|
|
91
93
|
// Optical: the 24px button centers on a 20 to 21px title line.
|
|
92
94
|
function SettingsButton({ colors, onPress }: { colors: Colors; onPress(): void }) {
|
|
95
|
+
const { messages: m } = useProgressLanguage();
|
|
93
96
|
return (
|
|
94
|
-
<PressScale accessibilityRole="button" accessibilityLabel=
|
|
97
|
+
<PressScale accessibilityRole="button" accessibilityLabel={m.openSettings} hitSlop={6} outerStyle={{ marginVertical: -2 }} onPress={onPress}
|
|
95
98
|
style={({ pressed, hovered }) => ({ padding: 4, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface1 : "transparent" })}>
|
|
96
99
|
<Icon name="Settings" size={16} color={colors.foregroundMuted} />
|
|
97
100
|
</PressScale>
|
|
@@ -99,18 +102,19 @@ function SettingsButton({ colors, onPress }: { colors: Colors; onPress(): void }
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
function EmptyState({ colors, file, openSettings }: { colors: Colors; file: string; openSettings(): void }) {
|
|
105
|
+
const { messages: m } = useProgressLanguage();
|
|
102
106
|
return (
|
|
103
107
|
<View style={{ padding: 12, paddingTop: 20, gap: 10 }}>
|
|
104
108
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
105
109
|
<Icon name="ListChecks" size={18} color={colors.foregroundMuted} />
|
|
106
110
|
<Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 14, lineHeight: 20, fontWeight: "500" }}>
|
|
107
|
-
|
|
111
|
+
{m.noProgress}
|
|
108
112
|
</Text>
|
|
109
113
|
<View style={{ flex: 1 }} />
|
|
110
114
|
<SettingsButton colors={colors} onPress={openSettings} />
|
|
111
115
|
</View>
|
|
112
116
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
113
|
-
|
|
117
|
+
{m.emptyIntro[0]}<Text style={{ color: colors.foreground }}>paseo-progress</Text>{m.emptyIntro[1]}
|
|
114
118
|
</Text>
|
|
115
119
|
<View style={{ backgroundColor: colors.surface1, borderRadius: 6, padding: 10 }}>
|
|
116
120
|
<Text selectable style={{ color: colors.foreground, fontFamily: mono(), fontSize: 12, lineHeight: 18 }}>
|
|
@@ -119,7 +123,7 @@ function EmptyState({ colors, file, openSettings }: { colors: Colors; file: stri
|
|
|
119
123
|
</View>
|
|
120
124
|
{/* So people know nothing lands in the repo. */}
|
|
121
125
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
122
|
-
|
|
126
|
+
{m.savedOutside[0]}<Text selectable style={{ color: colors.foreground }}>{file}</Text>{m.savedOutside[1]}
|
|
123
127
|
</Text>
|
|
124
128
|
</View>
|
|
125
129
|
);
|
|
@@ -159,6 +163,8 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
159
163
|
file: string;
|
|
160
164
|
navigation: PluginWorkspacePanelProps["navigation"];
|
|
161
165
|
}) {
|
|
166
|
+
const { messages: m, locale } = useProgressLanguage();
|
|
167
|
+
const headline = clientHeadline(locale, dashboard);
|
|
162
168
|
const now = useNow(15_000);
|
|
163
169
|
// Quiet isn't stale while an agent in the workspace is still running.
|
|
164
170
|
const stale = dashboard.stale && !agentRunning;
|
|
@@ -181,11 +187,11 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
181
187
|
{dashboard.run?.subtitle ? (
|
|
182
188
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{dashboard.run.subtitle}</Text>
|
|
183
189
|
) : null}
|
|
184
|
-
{
|
|
190
|
+
{headline.length ? (
|
|
185
191
|
<Text accessibilityLiveRegion="polite" style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
186
|
-
{
|
|
192
|
+
{headline.map((part, index) => (
|
|
187
193
|
<Text key={part.text} style={part.tone === "danger" ? { color: colors.statusDanger } : undefined}>
|
|
188
|
-
{index ?
|
|
194
|
+
{index ? m.listSeparator : ""}{part.text}
|
|
189
195
|
</Text>
|
|
190
196
|
))}
|
|
191
197
|
</Text>
|
|
@@ -201,7 +207,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
201
207
|
{dashboard.run?.finished ? (
|
|
202
208
|
<>
|
|
203
209
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
204
|
-
<Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>
|
|
210
|
+
<Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{m.finished}</Text> <When colors={colors} iso={dashboard.run.finished.at} now={now} />
|
|
205
211
|
</Text>
|
|
206
212
|
<Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 18 }}>{dashboard.run.finished.outcome}</Text>
|
|
207
213
|
</>
|
|
@@ -214,7 +220,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
214
220
|
{dashboard.updatedAt && !dashboard.run?.finished ? (
|
|
215
221
|
<StatusLine icon={<Icon name="Clock" size={12} color={colors.foregroundMuted} />}>
|
|
216
222
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
217
|
-
|
|
223
|
+
{m.lastUpdated}<When colors={colors} iso={dashboard.updatedAt} now={now} />
|
|
218
224
|
</Text>
|
|
219
225
|
</StatusLine>
|
|
220
226
|
) : null}
|
|
@@ -225,7 +231,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
225
231
|
{/* Optical: centers the 14px icon on the first 18px line. */}
|
|
226
232
|
<View style={{ paddingTop: 2 }}><Icon name="TriangleAlert" size={14} color={colors.statusWarning} /></View>
|
|
227
233
|
<Text style={{ flex: 1, color: colors.statusWarning, fontSize: 12, lineHeight: 18 }}>
|
|
228
|
-
|
|
234
|
+
{m.staleWarning(unbroken(formatClientMinutes(locale, minutesSince(dashboard.updatedAt, now))))}
|
|
229
235
|
</Text>
|
|
230
236
|
</View>
|
|
231
237
|
) : null}</Presence>
|
|
@@ -239,10 +245,10 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
239
245
|
) : null}</Presence>
|
|
240
246
|
<Presence show={Boolean(dashboard.stuck.length)} order={3}>{dashboard.stuck.length ? <StuckSection colors={colors} items={dashboard.stuck} now={now} /> : null}</Presence>
|
|
241
247
|
<Presence show order={4}>
|
|
242
|
-
<Card colors={colors} icon="ListChecks" title={
|
|
248
|
+
<Card colors={colors} icon="ListChecks" title={m.itemHeading(dashboard.run?.itemLabel ?? "Ticket")}>
|
|
243
249
|
{dashboard.tickets.length === 0 ? (
|
|
244
250
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
|
|
245
|
-
|
|
251
|
+
{m.noItems(dashboard.run?.itemLabel ?? "Ticket")}
|
|
246
252
|
</Text>
|
|
247
253
|
) : (
|
|
248
254
|
<TicketList colors={colors} dashboard={dashboard} live={live} now={now} workspaceId={workspaceId} onOpen={setOpenTicket} />
|
|
@@ -255,11 +261,11 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
255
261
|
<Presence show={hasHistory} order={5}>
|
|
256
262
|
{hasHistory ? (
|
|
257
263
|
<HistoryCard colors={colors} workspaceId={workspaceId} tabs={[
|
|
258
|
-
{ id: "activity", label:
|
|
264
|
+
{ id: "activity", label: m.activity, icon: "Activity", count: dashboard.activity.length, empty: m.noActivity,
|
|
259
265
|
content: <ActivityList colors={colors} activity={dashboard.activity} now={now} /> },
|
|
260
|
-
{ id: "answered", label:
|
|
266
|
+
{ id: "answered", label: m.answered, icon: "CircleCheck", count: dashboard.questions.answered.length, empty: m.noAnswers,
|
|
261
267
|
content: <AnsweredQuestionsList colors={colors} questions={dashboard.questions.answered} now={now} context={attachmentContext} /> },
|
|
262
|
-
{ id: "deliverables", label:
|
|
268
|
+
{ id: "deliverables", label: m.deliverables, icon: "Package", count: dashboard.deliverables.length, empty: m.noDeliverables,
|
|
263
269
|
content: <DeliverablesList colors={colors} deliverables={dashboard.deliverables} now={now} workspaceId={workspaceId}
|
|
264
270
|
workspaceDirectory={workspaceDirectory} navigation={navigation} /> },
|
|
265
271
|
]} />
|
|
@@ -323,14 +329,14 @@ function ActivityList({ colors, activity, now }: { colors: Colors; activity: Act
|
|
|
323
329
|
|
|
324
330
|
// Small and non-blocking: the rest of the dashboard still renders.
|
|
325
331
|
function IssuesNotice({ colors, issues, file }: { colors: Colors; issues: FileIssue[]; file: string }) {
|
|
332
|
+
const { messages: m } = useProgressLanguage();
|
|
326
333
|
const shown = issues.slice(0, 5);
|
|
327
334
|
return (
|
|
328
335
|
<View style={{ flexDirection: "row", gap: 8, margin: 12, marginBottom: 0, padding: 8, borderRadius: 6, backgroundColor: colors.surface1 }}>
|
|
329
336
|
<View style={{ paddingTop: 1.5 }}><Icon name="FileWarning" size={14} color={colors.statusWarning} /></View>
|
|
330
337
|
<Text selectable style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
331
|
-
|
|
332
|
-
{shown.map((issue) =>
|
|
333
|
-
{issues.length > shown.length ? `, and ${issues.length - shown.length} more` : ""}.
|
|
338
|
+
{m.skippedLines(issues.length, file)}
|
|
339
|
+
{m.issueList(shown.map((issue) => m.issueLine(issue.line, issue.reason)), issues.length - shown.length)}
|
|
334
340
|
</Text>
|
|
335
341
|
</View>
|
|
336
342
|
);
|
|
@@ -346,15 +352,16 @@ function Card({ colors, icon, title, children }: { colors: Colors; icon: string;
|
|
|
346
352
|
}
|
|
347
353
|
|
|
348
354
|
function ProgressBar({ colors, progress, live }: { colors: Colors; progress: Dashboard["progress"]; live: boolean }) {
|
|
355
|
+
const { messages: m, locale } = useProgressLanguage();
|
|
349
356
|
const narrow = useNarrow();
|
|
350
357
|
return (
|
|
351
358
|
<View style={{ paddingHorizontal: 12, paddingTop: 12, paddingBottom: 12, gap: 8, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
352
359
|
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
|
|
353
360
|
<Text style={{ flex: 1, color: colors.foregroundMuted, fontSize: 13, lineHeight: 18 }}>
|
|
354
|
-
<Text style={{ color: colors.foreground, fontWeight: "600" }}>{progress.percent}%</Text> {narrow ?
|
|
361
|
+
<Text style={{ color: colors.foreground, fontWeight: "600" }}>{progress.percent}%</Text> {narrow ? m.workDoneShort : m.workDoneLong}
|
|
355
362
|
</Text>
|
|
356
363
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
357
|
-
{
|
|
364
|
+
{formatClientWorkDone(locale, progress.doneMin, progress.totalMin, progress.tookMin)}
|
|
358
365
|
</Text>
|
|
359
366
|
</View>
|
|
360
367
|
<View accessibilityRole="progressbar" accessibilityValue={{ min: 0, max: 100, now: progress.percent }}
|
|
@@ -399,10 +406,11 @@ function Shimmer() {
|
|
|
399
406
|
const STUCK_ICON: Record<StuckItem["kind"], string> = { blocked: "Ban", overdue: "Hourglass", manual: "Flag" };
|
|
400
407
|
|
|
401
408
|
function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem[]; now: number }) {
|
|
409
|
+
const { messages: m, locale } = useProgressLanguage();
|
|
402
410
|
const narrow = useNarrow();
|
|
403
411
|
return (
|
|
404
412
|
<View style={{ margin: 12, marginBottom: 0, borderWidth: 1, borderColor: colors.statusDanger, borderRadius: 6, overflow: "hidden" }}>
|
|
405
|
-
<SectionTitle colors={colors} icon="OctagonAlert" title=
|
|
413
|
+
<SectionTitle colors={colors} icon="OctagonAlert" title={m.stuck} color={colors.statusDanger} style={{ borderBottomWidth: 1, borderBottomColor: colors.border }} />
|
|
406
414
|
{items.map((item, index) => (
|
|
407
415
|
<View key={item.key} style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}>
|
|
408
416
|
{/* Optical: centers the 14px icon on the 18px title line. */}
|
|
@@ -410,9 +418,9 @@ function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem
|
|
|
410
418
|
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
411
419
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{item.title}</Text>
|
|
412
420
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
413
|
-
{item.kind === "overdue" ?
|
|
414
|
-
{item.kind === "blocked" ?
|
|
415
|
-
{item.kind === "manual" ? `${item.ticketId ? `${item.reason}.
|
|
421
|
+
{item.kind === "overdue" ? m.overdueUpdate(unbroken(formatClientMinutes(locale, item.estimateMin))) : null}
|
|
422
|
+
{item.kind === "blocked" ? m.blockedSince(item.note, narrow) : null}
|
|
423
|
+
{item.kind === "manual" ? `${item.ticketId ? `${item.reason}${m.sentenceBreak}` : ""}${m.flagged}` : null}
|
|
416
424
|
<When colors={colors} iso={item.since} now={now} />
|
|
417
425
|
</Text>
|
|
418
426
|
</View>
|
|
@@ -452,15 +460,16 @@ function TicketList({ colors, dashboard, live, now, workspaceId, onOpen }: { col
|
|
|
452
460
|
// Sits on the ticket rows' columns: status icon, label, estimate, then the
|
|
453
461
|
// chevron, which points the way the list will move, like "Show 10 older".
|
|
454
462
|
function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tickets: Ticket[]; shown: boolean; onPress(): void }) {
|
|
463
|
+
const { messages: m, locale } = useProgressLanguage();
|
|
455
464
|
const done = tickets.filter((ticket) => ticket.status === "done").length;
|
|
456
465
|
const skipped = tickets.length - done;
|
|
457
|
-
const label = [done ?
|
|
466
|
+
const label = [done ? m.doneCount(done) : "", skipped ? m.skippedCount(skipped) : ""].filter(Boolean).join(m.listSeparator);
|
|
458
467
|
const estimateMin = tickets.reduce((sum, ticket) => sum + (ticket.status === "done" ? ticket.estimateMin : 0), 0);
|
|
459
468
|
const chevron = shown ? "ChevronUp" : "ChevronDown";
|
|
460
469
|
const narrow = useNarrow();
|
|
461
470
|
return (
|
|
462
471
|
<PressableRow colors={colors} accessibilityRole="button" accessibilityState={{ expanded: shown }}
|
|
463
|
-
accessibilityLabel={
|
|
472
|
+
accessibilityLabel={m.toggleFinished(shown, label)} onPress={onPress}
|
|
464
473
|
style={{ flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
|
|
465
474
|
<View style={{ width: 16, paddingTop: 2, alignItems: "center" }}>
|
|
466
475
|
<Icon name={done ? "Check" : "CircleSlash"} size={14} color={done ? colors.statusSuccess : colors.foregroundMuted} />
|
|
@@ -469,7 +478,7 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
|
|
|
469
478
|
{/* Narrow ticket rows drop their chevron, so this total would sit off their estimate column. */}
|
|
470
479
|
{narrow ? null : <>
|
|
471
480
|
<View style={{ width: 12 }} />
|
|
472
|
-
<Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ?
|
|
481
|
+
<Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatClientMinutes(locale, estimateMin) : ""}</Text>
|
|
473
482
|
</>}
|
|
474
483
|
<View style={{ paddingTop: 2 }}>
|
|
475
484
|
<IconSwap swapKey={chevron} size={14}><Icon name={chevron} size={14} color={colors.foregroundMuted} /></IconSwap>
|
|
@@ -480,16 +489,17 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
|
|
|
480
489
|
|
|
481
490
|
// Press for the ticket's story. Hover and press only tint the row: it's a list, pressed often.
|
|
482
491
|
function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors: Colors; ticket: Ticket; live: boolean; now: number; onOpen(): void; deliverables: number }) {
|
|
492
|
+
const { messages: m, locale } = useProgressLanguage();
|
|
483
493
|
const muted = ticket.status === "skipped";
|
|
484
494
|
const working = ticket.status === "working";
|
|
485
495
|
// Narrow: tighter gaps and no chevron (the whole row still presses), so the title keeps its width.
|
|
486
496
|
const narrow = useNarrow();
|
|
487
497
|
return (
|
|
488
498
|
<PressableRow colors={colors} onSurface1={working} transparentAtRest={!working}
|
|
489
|
-
accessibilityRole="button" accessibilityLabel={
|
|
499
|
+
accessibilityRole="button" accessibilityLabel={m.details(ticket.id)} onPress={onOpen}
|
|
490
500
|
style={{ flexDirection: "row", alignItems: "flex-start", gap: narrow ? 6 : 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
|
|
491
501
|
{/* The icon is the only status mark on the row; the ticket dialog spells it out in a badge. */}
|
|
492
|
-
<View accessible accessibilityLabel={ticket.waitingFor ?
|
|
502
|
+
<View accessible accessibilityLabel={ticket.waitingFor ? m.waiting : m.status[ticket.status]} style={{ width: 16, paddingTop: 2, alignItems: "center", marginRight: narrow ? 2 : 0 }}>
|
|
493
503
|
<IconSwap swapKey={ticket.waitingFor || ticket.status === "not_started" ? "waiting" : ticket.status === "working" ? `working-${live}` : ticket.status} size={14}><StatusIcon colors={colors} status={ticket.status} live={live} waiting={Boolean(ticket.waitingFor)} /></IconSwap>
|
|
494
504
|
</View>
|
|
495
505
|
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
@@ -498,24 +508,24 @@ function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors
|
|
|
498
508
|
</Text>
|
|
499
509
|
{ticket.status === "working" && ticket.stage && ticket.stageSince ? (
|
|
500
510
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
501
|
-
{ticket.stage}
|
|
511
|
+
{m.stageStarted(ticket.stage)}<When colors={colors} iso={ticket.stageSince} now={now} />
|
|
502
512
|
</Text>
|
|
503
513
|
) : null}
|
|
504
514
|
{ticket.status === "working" && ticket.workingSince ? (
|
|
505
515
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
506
|
-
{narrow ?
|
|
516
|
+
{narrow ? m.startedWorking : m.workingSince}<When colors={colors} iso={ticket.workingSince} now={now} />
|
|
507
517
|
</Text>
|
|
508
518
|
) : null}
|
|
509
519
|
{ticket.waitingFor ? (
|
|
510
|
-
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
520
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{m.waitsFor(shortTitle(ticket.waitingFor.title))}</Text>
|
|
511
521
|
) : null}
|
|
512
522
|
{ticket.note ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{ticket.note}</Text> : null}
|
|
513
523
|
</View>
|
|
514
524
|
{/* Fixed slots so the Package icon and estimates line up down the list, whether or not a row has deliverables; they're listed in its dialog. The estimate slot fits "45m" up to "9h 45m"; longer ones grow rather than clip. */}
|
|
515
|
-
<View accessibilityLabel={deliverables ? (deliverables
|
|
525
|
+
<View accessibilityLabel={deliverables ? m.hasDeliverables(deliverables) : undefined} style={{ width: 12, height: 18, justifyContent: "center" }}>
|
|
516
526
|
{deliverables ? <Icon name="Package" size={12} color={colors.foregroundMuted} /> : null}
|
|
517
527
|
</View>
|
|
518
|
-
<Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{
|
|
528
|
+
<Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{formatClientMinutes(locale, ticket.estimateMin)}</Text>
|
|
519
529
|
{narrow ? null : <View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>}
|
|
520
530
|
</PressableRow>
|
|
521
531
|
);
|
package/client/pill-popover.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { galleryFiles } from "./preview";
|
|
|
12
12
|
import { QuestionDetail, QuestionState, useCopy } from "./questions";
|
|
13
13
|
import { errorText } from "./errors";
|
|
14
14
|
import { raised } from "./surfaces";
|
|
15
|
+
import { useProgressLanguage } from "./language";
|
|
15
16
|
|
|
16
17
|
type Colors = PluginButtonContentProps["theme"]["colors"];
|
|
17
18
|
|
|
@@ -22,6 +23,7 @@ const LIST_MAX_HEIGHT = 340;
|
|
|
22
23
|
// What the pill opens: the open questions with their Copy buttons, and anything
|
|
23
24
|
// stuck, so the user can answer without leaving the chat.
|
|
24
25
|
export function AttentionPopover({ theme, host, layout, workspaceId, close, openPanel }: PluginButtonContentProps & { openPanel(): void }) {
|
|
26
|
+
const { messages: m } = useProgressLanguage();
|
|
25
27
|
const colors = theme.colors;
|
|
26
28
|
// Paseo draws the desktop popover on surface1 and the phone sheet on surface0.
|
|
27
29
|
const onSurface1 = !layout.compact;
|
|
@@ -53,7 +55,7 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
|
|
|
53
55
|
</List>
|
|
54
56
|
) : (
|
|
55
57
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
56
|
-
{query.error ?
|
|
58
|
+
{query.error ? m.readProgressError(errorText(query.error, m.unknownError)) : m.loading}
|
|
57
59
|
</Text>
|
|
58
60
|
)}
|
|
59
61
|
<StackedDialog colors={colors} now={Date.now()} onClose={() => setPreviewing(null)} context={{ workspaceId, workspaceDirectory: root }}
|
|
@@ -65,7 +67,7 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
|
|
|
65
67
|
backgroundColor: onSurface1 ? (pressed ? colors.surface1 : colors.surface2) : pressed ? colors.surface2 : colors.surface1 })}>
|
|
66
68
|
{/* Beside the chat on desktop; on a phone it opens as its own tab, shown with the tab's icon. */}
|
|
67
69
|
<Icon name={touch ? "ListChecks" : "PanelRight"} size={touch ? 16 : 12} color={colors.foreground} />
|
|
68
|
-
<Text style={{ color: colors.foreground, fontSize: touch ? 15 : 12, lineHeight: touch ? 20 : 16, fontWeight: "600" }}>
|
|
70
|
+
<Text style={{ color: colors.foreground, fontSize: touch ? 15 : 12, lineHeight: touch ? 20 : 16, fontWeight: "600" }}>{m.openProgress}</Text>
|
|
69
71
|
</PressScale>
|
|
70
72
|
</View>
|
|
71
73
|
</View>
|
|
@@ -87,10 +89,11 @@ export function AttentionList({ colors, dashboard, onSurface1, onCopy, onOpenAtt
|
|
|
87
89
|
onCopy(question: Dashboard["questions"]["open"][number], letter: string): void;
|
|
88
90
|
onOpenAttachment(attachment: Attachment, group: Attachment[]): void;
|
|
89
91
|
}) {
|
|
92
|
+
const { messages: m } = useProgressLanguage();
|
|
90
93
|
const now = Date.now();
|
|
91
94
|
// The last question can be answered while the popover is open.
|
|
92
95
|
if (!dashboard.questions.open.length && !dashboard.stuck.length) {
|
|
93
|
-
return <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
96
|
+
return <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{m.nothingNeedsYou}</Text>;
|
|
94
97
|
}
|
|
95
98
|
return (
|
|
96
99
|
<>
|
|
@@ -110,13 +113,13 @@ export function AttentionList({ colors, dashboard, onSurface1, onCopy, onOpenAtt
|
|
|
110
113
|
<View style={{ gap: 4 }}>
|
|
111
114
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
112
115
|
<Icon name="OctagonAlert" size={12} color={colors.statusDanger} />
|
|
113
|
-
<Text style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, fontWeight: "600" }}>
|
|
116
|
+
<Text style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, fontWeight: "600" }}>{m.stuck}</Text>
|
|
114
117
|
</View>
|
|
115
118
|
{dashboard.stuck.map((item) => (
|
|
116
119
|
<Text key={item.key} style={{ color: colors.foreground, fontSize: 12, lineHeight: 17 }}>
|
|
117
120
|
{item.title}
|
|
118
121
|
<Text style={{ color: colors.foregroundMuted }}>
|
|
119
|
-
{item.kind === "overdue" ?
|
|
122
|
+
{item.kind === "overdue" ? m.overdueAside : item.kind === "blocked" ? m.blockedAside(item.note) : ""}
|
|
120
123
|
</Text>
|
|
121
124
|
</Text>
|
|
122
125
|
))}
|
package/client/pill-state.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { messagesFor, type ProgressMessages } from "./i18n";
|
|
1
2
|
import type { AttentionResult } from "../shared/rpc";
|
|
2
3
|
|
|
3
4
|
// What an agent's Progress pill shows, decided in one place so its label, its
|
|
@@ -13,15 +14,15 @@ export type Pill =
|
|
|
13
14
|
|
|
14
15
|
// What needs the user first, else the nudge, else (on a phone) the way in.
|
|
15
16
|
// Null when there's no pill.
|
|
16
|
-
export function pillFor(report: AttentionResult | undefined, where: { phone: boolean }): Pill | null {
|
|
17
|
+
export function pillFor(report: AttentionResult | undefined, where: { phone: boolean }, m: ProgressMessages = messagesFor("en")): Pill | null {
|
|
17
18
|
if (!report?.configured) return null;
|
|
18
19
|
const { questions, stuck } = report;
|
|
19
20
|
if (questions || stuck) {
|
|
20
21
|
// Paseo cuts composer pills off at 160 px, so when something is stuck
|
|
21
22
|
// too, the questions shorten to "2Q" to leave room for "1 stuck".
|
|
22
23
|
const parts = [];
|
|
23
|
-
if (questions) parts.push(stuck ?
|
|
24
|
-
if (stuck) parts.push(
|
|
24
|
+
if (questions) parts.push(stuck ? m.shortQuestions(questions) : m.questionCount(questions));
|
|
25
|
+
if (stuck) parts.push(m.stuckCount(stuck));
|
|
25
26
|
return { mode: "attention", label: parts.join(" · "), icon: stuck ? "stuck" : "questions" };
|
|
26
27
|
}
|
|
27
28
|
if (report.runOpen && !report.panelOpened) return { mode: "started", label: "Progress", icon: "started" };
|
package/client/pills.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginButtonContentProps, PluginButtonIconProps, PluginButtonRegistration, PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
2
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
|
-
import React, { useSyncExternalStore } from "react";
|
|
3
|
+
import React, { useEffect, useSyncExternalStore } from "react";
|
|
4
4
|
import { Platform } from "react-native";
|
|
5
5
|
import { type AttentionResult, getAttention } from "../shared/rpc";
|
|
6
6
|
import { isIdle, type Pill, pillFor } from "./pill-state";
|
|
@@ -8,6 +8,8 @@ import { observeDirectory, type PaseoAgentUpdate, type PaseoWorkspaceUpdate } fr
|
|
|
8
8
|
import { onPanelOpened } from "./panel-opened";
|
|
9
9
|
import { IconSwap } from "./motion";
|
|
10
10
|
import { Spinner } from "./spinner";
|
|
11
|
+
import { messagesFor, resolveLocale } from "./i18n";
|
|
12
|
+
import { ProgressLanguageProvider, useProgressLanguage } from "./language";
|
|
11
13
|
import { AttentionPopover } from "./pill-popover";
|
|
12
14
|
|
|
13
15
|
export const PILL_POLL_MS = 5_000;
|
|
@@ -41,12 +43,13 @@ export function contributePills(client: PluginClientContext) {
|
|
|
41
43
|
const workspaces = new Map<string, string>();
|
|
42
44
|
// The last report per workspace; pillFor turns it into what the pill shows.
|
|
43
45
|
const reports = new Map<string, AttentionResult>();
|
|
46
|
+
let locale = resolveLocale("system");
|
|
44
47
|
const where = { phone: progressPanel().location === "workspace" };
|
|
45
|
-
const pillOf = (workspaceId: string) => pillFor(reports.get(workspaceId), where);
|
|
48
|
+
const pillOf = (workspaceId: string) => pillFor(reports.get(workspaceId), where, messagesFor(locale));
|
|
46
49
|
// Bumped on each report so a poll that was already in flight can't bring the pill back.
|
|
47
50
|
let openedReports = 0;
|
|
48
51
|
const listeners = new Set<() => void>();
|
|
49
|
-
const pills = new Map<string, { workspaceId: string; label: string; mode: Pill["mode"]; pill: PluginButtonRegistration }>();
|
|
52
|
+
const pills = new Map<string, { workspaceId: string; label: string; title: string; mode: Pill["mode"]; pill: PluginButtonRegistration }>();
|
|
50
53
|
const pending = new Set<string>();
|
|
51
54
|
// Polls left before an idle workspace is checked again, and when each was last checked.
|
|
52
55
|
const idleWait = new Map<string, number>();
|
|
@@ -56,6 +59,16 @@ export function contributePills(client: PluginClientContext) {
|
|
|
56
59
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
57
60
|
|
|
58
61
|
function PillIcon(props: PluginButtonIconProps) {
|
|
62
|
+
return <ProgressLanguageProvider><LocalizedPillIcon {...props} /></ProgressLanguageProvider>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function LocalizedPillIcon(props: PluginButtonIconProps) {
|
|
66
|
+
const language = useProgressLanguage();
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (stopped || locale === language.locale) return;
|
|
69
|
+
locale = language.locale;
|
|
70
|
+
publish();
|
|
71
|
+
}, [language.locale]);
|
|
59
72
|
const icon = useSyncExternalStore(
|
|
60
73
|
(listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
61
74
|
() => pillOf(props.workspaceId)?.icon ?? "open",
|
|
@@ -76,11 +89,11 @@ export function contributePills(client: PluginClientContext) {
|
|
|
76
89
|
};
|
|
77
90
|
|
|
78
91
|
const buttonFor = (workspaceId: string, mode: Pill["mode"], label: string) => mode === "attention"
|
|
79
|
-
? { title:
|
|
80
|
-
: { title:
|
|
92
|
+
? { title: messagesFor(locale).needsYou, icon: PillIcon, label, behavior: { kind: "popover" as const, Content: PillContent } }
|
|
93
|
+
: { title: messagesFor(locale).openProgress, icon: PillIcon, label, behavior: { kind: "action" as const, onPress: () => openPanel(workspaceId) } };
|
|
81
94
|
|
|
82
95
|
function PillContent(props: PluginButtonContentProps) {
|
|
83
|
-
return <AttentionPopover {...props} openPanel={() => openPanel(props.workspaceId)}
|
|
96
|
+
return <ProgressLanguageProvider><AttentionPopover {...props} openPanel={() => openPanel(props.workspaceId)} /></ProgressLanguageProvider>;
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
const publish = () => {
|
|
@@ -92,6 +105,7 @@ export function contributePills(client: PluginClientContext) {
|
|
|
92
105
|
const wanted = pillOf(workspaceId);
|
|
93
106
|
if (!wanted) continue;
|
|
94
107
|
const { mode, label } = wanted;
|
|
108
|
+
const title = mode === "attention" ? messagesFor(locale).needsYou : messagesFor(locale).openProgress;
|
|
95
109
|
seen.add(agent.id);
|
|
96
110
|
const existing = pills.get(agent.id);
|
|
97
111
|
if (existing && existing.workspaceId !== workspaceId) {
|
|
@@ -101,13 +115,13 @@ export function contributePills(client: PluginClientContext) {
|
|
|
101
115
|
const entry = pills.get(agent.id);
|
|
102
116
|
if (!entry) {
|
|
103
117
|
const pill = client.addComposerPill({ id: "progress-attention", workspaceId, agentId: agent.id, button: buttonFor(workspaceId, mode, label) });
|
|
104
|
-
pills.set(agent.id, { workspaceId, label, mode, pill });
|
|
118
|
+
pills.set(agent.id, { workspaceId, title, label, mode, pill });
|
|
105
119
|
} else if (entry.mode !== mode) {
|
|
106
120
|
entry.pill.update(buttonFor(workspaceId, mode, label));
|
|
107
|
-
Object.assign(entry, { mode, label });
|
|
108
|
-
} else if (entry.label !== label) {
|
|
109
|
-
entry.pill.update({ label });
|
|
110
|
-
entry
|
|
121
|
+
Object.assign(entry, { mode, label, title });
|
|
122
|
+
} else if (entry.label !== label || entry.title !== title) {
|
|
123
|
+
entry.pill.update({ label, title });
|
|
124
|
+
Object.assign(entry, { label, title });
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
127
|
for (const [agentId, entry] of pills) {
|
package/client/preview.tsx
CHANGED
|
@@ -9,7 +9,8 @@ import { folderAttachments, isFolder, previewKind } from "../shared/attachments"
|
|
|
9
9
|
import { canOpenOnHost, previewDeliverable, readDeliverablePiece } from "../shared/rpc";
|
|
10
10
|
import { PressScale } from "./motion";
|
|
11
11
|
import { imageOutline, raised } from "./surfaces";
|
|
12
|
-
import { loadVideo, VideoPlayer } from "./video";
|
|
12
|
+
import { loadVideo, VideoChangedError, VideoPlayer } from "./video";
|
|
13
|
+
import { useProgressLanguage } from "./language";
|
|
13
14
|
|
|
14
15
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
15
16
|
|
|
@@ -39,6 +40,7 @@ type PreviewProps = {
|
|
|
39
40
|
// `onLong` says when the file is too tall for the dialog, so the host turns off
|
|
40
41
|
// its own scrolling and the file scrolls with the actions pinned below.
|
|
41
42
|
export function PreviewContent({ colors, attachment, workspaceId, workspaceDirectory, onOpenOnHost, onCopyPath, onBack, gallery, onOpenEntry, onLong }: PreviewProps & { onLong(long: boolean): void }) {
|
|
43
|
+
const { messages: m } = useProgressLanguage();
|
|
42
44
|
const fetchPreview = useRpc(previewDeliverable);
|
|
43
45
|
const readPiece = useRpc(readDeliverablePiece);
|
|
44
46
|
const checkHost = useRpc(canOpenOnHost);
|
|
@@ -118,19 +120,19 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
118
120
|
useEffect(() => onLong(long), [long]);
|
|
119
121
|
// How far a video has loaded: under the spinner, or in place of the path while
|
|
120
122
|
// the last file stays on screen, so nothing moves.
|
|
121
|
-
const videoLoading = preview.isFetching && loading !== undefined && loading < 1 ?
|
|
123
|
+
const videoLoading = preview.isFetching && loading !== undefined && loading < 1 ? m.loadingVideo(Math.floor(loading * 100)) : null;
|
|
122
124
|
const caption = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 } as const;
|
|
123
125
|
const content = (
|
|
124
126
|
<>
|
|
125
127
|
{preview.isPending ? (
|
|
126
128
|
<View style={{ alignItems: "center", gap: 8, padding: 24 }}>
|
|
127
|
-
<ActivityIndicator color={colors.foregroundMuted} accessibilityLabel=
|
|
129
|
+
<ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingPreview} />
|
|
128
130
|
{videoLoading ? <Text style={[caption, { fontVariant: ["tabular-nums"] }]}>{videoLoading}</Text> : null}
|
|
129
131
|
</View>
|
|
130
132
|
) : null}
|
|
131
133
|
{preview.error ? (
|
|
132
134
|
<Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18 }}>
|
|
133
|
-
|
|
135
|
+
{m.previewError(preview.error instanceof VideoChangedError ? m.videoChanged : errorText(preview.error, m.unknownError))}
|
|
134
136
|
</Text>
|
|
135
137
|
) : null}
|
|
136
138
|
{preview.data?.kind === "text" ? (
|
|
@@ -140,7 +142,7 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
140
142
|
</Text>
|
|
141
143
|
{preview.data.truncated ? (
|
|
142
144
|
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16, paddingTop: 8 }}>
|
|
143
|
-
|
|
145
|
+
{m.truncatedText}{canOpen ? m.openRest : ""}
|
|
144
146
|
</Text>
|
|
145
147
|
) : null}
|
|
146
148
|
</View>
|
|
@@ -166,7 +168,7 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
166
168
|
{attachment.path}
|
|
167
169
|
</Text>
|
|
168
170
|
{/* Optical: the 22px button centers on the 17px caption line without growing it. */}
|
|
169
|
-
<PressScale accessibilityRole="button" accessibilityLabel={
|
|
171
|
+
<PressScale accessibilityRole="button" accessibilityLabel={m.copyItemPath(attachment.title)} hitSlop={6} outerStyle={{ marginVertical: -3 }}
|
|
170
172
|
onPress={onCopyPath}
|
|
171
173
|
style={({ pressed, hovered }) => ({ padding: 4, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
|
|
172
174
|
<Icon name="Copy" size={14} color={colors.foregroundMuted} />
|
|
@@ -181,7 +183,7 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
181
183
|
</View>
|
|
182
184
|
{canStep ? <Stepper colors={colors} index={index} count={items.length} onStep={step} nextRef={nextButton} /> : null}
|
|
183
185
|
<View style={{ flex: 1, alignItems: "flex-end" }}>
|
|
184
|
-
{canOpen ? <DialogButton colors={colors} icon="ExternalLink" label=
|
|
186
|
+
{canOpen ? <DialogButton colors={colors} icon="ExternalLink" label={m.open} accessibilityLabel={m.openDefault} onPress={onOpenOnHost} /> : null}
|
|
185
187
|
</View>
|
|
186
188
|
</View>
|
|
187
189
|
);
|
|
@@ -219,12 +221,13 @@ function estimatedHeight(data: { kind: "image" | "video" } | { kind: "text"; tex
|
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
function FolderList({ colors, folder, entries, truncated, dimmed, onOpen }: { colors: Colors; folder: Attachment; entries: Attachment[]; truncated: boolean; dimmed: boolean; onOpen(attachment: Attachment, group: Attachment[]): void }) {
|
|
224
|
+
const { messages: m } = useProgressLanguage();
|
|
222
225
|
const caption = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 } as const;
|
|
223
|
-
if (!entries.length) return <Text style={caption}>
|
|
226
|
+
if (!entries.length) return <Text style={caption}>{m.emptyFolder(folder.title)}</Text>;
|
|
224
227
|
return (
|
|
225
228
|
<View style={{ gap: 8, opacity: dimmed ? 0.6 : 1 }}>
|
|
226
229
|
<AttachmentList colors={colors} attachments={entries} onOpen={onOpen} />
|
|
227
|
-
{truncated ? <Text style={caption}>
|
|
230
|
+
{truncated ? <Text style={caption}>{m.firstEntries(entries.length)}</Text> : null}
|
|
228
231
|
</View>
|
|
229
232
|
);
|
|
230
233
|
}
|
|
@@ -261,16 +264,18 @@ export function DialogButton({ colors, icon, label, accessibilityLabel, onPress,
|
|
|
261
264
|
|
|
262
265
|
// Returns to the dialog this one was opened from. For surface1 dialogs.
|
|
263
266
|
export function BackButton({ colors, onPress }: { colors: Colors; onPress(): void }) {
|
|
264
|
-
|
|
267
|
+
const { messages: m } = useProgressLanguage();
|
|
268
|
+
return <DialogButton colors={colors} icon="ChevronLeft" label={m.back} onPress={onPress} outerStyle={{ alignSelf: "flex-start" }} />;
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
// Previous and next through a gallery, with "2 of 3" between.
|
|
268
272
|
function Stepper({ colors, index, count, onStep, nextRef }: { colors: Colors; index: number; count: number; onStep(offset: number): void; nextRef: React.Ref<View> }) {
|
|
273
|
+
const { messages: m } = useProgressLanguage();
|
|
269
274
|
return (
|
|
270
275
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
271
|
-
<DialogButton colors={colors} icon="ChevronLeft" accessibilityLabel=
|
|
272
|
-
<Text style={{ minWidth: 44, textAlign: "center", color: colors.foregroundMuted, fontSize: 13, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{index + 1
|
|
273
|
-
<DialogButton ref={nextRef} colors={colors} icon="ChevronRight" accessibilityLabel=
|
|
276
|
+
<DialogButton colors={colors} icon="ChevronLeft" accessibilityLabel={m.previousFile} onPress={() => onStep(-1)} />
|
|
277
|
+
<Text style={{ minWidth: 44, textAlign: "center", color: colors.foregroundMuted, fontSize: 13, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{m.galleryPosition(index + 1, count)}</Text>
|
|
278
|
+
<DialogButton ref={nextRef} colors={colors} icon="ChevronRight" accessibilityLabel={m.nextFile} onPress={() => onStep(1)} />
|
|
274
279
|
</View>
|
|
275
280
|
);
|
|
276
281
|
}
|