@hosty-sdk/app 0.26.0 → 0.27.23

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 CHANGED
@@ -11,6 +11,9 @@ npm install @hosty-sdk/app
11
11
  | --- | --- | --- |
12
12
  | `@hosty-sdk/app` | anywhere | status taxonomy, recovery decision, `hosty:auth-required` and `hosty:request-delegated-token` schemas, URL/env helpers |
13
13
  | `@hosty-sdk/app/server` | server only | Core revalidation with caching, cookie helpers, the app-code route factory, the app secrets client |
14
+ | `@hosty-sdk/app/next` | Next client | `HostyNextOverlay`, `HostyNextNavigationBridge` for App Router navigation |
15
+ | `@hosty-sdk/app/ui` | client | policy/navigation snapshots, replayable navigation, chooser and presentation operations |
16
+ | `@hosty-sdk/app/ui/react` | React client | navigation boundaries/hooks, app frames and retained panel workspaces |
14
17
  | `@hosty-sdk/app/react` | client | `<HostyOverlay />` — shared loading, identity, required setup and recovery UI; legacy bridges remain available |
15
18
  | `@hosty-sdk/app/session/server` | server | Framework-neutral session response adapter for authenticated Node servers |
16
19
  | `@hosty-sdk/app/embedder` | client | theme sender and legacy embedder message parsers; Shell no longer mints app credentials |
@@ -23,8 +26,8 @@ Minimal Next.js wiring (the configuration object stays server-side):
23
26
 
24
27
  ```tsx
25
28
  // app/layout.tsx — inside the body and the existing theme/launch providers
26
- import { HostyOverlay } from "@hosty-sdk/app/react";
27
- // <HostyOverlay>{children}</HostyOverlay>
29
+ import { HostyNextOverlay } from "@hosty-sdk/app/next";
30
+ // <HostyNextOverlay>{children}</HostyNextOverlay>
28
31
 
29
32
  // lib/hosty.ts
30
33
  export const hostyConfig = {
@@ -48,8 +51,9 @@ export const POST = createAppCodeRouteHandler(hostyConfig);
48
51
 
49
52
  Replace the protected scope's `AppIdentityBridge`, `AppActivityBridge`, custom sign-in rendering
50
53
  and `MissingPermissionsNotice` with one `HostyOverlay`. Keep public routes outside that scope.
51
- Existing launch/theme bridges and bootstrap scripts stay in place. The only optional overlay props
52
- are `probePath` and `appCodePath` for existing route names; it has no visual customization API.
54
+ Existing launch/theme bridges stay in place for transport compatibility; add the composition
55
+ bootstrap below. `probePath` and `appCodePath` support existing route names. `chrome`, `navigation`
56
+ and `panelLayout` configure UI composition without replacing authentication/recovery rendering.
53
57
  API routes and server-rendered data still enforce their own authorization. The overlay delays
54
58
  protected client mounting; it is not a server data-access boundary.
55
59
 
@@ -323,16 +327,28 @@ client's user-token input. Keep tokens server-side. See
323
327
  [Continuous App Access](../../docs/features/continuous-app-access/feature.md) for creation, logout,
324
328
  revocation, exact MCP tools and installation boundaries. Background grants do not create a scheduler.
325
329
 
326
- ## Shared panels (Core 0.129.0 / SDK 0.25.0)
330
+ ## Shared panels (Core 0.131.0 / SDK 0.27.0)
327
331
 
328
332
  `HostyOverlay` composes one UI owner per top-level browser document. Its default
329
333
  `panelLayout="auto"` adds an opened-tool rail, an **Add tool** chooser and a resizable
330
334
  workspace. In a directly embedded app it delegates presentation to the verified parent
331
- and renders no local rail. Use `panelLayout="slot"` with exactly one
332
- `HostyPanelWorkspace` inside your chrome, or `"none"` for a custom controller renderer.
335
+ and renders no local rail. Use `panelLayout="slot"` with a `HostyPanelWorkspace` per workspace
336
+ ID inside your chrome, or `"none"` with an explicitly registered custom renderer. Without a renderer,
337
+ disable local tools; an opening otherwise fails with `presentation_unavailable`.
333
338
  Panel hosting requires reviewed optional `apps.panels.read` (or existing `apps.read`);
334
339
  participating as a child does not require catalog access.
335
340
 
341
+ Panel headers expose **Minimize panel (keep tool open)** (`−`) without a close button.
342
+ Hosts with a tool rail can set `minimizeControl="drawer"` on `HostyPanelWorkspace` to omit the
343
+ minimize button in docked panels and use the active rail icon to toggle them. Responsive drawers
344
+ keep a visible minimize button because the rail is inert while a drawer is open. The default
345
+ `minimizeControl="always"` retains the button in standalone layouts.
346
+ Custom host layouts can use `UiHost.closePresentation(presentationId)` to release a tool view;
347
+ this does not end its conversation or background work.
348
+ To match host chrome, set `--hosty-panel-background`, `--hosty-panel-foreground`,
349
+ `--hosty-panel-muted` and `--hosty-panel-border` on the workspace or an ancestor.
350
+ These overrides apply in both themes; omitted properties retain the SDK light/dark defaults.
351
+
336
352
  ```ts
337
353
  // app/api/hosty/ui/[...action]/route.ts — own app configuration/session only
338
354
  import { createHostyUiRouteHandler } from "@hosty-sdk/app/server";
@@ -343,7 +359,7 @@ export const POST = handler;
343
359
 
344
360
  Non-Next servers use `createHostyUiResponse` from `@hosty-sdk/app/ui/server` with their
345
361
  revalidated own app-user session/token. `UiServerClient` provides only `bootstrap`,
346
- `listPanels` and `resolvePanel`; never expose a generic Core proxy or caller-supplied credentials.
362
+ `listPanels`, `resolvePanel`, `navigation` and `resolveNavigation`; never expose a generic Core proxy or caller-supplied credentials.
347
363
  Both server adapters return `no-store` metadata. The React root's `uiPath` defaults to
348
364
  `/api/hosty/ui` and must be a same-origin path.
349
365
 
@@ -394,3 +410,137 @@ peer metadata. Matching opaque Core/user contexts prevents accidental account mi
394
410
  between cooperating apps; it is not attestation of app code or delegated API authority.
395
411
  Every target API still authorizes its own session. Older surfaces remain `unreported`.
396
412
  See [the panel contract and verification](../../docs/features/sdk-panel-system/feature.md).
413
+
414
+ ## Navigation and UI composition
415
+
416
+ Supply identical root defaults to the before-paint bootstrap and overlay. Omitted defaults show
417
+ navigation and enable the standalone tool rail/chooser. Hints and verified host policy can override
418
+ each leaf independently; neither framing nor copied launch-mode storage hides the new navigation.
419
+
420
+ ```tsx
421
+ // app/layout.tsx
422
+ import { chromeBootstrapScript } from "@hosty-sdk/app";
423
+ import { HostyNextOverlay } from "@hosty-sdk/app/next";
424
+
425
+ const chrome = { navigation: "show" as const, tools: { enabled: true, launcher: "show" as const } };
426
+ // In <head>, before hydration and the legacy launch bridge:
427
+ // <script dangerouslySetInnerHTML={{ __html: chromeBootstrapScript(chrome) }} />
428
+ // In <body>, around protected app content:
429
+ // <HostyNextOverlay chrome={chrome}>{children}</HostyNextOverlay>
430
+ ```
431
+
432
+ For React without Next, use `HostyOverlay` and optionally pass
433
+ `navigation={{ renderer: "standard", router }}`. A router implements `getRoute(): string`,
434
+ `subscribe(listener): () => void` and `navigate(relativeRoute): void | Promise<void>`; optional
435
+ `endpoint` disambiguates several endpoint names on the same origin. Without an adapter the SDK
436
+ uses document navigation. The Next entry is optional and does not add Next to plain SDK consumers.
437
+
438
+ The standard renderer uses only manifest `ui.navigation`. Own navigation needs ordinary app access,
439
+ not `apps.panels.read`. Unavailable metadata has a retry state; an empty declaration adds no menu.
440
+ Same-endpoint links use cached validated metadata and the app router; endpoint changes revalidate
441
+ through Core and replace the same host view. Modified clicks use pinned standalone links.
442
+
443
+ For custom navigation, suppress the automatic menu and wrap only page navigation/branding:
444
+
445
+ ```tsx
446
+ import { HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "@hosty-sdk/app/ui/react";
447
+
448
+ // <HostyNextOverlay navigation={{ renderer: "custom" }}>
449
+ // <HostyNavigationBoundary><MyPageNavigation /></HostyNavigationBoundary>
450
+ // <ProjectSelector /> // Contextual controls stay reachable when navigation is hidden.
451
+ // {children}
452
+ // </HostyNextOverlay>
453
+ ```
454
+
455
+ The boundary retains component state while hidden and moves hidden focus to the content landmark.
456
+ Alternatively place `HostyNavigation` at a custom location. Framework-neutral renderers subscribe
457
+ to `ui.getChrome/subscribeChrome` and `ui.getNavigation/subscribeNavigation`, then apply equivalent
458
+ hidden/inert/focus behavior. Snapshots are immutable. `tools.owner` is derived by the SDK; it is not
459
+ a configurable default or URL field.
460
+
461
+ ```ts
462
+ const result = await ui.navigate(entry.key, { requestId: crypto.randomUUID() });
463
+ const picker = await ui.openToolPicker({ requestId: crypto.randomUUID() });
464
+ // accepted means navigation was accepted / the chooser became visible.
465
+ // Retain request IDs for uncertain retries; an accepted replay does not repeat the action.
466
+ ```
467
+
468
+ Document hints are `hosty_navigation=show|hide`, `hosty_tools=enabled|disabled` and
469
+ `hosty_tool_launcher=show|hide`. The SDK removes them from the visible URL, preserving history
470
+ state. They are not persisted. Verified host overrides take precedence, then hints, app defaults,
471
+ the current document's legacy launch fallback for navigation, and standalone defaults. Hints grant
472
+ no access or hosting authority. Hiding a launcher permits programmatic presentation; disabling
473
+ tools rejects new requests. A root disable applies to every source and collapses retained tools.
474
+
475
+ ### Host views and retained tools
476
+
477
+ `HostyAppFrame` accepts a view descriptor. A desktop host can choose different policy from Shell:
478
+
479
+ ```tsx
480
+ <HostyAppFrame surface={validatedSurface} title="Plans"
481
+ view={{ viewId: "plans-window-1", workspaceId: "default",
482
+ chrome: { navigation: "show", tools: { enabled: true, launcher: "hide" } },
483
+ navigate: surface => replaceWindowSurface(surface) }} />
484
+ ```
485
+
486
+ Omit `navigate` for the SDK's plain iframe replacement, or use it to update the host's route/window
487
+ model. Key a replaced React frame by its immutable surface; live `chrome` updates keep that key.
488
+ One top-level coordinator handles direct app/tool children. It never delegates credentials.
489
+
490
+ Panel declarations and presentations have different identities. `PanelRef` identifies a manifest
491
+ tool. `openSurface` returns an SDK `presentationId`; its `requestId` identifies only the operation.
492
+ Default `reuse: "exact"` focuses the matching target in the destination workspace. A distinct route
493
+ retains another frame; `reuse: "new"` creates another even for the same route. Rails have one item
494
+ per declaration and an instance switcher. Replaying a closed presentation's operation does not
495
+ resurrect it; use a new request to reopen. Domain sessions remain owned by the app.
496
+
497
+ ```ts
498
+ const opened = await ui.openSurface(target, {
499
+ placement: "side", requestId: crypto.randomUUID(), reuse: "exact",
500
+ });
501
+ // A host may satisfy "side" with a dock or tool window.
502
+ ```
503
+
504
+ Hosts use `useHostyUiHost()` or `createUiHost()`:
505
+
506
+ ```ts
507
+ host.setViewChrome("plans-window-1", { navigation: "show", tools: { launcher: "hide" } });
508
+ host.setPlacementResolver(({ source, target, presentations }) => ({ workspaceId: "shared" }));
509
+ host.focusPresentation(presentationId);
510
+ host.collapseWorkspace("shared");
511
+ await host.movePresentation(presentationId, "floating");
512
+ host.closePresentation(presentationId); // Does not delete the conversation.
513
+ ```
514
+
515
+ Register destinations before their views. React's `<HostyPanelWorkspace workspaceId="shared">`
516
+ implements the renderer; `panelLayout="slot"` lets the host place multiple named workspaces.
517
+ `layout="dock"` keeps dock geometry inside an independent tool window. The default `responsive`
518
+ layout switches to a drawer in a narrow region. Keep populated workspaces mounted until their
519
+ presentations are moved or explicitly closed.
520
+
521
+ Non-React hosts call `registerWorkspace({ workspaceId, renderer })`. A renderer implements async
522
+ `present(instance, signal)` and `showToolPicker(signal)` returning `accepted | unavailable`, plus
523
+ `focus(id)`, `collapse()` and `release(id)`. Acceptance requires an attached visible destination,
524
+ not a promise of later UI. Matching `frameLayer` object identities allow movement; the renderer
525
+ must preserve iframe DOM parent and browsing context, changing geometry instead of reparenting.
526
+ The supplied React renderer does this. A failed move leaves the original presentation in place.
527
+
528
+ A placement resolver can nominate `presentationId` together with its actual workspace, but only
529
+ an exact resolved target is accepted. Requests are ordered per destination before asynchronous
530
+ resolution; a newer selection or collapse supersedes older work. Closing an app view cancels its
531
+ unaccepted requests and preserves accepted shared presentations. All browser IDs and ledgers are
532
+ local to the document; Core stores no browser workspace/session registry.
533
+
534
+ ### Compatibility and migration
535
+
536
+ Both peers negotiate `ui.composition.v1` inside the existing verified wire-v1 handshake. New/old
537
+ peers keep legacy openings without new options. Explicit reuse, remote navigation or picker calls
538
+ to old hosts return `unsupported_composition`; the SDK does not silently discard those options.
539
+ Host loss preserves stale policy and exposes reconnect/separate recovery, without taking local
540
+ ownership. Silent session renewal preserves same-actor frames and drafts.
541
+
542
+ Remove `hosty-shell-chrome`/`data-hosty-launch` visibility CSS when adopting navigation boundaries.
543
+ Keep launch/auth exports for older hosts and native transport. Native clients send explicit hidden
544
+ navigation/launcher and disabled tools until they provide a panel host. `panelLayout="none"` is a
545
+ rendering choice, not a disable policy: explicitly disable local tools or register a renderer.
546
+ See [UI composition and browser acceptance](../../docs/features/sdk-ui-composition/feature.md).
package/dist/index.d.ts CHANGED
@@ -106,7 +106,7 @@ export interface PageLocation {
106
106
  */
107
107
  export declare function buildCoreOpenUrl(corePublicOrigin: string | null, appId: string, location: PageLocation): string | null;
108
108
  /**
109
- * How a shell is presenting this app.
109
+ * Legacy launch/transport context. Navigation visibility uses the independent composition policy.
110
110
  *
111
111
  * - `embedded` — framed by the browser Shell. The shell renders navigation for this app. The
112
112
  * app restores its per-tab grant, tries a silent Core redirect on first load, then uses its
@@ -117,19 +117,19 @@ export declare function buildCoreOpenUrl(corePublicOrigin: string | null, appId:
117
117
  * - `standalone` — a plain browser tab on the app's own origin. Nothing else renders navigation
118
118
  * for this app, so it keeps its own.
119
119
  *
120
- * Two independent decisions read this one value and they do not split it the same way: chrome
121
- * is hidden whenever the mode is not `standalone` (`hidesAppChrome`), while frame-local silent
122
- * sign-in and popup recovery belong to `embedded` alone (`AppIdentityBridge`). A native web
123
- * view keeps standalone recovery even though its app chrome is hidden.
120
+ * Legacy consumers can still use `hidesAppChrome`. New consumers use `useHostyChrome` and
121
+ * `HostyNavigationBoundary`; a desktop host can keep navigation visible in embedded apps.
122
+ * Authentication continues to use the actual transport environment.
124
123
  */
125
124
  export type AppLaunchMode = "embedded" | "native" | "standalone";
126
125
  /** The query parameter a shell appends to the launch URL to declare the mode. */
127
126
  export declare const LAUNCH_MODE_PARAM = "hosty_launch";
128
127
  /** Where a resolved mode is kept for the life of the tab or web view. */
129
128
  export declare const LAUNCH_MODE_STORAGE_KEY = "hosty.launch.mode";
130
- /** The root attribute the launch bridge writes; CSS hides duplicated chrome off it. */
129
+ /** Legacy launch attribute. Use data-hosty-navigation for composition visibility. */
131
130
  export declare const LAUNCH_MODE_ATTRIBUTE = "data-hosty-launch";
132
131
  /**
132
+ * @deprecated Use HostyNavigationBoundary or the effective navigation policy.
133
133
  * Marks an element that duplicates chrome a shell already renders — the app's own name and the
134
134
  * navigation between its manifest pages. Contextual controls and information a shell does not
135
135
  * render (a project picker, a refresh action, an identity badge) are not chrome in this sense
@@ -164,6 +164,7 @@ export declare function resolveLaunchMode(input: {
164
164
  heuristic: AppLaunchMode;
165
165
  }): LaunchModeResolution;
166
166
  /**
167
+ * @deprecated Use the effective composition navigation policy instead.
167
168
  * True when a shell around this app already renders its name and page navigation, so the app's
168
169
  * own copies are duplication. False for `standalone`, where nothing else renders them.
169
170
  */
@@ -203,3 +204,5 @@ export declare function decideRecoveryAction(input: {
203
204
  openUrl: string | null;
204
205
  redirectAlreadyAttempted: boolean;
205
206
  }): RecoveryAction;
207
+ /** Paint-time navigation policy. Kept in the pure entry point for Node and edge consumers. */
208
+ export declare function chromeBootstrapScript(defaults?: import("./ui-types").UiChromeOverrides): string;
package/dist/index.js CHANGED
@@ -142,9 +142,10 @@ export function buildCoreOpenUrl(corePublicOrigin, appId, location) {
142
142
  export const LAUNCH_MODE_PARAM = "hosty_launch";
143
143
  /** Where a resolved mode is kept for the life of the tab or web view. */
144
144
  export const LAUNCH_MODE_STORAGE_KEY = "hosty.launch.mode";
145
- /** The root attribute the launch bridge writes; CSS hides duplicated chrome off it. */
145
+ /** Legacy launch attribute. Use data-hosty-navigation for composition visibility. */
146
146
  export const LAUNCH_MODE_ATTRIBUTE = "data-hosty-launch";
147
147
  /**
148
+ * @deprecated Use HostyNavigationBoundary or the effective navigation policy.
148
149
  * Marks an element that duplicates chrome a shell already renders — the app's own name and the
149
150
  * navigation between its manifest pages. Contextual controls and information a shell does not
150
151
  * render (a project picker, a refresh action, an identity badge) are not chrome in this sense
@@ -188,6 +189,7 @@ export function resolveLaunchMode(input) {
188
189
  return { mode: stored ?? input.heuristic, fromParam: false };
189
190
  }
190
191
  /**
192
+ * @deprecated Use the effective composition navigation policy instead.
191
193
  * True when a shell around this app already renders its name and page navigation, so the app's
192
194
  * own copies are duplication. False for `standalone`, where nothing else renders them.
193
195
  */
@@ -260,3 +262,10 @@ export function decideRecoveryAction(input) {
260
262
  }
261
263
  return { kind: "card", card: "signin" };
262
264
  }
265
+ /** Paint-time navigation policy. Kept in the pure entry point for Node and edge consumers. */
266
+ export function chromeBootstrapScript(defaults = {}) {
267
+ const navigation = defaults.navigation;
268
+ if (navigation !== undefined && navigation !== "show" && navigation !== "hide")
269
+ throw new Error("Invalid navigation default.");
270
+ return `(()=>{try{const d=${JSON.stringify({ navigation })},p=new URLSearchParams(location.search),h=new URLSearchParams();for(const k of ["hosty_navigation","hosty_tools","hosty_tool_launcher","hosty_launch"])for(const v of p.getAll(k))h.append(k,v);document.documentElement.setAttribute('data-hosty-chrome-hints',h.toString());const one=k=>p.getAll(k).length===1?p.get(k):null,n=one('hosty_navigation'),l=one('hosty_launch');document.documentElement.setAttribute('data-hosty-navigation',n==='show'||n==='hide'?n:d.navigation??(l==='native'||l==='embedded'?'hide':'show'));}catch{}})();`;
271
+ }
package/dist/next.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { HostyOverlay } from "./overlay-react.js";
2
+ /** Next routing stays an optional adapter; the composition model does not patch browser history. */
3
+ export declare function HostyNextNavigationBridge(): null;
4
+ export declare function HostyNextOverlay({ children, ...props }: Parameters<typeof HostyOverlay>[0]): import("react").JSX.Element;
package/dist/next.js ADDED
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Suspense, useContext, useEffect, useMemo, useRef } from "react";
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
+ import { HostyOverlay } from "./overlay-react.js";
6
+ import { UiCompositionContext } from "./ui-context.js";
7
+ /** Next routing stays an optional adapter; the composition model does not patch browser history. */
8
+ export function HostyNextNavigationBridge() {
9
+ const model = useContext(UiCompositionContext), router = useRouter(), pathname = usePathname(), search = useSearchParams();
10
+ const current = useRef(router);
11
+ current.current = router;
12
+ const listeners = useRef(new Set());
13
+ const adapter = useMemo(() => ({
14
+ getRoute: () => `${window.location.pathname}${window.location.search}${window.location.hash}`,
15
+ subscribe(listener) {
16
+ listeners.current.add(listener);
17
+ window.addEventListener("hashchange", listener);
18
+ return () => { listeners.current.delete(listener); window.removeEventListener("hashchange", listener); };
19
+ },
20
+ navigate(route) { current.current.push(route); },
21
+ }), []);
22
+ useEffect(() => { model?.attachRouter(adapter); return () => model?.attachRouter(); }, [model, adapter]);
23
+ useEffect(() => { for (const listener of listeners.current)
24
+ listener(); }, [pathname, search]);
25
+ return null;
26
+ }
27
+ export function HostyNextOverlay({ children, ...props }) {
28
+ return _jsxs(HostyOverlay, { ...props, children: [_jsx(Suspense, { fallback: null, children: _jsx(HostyNextNavigationBridge, {}) }), children] });
29
+ }
@@ -1,8 +1,14 @@
1
1
  import { type ReactNode } from "react";
2
+ import type { UiChromeOverrides, UiRouterAdapter } from "./ui-types.js";
2
3
  /** Mount once around protected content. Presentation and recovery belong to Hosty. */
3
- export declare function HostyOverlay({ children, probePath, appCodePath, uiPath, panelLayout, localContent }: {
4
+ export declare function HostyOverlay({ children, probePath, appCodePath, uiPath, panelLayout, localContent, chrome, navigation }: {
4
5
  /** Explicitly public/local content available during an outage; never put protected actor data here. */
5
6
  localContent?: ReactNode;
7
+ chrome?: UiChromeOverrides;
8
+ navigation?: {
9
+ renderer: "standard" | "custom";
10
+ router?: UiRouterAdapter;
11
+ };
6
12
  children: ReactNode;
7
13
  probePath?: string;
8
14
  appCodePath?: string;
@@ -30,7 +30,7 @@ function sessionReady(session) {
30
30
  return session?.setup === "ready" && (!session.activityRequired || session.recoveryReady) && !activityExpired(session);
31
31
  }
32
32
  /** Mount once around protected content. Presentation and recovery belong to Hosty. */
33
- export function HostyOverlay({ children, probePath = "/api/hosty/session", appCodePath = "/api/auth/app-code", uiPath, panelLayout = "auto", localContent }) {
33
+ export function HostyOverlay({ children, probePath = "/api/hosty/session", appCodePath = "/api/auth/app-code", uiPath, panelLayout = "auto", localContent, chrome, navigation }) {
34
34
  const actor = useRef(null);
35
35
  const changingActor = useRef(false);
36
36
  const [session, setSession] = useState(null);
@@ -168,7 +168,7 @@ export function HostyOverlay({ children, probePath = "/api/hosty/session", appCo
168
168
  }
169
169
  const localOffline = (identity.ui.kind === "unavailable" || (identity.ui.kind === "active" && session?.setup === "unavailable"))
170
170
  && localContent !== undefined;
171
- return _jsxs(_Fragment, { children: [localOffline && _jsxs("div", { "data-hosty-local-content": true, children: [_jsx("p", { role: "status", children: "Hosty is unavailable. Local content remains available." }), localContent] }), _jsx("div", { "data-hosty-content": true, hidden: !ready, inert: !ready, style: ready ? { display: "contents" } : { display: "none" }, children: _jsx(HostyUiRoot, { uiPath: uiPath, panelLayout: panelLayout, active: ready, children: mounted.current ? children : null }) }), !ready && !localOffline && _jsx(OverlaySurface, { pending: pending, title: title, message: message, action: action, actionLabel: actionLabel, reviewUrl: reviewUrl })] });
171
+ return _jsxs(_Fragment, { children: [localOffline && _jsxs("div", { "data-hosty-local-content": true, children: [_jsx("p", { role: "status", children: "Hosty is unavailable. Local content remains available." }), localContent] }), _jsx("div", { "data-hosty-content": true, hidden: !ready, inert: !ready, style: ready ? { display: "contents" } : { display: "none" }, children: _jsx(HostyUiRoot, { chrome: chrome, navigation: navigation, uiPath: uiPath, panelLayout: panelLayout, active: ready, children: mounted.current ? children : null }) }), !ready && !localOffline && _jsx(OverlaySurface, { pending: pending, title: title, message: message, action: action, actionLabel: actionLabel, reviewUrl: reviewUrl })] });
172
172
  }
173
173
  function OverlaySurface({ pending, title, message, action, actionLabel, reviewUrl }) {
174
174
  const [portal, setPortal] = useState(null);
package/dist/react.d.ts CHANGED
@@ -109,3 +109,4 @@ export declare function HostThemeBridge({ followSystem, onTheme }?: HostThemeBri
109
109
  export { MissingPermissionsNotice } from "./permissions-react.js";
110
110
  export { AppActivityBridge } from "./activity-react.js";
111
111
  export { HostyOverlay } from "./overlay-react.js";
112
+ export { HostyNavigation, HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "./ui-navigation-react.js";
package/dist/react.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { captureChromeHints } from "./ui-composition.js";
3
4
  // React slice: the client identity bridge — probe, silent recovery, and the fallback
4
5
  // cards. Prop-less by design: the app id and browser-reachable Core origin arrive in the
5
6
  // probe response's `recovery` field (request-time values, never build-time props).
@@ -138,6 +139,7 @@ function currentLaunchMode() {
138
139
  */
139
140
  export function HostLaunchBridge() {
140
141
  useEffect(() => {
142
+ captureChromeHints();
141
143
  const url = new URL(window.location.href);
142
144
  const param = url.searchParams.get(LAUNCH_MODE_PARAM);
143
145
  const { mode, fromParam } = resolveLaunchMode({
@@ -565,3 +567,4 @@ export function HostThemeBridge({ followSystem = true, onTheme } = {}) {
565
567
  export { MissingPermissionsNotice } from "./permissions-react.js";
566
568
  export { AppActivityBridge } from "./activity-react.js";
567
569
  export { HostyOverlay } from "./overlay-react.js";
570
+ export { HostyNavigation, HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "./ui-navigation-react.js";
@@ -0,0 +1,71 @@
1
+ import { type UiChromeOverrides, type UiChromeSnapshot, type UiChromeSource, type UiEnvironment, type UiNavigationCatalog, type UiNavigationEntry, type UiNavigationSnapshot, type UiRouterAdapter } from "./ui-types.js";
2
+ export declare const CHROME_HINT_ATTRIBUTE = "data-hosty-chrome-hints";
3
+ export declare function parseChromeHints(search: string): UiChromeOverrides & {
4
+ legacy?: "show" | "hide";
5
+ };
6
+ export declare function validChromeOverrides(value: unknown): value is UiChromeOverrides;
7
+ export declare function resolveChrome(defaults: UiChromeOverrides, hints: ReturnType<typeof parseChromeHints>, host?: UiChromeOverrides): {
8
+ navigation: "hide" | "show";
9
+ enabled: boolean;
10
+ launcher: "hide" | "show";
11
+ sources: {
12
+ navigation: UiChromeSource;
13
+ enabled: UiChromeSource;
14
+ launcher: UiChromeSource;
15
+ };
16
+ };
17
+ export declare const INITIAL_CHROME: UiChromeSnapshot;
18
+ export declare const INITIAL_NAVIGATION: UiNavigationSnapshot;
19
+ /** Presentation hints are captured only for this document, never from inherited session storage. */
20
+ export declare function captureChromeHints(): string;
21
+ export declare function withChromeHints(href: string, chrome: UiChromeOverrides): string;
22
+ export { chromeBootstrapScript } from "./index.js";
23
+ export declare function activeNavigationKey(entries: readonly UiNavigationEntry[], endpoint: string | undefined, route: string): string | null;
24
+ /** Shared observable state for local and embedded clients; it owns no frame or identity authority. */
25
+ export declare class UiCompositionModel {
26
+ private readonly path;
27
+ private defaults;
28
+ private chrome;
29
+ private navigation;
30
+ private readonly chromeListeners;
31
+ private readonly navigationListeners;
32
+ private readonly hints;
33
+ private overrides;
34
+ private hostRevision;
35
+ private hostGeneration;
36
+ private rootDisabled;
37
+ private capabilities;
38
+ private environment;
39
+ private router?;
40
+ private stopRouter?;
41
+ private endpoint?;
42
+ private epoch;
43
+ private disposed;
44
+ private refreshing;
45
+ constructor(path: string, mode: UiEnvironment["mode"], defaults?: UiChromeOverrides);
46
+ getChrome: () => UiChromeSnapshot;
47
+ subscribeChrome: (listener: () => void) => () => void;
48
+ getNavigation: () => Readonly<{
49
+ status: "loading" | "ready" | "unavailable";
50
+ catalog: UiNavigationCatalog | null;
51
+ entries: readonly UiNavigationEntry[];
52
+ activeKey: string | null;
53
+ code?: string;
54
+ }>;
55
+ subscribeNavigation: (listener: () => void) => () => void;
56
+ configure(defaults: UiChromeOverrides): void;
57
+ setEnvironment(environment: UiEnvironment): void;
58
+ setHostPolicy(generation: string, revision: number, policy: UiChromeOverrides, rootDisabled: boolean, endpoint?: string): void;
59
+ setLegacyHost(): void;
60
+ private publishChrome;
61
+ attachRouter(router?: UiRouterAdapter): void;
62
+ private updateNavigation;
63
+ currentRoute: () => string;
64
+ currentEndpoint: () => string | undefined;
65
+ isLocalEntry(entry: UiNavigationEntry): boolean;
66
+ updateRoute: () => void;
67
+ refreshNavigation: () => Promise<void>;
68
+ resetNavigation(): void;
69
+ navigateLocal(entry: UiNavigationEntry): Promise<boolean>;
70
+ dispose(): void;
71
+ }