@hosty-sdk/app 0.26.0 → 0.27.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -8
- package/dist/index.d.ts +9 -6
- package/dist/index.js +10 -1
- package/dist/next.d.ts +4 -0
- package/dist/next.js +29 -0
- package/dist/overlay-react.d.ts +7 -1
- package/dist/overlay-react.js +2 -2
- package/dist/react.d.ts +1 -0
- package/dist/react.js +3 -0
- package/dist/ui-composition.d.ts +71 -0
- package/dist/ui-composition.js +232 -0
- package/dist/ui-context.d.ts +2 -0
- package/dist/ui-context.js +1 -0
- package/dist/ui-embedded.d.ts +18 -1
- package/dist/ui-embedded.js +123 -17
- package/dist/ui-frame-layer.d.ts +36 -0
- package/dist/ui-frame-layer.js +88 -0
- package/dist/ui-frame.d.ts +2 -0
- package/dist/ui-frame.js +43 -7
- package/dist/ui-host.d.ts +62 -15
- package/dist/ui-host.js +666 -76
- package/dist/ui-navigation-react.d.ts +17 -0
- package/dist/ui-navigation-react.js +89 -0
- package/dist/ui-react.d.ts +13 -3
- package/dist/ui-react.js +127 -50
- package/dist/ui-server.d.ts +3 -1
- package/dist/ui-server.js +14 -2
- package/dist/ui-transport.d.ts +2 -2
- package/dist/ui-transport.js +1 -1
- package/dist/ui-types.d.ts +88 -0
- package/dist/ui-types.js +1 -0
- package/dist/ui-workspaces.d.ts +54 -0
- package/dist/ui-workspaces.js +3 -0
- package/dist/ui.d.ts +7 -1
- package/dist/ui.js +22 -0
- package/package.json +5 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare function useHostyChrome(): import("./ui-types").UiChromeSnapshot;
|
|
3
|
+
export declare function useHostyNavigation(): Readonly<{
|
|
4
|
+
status: "loading" | "ready" | "unavailable";
|
|
5
|
+
catalog: import("./ui-types").UiNavigationCatalog | null;
|
|
6
|
+
entries: readonly import("./ui-types").UiNavigationEntry[];
|
|
7
|
+
activeKey: string | null;
|
|
8
|
+
code?: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function HostyNavigationBoundary({ children, className }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export declare function HostyToolLauncher(): import("react").JSX.Element | null;
|
|
15
|
+
export declare function HostyNavigation({ className }: {
|
|
16
|
+
className?: string;
|
|
17
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { ui, createPresentationRequestId } from "./ui.js";
|
|
5
|
+
import { INITIAL_CHROME, INITIAL_NAVIGATION } from "./ui-composition.js";
|
|
6
|
+
import { UiCompositionContext } from "./ui-context.js";
|
|
7
|
+
import { UI_COMPOSITION_CAPABILITY } from "./ui-types.js";
|
|
8
|
+
export function useHostyChrome() { return useSyncExternalStore(ui.subscribeChrome, ui.getChrome, () => INITIAL_CHROME); }
|
|
9
|
+
export function useHostyNavigation() { return useSyncExternalStore(ui.subscribeNavigation, ui.getNavigation, () => INITIAL_NAVIGATION); }
|
|
10
|
+
export function HostyNavigationBoundary({ children, className }) {
|
|
11
|
+
const chrome = useHostyChrome(), ref = useRef(null), hidden = chrome.navigation === "hide";
|
|
12
|
+
const focused = useRef(false);
|
|
13
|
+
useLayoutEffect(() => { if (hidden && focused.current) {
|
|
14
|
+
document.querySelector('[data-hosty-content][tabindex="-1"]')?.focus();
|
|
15
|
+
focused.current = false;
|
|
16
|
+
} }, [hidden]);
|
|
17
|
+
return _jsx("div", { ref: ref, onFocusCapture: () => { focused.current = true; }, onBlurCapture: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
18
|
+
focused.current = false; }, className: className, "data-hosty-navigation-boundary": true, hidden: hidden, inert: hidden, style: hidden ? { display: "none" } : { display: "contents" }, children: children });
|
|
19
|
+
}
|
|
20
|
+
export function HostyToolLauncher() {
|
|
21
|
+
const chrome = useHostyChrome(), [error, setError] = useState(null);
|
|
22
|
+
if (chrome.tools.owner === "local" || !chrome.tools.enabled || chrome.tools.launcher === "hide"
|
|
23
|
+
|| !chrome.capabilities.includes(UI_COMPOSITION_CAPABILITY))
|
|
24
|
+
return null;
|
|
25
|
+
return _jsxs("div", { className: "hosty-child-tools", children: [_jsx("button", { type: "button", disabled: chrome.tools.owner !== "host", onClick: async () => {
|
|
26
|
+
const result = await ui.openToolPicker({ requestId: createPresentationRequestId() });
|
|
27
|
+
setError(result.status === "accepted" ? null : "Tools are unavailable. Reconnect to your host or open the app separately.");
|
|
28
|
+
}, children: "Tools" }), error && _jsx("span", { role: "status", children: error })] });
|
|
29
|
+
}
|
|
30
|
+
export function HostyNavigation({ className = "" }) {
|
|
31
|
+
const composition = useContext(UiCompositionContext);
|
|
32
|
+
const model = useHostyNavigation(), chrome = useHostyChrome();
|
|
33
|
+
const [expanded, setExpanded] = useState(false), [error, setError] = useState(null);
|
|
34
|
+
const [narrow, setNarrow] = useState(false), dialog = useRef(null), opener = useRef(null);
|
|
35
|
+
useEffect(() => { const resize = () => setNarrow(window.innerWidth < 900); resize(); window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, []);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const modal = (event) => { if (event.detail !== "navigation")
|
|
38
|
+
setExpanded(false); };
|
|
39
|
+
window.addEventListener("hosty:ui:modal", modal);
|
|
40
|
+
return () => window.removeEventListener("hosty:ui:modal", modal);
|
|
41
|
+
}, []);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (expanded && narrow && chrome.navigation === "show") {
|
|
44
|
+
window.dispatchEvent(new CustomEvent("hosty:ui:modal", { detail: "navigation" }));
|
|
45
|
+
dialog.current?.showModal();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
if (dialog.current?.open) {
|
|
49
|
+
dialog.current.close();
|
|
50
|
+
if (chrome.navigation === "show")
|
|
51
|
+
opener.current?.focus();
|
|
52
|
+
else
|
|
53
|
+
document.querySelector('[data-hosty-content][tabindex="-1"]')?.focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, [expanded, narrow, chrome.navigation]);
|
|
57
|
+
const empty = model.status === "ready" && !model.entries.length;
|
|
58
|
+
const links = _jsxs(_Fragment, { children: [model.status === "loading" && _jsx("p", { role: "status", children: "Loading navigation\u2026" }), model.status === "unavailable" && _jsxs("p", { role: "status", children: ["Navigation is unavailable. ", _jsx("button", { type: "button", onClick: () => void composition?.refreshNavigation(), children: "Retry navigation" })] }), model.entries.map(entry => _jsxs("a", { href: entry.standaloneHref ?? undefined, "aria-current": model.activeKey === entry.key ? "page" : undefined, "aria-disabled": !entry.route || !entry.available, title: !entry.available ? entry.state : undefined, onClick: async (event) => {
|
|
59
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.button !== 0)
|
|
60
|
+
return;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
if (!entry.route || !entry.available)
|
|
63
|
+
return;
|
|
64
|
+
const result = await ui.navigate(entry.key, { requestId: createPresentationRequestId() });
|
|
65
|
+
if (result.status === "accepted") {
|
|
66
|
+
setExpanded(false);
|
|
67
|
+
setError(null);
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
setError(`Cannot open this page (${result.code}). Try again or open it separately.`);
|
|
71
|
+
}, children: [entry.icon && _jsx("img", { src: entry.icon, alt: "", width: 18, height: 18 }), _jsx("span", { children: entry.label })] }, entry.key)), error && _jsx("p", { role: "status", children: error })] });
|
|
72
|
+
if (empty)
|
|
73
|
+
return null;
|
|
74
|
+
return _jsxs(HostyNavigationBoundary, { children: [_jsx("style", { children: navigationStyles }), _jsx("div", { className: `hosty-navigation ${className}`, children: narrow ? _jsxs(_Fragment, { children: [_jsx("button", { ref: opener, type: "button", "aria-label": "Page navigation", "aria-expanded": expanded, onClick: () => setExpanded(true), children: "\u2630" }), _jsxs("dialog", { ref: dialog, className: "hosty-navigation-dialog", "aria-label": "Page navigation", onCancel: () => setExpanded(false), children: [_jsx("button", { type: "button", onClick: () => setExpanded(false), children: "Close navigation" }), _jsx("nav", { "aria-label": "App pages", children: links })] })] }) : _jsx("nav", { "aria-label": "App pages", children: links }) })] });
|
|
75
|
+
}
|
|
76
|
+
const navigationStyles = `
|
|
77
|
+
[data-hosty-navigation=hide] [data-hosty-navigation-boundary]{display:none!important}
|
|
78
|
+
.hosty-app-layout{display:flex;width:100%;height:100%;min-height:0;min-width:0}
|
|
79
|
+
.hosty-app-content{flex:1;min-width:0;min-height:0;overflow:auto;outline:none}
|
|
80
|
+
.hosty-navigation{flex:0 0 190px;box-sizing:border-box;padding:12px;border-right:1px solid color-mix(in srgb,currentColor 14%,transparent);font:14px system-ui}
|
|
81
|
+
.hosty-navigation nav,.hosty-navigation-dialog nav{display:flex;flex-direction:column;gap:4px}
|
|
82
|
+
.hosty-navigation a,.hosty-navigation-dialog a{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:6px;text-decoration:none;color:inherit}
|
|
83
|
+
.hosty-navigation a[aria-current=page],.hosty-navigation-dialog a[aria-current=page]{background:color-mix(in srgb,currentColor 10%,transparent);font-weight:600}
|
|
84
|
+
.hosty-navigation a[aria-disabled=true]{opacity:.55;cursor:not-allowed}
|
|
85
|
+
.hosty-navigation button,.hosty-child-tools button,.hosty-navigation-dialog button{font:inherit;color:inherit;background:transparent;border:1px solid color-mix(in srgb,currentColor 20%,transparent);border-radius:6px;padding:7px 10px;cursor:pointer}
|
|
86
|
+
.hosty-navigation-dialog{position:fixed;inset:0 auto 0 0;margin:0;height:100dvh;width:min(300px,85vw);box-sizing:border-box;border:0;padding:16px;background:Canvas;color:CanvasText}
|
|
87
|
+
.hosty-navigation-dialog::backdrop{background:#0007}.hosty-child-tools{padding:6px 10px}
|
|
88
|
+
@media(max-width:899px){.hosty-app-layout{flex-direction:column}.hosty-navigation{flex:0 0 auto;border-right:0;padding:6px 10px}}
|
|
89
|
+
`;
|
package/dist/ui-react.d.ts
CHANGED
|
@@ -2,18 +2,27 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import { type UiClient } from "./ui.js";
|
|
3
3
|
import { type UiHost } from "./ui-host.js";
|
|
4
4
|
import { type HostyAppFrameProps } from "./ui-frame.js";
|
|
5
|
-
import { type PanelTarget, type UiPanel } from "./ui-types.js";
|
|
5
|
+
import { type PanelTarget, type UiPanel, type UiChromeOverrides, type UiRouterAdapter } from "./ui-types.js";
|
|
6
|
+
export { HostyNavigation, HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "./ui-navigation-react.js";
|
|
6
7
|
export { HostyAppFrame, type HostyAppFrameProps } from "./ui-frame.js";
|
|
7
8
|
export declare function useHostyUi(): UiClient;
|
|
8
9
|
export declare function useHostyUiHost(): UiHost | null;
|
|
9
10
|
/** Internal composition root, owned by HostyOverlay rather than mounted again by domain apps. */
|
|
10
|
-
export declare function HostyUiRoot({ children, uiPath, panelLayout, active }: {
|
|
11
|
+
export declare function HostyUiRoot({ children, uiPath, panelLayout, active, chrome, navigation }: {
|
|
12
|
+
chrome?: UiChromeOverrides;
|
|
13
|
+
navigation?: {
|
|
14
|
+
renderer: "standard" | "custom";
|
|
15
|
+
router?: UiRouterAdapter;
|
|
16
|
+
};
|
|
11
17
|
children: ReactNode;
|
|
12
18
|
uiPath?: string;
|
|
13
19
|
panelLayout?: "auto" | "slot" | "none";
|
|
14
20
|
active?: boolean;
|
|
15
21
|
}): import("react").JSX.Element;
|
|
16
22
|
export type HostyPanelWorkspaceProps = {
|
|
23
|
+
workspaceId?: string;
|
|
24
|
+
/** A host-owned tool window can keep dock geometry even in a narrow container. */
|
|
25
|
+
layout?: "responsive" | "dock";
|
|
17
26
|
children: ReactNode;
|
|
18
27
|
railMode?: "opened" | "catalog";
|
|
19
28
|
className?: string;
|
|
@@ -26,10 +35,11 @@ export type HostyPanelWorkspaceProps = {
|
|
|
26
35
|
onAuthRequired?: HostyAppFrameProps["onAuthRequired"];
|
|
27
36
|
outbound?: {
|
|
28
37
|
target: PanelTarget;
|
|
38
|
+
presentationId?: string;
|
|
29
39
|
message: unknown;
|
|
30
40
|
nonce: number;
|
|
31
41
|
} | null;
|
|
32
42
|
renderIcon?: (panel: UiPanel) => ReactNode;
|
|
33
43
|
};
|
|
34
44
|
/** A single retained frame tree for dock and drawer; only CSS placement changes at the breakpoint. */
|
|
35
|
-
export declare function HostyPanelWorkspace({ children, railMode, className, initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }: HostyPanelWorkspaceProps): import("react").JSX.Element;
|
|
45
|
+
export declare function HostyPanelWorkspace({ children, workspaceId, layout, railMode, className, initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }: HostyPanelWorkspaceProps): import("react").JSX.Element;
|
package/dist/ui-react.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { bindUiClient, ui } from "./ui.js";
|
|
5
5
|
import { createUiHost } from "./ui-host.js";
|
|
6
6
|
import { EmbeddedUiClient } from "./ui-embedded.js";
|
|
7
|
-
import { UiHostContext } from "./ui-context.js";
|
|
8
|
-
import { HostyAppFrame } from "./ui-frame.js";
|
|
7
|
+
import { UiHostContext, UiCompositionContext } from "./ui-context.js";
|
|
9
8
|
import { panelKey } from "./ui-types.js";
|
|
9
|
+
import { HostyNavigation, HostyToolLauncher, useHostyChrome } from "./ui-navigation-react.js";
|
|
10
|
+
export { HostyNavigation, HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "./ui-navigation-react.js";
|
|
11
|
+
import { captureChromeHints } from "./ui-composition.js";
|
|
12
|
+
import { DEFAULT_WORKSPACE } from "./ui-workspaces.js";
|
|
13
|
+
import { UiFrameLayer, UiFrameLayerContext, HostyPresentationLayer, presentationElement } from "./ui-frame-layer.js";
|
|
10
14
|
import { uiPath as normalizeUiPath } from "./ui-transport.js";
|
|
11
15
|
export { HostyAppFrame } from "./ui-frame.js";
|
|
12
16
|
export function useHostyUi() { return ui; }
|
|
13
17
|
export function useHostyUiHost() { return useContext(UiHostContext); }
|
|
14
18
|
/** Internal composition root, owned by HostyOverlay rather than mounted again by domain apps. */
|
|
15
|
-
export function HostyUiRoot({ children, uiPath, panelLayout = "auto", active = true }) {
|
|
19
|
+
export function HostyUiRoot({ children, uiPath, panelLayout = "auto", active = true, chrome, navigation }) {
|
|
20
|
+
const [layer] = useState(() => new UiFrameLayer());
|
|
16
21
|
const [host, setHost] = useState(null);
|
|
17
22
|
const [environment, setEnvironment] = useState(null);
|
|
18
23
|
const mounted = useRef(false);
|
|
@@ -23,9 +28,15 @@ export function HostyUiRoot({ children, uiPath, panelLayout = "auto", active = t
|
|
|
23
28
|
clientRef.current?.setActive(active);
|
|
24
29
|
}, [active]);
|
|
25
30
|
useEffect(() => {
|
|
31
|
+
captureChromeHints();
|
|
32
|
+
const address = new URL(window.location.href);
|
|
33
|
+
for (const key of ["hosty_navigation", "hosty_tools", "hosty_tool_launcher"])
|
|
34
|
+
address.searchParams.delete(key);
|
|
35
|
+
window.history.replaceState(window.history.state, "", address.pathname + address.search + address.hash);
|
|
26
36
|
const path = normalizeUiPath(uiPath);
|
|
27
|
-
const client = window.parent === window ? createUiHost({ uiPath: path, active: activeRef.current }) : new EmbeddedUiClient(path);
|
|
37
|
+
const client = window.parent === window ? createUiHost({ uiPath: path, active: activeRef.current, chrome }) : new EmbeddedUiClient(path, chrome);
|
|
28
38
|
client.setActive(activeRef.current);
|
|
39
|
+
client.composition.attachRouter(navigation?.router);
|
|
29
40
|
clientRef.current = client;
|
|
30
41
|
const release = bindUiClient(client), stop = client.subscribe(event => { if (event.type === "environment")
|
|
31
42
|
setEnvironment(event.value); });
|
|
@@ -35,41 +46,89 @@ export function HostyUiRoot({ children, uiPath, panelLayout = "auto", active = t
|
|
|
35
46
|
setHost(client);
|
|
36
47
|
return () => { stop(); release(); client.dispose(); clientRef.current = null; };
|
|
37
48
|
}, [uiPath]);
|
|
49
|
+
useEffect(() => { clientRef.current?.composition.configure(chrome ?? {}); }, [chrome]);
|
|
50
|
+
useEffect(() => { clientRef.current?.composition.attachRouter(navigation?.router); }, [navigation?.router]);
|
|
38
51
|
const allowed = environment?.mode === "standalone" || environment?.connection === "connected" || environment?.connection === "unsupported";
|
|
39
52
|
if (allowed)
|
|
40
53
|
mounted.current = true;
|
|
41
|
-
|
|
54
|
+
const content = _jsxs("div", { className: "hosty-app-layout", children: [navigation?.renderer !== "custom" && _jsx(HostyNavigation, {}), _jsxs("div", { className: "hosty-app-content", "data-hosty-content": true, tabIndex: -1, children: [_jsx(HostyToolLauncher, {}), children] })] });
|
|
55
|
+
return _jsx(UiHostContext.Provider, { value: host, children: _jsx(UiCompositionContext.Provider, { value: clientRef.current?.composition ?? null, children: _jsxs(UiFrameLayerContext.Provider, { value: layer, children: [_jsx("style", { children: `.hosty-app-layout{display:flex;width:100%;height:100%;min-width:0;min-height:0}.hosty-app-content{flex:1;min-width:0;min-height:0;overflow:auto;outline:none}` }), _jsx("div", { hidden: !allowed, inert: !allowed, style: { display: allowed ? "contents" : "none" }, children: mounted.current && (panelLayout === "auto" && host ? _jsx(HostyPanelWorkspace, { children: content }) : content) }), !allowed && _jsx("div", { role: "status", className: "hosty-panel-notice", style: { padding: 24, font: "14px system-ui" }, children: environment?.connection === "blocked" ? _jsxs(_Fragment, { children: [_jsx("strong", { children: environment.code === "actor_mismatch" ? "Use the same Hosty account in this app and its host" : "Cannot verify this app’s host" }), _jsx("p", { children: "Check your account and access in Hosty Core, then reconnect. Your existing discussion is unchanged." }), _jsx("button", { type: "button", onClick: () => window.location.reload(), children: "Reconnect" })] }) : "Connecting to the workspace…" }), host && _jsx(HostyPresentationLayer, { layer: layer, host: host, active: !!allowed })] }) }) });
|
|
42
56
|
}
|
|
43
57
|
const empty = { bootstrap: null, catalog: [], tools: [], selected: null, expanded: false, width: 360, error: null };
|
|
44
58
|
const noop = () => () => { };
|
|
45
59
|
/** A single retained frame tree for dock and drawer; only CSS placement changes at the breakpoint. */
|
|
46
|
-
export function HostyPanelWorkspace({ children, railMode = "opened", className = "", initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }) {
|
|
60
|
+
export function HostyPanelWorkspace({ children, workspaceId = DEFAULT_WORKSPACE, layout = "responsive", railMode = "opened", className = "", initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }) {
|
|
47
61
|
const host = useHostyUiHost();
|
|
62
|
+
const layer = useContext(UiFrameLayerContext), chrome = useHostyChrome();
|
|
63
|
+
const viewport = useRef(null);
|
|
48
64
|
const state = useSyncExternalStore(host?.subscribeSnapshot ?? noop, host?.getSnapshot ?? (() => empty), () => empty);
|
|
49
65
|
const unsupported = state.error?.code.startsWith("unsupported") === true;
|
|
66
|
+
const workspace = state.workspaces?.find(w => w.workspaceId === workspaceId);
|
|
67
|
+
const workspaceTools = state.tools.filter(t => t.workspaceId === workspaceId);
|
|
50
68
|
const root = useRef(null), content = useRef(null), body = useRef(null), close = useRef(null);
|
|
51
69
|
const opener = useRef(null), panelOpener = useRef(null);
|
|
52
70
|
const [size, setSize] = useState(1200), [chooser, setChooser] = useState(false), [error, setError] = useState(null);
|
|
53
71
|
const chooserRef = useRef(null);
|
|
54
72
|
const bodyId = useId();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!host || !layer)
|
|
75
|
+
return;
|
|
76
|
+
const renderer = {
|
|
77
|
+
frameLayer: layer,
|
|
78
|
+
async present(instance) { return layer.ensure(instance.presentationId) ? "accepted" : "unavailable"; },
|
|
79
|
+
focus(id) { presentationElement(id)?.querySelector("iframe")?.focus(); },
|
|
80
|
+
collapse() { if (chooserRef.current?.open)
|
|
81
|
+
chooserRef.current.close(); setChooser(false); }, release(id) { layer.release(id); },
|
|
82
|
+
async showToolPicker(signal) {
|
|
83
|
+
const dialog = chooserRef.current;
|
|
84
|
+
if (signal.aborted || !dialog?.isConnected)
|
|
85
|
+
return "unavailable";
|
|
86
|
+
window.dispatchEvent(new CustomEvent("hosty:ui:modal", { detail: `picker:${workspaceId}` }));
|
|
87
|
+
if (!dialog.open)
|
|
88
|
+
dialog.showModal();
|
|
89
|
+
setChooser(true);
|
|
90
|
+
return "accepted";
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const release = host.registerWorkspace({ workspaceId, renderer });
|
|
94
|
+
return () => {
|
|
95
|
+
for (const tool of host.getSnapshot().tools.filter(t => t.workspaceId === workspaceId))
|
|
96
|
+
host.closePresentation(tool.presentationId);
|
|
97
|
+
release();
|
|
98
|
+
};
|
|
99
|
+
}, [host, layer, workspaceId]);
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
if (!layer || !viewport.current)
|
|
102
|
+
return;
|
|
103
|
+
return layer.register(workspaceId, { element: viewport.current, drawer: layout === "responsive" && size < 900, onAskAssistant, onAuthRequired, outbound });
|
|
104
|
+
}, [layer, workspaceId, size, layout, onAskAssistant, onAuthRequired, outbound]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const onModal = (event) => {
|
|
107
|
+
const kind = event.detail;
|
|
108
|
+
if (kind !== `picker:${workspaceId}`)
|
|
109
|
+
setChooser(false);
|
|
110
|
+
if (kind === "navigation" || layout === "responsive" && size < 900 && kind !== `tools:${workspaceId}` && kind !== `picker:${workspaceId}`)
|
|
111
|
+
host?.collapseWorkspace(workspaceId);
|
|
112
|
+
};
|
|
113
|
+
window.addEventListener("hosty:ui:modal", onModal);
|
|
114
|
+
return () => window.removeEventListener("hosty:ui:modal", onModal);
|
|
115
|
+
}, [host, workspaceId, size, layout]);
|
|
116
|
+
const drawer = layout === "responsive" && size < 900, expanded = !!workspace?.expanded && !!workspace.selectedId;
|
|
117
|
+
const tool = workspaceTools.find(t => t.presentationId === workspace?.selectedId);
|
|
118
|
+
const selectedKey = tool?.key ?? null;
|
|
119
|
+
const tabs = railMode === "catalog" ? state.catalog : [...new Set(workspaceTools.map(tool => tool.key))].map(key => state.catalog.find(p => panelKey(p.panel) === key)).filter((p) => !!p);
|
|
60
120
|
const selected = state.catalog.find(p => panelKey(p.panel) === selectedKey);
|
|
61
|
-
const
|
|
62
|
-
const width = Math.min(state.width, Math.max(280, size - 480 - 60));
|
|
121
|
+
const width = Math.min(workspace?.width ?? 360, Math.max(280, size - 480 - 60));
|
|
63
122
|
const optionsRef = useRef({ onWidthChange, onExpandedChange });
|
|
64
123
|
optionsRef.current = { onWidthChange, onExpandedChange };
|
|
65
124
|
useEffect(() => {
|
|
66
125
|
if (!host)
|
|
67
126
|
return;
|
|
68
127
|
if (initialWidth !== undefined)
|
|
69
|
-
host.setWidth(initialWidth);
|
|
128
|
+
host.setWidth(initialWidth, workspaceId);
|
|
70
129
|
if (initialExpanded !== undefined)
|
|
71
|
-
host.setExpanded(initialExpanded);
|
|
72
|
-
}, [host]);
|
|
130
|
+
host.setExpanded(initialExpanded, workspaceId);
|
|
131
|
+
}, [host, workspaceId]);
|
|
73
132
|
useEffect(() => {
|
|
74
133
|
if (!host || railMode !== "catalog")
|
|
75
134
|
return;
|
|
@@ -77,18 +136,24 @@ export function HostyPanelWorkspace({ children, railMode = "opened", className =
|
|
|
77
136
|
let attemptedCatalog = null;
|
|
78
137
|
const restore = () => {
|
|
79
138
|
const current = host.getSnapshot();
|
|
80
|
-
|
|
139
|
+
const slot = current.workspaces?.find(w => w.workspaceId === workspaceId);
|
|
140
|
+
if (!slot?.expanded) {
|
|
141
|
+
completed = false;
|
|
81
142
|
attemptedCatalog = null;
|
|
82
143
|
return;
|
|
83
144
|
}
|
|
84
|
-
if (
|
|
145
|
+
if (completed && !slot.selectedId) {
|
|
146
|
+
completed = false;
|
|
147
|
+
attemptedCatalog = null;
|
|
148
|
+
}
|
|
149
|
+
if (!live || pending || completed || slot.selectedId || current.catalog === attemptedCatalog)
|
|
85
150
|
return;
|
|
86
151
|
const first = current.catalog.find(panel => panel.available);
|
|
87
152
|
if (!first)
|
|
88
153
|
return;
|
|
89
154
|
pending = true;
|
|
90
155
|
attemptedCatalog = current.catalog;
|
|
91
|
-
void host.selectPanel(first.panel).then(result => { completed = result.status === "opened"; })
|
|
156
|
+
void host.selectPanel(first.panel, workspaceId).then(result => { completed = result.status === "opened"; })
|
|
92
157
|
.finally(() => { pending = false; restore(); });
|
|
93
158
|
};
|
|
94
159
|
// A refusal consumes only this catalog generation. A refresh can retry, including one
|
|
@@ -96,17 +161,19 @@ export function HostyPanelWorkspace({ children, railMode = "opened", className =
|
|
|
96
161
|
const stop = host.subscribeSnapshot(restore);
|
|
97
162
|
restore();
|
|
98
163
|
return () => { live = false; stop(); };
|
|
99
|
-
}, [host, railMode]);
|
|
164
|
+
}, [host, railMode, workspaceId]);
|
|
100
165
|
const lastPrefs = useRef({ width: initialWidth ?? 360, expanded: initialExpanded ?? false });
|
|
101
166
|
useEffect(() => {
|
|
102
167
|
if (!host || state !== host.getSnapshot())
|
|
103
168
|
return;
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
optionsRef.current.
|
|
108
|
-
|
|
109
|
-
|
|
169
|
+
if (!workspace)
|
|
170
|
+
return;
|
|
171
|
+
if (workspace.width !== lastPrefs.current.width)
|
|
172
|
+
optionsRef.current.onWidthChange?.(workspace.width);
|
|
173
|
+
if (workspace.expanded !== lastPrefs.current.expanded)
|
|
174
|
+
optionsRef.current.onExpandedChange?.(workspace.expanded);
|
|
175
|
+
lastPrefs.current = { width: workspace.width, expanded: workspace.expanded };
|
|
176
|
+
}, [host, state, workspace]);
|
|
110
177
|
useLayoutEffect(() => {
|
|
111
178
|
if (!root.current)
|
|
112
179
|
return;
|
|
@@ -126,33 +193,40 @@ export function HostyPanelWorkspace({ children, railMode = "opened", className =
|
|
|
126
193
|
useLayoutEffect(() => {
|
|
127
194
|
if (!expanded || !drawer)
|
|
128
195
|
return;
|
|
196
|
+
window.dispatchEvent(new CustomEvent("hosty:ui:modal", { detail: `tools:${workspaceId}` }));
|
|
129
197
|
opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
130
198
|
close.current?.focus();
|
|
131
|
-
const dismiss = () =>
|
|
199
|
+
const dismiss = (event) => { if (event.detail === workspaceId)
|
|
200
|
+
host?.setExpanded(false, workspaceId); };
|
|
132
201
|
window.addEventListener("hosty:ui:drawer-dismiss", dismiss);
|
|
133
202
|
return () => { window.removeEventListener("hosty:ui:drawer-dismiss", dismiss); if (opener.current?.isConnected)
|
|
134
203
|
opener.current.focus(); };
|
|
135
|
-
}, [expanded, drawer, host]);
|
|
204
|
+
}, [expanded, drawer, host, workspaceId]);
|
|
136
205
|
useEffect(() => {
|
|
137
206
|
if (expanded && tool?.readiness === "ready")
|
|
138
|
-
|
|
139
|
-
}, [expanded,
|
|
207
|
+
presentationElement(workspace?.selectedId ?? "")?.querySelector("iframe")?.focus();
|
|
208
|
+
}, [expanded, workspace?.selectedId, tool?.readiness]);
|
|
140
209
|
useEffect(() => {
|
|
141
210
|
const dialog = chooserRef.current;
|
|
142
|
-
if (!
|
|
211
|
+
if (!dialog)
|
|
143
212
|
return;
|
|
144
|
-
dialog.
|
|
213
|
+
if (chooser && !dialog.open) {
|
|
214
|
+
window.dispatchEvent(new CustomEvent("hosty:ui:modal", { detail: `picker:${workspaceId}` }));
|
|
215
|
+
dialog.showModal();
|
|
216
|
+
}
|
|
217
|
+
else if (!chooser && dialog.open)
|
|
218
|
+
dialog.close();
|
|
145
219
|
return () => { if (dialog.open)
|
|
146
220
|
dialog.close(); };
|
|
147
|
-
}, [chooser]);
|
|
221
|
+
}, [chooser, workspaceId]);
|
|
148
222
|
async function select(panel) {
|
|
149
223
|
if (!host)
|
|
150
224
|
return;
|
|
151
225
|
if (expanded && selectedKey === panelKey(panel.panel)) {
|
|
152
|
-
host.setExpanded(false);
|
|
226
|
+
host.setExpanded(false, workspaceId);
|
|
153
227
|
return;
|
|
154
228
|
}
|
|
155
|
-
const result = await host.selectPanel(panel.panel);
|
|
229
|
+
const result = await host.selectPanel(panel.panel, workspaceId);
|
|
156
230
|
if (result.status !== "opened")
|
|
157
231
|
setError(panel.available ? "Could not open this tool. Try again." : `${panel.appName} is ${panel.state}.`);
|
|
158
232
|
else {
|
|
@@ -163,51 +237,54 @@ export function HostyPanelWorkspace({ children, railMode = "opened", className =
|
|
|
163
237
|
const resize = (event) => {
|
|
164
238
|
const start = event.clientX, old = width, handle = event.currentTarget;
|
|
165
239
|
handle.setPointerCapture(event.pointerId);
|
|
166
|
-
const move = (next) => host?.setWidth(Math.min(size - 540, old + start - next.clientX));
|
|
240
|
+
const move = (next) => host?.setWidth(Math.min(size - 540, old + start - next.clientX), workspaceId);
|
|
167
241
|
const end = () => { handle.removeEventListener("pointermove", move); handle.removeEventListener("pointerup", end); handle.removeEventListener("lostpointercapture", end); };
|
|
168
242
|
handle.addEventListener("pointermove", move);
|
|
169
243
|
handle.addEventListener("pointerup", end);
|
|
170
244
|
handle.addEventListener("lostpointercapture", end);
|
|
171
245
|
};
|
|
172
|
-
return _jsxs("div", { ref: root, className: `hosty-panel-workspace ${className}`, "data-hosty-panel-layout": drawer ? "drawer" : "dock", style: { "--hosty-panel-width": `${width}px`, ...(unsupported ? { display: "contents" } : {}) }, children: [_jsx("style", { children: styles }), _jsx("div", { ref: content, className: "hosty-panel-document", style: unsupported ? { display: "contents" } : undefined, inert: expanded && drawer, children: children }), _jsxs("nav", { className: "hosty-panel-rail", "aria-label": "Tools", style: unsupported ? { display: "none" } : undefined, inert: unsupported || expanded && drawer, children: [tabs.map((panel, index) => _jsxs("button", { type: "button", title: `${panel.appName}: ${panel.label}`, "aria-label": panel.label, "aria-controls": bodyId, "aria-expanded": expanded && selectedKey === panelKey(panel.panel), onClick: () => void select(panel), onKeyDown: event => {
|
|
246
|
+
return _jsxs("div", { ref: root, className: `hosty-panel-workspace ${className}`, "data-hosty-panel-layout": drawer ? "drawer" : "dock", style: { "--hosty-panel-width": `${width}px`, ...(unsupported ? { display: "contents" } : {}) }, children: [_jsx("style", { children: styles }), _jsx("div", { ref: content, className: "hosty-panel-document", style: unsupported ? { display: "contents" } : undefined, inert: expanded && drawer, children: children }), _jsxs("nav", { className: "hosty-panel-rail", "aria-label": "Tools", style: unsupported || !chrome.tools.enabled || chrome.tools.launcher === "hide" ? { display: "none" } : undefined, inert: unsupported || expanded && drawer, children: [tabs.map((panel, index) => _jsxs("button", { type: "button", title: `${panel.appName}: ${panel.label}`, "aria-label": panel.label, "aria-controls": bodyId, "aria-expanded": expanded && selectedKey === panelKey(panel.panel), onClick: () => void select(panel), onKeyDown: event => {
|
|
173
247
|
const delta = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
|
|
174
248
|
const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : delta ? (index + delta + tabs.length) % tabs.length : null;
|
|
175
249
|
if (next !== null) {
|
|
176
250
|
event.preventDefault();
|
|
177
251
|
event.currentTarget.parentElement?.children[next]?.focus();
|
|
178
252
|
}
|
|
179
|
-
}, children: [renderIcon?.(panel) ?? panel.label.slice(0, 2),
|
|
253
|
+
}, children: [renderIcon?.(panel) ?? panel.label.slice(0, 2), workspaceTools.some(t => t.key === panelKey(panel.panel) && t.attention > 0) ? _jsx("span", { className: "hosty-panel-attention", "aria-label": "Needs attention", children: "\u2022" }) : null] }, panelKey(panel.panel))), _jsx("button", { type: "button", "aria-label": "Add tool", title: "Add tool", onClick: () => { setError(null); setChooser(true); void host?.refresh(); }, children: "+" })] }), expanded && drawer && _jsx("button", { type: "button", className: "hosty-panel-backdrop", "aria-label": "Return to document", onClick: () => host?.setExpanded(false, workspaceId) }), _jsxs("aside", { ref: body, id: bodyId, className: "hosty-panel-body", hidden: !expanded, inert: !expanded, role: drawer ? "dialog" : "complementary", "aria-modal": drawer && expanded ? true : undefined, "aria-label": selected?.label ?? "Tool", onKeyDown: event => {
|
|
180
254
|
if (!drawer)
|
|
181
255
|
return;
|
|
182
256
|
if (event.key === "Escape") {
|
|
183
257
|
event.preventDefault();
|
|
184
|
-
host?.setExpanded(false);
|
|
258
|
+
host?.setExpanded(false, workspaceId);
|
|
185
259
|
}
|
|
186
260
|
if (event.key === "Tab") {
|
|
187
|
-
const focusable = Array.from(body.current?.querySelectorAll('button,a[href],
|
|
261
|
+
const focusable = Array.from(body.current?.querySelectorAll('button,a[href],select,[tabindex="0"]') ?? []).filter(e => !e.closest("[hidden]") && !e.classList.contains("hosty-panel-focus-guard") && !e.disabled);
|
|
188
262
|
const first = focusable[0], last = focusable.at(-1);
|
|
263
|
+
const frame = presentationElement(workspace?.selectedId ?? "")?.querySelector("iframe");
|
|
189
264
|
if (event.shiftKey && document.activeElement === first) {
|
|
190
265
|
event.preventDefault();
|
|
191
|
-
last?.focus();
|
|
266
|
+
(frame ?? last)?.focus();
|
|
192
267
|
}
|
|
193
268
|
else if (!event.shiftKey && document.activeElement === last) {
|
|
194
269
|
event.preventDefault();
|
|
195
|
-
first?.focus();
|
|
270
|
+
(frame ?? first)?.focus();
|
|
196
271
|
}
|
|
197
272
|
}
|
|
198
|
-
}, children: [drawer && expanded && _jsx("span", { tabIndex: 0, "aria-hidden": "true", className: "hosty-panel-focus-guard", onFocus: () =>
|
|
273
|
+
}, children: [drawer && expanded && _jsx("span", { tabIndex: 0, "aria-hidden": "true", className: "hosty-panel-focus-guard", onFocus: () => presentationElement(workspace?.selectedId ?? "")?.querySelector("iframe")?.focus() }), _jsx("div", { className: "hosty-panel-resize", role: "separator", "aria-label": "Tool panel width", "aria-orientation": "vertical", tabIndex: drawer ? -1 : 0, "aria-valuemin": 280, "aria-valuemax": Math.max(280, Math.min(800, size - 540)), "aria-valuenow": width, onPointerDown: resize, onDoubleClick: () => host?.setWidth(360, workspaceId), onKeyDown: event => {
|
|
199
274
|
if (event.key === "Home") {
|
|
200
275
|
event.preventDefault();
|
|
201
|
-
host?.setWidth(360);
|
|
276
|
+
host?.setWidth(360, workspaceId);
|
|
202
277
|
}
|
|
203
278
|
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
204
279
|
event.preventDefault();
|
|
205
|
-
host?.setWidth(width + (event.key === "ArrowLeft" ? 16 : -16));
|
|
280
|
+
host?.setWidth(width + (event.key === "ArrowLeft" ? 16 : -16), workspaceId);
|
|
206
281
|
}
|
|
207
|
-
} }), _jsxs("header", { children: [_jsx("strong", { children: selected?.label ?? "Tool" }), tool?.resolved.standaloneHref && _jsx("a", { href: tool.resolved.standaloneHref, target: "_blank", rel: "noopener noreferrer", title: "Open separately", "aria-label": "Open separately", children: "\u2197" }), _jsx("button", { ref: close, type: "button", onClick: () => host?.setExpanded(false), "aria-label": "Return to document", title: "Return to document", children: "\u00D7" })] }), tool?.resolved.state === "degraded" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "This app\u2019s health check is failing. Its page may still work." }), tool?.readiness === "unreported" && _jsxs("div", { className: "hosty-panel-state", role: "status", children: ["This tool has not confirmed that it loaded. If the page is blank or refuses to connect, use ", _jsx("a", { href: tool.resolved.standaloneHref ?? undefined, target: "_blank", rel: "noopener noreferrer", children: "Open separately" }), "."] }), tool?.readiness === "auth-required" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "Check access in the tool below, then retry opening the discussion." }), state.
|
|
208
|
-
: _jsxs("div", { className: "hosty-panel-notice", children: [_jsxs("p", { children: ["This tool is unavailable (", item.resolved.state, ")."] }), _jsx("button", { type: "button", onClick: () => void host?.selectPanel(item.target.panel), children: "Retry opening" })] }) }, item.key)), drawer && expanded && _jsx("span", { tabIndex: 0, "aria-hidden": "true", className: "hosty-panel-focus-guard", onFocus: () => close.current?.focus() })] }), chooser && _jsxs("dialog", { ref: chooserRef, className: "hosty-tool-chooser", onCancel: () => setChooser(false), "aria-label": "Add tool", children: [_jsxs("header", { children: [_jsx("strong", { children: "Add tool" }), _jsx("button", { type: "button", onClick: () => setChooser(false), "aria-label": "Close tool chooser", children: "\u00D7" })] }), _jsx("p", { children: "Open a tool beside your document." }), error && _jsx("p", { role: "status", children: error }), state.error && _jsx("p", { role: "status", children: state.error.message }), !state.bootstrap?.canHost && _jsx("p", { children: "This app needs optional panel access in Hosty Core to show tools here. You can still use the app." }), state.bootstrap?.canHost && !state.catalog.length && _jsx("p", { children: "No tools are available for your account." }), _jsx("ul", { children: state.catalog.map(panel => _jsxs("li", { children: [_jsxs("button", { type: "button", onClick: () => void select(panel), children: [_jsx("strong", { children: panel.label }), _jsxs("span", { children: [panel.appName, panel.available ? "" : ` · ${panel.state}`] })] }), !panel.available && panel.canStart && onStartApp && _jsx("button", { type: "button", onClick: () => void Promise.resolve(onStartApp(panel.panel.appId)).then(() => host?.refresh()), children: "Start app" })] }, panelKey(panel.panel))) }), _jsx("button", { type: "button", onClick: () => void host?.refresh(), children: "Refresh tools" })] }), error && !chooser && _jsxs("div", { className: "hosty-panel-error", role: "status", children: [error, " ", _jsx("button", { type: "button", onClick: () => { setChooser(true); setError(null); }, children: "Choose tool" })] })] });
|
|
282
|
+
} }), _jsxs("header", { children: [_jsx("strong", { children: selected?.label ?? "Tool" }), tool?.resolved.standaloneHref && _jsx("a", { href: tool.resolved.standaloneHref, target: "_blank", rel: "noopener noreferrer", title: "Open separately", "aria-label": "Open separately", children: "\u2197" }), _jsx("button", { ref: close, type: "button", onClick: () => host?.setExpanded(false, workspaceId), "aria-label": "Return to document", title: "Return to document", children: "\u00D7" })] }), tool?.resolved.state === "degraded" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "This app\u2019s health check is failing. Its page may still work." }), tool?.readiness === "unreported" && _jsxs("div", { className: "hosty-panel-state", role: "status", children: ["This tool has not confirmed that it loaded. If the page is blank or refuses to connect, use ", _jsx("a", { href: tool.resolved.standaloneHref ?? undefined, target: "_blank", rel: "noopener noreferrer", children: "Open separately" }), "."] }), tool?.readiness === "auth-required" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "Check access in the tool below, then retry opening the discussion." }), tool && workspaceTools.filter(t => t.key === tool.key).length > 1 && _jsxs("label", { className: "hosty-panel-state", children: ["Open views", _jsx("select", { "aria-label": "Tool instance", value: tool.presentationId, onChange: event => host?.focusPresentation(event.target.value), children: workspaceTools.filter(t => t.key === tool.key).map((item, index) => _jsxs("option", { value: item.presentationId, children: [selected?.label ?? "Tool", " ", index + 1] }, item.presentationId)) })] }), tool && _jsx("button", { type: "button", className: "hosty-panel-state", onClick: () => host?.closePresentation(tool.presentationId), children: "Close this tool view" }), _jsx("div", { ref: viewport, className: "hosty-panel-viewport", "aria-owns": tool ? `hosty-presentation-${tool.presentationId}` : undefined }), drawer && expanded && _jsx("span", { tabIndex: 0, "aria-hidden": "true", className: "hosty-panel-focus-guard", onFocus: () => close.current?.focus() })] }), _jsxs("dialog", { ref: chooserRef, className: "hosty-tool-chooser", onCancel: () => setChooser(false), "aria-label": "Add tool", children: [_jsxs("header", { children: [_jsx("strong", { children: "Add tool" }), _jsx("button", { type: "button", onClick: () => setChooser(false), "aria-label": "Close tool chooser", children: "\u00D7" })] }), _jsx("p", { children: "Open a tool beside your document." }), error && _jsx("p", { role: "status", children: error }), state.error && _jsx("p", { role: "status", children: state.error.message }), !state.bootstrap?.canHost && _jsx("p", { children: "This app needs optional panel access in Hosty Core to show tools here. You can still use the app." }), state.bootstrap?.canHost && !state.catalog.length && _jsx("p", { children: "No tools are available for your account." }), _jsx("ul", { children: state.catalog.map(panel => _jsxs("li", { children: [_jsxs("button", { type: "button", onClick: () => void select(panel), children: [_jsx("strong", { children: panel.label }), _jsxs("span", { children: [panel.appName, panel.available ? "" : ` · ${panel.state}`] })] }), !panel.available && panel.canStart && onStartApp && _jsx("button", { type: "button", onClick: () => void Promise.resolve(onStartApp(panel.panel.appId)).then(() => host?.refresh()), children: "Start app" })] }, panelKey(panel.panel))) }), _jsx("button", { type: "button", onClick: () => void host?.refresh(), children: "Refresh tools" })] }), error && !chooser && _jsxs("div", { className: "hosty-panel-error", role: "status", children: [error, " ", _jsx("button", { type: "button", onClick: () => { setChooser(true); setError(null); }, children: "Choose tool" })] })] });
|
|
209
283
|
}
|
|
210
284
|
const styles = `
|
|
285
|
+
.hosty-panel-viewport{flex:1;min-height:0;min-width:0}
|
|
286
|
+
.hosty-app-layout{display:flex;width:100%;height:100%;min-width:0;min-height:0}.hosty-app-content{flex:1;min-width:0;min-height:0;overflow:auto}
|
|
287
|
+
|
|
211
288
|
.hosty-panel-workspace{--hp-bg:#f8f9fb;--hp-fg:#20242c;--hp-muted:#606875;--hp-border:#e0e4eb;display:flex;position:relative;min-width:0;min-height:0;width:100%;height:100dvh;overflow:hidden;background:var(--hp-bg);color:var(--hp-fg)}
|
|
212
289
|
html.dark .hosty-panel-workspace,html[data-hosty-theme=dark] .hosty-panel-workspace{--hp-bg:#14171c;--hp-fg:#f0f2f5;--hp-muted:#a2aab7;--hp-border:#323843}
|
|
213
290
|
.hosty-panel-document{flex:1;min-width:0;min-height:0;overflow:auto}.hosty-panel-rail{order:3;flex:0 0 48px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:10px 0;border-left:1px solid var(--hp-border);overflow:auto}
|
|
@@ -219,7 +296,7 @@ html.dark .hosty-panel-workspace,html[data-hosty-theme=dark] .hosty-panel-worksp
|
|
|
219
296
|
.hosty-panel-focus-guard{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
220
297
|
.hosty-panel-frame{position:relative;flex:1;min-height:0;overflow:hidden}.hosty-panel-frame iframe{display:block}.hosty-panel-resize{position:absolute;left:-12px;top:0;bottom:0;width:12px;cursor:col-resize;touch-action:none}.hosty-panel-resize:hover{background:color-mix(in srgb,var(--hp-fg) 8%,transparent)}
|
|
221
298
|
.hosty-panel-state{padding:8px 12px;font:12px/1.4 system-ui;border-bottom:1px solid var(--hp-border);color:var(--hp-muted)}.hosty-panel-state a{text-decoration:underline}
|
|
222
|
-
.hosty-panel-workspace[data-hosty-panel-layout=drawer] .hosty-panel-body{position:absolute;inset:0 0 0 auto;width:min(440px,100%);z-index:
|
|
223
|
-
.hosty-panel-backdrop{position:absolute;inset:0;border:0;background:#0005;z-index:
|
|
299
|
+
.hosty-panel-workspace[data-hosty-panel-layout=drawer] .hosty-panel-body{position:absolute;inset:0 0 0 auto;width:min(440px,100%);z-index:102;margin:0;box-shadow:-8px 0 30px #0002}.hosty-panel-workspace[data-hosty-panel-layout=drawer] .hosty-panel-resize{display:none}
|
|
300
|
+
.hosty-panel-backdrop{position:absolute;inset:0;border:0;background:#0005;z-index:101;cursor:pointer}.hosty-panel-notice{padding:24px;font:14px/1.5 system-ui}.hosty-panel-review{position:absolute;top:8px;right:8px;z-index:2;padding:10px;background:var(--hp-bg);border:1px solid var(--hp-border);border-radius:8px}
|
|
224
301
|
.hosty-tool-chooser{width:min(440px,calc(100vw - 32px));max-height:80dvh;border:1px solid var(--hp-border);border-radius:12px;padding:16px;background:var(--hp-bg);color:var(--hp-fg);font:14px/1.5 system-ui}.hosty-tool-chooser::backdrop{background:#0005}.hosty-tool-chooser ul{list-style:none;padding:0;display:grid;gap:8px}.hosty-tool-chooser li{display:flex;gap:8px}.hosty-tool-chooser li>button{flex:1;border:1px solid var(--hp-border);background:transparent;color:inherit;border-radius:8px;padding:12px;text-align:left;cursor:pointer}.hosty-tool-chooser li span{display:block;color:var(--hp-muted);font-size:12px}.hosty-panel-error{position:absolute;bottom:16px;right:60px;z-index:4;background:var(--hp-bg);border:1px solid var(--hp-border);border-radius:8px;padding:12px;font:13px system-ui}
|
|
225
302
|
`;
|
package/dist/ui-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderServerOptions } from "@hosty-sdk/app/providers/server";
|
|
2
|
-
import { type PanelTarget, type UiBootstrap, type UiPanel, type UiResolvedPanel } from "@hosty-sdk/app/ui/types";
|
|
2
|
+
import { type PanelTarget, type UiBootstrap, type UiPanel, type UiResolvedPanel, type UiNavigationCatalog, type UiNavigationTarget, type UiResolvedNavigation } from "@hosty-sdk/app/ui/types";
|
|
3
3
|
/** Server transport for generic UI metadata. Credentials never appear in returned metadata. */
|
|
4
4
|
export declare class UiServerClient {
|
|
5
5
|
private readonly appId;
|
|
@@ -11,6 +11,8 @@ export declare class UiServerClient {
|
|
|
11
11
|
parentOrigin?: string;
|
|
12
12
|
}, token: string, signal?: AbortSignal): Promise<UiBootstrap>;
|
|
13
13
|
listPanels(token: string, signal?: AbortSignal): Promise<UiPanel[]>;
|
|
14
|
+
navigation(token: string, signal?: AbortSignal): Promise<UiNavigationCatalog>;
|
|
15
|
+
resolveNavigation(target: UiNavigationTarget, token: string, signal?: AbortSignal): Promise<UiResolvedNavigation>;
|
|
14
16
|
resolvePanel(target: PanelTarget, token: string, signal?: AbortSignal): Promise<UiResolvedPanel>;
|
|
15
17
|
}
|
|
16
18
|
type UiAuthenticatedContext = {
|
package/dist/ui-server.js
CHANGED
|
@@ -35,6 +35,12 @@ export class UiServerClient {
|
|
|
35
35
|
async listPanels(token, signal) {
|
|
36
36
|
return (await this.request("panels", token, undefined, signal)).panels;
|
|
37
37
|
}
|
|
38
|
+
navigation(token, signal) {
|
|
39
|
+
return this.request("navigation", token, undefined, signal);
|
|
40
|
+
}
|
|
41
|
+
resolveNavigation(target, token, signal) {
|
|
42
|
+
return this.request("navigation/resolve", token, target, signal);
|
|
43
|
+
}
|
|
38
44
|
resolvePanel(target, token, signal) {
|
|
39
45
|
return this.request("panels/resolve", token, target, signal);
|
|
40
46
|
}
|
|
@@ -62,11 +68,17 @@ export async function createHostyUiResponse(request, context, options = {}) {
|
|
|
62
68
|
else if (request.method === "GET" && route === "panels") {
|
|
63
69
|
result = { panels: await client.listPanels(context.token, request.signal) };
|
|
64
70
|
}
|
|
65
|
-
else if (request.method === "
|
|
71
|
+
else if (request.method === "GET" && route === "navigation") {
|
|
72
|
+
if (url.search)
|
|
73
|
+
throw new UiError("ui_request_invalid", "Unexpected navigation parameters.", 400);
|
|
74
|
+
result = await client.navigation(context.token, request.signal);
|
|
75
|
+
}
|
|
76
|
+
else if (request.method === "POST" && (route === "panels/resolve" || route === "navigation/resolve")) {
|
|
66
77
|
if (!sameOrigin(request) || request.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json")
|
|
67
78
|
throw new UiError("origin_denied", "Open panels from this application page.", 403);
|
|
68
79
|
const body = await boundedBody(request);
|
|
69
|
-
result = await client.resolvePanel(JSON.parse(body), context.token, request.signal)
|
|
80
|
+
result = route === "panels/resolve" ? await client.resolvePanel(JSON.parse(body), context.token, request.signal)
|
|
81
|
+
: await client.resolveNavigation(JSON.parse(body), context.token, request.signal);
|
|
70
82
|
}
|
|
71
83
|
else
|
|
72
84
|
throw new UiError("ui_route_not_found", "Unknown panel operation.", 404);
|
package/dist/ui-transport.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PanelTarget, type PresentationResult } from "./ui-types.js";
|
|
1
|
+
import { type PanelTarget, type PresentationResult, type UiActionResult } from "./ui-types.js";
|
|
2
2
|
export declare const UI_VERSION = 1;
|
|
3
3
|
export declare const DISCOVERY_TIMEOUT = 5000;
|
|
4
4
|
export declare const RESPONSE_TIMEOUT = 10000;
|
|
@@ -13,4 +13,4 @@ export declare function validTarget(value: unknown): value is PanelTarget;
|
|
|
13
13
|
export declare function uiPath(path?: string): string;
|
|
14
14
|
export declare function uiRequest<T>(path: string, suffix: string, body?: unknown): Promise<T>;
|
|
15
15
|
export declare function failure(requestId: string, error: unknown): PresentationResult;
|
|
16
|
-
export declare function waitForPresentation(operation: Promise<
|
|
16
|
+
export declare function waitForPresentation<T extends PresentationResult | UiActionResult>(operation: Promise<T>, requestId: string, signal?: AbortSignal): Promise<T>;
|
package/dist/ui-transport.js
CHANGED
|
@@ -21,7 +21,7 @@ export function canonicalSurfaceRoute(value) {
|
|
|
21
21
|
if (!isSurfaceRoute(value))
|
|
22
22
|
return null;
|
|
23
23
|
const route = new URL(value, "https://surface.invalid");
|
|
24
|
-
for (const key of ["hosty_launch", "hosty_theme", "hosty_theme_preference"])
|
|
24
|
+
for (const key of ["hosty_launch", "hosty_theme", "hosty_theme_preference", "hosty_navigation", "hosty_tools", "hosty_tool_launcher"])
|
|
25
25
|
route.searchParams.delete(key);
|
|
26
26
|
return `${route.pathname}${route.search}${route.hash}`;
|
|
27
27
|
}
|