@domicile-desktop/component-library 0.0.0-alpha-183ca460bbae → 0.0.0-alpha-c4fe91522852
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-c4fe91522852",
|
|
6
|
+
"@domicile-desktop/system-apps": "0.0.0-alpha-c4fe91522852",
|
|
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-c4fe91522852"
|
|
118
118
|
}
|
|
@@ -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,
|