@domicile-desktop/component-library 0.0.0-alpha-73465ca8dd9d → 0.0.0-alpha-75f1c331c3ad

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,8 @@ Each export is `@domicile-desktop/component-library/<name>`.
30
30
  | `list-walk` | Arrow-key movement of a highlight through a list while focus stays in an input. |
31
31
  | `ModalDialog` | Modal dialog with `title`, `footer` and `trigger` props. |
32
32
  | `ContextMenu` | Menu opened at a point, such as a right click's. |
33
- | `PortalDialogs` | Every dialog applications ask for through the desktop portal. Mount one per shell with `host={domicile}`; it refuses kinds it cannot draw. See [PORTALS.md](/docs/architecture/PORTALS.md). |
33
+ | `PortalDialogs` | Every dialog applications ask for through the desktop portal. Mount one per shell with `host={domicile}`; it refuses kinds it cannot draw. `omitScreenCasts` leaves screen casts out of its indicator. See [PORTALS.md](/docs/architecture/PORTALS.md). |
34
+ | `source-name` | `sourceName`: how a screen cast's source is named. |
34
35
  | `Popover` | Non-modal panel anchored to the control that opened it. |
35
36
  | `Provider` | base-ui `DirectionProvider` wrapper. Every app roots its tree in it. |
36
37
  | `Screen` | Renders its children once per selected display. |
@@ -46,7 +47,9 @@ Each export is `@domicile-desktop/component-library/<name>`.
46
47
  | `Toaster` | Stack of toasts in a corner. The caller renders each card. |
47
48
  | `ThemeProvider` | Light/dark theme state. Sets `data-theme` on `<html>`. |
48
49
  | `ThemeSwitch` | Button that flips the theme. |
50
+ | `useApps` | Hook: names and icons of applications by app id, from their desktop entries. |
49
51
  | `usePictureUrl` | Hook: a URL an `<img>` can show for a file read through the SDK's `system`. |
52
+ | `usePortalWallpaper` | Hook: the pictures applications set through the Wallpaper portal, as paths. |
50
53
  | `theme-core`, `theme-source`, `standalone-theme-source` | Theme types and sources. Use the standalone source where there is no compositor, such as Storybook. |
51
54
  | `control-sizes` | `Size` type and `SIZES` array for sized controls. |
52
55
  | `spacing` | rem value of one spacing step, for runtime math. |
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-73465ca8dd9d",
6
- "@domicile-desktop/system-apps": "0.0.0-alpha-73465ca8dd9d",
5
+ "@domicile-desktop/sdk": "0.0.0-alpha-75f1c331c3ad",
6
+ "@domicile-desktop/system-apps": "0.0.0-alpha-75f1c331c3ad",
7
7
  "@phosphor-icons/react": "^2.1.10"
8
8
  },
9
9
  "description": "React components and the Panda CSS design system that Domicile's shells are built from.",
@@ -56,6 +56,7 @@
56
56
  "./SlideOver": "./src/SlideOver/SlideOver.tsx",
57
57
  "./Slider": "./src/Slider/Slider.tsx",
58
58
  "./Switch": "./src/Switch/Switch.tsx",
59
+ "./source-name": "./src/PortalDialogs/source-name.ts",
59
60
  "./spacing": "./src/spacing.ts",
60
61
  "./standalone-theme-source": "./src/ThemeSwitch/standalone-theme-source.ts",
61
62
  "./TabRail": "./src/TabRail/TabRail.tsx",
@@ -66,7 +67,9 @@
66
67
  "./Toaster": "./src/Toaster/Toaster.tsx",
67
68
  "./theme-core": "./src/ThemeSwitch/theme-core.ts",
68
69
  "./theme-source": "./src/ThemeSwitch/theme-source.ts",
70
+ "./useApps": "./src/useApps/useApps.ts",
69
71
  "./usePictureUrl": "./src/usePictureUrl/usePictureUrl.ts",
72
+ "./usePortalWallpaper": "./src/usePortalWallpaper/usePortalWallpaper.ts",
70
73
  "./vite-shell": "./src/vite-shell.ts"
71
74
  },
72
75
  "files": [
@@ -99,5 +102,5 @@
99
102
  "test:unit": "bun test"
100
103
  },
101
104
  "type": "module",
102
- "version": "0.0.0-alpha-73465ca8dd9d"
105
+ "version": "0.0.0-alpha-75f1c331c3ad"
103
106
  }
@@ -1,13 +1,15 @@
1
- import type { Captured, Capturing } from "@domicile-desktop/sdk/portal";
2
- import { CapturedKind, CapturingKind } from "@domicile-desktop/sdk/portal";
1
+ import type { Capturing } from "@domicile-desktop/sdk/portal";
2
+ import { CapturingKind } from "@domicile-desktop/sdk/portal";
3
3
  import { css } from "../../styled-system/css";
4
4
  import { flex, hstack } from "../../styled-system/patterns";
5
5
  import { Button } from "../Button/Button";
6
6
  import { useScreenRegion } from "../Screen/DisplayProvider";
7
- import { appName } from "./app-name";
7
+ import type { App } from "../useApps/useApps";
8
8
  import { deviceNames } from "./device-names";
9
+ import { sourceName } from "./source-name";
9
10
 
10
11
  type Props = {
12
+ apps: (appId: string) => App;
11
13
  sessions: readonly Capturing[];
12
14
  stop: (id: number) => void;
13
15
  screen: string | undefined;
@@ -17,14 +19,14 @@ type Props = {
17
19
  * A row per session that records the screen or controls or captures input,
18
20
  * naming its application and what it holds, with a button that ends it.
19
21
  */
20
- export const CapturingIndicator = ({ screen, sessions, stop }: Props) => {
22
+ export const CapturingIndicator = ({ apps, screen, sessions, stop }: Props) => {
21
23
  const region = useScreenRegion(screen);
22
24
  return sessions.length === 0 ? undefined : (
23
25
  <div className={regionStyles} style={region}>
24
26
  <ul aria-label="Sharing and remote control" className={listStyles}>
25
27
  {sessions.map((session) => (
26
28
  <li className={itemStyles} key={session.id}>
27
- <span>{summary(session)}</span>
29
+ <AppRow app={apps(session.appId)} session={session} />
28
30
  <Button
29
31
  onClick={() => {
30
32
  stop(session.id);
@@ -62,6 +64,8 @@ const listStyles = flex({
62
64
  position: "absolute",
63
65
  });
64
66
 
67
+ const iconStyles = css({ blockSize: 4, flexShrink: 0, inlineSize: 4 });
68
+
65
69
  const itemStyles = hstack({
66
70
  backgroundColor: "card",
67
71
  border: "1px solid {colors.warning}",
@@ -76,9 +80,18 @@ const itemStyles = hstack({
76
80
  pointerEvents: "auto",
77
81
  });
78
82
 
83
+ /** A session's application icon, when it has one, and what it holds. */
84
+ const AppRow = ({ app, session }: { app: App; session: Capturing }) => (
85
+ <>
86
+ {app.icon !== undefined && (
87
+ <img alt="" className={iconStyles} src={app.icon} />
88
+ )}
89
+ <span>{summary(session, app.name)}</span>
90
+ </>
91
+ );
92
+
79
93
  /** What a session's row says. */
80
- const summary = (session: Capturing): string => {
81
- const name = appName(session.appId);
94
+ const summary = (session: Capturing, name: string): string => {
82
95
  switch (session.kind) {
83
96
  case CapturingKind.RemoteDesktop:
84
97
  return `Remote control: ${name} — ${[
@@ -93,15 +106,3 @@ const summary = (session: Capturing): string => {
93
106
  return `${session.wireKind}: ${name}`;
94
107
  }
95
108
  };
96
-
97
- /** How a row names a recorded source. */
98
- const sourceName = (source: Captured): string => {
99
- switch (source.kind) {
100
- case CapturedKind.Window:
101
- return source.title === "" ? "Untitled window" : source.title;
102
- case CapturedKind.Monitor:
103
- return `Screen ${source.name}`;
104
- case CapturedKind.Region:
105
- return "Region";
106
- }
107
- };
@@ -3,6 +3,7 @@ import { fireGlobalShortcuts } from "@domicile-desktop/sdk/global-shortcuts";
3
3
  import type { Capturing, PortalRequest } from "@domicile-desktop/sdk/portal";
4
4
  import {
5
5
  answerPortalRequest,
6
+ CapturingKind,
6
7
  PortalAnswer,
7
8
  PortalKind,
8
9
  stopCapturing,
@@ -13,10 +14,10 @@ import type { System, SystemHost } from "@domicile-desktop/sdk/system";
13
14
  import { system } from "@domicile-desktop/sdk/system";
14
15
  import { useCallback, useEffect, useMemo, useState } from "react";
15
16
  import { listDirectory } from "../FilePicker/list-directory";
17
+ import { useApps } from "../useApps/useApps";
16
18
  import { AccessDialog } from "./AccessDialog";
17
19
  import { AccountDialog } from "./AccountDialog";
18
20
  import { AppChooserDialog } from "./AppChooserDialog";
19
- import { appName } from "./app-name";
20
21
  import { CapturingIndicator } from "./CapturingIndicator";
21
22
  import { FileChooserDialog } from "./FileChooserDialog";
22
23
  import { GlobalShortcutsDialog } from "./GlobalShortcutsDialog";
@@ -41,6 +42,11 @@ type Props = {
41
42
  * `DisplayProvider`; without it, a dialog is centered on the whole page.
42
43
  */
43
44
  screen?: string | undefined;
45
+ /**
46
+ * Leave screen casts out of the capturing indicator, for a shell that shows
47
+ * them itself.
48
+ */
49
+ omitScreenCasts?: boolean | undefined;
44
50
  /** How dialogs read the desktop's files, for tests. */
45
51
  systemOf?: typeof system | undefined;
46
52
  /**
@@ -62,6 +68,7 @@ type Props = {
62
68
  */
63
69
  export const PortalDialogs = ({
64
70
  host,
71
+ omitScreenCasts = false,
65
72
  screen,
66
73
  screenOf,
67
74
  shellChords = NONE,
@@ -74,6 +81,10 @@ export const PortalDialogs = ({
74
81
  [files],
75
82
  );
76
83
  const [sessions, setSessions] = useState<readonly Capturing[]>([]);
84
+ const apps = useApps(files, [
85
+ ...requests.map((request) => request.appId),
86
+ ...sessions.map((session) => session.appId),
87
+ ]);
77
88
 
78
89
  useEffect(() => watchPortalRequests(host, setRequests), [host]);
79
90
  useEffect(() => watchCapturing(host, setSessions), [host]);
@@ -91,8 +102,15 @@ export const PortalDialogs = ({
91
102
  return (
92
103
  <>
93
104
  <CapturingIndicator
105
+ apps={apps}
94
106
  screen={screen}
95
- sessions={sessions}
107
+ sessions={
108
+ omitScreenCasts
109
+ ? sessions.filter(
110
+ (session) => session.kind !== CapturingKind.ScreenCast,
111
+ )
112
+ : sessions
113
+ }
96
114
  stop={(id) => {
97
115
  stopCapturing(host, id);
98
116
  }}
@@ -102,6 +120,7 @@ export const PortalDialogs = ({
102
120
  answer={(answer) => {
103
121
  answerPortalRequest(host, shown.id, answer);
104
122
  }}
123
+ asker={apps(shown.appId).name}
105
124
  key={shown.id}
106
125
  list={list}
107
126
  request={shown}
@@ -117,6 +136,7 @@ export const PortalDialogs = ({
117
136
  /** The dialog for `request`'s kind. */
118
137
  const Dialog = ({
119
138
  answer,
139
+ asker,
120
140
  list,
121
141
  request,
122
142
  screen,
@@ -124,6 +144,7 @@ const Dialog = ({
124
144
  system: files,
125
145
  }: {
126
146
  answer: (answer: PortalAnswer) => void;
147
+ asker: string;
127
148
  list: (path: string) => Promise<readonly string[]>;
128
149
  request: PortalRequest;
129
150
  screen: string | undefined;
@@ -135,7 +156,7 @@ const Dialog = ({
135
156
  return (
136
157
  <AccessDialog
137
158
  answer={answer}
138
- asker={appName(request.appId)}
159
+ asker={asker}
139
160
  body={request.body}
140
161
  screen={screen}
141
162
  />
@@ -144,7 +165,7 @@ const Dialog = ({
144
165
  return (
145
166
  <RemoteDesktopDialog
146
167
  answer={answer}
147
- asker={appName(request.appId)}
168
+ asker={asker}
148
169
  body={request.body}
149
170
  screen={screen}
150
171
  />
@@ -153,7 +174,7 @@ const Dialog = ({
153
174
  return (
154
175
  <InputCaptureDialog
155
176
  answer={answer}
156
- asker={appName(request.appId)}
177
+ asker={asker}
157
178
  body={request.body}
158
179
  screen={screen}
159
180
  />
@@ -162,7 +183,7 @@ const Dialog = ({
162
183
  return (
163
184
  <AppChooserDialog
164
185
  answer={answer}
165
- asker={appName(request.appId)}
186
+ asker={asker}
166
187
  body={request.body}
167
188
  screen={screen}
168
189
  system={files}
@@ -181,7 +202,7 @@ const Dialog = ({
181
202
  return (
182
203
  <AccountDialog
183
204
  answer={answer}
184
- asker={appName(request.appId)}
205
+ asker={asker}
185
206
  body={request.body}
186
207
  screen={screen}
187
208
  />
@@ -190,7 +211,7 @@ const Dialog = ({
190
211
  return (
191
212
  <GlobalShortcutsDialog
192
213
  answer={answer}
193
- asker={appName(request.appId)}
214
+ asker={asker}
194
215
  body={request.body}
195
216
  screen={screen}
196
217
  shellChords={shellChords}
@@ -200,7 +221,7 @@ const Dialog = ({
200
221
  return (
201
222
  <WallpaperDialog
202
223
  answer={answer}
203
- asker={appName(request.appId)}
224
+ asker={asker}
204
225
  body={request.body}
205
226
  files={files}
206
227
  screen={screen}
@@ -210,7 +231,7 @@ const Dialog = ({
210
231
  return (
211
232
  <LauncherDialog
212
233
  answer={answer}
213
- asker={appName(request.appId)}
234
+ asker={asker}
214
235
  body={request.body}
215
236
  screen={screen}
216
237
  />
@@ -219,7 +240,7 @@ const Dialog = ({
219
240
  return (
220
241
  <UsbDialog
221
242
  answer={answer}
222
- asker={appName(request.appId)}
243
+ asker={asker}
223
244
  body={request.body}
224
245
  screen={screen}
225
246
  />
@@ -228,7 +249,7 @@ const Dialog = ({
228
249
  return (
229
250
  <ScreenCastDialog
230
251
  answer={answer}
231
- asker={appName(request.appId)}
252
+ asker={asker}
232
253
  body={request.body}
233
254
  screen={screen}
234
255
  />
@@ -237,7 +258,7 @@ const Dialog = ({
237
258
  return (
238
259
  <PrintDialog
239
260
  answer={answer}
240
- asker={appName(request.appId)}
261
+ asker={asker}
241
262
  body={request.body}
242
263
  screen={screen}
243
264
  />
@@ -246,7 +267,7 @@ const Dialog = ({
246
267
  return (
247
268
  <ScreenshotDialog
248
269
  answer={answer}
249
- asker={appName(request.appId)}
270
+ asker={asker}
250
271
  body={request.body}
251
272
  screen={screen}
252
273
  />
@@ -255,7 +276,7 @@ const Dialog = ({
255
276
  return (
256
277
  <PickColorDialog
257
278
  answer={answer}
258
- asker={appName(request.appId)}
279
+ asker={asker}
259
280
  body={request.body}
260
281
  screen={screen}
261
282
  />
@@ -1,29 +1,16 @@
1
1
  // The applications an app chooser offers, described from their desktop
2
2
  // entries, and which to pick first.
3
3
 
4
- import type { Option } from "@cprussin/option-result";
5
- import { None, Ok, Result } from "@cprussin/option-result";
4
+ import type { Result } from "@cprussin/option-result";
5
+ import { Ok } from "@cprussin/option-result";
6
6
  import type { System, SystemError } from "@domicile-desktop/sdk/system";
7
- import type { IconLookup } from "@domicile-desktop/system-apps/app-icons";
8
- import { appIcons } from "@domicile-desktop/system-apps/app-icons";
9
- import { dataDirs } from "@domicile-desktop/system-apps/data-dirs";
10
- import type { DesktopEntry } from "@domicile-desktop/system-apps/desktop-entry";
11
- import { installedApps } from "@domicile-desktop/system-apps/installed";
7
+ import type { DescribedApp } from "@domicile-desktop/system-apps/describe-apps";
8
+ import { describeApps } from "@domicile-desktop/system-apps/describe-apps";
12
9
  import { defaultApps } from "@domicile-desktop/system-apps/mime-apps";
13
10
 
14
- /** One application offered. */
15
- export type AppChoice = {
16
- /** Its desktop file ID without `.desktop`, as the portal names it. */
17
- id: string;
18
- /** Its entry's `Name`, or `id` when it has no entry to read. */
19
- name: string;
20
- /** A `data:` URL, or `undefined` for none. */
21
- icon: string | undefined;
22
- };
23
-
24
11
  /** The offered applications, described, and the defaults for the type. */
25
12
  export type DescribedChoices = {
26
- apps: AppChoice[];
13
+ apps: DescribedApp[];
27
14
  /** `mimeapps.list`'s defaults, desktop file IDs without `.desktop`. */
28
15
  defaults: string[];
29
16
  };
@@ -36,32 +23,20 @@ export const describeChoices = async (
36
23
  system: System,
37
24
  choices: readonly string[],
38
25
  contentType: string | undefined,
39
- ): Promise<Result<DescribedChoices, SystemError>> =>
40
- (await dataDirs(system)).andThenAsync(async (dirs) => {
41
- const icons = appIcons(system, dirs);
42
- const apps = (await installedApps(system, dirs)).andThenAsync(
43
- async (installed) => {
44
- const entries = new Map(
45
- installed.map((entry) => [withoutSuffix(entry.id), entry]),
46
- );
47
- return Result.collect(
48
- await Promise.all(
49
- choices.map((id) => described(icons, id, entries.get(id))),
50
- ),
51
- );
52
- },
53
- );
54
- const defaults =
55
- contentType === undefined
56
- ? Ok<string[], SystemError>([])
57
- : await defaultApps(system, contentType);
58
- return (await apps).andThen((found) =>
59
- defaults.map((ids) => ({
60
- apps: found,
61
- defaults: ids.map(withoutSuffix),
62
- })),
63
- );
64
- });
26
+ ): Promise<Result<DescribedChoices, SystemError>> => {
27
+ const [apps, defaults] = await Promise.all([
28
+ describeApps(system, choices),
29
+ contentType === undefined
30
+ ? Ok<string[], SystemError>([])
31
+ : defaultApps(system, contentType),
32
+ ]);
33
+ return apps.andThen((found) =>
34
+ defaults.map((ids) => ({
35
+ apps: found,
36
+ defaults: ids.map(withoutSuffix),
37
+ })),
38
+ );
39
+ };
65
40
 
66
41
  /**
67
42
  * The application to pick first: the last one chosen, else the first default
@@ -76,21 +51,4 @@ export const preselected = (
76
51
  (id) => id !== undefined && choices.includes(id),
77
52
  );
78
53
 
79
- /** Choice `id`, named and drawn by its `entry` when it has one. */
80
- const described = async (
81
- icons: IconLookup,
82
- id: string,
83
- entry: DesktopEntry | undefined,
84
- ): Promise<Result<AppChoice, SystemError>> =>
85
- entry === undefined
86
- ? Ok({ icon: undefined, id, name: id })
87
- : (entry.icon === undefined
88
- ? Ok<Option<string>, SystemError>(None())
89
- : await icons(entry.icon)
90
- ).map((icon) => ({
91
- icon: icon.match({ None: () => undefined, Some: (url) => url }),
92
- id,
93
- name: entry.name,
94
- }));
95
-
96
54
  const withoutSuffix = (id: string): string => id.replace(/\.desktop$/, "");
@@ -0,0 +1,14 @@
1
+ import type { Captured } from "@domicile-desktop/sdk/portal";
2
+ import { CapturedKind } from "@domicile-desktop/sdk/portal";
3
+
4
+ /** How the shell names a source a screen cast records. */
5
+ export const sourceName = (source: Captured): string => {
6
+ switch (source.kind) {
7
+ case CapturedKind.Window:
8
+ return source.title === "" ? "Untitled window" : source.title;
9
+ case CapturedKind.Monitor:
10
+ return `Screen ${source.name}`;
11
+ case CapturedKind.Region:
12
+ return "Region";
13
+ }
14
+ };
@@ -0,0 +1,72 @@
1
+ import type { System } from "@domicile-desktop/sdk/system";
2
+ import type { DescribedApp } from "@domicile-desktop/system-apps/describe-apps";
3
+ import { describeApps } from "@domicile-desktop/system-apps/describe-apps";
4
+ import { useCallback, useEffect, useState } from "react";
5
+
6
+ /** An application as a dialog or indicator shows it. */
7
+ export type App = {
8
+ name: string;
9
+ /** A `data:` URL, or `undefined` for none. */
10
+ icon: string | undefined;
11
+ };
12
+
13
+ /**
14
+ * Names each application by its desktop entry, with its icon, reading the
15
+ * entries for `ids` again when they change. An id with no entry, or not read
16
+ * yet, is named by itself; the empty id is "An application".
17
+ */
18
+ export const useApps = (
19
+ system: System,
20
+ ids: readonly string[],
21
+ ): ((appId: string) => App) => {
22
+ const [described, setDescribed] = useState<ReadonlyMap<string, DescribedApp>>(
23
+ new Map(),
24
+ );
25
+ // The array is new on every push; its contents are what change.
26
+ const key = [...new Set(ids.filter((id) => id !== ""))].toSorted().join("\n");
27
+
28
+ useEffect(() => {
29
+ if (key === "") {
30
+ return undefined;
31
+ } else {
32
+ let current = true;
33
+ describeApps(system, key.split("\n"))
34
+ .then((result) => {
35
+ result.match({
36
+ Err: (error) => {
37
+ throw new Error(
38
+ `could not read the applications: ${error.message}`,
39
+ );
40
+ },
41
+ Ok: (found) => {
42
+ if (current) {
43
+ setDescribed(new Map(found.map((app) => [app.id, app])));
44
+ }
45
+ },
46
+ });
47
+ })
48
+ .catch((error: unknown) => {
49
+ // biome-ignore lint/suspicious/noConsole: applications stay named by their ids; say why
50
+ console.error("Could not name the applications", error);
51
+ });
52
+ return () => {
53
+ current = false;
54
+ };
55
+ }
56
+ }, [system, key]);
57
+
58
+ return useCallback((appId) => app(described, appId), [described]);
59
+ };
60
+
61
+ /** `appId` as `described` names it. */
62
+ const app = (
63
+ described: ReadonlyMap<string, DescribedApp>,
64
+ appId: string,
65
+ ): App => {
66
+ const entry = described.get(appId);
67
+ if (entry === undefined) {
68
+ return { icon: undefined, name: appId === "" ? "An application" : appId };
69
+ } else {
70
+ return { icon: entry.icon, name: entry.name };
71
+ }
72
+ };
@@ -0,0 +1,20 @@
1
+ import type { PortalHost, PortalWallpaper } from "@domicile-desktop/sdk/portal";
2
+ import { watchPortalWallpaper } from "@domicile-desktop/sdk/portal";
3
+ import { useEffect, useState } from "react";
4
+
5
+ const NONE_SET: PortalWallpaper = {
6
+ background: undefined,
7
+ lockscreen: undefined,
8
+ };
9
+
10
+ /**
11
+ * The pictures applications set through the Wallpaper portal, as paths. See
12
+ * `docs/architecture/PORTALS.md`.
13
+ */
14
+ export const usePortalWallpaper = (host: PortalHost): PortalWallpaper => {
15
+ const [wallpaper, setWallpaper] = useState(NONE_SET);
16
+
17
+ useEffect(() => watchPortalWallpaper(host, setWallpaper), [host]);
18
+
19
+ return wallpaper;
20
+ };
@@ -1,3 +0,0 @@
1
- /** How a dialog or indicator names an application. */
2
- export const appName = (appId: string): string =>
3
- appId === "" ? "An application" : appId;