@forgeax/app-shell 0.39.1 → 0.41.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/react.d.ts CHANGED
@@ -10,6 +10,27 @@ declare function isSlotDebugEnabled(search?: string): boolean;
10
10
  /** Dev-time visualization derived only from live `data-fx-slot` markers. */
11
11
  declare function SlotDebugOverlay(): ReactElement;
12
12
 
13
+ interface PersistentSizeStorage {
14
+ getItem(key: string): string | null;
15
+ setItem(key: string, value: string): void;
16
+ }
17
+ interface PersistentSizeStoreOptions {
18
+ storageKey: string;
19
+ defaultSize: number;
20
+ minSize: number;
21
+ maxSize: number;
22
+ storage?: PersistentSizeStorage | null;
23
+ }
24
+ type PersistentSizeUpdate = number | ((previous: number) => number);
25
+ interface PersistentSizeStore {
26
+ getSnapshot(): number;
27
+ subscribe(listener: () => void): () => void;
28
+ setSize(next: PersistentSizeUpdate): void;
29
+ reload(): void;
30
+ }
31
+ /** Product-neutral persistent numeric state for resizable shell regions. */
32
+ declare function createPersistentSizeStore(options: PersistentSizeStoreOptions): PersistentSizeStore;
33
+ declare function usePersistentSizeStore(store: PersistentSizeStore): number;
13
34
  declare function useLocalSize(key: string, initial: number, min: number, max: number): readonly [number, (next: number | ((previous: number) => number)) => void];
14
35
  interface ResizeHandleProps {
15
36
  orientation: 'col' | 'row';
@@ -20,7 +41,25 @@ interface ResizeHandleProps {
20
41
  ariaLabel?: string;
21
42
  title?: string;
22
43
  }
44
+ type AnchoredResizeDirection = 'subtract' | 'add';
45
+ interface AnchoredResizeSession {
46
+ readonly startSize: number;
47
+ readonly direction: AnchoredResizeDirection;
48
+ totalDelta: number;
49
+ }
50
+ /** Capture one unclamped drag origin so overshoot stays pinned until the pointer returns. */
51
+ declare function beginAnchoredResize(startSize: number, direction?: AnchoredResizeDirection): AnchoredResizeSession;
52
+ /** Project incremental pointer travel from the stable drag origin. */
53
+ declare function applyAnchoredResizeDelta(session: AnchoredResizeSession, delta: number): number;
23
54
  declare function ResizeHandle({ orientation, onDrag, onDragStart, onDragEnd, className, ariaLabel, title, }: ResizeHandleProps): react.JSX.Element;
55
+ interface AnchoredResizeHandleProps extends Omit<ResizeHandleProps, 'onDrag' | 'onDragStart' | 'onDragEnd'> {
56
+ readSize: () => number;
57
+ writeSize: (next: number) => void;
58
+ direction?: AnchoredResizeDirection;
59
+ resizingBodyClassName?: string;
60
+ }
61
+ /** Product-neutral anchored resize session over the public pointer carrier. */
62
+ declare function AnchoredResizeHandle({ readSize, writeSize, direction, resizingBodyClassName, ...handleProps }: AnchoredResizeHandleProps): react.JSX.Element;
24
63
 
25
64
  type PanelContentPadding = 'none' | 'sm' | 'md';
26
65
  type PanelContentScroll = 'none' | 'auto';
@@ -163,4 +202,4 @@ interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
163
202
  /** Structural shell marker that preserves the caller's semantic element. */
164
203
  declare function ShellSlot({ as, name, ...props }: ShellSlotProps): ReactElement;
165
204
 
166
- export { DetachedPanelBoundary, type DetachedPanelBoundaryProps, DetachedSurfaceFrame, type DetachedSurfaceFrameProps, DetachedSurfaceStatus, type DetachedSurfaceStatusProps, type DetachedSurfaceStatusTone, type DockLayoutControlBinding, type DockLayoutControlBindingOptions, DockLayoutMenu, type DockLayoutMenuPanel, type DockLayoutMenuProps, FloatingMenu, type FloatingMenuAnchor, type FloatingMenuPoint, type FloatingMenuProps, type PanelContentPadding, type PanelContentPolicy, type PanelContentScroll, type PanelContentTone, PanelEmptyState, type PanelEmptyStateProps, PanelSurface, type PanelSurfaceProps, ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, SurfacePlaceholder, type SurfacePlaceholderProps, SurfaceRegion, type SurfaceRegionProps, hashSlotHue, isSlotDebugEnabled, useDockLayoutControlBinding, useLocalSize };
205
+ export { type AnchoredResizeDirection, AnchoredResizeHandle, type AnchoredResizeHandleProps, type AnchoredResizeSession, DetachedPanelBoundary, type DetachedPanelBoundaryProps, DetachedSurfaceFrame, type DetachedSurfaceFrameProps, DetachedSurfaceStatus, type DetachedSurfaceStatusProps, type DetachedSurfaceStatusTone, type DockLayoutControlBinding, type DockLayoutControlBindingOptions, DockLayoutMenu, type DockLayoutMenuPanel, type DockLayoutMenuProps, FloatingMenu, type FloatingMenuAnchor, type FloatingMenuPoint, type FloatingMenuProps, type PanelContentPadding, type PanelContentPolicy, type PanelContentScroll, type PanelContentTone, PanelEmptyState, type PanelEmptyStateProps, PanelSurface, type PanelSurfaceProps, type PersistentSizeStorage, type PersistentSizeStore, type PersistentSizeStoreOptions, type PersistentSizeUpdate, ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, SurfacePlaceholder, type SurfacePlaceholderProps, SurfaceRegion, type SurfaceRegionProps, applyAnchoredResizeDelta, beginAnchoredResize, createPersistentSizeStore, hashSlotHue, isSlotDebugEnabled, useDockLayoutControlBinding, useLocalSize, usePersistentSizeStore };
package/dist/react.js CHANGED
@@ -595,8 +595,74 @@ function SlotDebugOverlay() {
595
595
  }
596
596
 
597
597
  // src/resize.tsx
598
- import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
598
+ import { useEffect as useEffect2, useRef, useState as useState2, useSyncExternalStore } from "react";
599
599
  import { jsx as jsx2 } from "react/jsx-runtime";
600
+ function browserPersistentSizeStorage() {
601
+ if (typeof window === "undefined") return null;
602
+ try {
603
+ return window.localStorage;
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ function createPersistentSizeStore(options) {
609
+ const minSize = Math.min(options.minSize, options.maxSize);
610
+ const maxSize = Math.max(options.minSize, options.maxSize);
611
+ const normalize = (value2, fallback) => {
612
+ if (!Number.isFinite(value2)) return fallback;
613
+ return Math.min(maxSize, Math.max(minSize, Math.round(value2)));
614
+ };
615
+ const defaultSize = normalize(options.defaultSize, minSize);
616
+ const storage = options.storage === void 0 ? browserPersistentSizeStorage() : options.storage;
617
+ const read = () => {
618
+ try {
619
+ const raw = storage?.getItem(options.storageKey);
620
+ if (!raw) return defaultSize;
621
+ const parsed = Number.parseInt(raw, 10);
622
+ return normalize(parsed, defaultSize);
623
+ } catch {
624
+ return defaultSize;
625
+ }
626
+ };
627
+ let value = read();
628
+ const listeners = /* @__PURE__ */ new Set();
629
+ const commit = (next) => {
630
+ if (Object.is(value, next)) return;
631
+ value = next;
632
+ for (const listener of [...listeners]) {
633
+ try {
634
+ listener();
635
+ } catch {
636
+ }
637
+ }
638
+ };
639
+ return {
640
+ getSnapshot: () => value,
641
+ subscribe: (listener) => {
642
+ listeners.add(listener);
643
+ let active = true;
644
+ return () => {
645
+ if (!active) return;
646
+ active = false;
647
+ listeners.delete(listener);
648
+ };
649
+ },
650
+ setSize: (next) => {
651
+ const candidate = typeof next === "function" ? next(value) : next;
652
+ const normalized = normalize(candidate, value);
653
+ if (Object.is(value, normalized)) return;
654
+ try {
655
+ storage?.setItem(options.storageKey, String(normalized));
656
+ } catch {
657
+ }
658
+ commit(normalized);
659
+ },
660
+ reload: () => commit(read())
661
+ };
662
+ }
663
+ function usePersistentSizeStore(store) {
664
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
665
+ }
600
666
  function useLocalSize(key, initial, min, max) {
601
667
  const clamp = (value2) => Math.min(max, Math.max(min, value2));
602
668
  const [value, setValueRaw] = useState2(() => {
@@ -624,6 +690,46 @@ function useLocalSize(key, initial, min, max) {
624
690
  };
625
691
  return [value, setValue];
626
692
  }
693
+ var bodyClassLeases = /* @__PURE__ */ new WeakMap();
694
+ function acquireBodyClassLease(body, tokens) {
695
+ const uniqueTokens = [...new Set(tokens)];
696
+ let records = bodyClassLeases.get(body);
697
+ if (!records) {
698
+ records = /* @__PURE__ */ new Map();
699
+ bodyClassLeases.set(body, records);
700
+ }
701
+ for (const token of uniqueTokens) {
702
+ const existing = records.get(token);
703
+ if (existing) {
704
+ existing.count += 1;
705
+ continue;
706
+ }
707
+ const owned = !body.classList.contains(token);
708
+ if (owned) body.classList.add(token);
709
+ records.set(token, { count: 1, owned });
710
+ }
711
+ let active = true;
712
+ return () => {
713
+ if (!active) return;
714
+ active = false;
715
+ for (const token of uniqueTokens) {
716
+ const record = records?.get(token);
717
+ if (!record) continue;
718
+ record.count -= 1;
719
+ if (record.count > 0) continue;
720
+ records?.delete(token);
721
+ if (record.owned) body.classList.remove(token);
722
+ }
723
+ if (records?.size === 0) bodyClassLeases.delete(body);
724
+ };
725
+ }
726
+ function beginAnchoredResize(startSize, direction = "subtract") {
727
+ return { startSize, direction, totalDelta: 0 };
728
+ }
729
+ function applyAnchoredResizeDelta(session, delta) {
730
+ session.totalDelta += delta;
731
+ return session.startSize + (session.direction === "subtract" ? -session.totalDelta : session.totalDelta);
732
+ }
627
733
  function ResizeHandle({
628
734
  orientation,
629
735
  onDrag,
@@ -644,24 +750,47 @@ function ResizeHandle({
644
750
  if (!startRef.current) return;
645
751
  startRef.current = null;
646
752
  resetGlobalDragStyles();
647
- onDragEndRef.current?.();
753
+ try {
754
+ onDragEndRef.current?.();
755
+ } catch {
756
+ }
648
757
  };
649
758
  useEffect2(() => finishDrag, []);
650
759
  const onPointerDown = (event) => {
651
760
  if (startRef.current) return;
652
761
  event.preventDefault();
653
- event.currentTarget.setPointerCapture(event.pointerId);
762
+ try {
763
+ event.currentTarget.setPointerCapture(event.pointerId);
764
+ } catch {
765
+ return;
766
+ }
654
767
  startRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
655
768
  document.body.style.cursor = orientation === "col" ? "col-resize" : "row-resize";
656
769
  document.body.style.userSelect = "none";
657
- onDragStart?.();
770
+ try {
771
+ onDragStart?.();
772
+ } catch {
773
+ try {
774
+ event.currentTarget.releasePointerCapture(event.pointerId);
775
+ } catch {
776
+ }
777
+ finishDrag();
778
+ }
658
779
  };
659
780
  const onPointerMove = (event) => {
660
781
  if (!startRef.current || startRef.current.pointerId !== event.pointerId) return;
661
782
  const deltaX = event.clientX - startRef.current.x;
662
783
  const deltaY = event.clientY - startRef.current.y;
663
784
  startRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
664
- onDrag(orientation === "col" ? deltaX : deltaY);
785
+ try {
786
+ onDrag(orientation === "col" ? deltaX : deltaY);
787
+ } catch {
788
+ try {
789
+ event.currentTarget.releasePointerCapture(event.pointerId);
790
+ } catch {
791
+ }
792
+ finishDrag();
793
+ }
665
794
  };
666
795
  const finish = (event) => {
667
796
  if (!startRef.current || startRef.current.pointerId !== event.pointerId) return;
@@ -686,6 +815,58 @@ function ResizeHandle({
686
815
  }
687
816
  );
688
817
  }
818
+ function AnchoredResizeHandle({
819
+ readSize,
820
+ writeSize,
821
+ direction = "subtract",
822
+ resizingBodyClassName,
823
+ ...handleProps
824
+ }) {
825
+ const readSizeRef = useRef(readSize);
826
+ const writeSizeRef = useRef(writeSize);
827
+ const directionRef = useRef(direction);
828
+ const bodyClassNameRef = useRef(resizingBodyClassName);
829
+ readSizeRef.current = readSize;
830
+ writeSizeRef.current = writeSize;
831
+ directionRef.current = direction;
832
+ bodyClassNameRef.current = resizingBodyClassName;
833
+ const sessionRef = useRef(null);
834
+ const finishSession = () => {
835
+ const active = sessionRef.current;
836
+ if (!active) return;
837
+ sessionRef.current = null;
838
+ try {
839
+ active.releaseBodyClass();
840
+ } catch {
841
+ }
842
+ };
843
+ useEffect2(() => finishSession, []);
844
+ const startSession = () => {
845
+ finishSession();
846
+ const bodyClassTokens = bodyClassNameRef.current?.trim().split(/\s+/).filter(Boolean) ?? [];
847
+ const resize = beginAnchoredResize(readSizeRef.current(), directionRef.current);
848
+ const releaseBodyClass = typeof document !== "undefined" && bodyClassTokens.length > 0 ? acquireBodyClassLease(document.body, bodyClassTokens) : () => {
849
+ };
850
+ sessionRef.current = {
851
+ resize,
852
+ releaseBodyClass
853
+ };
854
+ };
855
+ const applyDelta = (delta) => {
856
+ const active = sessionRef.current;
857
+ if (!active) return;
858
+ writeSizeRef.current(applyAnchoredResizeDelta(active.resize, delta));
859
+ };
860
+ return /* @__PURE__ */ jsx2(
861
+ ResizeHandle,
862
+ {
863
+ ...handleProps,
864
+ onDrag: applyDelta,
865
+ onDragStart: startSession,
866
+ onDragEnd: finishSession
867
+ }
868
+ );
869
+ }
689
870
 
690
871
  // src/panel.tsx
691
872
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
@@ -829,7 +1010,7 @@ var SurfaceRegion = forwardRef(
829
1010
  );
830
1011
 
831
1012
  // src/dock-layout-control.ts
832
- import { useEffect as useEffect3, useMemo, useSyncExternalStore } from "react";
1013
+ import { useEffect as useEffect3, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
833
1014
  function useDockLayoutControlBinding({
834
1015
  state,
835
1016
  onToggle,
@@ -837,7 +1018,7 @@ function useDockLayoutControlBinding({
837
1018
  closePanel,
838
1019
  reopenPanel
839
1020
  }) {
840
- const snapshot = useSyncExternalStore(
1021
+ const snapshot = useSyncExternalStore2(
841
1022
  state.subscribe,
842
1023
  state.getSnapshot,
843
1024
  state.getSnapshot
@@ -1037,6 +1218,7 @@ function ShellSlot({
1037
1218
  return createElement(as, { ...props, "data-fx-slot": name });
1038
1219
  }
1039
1220
  export {
1221
+ AnchoredResizeHandle,
1040
1222
  DetachedPanelBoundary,
1041
1223
  DetachedSurfaceFrame,
1042
1224
  DetachedSurfaceStatus,
@@ -1049,10 +1231,14 @@ export {
1049
1231
  SlotDebugOverlay,
1050
1232
  SurfacePlaceholder,
1051
1233
  SurfaceRegion,
1234
+ applyAnchoredResizeDelta,
1235
+ beginAnchoredResize,
1236
+ createPersistentSizeStore,
1052
1237
  hashSlotHue,
1053
1238
  isSlotDebugEnabled,
1054
1239
  useDockLayoutControlBinding,
1055
- useLocalSize
1240
+ useLocalSize,
1241
+ usePersistentSizeStore
1056
1242
  };
1057
1243
  /*! Bundled license information:
1058
1244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/app-shell",
3
- "version": "0.39.1",
3
+ "version": "0.41.0",
4
4
  "description": "Generic Dock, Panel, Window, and Slot composition primitives",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",