@domicile-desktop/component-library 0.0.0-alpha-ddde2bfbbaad → 0.0.0-alpha-da1cf1c5441c
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-da1cf1c5441c",
|
|
6
|
+
"@domicile-desktop/system-apps": "0.0.0-alpha-da1cf1c5441c",
|
|
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.",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"exports": {
|
|
32
32
|
"./Accordion": "./src/Accordion/Accordion.tsx",
|
|
33
|
+
"./AppChooser": "./src/AppChooser/AppChooser.tsx",
|
|
33
34
|
"./Autocomplete": "./src/Autocomplete/Autocomplete.tsx",
|
|
34
35
|
"./Avatar": "./src/Avatar/Avatar.tsx",
|
|
35
36
|
"./Button": "./src/Button/Button.tsx",
|
|
@@ -102,5 +103,5 @@
|
|
|
102
103
|
"test:unit": "bun test"
|
|
103
104
|
},
|
|
104
105
|
"type": "module",
|
|
105
|
-
"version": "0.0.0-alpha-
|
|
106
|
+
"version": "0.0.0-alpha-da1cf1c5441c"
|
|
106
107
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { System } from "@domicile-desktop/sdk/system";
|
|
2
|
+
import { AppWindowIcon } from "@phosphor-icons/react/dist/ssr/AppWindow";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { css } from "../../styled-system/css";
|
|
5
|
+
import { flex, hstack } from "../../styled-system/patterns";
|
|
6
|
+
import { Button } from "../Button/Button";
|
|
7
|
+
import { keepInView, stepOf, steppedTo } from "../list-walk";
|
|
8
|
+
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
9
|
+
import { preselected } from "./app-choices";
|
|
10
|
+
import { useDescribedChoices } from "./useDescribedChoices";
|
|
11
|
+
|
|
12
|
+
const ICON_SIZE = 24;
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
/** Desktop file IDs without `.desktop`, in the order offered. */
|
|
16
|
+
choices: readonly string[];
|
|
17
|
+
/** The type opened, whose defaults start picked; none when `undefined`. */
|
|
18
|
+
contentType: string | undefined;
|
|
19
|
+
/** A line under the title, such as who asks. */
|
|
20
|
+
description?: string | undefined;
|
|
21
|
+
/** The application chosen last time, which starts picked. */
|
|
22
|
+
lastChoice?: string | undefined;
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
/** Called with the ID of the application to open with. */
|
|
25
|
+
onChoose: (id: string) => void;
|
|
26
|
+
screen: string | undefined;
|
|
27
|
+
/** Where the applications' desktop entries are read. */
|
|
28
|
+
system: System;
|
|
29
|
+
title: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pick an application to open a file or URI with. The last choice, else the
|
|
34
|
+
* type's default, else the first offered, starts picked. Dismissing it
|
|
35
|
+
* cancels.
|
|
36
|
+
*/
|
|
37
|
+
export const AppChooser = ({
|
|
38
|
+
choices,
|
|
39
|
+
contentType,
|
|
40
|
+
description,
|
|
41
|
+
lastChoice,
|
|
42
|
+
onCancel,
|
|
43
|
+
onChoose,
|
|
44
|
+
screen,
|
|
45
|
+
system,
|
|
46
|
+
title,
|
|
47
|
+
}: Props) => {
|
|
48
|
+
const listId = useId();
|
|
49
|
+
const described = useDescribedChoices(system, choices, contentType);
|
|
50
|
+
const [picked, setPicked] = useState<string | undefined>();
|
|
51
|
+
const apps = described?.apps ?? [];
|
|
52
|
+
const first =
|
|
53
|
+
described === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: preselected(choices, lastChoice, described.defaults);
|
|
56
|
+
// A pick the caller has since taken away no longer counts.
|
|
57
|
+
const chosen =
|
|
58
|
+
picked !== undefined && choices.includes(picked) ? picked : first;
|
|
59
|
+
const at = apps.findIndex(({ id }) => id === chosen);
|
|
60
|
+
const choose = (id: string | undefined) => {
|
|
61
|
+
if (id !== undefined) {
|
|
62
|
+
onChoose(id);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return (
|
|
66
|
+
<ModalDialog
|
|
67
|
+
closeButton={false}
|
|
68
|
+
footer={
|
|
69
|
+
<>
|
|
70
|
+
<Button onClick={onCancel} variant="outline">
|
|
71
|
+
Cancel
|
|
72
|
+
</Button>
|
|
73
|
+
<Button
|
|
74
|
+
disabled={chosen === undefined}
|
|
75
|
+
onClick={() => {
|
|
76
|
+
choose(chosen);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
Open
|
|
80
|
+
</Button>
|
|
81
|
+
</>
|
|
82
|
+
}
|
|
83
|
+
onOpenChange={(open) => {
|
|
84
|
+
if (!open) {
|
|
85
|
+
onCancel();
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
open
|
|
89
|
+
screen={screen}
|
|
90
|
+
title={title}
|
|
91
|
+
>
|
|
92
|
+
{description !== undefined && (
|
|
93
|
+
<p className={descriptionStyles}>{description}</p>
|
|
94
|
+
)}
|
|
95
|
+
<div
|
|
96
|
+
aria-activedescendant={at === -1 ? undefined : optionId(listId, at)}
|
|
97
|
+
aria-label="Applications"
|
|
98
|
+
className={listStyles}
|
|
99
|
+
onKeyDown={(event) => {
|
|
100
|
+
const step = stepOf(event);
|
|
101
|
+
const next = apps[steppedTo(at, step ?? 0, apps.length)];
|
|
102
|
+
if (event.key === "Enter") {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
choose(chosen);
|
|
105
|
+
} else if (step !== undefined && next !== undefined) {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
setPicked(next.id);
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
role="listbox"
|
|
111
|
+
tabIndex={0}
|
|
112
|
+
>
|
|
113
|
+
{apps.map((app, index) => (
|
|
114
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: the listbox handles the keyboard via `aria-activedescendant`
|
|
115
|
+
<div
|
|
116
|
+
aria-selected={app.id === chosen}
|
|
117
|
+
className={optionStyles}
|
|
118
|
+
id={optionId(listId, index)}
|
|
119
|
+
key={app.id}
|
|
120
|
+
onClick={() => {
|
|
121
|
+
setPicked(app.id);
|
|
122
|
+
}}
|
|
123
|
+
onDoubleClick={() => {
|
|
124
|
+
choose(app.id);
|
|
125
|
+
}}
|
|
126
|
+
ref={app.id === chosen ? keepInView : undefined}
|
|
127
|
+
role="option"
|
|
128
|
+
tabIndex={-1}
|
|
129
|
+
>
|
|
130
|
+
{app.icon === undefined ? (
|
|
131
|
+
<AppWindowIcon size={ICON_SIZE} />
|
|
132
|
+
) : (
|
|
133
|
+
<img
|
|
134
|
+
alt=""
|
|
135
|
+
className={iconStyles}
|
|
136
|
+
height={ICON_SIZE}
|
|
137
|
+
src={app.icon}
|
|
138
|
+
width={ICON_SIZE}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
<span className={nameStyles}>{app.name}</span>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</ModalDialog>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const descriptionStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
150
|
+
|
|
151
|
+
const listStyles = flex({
|
|
152
|
+
direction: "column",
|
|
153
|
+
gap: 0.5,
|
|
154
|
+
marginBlockStart: 3,
|
|
155
|
+
maxBlockSize: 80,
|
|
156
|
+
overflowY: "auto",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// As the file picker's rows.
|
|
160
|
+
const optionStyles = hstack({
|
|
161
|
+
"&[aria-selected=true]": {
|
|
162
|
+
backgroundImage:
|
|
163
|
+
"linear-gradient(to right, color-mix(in oklab, {colors.accent} 30%, transparent), color-mix(in oklab, {colors.accent} 8%, transparent))",
|
|
164
|
+
},
|
|
165
|
+
borderRadius: "md",
|
|
166
|
+
color: "foreground",
|
|
167
|
+
cursor: "pointer",
|
|
168
|
+
gap: 3,
|
|
169
|
+
paddingBlock: 1.5,
|
|
170
|
+
paddingInline: 2,
|
|
171
|
+
transition: "background-color {durations.fast} {easings.out}",
|
|
172
|
+
userSelect: "none",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const iconStyles = css({ flexShrink: 0, objectFit: "contain" });
|
|
176
|
+
|
|
177
|
+
const nameStyles = css({
|
|
178
|
+
fontSize: "sm",
|
|
179
|
+
fontWeight: "medium",
|
|
180
|
+
overflow: "hidden",
|
|
181
|
+
textOverflow: "ellipsis",
|
|
182
|
+
whiteSpace: "nowrap",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const optionId = (listId: string, index: number): string =>
|
|
186
|
+
`${listId}-${String(index)}`;
|
|
@@ -4,17 +4,7 @@ import type {
|
|
|
4
4
|
} from "@domicile-desktop/sdk/portal";
|
|
5
5
|
import { PortalAnswer as Answer } from "@domicile-desktop/sdk/portal";
|
|
6
6
|
import type { System } from "@domicile-desktop/sdk/system";
|
|
7
|
-
import {
|
|
8
|
-
import { useId, useState } from "react";
|
|
9
|
-
import { css } from "../../styled-system/css";
|
|
10
|
-
import { flex, hstack } from "../../styled-system/patterns";
|
|
11
|
-
import { Button } from "../Button/Button";
|
|
12
|
-
import { keepInView, stepOf, steppedTo } from "../list-walk";
|
|
13
|
-
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
14
|
-
import { preselected } from "./app-choices";
|
|
15
|
-
import { useDescribedChoices } from "./useDescribedChoices";
|
|
16
|
-
|
|
17
|
-
const ICON_SIZE = 24;
|
|
7
|
+
import { AppChooser } from "../AppChooser/AppChooser";
|
|
18
8
|
|
|
19
9
|
type Props = {
|
|
20
10
|
answer: (answer: PortalAnswer) => void;
|
|
@@ -26,162 +16,33 @@ type Props = {
|
|
|
26
16
|
system: System;
|
|
27
17
|
};
|
|
28
18
|
|
|
29
|
-
/**
|
|
30
|
-
* Pick an application to open a file or URI with. The last choice, else the
|
|
31
|
-
* type's default, starts picked. Dismissing it cancels.
|
|
32
|
-
*/
|
|
19
|
+
/** The AppChooser portal's {@link AppChooser}. Dismissing it cancels. */
|
|
33
20
|
export const AppChooserDialog = ({
|
|
34
21
|
answer,
|
|
35
22
|
asker,
|
|
36
23
|
body,
|
|
37
24
|
screen,
|
|
38
25
|
system,
|
|
39
|
-
}: Props) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const chosen =
|
|
50
|
-
picked !== undefined && body.choices.includes(picked) ? picked : first;
|
|
51
|
-
const at = apps.findIndex(({ id }) => id === chosen);
|
|
52
|
-
const choose = (id: string | undefined) => {
|
|
53
|
-
if (id !== undefined) {
|
|
26
|
+
}: Props) => (
|
|
27
|
+
<AppChooser
|
|
28
|
+
choices={body.choices}
|
|
29
|
+
contentType={body.contentType}
|
|
30
|
+
description={`${asker} asks`}
|
|
31
|
+
lastChoice={body.lastChoice}
|
|
32
|
+
onCancel={() => {
|
|
33
|
+
answer(Answer.Canceled());
|
|
34
|
+
}}
|
|
35
|
+
onChoose={(id) => {
|
|
54
36
|
answer(Answer.AppChooser(id));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<>
|
|
62
|
-
<Button
|
|
63
|
-
onClick={() => {
|
|
64
|
-
answer(Answer.Canceled());
|
|
65
|
-
}}
|
|
66
|
-
variant="outline"
|
|
67
|
-
>
|
|
68
|
-
Cancel
|
|
69
|
-
</Button>
|
|
70
|
-
<Button
|
|
71
|
-
disabled={chosen === undefined}
|
|
72
|
-
onClick={() => {
|
|
73
|
-
choose(chosen);
|
|
74
|
-
}}
|
|
75
|
-
>
|
|
76
|
-
Open
|
|
77
|
-
</Button>
|
|
78
|
-
</>
|
|
79
|
-
}
|
|
80
|
-
onOpenChange={(open) => {
|
|
81
|
-
if (!open) {
|
|
82
|
-
answer(Answer.Canceled());
|
|
83
|
-
}
|
|
84
|
-
}}
|
|
85
|
-
open
|
|
86
|
-
screen={screen}
|
|
87
|
-
title={titleOf(body)}
|
|
88
|
-
>
|
|
89
|
-
<p className={askerStyles}>{`${asker} asks`}</p>
|
|
90
|
-
<div
|
|
91
|
-
aria-activedescendant={at === -1 ? undefined : optionId(listId, at)}
|
|
92
|
-
aria-label="Applications"
|
|
93
|
-
className={listStyles}
|
|
94
|
-
onKeyDown={(event) => {
|
|
95
|
-
const step = stepOf(event);
|
|
96
|
-
const next = apps[steppedTo(at, step ?? 0, apps.length)];
|
|
97
|
-
if (event.key === "Enter") {
|
|
98
|
-
event.preventDefault();
|
|
99
|
-
choose(chosen);
|
|
100
|
-
} else if (step !== undefined && next !== undefined) {
|
|
101
|
-
event.preventDefault();
|
|
102
|
-
setPicked(next.id);
|
|
103
|
-
}
|
|
104
|
-
}}
|
|
105
|
-
role="listbox"
|
|
106
|
-
tabIndex={0}
|
|
107
|
-
>
|
|
108
|
-
{apps.map((app, index) => (
|
|
109
|
-
// biome-ignore lint/a11y/useKeyWithClickEvents: the listbox handles the keyboard via `aria-activedescendant`
|
|
110
|
-
<div
|
|
111
|
-
aria-selected={app.id === chosen}
|
|
112
|
-
className={optionStyles}
|
|
113
|
-
id={optionId(listId, index)}
|
|
114
|
-
key={app.id}
|
|
115
|
-
onClick={() => {
|
|
116
|
-
setPicked(app.id);
|
|
117
|
-
}}
|
|
118
|
-
onDoubleClick={() => {
|
|
119
|
-
choose(app.id);
|
|
120
|
-
}}
|
|
121
|
-
ref={app.id === chosen ? keepInView : undefined}
|
|
122
|
-
role="option"
|
|
123
|
-
tabIndex={-1}
|
|
124
|
-
>
|
|
125
|
-
{app.icon === undefined ? (
|
|
126
|
-
<AppWindowIcon size={ICON_SIZE} />
|
|
127
|
-
) : (
|
|
128
|
-
<img
|
|
129
|
-
alt=""
|
|
130
|
-
className={iconStyles}
|
|
131
|
-
height={ICON_SIZE}
|
|
132
|
-
src={app.icon}
|
|
133
|
-
width={ICON_SIZE}
|
|
134
|
-
/>
|
|
135
|
-
)}
|
|
136
|
-
<span className={nameStyles}>{app.name}</span>
|
|
137
|
-
</div>
|
|
138
|
-
))}
|
|
139
|
-
</div>
|
|
140
|
-
</ModalDialog>
|
|
141
|
-
);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
145
|
-
|
|
146
|
-
const listStyles = flex({
|
|
147
|
-
direction: "column",
|
|
148
|
-
gap: 0.5,
|
|
149
|
-
marginBlockStart: 3,
|
|
150
|
-
maxBlockSize: 80,
|
|
151
|
-
overflowY: "auto",
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// As the file picker's rows.
|
|
155
|
-
const optionStyles = hstack({
|
|
156
|
-
"&[aria-selected=true]": {
|
|
157
|
-
backgroundImage:
|
|
158
|
-
"linear-gradient(to right, color-mix(in oklab, {colors.accent} 30%, transparent), color-mix(in oklab, {colors.accent} 8%, transparent))",
|
|
159
|
-
},
|
|
160
|
-
borderRadius: "md",
|
|
161
|
-
color: "foreground",
|
|
162
|
-
cursor: "pointer",
|
|
163
|
-
gap: 3,
|
|
164
|
-
paddingBlock: 1.5,
|
|
165
|
-
paddingInline: 2,
|
|
166
|
-
transition: "background-color {durations.fast} {easings.out}",
|
|
167
|
-
userSelect: "none",
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const iconStyles = css({ flexShrink: 0, objectFit: "contain" });
|
|
171
|
-
|
|
172
|
-
const nameStyles = css({
|
|
173
|
-
fontSize: "sm",
|
|
174
|
-
fontWeight: "medium",
|
|
175
|
-
overflow: "hidden",
|
|
176
|
-
textOverflow: "ellipsis",
|
|
177
|
-
whiteSpace: "nowrap",
|
|
178
|
-
});
|
|
37
|
+
}}
|
|
38
|
+
screen={screen}
|
|
39
|
+
system={system}
|
|
40
|
+
title={titleOf(body)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
179
43
|
|
|
180
44
|
/** What the dialog opens, as its title names it. */
|
|
181
45
|
const titleOf = ({ filename, uri }: AppChooserBody): string => {
|
|
182
46
|
const opened = filename ?? uri;
|
|
183
47
|
return opened === undefined ? "Open with" : `Open ${opened} with`;
|
|
184
48
|
};
|
|
185
|
-
|
|
186
|
-
const optionId = (listId: string, index: number): string =>
|
|
187
|
-
`${listId}-${String(index)}`;
|
|
File without changes
|
|
File without changes
|