@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 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.7.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
  },