@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.
- 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 +219 -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 +134 -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 +288 -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 +351 -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,10 @@
|
|
|
1
|
+
import type { Avatar } from "@fungi.computer/sprite/avatar";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
/** Member and picker previews use Parakeet's shared avatar mapper and service. */
|
|
4
|
+
export declare function AvatarPreview({ avatar, activity, expression, className, style, }: {
|
|
5
|
+
avatar: Avatar;
|
|
6
|
+
activity?: string;
|
|
7
|
+
expression?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: CSSProperties;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ShiitakeAvatar } from "@fungi.computer/parakeet/agent-head";
|
|
3
|
+
/** Member and picker previews use Parakeet's shared avatar mapper and service. */
|
|
4
|
+
export function AvatarPreview({ avatar, activity = "idle", expression = avatar.expression, className, style, }) {
|
|
5
|
+
return (_jsx("span", { className: className, style: { display: "block", ...style }, "aria-hidden": "true", children: _jsx(ShiitakeAvatar, { avatar: avatar, className: "h-full w-full", state: { activity, expression } }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Presentation only: window state and command ownership stay with their owners. */
|
|
3
|
+
export declare function DesktopMenuBar({ teamName, focusedId, title, appBar, tiled, onLayoutChange, onRestoreLayout, onArrangeDesktop, focusedControls, onOpenSettings, onEditAvatar, systemMenuItems, onShowDesktop, }: {
|
|
4
|
+
teamName: string;
|
|
5
|
+
focusedId: string | null;
|
|
6
|
+
title: string;
|
|
7
|
+
appBar: ReactNode;
|
|
8
|
+
tiled: boolean;
|
|
9
|
+
onLayoutChange?: ((tiled: boolean) => void) | undefined;
|
|
10
|
+
onRestoreLayout?: (() => void) | undefined;
|
|
11
|
+
/** Opens the desktop's own arrange mode, a Layout-menu command. */
|
|
12
|
+
onArrangeDesktop?: (() => void) | undefined;
|
|
13
|
+
focusedControls?: ReactNode;
|
|
14
|
+
onOpenSettings: () => void;
|
|
15
|
+
onEditAvatar: () => void;
|
|
16
|
+
systemMenuItems?: ReactNode;
|
|
17
|
+
onShowDesktop?: (() => void) | undefined;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ShellMenuBar } from "./menu-bar.js";
|
|
3
|
+
import { IconButton } from "@fungi.computer/caps/components/icon-button";
|
|
4
|
+
import { Home } from "lucide-react";
|
|
5
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
6
|
+
import { useLayer } from "@fungi.computer/maosaico";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, } from "@fungi.computer/caps/components/dropdown-menu";
|
|
9
|
+
import { FocusedAppMenus } from "./FocusedAppMenus.js";
|
|
10
|
+
import { MushroomMark } from "./MushroomMark.js";
|
|
11
|
+
/** Presentation only: window state and command ownership stay with their owners. */
|
|
12
|
+
export function DesktopMenuBar({ teamName, focusedId, title, appBar, tiled, onLayoutChange, onRestoreLayout, onArrangeDesktop, focusedControls, onOpenSettings, onEditAvatar, systemMenuItems, onShowDesktop, }) {
|
|
13
|
+
const popover = useLayer("popover");
|
|
14
|
+
const [systemOpen, setSystemOpen] = useState(false);
|
|
15
|
+
const [layoutOpen, setLayoutOpen] = useState(false);
|
|
16
|
+
return (_jsxs(ShellMenuBar, { children: [_jsxs(DropdownMenu, { modal: false, open: systemOpen, onOpenChange: setSystemOpen, children: [_jsxs(DropdownMenuTrigger, { className: "tourtois-system-menu-trigger", render: _jsx(Button, { size: "menubar", variant: "ghost" }), "aria-label": "Team and account menu", "aria-description": "Current Team: " + teamName, title: teamName, children: [_jsx(MushroomMark, {}), _jsx("span", { className: "tourtois-team-name truncate", children: teamName })] }), _jsxs(DropdownMenuContent, { align: "start", container: popover,
|
|
17
|
+
// A sibling App click keeps focus there; Escape still returns here.
|
|
18
|
+
finalFocus: (interaction) => interaction === "keyboard" && document.hasFocus(), children: [_jsx(DropdownMenuItem, { onSelect: onOpenSettings, children: "System Settings\u2026" }), _jsx(DropdownMenuItem, { onSelect: onEditAvatar, children: "Edit avatar\u2026" }), systemMenuItems] })] }), appBar, onShowDesktop ? (_jsx(IconButton, { size: "responsive", icon: _jsx(Home, {}), "aria-label": "Show desktop", title: "Show desktop", onClick: onShowDesktop })) : null, _jsx("strong", { className: "tourtois-focused-app", children: title }), _jsx(FocusedAppMenus, {}, `menus:${focusedId ?? "shell"}`), onLayoutChange || onRestoreLayout || onArrangeDesktop ? (_jsxs(DropdownMenu, { modal: false, open: layoutOpen, onOpenChange: setLayoutOpen, children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { size: "menubar", variant: "ghost" }), children: "Layout" }), _jsxs(DropdownMenuContent, { align: "start", container: popover, finalFocus: (interaction) => interaction === "keyboard" && document.hasFocus(), children: [onLayoutChange ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuCheckboxItem, { checked: tiled, onCheckedChange: () => {
|
|
19
|
+
onLayoutChange(true);
|
|
20
|
+
}, children: "Tiled" }), _jsx(DropdownMenuCheckboxItem, { checked: !tiled, onCheckedChange: () => {
|
|
21
|
+
onLayoutChange(false);
|
|
22
|
+
}, children: "Floating" })] })) : null, onRestoreLayout ? (_jsx(DropdownMenuItem, { onSelect: onRestoreLayout, children: "Restore layout" })) : null, onArrangeDesktop ? (_jsx(DropdownMenuItem, { onSelect: onArrangeDesktop, children: "Arrange desktop" })) : null] })] }, `window:${focusedId ?? "shell"}`)) : null, focusedControls] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayer } from "@fungi.computer/maosaico";
|
|
3
|
+
import { AppMenus } from "./app-menus.js";
|
|
4
|
+
import { useAppSurfaceHost } from "./app-surface.js";
|
|
5
|
+
/** Render the focused App's commands with the shared shell renderer. */
|
|
6
|
+
export function FocusedAppMenus() {
|
|
7
|
+
const container = useLayer("popover");
|
|
8
|
+
const host = useAppSurfaceHost();
|
|
9
|
+
return _jsx(AppMenus, { host: host, container: container });
|
|
10
|
+
}
|
package/dist/Keys.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** One App's level a key may hold: read-only, or read and change. */
|
|
3
|
+
declare const keyAppAccessLevelSchema: z.ZodEnum<{
|
|
4
|
+
read: "read";
|
|
5
|
+
write: "write";
|
|
6
|
+
}>;
|
|
7
|
+
export type KeyAppAccessLevel = z.output<typeof keyAppAccessLevelSchema>;
|
|
8
|
+
/** What a grant reaches: every installed App (including future installs), or named ones. */
|
|
9
|
+
export declare const keyGrantTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
10
|
+
kind: z.ZodLiteral<"all">;
|
|
11
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
12
|
+
kind: z.ZodLiteral<"selected">;
|
|
13
|
+
installationIds: z.ZodArray<z.ZodString>;
|
|
14
|
+
}, z.core.$strict>], "kind">;
|
|
15
|
+
export type KeyGrantTarget = z.output<typeof keyGrantTargetSchema>;
|
|
16
|
+
/** One App-scoped grant a key holds. */
|
|
17
|
+
export declare const keyGrantSchema: z.ZodObject<{
|
|
18
|
+
level: z.ZodEnum<{
|
|
19
|
+
read: "read";
|
|
20
|
+
write: "write";
|
|
21
|
+
}>;
|
|
22
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
23
|
+
kind: z.ZodLiteral<"all">;
|
|
24
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
25
|
+
kind: z.ZodLiteral<"selected">;
|
|
26
|
+
installationIds: z.ZodArray<z.ZodString>;
|
|
27
|
+
}, z.core.$strict>], "kind">;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export type KeyGrant = z.output<typeof keyGrantSchema>;
|
|
30
|
+
/** One key as the host lists it; its secret never comes back after issuing. */
|
|
31
|
+
export declare const keyRecordSchema: z.ZodObject<{
|
|
32
|
+
keyId: z.ZodString;
|
|
33
|
+
name: z.ZodString;
|
|
34
|
+
grants: z.ZodArray<z.ZodObject<{
|
|
35
|
+
level: z.ZodEnum<{
|
|
36
|
+
read: "read";
|
|
37
|
+
write: "write";
|
|
38
|
+
}>;
|
|
39
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
40
|
+
kind: z.ZodLiteral<"all">;
|
|
41
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
42
|
+
kind: z.ZodLiteral<"selected">;
|
|
43
|
+
installationIds: z.ZodArray<z.ZodString>;
|
|
44
|
+
}, z.core.$strict>], "kind">;
|
|
45
|
+
}, z.core.$strict>>;
|
|
46
|
+
computerAccess: z.ZodBoolean;
|
|
47
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
48
|
+
createdAt: z.ZodNumber;
|
|
49
|
+
lastUsedAt: z.ZodNullable<z.ZodNumber>;
|
|
50
|
+
revokedAt: z.ZodNullable<z.ZodNumber>;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
export type KeyRecord = z.output<typeof keyRecordSchema>;
|
|
53
|
+
/** Every key this owner has issued. */
|
|
54
|
+
export declare const keysViewSchema: z.ZodObject<{
|
|
55
|
+
keys: z.ZodArray<z.ZodObject<{
|
|
56
|
+
keyId: z.ZodString;
|
|
57
|
+
name: z.ZodString;
|
|
58
|
+
grants: z.ZodArray<z.ZodObject<{
|
|
59
|
+
level: z.ZodEnum<{
|
|
60
|
+
read: "read";
|
|
61
|
+
write: "write";
|
|
62
|
+
}>;
|
|
63
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
64
|
+
kind: z.ZodLiteral<"all">;
|
|
65
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
66
|
+
kind: z.ZodLiteral<"selected">;
|
|
67
|
+
installationIds: z.ZodArray<z.ZodString>;
|
|
68
|
+
}, z.core.$strict>], "kind">;
|
|
69
|
+
}, z.core.$strict>>;
|
|
70
|
+
computerAccess: z.ZodBoolean;
|
|
71
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
72
|
+
createdAt: z.ZodNumber;
|
|
73
|
+
lastUsedAt: z.ZodNullable<z.ZodNumber>;
|
|
74
|
+
revokedAt: z.ZodNullable<z.ZodNumber>;
|
|
75
|
+
}, z.core.$strict>>;
|
|
76
|
+
}, z.core.$strict>;
|
|
77
|
+
export type KeysView = z.output<typeof keysViewSchema>;
|
|
78
|
+
/** What the owner submits to issue one key. */
|
|
79
|
+
export type KeyIssueDraft = Readonly<{
|
|
80
|
+
name: string;
|
|
81
|
+
grants: readonly KeyGrant[];
|
|
82
|
+
computerAccess: boolean;
|
|
83
|
+
expiresAt: number | null;
|
|
84
|
+
}>;
|
|
85
|
+
/** The plaintext secret, shown exactly once, plus the issued record. */
|
|
86
|
+
export declare const keyIssueResultSchema: z.ZodObject<{
|
|
87
|
+
plaintext: z.ZodString;
|
|
88
|
+
key: z.ZodObject<{
|
|
89
|
+
keyId: z.ZodString;
|
|
90
|
+
name: z.ZodString;
|
|
91
|
+
grants: z.ZodArray<z.ZodObject<{
|
|
92
|
+
level: z.ZodEnum<{
|
|
93
|
+
read: "read";
|
|
94
|
+
write: "write";
|
|
95
|
+
}>;
|
|
96
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
97
|
+
kind: z.ZodLiteral<"all">;
|
|
98
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
99
|
+
kind: z.ZodLiteral<"selected">;
|
|
100
|
+
installationIds: z.ZodArray<z.ZodString>;
|
|
101
|
+
}, z.core.$strict>], "kind">;
|
|
102
|
+
}, z.core.$strict>>;
|
|
103
|
+
computerAccess: z.ZodBoolean;
|
|
104
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
105
|
+
createdAt: z.ZodNumber;
|
|
106
|
+
lastUsedAt: z.ZodNullable<z.ZodNumber>;
|
|
107
|
+
revokedAt: z.ZodNullable<z.ZodNumber>;
|
|
108
|
+
}, z.core.$strict>;
|
|
109
|
+
}, z.core.$strict>;
|
|
110
|
+
export type KeyIssueResult = z.output<typeof keyIssueResultSchema>;
|
|
111
|
+
/** The host's owner-direct custody for Keys. */
|
|
112
|
+
export type KeysPort = Readonly<{
|
|
113
|
+
list(): Promise<KeysView>;
|
|
114
|
+
issue(draft: KeyIssueDraft): Promise<KeyIssueResult>;
|
|
115
|
+
revoke(keyId: string, requestId: string): Promise<KeyRecord>;
|
|
116
|
+
}>;
|
|
117
|
+
/** One App this Oyster has installed, as the Keys page needs it. */
|
|
118
|
+
export type KeyAppOption = Readonly<{
|
|
119
|
+
installationId: string;
|
|
120
|
+
displayName: string;
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* The Keys settings on a standalone host: issue, list and revoke keys that
|
|
124
|
+
* act with the owner's own access, limited to the Apps, level and expiry
|
|
125
|
+
* chosen here. A key never gets its own identity; it narrows the owner's.
|
|
126
|
+
*/
|
|
127
|
+
export declare function Keys({ port, apps, }: {
|
|
128
|
+
port: KeysPort;
|
|
129
|
+
apps: readonly KeyAppOption[];
|
|
130
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
131
|
+
export {};
|
package/dist/Keys.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { SectionHeading } from "@fungi.computer/caps/blocks/section-heading";
|
|
3
|
+
import { ActionRow } from "@fungi.computer/caps/components/action-row";
|
|
4
|
+
import { Badge } from "@fungi.computer/caps/components/badge";
|
|
5
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
6
|
+
import { Checkbox } from "@fungi.computer/caps/components/checkbox";
|
|
7
|
+
import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
|
|
8
|
+
import { Field, FormFooter } from "@fungi.computer/caps/components/field";
|
|
9
|
+
import { Select } from "@fungi.computer/caps/components/select";
|
|
10
|
+
import { FormFields, SchemaForm, field, useSchemaForm, useSchemaFormStatus, } from "@fungi.computer/caps/form";
|
|
11
|
+
import { defineIdFamily } from "@fungi.computer/cairn/id";
|
|
12
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import * as z from "zod";
|
|
15
|
+
/** One App's level a key may hold: read-only, or read and change. */
|
|
16
|
+
const keyAppAccessLevelSchema = z.enum(["read", "write"]);
|
|
17
|
+
/** What a grant reaches: every installed App (including future installs), or named ones. */
|
|
18
|
+
export const keyGrantTargetSchema = z.discriminatedUnion("kind", [
|
|
19
|
+
z.strictObject({ kind: z.literal("all") }),
|
|
20
|
+
z.strictObject({
|
|
21
|
+
kind: z.literal("selected"),
|
|
22
|
+
installationIds: z.array(z.string().min(1)),
|
|
23
|
+
}),
|
|
24
|
+
]);
|
|
25
|
+
/** One App-scoped grant a key holds. */
|
|
26
|
+
export const keyGrantSchema = z.strictObject({
|
|
27
|
+
level: keyAppAccessLevelSchema,
|
|
28
|
+
target: keyGrantTargetSchema,
|
|
29
|
+
});
|
|
30
|
+
/** One key as the host lists it; its secret never comes back after issuing. */
|
|
31
|
+
export const keyRecordSchema = z.strictObject({
|
|
32
|
+
keyId: z.string().min(1),
|
|
33
|
+
name: z.string().min(1),
|
|
34
|
+
grants: z.array(keyGrantSchema),
|
|
35
|
+
/** The computer.use grant: off unless the owner turns it on for this key. */
|
|
36
|
+
computerAccess: z.boolean(),
|
|
37
|
+
expiresAt: z.number().int().nonnegative().nullable(),
|
|
38
|
+
createdAt: z.number().int().nonnegative(),
|
|
39
|
+
lastUsedAt: z.number().int().nonnegative().nullable(),
|
|
40
|
+
revokedAt: z.number().int().nonnegative().nullable(),
|
|
41
|
+
});
|
|
42
|
+
/** Every key this owner has issued. */
|
|
43
|
+
export const keysViewSchema = z.strictObject({
|
|
44
|
+
keys: z.array(keyRecordSchema),
|
|
45
|
+
});
|
|
46
|
+
/** The plaintext secret, shown exactly once, plus the issued record. */
|
|
47
|
+
export const keyIssueResultSchema = z.strictObject({
|
|
48
|
+
plaintext: z.string().min(1),
|
|
49
|
+
key: keyRecordSchema,
|
|
50
|
+
});
|
|
51
|
+
const revokeIds = defineIdFamily({
|
|
52
|
+
family: "tourtois-keys-revoke-request",
|
|
53
|
+
version: 1,
|
|
54
|
+
kinds: { request: { prefix: "request" } },
|
|
55
|
+
});
|
|
56
|
+
const queryKey = ["tourtois", "keys"];
|
|
57
|
+
const accessLevelLabel = {
|
|
58
|
+
read: "Read",
|
|
59
|
+
write: "Read and change",
|
|
60
|
+
};
|
|
61
|
+
function grantSummary(grants, apps) {
|
|
62
|
+
const all = grants.find((grant) => grant.target.kind === "all");
|
|
63
|
+
if (all)
|
|
64
|
+
return `All Apps, including future installs (${accessLevelLabel[all.level]})`;
|
|
65
|
+
if (grants.length === 0)
|
|
66
|
+
return "No App access";
|
|
67
|
+
const names = new Map(apps.map((app) => [app.installationId, app.displayName]));
|
|
68
|
+
return grants
|
|
69
|
+
.flatMap((grant) => grant.target.kind === "selected"
|
|
70
|
+
? grant.target.installationIds.map((id) => `${names.get(id) ?? id} (${accessLevelLabel[grant.level]})`)
|
|
71
|
+
: [])
|
|
72
|
+
.join(", ");
|
|
73
|
+
}
|
|
74
|
+
function expirySummary(key) {
|
|
75
|
+
if (key.expiresAt === null)
|
|
76
|
+
return "Never expires";
|
|
77
|
+
const date = new Date(key.expiresAt).toLocaleDateString();
|
|
78
|
+
return key.expiresAt <= Date.now() ? `Expired ${date}` : `Expires ${date}`;
|
|
79
|
+
}
|
|
80
|
+
function lastUsedSummary(key) {
|
|
81
|
+
return key.lastUsedAt === null
|
|
82
|
+
? "Never used"
|
|
83
|
+
: `Last used ${new Date(key.lastUsedAt).toLocaleDateString()}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The Keys settings on a standalone host: issue, list and revoke keys that
|
|
87
|
+
* act with the owner's own access, limited to the Apps, level and expiry
|
|
88
|
+
* chosen here. A key never gets its own identity; it narrows the owner's.
|
|
89
|
+
*/
|
|
90
|
+
export function Keys({ port, apps, }) {
|
|
91
|
+
const client = useQueryClient();
|
|
92
|
+
const view = useQuery({ queryKey, queryFn: () => port.list() });
|
|
93
|
+
const refresh = () => client.invalidateQueries({ queryKey });
|
|
94
|
+
const revoke = useMutation({
|
|
95
|
+
mutationFn: (keyId) => port.revoke(keyId, revokeIds.request.generate()),
|
|
96
|
+
onSuccess: refresh,
|
|
97
|
+
});
|
|
98
|
+
const keys = view.data?.keys ?? [];
|
|
99
|
+
const [formKey, setFormKey] = useState(0);
|
|
100
|
+
const [issued, setIssued] = useState(null);
|
|
101
|
+
return (_jsxs("section", { className: "tourtois-settings-content tourtois-keys", "aria-label": "Keys", children: [_jsx(SectionHeading, { children: "Keys" }), _jsx("p", { className: "tourtois-settings-lede", children: "Anyone holding this key can read the Apps you pick. Where you choose \u201CRead and change,\u201D it can change data without asking you first. If it can change Workflows, it can also run Workflows with your access, but only on the Apps it can reach." }), view.isPending ? _jsx("p", { children: "Loading keys\u2026" }) : null, view.isError ? (_jsxs("div", { role: "status", children: [_jsx("p", { children: "Keys could not be loaded." }), _jsx(Button, { size: "sm", onClick: () => void view.refetch(), children: "Retry" })] })) : null, view.isSuccess && keys.length === 0 ? _jsx("p", { children: "No keys yet." }) : null, keys.length > 0 ? (_jsx(DataList, { "aria-label": "Keys", children: keys.map((key) => {
|
|
102
|
+
const revoked = key.revokedAt !== null;
|
|
103
|
+
return (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { label: _jsxs(_Fragment, { children: [key.name, " ", revoked ? (_jsx(Badge, { size: "sm", variant: "quiet", children: "Revoked" })) : null, key.computerAccess ? (_jsx(Badge, { size: "sm", variant: "quiet", children: "Computer" })) : null] }), description: `${grantSummary(key.grants, apps)} · ${expirySummary(key)} · ${lastUsedSummary(key)}`, trailing: revoked ? null : (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Revoke ${key.name}`, disabled: revoke.isPending, onClick: () => {
|
|
104
|
+
revoke.mutate(key.keyId);
|
|
105
|
+
}, children: "Revoke" })) }) }, key.keyId));
|
|
106
|
+
}) })) : null, revoke.isError ? (_jsx("p", { role: "alert", children: "The key could not be revoked." })) : null, _jsx(SectionHeading, { children: "New key" }), issued === null ? (_jsx(IssueKey, { apps: apps, onIssue: async (draft) => {
|
|
107
|
+
const result = await port.issue(draft);
|
|
108
|
+
setIssued(result);
|
|
109
|
+
await refresh();
|
|
110
|
+
} }, formKey)) : (_jsxs("div", { className: "grid gap-sm", role: "status", children: [_jsx("p", { children: "Copy this key now. It is shown once and cannot be retrieved later." }), _jsx("code", { className: "break-all", children: issued.plaintext }), _jsx(Button, { type: "button", variant: "outline", onClick: () => {
|
|
111
|
+
setIssued(null);
|
|
112
|
+
setFormKey((current) => current + 1);
|
|
113
|
+
}, children: "I copied the key" })] }))] }));
|
|
114
|
+
}
|
|
115
|
+
const expiryChoiceSchema = z.enum(["never", "30", "90", "365"]);
|
|
116
|
+
const issueFormFields = z.object({
|
|
117
|
+
name: z
|
|
118
|
+
.string()
|
|
119
|
+
.trim()
|
|
120
|
+
.min(1, "Give this key a name.")
|
|
121
|
+
.max(128)
|
|
122
|
+
.meta(field({ label: "Name", required: true, maxLength: 128 })),
|
|
123
|
+
expiry: expiryChoiceSchema.meta(field({
|
|
124
|
+
label: "Expires after",
|
|
125
|
+
options: {
|
|
126
|
+
never: "Never",
|
|
127
|
+
"30": "30 days",
|
|
128
|
+
"90": "90 days",
|
|
129
|
+
"365": "1 year",
|
|
130
|
+
},
|
|
131
|
+
})),
|
|
132
|
+
});
|
|
133
|
+
const issueFormDefaults = { name: "", expiry: "never" };
|
|
134
|
+
const perAppChoiceSchema = z.enum(["none", "read", "write"]);
|
|
135
|
+
function IssueKey({ apps, onIssue, }) {
|
|
136
|
+
const [allApps, setAllApps] = useState(false);
|
|
137
|
+
const [allAppsLevel, setAllAppsLevel] = useState("read");
|
|
138
|
+
const [perApp, setPerApp] = useState(() => Object.fromEntries(apps.map((app) => [app.installationId, "none"])));
|
|
139
|
+
const [computerAccess, setComputerAccess] = useState(false);
|
|
140
|
+
const selectedCount = Object.values(perApp).filter((choice) => choice !== "none").length;
|
|
141
|
+
const hasAccess = allApps || selectedCount > 0;
|
|
142
|
+
const model = useSchemaForm({
|
|
143
|
+
schema: issueFormFields,
|
|
144
|
+
defaultValues: issueFormDefaults,
|
|
145
|
+
onSubmit: async (value) => {
|
|
146
|
+
if (!hasAccess)
|
|
147
|
+
return {
|
|
148
|
+
kind: "refused",
|
|
149
|
+
error: "Choose at least one App before issuing a key.",
|
|
150
|
+
};
|
|
151
|
+
const grants = allApps
|
|
152
|
+
? [{ level: allAppsLevel, target: { kind: "all" } }]
|
|
153
|
+
: ["write", "read"].flatMap((level) => {
|
|
154
|
+
const installationIds = Object.entries(perApp)
|
|
155
|
+
.filter(([, choice]) => choice === level)
|
|
156
|
+
.map(([installationId]) => installationId);
|
|
157
|
+
return installationIds.length === 0
|
|
158
|
+
? []
|
|
159
|
+
: [
|
|
160
|
+
{
|
|
161
|
+
level,
|
|
162
|
+
target: { kind: "selected", installationIds },
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
await onIssue({
|
|
168
|
+
name: value.name,
|
|
169
|
+
grants,
|
|
170
|
+
computerAccess,
|
|
171
|
+
expiresAt: value.expiry === "never"
|
|
172
|
+
? null
|
|
173
|
+
: Date.now() + Number(value.expiry) * 24 * 60 * 60 * 1000,
|
|
174
|
+
});
|
|
175
|
+
return { kind: "saved" };
|
|
176
|
+
}
|
|
177
|
+
catch (cause) {
|
|
178
|
+
return {
|
|
179
|
+
kind: "refused",
|
|
180
|
+
error: cause instanceof Error
|
|
181
|
+
? cause.message
|
|
182
|
+
: "The key could not be issued.",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const { submitting: issuing } = useSchemaFormStatus(model);
|
|
188
|
+
return (_jsxs(SchemaForm, { model: model, "aria-label": "Issue a key", disabled: issuing, footer: _jsx(FormFooter, { error: model.error, children: _jsx(Button, { type: "submit", disabled: !hasAccess || issuing, children: issuing ? "Issuing…" : "Issue key" }) }), children: [_jsx(FormFields, { model: model, disabled: issuing }), _jsx(Field, { label: "All Apps, including future installs", orientation: "inline", children: _jsx(Checkbox, { name: "allApps", checked: allApps, disabled: issuing, onChange: (event) => {
|
|
189
|
+
setAllApps(event.currentTarget.checked);
|
|
190
|
+
} }) }), allApps ? (_jsx(Field, { label: "Access level for all Apps", orientation: "horizontal", children: _jsxs(Select, { name: "allAppsLevel", disabled: issuing, value: allAppsLevel, onChange: (event) => {
|
|
191
|
+
setAllAppsLevel(keyAppAccessLevelSchema.parse(event.currentTarget.value));
|
|
192
|
+
}, children: [_jsx("option", { value: "read", children: "Read" }), _jsx("option", { value: "write", children: "Read and change" })] }) })) : apps.length === 0 ? (_jsx("p", { children: "No Apps installed yet." })) : (apps.map((app) => (_jsx(Field, { label: app.displayName, orientation: "horizontal", children: _jsxs(Select, { name: `app:${app.installationId}`, disabled: issuing, value: perApp[app.installationId] ?? "none", onChange: (event) => {
|
|
193
|
+
const choice = perAppChoiceSchema.parse(event.currentTarget.value);
|
|
194
|
+
setPerApp((current) => ({
|
|
195
|
+
...current,
|
|
196
|
+
[app.installationId]: choice,
|
|
197
|
+
}));
|
|
198
|
+
}, children: [_jsx("option", { value: "none", children: "No access" }), _jsx("option", { value: "read", children: "Read" }), _jsx("option", { value: "write", children: "Read and change" })] }) }, app.installationId)))), _jsx(Field, { label: "Computer access", description: "Off by default. On lets this key run commands on this Oyster\u2019s Computer.", orientation: "inline", children: _jsx(Checkbox, { name: "computerAccess", checked: computerAccess, disabled: issuing, onChange: (event) => {
|
|
199
|
+
setComputerAccess(event.currentTarget.checked);
|
|
200
|
+
} }) })] }));
|
|
201
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Selected softer-edge mark. Original trace and studies remain in Caps Ladle. */
|
|
3
|
+
export function MushroomMark() {
|
|
4
|
+
return (_jsxs("svg", { viewBox: "300 210 680 550", fill: "currentColor", role: "img", "aria-label": "Fungi", className: "tourtois-brand-mark", children: [_jsx("path", { d: "\n M 333 469\n C 331 423 394 347 448 300\n C 491 262 548 238 596 238\n C 665 235 721 281 787 300\n C 831 315 861 305 890 303\n C 918 301 939 320 944 346\n C 953 401 913 453 865 487\n C 829 510 779 527 735 524\n C 730 521 735 518 745 513\n C 805 494 854 470 852 451\n C 852 429 792 436 738 450\n C 650 474 532 524 429 529\n C 375 532 331 512 333 469 Z" }), _jsx("path", { d: "\n M 550 518\n C 583 508 657 503 690 513 C 694 514 695 517 695 522\n C 695 572 714 618 744 665\n C 767 703 729 729 628 730\n C 551 732 490 720 489 688\n C 486 669 526 631 540 584\n C 546 559 549 538 550 518 Z" })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SettingsLocation } from "./settings-routes.js";
|
|
2
|
+
import type { TeamAdministrationPort } from "./settings-sections.js";
|
|
3
|
+
/** Read-only projections of data the host already loaded. Null is not zero. */
|
|
4
|
+
export type SettingsHomeSummary = Readonly<{
|
|
5
|
+
appCount: number | null;
|
|
6
|
+
defaultAppCount: number | null;
|
|
7
|
+
computerCount: number | null;
|
|
8
|
+
}>;
|
|
9
|
+
/** A quiet index, not a second settings editor or a new data reader. */
|
|
10
|
+
export declare function SettingsHome({ team, summary, sections, desktopOnly, onNavigate, }: {
|
|
11
|
+
/** The host's Team administration links, when it has them. */
|
|
12
|
+
team?: TeamAdministrationPort | undefined;
|
|
13
|
+
summary: SettingsHomeSummary;
|
|
14
|
+
/** Destinations this host backs. */
|
|
15
|
+
sections: readonly SettingsLocation["category"][];
|
|
16
|
+
desktopOnly?: boolean;
|
|
17
|
+
onNavigate: (location: SettingsLocation | null) => void;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DataListHeading } from "@fungi.computer/caps/components/data-list";
|
|
3
|
+
import { ActionRow } from "@fungi.computer/caps/components/action-row";
|
|
4
|
+
import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
|
|
5
|
+
import { Sigil } from "@fungi.computer/sigils";
|
|
6
|
+
import { renderStyleLabels } from "@fungi.computer/sprite";
|
|
7
|
+
import { OwnSprite, useMemberAvatar } from "./member-avatar.js";
|
|
8
|
+
function OverviewValue({ children }) {
|
|
9
|
+
return (_jsx("span", { className: "caps-action-row-description whitespace-nowrap tabular-nums", children: children }));
|
|
10
|
+
}
|
|
11
|
+
function AvatarOverview({ onNavigate, }) {
|
|
12
|
+
const sprite = useMemberAvatar();
|
|
13
|
+
return (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Avatar", icon: _jsx(OwnSprite, { "aria-hidden": "true", className: "tourtois-settings-nav-sprite" }), label: "Avatar", description: "Your character, colours and drawing style.", trailing: _jsx(OverviewValue, { children: renderStyleLabels[sprite.avatar.renderStyle] }), onClick: () => {
|
|
14
|
+
onNavigate({ category: "avatar" });
|
|
15
|
+
} }) }));
|
|
16
|
+
}
|
|
17
|
+
/** A quiet index, not a second settings editor or a new data reader. */
|
|
18
|
+
export function SettingsHome({ team, summary, sections, desktopOnly = false, onNavigate, }) {
|
|
19
|
+
const offers = (category) => sections.includes(category);
|
|
20
|
+
const close = team?.close;
|
|
21
|
+
return (_jsxs("section", { className: "tourtois-settings-content", "aria-label": "Settings home", children: [_jsxs(DataList, { density: "compact", "aria-label": "Settings overview", children: [offers("channels") ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Website assistant", label: "Website assistant", description: "Set up chat on your WordPress site.", icon: _jsx(Sigil, { name: "internet" }), onClick: () => {
|
|
22
|
+
onNavigate({ category: "channels" });
|
|
23
|
+
} }) })) : null, offers("selling") ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Selling", label: "Selling", description: "Sell Apps on the Bazaar.", icon: _jsx(Sigil, { name: "sell" }), onClick: () => {
|
|
24
|
+
onNavigate({ category: "selling", appId: null });
|
|
25
|
+
} }) })) : null, offers("avatar") ? _jsx(AvatarOverview, { onNavigate: onNavigate }) : null, offers("defaults") ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Default apps", icon: _jsx(Sigil, { name: "installApps" }), label: "Default apps", description: "Apps for everyday tasks.", trailing: summary.defaultAppCount === null ? null : (_jsx(OverviewValue, { children: String(summary.defaultAppCount) +
|
|
26
|
+
(summary.defaultAppCount === 1 ? " role" : " roles") })), onClick: () => {
|
|
27
|
+
onNavigate({ category: "defaults" });
|
|
28
|
+
} }) })) : null, team !== undefined ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Members and invites", icon: _jsx(Sigil, { name: "members" }), label: "Members and invites", description: "People and invitations.", onClick: team.openMembers }) })) : null, offers("connectors") ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Connectors", icon: _jsx(Sigil, { name: "read" }), label: "Connectors", description: "What Agents can reach.", onClick: () => {
|
|
29
|
+
onNavigate({
|
|
30
|
+
category: "connectors",
|
|
31
|
+
target: { kind: "list" },
|
|
32
|
+
});
|
|
33
|
+
} }) })) : null, offers("computers") ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Internet access", icon: _jsx(Sigil, { name: "internet" }), label: "Internet access", description: "Each Computer\u2019s outside access.", trailing: summary.computerCount === null ? null : (_jsx(OverviewValue, { children: String(summary.computerCount) +
|
|
34
|
+
(summary.computerCount === 1
|
|
35
|
+
? " Computer"
|
|
36
|
+
: " Computers") })), onClick: () => {
|
|
37
|
+
onNavigate({ category: "computers" });
|
|
38
|
+
} }) })) : null, offers("apps") ? (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": desktopOnly ? "App desktop" : "App permissions", icon: _jsx(Sigil, { name: "restricted" }), label: desktopOnly ? "App desktop" : "App permissions", description: "Choose an App to review.", trailing: summary.appCount === null ? null : (_jsx(OverviewValue, { children: String(summary.appCount) +
|
|
39
|
+
(summary.appCount === 1 ? " App" : " Apps") })), onClick: () => {
|
|
40
|
+
onNavigate({
|
|
41
|
+
category: "apps",
|
|
42
|
+
appKey: null,
|
|
43
|
+
section: desktopOnly ? "desktop" : "permissions",
|
|
44
|
+
});
|
|
45
|
+
} }) })) : null] }), close !== undefined ? (_jsxs("section", { className: "grid gap-sm", "aria-label": "Danger zone", children: [_jsx(DataListHeading, { level: 3, className: "tourtois-settings-app-heading", children: "Danger zone" }), _jsx(DataList, { density: "compact", "aria-label": "Team closure", children: _jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { "aria-label": "Close Team", icon: _jsx(Sigil, { name: "restricted" }), label: "Close Team", description: close.available
|
|
46
|
+
? "Stop billing and hide this Team"
|
|
47
|
+
: "Closing is not available yet", disabled: !close.available, onClick: close.open }) }) })] })) : null] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AppAddress } from "@fungi.computer/oyster/shell/app-directory";
|
|
2
|
+
import type { SettingsLocation } from "./settings-routes.js";
|
|
3
|
+
export type NavigationApp = Readonly<{
|
|
4
|
+
address: AppAddress;
|
|
5
|
+
appId: string;
|
|
6
|
+
displayName?: string | undefined;
|
|
7
|
+
icon?: string | null | undefined;
|
|
8
|
+
roles: readonly string[];
|
|
9
|
+
}>;
|
|
10
|
+
export declare function SettingsNavigation({ location, apps, sections, desktopOnly, onNavigate, }: {
|
|
11
|
+
location: SettingsLocation | null;
|
|
12
|
+
apps: readonly NavigationApp[];
|
|
13
|
+
/** Only destinations backed by the embedding host. */
|
|
14
|
+
sections: readonly SettingsLocation["category"][];
|
|
15
|
+
desktopOnly?: boolean;
|
|
16
|
+
onNavigate: (location: SettingsLocation | null) => void;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element;
|