@domicile-desktop/component-library 0.0.0-alpha-183ca460bbae → 0.0.0-alpha-3e5b976f2009
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@base-ui/react": "^1.8.0",
|
|
4
4
|
"@cprussin/option-result": "^2.0.1",
|
|
5
|
-
"@domicile-desktop/sdk": "0.0.0-alpha-
|
|
6
|
-
"@domicile-desktop/system-apps": "0.0.0-alpha-
|
|
5
|
+
"@domicile-desktop/sdk": "0.0.0-alpha-3e5b976f2009",
|
|
6
|
+
"@domicile-desktop/system-apps": "0.0.0-alpha-3e5b976f2009",
|
|
7
7
|
"@pandacss/dev": "^1.12.1",
|
|
8
8
|
"@phosphor-icons/react": "^2.1.10"
|
|
9
9
|
},
|
|
@@ -114,5 +114,5 @@
|
|
|
114
114
|
"test:unit": "bun test"
|
|
115
115
|
},
|
|
116
116
|
"type": "module",
|
|
117
|
-
"version": "0.0.0-alpha-
|
|
117
|
+
"version": "0.0.0-alpha-3e5b976f2009"
|
|
118
118
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
PageRange,
|
|
2
3
|
PortalAnswer,
|
|
3
4
|
PrintBody,
|
|
4
5
|
Printer,
|
|
@@ -11,19 +12,26 @@ import {
|
|
|
11
12
|
PrintQuality,
|
|
12
13
|
Sides,
|
|
13
14
|
} from "@domicile-desktop/sdk/portal";
|
|
15
|
+
import { FilePdfIcon } from "@phosphor-icons/react/dist/ssr/FilePdf";
|
|
16
|
+
import { PrinterIcon } from "@phosphor-icons/react/dist/ssr/Printer";
|
|
14
17
|
import { useState } from "react";
|
|
15
18
|
import { css } from "../../styled-system/css";
|
|
16
19
|
import { grid } from "../../styled-system/patterns";
|
|
20
|
+
import { focusOnAttach } from "../_control/focusOnAttach";
|
|
17
21
|
import { Button } from "../Button/Button";
|
|
22
|
+
import { Field } from "../Field/Field";
|
|
23
|
+
import { FilePicker } from "../FilePicker/FilePicker";
|
|
24
|
+
import { ChooserMode } from "../FilePicker/file-request";
|
|
18
25
|
import { Input } from "../Input/Input";
|
|
19
26
|
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
27
|
+
import { useScreenRegion } from "../Screen/DisplayProvider";
|
|
20
28
|
import { Select } from "../Select/Select";
|
|
21
29
|
import { pageRanges } from "./page-ranges";
|
|
22
30
|
|
|
23
31
|
const SIDES = {
|
|
24
|
-
[Sides.OneSided]: "
|
|
25
|
-
[Sides.TwoSidedLongEdge]: "
|
|
26
|
-
[Sides.TwoSidedShortEdge]: "
|
|
32
|
+
[Sides.OneSided]: "Off",
|
|
33
|
+
[Sides.TwoSidedLongEdge]: "Long edge",
|
|
34
|
+
[Sides.TwoSidedShortEdge]: "Short edge",
|
|
27
35
|
} as const;
|
|
28
36
|
|
|
29
37
|
const COLOR_MODES = {
|
|
@@ -34,8 +42,8 @@ const COLOR_MODES = {
|
|
|
34
42
|
const ORIENTATIONS = {
|
|
35
43
|
[Orientation.Portrait]: "Portrait",
|
|
36
44
|
[Orientation.Landscape]: "Landscape",
|
|
37
|
-
[Orientation.ReverseLandscape]: "
|
|
38
|
-
[Orientation.ReversePortrait]: "
|
|
45
|
+
[Orientation.ReverseLandscape]: "Landscape, upside down",
|
|
46
|
+
[Orientation.ReversePortrait]: "Portrait, upside down",
|
|
39
47
|
} as const;
|
|
40
48
|
|
|
41
49
|
const QUALITIES = {
|
|
@@ -44,40 +52,77 @@ const QUALITIES = {
|
|
|
44
52
|
[PrintQuality.High]: "High",
|
|
45
53
|
} as const;
|
|
46
54
|
|
|
55
|
+
/** Whether to print every page or the ones typed. */
|
|
56
|
+
enum PageChoice {
|
|
57
|
+
All,
|
|
58
|
+
Custom,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const PAGE_CHOICES = [
|
|
62
|
+
{ label: "All", value: PageChoice.All },
|
|
63
|
+
{ label: "Custom", value: PageChoice.Custom },
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
47
66
|
type Props = {
|
|
48
67
|
answer: (answer: PortalAnswer) => void;
|
|
49
68
|
/** Who asks, as the dialog names them. */
|
|
50
69
|
asker: string;
|
|
51
70
|
body: PrintBody;
|
|
71
|
+
/** Lists a directory for the PDF's file picker; see `FileRequest.list`. */
|
|
72
|
+
list: (path: string) => Promise<readonly string[]>;
|
|
52
73
|
screen: string | undefined;
|
|
53
74
|
};
|
|
54
75
|
|
|
55
76
|
/**
|
|
56
|
-
* A print dialog: a printer and the options
|
|
57
|
-
*
|
|
58
|
-
* only after it closes.
|
|
77
|
+
* A print dialog: a printer or Save to PDF, and the options each supports.
|
|
78
|
+
* Saving to a PDF picks its file after. Dismissing it cancels. No preview:
|
|
79
|
+
* the document arrives only after it closes.
|
|
59
80
|
*/
|
|
60
|
-
export const PrintDialog = ({ answer, asker, body, screen }: Props) => {
|
|
61
|
-
const [chosen, setChosen] = useState(() =>
|
|
62
|
-
|
|
81
|
+
export const PrintDialog = ({ answer, asker, body, list, screen }: Props) => {
|
|
82
|
+
const [chosen, setChosen] = useState(() => startingChoice(body));
|
|
83
|
+
const [picking, setPicking] = useState(false);
|
|
84
|
+
const options = readOptions(chosen);
|
|
85
|
+
const pdf = chosen.printer === body.pdf;
|
|
86
|
+
return picking && options !== undefined ? (
|
|
87
|
+
<PdfPicker
|
|
88
|
+
answer={answer}
|
|
89
|
+
back={() => {
|
|
90
|
+
setPicking(false);
|
|
91
|
+
}}
|
|
92
|
+
body={body}
|
|
93
|
+
list={list}
|
|
94
|
+
options={options}
|
|
95
|
+
screen={screen}
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
63
98
|
<ModalDialog
|
|
64
99
|
closeButton={false}
|
|
65
100
|
footer={
|
|
66
|
-
|
|
101
|
+
<>
|
|
67
102
|
<Button
|
|
68
103
|
onClick={() => {
|
|
69
104
|
answer(Answer.Canceled());
|
|
70
105
|
}}
|
|
106
|
+
variant="outline"
|
|
71
107
|
>
|
|
72
|
-
|
|
108
|
+
Cancel
|
|
73
109
|
</Button>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
<Button
|
|
111
|
+
disabled={options === undefined}
|
|
112
|
+
onClick={() => {
|
|
113
|
+
if (options !== undefined) {
|
|
114
|
+
if (pdf) {
|
|
115
|
+
setPicking(true);
|
|
116
|
+
} else {
|
|
117
|
+
answer(Answer.Print(chosen.printer.name, options));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}}
|
|
121
|
+
variant="accent"
|
|
122
|
+
>
|
|
123
|
+
{pdf ? "Save…" : (body.acceptLabel ?? "Print")}
|
|
124
|
+
</Button>
|
|
125
|
+
</>
|
|
81
126
|
}
|
|
82
127
|
onOpenChange={(open) => {
|
|
83
128
|
if (!open) {
|
|
@@ -89,128 +134,139 @@ export const PrintDialog = ({ answer, asker, body, screen }: Props) => {
|
|
|
89
134
|
title="Print"
|
|
90
135
|
>
|
|
91
136
|
<p className={askerStyles}>{`${asker} prints “${body.title}”`}</p>
|
|
92
|
-
{
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
137
|
+
<div className={formStyles}>
|
|
138
|
+
<div className={wideStyles}>
|
|
139
|
+
<Field
|
|
140
|
+
hint={
|
|
141
|
+
body.printers.length === 0 ? "No printers are set up." : undefined
|
|
142
|
+
}
|
|
143
|
+
label="Destination"
|
|
144
|
+
>
|
|
145
|
+
<Select
|
|
146
|
+
onValueChange={(picked) => {
|
|
147
|
+
if (picked !== null) {
|
|
148
|
+
setChosen(startOf(picked));
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
options={[...body.printers, body.pdf].map((listed) => ({
|
|
152
|
+
label: (
|
|
153
|
+
<span className={destinationStyles}>
|
|
154
|
+
<DestinationIcon pdf={listed === body.pdf} />
|
|
155
|
+
{nameOf(listed)}
|
|
156
|
+
</span>
|
|
157
|
+
),
|
|
158
|
+
textLabel: nameOf(listed),
|
|
159
|
+
value: listed,
|
|
160
|
+
}))}
|
|
161
|
+
prefixIcon={<DestinationIcon pdf={pdf} />}
|
|
162
|
+
value={chosen.printer}
|
|
163
|
+
/>
|
|
164
|
+
</Field>
|
|
165
|
+
</div>
|
|
166
|
+
<Options chosen={chosen} onChange={setChosen} />
|
|
167
|
+
</div>
|
|
101
168
|
</ModalDialog>
|
|
102
169
|
);
|
|
103
170
|
};
|
|
104
171
|
|
|
172
|
+
const DestinationIcon = ({ pdf }: { pdf: boolean }) =>
|
|
173
|
+
pdf ? <FilePdfIcon /> : <PrinterIcon />;
|
|
174
|
+
|
|
105
175
|
/** A printer and the options as the user has set them so far. */
|
|
106
176
|
type Chosen = {
|
|
107
177
|
/** As typed. */
|
|
108
178
|
copies: string;
|
|
179
|
+
pageChoice: PageChoice;
|
|
109
180
|
/** As typed. */
|
|
110
181
|
pages: string;
|
|
111
182
|
options: PrintOptions;
|
|
112
183
|
printer: Printer;
|
|
113
184
|
};
|
|
114
185
|
|
|
115
|
-
|
|
116
|
-
acceptLabel,
|
|
117
|
-
answer,
|
|
118
|
-
chosen,
|
|
119
|
-
}: {
|
|
120
|
-
acceptLabel: string;
|
|
121
|
-
answer: (answer: PortalAnswer) => void;
|
|
122
|
-
chosen: Chosen;
|
|
123
|
-
}) => {
|
|
124
|
-
const options = readOptions(chosen);
|
|
125
|
-
return (
|
|
126
|
-
<>
|
|
127
|
-
<Button
|
|
128
|
-
onClick={() => {
|
|
129
|
-
answer(Answer.Canceled());
|
|
130
|
-
}}
|
|
131
|
-
variant="outline"
|
|
132
|
-
>
|
|
133
|
-
Cancel
|
|
134
|
-
</Button>
|
|
135
|
-
<Button
|
|
136
|
-
disabled={options === undefined}
|
|
137
|
-
onClick={() => {
|
|
138
|
-
if (options !== undefined) {
|
|
139
|
-
answer(Answer.Print(chosen.printer.name, options));
|
|
140
|
-
}
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{acceptLabel}
|
|
144
|
-
</Button>
|
|
145
|
-
</>
|
|
146
|
-
);
|
|
147
|
-
};
|
|
148
|
-
|
|
186
|
+
/** The options `chosen.printer` supports, each in a labeled field. */
|
|
149
187
|
const Options = ({
|
|
150
188
|
chosen,
|
|
151
189
|
onChange,
|
|
152
|
-
printers,
|
|
153
190
|
}: {
|
|
154
191
|
chosen: Chosen;
|
|
155
192
|
onChange: (chosen: Chosen) => void;
|
|
156
|
-
printers: readonly Printer[];
|
|
157
193
|
}) => {
|
|
158
194
|
const { options, printer } = chosen;
|
|
159
195
|
const set = (changed: Partial<PrintOptions>) => {
|
|
160
196
|
onChange({ ...chosen, options: { ...options, ...changed } });
|
|
161
197
|
};
|
|
198
|
+
const custom = chosen.pageChoice === PageChoice.Custom;
|
|
162
199
|
return (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
<>
|
|
201
|
+
{printer.pageRanges && (
|
|
202
|
+
<div className={pagesStyles}>
|
|
203
|
+
<Field label="Pages">
|
|
204
|
+
<Select
|
|
205
|
+
onValueChange={(pageChoice) => {
|
|
206
|
+
if (pageChoice !== null) {
|
|
207
|
+
onChange({ ...chosen, pageChoice });
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
options={PAGE_CHOICES}
|
|
211
|
+
value={chosen.pageChoice}
|
|
212
|
+
/>
|
|
213
|
+
</Field>
|
|
214
|
+
{custom && (
|
|
215
|
+
<Field
|
|
216
|
+
invalid={chosen.pages !== "" && pagesOf(chosen) === undefined}
|
|
217
|
+
>
|
|
218
|
+
<Input
|
|
219
|
+
aria-label="Page range"
|
|
220
|
+
autoFocus
|
|
221
|
+
onChange={(event) => {
|
|
222
|
+
onChange({ ...chosen, pages: event.target.value });
|
|
223
|
+
}}
|
|
224
|
+
placeholder="e.g. 1-3, 5"
|
|
225
|
+
value={chosen.pages}
|
|
226
|
+
/>
|
|
227
|
+
</Field>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
{printer.copiesMax > 1 && (
|
|
232
|
+
<Field label="Copies">
|
|
233
|
+
<Input
|
|
234
|
+
max={printer.copiesMax}
|
|
235
|
+
min={1}
|
|
236
|
+
onChange={(event) => {
|
|
237
|
+
onChange({ ...chosen, copies: event.target.value });
|
|
238
|
+
}}
|
|
239
|
+
type="number"
|
|
240
|
+
value={chosen.copies}
|
|
241
|
+
/>
|
|
242
|
+
</Field>
|
|
243
|
+
)}
|
|
178
244
|
{printer.media.length > 0 && (
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
245
|
+
<Field label="Paper">
|
|
246
|
+
<Select
|
|
247
|
+
onValueChange={(media) => {
|
|
248
|
+
set({ media: media ?? undefined });
|
|
249
|
+
}}
|
|
250
|
+
options={printer.media.map((media) => ({
|
|
251
|
+
label: media.label,
|
|
252
|
+
value: media.name,
|
|
253
|
+
}))}
|
|
254
|
+
placeholder="Default"
|
|
255
|
+
value={options.media ?? null}
|
|
256
|
+
/>
|
|
257
|
+
</Field>
|
|
191
258
|
)}
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
onChange={(
|
|
197
|
-
|
|
259
|
+
<Choice
|
|
260
|
+
label="Layout"
|
|
261
|
+
labels={ORIENTATIONS}
|
|
262
|
+
offered={printer.orientations}
|
|
263
|
+
onChange={(orientation) => {
|
|
264
|
+
set({ orientation });
|
|
198
265
|
}}
|
|
199
|
-
|
|
200
|
-
value={chosen.copies}
|
|
266
|
+
value={options.orientation}
|
|
201
267
|
/>
|
|
202
|
-
{printer.pageRanges && (
|
|
203
|
-
<Input
|
|
204
|
-
aria-label="Pages"
|
|
205
|
-
onChange={(event) => {
|
|
206
|
-
onChange({ ...chosen, pages: event.target.value });
|
|
207
|
-
}}
|
|
208
|
-
placeholder="All, or 1-3, 5"
|
|
209
|
-
value={chosen.pages}
|
|
210
|
-
/>
|
|
211
|
-
)}
|
|
212
268
|
<Choice
|
|
213
|
-
label="
|
|
269
|
+
label="Two-sided"
|
|
214
270
|
labels={SIDES}
|
|
215
271
|
offered={printer.sides}
|
|
216
272
|
onChange={(sides) => {
|
|
@@ -227,15 +283,6 @@ const Options = ({
|
|
|
227
283
|
}}
|
|
228
284
|
value={options.colorMode}
|
|
229
285
|
/>
|
|
230
|
-
<Choice
|
|
231
|
-
label="Orientation"
|
|
232
|
-
labels={ORIENTATIONS}
|
|
233
|
-
offered={printer.orientations}
|
|
234
|
-
onChange={(orientation) => {
|
|
235
|
-
set({ orientation });
|
|
236
|
-
}}
|
|
237
|
-
value={options.orientation}
|
|
238
|
-
/>
|
|
239
286
|
<Choice
|
|
240
287
|
label="Quality"
|
|
241
288
|
labels={QUALITIES}
|
|
@@ -245,11 +292,11 @@ const Options = ({
|
|
|
245
292
|
}}
|
|
246
293
|
value={options.quality}
|
|
247
294
|
/>
|
|
248
|
-
|
|
295
|
+
</>
|
|
249
296
|
);
|
|
250
297
|
};
|
|
251
298
|
|
|
252
|
-
/** A select of the values a printer offers
|
|
299
|
+
/** A labeled select of the values a printer offers; none if it offers none. */
|
|
253
300
|
const Choice = <V extends number>({
|
|
254
301
|
label,
|
|
255
302
|
labels,
|
|
@@ -264,43 +311,136 @@ const Choice = <V extends number>({
|
|
|
264
311
|
value: V | undefined;
|
|
265
312
|
}) =>
|
|
266
313
|
offered.length > 0 && (
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
314
|
+
<Field label={label}>
|
|
315
|
+
<Select
|
|
316
|
+
onValueChange={(picked) => {
|
|
317
|
+
onChange(picked ?? undefined);
|
|
318
|
+
}}
|
|
319
|
+
options={offered.map((offer) => ({
|
|
320
|
+
label: labels[offer],
|
|
321
|
+
value: offer,
|
|
322
|
+
}))}
|
|
323
|
+
placeholder="Default"
|
|
324
|
+
value={value ?? null}
|
|
325
|
+
/>
|
|
326
|
+
</Field>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The file picker for a PDF, over `screen`, starting in the home with the
|
|
331
|
+
* document's title as its name. Canceling goes back to the options.
|
|
332
|
+
*/
|
|
333
|
+
const PdfPicker = ({
|
|
334
|
+
answer,
|
|
335
|
+
back,
|
|
336
|
+
body,
|
|
337
|
+
list,
|
|
338
|
+
options,
|
|
339
|
+
screen,
|
|
340
|
+
}: {
|
|
341
|
+
answer: (answer: PortalAnswer) => void;
|
|
342
|
+
back: () => void;
|
|
343
|
+
body: PrintBody;
|
|
344
|
+
list: (path: string) => Promise<readonly string[]>;
|
|
345
|
+
options: PrintOptions;
|
|
346
|
+
screen: string | undefined;
|
|
347
|
+
}) => {
|
|
348
|
+
const region = useScreenRegion(screen);
|
|
349
|
+
return (
|
|
350
|
+
<div className={regionStyles} style={region}>
|
|
351
|
+
<FilePicker
|
|
352
|
+
ref={focusOnAttach}
|
|
353
|
+
request={{
|
|
354
|
+
accept: ["pdf"],
|
|
355
|
+
acceptLabel: "Save",
|
|
356
|
+
cancel: back,
|
|
357
|
+
choose: ([path]) => {
|
|
358
|
+
if (path === undefined) {
|
|
359
|
+
throw new Error("a save chose no file");
|
|
360
|
+
} else {
|
|
361
|
+
answer(Answer.SaveToPdf(path, options));
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
home: body.home,
|
|
365
|
+
list,
|
|
366
|
+
mode: ChooserMode.Save,
|
|
367
|
+
suggestedName: pdfName(body.title),
|
|
368
|
+
title: "Save as PDF",
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
276
372
|
);
|
|
373
|
+
};
|
|
277
374
|
|
|
278
375
|
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
279
376
|
|
|
280
|
-
const
|
|
377
|
+
const formStyles = grid({
|
|
378
|
+
columnGap: 3,
|
|
379
|
+
columns: 2,
|
|
380
|
+
marginBlockStart: 4,
|
|
381
|
+
rowGap: 4,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const wideStyles = css({ gridColumn: "1 / -1" });
|
|
385
|
+
|
|
386
|
+
const destinationStyles = css({
|
|
387
|
+
alignItems: "center",
|
|
388
|
+
display: "inline-flex",
|
|
389
|
+
gap: 2,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const pagesStyles = grid({
|
|
393
|
+
alignItems: "end",
|
|
394
|
+
columnGap: 3,
|
|
395
|
+
columns: 2,
|
|
396
|
+
gridColumn: "1 / -1",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const regionStyles = css({
|
|
400
|
+
inset: 0,
|
|
401
|
+
position: "fixed",
|
|
402
|
+
});
|
|
281
403
|
|
|
282
|
-
|
|
404
|
+
/** `printer` for people. */
|
|
405
|
+
const nameOf = (printer: Printer): string =>
|
|
406
|
+
printer.description ?? printer.name;
|
|
283
407
|
|
|
284
|
-
/**
|
|
285
|
-
const
|
|
286
|
-
|
|
408
|
+
/** Where the dialog starts: the application's printer, else the first, else a PDF. */
|
|
409
|
+
const startingChoice = (body: PrintBody): Chosen =>
|
|
410
|
+
startOf(
|
|
287
411
|
body.printers.find((listed) => listed.name === body.printer) ??
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
412
|
+
body.printers[0] ??
|
|
413
|
+
body.pdf,
|
|
414
|
+
);
|
|
291
415
|
|
|
292
416
|
/** `printer` with its initial options. */
|
|
293
417
|
const startOf = (printer: Printer): Chosen => ({
|
|
294
418
|
copies: String(printer.initial.copies),
|
|
295
419
|
options: printer.initial,
|
|
296
|
-
|
|
420
|
+
pageChoice:
|
|
421
|
+
printer.initial.pages.length === 0 ? PageChoice.All : PageChoice.Custom,
|
|
422
|
+
pages: printer.initial.pages
|
|
423
|
+
.map(({ first, last }) =>
|
|
424
|
+
first === last ? `${first}` : `${first}-${last}`,
|
|
425
|
+
)
|
|
426
|
+
.join(", "),
|
|
297
427
|
printer,
|
|
298
428
|
});
|
|
299
429
|
|
|
430
|
+
/** The pages chosen, or `undefined` while custom pages name none it reads. */
|
|
431
|
+
const pagesOf = (chosen: Chosen): readonly PageRange[] | undefined => {
|
|
432
|
+
switch (chosen.pageChoice) {
|
|
433
|
+
case PageChoice.All:
|
|
434
|
+
return [];
|
|
435
|
+
case PageChoice.Custom:
|
|
436
|
+
return chosen.pages.trim() === "" ? undefined : pageRanges(chosen.pages);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
300
440
|
/** The options to print with, or `undefined` while copies or pages do not read. */
|
|
301
441
|
const readOptions = (chosen: Chosen): PrintOptions | undefined => {
|
|
302
442
|
const copies = Number(chosen.copies);
|
|
303
|
-
const pages =
|
|
443
|
+
const pages = pagesOf(chosen);
|
|
304
444
|
const copiesRead =
|
|
305
445
|
Number.isInteger(copies) &&
|
|
306
446
|
copies >= 1 &&
|
|
@@ -309,3 +449,7 @@ const readOptions = (chosen: Chosen): PrintOptions | undefined => {
|
|
|
309
449
|
? { ...chosen.options, copies, pages }
|
|
310
450
|
: undefined;
|
|
311
451
|
};
|
|
452
|
+
|
|
453
|
+
/** A file name for a PDF of `title`: no slashes, and `Document` when blank. */
|
|
454
|
+
const pdfName = (title: string): string =>
|
|
455
|
+
`${title.trim() === "" ? "Document" : title.replaceAll("/", "-")}.pdf`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Radio } from "@base-ui/react/radio";
|
|
2
|
+
import { RadioGroup } from "@base-ui/react/radio-group";
|
|
1
3
|
import type { DomicileBrowserWindow } from "@domicile-desktop/sdk/domicile-host";
|
|
2
4
|
import type {
|
|
3
5
|
FrozenDesk,
|
|
@@ -7,18 +9,16 @@ import type {
|
|
|
7
9
|
import { PortalAnswer as Answer } from "@domicile-desktop/sdk/portal";
|
|
8
10
|
import { GlobeIcon } from "@phosphor-icons/react/dist/ssr/Globe";
|
|
9
11
|
import { MonitorIcon } from "@phosphor-icons/react/dist/ssr/Monitor";
|
|
10
|
-
import type { PointerEvent, ReactNode } from "react";
|
|
12
|
+
import type { CSSProperties, PointerEvent, ReactNode } from "react";
|
|
11
13
|
import { useEffect, useState } from "react";
|
|
12
14
|
import { css } from "../../styled-system/css";
|
|
13
|
-
import { flex } from "../../styled-system/patterns";
|
|
15
|
+
import { center, flex } from "../../styled-system/patterns";
|
|
14
16
|
import { Button } from "../Button/Button";
|
|
15
17
|
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
16
18
|
import type { App } from "../useApps/useApps";
|
|
17
19
|
import type { FramePoint } from "./frame-point";
|
|
18
20
|
import { draggedArea, framePoint } from "./frame-point";
|
|
19
21
|
import { onFrame } from "./on-frame";
|
|
20
|
-
import type { SourceTile } from "./SourceTiles";
|
|
21
|
-
import { SourceTiles } from "./SourceTiles";
|
|
22
22
|
import type { ShownWindow } from "./shown-windows";
|
|
23
23
|
import { ShownWindowKind } from "./shown-windows";
|
|
24
24
|
import { WindowIcon } from "./WindowIcon";
|
|
@@ -69,15 +69,24 @@ const Pick = {
|
|
|
69
69
|
|
|
70
70
|
type Pick = ReturnType<(typeof Pick)[keyof typeof Pick]>;
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Something the dialog offers to save, and how its row shows it: `name`
|
|
74
|
+
* over `detail`. `label` names it to a screen reader.
|
|
75
|
+
*/
|
|
76
|
+
type Choice = {
|
|
77
|
+
detail: string | undefined;
|
|
78
|
+
icon: ReactNode;
|
|
79
|
+
label: string;
|
|
80
|
+
name: string;
|
|
81
|
+
pick: Pick;
|
|
82
|
+
};
|
|
74
83
|
|
|
75
84
|
/**
|
|
76
|
-
* Picks what to save of a frozen desk: all of it, a screen
|
|
77
|
-
* a
|
|
78
|
-
*
|
|
79
|
-
* over it, such as a notification, is left out. Ctrl+C copies the
|
|
80
|
-
* the clipboard instead of saving it. Dismissing it cancels.
|
|
85
|
+
* Picks what to save of a frozen desk: all of it, a screen or a window from
|
|
86
|
+
* a list beside the desk, or an area dragged over the desk. The desk outlines
|
|
87
|
+
* the pick, with its size. A window is saved from its own frame, so what is
|
|
88
|
+
* over it, such as a notification, is left out. Copy, or Ctrl+C, copies the
|
|
89
|
+
* pick to the clipboard instead of saving it. Dismissing it cancels.
|
|
81
90
|
*/
|
|
82
91
|
export const ScreenshotDialog = ({
|
|
83
92
|
answer,
|
|
@@ -92,7 +101,9 @@ export const ScreenshotDialog = ({
|
|
|
92
101
|
const [shown] = useState(shownWindows);
|
|
93
102
|
const screens: Choice[] = body.monitors.map(
|
|
94
103
|
({ area, description, name }) => ({
|
|
104
|
+
detail: description === "" ? undefined : name,
|
|
95
105
|
icon: <MonitorIcon aria-hidden />,
|
|
106
|
+
label: description === "" ? name : description,
|
|
96
107
|
name: description === "" ? name : description,
|
|
97
108
|
pick: Pick.Area(area),
|
|
98
109
|
}),
|
|
@@ -108,7 +119,9 @@ export const ScreenshotDialog = ({
|
|
|
108
119
|
...shown.flatMap((window) => browserChoices(body, browserWindows, window)),
|
|
109
120
|
];
|
|
110
121
|
const whole: Choice = {
|
|
122
|
+
detail: `${String(body.width)} × ${String(body.height)}`,
|
|
111
123
|
icon: <MonitorIcon aria-hidden />,
|
|
124
|
+
label: "Whole desk",
|
|
112
125
|
name: "Whole desk",
|
|
113
126
|
pick: Pick.Area({ height: body.height, width: body.width, x: 0, y: 0 }),
|
|
114
127
|
};
|
|
@@ -133,32 +146,6 @@ export const ScreenshotDialog = ({
|
|
|
133
146
|
document.removeEventListener("keydown", copy);
|
|
134
147
|
};
|
|
135
148
|
}, [answer, pick]);
|
|
136
|
-
// A choice's tile, keyed by its index: names repeat, as two untitled
|
|
137
|
-
// windows do, and the order is fixed.
|
|
138
|
-
const group = (label: string, members: readonly Choice[], first: number) =>
|
|
139
|
-
members.length > 0 && (
|
|
140
|
-
<SourceTiles
|
|
141
|
-
desk={body}
|
|
142
|
-
label={label}
|
|
143
|
-
multiple={false}
|
|
144
|
-
onValueChange={([value]) => {
|
|
145
|
-
// Picking the picked tile again keeps it.
|
|
146
|
-
if (value !== undefined) {
|
|
147
|
-
setPicked(Number(value));
|
|
148
|
-
}
|
|
149
|
-
}}
|
|
150
|
-
tiles={members.map((choice, offset) =>
|
|
151
|
-
tileOf(choice, String(first + offset)),
|
|
152
|
-
)}
|
|
153
|
-
value={
|
|
154
|
-
typeof picked === "number" &&
|
|
155
|
-
picked >= first &&
|
|
156
|
-
picked < first + members.length
|
|
157
|
-
? [String(picked)]
|
|
158
|
-
: []
|
|
159
|
-
}
|
|
160
|
-
/>
|
|
161
|
-
);
|
|
162
149
|
const pointAt = (event: PointerEvent<HTMLDivElement>) =>
|
|
163
150
|
framePoint(
|
|
164
151
|
{ x: event.clientX, y: event.clientY },
|
|
@@ -178,6 +165,14 @@ export const ScreenshotDialog = ({
|
|
|
178
165
|
>
|
|
179
166
|
Cancel
|
|
180
167
|
</Button>
|
|
168
|
+
<Button
|
|
169
|
+
onClick={() => {
|
|
170
|
+
answer(answerOf(pick, true));
|
|
171
|
+
}}
|
|
172
|
+
variant="outline"
|
|
173
|
+
>
|
|
174
|
+
Copy
|
|
175
|
+
</Button>
|
|
181
176
|
<Button
|
|
182
177
|
onClick={() => {
|
|
183
178
|
answer(answerOf(pick, false));
|
|
@@ -198,57 +193,209 @@ export const ScreenshotDialog = ({
|
|
|
198
193
|
title="Take a screenshot"
|
|
199
194
|
>
|
|
200
195
|
{asker !== undefined && <p className={askerStyles}>{`${asker} asks`}</p>}
|
|
201
|
-
<div className={
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
196
|
+
<div className={layoutStyles}>
|
|
197
|
+
{/* `null` is Base UI's "no radio checked", for an area dragged. */}
|
|
198
|
+
<RadioGroup<number | null>
|
|
199
|
+
aria-label="What to save"
|
|
200
|
+
className={listStyles}
|
|
201
|
+
onValueChange={(value) => {
|
|
202
|
+
if (value === null) {
|
|
203
|
+
throw new Error("a radio unchecked the screenshot choices");
|
|
204
|
+
} else {
|
|
205
|
+
setPicked(value);
|
|
206
|
+
}
|
|
207
|
+
}}
|
|
208
|
+
value={typeof picked === "number" ? picked : null}
|
|
209
|
+
>
|
|
210
|
+
<ChoiceRow choice={whole} index={0} />
|
|
211
|
+
<ChoiceSection choices={screens} first={1} label="Screens" />
|
|
212
|
+
<ChoiceSection
|
|
213
|
+
choices={windows}
|
|
214
|
+
first={1 + screens.length}
|
|
215
|
+
label="Windows"
|
|
216
|
+
/>
|
|
217
|
+
</RadioGroup>
|
|
218
|
+
<figure className={stageStyles}>
|
|
219
|
+
<div
|
|
220
|
+
aria-label="Drag to pick an area"
|
|
221
|
+
className={deskStyles}
|
|
222
|
+
onPointerDown={(event) => {
|
|
223
|
+
const from = pointAt(event);
|
|
224
|
+
setDragFrom(from);
|
|
225
|
+
setPicked(draggedArea(from, from));
|
|
226
|
+
}}
|
|
227
|
+
onPointerMove={(event) => {
|
|
228
|
+
if (dragFrom !== undefined) {
|
|
229
|
+
setPicked(draggedArea(dragFrom, pointAt(event)));
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
onPointerUp={() => {
|
|
233
|
+
setDragFrom(undefined);
|
|
234
|
+
}}
|
|
235
|
+
role="group"
|
|
236
|
+
style={fitted(body)}
|
|
237
|
+
>
|
|
238
|
+
<img
|
|
239
|
+
alt="The desk"
|
|
240
|
+
className={frameStyles}
|
|
241
|
+
draggable={false}
|
|
242
|
+
src={body.frame}
|
|
243
|
+
/>
|
|
244
|
+
{pick.area !== undefined && (
|
|
245
|
+
<div className={pickedStyles} style={placed(pick.area, body)}>
|
|
246
|
+
<span className={sizeStyles}>
|
|
247
|
+
{`${String(pick.area.width)} × ${String(pick.area.height)}`}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<figcaption className={captionStyles}>
|
|
253
|
+
{pick.kind === PickKind.Window && pick.area === undefined
|
|
254
|
+
? "Not on screen: saved as it last drew itself."
|
|
255
|
+
: "Drag over the desk to pick an area."}
|
|
256
|
+
</figcaption>
|
|
257
|
+
</figure>
|
|
238
258
|
</div>
|
|
239
259
|
</ModalDialog>
|
|
240
260
|
);
|
|
241
261
|
};
|
|
242
262
|
|
|
263
|
+
/**
|
|
264
|
+
* A labeled run of rows, numbered from `first` in the dialog's choices, or
|
|
265
|
+
* nothing when there are none.
|
|
266
|
+
*/
|
|
267
|
+
const ChoiceSection = ({
|
|
268
|
+
choices,
|
|
269
|
+
first,
|
|
270
|
+
label,
|
|
271
|
+
}: {
|
|
272
|
+
choices: readonly Choice[];
|
|
273
|
+
first: number;
|
|
274
|
+
label: string;
|
|
275
|
+
}) =>
|
|
276
|
+
choices.length > 0 && (
|
|
277
|
+
<div aria-label={label} className={sectionStyles} role="group">
|
|
278
|
+
<span aria-hidden className={sectionLabelStyles}>
|
|
279
|
+
{label}
|
|
280
|
+
</span>
|
|
281
|
+
{choices.map((choice, offset) => (
|
|
282
|
+
// Keyed by index: names repeat, as two untitled windows do, and the
|
|
283
|
+
// order is fixed.
|
|
284
|
+
<ChoiceRow
|
|
285
|
+
choice={choice}
|
|
286
|
+
index={first + offset}
|
|
287
|
+
key={first + offset}
|
|
288
|
+
/>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
/** `choice`'s row, picking it as the dialog's choice `index`. */
|
|
294
|
+
const ChoiceRow = ({ choice, index }: { choice: Choice; index: number }) => (
|
|
295
|
+
<Radio.Root aria-label={choice.label} className={rowStyles} value={index}>
|
|
296
|
+
{choice.icon}
|
|
297
|
+
<span className={rowTextStyles}>
|
|
298
|
+
<span className={rowNameStyles}>{choice.name}</span>
|
|
299
|
+
{choice.detail !== undefined && (
|
|
300
|
+
<span className={rowDetailStyles}>{choice.detail}</span>
|
|
301
|
+
)}
|
|
302
|
+
</span>
|
|
303
|
+
</Radio.Root>
|
|
304
|
+
);
|
|
305
|
+
|
|
243
306
|
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
244
307
|
|
|
245
|
-
const
|
|
246
|
-
|
|
308
|
+
const layoutStyles = css({
|
|
309
|
+
alignItems: "stretch",
|
|
310
|
+
display: "grid",
|
|
247
311
|
gap: 4,
|
|
248
|
-
|
|
312
|
+
gridTemplateColumns: "{spacing.64} minmax(0, 1fr)",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// As tall as the desk beside it, scrolling when the rows run longer: a zero
|
|
316
|
+
// height keeps the rows out of the grid row's height.
|
|
317
|
+
const listStyles = flex({
|
|
318
|
+
blockSize: 0,
|
|
319
|
+
direction: "column",
|
|
320
|
+
gap: 3,
|
|
321
|
+
minBlockSize: "full",
|
|
322
|
+
overflowY: "auto",
|
|
323
|
+
// Room for the focus ring, which the scrolling would clip.
|
|
324
|
+
padding: 1,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const sectionStyles = flex({ direction: "column", gap: 0.5 });
|
|
328
|
+
|
|
329
|
+
const sectionLabelStyles = css({
|
|
330
|
+
color: "muted",
|
|
331
|
+
fontSize: "xs",
|
|
332
|
+
fontWeight: "medium",
|
|
333
|
+
letterSpacing: "wide",
|
|
334
|
+
paddingBlockEnd: 1,
|
|
335
|
+
paddingInline: 2.5,
|
|
336
|
+
textTransform: "uppercase",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const rowStyles = css({
|
|
340
|
+
_checked: {
|
|
341
|
+
backgroundColor: "color-mix(in oklab, {colors.accent} 20%, transparent)",
|
|
342
|
+
borderColor: "accent",
|
|
343
|
+
},
|
|
344
|
+
_hover: {
|
|
345
|
+
backgroundColor: "color-mix(in oklab, {colors.foreground} 6%, transparent)",
|
|
346
|
+
},
|
|
347
|
+
"& > :is(svg, img)": {
|
|
348
|
+
blockSize: 5,
|
|
349
|
+
color: "muted",
|
|
350
|
+
flexShrink: 0,
|
|
351
|
+
inlineSize: 5,
|
|
352
|
+
},
|
|
353
|
+
alignItems: "center",
|
|
354
|
+
border: "1px solid transparent",
|
|
355
|
+
borderRadius: "md",
|
|
356
|
+
color: "foreground",
|
|
357
|
+
cursor: "pointer",
|
|
358
|
+
display: "flex",
|
|
359
|
+
flexShrink: 0,
|
|
360
|
+
gap: 2.5,
|
|
361
|
+
minInlineSize: 0,
|
|
362
|
+
paddingBlock: 1.5,
|
|
363
|
+
paddingInline: 2.5,
|
|
364
|
+
transition: "background-color {durations.fast} {easings.out}",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const rowTextStyles = flex({ direction: "column", minInlineSize: 0 });
|
|
368
|
+
|
|
369
|
+
const rowNameStyles = css({
|
|
370
|
+
fontSize: "sm",
|
|
371
|
+
overflow: "hidden",
|
|
372
|
+
textOverflow: "ellipsis",
|
|
373
|
+
whiteSpace: "nowrap",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const rowDetailStyles = css({
|
|
377
|
+
color: "muted",
|
|
378
|
+
fontSize: "xs",
|
|
379
|
+
overflow: "hidden",
|
|
380
|
+
textOverflow: "ellipsis",
|
|
381
|
+
whiteSpace: "nowrap",
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const stageStyles = flex({
|
|
385
|
+
align: "center",
|
|
386
|
+
backgroundColor: "background",
|
|
387
|
+
border: "1px solid {colors.border}",
|
|
388
|
+
borderRadius: "lg",
|
|
389
|
+
direction: "column",
|
|
390
|
+
gap: 3,
|
|
391
|
+
justify: "center",
|
|
392
|
+
margin: 0,
|
|
393
|
+
minBlockSize: 72,
|
|
394
|
+
padding: 4,
|
|
249
395
|
});
|
|
250
396
|
|
|
251
397
|
const deskStyles = css({
|
|
398
|
+
borderRadius: "sm",
|
|
252
399
|
cursor: "crosshair",
|
|
253
400
|
// Keeps the shade around the picked area on the frame.
|
|
254
401
|
overflow: "hidden",
|
|
@@ -257,17 +404,47 @@ const deskStyles = css({
|
|
|
257
404
|
userSelect: "none",
|
|
258
405
|
});
|
|
259
406
|
|
|
260
|
-
const frameStyles = css({
|
|
407
|
+
const frameStyles = css({
|
|
408
|
+
blockSize: "full",
|
|
409
|
+
display: "block",
|
|
410
|
+
inlineSize: "full",
|
|
411
|
+
});
|
|
261
412
|
|
|
262
413
|
const pickedStyles = css({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
borderWidth: "2px",
|
|
266
|
-
boxShadow: "0 0 0 9999px rgb(0 0 0 / 0.45)",
|
|
414
|
+
border: "2px solid {colors.accent}",
|
|
415
|
+
boxShadow: "0 0 0 100vmax {colors.backdrop}",
|
|
267
416
|
pointerEvents: "none",
|
|
268
417
|
position: "absolute",
|
|
269
418
|
});
|
|
270
419
|
|
|
420
|
+
const sizeStyles = center({
|
|
421
|
+
backgroundColor: "accent",
|
|
422
|
+
borderEndEndRadius: "sm",
|
|
423
|
+
color: "background",
|
|
424
|
+
fontSize: "xs",
|
|
425
|
+
fontVariantNumeric: "tabular-nums",
|
|
426
|
+
fontWeight: "medium",
|
|
427
|
+
insetBlockStart: 0,
|
|
428
|
+
insetInlineStart: 0,
|
|
429
|
+
paddingBlock: 0.5,
|
|
430
|
+
paddingInline: 1.5,
|
|
431
|
+
position: "absolute",
|
|
432
|
+
whiteSpace: "nowrap",
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const captionStyles = css({ color: "muted", fontSize: "xs" });
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The desk's box in the stage: as wide as the stage allows, at the frame's
|
|
439
|
+
* aspect ratio, no taller than 56% of the screen so the dialog fits on it.
|
|
440
|
+
*/
|
|
441
|
+
const fitted = (frame: { width: number; height: number }): CSSProperties => {
|
|
442
|
+
const ratio = `${String(frame.width)} / ${String(frame.height)}`;
|
|
443
|
+
return {
|
|
444
|
+
aspectRatio: ratio,
|
|
445
|
+
inlineSize: `min(100%, calc(56cqh * ${ratio}))`,
|
|
446
|
+
};
|
|
447
|
+
};
|
|
271
448
|
/** `area`'s place over the drawn frame, in percent of `frame`. */
|
|
272
449
|
const placed = (
|
|
273
450
|
area: ShotRect,
|
|
@@ -279,14 +456,6 @@ const placed = (
|
|
|
279
456
|
width: `${String((area.width / frame.width) * 100)}%`,
|
|
280
457
|
});
|
|
281
458
|
|
|
282
|
-
/** `choice`'s tile under `key`, previewing what it saves. */
|
|
283
|
-
const tileOf = (choice: Choice, key: string): SourceTile => ({
|
|
284
|
-
area: choice.pick.area,
|
|
285
|
-
icon: choice.icon,
|
|
286
|
-
key,
|
|
287
|
-
name: choice.name,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
459
|
/** The name of a window titled `title`, which may be empty. */
|
|
291
460
|
const titled = (title: string): string =>
|
|
292
461
|
title === "" ? "Untitled window" : title;
|
|
@@ -346,8 +515,10 @@ const browserChoices = (
|
|
|
346
515
|
? []
|
|
347
516
|
: [
|
|
348
517
|
{
|
|
518
|
+
detail: "Browser",
|
|
349
519
|
icon: <GlobeIcon aria-hidden />,
|
|
350
|
-
|
|
520
|
+
label: `Browser: ${titled(browser.title)}`,
|
|
521
|
+
name: titled(browser.title),
|
|
351
522
|
pick: Pick.BrowserWindow(browser.id, area),
|
|
352
523
|
},
|
|
353
524
|
];
|
|
@@ -361,7 +532,9 @@ const windowChoice = (
|
|
|
361
532
|
title: string,
|
|
362
533
|
app: App | undefined,
|
|
363
534
|
): Choice => ({
|
|
535
|
+
detail: app?.name,
|
|
364
536
|
icon: <WindowIcon app={app} />,
|
|
365
|
-
|
|
537
|
+
label: app === undefined ? title : `${app.name}: ${title}`,
|
|
538
|
+
name: title,
|
|
366
539
|
pick,
|
|
367
540
|
});
|
|
@@ -29,7 +29,7 @@ type Props = {
|
|
|
29
29
|
/**
|
|
30
30
|
* A labeled grid of sources to pick, each a tile previewing it from the
|
|
31
31
|
* frozen desk, or showing its icon large without a preview, over its icon
|
|
32
|
-
* and name
|
|
32
|
+
* and name, for the screen cast picker.
|
|
33
33
|
*/
|
|
34
34
|
export const SourceTiles = ({
|
|
35
35
|
desk,
|