@stevecastaneda/paseo-progress 1.3.1 → 1.5.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/OVERVIEW.md +1 -1
- package/README.md +28 -4
- 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 +77 -47
- 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 +42 -18
- package/client/settings.tsx +58 -0
- package/client/show-more.tsx +4 -2
- package/client/spinner.tsx +7 -3
- package/client/status-badge.tsx +3 -9
- package/client/surfaces.ts +20 -8
- package/client/ticket-dialog.tsx +37 -30
- package/client/video.tsx +3 -1
- package/client/whats-new.tsx +125 -0
- package/client/when.tsx +5 -3
- package/client/write-up.tsx +8 -5
- package/dist/server/cli.js +85 -20
- package/images/1-panel.png +0 -0
- package/images/3-question.png +0 -0
- package/images/5-settings.png +0 -0
- package/index.client.tsx +13 -1
- package/index.server.ts +12 -2
- package/package.json +1 -1
- package/paseo-plugin.json +2 -1
- package/server/cli.ts +28 -11
- package/server/question-mode.ts +61 -0
- package/server/store.ts +21 -4
- package/server/whats-new.ts +127 -0
- package/shared/dashboard.ts +9 -2
- package/shared/events.ts +9 -4
- package/shared/question-modes.ts +45 -0
- package/shared/rpc.ts +18 -0
- package/shared/settings.ts +20 -0
- package/shared/show.ts +3 -1
- package/shared/ticket-story.ts +12 -10
- package/shared/whats-new.ts +16 -0
- package/shared/write-up.ts +9 -8
- package/skills/paseo-progress/SKILL.md +26 -68
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
|
}
|
package/client/questions.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { PressScale } from "./motion";
|
|
|
10
10
|
import { StackedDialog } from "./dialog-stack";
|
|
11
11
|
import { raised } from "./surfaces";
|
|
12
12
|
import { When } from "./when";
|
|
13
|
+
import { useProgressLanguage } from "./language";
|
|
13
14
|
|
|
14
15
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
15
16
|
|
|
@@ -23,13 +24,14 @@ export function replyWithChoice(question: Pick<Question, "id" | "title">, letter
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function useCopy() {
|
|
27
|
+
const { messages: m } = useProgressLanguage();
|
|
26
28
|
const toast = useToast();
|
|
27
29
|
return async function copy(question: Question, letter?: string) {
|
|
28
30
|
try {
|
|
29
31
|
await copyText(letter ? replyWithChoice(question, letter) : replyPrefix(question));
|
|
30
|
-
toast.show(letter ?
|
|
32
|
+
toast.show(letter ? m.copiedChoice(question.id, letter) : m.copiedQuestion(question.id), { variant: "success" });
|
|
31
33
|
} catch {
|
|
32
|
-
toast.error(
|
|
34
|
+
toast.error(m.copyFailure);
|
|
33
35
|
}
|
|
34
36
|
};
|
|
35
37
|
}
|
|
@@ -50,15 +52,16 @@ export function QuestionsSection({ colors, questions, answered, now, context }:
|
|
|
50
52
|
now: number;
|
|
51
53
|
context: AttachmentContext;
|
|
52
54
|
}) {
|
|
55
|
+
const { messages: m } = useProgressLanguage();
|
|
53
56
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
54
57
|
const copy = useCopy();
|
|
55
58
|
return (
|
|
56
59
|
<View style={{ margin: 12, marginBottom: 0, borderWidth: 1, borderColor: colors.accent, borderRadius: 6, overflow: "hidden" }}>
|
|
57
60
|
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8, paddingHorizontal: 10, paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
58
61
|
<Icon name="MessageCircleQuestion" size={14} color={colors.accent} />
|
|
59
|
-
<Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>
|
|
62
|
+
<Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>{m.questions}</Text>
|
|
60
63
|
<View style={{ borderWidth: 1, borderColor: colors.accent, backgroundColor: colors.accent, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 1 }}>
|
|
61
|
-
<Text style={{ color: colors.accentForeground, fontSize: 11, lineHeight: 16 }}>{questions.length}
|
|
64
|
+
<Text style={{ color: colors.accentForeground, fontSize: 11, lineHeight: 16 }}>{m.waitingCount(questions.length)}</Text>
|
|
62
65
|
</View>
|
|
63
66
|
</View>
|
|
64
67
|
{questions.map((question, index) => (
|
|
@@ -79,6 +82,7 @@ export function AnsweredQuestionsList({ colors, questions, now, context }: {
|
|
|
79
82
|
now: number;
|
|
80
83
|
context: AttachmentContext;
|
|
81
84
|
}) {
|
|
85
|
+
const { messages: m } = useProgressLanguage();
|
|
82
86
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
83
87
|
const paged = usePaged(questions);
|
|
84
88
|
return (
|
|
@@ -88,7 +92,7 @@ export function AnsweredQuestionsList({ colors, questions, now, context }: {
|
|
|
88
92
|
const chosen = question.options.find((option) => option.letter === answer.choice);
|
|
89
93
|
return (
|
|
90
94
|
<View key={question.id} style={{ borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}>
|
|
91
|
-
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={
|
|
95
|
+
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={m.details(question.id)} onPress={() => setOpenId(question.id)}
|
|
92
96
|
style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 10, paddingVertical: 8 }}>
|
|
93
97
|
{/* Room for two-digit ids so titles line up; longer ones widen instead of wrapping. */}
|
|
94
98
|
<Text numberOfLines={1} style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, fontWeight: "600", fontVariant: ["tabular-nums"], minWidth: 30, flexShrink: 0 }}>{question.id}</Text>
|
|
@@ -110,6 +114,7 @@ export function AnsweredQuestionsList({ colors, questions, now, context }: {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
function AnswerSummary({ colors, question, now }: { colors: Colors; question: Question; now: number }) {
|
|
117
|
+
const { messages: m } = useProgressLanguage();
|
|
113
118
|
const answer = question.answer!;
|
|
114
119
|
const chosen = question.options.find((option) => option.letter === answer.choice);
|
|
115
120
|
return (
|
|
@@ -119,11 +124,11 @@ function AnswerSummary({ colors, question, now }: { colors: Colors; question: Qu
|
|
|
119
124
|
{chosen ? ` ${chosen.label}.` : ""}
|
|
120
125
|
{answer.words ? <Text style={{ color: colors.foregroundMuted }}> “{answer.words}”</Text> : null}
|
|
121
126
|
<Text style={{ color: colors.foregroundMuted }}>
|
|
122
|
-
{answer.changedCourse ?
|
|
127
|
+
{answer.changedCourse ? m.changedCourse : ""}
|
|
123
128
|
</Text>
|
|
124
129
|
</Text>
|
|
125
130
|
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
|
|
126
|
-
|
|
131
|
+
{m.answerRecorded}<When colors={colors} iso={answer.at} now={now} />{question.raisedBy ? m.raisedBy(question.raisedBy) : ""}
|
|
127
132
|
</Text>
|
|
128
133
|
</View>
|
|
129
134
|
);
|
|
@@ -136,9 +141,10 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
136
141
|
onOpen(): void;
|
|
137
142
|
onCopy(): void;
|
|
138
143
|
}) {
|
|
144
|
+
const { messages: m } = useProgressLanguage();
|
|
139
145
|
return (
|
|
140
146
|
<View style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 10, borderTopWidth: first ? 0 : 1, borderTopColor: colors.border }}>
|
|
141
|
-
<PressScale accessibilityRole="button" accessibilityLabel={
|
|
147
|
+
<PressScale accessibilityRole="button" accessibilityLabel={m.copyQuestion(question.id)} onPress={onCopy} hitSlop={6}
|
|
142
148
|
outerStyle={{ alignSelf: "flex-start", marginTop: 1 }}
|
|
143
149
|
style={({ pressed }) => ({
|
|
144
150
|
// One width for every id, so the questions beside them start on one edge.
|
|
@@ -151,9 +157,12 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
151
157
|
})}>
|
|
152
158
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 17, fontWeight: "600" }}>{question.id}</Text>
|
|
153
159
|
</PressScale>
|
|
154
|
-
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={
|
|
160
|
+
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={m.questionChoices(question.id)} onPress={onOpen}
|
|
155
161
|
style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "flex-start", gap: 8, borderRadius: 4, margin: -4, padding: 4 }}>
|
|
156
|
-
<
|
|
162
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
163
|
+
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
|
|
164
|
+
<QuestionState colors={colors} question={question} />
|
|
165
|
+
</View>
|
|
157
166
|
{/* Optical: centers the 14px chevron on the 19px first line. */}
|
|
158
167
|
<View style={{ paddingTop: 2.5 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
159
168
|
</PressableRow>
|
|
@@ -183,10 +192,24 @@ export function QuestionView({ colors, question, now, onCopy, onOpenAttachment,
|
|
|
183
192
|
);
|
|
184
193
|
}
|
|
185
194
|
|
|
186
|
-
//
|
|
195
|
+
// Under an open question: whether the agent stopped, waits, or builds its pick,
|
|
196
|
+
// and its pick when there are no lettered options to tag.
|
|
187
197
|
export function QuestionState({ colors, question }: { colors: Colors; question: Question }) {
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
const { messages: m } = useProgressLanguage();
|
|
199
|
+
const value = { color: colors.foreground };
|
|
200
|
+
// The letter alone means nothing in the rows that don't list the options.
|
|
201
|
+
const pick = question.options.find((option) => option.letter === question.recommended);
|
|
202
|
+
return (
|
|
203
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
204
|
+
{question.state === "building" ? <>{m.buildingWith}<Text style={value}>{question.recommended}{pick ? `${m.colon}${pick.label}` : ""}</Text></> : (
|
|
205
|
+
<>
|
|
206
|
+
{!question.options.length && question.recommended ? <>{m.recommended}{m.colon}<Text style={value}>{question.recommended}</Text>{m.sentenceBreak}</> : null}
|
|
207
|
+
{question.state === "stopped" ? m.agentStopped : m.waitingForYou}
|
|
208
|
+
{question.big ? <>{m.sentenceBreak}<Text style={value}>{m.big}</Text>{m.colon}{question.big}</> : null}
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</Text>
|
|
212
|
+
);
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
// `onSurface1` when drawn on a surface1 background (Paseo's dialogs), so the
|
|
@@ -199,6 +222,7 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
|
|
|
199
222
|
onOpenAttachment(attachment: Attachment, group: Attachment[]): void;
|
|
200
223
|
onSurface1?: boolean;
|
|
201
224
|
}) {
|
|
225
|
+
const { messages: m } = useProgressLanguage();
|
|
202
226
|
const button = onSurface1 ? { rest: colors.surface2, pressed: colors.surface1 } : { rest: colors.surface1, pressed: colors.surface2 };
|
|
203
227
|
return (
|
|
204
228
|
<View style={{ gap: 8, paddingTop: 2 }}>
|
|
@@ -214,17 +238,17 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
|
|
|
214
238
|
<View style={{ flex: 1, gap: 2 }}>
|
|
215
239
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>
|
|
216
240
|
{option.label}
|
|
217
|
-
{chosen ? <Text style={{ color: colors.statusSuccess }}>
|
|
218
|
-
{recommended ? <Text style={{ color: settled ? colors.foregroundMuted : colors.accent }}>
|
|
241
|
+
{chosen ? <Text style={{ color: colors.statusSuccess }}>{" " + m.yourAnswer}</Text> : null}
|
|
242
|
+
{recommended ? <Text style={{ color: settled ? colors.foregroundMuted : colors.accent }}>{" " + m.recommended}</Text> : null}
|
|
219
243
|
</Text>
|
|
220
244
|
{option.consequence ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{option.consequence}</Text> : null}
|
|
221
245
|
</View>
|
|
222
246
|
{!question.answer ? (
|
|
223
|
-
<PressScale accessibilityRole="button" accessibilityLabel={
|
|
247
|
+
<PressScale accessibilityRole="button" accessibilityLabel={m.copyChoiceForAgent(question.id, option.letter)}
|
|
224
248
|
onPress={() => onCopy(option.letter)} hitSlop={4}
|
|
225
249
|
style={({ pressed }) => ({ flexDirection: "row", alignItems: "center", gap: 4, paddingLeft: 6, paddingRight: 8, paddingVertical: 3, borderRadius: 4, ...raised(colors), backgroundColor: pressed ? button.pressed : button.rest })}>
|
|
226
250
|
<Icon name="Copy" size={12} color={colors.foreground} />
|
|
227
|
-
<Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16 }}>
|
|
251
|
+
<Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16 }}>{m.copyChoice(question.id, option.letter)}</Text>
|
|
228
252
|
</PressScale>
|
|
229
253
|
) : null}
|
|
230
254
|
</View>
|
|
@@ -235,7 +259,7 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
|
|
|
235
259
|
) : null}
|
|
236
260
|
<AttachmentList colors={colors} attachments={questionAttachments(question)} onOpen={onOpenAttachment} />
|
|
237
261
|
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
|
|
238
|
-
|
|
262
|
+
{m.asked}<When colors={colors} iso={question.askedAt} now={now} />{question.raisedBy ? m.raisedBy(question.raisedBy) : ""}
|
|
239
263
|
</Text>
|
|
240
264
|
</View>
|
|
241
265
|
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { SettingsAction, SettingsCard, SettingsSection, SettingsSelect } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Text } from "react-native";
|
|
5
|
+
import { DEFAULT_QUESTION_MODE, QUESTION_MODES } from "../shared/question-modes";
|
|
6
|
+
import { questionSettings, LANGUAGE_PREFERENCES } from "../shared/settings";
|
|
7
|
+
import { useProgressLanguage } from "./language";
|
|
8
|
+
|
|
9
|
+
export function QuestionSettings({ theme }: PluginSurfaceProps) {
|
|
10
|
+
return <><QuestionModeSettings theme={theme} /><DisplaySettings theme={theme} /></>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Colors = PluginSurfaceProps["theme"]["colors"];
|
|
14
|
+
|
|
15
|
+
function DisplaySettings({ theme }: Pick<PluginSurfaceProps, "theme">) {
|
|
16
|
+
const { messages: m, settings } = useProgressLanguage();
|
|
17
|
+
const muted = { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 19 };
|
|
18
|
+
const options = LANGUAGE_PREFERENCES.map((value) => ({ value, label: value === "system" ? m.systemLanguage : value === "en" ? "English" : "简体中文" }));
|
|
19
|
+
return (
|
|
20
|
+
<SettingsSection title={m.display}>
|
|
21
|
+
{settings.status === "loading" ? <Text style={muted}>{m.loadingSettings}</Text>
|
|
22
|
+
: settings.status !== "ready" ? <SettingsRecovery colors={theme.colors} settings={settings} />
|
|
23
|
+
: <SettingsCard><SettingsSelect label={m.language} hint={m.languageHint} value={settings.values.language} options={options}
|
|
24
|
+
disabled={settings.saving} error={settings.saveError}
|
|
25
|
+
onValueChange={(language) => void settings.save({ ...settings.values, language }, settings.revision)} /></SettingsCard>}
|
|
26
|
+
</SettingsSection>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function QuestionModeSettings({ theme }: Pick<PluginSurfaceProps, "theme">) {
|
|
31
|
+
const { messages: m } = useProgressLanguage();
|
|
32
|
+
const settings = useSettings(questionSettings);
|
|
33
|
+
const muted = { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 19 };
|
|
34
|
+
return (
|
|
35
|
+
<SettingsSection title={m.questions}>
|
|
36
|
+
{settings.status === "loading" ? <Text style={muted}>{m.loadingSettings}</Text>
|
|
37
|
+
: settings.status !== "ready" ? <SettingsRecovery colors={theme.colors} settings={settings} resetLabel={m.resetTo(m.questionModes[DEFAULT_QUESTION_MODE].label)} />
|
|
38
|
+
: <SettingsCard><SettingsSelect label={m.questionMode} hint={m.questionModes[settings.values.mode].hint}
|
|
39
|
+
error={settings.saveError} value={settings.values.mode} options={QUESTION_MODES.map((value) => ({ value, label: m.questionModes[value].label }))}
|
|
40
|
+
disabled={settings.saving} onValueChange={(mode) => void settings.save({ ...settings.values, mode }, settings.revision)} /></SettingsCard>}
|
|
41
|
+
</SettingsSection>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function SettingsRecovery({ colors, settings, resetLabel }: {
|
|
46
|
+
colors: Colors;
|
|
47
|
+
settings: { status: "error" | "invalid"; error: string; reload(): Promise<void>; reset(): Promise<boolean> };
|
|
48
|
+
resetLabel?: string;
|
|
49
|
+
}) {
|
|
50
|
+
const { messages: m } = useProgressLanguage();
|
|
51
|
+
return <>
|
|
52
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 13, lineHeight: 19 }}>{settings.error}</Text>
|
|
53
|
+
<SettingsCard>
|
|
54
|
+
<SettingsAction label={m.tryAgain} actionLabel={m.reload} onPress={settings.reload} />
|
|
55
|
+
{settings.status === "invalid" ? <SettingsAction label={resetLabel ?? m.resetTo(m.systemLanguage)} actionLabel={m.reset} onPress={settings.reset} /> : null}
|
|
56
|
+
</SettingsCard>
|
|
57
|
+
</>;
|
|
58
|
+
}
|
package/client/show-more.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import React, { useState } from "react";
|
|
|
4
4
|
import { Text } from "react-native";
|
|
5
5
|
import { IconSwap } from "./motion";
|
|
6
6
|
import { PressableRow } from "./row";
|
|
7
|
+
import { useProgressLanguage } from "./language";
|
|
7
8
|
|
|
8
9
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
9
10
|
|
|
@@ -23,8 +24,9 @@ export function usePaged<T>(items: T[]): { shown: T[]; older: number; showOlder(
|
|
|
23
24
|
|
|
24
25
|
// The row under a paged list: "Show 10 older", or "Show fewer" once all are shown.
|
|
25
26
|
export function ShowMoreRow({ colors, total, paged }: { colors: Colors; total: number; paged: ReturnType<typeof usePaged> }) {
|
|
26
|
-
const
|
|
27
|
-
|
|
27
|
+
const { messages: m } = useProgressLanguage();
|
|
28
|
+
const more = paged.older > 0 ? { label: m.showOlder(Math.min(paged.older, PAGE)), icon: "ChevronDown", onPress: paged.showOlder }
|
|
29
|
+
: total > PAGE ? { label: m.showFewer, icon: "ChevronUp", onPress: paged.showFewer }
|
|
28
30
|
: null;
|
|
29
31
|
if (!more) return null;
|
|
30
32
|
return (
|
package/client/spinner.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
2
2
|
import React, { useEffect, useRef } from "react";
|
|
3
3
|
import { ActivityIndicator, Animated, Easing, Platform, View } from "react-native";
|
|
4
|
+
import { useProgressLanguage } from "./language";
|
|
4
5
|
import { nativeDriver } from "./motion";
|
|
5
6
|
|
|
6
7
|
// The system spinner, sized to sit beside a line of text. iOS draws it only
|
|
7
8
|
// at 20pt, so it's scaled into the slot there.
|
|
8
9
|
export function Spinner({ color, size = 12 }: { color: string; size?: number }) {
|
|
10
|
+
const { messages: m } = useProgressLanguage();
|
|
9
11
|
return (
|
|
10
|
-
<View accessibilityLabel=
|
|
12
|
+
<View accessibilityLabel={m.status.working} style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}>
|
|
11
13
|
{Platform.OS === "ios"
|
|
12
14
|
? <ActivityIndicator size="small" color={color} style={{ transform: [{ scale: size / 20 }] }} />
|
|
13
15
|
: <ActivityIndicator size={size} color={color} />}
|
|
@@ -18,6 +20,7 @@ export function Spinner({ color, size = 12 }: { color: string; size?: number })
|
|
|
18
20
|
// Shown in place of the spinner once the dashboard is stale: the work may have
|
|
19
21
|
// stopped, so the icon asks for attention instead of looking busy.
|
|
20
22
|
export function StalledPulse({ color, size = 12 }: { color: string; size?: number }) {
|
|
23
|
+
const { messages: m } = useProgressLanguage();
|
|
21
24
|
const opacity = useRef(new Animated.Value(1)).current;
|
|
22
25
|
useEffect(() => {
|
|
23
26
|
const loop = Animated.loop(Animated.sequence([
|
|
@@ -28,7 +31,7 @@ export function StalledPulse({ color, size = 12 }: { color: string; size?: numbe
|
|
|
28
31
|
return () => loop.stop();
|
|
29
32
|
}, [opacity]);
|
|
30
33
|
return (
|
|
31
|
-
<Animated.View accessibilityLabel=
|
|
34
|
+
<Animated.View accessibilityLabel={m.possiblyStalled} style={{ width: size, height: size, opacity }}>
|
|
32
35
|
<Icon name="TriangleAlert" size={size} color={color} />
|
|
33
36
|
</Animated.View>
|
|
34
37
|
);
|
|
@@ -37,9 +40,10 @@ export function StalledPulse({ color, size = 12 }: { color: string; size?: numbe
|
|
|
37
40
|
// A ticket not started yet, or waiting on another one: a still, faded dot, so it
|
|
38
41
|
// reads as parked rather than busy. Centered in the same slot an icon would take.
|
|
39
42
|
export function WaitingDot({ color, size = 12 }: { color: string; size?: number }) {
|
|
43
|
+
const { messages: m } = useProgressLanguage();
|
|
40
44
|
const dot = Math.round(size / 2);
|
|
41
45
|
return (
|
|
42
|
-
<View accessibilityLabel=
|
|
46
|
+
<View accessibilityLabel={m.waiting} style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}>
|
|
43
47
|
<View style={{ width: dot, height: dot, borderRadius: dot / 2, backgroundColor: color, opacity: 0.5 }} />
|
|
44
48
|
</View>
|
|
45
49
|
);
|
package/client/status-badge.tsx
CHANGED
|
@@ -2,19 +2,13 @@ import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Text, View } from "react-native";
|
|
4
4
|
import type { TicketStatus } from "../shared/events";
|
|
5
|
+
import { useProgressLanguage } from "./language";
|
|
5
6
|
|
|
6
7
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
7
8
|
|
|
8
|
-
export const STATUS_LABEL: Record<TicketStatus, string> = {
|
|
9
|
-
not_started: "Not started",
|
|
10
|
-
working: "Working",
|
|
11
|
-
blocked: "Blocked",
|
|
12
|
-
done: "Done",
|
|
13
|
-
skipped: "Skipped",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
9
|
// `waiting`: held for another ticket. Shown as calm as "Not started", whatever the status.
|
|
17
10
|
export function StatusBadge({ colors, status, waiting = false }: { colors: Colors; status: TicketStatus; waiting?: boolean }) {
|
|
11
|
+
const { messages: m } = useProgressLanguage();
|
|
18
12
|
const tone = waiting ? { fg: colors.foregroundMuted, bg: "transparent", border: colors.border } : {
|
|
19
13
|
done: { fg: colors.statusSuccess, bg: "transparent", border: colors.statusSuccess },
|
|
20
14
|
working: { fg: colors.accentForeground, bg: colors.accent, border: colors.accent },
|
|
@@ -24,7 +18,7 @@ export function StatusBadge({ colors, status, waiting = false }: { colors: Color
|
|
|
24
18
|
}[status];
|
|
25
19
|
return (
|
|
26
20
|
<View style={{ borderWidth: 1, borderColor: tone.border, backgroundColor: tone.bg, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 1 }}>
|
|
27
|
-
<Text style={{ color: tone.fg, fontSize: 11, lineHeight: 16 }}>{waiting ?
|
|
21
|
+
<Text style={{ color: tone.fg, fontSize: 11, lineHeight: 16 }}>{waiting ? m.waiting : m.status[status]}</Text>
|
|
28
22
|
</View>
|
|
29
23
|
);
|
|
30
24
|
}
|
package/client/surfaces.ts
CHANGED
|
@@ -3,17 +3,20 @@ import type { ViewStyle } from "react-native";
|
|
|
3
3
|
|
|
4
4
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
function
|
|
8
|
-
let rgb: number[] | null = null;
|
|
6
|
+
// The red, green and blue of a #rgb/#rrggbb or rgb() color, or null when unreadable.
|
|
7
|
+
function channels(color: string): number[] | null {
|
|
9
8
|
const hex = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
10
9
|
if (hex) {
|
|
11
10
|
const digits = hex[1].length === 3 ? [...hex[1]].map((d) => d + d) : hex[1].match(/../g)!;
|
|
12
|
-
|
|
13
|
-
} else {
|
|
14
|
-
const fn = color.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
15
|
-
if (fn) rgb = fn.slice(1, 4).map(Number);
|
|
11
|
+
return digits.map((d) => parseInt(d, 16));
|
|
16
12
|
}
|
|
13
|
+
const fn = color.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
14
|
+
return fn ? fn.slice(1, 4).map(Number) : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Relative luminance, or null when the color is unreadable.
|
|
18
|
+
function luminance(color: string): number | null {
|
|
19
|
+
const rgb = channels(color);
|
|
17
20
|
if (!rgb) return null;
|
|
18
21
|
const [r, g, b] = rgb.map((v) => {
|
|
19
22
|
const c = v / 255;
|
|
@@ -22,7 +25,7 @@ function luminance(color: string): number | null {
|
|
|
22
25
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
function isLight(colors: Colors): boolean | null {
|
|
28
|
+
export function isLight(colors: Colors): boolean | null {
|
|
26
29
|
const value = luminance(colors.surface0);
|
|
27
30
|
return value === null ? null : value > 0.4;
|
|
28
31
|
}
|
|
@@ -43,3 +46,12 @@ export function raised(colors: Colors): ViewStyle {
|
|
|
43
46
|
: "0px 0px 0px 1px rgba(255, 255, 255, 0.08)",
|
|
44
47
|
};
|
|
45
48
|
}
|
|
49
|
+
|
|
50
|
+
// A color at low opacity, for a tag's fill: the same hue as its text, never a
|
|
51
|
+
// drifting second color. Paseo's own status badges use 12% in light themes and
|
|
52
|
+
// 16% in dark ones, where the surface swallows more of it.
|
|
53
|
+
export function tint(color: string, colors: Colors): string {
|
|
54
|
+
const rgb = channels(color);
|
|
55
|
+
if (!rgb) return colors.surface2;
|
|
56
|
+
return `rgba(${rgb.join(", ")}, ${isLight(colors) ? 0.12 : 0.16})`;
|
|
57
|
+
}
|