@fungi.computer/tourtois 0.0.0-stage → 0.1.0

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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -2
  3. package/dist/AgentMcpServers.d.ts +34 -0
  4. package/dist/AgentMcpServers.js +124 -0
  5. package/dist/AppAccessConsent.d.ts +23 -0
  6. package/dist/AppAccessConsent.js +122 -0
  7. package/dist/AppCallPermissions.d.ts +9 -0
  8. package/dist/AppCallPermissions.js +21 -0
  9. package/dist/AppDesktopSettings.d.ts +11 -0
  10. package/dist/AppDesktopSettings.js +102 -0
  11. package/dist/AppPane.d.ts +37 -0
  12. package/dist/AppPane.js +219 -0
  13. package/dist/AppWorkbench.d.ts +44 -0
  14. package/dist/AppWorkbench.js +575 -0
  15. package/dist/AvatarPicker.d.ts +7 -0
  16. package/dist/AvatarPicker.js +78 -0
  17. package/dist/AvatarPreview.d.ts +10 -0
  18. package/dist/AvatarPreview.js +6 -0
  19. package/dist/DesktopMenuBar.d.ts +18 -0
  20. package/dist/DesktopMenuBar.js +23 -0
  21. package/dist/FocusedAppMenus.d.ts +2 -0
  22. package/dist/FocusedAppMenus.js +10 -0
  23. package/dist/Keys.d.ts +131 -0
  24. package/dist/Keys.js +201 -0
  25. package/dist/MushroomMark.d.ts +2 -0
  26. package/dist/MushroomMark.js +5 -0
  27. package/dist/SettingsHome.d.ts +18 -0
  28. package/dist/SettingsHome.js +48 -0
  29. package/dist/SettingsNavigation.d.ts +17 -0
  30. package/dist/SettingsNavigation.js +153 -0
  31. package/dist/SystemSettings.d.ts +24 -0
  32. package/dist/SystemSettings.js +177 -0
  33. package/dist/TeamDesktop.d.ts +36 -0
  34. package/dist/TeamDesktop.js +39 -0
  35. package/dist/WorkbenchAppBar.d.ts +5 -0
  36. package/dist/WorkbenchAppBar.js +12 -0
  37. package/dist/app-authority.d.ts +150 -0
  38. package/dist/app-authority.js +41 -0
  39. package/dist/app-directory-reconciliation.d.ts +14 -0
  40. package/dist/app-directory-reconciliation.js +72 -0
  41. package/dist/app-directory.d.ts +206 -0
  42. package/dist/app-directory.js +21 -0
  43. package/dist/app-icon.d.ts +9 -0
  44. package/dist/app-icon.js +27 -0
  45. package/dist/app-launch.d.ts +53 -0
  46. package/dist/app-launch.js +204 -0
  47. package/dist/app-menus.d.ts +6 -0
  48. package/dist/app-menus.js +81 -0
  49. package/dist/app-open-url.d.ts +8 -0
  50. package/dist/app-open-url.js +63 -0
  51. package/dist/app-surface.d.ts +62 -0
  52. package/dist/app-surface.js +10 -0
  53. package/dist/app-windows.d.ts +134 -0
  54. package/dist/app-windows.js +21 -0
  55. package/dist/chrome.css +153 -0
  56. package/dist/command-dialogs.d.ts +42 -0
  57. package/dist/command-dialogs.js +194 -0
  58. package/dist/desktop/Desktop.d.ts +34 -0
  59. package/dist/desktop/Desktop.js +275 -0
  60. package/dist/desktop/consent.d.ts +4 -0
  61. package/dist/desktop/consent.js +64 -0
  62. package/dist/desktop/gill-frame.d.ts +14 -0
  63. package/dist/desktop/gill-frame.js +41 -0
  64. package/dist/desktop/gill-param-picker.d.ts +15 -0
  65. package/dist/desktop/gill-param-picker.js +145 -0
  66. package/dist/desktop/launcher-gill.d.ts +20 -0
  67. package/dist/desktop/launcher-gill.js +49 -0
  68. package/dist/desktop/own-launchers.d.ts +9 -0
  69. package/dist/desktop/own-launchers.js +9 -0
  70. package/dist/desktop/row-gill.d.ts +24 -0
  71. package/dist/desktop/row-gill.js +136 -0
  72. package/dist/desktop-api.d.ts +63 -0
  73. package/dist/desktop-api.js +31 -0
  74. package/dist/desktop-client.d.ts +19 -0
  75. package/dist/desktop-client.js +127 -0
  76. package/dist/desktop-layout.d.ts +117 -0
  77. package/dist/desktop-layout.js +305 -0
  78. package/dist/desktop-visibility.d.ts +8 -0
  79. package/dist/desktop-visibility.js +5 -0
  80. package/dist/layers.d.ts +5 -0
  81. package/dist/layers.js +31 -0
  82. package/dist/main.d.ts +2 -0
  83. package/dist/main.js +17 -0
  84. package/dist/member-avatar-cache.d.ts +15 -0
  85. package/dist/member-avatar-cache.js +69 -0
  86. package/dist/member-avatar.d.ts +39 -0
  87. package/dist/member-avatar.js +88 -0
  88. package/dist/menu-bar.d.ts +5 -0
  89. package/dist/menu-bar.js +5 -0
  90. package/dist/model.d.ts +6 -0
  91. package/dist/model.js +13 -0
  92. package/dist/oyster-platform-controls.d.ts +4 -0
  93. package/dist/oyster-platform-controls.js +107 -0
  94. package/dist/platform-controls.d.ts +84 -0
  95. package/dist/platform-controls.js +266 -0
  96. package/dist/queries.d.ts +7 -0
  97. package/dist/queries.js +7 -0
  98. package/dist/session-registry.d.ts +16 -0
  99. package/dist/session-registry.js +69 -0
  100. package/dist/session-selection.d.ts +6 -0
  101. package/dist/session-selection.js +22 -0
  102. package/dist/settings-leave-guard.d.ts +5 -0
  103. package/dist/settings-leave-guard.js +9 -0
  104. package/dist/settings-navigation.css +129 -0
  105. package/dist/settings-routes.d.ts +118 -0
  106. package/dist/settings-routes.js +216 -0
  107. package/dist/settings-sections.d.ts +74 -0
  108. package/dist/settings-sections.js +17 -0
  109. package/dist/shell-commands.d.ts +15 -0
  110. package/dist/shell-commands.js +95 -0
  111. package/dist/shell-host.d.ts +52 -0
  112. package/dist/shell-host.js +288 -0
  113. package/dist/shell-module.d.ts +12 -0
  114. package/dist/shell-module.js +17 -0
  115. package/dist/shell-release.d.ts +3 -0
  116. package/dist/shell-release.js +4 -0
  117. package/dist/shell.css +858 -0
  118. package/dist/shell.d.ts +6 -0
  119. package/dist/shell.js +83 -0
  120. package/dist/spa.css +11 -0
  121. package/dist/status-bar.d.ts +9 -0
  122. package/dist/status-bar.js +51 -0
  123. package/dist/team-directory.d.ts +3 -0
  124. package/dist/team-directory.js +6 -0
  125. package/dist/use-window-motion.d.ts +3 -0
  126. package/dist/use-window-motion.js +37 -0
  127. package/dist/useAppOpenUrlHandoff.d.ts +11 -0
  128. package/dist/useAppOpenUrlHandoff.js +62 -0
  129. package/dist/useAppWindowManager.d.ts +351 -0
  130. package/dist/useAppWindowManager.js +47 -0
  131. package/dist/useDirectoryCommands.d.ts +12 -0
  132. package/dist/useDirectoryCommands.js +80 -0
  133. package/dist/workbench.d.ts +3 -0
  134. package/dist/workbench.js +2 -0
  135. package/package.json +222 -3
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useMemo } from "react";
3
+ import { CommandMenu } from "@fungi.computer/caps/blocks/command-menu";
4
+ import { Combobox } from "@fungi.computer/caps/components/combobox";
5
+ import { Dialog, DialogContent, DialogDescription, DialogTitle, } from "@fungi.computer/caps/components/dialog";
6
+ import { appDirectorySourceId, } from "@fungi.computer/oyster/shell/command-host";
7
+ import { leaderKeyMatches, leaderKeys, parseChord, } from "@fungi.computer/oyster/shell/keys";
8
+ function isMac() {
9
+ return /Mac|iPhone|iPad/u.test(navigator.userAgent);
10
+ }
11
+ function appCommandId(surfaceId, commandId) {
12
+ return `${encodeURIComponent(surfaceId)}:${commandId}`;
13
+ }
14
+ function directoryCommandId(surfaceId, commandId) {
15
+ return `app-directory:${encodeURIComponent(surfaceId)}-${encodeURIComponent(commandId)}`;
16
+ }
17
+ function chordKey(key) {
18
+ switch (key) {
19
+ case "enter":
20
+ return "Enter";
21
+ case "escape":
22
+ return "Esc";
23
+ case "left":
24
+ return "←";
25
+ case "right":
26
+ return "→";
27
+ case "up":
28
+ return "↑";
29
+ case "down":
30
+ return "↓";
31
+ default:
32
+ return key.length === 1 ? key.toUpperCase() : key;
33
+ }
34
+ }
35
+ function commandKeycaps(commandId, active, mac, declared) {
36
+ const leader = leaderKeys(active).find((row) => row.commandId === commandId);
37
+ if (leader !== undefined)
38
+ return [mac ? "⌘ K" : "Ctrl K", "then", chordKey(leader.key)];
39
+ const binding = active.bindings.find((row) => row.commandId === commandId);
40
+ const declaredBinding = declared?.projections?.bindings?.values.find((value) => value.startsWith("<leader> "));
41
+ if (declaredBinding !== undefined)
42
+ return [mac ? "⌘ K" : "Ctrl K", "then", chordKey(declaredBinding.slice(9))];
43
+ if (binding === undefined)
44
+ return [];
45
+ const chord = parseChord(binding.binding, mac);
46
+ if (chord === null)
47
+ return [];
48
+ return [
49
+ ...(chord.meta ? ["⌘"] : []),
50
+ ...(chord.ctrl ? ["Ctrl"] : []),
51
+ ...(chord.alt ? ["Alt"] : []),
52
+ ...(chord.shift ? ["Shift"] : []),
53
+ chordKey(chord.key),
54
+ ];
55
+ }
56
+ function outcomeMessage(outcome) {
57
+ if (outcome.status === "handled")
58
+ return null;
59
+ if (outcome.status === "failed")
60
+ return "That command couldn’t finish. Please try again.";
61
+ if (outcome.status === "unavailable")
62
+ return outcome.reason;
63
+ return "That command is no longer available.";
64
+ }
65
+ /** Which-key list and `:` line for the focused window, drawn from Caps. */
66
+ export function CommandDialogs({ host, state, focusedWindow, directoryApps, onStatus, finalFocus, container: dialog, renderIcon, }) {
67
+ const { active, keysOpen, setKeysOpen, commandLineOpen, setCommandLineOpen } = state;
68
+ const focusedTitle = focusedWindow?.launch.title ?? null;
69
+ const keys = useMemo(() => leaderKeys(active), [active]);
70
+ const groups = useMemo(() => {
71
+ const shell = keys.filter((row) => row.scope === undefined);
72
+ const app = keys.filter((row) => row.scope !== undefined);
73
+ return [
74
+ { label: "Workspace", rows: shell },
75
+ { label: focusedTitle ?? "Focused App", rows: app },
76
+ ].filter((group) => group.rows.length > 0);
77
+ }, [focusedTitle, keys]);
78
+ const palette = useMemo(() => active.palette.filter((command) => command.availability.status === "available"), [active]);
79
+ const focusedApp = focusedWindow
80
+ ? {
81
+ commands: focusedWindow.launch.commands,
82
+ name: focusedWindow.launch.title,
83
+ icon: focusedWindow.launch.icon,
84
+ roles: focusedWindow.launch.roles,
85
+ surfaceId: focusedWindow.launch.surfaceId,
86
+ }
87
+ : undefined;
88
+ const focusedCommandIds = new Set(focusedApp?.commands.flatMap((command) => [
89
+ appCommandId(focusedApp.surfaceId, command.id),
90
+ directoryCommandId(focusedApp.surfaceId, command.id),
91
+ ]) ?? []);
92
+ const isFocusedCommand = (command) => command.scope === focusedApp?.surfaceId ||
93
+ focusedCommandIds.has(command.commandId);
94
+ const orderedPaletteRows = [
95
+ ...palette.filter(isFocusedCommand),
96
+ ...palette.filter((command) => command.sourceId === appDirectorySourceId && !isFocusedCommand(command)),
97
+ ...palette.filter((command) => command.scope === undefined &&
98
+ command.sourceId !== appDirectorySourceId),
99
+ ];
100
+ const seenPaletteIds = new Set();
101
+ const paletteRows = orderedPaletteRows.filter((command) => {
102
+ if (seenPaletteIds.has(command.commandId))
103
+ return false;
104
+ seenPaletteIds.add(command.commandId);
105
+ return true;
106
+ });
107
+ const appByCommandId = new Map();
108
+ if (focusedApp)
109
+ for (const command of focusedApp.commands)
110
+ appByCommandId.set(appCommandId(focusedApp.surfaceId, command.id), focusedApp);
111
+ for (const app of directoryApps)
112
+ for (const command of app.commands)
113
+ appByCommandId.set(directoryCommandId(app.surfaceId, command.id), app);
114
+ const mac = isMac();
115
+ const run = useCallback((commandId, origin) => {
116
+ if (!host)
117
+ return;
118
+ void host.execute(commandId, { origin }).then((outcome) => {
119
+ const message = outcomeMessage(outcome);
120
+ if (message !== null)
121
+ onStatus(message);
122
+ });
123
+ }, [host, onStatus]);
124
+ const choose = useCallback((row) => {
125
+ setKeysOpen(false);
126
+ if (row.availability.status === "unavailable") {
127
+ onStatus(row.availability.reason);
128
+ return;
129
+ }
130
+ run(row.commandId, "keyboard");
131
+ }, [onStatus, run, setKeysOpen]);
132
+ useLayoutEffect(() => {
133
+ if (!keysOpen)
134
+ return;
135
+ // The leader sequence belongs to the shell as soon as it opens; Base UI
136
+ // focuses its dialog on the next animation frame, after a fast second key.
137
+ const owner = dialog?.ownerDocument ?? document;
138
+ const onKeyDown = (event) => {
139
+ const row = keys.find((candidate) => leaderKeyMatches(event, candidate.key));
140
+ if (row === undefined)
141
+ return;
142
+ event.preventDefault();
143
+ event.stopPropagation();
144
+ choose(row);
145
+ };
146
+ owner.addEventListener("keydown", onKeyDown, true);
147
+ return () => {
148
+ owner.removeEventListener("keydown", onKeyDown, true);
149
+ };
150
+ }, [choose, dialog, keys, keysOpen]);
151
+ return (_jsxs(_Fragment, { children: [_jsx(Dialog, { open: keysOpen, onOpenChange: setKeysOpen, children: _jsxs(DialogContent, { container: dialog, className: "tourtois-command-palette", "data-tourtois-which-key": "", finalFocus: finalFocus, children: [_jsx(DialogTitle, { children: "Keys" }), _jsxs(DialogDescription, { children: ["Press a key, or ", isMac() ? "⌘K" : "Ctrl+K", " again for the command line."] }), groups.map((group) => (_jsxs("section", { "aria-label": group.label, children: [_jsx("p", { children: group.label }), _jsx(CommandMenu, { "aria-label": `${group.label} keys`, items: group.rows.map((row) => ({
152
+ id: row.commandId,
153
+ label: row.title,
154
+ shortcut: row.key,
155
+ disabled: row.availability.status === "unavailable",
156
+ })), onSelect: (id) => {
157
+ const row = group.rows.find((item) => item.commandId === id);
158
+ if (row)
159
+ choose(row);
160
+ } })] }, group.label)))] }) }), _jsx(Dialog, { open: commandLineOpen, onOpenChange: setCommandLineOpen, children: _jsxs(DialogContent, { container: dialog, className: "tourtois-command-palette", finalFocus: finalFocus, children: [_jsx(DialogTitle, { className: "sr-only", children: "Command palette" }), _jsxs("div", { className: "tourtois-command-palette-heading", children: [focusedApp
161
+ ? renderIcon(focusedApp.roles, focusedApp.icon ?? undefined)
162
+ : null, _jsx("span", { children: focusedTitle ?? "Commands" })] }), _jsx(Combobox, { activeTone: "primary", "aria-label": "Available commands", emptyMessage: "No matching commands", getKey: (command) => command.commandId, getLabel: (command) => command.title, getSearchText: (command) => {
163
+ const app = appByCommandId.get(command.commandId);
164
+ return `${command.title} ${command.category} ${command.description ?? ""} ${app?.name ?? ""} ${app ? `opens ${app.name}` : ""}`;
165
+ }, groupBy: (command) => {
166
+ if (isFocusedCommand(command))
167
+ return "";
168
+ if (command.sourceId === appDirectorySourceId)
169
+ return "Other apps";
170
+ return "Workspace";
171
+ }, inputProps: { "aria-label": "Search commands" }, items: paletteRows, placeholder: ":", renderItem: (command) => (_jsxs("div", { className: "tourtois-command-palette-item", children: [_jsx("span", { className: "tourtois-command-palette-item-icon", children: (() => {
172
+ const app = appByCommandId.get(command.commandId);
173
+ if (app === undefined)
174
+ return null;
175
+ const declared = app.commands.find((entry) => (command.sourceId === appDirectorySourceId
176
+ ? directoryCommandId(app.surfaceId, entry.id)
177
+ : appCommandId(app.surfaceId, entry.id)) ===
178
+ command.commandId);
179
+ return renderIcon(app.roles, declared?.sigil ?? app.icon ?? undefined);
180
+ })() }), _jsx("span", { className: "tourtois-command-palette-item-title", children: command.title }), command.sourceId === appDirectorySourceId ? (appByCommandId.get(command.commandId)?.name !==
181
+ focusedApp?.name ? (_jsxs("small", { children: ["opens", " ", appByCommandId.get(command.commandId)?.name ?? "App"] })) : null) : null, (() => {
182
+ const app = appByCommandId.get(command.commandId);
183
+ const declared = app?.commands.find((entry) => [
184
+ appCommandId(app.surfaceId, entry.id),
185
+ directoryCommandId(app.surfaceId, entry.id),
186
+ ].includes(command.commandId));
187
+ return (_jsx("span", { className: "tourtois-command-palette-shortcut", children: commandKeycaps(command.commandId, active, mac, declared).map((key, index) => key === "then" ? (_jsx("span", { children: "then" }, `${key}-${String(index)}`)) : (_jsx("kbd", { className: "kbd kbd-sm", children: key }, `${key}-${String(index)}`))) }));
188
+ })()] })), value: null, onValueChange: (_value, command) => {
189
+ if (!command)
190
+ return;
191
+ setCommandLineOpen(false);
192
+ run(command.commandId, "palette");
193
+ } }), _jsxs("div", { className: "tourtois-command-palette-footer", children: [_jsx("span", { className: "tourtois-command-palette-close-hint", children: "Esc to close" }), _jsxs("span", { className: "tourtois-command-palette-leader-hint", children: [_jsx("kbd", { className: "kbd kbd-sm", children: mac ? "⌘ K" : "Ctrl K" }), _jsx("span", { children: "then" }), _jsx("kbd", { className: "kbd kbd-sm", children: "?" }), _jsx("span", { children: "for all keys" })] })] })] }) })] }));
194
+ }
@@ -0,0 +1,34 @@
1
+ import type { AppOpenInput } from "@fungi.computer/agar/apps";
2
+ import type { DesktopMethods } from "../desktop-api.js";
3
+ import type { AppStorage } from "@fungi.computer/agar/storage";
4
+ import type { AppBarItem } from "@fungi.computer/caps/blocks/app-bar";
5
+ import type { WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
6
+ import { type DesktopLayoutClient } from "../desktop-client.js";
7
+ import { type DesktopGillTransport } from "./row-gill.js";
8
+ export type DesktopProps = {
9
+ phone: boolean;
10
+ apps: WorkbenchDirectory["apps"];
11
+ inboxSurfaceId: string | null;
12
+ launcherItems: readonly AppBarItem[];
13
+ needsUpdate: NonNullable<WorkbenchDirectory["needsUpdate"]>;
14
+ onAppSettings: (app: WorkbenchDirectory["apps"][number]) => void;
15
+ storage: AppStorage;
16
+ methods: DesktopMethods;
17
+ transport: DesktopGillTransport;
18
+ paused: boolean;
19
+ loading: boolean;
20
+ unavailable: boolean;
21
+ onRetryApps: () => void;
22
+ onAllApps: () => void;
23
+ onOpen: (target: AppOpenInput) => void;
24
+ computersForGill?: ((surfaceId: string) => readonly {
25
+ computerId: string;
26
+ name: string;
27
+ }[]) | undefined;
28
+ onOwner?: (owner: DesktopLayoutClient | null) => void;
29
+ /** Arrange mode is a Layout-menu command, so the Workbench owns it. */
30
+ editing: boolean;
31
+ onEditingChange: (editing: boolean) => void;
32
+ };
33
+ /** The App owns one canonical four-column layout, never breakpoint storage. */
34
+ export declare function Desktop(props: DesktopProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,275 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { GillParamPicker } from "./gill-param-picker.js";
3
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { arrangeBento, useMaosaicoBento } from "@fungi.computer/maosaico";
5
+ import { Button } from "@fungi.computer/caps/components/button";
6
+ import { IconButton } from "@fungi.computer/caps/components/icon-button";
7
+ import { GripVertical, Trash2 } from "lucide-react";
8
+ import { Select } from "@fungi.computer/caps/components/select";
9
+ import { Badge } from "@fungi.computer/caps/components/badge";
10
+ import { Sigil } from "@fungi.computer/sigils";
11
+ import { DeclaredAppIcon } from "../app-icon.js";
12
+ import { createDesktopLayoutClient, } from "../desktop-client.js";
13
+ import { availableDesktopLayout, desktopPlacements, desktopBentoIntent, } from "../desktop-layout.js";
14
+ import { LauncherGill } from "./launcher-gill.js";
15
+ import { RowGill } from "./row-gill.js";
16
+ const gap = 16;
17
+ const rowHeight = 280;
18
+ /** The App owns one canonical four-column layout, never breakpoint storage. */
19
+ export function Desktop(props) {
20
+ const [owner, setOwner] = useState(null);
21
+ const [error, setError] = useState(null);
22
+ const [retry, setRetry] = useState(0);
23
+ useEffect(() => {
24
+ let retired = false;
25
+ let current;
26
+ setOwner(null);
27
+ void createDesktopLayoutClient({
28
+ storage: props.storage,
29
+ methods: props.methods,
30
+ })
31
+ .then((next) => {
32
+ if (retired) {
33
+ next.close();
34
+ return;
35
+ }
36
+ current = next;
37
+ setError(null);
38
+ setOwner(next);
39
+ props.onOwner?.(next);
40
+ })
41
+ .catch((cause) => {
42
+ console.error("Desktop arrangement read failed", cause);
43
+ if (!retired)
44
+ setError("Your desktop could not be loaded.");
45
+ });
46
+ return () => {
47
+ retired = true;
48
+ props.onOwner?.(null);
49
+ current?.close();
50
+ };
51
+ }, [
52
+ props.methods,
53
+ props.storage,
54
+ props.apps,
55
+ props.inboxSurfaceId,
56
+ props.onOwner,
57
+ retry,
58
+ ]);
59
+ return (_jsxs("div", { className: "tourtois-desktop", "data-desktop-bento": true, children: [error ? (_jsxs("p", { role: "alert", children: [error, " ", _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
60
+ setRetry((value) => value + 1);
61
+ }, children: "Retry" })] })) : null, !owner && !error ? _jsx("p", { role: "status", children: "Loading desktop\u2026" }) : null, owner ? _jsx(ArrangedDesktop, { ...props, owner: owner }) : null] }));
62
+ }
63
+ function ArrangedDesktop({ owner, ...props }) {
64
+ const snapshot = useSyncExternalStore(owner.subscribe, owner.get, owner.get);
65
+ const catalog = snapshot.widgets;
66
+ const visibleItems = availableDesktopLayout(snapshot.layout, catalog).items;
67
+ const { editing, onEditingChange } = props;
68
+ const [configuration, setConfiguration] = useState(null);
69
+ const [error, setError] = useState(null);
70
+ const [saving, setSaving] = useState(false);
71
+ const stage = useRef(null);
72
+ const [width, setWidth] = useState(0);
73
+ const [contentHeights, setContentHeights] = useState(new Map());
74
+ useEffect(() => {
75
+ const node = stage.current;
76
+ if (!node)
77
+ return;
78
+ const observer = new ResizeObserver(([entry]) => {
79
+ setWidth(entry?.contentRect.width ?? 0);
80
+ });
81
+ observer.observe(node);
82
+ return () => {
83
+ observer.disconnect();
84
+ };
85
+ }, []);
86
+ const columns = props.phone ? 1 : 4;
87
+ useEffect(() => {
88
+ setContentHeights((previous) => {
89
+ const current = new Map([...previous].filter(([id]) => snapshot.layout.items.some((item) => item.id === id)));
90
+ return current.size === previous.size ? previous : current;
91
+ });
92
+ }, [snapshot.layout.items]);
93
+ const projected = desktopPlacements(snapshot.layout)
94
+ .filter((cell) => visibleItems.some((item) => item.id === cell.id))
95
+ .map((cell) => {
96
+ const item = snapshot.layout.items.find((item) => item.id === cell.id);
97
+ const kind = catalog.find((gill) => gill.surfaceId === item?.surfaceId && gill.gillId === item.gillId)?.kind;
98
+ // Launcher M needs two icon rows on phones. This is presentation only:
99
+ // canonical sizes/cells still come from the four-column storage owner.
100
+ return props.phone && item?.size === "M" && kind === "launcher"
101
+ ? { ...cell, h: 2 }
102
+ : cell;
103
+ });
104
+ const placements = arrangeBento(projected, columns);
105
+ const save = async (method, input, scope = "member") => {
106
+ setSaving(true);
107
+ try {
108
+ const result = await owner.mutate(method, {
109
+ scope,
110
+ basedOn: scope === "team" ? snapshot.teamRevision : snapshot.memberRevision,
111
+ ...input,
112
+ });
113
+ setError(result.kind === "accepted"
114
+ ? null
115
+ : result.code === "conflict"
116
+ ? "The desktop changed elsewhere. Try your change again."
117
+ : result.code === "too_large"
118
+ ? "This desktop arrangement is too large to save. Remove some widgets or Launcher preferences and try again."
119
+ : result.code === "quota_exceeded"
120
+ ? "The App's storage quota is full. Free some storage before saving again."
121
+ : `Desktop change refused: ${result.code}.`);
122
+ return result.kind === "accepted";
123
+ }
124
+ catch (cause) {
125
+ console.error("Desktop arrangement update failed", cause);
126
+ setError("Your desktop change could not be confirmed.");
127
+ return false;
128
+ }
129
+ finally {
130
+ setSaving(false);
131
+ }
132
+ };
133
+ const rowHeights = Array.from({
134
+ length: Math.max(0, ...placements.map((item) => item.row + item.h)),
135
+ }, (_, row) => Math.max(0, ...placements
136
+ .filter((item) => item.row <= row && row < item.row + item.h)
137
+ .map((item) => {
138
+ const declaration = snapshot.layout.items.find((entry) => entry.id === item.id);
139
+ const isLauncher = declaration !== undefined &&
140
+ catalog.some((gill) => gill.kind === "launcher" &&
141
+ gill.surfaceId === declaration.surfaceId &&
142
+ gill.gillId === declaration.gillId);
143
+ const canonical = item.h * rowHeight + (item.h - 1) * gap;
144
+ const natural = contentHeights.get(item.id) ?? canonical;
145
+ const height = isLauncher
146
+ ? props.phone
147
+ ? natural
148
+ : Math.max(canonical, natural)
149
+ : props.phone
150
+ ? Math.min(natural, canonical)
151
+ : canonical;
152
+ return Math.max(0, height - (item.h - 1) * gap) / item.h;
153
+ })));
154
+ const binding = useMaosaicoBento({
155
+ placements,
156
+ columns,
157
+ width,
158
+ rowHeight,
159
+ rowHeights,
160
+ gap,
161
+ enabled: editing && !saving,
162
+ onChange: (ops) => {
163
+ // Maosaico emits one completed drag or keyboard intent per change.
164
+ const op = ops[0];
165
+ const intent = op
166
+ ? desktopBentoIntent(snapshot.layout, placements, op)
167
+ : null;
168
+ if (intent?.kind === "move")
169
+ void save("layout.move", { id: intent.id, to: intent.to });
170
+ else if (intent?.kind === "resize")
171
+ void save("layout.resize", { id: intent.id, size: intent.size });
172
+ },
173
+ });
174
+ const frames = [...binding.frames.values()];
175
+ const height = Math.max(0, ...frames.map((frame) => frame.y + frame.height));
176
+ const warnings = [
177
+ ...props.apps
178
+ .filter((app) => app.availability !== "available" ||
179
+ app.releaseId === null ||
180
+ app.surface === null)
181
+ .map((app) => ({
182
+ id: `unavailable:${app.surfaceId}`,
183
+ label: app.displayName ?? "App",
184
+ icon: _jsx(DeclaredAppIcon, { icon: app.icon }),
185
+ muted: true,
186
+ status: (_jsx(Badge, { role: "status", tone: "warning", variant: "quiet", size: "sm", children: "Unavailable \u00B7 Settings" })),
187
+ onSelect: () => {
188
+ props.onAppSettings(app);
189
+ },
190
+ })),
191
+ ...props.needsUpdate.map((app, index) => ({
192
+ id: `needs-update:${app.installationId ?? app.appId ?? String(index)}`,
193
+ label: app.displayName,
194
+ icon: _jsx(Sigil, { name: "installApps" }),
195
+ disabled: true,
196
+ status: (_jsx(Badge, { role: "status", tone: "warning", variant: "quiet", size: "sm", children: "Needs update" })),
197
+ onSelect: () => undefined,
198
+ })),
199
+ ];
200
+ return (_jsxs(_Fragment, { children: [snapshot.refreshFailure ? (_jsx("p", { role: "alert", children: snapshot.refreshFailure.message })) : null, editing ? (_jsxs("div", { className: "tourtois-desktop-controls", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
201
+ onEditingChange(false);
202
+ }, children: "Done arranging" }), _jsx("div", { className: "tourtois-desktop-add-gill", children: _jsxs(Select, { "aria-label": "Add widget", value: "", disabled: saving, onChange: (event) => {
203
+ const gill = catalog[Number(event.currentTarget.value)];
204
+ const size = gill?.sizes[0];
205
+ if (gill && size && (gill.params?.length ?? 0) > 0)
206
+ setConfiguration({ gill });
207
+ else if (gill && size)
208
+ void save("layout.place", {
209
+ surfaceId: gill.surfaceId,
210
+ gillId: gill.gillId,
211
+ size,
212
+ });
213
+ }, children: [_jsx("option", { value: "", children: "Add widget\u2026" }), catalog.map((gill, index) => (_jsx("option", { value: index, children: gill.title }, `${gill.surfaceId}:${gill.gillId}`)))] }) }), _jsx(Button, { size: "sm", variant: "ghost", disabled: saving, onClick: () => {
214
+ void save("layout.reset", {});
215
+ }, children: "Reset to Team default" }), snapshot.canWriteTeam ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: saving, onClick: () => {
216
+ void save("layout.set", { layout: snapshot.layout }, "team");
217
+ }, children: "Save as Team default" })) : null] })) : null, configuration ? (_jsx(GillParamPicker, { gill: configuration.gill, initialParams: snapshot.layout.items.find((item) => item.id === configuration.itemId)?.params, computers: props.computersForGill?.(configuration.gill.surfaceId) ?? [], transport: props.transport, editing: configuration.itemId !== undefined, onClose: () => {
218
+ setConfiguration(null);
219
+ }, onSave: (params) => {
220
+ if (configuration.itemId !== undefined)
221
+ return save("layout.set", {
222
+ layout: {
223
+ ...snapshot.layout,
224
+ items: snapshot.layout.items.map((item) => item.id === configuration.itemId
225
+ ? { ...item, params }
226
+ : item),
227
+ },
228
+ });
229
+ const size = configuration.gill.sizes[0];
230
+ return size
231
+ ? save("layout.place", {
232
+ surfaceId: configuration.gill.surfaceId,
233
+ gillId: configuration.gill.gillId,
234
+ size,
235
+ params,
236
+ })
237
+ : Promise.resolve(false);
238
+ } }, configuration.itemId ??
239
+ `${configuration.gill.surfaceId}:${configuration.gill.gillId}`)) : null, snapshot.notice ? _jsx("p", { role: "alert", children: snapshot.notice }) : null, error ? _jsx("p", { role: "alert", children: error }) : null, _jsx("div", { ref: stage, className: "tourtois-desktop-stage", style: { height }, "data-desktop-columns": columns, "data-preview": binding.preview, children: visibleItems.map((item) => {
240
+ const frame = binding.frames.get(item.id);
241
+ const gill = catalog.find((gill) => gill.surfaceId === item.surfaceId && gill.gillId === item.gillId);
242
+ if (!frame || !gill)
243
+ return null;
244
+ const controls = editing ? (_jsxs("div", { className: "tourtois-gill-controls", children: [_jsx(IconButton, { size: "responsive", "aria-label": `Move ${gill.title}`, title: `Move ${gill.title}`, icon: _jsx(GripVertical, {}), ...binding.bind(item.id, "move") }), _jsx("div", { className: "join tourtois-gill-sizes", role: "group", "aria-label": `Size of ${gill.title}`, children: gill.sizes.map((size) => (_jsx(IconButton, { className: "join-item", size: "xs", icon: size, "aria-label": `Size ${size === "S" ? "small" : size === "M" ? "medium" : "large"}`, title: `${gill.title}: ${size}`, variant: size === item.size ? "secondary" : "ghost", "aria-pressed": size === item.size, disabled: saving, onClick: () => {
245
+ void save("layout.resize", { id: item.id, size });
246
+ } }, size))) }), _jsx(IconButton, { size: "responsive", "aria-label": `Remove ${gill.title}`, title: `Remove ${gill.title}`, icon: _jsx(Trash2, {}), disabled: saving, onClick: () => {
247
+ void save("layout.remove", { id: item.id });
248
+ } })] })) : undefined;
249
+ const onContentHeight = (height) => {
250
+ setContentHeights((previous) => previous.get(item.id) === height
251
+ ? previous
252
+ : new Map(previous).set(item.id, height));
253
+ };
254
+ const app = props.apps.find((app) => app.surfaceId === item.surfaceId);
255
+ const declaration = app?.gills.find((declaration) => declaration.id === item.gillId);
256
+ return (_jsx("div", { "data-desktop-gill": item.id, className: "tourtois-desktop-tile", style: {
257
+ left: frame.x,
258
+ top: frame.y,
259
+ width: frame.width,
260
+ height: frame.height,
261
+ }, children: gill.kind === "launcher" ? (_jsx(LauncherGill, { items: props.launcherItems, size: item.size, onAllApps: props.onAllApps, loading: props.loading, unavailable: props.unavailable, onRetry: props.onRetryApps, warnings: warnings, preferences: snapshot.layout.launcher, onPreference: (id, state) => {
262
+ void save("layout.launcher", { appId: id, state });
263
+ }, editHandle: controls, onContentHeight: onContentHeight })) : declaration && app ? (_jsx(RowGill, { surfaceId: item.surfaceId, appHeader: {
264
+ name: app.displayName ?? "App",
265
+ icon: _jsx(DeclaredAppIcon, { icon: app.icon }),
266
+ }, declaration: declaration, params: item.params, size: item.size, paused: props.paused, transport: props.transport, onOpen: (target) => {
267
+ props.onOpen({
268
+ appId: app.appId,
269
+ intent: { kind: "navigate", params: target.params },
270
+ });
271
+ }, onConfigure: () => {
272
+ setConfiguration({ gill, itemId: item.id });
273
+ }, launch: props.launcherItems.find((app) => app.id === `app:${item.surfaceId}`), editHandle: controls, onContentHeight: onContentHeight }, JSON.stringify(item.params ?? {}))) : null }, item.id));
274
+ }) })] }));
275
+ }
@@ -0,0 +1,4 @@
1
+ import { type WorkbenchDesktopSettings } from "@fungi.computer/oyster/shell/workbench";
2
+ import type { DesktopLayoutClient } from "../desktop-client.js";
3
+ /** Only the host's member-consent UI invokes this admitted dashboard callback. */
4
+ export declare function desktopSettingsHandler(current: () => DesktopLayoutClient | null): WorkbenchDesktopSettings;
@@ -0,0 +1,64 @@
1
+ import { AppError } from "@fungi.computer/agar";
2
+ import { desktopGillAdditionSchema, desktopGillSelectionSchema, } from "@fungi.computer/oyster/shell/workbench";
3
+ /** Only the host's member-consent UI invokes this admitted dashboard callback. */
4
+ export function desktopSettingsHandler(current) {
5
+ const owner = () => {
6
+ const value = current();
7
+ if (!value)
8
+ throw new AppError("Your desktop is still loading. Try again.", {
9
+ code: "HOST_FAILURE",
10
+ retryable: true,
11
+ });
12
+ return value;
13
+ };
14
+ const matching = (value, request) => value
15
+ .get()
16
+ .layout.items.filter((item) => item.surfaceId === request.surfaceId &&
17
+ item.gillId === request.gillId);
18
+ const confirmed = (result) => {
19
+ if (result.kind === "rejected")
20
+ throw new AppError(result.code === "conflict"
21
+ ? "Your desktop changed elsewhere. Try again."
22
+ : result.code === "too_many"
23
+ ? "Your desktop is full. Remove a widget before adding another."
24
+ : "This widget cannot be changed on your desktop. Check its availability and try again.", { code: "INVALID_ARGUMENT", retryable: result.code === "conflict" });
25
+ };
26
+ return {
27
+ readGill: (input) => {
28
+ const request = desktopGillSelectionSchema.parse(input);
29
+ return Promise.resolve(matching(owner(), request).map((item) => item.size));
30
+ },
31
+ async addGill(input) {
32
+ const request = desktopGillAdditionSchema.parse(input);
33
+ const value = owner();
34
+ if (matching(value, request).length)
35
+ throw new AppError("This widget is already on your desktop.", {
36
+ code: "INVALID_ARGUMENT",
37
+ retryable: false,
38
+ });
39
+ confirmed(await value.mutate("layout.place", {
40
+ scope: "member",
41
+ basedOn: value.get().memberRevision,
42
+ ...request,
43
+ }));
44
+ // The method commits through the host storage owner and refreshes layout.get.
45
+ },
46
+ async removeGill(input) {
47
+ const request = desktopGillSelectionSchema.parse(input);
48
+ const value = owner();
49
+ const items = matching(value, request);
50
+ if (!items.length)
51
+ return;
52
+ confirmed(await value.mutate("layout.set", {
53
+ scope: "member",
54
+ basedOn: value.get().memberRevision,
55
+ layout: {
56
+ ...value.get().layout,
57
+ items: value
58
+ .get()
59
+ .layout.items.filter((item) => !items.some((selected) => selected.id === item.id)),
60
+ },
61
+ }));
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ import type { AppGillDesktopSize } from "@fungi.computer/gills";
3
+ /** Host-owned presentation; content, edit gestures and size selection stay with the caller. */
4
+ export declare function GillFrame({ title, appHeader, size, editHandle, children, onContentHeight, }: {
5
+ title: string;
6
+ appHeader?: Readonly<{
7
+ name: string;
8
+ icon: ReactNode;
9
+ }> | undefined;
10
+ size: AppGillDesktopSize;
11
+ editHandle?: ReactNode;
12
+ children: ReactNode;
13
+ onContentHeight?: ((height: number) => void) | undefined;
14
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from "react";
3
+ import { SectionCard } from "@fungi.computer/caps/blocks/section-card";
4
+ /** Host-owned presentation; content, edit gestures and size selection stay with the caller. */
5
+ export function GillFrame({ title, appHeader, size, editHandle, children, onContentHeight, }) {
6
+ const card = useRef(null);
7
+ const content = useRef(null);
8
+ const report = useRef(onContentHeight);
9
+ report.current = onContentHeight;
10
+ const measuring = onContentHeight !== undefined;
11
+ useLayoutEffect(() => {
12
+ if (!measuring)
13
+ return;
14
+ const root = card.current;
15
+ const body = content.current;
16
+ const header = root?.firstElementChild;
17
+ const section = body?.parentElement?.parentElement;
18
+ if (!root || !body || !header || !section)
19
+ return;
20
+ const measure = () => {
21
+ const rootStyle = getComputedStyle(root);
22
+ const sectionStyle = getComputedStyle(section);
23
+ const height = header.getBoundingClientRect().height +
24
+ body.getBoundingClientRect().height +
25
+ parseFloat(sectionStyle.paddingTop) +
26
+ parseFloat(sectionStyle.paddingBottom) +
27
+ parseFloat(rootStyle.borderTopWidth) +
28
+ parseFloat(rootStyle.borderBottomWidth);
29
+ if (Number.isFinite(height) && height > 0)
30
+ report.current?.(Math.ceil(height));
31
+ };
32
+ const observer = new ResizeObserver(measure);
33
+ observer.observe(body);
34
+ observer.observe(header);
35
+ measure();
36
+ return () => {
37
+ observer.disconnect();
38
+ };
39
+ }, [measuring]);
40
+ return (_jsx(SectionCard, { ref: card, className: "tourtois-gill-frame", heading: appHeader ? (_jsxs("span", { className: "flex min-w-0 items-center gap-sm", children: [_jsx("span", { className: "inline-flex size-5 shrink-0 [&_:is(svg,.caps-monogram)]:size-5", children: appHeader.icon }), _jsx("span", { className: "truncate", children: appHeader.name })] })) : (title), description: appHeader ? title : undefined, "data-gill-size": size, actions: editHandle, children: _jsx("div", { className: "tourtois-gill-body", children: _jsx("div", { ref: content, className: "tourtois-gill-content", children: children }) }) }));
41
+ }
@@ -0,0 +1,15 @@
1
+ import { type DesktopGill } from "../desktop-layout.js";
2
+ import type { DesktopGillTransport } from "./row-gill.js";
3
+ /** Chooses placement context through the Gill's own declaration and host inventory. */
4
+ export declare function GillParamPicker({ gill, initialParams, computers, transport, editing, onSave, onClose, }: {
5
+ gill: DesktopGill;
6
+ initialParams?: Readonly<Record<string, string>> | undefined;
7
+ computers: readonly Readonly<{
8
+ computerId: string;
9
+ name: string;
10
+ }>[];
11
+ transport: DesktopGillTransport;
12
+ editing: boolean;
13
+ onSave: (params: Record<string, string>) => Promise<boolean>;
14
+ onClose: () => void;
15
+ }): import("react/jsx-runtime").JSX.Element;