@treasuryspatial/viewer-react 0.1.44 → 0.1.48

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.
Files changed (59) hide show
  1. package/dist/AccessAssetsMenu.d.ts.map +1 -1
  2. package/dist/AccessAssetsMenu.js +53 -45
  3. package/dist/AssetUploadCard.d.ts +2 -1
  4. package/dist/AssetUploadCard.d.ts.map +1 -1
  5. package/dist/AssetUploadCard.js +3 -2
  6. package/dist/ViewControls.d.ts +36 -2
  7. package/dist/ViewControls.d.ts.map +1 -1
  8. package/dist/ViewControls.js +154 -45
  9. package/dist/ViewerCanvas.d.ts.map +1 -1
  10. package/dist/ViewerCanvas.js +15 -40
  11. package/dist/composerRouting.d.ts +6 -0
  12. package/dist/composerRouting.d.ts.map +1 -1
  13. package/dist/composerRouting.js +18 -9
  14. package/dist/index.d.ts +13 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -4
  17. package/dist/tsconfig.tsbuildinfo +1 -1
  18. package/dist/useAdminGallery.d.ts +2 -30
  19. package/dist/useAdminGallery.d.ts.map +1 -1
  20. package/dist/useAdminGallery.js +15 -6
  21. package/dist/useAdminImageStream.d.ts +3 -31
  22. package/dist/useAdminImageStream.d.ts.map +1 -1
  23. package/dist/useAdminImageStream.js +22 -10
  24. package/dist/useComposerAccessGate.js +4 -4
  25. package/dist/useComposerAuthState.d.ts +1 -0
  26. package/dist/useComposerAuthState.d.ts.map +1 -1
  27. package/dist/useComposerAuthState.js +4 -1
  28. package/dist/useComposerModeDraftState.d.ts +21 -0
  29. package/dist/useComposerModeDraftState.d.ts.map +1 -0
  30. package/dist/useComposerModeDraftState.js +89 -0
  31. package/dist/useComposerUiState.d.ts +11 -4
  32. package/dist/useComposerUiState.d.ts.map +1 -1
  33. package/dist/useComposerUiState.js +118 -80
  34. package/dist/useComposerWorkspaceSession.d.ts +21 -0
  35. package/dist/useComposerWorkspaceSession.d.ts.map +1 -0
  36. package/dist/useComposerWorkspaceSession.js +291 -0
  37. package/dist/useGrasshopperSession.d.ts.map +1 -1
  38. package/dist/useGrasshopperSession.js +10 -4
  39. package/dist/useModeShellState.d.ts.map +1 -1
  40. package/dist/useModeShellState.js +52 -6
  41. package/dist/usePromptPackSelection.d.ts +20 -0
  42. package/dist/usePromptPackSelection.d.ts.map +1 -0
  43. package/dist/usePromptPackSelection.js +81 -0
  44. package/dist/useRightRailTabs.d.ts.map +1 -1
  45. package/dist/useRightRailTabs.js +4 -3
  46. package/dist/useSessionLogout.d.ts.map +1 -1
  47. package/dist/useSessionLogout.js +12 -1
  48. package/dist/useSessionRevocationPing.d.ts.map +1 -1
  49. package/dist/useSessionRevocationPing.js +15 -2
  50. package/dist/useShellPanelToggle.d.ts +7 -1
  51. package/dist/useShellPanelToggle.d.ts.map +1 -1
  52. package/dist/useShellPanelToggle.js +14 -6
  53. package/dist/workspaceSession.d.ts +36 -0
  54. package/dist/workspaceSession.d.ts.map +1 -0
  55. package/dist/workspaceSession.js +141 -0
  56. package/package.json +9 -8
  57. package/dist/usePromptPackSelectionByMode.d.ts +0 -26
  58. package/dist/usePromptPackSelectionByMode.d.ts.map +0 -1
  59. package/dist/usePromptPackSelectionByMode.js +0 -111
@@ -1,119 +1,157 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  const resolveString = (value, fallback) => typeof value === "string" && value.length > 0 ? value : fallback;
3
- const modeKey = (mode, key) => `ui${key}_${mode}`;
4
3
  const resolveBoolean = (value, fallback) => typeof value === "boolean" ? value : fallback;
5
4
  const resolvePresetId = (value) => (typeof value === "string" ? value : null);
5
+ const resolveRightPanelTab = (value, fallback) => {
6
+ if (value === "stream")
7
+ return "feed";
8
+ return resolveString(value, fallback);
9
+ };
6
10
  const resolveDefaultPrimaryTab = (mode, defaultPrimaryTabByMode) => defaultPrimaryTabByMode?.[mode] ?? (mode === "imagine" ? "render" : "controls");
7
- export function useComposerUiState({ mode, sessionContext, setSessionContext, defaults, promptKitModes = ["imagine"], moduleKitModes = ["design", "map"], defaultPrimaryTabByMode, }) {
11
+ export function useComposerUiState({ mode, workspaceSession, setWorkspaceSession, defaults, promptKitModes = ["imagine"], moduleKitModes = ["design", "map"], defaultPrimaryTabByMode, }) {
8
12
  const modeDefaultTab = resolveDefaultPrimaryTab(mode, defaultPrimaryTabByMode);
9
- const initialPrimaryTab = resolveString(sessionContext?.[modeKey(mode, "primaryTab")] ?? sessionContext?.uiPrimaryTab, defaults?.primaryTab ?? modeDefaultTab);
10
- const initialPromptOpen = resolveBoolean(sessionContext?.[modeKey(mode, "promptKitOpen")] ?? sessionContext?.uiPromptKitOpen, defaults?.promptKitOpen ?? false);
11
- const initialModuleOpen = resolveBoolean(sessionContext?.[modeKey(mode, "moduleKitOpen")] ?? sessionContext?.uiModuleKitOpen, defaults?.moduleKitOpen ?? false);
12
- const initialPresetId = resolvePresetId(sessionContext?.[modeKey(mode, "activeImaginePresetId")] ??
13
- sessionContext?.uiActiveImaginePresetId) ?? defaults?.activeImaginePresetId ?? null;
14
- const initialControlOpen = resolveBoolean(sessionContext?.[modeKey(mode, "controlPanelOpen")] ??
15
- sessionContext?.uiControlPanelOpen, defaults?.controlPanelOpen ?? true);
16
- const initialRightOpen = resolveBoolean(sessionContext?.[modeKey(mode, "rightPanelOpen")] ?? sessionContext?.uiRightPanelOpen, defaults?.rightPanelOpen ?? false);
17
- const initialRightTab = resolveString(sessionContext?.[modeKey(mode, "rightPanelTab")] ?? sessionContext?.uiRightPanelTab, defaults?.rightPanelTab ?? "images");
18
- const [primaryTab, setPrimaryTabState] = useState(initialPrimaryTab);
19
- const [promptKitOpen, setPromptKitOpenState] = useState(initialPromptOpen);
20
- const [moduleKitOpen, setModuleKitOpenState] = useState(initialModuleOpen);
21
- const [activeImaginePresetId, setActiveImaginePresetIdState] = useState(initialPresetId);
22
- const [controlPanelOpen, setControlPanelOpenState] = useState(initialControlOpen);
23
- const [rightPanelOpen, setRightPanelOpenState] = useState(initialRightOpen);
24
- const [rightPanelTab, setRightPanelTabState] = useState(initialRightTab);
25
- const hydratedRef = useRef(false);
13
+ const resolveWorkspaceValue = useCallback(() => ({
14
+ primaryTab: resolveString(workspaceSession?.uiState.primaryTabByMode?.[mode], defaults?.primaryTab ?? modeDefaultTab),
15
+ promptKitOpen: resolveBoolean(workspaceSession?.uiState.promptKitOpenByMode?.[mode], defaults?.promptKitOpen ?? false),
16
+ moduleKitOpen: resolveBoolean(workspaceSession?.uiState.moduleKitOpenByMode?.[mode], defaults?.moduleKitOpen ?? false),
17
+ activeImaginePresetId: resolvePresetId(workspaceSession?.uiState.activeImaginePresetIdByMode?.[mode]) ??
18
+ defaults?.activeImaginePresetId ??
19
+ null,
20
+ autoGenerateEnabled: resolveBoolean(workspaceSession?.uiState.autoGenerateEnabledByMode?.[mode], defaults?.autoGenerateEnabled ?? false),
21
+ controlPanelOpen: resolveBoolean(workspaceSession?.uiState.controlPanelOpenByMode?.[mode], defaults?.controlPanelOpen ?? true),
22
+ rightPanelOpen: resolveBoolean(workspaceSession?.uiState.rightPanelOpenByMode?.[mode], defaults?.rightPanelOpen ?? false),
23
+ rightPanelTab: resolveRightPanelTab(workspaceSession?.uiState.rightPanelTabByMode?.[mode], defaults?.rightPanelTab ?? "feed"),
24
+ }), [defaults, mode, modeDefaultTab, workspaceSession]);
25
+ const initialValue = resolveWorkspaceValue();
26
+ const [primaryTab, setPrimaryTabState] = useState(initialValue.primaryTab);
27
+ const [promptKitOpen, setPromptKitOpenState] = useState(initialValue.promptKitOpen);
28
+ const [moduleKitOpen, setModuleKitOpenState] = useState(initialValue.moduleKitOpen);
29
+ const [activeImaginePresetId, setActiveImaginePresetIdState] = useState(initialValue.activeImaginePresetId);
30
+ const [autoGenerateEnabled, setAutoGenerateEnabledState] = useState(initialValue.autoGenerateEnabled);
31
+ const [controlPanelOpen, setControlPanelOpenState] = useState(initialValue.controlPanelOpen);
32
+ const [rightPanelOpen, setRightPanelOpenState] = useState(initialValue.rightPanelOpen);
33
+ const [rightPanelTab, setRightPanelTabState] = useState(initialValue.rightPanelTab);
34
+ // Per-field delta sync: only apply a session value when the SESSION
35
+ // changed that field. The previous whole-snapshot stomp raced local
36
+ // writes — clicking a toggle (e.g. the module chooser) opened it locally
37
+ // while the session write was still in flight; any unrelated session
38
+ // churn (draft saves, hydration) then re-applied the stale session value
39
+ // and instantly reverted the click.
40
+ const lastResolvedRef = useRef(null);
26
41
  useEffect(() => {
27
- if (hydratedRef.current)
42
+ const next = resolveWorkspaceValue();
43
+ const prev = lastResolvedRef.current;
44
+ lastResolvedRef.current = next;
45
+ if (!prev)
46
+ return; // initial useState already seeded from the session
47
+ if (next.primaryTab !== prev.primaryTab)
48
+ setPrimaryTabState(next.primaryTab);
49
+ if (next.promptKitOpen !== prev.promptKitOpen)
50
+ setPromptKitOpenState(next.promptKitOpen);
51
+ if (next.moduleKitOpen !== prev.moduleKitOpen)
52
+ setModuleKitOpenState(next.moduleKitOpen);
53
+ if (next.activeImaginePresetId !== prev.activeImaginePresetId)
54
+ setActiveImaginePresetIdState(next.activeImaginePresetId);
55
+ if (next.autoGenerateEnabled !== prev.autoGenerateEnabled)
56
+ setAutoGenerateEnabledState(next.autoGenerateEnabled);
57
+ if (next.controlPanelOpen !== prev.controlPanelOpen)
58
+ setControlPanelOpenState(next.controlPanelOpen);
59
+ if (next.rightPanelOpen !== prev.rightPanelOpen)
60
+ setRightPanelOpenState(next.rightPanelOpen);
61
+ if (next.rightPanelTab !== prev.rightPanelTab)
62
+ setRightPanelTabState(next.rightPanelTab);
63
+ }, [resolveWorkspaceValue]);
64
+ const updateWorkspaceUiState = useCallback((updater) => {
65
+ if (!setWorkspaceSession)
28
66
  return;
29
- hydratedRef.current = true;
30
- if (!sessionContext)
31
- return;
32
- setPrimaryTabState(resolveString(sessionContext.uiPrimaryTab, initialPrimaryTab));
33
- setPromptKitOpenState(resolveBoolean(sessionContext.uiPromptKitOpen, initialPromptOpen));
34
- setModuleKitOpenState(resolveBoolean(sessionContext.uiModuleKitOpen, initialModuleOpen));
35
- setActiveImaginePresetIdState(resolvePresetId(sessionContext.uiActiveImaginePresetId) ?? initialPresetId);
36
- setControlPanelOpenState(resolveBoolean(sessionContext.uiControlPanelOpen, initialControlOpen));
37
- setRightPanelOpenState(resolveBoolean(sessionContext.uiRightPanelOpen, initialRightOpen));
38
- setRightPanelTabState(resolveString(sessionContext.uiRightPanelTab, initialRightTab));
39
- }, [
40
- initialControlOpen,
41
- initialModuleOpen,
42
- initialPresetId,
43
- initialPrimaryTab,
44
- initialPromptOpen,
45
- initialRightOpen,
46
- initialRightTab,
47
- sessionContext,
48
- ]);
49
- const setContext = useCallback((partial) => {
50
- if (!setSessionContext)
51
- return;
52
- setSessionContext(partial);
53
- }, [setSessionContext]);
67
+ setWorkspaceSession((session) => ({
68
+ ...session,
69
+ uiState: updater(session.uiState),
70
+ }));
71
+ }, [setWorkspaceSession]);
54
72
  const setPrimaryTab = useCallback((next) => {
55
73
  setPrimaryTabState(next);
56
- setContext({ uiPrimaryTab: next, [modeKey(mode, "primaryTab")]: next });
57
- }, [mode, setContext]);
74
+ updateWorkspaceUiState((uiState) => ({
75
+ ...uiState,
76
+ primaryTabByMode: { ...uiState.primaryTabByMode, [mode]: next },
77
+ }));
78
+ }, [mode, updateWorkspaceUiState]);
58
79
  const setPromptKitOpen = useCallback((next) => {
59
80
  setPromptKitOpenState(next);
60
- setContext({ uiPromptKitOpen: next, [modeKey(mode, "promptKitOpen")]: next });
61
- }, [mode, setContext]);
81
+ updateWorkspaceUiState((uiState) => ({
82
+ ...uiState,
83
+ promptKitOpenByMode: { ...uiState.promptKitOpenByMode, [mode]: next },
84
+ }));
85
+ }, [mode, updateWorkspaceUiState]);
62
86
  const setModuleKitOpen = useCallback((next) => {
63
87
  setModuleKitOpenState(next);
64
- setContext({ uiModuleKitOpen: next, [modeKey(mode, "moduleKitOpen")]: next });
65
- }, [mode, setContext]);
88
+ updateWorkspaceUiState((uiState) => ({
89
+ ...uiState,
90
+ moduleKitOpenByMode: { ...uiState.moduleKitOpenByMode, [mode]: next },
91
+ }));
92
+ }, [mode, updateWorkspaceUiState]);
66
93
  const setActiveImaginePresetId = useCallback((next) => {
67
94
  setActiveImaginePresetIdState(next);
68
- setContext({ uiActiveImaginePresetId: next, [modeKey(mode, "activeImaginePresetId")]: next });
69
- }, [mode, setContext]);
95
+ updateWorkspaceUiState((uiState) => ({
96
+ ...uiState,
97
+ activeImaginePresetIdByMode: { ...uiState.activeImaginePresetIdByMode, [mode]: next },
98
+ }));
99
+ }, [mode, updateWorkspaceUiState]);
100
+ const setAutoGenerateEnabled = useCallback((next) => {
101
+ setAutoGenerateEnabledState(next);
102
+ updateWorkspaceUiState((uiState) => ({
103
+ ...uiState,
104
+ autoGenerateEnabledByMode: { ...uiState.autoGenerateEnabledByMode, [mode]: next },
105
+ }));
106
+ }, [mode, updateWorkspaceUiState]);
70
107
  const setControlPanelOpen = useCallback((next) => {
71
108
  setControlPanelOpenState(next);
72
- setContext({ uiControlPanelOpen: next, [modeKey(mode, "controlPanelOpen")]: next });
73
- }, [mode, setContext]);
109
+ updateWorkspaceUiState((uiState) => ({
110
+ ...uiState,
111
+ controlPanelOpenByMode: { ...uiState.controlPanelOpenByMode, [mode]: next },
112
+ }));
113
+ }, [mode, updateWorkspaceUiState]);
74
114
  const setRightPanelOpen = useCallback((next) => {
75
115
  setRightPanelOpenState(next);
76
- setContext({ uiRightPanelOpen: next, [modeKey(mode, "rightPanelOpen")]: next });
77
- }, [mode, setContext]);
116
+ updateWorkspaceUiState((uiState) => ({
117
+ ...uiState,
118
+ rightPanelOpenByMode: { ...uiState.rightPanelOpenByMode, [mode]: next },
119
+ }));
120
+ }, [mode, updateWorkspaceUiState]);
78
121
  const setRightPanelTab = useCallback((next) => {
79
122
  setRightPanelTabState(next);
80
- setContext({ uiRightPanelTab: next, [modeKey(mode, "rightPanelTab")]: next });
81
- }, [mode, setContext]);
82
- const modeRef = useRef(null);
83
- useEffect(() => {
84
- if (modeRef.current === mode)
85
- return;
86
- modeRef.current = mode;
87
- if (!sessionContext)
88
- return;
89
- setPrimaryTabState(resolveString(sessionContext?.[modeKey(mode, "primaryTab")] ?? sessionContext?.uiPrimaryTab, resolveDefaultPrimaryTab(mode, defaultPrimaryTabByMode)));
90
- setPromptKitOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "promptKitOpen")] ??
91
- sessionContext?.uiPromptKitOpen, false));
92
- setModuleKitOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "moduleKitOpen")] ??
93
- sessionContext?.uiModuleKitOpen, false));
94
- setActiveImaginePresetIdState(resolvePresetId(sessionContext?.[modeKey(mode, "activeImaginePresetId")] ??
95
- sessionContext?.uiActiveImaginePresetId) ?? null);
96
- setControlPanelOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "controlPanelOpen")] ??
97
- sessionContext?.uiControlPanelOpen, true));
98
- setRightPanelOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "rightPanelOpen")] ??
99
- sessionContext?.uiRightPanelOpen, false));
100
- setRightPanelTabState(resolveString(sessionContext?.[modeKey(mode, "rightPanelTab")] ??
101
- sessionContext?.uiRightPanelTab, "images"));
102
- }, [defaultPrimaryTabByMode, mode, sessionContext]);
123
+ updateWorkspaceUiState((uiState) => ({
124
+ ...uiState,
125
+ rightPanelTabByMode: { ...uiState.rightPanelTabByMode, [mode]: next },
126
+ }));
127
+ }, [mode, updateWorkspaceUiState]);
103
128
  const gatedPromptKitOpen = useMemo(() => (promptKitModes.includes(mode) ? promptKitOpen : false), [mode, promptKitOpen, promptKitModes]);
104
129
  const gatedModuleKitOpen = useMemo(() => (moduleKitModes.includes(mode) ? moduleKitOpen : false), [mode, moduleKitOpen, moduleKitModes]);
130
+ const chooserOpen = gatedPromptKitOpen || gatedModuleKitOpen;
131
+ const setChooserOpen = useCallback((next) => {
132
+ if (promptKitModes.includes(mode)) {
133
+ setPromptKitOpen(next);
134
+ }
135
+ if (moduleKitModes.includes(mode)) {
136
+ setModuleKitOpen(next);
137
+ }
138
+ }, [mode, moduleKitModes, promptKitModes, setModuleKitOpen, setPromptKitOpen]);
105
139
  return {
106
140
  primaryTab,
107
141
  promptKitOpen: gatedPromptKitOpen,
108
142
  moduleKitOpen: gatedModuleKitOpen,
143
+ chooserOpen,
109
144
  activeImaginePresetId,
145
+ autoGenerateEnabled,
110
146
  controlPanelOpen,
111
147
  rightPanelOpen,
112
148
  rightPanelTab,
113
149
  setPrimaryTab,
114
150
  setPromptKitOpen,
115
151
  setModuleKitOpen,
152
+ setChooserOpen,
116
153
  setActiveImaginePresetId,
154
+ setAutoGenerateEnabled,
117
155
  setControlPanelOpen,
118
156
  setRightPanelOpen,
119
157
  setRightPanelTab,
@@ -0,0 +1,21 @@
1
+ import { type ComposerWorkspaceSession, type ComposerWorkspaceSessionUpdater } from './workspaceSession';
2
+ export type WorkspaceSessionRemoteStatus = 'idle' | 'loading' | 'ready' | 'error';
3
+ export type UseComposerWorkspaceSessionOptions<TDraft> = {
4
+ activeMode: string;
5
+ modeIds?: string[];
6
+ enabled?: boolean;
7
+ remoteEndpoint?: string;
8
+ storageKey?: string;
9
+ createDraft: () => TDraft;
10
+ restoreDraft: (raw: unknown) => TDraft;
11
+ };
12
+ export declare function useComposerWorkspaceSession<TDraft>({ activeMode, modeIds, enabled, remoteEndpoint, storageKey, createDraft, restoreDraft, }: UseComposerWorkspaceSessionOptions<TDraft>): {
13
+ workspaceSession: ComposerWorkspaceSession<TDraft>;
14
+ workspaceSessionHydrated: boolean;
15
+ remoteHydrated: boolean;
16
+ remoteStatus: WorkspaceSessionRemoteStatus;
17
+ remoteError: string | null;
18
+ setWorkspaceSession: (updater: ComposerWorkspaceSessionUpdater<TDraft>) => void;
19
+ clearWorkspaceSession: () => void;
20
+ };
21
+ //# sourceMappingURL=useComposerWorkspaceSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComposerWorkspaceSession.d.ts","sourceRoot":"","sources":["../src/useComposerWorkspaceSession.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACrC,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,4BAA4B,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAElF,MAAM,MAAM,kCAAkC,CAAC,MAAM,IAAI;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,MAAM,CAAC;IAC1B,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;CACxC,CAAC;AA0CF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,EAClD,UAAU,EACV,OAAY,EACZ,OAAe,EACf,cAAyC,EACzC,UAAgC,EAChC,WAAW,EACX,YAAY,GACb,EAAE,kCAAkC,CAAC,MAAM,CAAC,GAAG;IAC9C,gBAAgB,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACnD,wBAAwB,EAAE,OAAO,CAAC;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,4BAA4B,CAAC;IAC3C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,mBAAmB,EAAE,CAAC,OAAO,EAAE,+BAA+B,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IAChF,qBAAqB,EAAE,MAAM,IAAI,CAAC;CACnC,CA2QA"}
@@ -0,0 +1,291 @@
1
+ 'use client';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createEmptyComposerWorkspaceSession, restoreComposerWorkspaceSession, } from './workspaceSession';
4
+ const DEFAULT_STORAGE_KEY = 'treasury:composer-workspace-session:v1';
5
+ const REMOTE_SAVE_DEBOUNCE_MS = 800;
6
+ const isAbortError = (error) => error instanceof Error && error.name === 'AbortError';
7
+ const readUpdatedAtMs = (session) => {
8
+ const parsed = Date.parse(session.updatedAt ?? '');
9
+ return Number.isFinite(parsed) ? parsed : null;
10
+ };
11
+ const shouldPreferLocalSession = (localSession, remoteSession) => {
12
+ const localUpdatedAtMs = readUpdatedAtMs(localSession);
13
+ const remoteUpdatedAtMs = readUpdatedAtMs(remoteSession);
14
+ if (localUpdatedAtMs !== null && remoteUpdatedAtMs !== null) {
15
+ return localUpdatedAtMs > remoteUpdatedAtMs;
16
+ }
17
+ if (localUpdatedAtMs !== null && remoteUpdatedAtMs === null) {
18
+ return true;
19
+ }
20
+ return false;
21
+ };
22
+ const resolveRemoteError = (payload, status, fallback) => {
23
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
24
+ const record = payload;
25
+ const error = record.error;
26
+ if (typeof error === 'string' && error.trim())
27
+ return error.trim();
28
+ if (error && typeof error === 'object' && !Array.isArray(error)) {
29
+ const message = error.message;
30
+ if (typeof message === 'string' && message.trim())
31
+ return message.trim();
32
+ }
33
+ if (typeof record.message === 'string' && record.message.trim())
34
+ return record.message.trim();
35
+ }
36
+ return `${fallback} (${status || 0})`;
37
+ };
38
+ export function useComposerWorkspaceSession({ activeMode, modeIds = [], enabled = false, remoteEndpoint = '/api/workspace/session', storageKey = DEFAULT_STORAGE_KEY, createDraft, restoreDraft, }) {
39
+ const restoreOptions = useMemo(() => ({
40
+ createDraft,
41
+ restoreDraft,
42
+ activeMode,
43
+ modeIds,
44
+ }), [activeMode, createDraft, modeIds, restoreDraft]);
45
+ const normalizeSession = useCallback((value) => restoreComposerWorkspaceSession(value, restoreOptions), [restoreOptions]);
46
+ const createEmptySession = useCallback(() => createEmptyComposerWorkspaceSession(createDraft, { activeMode, modeIds }), [activeMode, createDraft, modeIds]);
47
+ const [workspaceSession, setWorkspaceSessionState] = useState(() => createEmptyComposerWorkspaceSession(createDraft, { activeMode, modeIds }));
48
+ const [workspaceSessionHydrated, setWorkspaceSessionHydrated] = useState(false);
49
+ const [remoteHydrated, setRemoteHydrated] = useState(!enabled);
50
+ const [remoteStatus, setRemoteStatus] = useState(enabled ? 'loading' : 'idle');
51
+ const [remoteError, setRemoteError] = useState(null);
52
+ const lastSyncedSignatureRef = useRef('');
53
+ const remoteSaveTimerRef = useRef(null);
54
+ const setWorkspaceSession = useCallback((updater) => {
55
+ setWorkspaceSessionState((prev) => {
56
+ const normalizedPrev = normalizeSession(prev);
57
+ const nextCandidate = updater(normalizedPrev);
58
+ const normalizedNext = normalizeSession({
59
+ ...nextCandidate,
60
+ activeMode: nextCandidate.activeMode || activeMode,
61
+ });
62
+ const previousSignature = JSON.stringify(normalizedPrev);
63
+ const nextSignature = JSON.stringify(normalizedNext);
64
+ if (previousSignature === nextSignature) {
65
+ return prev;
66
+ }
67
+ return {
68
+ ...normalizedNext,
69
+ updatedAt: new Date().toISOString(),
70
+ };
71
+ });
72
+ }, [activeMode, normalizeSession]);
73
+ const clearWorkspaceSession = useCallback(() => {
74
+ const next = createEmptySession();
75
+ const nextSignature = JSON.stringify(normalizeSession(next));
76
+ if (typeof window !== 'undefined') {
77
+ window.sessionStorage.removeItem(storageKey);
78
+ }
79
+ setWorkspaceSessionState(next);
80
+ setRemoteError(null);
81
+ lastSyncedSignatureRef.current = '';
82
+ if (!enabled) {
83
+ setRemoteStatus('idle');
84
+ setRemoteHydrated(true);
85
+ lastSyncedSignatureRef.current = nextSignature;
86
+ return;
87
+ }
88
+ setRemoteStatus('loading');
89
+ setRemoteHydrated(false);
90
+ void (async () => {
91
+ try {
92
+ const response = await fetch(remoteEndpoint, {
93
+ method: 'DELETE',
94
+ cache: 'no-store',
95
+ credentials: 'same-origin',
96
+ });
97
+ const payload = await response.json().catch(() => null);
98
+ if (!response.ok || !payload?.success) {
99
+ throw new Error(resolveRemoteError(payload, response.status, 'Workspace session delete failed'));
100
+ }
101
+ lastSyncedSignatureRef.current = nextSignature;
102
+ setRemoteStatus('ready');
103
+ setRemoteError(null);
104
+ }
105
+ catch (error) {
106
+ setRemoteStatus('error');
107
+ setRemoteError(error instanceof Error ? error.message : 'Workspace session delete failed');
108
+ }
109
+ finally {
110
+ setRemoteHydrated(true);
111
+ }
112
+ })();
113
+ }, [createEmptySession, enabled, normalizeSession, remoteEndpoint, storageKey]);
114
+ useEffect(() => {
115
+ if (typeof window === 'undefined') {
116
+ setWorkspaceSessionHydrated(true);
117
+ return;
118
+ }
119
+ try {
120
+ const raw = window.sessionStorage.getItem(storageKey);
121
+ if (!raw) {
122
+ setWorkspaceSessionState(createEmptySession());
123
+ }
124
+ else {
125
+ const parsed = JSON.parse(raw);
126
+ setWorkspaceSessionState(normalizeSession(parsed));
127
+ }
128
+ }
129
+ catch {
130
+ setWorkspaceSessionState(createEmptySession());
131
+ }
132
+ finally {
133
+ setWorkspaceSessionHydrated(true);
134
+ }
135
+ }, [createEmptySession, normalizeSession, storageKey]);
136
+ useEffect(() => {
137
+ if (!workspaceSessionHydrated)
138
+ return;
139
+ setWorkspaceSession((session) => {
140
+ if (session.activeMode === activeMode)
141
+ return session;
142
+ return { ...session, activeMode };
143
+ });
144
+ }, [activeMode, setWorkspaceSession, workspaceSessionHydrated]);
145
+ useEffect(() => {
146
+ if (!workspaceSessionHydrated || typeof window === 'undefined')
147
+ return;
148
+ try {
149
+ window.sessionStorage.setItem(storageKey, JSON.stringify(workspaceSession));
150
+ }
151
+ catch {
152
+ // Ignore crash-cache persistence failures.
153
+ }
154
+ }, [storageKey, workspaceSession, workspaceSessionHydrated]);
155
+ useEffect(() => {
156
+ if (!workspaceSessionHydrated)
157
+ return;
158
+ if (!enabled) {
159
+ setRemoteHydrated(true);
160
+ setRemoteStatus('idle');
161
+ setRemoteError(null);
162
+ return;
163
+ }
164
+ let cancelled = false;
165
+ const controller = new AbortController();
166
+ setRemoteHydrated(false);
167
+ setRemoteStatus('loading');
168
+ setRemoteError(null);
169
+ const loadRemoteWorkspaceSession = async () => {
170
+ try {
171
+ const response = await fetch(remoteEndpoint, {
172
+ method: 'GET',
173
+ cache: 'no-store',
174
+ credentials: 'same-origin',
175
+ signal: controller.signal,
176
+ });
177
+ const payload = await response.json().catch(() => null);
178
+ if (!response.ok || !payload?.success) {
179
+ throw new Error(resolveRemoteError(payload, response.status, 'Workspace session load failed'));
180
+ }
181
+ if (cancelled)
182
+ return;
183
+ const remoteSessionRaw = payload?.session ?? null;
184
+ setWorkspaceSessionState((prev) => {
185
+ const localSession = normalizeSession(prev);
186
+ if (!remoteSessionRaw) {
187
+ return localSession;
188
+ }
189
+ const remoteSession = normalizeSession(remoteSessionRaw);
190
+ if (shouldPreferLocalSession(localSession, remoteSession)) {
191
+ return localSession;
192
+ }
193
+ lastSyncedSignatureRef.current = JSON.stringify(remoteSession);
194
+ const localSignature = JSON.stringify(localSession);
195
+ const remoteSignature = lastSyncedSignatureRef.current;
196
+ return localSignature === remoteSignature ? prev : remoteSession;
197
+ });
198
+ setRemoteStatus('ready');
199
+ setRemoteError(null);
200
+ }
201
+ catch (error) {
202
+ if (cancelled || isAbortError(error))
203
+ return;
204
+ setRemoteStatus('error');
205
+ setRemoteError(error instanceof Error ? error.message : 'Workspace session load failed');
206
+ }
207
+ finally {
208
+ if (!cancelled) {
209
+ setRemoteHydrated(true);
210
+ }
211
+ }
212
+ };
213
+ void loadRemoteWorkspaceSession();
214
+ return () => {
215
+ cancelled = true;
216
+ controller.abort();
217
+ };
218
+ }, [enabled, normalizeSession, remoteEndpoint, workspaceSessionHydrated]);
219
+ useEffect(() => {
220
+ if (!workspaceSessionHydrated)
221
+ return;
222
+ if (!enabled || !remoteHydrated)
223
+ return;
224
+ const normalizedSession = normalizeSession(workspaceSession);
225
+ const nextSignature = JSON.stringify(normalizedSession);
226
+ if (nextSignature === lastSyncedSignatureRef.current) {
227
+ return;
228
+ }
229
+ if (remoteSaveTimerRef.current) {
230
+ clearTimeout(remoteSaveTimerRef.current);
231
+ remoteSaveTimerRef.current = null;
232
+ }
233
+ remoteSaveTimerRef.current = setTimeout(() => {
234
+ const controller = new AbortController();
235
+ const persistRemoteWorkspaceSession = async () => {
236
+ try {
237
+ const response = await fetch(remoteEndpoint, {
238
+ method: 'PUT',
239
+ cache: 'no-store',
240
+ credentials: 'same-origin',
241
+ headers: {
242
+ 'Content-Type': 'application/json',
243
+ },
244
+ body: JSON.stringify({ session: normalizedSession }),
245
+ signal: controller.signal,
246
+ });
247
+ const payload = await response.json().catch(() => null);
248
+ if (!response.ok || !payload?.success) {
249
+ throw new Error(resolveRemoteError(payload, response.status, 'Workspace session save failed'));
250
+ }
251
+ const savedSession = normalizeSession(payload?.session ?? normalizedSession);
252
+ const savedSignature = JSON.stringify(savedSession);
253
+ lastSyncedSignatureRef.current = savedSignature;
254
+ setRemoteStatus('ready');
255
+ setRemoteError(null);
256
+ setWorkspaceSessionState((prev) => {
257
+ const localSession = normalizeSession(prev);
258
+ const localSignature = JSON.stringify(localSession);
259
+ if (localSignature === savedSignature || shouldPreferLocalSession(localSession, savedSession)) {
260
+ return prev;
261
+ }
262
+ return savedSession;
263
+ });
264
+ }
265
+ catch (error) {
266
+ if (isAbortError(error))
267
+ return;
268
+ setRemoteStatus('error');
269
+ setRemoteError(error instanceof Error ? error.message : 'Workspace session save failed');
270
+ }
271
+ };
272
+ void persistRemoteWorkspaceSession();
273
+ return () => controller.abort();
274
+ }, REMOTE_SAVE_DEBOUNCE_MS);
275
+ return () => {
276
+ if (remoteSaveTimerRef.current) {
277
+ clearTimeout(remoteSaveTimerRef.current);
278
+ remoteSaveTimerRef.current = null;
279
+ }
280
+ };
281
+ }, [enabled, normalizeSession, remoteEndpoint, remoteHydrated, workspaceSession, workspaceSessionHydrated]);
282
+ return {
283
+ workspaceSession,
284
+ workspaceSessionHydrated,
285
+ remoteHydrated,
286
+ remoteStatus,
287
+ remoteError,
288
+ setWorkspaceSession,
289
+ clearWorkspaceSession,
290
+ };
291
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"useGrasshopperSession.d.ts","sourceRoot":"","sources":["../src/useGrasshopperSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACb,MAAM,6BAA6B,CAAC;AAQrC,KAAK,iBAAiB,GAAG,gBAAgB,GAAG;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiGF,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;QACnB,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC5B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;IACnD,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACrD,KAAK,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IAC5F,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EACpC,QAA0B,EAC1B,OAAO,EACP,kBAAkB,EAClB,UAAU,EACV,OAAO,GACR,GAAE,yBAA8B,GAAG,uBAAuB,CAgI1D"}
1
+ {"version":3,"file":"useGrasshopperSession.d.ts","sourceRoot":"","sources":["../src/useGrasshopperSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACb,MAAM,6BAA6B,CAAC;AAQrC,KAAK,iBAAiB,GAAG,gBAAgB,GAAG;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAuGF,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;QACnB,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC5B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;IACnD,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACrD,KAAK,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IAC5F,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EACpC,QAA0B,EAC1B,OAAO,EACP,kBAAkB,EAClB,UAAU,EACV,OAAO,GACR,GAAE,yBAA8B,GAAG,uBAAuB,CAgI1D"}
@@ -2,6 +2,8 @@ import { useCallback, useMemo, useRef, useState } from "react";
2
2
  import { buildGrasshopperDefaults, buildGrasshopperValues, solveGrasshopperFile, warmGrasshopperSolver, } from "@treasuryspatial/viewer-kit";
3
3
  const sanitizeGhKey = (value) => value.replace(/[^a-zA-Z0-9_-]/g, "_");
4
4
  const resolveGhParamName = (input, index) => {
5
+ if (input.paramName)
6
+ return input.paramName;
5
7
  if (input.name)
6
8
  return input.name;
7
9
  if (input.nickname)
@@ -56,10 +58,14 @@ const buildGhPanelField = (input, index) => {
56
58
  const label = resolveGhLabel(input, index);
57
59
  const type = (input.type ?? "").toLowerCase();
58
60
  const options = Array.isArray(input.options)
59
- ? input.options.map((option, optionIndex) => ({
60
- value: option?.value ?? optionIndex,
61
- label: option?.label ?? String(option?.value ?? optionIndex),
62
- }))
61
+ ? input.options.map((option, optionIndex) => {
62
+ const optionRecord = typeof option === "object" && option !== null ? option : null;
63
+ const value = optionRecord?.value ?? optionIndex;
64
+ return {
65
+ value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : optionIndex,
66
+ label: typeof optionRecord?.label === "string" ? optionRecord.label : String(value),
67
+ };
68
+ })
63
69
  : undefined;
64
70
  if (options && options.length > 0) {
65
71
  return { key, label, control: "select", options };
@@ -1 +1 @@
1
- {"version":3,"file":"useModeShellState.d.ts","sourceRoot":"","sources":["../src/useModeShellState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAWF,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,MAAM,EACN,gBAAuB,EACvB,kBAA0B,EAC1B,oBAA4B,EAC5B,OAAc,GACf,EAAE,wBAAwB;;;;;;;;;;EA0E1B"}
1
+ {"version":3,"file":"useModeShellState.d.ts","sourceRoot":"","sources":["../src/useModeShellState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAKtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAWF,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,MAAM,EACN,gBAAuB,EACvB,kBAA0B,EAC1B,oBAA4B,EAC5B,OAAc,GACf,EAAE,wBAAwB;;;;;;;;;;EAyH1B"}