@forgeax/app-shell 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dock.d.ts CHANGED
@@ -43,6 +43,32 @@ interface SerializedDockLayoutLike {
43
43
  * leaves and branches. The input snapshot is never mutated.
44
44
  */
45
45
  declare function pruneSerializedDockLayout<T extends SerializedDockLayoutLike>(layout: T, knownComponents: ReadonlySet<string>, allowedPanelIds?: ReadonlySet<string>): T | null;
46
+ type DockLayoutOrientation = 'HORIZONTAL' | 'VERTICAL';
47
+ type DockReopenDirection = 'left' | 'right' | 'above' | 'below' | 'within';
48
+ /** Structural shape shared by concrete serialized dock tree implementations. */
49
+ interface AuthoredDockNodeLike {
50
+ readonly type: 'leaf' | 'branch';
51
+ readonly data: unknown;
52
+ }
53
+ interface AuthoredDockLayoutLike {
54
+ readonly grid?: {
55
+ readonly orientation: DockLayoutOrientation;
56
+ readonly root?: AuthoredDockNodeLike;
57
+ };
58
+ }
59
+ type DesignedDockPanelPosition = {
60
+ readonly kind: 'relative';
61
+ readonly referencePanel: string;
62
+ readonly direction: DockReopenDirection;
63
+ } | {
64
+ readonly kind: 'edge';
65
+ readonly direction: Exclude<DockReopenDirection, 'within'>;
66
+ };
67
+ /**
68
+ * Derive a closed panel's authored seat against a panel that is still open.
69
+ * Returns undefined when the panel is absent or no live sibling can anchor it.
70
+ */
71
+ declare function designedDockPanelPosition(layout: AuthoredDockLayoutLike, panelId: string, isOpen: (id: string) => boolean): DesignedDockPanelPosition | undefined;
46
72
  type SideEdge = 'left' | 'right';
47
73
  interface RectLike {
48
74
  left: number;
@@ -80,4 +106,4 @@ declare function handleCrossInstanceDrop(event: CrossInstanceDropEvent, targetRe
80
106
  titleFor?: (panelId: string) => string | undefined;
81
107
  }): void;
82
108
 
83
- export { type CrossInstanceDropEvent, DOCK_REGIONS, type DockRegion, type DockRegionEntry, type DockviewApiLike, type PanelDescriptorLite, REGIONS, type RectLike, type Region, type SerializedDockLayoutLike, type SerializedDockPanelLike, type SideEdge, getDockRegions, getDockviewApi, handleCrossInstanceDrop, hasMountedPanelPlacement, isDockPanelVisible, isDockRegion, isOnSideEdge, nearerSideEdge, pruneSerializedDockLayout, registerDockRegion, registerDockviewApi, resolveRegion, trackDockPanelVisibility };
109
+ export { type AuthoredDockLayoutLike, type AuthoredDockNodeLike, type CrossInstanceDropEvent, DOCK_REGIONS, type DesignedDockPanelPosition, type DockLayoutOrientation, type DockRegion, type DockRegionEntry, type DockReopenDirection, type DockviewApiLike, type PanelDescriptorLite, REGIONS, type RectLike, type Region, type SerializedDockLayoutLike, type SerializedDockPanelLike, type SideEdge, designedDockPanelPosition, getDockRegions, getDockviewApi, handleCrossInstanceDrop, hasMountedPanelPlacement, isDockPanelVisible, isDockRegion, isOnSideEdge, nearerSideEdge, pruneSerializedDockLayout, registerDockRegion, registerDockviewApi, resolveRegion, trackDockPanelVisibility };
package/dist/dock.js CHANGED
@@ -84,6 +84,75 @@ function pruneSerializedDockLayout(layout, knownComponents, allowedPanelIds) {
84
84
  grid: layout.grid ? { ...layout.grid, root } : layout.grid
85
85
  };
86
86
  }
87
+ function authoredLeafViews(node) {
88
+ if (node.type !== "leaf" || !node.data || typeof node.data !== "object") return [];
89
+ const views = node.data.views;
90
+ return Array.isArray(views) ? views : [];
91
+ }
92
+ function authoredBranchChildren(node) {
93
+ return node.type === "branch" && Array.isArray(node.data) ? node.data : [];
94
+ }
95
+ function firstOpenAuthoredPanel(node, isOpen) {
96
+ if (node.type === "leaf") return authoredLeafViews(node).find(isOpen);
97
+ for (const child of authoredBranchChildren(node)) {
98
+ const match = firstOpenAuthoredPanel(child, isOpen);
99
+ if (match) return match;
100
+ }
101
+ return void 0;
102
+ }
103
+ function findAuthoredPanelPath(root, panelId) {
104
+ if (root.type === "leaf") {
105
+ return authoredLeafViews(root).includes(panelId) ? [root] : void 0;
106
+ }
107
+ for (const child of authoredBranchChildren(root)) {
108
+ const path = findAuthoredPanelPath(child, panelId);
109
+ if (path) return [root, ...path];
110
+ }
111
+ return void 0;
112
+ }
113
+ function authoredBranchOrientation(rootOrientation, depth) {
114
+ if (depth % 2 === 0) return rootOrientation;
115
+ return rootOrientation === "HORIZONTAL" ? "VERTICAL" : "HORIZONTAL";
116
+ }
117
+ function designedDockPanelPosition(layout, panelId, isOpen) {
118
+ const root = layout.grid?.root;
119
+ if (!root) return void 0;
120
+ const path = findAuthoredPanelPath(root, panelId);
121
+ if (!path) return void 0;
122
+ const leaf = path[path.length - 1];
123
+ const tabMate = authoredLeafViews(leaf).find((id) => id !== panelId && isOpen(id));
124
+ if (tabMate) return { kind: "relative", referencePanel: tabMate, direction: "within" };
125
+ if (path.length === 2 && path[0].type === "branch") {
126
+ const siblings = authoredBranchChildren(path[0]);
127
+ const index = siblings.indexOf(leaf);
128
+ const horizontal = layout.grid?.orientation === "HORIZONTAL";
129
+ if (index === 0) return { kind: "edge", direction: horizontal ? "left" : "above" };
130
+ if (index === siblings.length - 1) {
131
+ return { kind: "edge", direction: horizontal ? "right" : "below" };
132
+ }
133
+ }
134
+ for (let depth = path.length - 2; depth >= 0; depth--) {
135
+ const branch = path[depth];
136
+ if (branch.type !== "branch") continue;
137
+ const children = authoredBranchChildren(branch);
138
+ const index = children.indexOf(path[depth + 1]);
139
+ if (index < 0) continue;
140
+ const horizontal = authoredBranchOrientation(layout.grid.orientation, depth) === "HORIZONTAL";
141
+ for (let i = index + 1; i < children.length; i++) {
142
+ const referencePanel = firstOpenAuthoredPanel(children[i], isOpen);
143
+ if (referencePanel) {
144
+ return { kind: "relative", referencePanel, direction: horizontal ? "left" : "above" };
145
+ }
146
+ }
147
+ for (let i = index - 1; i >= 0; i--) {
148
+ const referencePanel = firstOpenAuthoredPanel(children[i], isOpen);
149
+ if (referencePanel) {
150
+ return { kind: "relative", referencePanel, direction: horizontal ? "right" : "below" };
151
+ }
152
+ }
153
+ }
154
+ return void 0;
155
+ }
87
156
  function isOnSideEdge(location) {
88
157
  return location.type === "edge" && (location.position === "left" || location.position === "right");
89
158
  }
@@ -138,6 +207,7 @@ function handleCrossInstanceDrop(event, targetRegion, moveTo, options) {
138
207
  export {
139
208
  DOCK_REGIONS,
140
209
  REGIONS,
210
+ designedDockPanelPosition,
141
211
  getDockRegions,
142
212
  getDockviewApi,
143
213
  handleCrossInstanceDrop,
@@ -0,0 +1,31 @@
1
+ type SurfacePane = 'left' | 'center';
2
+ type SurfaceKind = 'plugin' | 'panel';
3
+ /** Structural identity shared by in-window and detached surface carriers. */
4
+ interface SurfaceDescriptor {
5
+ readonly kind: SurfaceKind;
6
+ readonly id: string;
7
+ readonly pane?: SurfacePane;
8
+ readonly instance?: string;
9
+ }
10
+ /** Complete carrier-neutral declaration for opening one detached surface. */
11
+ interface DetachedWindowTarget {
12
+ readonly surface: SurfaceDescriptor;
13
+ readonly title: string;
14
+ readonly width: number;
15
+ readonly height: number;
16
+ readonly dockBehavior: 'close' | 'keep-anchor';
17
+ }
18
+ /** Presence of this factory declares that a surface may be detached. */
19
+ interface DetachedWindowCapability<Context = void> {
20
+ createTarget(context: Context): DetachedWindowTarget;
21
+ }
22
+ /** Stable identity shared by keep-alive registries and physical carriers. */
23
+ declare function surfaceKey(surface: SurfaceDescriptor): string;
24
+ /** Deterministic, injective and carrier-safe label for the complete identity. */
25
+ declare function surfaceWindowLabel(surface: SurfaceDescriptor): string;
26
+ /** Encode only structural surface identity; product carrier policy stays outside. */
27
+ declare function encodeSurfaceQuery(surface: SurfaceDescriptor): string;
28
+ /** Decode structural identity, or null for a normal shell/invalid entry. */
29
+ declare function decodeSurfaceFromLocation(search?: string): SurfaceDescriptor | null;
30
+
31
+ export { type DetachedWindowCapability, type DetachedWindowTarget, type SurfaceDescriptor, type SurfaceKind, type SurfacePane, decodeSurfaceFromLocation, encodeSurfaceQuery, surfaceKey, surfaceWindowLabel };
package/dist/window.js ADDED
@@ -0,0 +1,57 @@
1
+ // src/window.ts
2
+ function surfaceKey(surface) {
3
+ return JSON.stringify([
4
+ surface.kind,
5
+ surface.id,
6
+ surface.pane ?? null,
7
+ surface.instance ?? null
8
+ ]);
9
+ }
10
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
11
+ function utf8ToBase64Url(value) {
12
+ const bytes = new TextEncoder().encode(value);
13
+ let encoded = "";
14
+ for (let index = 0; index < bytes.length; index += 3) {
15
+ const first = bytes[index];
16
+ const second = bytes[index + 1];
17
+ const third = bytes[index + 2];
18
+ encoded += BASE64URL_ALPHABET[first >> 2];
19
+ encoded += BASE64URL_ALPHABET[(first & 3) << 4 | (second ?? 0) >> 4];
20
+ if (second !== void 0) {
21
+ encoded += BASE64URL_ALPHABET[(second & 15) << 2 | (third ?? 0) >> 6];
22
+ }
23
+ if (third !== void 0) encoded += BASE64URL_ALPHABET[third & 63];
24
+ }
25
+ return encoded;
26
+ }
27
+ function surfaceWindowLabel(surface) {
28
+ return `fx-surface-${utf8ToBase64Url(surfaceKey(surface))}`;
29
+ }
30
+ function encodeSurfaceQuery(surface) {
31
+ const params = new URLSearchParams();
32
+ params.set("surface", surface.kind);
33
+ params.set("id", surface.id);
34
+ if (surface.pane) params.set("pane", surface.pane);
35
+ if (surface.instance !== void 0) params.set("instance", surface.instance);
36
+ return params.toString();
37
+ }
38
+ function decodeSurfaceFromLocation(search = typeof window !== "undefined" ? window.location.search : "") {
39
+ const params = new URLSearchParams(search);
40
+ const kind = params.get("surface");
41
+ const id = params.get("id");
42
+ if (!id || kind !== "plugin" && kind !== "panel") return null;
43
+ const pane = params.get("pane");
44
+ const instance = params.get("instance");
45
+ return {
46
+ kind,
47
+ id,
48
+ pane: pane === "left" || pane === "center" ? pane : void 0,
49
+ instance: instance ?? void 0
50
+ };
51
+ }
52
+ export {
53
+ decodeSurfaceFromLocation,
54
+ encodeSurfaceQuery,
55
+ surfaceKey,
56
+ surfaceWindowLabel
57
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/app-shell",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Generic Dock, Panel, Window, and Slot composition primitives",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,6 +17,10 @@
17
17
  "types": "./dist/dock.d.ts",
18
18
  "import": "./dist/dock.js"
19
19
  },
20
+ "./window": {
21
+ "types": "./dist/window.d.ts",
22
+ "import": "./dist/window.js"
23
+ },
20
24
  "./react": {
21
25
  "types": "./dist/react.d.ts",
22
26
  "import": "./dist/react.js"
@@ -27,7 +31,7 @@
27
31
  "scripts": {
28
32
  "typecheck": "tsc --noEmit",
29
33
  "test": "bun test test",
30
- "build": "tsup src/index.ts src/dock.ts src/react.tsx --format esm --dts --outDir dist && bun run scripts/copy-resize-css.ts && bun run scripts/copy-panel-css.ts",
34
+ "build": "tsup src/index.ts src/dock.ts src/window.ts src/react.tsx --format esm --dts --outDir dist && bun run scripts/copy-resize-css.ts && bun run scripts/copy-panel-css.ts",
31
35
  "check": "bun run typecheck && bun run test && bun run build",
32
36
  "release:preflight": "bun run scripts/release-preflight.ts"
33
37
  },