@forgeax/app-shell 0.7.0 → 0.9.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 +31 -0
- package/dist/dock.d.ts +22 -1
- package/dist/dock.js +62 -1
- package/dist/panel.css +41 -0
- package/dist/react.d.ts +21 -2
- package/dist/react.js +39 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -67,6 +67,37 @@ const [width, setWidth] = useLocalSize('product.sidebar.width', 280, 180, 640);
|
|
|
67
67
|
</aside>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
Generic panel sections keep product renderers and action policies outside App
|
|
71
|
+
Shell while providing stable shell/content metadata:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import '@forgeax/app-shell/panel.css';
|
|
75
|
+
import { PanelSurface } from '@forgeax/app-shell/react';
|
|
76
|
+
|
|
77
|
+
<PanelSurface
|
|
78
|
+
id="project-tree"
|
|
79
|
+
registered
|
|
80
|
+
header={<ProductPanelActions />}
|
|
81
|
+
content={{ padding: 'sm', scroll: 'auto', tone: 'tool' }}
|
|
82
|
+
>
|
|
83
|
+
<ProjectTree />
|
|
84
|
+
</PanelSurface>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Dock state adapters are available without a Dockview runtime dependency:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import {
|
|
91
|
+
hasMountedPanelPlacement,
|
|
92
|
+
isDockPanelVisible,
|
|
93
|
+
pruneSerializedDockLayout,
|
|
94
|
+
trackDockPanelVisibility,
|
|
95
|
+
} from '@forgeax/app-shell/dock';
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Products own event subscription and layout persistence; the adapters only track
|
|
99
|
+
stable panel ids and transform structural serialized snapshots.
|
|
100
|
+
|
|
70
101
|
## Ownership boundary
|
|
71
102
|
|
|
72
103
|
| Package | Owns |
|
package/dist/dock.d.ts
CHANGED
|
@@ -25,6 +25,27 @@ interface DockRegionEntry {
|
|
|
25
25
|
}
|
|
26
26
|
declare function registerDockRegion(entry: DockRegionEntry): () => void;
|
|
27
27
|
declare function getDockRegions(): DockRegionEntry[];
|
|
28
|
+
/** Track one live mount of a dock panel and return an idempotent release. */
|
|
29
|
+
declare function trackDockPanelVisibility(panelId: string): () => void;
|
|
30
|
+
declare function isDockPanelVisible(panelId: string): boolean;
|
|
31
|
+
declare function hasMountedPanelPlacement(panelIds: readonly string[], mountedPanelIds: ReadonlySet<string>): boolean;
|
|
32
|
+
interface SerializedDockPanelLike {
|
|
33
|
+
readonly contentComponent?: string;
|
|
34
|
+
readonly [key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
interface SerializedDockLayoutLike {
|
|
37
|
+
readonly panels?: Readonly<Record<string, SerializedDockPanelLike>>;
|
|
38
|
+
readonly grid?: Readonly<{
|
|
39
|
+
readonly root?: unknown;
|
|
40
|
+
readonly [key: string]: unknown;
|
|
41
|
+
}>;
|
|
42
|
+
readonly [key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Remove panels that the current shell cannot render, then prune empty grid
|
|
46
|
+
* leaves and branches. The input snapshot is never mutated.
|
|
47
|
+
*/
|
|
48
|
+
declare function pruneSerializedDockLayout<T extends SerializedDockLayoutLike>(layout: T, knownComponents: ReadonlySet<string>, allowedPanelIds?: ReadonlySet<string>): T | null;
|
|
28
49
|
type SideEdge = 'left' | 'right';
|
|
29
50
|
interface RectLike {
|
|
30
51
|
left: number;
|
|
@@ -62,4 +83,4 @@ declare function handleCrossInstanceDrop(event: CrossInstanceDropEvent, targetRe
|
|
|
62
83
|
titleFor?: (panelId: string) => string | undefined;
|
|
63
84
|
}): void;
|
|
64
85
|
|
|
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 };
|
|
86
|
+
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 };
|
package/dist/dock.js
CHANGED
|
@@ -27,6 +27,63 @@ function registerDockRegion(entry) {
|
|
|
27
27
|
function getDockRegions() {
|
|
28
28
|
return [...dockRegions.values()];
|
|
29
29
|
}
|
|
30
|
+
var visibleDockPanels = /* @__PURE__ */ new Map();
|
|
31
|
+
function trackDockPanelVisibility(panelId) {
|
|
32
|
+
visibleDockPanels.set(panelId, (visibleDockPanels.get(panelId) ?? 0) + 1);
|
|
33
|
+
let active = true;
|
|
34
|
+
return () => {
|
|
35
|
+
if (!active) return;
|
|
36
|
+
active = false;
|
|
37
|
+
const next = (visibleDockPanels.get(panelId) ?? 1) - 1;
|
|
38
|
+
if (next > 0) visibleDockPanels.set(panelId, next);
|
|
39
|
+
else visibleDockPanels.delete(panelId);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isDockPanelVisible(panelId) {
|
|
43
|
+
return visibleDockPanels.has(panelId);
|
|
44
|
+
}
|
|
45
|
+
function hasMountedPanelPlacement(panelIds, mountedPanelIds) {
|
|
46
|
+
return panelIds.some((id) => mountedPanelIds.has(id));
|
|
47
|
+
}
|
|
48
|
+
function pruneSerializedDockLayout(layout, knownComponents, allowedPanelIds) {
|
|
49
|
+
if (!layout.panels) return layout;
|
|
50
|
+
const removed = new Set(Object.entries(layout.panels).filter(([id, panel]) => !knownComponents.has(panel.contentComponent ?? id) || allowedPanelIds !== void 0 && !allowedPanelIds.has(id)).map(([id]) => id));
|
|
51
|
+
if (removed.size === 0) return layout;
|
|
52
|
+
const panels = Object.fromEntries(
|
|
53
|
+
Object.entries(layout.panels).filter(([id]) => !removed.has(id))
|
|
54
|
+
);
|
|
55
|
+
if (Object.keys(panels).length === 0) return null;
|
|
56
|
+
const pruneNode = (node) => {
|
|
57
|
+
if (!node || typeof node !== "object") return node;
|
|
58
|
+
const value = node;
|
|
59
|
+
if (value.type === "leaf") {
|
|
60
|
+
const data = value.data;
|
|
61
|
+
if (!Array.isArray(data?.views)) return node;
|
|
62
|
+
const views = data.views.filter((id) => !removed.has(id));
|
|
63
|
+
if (views.length === 0) return null;
|
|
64
|
+
return {
|
|
65
|
+
...value,
|
|
66
|
+
data: {
|
|
67
|
+
...data,
|
|
68
|
+
views,
|
|
69
|
+
activeView: views.includes(data.activeView ?? "") ? data.activeView : views[0]
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (value.type === "branch" && Array.isArray(value.data)) {
|
|
74
|
+
const children = value.data.map(pruneNode).filter((child) => child !== null);
|
|
75
|
+
return children.length === 0 ? null : { ...value, data: children };
|
|
76
|
+
}
|
|
77
|
+
return node;
|
|
78
|
+
};
|
|
79
|
+
const root = pruneNode(layout.grid?.root);
|
|
80
|
+
if (root === null) return null;
|
|
81
|
+
return {
|
|
82
|
+
...layout,
|
|
83
|
+
panels,
|
|
84
|
+
grid: layout.grid ? { ...layout.grid, root } : layout.grid
|
|
85
|
+
};
|
|
86
|
+
}
|
|
30
87
|
function isOnSideEdge(location) {
|
|
31
88
|
return location.type === "edge" && (location.position === "left" || location.position === "right");
|
|
32
89
|
}
|
|
@@ -84,10 +141,14 @@ export {
|
|
|
84
141
|
getDockRegions,
|
|
85
142
|
getDockviewApi,
|
|
86
143
|
handleCrossInstanceDrop,
|
|
144
|
+
hasMountedPanelPlacement,
|
|
145
|
+
isDockPanelVisible,
|
|
87
146
|
isDockRegion,
|
|
88
147
|
isOnSideEdge,
|
|
89
148
|
nearerSideEdge,
|
|
149
|
+
pruneSerializedDockLayout,
|
|
90
150
|
registerDockRegion,
|
|
91
151
|
registerDockviewApi,
|
|
92
|
-
resolveRegion
|
|
152
|
+
resolveRegion,
|
|
153
|
+
trackDockPanelVisibility
|
|
93
154
|
};
|
package/dist/panel.css
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.fx-panel {
|
|
2
|
+
height: 100%;
|
|
3
|
+
width: 100%;
|
|
4
|
+
min-width: 0;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
background: var(--color-background-base, #191919);
|
|
9
|
+
color: var(--color-text-primary, #e8eaed);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.fx-panel-content {
|
|
13
|
+
flex: 1 1 auto;
|
|
14
|
+
min-width: 0;
|
|
15
|
+
min-height: 0;
|
|
16
|
+
overflow: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.fx-panel-content[data-scroll="none"] {
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.fx-panel-content[data-padding="none"] {
|
|
24
|
+
padding: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.fx-panel-content[data-padding="sm"] {
|
|
28
|
+
padding: 8px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.fx-panel-content[data-padding="md"] {
|
|
32
|
+
padding: 12px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.fx-panel-content[data-tone="surface"] {
|
|
36
|
+
background: var(--color-background-elevated, #161619);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.fx-panel-content[data-tone="tool"] {
|
|
40
|
+
background: var(--color-background-base, #191919);
|
|
41
|
+
}
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactElement, HTMLAttributes } from 'react';
|
|
2
|
+
import { ReactElement, HTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
/** Stable FNV-1a color bucket for a shell slot name. */
|
|
5
5
|
declare function hashSlotHue(name: string): number;
|
|
@@ -20,6 +20,25 @@ interface ResizeHandleProps {
|
|
|
20
20
|
}
|
|
21
21
|
declare function ResizeHandle({ orientation, onDrag, onDragStart, onDragEnd, className, ariaLabel, title, }: ResizeHandleProps): react.JSX.Element;
|
|
22
22
|
|
|
23
|
+
type PanelContentPadding = 'none' | 'sm' | 'md';
|
|
24
|
+
type PanelContentScroll = 'none' | 'auto';
|
|
25
|
+
type PanelContentTone = 'default' | 'surface' | 'tool';
|
|
26
|
+
interface PanelContentPolicy {
|
|
27
|
+
readonly padding?: PanelContentPadding;
|
|
28
|
+
readonly scroll?: PanelContentScroll;
|
|
29
|
+
readonly tone?: PanelContentTone;
|
|
30
|
+
}
|
|
31
|
+
interface PanelSurfaceProps extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'content' | 'id'> {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly registered?: boolean;
|
|
34
|
+
readonly singleTab?: 'default' | 'full' | 'hideTitle';
|
|
35
|
+
readonly header?: ReactNode;
|
|
36
|
+
readonly content?: PanelContentPolicy;
|
|
37
|
+
readonly children?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
/** Product-neutral panel section and content presentation. */
|
|
40
|
+
declare function PanelSurface({ id, registered, singleTab, header, content, children, className, ...props }: PanelSurfaceProps): ReactElement;
|
|
41
|
+
|
|
23
42
|
type ShellSlotElement = 'aside' | 'div' | 'main' | 'section';
|
|
24
43
|
interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
|
|
25
44
|
as?: ShellSlotElement;
|
|
@@ -28,4 +47,4 @@ interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
|
|
|
28
47
|
/** Structural shell marker that preserves the caller's semantic element. */
|
|
29
48
|
declare function ShellSlot({ as, name, ...props }: ShellSlotProps): ReactElement;
|
|
30
49
|
|
|
31
|
-
export { ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, hashSlotHue, isSlotDebugEnabled, useLocalSize };
|
|
50
|
+
export { type PanelContentPadding, type PanelContentPolicy, type PanelContentScroll, type PanelContentTone, PanelSurface, type PanelSurfaceProps, ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, hashSlotHue, isSlotDebugEnabled, useLocalSize };
|
package/dist/react.js
CHANGED
|
@@ -255,6 +255,44 @@ function ResizeHandle({
|
|
|
255
255
|
);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// src/panel.tsx
|
|
259
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
260
|
+
function PanelSurface({
|
|
261
|
+
id,
|
|
262
|
+
registered = false,
|
|
263
|
+
singleTab,
|
|
264
|
+
header,
|
|
265
|
+
content,
|
|
266
|
+
children,
|
|
267
|
+
className,
|
|
268
|
+
...props
|
|
269
|
+
}) {
|
|
270
|
+
return /* @__PURE__ */ jsxs(
|
|
271
|
+
"section",
|
|
272
|
+
{
|
|
273
|
+
...props,
|
|
274
|
+
className: className ? `fx-panel ${className}` : "fx-panel",
|
|
275
|
+
"data-fx-panel-id": id,
|
|
276
|
+
"data-panel-registered": registered ? "true" : "false",
|
|
277
|
+
"data-dock-single-tab": singleTab,
|
|
278
|
+
"data-fx-slot": `DockPanel:${id}`,
|
|
279
|
+
children: [
|
|
280
|
+
header,
|
|
281
|
+
/* @__PURE__ */ jsx3(
|
|
282
|
+
"div",
|
|
283
|
+
{
|
|
284
|
+
className: "fx-panel-content",
|
|
285
|
+
"data-padding": content?.padding ?? "none",
|
|
286
|
+
"data-scroll": content?.scroll ?? "auto",
|
|
287
|
+
"data-tone": content?.tone ?? "default",
|
|
288
|
+
children
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
258
296
|
// src/react.tsx
|
|
259
297
|
function ShellSlot({
|
|
260
298
|
as = "div",
|
|
@@ -264,6 +302,7 @@ function ShellSlot({
|
|
|
264
302
|
return createElement(as, { ...props, "data-fx-slot": name });
|
|
265
303
|
}
|
|
266
304
|
export {
|
|
305
|
+
PanelSurface,
|
|
267
306
|
ResizeHandle,
|
|
268
307
|
ShellSlot,
|
|
269
308
|
SlotDebugOverlay,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Generic Dock, Panel, Window, and Slot composition primitives",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"sideEffects": ["./dist/resize.css"],
|
|
7
|
+
"sideEffects": ["./dist/resize.css", "./dist/panel.css"],
|
|
8
8
|
"files": ["dist"],
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
"types": "./dist/react.d.ts",
|
|
22
22
|
"import": "./dist/react.js"
|
|
23
23
|
},
|
|
24
|
-
"./resize.css": "./dist/resize.css"
|
|
24
|
+
"./resize.css": "./dist/resize.css",
|
|
25
|
+
"./panel.css": "./dist/panel.css"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"typecheck": "tsc --noEmit",
|
|
28
29
|
"test": "bun test test",
|
|
29
|
-
"build": "tsup src/index.ts src/dock.ts src/react.tsx --format esm --dts --outDir dist && bun run scripts/copy-resize-css.ts",
|
|
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",
|
|
30
31
|
"check": "bun run typecheck && bun run test && bun run build",
|
|
31
32
|
"release:preflight": "bun run scripts/release-preflight.ts"
|
|
32
33
|
},
|