@mongoosejs/studio 0.2.13 → 0.3.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.
Files changed (81) hide show
  1. package/backend/actions/ChatMessage/executeScript.js +5 -1
  2. package/backend/actions/ChatThread/createChatMessage.js +2 -1
  3. package/backend/actions/ChatThread/streamChatMessage.js +2 -2
  4. package/eslint.config.js +4 -1
  5. package/frontend/public/app.js +24642 -543
  6. package/frontend/public/dark-theme.css +365 -0
  7. package/frontend/public/images/mongoose-studio.svg +4 -0
  8. package/frontend/public/index.html +21 -1
  9. package/frontend/public/style.css +5 -7
  10. package/frontend/public/theme-variables.css +294 -0
  11. package/frontend/public/tw.css +305 -252
  12. package/frontend/src/ace-editor/ace-editor.html +4 -0
  13. package/frontend/src/ace-editor/ace-editor.js +89 -0
  14. package/frontend/src/aceEditor.js +69 -0
  15. package/frontend/src/chat/chat-message/chat-message.html +1 -1
  16. package/frontend/src/chat/chat-message/chat-message.js +1 -1
  17. package/frontend/src/chat/chat-message-script/chat-message-script.html +51 -34
  18. package/frontend/src/chat/chat-message-script/chat-message-script.js +12 -55
  19. package/frontend/src/chat/chat.html +68 -39
  20. package/frontend/src/chat/chat.js +26 -2
  21. package/frontend/src/clone-document/clone-document.html +7 -2
  22. package/frontend/src/clone-document/clone-document.js +1 -8
  23. package/frontend/src/create-dashboard/create-dashboard.html +11 -6
  24. package/frontend/src/create-dashboard/create-dashboard.js +0 -7
  25. package/frontend/src/create-document/create-document.html +15 -9
  26. package/frontend/src/create-document/create-document.js +5 -12
  27. package/frontend/src/dashboard/dashboard.html +14 -12
  28. package/frontend/src/dashboard/dashboard.js +12 -4
  29. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.html +13 -7
  30. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +13 -21
  31. package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html +19 -17
  32. package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js +97 -2
  33. package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +27 -3
  34. package/frontend/src/dashboard-result/dashboard-result.html +3 -3
  35. package/frontend/src/dashboards/dashboards.html +101 -109
  36. package/frontend/src/dashboards/dashboards.js +25 -1
  37. package/frontend/src/detail-default/detail-default.html +2 -2
  38. package/frontend/src/detail-default/detail-default.js +24 -3
  39. package/frontend/src/document/confirm-changes/confirm-changes.html +1 -1
  40. package/frontend/src/document/confirm-delete/confirm-delete.html +1 -1
  41. package/frontend/src/document/document.css +1 -1
  42. package/frontend/src/document/document.html +28 -28
  43. package/frontend/src/document/execute-script/execute-script.html +20 -21
  44. package/frontend/src/document/execute-script/execute-script.js +1 -43
  45. package/frontend/src/document-details/document-details.css +4 -9
  46. package/frontend/src/document-details/document-details.html +34 -33
  47. package/frontend/src/document-details/document-details.js +2 -53
  48. package/frontend/src/document-details/document-property/document-property.html +12 -12
  49. package/frontend/src/edit-array/edit-array.html +7 -6
  50. package/frontend/src/edit-array/edit-array.js +10 -50
  51. package/frontend/src/edit-boolean/edit-boolean.html +12 -12
  52. package/frontend/src/edit-date/edit-date.html +2 -2
  53. package/frontend/src/edit-default/edit-default.html +1 -1
  54. package/frontend/src/edit-string/edit-string.html +3 -3
  55. package/frontend/src/edit-subdocument/edit-subdocument.html +5 -3
  56. package/frontend/src/edit-subdocument/edit-subdocument.js +1 -15
  57. package/frontend/src/export-query-results/export-query-results.html +3 -3
  58. package/frontend/src/json-node/json-node.html +3 -3
  59. package/frontend/src/list-json/json-node.html +1 -1
  60. package/frontend/src/models/document-search/document-search.html +3 -3
  61. package/frontend/src/models/model-switcher/model-switcher.html +53 -0
  62. package/frontend/src/models/model-switcher/model-switcher.js +123 -0
  63. package/frontend/src/models/models.css +3 -10
  64. package/frontend/src/models/models.html +146 -80
  65. package/frontend/src/models/models.js +108 -4
  66. package/frontend/src/navbar/navbar.html +157 -97
  67. package/frontend/src/navbar/navbar.js +31 -12
  68. package/frontend/src/routes.js +1 -1
  69. package/frontend/src/splash/splash.html +5 -5
  70. package/frontend/src/task-single/task-single.html +29 -29
  71. package/frontend/src/task-single/task-single.js +10 -10
  72. package/frontend/src/tasks/task-details/task-details.html +38 -38
  73. package/frontend/src/tasks/task-details/task-details.js +7 -2
  74. package/frontend/src/tasks/tasks.html +36 -35
  75. package/frontend/src/tasks/tasks.js +2 -25
  76. package/frontend/src/team/new-invitation/new-invitation.html +8 -8
  77. package/frontend/src/team/team.html +27 -27
  78. package/frontend/src/update-document/update-document.html +7 -2
  79. package/frontend/src/update-document/update-document.js +2 -11
  80. package/package.json +2 -1
  81. package/tailwind.config.js +75 -11
@@ -13,6 +13,8 @@ appendCSS(require('./models.css'));
13
13
  const limit = 20;
14
14
  const OUTPUT_TYPE_STORAGE_KEY = 'studio:model-output-type';
15
15
  const SELECTED_GEO_FIELD_STORAGE_KEY = 'studio:model-selected-geo-field';
16
+ const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
17
+ const MAX_RECENT_MODELS = 4;
16
18
 
17
19
  module.exports = app => app.component('models', {
18
20
  template: template,
@@ -51,21 +53,28 @@ module.exports = app => app.component('models', {
51
53
  selectedGeoField: null,
52
54
  mapInstance: null,
53
55
  mapLayer: null,
56
+ mapTileLayer: null,
54
57
  hideSidebar: null,
55
58
  lastSelectedIndex: null,
56
59
  error: null,
57
60
  showActionsMenu: false,
58
- collectionInfo: null
61
+ collectionInfo: null,
62
+ modelSearch: '',
63
+ recentlyViewedModels: [],
64
+ showModelSwitcher: false
59
65
  }),
60
66
  created() {
61
67
  this.currentModel = this.model;
62
68
  this.loadOutputPreference();
63
69
  this.loadSelectedGeoField();
70
+ this.loadRecentlyViewedModels();
64
71
  },
65
72
  beforeDestroy() {
66
73
  document.removeEventListener('scroll', this.onScroll, true);
67
74
  window.removeEventListener('popstate', this.onPopState, true);
68
75
  document.removeEventListener('click', this.onOutsideActionsMenuClick, true);
76
+ document.documentElement.removeEventListener('studio-theme-changed', this.onStudioThemeChanged);
77
+ document.removeEventListener('keydown', this.onCtrlP, true);
69
78
  this.destroyMap();
70
79
  },
71
80
  async mounted() {
@@ -83,6 +92,15 @@ module.exports = app => app.component('models', {
83
92
  }
84
93
  };
85
94
  document.addEventListener('click', this.onOutsideActionsMenuClick, true);
95
+ this.onStudioThemeChanged = () => this.updateMapTileLayer();
96
+ document.documentElement.addEventListener('studio-theme-changed', this.onStudioThemeChanged);
97
+ this.onCtrlP = (event) => {
98
+ if ((event.ctrlKey || event.metaKey) && event.key === 'p') {
99
+ event.preventDefault();
100
+ this.openModelSwitcher();
101
+ }
102
+ };
103
+ document.addEventListener('keydown', this.onCtrlP, true);
86
104
  const { models, readyState } = await api.Model.listModels();
87
105
  this.models = models;
88
106
  await this.loadModelCounts();
@@ -140,6 +158,21 @@ module.exports = app => app.component('models', {
140
158
  }
141
159
  return map;
142
160
  },
161
+ filteredModels() {
162
+ if (!this.modelSearch.trim()) {
163
+ return this.models;
164
+ }
165
+ const search = this.modelSearch.trim().toLowerCase();
166
+ return this.models.filter(m => m.toLowerCase().includes(search));
167
+ },
168
+ filteredRecentModels() {
169
+ const recent = this.recentlyViewedModels.filter(m => this.models.includes(m));
170
+ if (!this.modelSearch.trim()) {
171
+ return recent;
172
+ }
173
+ const search = this.modelSearch.trim().toLowerCase();
174
+ return recent.filter(m => m.toLowerCase().includes(search));
175
+ },
143
176
  geoJsonFields() {
144
177
  // Find schema paths that look like GeoJSON fields
145
178
  // GeoJSON fields have nested 'type' and 'coordinates' properties
@@ -192,6 +225,49 @@ module.exports = app => app.component('models', {
192
225
  }
193
226
  },
194
227
  methods: {
228
+ highlightMatch(model) {
229
+ const search = this.modelSearch.trim();
230
+ if (!search) {
231
+ return model;
232
+ }
233
+ const idx = model.toLowerCase().indexOf(search.toLowerCase());
234
+ if (idx === -1) {
235
+ return model;
236
+ }
237
+ const before = model.slice(0, idx);
238
+ const match = model.slice(idx, idx + search.length);
239
+ const after = model.slice(idx + search.length);
240
+ return `${xss(before)}<strong>${xss(match)}</strong>${xss(after)}`;
241
+ },
242
+ loadRecentlyViewedModels() {
243
+ if (typeof window === 'undefined' || !window.localStorage) {
244
+ return;
245
+ }
246
+ try {
247
+ const stored = window.localStorage.getItem(RECENTLY_VIEWED_MODELS_KEY);
248
+ if (stored) {
249
+ const parsed = JSON.parse(stored);
250
+ if (Array.isArray(parsed)) {
251
+ this.recentlyViewedModels = parsed.map(item => String(item)).slice(0, MAX_RECENT_MODELS);
252
+ } else {
253
+ this.recentlyViewedModels = [];
254
+ }
255
+ }
256
+ } catch (err) {
257
+ this.recentlyViewedModels = [];
258
+ }
259
+ },
260
+ trackRecentModel(model) {
261
+ if (!model) {
262
+ return;
263
+ }
264
+ const filtered = this.recentlyViewedModels.filter(m => m !== model);
265
+ filtered.unshift(model);
266
+ this.recentlyViewedModels = filtered.slice(0, MAX_RECENT_MODELS);
267
+ if (typeof window !== 'undefined' && window.localStorage) {
268
+ window.localStorage.setItem(RECENTLY_VIEWED_MODELS_KEY, JSON.stringify(this.recentlyViewedModels));
269
+ }
270
+ },
195
271
  loadOutputPreference() {
196
272
  if (typeof window === 'undefined' || !window.localStorage) {
197
273
  return;
@@ -265,9 +341,7 @@ module.exports = app => app.component('models', {
265
341
  mapElement.style.setProperty('width', '100%', 'important');
266
342
 
267
343
  this.mapInstance = L.map(this.$refs.modelsMap).setView([0, 0], 2);
268
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
269
- attribution: '&copy; OpenStreetMap contributors'
270
- }).addTo(this.mapInstance);
344
+ this.updateMapTileLayer();
271
345
 
272
346
  this.$nextTick(() => {
273
347
  if (this.mapInstance) {
@@ -276,11 +350,30 @@ module.exports = app => app.component('models', {
276
350
  }
277
351
  });
278
352
  },
353
+ getMapTileLayerOptions() {
354
+ const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
355
+ return isDark
356
+ ? { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20 }
357
+ : { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '&copy; OpenStreetMap contributors' };
358
+ },
359
+ updateMapTileLayer() {
360
+ if (!this.mapInstance || typeof L === 'undefined') return;
361
+ if (this.mapTileLayer) {
362
+ this.mapTileLayer.remove();
363
+ this.mapTileLayer = null;
364
+ }
365
+ const opts = this.getMapTileLayerOptions();
366
+ this.mapTileLayer = L.tileLayer(opts.url, opts).addTo(this.mapInstance);
367
+ },
279
368
  destroyMap() {
280
369
  if (this.mapLayer) {
281
370
  this.mapLayer.remove();
282
371
  this.mapLayer = null;
283
372
  }
373
+ if (this.mapTileLayer) {
374
+ this.mapTileLayer.remove();
375
+ this.mapTileLayer = null;
376
+ }
284
377
  if (this.mapInstance) {
285
378
  this.mapInstance.remove();
286
379
  this.mapInstance = null;
@@ -620,6 +713,9 @@ module.exports = app => app.component('models', {
620
713
  }
621
714
  },
622
715
  async getDocuments() {
716
+ // Track recently viewed model
717
+ this.trackRecentModel(this.currentModel);
718
+
623
719
  // Clear previous data
624
720
  this.documents = [];
625
721
  this.schemaPaths = [];
@@ -864,6 +960,14 @@ module.exports = app => app.component('models', {
864
960
  this.selectMultiple = true;
865
961
  }
866
962
  },
963
+ openModelSwitcher() {
964
+ this.showModelSwitcher = true;
965
+ },
966
+ selectSwitcherModel(model) {
967
+ this.showModelSwitcher = false;
968
+ this.trackRecentModel(model);
969
+ this.$router.push('/model/' + model);
970
+ },
867
971
  async loadModelCounts() {
868
972
  if (!Array.isArray(this.models) || this.models.length === 0) {
869
973
  return;
@@ -1,12 +1,67 @@
1
- <div class="navbar w-full bg-white flex justify-between border-b border-gray-200 !h-[55px]">
2
- <div class="flex items-center gap-4 md:gap-6 h-full pl-4">
3
- <router-link :to="{ name: defaultRoute }">
4
- <img src="images/logo.svg" class="h-10 mr-1" alt="Mongoose Studio Logo" />
1
+ <div class="navbar w-full bg-page border-b border-edge !h-[55px] hidden md:grid grid-cols-[1fr_auto_1fr] items-stretch px-4">
2
+ <!-- Left: Logo + Brand -->
3
+ <div class="flex items-center gap-2 self-center">
4
+ <router-link class="flex items-center gap-2" :to="{ name: defaultRoute }">
5
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-10 text-primary" viewBox="0 0 250 250" fill="currentColor" aria-label="Mongoose Studio Logo">
6
+ <path d="M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z"/>
7
+ <path d="M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z"/>
8
+ </svg>
9
+ <span class="text-base font-semibold text-content whitespace-nowrap">Mongoose Studio</span>
5
10
  </router-link>
11
+ </div>
12
+ <!-- Center: Nav Links -->
13
+ <nav class="flex items-stretch gap-6">
14
+ <a v-if="hasAccess(roles, 'root')"
15
+ href="#/"
16
+ class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
17
+ :class="documentView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">Documents</a>
18
+ <span v-else class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed" aria-disabled="true">
19
+ Documents
20
+ <svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
21
+ <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
22
+ </svg>
23
+ </span>
24
+ <a v-if="hasAccess(roles, 'dashboards')"
25
+ href="#/dashboards"
26
+ class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
27
+ :class="dashboardView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">Dashboards</a>
28
+ <span v-else class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed">
29
+ Dashboards
30
+ <svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
31
+ <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
32
+ </svg>
33
+ </span>
34
+ <a v-if="hasAccess(roles, 'chat')"
35
+ href="#/chat"
36
+ class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
37
+ :class="chatView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">Chat</a>
38
+ <span v-else class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed">
39
+ Chat
40
+ <svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
41
+ <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
42
+ </svg>
43
+ </span>
44
+ <a
45
+ :href="hasTaskVisualizer"
46
+ class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
47
+ :class="taskView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">
48
+ Tasks
49
+ </a>
50
+ <a
51
+ href="https://studio.mongoosejs.io/docs"
52
+ target="_blank"
53
+ rel="noopener noreferrer"
54
+ class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-content-tertiary hover:text-content"
55
+ >
56
+ Docs
57
+ </a>
58
+ </nav>
59
+ <!-- Right: Env Badge + User -->
60
+ <div class="flex items-center justify-end gap-3 self-center">
6
61
  <div
7
62
  v-if="!!state.nodeEnv"
8
63
  title="NODE_ENV"
9
- class="inline-flex items-center rounded px-2 py-1 text-xs text-gray-500 bg-white border border-gray-200 gap-2"
64
+ class="inline-flex items-center rounded px-2 py-1 text-xs text-content-tertiary bg-surface border border-edge gap-2"
10
65
  >
11
66
  <span
12
67
  :class="warnEnv ? 'bg-red-400' : 'bg-yellow-400'"
@@ -15,94 +70,77 @@
15
70
  ></span>
16
71
  <span>{{state.nodeEnv}}</span>
17
72
  </div>
18
- </div>
19
- <div class="h-full pr-4 hidden md:block">
20
- <div class="sm:ml-6 sm:flex sm:space-x-8 h-full">
21
- <a v-if="hasAccess(roles, 'root')"
22
- href="#/"
23
- class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
24
- :class="documentView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Documents</a>
25
- <span v-else class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium text-gray-300 cursor-not-allowed" aria-disabled="true">
26
- Documents
27
- <svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
28
- <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
29
- </svg>
30
- </span>
31
- <a v-if="hasAccess(roles, 'dashboards')"
32
- href="#/dashboards"
33
- class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
34
- :class="dashboardView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Dashboards</a>
35
- <span v-else class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 cursor-not-allowed">
36
- Dashboards
37
- <svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
38
- <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
39
- </svg>
40
- </span>
41
- <a v-if="hasAccess(roles, 'chat')"
42
- href="#/chat"
43
- class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
44
- :class="chatView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Chat</a>
45
- <span v-else class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium text-gray-300 cursor-not-allowed">
46
- Chat
47
- <svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
48
- <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
49
- </svg>
50
- </span>
51
- <a
52
- :href="hasTaskVisualizer"
53
- class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
54
- :class="taskView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">
55
- Tasks
56
- </a>
57
- <a
58
- href="https://studio.mongoosejs.io/docs"
59
- target="_blank"
60
- rel="noopener noreferrer"
61
- class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 border-transparent"
62
- >
63
- Docs
64
- <svg xmlns="http://www.w3.org/2000/svg" class="ml-1 h-4 w-4" viewBox="0 -960 960 960" fill="currentColor"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/></svg>
65
- </a>
73
+ <button
74
+ v-if="!user || !hasAPIKey"
75
+ type="button"
76
+ @click="toggleDarkMode"
77
+ :title="darkMode ? 'Switch to light mode' : 'Switch to dark mode'"
78
+ class="inline-flex items-center justify-center rounded-md p-2 text-content-tertiary hover:text-content-secondary hover:bg-muted"
79
+ aria-label="Toggle dark mode">
80
+ <svg v-if="darkMode" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
81
+ <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
82
+ </svg>
83
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
84
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
85
+ </svg>
86
+ </button>
87
+ <div class="flex items-center" v-if="!user && hasAPIKey">
88
+ <button
89
+ type="button"
90
+ @click="loginWithGithub"
91
+ class="rounded bg-primary px-2 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
92
+ Login
93
+ </button>
94
+ </div>
95
+ <div v-if="user && hasAPIKey" class="flex items-center relative" v-clickOutside="hideFlyout">
96
+ <button type="button" @click="showFlyout = !showFlyout" class="relative flex rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-gray-300" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
97
+ <span class="absolute -inset-1.5"></span>
98
+ <span class="sr-only">Open user menu</span>
99
+ <img class="size-8 rounded-full" :src="user.picture" alt="">
100
+ </button>
66
101
 
67
- <div class="h-full flex items-center" v-if="!user && hasAPIKey">
102
+ <div
103
+ v-if="showFlyout"
104
+ class="absolute right-0 top-[90%] w-48 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5 focus:outline-none"
105
+ role="menu"
106
+ aria-orientation="vertical"
107
+ aria-labelledby="user-menu-button"
108
+ style="z-index: 10000"
109
+ tabindex="-1">
68
110
  <button
69
111
  type="button"
70
- @click="loginWithGithub"
71
- class="rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
72
- Login
73
- </button>
74
- </div>
75
- <div v-if="user && hasAPIKey" class="h-full flex items-center relative" v-clickOutside="hideFlyout">
76
- <div>
77
- <button type="button" @click="showFlyout = !showFlyout" class="relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
78
- <span class="absolute -inset-1.5"></span>
79
- <span class="sr-only">Open user menu</span>
80
- <img class="size-8 rounded-full" :src="user.picture" alt="">
81
- </button>
82
- </div>
83
-
84
- <div
85
- v-if="showFlyout"
86
- class="absolute right-0 top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none"
87
- role="menu"
88
- aria-orientation="vertical"
89
- aria-labelledby="user-menu-button"
90
- style="z-index: 10000"
112
+ @click="toggleDarkMode(); showFlyout = false"
113
+ class="w-full cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle text-left flex items-center gap-2"
114
+ role="menuitem"
91
115
  tabindex="-1">
92
- <router-link to="/team" v-if="hasAccess(roles, 'team')" @click="showFlyout = false" class="cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200" role="menuitem" tabindex="-1" id="user-menu-item-2">Team</router-link>
93
- <span v-else class="block px-4 py-2 text-sm text-gray-300 cursor-not-allowed" role="menuitem" tabindex="-1" id="user-menu-item-2">
94
- Team
95
- <svg class="h-4 w-4 ml-1 inline" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
96
- <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
97
- </svg>
98
- </span>
99
- <span @click="logout" class="cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</span>
100
- </div>
116
+ <svg v-if="darkMode" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
117
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
118
+ {{ darkMode ? 'Light mode' : 'Dark mode' }}
119
+ </button>
120
+ <router-link to="/team" v-if="hasAccess(roles, 'team')" @click="showFlyout = false" class="cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle" role="menuitem" tabindex="-1" id="user-menu-item-2">Team</router-link>
121
+ <span v-else class="block px-4 py-2 text-sm text-gray-300 cursor-not-allowed" role="menuitem" tabindex="-1" id="user-menu-item-2">
122
+ Team
123
+ <svg class="h-4 w-4 ml-1 inline" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
124
+ <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
125
+ </svg>
126
+ </span>
127
+ <span @click="logout" class="cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</span>
101
128
  </div>
102
-
103
129
  </div>
104
130
  </div>
105
- <div class="md:hidden flex items-center">
131
+ </div>
132
+ <!-- Mobile navbar -->
133
+ <div class="navbar w-full bg-page flex justify-between border-b border-edge !h-[55px] md:hidden">
134
+ <div class="flex items-center gap-2 pl-4">
135
+ <router-link class="flex items-center gap-2" :to="{ name: defaultRoute }">
136
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-8 text-primary" viewBox="0 0 250 250" fill="currentColor" aria-label="Mongoose Studio Logo">
137
+ <path d="M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z"/>
138
+ <path d="M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z"/>
139
+ </svg>
140
+ <span class="text-base font-semibold text-content">Mongoose Studio</span>
141
+ </router-link>
142
+ </div>
143
+ <div class="flex items-center">
106
144
  <!-- Mobile menu toggle, controls the 'mobileMenuOpen' state. -->
107
145
  <button type="button" id="open-mobile-menu" class="-ml-2 rounded-md p-2 pr-4 text-gray-400">
108
146
  <span class="sr-only">Open menu</span>
@@ -115,10 +153,13 @@
115
153
  <!-- Mobile menu mask -->
116
154
  <div id="mobile-menu-mask" class="fixed inset-0 bg-black bg-opacity-40 z-40 hidden"></div>
117
155
  <!-- Mobile menu drawer -->
118
- <div id="mobile-menu" style="z-index: 1000" class="fixed inset-0 bg-white shadow-lg transform translate-x-full transition-transform duration-200 ease-in-out flex flex-col">
119
- <div class="flex items-center justify-between px-4 !h-[55px] border-b border-gray-200">
156
+ <div id="mobile-menu" style="z-index: 10000" class="fixed inset-0 bg-page shadow-lg transform translate-x-full transition-transform duration-200 ease-in-out flex flex-col">
157
+ <div class="flex items-center justify-between px-4 !h-[55px] border-b border-edge">
120
158
  <router-link :to="{ name: defaultRoute }">
121
- <img src="images/logo.svg" class="h-[32px]" alt="Mongoose Studio Logo" />
159
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-[32px] text-primary" viewBox="0 0 250 250" fill="currentColor" aria-label="Mongoose Studio Logo">
160
+ <path d="M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z"/>
161
+ <path d="M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z"/>
162
+ </svg>
122
163
  </router-link>
123
164
  <button type="button" id="close-mobile-menu" class="text-gray-400 p-2 rounded-md">
124
165
  <span class="sr-only">Close menu</span>
@@ -128,10 +169,21 @@
128
169
  </button>
129
170
  </div>
130
171
  <nav class="flex-1 px-4 py-4 space-y-2">
172
+ <div class="flex items-center gap-2 px-3 py-2" v-if="!user || !hasAPIKey">
173
+ <button
174
+ type="button"
175
+ @click="toggleDarkMode"
176
+ class="inline-flex items-center gap-2 rounded-md px-3 py-2 text-base font-medium text-content-secondary hover:bg-muted"
177
+ aria-label="Toggle dark mode">
178
+ <svg v-if="darkMode" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
179
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
180
+ <span>{{ darkMode ? 'Light mode' : 'Dark mode' }}</span>
181
+ </button>
182
+ </div>
131
183
  <a v-if="hasAccess(roles, 'root')"
132
184
  href="#/"
133
185
  class="block px-3 py-2 rounded-md text-base font-medium"
134
- :class="documentView ? 'text-ultramarine-700 bg-ultramarine-100' : 'text-gray-700 hover:bg-gray-100'">Documents</a>
186
+ :class="documentView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Documents</a>
135
187
  <span v-else class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed">
136
188
  Documents
137
189
  <svg class="h-4 w-4 ml-1 inline" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -141,7 +193,7 @@
141
193
  <a v-if="hasAccess(roles, 'dashboards')"
142
194
  href="#/dashboards"
143
195
  class="block px-3 py-2 rounded-md text-base font-medium"
144
- :class="dashboardView ? 'text-ultramarine-700 bg-ultramarine-100' : 'text-gray-700 hover:bg-gray-100'">Dashboards</a>
196
+ :class="dashboardView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Dashboards</a>
145
197
  <span v-else class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed">
146
198
  Dashboards
147
199
  <svg class="h-4 w-4 ml-1 inline" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -151,7 +203,7 @@
151
203
  <a v-if="hasAccess(roles, 'chat')"
152
204
  href="#/chat"
153
205
  class="block px-3 py-2 rounded-md text-base font-medium"
154
- :class="chatView ? 'text-ultramarine-700 bg-ultramarine-100' : 'text-gray-700 hover:bg-gray-100'">Chat</a>
206
+ :class="chatView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Chat</a>
155
207
  <span v-else class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed">
156
208
  Chat
157
209
  <svg class="h-4 w-4 ml-1 inline" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -162,24 +214,32 @@
162
214
  <button
163
215
  type="button"
164
216
  @click="loginWithGithub"
165
- class="w-full rounded bg-ultramarine-600 px-3 py-2 text-base font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
217
+ class="w-full rounded bg-muted text-content px-3 py-2 text-base font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
166
218
  Login
167
219
  </button>
168
220
  </div>
169
221
  <div v-if="user && hasAPIKey" class="mt-4">
170
- <div class="flex items-center gap-3 px-3 py-2 bg-gray-50 rounded-md">
222
+ <div class="flex items-center gap-3 px-3 py-2 bg-page rounded-md">
171
223
  <img class="size-8 rounded-full" :src="user.picture" alt="">
172
- <span class="text-gray-900 font-medium">{{ user.name }}</span>
224
+ <span class="text-content font-medium">{{ user.name }}</span>
173
225
  </div>
174
226
  <div class="mt-2 space-y-1">
175
- <router-link to="/team" v-if="hasAccess(roles, 'team')" class="block px-3 py-2 rounded-md text-base text-gray-700 hover:bg-ultramarine-100">Team</router-link>
227
+ <button
228
+ type="button"
229
+ @click="toggleDarkMode"
230
+ class="w-full inline-flex items-center gap-2 px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle text-left">
231
+ <svg v-if="darkMode" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
232
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
233
+ {{ darkMode ? 'Light mode' : 'Dark mode' }}
234
+ </button>
235
+ <router-link to="/team" v-if="hasAccess(roles, 'team')" class="block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle">Team</router-link>
176
236
  <span v-else class="block px-3 py-2 rounded-md text-base text-gray-300 cursor-not-allowed">
177
237
  Team
178
238
  <svg class="h-4 w-4 ml-1 inline" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
179
239
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
180
240
  </svg>
181
241
  </span>
182
- <span @click="logout" class="block px-3 py-2 rounded-md text-base text-gray-700 hover:bg-ultramarine-100 cursor-pointer">Sign out</span>
242
+ <span @click="logout" class="block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle cursor-pointer">Sign out</span>
183
243
  </div>
184
244
  </div>
185
245
  </nav>
@@ -13,23 +13,29 @@ module.exports = app => app.component('navbar', {
13
13
  template: template,
14
14
  props: ['user', 'roles'],
15
15
  inject: ['state'],
16
- data: () => ({ showFlyout: false }),
16
+ data: () => ({
17
+ showFlyout: false,
18
+ darkMode: typeof localStorage !== 'undefined' && localStorage.getItem('studio-theme') === 'dark'
19
+ }),
17
20
  mounted: function() {
18
21
  const mobileMenuMask = document.querySelector('#mobile-menu-mask');
19
22
  const mobileMenu = document.querySelector('#mobile-menu');
23
+ const openBtn = document.querySelector('#open-mobile-menu');
20
24
 
21
- document.querySelector('#open-mobile-menu').addEventListener('click', (event) => {
22
- event.stopPropagation();
23
- mobileMenuMask.style.display = 'block';
24
- mobileMenu.classList.remove('translate-x-full');
25
- mobileMenu.classList.add('translate-x-0');
26
- });
25
+ if (openBtn && mobileMenuMask && mobileMenu) {
26
+ openBtn.addEventListener('click', (event) => {
27
+ event.stopPropagation();
28
+ mobileMenuMask.style.display = 'block';
29
+ mobileMenu.classList.remove('translate-x-full');
30
+ mobileMenu.classList.add('translate-x-0');
31
+ });
27
32
 
28
- document.querySelector('body').addEventListener('click', () => {
29
- mobileMenuMask.style.display = 'none';
30
- mobileMenu.classList.remove('translate-x-0');
31
- mobileMenu.classList.add('translate-x-full');
32
- });
33
+ document.querySelector('body').addEventListener('click', () => {
34
+ mobileMenuMask.style.display = 'none';
35
+ mobileMenu.classList.remove('translate-x-0');
36
+ mobileMenu.classList.add('translate-x-full');
37
+ });
38
+ }
33
39
  },
34
40
  computed: {
35
41
  dashboardView() {
@@ -82,6 +88,19 @@ module.exports = app => app.component('navbar', {
82
88
  logout() {
83
89
  window.localStorage.setItem('_mongooseStudioAccessToken', '');
84
90
  window.location.reload();
91
+ },
92
+ toggleDarkMode() {
93
+ this.darkMode = !this.darkMode;
94
+ const theme = this.darkMode ? 'dark' : 'light';
95
+ window.localStorage.setItem('studio-theme', theme);
96
+ if (this.darkMode) {
97
+ document.documentElement.classList.add('dark');
98
+ } else {
99
+ document.documentElement.classList.remove('dark');
100
+ }
101
+ const meta = document.querySelector('meta[name="theme-color"]');
102
+ if (meta) meta.setAttribute('content', this.darkMode ? '#0f0f0f' : '#ffffff');
103
+ document.documentElement.dispatchEvent(new CustomEvent('studio-theme-changed', { detail: { dark: this.darkMode } }));
85
104
  }
86
105
  },
87
106
  directives: {
@@ -9,7 +9,7 @@ const roleAccess = {
9
9
  dashboards: ['dashboards', 'dashboard']
10
10
  };
11
11
 
12
- const allowedRoutesForLocalDev = ['document', 'root', 'chat', 'model', 'tasks', 'taskByName', 'taskSingle'];
12
+ const allowedRoutesForLocalDev = ['document', 'dashboards', 'dashboard', 'root', 'chat', 'model', 'tasks', 'taskByName', 'taskSingle'];
13
13
 
14
14
  // Helper function to check if a role has access to a route
15
15
  function hasAccess(roles, routeName) {