@forgeax/app-shell 0.40.0 → 0.42.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,6 +41,18 @@ interface ResizeHandleProps {
20
41
  ariaLabel?: string;
21
42
  title?: string;
22
43
  }
44
+ type PointerDragFinishReason = 'pointerup' | 'pointercancel' | 'lostpointercapture' | 'dispose' | 'error';
45
+ interface PointerDragSessionOptions<Session> {
46
+ readonly element: HTMLElement;
47
+ readonly begin: (event: globalThis.PointerEvent) => Session | null;
48
+ readonly move: (session: Session, event: globalThis.PointerEvent) => void;
49
+ readonly end?: (session: Session, reason: PointerDragFinishReason) => void;
50
+ readonly preventDefault?: boolean;
51
+ readonly stopPropagation?: boolean;
52
+ readonly draggingClassName?: string;
53
+ }
54
+ /** Own one imperative pointer-capture drag session and its balanced teardown. */
55
+ declare function installPointerDragSession<Session>(options: PointerDragSessionOptions<Session>): () => void;
23
56
  type AnchoredResizeDirection = 'subtract' | 'add';
24
57
  interface AnchoredResizeSession {
25
58
  readonly startSize: number;
@@ -181,4 +214,4 @@ interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
181
214
  /** Structural shell marker that preserves the caller's semantic element. */
182
215
  declare function ShellSlot({ as, name, ...props }: ShellSlotProps): ReactElement;
183
216
 
184
- 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, ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, SurfacePlaceholder, type SurfacePlaceholderProps, SurfaceRegion, type SurfaceRegionProps, applyAnchoredResizeDelta, beginAnchoredResize, hashSlotHue, isSlotDebugEnabled, useDockLayoutControlBinding, useLocalSize };
217
+ 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, type PointerDragFinishReason, type PointerDragSessionOptions, ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, SurfacePlaceholder, type SurfacePlaceholderProps, SurfaceRegion, type SurfaceRegionProps, applyAnchoredResizeDelta, beginAnchoredResize, createPersistentSizeStore, hashSlotHue, installPointerDragSession, 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,13 +690,13 @@ function useLocalSize(key, initial, min, max) {
624
690
  };
625
691
  return [value, setValue];
626
692
  }
627
- var bodyClassLeases = /* @__PURE__ */ new WeakMap();
628
- function acquireBodyClassLease(body, tokens) {
693
+ var elementClassLeases = /* @__PURE__ */ new WeakMap();
694
+ function acquireElementClassLease(element, tokens) {
629
695
  const uniqueTokens = [...new Set(tokens)];
630
- let records = bodyClassLeases.get(body);
696
+ let records = elementClassLeases.get(element);
631
697
  if (!records) {
632
698
  records = /* @__PURE__ */ new Map();
633
- bodyClassLeases.set(body, records);
699
+ elementClassLeases.set(element, records);
634
700
  }
635
701
  for (const token of uniqueTokens) {
636
702
  const existing = records.get(token);
@@ -638,8 +704,8 @@ function acquireBodyClassLease(body, tokens) {
638
704
  existing.count += 1;
639
705
  continue;
640
706
  }
641
- const owned = !body.classList.contains(token);
642
- if (owned) body.classList.add(token);
707
+ const owned = !element.classList.contains(token);
708
+ if (owned) element.classList.add(token);
643
709
  records.set(token, { count: 1, owned });
644
710
  }
645
711
  let active = true;
@@ -652,9 +718,97 @@ function acquireBodyClassLease(body, tokens) {
652
718
  record.count -= 1;
653
719
  if (record.count > 0) continue;
654
720
  records?.delete(token);
655
- if (record.owned) body.classList.remove(token);
721
+ if (record.owned) element.classList.remove(token);
656
722
  }
657
- if (records?.size === 0) bodyClassLeases.delete(body);
723
+ if (records?.size === 0) elementClassLeases.delete(element);
724
+ };
725
+ }
726
+ function installPointerDragSession(options) {
727
+ const { element } = options;
728
+ let disposed = false;
729
+ let starting = false;
730
+ let active = null;
731
+ const removeSessionListeners = () => {
732
+ element.removeEventListener("pointermove", onPointerMove);
733
+ element.removeEventListener("pointerup", onPointerFinish);
734
+ element.removeEventListener("pointercancel", onPointerFinish);
735
+ element.removeEventListener("lostpointercapture", onPointerFinish);
736
+ };
737
+ const finish = (reason) => {
738
+ const current = active;
739
+ if (!current) return;
740
+ active = null;
741
+ removeSessionListeners();
742
+ try {
743
+ element.releasePointerCapture(current.pointerId);
744
+ } catch {
745
+ }
746
+ try {
747
+ current.releaseClass();
748
+ } catch {
749
+ }
750
+ try {
751
+ options.end?.(current.session, reason);
752
+ } catch {
753
+ }
754
+ };
755
+ function onPointerMove(event) {
756
+ const current = active;
757
+ if (!current || current.pointerId !== event.pointerId) return;
758
+ try {
759
+ options.move(current.session, event);
760
+ } catch {
761
+ finish("error");
762
+ }
763
+ }
764
+ function onPointerFinish(event) {
765
+ if (!active || active.pointerId !== event.pointerId) return;
766
+ const reason = event.type === "pointercancel" ? "pointercancel" : event.type === "lostpointercapture" ? "lostpointercapture" : "pointerup";
767
+ finish(reason);
768
+ }
769
+ const onPointerDown = (event) => {
770
+ if (disposed || active || starting) return;
771
+ starting = true;
772
+ let session;
773
+ try {
774
+ session = options.begin(event);
775
+ } catch {
776
+ starting = false;
777
+ return;
778
+ }
779
+ starting = false;
780
+ if (session === null) return;
781
+ if (disposed) {
782
+ try {
783
+ options.end?.(session, "dispose");
784
+ } catch {
785
+ }
786
+ return;
787
+ }
788
+ if (options.preventDefault) event.preventDefault();
789
+ if (options.stopPropagation) event.stopPropagation();
790
+ const classTokens = options.draggingClassName?.trim().split(/\s+/).filter(Boolean) ?? [];
791
+ active = {
792
+ pointerId: event.pointerId,
793
+ session,
794
+ releaseClass: acquireElementClassLease(element, classTokens)
795
+ };
796
+ element.addEventListener("pointermove", onPointerMove);
797
+ element.addEventListener("pointerup", onPointerFinish);
798
+ element.addEventListener("pointercancel", onPointerFinish);
799
+ element.addEventListener("lostpointercapture", onPointerFinish);
800
+ try {
801
+ element.setPointerCapture(event.pointerId);
802
+ } catch {
803
+ finish("error");
804
+ }
805
+ };
806
+ element.addEventListener("pointerdown", onPointerDown);
807
+ return () => {
808
+ if (disposed) return;
809
+ disposed = true;
810
+ element.removeEventListener("pointerdown", onPointerDown);
811
+ finish("dispose");
658
812
  };
659
813
  }
660
814
  function beginAnchoredResize(startSize, direction = "subtract") {
@@ -779,7 +933,7 @@ function AnchoredResizeHandle({
779
933
  finishSession();
780
934
  const bodyClassTokens = bodyClassNameRef.current?.trim().split(/\s+/).filter(Boolean) ?? [];
781
935
  const resize = beginAnchoredResize(readSizeRef.current(), directionRef.current);
782
- const releaseBodyClass = typeof document !== "undefined" && bodyClassTokens.length > 0 ? acquireBodyClassLease(document.body, bodyClassTokens) : () => {
936
+ const releaseBodyClass = typeof document !== "undefined" && bodyClassTokens.length > 0 ? acquireElementClassLease(document.body, bodyClassTokens) : () => {
783
937
  };
784
938
  sessionRef.current = {
785
939
  resize,
@@ -944,7 +1098,7 @@ var SurfaceRegion = forwardRef(
944
1098
  );
945
1099
 
946
1100
  // src/dock-layout-control.ts
947
- import { useEffect as useEffect3, useMemo, useSyncExternalStore } from "react";
1101
+ import { useEffect as useEffect3, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
948
1102
  function useDockLayoutControlBinding({
949
1103
  state,
950
1104
  onToggle,
@@ -952,7 +1106,7 @@ function useDockLayoutControlBinding({
952
1106
  closePanel,
953
1107
  reopenPanel
954
1108
  }) {
955
- const snapshot = useSyncExternalStore(
1109
+ const snapshot = useSyncExternalStore2(
956
1110
  state.subscribe,
957
1111
  state.getSnapshot,
958
1112
  state.getSnapshot
@@ -1167,10 +1321,13 @@ export {
1167
1321
  SurfaceRegion,
1168
1322
  applyAnchoredResizeDelta,
1169
1323
  beginAnchoredResize,
1324
+ createPersistentSizeStore,
1170
1325
  hashSlotHue,
1326
+ installPointerDragSession,
1171
1327
  isSlotDebugEnabled,
1172
1328
  useDockLayoutControlBinding,
1173
- useLocalSize
1329
+ useLocalSize,
1330
+ usePersistentSizeStore
1174
1331
  };
1175
1332
  /*! Bundled license information:
1176
1333
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/app-shell",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Generic Dock, Panel, Window, and Slot composition primitives",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",