@forgeax/app-shell 0.4.0 → 0.5.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
@@ -38,9 +38,11 @@ React entry point:
38
38
 
39
39
  ```tsx
40
40
  import {
41
+ ResizeHandle,
41
42
  ShellSlot,
42
43
  SlotDebugOverlay,
43
44
  isSlotDebugEnabled,
45
+ useLocalSize,
44
46
  } from '@forgeax/app-shell/react';
45
47
 
46
48
  export function ProductShell() {
@@ -52,6 +54,16 @@ export function ProductShell() {
52
54
  }
53
55
  ```
54
56
 
57
+ Generic persisted sizing and pointer-capture handles use the same public entry:
58
+
59
+ ```tsx
60
+ const [width, setWidth] = useLocalSize('product.sidebar.width', 280, 180, 640);
61
+
62
+ <aside style={{ width }}>
63
+ <ResizeHandle orientation="col" onDrag={(delta) => setWidth((value) => value + delta)} />
64
+ </aside>
65
+ ```
66
+
55
67
  ## Ownership boundary
56
68
 
57
69
  | Package | Owns |
package/dist/react.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as react from 'react';
1
2
  import { ReactElement, HTMLAttributes } from 'react';
2
3
 
3
4
  /** Stable FNV-1a color bucket for a shell slot name. */
@@ -7,6 +8,14 @@ declare function isSlotDebugEnabled(search?: string): boolean;
7
8
  /** Dev-time visualization derived only from live `data-fx-slot` markers. */
8
9
  declare function SlotDebugOverlay(): ReactElement;
9
10
 
11
+ declare function useLocalSize(key: string, initial: number, min: number, max: number): readonly [number, (next: number | ((previous: number) => number)) => void];
12
+ interface ResizeHandleProps {
13
+ orientation: 'col' | 'row';
14
+ onDrag: (delta: number) => void;
15
+ title?: string;
16
+ }
17
+ declare function ResizeHandle({ orientation, onDrag, title }: ResizeHandleProps): react.JSX.Element;
18
+
10
19
  type ShellSlotElement = 'aside' | 'div' | 'main' | 'section';
11
20
  interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
12
21
  as?: ShellSlotElement;
@@ -15,4 +24,4 @@ interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
15
24
  /** Structural shell marker that preserves the caller's semantic element. */
16
25
  declare function ShellSlot({ as, name, ...props }: ShellSlotProps): ReactElement;
17
26
 
18
- export { ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, hashSlotHue, isSlotDebugEnabled };
27
+ export { ResizeHandle, type ResizeHandleProps, ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, hashSlotHue, isSlotDebugEnabled, useLocalSize };
package/dist/react.js CHANGED
@@ -162,6 +162,83 @@ function SlotDebugOverlay() {
162
162
  );
163
163
  }
164
164
 
165
+ // src/resize.tsx
166
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
167
+ import { jsx as jsx2 } from "react/jsx-runtime";
168
+ function useLocalSize(key, initial, min, max) {
169
+ const clamp = (value2) => Math.min(max, Math.max(min, value2));
170
+ const [value, setValueRaw] = useState2(() => {
171
+ if (typeof window === "undefined") return initial;
172
+ try {
173
+ const raw = window.localStorage.getItem(key);
174
+ if (!raw) return initial;
175
+ const persisted = Number(raw);
176
+ if (!Number.isFinite(persisted)) return initial;
177
+ return clamp(persisted);
178
+ } catch {
179
+ return initial;
180
+ }
181
+ });
182
+ useEffect2(() => {
183
+ try {
184
+ window.localStorage.setItem(key, String(value));
185
+ } catch {
186
+ }
187
+ }, [key, value]);
188
+ const setValue = (next) => {
189
+ setValueRaw((previous) => clamp(
190
+ typeof next === "function" ? next(previous) : next
191
+ ));
192
+ };
193
+ return [value, setValue];
194
+ }
195
+ function ResizeHandle({ orientation, onDrag, title }) {
196
+ const startRef = useRef(null);
197
+ const resetGlobalDragStyles = () => {
198
+ document.body.style.cursor = "";
199
+ document.body.style.userSelect = "";
200
+ };
201
+ useEffect2(() => () => {
202
+ if (startRef.current) resetGlobalDragStyles();
203
+ }, []);
204
+ const onPointerDown = (event) => {
205
+ event.preventDefault();
206
+ event.currentTarget.setPointerCapture(event.pointerId);
207
+ startRef.current = { x: event.clientX, y: event.clientY };
208
+ document.body.style.cursor = orientation === "col" ? "col-resize" : "row-resize";
209
+ document.body.style.userSelect = "none";
210
+ };
211
+ const onPointerMove = (event) => {
212
+ if (!startRef.current) return;
213
+ const deltaX = event.clientX - startRef.current.x;
214
+ const deltaY = event.clientY - startRef.current.y;
215
+ startRef.current = { x: event.clientX, y: event.clientY };
216
+ onDrag(orientation === "col" ? deltaX : deltaY);
217
+ };
218
+ const finish = (event) => {
219
+ if (!startRef.current) return;
220
+ startRef.current = null;
221
+ try {
222
+ event.currentTarget.releasePointerCapture(event.pointerId);
223
+ } catch {
224
+ }
225
+ resetGlobalDragStyles();
226
+ };
227
+ return /* @__PURE__ */ jsx2(
228
+ "div",
229
+ {
230
+ className: `resize-handle resize-handle-${orientation}`,
231
+ onPointerDown,
232
+ onPointerMove,
233
+ onPointerUp: finish,
234
+ onPointerCancel: finish,
235
+ title,
236
+ role: "separator",
237
+ "aria-orientation": orientation === "col" ? "vertical" : "horizontal"
238
+ }
239
+ );
240
+ }
241
+
165
242
  // src/react.tsx
166
243
  function ShellSlot({
167
244
  as = "div",
@@ -171,8 +248,10 @@ function ShellSlot({
171
248
  return createElement(as, { ...props, "data-fx-slot": name });
172
249
  }
173
250
  export {
251
+ ResizeHandle,
174
252
  ShellSlot,
175
253
  SlotDebugOverlay,
176
254
  hashSlotHue,
177
- isSlotDebugEnabled
255
+ isSlotDebugEnabled,
256
+ useLocalSize
178
257
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/app-shell",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Generic Dock, Panel, Window, and Slot composition primitives",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",