@domicile-desktop/component-library 0.0.0-alpha-59f4d795d464 → 0.0.0-alpha-9fd298a6794f
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 +3 -3
- package/src/PortalDialogs/PortalDialogs.tsx +2 -0
- package/src/PortalDialogs/ScreenCastDialog.tsx +124 -117
- package/src/PortalDialogs/ScreenshotDialog.tsx +46 -66
- package/src/PortalDialogs/SourceTiles.tsx +173 -0
- package/src/PortalDialogs/WindowIcon.tsx +10 -0
- package/src/PortalDialogs/source-name.ts +2 -0
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-9fd298a6794f",
|
|
6
|
+
"@domicile-desktop/system-apps": "0.0.0-alpha-9fd298a6794f",
|
|
7
7
|
"@pandacss/dev": "^1.12.1",
|
|
8
8
|
"@phosphor-icons/react": "^2.1.10"
|
|
9
9
|
},
|
|
@@ -110,5 +110,5 @@
|
|
|
110
110
|
"test:unit": "bun test"
|
|
111
111
|
},
|
|
112
112
|
"type": "module",
|
|
113
|
-
"version": "0.0.0-alpha-
|
|
113
|
+
"version": "0.0.0-alpha-9fd298a6794f"
|
|
114
114
|
}
|
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ToggleGroup } from "@base-ui/react/toggle-group";
|
|
1
|
+
import type { DomicileBrowserWindow } from "@domicile-desktop/sdk/domicile-host";
|
|
3
2
|
import type {
|
|
4
3
|
CastSource,
|
|
4
|
+
FrozenDesk,
|
|
5
5
|
PortalAnswer,
|
|
6
6
|
ScreenCastBody,
|
|
7
|
+
ShotRect,
|
|
7
8
|
} from "@domicile-desktop/sdk/portal";
|
|
8
9
|
import {
|
|
9
10
|
PortalAnswer as Answer,
|
|
10
11
|
CastSourceKind,
|
|
11
12
|
CastSource as Source,
|
|
12
13
|
} from "@domicile-desktop/sdk/portal";
|
|
13
|
-
import {
|
|
14
|
+
import { GlobeIcon } from "@phosphor-icons/react/dist/ssr/Globe";
|
|
14
15
|
import { MonitorIcon } from "@phosphor-icons/react/dist/ssr/Monitor";
|
|
15
16
|
import { useState } from "react";
|
|
16
17
|
import { css } from "../../styled-system/css";
|
|
17
18
|
import { Button } from "../Button/Button";
|
|
18
19
|
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
19
|
-
import { Tabs } from "../Tabs/Tabs";
|
|
20
20
|
import type { App } from "../useApps/useApps";
|
|
21
|
+
import { onFrame } from "./on-frame";
|
|
21
22
|
import { RegionPicker } from "./RegionPicker";
|
|
23
|
+
import type { SourceTile } from "./SourceTiles";
|
|
24
|
+
import { SourceTiles } from "./SourceTiles";
|
|
25
|
+
import type { ShownWindow } from "./shown-windows";
|
|
26
|
+
import { ShownWindowKind } from "./shown-windows";
|
|
27
|
+
import { WindowIcon } from "./WindowIcon";
|
|
22
28
|
|
|
23
29
|
type Props = {
|
|
24
30
|
answer: (answer: PortalAnswer) => void;
|
|
@@ -27,13 +33,18 @@ type Props = {
|
|
|
27
33
|
/** Who asks, as the dialog names them. */
|
|
28
34
|
asker: string;
|
|
29
35
|
body: ScreenCastBody;
|
|
36
|
+
/** The desktop's browser windows, as the engine lists them. */
|
|
37
|
+
browserWindows: readonly DomicileBrowserWindow[];
|
|
30
38
|
screen: string | undefined;
|
|
39
|
+
/** The windows the shell draws on screen, read once as the dialog opens. */
|
|
40
|
+
shownWindows: () => readonly ShownWindow[];
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
/**
|
|
34
|
-
* A source picker: the windows and screens an application
|
|
35
|
-
*
|
|
36
|
-
* asks for one.
|
|
44
|
+
* A source picker: the windows, browser windows and screens an application
|
|
45
|
+
* may record, each a tile previewing it on the desk, and a region drawn on
|
|
46
|
+
* the desk when it asks for one. A browser window is its page alone, from
|
|
47
|
+
* the engine. Picks one, or several when the application asks for more.
|
|
37
48
|
* Dismissing it cancels.
|
|
38
49
|
*/
|
|
39
50
|
export const ScreenCastDialog = ({
|
|
@@ -41,52 +52,49 @@ export const ScreenCastDialog = ({
|
|
|
41
52
|
apps,
|
|
42
53
|
asker,
|
|
43
54
|
body,
|
|
55
|
+
browserWindows,
|
|
44
56
|
screen,
|
|
57
|
+
shownWindows,
|
|
45
58
|
}: Props) => {
|
|
59
|
+
// Read once, as the desk was shot; a later move would not match it.
|
|
60
|
+
const [shown] = useState(shownWindows);
|
|
46
61
|
const [picked, setPicked] = useState<readonly string[]>([]);
|
|
47
62
|
const [drawing, setDrawing] = useState(false);
|
|
48
63
|
const windows = body.sources.filter(
|
|
49
64
|
(source) => source.kind === CastSourceKind.Window,
|
|
50
65
|
);
|
|
66
|
+
const browsers = body.browserWindows
|
|
67
|
+
? browserWindows.map(({ id, title }) => Source.BrowserWindow({ id, title }))
|
|
68
|
+
: [];
|
|
51
69
|
const monitors = body.sources.filter(
|
|
52
70
|
(source) => source.kind === CastSourceKind.Monitor,
|
|
53
71
|
);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
const offered = [...windows, ...browsers, ...monitors];
|
|
73
|
+
const group = (label: string, sources: readonly CastSource[]) =>
|
|
74
|
+
sources.length > 0 && (
|
|
75
|
+
<SourceTiles
|
|
76
|
+
desk={body.desk}
|
|
77
|
+
label={label}
|
|
78
|
+
multiple={body.multiple}
|
|
79
|
+
onValueChange={(value) => {
|
|
80
|
+
setPicked(
|
|
81
|
+
body.multiple
|
|
82
|
+
? [
|
|
83
|
+
...picked.filter(
|
|
84
|
+
(key) =>
|
|
85
|
+
!sources.some((source) => sourceKey(source) === key),
|
|
86
|
+
),
|
|
87
|
+
...value,
|
|
88
|
+
]
|
|
89
|
+
: value,
|
|
90
|
+
);
|
|
91
|
+
}}
|
|
92
|
+
tiles={sources.map((source) => tileOf(source, apps, body.desk, shown))}
|
|
93
|
+
value={picked.filter((key) =>
|
|
94
|
+
sources.some((source) => sourceKey(source) === key),
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
64
97
|
);
|
|
65
|
-
};
|
|
66
|
-
const list = (group: readonly CastSource[]) => (
|
|
67
|
-
<ToggleGroup
|
|
68
|
-
className={listStyles}
|
|
69
|
-
multiple={body.multiple}
|
|
70
|
-
onValueChange={(value) => {
|
|
71
|
-
pick(group, value);
|
|
72
|
-
}}
|
|
73
|
-
orientation="vertical"
|
|
74
|
-
value={picked.filter((key) =>
|
|
75
|
-
group.some((source) => sourceKey(source) === key),
|
|
76
|
-
)}
|
|
77
|
-
>
|
|
78
|
-
{group.map((source) => (
|
|
79
|
-
<Toggle
|
|
80
|
-
className={rowStyles}
|
|
81
|
-
key={sourceKey(source)}
|
|
82
|
-
value={sourceKey(source)}
|
|
83
|
-
>
|
|
84
|
-
<SourceIcon app={appOf(apps, source)} source={source} />
|
|
85
|
-
<span>{sourceName(source, appOf(apps, source))}</span>
|
|
86
|
-
</Toggle>
|
|
87
|
-
))}
|
|
88
|
-
</ToggleGroup>
|
|
89
|
-
);
|
|
90
98
|
return drawing ? (
|
|
91
99
|
<RegionPicker
|
|
92
100
|
cancel={() => {
|
|
@@ -124,7 +132,7 @@ export const ScreenCastDialog = ({
|
|
|
124
132
|
onClick={() => {
|
|
125
133
|
answer(
|
|
126
134
|
Answer.ScreenCast(
|
|
127
|
-
|
|
135
|
+
offered.filter((source) =>
|
|
128
136
|
picked.includes(sourceKey(source)),
|
|
129
137
|
),
|
|
130
138
|
),
|
|
@@ -142,78 +150,34 @@ export const ScreenCastDialog = ({
|
|
|
142
150
|
}}
|
|
143
151
|
open
|
|
144
152
|
screen={screen}
|
|
145
|
-
|
|
153
|
+
size="xl"
|
|
154
|
+
title={title(
|
|
155
|
+
body.multiple,
|
|
156
|
+
windows.length + browsers.length > 0,
|
|
157
|
+
monitors.length > 0,
|
|
158
|
+
)}
|
|
146
159
|
>
|
|
147
160
|
<p className={askerStyles}>{`${asker} wants to record`}</p>
|
|
148
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{ content: list(monitors), label: "Screens", value: "screens" },
|
|
154
|
-
]}
|
|
155
|
-
/>
|
|
156
|
-
) : (
|
|
157
|
-
list(body.sources)
|
|
158
|
-
)}
|
|
161
|
+
<div className={groupsStyles}>
|
|
162
|
+
{group("Windows", windows)}
|
|
163
|
+
{group("Browser windows", browsers)}
|
|
164
|
+
{group("Screens", monitors)}
|
|
165
|
+
</div>
|
|
159
166
|
</ModalDialog>
|
|
160
167
|
);
|
|
161
168
|
};
|
|
162
169
|
|
|
163
|
-
/** A window's application icon, a window outline without one, or a screen. */
|
|
164
|
-
const SourceIcon = ({
|
|
165
|
-
app,
|
|
166
|
-
source,
|
|
167
|
-
}: {
|
|
168
|
-
app: App | undefined;
|
|
169
|
-
source: CastSource;
|
|
170
|
-
}) => {
|
|
171
|
-
switch (source.kind) {
|
|
172
|
-
case CastSourceKind.Window:
|
|
173
|
-
return app?.icon === undefined ? (
|
|
174
|
-
<AppWindowIcon aria-hidden className={iconStyles} />
|
|
175
|
-
) : (
|
|
176
|
-
<img alt="" className={iconStyles} src={app.icon} />
|
|
177
|
-
);
|
|
178
|
-
case CastSourceKind.Monitor:
|
|
179
|
-
case CastSourceKind.Region:
|
|
180
|
-
return <MonitorIcon aria-hidden className={iconStyles} />;
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
|
|
184
170
|
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
185
171
|
|
|
186
|
-
const
|
|
172
|
+
const groupsStyles = css({
|
|
187
173
|
display: "flex",
|
|
188
174
|
flexDirection: "column",
|
|
189
|
-
gap:
|
|
175
|
+
gap: 4,
|
|
190
176
|
marginBlockStart: 3,
|
|
191
|
-
maxBlockSize:
|
|
177
|
+
maxBlockSize: "60vh",
|
|
192
178
|
overflowY: "auto",
|
|
193
179
|
});
|
|
194
180
|
|
|
195
|
-
const rowStyles = css({
|
|
196
|
-
_hover: { backgroundColor: "card" },
|
|
197
|
-
"&[data-pressed]": {
|
|
198
|
-
backgroundColor: "color-mix(in oklab, {colors.accent} 20%, transparent)",
|
|
199
|
-
borderColor: "accent",
|
|
200
|
-
},
|
|
201
|
-
alignItems: "center",
|
|
202
|
-
backgroundColor: "transparent",
|
|
203
|
-
border: "1px solid transparent",
|
|
204
|
-
borderRadius: "md",
|
|
205
|
-
color: "foreground",
|
|
206
|
-
cursor: "pointer",
|
|
207
|
-
display: "flex",
|
|
208
|
-
font: "inherit",
|
|
209
|
-
gap: 3,
|
|
210
|
-
paddingBlock: 2,
|
|
211
|
-
paddingInline: 3,
|
|
212
|
-
textAlign: "start",
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const iconStyles = css({ blockSize: 6, flexShrink: 0, inlineSize: 6 });
|
|
216
|
-
|
|
217
181
|
/** The dialog's title for what it offers. */
|
|
218
182
|
const title = (
|
|
219
183
|
multiple: boolean,
|
|
@@ -234,6 +198,8 @@ const sourceKey = (source: CastSource): string => {
|
|
|
234
198
|
switch (source.kind) {
|
|
235
199
|
case CastSourceKind.Window:
|
|
236
200
|
return `window:${source.id}`;
|
|
201
|
+
case CastSourceKind.BrowserWindow:
|
|
202
|
+
return `browser-window:${source.id}`;
|
|
237
203
|
case CastSourceKind.Monitor:
|
|
238
204
|
return `monitor:${source.name}`;
|
|
239
205
|
case CastSourceKind.Region:
|
|
@@ -241,27 +207,68 @@ const sourceKey = (source: CastSource): string => {
|
|
|
241
207
|
}
|
|
242
208
|
};
|
|
243
209
|
|
|
244
|
-
/**
|
|
245
|
-
const
|
|
246
|
-
apps: (appId: string) => App,
|
|
210
|
+
/** `source`'s tile, previewed where `desk` shows it. */
|
|
211
|
+
const tileOf = (
|
|
247
212
|
source: CastSource,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
/** How the picker names a source, a window by its `app` too. */
|
|
254
|
-
const sourceName = (source: CastSource, app: App | undefined): string => {
|
|
213
|
+
apps: (appId: string) => App,
|
|
214
|
+
desk: FrozenDesk | undefined,
|
|
215
|
+
shown: readonly ShownWindow[],
|
|
216
|
+
): SourceTile => {
|
|
255
217
|
switch (source.kind) {
|
|
256
218
|
case CastSourceKind.Window: {
|
|
257
|
-
const
|
|
258
|
-
|
|
219
|
+
const app = source.appId === "" ? undefined : apps(source.appId);
|
|
220
|
+
const title = titled(source.title);
|
|
221
|
+
return {
|
|
222
|
+
area: shownArea(desk, shown, ShownWindowKind.App, source.id),
|
|
223
|
+
icon: <WindowIcon app={app} />,
|
|
224
|
+
key: sourceKey(source),
|
|
225
|
+
name: app === undefined ? title : `${app.name}: ${title}`,
|
|
226
|
+
};
|
|
259
227
|
}
|
|
228
|
+
case CastSourceKind.BrowserWindow:
|
|
229
|
+
return {
|
|
230
|
+
area: shownArea(desk, shown, ShownWindowKind.Browser, source.id),
|
|
231
|
+
icon: <GlobeIcon aria-hidden />,
|
|
232
|
+
key: sourceKey(source),
|
|
233
|
+
name: `Browser: ${titled(source.title)}`,
|
|
234
|
+
};
|
|
260
235
|
case CastSourceKind.Monitor:
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
:
|
|
236
|
+
return {
|
|
237
|
+
area: desk?.monitors.find(({ name }) => name === source.name)?.area,
|
|
238
|
+
icon: <MonitorIcon aria-hidden />,
|
|
239
|
+
key: sourceKey(source),
|
|
240
|
+
name:
|
|
241
|
+
source.description === ""
|
|
242
|
+
? source.name
|
|
243
|
+
: `${source.name}: ${source.description}`,
|
|
244
|
+
};
|
|
264
245
|
case CastSourceKind.Region:
|
|
265
|
-
return
|
|
246
|
+
return {
|
|
247
|
+
area: undefined,
|
|
248
|
+
icon: <MonitorIcon aria-hidden />,
|
|
249
|
+
key: sourceKey(source),
|
|
250
|
+
name: "Region",
|
|
251
|
+
};
|
|
266
252
|
}
|
|
267
253
|
};
|
|
254
|
+
|
|
255
|
+
/** The name of a window titled `title`, which may be empty. */
|
|
256
|
+
const titled = (title: string): string =>
|
|
257
|
+
title === "" ? "Untitled window" : title;
|
|
258
|
+
|
|
259
|
+
/** Where `desk` shows the window of `kind` and `id`, if it does. */
|
|
260
|
+
const shownArea = (
|
|
261
|
+
desk: FrozenDesk | undefined,
|
|
262
|
+
shown: readonly ShownWindow[],
|
|
263
|
+
kind: ShownWindowKind,
|
|
264
|
+
id: string,
|
|
265
|
+
): ShotRect | undefined => {
|
|
266
|
+
const window = shown.find((window) =>
|
|
267
|
+
window.kind === ShownWindowKind.App
|
|
268
|
+
? kind === ShownWindowKind.App && window.appId === id
|
|
269
|
+
: kind === ShownWindowKind.Browser && window.id === id,
|
|
270
|
+
);
|
|
271
|
+
return desk === undefined || window === undefined
|
|
272
|
+
? undefined
|
|
273
|
+
: onFrame(desk, window.box);
|
|
274
|
+
};
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
ShotRect,
|
|
6
6
|
} from "@domicile-desktop/sdk/portal";
|
|
7
7
|
import { PortalAnswer as Answer } from "@domicile-desktop/sdk/portal";
|
|
8
|
-
import { AppWindowIcon } from "@phosphor-icons/react/dist/ssr/AppWindow";
|
|
9
8
|
import { GlobeIcon } from "@phosphor-icons/react/dist/ssr/Globe";
|
|
10
9
|
import { MonitorIcon } from "@phosphor-icons/react/dist/ssr/Monitor";
|
|
11
10
|
import type { PointerEvent, ReactNode } from "react";
|
|
@@ -18,8 +17,11 @@ import type { App } from "../useApps/useApps";
|
|
|
18
17
|
import type { FramePoint } from "./frame-point";
|
|
19
18
|
import { draggedArea, framePoint } from "./frame-point";
|
|
20
19
|
import { onFrame } from "./on-frame";
|
|
20
|
+
import type { SourceTile } from "./SourceTiles";
|
|
21
|
+
import { SourceTiles } from "./SourceTiles";
|
|
21
22
|
import type { ShownWindow } from "./shown-windows";
|
|
22
23
|
import { ShownWindowKind } from "./shown-windows";
|
|
24
|
+
import { WindowIcon } from "./WindowIcon";
|
|
23
25
|
|
|
24
26
|
type Props = {
|
|
25
27
|
answer: (answer: PortalAnswer) => void;
|
|
@@ -71,8 +73,9 @@ type Pick = ReturnType<(typeof Pick)[keyof typeof Pick]>;
|
|
|
71
73
|
type Choice = { icon: ReactNode; name: string; pick: Pick };
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
|
-
* Picks what to save of a frozen desk: all of it, a screen, a window,
|
|
75
|
-
*
|
|
76
|
+
* Picks what to save of a frozen desk: all of it, a screen, a window, each
|
|
77
|
+
* a tile previewing it as the screen cast picker's do, or an area dragged
|
|
78
|
+
* over the desk. A window is saved from its own frame, so what is
|
|
76
79
|
* over it, such as a notification, is left out. Ctrl+C copies the pick to
|
|
77
80
|
* the clipboard instead of saving it. Dismissing it cancels.
|
|
78
81
|
*/
|
|
@@ -105,7 +108,7 @@ export const ScreenshotDialog = ({
|
|
|
105
108
|
...shown.flatMap((window) => browserChoices(body, browserWindows, window)),
|
|
106
109
|
];
|
|
107
110
|
const whole: Choice = {
|
|
108
|
-
icon:
|
|
111
|
+
icon: <MonitorIcon aria-hidden />,
|
|
109
112
|
name: "Whole desk",
|
|
110
113
|
pick: Pick.Area({ height: body.height, width: body.width, x: 0, y: 0 }),
|
|
111
114
|
};
|
|
@@ -130,21 +133,32 @@ export const ScreenshotDialog = ({
|
|
|
130
133
|
document.removeEventListener("keydown", copy);
|
|
131
134
|
};
|
|
132
135
|
}, [answer, pick]);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
);
|
|
148
162
|
const pointAt = (event: PointerEvent<HTMLDivElement>) =>
|
|
149
163
|
framePoint(
|
|
150
164
|
{ x: event.clientX, y: event.clientY },
|
|
@@ -185,19 +199,9 @@ export const ScreenshotDialog = ({
|
|
|
185
199
|
>
|
|
186
200
|
{asker !== undefined && <p className={askerStyles}>{`${asker} asks`}</p>}
|
|
187
201
|
<div className={choicesStyles}>
|
|
188
|
-
{
|
|
189
|
-
{screens
|
|
190
|
-
|
|
191
|
-
{screens.map((choice, offset) => button(choice, 1 + offset))}
|
|
192
|
-
</ChoiceGroup>
|
|
193
|
-
)}
|
|
194
|
-
{windows.length > 0 && (
|
|
195
|
-
<ChoiceGroup label="Windows">
|
|
196
|
-
{windows.map((choice, offset) =>
|
|
197
|
-
button(choice, 1 + screens.length + offset),
|
|
198
|
-
)}
|
|
199
|
-
</ChoiceGroup>
|
|
200
|
-
)}
|
|
202
|
+
{group("Desk", [whole], 0)}
|
|
203
|
+
{group("Screens", screens, 1)}
|
|
204
|
+
{group("Windows", windows, 1 + screens.length)}
|
|
201
205
|
</div>
|
|
202
206
|
{pick.kind === PickKind.Window && pick.area === undefined && (
|
|
203
207
|
<p className={askerStyles}>
|
|
@@ -236,41 +240,14 @@ export const ScreenshotDialog = ({
|
|
|
236
240
|
);
|
|
237
241
|
};
|
|
238
242
|
|
|
239
|
-
/** A labeled row of choices. */
|
|
240
|
-
const ChoiceGroup = ({
|
|
241
|
-
children,
|
|
242
|
-
label,
|
|
243
|
-
}: {
|
|
244
|
-
children: ReactNode;
|
|
245
|
-
label: string;
|
|
246
|
-
}) => (
|
|
247
|
-
<div aria-label={label} className={groupStyles} role="group">
|
|
248
|
-
<span aria-hidden className={groupLabelStyles}>
|
|
249
|
-
{label}
|
|
250
|
-
</span>
|
|
251
|
-
{children}
|
|
252
|
-
</div>
|
|
253
|
-
);
|
|
254
|
-
|
|
255
243
|
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
256
244
|
|
|
257
245
|
const choicesStyles = flex({
|
|
258
|
-
align: "start",
|
|
259
246
|
direction: "column",
|
|
260
|
-
gap:
|
|
247
|
+
gap: 4,
|
|
261
248
|
marginBlock: 3,
|
|
262
249
|
});
|
|
263
250
|
|
|
264
|
-
const groupStyles = flex({ align: "center", gap: 2, wrap: "wrap" });
|
|
265
|
-
|
|
266
|
-
const groupLabelStyles = css({
|
|
267
|
-
color: "muted",
|
|
268
|
-
fontSize: "sm",
|
|
269
|
-
minInlineSize: 20,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const appIconStyles = css({ blockSize: 4, inlineSize: 4 });
|
|
273
|
-
|
|
274
251
|
const deskStyles = css({
|
|
275
252
|
cursor: "crosshair",
|
|
276
253
|
// Keeps the shade around the picked area on the frame.
|
|
@@ -302,6 +279,14 @@ const placed = (
|
|
|
302
279
|
width: `${String((area.width / frame.width) * 100)}%`,
|
|
303
280
|
});
|
|
304
281
|
|
|
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
|
+
|
|
305
290
|
/** The name of a window titled `title`, which may be empty. */
|
|
306
291
|
const titled = (title: string): string =>
|
|
307
292
|
title === "" ? "Untitled window" : title;
|
|
@@ -376,12 +361,7 @@ const windowChoice = (
|
|
|
376
361
|
title: string,
|
|
377
362
|
app: App | undefined,
|
|
378
363
|
): Choice => ({
|
|
379
|
-
icon:
|
|
380
|
-
app?.icon === undefined ? (
|
|
381
|
-
<AppWindowIcon aria-hidden />
|
|
382
|
-
) : (
|
|
383
|
-
<img alt="" className={appIconStyles} src={app.icon} />
|
|
384
|
-
),
|
|
364
|
+
icon: <WindowIcon app={app} />,
|
|
385
365
|
name: app === undefined ? title : `${app.name}: ${title}`,
|
|
386
366
|
pick,
|
|
387
367
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Toggle } from "@base-ui/react/toggle";
|
|
2
|
+
import { ToggleGroup } from "@base-ui/react/toggle-group";
|
|
3
|
+
import type { FrozenDesk, ShotRect } from "@domicile-desktop/sdk/portal";
|
|
4
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
5
|
+
import { css } from "../../styled-system/css";
|
|
6
|
+
|
|
7
|
+
/** A source a picker offers, as its tile shows it. */
|
|
8
|
+
export type SourceTile = {
|
|
9
|
+
/** Where the source is on the desk's frame, to preview it from. */
|
|
10
|
+
area: ShotRect | undefined;
|
|
11
|
+
/** The source's application icon, or one for its kind. */
|
|
12
|
+
icon: ReactNode;
|
|
13
|
+
/** Unique among the tiles. */
|
|
14
|
+
key: string;
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
/** The frozen desk to preview the sources on, if there is one. */
|
|
20
|
+
desk: FrozenDesk | undefined;
|
|
21
|
+
label: string;
|
|
22
|
+
multiple: boolean;
|
|
23
|
+
onValueChange: (value: string[]) => void;
|
|
24
|
+
tiles: readonly SourceTile[];
|
|
25
|
+
/** The keys of the tiles picked. */
|
|
26
|
+
value: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A labeled grid of sources to pick, each a tile previewing it from the
|
|
31
|
+
* frozen desk, or showing its icon large without a preview, over its icon
|
|
32
|
+
* and name. The screen cast and screenshot pickers share it.
|
|
33
|
+
*/
|
|
34
|
+
export const SourceTiles = ({
|
|
35
|
+
desk,
|
|
36
|
+
label,
|
|
37
|
+
multiple,
|
|
38
|
+
onValueChange,
|
|
39
|
+
tiles,
|
|
40
|
+
value,
|
|
41
|
+
}: Props) => (
|
|
42
|
+
<div className={groupStyles}>
|
|
43
|
+
<span aria-hidden className={labelStyles}>
|
|
44
|
+
{label}
|
|
45
|
+
</span>
|
|
46
|
+
<ToggleGroup
|
|
47
|
+
aria-label={label}
|
|
48
|
+
className={gridStyles}
|
|
49
|
+
multiple={multiple}
|
|
50
|
+
onValueChange={onValueChange}
|
|
51
|
+
value={[...value]}
|
|
52
|
+
>
|
|
53
|
+
{tiles.map((tile) => (
|
|
54
|
+
<Toggle className={tileStyles} key={tile.key} value={tile.key}>
|
|
55
|
+
<span aria-hidden className={frameStyles}>
|
|
56
|
+
{desk === undefined || tile.area === undefined ? (
|
|
57
|
+
<span className={iconPreviewStyles} data-preview>
|
|
58
|
+
{tile.icon}
|
|
59
|
+
</span>
|
|
60
|
+
) : (
|
|
61
|
+
<span
|
|
62
|
+
className={shotStyles}
|
|
63
|
+
data-preview
|
|
64
|
+
style={cropped(desk, tile.area)}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</span>
|
|
68
|
+
<span className={captionStyles}>
|
|
69
|
+
{tile.icon}
|
|
70
|
+
<span className={nameStyles}>{tile.name}</span>
|
|
71
|
+
</span>
|
|
72
|
+
</Toggle>
|
|
73
|
+
))}
|
|
74
|
+
</ToggleGroup>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const groupStyles = css({
|
|
79
|
+
display: "flex",
|
|
80
|
+
flexDirection: "column",
|
|
81
|
+
gap: 2,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const labelStyles = css({ color: "muted", fontSize: "sm" });
|
|
85
|
+
|
|
86
|
+
const gridStyles = css({
|
|
87
|
+
display: "grid",
|
|
88
|
+
gap: 2,
|
|
89
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(10rem, 1fr))",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const tileStyles = css({
|
|
93
|
+
_hover: { backgroundColor: "card" },
|
|
94
|
+
"&[data-pressed]": {
|
|
95
|
+
backgroundColor: "color-mix(in oklab, {colors.accent} 20%, transparent)",
|
|
96
|
+
borderColor: "accent",
|
|
97
|
+
},
|
|
98
|
+
alignItems: "stretch",
|
|
99
|
+
backgroundColor: "transparent",
|
|
100
|
+
border: "1px solid transparent",
|
|
101
|
+
borderRadius: "md",
|
|
102
|
+
color: "foreground",
|
|
103
|
+
cursor: "pointer",
|
|
104
|
+
display: "flex",
|
|
105
|
+
flexDirection: "column",
|
|
106
|
+
font: "inherit",
|
|
107
|
+
gap: 2,
|
|
108
|
+
minInlineSize: 0,
|
|
109
|
+
padding: 2,
|
|
110
|
+
textAlign: "start",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const frameStyles = css({
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
aspectRatio: "16 / 10",
|
|
116
|
+
backgroundColor: "card",
|
|
117
|
+
borderRadius: "sm",
|
|
118
|
+
display: "flex",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
overflow: "hidden",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const iconPreviewStyles = css({
|
|
124
|
+
"& :is(svg, img)": { blockSize: 10, inlineSize: 10 },
|
|
125
|
+
color: "muted",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const shotStyles = css({
|
|
129
|
+
backgroundRepeat: "no-repeat",
|
|
130
|
+
maxBlockSize: "full",
|
|
131
|
+
maxInlineSize: "full",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const captionStyles = css({
|
|
135
|
+
"& > :is(svg, img)": { blockSize: 4, flexShrink: 0, inlineSize: 4 },
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
display: "flex",
|
|
138
|
+
fontSize: "sm",
|
|
139
|
+
gap: 2,
|
|
140
|
+
minInlineSize: 0,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const nameStyles = css({
|
|
144
|
+
overflow: "hidden",
|
|
145
|
+
textOverflow: "ellipsis",
|
|
146
|
+
whiteSpace: "nowrap",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Draws `area` of `desk`'s frame filling the tile's frame on its long side,
|
|
151
|
+
* at its own aspect ratio.
|
|
152
|
+
*/
|
|
153
|
+
const cropped = (desk: FrozenDesk, area: ShotRect): CSSProperties => ({
|
|
154
|
+
aspectRatio: `${String(area.width)} / ${String(area.height)}`,
|
|
155
|
+
backgroundImage: `url(${desk.frame})`,
|
|
156
|
+
backgroundPosition: `${offset(area.x, area.width, desk.width)} ${offset(area.y, area.height, desk.height)}`,
|
|
157
|
+
backgroundSize: `${percent(desk.width, area.width)} ${percent(desk.height, area.height)}`,
|
|
158
|
+
// Wider than the frame's 16:10 fills its width; taller fills its height.
|
|
159
|
+
...(area.width * 10 >= area.height * 16
|
|
160
|
+
? { inlineSize: "100%" }
|
|
161
|
+
: { blockSize: "100%" }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** `whole` as a percent of `part`. */
|
|
165
|
+
const percent = (whole: number, part: number): string =>
|
|
166
|
+
`${String((whole / part) * 100)}%`;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The background position that puts `start` of a `whole` frame at the
|
|
170
|
+
* edge of a `length` box.
|
|
171
|
+
*/
|
|
172
|
+
const offset = (start: number, length: number, whole: number): string =>
|
|
173
|
+
whole === length ? "0%" : `${String((start / (whole - length)) * 100)}%`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AppWindowIcon } from "@phosphor-icons/react/dist/ssr/AppWindow";
|
|
2
|
+
import type { App } from "../useApps/useApps";
|
|
3
|
+
|
|
4
|
+
/** A window's application icon, or a window outline without one. */
|
|
5
|
+
export const WindowIcon = ({ app }: { app: App | undefined }) =>
|
|
6
|
+
app?.icon === undefined ? (
|
|
7
|
+
<AppWindowIcon aria-hidden />
|
|
8
|
+
) : (
|
|
9
|
+
<img alt="" src={app.icon} />
|
|
10
|
+
);
|
|
@@ -6,6 +6,8 @@ export const sourceName = (source: Captured): string => {
|
|
|
6
6
|
switch (source.kind) {
|
|
7
7
|
case CapturedKind.Window:
|
|
8
8
|
return source.title === "" ? "Untitled window" : source.title;
|
|
9
|
+
case CapturedKind.BrowserWindow:
|
|
10
|
+
return `Browser: ${source.title}`;
|
|
9
11
|
case CapturedKind.Monitor:
|
|
10
12
|
return `Screen ${source.name}`;
|
|
11
13
|
case CapturedKind.Region:
|