@deadragdoll/reactnu 0.1.75 → 0.1.77

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/index.cjs CHANGED
@@ -3124,28 +3124,94 @@ function getNumericStyleValue(value) {
3124
3124
  }
3125
3125
  return void 0;
3126
3126
  }
3127
+ function getWindowStorageKey(windowStoreKey) {
3128
+ const normalizedKey = windowStoreKey?.trim();
3129
+ return normalizedKey ? `reactnu.window.${normalizedKey}` : null;
3130
+ }
3131
+ function readStoredWindowSnapshot(windowStoreKey) {
3132
+ const storageKey = getWindowStorageKey(windowStoreKey);
3133
+ if (!storageKey || typeof window === "undefined") {
3134
+ return void 0;
3135
+ }
3136
+ try {
3137
+ const rawValue = window.localStorage.getItem(storageKey);
3138
+ if (!rawValue) {
3139
+ return void 0;
3140
+ }
3141
+ const parsedValue = JSON.parse(rawValue);
3142
+ if (!parsedValue || typeof parsedValue !== "object") {
3143
+ return void 0;
3144
+ }
3145
+ const storedValue = parsedValue;
3146
+ const snapshot = {};
3147
+ for (const property of ["height", "width"]) {
3148
+ const value = storedValue[property];
3149
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
3150
+ snapshot[property] = value;
3151
+ }
3152
+ }
3153
+ for (const property of ["left", "top"]) {
3154
+ const value = storedValue[property];
3155
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
3156
+ snapshot[property] = value;
3157
+ }
3158
+ }
3159
+ for (const property of ["maximized", "minimized"]) {
3160
+ const value = storedValue[property];
3161
+ if (typeof value === "boolean") {
3162
+ snapshot[property] = value;
3163
+ }
3164
+ }
3165
+ return Object.keys(snapshot).length > 0 ? snapshot : void 0;
3166
+ } catch {
3167
+ return void 0;
3168
+ }
3169
+ }
3170
+ function writeStoredWindowSnapshot(windowStoreKey, snapshot) {
3171
+ const storageKey = getWindowStorageKey(windowStoreKey);
3172
+ if (!storageKey || typeof window === "undefined") {
3173
+ return;
3174
+ }
3175
+ try {
3176
+ window.localStorage.setItem(storageKey, JSON.stringify(snapshot));
3177
+ } catch {
3178
+ }
3179
+ }
3180
+ var WINDOW_STORE_WRITE_DEBOUNCE_MS = 250;
3127
3181
  function resolveSavedWindowStyle(definition, mode, ownerCenteredStyle, index) {
3128
- const snapshot = definition.onOpen?.();
3129
- const savedStyle = snapshot ? {
3130
- height: snapshot.height,
3131
- left: snapshot.left,
3132
- top: snapshot.top,
3133
- transform: snapshot.left !== void 0 || snapshot.top !== void 0 ? "none" : void 0,
3134
- width: snapshot.width
3135
- } : void 0;
3182
+ const snapshot = definition.onOpen?.() ?? readStoredWindowSnapshot(definition.windowStoreKey);
3183
+ const savedStyle = {};
3184
+ if (snapshot) {
3185
+ if (snapshot.height !== void 0) {
3186
+ savedStyle.height = snapshot.height;
3187
+ }
3188
+ if (snapshot.left !== void 0) {
3189
+ savedStyle.left = snapshot.left;
3190
+ }
3191
+ if (snapshot.top !== void 0) {
3192
+ savedStyle.top = snapshot.top;
3193
+ }
3194
+ if (snapshot.width !== void 0) {
3195
+ savedStyle.width = snapshot.width;
3196
+ }
3197
+ if (snapshot.left !== void 0 || snapshot.top !== void 0) {
3198
+ savedStyle.transform = "none";
3199
+ }
3200
+ }
3136
3201
  return {
3137
3202
  maximized: snapshot?.maximized === true,
3138
3203
  minimized: snapshot?.minimized === true,
3139
- restoreStyle: snapshot?.maximized === true && savedStyle ? {
3204
+ restoreStyle: snapshot?.maximized ? {
3140
3205
  ...getDefaultWindowStyle(mode, index),
3141
3206
  ...ownerCenteredStyle,
3207
+ ...definition.style,
3142
3208
  ...savedStyle
3143
3209
  } : void 0,
3144
3210
  style: {
3145
3211
  ...getDefaultWindowStyle(mode, index),
3146
3212
  ...ownerCenteredStyle,
3147
- ...savedStyle,
3148
- ...definition.style
3213
+ ...definition.style,
3214
+ ...savedStyle
3149
3215
  }
3150
3216
  };
3151
3217
  }
@@ -3250,6 +3316,7 @@ function NuWindowProvider({
3250
3316
  const creationOrderRef = (0, import_react19.useRef)(0);
3251
3317
  const idRef = (0, import_react19.useRef)(0);
3252
3318
  const windowBoundsByIdRef = (0, import_react19.useRef)({});
3319
+ const pendingWindowStoreWritesRef = (0, import_react19.useRef)(/* @__PURE__ */ new Map());
3253
3320
  const [windows, setWindows] = (0, import_react19.useState)([]);
3254
3321
  const [windowBoundsById, setWindowBoundsById] = (0, import_react19.useState)({});
3255
3322
  const appHostMenuContext = (0, import_react19.useContext)(AppHostMenuContext);
@@ -3671,6 +3738,53 @@ function NuWindowProvider({
3671
3738
  (0, import_react19.useEffect)(() => {
3672
3739
  onAppModalChange?.(hasAppModal);
3673
3740
  }, [hasAppModal, onAppModalChange]);
3741
+ (0, import_react19.useEffect)(() => {
3742
+ const pendingWindowStoreWrites = pendingWindowStoreWritesRef.current;
3743
+ const windowStoreKeyById = new Map(
3744
+ windows.map((windowEntry) => [windowEntry.id, windowEntry.windowStoreKey])
3745
+ );
3746
+ for (const [id, pending] of pendingWindowStoreWrites) {
3747
+ if (windowStoreKeyById.get(id) === pending.windowStoreKey) {
3748
+ continue;
3749
+ }
3750
+ clearTimeout(pending.timeoutId);
3751
+ pendingWindowStoreWrites.delete(id);
3752
+ writeStoredWindowSnapshot(pending.windowStoreKey, pending.snapshot);
3753
+ }
3754
+ for (const windowEntry of windows) {
3755
+ if (!windowEntry.windowStoreKey) {
3756
+ continue;
3757
+ }
3758
+ const existing = pendingWindowStoreWrites.get(windowEntry.id);
3759
+ if (existing) {
3760
+ clearTimeout(existing.timeoutId);
3761
+ }
3762
+ const windowStoreKey = windowEntry.windowStoreKey;
3763
+ const snapshot = resolveManagedWindowSnapshot(
3764
+ windowEntry,
3765
+ windowBoundsById
3766
+ );
3767
+ const timeoutId = setTimeout(() => {
3768
+ writeStoredWindowSnapshot(windowStoreKey, snapshot);
3769
+ pendingWindowStoreWrites.delete(windowEntry.id);
3770
+ }, WINDOW_STORE_WRITE_DEBOUNCE_MS);
3771
+ pendingWindowStoreWrites.set(windowEntry.id, {
3772
+ timeoutId,
3773
+ windowStoreKey,
3774
+ snapshot
3775
+ });
3776
+ }
3777
+ }, [windowBoundsById, windows]);
3778
+ (0, import_react19.useEffect)(() => {
3779
+ const pendingWindowStoreWrites = pendingWindowStoreWritesRef.current;
3780
+ return () => {
3781
+ for (const entry of pendingWindowStoreWrites.values()) {
3782
+ clearTimeout(entry.timeoutId);
3783
+ writeStoredWindowSnapshot(entry.windowStoreKey, entry.snapshot);
3784
+ }
3785
+ pendingWindowStoreWrites.clear();
3786
+ };
3787
+ }, []);
3674
3788
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(NuWindowContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: ["nu-window-host", className].filter(Boolean).join(" "), children: [
3675
3789
  children,
3676
3790
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "nu-window-layer", children: renderWindows.map((windowEntry) => {
@@ -4970,7 +5084,7 @@ function NuIconGridItem({
4970
5084
  icon.contextMenuItems,
4971
5085
  icon
4972
5086
  );
4973
- const dragSource = useNuDragSource({
5087
+ const { dragRef, isDragging } = useNuDragSource({
4974
5088
  disabled: icon.disabled,
4975
5089
  getItem: () => ({ data: icon, id: icon.id, type: "icon" }),
4976
5090
  onDropAccepted: (result) => {
@@ -5013,14 +5127,14 @@ function NuIconGridItem({
5013
5127
  {
5014
5128
  "aria-haspopup": contextMenuItems.length > 0 ? "menu" : void 0,
5015
5129
  className: "nu-icon-grid__icon",
5016
- "data-dragging": dragSource.isDragging || void 0,
5130
+ "data-dragging": isDragging || void 0,
5017
5131
  "data-selected": manager.selectedIconId === icon.id || void 0,
5018
5132
  disabled: icon.disabled,
5019
5133
  onClick: handleClick,
5020
5134
  onContextMenu: handleContextMenu,
5021
5135
  onDoubleClick: icon.onDoubleClick,
5022
5136
  onKeyDown: handleKeyDown,
5023
- ref: dragSource.dragRef,
5137
+ ref: (node) => dragRef(node),
5024
5138
  style: { left: icon.position.x, top: icon.position.y },
5025
5139
  type: "button",
5026
5140
  children: [
package/dist/index.js CHANGED
@@ -3072,28 +3072,94 @@ function getNumericStyleValue(value) {
3072
3072
  }
3073
3073
  return void 0;
3074
3074
  }
3075
+ function getWindowStorageKey(windowStoreKey) {
3076
+ const normalizedKey = windowStoreKey?.trim();
3077
+ return normalizedKey ? `reactnu.window.${normalizedKey}` : null;
3078
+ }
3079
+ function readStoredWindowSnapshot(windowStoreKey) {
3080
+ const storageKey = getWindowStorageKey(windowStoreKey);
3081
+ if (!storageKey || typeof window === "undefined") {
3082
+ return void 0;
3083
+ }
3084
+ try {
3085
+ const rawValue = window.localStorage.getItem(storageKey);
3086
+ if (!rawValue) {
3087
+ return void 0;
3088
+ }
3089
+ const parsedValue = JSON.parse(rawValue);
3090
+ if (!parsedValue || typeof parsedValue !== "object") {
3091
+ return void 0;
3092
+ }
3093
+ const storedValue = parsedValue;
3094
+ const snapshot = {};
3095
+ for (const property of ["height", "width"]) {
3096
+ const value = storedValue[property];
3097
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
3098
+ snapshot[property] = value;
3099
+ }
3100
+ }
3101
+ for (const property of ["left", "top"]) {
3102
+ const value = storedValue[property];
3103
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
3104
+ snapshot[property] = value;
3105
+ }
3106
+ }
3107
+ for (const property of ["maximized", "minimized"]) {
3108
+ const value = storedValue[property];
3109
+ if (typeof value === "boolean") {
3110
+ snapshot[property] = value;
3111
+ }
3112
+ }
3113
+ return Object.keys(snapshot).length > 0 ? snapshot : void 0;
3114
+ } catch {
3115
+ return void 0;
3116
+ }
3117
+ }
3118
+ function writeStoredWindowSnapshot(windowStoreKey, snapshot) {
3119
+ const storageKey = getWindowStorageKey(windowStoreKey);
3120
+ if (!storageKey || typeof window === "undefined") {
3121
+ return;
3122
+ }
3123
+ try {
3124
+ window.localStorage.setItem(storageKey, JSON.stringify(snapshot));
3125
+ } catch {
3126
+ }
3127
+ }
3128
+ var WINDOW_STORE_WRITE_DEBOUNCE_MS = 250;
3075
3129
  function resolveSavedWindowStyle(definition, mode, ownerCenteredStyle, index) {
3076
- const snapshot = definition.onOpen?.();
3077
- const savedStyle = snapshot ? {
3078
- height: snapshot.height,
3079
- left: snapshot.left,
3080
- top: snapshot.top,
3081
- transform: snapshot.left !== void 0 || snapshot.top !== void 0 ? "none" : void 0,
3082
- width: snapshot.width
3083
- } : void 0;
3130
+ const snapshot = definition.onOpen?.() ?? readStoredWindowSnapshot(definition.windowStoreKey);
3131
+ const savedStyle = {};
3132
+ if (snapshot) {
3133
+ if (snapshot.height !== void 0) {
3134
+ savedStyle.height = snapshot.height;
3135
+ }
3136
+ if (snapshot.left !== void 0) {
3137
+ savedStyle.left = snapshot.left;
3138
+ }
3139
+ if (snapshot.top !== void 0) {
3140
+ savedStyle.top = snapshot.top;
3141
+ }
3142
+ if (snapshot.width !== void 0) {
3143
+ savedStyle.width = snapshot.width;
3144
+ }
3145
+ if (snapshot.left !== void 0 || snapshot.top !== void 0) {
3146
+ savedStyle.transform = "none";
3147
+ }
3148
+ }
3084
3149
  return {
3085
3150
  maximized: snapshot?.maximized === true,
3086
3151
  minimized: snapshot?.minimized === true,
3087
- restoreStyle: snapshot?.maximized === true && savedStyle ? {
3152
+ restoreStyle: snapshot?.maximized ? {
3088
3153
  ...getDefaultWindowStyle(mode, index),
3089
3154
  ...ownerCenteredStyle,
3155
+ ...definition.style,
3090
3156
  ...savedStyle
3091
3157
  } : void 0,
3092
3158
  style: {
3093
3159
  ...getDefaultWindowStyle(mode, index),
3094
3160
  ...ownerCenteredStyle,
3095
- ...savedStyle,
3096
- ...definition.style
3161
+ ...definition.style,
3162
+ ...savedStyle
3097
3163
  }
3098
3164
  };
3099
3165
  }
@@ -3198,6 +3264,7 @@ function NuWindowProvider({
3198
3264
  const creationOrderRef = useRef8(0);
3199
3265
  const idRef = useRef8(0);
3200
3266
  const windowBoundsByIdRef = useRef8({});
3267
+ const pendingWindowStoreWritesRef = useRef8(/* @__PURE__ */ new Map());
3201
3268
  const [windows, setWindows] = useState10([]);
3202
3269
  const [windowBoundsById, setWindowBoundsById] = useState10({});
3203
3270
  const appHostMenuContext = useContext7(AppHostMenuContext);
@@ -3619,6 +3686,53 @@ function NuWindowProvider({
3619
3686
  useEffect6(() => {
3620
3687
  onAppModalChange?.(hasAppModal);
3621
3688
  }, [hasAppModal, onAppModalChange]);
3689
+ useEffect6(() => {
3690
+ const pendingWindowStoreWrites = pendingWindowStoreWritesRef.current;
3691
+ const windowStoreKeyById = new Map(
3692
+ windows.map((windowEntry) => [windowEntry.id, windowEntry.windowStoreKey])
3693
+ );
3694
+ for (const [id, pending] of pendingWindowStoreWrites) {
3695
+ if (windowStoreKeyById.get(id) === pending.windowStoreKey) {
3696
+ continue;
3697
+ }
3698
+ clearTimeout(pending.timeoutId);
3699
+ pendingWindowStoreWrites.delete(id);
3700
+ writeStoredWindowSnapshot(pending.windowStoreKey, pending.snapshot);
3701
+ }
3702
+ for (const windowEntry of windows) {
3703
+ if (!windowEntry.windowStoreKey) {
3704
+ continue;
3705
+ }
3706
+ const existing = pendingWindowStoreWrites.get(windowEntry.id);
3707
+ if (existing) {
3708
+ clearTimeout(existing.timeoutId);
3709
+ }
3710
+ const windowStoreKey = windowEntry.windowStoreKey;
3711
+ const snapshot = resolveManagedWindowSnapshot(
3712
+ windowEntry,
3713
+ windowBoundsById
3714
+ );
3715
+ const timeoutId = setTimeout(() => {
3716
+ writeStoredWindowSnapshot(windowStoreKey, snapshot);
3717
+ pendingWindowStoreWrites.delete(windowEntry.id);
3718
+ }, WINDOW_STORE_WRITE_DEBOUNCE_MS);
3719
+ pendingWindowStoreWrites.set(windowEntry.id, {
3720
+ timeoutId,
3721
+ windowStoreKey,
3722
+ snapshot
3723
+ });
3724
+ }
3725
+ }, [windowBoundsById, windows]);
3726
+ useEffect6(() => {
3727
+ const pendingWindowStoreWrites = pendingWindowStoreWritesRef.current;
3728
+ return () => {
3729
+ for (const entry of pendingWindowStoreWrites.values()) {
3730
+ clearTimeout(entry.timeoutId);
3731
+ writeStoredWindowSnapshot(entry.windowStoreKey, entry.snapshot);
3732
+ }
3733
+ pendingWindowStoreWrites.clear();
3734
+ };
3735
+ }, []);
3622
3736
  return /* @__PURE__ */ jsx29(NuWindowContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs17("div", { className: ["nu-window-host", className].filter(Boolean).join(" "), children: [
3623
3737
  children,
3624
3738
  /* @__PURE__ */ jsx29("div", { className: "nu-window-layer", children: renderWindows.map((windowEntry) => {
@@ -4935,7 +5049,7 @@ function NuIconGridItem({
4935
5049
  icon.contextMenuItems,
4936
5050
  icon
4937
5051
  );
4938
- const dragSource = useNuDragSource({
5052
+ const { dragRef, isDragging } = useNuDragSource({
4939
5053
  disabled: icon.disabled,
4940
5054
  getItem: () => ({ data: icon, id: icon.id, type: "icon" }),
4941
5055
  onDropAccepted: (result) => {
@@ -4978,14 +5092,14 @@ function NuIconGridItem({
4978
5092
  {
4979
5093
  "aria-haspopup": contextMenuItems.length > 0 ? "menu" : void 0,
4980
5094
  className: "nu-icon-grid__icon",
4981
- "data-dragging": dragSource.isDragging || void 0,
5095
+ "data-dragging": isDragging || void 0,
4982
5096
  "data-selected": manager.selectedIconId === icon.id || void 0,
4983
5097
  disabled: icon.disabled,
4984
5098
  onClick: handleClick,
4985
5099
  onContextMenu: handleContextMenu,
4986
5100
  onDoubleClick: icon.onDoubleClick,
4987
5101
  onKeyDown: handleKeyDown,
4988
- ref: dragSource.dragRef,
5102
+ ref: (node) => dragRef(node),
4989
5103
  style: { left: icon.position.x, top: icon.position.y },
4990
5104
  type: "button",
4991
5105
  children: [
@@ -39,6 +39,11 @@ export type NuManagedWindowDefinition = {
39
39
  style?: CSSProperties;
40
40
  titleButtons?: WindowTitleButtonDefinition[];
41
41
  title: string;
42
+ /**
43
+ * Persists this managed window's geometry and minimized/maximized state in
44
+ * localStorage. The key is scoped to `reactnu.window.`.
45
+ */
46
+ windowStoreKey?: string;
42
47
  };
43
48
  export type NuManagedWindowControls = {
44
49
  bringToFront: () => void;
@@ -126,6 +126,8 @@ The host owns:
126
126
 
127
127
  Managed windows can persist geometry with:
128
128
 
129
+ - `windowStoreKey?: string` for automatic browser-local persistence under
130
+ `reactnu.window.<key>`
129
131
  - `onOpen?: () => NuManagedWindowSnapshot | void`
130
132
  - `onClose?: (snapshot) => boolean | void`
131
133
 
@@ -29,7 +29,26 @@ Use `useNuWindowManager()` to access:
29
29
 
30
30
  ## Managed Window Persistence
31
31
 
32
- `NuManagedWindowDefinition` also supports:
32
+ Set `windowStoreKey` to persist a managed window's last position, size, and
33
+ minimized/maximized state automatically. ReactNU stores the snapshot in
34
+ `localStorage` as `reactnu.window.<key>`. Use a stable, unique key for each
35
+ logical window; re-opening that window with the same key restores its saved
36
+ geometry.
37
+
38
+ ```tsx
39
+ windowManager.openWindow({
40
+ content: <Inspector />,
41
+ title: "Inspector",
42
+ windowStoreKey: "workspace.inspector"
43
+ });
44
+ ```
45
+
46
+ Browser storage is optional: malformed or unavailable storage is ignored.
47
+ `onOpen` remains the explicit override for applications that restore state
48
+ from their own storage, and `onClose` still receives the current snapshot.
49
+ Clear `reactnu.window.workspace.inspector` to reset this example.
50
+
51
+ For host-owned persistence, `NuManagedWindowDefinition` also supports:
33
52
 
34
53
  - `onOpen?: () => NuManagedWindowSnapshot | void`
35
54
  - `onClose?: (snapshot: NuManagedWindowSnapshot) => boolean | void`
package/docs/Window.md CHANGED
@@ -56,6 +56,10 @@ to `useNuWindowManager().openWindow(...)`; it is resolved by
56
56
  `NuWindowProvider`, rather than by a standalone `Window` instance. See
57
57
  [NuWindowProvider](./NuWindowProvider.md#linked-activation).
58
58
 
59
+ `windowStoreKey?: string` is also available on a managed window definition.
60
+ It automatically restores and saves that window's geometry in browser storage;
61
+ see [NuWindowProvider](./NuWindowProvider.md#managed-window-persistence).
62
+
59
63
  ## Notes
60
64
 
61
65
  - Drag and resize are implemented with pointer events and local DOM updates for smoother motion.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deadragdoll/reactnu",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "private": false,
5
5
  "description": "React components with a precise DOS-era utility interface.",
6
6
  "license": "MIT",