@devframes/hub 0.8.2 → 0.9.0-beta.10

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.
@@ -1,7 +1,6 @@
1
- import { N as NavTarget, O as DevframeViewIframe, P as RemoteConnectionInfo, a as DevframeCommandEntry, g as DevframeDockEntry, h as DevframeDockEntriesGrouped, n as DevframeClientCommand, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings, x as DevframeDockUserEntry, y as DevframeDockEntryIcon } from "../settings-Byh48aCd.mjs";
2
- import { DEFAULT_CATEGORIES_ORDER } from "../constants.mjs";
3
- import { w as DevframeMessagesClient, y as DevframeMessageEntryInput } from "../context-BSgzLnsu.mjs";
4
- import "../index-IzywMKfP.mjs";
1
+ import { E as DevframeMessagesClient, x as DevframeMessageEntryInput } from "../context-D5TJIHwp.mjs";
2
+ import { N as NavTarget, P as RemoteConnectionInfo, S as DevframeDockUserEntry, _ as DevframeDockEntry, a as DevframeCommandEntry, b as DevframeDockEntryIcon, g as DevframeDockEntriesGrouped, k as DevframeViewIframe, n as DevframeClientCommand, p as ClientScriptEntry, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings } from "../settings-DfrRa9Wf.mjs";
3
+ import "../index-coDmWngQ.mjs";
5
4
  import { DevframeClientRpcHost, DevframeConnection, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientOptions, DevframeRpcContext, RpcClientEvents, RpcClientEvents as RpcClientEvents$1 } from "devframe/client";
6
5
  import { EventEmitter } from "devframe/types";
7
6
  import { SharedState } from "devframe/utils/shared-state";
@@ -11,10 +10,17 @@ export * from "devframe/client";
11
10
  /**
12
11
  * Options handed to a dock renderer when the client host mounts a dock entry.
13
12
  */
14
- interface DockRendererMountOptions {
13
+ interface DockRendererMountOptions<Entry extends DevframeDockEntry = DevframeDockEntry> {
15
14
  /** The dock entry being rendered (carries the entry's serializable payload). */
16
- entry: DevframeDockEntry;
17
- /** The DOM element the renderer should mount into. */
15
+ entry: Entry;
16
+ /**
17
+ * The DOM element the renderer should mount into. It may live inside a
18
+ * **shadow root** (the reference viewer isolates dock content that way), so
19
+ * a renderer must deliver its own styles into `container.getRootNode()`
20
+ * rather than assume a page-level stylesheet. The viewer mirrors a live
21
+ * `dark` class onto this element (the theme contract), and CSS custom
22
+ * properties inherit across the shadow boundary for brand theming.
23
+ */
18
24
  container: HTMLElement;
19
25
  /** The assembled client host context (rpc, docks, commands, …). */
20
26
  context: DevframeClientContext;
@@ -26,33 +32,99 @@ interface DockRendererInstance {
26
32
  }
27
33
  /**
28
34
  * A renderer for a dock `type`. The headless hub is renderer-agnostic — a
29
- * host application registers renderers at boot (e.g. injecting
30
- * `@devframes/json-render-ui` for the `'json-render'` type). The renderer
31
- * owns its framework (Vue, React, …); the hub only routes a dock type to it
32
- * and disposes it on deactivation.
35
+ * host registers renderers at boot (e.g. injecting `@devframes/json-render-ui`
36
+ * for the `'json-render'` type) or serves them as prebuilt modules through the
37
+ * hub's renderer manifest (`initHub({ renderers })`). The renderer owns its
38
+ * framework (Vue, React, …); the hub only routes a dock type to it and
39
+ * disposes it on deactivation.
40
+ *
41
+ * Integration packages narrow `Entry` to export a precisely-typed contract —
42
+ * e.g. `@devframes/json-render/hub` exports
43
+ * `JsonRenderDockRenderer = DockRenderer<DevframeJsonRenderDockEntry>`.
33
44
  */
34
- type DockRenderer = (options: DockRendererMountOptions) => DockRendererInstance | Promise<DockRendererInstance>;
45
+ type DockRenderer<Entry extends DevframeDockEntry = DevframeDockEntry> = (options: DockRendererMountOptions<Entry>) => DockRendererInstance | Promise<DockRendererInstance>;
46
+ /**
47
+ * The outcome of {@link DockRenderersContext.mount}. A missing renderer is an
48
+ * expected, non-exceptional state — a viewer renders its "no renderer for this
49
+ * dock type" fallback from `missing-renderer`, and its error variant (with a
50
+ * retry affordance) from `load-error`.
51
+ */
52
+ type DockRendererMountResult = {
53
+ status: 'mounted';
54
+ dispose: () => void;
55
+ } | {
56
+ status: 'missing-renderer';
57
+ } | {
58
+ status: 'load-error';
59
+ error: unknown;
60
+ };
35
61
  /**
36
62
  * The dock-renderer registry surfaced on the client host context. A viewer
37
63
  * calls {@link DockRenderersContext.mount} to render a dock whose `type` has a
38
- * registered renderer into a container it owns; the host tracks the instance
39
- * and disposes it when the entry deactivates.
64
+ * renderer — registered locally at boot, or served as a prebuilt module by the
65
+ * hub's renderer manifest — into a container it owns; the host tracks the
66
+ * instance and disposes it when the entry deactivates.
40
67
  */
41
68
  interface DockRenderersContext {
42
- /** Register a renderer for a dock `type`. Returns an unregister function. */
43
- register: (type: string, renderer: DockRenderer) => () => void;
44
- /** Look up the renderer registered for a dock `type`, if any. */
69
+ /**
70
+ * Register a renderer for a dock `type`. Returns an unregister function.
71
+ * Accepts a renderer narrowed to any specific entry variant (e.g. a
72
+ * {@link DockRenderer}<DevframeJsonRenderDockEntry> from an integration
73
+ * package) — the type routes only its own entries to it.
74
+ */
75
+ register: (type: string, renderer: DockRenderer<any>) => () => void;
76
+ /** Look up the locally-registered renderer for a dock `type`, if any. */
45
77
  get: (type: string) => DockRenderer | undefined;
46
- /** Whether a renderer is registered for a dock `type`. */
78
+ /**
79
+ * Whether a renderer is available for a dock `type` — registered locally
80
+ * **or** provided by the hub's renderer manifest. A viewer checks this
81
+ * before mounting to render its missing-renderer fallback declaratively.
82
+ */
47
83
  has: (type: string) => boolean;
48
84
  /**
49
- * Mount the entry's registered renderer into `container`. Resolves to a
50
- * disposer; the same instance is also disposed automatically when the entry
51
- * deactivates. Warns and resolves to a no-op disposer when no renderer is
52
- * registered for the entry's type.
85
+ * Mount the entry's renderer into `container` and resolve the
86
+ * {@link DockRendererMountResult}. A local registration wins; otherwise the
87
+ * manifest module for the entry's type is imported (lazily, cached) and
88
+ * registered. The mounted instance is also disposed automatically when the
89
+ * entry deactivates. Resolves `missing-renderer` (with a `console.warn`)
90
+ * when neither source has the type, and `load-error` when the module import
91
+ * or the renderer itself fails — a failed import is not cached, so a retry
92
+ * re-imports.
53
93
  */
54
- mount: (entry: DevframeDockEntry, container: HTMLElement) => Promise<() => void>;
94
+ mount: (entry: DevframeDockEntry, container: HTMLElement) => Promise<DockRendererMountResult>;
55
95
  }
96
+ /**
97
+ * The renderer manifest published by the hub at the
98
+ * `devframe:dock-renderers` shared-state slot — one {@link ClientScriptEntry}
99
+ * per dock `type`, whose `importFrom` is a URL path the hub serves
100
+ * (`<base>__renderers/<type>.mjs`). Mirrors the dock client-script
101
+ * convention: the module's `importName` export (default `'default'`) is a
102
+ * ready {@link DockRenderer}.
103
+ */
104
+ type DockRendererManifest = Record<string, ClientScriptEntry>;
105
+ /** Options for {@link createDockRenderersContext}. */
106
+ interface CreateDockRenderersContextOptions {
107
+ /** The assembled client context handed to renderers at mount. */
108
+ context: () => DevframeClientContext;
109
+ /** Renderers registered locally at boot — these win over manifest modules. */
110
+ local?: Record<string, DockRenderer<any>>;
111
+ /** The current {@link DockRendererManifest} (live getter). */
112
+ manifest?: () => DockRendererManifest;
113
+ /**
114
+ * Called with each successful mount's disposer so the caller can tie
115
+ * disposal to its own lifecycle (entry deactivation, host teardown). An
116
+ * optional returned cleanup runs exactly once when the mount is disposed —
117
+ * whichever side (caller or viewer) triggers it first.
118
+ */
119
+ onMounted?: (dispose: () => void, entry: DevframeDockEntry) => (() => void) | void;
120
+ }
121
+ /**
122
+ * Build the {@link DockRenderersContext} shared by every hub-aware client —
123
+ * `createDevframeClientHost` and viewers that assemble their own context
124
+ * (`@devframes/hub-ui`) both delegate here so local-first resolution, lazy
125
+ * manifest imports, and the typed mount result behave identically everywhere.
126
+ */
127
+ declare function createDockRenderersContext(options: CreateDockRenderersContextOptions): DockRenderersContext;
56
128
  //#endregion
57
129
  //#region src/client/docks.d.ts
58
130
  interface DockPanelStorage {
@@ -62,9 +134,30 @@ interface DockPanelStorage {
62
134
  top: number;
63
135
  left: number;
64
136
  position: 'left' | 'right' | 'bottom' | 'top';
65
- open: boolean;
66
137
  inactiveTimeout: number;
67
138
  }
139
+ /**
140
+ * Per-tab UI state of the dock panel, distinct from the browser-shared geometry
141
+ * in {@link DockPanelStorage}. A viewer persists this to `sessionStorage` so a
142
+ * reload (or the RPC auth handshake that follows one) restores the panel to
143
+ * exactly where the developer left it — which dock was open, and the
144
+ * address-bar route of that dock's iframe. Kept `sessionStorage`-scoped (not
145
+ * `localStorage`) because it is per-tab navigation state: two tabs open against
146
+ * the same dev server each keep their own selection rather than fighting over a
147
+ * shared one. Every field is serializable.
148
+ */
149
+ interface DockSessionStorage {
150
+ /** Whether the dock panel is open. */
151
+ open: boolean;
152
+ /** The currently selected dock entry id, or `null` when nothing is open. */
153
+ selectedDockId: string | null;
154
+ /**
155
+ * The live address-bar URL of the selected iframe dock, restored as that
156
+ * iframe's boot `src` on the next load so the deep-linked route survives a
157
+ * reload. `null` when the selected dock has no iframe route to remember.
158
+ */
159
+ selectedDockRoute: string | null;
160
+ }
68
161
  type DockClientType = 'embedded' | 'standalone';
69
162
  interface DocksContext extends DevframeRpcContext {
70
163
  /**
@@ -124,16 +217,41 @@ interface WhenClauseContext {
124
217
  type DevframeClientContext = DocksContext;
125
218
  interface DocksPanelContext {
126
219
  store: DockPanelStorage;
220
+ /**
221
+ * Per-tab session UI state — whether the panel is open, which dock is
222
+ * selected, and that dock's iframe route. Restored across reloads (and the
223
+ * auth handshake that follows one) by a viewer that persists it to
224
+ * `sessionStorage`.
225
+ */
226
+ session: DockSessionStorage;
127
227
  isDragging: boolean;
128
228
  isResizing: boolean;
129
229
  readonly isVertical: boolean;
230
+ /**
231
+ * Claim the persisted {@link DockSessionStorage.selectedDockRoute boot route} for a dock
232
+ * id, once. Returns the saved address-bar URL only for the dock that was
233
+ * selected before the last reload — and only on the first call for it — so a
234
+ * restored iframe boots deep-linked while a later switch to a different dock
235
+ * (whose live route isn't reflected in {@link DockSessionStorage.selectedDockRoute} yet)
236
+ * doesn't reuse the stale value. Returns `null` otherwise.
237
+ */
238
+ consumeBootRoute?: (id: string) => string | null;
130
239
  }
131
240
  interface DocksEntriesContext {
132
241
  selectedId: string | null;
242
+ selectedRoute: string | null;
133
243
  readonly selected: DevframeDockEntry | null;
134
244
  entries: DevframeDockEntry[];
135
245
  entryToStateMap: Map<string, DockEntryState>;
136
246
  groupedEntries: DevframeDockEntriesGrouped;
247
+ /**
248
+ * The resolved top-level category ordering — `DEFAULT_CATEGORIES_ORDER`,
249
+ * overridden by every installed devframe's own `dock.categoryOrder`
250
+ * (`ConnectionMeta.configs.dock.categoryOrder`), overridden again by the
251
+ * host page's own `createDevframeClientHost({ categoryOrder })`. Fixed
252
+ * for the life of the session — resolved once at boot.
253
+ */
254
+ readonly categoryOrder: Record<string, number>;
137
255
  settings: SharedState<DevframeDocksUserSettings>;
138
256
  /**
139
257
  * Get the state of a dock entry by its ID
@@ -266,6 +384,79 @@ declare function resolveDockUrl(url: string, connection: DevframeConnection): st
266
384
  /** Resolve URL-backed dock icons while preserving Iconify names and data URLs. */
267
385
  declare function resolveDockIcon(icon: DevframeDockEntryIcon, connection: DevframeConnection): DevframeDockEntryIcon;
268
386
  //#endregion
387
+ //#region src/client/frame-location.d.ts
388
+ /**
389
+ * Live location tracking for an iframe dock.
390
+ *
391
+ * An iframe dock's address bar — and the route persisted from it into
392
+ * `DockSessionStorage.selectedDockRoute` — has to follow wherever the embedded
393
+ * app actually goes. The `load` event only covers whole-document navigations, so
394
+ * an SPA router moving between routes with `history.pushState()` leaves both
395
+ * showing the URL the frame booted with.
396
+ *
397
+ * {@link watchFrameLocation} closes that gap for a same-origin frame, reporting
398
+ * `location.href` on every navigation it can observe:
399
+ *
400
+ * - `popstate` / `hashchange` — back/forward and hash routing;
401
+ * - `history.pushState`/`replaceState`, wrapped in place (and restored on
402
+ * dispose) because those two fire no event of their own;
403
+ * - the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API)'s
404
+ * `currententrychange` where it exists, which reports post-commit and so also
405
+ * catches what a wrapper structurally cannot — a router holding a reference to
406
+ * `pushState` captured before this watch attached;
407
+ * - `load`, which also re-subscribes: a document navigation swaps the frame's
408
+ * `history`/`navigation` objects, so the previous subscription dies with them.
409
+ *
410
+ * These sources overlap deliberately rather than being chosen between, since a
411
+ * report is deduped against the last href — the cost of hearing the same
412
+ * navigation twice is nothing, and the cost of missing one is a stale route.
413
+ *
414
+ * A cross-origin frame reports nothing — its location is unreadable by design,
415
+ * so the caller keeps the last URL it knew.
416
+ */
417
+ /** The minimal `history` surface the watcher wraps. Injectable for tests. */
418
+ interface FrameLocationHistory {
419
+ pushState: (...args: any[]) => void;
420
+ replaceState: (...args: any[]) => void;
421
+ }
422
+ /** The minimal frame-window surface the watcher reads and subscribes to. */
423
+ interface FrameLocationWindow {
424
+ readonly location: {
425
+ readonly href: string;
426
+ };
427
+ readonly history?: FrameLocationHistory;
428
+ /** Present only where the Navigation API ships. */
429
+ readonly navigation?: {
430
+ addEventListener: (type: 'currententrychange', listener: () => void) => void;
431
+ removeEventListener: (type: 'currententrychange', listener: () => void) => void;
432
+ };
433
+ addEventListener: (type: 'popstate' | 'hashchange', listener: () => void) => void;
434
+ removeEventListener: (type: 'popstate' | 'hashchange', listener: () => void) => void;
435
+ }
436
+ /** The minimal iframe surface the watcher needs. Injectable for tests. */
437
+ interface FrameLocationTarget {
438
+ readonly contentWindow: FrameLocationWindow | null;
439
+ addEventListener: (type: 'load', listener: () => void) => void;
440
+ removeEventListener: (type: 'load', listener: () => void) => void;
441
+ }
442
+ interface WatchFrameLocationOptions {
443
+ /** The live iframe whose location is tracked. */
444
+ iframe: FrameLocationTarget;
445
+ /** Called with the frame's `location.href` on each observed change. */
446
+ onChange: (href: string) => void;
447
+ /**
448
+ * The href the caller already shows, so an initial report is only made when
449
+ * the frame is somewhere else (a frame re-attached mid-session, say).
450
+ */
451
+ initial?: string;
452
+ }
453
+ /**
454
+ * Track a frame's location until the returned disposer is called. Disposing
455
+ * detaches every listener and restores any wrapped `history` method, leaving the
456
+ * embedded page as it was found.
457
+ */
458
+ declare function watchFrameLocation(options: WatchFrameLocationOptions): () => void;
459
+ //#endregion
269
460
  //#region src/client/frame-nav.d.ts
270
461
  /**
271
462
  * Shared-iframe soft navigation — the viewer-side half of a host↔iframe
@@ -413,8 +604,11 @@ interface DevframeClientHostOptions {
413
604
  * application injects the ones it wants (e.g.
414
605
  * `{ 'json-render': createJsonRenderDockRenderer() }` from
415
606
  * `@devframes/json-render-ui`). The hub ships none by default.
607
+ *
608
+ * Local registrations take precedence over the hub's renderer manifest
609
+ * (`initHub({ renderers })`) — explicit local code beats wire config.
416
610
  */
417
- renderers?: Record<string, DockRenderer>;
611
+ renderers?: Record<string, DockRenderer<any>>;
418
612
  /**
419
613
  * Hub-wide override of the top-level dock-bar category ordering — a map of
420
614
  * category id → ordering weight (lower sorts earlier), mirroring the shape
@@ -428,7 +622,7 @@ interface DevframeClientHostOptions {
428
622
  *
429
623
  * Keys are merged over `DEFAULT_CATEGORIES_ORDER`, so the host app only
430
624
  * lists the categories it wants to move; any category absent from the map
431
- * keeps its default weight (falling back to `0`).
625
+ * keeps its default weight.
432
626
  *
433
627
  * @example
434
628
  * ```ts
@@ -504,4 +698,4 @@ declare function parseRemoteConnection(input?: string): RemoteConnectionInfo | n
504
698
  */
505
699
  declare function connectRemoteDevframe(options?: ConnectRemoteDevframeOptions): Promise<DevframeRpcClient>;
506
700
  //#endregion
507
- export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, DEFAULT_CATEGORIES_ORDER, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererMountOptions, DockRenderersContext, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl };
701
+ export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, CreateDockRenderersContextOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererManifest, DockRendererMountOptions, DockRendererMountResult, DockRenderersContext, DockSessionStorage, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameLocationHistory, FrameLocationTarget, FrameLocationWindow, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WatchFrameLocationOptions, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation };