@forgeax/app-shell 0.2.0 → 0.4.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/README.md CHANGED
@@ -33,6 +33,25 @@ registry.register(createShellContribution({
33
33
  }));
34
34
  ```
35
35
 
36
+ React shell and slot-diagnostic primitives are available from the explicit
37
+ React entry point:
38
+
39
+ ```tsx
40
+ import {
41
+ ShellSlot,
42
+ SlotDebugOverlay,
43
+ isSlotDebugEnabled,
44
+ } from '@forgeax/app-shell/react';
45
+
46
+ export function ProductShell() {
47
+ return (
48
+ <ShellSlot as="main" name="Workspace">
49
+ {isSlotDebugEnabled() ? <SlotDebugOverlay /> : null}
50
+ </ShellSlot>
51
+ );
52
+ }
53
+ ```
54
+
36
55
  ## Ownership boundary
37
56
 
38
57
  | Package | Owns |
package/dist/dock.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ declare const REGIONS: readonly ["DockShell", "AuxBar", "ChatDock", "StatusBar"];
2
+ type Region = typeof REGIONS[number];
3
+ declare const DOCK_REGIONS: readonly ["DockShell", "AuxBar", "ChatDock"];
4
+ type DockRegion = typeof DOCK_REGIONS[number];
5
+ declare function isDockRegion(value: string): value is DockRegion;
6
+ interface PanelDescriptorLite {
7
+ defaultRegion?: DockRegion;
8
+ }
9
+ declare function resolveRegion(id: string, descriptor: PanelDescriptorLite, overrides: Readonly<Record<string, DockRegion>>): DockRegion;
10
+ interface DockviewApiLike {
11
+ readonly id: string;
12
+ getPanel(id: string): {
13
+ readonly api: {
14
+ close(): void;
15
+ };
16
+ } | undefined;
17
+ }
18
+ declare function registerDockviewApi(api: DockviewApiLike): () => void;
19
+ declare function getDockviewApi(viewId: string): DockviewApiLike | undefined;
20
+ interface DockRegionEntry {
21
+ readonly viewId: string;
22
+ readonly region: string;
23
+ readonly api: unknown;
24
+ readonly wrapEl: HTMLElement;
25
+ }
26
+ declare function registerDockRegion(entry: DockRegionEntry): () => void;
27
+ declare function getDockRegions(): DockRegionEntry[];
28
+ type SideEdge = 'left' | 'right';
29
+ interface RectLike {
30
+ left: number;
31
+ right: number;
32
+ }
33
+ declare function isOnSideEdge(location: {
34
+ type: string;
35
+ position?: string;
36
+ }): boolean;
37
+ declare function nearerSideEdge(panel: RectLike, shell: RectLike): SideEdge;
38
+ type DropPosition = 'top' | 'bottom' | 'left' | 'right' | 'center';
39
+ type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
40
+ interface AddPanelPosition {
41
+ referenceGroup?: unknown;
42
+ direction?: Direction;
43
+ }
44
+ interface CrossInstanceDropEvent {
45
+ readonly api: DockviewApiLike & {
46
+ addPanel(options: {
47
+ id: string;
48
+ component: string;
49
+ title?: string;
50
+ position?: AddPanelPosition;
51
+ }): unknown;
52
+ };
53
+ readonly position?: DropPosition;
54
+ readonly group?: unknown;
55
+ getData(): {
56
+ readonly viewId: string;
57
+ readonly panelId: string | null;
58
+ } | undefined;
59
+ }
60
+ declare function handleCrossInstanceDrop(event: CrossInstanceDropEvent, targetRegion: DockRegion, moveTo: (panelId: string, region: DockRegion) => void, options?: {
61
+ componentFor?: (panelId: string) => string;
62
+ titleFor?: (panelId: string) => string | undefined;
63
+ }): void;
64
+
65
+ export { type CrossInstanceDropEvent, DOCK_REGIONS, type DockRegion, type DockRegionEntry, type DockviewApiLike, type PanelDescriptorLite, REGIONS, type RectLike, type Region, type SideEdge, getDockRegions, getDockviewApi, handleCrossInstanceDrop, isDockRegion, isOnSideEdge, nearerSideEdge, registerDockRegion, registerDockviewApi, resolveRegion };
package/dist/dock.js ADDED
@@ -0,0 +1,93 @@
1
+ // src/dock.ts
2
+ var REGIONS = ["DockShell", "AuxBar", "ChatDock", "StatusBar"];
3
+ var DOCK_REGIONS = ["DockShell", "AuxBar", "ChatDock"];
4
+ function isDockRegion(value) {
5
+ return DOCK_REGIONS.includes(value);
6
+ }
7
+ function resolveRegion(id, descriptor, overrides) {
8
+ return overrides[id] ?? descriptor.defaultRegion ?? "DockShell";
9
+ }
10
+ var dockviewApis = /* @__PURE__ */ new Map();
11
+ function registerDockviewApi(api) {
12
+ dockviewApis.set(api.id, api);
13
+ return () => {
14
+ if (dockviewApis.get(api.id) === api) dockviewApis.delete(api.id);
15
+ };
16
+ }
17
+ function getDockviewApi(viewId) {
18
+ return dockviewApis.get(viewId);
19
+ }
20
+ var dockRegions = /* @__PURE__ */ new Map();
21
+ function registerDockRegion(entry) {
22
+ dockRegions.set(entry.viewId, entry);
23
+ return () => {
24
+ if (dockRegions.get(entry.viewId) === entry) dockRegions.delete(entry.viewId);
25
+ };
26
+ }
27
+ function getDockRegions() {
28
+ return [...dockRegions.values()];
29
+ }
30
+ function isOnSideEdge(location) {
31
+ return location.type === "edge" && (location.position === "left" || location.position === "right");
32
+ }
33
+ function nearerSideEdge(panel, shell) {
34
+ const middle = (panel.left + panel.right) / 2;
35
+ return middle - shell.left <= shell.right - middle ? "left" : "right";
36
+ }
37
+ function toDirection(position) {
38
+ switch (position) {
39
+ case "top":
40
+ return "above";
41
+ case "bottom":
42
+ return "below";
43
+ case "left":
44
+ return "left";
45
+ case "right":
46
+ return "right";
47
+ case "center":
48
+ return "within";
49
+ default:
50
+ return void 0;
51
+ }
52
+ }
53
+ function handleCrossInstanceDrop(event, targetRegion, moveTo, options) {
54
+ const transfer = event.getData();
55
+ if (!transfer?.panelId || transfer.viewId === event.api.id) return;
56
+ const sourceApi = getDockviewApi(transfer.viewId);
57
+ if (sourceApi) {
58
+ try {
59
+ sourceApi.getPanel(transfer.panelId)?.api.close();
60
+ } catch {
61
+ }
62
+ }
63
+ const direction = toDirection(event.position);
64
+ let position;
65
+ if (event.group && direction) {
66
+ position = { referenceGroup: event.group, direction };
67
+ } else if (direction && direction !== "within") {
68
+ position = { direction };
69
+ }
70
+ try {
71
+ event.api.addPanel({
72
+ id: transfer.panelId,
73
+ component: options?.componentFor?.(transfer.panelId) ?? transfer.panelId,
74
+ title: options?.titleFor?.(transfer.panelId),
75
+ ...position ? { position } : {}
76
+ });
77
+ } catch {
78
+ }
79
+ moveTo(transfer.panelId, targetRegion);
80
+ }
81
+ export {
82
+ DOCK_REGIONS,
83
+ REGIONS,
84
+ getDockRegions,
85
+ getDockviewApi,
86
+ handleCrossInstanceDrop,
87
+ isDockRegion,
88
+ isOnSideEdge,
89
+ nearerSideEdge,
90
+ registerDockRegion,
91
+ registerDockviewApi,
92
+ resolveRegion
93
+ };
@@ -0,0 +1,18 @@
1
+ import { ReactElement, HTMLAttributes } from 'react';
2
+
3
+ /** Stable FNV-1a color bucket for a shell slot name. */
4
+ declare function hashSlotHue(name: string): number;
5
+ /** Whether a composable `debug` query contains the exact `slots` token. */
6
+ declare function isSlotDebugEnabled(search?: string): boolean;
7
+ /** Dev-time visualization derived only from live `data-fx-slot` markers. */
8
+ declare function SlotDebugOverlay(): ReactElement;
9
+
10
+ type ShellSlotElement = 'aside' | 'div' | 'main' | 'section';
11
+ interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
12
+ as?: ShellSlotElement;
13
+ name: string;
14
+ }
15
+ /** Structural shell marker that preserves the caller's semantic element. */
16
+ declare function ShellSlot({ as, name, ...props }: ShellSlotProps): ReactElement;
17
+
18
+ export { ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, hashSlotHue, isSlotDebugEnabled };
package/dist/react.js ADDED
@@ -0,0 +1,178 @@
1
+ // src/react.tsx
2
+ import { createElement } from "react";
3
+
4
+ // src/slot-debug.tsx
5
+ import { useEffect, useState } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var OVERLAY_Z = 2147483e3;
8
+ var LABEL_CORNERS = [
9
+ { top: 2, left: 2 },
10
+ { top: 2, right: 2 },
11
+ { bottom: 2, left: 2 },
12
+ { bottom: 2, right: 2 }
13
+ ];
14
+ function hashSlotHue(name) {
15
+ let hash = 2166136261;
16
+ for (let index = 0; index < name.length; index += 1) {
17
+ hash ^= name.charCodeAt(index);
18
+ hash = Math.imul(hash, 16777619);
19
+ }
20
+ return (hash >>> 0) % 360;
21
+ }
22
+ function isSlotDebugEnabled(search = typeof window !== "undefined" ? window.location.search : "") {
23
+ try {
24
+ const debug = new URLSearchParams(search).get("debug");
25
+ return debug?.split(",").some((flag) => flag.trim() === "slots") ?? false;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ function measureSlots() {
31
+ const elements = Array.from(document.querySelectorAll("[data-fx-slot]"));
32
+ const boxes = [];
33
+ for (const element of elements) {
34
+ const name = element.getAttribute("data-fx-slot") ?? "";
35
+ if (!name) continue;
36
+ let rect;
37
+ if (element.firstChild) {
38
+ const range = document.createRange();
39
+ range.selectNodeContents(element);
40
+ rect = range.getBoundingClientRect();
41
+ } else {
42
+ rect = element.getBoundingClientRect();
43
+ }
44
+ if (rect.width === 0 && rect.height === 0) continue;
45
+ let depth = 0;
46
+ let parentName = null;
47
+ let cursor = element.parentElement;
48
+ while (cursor) {
49
+ if (cursor.hasAttribute("data-fx-slot")) {
50
+ parentName ??= cursor.getAttribute("data-fx-slot");
51
+ depth += 1;
52
+ }
53
+ cursor = cursor.parentElement;
54
+ }
55
+ boxes.push({
56
+ name,
57
+ parentName,
58
+ depth,
59
+ left: rect.left,
60
+ top: rect.top,
61
+ width: rect.width,
62
+ height: rect.height
63
+ });
64
+ }
65
+ return boxes;
66
+ }
67
+ function SlotDebugOverlay() {
68
+ const [boxes, setBoxes] = useState([]);
69
+ useEffect(() => {
70
+ if (typeof document === "undefined") return;
71
+ let frame = 0;
72
+ const schedule = () => {
73
+ cancelAnimationFrame(frame);
74
+ frame = requestAnimationFrame(() => setBoxes(measureSlots()));
75
+ };
76
+ setBoxes(measureSlots());
77
+ const resizeObserver = new ResizeObserver(schedule);
78
+ const attach = () => {
79
+ resizeObserver.disconnect();
80
+ document.querySelectorAll("[data-fx-slot]").forEach((element) => resizeObserver.observe(element));
81
+ };
82
+ attach();
83
+ const mutationObserver = new MutationObserver((records) => {
84
+ let markerSetChanged = false;
85
+ for (const record of records) {
86
+ if (record.type === "attributes" && record.attributeName === "data-fx-slot") {
87
+ markerSetChanged = true;
88
+ break;
89
+ }
90
+ if (record.type !== "childList") continue;
91
+ const nodes = [...record.addedNodes, ...record.removedNodes];
92
+ markerSetChanged = nodes.some((node) => node instanceof Element && (node.matches("[data-fx-slot]") || node.querySelector("[data-fx-slot]")));
93
+ if (markerSetChanged) break;
94
+ }
95
+ if (markerSetChanged) attach();
96
+ schedule();
97
+ });
98
+ mutationObserver.observe(document.body, {
99
+ subtree: true,
100
+ childList: true,
101
+ attributes: true,
102
+ attributeFilter: ["data-fx-slot"]
103
+ });
104
+ window.addEventListener("resize", schedule);
105
+ window.addEventListener("scroll", schedule, true);
106
+ return () => {
107
+ cancelAnimationFrame(frame);
108
+ resizeObserver.disconnect();
109
+ mutationObserver.disconnect();
110
+ window.removeEventListener("resize", schedule);
111
+ window.removeEventListener("scroll", schedule, true);
112
+ };
113
+ }, []);
114
+ return /* @__PURE__ */ jsx(
115
+ "div",
116
+ {
117
+ style: {
118
+ position: "fixed",
119
+ inset: 0,
120
+ pointerEvents: "none",
121
+ zIndex: OVERLAY_Z
122
+ },
123
+ "data-fx-slot-overlay": "",
124
+ children: boxes.map((box, index) => {
125
+ const hue = hashSlotHue(box.name);
126
+ const corner = LABEL_CORNERS[box.depth % LABEL_CORNERS.length];
127
+ const label = box.parentName ? `${box.parentName} \u2192 ${box.name}` : box.name;
128
+ return /* @__PURE__ */ jsx(
129
+ "div",
130
+ {
131
+ style: {
132
+ position: "fixed",
133
+ left: box.left,
134
+ top: box.top,
135
+ width: box.width,
136
+ height: box.height,
137
+ background: `hsla(${hue}, 65%, 55%, ${Math.min(0.35, 0.1 + box.depth * 0.06)})`,
138
+ outline: `${Math.max(1, 3 - box.depth)}px solid hsla(${hue}, 65%, 55%, 0.7)`,
139
+ pointerEvents: "none"
140
+ },
141
+ children: /* @__PURE__ */ jsx(
142
+ "span",
143
+ {
144
+ style: {
145
+ position: "absolute",
146
+ ...corner,
147
+ background: `hsla(${hue}, 65%, 25%, 0.9)`,
148
+ color: "#fff",
149
+ font: "10px/14px ui-monospace, monospace",
150
+ padding: "1px 4px",
151
+ borderRadius: 2,
152
+ whiteSpace: "nowrap"
153
+ },
154
+ children: label
155
+ }
156
+ )
157
+ },
158
+ `${box.name}:${index}`
159
+ );
160
+ })
161
+ }
162
+ );
163
+ }
164
+
165
+ // src/react.tsx
166
+ function ShellSlot({
167
+ as = "div",
168
+ name,
169
+ ...props
170
+ }) {
171
+ return createElement(as, { ...props, "data-fx-slot": name });
172
+ }
173
+ export {
174
+ ShellSlot,
175
+ SlotDebugOverlay,
176
+ hashSlotHue,
177
+ isSlotDebugEnabled
178
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/app-shell",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Generic Dock, Panel, Window, and Slot composition primitives",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -12,20 +12,36 @@
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
+ },
16
+ "./dock": {
17
+ "types": "./dist/dock.d.ts",
18
+ "import": "./dist/dock.js"
19
+ },
20
+ "./react": {
21
+ "types": "./dist/react.d.ts",
22
+ "import": "./dist/react.js"
15
23
  }
16
24
  },
17
25
  "scripts": {
18
26
  "typecheck": "tsc --noEmit",
19
- "test": "bun test test/*.test.ts",
20
- "build": "tsup src/index.ts --format esm --dts --outDir dist",
27
+ "test": "bun test test",
28
+ "build": "tsup src/index.ts src/dock.ts src/react.tsx --format esm --dts --outDir dist",
21
29
  "check": "bun run typecheck && bun run test && bun run build",
22
30
  "release:preflight": "bun run scripts/release-preflight.ts"
23
31
  },
24
32
  "dependencies": {
25
33
  "@forgeax/extension-contracts": "^0.1.0"
26
34
  },
35
+ "peerDependencies": {
36
+ "react": "^19.0.0"
37
+ },
27
38
  "devDependencies": {
39
+ "@happy-dom/global-registrator": "^20.10.2",
40
+ "@types/react": "^19.0.0",
41
+ "@types/react-dom": "^19.0.0",
28
42
  "bun-types": "^1.3.14",
43
+ "react": "^19.0.0",
44
+ "react-dom": "^19.0.0",
29
45
  "tsup": "^8.5.0",
30
46
  "typescript": "^5.9.2"
31
47
  },