@geode/opengeodeweb-front 10.18.0 → 10.18.1-rc.2

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.
@@ -40,18 +40,18 @@ onUnmounted(() => {
40
40
  </script>
41
41
 
42
42
  <template>
43
- <v-sheet color="transparent" height="160" class="position-relative overflow-visible mb-4">
43
+ <v-sheet color="transparent" min-height="160" class="position-relative overflow-visible mb-4">
44
44
  <v-scroll-y-reverse-transition mode="out-in">
45
45
  <v-card
46
46
  :key="currentMessage"
47
47
  rounded="lg"
48
- class="pa-6 border mx-auto"
48
+ class="pa-4 pa-sm-6 border mx-auto"
49
49
  color="rgba(255, 255, 255, 0.05)"
50
50
  elevation="0"
51
51
  style="border-color: rgba(255, 255, 255, 0.1) !important"
52
52
  >
53
53
  <v-card-title
54
- class="d-flex align-center ga-3 pa-0 mb-3 text-subtitle-1 font-weight-bold text-white text-wrap"
54
+ class="d-flex align-center ga-3 pa-0 mb-2 mb-sm-3 text-body-1 text-sm-subtitle-1 font-weight-bold text-white text-wrap"
55
55
  >
56
56
  <v-icon
57
57
  :icon="ecoMessages[currentMessage].icon"
@@ -8,18 +8,17 @@ const { logo } = defineProps({
8
8
  </script>
9
9
 
10
10
  <template>
11
- <v-row justify="center" class="mb-8">
11
+ <v-row justify="center" class="mb-4 mb-sm-8">
12
12
  <v-img
13
13
  :src="logo"
14
- height="180"
15
- width="180"
14
+ class="responsive-logo"
16
15
  style="filter: drop-shadow(0 0 20px rgba(var(--v-theme-primary), 0.4))"
17
16
  />
18
17
  </v-row>
19
18
 
20
- <v-card color="transparent" elevation="0" class="mb-8 overflow-visible">
19
+ <v-card color="transparent" elevation="0" class="mb-4 mb-sm-8 overflow-visible">
21
20
  <v-card-title
22
- class="text-h2 font-weight-black text-white text-wrap pa-0 d-block"
21
+ class="font-weight-black text-white text-wrap pa-0 d-block responsive-title"
23
22
  style="text-shadow: 0 0 20px rgba(255, 255, 255, 0.3)"
24
23
  >
25
24
  STARTING UP
@@ -35,7 +34,17 @@ const { logo } = defineProps({
35
34
  </template>
36
35
 
37
36
  <style scoped>
37
+ .responsive-logo {
38
+ width: clamp(100px, 20vw, 180px);
39
+ height: clamp(100px, 20vw, 180px);
40
+ }
41
+
42
+ .responsive-title {
43
+ font-size: clamp(2rem, 8vw, 4rem) !important;
44
+ line-height: 1.1 !important;
45
+ }
46
+
38
47
  .ls-widest {
39
- letter-spacing: 0.6em !important;
48
+ letter-spacing: clamp(0.2em, 2vw, 0.6em) !important;
40
49
  }
41
50
  </style>
@@ -15,7 +15,7 @@ const { progress } = defineProps({
15
15
  rounded
16
16
  bg-color="white"
17
17
  bg-opacity="0.15"
18
- class="custom-progress-glow mt-8"
18
+ class="custom-progress-glow mt-4 mt-sm-8"
19
19
  />
20
20
  </template>
21
21
 
@@ -44,7 +44,7 @@ onUnmounted(() => {
44
44
  <Teleport to="body">
45
45
  <div
46
46
  v-if="show"
47
- class="d-flex align-center justify-center transition-swing"
47
+ class="transition-swing overflow-y-auto"
48
48
  style="
49
49
  position: fixed;
50
50
  inset: 0;
@@ -58,23 +58,26 @@ onUnmounted(() => {
58
58
  >
59
59
  <div
60
60
  style="
61
- position: absolute;
61
+ position: fixed;
62
62
  inset: 0;
63
63
  background-image: radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 0);
64
64
  background-size: 40px 40px;
65
65
  background-position: center;
66
66
  pointer-events: none;
67
+ z-index: -1;
67
68
  "
68
69
  />
69
70
 
70
- <div
71
- class="d-flex flex-column align-center text-center"
72
- style="max-width: 650px; width: 100%; padding: 0 24px; gap: 1.5rem"
73
- >
74
- <LoadingHeader :logo="logo" />
75
- <LoadingEcoMessages :app-name="appName" />
76
- <LoadingProgress :progress="progress" />
77
- <LoadingFooter />
71
+ <div class="d-flex align-center justify-center pa-6" style="min-height: 100%">
72
+ <div
73
+ class="d-flex flex-column align-center text-center w-100"
74
+ style="max-width: 650px; gap: clamp(1rem, 4vh, 2rem)"
75
+ >
76
+ <LoadingHeader :logo="logo" />
77
+ <LoadingEcoMessages :app-name="appName" />
78
+ <LoadingProgress :progress="progress" />
79
+ <LoadingFooter />
80
+ </div>
78
81
  </div>
79
82
  </div>
80
83
  </Teleport>
@@ -163,10 +163,10 @@ const { focusedIndex, handleKeyDown } = useTreeKeyboardNav(
163
163
  }
164
164
 
165
165
  .tree-row-wrapper {
166
- min-height: 44px !important;
166
+ min-height: 28px !important;
167
167
  cursor: pointer;
168
- border-radius: 8px;
169
- margin: 1px 4px;
168
+ border-radius: 4px;
169
+ margin: 0 2px;
170
170
  }
171
171
 
172
172
  .tree-row-wrapper.is-focused {
@@ -2,14 +2,15 @@
2
2
  import ActionButton from "@ogw_front/components/ActionButton.vue";
3
3
  import SearchBar from "@ogw_front/components/SearchBar.vue";
4
4
 
5
- const { search, sortType, filterOptions, availableFilterOptions } = defineProps({
5
+ const { search, sortType, filterOptions, availableFilterOptions, isCollapsed } = defineProps({
6
6
  search: { type: String, required: true },
7
7
  sortType: { type: String, required: true },
8
8
  filterOptions: { type: Object, required: true },
9
9
  availableFilterOptions: { type: Array, required: true },
10
+ isCollapsed: { type: Boolean, required: false, default: false },
10
11
  });
11
12
 
12
- const emit = defineEmits(["update:search", "toggle-sort", "collapse-all"]);
13
+ const emit = defineEmits(["update:search", "toggle-sort", "collapse-all", "expand-all"]);
13
14
 
14
15
  const showSearch = ref(false);
15
16
 
@@ -24,7 +25,7 @@ watch(
24
25
  </script>
25
26
 
26
27
  <template>
27
- <v-row dense align="center" class="pa-2 py-1">
28
+ <v-row dense align="center" class="pa-1 py-0">
28
29
  <v-col cols="12">
29
30
  <div
30
31
  class="controls-capsule d-flex align-center rounded-pill px-1 overflow-hidden"
@@ -98,6 +99,7 @@ watch(
98
99
  </v-list>
99
100
  </v-menu>
100
101
  <ActionButton
102
+ v-if="!isCollapsed"
101
103
  tooltip="Collapse All"
102
104
  icon="mdi-collapse-all-outline"
103
105
  variant="text"
@@ -105,6 +107,15 @@ watch(
105
107
  tooltipLocation="bottom"
106
108
  @click="emit('collapse-all')"
107
109
  />
110
+ <ActionButton
111
+ v-else
112
+ tooltip="Expand All"
113
+ icon="mdi-expand-all-outline"
114
+ variant="text"
115
+ color="black"
116
+ tooltipLocation="bottom"
117
+ @click="emit('expand-all')"
118
+ />
108
119
  </div>
109
120
  </div>
110
121
  </v-col>
@@ -113,7 +124,7 @@ watch(
113
124
 
114
125
  <style scoped>
115
126
  .controls-capsule {
116
- height: 40px;
127
+ height: 32px;
117
128
  border: 1px solid transparent;
118
129
  transition: all 0.3s ease;
119
130
  width: fit-content;
@@ -133,12 +144,12 @@ watch(
133
144
  :deep(.v-field__input) {
134
145
  padding-top: 0 !important;
135
146
  padding-bottom: 0 !important;
136
- min-height: 40px !important;
147
+ min-height: 32px !important;
137
148
  display: flex;
138
149
  align-items: center;
139
150
  }
140
151
 
141
152
  :deep(.v-field__field) {
142
- height: 40px !important;
153
+ height: 32px !important;
143
154
  }
144
155
  </style>
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { middleTruncate } from "@ogw_front/utils/string";
3
+
2
4
  const { item, isLeaf } = defineProps({
3
5
  item: { type: Object, required: true },
4
6
  isLeaf: { type: Boolean, required: false, default: undefined },
@@ -6,8 +8,39 @@ const { item, isLeaf } = defineProps({
6
8
 
7
9
  const emit = defineEmits(["contextmenu", "mouseenter", "mouseleave"]);
8
10
 
11
+ const labelContainer = useTemplateRef("label-container");
12
+ const { width: containerWidth } = useElementSize(labelContainer);
13
+
9
14
  const actualItem = computed(() => item.raw || item);
10
15
 
16
+ const UUID_END_CHARS = 12;
17
+ const ELLIPSIS_LENGTH = 3;
18
+ const MIN_START_CHARS = 4;
19
+
20
+ const displayTitle = computed(() => {
21
+ const { title } = actualItem.value;
22
+ if (!title) {
23
+ return "";
24
+ }
25
+
26
+ // Estimate max characters based on width (approx 9px per char for typical font)
27
+ // We subtract some padding/icon space
28
+ const estimatedCharWidth = 8.5;
29
+ const maxChars = Math.floor(containerWidth.value / estimatedCharWidth);
30
+
31
+ // Only truncate if the text is longer than what fits
32
+ if (title.length <= maxChars) {
33
+ return title;
34
+ }
35
+
36
+ // Calculate dynamic start/end based on available space
37
+ // For UUIDs, showing the last 12 characters is often useful
38
+ const endChars = Math.min(UUID_END_CHARS, Math.floor(maxChars / ELLIPSIS_LENGTH));
39
+ const startChars = Math.max(MIN_START_CHARS, maxChars - endChars - ELLIPSIS_LENGTH);
40
+
41
+ return middleTruncate(title, maxChars, startChars, endChars);
42
+ });
43
+
11
44
  const tooltipDisabled = computed(() => {
12
45
  if (isLeaf !== undefined) {
13
46
  return !isLeaf;
@@ -17,7 +50,7 @@ const tooltipDisabled = computed(() => {
17
50
  </script>
18
51
 
19
52
  <template>
20
- <div class="tree-item-label-container w-100">
53
+ <div ref="label-container" class="tree-item-label-container w-100">
21
54
  <v-tooltip :disabled="tooltipDisabled" location="right" open-delay="400">
22
55
  <template #activator="{ props: tooltipProps }">
23
56
  <span
@@ -28,7 +61,7 @@ const tooltipDisabled = computed(() => {
28
61
  @mouseenter="emit('mouseenter')"
29
62
  @mouseleave="emit('mouseleave')"
30
63
  >
31
- {{ actualItem.title }}
64
+ {{ displayTitle }}
32
65
  </span>
33
66
  </template>
34
67
 
@@ -65,6 +98,7 @@ const tooltipDisabled = computed(() => {
65
98
  display: inline-flex;
66
99
  align-items: center;
67
100
  cursor: pointer;
101
+ font-size: 0.8rem;
68
102
  }
69
103
 
70
104
  .inactive-item {
@@ -9,11 +9,11 @@ const { item, itemProps, selection, isSelected, getIndeterminate } = defineProps
9
9
 
10
10
  defineEmits(["toggle-open", "toggle-select"]);
11
11
 
12
- const INDENT_STEP = 16;
12
+ const INDENT_STEP = 10;
13
13
  </script>
14
14
 
15
15
  <template>
16
- <div class="tree-row-content d-flex align-center px-4 ps-3 w-100">
16
+ <div class="tree-row-content d-flex align-center px-2 ps-2 w-100">
17
17
  <div
18
18
  v-if="item.depth > 0"
19
19
  class="flex-shrink-0"
@@ -34,10 +34,10 @@ const INDENT_STEP = 16;
34
34
  v-if="selection.selectable"
35
35
  :icon="
36
36
  getIndeterminate(item.raw)
37
- ? 'mdi-eye-minus'
37
+ ? 'mdi-eye-minus-outline'
38
38
  : isSelected(item.raw)
39
39
  ? 'mdi-eye'
40
- : 'mdi-eye-off'
40
+ : 'mdi-eye-off-outline'
41
41
  "
42
42
  variant="text"
43
43
  density="compact"
@@ -50,7 +50,11 @@ const INDENT_STEP = 16;
50
50
 
51
51
  <div class="tree-title flex-grow-1 overflow-hidden d-flex align-center ms-1 pt-1">
52
52
  <slot name="title" :item="item.raw" :is-leaf="item.isLeaf">
53
- <v-list-item-title :class="{ 'font-weight-bold': !item.isLeaf }" class="text-black">
53
+ <v-list-item-title
54
+ :class="{ 'font-weight-bold': !item.isLeaf }"
55
+ class="text-black"
56
+ style="font-size: 0.8rem !important"
57
+ >
54
58
  {{ item.raw[itemProps.title] || item.id }}
55
59
  </v-list-item-title>
56
60
  </slot>
@@ -64,7 +68,7 @@ const INDENT_STEP = 16;
64
68
 
65
69
  <style scoped>
66
70
  .tree-row-content {
67
- min-height: 44px;
71
+ min-height: 28px;
68
72
  }
69
73
 
70
74
  .icon-placeholder {
@@ -1,68 +1,27 @@
1
1
  <script setup>
2
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
2
+ import { useAdaptiveStyles } from "@ogw_front/composables/use_adaptive_styles";
3
3
 
4
4
  const SCROLL_SYNC_DELAY = 50;
5
5
  const SCROLL_THRESHOLD = 1;
6
- const { title, closable, icon, mdiIcon, scrollTop } = defineProps({
6
+ const { title, closable, icon, mdiIcon, scrollTop, borderRadius } = defineProps({
7
7
  title: { type: String, required: true },
8
8
  closable: { type: Boolean, required: false, default: false },
9
9
  icon: { type: String, required: false, default: "" },
10
10
  mdiIcon: { type: String, required: false, default: "" },
11
11
  scrollTop: { type: Number, required: false, default: 0 },
12
+ borderRadius: { type: String, required: false, default: "16px" },
13
+ borderLeft: { type: Boolean, required: false, default: true },
12
14
  });
13
15
  const emit = defineEmits(["close", "dragstart", "update:scrollTop"]);
14
16
 
15
17
  const scrollContainer = useTemplateRef("scroll-container");
16
18
  const treeviewBox = useTemplateRef("treeview-box");
17
- const hybridViewerStore = useHybridViewerStore();
18
19
 
19
- const LUMINANCE_THRESHOLD = 0.65;
20
- const ADAPTIVE_EXPONENT = 0.3;
21
-
22
- const MIN_BLUR = 8;
23
- const MAX_BLUR = 25;
24
-
25
- const MIN_OPACITY = 0;
26
- const MAX_OPACITY = 0.5;
27
-
28
- const MIN_BOOST = 1;
29
- const MAX_BOOST = 1.2;
30
- const ADAPTIVE_REFRESH_RATE = 150;
31
-
32
- const { x, y, width, height } = useElementBounding(treeviewBox);
33
- const brightness = ref(LUMINANCE_THRESHOLD);
34
-
35
- const updateBrightness = useThrottleFn(() => {
36
- brightness.value = hybridViewerStore.getAverageBrightness({
37
- x: x.value,
38
- y: y.value,
39
- width: width.value,
40
- height: height.value,
41
- });
42
- }, ADAPTIVE_REFRESH_RATE);
20
+ const { adaptiveStyles } = useAdaptiveStyles(treeviewBox);
43
21
 
44
22
  let isApplyingScroll = false;
45
23
  let resizeObserver = undefined;
46
24
 
47
- watch([x, y, width, height, () => hybridViewerStore.latestImage], updateBrightness, {
48
- immediate: true,
49
- });
50
-
51
- const adaptiveStyles = computed(() => {
52
- const normalized = Math.min(1, brightness.value / LUMINANCE_THRESHOLD);
53
- const darkFactor = (1 - normalized) ** ADAPTIVE_EXPONENT;
54
-
55
- const blur = MIN_BLUR + darkFactor * (MAX_BLUR - MIN_BLUR);
56
- const opacity = MIN_OPACITY + darkFactor * (MAX_OPACITY - MIN_OPACITY);
57
- const brightnessBoost = MIN_BOOST + darkFactor * (MAX_BOOST - MIN_BOOST);
58
-
59
- return {
60
- "--adaptive-blur": `${blur}px`,
61
- "--adaptive-opacity": opacity,
62
- "--adaptive-brightness": brightnessBoost,
63
- };
64
- });
65
-
66
25
  function handleScroll(event) {
67
26
  if (isApplyingScroll) {
68
27
  return;
@@ -121,7 +80,7 @@ watch(
121
80
  ref="treeview-box"
122
81
  variant="outlined"
123
82
  class="tree-box d-flex flex-column"
124
- :style="adaptiveStyles"
83
+ :style="[adaptiveStyles, { borderRadius, borderLeft: borderLeft ? undefined : 'none' }]"
125
84
  >
126
85
  <v-card-title
127
86
  class="tree-box-header d-flex align-center"
@@ -181,7 +140,6 @@ watch(
181
140
  .tree-box {
182
141
  height: 100%;
183
142
  min-height: 0;
184
- border-radius: 16px;
185
143
  background-color: transparent !important;
186
144
  border: 1px solid rgba(255, 255, 255, 0.2) !important;
187
145
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
@@ -214,8 +172,8 @@ watch(
214
172
  }
215
173
 
216
174
  .tree-box-header {
217
- height: 40px !important;
218
- padding: 0 12px !important;
175
+ height: 32px !important;
176
+ padding: 0 10px !important;
219
177
  background-color: rgba(255, 255, 255, 0.05);
220
178
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
221
179
  flex-shrink: 0;
@@ -3,15 +3,17 @@ import GlobalObjects from "@ogw_front/components/Viewer/ObjectTree/Views/GlobalO
3
3
  import ModelComponents from "@ogw_front/components/Viewer/ObjectTree/Views/ModelComponents.vue";
4
4
  import ViewerObjectTreeBox from "@ogw_front/components/Viewer/ObjectTree/Box.vue";
5
5
  import { geode_objects } from "@ogw_front/assets/geode_objects";
6
+ import { useAdaptiveStyles } from "@ogw_front/composables/use_adaptive_styles";
6
7
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
7
8
 
8
- const WIDTH_MIN = 200;
9
+ const WIDTH_MIN = 100;
9
10
  const HEIGHT_MIN = 150;
10
- const GAP_WIDTH = 10;
11
+ const GAP_WIDTH = 0;
11
12
  const PERCENT_100 = 100;
12
13
 
13
14
  const TOTAL_PERCENT = 100;
14
15
  const MAX_PANEL_WIDTH_RATIO = 0.8;
16
+ const AUTO_CLOSE_THRESHOLD = 80;
15
17
 
16
18
  const { containerWidth } = defineProps({
17
19
  containerWidth: { type: Number, required: true },
@@ -20,16 +22,23 @@ const { containerWidth } = defineProps({
20
22
  const treeviewStore = useTreeviewStore();
21
23
  const emit = defineEmits(["show-menu"]);
22
24
 
25
+ const activityBar = useTemplateRef("activity-bar");
26
+ const { adaptiveStyles: activityBarAdaptiveStyles } = useAdaptiveStyles(activityBar);
27
+
23
28
  const maxWidth = computed(() => containerWidth * MAX_PANEL_WIDTH_RATIO);
24
29
 
25
- const mainView = computed(() => treeviewStore.opened_views[0]);
26
- const additionalViews = computed(() => treeviewStore.opened_views.slice(1));
30
+ const mainView = computed(() => treeviewStore.opened_views.find((view) => view.id === "main"));
31
+ const additionalViews = computed(() =>
32
+ treeviewStore.opened_views.filter((view) => view.id !== "main"),
33
+ );
27
34
 
28
35
  const totalWidth = computed(() => {
29
36
  const hasAdditional = additionalViews.value.length > 0;
30
- const gap = hasAdditional ? GAP_WIDTH : 0;
37
+ const hasMain = Boolean(mainView.value);
38
+ const gap = hasAdditional && hasMain ? GAP_WIDTH : 0;
39
+ const firstColWidth = hasMain ? treeviewStore.panelWidth : 0;
31
40
  const secondColWidth = hasAdditional ? treeviewStore.additionalPanelWidth : 0;
32
- return `${treeviewStore.panelWidth + secondColWidth + gap}px`;
41
+ return `${firstColWidth + secondColWidth + gap}px`;
33
42
  });
34
43
 
35
44
  const rowHeights = computed({
@@ -90,7 +99,7 @@ function onResizeStart(event) {
90
99
  const startX = event.clientX;
91
100
  function resize(move_event) {
92
101
  const deltaX = move_event.clientX - startX;
93
- let newWidth = Math.max(WIDTH_MIN, startWidth + deltaX);
102
+ let newWidth = startWidth + deltaX;
94
103
  const hasAdditional = additionalViews.value.length > 0;
95
104
  const gap = hasAdditional ? GAP_WIDTH : 0;
96
105
  const currentTotalWidth =
@@ -98,6 +107,12 @@ function onResizeStart(event) {
98
107
  if (currentTotalWidth > maxWidth.value) {
99
108
  newWidth = maxWidth.value - (hasAdditional ? treeviewStore.additionalPanelWidth : 0) - gap;
100
109
  }
110
+
111
+ if (newWidth < AUTO_CLOSE_THRESHOLD) {
112
+ treeviewStore.closeView("main");
113
+ return;
114
+ }
115
+
101
116
  treeviewStore.setPanelWidth(Math.max(WIDTH_MIN, newWidth));
102
117
  document.body.style.userSelect = "none";
103
118
  }
@@ -115,11 +130,13 @@ function onAdditionalResizeStart(event) {
115
130
  const startX = event.clientX;
116
131
  function resize(move_event) {
117
132
  const deltaX = move_event.clientX - startX;
118
- let newWidth = Math.max(WIDTH_MIN, startWidth + deltaX);
133
+ const newWidth = startWidth + deltaX;
119
134
  const currentTotalWidth = treeviewStore.panelWidth + newWidth + GAP_WIDTH;
120
- if (currentTotalWidth > maxWidth.value) {
121
- newWidth = maxWidth.value - treeviewStore.panelWidth - GAP_WIDTH;
135
+ if (newWidth < AUTO_CLOSE_THRESHOLD) {
136
+ treeviewStore.closeView(additionalViews.value.at(-1).id);
137
+ return;
122
138
  }
139
+
123
140
  treeviewStore.setAdditionalPanelWidth(Math.max(WIDTH_MIN, newWidth));
124
141
  document.body.style.userSelect = "none";
125
142
  }
@@ -177,88 +194,154 @@ function onVerticalResizeStart(event, index) {
177
194
  <template>
178
195
  <div
179
196
  v-if="treeviewStore.items.length > 0"
180
- class="treeview-container d-flex"
181
- :style="{ width: totalWidth }"
197
+ class="treeview-layout d-flex"
182
198
  @contextmenu.prevent
183
199
  @mousedown.stop
184
200
  >
185
201
  <div
186
- class="column main-column"
187
- :style="{
188
- width: `${treeviewStore.panelWidth}px`,
189
- }"
202
+ ref="activity-bar"
203
+ class="activity-bar d-flex flex-column align-center py-2"
204
+ :style="activityBarAdaptiveStyles"
190
205
  >
191
- <ViewerObjectTreeBox
192
- :title="mainView.title"
193
- mdi-icon="mdi-file-tree-outline"
194
- :scroll-top="mainView.scrollTop"
195
- @update:scroll-top="treeviewStore.setScrollTop(mainView.id, $event)"
196
- >
197
- <GlobalObjects data-testid="mainObjectTree" @show-menu="emit('show-menu', $event)" />
198
- </ViewerObjectTreeBox>
206
+ <v-btn
207
+ icon="mdi-file-tree-outline"
208
+ variant="text"
209
+ :color="mainView ? 'primary' : 'black'"
210
+ class="mb-2"
211
+ v-tooltip="'Toggle Objects'"
212
+ @click="treeviewStore.toggleView('main')"
213
+ />
199
214
  </div>
200
215
 
201
- <div v-if="additionalViews.length > 0" class="column-separator" @mousedown="onResizeStart" />
202
-
203
- <div
204
- v-if="additionalViews.length > 0"
205
- class="column additional-column"
206
- :style="{
207
- width: `${treeviewStore.additionalPanelWidth}px`,
208
- }"
209
- >
210
- <template v-for="(view, index) in additionalViews" :key="view.id">
211
- <div
212
- class="view-wrapper"
213
- :class="{
214
- 'drag-over': draggedIndex !== undefined && draggedIndex !== index + 1,
215
- }"
216
- :style="{ flex: `0 0 ${rowHeights[index]}%` }"
217
- @dragover="onDragOver"
218
- @drop="onDrop(index + 1)"
216
+ <div class="treeview-container d-flex" :style="{ width: totalWidth }">
217
+ <div
218
+ v-if="mainView"
219
+ class="column main-column"
220
+ :style="{
221
+ width: `${treeviewStore.panelWidth}px`,
222
+ }"
223
+ >
224
+ <ViewerObjectTreeBox
225
+ :title="mainView?.title || 'Objects'"
226
+ mdi-icon="mdi-file-tree-outline"
227
+ :scroll-top="mainView?.scrollTop || 0"
228
+ closable
229
+ :border-radius="additionalViews.length > 0 ? '0' : '0 16px 16px 0'"
230
+ :border-left="false"
231
+ @close="treeviewStore.closeView('main')"
232
+ @update:scroll-top="mainView && treeviewStore.setScrollTop(mainView.id, $event)"
219
233
  >
220
- <ViewerObjectTreeBox
221
- :title="view.title"
222
- :icon="geode_objects[view.geode_object_type]?.image"
223
- :scroll-top="view.scrollTop"
224
- closable
225
- @close="treeviewStore.closeView(index + 1)"
226
- @dragstart="onDragStart(index + 1)"
227
- @update:scroll-top="treeviewStore.setScrollTop(view.id, $event)"
234
+ <GlobalObjects data-testid="mainObjectTree" @show-menu="emit('show-menu', $event)" />
235
+ </ViewerObjectTreeBox>
236
+ </div>
237
+
238
+ <div
239
+ v-if="mainView && additionalViews.length > 0"
240
+ class="column-separator"
241
+ @mousedown="onResizeStart"
242
+ />
243
+
244
+ <div
245
+ v-if="additionalViews.length > 0"
246
+ class="column additional-column"
247
+ :style="{
248
+ width: `${treeviewStore.additionalPanelWidth}px`,
249
+ }"
250
+ >
251
+ <template v-for="(view, index) in additionalViews" :key="view.id">
252
+ <div
253
+ class="view-wrapper"
254
+ :class="{
255
+ 'drag-over': draggedIndex !== undefined && draggedIndex !== index + 1,
256
+ }"
257
+ :style="{ flex: `0 0 ${rowHeights[index]}%` }"
258
+ @dragover="onDragOver"
259
+ @drop="onDrop(index + 1)"
228
260
  >
229
- <ModelComponents
230
- data-testid="modelComponentsObjectTree"
231
- :id="view.id"
232
- @show-menu="emit('show-menu', $event)"
233
- />
234
- </ViewerObjectTreeBox>
235
- </div>
236
- <div
237
- v-if="index < additionalViews.length - 1"
238
- class="v-split-resizer"
239
- @mousedown="onVerticalResizeStart($event, index)"
240
- />
241
- </template>
261
+ <ViewerObjectTreeBox
262
+ :title="view.title"
263
+ :icon="geode_objects[view.geode_object_type]?.image"
264
+ :scroll-top="view.scrollTop"
265
+ closable
266
+ :border-radius="index === additionalViews.length - 1 ? '0 16px 16px 0' : '0'"
267
+ :border-left="false"
268
+ @close="treeviewStore.closeView(view.id)"
269
+ @dragstart="onDragStart(index + 1)"
270
+ @update:scroll-top="treeviewStore.setScrollTop(view.id, $event)"
271
+ >
272
+ <ModelComponents
273
+ data-testid="modelComponentsObjectTree"
274
+ :id="view.id"
275
+ @show-menu="emit('show-menu', $event)"
276
+ />
277
+ </ViewerObjectTreeBox>
278
+ </div>
279
+ <div
280
+ v-if="index < additionalViews.length - 1"
281
+ class="v-split-resizer"
282
+ @mousedown="onVerticalResizeStart($event, index)"
283
+ />
284
+ </template>
285
+ </div>
286
+ <div
287
+ v-if="treeviewStore.opened_views.length > 0"
288
+ class="total-resizer"
289
+ @mousedown="
290
+ additionalViews.length > 0
291
+ ? onAdditionalResizeStart($event)
292
+ : mainView
293
+ ? onResizeStart($event)
294
+ : undefined
295
+ "
296
+ />
242
297
  </div>
243
- <div
244
- class="total-resizer"
245
- @mousedown="
246
- additionalViews.length > 0 ? onAdditionalResizeStart($event) : onResizeStart($event)
247
- "
248
- />
249
298
  </div>
250
299
  </template>
251
300
 
252
301
  <style scoped>
253
- .treeview-container {
302
+ .treeview-layout {
254
303
  position: absolute;
255
304
  z-index: 2;
256
305
  left: 0;
257
306
  top: 0;
258
307
  height: calc(100vh - 100px);
259
- margin-top: 10px;
260
- margin-left: 10px;
308
+ margin-top: 8px;
261
309
  pointer-events: auto;
310
+ }
311
+
312
+ .activity-bar {
313
+ width: 48px;
314
+ height: 100%;
315
+ border-radius: 16px 0 0 16px;
316
+ margin-left: 10px;
317
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
318
+ position: relative;
319
+ overflow: hidden;
320
+ }
321
+
322
+ .activity-bar::before {
323
+ content: "";
324
+ position: absolute;
325
+ inset: 0;
326
+ background: rgba(255, 255, 255, var(--adaptive-opacity));
327
+ backdrop-filter: blur(var(--adaptive-blur)) brightness(var(--adaptive-brightness));
328
+ -webkit-backdrop-filter: blur(var(--adaptive-blur)) brightness(var(--adaptive-brightness));
329
+ mix-blend-mode: lighten;
330
+ z-index: 0;
331
+ pointer-events: none;
332
+ border-radius: inherit;
333
+ transition:
334
+ background-color 0.3s ease,
335
+ backdrop-filter 0.3s ease;
336
+ }
337
+
338
+ .activity-bar > * {
339
+ position: relative;
340
+ z-index: 1;
341
+ }
342
+
343
+ .treeview-container {
344
+ height: 100%;
262
345
  width: max-content;
263
346
  min-width: min-content;
264
347
  }
@@ -289,7 +372,6 @@ function onVerticalResizeStart(event, index) {
289
372
  display: flex;
290
373
  flex-direction: column;
291
374
  overflow: hidden;
292
- padding: 2px;
293
375
  transition: transform 0.2s;
294
376
  min-height: 150px;
295
377
  }
@@ -361,6 +443,6 @@ function onVerticalResizeStart(event, index) {
361
443
  }
362
444
 
363
445
  .total-resizer:hover {
364
- background-color: rgba(0, 0, 0, 0.2);
446
+ background-color: transparent;
365
447
  }
366
448
  </style>
@@ -99,6 +99,20 @@ function handleHoverLeave({ item }) {
99
99
  }
100
100
  onHoverLeave(actualItem.id);
101
101
  }
102
+
103
+ function expandAll() {
104
+ const allIds = [];
105
+ function traverse(itemsList) {
106
+ for (const item of itemsList) {
107
+ if (item.children && item.children.length > 0) {
108
+ allIds.push(item.id);
109
+ traverse(item.children);
110
+ }
111
+ }
112
+ }
113
+ traverse(treeviewStore.items);
114
+ opened.value = allIds;
115
+ }
102
116
  </script>
103
117
 
104
118
  <template>
@@ -108,8 +122,10 @@ function handleHoverLeave({ item }) {
108
122
  :sort-type="sortType"
109
123
  :filter-options="filterOptions"
110
124
  :available-filter-options="availableFilterOptions"
125
+ :is-collapsed="opened.length === 0"
111
126
  @toggle-sort="toggleSort"
112
127
  @collapse-all="opened = []"
128
+ @expand-all="expandAll"
113
129
  />
114
130
 
115
131
  <CommonTreeView
@@ -177,6 +193,6 @@ function handleHoverLeave({ item }) {
177
193
 
178
194
  .transparent-treeview {
179
195
  background-color: transparent;
180
- margin: 4px 0;
196
+ margin: 2px 0;
181
197
  }
182
198
  </style>
@@ -111,6 +111,20 @@ function handleHoverEnter({ item, immediate = false }) {
111
111
  function handleHoverLeave() {
112
112
  onHoverLeave(id);
113
113
  }
114
+
115
+ function expandAll() {
116
+ const allIds = [];
117
+ function traverse(itemsList) {
118
+ for (const item of itemsList) {
119
+ if (item.children && item.children.length > 0) {
120
+ allIds.push(item.id);
121
+ traverse(item.children);
122
+ }
123
+ }
124
+ }
125
+ traverse(itemsForTreeView.value);
126
+ opened.value = allIds;
127
+ }
114
128
  </script>
115
129
 
116
130
  <template>
@@ -120,8 +134,10 @@ function handleHoverLeave() {
120
134
  :sort-type="sortType"
121
135
  :filter-options="filterOptions"
122
136
  :available-filter-options="availableFilterOptions"
137
+ :is-collapsed="opened.length === 0"
123
138
  @toggle-sort="toggleSort"
124
139
  @collapse-all="opened = []"
140
+ @expand-all="expandAll"
125
141
  />
126
142
 
127
143
  <FetchingData v-if="rawItems === undefined" :size="48" :width="4" text="" />
@@ -0,0 +1,55 @@
1
+ import { computed, ref, watch } from "vue";
2
+ import { useElementBounding, useThrottleFn } from "@vueuse/core";
3
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
4
+
5
+ const LUMINANCE_THRESHOLD = 0.65;
6
+ const ADAPTIVE_EXPONENT = 0.3;
7
+
8
+ const MIN_BLUR = 8;
9
+ const MAX_BLUR = 25;
10
+
11
+ const MIN_OPACITY = 0;
12
+ const MAX_OPACITY = 0.5;
13
+
14
+ const MIN_BOOST = 1;
15
+ const MAX_BOOST = 1.2;
16
+ const ADAPTIVE_REFRESH_RATE = 150;
17
+
18
+ export function useAdaptiveStyles(targetRef) {
19
+ const hybridViewerStore = useHybridViewerStore();
20
+ const { x, y, width, height } = useElementBounding(targetRef);
21
+ const brightness = ref(LUMINANCE_THRESHOLD);
22
+
23
+ const updateBrightness = useThrottleFn(() => {
24
+ brightness.value = hybridViewerStore.getAverageBrightness({
25
+ x: x.value,
26
+ y: y.value,
27
+ width: width.value,
28
+ height: height.value,
29
+ });
30
+ }, ADAPTIVE_REFRESH_RATE);
31
+
32
+ watch([x, y, width, height, () => hybridViewerStore.latestImage], updateBrightness, {
33
+ immediate: true,
34
+ });
35
+
36
+ const adaptiveStyles = computed(() => {
37
+ const normalized = Math.min(1, brightness.value / LUMINANCE_THRESHOLD);
38
+ const darkFactor = (1 - normalized) ** ADAPTIVE_EXPONENT;
39
+
40
+ const blur = MIN_BLUR + darkFactor * (MAX_BLUR - MIN_BLUR);
41
+ const opacity = MIN_OPACITY + darkFactor * (MAX_OPACITY - MIN_OPACITY);
42
+ const brightnessBoost = MIN_BOOST + darkFactor * (MAX_BOOST - MIN_BOOST);
43
+
44
+ return {
45
+ "--adaptive-blur": `${blur}px`,
46
+ "--adaptive-opacity": opacity,
47
+ "--adaptive-brightness": brightnessBoost,
48
+ };
49
+ });
50
+
51
+ return {
52
+ adaptiveStyles,
53
+ brightness,
54
+ };
55
+ }
@@ -112,10 +112,6 @@ export function useVirtualTree(propsIn, emit) {
112
112
  : (item[actualItemProps.value.title] || "").toLowerCase().includes(lowerSearch) ||
113
113
  String(id).toLowerCase().includes(lowerSearch);
114
114
 
115
- if (!hasChildren && !matches) {
116
- continue;
117
- }
118
-
119
115
  if (hasChildren) {
120
116
  const subtree = [];
121
117
  flattenTree(children, depth + 1, subtree);
@@ -127,10 +123,15 @@ export function useVirtualTree(propsIn, emit) {
127
123
  raw: item,
128
124
  id,
129
125
  depth,
130
- isOpen: true,
126
+ isOpen,
131
127
  isLeaf: false,
132
128
  });
133
- result.push(...subtree);
129
+ if (isOpen) {
130
+ result.push(...subtree);
131
+ }
132
+ continue;
133
+ }
134
+ if (!matches) {
134
135
  continue;
135
136
  }
136
137
  }
@@ -150,8 +151,29 @@ export function useVirtualTree(propsIn, emit) {
150
151
  return result;
151
152
  }
152
153
 
154
+ function traverse(itemsList, allIds) {
155
+ for (const item of itemsList) {
156
+ const children = item[actualItemProps.value.children];
157
+ if (children && children.length > 0) {
158
+ allIds.push(item[actualItemProps.value.value]);
159
+ traverse(children, allIds);
160
+ }
161
+ }
162
+ }
163
+
153
164
  const displayItems = computed(() => flattenTree(props.value.items || []));
154
165
 
166
+ watch(
167
+ () => props.value.search,
168
+ (newSearch, oldSearch) => {
169
+ if (newSearch && !oldSearch) {
170
+ const allIds = [];
171
+ traverse(props.value.items || [], allIds);
172
+ emit("update:opened", [...new Set([...(props.value.opened || []), ...allIds])]);
173
+ }
174
+ },
175
+ );
176
+
155
177
  return {
156
178
  actualItemProps,
157
179
  actualSelection,
@@ -68,9 +68,22 @@ export const useTreeviewStore = defineStore("treeview", () => {
68
68
  }
69
69
  });
70
70
 
71
- function closeView(index) {
72
- if (index > 0) {
73
- opened_views.value = opened_views.value.filter((view, view_index) => view_index !== index);
71
+ function closeView(id) {
72
+ opened_views.value = opened_views.value.filter((view) => view.id !== id);
73
+ }
74
+
75
+ function toggleView(id) {
76
+ const index = opened_views.value.findIndex((view) => view.id === id);
77
+ if (index !== -1) {
78
+ closeView(id);
79
+ } else if (id === "main") {
80
+ opened_views.value.unshift({
81
+ type: "object",
82
+ id: "main",
83
+ title: "Objects",
84
+ scrollTop: 0,
85
+ opened: [],
86
+ });
74
87
  }
75
88
  }
76
89
 
@@ -116,7 +129,7 @@ export const useTreeviewStore = defineStore("treeview", () => {
116
129
  function displayAdditionalTree(id, title, geodeObjectType) {
117
130
  const index = opened_views.value.findIndex((view) => view.id === id);
118
131
  if (index !== -1) {
119
- return closeView(index);
132
+ return closeView(id);
120
133
  }
121
134
  additionalPanelWidth.value = panelWidth.value;
122
135
  opened_views.value.push({
@@ -216,6 +229,24 @@ export const useTreeviewStore = defineStore("treeview", () => {
216
229
  };
217
230
  }
218
231
 
232
+ function renameItem(id, newName) {
233
+ for (const group of items.value) {
234
+ const child = group.children.find((childItem) => childItem.id === id);
235
+ if (child) {
236
+ child.title = newName;
237
+ const options = { numeric: true, sensitivity: "base" };
238
+ group.children.sort((childA, childB) =>
239
+ childA.title.localeCompare(childB.title, undefined, options),
240
+ );
241
+ break;
242
+ }
243
+ }
244
+ const view = opened_views.value.find((openedView) => openedView.id === id);
245
+ if (view) {
246
+ view.title = newName;
247
+ }
248
+ }
249
+
219
250
  return {
220
251
  items,
221
252
  selection,
@@ -226,8 +257,10 @@ export const useTreeviewStore = defineStore("treeview", () => {
226
257
  rowHeights,
227
258
  addItem,
228
259
  removeItem,
260
+ renameItem,
229
261
  displayAdditionalTree,
230
262
  closeView,
263
+ toggleView,
231
264
  moveView,
232
265
  importStores,
233
266
  displayFileTree,
@@ -0,0 +1,26 @@
1
+ const DEFAULT_MAX_LENGTH = 20;
2
+ const DEFAULT_START_CHARS = 8;
3
+ const DEFAULT_END_CHARS = 6;
4
+
5
+ /**
6
+ * Truncates a string in the middle.
7
+ * @param {string} text - The string to truncate.
8
+ * @param {number} maxLength - The maximum length of the string before truncation.
9
+ * @param {number} startChars - Number of characters to keep at the beginning.
10
+ * @param {number} endChars - Number of characters to keep at the end.
11
+ * @returns {string} The truncated string.
12
+ */
13
+ export function middleTruncate(
14
+ text,
15
+ maxLength = DEFAULT_MAX_LENGTH,
16
+ startChars = DEFAULT_START_CHARS,
17
+ endChars = DEFAULT_END_CHARS,
18
+ ) {
19
+ if (!text || text.length <= maxLength) {
20
+ return text;
21
+ }
22
+
23
+ const start = text.slice(0, startChars);
24
+ const end = text.slice(-endChars);
25
+ return `${start}...${end}`;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.18.0",
3
+ "version": "10.18.1-rc.2",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -34,8 +34,8 @@
34
34
  "build": ""
35
35
  },
36
36
  "dependencies": {
37
- "@geode/opengeodeweb-back": "latest",
38
- "@geode/opengeodeweb-viewer": "latest",
37
+ "@geode/opengeodeweb-back": "next",
38
+ "@geode/opengeodeweb-viewer": "next",
39
39
  "@google-cloud/run": "3.2.0",
40
40
  "@kitware/vtk.js": "33.3.0",
41
41
  "@mdi/font": "7.4.47",