@fungi.computer/tourtois 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +127 -2
- package/dist/AgentMcpServers.d.ts +34 -0
- package/dist/AgentMcpServers.js +124 -0
- package/dist/AppAccessConsent.d.ts +23 -0
- package/dist/AppAccessConsent.js +122 -0
- package/dist/AppCallPermissions.d.ts +9 -0
- package/dist/AppCallPermissions.js +21 -0
- package/dist/AppDesktopSettings.d.ts +11 -0
- package/dist/AppDesktopSettings.js +102 -0
- package/dist/AppPane.d.ts +37 -0
- package/dist/AppPane.js +184 -0
- package/dist/AppWorkbench.d.ts +44 -0
- package/dist/AppWorkbench.js +575 -0
- package/dist/AvatarPicker.d.ts +7 -0
- package/dist/AvatarPicker.js +78 -0
- package/dist/AvatarPreview.d.ts +10 -0
- package/dist/AvatarPreview.js +6 -0
- package/dist/DesktopMenuBar.d.ts +18 -0
- package/dist/DesktopMenuBar.js +23 -0
- package/dist/FocusedAppMenus.d.ts +2 -0
- package/dist/FocusedAppMenus.js +10 -0
- package/dist/Keys.d.ts +131 -0
- package/dist/Keys.js +201 -0
- package/dist/MushroomMark.d.ts +2 -0
- package/dist/MushroomMark.js +5 -0
- package/dist/SettingsHome.d.ts +18 -0
- package/dist/SettingsHome.js +48 -0
- package/dist/SettingsNavigation.d.ts +17 -0
- package/dist/SettingsNavigation.js +153 -0
- package/dist/SystemSettings.d.ts +24 -0
- package/dist/SystemSettings.js +177 -0
- package/dist/TeamDesktop.d.ts +36 -0
- package/dist/TeamDesktop.js +39 -0
- package/dist/WorkbenchAppBar.d.ts +5 -0
- package/dist/WorkbenchAppBar.js +12 -0
- package/dist/app-authority.d.ts +150 -0
- package/dist/app-authority.js +41 -0
- package/dist/app-directory-reconciliation.d.ts +14 -0
- package/dist/app-directory-reconciliation.js +72 -0
- package/dist/app-directory.d.ts +206 -0
- package/dist/app-directory.js +21 -0
- package/dist/app-icon.d.ts +9 -0
- package/dist/app-icon.js +27 -0
- package/dist/app-launch.d.ts +53 -0
- package/dist/app-launch.js +204 -0
- package/dist/app-menus.d.ts +6 -0
- package/dist/app-menus.js +81 -0
- package/dist/app-open-url.d.ts +8 -0
- package/dist/app-open-url.js +63 -0
- package/dist/app-surface.d.ts +62 -0
- package/dist/app-surface.js +10 -0
- package/dist/app-windows.d.ts +107 -0
- package/dist/app-windows.js +21 -0
- package/dist/chrome.css +153 -0
- package/dist/command-dialogs.d.ts +42 -0
- package/dist/command-dialogs.js +194 -0
- package/dist/desktop/Desktop.d.ts +34 -0
- package/dist/desktop/Desktop.js +275 -0
- package/dist/desktop/consent.d.ts +4 -0
- package/dist/desktop/consent.js +64 -0
- package/dist/desktop/gill-frame.d.ts +14 -0
- package/dist/desktop/gill-frame.js +41 -0
- package/dist/desktop/gill-param-picker.d.ts +15 -0
- package/dist/desktop/gill-param-picker.js +145 -0
- package/dist/desktop/launcher-gill.d.ts +20 -0
- package/dist/desktop/launcher-gill.js +49 -0
- package/dist/desktop/own-launchers.d.ts +9 -0
- package/dist/desktop/own-launchers.js +9 -0
- package/dist/desktop/row-gill.d.ts +24 -0
- package/dist/desktop/row-gill.js +136 -0
- package/dist/desktop-api.d.ts +63 -0
- package/dist/desktop-api.js +31 -0
- package/dist/desktop-client.d.ts +19 -0
- package/dist/desktop-client.js +127 -0
- package/dist/desktop-layout.d.ts +117 -0
- package/dist/desktop-layout.js +305 -0
- package/dist/desktop-visibility.d.ts +8 -0
- package/dist/desktop-visibility.js +5 -0
- package/dist/layers.d.ts +5 -0
- package/dist/layers.js +31 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +17 -0
- package/dist/member-avatar-cache.d.ts +15 -0
- package/dist/member-avatar-cache.js +69 -0
- package/dist/member-avatar.d.ts +39 -0
- package/dist/member-avatar.js +88 -0
- package/dist/menu-bar.d.ts +5 -0
- package/dist/menu-bar.js +5 -0
- package/dist/model.d.ts +6 -0
- package/dist/model.js +13 -0
- package/dist/oyster-platform-controls.d.ts +4 -0
- package/dist/oyster-platform-controls.js +107 -0
- package/dist/platform-controls.d.ts +84 -0
- package/dist/platform-controls.js +266 -0
- package/dist/queries.d.ts +7 -0
- package/dist/queries.js +7 -0
- package/dist/session-registry.d.ts +16 -0
- package/dist/session-registry.js +69 -0
- package/dist/session-selection.d.ts +6 -0
- package/dist/session-selection.js +22 -0
- package/dist/settings-leave-guard.d.ts +5 -0
- package/dist/settings-leave-guard.js +9 -0
- package/dist/settings-navigation.css +129 -0
- package/dist/settings-routes.d.ts +118 -0
- package/dist/settings-routes.js +216 -0
- package/dist/settings-sections.d.ts +74 -0
- package/dist/settings-sections.js +17 -0
- package/dist/shell-commands.d.ts +15 -0
- package/dist/shell-commands.js +95 -0
- package/dist/shell-host.d.ts +52 -0
- package/dist/shell-host.js +224 -0
- package/dist/shell-module.d.ts +12 -0
- package/dist/shell-module.js +17 -0
- package/dist/shell-release.d.ts +3 -0
- package/dist/shell-release.js +4 -0
- package/dist/shell.css +858 -0
- package/dist/shell.d.ts +6 -0
- package/dist/shell.js +83 -0
- package/dist/spa.css +11 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +51 -0
- package/dist/team-directory.d.ts +3 -0
- package/dist/team-directory.js +6 -0
- package/dist/use-window-motion.d.ts +3 -0
- package/dist/use-window-motion.js +37 -0
- package/dist/useAppOpenUrlHandoff.d.ts +11 -0
- package/dist/useAppOpenUrlHandoff.js +62 -0
- package/dist/useAppWindowManager.d.ts +323 -0
- package/dist/useAppWindowManager.js +47 -0
- package/dist/useDirectoryCommands.d.ts +12 -0
- package/dist/useDirectoryCommands.js +80 -0
- package/dist/workbench.d.ts +3 -0
- package/dist/workbench.js +2 -0
- package/package.json +222 -3
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { desktopSettingsHandler } from "./desktop/consent.js";
|
|
3
|
+
import { AppError } from "@fungi.computer/agar";
|
|
4
|
+
import {} from "@fungi.computer/caps/blocks/app-bar";
|
|
5
|
+
import { toast, Toaster } from "@fungi.computer/caps/components/toaster";
|
|
6
|
+
import { WindowSnapPreview } from "@fungi.computer/caps/components/window";
|
|
7
|
+
import { MaosaicoRegion, useLayer, useMaosaicoLayout, } from "@fungi.computer/maosaico";
|
|
8
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
9
|
+
import { createPortal } from "react-dom";
|
|
10
|
+
import { addressKey } from "@fungi.computer/oyster/shell/app-directory";
|
|
11
|
+
import { reconcileAppDirectory, reconcileAppWindowFromDirectory, } from "./app-directory-reconciliation.js";
|
|
12
|
+
import { useAppDirectory } from "./app-directory.js";
|
|
13
|
+
import { AppIcon } from "./app-icon.js";
|
|
14
|
+
import { appForRole, composeOpenRequests, createAppLaunchDescriptor, attachWindowComputer, launchComputerId, launchForComputer, launchForRoleIntent, requireHandledNavigation, resolveFileEditorWindow, } from "./app-launch.js";
|
|
15
|
+
import { useAppSurfaceHost, } from "./app-surface.js";
|
|
16
|
+
import { hasVisibleAppWindows, } from "./app-windows.js";
|
|
17
|
+
import { desktopVisibility } from "./desktop-visibility.js";
|
|
18
|
+
import { AppPane, FocusedWindowControls } from "./AppPane.js";
|
|
19
|
+
import { WorkbenchAppBar } from "./WorkbenchAppBar.js";
|
|
20
|
+
import { DesktopMenuBar } from "./DesktopMenuBar.js";
|
|
21
|
+
import { ShellCommandDialogs, useShellCommands } from "./shell-commands.js";
|
|
22
|
+
import { OwnSprite } from "./member-avatar.js";
|
|
23
|
+
import { Desktop } from "./desktop/Desktop.js";
|
|
24
|
+
import { useAppOpenUrlHandoff } from "./useAppOpenUrlHandoff.js";
|
|
25
|
+
import { useAppWindowManager } from "./useAppWindowManager.js";
|
|
26
|
+
import { useDirectoryCommands } from "./useDirectoryCommands.js";
|
|
27
|
+
const emptyAppBarLabel = (loading, failed) => {
|
|
28
|
+
if (loading)
|
|
29
|
+
return "Loading Apps…";
|
|
30
|
+
if (failed)
|
|
31
|
+
return "Apps unavailable";
|
|
32
|
+
return "Apps will appear here when available.";
|
|
33
|
+
};
|
|
34
|
+
/** Phone Home uses the same public shell command as the keyboard. */
|
|
35
|
+
function phoneShowDesktop(phone, covered, host, onFailure) {
|
|
36
|
+
if (!phone || !covered || !host)
|
|
37
|
+
return undefined;
|
|
38
|
+
return () => {
|
|
39
|
+
void host
|
|
40
|
+
.execute("shell:desktop.show", { origin: "menu" })
|
|
41
|
+
.then((outcome) => {
|
|
42
|
+
if (outcome.status !== "handled") {
|
|
43
|
+
console.error("Phone Show desktop refused", outcome);
|
|
44
|
+
onFailure("The desktop could not be shown. Try again.");
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch((cause) => {
|
|
48
|
+
console.error("Phone Show desktop failed", cause);
|
|
49
|
+
onFailure("The desktop could not be shown. Try again.");
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialComputerId, computers, computersUnavailable, extraPanes, initialFocusId, onLayoutChange, extraLaunchers = [], settingsLaunchers = [], systemMenuItems, directoryReader, takeAppOpenRequest, platform, desktopStorage, desktopMethods, gills, }) {
|
|
54
|
+
const setSettingsLocation = platform.openSettings;
|
|
55
|
+
const [appDialOpen, setAppDialOpen] = useState(false);
|
|
56
|
+
// The Layout menu's "Arrange desktop" command and the desktop's own
|
|
57
|
+
// controls share one state: this is their common ancestor.
|
|
58
|
+
const [desktopEditing, setDesktopEditing] = useState(false);
|
|
59
|
+
const appDialContainer = useLayer("popover");
|
|
60
|
+
const noticeContainer = useLayer("toast");
|
|
61
|
+
const layout = useMaosaicoLayout();
|
|
62
|
+
const { activeId, focus, mode, order } = layout;
|
|
63
|
+
const appSurfaceHost = useAppSurfaceHost();
|
|
64
|
+
const selectionRefreshes = useRef(new Map());
|
|
65
|
+
const appWindows = useAppWindowManager(appSurfaceHost, teamId, {
|
|
66
|
+
onNavigation: (input) => openNavigation(input),
|
|
67
|
+
onAppOpen: (input) => openApp(input),
|
|
68
|
+
onAppRole: (role) => {
|
|
69
|
+
const chosen = appForRole(appsQuery.data?.apps ?? [], directoryRoles, role);
|
|
70
|
+
return Promise.resolve(chosen ? { displayName: chosen.displayName ?? null } : null);
|
|
71
|
+
},
|
|
72
|
+
onSelectionChanged: (launch) => refreshAppDirectory(launch),
|
|
73
|
+
// Raise only: the App already holds keyboard focus inside its frame.
|
|
74
|
+
onAppFocus: (launch) => {
|
|
75
|
+
focus(launch.windowId);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const appProjection = useSyncExternalStore(appWindows.subscribe, appWindows.read, appWindows.read);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
for (const window of appProjection.windows)
|
|
81
|
+
if (window.status === "ready")
|
|
82
|
+
selectionRefreshes.current.delete(window.windowId);
|
|
83
|
+
}, [appProjection]);
|
|
84
|
+
const [appStatus, setAppStatus] = useState(null);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!appStatus)
|
|
87
|
+
return;
|
|
88
|
+
const disconnected = appStatus instanceof AppError && appStatus.code === "DISCONNECTED";
|
|
89
|
+
let message = appStatus instanceof Error
|
|
90
|
+
? "This App couldn’t complete that request."
|
|
91
|
+
: appStatus;
|
|
92
|
+
if (disconnected)
|
|
93
|
+
message = "This App lost its connection.";
|
|
94
|
+
const notice = {
|
|
95
|
+
id: "workbench-status",
|
|
96
|
+
toasterId: "workbench-notices",
|
|
97
|
+
closeButton: true,
|
|
98
|
+
dismissible: true,
|
|
99
|
+
};
|
|
100
|
+
if (disconnected) {
|
|
101
|
+
toast(message, {
|
|
102
|
+
...notice,
|
|
103
|
+
duration: Infinity,
|
|
104
|
+
description: "Reload the workspace to reconnect.",
|
|
105
|
+
action: {
|
|
106
|
+
label: "Reload",
|
|
107
|
+
onClick: () => {
|
|
108
|
+
window.location.reload();
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
toast(message, notice);
|
|
115
|
+
}, [appStatus]);
|
|
116
|
+
const reconciledDirectoryRef = useRef(null);
|
|
117
|
+
const focusWindow = useCallback((id) => {
|
|
118
|
+
focus(id);
|
|
119
|
+
requestAnimationFrame(() => {
|
|
120
|
+
const pane = [
|
|
121
|
+
...(paneLayerRef.current?.querySelectorAll("[data-window-id]") ?? []),
|
|
122
|
+
].find((candidate) => candidate.dataset.windowId === id);
|
|
123
|
+
pane
|
|
124
|
+
?.querySelector("[data-caps-window-handle]")
|
|
125
|
+
?.focus();
|
|
126
|
+
});
|
|
127
|
+
}, [focus, paneLayerRef]);
|
|
128
|
+
const afterHide = (id) => {
|
|
129
|
+
if (layout.zoomedId === id)
|
|
130
|
+
layout.zoom(null);
|
|
131
|
+
const next = appWindows
|
|
132
|
+
.read()
|
|
133
|
+
.windows.filter((item) => item.windowId !== id && item.status !== "minimized")
|
|
134
|
+
.at(-1);
|
|
135
|
+
if (next)
|
|
136
|
+
focusWindow(next.windowId);
|
|
137
|
+
else
|
|
138
|
+
requestAnimationFrame(() => {
|
|
139
|
+
paneLayerRef.current
|
|
140
|
+
?.closest(".tourtois-workbench-layout")
|
|
141
|
+
?.querySelector(".caps-app-bar-launcher-button")
|
|
142
|
+
?.focus();
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
const activeWindow = appProjection.windows.find((item) => item.windowId === activeId && item.status !== "minimized");
|
|
146
|
+
const activeSurfaceId = activeWindow?.launch.surfaceId ?? null;
|
|
147
|
+
// The App owns its history; a refusal or failure leaves Back in place.
|
|
148
|
+
const goBack = (windowId) => {
|
|
149
|
+
void appWindows.back(windowId).catch((cause) => {
|
|
150
|
+
console.error("App workbench operation failed", cause);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
// The shell's window keys run through Whistle so /keys, the `:` line and
|
|
154
|
+
// which-key list them; these are the behaviors behind those commands.
|
|
155
|
+
const stepTarget = (step) => order.length < 2
|
|
156
|
+
? undefined
|
|
157
|
+
: order[(Math.max(0, order.indexOf(activeId ?? "")) + step + order.length) %
|
|
158
|
+
order.length];
|
|
159
|
+
const desktopOwner = useRef(null);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
let retired = false;
|
|
162
|
+
let registration;
|
|
163
|
+
const close = (value) => {
|
|
164
|
+
if (value)
|
|
165
|
+
void value.close().catch((cause) => {
|
|
166
|
+
console.error("Desktop settings registration close failed", cause);
|
|
167
|
+
return { kind: "failed", cause };
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
void platform
|
|
171
|
+
.registerDesktopSettings(desktopSettingsHandler(() => desktopOwner.current))
|
|
172
|
+
.then((value) => {
|
|
173
|
+
if (retired)
|
|
174
|
+
close(value);
|
|
175
|
+
else
|
|
176
|
+
registration = value;
|
|
177
|
+
})
|
|
178
|
+
.catch((cause) => {
|
|
179
|
+
console.error("Desktop settings registration failed", cause);
|
|
180
|
+
setAppStatus("Desktop settings are unavailable. Reopen the desktop to try again.");
|
|
181
|
+
return { kind: "failed", cause };
|
|
182
|
+
});
|
|
183
|
+
return () => {
|
|
184
|
+
retired = true;
|
|
185
|
+
close(registration);
|
|
186
|
+
};
|
|
187
|
+
}, [platform]);
|
|
188
|
+
const receiveDesktopOwner = useCallback((owner) => {
|
|
189
|
+
desktopOwner.current = owner;
|
|
190
|
+
}, []);
|
|
191
|
+
const shellCommands = useShellCommands(appSurfaceHost, {
|
|
192
|
+
openApp: () => {
|
|
193
|
+
setAppDialOpen(true);
|
|
194
|
+
},
|
|
195
|
+
showDesktop: async () => {
|
|
196
|
+
layout.zoom(null);
|
|
197
|
+
if (hasVisibleAppWindows(appWindows.read())) {
|
|
198
|
+
const hiding = appWindows.hideAll();
|
|
199
|
+
afterHide("");
|
|
200
|
+
await hiding;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
await appWindows.restoreHidden();
|
|
204
|
+
const restored = appWindows
|
|
205
|
+
.read()
|
|
206
|
+
.windows.find((item) => item.status !== "minimized");
|
|
207
|
+
if (restored)
|
|
208
|
+
focusWindow(restored.windowId);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
focusStep: (step) => {
|
|
212
|
+
const next = stepTarget(step);
|
|
213
|
+
if (!next)
|
|
214
|
+
return;
|
|
215
|
+
layout.zoom(null);
|
|
216
|
+
focusWindow(next);
|
|
217
|
+
},
|
|
218
|
+
swapStep: (step) => {
|
|
219
|
+
const next = stepTarget(step);
|
|
220
|
+
if (next && activeId)
|
|
221
|
+
layout.swap(activeId, next);
|
|
222
|
+
},
|
|
223
|
+
toggleZoom: () => {
|
|
224
|
+
if (activeId)
|
|
225
|
+
layout.zoom(layout.zoomedId === activeId ? null : activeId);
|
|
226
|
+
},
|
|
227
|
+
back: () => {
|
|
228
|
+
if (activeId)
|
|
229
|
+
goBack(activeId);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
appSurfaceHost?.focus?.(activeSurfaceId);
|
|
234
|
+
}, [appSurfaceHost, activeSurfaceId, activeWindow?.status]);
|
|
235
|
+
const { query: appsQuery, apps: listedApps, needsUpdate, roles: directoryRoles, } = useAppDirectory(teamId, directoryReader);
|
|
236
|
+
const refetchApps = appsQuery.refetch;
|
|
237
|
+
useEffect(() => platform.subscribeChanges(() => {
|
|
238
|
+
void refetchApps();
|
|
239
|
+
}), [platform, refetchApps]);
|
|
240
|
+
const launchFor = (installation, currentLaunch) => {
|
|
241
|
+
return createAppLaunchDescriptor(installation, currentLaunch ? launchComputerId(currentLaunch.href) : initialComputerId, window.location.origin, computers, computersUnavailable);
|
|
242
|
+
};
|
|
243
|
+
const refreshAppDirectory = async (launch) => {
|
|
244
|
+
const attempts = selectionRefreshes.current.get(launch.windowId) ?? 0;
|
|
245
|
+
if (attempts >= 3)
|
|
246
|
+
return false;
|
|
247
|
+
selectionRefreshes.current.set(launch.windowId, attempts + 1);
|
|
248
|
+
const refreshed = await appsQuery.refetch({ throwOnError: true });
|
|
249
|
+
const apps = refreshed.data?.apps ?? [];
|
|
250
|
+
reconciledDirectoryRef.current = apps;
|
|
251
|
+
const failure = { action: null };
|
|
252
|
+
const replaced = await reconcileAppWindowFromDirectory(appWindows, launch, apps, launchFor, { kind: "selection-changed" }, (action) => {
|
|
253
|
+
failure.action = action;
|
|
254
|
+
});
|
|
255
|
+
if (!replaced && failure.action === null)
|
|
256
|
+
setAppStatus("The App selection changed. This window stayed open; close it after saving or discarding work, then reopen the App.");
|
|
257
|
+
else if (!replaced)
|
|
258
|
+
setAppStatus(failure.action === "close"
|
|
259
|
+
? "App could not be closed."
|
|
260
|
+
: "App could not be replaced.");
|
|
261
|
+
return replaced;
|
|
262
|
+
};
|
|
263
|
+
const openNavigation = async (input) => {
|
|
264
|
+
if (!appSurfaceHost)
|
|
265
|
+
throw new Error("Open an editor App before opening this file.");
|
|
266
|
+
const editor = await resolveFileEditorWindow({
|
|
267
|
+
apps: listedApps ?? [],
|
|
268
|
+
roles: directoryRoles,
|
|
269
|
+
appWindows,
|
|
270
|
+
appSurfaceHost,
|
|
271
|
+
computerId: input.computerId,
|
|
272
|
+
computers,
|
|
273
|
+
origin: window.location.origin,
|
|
274
|
+
onPermissionNeeded: (app) => {
|
|
275
|
+
setSettingsLocation({
|
|
276
|
+
category: "apps",
|
|
277
|
+
appKey: addressKey(app.address),
|
|
278
|
+
section: "permissions",
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const { launch } = editor;
|
|
283
|
+
const previousLaunch = launch;
|
|
284
|
+
const nextLaunch = launchForComputer(launch, input.computerId);
|
|
285
|
+
try {
|
|
286
|
+
await appWindows.restore(launch.windowId);
|
|
287
|
+
// File navigation carries the source Computer. Reconfigure Inkcap's
|
|
288
|
+
// mounted host first so its open-file command receives the exact
|
|
289
|
+
// target; the existing iframe and command contribution stay in place.
|
|
290
|
+
await appWindows.reconfigure(launch.windowId, nextLaunch);
|
|
291
|
+
const mounted = appWindows
|
|
292
|
+
.read()
|
|
293
|
+
.windows.find((window) => window.windowId === launch.windowId);
|
|
294
|
+
if (mounted?.launch.href !== nextLaunch.href)
|
|
295
|
+
throw new Error("The editor could not use the source Computer.");
|
|
296
|
+
layout.zoom(null);
|
|
297
|
+
focusWindow(launch.windowId);
|
|
298
|
+
appSurfaceHost.focus?.(launch.surfaceId);
|
|
299
|
+
const commandId = `${launch.surfaceId}:open-file`;
|
|
300
|
+
const outcome = await appSurfaceHost.execute(commandId, {
|
|
301
|
+
origin: "programmatic",
|
|
302
|
+
arguments: input,
|
|
303
|
+
});
|
|
304
|
+
requireHandledNavigation(outcome);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (appWindows
|
|
308
|
+
.read()
|
|
309
|
+
.windows.some((window) => window.windowId === launch.windowId &&
|
|
310
|
+
window.launch.href === nextLaunch.href))
|
|
311
|
+
await appWindows
|
|
312
|
+
.reconfigure(launch.windowId, previousLaunch)
|
|
313
|
+
.catch((cause) => {
|
|
314
|
+
console.error("App workbench operation failed", cause);
|
|
315
|
+
return undefined;
|
|
316
|
+
});
|
|
317
|
+
if (editor.opened)
|
|
318
|
+
await appWindows.close(launch.windowId).catch((cause) => {
|
|
319
|
+
console.error("App workbench operation failed", cause);
|
|
320
|
+
return undefined;
|
|
321
|
+
});
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const openAppPermissions = (surfaceId) => {
|
|
326
|
+
const app = directory.find((item) => item.surfaceId === surfaceId);
|
|
327
|
+
setSettingsLocation({
|
|
328
|
+
category: "apps",
|
|
329
|
+
appKey: app ? addressKey(app.address) : null,
|
|
330
|
+
section: "permissions",
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
/** The person chose another Computer in one window's status bar. */
|
|
334
|
+
const selectWindowComputer = async (windowId, computerId) => {
|
|
335
|
+
setAppStatus(null);
|
|
336
|
+
try {
|
|
337
|
+
await attachWindowComputer(appWindows, windowId, computerId);
|
|
338
|
+
}
|
|
339
|
+
catch (cause) {
|
|
340
|
+
console.error("App workbench operation failed", cause);
|
|
341
|
+
setAppStatus("Computer unavailable. Review this App's permissions.");
|
|
342
|
+
const surfaceId = appWindows
|
|
343
|
+
.read()
|
|
344
|
+
.windows.find((item) => item.windowId === windowId)?.launch.surfaceId;
|
|
345
|
+
if (surfaceId)
|
|
346
|
+
openAppPermissions(surfaceId);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const openApp = async (input) => {
|
|
350
|
+
const current = await appsQuery.refetch({ throwOnError: true });
|
|
351
|
+
const requested = launchForRoleIntent(current.data?.apps ?? [], directoryRoles, input, composeOpenRequests.request.generate(), (app) => launchFor(app));
|
|
352
|
+
const existing = appWindows
|
|
353
|
+
.read()
|
|
354
|
+
.windows.some((item) => item.windowId === requested.windowId);
|
|
355
|
+
if (existing) {
|
|
356
|
+
await appWindows.restore(requested.windowId);
|
|
357
|
+
await appWindows.reconfigure(requested.windowId, requested);
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
await appWindows.open(requested);
|
|
361
|
+
layout.zoom(null);
|
|
362
|
+
focusWindow(requested.windowId);
|
|
363
|
+
};
|
|
364
|
+
useAppOpenUrlHandoff({
|
|
365
|
+
teamId,
|
|
366
|
+
host: appSurfaceHost,
|
|
367
|
+
ready: appsQuery.isSuccess,
|
|
368
|
+
takeAppOpenRequest,
|
|
369
|
+
onOpen: openApp,
|
|
370
|
+
setError: setAppStatus,
|
|
371
|
+
});
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (initialFocusId)
|
|
374
|
+
focus(initialFocusId);
|
|
375
|
+
}, [focus, initialFocusId]);
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (!listedApps)
|
|
378
|
+
return;
|
|
379
|
+
if (reconciledDirectoryRef.current === listedApps) {
|
|
380
|
+
reconciledDirectoryRef.current = null;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
void reconcileAppDirectory(appWindows, listedApps, launchFor, (action) => {
|
|
384
|
+
setAppStatus(action === "close"
|
|
385
|
+
? "App could not be closed."
|
|
386
|
+
: "App could not be replaced.");
|
|
387
|
+
});
|
|
388
|
+
}, [appWindows, listedApps, initialComputerId, computers]);
|
|
389
|
+
const directory = useMemo(() => listedApps ?? [], [listedApps]);
|
|
390
|
+
const launchApp = (installation, requestedLaunch) => {
|
|
391
|
+
const windowId = "app:" + installation.surfaceId;
|
|
392
|
+
const launch = requestedLaunch ?? launchFor(installation);
|
|
393
|
+
if (!launch) {
|
|
394
|
+
setAppStatus(installation.availability !== "available"
|
|
395
|
+
? `${installation.displayName ?? "App"} is unavailable.`
|
|
396
|
+
: `${installation.displayName ?? "App"} has no available surface.`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
setAppStatus(null);
|
|
400
|
+
performance.mark("fungi.app-open.tourtois-launch-start");
|
|
401
|
+
const existing = appWindows
|
|
402
|
+
.read()
|
|
403
|
+
.windows.find((item) => item.windowId === windowId);
|
|
404
|
+
const opening = existing && launch.openRequest !== null
|
|
405
|
+
? appWindows
|
|
406
|
+
.restore(windowId)
|
|
407
|
+
.then(() => appWindows.reconfigure(windowId, launch))
|
|
408
|
+
: appWindows.open(launch);
|
|
409
|
+
void opening
|
|
410
|
+
.then(() => {
|
|
411
|
+
performance.mark("fungi.app-open.tourtois-launch-ready");
|
|
412
|
+
layout.zoom(null);
|
|
413
|
+
focusWindow(windowId);
|
|
414
|
+
})
|
|
415
|
+
.catch((cause) => {
|
|
416
|
+
console.error("App workbench operation failed", cause);
|
|
417
|
+
setAppStatus(`${installation.displayName ?? "App"} could not be opened.`);
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
useDirectoryCommands({
|
|
421
|
+
host: appSurfaceHost,
|
|
422
|
+
directory,
|
|
423
|
+
manager: appWindows,
|
|
424
|
+
launchFor,
|
|
425
|
+
onFocus: (windowId) => {
|
|
426
|
+
layout.zoom(null);
|
|
427
|
+
focusWindow(windowId);
|
|
428
|
+
},
|
|
429
|
+
onError: setAppStatus,
|
|
430
|
+
});
|
|
431
|
+
// A dashboard App opens as the Team's workspace, never as a window in it.
|
|
432
|
+
const windowApps = useMemo(() => directory.filter((installation) => !installation.roles.includes("dashboard")), [directory]);
|
|
433
|
+
const launchableItems = windowApps
|
|
434
|
+
.filter((installation) => launchFor(installation) !== null)
|
|
435
|
+
.map((installation) => {
|
|
436
|
+
const windowId = "app:" + installation.surfaceId;
|
|
437
|
+
return {
|
|
438
|
+
id: windowId,
|
|
439
|
+
label: installation.displayName ?? "App",
|
|
440
|
+
icon: _jsx(AppIcon, { roles: installation.roles, icon: installation.icon }),
|
|
441
|
+
onSelect: () => {
|
|
442
|
+
launchApp(installation);
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
const appLaunchers = launchableItems.filter((launcher) => !appProjection.windows.some((window) => window.windowId === launcher.id));
|
|
447
|
+
appLaunchers.push(...settingsLaunchers.map((launcher) => {
|
|
448
|
+
const item = {
|
|
449
|
+
id: launcher.id,
|
|
450
|
+
label: launcher.label,
|
|
451
|
+
onSelect: () => {
|
|
452
|
+
setSettingsLocation(launcher.location);
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
return launcher.icon ? { ...item, icon: launcher.icon } : item;
|
|
456
|
+
}), ...extraLaunchers);
|
|
457
|
+
const inboxSurfaceId = useMemo(() => appForRole(directory, directoryRoles, "inbox")?.surfaceId ?? null, [directory, directoryRoles]);
|
|
458
|
+
const visibility = desktopVisibility({
|
|
459
|
+
hasVisibleWindows: hasVisibleAppWindows(appProjection) ||
|
|
460
|
+
order.some((id) => !appProjection.windows.some((window) => window.windowId === id)),
|
|
461
|
+
mode,
|
|
462
|
+
zoomedId: layout.zoomedId,
|
|
463
|
+
phone,
|
|
464
|
+
});
|
|
465
|
+
const covered = visibility === "covered";
|
|
466
|
+
const desktopRef = useRef(null);
|
|
467
|
+
useLayoutEffect(() => {
|
|
468
|
+
if (desktopRef.current)
|
|
469
|
+
desktopRef.current.inert = covered;
|
|
470
|
+
}, [covered]);
|
|
471
|
+
return (_jsxs("div", { className: "tourtois-workbench-layout", children: [_jsx(DesktopMenuBar, { onShowDesktop: phoneShowDesktop(phone, covered, appSurfaceHost, setAppStatus), teamName: teamName, appBar: _jsx(WorkbenchAppBar, { phone: phone, container: appDialContainer, isOpen: appDialOpen, launcherLabels: "wrap", onOpenChange: setAppDialOpen, emptyLabel: emptyAppBarLabel(appsQuery.isLoading, appsQuery.isError), windows: appProjection.windows.map((projection) => ({
|
|
472
|
+
id: projection.windowId,
|
|
473
|
+
label: projection.launch.title,
|
|
474
|
+
icon: (_jsx(AppIcon, { roles: projection.launch.roles, icon: directory.find((app) => app.surfaceId === projection.launch.surfaceId)?.icon })),
|
|
475
|
+
active: activeId === projection.windowId,
|
|
476
|
+
onSelect: () => {
|
|
477
|
+
void appWindows
|
|
478
|
+
.restore(projection.windowId)
|
|
479
|
+
.then(() => {
|
|
480
|
+
layout.zoom(null);
|
|
481
|
+
focusWindow(projection.windowId);
|
|
482
|
+
})
|
|
483
|
+
.catch((cause) => {
|
|
484
|
+
console.error("App workbench operation failed", cause);
|
|
485
|
+
setAppStatus("App could not be restored.");
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
onClose: () => {
|
|
489
|
+
void appWindows
|
|
490
|
+
.close(projection.windowId)
|
|
491
|
+
.then(() => {
|
|
492
|
+
if (!appWindows
|
|
493
|
+
.read()
|
|
494
|
+
.windows.some((item) => item.windowId === projection.windowId))
|
|
495
|
+
afterHide(projection.windowId);
|
|
496
|
+
})
|
|
497
|
+
.catch((cause) => {
|
|
498
|
+
console.error("App workbench operation failed", cause);
|
|
499
|
+
setAppStatus("App could not be closed.");
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
})), launchers: appLaunchers, launcher: {
|
|
503
|
+
label: "Open Apps",
|
|
504
|
+
icon: (_jsx(OwnSprite, { "aria-hidden": "true", style: { width: "100%", height: "100%" } })),
|
|
505
|
+
} }), onOpenSettings: () => {
|
|
506
|
+
setSettingsLocation({ category: "home" });
|
|
507
|
+
}, onEditAvatar: () => {
|
|
508
|
+
setSettingsLocation({ category: "avatar" });
|
|
509
|
+
}, focusedId: activeWindow?.windowId ?? null, title: activeWindow?.launch.title ?? "Apps", tiled: mode === "tiled", onLayoutChange: onLayoutChange
|
|
510
|
+
? (tiled) => {
|
|
511
|
+
layout.zoom(null);
|
|
512
|
+
onLayoutChange(tiled);
|
|
513
|
+
}
|
|
514
|
+
: undefined, onRestoreLayout: layout.zoomedId
|
|
515
|
+
? () => {
|
|
516
|
+
layout.zoom(null);
|
|
517
|
+
}
|
|
518
|
+
: undefined, onArrangeDesktop: () => {
|
|
519
|
+
setDesktopEditing(true);
|
|
520
|
+
}, focusedControls: activeWindow ? (_jsx(FocusedWindowControls, { projection: activeWindow, app: directory.find((app) => app.surfaceId === activeWindow.launch.surfaceId), onOpenSettings: () => {
|
|
521
|
+
const app = directory.find((item) => item.surfaceId === activeWindow.launch.surfaceId);
|
|
522
|
+
setSettingsLocation({
|
|
523
|
+
category: "apps",
|
|
524
|
+
appKey: app ? addressKey(app.address) : null,
|
|
525
|
+
section: "advanced",
|
|
526
|
+
});
|
|
527
|
+
}, onOpenPermissions: () => {
|
|
528
|
+
openAppPermissions(activeWindow.launch.surfaceId);
|
|
529
|
+
} }, activeWindow.windowId)) : null, systemMenuItems: systemMenuItems }), noticeContainer
|
|
530
|
+
? createPortal(_jsx(Toaster, { id: "workbench-notices", position: "bottom-right", offset: 24, mobileOffset: 24, closeButton: true }), noticeContainer)
|
|
531
|
+
: null, _jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx(MaosaicoRegion, { layer: "desktop", style: { position: "absolute", inset: 0 }, "data-desktop-visibility": visibility, ref: desktopRef, "aria-hidden": covered ? true : undefined, children: _jsx(Desktop, { phone: phone, editing: desktopEditing, onEditingChange: setDesktopEditing, inboxSurfaceId: inboxSurfaceId, onOwner: receiveDesktopOwner, launcherItems: launchableItems, onAllApps: () => {
|
|
532
|
+
setAppDialOpen(true);
|
|
533
|
+
}, paused: covered, apps: windowApps, needsUpdate: needsUpdate, onAppSettings: (app) => {
|
|
534
|
+
setSettingsLocation({
|
|
535
|
+
category: "apps",
|
|
536
|
+
appKey: addressKey(app.address),
|
|
537
|
+
section: "advanced",
|
|
538
|
+
});
|
|
539
|
+
}, loading: appsQuery.isLoading, unavailable: appsQuery.isError, onOpen: (input) => {
|
|
540
|
+
void openApp(input).catch((cause) => {
|
|
541
|
+
console.error("Gill destination could not be opened", cause);
|
|
542
|
+
setAppStatus("The widget’s App could not be opened.");
|
|
543
|
+
});
|
|
544
|
+
}, computersForGill: (surfaceId) => {
|
|
545
|
+
const app = windowApps.find((app) => app.surfaceId === surfaceId);
|
|
546
|
+
return app ? (launchFor(app)?.computers ?? []) : [];
|
|
547
|
+
}, storage: desktopStorage, methods: desktopMethods, transport: gills, onRetryApps: () => {
|
|
548
|
+
void appsQuery.refetch();
|
|
549
|
+
} }) }), _jsxs(MaosaicoRegion, { layer: "windows", style: { height: "100%", pointerEvents: "none" }, ref: paneLayerRef, className: "tourtois-pane-layer", "data-layout-mode": mode, children: [appProjection.windows.map((projection, index) => (_jsx(AppPane, { index: index, focusWindow: focusWindow, afterHide: afterHide, onBack: goBack, manager: appWindows, projection: projection, icon: directory.find((app) => app.surfaceId === projection.launch.surfaceId)?.icon, onError: setAppStatus, onSelectComputer: (windowId, computerId) => {
|
|
550
|
+
void selectWindowComputer(windowId, computerId);
|
|
551
|
+
} }, projection.windowId))), extraPanes?.({ activeId, order, focus }), layout.snapPreview && (_jsx(WindowSnapPreview, { "data-snap-side": layout.snapPreview.side, style: {
|
|
552
|
+
left: layout.snapPreview.frame.x,
|
|
553
|
+
top: layout.snapPreview.frame.y,
|
|
554
|
+
width: layout.snapPreview.frame.width,
|
|
555
|
+
height: layout.snapPreview.frame.height,
|
|
556
|
+
...layout.stackingStyle,
|
|
557
|
+
} })), layout.divider ? (_jsx("div", { className: "tourtois-divider", role: "separator", "aria-label": "Main window width", "aria-orientation": "vertical", "aria-valuemin": Math.round(layout.divider.min), "aria-valuemax": Math.round(layout.divider.max), "aria-valuenow": Math.round(layout.divider.position), tabIndex: 0, style: { left: layout.divider.position, ...layout.stackingStyle }, ...layout.divider.props, onKeyDown: (event) => {
|
|
558
|
+
if (!layout.divider)
|
|
559
|
+
return;
|
|
560
|
+
const step = new Map([
|
|
561
|
+
["ArrowLeft", -32],
|
|
562
|
+
["ArrowRight", 32],
|
|
563
|
+
]).get(event.key) ?? 0;
|
|
564
|
+
if (step) {
|
|
565
|
+
event.preventDefault();
|
|
566
|
+
event.stopPropagation();
|
|
567
|
+
layout.resizeMain(layout.divider.position + step);
|
|
568
|
+
}
|
|
569
|
+
} })) : null] })] }), _jsx(ShellCommandDialogs, { host: appSurfaceHost, state: shellCommands, focusedWindow: activeWindow, directoryApps: directory.map((app) => ({
|
|
570
|
+
commands: app.commands,
|
|
571
|
+
name: app.displayName ?? "App",
|
|
572
|
+
roles: app.roles,
|
|
573
|
+
surfaceId: app.surfaceId,
|
|
574
|
+
})), onStatus: setAppStatus, finalFocus: () => document.querySelector(".caps-app-bar-launcher-button") })] }));
|
|
575
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type MemberAvatar } from "@fungi.computer/sprite-zdog/avatar";
|
|
2
|
+
/** Catalog-driven controls; the account owner handles saving and rollback. */
|
|
3
|
+
export declare function AvatarPicker({ value, disabled, onChange, }: {
|
|
4
|
+
value: MemberAvatar;
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
onChange: (next: MemberAvatar) => void;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Card } from "@fungi.computer/caps/components/card";
|
|
3
|
+
import { DataListHeading } from "@fungi.computer/caps/components/data-list";
|
|
4
|
+
import { RadioGroup, RadioGroupItem, RadioGroupCard, } from "@fungi.computer/caps/components/radio-group";
|
|
5
|
+
import { Switch } from "@fungi.computer/caps/components/switch";
|
|
6
|
+
import { Input } from "@fungi.computer/caps/components/input";
|
|
7
|
+
import { Field } from "@fungi.computer/caps/components/field";
|
|
8
|
+
import { renderStyles, renderStyleLabels, renderStyleSchema, resolveAppearance, } from "@fungi.computer/sprite";
|
|
9
|
+
import { avatarCharacters, avatarEnvelopeSchema, } from "@fungi.computer/sprite/avatar";
|
|
10
|
+
import { avatarCatalog } from "@fungi.computer/sprite-zdog/catalog";
|
|
11
|
+
import { memberAvatarSchema, deriveAgentAvatar, } from "@fungi.computer/sprite-zdog/avatar";
|
|
12
|
+
import { AvatarPreview } from "./AvatarPreview.js";
|
|
13
|
+
const names = { mushroom: "Shiitake", cat: "Cat", burger: "Burger" };
|
|
14
|
+
const swatches = [
|
|
15
|
+
["Rose", "#f98284", "#ffe6c6"],
|
|
16
|
+
["Peach", "#f6a55c", "#ffe9c9"],
|
|
17
|
+
["Gold", "#f2d06b", "#fff6d1"],
|
|
18
|
+
["Leaf", "#9bd47a", "#e6f8d8"],
|
|
19
|
+
["Mint", "#5fc7b4", "#d9f6ef"],
|
|
20
|
+
["Sky", "#6aaee8", "#dcefff"],
|
|
21
|
+
["Lilac", "#9b8cf0", "#ebe6ff"],
|
|
22
|
+
["Plum", "#e58bd6", "#fbe2f6"],
|
|
23
|
+
["Clay", "#c98c6a", "#f6e2d4"],
|
|
24
|
+
["Slate", "#8a8f9c", "#e3e5ea"],
|
|
25
|
+
];
|
|
26
|
+
const exampleNames = ["Gardener", "Scout", "Ledger", "Pip", "Moss"];
|
|
27
|
+
/** Catalog-driven controls; the account owner handles saving and rollback. */
|
|
28
|
+
export function AvatarPicker({ value, disabled, onChange, }) {
|
|
29
|
+
const { avatar, agentColors } = value;
|
|
30
|
+
const catalog = avatarCatalog[avatar.character];
|
|
31
|
+
const resolved = resolveAppearance(catalog.controls, avatar.appearance);
|
|
32
|
+
const change = (next) => {
|
|
33
|
+
onChange(memberAvatarSchema.parse({ ...value, avatar: { ...avatar, ...next } }));
|
|
34
|
+
};
|
|
35
|
+
const primarySlot = catalog.identitySlots[0];
|
|
36
|
+
const selectedSwatch = swatches.find(([, primary, secondary]) => catalog.identitySlots.every((slot, index) => resolved[slot] === (index === 0 ? primary : secondary)))?.[0] ?? "";
|
|
37
|
+
return (_jsxs("div", { className: "tourtois-avatar-picker", children: [_jsxs(Card, { className: "tourtois-avatar-preview", children: [_jsx(AvatarPreview, { avatar: avatar, className: "tourtois-avatar-large" }), _jsx("strong", { children: "Your avatar" }), _jsx("p", { children: "One look for your account, across Teams." })] }), _jsxs("fieldset", { disabled: disabled, className: "tourtois-avatar-controls", children: [_jsx("legend", { className: "sr-only", children: "Customize your avatar" }), _jsxs("section", { "aria-labelledby": "avatar-character", children: [_jsx(DataListHeading, { level: 3, id: "avatar-character", children: "Character" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-character", className: "tourtois-avatar-characters", orientation: "horizontal", value: avatar.character, onValueChange: (raw) => {
|
|
38
|
+
const character = avatarEnvelopeSchema.shape.character.parse(raw);
|
|
39
|
+
change({ character, appearance: {}, expression: "normal" });
|
|
40
|
+
}, children: avatarCharacters.map((character) => (_jsx(RadioGroupCard, { value: character, title: names[character], description: _jsx(AvatarPreview, { avatar: {
|
|
41
|
+
...avatar,
|
|
42
|
+
character,
|
|
43
|
+
appearance: character === avatar.character ? avatar.appearance : {},
|
|
44
|
+
expression: "normal",
|
|
45
|
+
}, className: "tourtois-avatar-character-preview" }) }, character))) })] }), _jsxs("section", { "aria-labelledby": "avatar-colours", children: [_jsx(DataListHeading, { level: 3, id: "avatar-colours", children: avatar.character === "mushroom" ? "Cap colour" : "Colour" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-colours", className: "tourtois-avatar-swatches", orientation: "horizontal", value: selectedSwatch, onValueChange: (name) => {
|
|
46
|
+
const swatch = swatches.find(([label]) => label === name);
|
|
47
|
+
if (!swatch)
|
|
48
|
+
return;
|
|
49
|
+
change({
|
|
50
|
+
appearance: {
|
|
51
|
+
...avatar.appearance,
|
|
52
|
+
...Object.fromEntries(catalog.identitySlots.map((slot, index) => [
|
|
53
|
+
slot,
|
|
54
|
+
swatch[index === 0 ? 1 : 2],
|
|
55
|
+
])),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}, children: swatches.map(([name, color]) => (_jsx("label", { title: name, className: "tourtois-avatar-swatch", style: { backgroundColor: color }, children: _jsx(RadioGroupItem, { value: name, "aria-label": name }) }, name))) }), _jsx("div", { className: "tourtois-avatar-custom-colours", children: catalog.identitySlots.map((slot) => {
|
|
59
|
+
const control = catalog.controls[slot];
|
|
60
|
+
if (control?.type !== "color")
|
|
61
|
+
return null;
|
|
62
|
+
return (_jsx(Field, { label: slot === primarySlot ? "Custom colour" : "Accent colour", children: _jsx(Input, { type: "color", defaultValue: String(resolved[slot]), onBlur: (event) => {
|
|
63
|
+
if (event.currentTarget.value !== resolved[slot])
|
|
64
|
+
change({
|
|
65
|
+
appearance: {
|
|
66
|
+
...avatar.appearance,
|
|
67
|
+
[slot]: event.currentTarget.value,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
} }) }, `${avatar.character}:${slot}:${String(resolved[slot])}`));
|
|
71
|
+
}) })] }), _jsxs("section", { "aria-labelledby": "avatar-expression", children: [_jsx(DataListHeading, { level: 3, id: "avatar-expression", children: "Resting face" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-expression", className: "tourtois-avatar-faces", orientation: "horizontal", value: avatar.expression, onValueChange: (expression) => {
|
|
72
|
+
change({ expression });
|
|
73
|
+
}, children: catalog.expressions.map((expression) => (_jsx(RadioGroupCard, { value: expression, title: expression.charAt(0).toUpperCase() + expression.slice(1) }, expression))) })] }), _jsxs("section", { "aria-labelledby": "avatar-style", children: [_jsx(DataListHeading, { level: 3, id: "avatar-style", children: "Drawing style" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-style", className: "tourtois-sprite-styles", orientation: "horizontal", value: avatar.renderStyle, onValueChange: (raw) => {
|
|
74
|
+
change({ renderStyle: renderStyleSchema.parse(raw) });
|
|
75
|
+
}, children: renderStyles.map((renderStyle) => (_jsx(RadioGroupCard, { value: renderStyle, title: renderStyleLabels[renderStyle], description: _jsx(AvatarPreview, { avatar: { ...avatar, renderStyle }, className: "tourtois-sprite-style-preview" }) }, renderStyle))) })] }), _jsxs("section", { "aria-labelledby": "avatar-agents", children: [_jsx(DataListHeading, { level: 3, id: "avatar-agents", children: "Your new Agents" }), _jsx(Field, { label: "Give each Agent its own colour", children: _jsx(Switch, { checked: agentColors === "shift", onCheckedChange: (checked) => {
|
|
76
|
+
onChange({ ...value, agentColors: checked ? "shift" : "keep" });
|
|
77
|
+
} }) }), _jsx("p", { className: "tourtois-settings-lede", children: "A preview of your preference for new Agents. Existing Agents keep their look." }), _jsx("div", { className: "tourtois-avatar-agents", children: exampleNames.map((name) => (_jsxs("div", { children: [_jsx(AvatarPreview, { avatar: deriveAgentAvatar(avatar, agentColors, name), className: "tourtois-avatar-agent-preview" }), _jsx("span", { children: name })] }, name))) })] })] })] }));
|
|
78
|
+
}
|