hbauzan commited on
Commit
25ea490
·
2 Parent(s): 0181541ac96a64

merge(feat): Galaxy VIEW Slice 2 — Navbar chrome locks

Browse files
src/main.js CHANGED
@@ -54,6 +54,11 @@ import { wireFieldInfo } from './ui/fieldInfo.js';
54
  import {
55
  DEFAULT_VIEW_MODE,
56
  } from './ui/appViewDefaults.js';
 
 
 
 
 
57
  import {
58
  loadArithmeticSettings,
59
  saveArithmeticSettings,
@@ -87,6 +92,9 @@ class VHectorLabApp {
87
 
88
  // 3. View Mode State — startup ARITHMETIC | ANALYSIS | POINTS
89
  this.viewMode = DEFAULT_VIEW_MODE;
 
 
 
90
 
91
  // 4. UI Components (Modal, HUD, Navbar, Sidebar, ComparePanel, ThreadLabels)
92
  // Bottom HUD is always visible (roadmap D3) — not hosted in a collapsible dock.
@@ -109,18 +117,22 @@ class VHectorLabApp {
109
  this.navbar = new Navbar(
110
  this.appContainer,
111
  (renderMode) => {
 
112
  state.setRenderMode(renderMode);
113
  this.applyContextViewDefaults();
114
  this.refreshRender();
115
  },
116
  (viewMode) => {
117
- this.viewMode = viewMode;
118
- this.applyContextViewDefaults();
119
- this.refreshRender();
120
  },
121
  (workspaceMode) => {
 
122
  this.handleWorkspaceModeChange(workspaceMode);
123
- }
 
 
 
 
124
  );
125
 
126
  // Tap/click "i" tips on editable fields (mobile-friendly; one tip at a time).
@@ -345,6 +357,72 @@ class VHectorLabApp {
345
  }
346
  }
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  refreshRender() {
349
  if (state.workspaceMode === 'COMPARE') {
350
  if (state.compareData) {
 
54
  import {
55
  DEFAULT_VIEW_MODE,
56
  } from './ui/appViewDefaults.js';
57
+ import {
58
+ enterGalaxyChrome,
59
+ isGalaxyView,
60
+ leaveGalaxyChrome,
61
+ } from './ui/galaxyChrome.js';
62
  import {
63
  loadArithmeticSettings,
64
  saveArithmeticSettings,
 
92
 
93
  // 3. View Mode State — startup ARITHMETIC | ANALYSIS | POINTS
94
  this.viewMode = DEFAULT_VIEW_MODE;
95
+ /** @type {{ workspaceMode: string, viewMode: string, renderMode: string }|null} */
96
+ this._preGalaxyTriad = null;
97
+ this.galaxyMethod = 'umap';
98
 
99
  // 4. UI Components (Modal, HUD, Navbar, Sidebar, ComparePanel, ThreadLabels)
100
  // Bottom HUD is always visible (roadmap D3) — not hosted in a collapsible dock.
 
117
  this.navbar = new Navbar(
118
  this.appContainer,
119
  (renderMode) => {
120
+ if (isGalaxyView(this.viewMode)) return;
121
  state.setRenderMode(renderMode);
122
  this.applyContextViewDefaults();
123
  this.refreshRender();
124
  },
125
  (viewMode) => {
126
+ this.handleViewModeChange(viewMode);
 
 
127
  },
128
  (workspaceMode) => {
129
+ if (isGalaxyView(this.viewMode)) return;
130
  this.handleWorkspaceModeChange(workspaceMode);
131
+ },
132
+ (method) => {
133
+ this.galaxyMethod = method;
134
+ // Projection pipeline wires in later Galaxy slices.
135
+ },
136
  );
137
 
138
  // Tap/click "i" tips on editable fields (mobile-friendly; one tip at a time).
 
357
  }
358
  }
359
 
360
+ /**
361
+ * VIEW triad: Galaxy locks MODE=COMPARE + RENDER=POINTS; leaving restores snapshot.
362
+ * @param {string} nextView
363
+ */
364
+ handleViewModeChange(nextView) {
365
+ const wasGalaxy = isGalaxyView(this.viewMode);
366
+ const entering = isGalaxyView(nextView);
367
+
368
+ if (entering && !wasGalaxy) {
369
+ const entered = enterGalaxyChrome({
370
+ workspaceMode: state.workspaceMode,
371
+ viewMode: this.viewMode,
372
+ renderMode: state.renderMode,
373
+ });
374
+ this._preGalaxyTriad = entered.restore;
375
+ this.viewMode = entered.viewMode;
376
+ this.galaxyMethod = entered.method;
377
+ this.navbar.setModeRenderLocked(true);
378
+ this.navbar.setGalaxyMethod(entered.method);
379
+ this.navbar.setViewMode(entered.viewMode);
380
+
381
+ if (state.renderMode !== entered.renderMode) {
382
+ state.setRenderMode(entered.renderMode);
383
+ }
384
+ this.navbar.setRenderMode(entered.renderMode);
385
+
386
+ if (state.workspaceMode !== entered.workspaceMode) {
387
+ this.navbar.setWorkspaceMode(entered.workspaceMode);
388
+ this.handleWorkspaceModeChange(entered.workspaceMode);
389
+ } else {
390
+ this.navbar.setWorkspaceMode(entered.workspaceMode);
391
+ this.applyContextViewDefaults();
392
+ this.refreshRender();
393
+ }
394
+ return;
395
+ }
396
+
397
+ if (!entering && wasGalaxy) {
398
+ const left = leaveGalaxyChrome(this._preGalaxyTriad, nextView);
399
+ this._preGalaxyTriad = null;
400
+ this.viewMode = left.viewMode;
401
+ this.navbar.setModeRenderLocked(false);
402
+ this.navbar.setViewMode(left.viewMode);
403
+
404
+ if (state.renderMode !== left.renderMode) {
405
+ state.setRenderMode(left.renderMode);
406
+ }
407
+ this.navbar.setRenderMode(left.renderMode);
408
+
409
+ if (state.workspaceMode !== left.workspaceMode) {
410
+ this.navbar.setWorkspaceMode(left.workspaceMode);
411
+ this.handleWorkspaceModeChange(left.workspaceMode);
412
+ } else {
413
+ this.navbar.setWorkspaceMode(left.workspaceMode);
414
+ this.applyContextViewDefaults();
415
+ this.refreshRender();
416
+ }
417
+ return;
418
+ }
419
+
420
+ this.viewMode = nextView;
421
+ this.navbar.setViewMode(nextView);
422
+ this.applyContextViewDefaults();
423
+ this.refreshRender();
424
+ }
425
+
426
  refreshRender() {
427
  if (state.workspaceMode === 'COMPARE') {
428
  if (state.compareData) {
src/style.css CHANGED
@@ -174,6 +174,64 @@ canvas {
174
  box-shadow: 0 0 12px rgba(0, 255, 170, 0.4);
175
  }
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  .hidden {
178
  display: none !important;
179
  }
@@ -2001,6 +2059,19 @@ canvas {
2001
  line-height: 1;
2002
  }
2003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2004
  .status-indicator {
2005
  margin-left: 0;
2006
  flex-shrink: 0;
 
174
  box-shadow: 0 0 12px rgba(0, 255, 170, 0.4);
175
  }
176
 
177
+ .mode-tab:disabled,
178
+ .workspace-tab:disabled,
179
+ .mode-tab.tab-locked,
180
+ .workspace-tab.tab-locked {
181
+ opacity: 0.45;
182
+ cursor: not-allowed;
183
+ box-shadow: none;
184
+ }
185
+
186
+ .mode-tab:disabled.active,
187
+ .workspace-tab:disabled.active,
188
+ .mode-tab.tab-locked.active,
189
+ .workspace-tab.tab-locked.active {
190
+ opacity: 0.75;
191
+ }
192
+
193
+ .galaxy-view-cluster {
194
+ display: flex;
195
+ flex-direction: column;
196
+ align-items: stretch;
197
+ gap: 3px;
198
+ }
199
+
200
+ .galaxy-method-chips {
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ gap: 3px;
205
+ }
206
+
207
+ .galaxy-method-chips[hidden] {
208
+ display: none !important;
209
+ }
210
+
211
+ .galaxy-method-chip {
212
+ background: rgba(255, 255, 255, 0.04);
213
+ border: 1px solid rgba(255, 255, 255, 0.08);
214
+ color: var(--text-muted);
215
+ padding: 1px 6px;
216
+ font-size: 0.62rem;
217
+ font-weight: 700;
218
+ letter-spacing: 0.02em;
219
+ border-radius: 4px;
220
+ cursor: pointer;
221
+ line-height: 1.3;
222
+ }
223
+
224
+ .galaxy-method-chip.active {
225
+ background: rgba(0, 255, 170, 0.25);
226
+ border-color: rgba(0, 255, 170, 0.55);
227
+ color: var(--text-main);
228
+ }
229
+
230
+ .galaxy-method-chip:disabled {
231
+ opacity: 0.35;
232
+ cursor: not-allowed;
233
+ }
234
+
235
  .hidden {
236
  display: none !important;
237
  }
 
2059
  line-height: 1;
2060
  }
2061
 
2062
+ /* Keep Galaxy method chips in the single slim row */
2063
+ .galaxy-view-cluster {
2064
+ flex-direction: row;
2065
+ align-items: center;
2066
+ gap: 2px;
2067
+ }
2068
+
2069
+ .galaxy-method-chip {
2070
+ padding: 0 4px;
2071
+ font-size: 0.5rem;
2072
+ min-height: 16px;
2073
+ }
2074
+
2075
  .status-indicator {
2076
  margin-left: 0;
2077
  flex-shrink: 0;
src/ui/Navbar.js CHANGED
@@ -1,5 +1,6 @@
1
  /**
2
- * Top Navbar component with title, status indicator, View Mode tabs (ANALYSIS | NAVIGATION), and Render Mode selector tabs (POINTS | RIBBONS).
 
3
  * Mobile: overflow tab strip gets ◀ ▶ scroll arrows.
4
  */
5
  import { getTabsScrollState, nextTabsScrollLeft } from './navbarTabsScroll.js';
@@ -8,18 +9,42 @@ import {
8
  DEFAULT_VIEW_MODE,
9
  DEFAULT_WORKSPACE_MODE,
10
  } from './appViewDefaults.js';
 
 
 
 
 
 
11
 
12
  export class Navbar {
13
- constructor(containerElement, onRenderModeChangeCallback, onViewModeChangeCallback, onWorkspaceModeChangeCallback) {
 
 
 
 
 
 
14
  this.container = containerElement || document.body;
15
  this.onRenderModeChange = onRenderModeChangeCallback;
16
  this.onViewModeChange = onViewModeChangeCallback;
17
  this.onWorkspaceModeChange = onWorkspaceModeChangeCallback;
 
 
 
18
 
19
  const workspaceActive = (mode) => (mode === DEFAULT_WORKSPACE_MODE ? ' active' : '');
20
  const viewActive = (mode) => (mode === DEFAULT_VIEW_MODE ? ' active' : '');
21
  const renderActive = (mode) => (mode === DEFAULT_RENDER_MODE ? ' active' : '');
22
 
 
 
 
 
 
 
 
 
 
23
  this.element = document.createElement('header');
24
  this.element.id = 'top-navbar';
25
  this.element.className = 'glass-navbar';
@@ -45,6 +70,12 @@ export class Navbar {
45
  <span class="tab-label">VIEW:</span>
46
  <button data-view="ANALYSIS" class="view-tab${viewActive('ANALYSIS')}">ANALYSIS</button>
47
  <button data-view="NAVIGATION" class="view-tab${viewActive('NAVIGATION')}">NAVIGATION</button>
 
 
 
 
 
 
48
  </div>
49
 
50
  <div class="render-mode-tabs">
@@ -67,6 +98,8 @@ export class Navbar {
67
  this.renderTabs = this.element.querySelectorAll('.mode-tab');
68
  this.viewTabs = this.element.querySelectorAll('.view-tab');
69
  this.workspaceTabs = this.element.querySelectorAll('.workspace-tab');
 
 
70
  this.statusDot = this.element.querySelector('#backend-status-dot');
71
  this.statusText = this.element.querySelector('#backend-status-text');
72
  this.tabsTrack = this.element.querySelector('.navbar-center-controls');
@@ -79,41 +112,48 @@ export class Navbar {
79
  }
80
 
81
  initEventListeners() {
82
- this.workspaceTabs.forEach(tab => {
83
  tab.addEventListener('click', (e) => {
84
- const mode = e.target.getAttribute('data-workspace');
85
- this.workspaceTabs.forEach(t => t.classList.remove('active'));
86
- e.target.classList.add('active');
87
-
88
  if (this.onWorkspaceModeChange) {
89
  this.onWorkspaceModeChange(mode);
90
  }
91
  });
92
  });
93
 
94
- this.renderTabs.forEach(tab => {
95
  tab.addEventListener('click', (e) => {
96
- const mode = e.target.getAttribute('data-mode');
97
- this.renderTabs.forEach(t => t.classList.remove('active'));
98
- e.target.classList.add('active');
99
-
100
  if (this.onRenderModeChange) {
101
  this.onRenderModeChange(mode);
102
  }
103
  });
104
  });
105
 
106
- this.viewTabs.forEach(tab => {
107
  tab.addEventListener('click', (e) => {
108
- const view = e.target.getAttribute('data-view');
109
- this.viewTabs.forEach(t => t.classList.remove('active'));
110
- e.target.classList.add('active');
111
-
112
  if (this.onViewModeChange) {
113
  this.onViewModeChange(view);
114
  }
115
  });
116
  });
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
 
119
  initTabsScroll() {
@@ -137,7 +177,6 @@ export class Navbar {
137
  this._tabsResizeObserver.observe(this.tabsScroller);
138
  }
139
 
140
- // Layout may settle after fonts / first paint
141
  requestAnimationFrame(sync);
142
  window.addEventListener('resize', sync);
143
  this._tabsResizeHandler = sync;
@@ -173,17 +212,18 @@ export class Navbar {
173
  }
174
 
175
  setViewMode(viewMode) {
176
- this.viewTabs.forEach(t => {
177
  if (t.getAttribute('data-view') === viewMode) {
178
  t.classList.add('active');
179
  } else {
180
  t.classList.remove('active');
181
  }
182
  });
 
183
  }
184
 
185
  setWorkspaceMode(workspaceMode) {
186
- this.workspaceTabs.forEach(t => {
187
  if (t.getAttribute('data-workspace') === workspaceMode) {
188
  t.classList.add('active');
189
  } else {
@@ -192,10 +232,54 @@ export class Navbar {
192
  });
193
  }
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  setStatus(online, modelName = '', device = '') {
196
  if (online) {
197
  this.statusDot.className = 'status-dot online';
198
- // Inline format keeps Navbar free of circular imports with arithmeticDefaults
199
  const model = String(modelName || '').trim();
200
  const dev = String(device || '').trim().toLowerCase();
201
  let label = 'ONLINE';
 
1
  /**
2
+ * Top Navbar: MODE | VIEW (ANALYSIS | NAVIGATION | GALAXY) | RENDER.
3
+ * Galaxy: method chips (UMAP active; PCA/t-SNE disabled) + MODE/RENDER lock.
4
  * Mobile: overflow tab strip gets ◀ ▶ scroll arrows.
5
  */
6
  import { getTabsScrollState, nextTabsScrollLeft } from './navbarTabsScroll.js';
 
9
  DEFAULT_VIEW_MODE,
10
  DEFAULT_WORKSPACE_MODE,
11
  } from './appViewDefaults.js';
12
+ import {
13
+ DEFAULT_GALAXY_METHOD,
14
+ GALAXY_METHODS,
15
+ GALAXY_VIEW,
16
+ isGalaxyView,
17
+ } from './galaxyChrome.js';
18
 
19
  export class Navbar {
20
+ constructor(
21
+ containerElement,
22
+ onRenderModeChangeCallback,
23
+ onViewModeChangeCallback,
24
+ onWorkspaceModeChangeCallback,
25
+ onGalaxyMethodChangeCallback,
26
+ ) {
27
  this.container = containerElement || document.body;
28
  this.onRenderModeChange = onRenderModeChangeCallback;
29
  this.onViewModeChange = onViewModeChangeCallback;
30
  this.onWorkspaceModeChange = onWorkspaceModeChangeCallback;
31
+ this.onGalaxyMethodChange = onGalaxyMethodChangeCallback;
32
+ this._modeRenderLocked = false;
33
+ this._galaxyMethod = DEFAULT_GALAXY_METHOD;
34
 
35
  const workspaceActive = (mode) => (mode === DEFAULT_WORKSPACE_MODE ? ' active' : '');
36
  const viewActive = (mode) => (mode === DEFAULT_VIEW_MODE ? ' active' : '');
37
  const renderActive = (mode) => (mode === DEFAULT_RENDER_MODE ? ' active' : '');
38
 
39
+ const methodChipsHtml = Object.values(GALAXY_METHODS)
40
+ .map((m) => {
41
+ const disabled = m.enabled ? '' : ' disabled';
42
+ const title = m.title ? ` title="${m.title}"` : '';
43
+ const active = m.id === DEFAULT_GALAXY_METHOD ? ' active' : '';
44
+ return `<button type="button" data-galaxy-method="${m.id}" class="galaxy-method-chip${active}"${disabled}${title}>${m.label}</button>`;
45
+ })
46
+ .join('');
47
+
48
  this.element = document.createElement('header');
49
  this.element.id = 'top-navbar';
50
  this.element.className = 'glass-navbar';
 
70
  <span class="tab-label">VIEW:</span>
71
  <button data-view="ANALYSIS" class="view-tab${viewActive('ANALYSIS')}">ANALYSIS</button>
72
  <button data-view="NAVIGATION" class="view-tab${viewActive('NAVIGATION')}">NAVIGATION</button>
73
+ <div class="galaxy-view-cluster">
74
+ <button data-view="${GALAXY_VIEW}" class="view-tab${viewActive(GALAXY_VIEW)}">GALAXY</button>
75
+ <div class="galaxy-method-chips" hidden aria-label="Galaxy projection method">
76
+ ${methodChipsHtml}
77
+ </div>
78
+ </div>
79
  </div>
80
 
81
  <div class="render-mode-tabs">
 
98
  this.renderTabs = this.element.querySelectorAll('.mode-tab');
99
  this.viewTabs = this.element.querySelectorAll('.view-tab');
100
  this.workspaceTabs = this.element.querySelectorAll('.workspace-tab');
101
+ this.galaxyMethodChips = this.element.querySelector('.galaxy-method-chips');
102
+ this.galaxyMethodButtons = this.element.querySelectorAll('[data-galaxy-method]');
103
  this.statusDot = this.element.querySelector('#backend-status-dot');
104
  this.statusText = this.element.querySelector('#backend-status-text');
105
  this.tabsTrack = this.element.querySelector('.navbar-center-controls');
 
112
  }
113
 
114
  initEventListeners() {
115
+ this.workspaceTabs.forEach((tab) => {
116
  tab.addEventListener('click', (e) => {
117
+ if (this._modeRenderLocked || tab.disabled) return;
118
+ const mode = e.currentTarget.getAttribute('data-workspace');
119
+ this.setWorkspaceMode(mode);
 
120
  if (this.onWorkspaceModeChange) {
121
  this.onWorkspaceModeChange(mode);
122
  }
123
  });
124
  });
125
 
126
+ this.renderTabs.forEach((tab) => {
127
  tab.addEventListener('click', (e) => {
128
+ if (this._modeRenderLocked || tab.disabled) return;
129
+ const mode = e.currentTarget.getAttribute('data-mode');
130
+ this.setRenderMode(mode);
 
131
  if (this.onRenderModeChange) {
132
  this.onRenderModeChange(mode);
133
  }
134
  });
135
  });
136
 
137
+ this.viewTabs.forEach((tab) => {
138
  tab.addEventListener('click', (e) => {
139
+ const view = e.currentTarget.getAttribute('data-view');
140
+ this.setViewMode(view);
 
 
141
  if (this.onViewModeChange) {
142
  this.onViewModeChange(view);
143
  }
144
  });
145
  });
146
+
147
+ this.galaxyMethodButtons.forEach((btn) => {
148
+ btn.addEventListener('click', (e) => {
149
+ if (btn.disabled) return;
150
+ const method = e.currentTarget.getAttribute('data-galaxy-method');
151
+ this.setGalaxyMethod(method);
152
+ if (this.onGalaxyMethodChange) {
153
+ this.onGalaxyMethodChange(method);
154
+ }
155
+ });
156
+ });
157
  }
158
 
159
  initTabsScroll() {
 
177
  this._tabsResizeObserver.observe(this.tabsScroller);
178
  }
179
 
 
180
  requestAnimationFrame(sync);
181
  window.addEventListener('resize', sync);
182
  this._tabsResizeHandler = sync;
 
212
  }
213
 
214
  setViewMode(viewMode) {
215
+ this.viewTabs.forEach((t) => {
216
  if (t.getAttribute('data-view') === viewMode) {
217
  t.classList.add('active');
218
  } else {
219
  t.classList.remove('active');
220
  }
221
  });
222
+ this.setGalaxyMethodChipsVisible(isGalaxyView(viewMode));
223
  }
224
 
225
  setWorkspaceMode(workspaceMode) {
226
+ this.workspaceTabs.forEach((t) => {
227
  if (t.getAttribute('data-workspace') === workspaceMode) {
228
  t.classList.add('active');
229
  } else {
 
232
  });
233
  }
234
 
235
+ setRenderMode(renderMode) {
236
+ this.renderTabs.forEach((t) => {
237
+ if (t.getAttribute('data-mode') === renderMode) {
238
+ t.classList.add('active');
239
+ } else {
240
+ t.classList.remove('active');
241
+ }
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Lock MODE + RENDER while in Galaxy (COMPARE + POINTS forced).
247
+ * @param {boolean} locked
248
+ */
249
+ setModeRenderLocked(locked) {
250
+ this._modeRenderLocked = !!locked;
251
+ this.workspaceTabs.forEach((t) => {
252
+ t.disabled = this._modeRenderLocked;
253
+ t.classList.toggle('tab-locked', this._modeRenderLocked);
254
+ t.setAttribute('aria-disabled', this._modeRenderLocked ? 'true' : 'false');
255
+ });
256
+ this.renderTabs.forEach((t) => {
257
+ t.disabled = this._modeRenderLocked;
258
+ t.classList.toggle('tab-locked', this._modeRenderLocked);
259
+ t.setAttribute('aria-disabled', this._modeRenderLocked ? 'true' : 'false');
260
+ });
261
+ }
262
+
263
+ setGalaxyMethodChipsVisible(visible) {
264
+ if (!this.galaxyMethodChips) return;
265
+ if (visible) {
266
+ this.galaxyMethodChips.removeAttribute('hidden');
267
+ } else {
268
+ this.galaxyMethodChips.setAttribute('hidden', '');
269
+ }
270
+ }
271
+
272
+ setGalaxyMethod(method) {
273
+ this._galaxyMethod = method || DEFAULT_GALAXY_METHOD;
274
+ this.galaxyMethodButtons.forEach((btn) => {
275
+ const id = btn.getAttribute('data-galaxy-method');
276
+ btn.classList.toggle('active', id === this._galaxyMethod);
277
+ });
278
+ }
279
+
280
  setStatus(online, modelName = '', device = '') {
281
  if (online) {
282
  this.statusDot.className = 'status-dot online';
 
283
  const model = String(modelName || '').trim();
284
  const dev = String(device || '').trim().toLowerCase();
285
  let label = 'ONLINE';
src/ui/galaxyChrome.js ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Galaxy VIEW chrome — triad lock/restore (no layout / no /project).
3
+ * Entering Galaxy forces COMPARE + POINTS; leaving restores the pre-Galaxy triad.
4
+ */
5
+
6
+ export const GALAXY_VIEW = 'GALAXY';
7
+ export const DEFAULT_GALAXY_METHOD = 'umap';
8
+
9
+ /** Methods shown under GALAXY; only umap is enabled in v1. */
10
+ export const GALAXY_METHODS = Object.freeze({
11
+ umap: { id: 'umap', label: 'UMAP', enabled: true },
12
+ pca: {
13
+ id: 'pca',
14
+ label: 'PCA',
15
+ enabled: false,
16
+ title: 'Coming next — PCA / t-SNE projection',
17
+ },
18
+ tsne: {
19
+ id: 'tsne',
20
+ label: 't-SNE',
21
+ enabled: false,
22
+ title: 'Coming next — PCA / t-SNE projection',
23
+ },
24
+ });
25
+
26
+ /**
27
+ * @param {string} [viewMode]
28
+ * @returns {boolean}
29
+ */
30
+ export function isGalaxyView(viewMode) {
31
+ return viewMode === GALAXY_VIEW;
32
+ }
33
+
34
+ /**
35
+ * @param {{ workspaceMode?: string, viewMode?: string, renderMode?: string }} triad
36
+ * @returns {{ workspaceMode: string, viewMode: string, renderMode: string }}
37
+ */
38
+ export function snapshotTriad(triad = {}) {
39
+ return {
40
+ workspaceMode: triad.workspaceMode || 'ARITHMETIC',
41
+ viewMode: triad.viewMode || 'ANALYSIS',
42
+ renderMode: triad.renderMode || 'POINTS',
43
+ };
44
+ }
45
+
46
+ /**
47
+ * @param {{ workspaceMode?: string, viewMode?: string, renderMode?: string }} prev
48
+ * @returns {{
49
+ * viewMode: string,
50
+ * workspaceMode: string,
51
+ * renderMode: string,
52
+ * method: string,
53
+ * modeRenderLocked: boolean,
54
+ * restore: { workspaceMode: string, viewMode: string, renderMode: string },
55
+ * }}
56
+ */
57
+ export function enterGalaxyChrome(prev = {}) {
58
+ const restore = snapshotTriad(prev);
59
+ return {
60
+ viewMode: GALAXY_VIEW,
61
+ workspaceMode: 'COMPARE',
62
+ renderMode: 'POINTS',
63
+ method: DEFAULT_GALAXY_METHOD,
64
+ modeRenderLocked: true,
65
+ restore,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * @param {{ workspaceMode?: string, viewMode?: string, renderMode?: string }|null|undefined} restore
71
+ * @param {string} nextView ANALYSIS | NAVIGATION (or anything non-GALAXY)
72
+ * @returns {{
73
+ * viewMode: string,
74
+ * workspaceMode: string,
75
+ * renderMode: string,
76
+ * modeRenderLocked: boolean,
77
+ * }}
78
+ */
79
+ export function leaveGalaxyChrome(restore, nextView) {
80
+ const snap = snapshotTriad(restore || {});
81
+ const viewMode = nextView === GALAXY_VIEW ? 'ANALYSIS' : nextView;
82
+ return {
83
+ viewMode,
84
+ workspaceMode: snap.workspaceMode,
85
+ renderMode: snap.renderMode,
86
+ modeRenderLocked: false,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * @param {string} [method]
92
+ * @returns {boolean}
93
+ */
94
+ export function isGalaxyMethodEnabled(method) {
95
+ const m = GALAXY_METHODS[method];
96
+ return !!(m && m.enabled);
97
+ }
tests/galaxyChrome.test.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ DEFAULT_GALAXY_METHOD,
4
+ GALAXY_METHODS,
5
+ GALAXY_VIEW,
6
+ enterGalaxyChrome,
7
+ isGalaxyMethodEnabled,
8
+ isGalaxyView,
9
+ leaveGalaxyChrome,
10
+ snapshotTriad,
11
+ } from '../src/ui/galaxyChrome.js';
12
+ import {
13
+ DEFAULT_RENDER_MODE,
14
+ DEFAULT_VIEW_MODE,
15
+ DEFAULT_WORKSPACE_MODE,
16
+ } from '../src/ui/appViewDefaults.js';
17
+
18
+ describe('galaxyChrome', () => {
19
+ it('isGalaxyView only for GALAXY', () => {
20
+ expect(isGalaxyView(GALAXY_VIEW)).toBe(true);
21
+ expect(isGalaxyView('ANALYSIS')).toBe(false);
22
+ expect(isGalaxyView('NAVIGATION')).toBe(false);
23
+ });
24
+
25
+ it('enterGalaxyChrome forces COMPARE + POINTS and locks MODE/RENDER', () => {
26
+ const entered = enterGalaxyChrome({
27
+ workspaceMode: 'ARITHMETIC',
28
+ viewMode: 'ANALYSIS',
29
+ renderMode: 'RIBBONS',
30
+ });
31
+ expect(entered.viewMode).toBe(GALAXY_VIEW);
32
+ expect(entered.workspaceMode).toBe('COMPARE');
33
+ expect(entered.renderMode).toBe('POINTS');
34
+ expect(entered.method).toBe(DEFAULT_GALAXY_METHOD);
35
+ expect(entered.modeRenderLocked).toBe(true);
36
+ expect(entered.restore).toEqual({
37
+ workspaceMode: 'ARITHMETIC',
38
+ viewMode: 'ANALYSIS',
39
+ renderMode: 'RIBBONS',
40
+ });
41
+ });
42
+
43
+ it('leaveGalaxyChrome restores pre-Galaxy triad and unlocks', () => {
44
+ const entered = enterGalaxyChrome({
45
+ workspaceMode: 'COMPARE',
46
+ viewMode: 'NAVIGATION',
47
+ renderMode: 'RIBBONS',
48
+ });
49
+ const left = leaveGalaxyChrome(entered.restore, 'ANALYSIS');
50
+ expect(left.viewMode).toBe('ANALYSIS');
51
+ expect(left.workspaceMode).toBe('COMPARE');
52
+ expect(left.renderMode).toBe('RIBBONS');
53
+ expect(left.modeRenderLocked).toBe(false);
54
+ });
55
+
56
+ it('leaveGalaxyChrome to NAVIGATION keeps restored MODE/RENDER', () => {
57
+ const left = leaveGalaxyChrome(
58
+ {
59
+ workspaceMode: 'ARITHMETIC',
60
+ viewMode: 'ANALYSIS',
61
+ renderMode: 'POINTS',
62
+ },
63
+ 'NAVIGATION',
64
+ );
65
+ expect(left.viewMode).toBe('NAVIGATION');
66
+ expect(left.workspaceMode).toBe('ARITHMETIC');
67
+ expect(left.renderMode).toBe('POINTS');
68
+ });
69
+
70
+ it('PCA and t-SNE chips are disabled with coming-next title; UMAP enabled', () => {
71
+ expect(isGalaxyMethodEnabled('umap')).toBe(true);
72
+ expect(isGalaxyMethodEnabled('pca')).toBe(false);
73
+ expect(isGalaxyMethodEnabled('tsne')).toBe(false);
74
+ expect(GALAXY_METHODS.pca.title).toMatch(/Coming next/i);
75
+ expect(GALAXY_METHODS.tsne.title).toMatch(/Coming next/i);
76
+ });
77
+
78
+ it('startup defaults stay ARITHMETIC | ANALYSIS | POINTS (Galaxy is opt-in)', () => {
79
+ expect(DEFAULT_WORKSPACE_MODE).toBe('ARITHMETIC');
80
+ expect(DEFAULT_VIEW_MODE).toBe('ANALYSIS');
81
+ expect(DEFAULT_RENDER_MODE).toBe('POINTS');
82
+ expect(snapshotTriad({})).toEqual({
83
+ workspaceMode: 'ARITHMETIC',
84
+ viewMode: 'ANALYSIS',
85
+ renderMode: 'POINTS',
86
+ });
87
+ });
88
+ });