@necrolab/dashboard 0.5.24 → 0.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +14 -0
- package/package.json +1 -1
- package/src/App.vue +70 -4
- package/src/assets/css/components/headers.scss +8 -9
- package/src/assets/css/components/search-groups.scss +49 -1
- package/src/assets/css/components/utilities.scss +0 -21
- package/src/assets/css/main.scss +7 -0
- package/src/components/Console/ConsoleToolbar.vue +2 -2
- package/src/components/Editors/AdminFileEditor.vue +46 -11
- package/src/components/Editors/TagToggle.vue +2 -1
- package/src/components/Filter/Filter.vue +1 -1
- package/src/components/ui/Navbar.vue +104 -57
- package/src/composables/useNotchHandling.js +31 -57
- package/src/views/Accounts.vue +1 -1
- package/src/views/Console.vue +111 -43
- package/src/views/Editor.vue +1 -1
- package/src/views/FilterBuilder.vue +4 -4
package/index.html
CHANGED
|
@@ -14,6 +14,20 @@
|
|
|
14
14
|
<meta name="description" content="Necro Lab - dashboard" />
|
|
15
15
|
<meta name="darkreader-lock" />
|
|
16
16
|
<meta name="color-scheme" content="dark" />
|
|
17
|
+
<meta name="supported-color-schemes" content="dark" />
|
|
18
|
+
<!-- Prevent Noir and other dark mode extensions from modifying the site -->
|
|
19
|
+
<meta name="noir" content="disabled" />
|
|
20
|
+
<meta name="nighteye" content="disabled" />
|
|
21
|
+
<style>
|
|
22
|
+
/* Lock color scheme to prevent iOS extensions from overriding */
|
|
23
|
+
:root {
|
|
24
|
+
color-scheme: dark only;
|
|
25
|
+
-webkit-color-scheme: dark;
|
|
26
|
+
}
|
|
27
|
+
html, body {
|
|
28
|
+
color-scheme: dark only !important;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
17
31
|
<title>Necro Lab - Dashboard</title>
|
|
18
32
|
<!-- DNS prefetch for external resources -->
|
|
19
33
|
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<ReconnectIndicator :message="ui.spinnerMessage" />
|
|
19
19
|
</div>
|
|
20
20
|
<div v-else key="main-components" class="flex">
|
|
21
|
-
<Navbar v-if="layout == 'dashboard'" class="fixed" />
|
|
22
|
-
<div class="router-wrapper w-full">
|
|
21
|
+
<Navbar v-if="layout == 'dashboard'" ref="navbarRef" class="fixed" />
|
|
22
|
+
<div class="router-wrapper w-full" :style="routerWrapperStyle">
|
|
23
23
|
<router-view v-slot="{ Component, route }">
|
|
24
24
|
<transition name="page-transition" mode="out-in">
|
|
25
25
|
<component
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
<script setup>
|
|
38
38
|
import { storeToRefs } from "pinia";
|
|
39
|
-
import { ref, computed } from "vue";
|
|
39
|
+
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
|
|
40
40
|
import { useRouter } from "vue-router";
|
|
41
41
|
import Navbar from "@/components/ui/Navbar.vue";
|
|
42
42
|
import { useUIStore } from "@/stores/ui";
|
|
@@ -50,6 +50,8 @@ const ui = useUIStore();
|
|
|
50
50
|
const { showSpinner: spinner } = storeToRefs(ui);
|
|
51
51
|
const router = useRouter();
|
|
52
52
|
const isLoading = ref(false);
|
|
53
|
+
const navbarRef = ref(null);
|
|
54
|
+
let navbarResizeObserver = null;
|
|
53
55
|
|
|
54
56
|
// Initialize zoom prevention (all browsers)
|
|
55
57
|
const { KEY_CODES } = useZoomPrevention();
|
|
@@ -78,6 +80,71 @@ document.onkeydown = function (evt) {
|
|
|
78
80
|
if (!window.location.href.includes(":5173")) ui.startSpinner("Loading...");
|
|
79
81
|
|
|
80
82
|
const layout = computed(() => router.currentRoute.value.meta.layout);
|
|
83
|
+
const routerWrapperStyle = computed(() => ({
|
|
84
|
+
paddingTop: layout.value === "dashboard" ? "var(--dashboard-navbar-height, 80px)" : "0px",
|
|
85
|
+
paddingLeft: "var(--safe-area-left, 0px)",
|
|
86
|
+
paddingRight: "var(--safe-area-right, 0px)",
|
|
87
|
+
paddingBottom: "var(--safe-area-bottom, 0px)"
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
const updateNavbarHeight = () => {
|
|
91
|
+
if (layout.value !== "dashboard") {
|
|
92
|
+
document.documentElement.style.setProperty("--dashboard-navbar-height", "0px");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const navbarElement = navbarRef.value?.$el;
|
|
97
|
+
if (!navbarElement) return;
|
|
98
|
+
|
|
99
|
+
const height = Math.ceil(navbarElement.getBoundingClientRect().height);
|
|
100
|
+
if (height > 0) {
|
|
101
|
+
document.documentElement.style.setProperty("--dashboard-navbar-height", `${height}px`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const scheduleNavbarHeightUpdate = () => {
|
|
106
|
+
requestAnimationFrame(updateNavbarHeight);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const syncNavbarObserver = () => {
|
|
110
|
+
if (!navbarResizeObserver) return;
|
|
111
|
+
|
|
112
|
+
navbarResizeObserver.disconnect();
|
|
113
|
+
const navbarElement = navbarRef.value?.$el;
|
|
114
|
+
if (layout.value === "dashboard" && navbarElement) {
|
|
115
|
+
navbarResizeObserver.observe(navbarElement);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
watch(
|
|
120
|
+
[layout, () => navbarRef.value?.$el],
|
|
121
|
+
() => {
|
|
122
|
+
nextTick(() => {
|
|
123
|
+
syncNavbarObserver();
|
|
124
|
+
scheduleNavbarHeightUpdate();
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
{ immediate: true }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
onMounted(() => {
|
|
131
|
+
scheduleNavbarHeightUpdate();
|
|
132
|
+
window.addEventListener("resize", scheduleNavbarHeightUpdate, { passive: true });
|
|
133
|
+
window.visualViewport?.addEventListener("resize", scheduleNavbarHeightUpdate);
|
|
134
|
+
|
|
135
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
136
|
+
navbarResizeObserver = new ResizeObserver(scheduleNavbarHeightUpdate);
|
|
137
|
+
syncNavbarObserver();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
onUnmounted(() => {
|
|
142
|
+
window.removeEventListener("resize", scheduleNavbarHeightUpdate);
|
|
143
|
+
window.visualViewport?.removeEventListener("resize", scheduleNavbarHeightUpdate);
|
|
144
|
+
navbarResizeObserver?.disconnect();
|
|
145
|
+
navbarResizeObserver = null;
|
|
146
|
+
document.documentElement.style.setProperty("--dashboard-navbar-height", "0px");
|
|
147
|
+
});
|
|
81
148
|
</script>
|
|
82
149
|
|
|
83
150
|
<style lang="scss">
|
|
@@ -148,7 +215,6 @@ const layout = computed(() => router.currentRoute.value.meta.layout);
|
|
|
148
215
|
|
|
149
216
|
/* Router wrapper */
|
|
150
217
|
.router-wrapper {
|
|
151
|
-
@apply pt-16 lg:pt-20;
|
|
152
218
|
z-index: 0;
|
|
153
219
|
}
|
|
154
220
|
|
|
@@ -5,14 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
.page-header {
|
|
7
7
|
@apply flex items-center justify-between;
|
|
8
|
-
padding-top:
|
|
8
|
+
padding-top: 0.75rem;
|
|
9
9
|
padding-bottom: 0.5rem;
|
|
10
|
+
flex-wrap: nowrap;
|
|
11
|
+
gap: 0.5rem;
|
|
10
12
|
|
|
11
13
|
.page-header-card {
|
|
12
14
|
@apply flex items-center gap-2.5 rounded-lg;
|
|
13
15
|
padding: 0.375rem 0.75rem;
|
|
14
16
|
background: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%);
|
|
15
17
|
border: 1px solid var(--color-border-light);
|
|
18
|
+
flex: 0 1 auto;
|
|
19
|
+
min-width: 0;
|
|
20
|
+
max-width: 60%;
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
overflow: hidden;
|
|
16
23
|
|
|
17
24
|
svg, img {
|
|
18
25
|
width: 17px;
|
|
@@ -74,8 +81,6 @@
|
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
@screen sm {
|
|
77
|
-
padding-top: 1rem;
|
|
78
|
-
|
|
79
84
|
.page-header-card {
|
|
80
85
|
gap: 0.625rem;
|
|
81
86
|
padding: 0.5rem 0.875rem;
|
|
@@ -111,9 +116,3 @@
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
|
-
|
|
115
|
-
@media (display-mode: standalone) {
|
|
116
|
-
.page-header {
|
|
117
|
-
padding-top: 3.5rem !important;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
background: var(--color-bg-input);
|
|
12
12
|
transition: all 0.15s ease;
|
|
13
13
|
display: flex;
|
|
14
|
+
position: relative;
|
|
14
15
|
|
|
15
16
|
.tag-toggle,
|
|
16
17
|
.dropdown,
|
|
@@ -33,18 +34,65 @@
|
|
|
33
34
|
.tag-toggle {
|
|
34
35
|
padding: 0;
|
|
35
36
|
border: 0 !important;
|
|
37
|
+
flex-shrink: 0 !important;
|
|
38
|
+
min-width: max-content !important;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
.tag-toggle button {
|
|
39
42
|
font-size: 0.875rem;
|
|
40
43
|
padding: 0.5rem 0.75rem;
|
|
41
|
-
min-width: fit-content;
|
|
44
|
+
min-width: fit-content !important;
|
|
42
45
|
border: 0 !important;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
flex-shrink: 0 !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* iPad - prevent TagToggle from being compressed */
|
|
51
|
+
@media (min-width: 768px) and (max-width: 1024px) {
|
|
52
|
+
.tag-toggle {
|
|
53
|
+
flex-shrink: 0 !important;
|
|
54
|
+
min-width: max-content !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.tag-toggle button {
|
|
58
|
+
padding: 0.375rem 0.5rem;
|
|
59
|
+
font-size: 0.75rem;
|
|
60
|
+
flex-shrink: 0 !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
input {
|
|
64
|
+
flex: 0 1 auto;
|
|
65
|
+
min-width: 100px !important;
|
|
66
|
+
}
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
.dropdown {
|
|
46
70
|
width: 120px !important;
|
|
47
71
|
min-width: 120px !important;
|
|
72
|
+
overflow: visible !important;
|
|
73
|
+
position: static !important;
|
|
74
|
+
|
|
75
|
+
/* iPad responsive - ensure arrow is visible */
|
|
76
|
+
@media (min-width: 768px) and (max-width: 1024px) {
|
|
77
|
+
width: 140px !important;
|
|
78
|
+
min-width: 140px !important;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Mobile - more flexible */
|
|
83
|
+
@media (max-width: 767px) {
|
|
84
|
+
width: auto !important;
|
|
85
|
+
min-width: 90px !important;
|
|
86
|
+
max-width: 110px !important;
|
|
87
|
+
flex-shrink: 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Ensure dropdown menu portal isn't clipped */
|
|
92
|
+
.dropdown-menu-portal,
|
|
93
|
+
.dropdown-content-portal {
|
|
94
|
+
overflow: visible !important;
|
|
95
|
+
z-index: 9999 !important;
|
|
48
96
|
}
|
|
49
97
|
|
|
50
98
|
.dropdown:not(.transparent):focus-within,
|
|
@@ -78,43 +78,22 @@
|
|
|
78
78
|
/* Console main container with responsive heights */
|
|
79
79
|
.console-main {
|
|
80
80
|
@apply relative overflow-x-auto overflow-y-auto rounded border-2 border-dark-550 bg-dark-400 p-2 font-mono text-white;
|
|
81
|
-
height: calc(100vh - 20rem);
|
|
82
|
-
max-height: calc(100vh - 20rem);
|
|
83
81
|
scroll-padding: 0.5rem;
|
|
84
82
|
-webkit-overflow-scrolling: touch;
|
|
85
83
|
overscroll-behavior: contain;
|
|
86
84
|
min-height: 12rem !important;
|
|
87
85
|
touch-action: pan-y pan-up pan-down;
|
|
88
86
|
|
|
89
|
-
@screen md {
|
|
90
|
-
height: calc(100vh - 18rem);
|
|
91
|
-
max-height: calc(100vh - 18rem);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
87
|
@screen lg {
|
|
95
|
-
height: calc(100vh - 16rem);
|
|
96
|
-
max-height: calc(100vh - 16rem);
|
|
97
88
|
padding: 1.25rem;
|
|
98
89
|
}
|
|
99
90
|
|
|
100
91
|
@screen mobile-portrait {
|
|
101
|
-
height: calc(100vh - 22rem);
|
|
102
|
-
max-height: calc(100vh - 22rem);
|
|
103
92
|
overflow: auto;
|
|
104
93
|
padding: 0.25rem;
|
|
105
94
|
font-size: 0.75rem;
|
|
106
95
|
line-height: 1rem;
|
|
107
96
|
}
|
|
108
|
-
|
|
109
|
-
@media (orientation: landscape) and (max-width: 1023px) {
|
|
110
|
-
height: calc(100vh - 12rem);
|
|
111
|
-
max-height: calc(100vh - 12rem);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@media (max-width: 767px) and (orientation: landscape) {
|
|
115
|
-
height: auto;
|
|
116
|
-
max-height: 65vh;
|
|
117
|
-
}
|
|
118
97
|
}
|
|
119
98
|
|
|
120
99
|
/* FilterBuilder action button */
|
package/src/assets/css/main.scss
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
BODY LAYOUT & BACKGROUND
|
|
23
23
|
========================================================================== */
|
|
24
24
|
|
|
25
|
+
:root {
|
|
26
|
+
--safe-area-top: 0px;
|
|
27
|
+
--safe-area-right: 0px;
|
|
28
|
+
--safe-area-bottom: 0px;
|
|
29
|
+
--safe-area-left: 0px;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
html {
|
|
26
33
|
@apply bg-dark-300;
|
|
27
34
|
min-height: 100vh;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<div class="mb-
|
|
3
|
+
<div class="mb-2 flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
|
4
4
|
<div class="flex flex-col gap-3 md:flex-1 md:flex-row md:items-center">
|
|
5
5
|
<div class="w-full md:w-64">
|
|
6
6
|
<Dropdown
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
</button>
|
|
80
80
|
</div>
|
|
81
81
|
</div>
|
|
82
|
-
<div class="mb-
|
|
82
|
+
<div class="mb-2 mt-2 flex justify-between gap-2 md:hidden">
|
|
83
83
|
<button class="flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
|
|
84
84
|
<h3 class="text-sm text-white">Hide Monitors</h3>
|
|
85
85
|
<Switch class="scale-75" :model-value="filteredLogs" @update:model-value="$emit('update:filteredLogs', $event)" />
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
<div class="admin-editor-section" :class="{ 'landscape-hidden': isHidden }" v-if="!isHidden">
|
|
3
3
|
<h5 class="text-white text-xl font-bold flex gap-x-3 mb-3">Admin Editor</h5>
|
|
4
4
|
<div class="flex items-center gap-2 w-full">
|
|
5
|
-
<div class="
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
<div class="admin-file-selector flex-1 lg:flex-initial lg:w-64 dropdown-container z-dropdown-high">
|
|
6
|
+
<div class="relative min-w-0 flex-grow">
|
|
7
|
+
<Dropdown
|
|
8
|
+
class="admin-file-dropdown w-full"
|
|
9
|
+
default="Select a file"
|
|
10
|
+
:onClick="loadFile"
|
|
11
|
+
:options="availableFiles"
|
|
12
|
+
:allowDefault="false"
|
|
13
|
+
:includeAdjacentButtons="true"
|
|
14
|
+
rightAmount="right-1" />
|
|
15
|
+
</div>
|
|
14
16
|
<button
|
|
15
|
-
class="refresh-button
|
|
16
|
-
style="transition: all 0.2s ease"
|
|
17
|
+
class="refresh-button"
|
|
17
18
|
@click="$emit('refresh-files')"
|
|
18
19
|
title="Refresh file list">
|
|
19
20
|
<ReloadIcon class="refresh-icon icon-md" />
|
|
@@ -180,6 +181,40 @@ onMounted(() => {
|
|
|
180
181
|
</script>
|
|
181
182
|
|
|
182
183
|
<style scoped>
|
|
184
|
+
.admin-file-selector {
|
|
185
|
+
@apply flex items-center overflow-hidden rounded-lg border-2 border-dark-550 bg-dark-500;
|
|
186
|
+
min-height: 40px;
|
|
187
|
+
transition: border-color 0.15s ease;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.admin-file-selector:hover {
|
|
191
|
+
@apply border-dark-650;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.admin-file-selector:focus-within {
|
|
195
|
+
border-color: var(--color-primary);
|
|
196
|
+
outline: 1px solid var(--color-primary);
|
|
197
|
+
outline-offset: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
:deep(.admin-file-dropdown.dropdown) {
|
|
201
|
+
@apply h-10 border-0 rounded-none bg-transparent shadow-none;
|
|
202
|
+
padding: 0 0.75rem !important;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:deep(.admin-file-dropdown .dropdown-display) {
|
|
206
|
+
padding: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.refresh-button {
|
|
210
|
+
@apply flex h-10 w-10 flex-shrink-0 items-center justify-center border-l border-dark-550 bg-dark-400 text-white;
|
|
211
|
+
transition: all 0.2s ease;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.refresh-button:hover {
|
|
215
|
+
@apply border-dark-625 bg-dark-450;
|
|
216
|
+
}
|
|
217
|
+
|
|
183
218
|
.editor-action-btn {
|
|
184
219
|
@apply flex items-center justify-center w-9 h-9 rounded border-2 border-dark-550 bg-dark-400 text-white;
|
|
185
220
|
transition: all 0.2s ease;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<button
|
|
3
3
|
@click="increase"
|
|
4
|
-
class="h-10 px-
|
|
4
|
+
class="h-10 px-3 text-white bg-dark-500 relative overflow-hidden"
|
|
5
5
|
:class="noBorder ? 'border-0' : 'border-2 border-dark-550'"
|
|
6
|
+
style="min-width: max-content;"
|
|
6
7
|
>
|
|
7
8
|
<span v-if="sortOptions[currentOpt % sortOptions.length] === 'Enabled'"
|
|
8
9
|
><img class="mx-auto" height="16px" width="14px" src="@/assets/img/square_check.svg"
|
|
@@ -300,7 +300,7 @@ props.filterBuilder.onUpdate(() => {
|
|
|
300
300
|
|
|
301
301
|
<style scoped>
|
|
302
302
|
.filter-card {
|
|
303
|
-
@apply bg-dark-500 border border-dark-625/30 relative
|
|
303
|
+
@apply bg-dark-500 border border-dark-625/30 relative transition-colors duration-200;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
.filter-card:hover:not(.expanded-filter) {
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="navbar" :class="{ 'force-z': menuOpen }">
|
|
3
|
-
<div
|
|
3
|
+
<div
|
|
4
|
+
:class="[
|
|
5
|
+
'component-container ios-wrapper relative flex items-center px-3 lg:px-4',
|
|
6
|
+
{ 'ios-wrapper': landscapeIos }
|
|
7
|
+
]">
|
|
4
8
|
<router-link to="/">
|
|
5
|
-
<img
|
|
9
|
+
<img
|
|
10
|
+
src="@/assets/img/logo_trans.png"
|
|
11
|
+
class="z-30 mr-4 h-6 cursor-pointer object-cover lg:h-8"
|
|
12
|
+
alt="Logo: Necro" />
|
|
6
13
|
</router-link>
|
|
7
14
|
<ul class="hidden lg:flex">
|
|
8
15
|
<li>
|
|
9
|
-
<router-link to="/"
|
|
16
|
+
<router-link to="/">
|
|
17
|
+
<TasksIcon />
|
|
18
|
+
<span class="hidden xl:block">Tasks</span>
|
|
19
|
+
</router-link>
|
|
10
20
|
</li>
|
|
11
21
|
<li>
|
|
12
|
-
<router-link to="/editor"
|
|
22
|
+
<router-link to="/editor">
|
|
23
|
+
<EditIcon />
|
|
24
|
+
<span class="hidden xl:block">Editor</span>
|
|
25
|
+
</router-link>
|
|
13
26
|
</li>
|
|
14
27
|
<li v-if="ui.profile?.admin">
|
|
15
|
-
<router-link to="/console"
|
|
28
|
+
<router-link to="/console">
|
|
29
|
+
<ConsoleIcon />
|
|
30
|
+
<span class="hidden xl:block">Console</span>
|
|
31
|
+
</router-link>
|
|
16
32
|
</li>
|
|
17
33
|
<li>
|
|
18
34
|
<router-link to="/profiles">
|
|
@@ -27,45 +43,60 @@
|
|
|
27
43
|
</router-link>
|
|
28
44
|
</li>
|
|
29
45
|
<li>
|
|
30
|
-
<router-link to="/filter"
|
|
46
|
+
<router-link to="/filter">
|
|
47
|
+
<FilterIcon />
|
|
48
|
+
<span class="hidden xl:block">Filter</span>
|
|
49
|
+
</router-link>
|
|
31
50
|
</li>
|
|
32
51
|
</ul>
|
|
33
52
|
|
|
34
|
-
<button class="
|
|
53
|
+
<button class="smooth-hover ml-auto mr-4 hidden lg:block" @click="logout()" aria-label="Logout">
|
|
35
54
|
<LogoutIcon />
|
|
36
55
|
</button>
|
|
37
|
-
<h4 v-if="ui.profile?.name" class="hidden
|
|
38
|
-
<span class="text-lightgray">Logged in as
|
|
39
|
-
<span class="font-black">
|
|
56
|
+
<h4 v-if="ui.profile?.name" class="hidden text-sm font-medium text-white lg:block">
|
|
57
|
+
<span class="text-lightgray">Logged in as</span>
|
|
58
|
+
<span class="ml-1 font-black">{{ ui.profile?.name }}</span>
|
|
40
59
|
</h4>
|
|
41
|
-
<h4 v-else class="hidden
|
|
42
|
-
<span class="text-lightgray">Loading
|
|
60
|
+
<h4 v-else class="hidden text-sm font-medium text-white lg:block">
|
|
61
|
+
<span class="text-lightgray">Loading</span>
|
|
43
62
|
</h4>
|
|
44
63
|
<img
|
|
45
64
|
v-if="ui.profile?.profilePicture"
|
|
46
65
|
:src="ui.profile?.profilePicture"
|
|
47
66
|
alt="Profile Picture"
|
|
48
|
-
class="size-10 rounded-full
|
|
49
|
-
/>
|
|
50
|
-
<
|
|
51
|
-
<CountryChooser class="hidden lg:block" />
|
|
67
|
+
class="ipad-safe-top mx-4 hidden size-10 rounded-full lg:block" />
|
|
68
|
+
<div v-else class="ipad-safe-top mx-4 hidden size-10 rounded-full bg-dark-400 lg:block" />
|
|
69
|
+
<CountryChooser class="ipad-safe-top hidden lg:block" />
|
|
52
70
|
|
|
53
|
-
<button
|
|
71
|
+
<button
|
|
72
|
+
class="z-30 -m-2 ml-auto flex p-2 lg:hidden"
|
|
73
|
+
@click="toggleMenu"
|
|
74
|
+
aria-label="Toggle navigation menu"
|
|
75
|
+
:aria-expanded="menuOpen">
|
|
54
76
|
<MenuIcon />
|
|
55
77
|
</button>
|
|
56
78
|
</div>
|
|
57
79
|
<transition name="fade">
|
|
58
|
-
<div class="mobile-menu flex
|
|
59
|
-
<CountryChooser class="mx-auto landscape:block
|
|
60
|
-
<ul class="
|
|
80
|
+
<div class="mobile-menu z-30 flex flex-col lg:hidden" v-if="menuOpen">
|
|
81
|
+
<CountryChooser class="mx-auto mt-4 hidden landscape:block" />
|
|
82
|
+
<ul class="mx-auto mt-12 flex grid-cols-3 flex-col landscape:grid">
|
|
61
83
|
<li @click="toggleMenu">
|
|
62
|
-
<router-link to="/"
|
|
84
|
+
<router-link to="/">
|
|
85
|
+
<TasksIcon />
|
|
86
|
+
Tasks
|
|
87
|
+
</router-link>
|
|
63
88
|
</li>
|
|
64
89
|
<li @click="toggleMenu">
|
|
65
|
-
<router-link to="/editor"
|
|
90
|
+
<router-link to="/editor">
|
|
91
|
+
<EditIcon />
|
|
92
|
+
Editor
|
|
93
|
+
</router-link>
|
|
66
94
|
</li>
|
|
67
95
|
<li v-if="ui.profile?.admin" @click="toggleMenu">
|
|
68
|
-
<router-link to="/console"
|
|
96
|
+
<router-link to="/console">
|
|
97
|
+
<ConsoleIcon />
|
|
98
|
+
Console
|
|
99
|
+
</router-link>
|
|
69
100
|
</li>
|
|
70
101
|
<li @click="toggleMenu">
|
|
71
102
|
<router-link to="/profiles">
|
|
@@ -80,22 +111,25 @@
|
|
|
80
111
|
</router-link>
|
|
81
112
|
</li>
|
|
82
113
|
<li @click="toggleMenu">
|
|
83
|
-
<router-link to="/filter"
|
|
114
|
+
<router-link to="/filter">
|
|
115
|
+
<FilterIcon />
|
|
116
|
+
Filter
|
|
117
|
+
</router-link>
|
|
84
118
|
</li>
|
|
85
119
|
</ul>
|
|
86
|
-
<CountryChooser class="mx-auto block landscape:hidden
|
|
87
|
-
<div class="
|
|
120
|
+
<CountryChooser class="mx-auto mb-auto block landscape:hidden" />
|
|
121
|
+
<div class="mx-auto flex items-center landscape:mb-0">
|
|
88
122
|
<button class="mr-4" @click="logout()" aria-label="Logout">
|
|
89
123
|
<LogoutIcon />
|
|
90
124
|
</button>
|
|
91
|
-
<h4 class="
|
|
92
|
-
<span class="text-lightgray">Logged in as
|
|
93
|
-
<span class="font-black">
|
|
125
|
+
<h4 class="mr-4 text-sm font-medium text-white">
|
|
126
|
+
<span class="text-lightgray">Logged in as</span>
|
|
127
|
+
<span class="ml-1 font-black">{{ ui.profile?.name }}</span>
|
|
94
128
|
</h4>
|
|
95
129
|
<img :src="ui.profile?.profilePicture" alt="" class="size-10 rounded-full" />
|
|
96
130
|
</div>
|
|
97
131
|
|
|
98
|
-
<div class="mx-auto mt-6
|
|
132
|
+
<div class="mx-auto mb-14 mt-6 flex items-center gap-3">
|
|
99
133
|
<div class="version-badge">
|
|
100
134
|
<span class="version-label">Dashboard</span>
|
|
101
135
|
<span class="version-number">v{{ dashVersion }}</span>
|
|
@@ -111,25 +145,27 @@
|
|
|
111
145
|
</template>
|
|
112
146
|
<style lang="scss" scoped>
|
|
113
147
|
.navbar {
|
|
114
|
-
@apply border-b py-5
|
|
148
|
+
@apply fixed w-full border-b py-5;
|
|
115
149
|
top: 0;
|
|
116
150
|
left: 0;
|
|
117
151
|
z-index: 1000;
|
|
118
|
-
background: theme(
|
|
152
|
+
background: theme("colors.dark.300 / 0.95");
|
|
119
153
|
backdrop-filter: blur(23px);
|
|
120
154
|
-webkit-backdrop-filter: blur(23px);
|
|
121
|
-
border-color: theme(
|
|
155
|
+
border-color: theme("colors.border");
|
|
122
156
|
|
|
123
157
|
// Consistent padding base
|
|
124
|
-
padding-top: 1.
|
|
158
|
+
padding-top: calc(1.1rem + var(--safe-area-top, 0px));
|
|
125
159
|
padding-bottom: 1.25rem;
|
|
160
|
+
padding-left: var(--safe-area-left, 0px);
|
|
161
|
+
padding-right: var(--safe-area-right, 0px);
|
|
126
162
|
|
|
127
163
|
// Only add safe area top padding for portrait notch devices
|
|
128
|
-
@supports (padding-top: env(safe-area-inset-top)) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
164
|
+
// @supports (padding-top: env(safe-area-inset-top)) {
|
|
165
|
+
// @media (max-device-width: 430px) and (orientation: portrait) {
|
|
166
|
+
// padding-top: max(1.25rem, env(safe-area-inset-top));
|
|
167
|
+
// }
|
|
168
|
+
// }
|
|
133
169
|
|
|
134
170
|
// Extend background above viewport for notch area with theme gradient
|
|
135
171
|
&::before {
|
|
@@ -139,7 +175,7 @@
|
|
|
139
175
|
left: 0;
|
|
140
176
|
right: 0;
|
|
141
177
|
height: 100px;
|
|
142
|
-
background: theme(
|
|
178
|
+
background: theme("colors.dark.300");
|
|
143
179
|
backdrop-filter: blur(8px);
|
|
144
180
|
-webkit-backdrop-filter: blur(8px);
|
|
145
181
|
z-index: -1;
|
|
@@ -158,7 +194,7 @@
|
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
li a {
|
|
161
|
-
@apply flex
|
|
197
|
+
@apply flex items-center rounded-lg text-sm text-white;
|
|
162
198
|
height: 40px;
|
|
163
199
|
border: 1px solid transparent;
|
|
164
200
|
border-left: 3px solid transparent;
|
|
@@ -170,16 +206,16 @@
|
|
|
170
206
|
}
|
|
171
207
|
|
|
172
208
|
&.router-link-exact-active {
|
|
173
|
-
border-bottom: 2px solid theme(
|
|
174
|
-
color: theme(
|
|
175
|
-
background: theme(
|
|
209
|
+
border-bottom: 2px solid theme("colors.primary");
|
|
210
|
+
color: theme("colors.primary");
|
|
211
|
+
background: theme("colors.primary / 0.08");
|
|
176
212
|
margin-bottom: -2px;
|
|
177
213
|
|
|
178
214
|
// Mobile styling
|
|
179
215
|
@media (max-width: 1023px) {
|
|
180
|
-
border: 2px solid theme(
|
|
181
|
-
color: theme(
|
|
182
|
-
background: theme(
|
|
216
|
+
border: 2px solid theme("colors.primary");
|
|
217
|
+
color: theme("colors.primary");
|
|
218
|
+
background: theme("colors.primary / 0.08");
|
|
183
219
|
padding: 0.5rem 0.75rem;
|
|
184
220
|
border-radius: 0.5rem;
|
|
185
221
|
margin-bottom: 0;
|
|
@@ -213,9 +249,9 @@
|
|
|
213
249
|
}
|
|
214
250
|
|
|
215
251
|
&.router-link-exact-active {
|
|
216
|
-
border: 2px solid theme(
|
|
217
|
-
color: theme(
|
|
218
|
-
background: theme(
|
|
252
|
+
border: 2px solid theme("colors.primary") !important;
|
|
253
|
+
color: theme("colors.primary") !important;
|
|
254
|
+
background: theme("colors.primary / 0.08") !important;
|
|
219
255
|
border-radius: 0.5rem !important;
|
|
220
256
|
width: 40px !important;
|
|
221
257
|
height: 40px !important;
|
|
@@ -237,6 +273,13 @@
|
|
|
237
273
|
}
|
|
238
274
|
}
|
|
239
275
|
|
|
276
|
+
/* iPad - push right-side elements down from status bar */
|
|
277
|
+
@media (min-width: 768px) and (max-width: 1024px) {
|
|
278
|
+
.ipad-safe-top {
|
|
279
|
+
margin-top: 3px;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
240
283
|
.force-z {
|
|
241
284
|
z-index: 20000;
|
|
242
285
|
}
|
|
@@ -246,10 +289,14 @@
|
|
|
246
289
|
height: 100vh;
|
|
247
290
|
height: 100dvh; // Dynamic viewport height for mobile
|
|
248
291
|
will-change: transform, opacity, filter;
|
|
249
|
-
@apply
|
|
292
|
+
@apply fixed inset-0 z-10 w-full bg-dark-400 pb-8 pt-20;
|
|
293
|
+
padding-top: calc(5rem + var(--safe-area-top, 0px));
|
|
294
|
+
padding-bottom: calc(2rem + var(--safe-area-bottom, 0px));
|
|
295
|
+
padding-left: var(--safe-area-left, 0px);
|
|
296
|
+
padding-right: var(--safe-area-right, 0px);
|
|
250
297
|
|
|
251
298
|
ul li {
|
|
252
|
-
@apply mb-4
|
|
299
|
+
@apply mx-auto mb-4;
|
|
253
300
|
|
|
254
301
|
a {
|
|
255
302
|
font-size: 20px !important;
|
|
@@ -258,18 +305,18 @@
|
|
|
258
305
|
}
|
|
259
306
|
|
|
260
307
|
.version-badge {
|
|
261
|
-
@apply flex items-center gap-x-2 px-3 py-2
|
|
262
|
-
background: theme(
|
|
263
|
-
border-color: theme(
|
|
308
|
+
@apply flex items-center gap-x-2 rounded-lg border px-3 py-2;
|
|
309
|
+
background: theme("colors.bg-elevated");
|
|
310
|
+
border-color: theme("colors.dark.550");
|
|
264
311
|
|
|
265
312
|
.version-label {
|
|
266
313
|
@apply text-xs font-medium;
|
|
267
|
-
color: theme(
|
|
314
|
+
color: theme("colors.text-muted");
|
|
268
315
|
}
|
|
269
316
|
|
|
270
317
|
.version-number {
|
|
271
318
|
@apply text-xs font-bold;
|
|
272
|
-
color: theme(
|
|
319
|
+
color: theme("colors.primary");
|
|
273
320
|
}
|
|
274
321
|
}
|
|
275
322
|
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ref, watch, onMounted } from
|
|
2
|
-
import { useRouter } from
|
|
3
|
-
import { useDeviceDetection } from
|
|
4
|
-
import { DEBUG } from
|
|
1
|
+
import { ref, watch, onMounted } from "vue";
|
|
2
|
+
import { useRouter } from "vue-router";
|
|
3
|
+
import { useDeviceDetection } from "./useDeviceDetection";
|
|
4
|
+
import { DEBUG } from "@/utils/debug";
|
|
5
5
|
|
|
6
6
|
export function useNotchHandling(logger) {
|
|
7
7
|
const { isIOS, isIpadOS } = useDeviceDetection();
|
|
@@ -44,11 +44,19 @@ export function useNotchHandling(logger) {
|
|
|
44
44
|
if (isNotchBusy && !force) return;
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
|
-
if (!isIOS()
|
|
47
|
+
if (!isIOS()) {
|
|
48
|
+
document.documentElement.style.setProperty("--safe-area-top", "0px");
|
|
49
|
+
document.documentElement.style.setProperty("--safe-area-right", "0px");
|
|
50
|
+
document.documentElement.style.setProperty("--safe-area-bottom", "0px");
|
|
51
|
+
document.documentElement.style.setProperty("--safe-area-left", "0px");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
if (isIpadOS()) {
|
|
56
|
+
document.documentElement.style.setProperty("--safe-area-top", "12px");
|
|
57
|
+
document.documentElement.style.setProperty("--safe-area-right", "0px");
|
|
58
|
+
document.documentElement.style.setProperty("--safe-area-bottom", "0px");
|
|
59
|
+
document.documentElement.style.setProperty("--safe-area-left", "0px");
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -78,56 +86,22 @@ export function useNotchHandling(logger) {
|
|
|
78
86
|
});
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
if (hasNotch
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
testDiv.style.visibility = "hidden";
|
|
87
|
-
testDiv.style.paddingLeft = "env(safe-area-inset-left)";
|
|
88
|
-
testDiv.style.paddingRight = "env(safe-area-inset-right)";
|
|
89
|
-
document.body.appendChild(testDiv);
|
|
90
|
-
|
|
91
|
-
const computedStyle = getComputedStyle(testDiv);
|
|
92
|
-
const leftInset = parseFloat(computedStyle.paddingLeft) || 0;
|
|
93
|
-
const rightInset = parseFloat(computedStyle.paddingRight) || 0;
|
|
94
|
-
|
|
95
|
-
document.body.removeChild(testDiv);
|
|
96
|
-
|
|
97
|
-
if (DEBUG && logger) logger.Debug("🔍 Safe area insets:", { leftInset, rightInset });
|
|
98
|
-
|
|
99
|
-
wrappers.forEach((wrapper) => {
|
|
100
|
-
if (leftInset > 0) {
|
|
101
|
-
wrapper.style.paddingLeft = "env(safe-area-inset-left)";
|
|
102
|
-
wrapper.style.paddingRight = "0.5rem";
|
|
103
|
-
} else if (rightInset > 0) {
|
|
104
|
-
wrapper.style.paddingLeft = "0.5rem";
|
|
105
|
-
wrapper.style.paddingRight = "env(safe-area-inset-right)";
|
|
106
|
-
} else {
|
|
107
|
-
wrapper.style.paddingLeft = "0.5rem";
|
|
108
|
-
wrapper.style.paddingRight = "0.5rem";
|
|
109
|
-
}
|
|
110
|
-
});
|
|
89
|
+
if (hasNotch) {
|
|
90
|
+
document.documentElement.style.setProperty("--safe-area-top", "min(env(safe-area-inset-top), 32px)");
|
|
91
|
+
document.documentElement.style.setProperty("--safe-area-right", "env(safe-area-inset-right)");
|
|
92
|
+
document.documentElement.style.setProperty("--safe-area-bottom", "env(safe-area-inset-bottom)");
|
|
93
|
+
document.documentElement.style.setProperty("--safe-area-left", "env(safe-area-inset-left)");
|
|
111
94
|
} else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
: window.innerWidth > RESPONSIVE_PADDING.LG.minWidth
|
|
123
|
-
? RESPONSIVE_PADDING.LG.padding
|
|
124
|
-
: window.innerWidth > RESPONSIVE_PADDING.MD.minWidth
|
|
125
|
-
? RESPONSIVE_PADDING.MD.padding
|
|
126
|
-
: RESPONSIVE_PADDING.DEFAULT.padding;
|
|
127
|
-
|
|
128
|
-
wrappers.forEach((wrapper) => {
|
|
129
|
-
wrapper.style.paddingLeft = padding;
|
|
130
|
-
wrapper.style.paddingRight = padding;
|
|
95
|
+
document.documentElement.style.setProperty("--safe-area-top", "0px");
|
|
96
|
+
document.documentElement.style.setProperty("--safe-area-right", "0px");
|
|
97
|
+
document.documentElement.style.setProperty("--safe-area-bottom", "0px");
|
|
98
|
+
document.documentElement.style.setProperty("--safe-area-left", "0px");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (DEBUG && logger) {
|
|
102
|
+
logger.Debug("🔍 Safe area vars updated", {
|
|
103
|
+
hasNotch,
|
|
104
|
+
isLandscape
|
|
131
105
|
});
|
|
132
106
|
}
|
|
133
107
|
|
package/src/views/Accounts.vue
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
@change="(f) => (ui.search.accounts.show = f)"
|
|
39
39
|
:noBorder="true" />
|
|
40
40
|
<input
|
|
41
|
-
class="h-10 w-44 text-white text-sm p-2 bg-dark-500 flex items-center relative"
|
|
41
|
+
class="h-10 w-28 md:w-36 lg:w-44 text-white text-sm p-2 bg-dark-500 flex items-center relative flex-shrink-1"
|
|
42
42
|
placeholder="Search Email"
|
|
43
43
|
aria-label="Search email"
|
|
44
44
|
autocomplete="new-password"
|
package/src/views/Console.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="mb-
|
|
3
|
-
<div class="page-header" style="padding-bottom: 0.
|
|
2
|
+
<div class="mb-2 md:mb-0" ref="consolePageRef">
|
|
3
|
+
<div class="page-header" style="padding-bottom: 0.5rem;">
|
|
4
4
|
<div class="page-header-card">
|
|
5
5
|
<ConsoleIcon />
|
|
6
6
|
<h4>Console</h4>
|
|
@@ -8,21 +8,24 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
<div ref="toolbarRef">
|
|
12
|
+
<ConsoleToolbar
|
|
13
|
+
v-model:currentTaskLog="currentTaskLog"
|
|
14
|
+
v-model:searchQuery="searchQuery"
|
|
15
|
+
v-model:filteredLogs="filteredLogs"
|
|
16
|
+
v-model:autoscrollToggled="autoscrollToggled"
|
|
17
|
+
:taskLogMapping="taskLogMapping"
|
|
18
|
+
:userScrolledUp="userScrolledUp"
|
|
19
|
+
:filteredCount="filteredCount"
|
|
20
|
+
@scroll="startScrolling"
|
|
21
|
+
@scroll-stop="stopScrolling"
|
|
22
|
+
@autoscroll-toggle="onAutoscrollToggle" />
|
|
23
|
+
</div>
|
|
22
24
|
|
|
23
25
|
<Smoothie
|
|
24
26
|
:weight="0.2"
|
|
25
27
|
class="console-main"
|
|
28
|
+
:style="consoleMainStyle"
|
|
26
29
|
ref="$autoscroll"
|
|
27
30
|
@wheel.stop
|
|
28
31
|
@touchmove.stop
|
|
@@ -45,22 +48,6 @@
|
|
|
45
48
|
v-bind:key="`log-${index}`"
|
|
46
49
|
:style="{ '--index': index }"><code class="md:text-sm lg:text-base mobile-portrait:text-xs+ mobile-portrait:leading-tight" v-html="line"></code></pre>
|
|
47
50
|
</Smoothie>
|
|
48
|
-
<div class="mb-6 mt-4 flex justify-between md:hidden mobile-portrait:mb-16 mobile-portrait:mt-6">
|
|
49
|
-
<button
|
|
50
|
-
class="flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
|
|
51
|
-
<h3 class="text-sm text-white">Hide Monitors</h3>
|
|
52
|
-
<Switch class="scale-75" v-model="filteredLogs" />
|
|
53
|
-
</button>
|
|
54
|
-
<button
|
|
55
|
-
class="relative flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
|
|
56
|
-
<h3 class="text-sm text-white">Auto</h3>
|
|
57
|
-
<Switch class="scale-75" v-model="autoscrollToggled" @change="onAutoscrollToggle" />
|
|
58
|
-
<div
|
|
59
|
-
v-if="userScrolledUp && autoscrollToggled"
|
|
60
|
-
class="absolute -right-1 -top-1 h-2 w-2 animate-pulse rounded-full bg-yellow-500"
|
|
61
|
-
title="Autoscroll paused - scroll to bottom to resume"></div>
|
|
62
|
-
</button>
|
|
63
|
-
</div>
|
|
64
51
|
</div>
|
|
65
52
|
</div>
|
|
66
53
|
</template>
|
|
@@ -123,12 +110,14 @@ import { DEBUG } from "@/utils/debug";
|
|
|
123
110
|
|
|
124
111
|
import Filter from "@/libs/ansii.js";
|
|
125
112
|
import { ConsoleIcon } from "@/components/icons";
|
|
126
|
-
import Switch from "@/components/ui/controls/atomic/Switch.vue";
|
|
127
113
|
import WebsocketHeartbeatJs from "websocket-heartbeat-js";
|
|
128
114
|
import { onMounted, onUnmounted, ref, nextTick, computed, watch } from "vue";
|
|
129
115
|
import ConsoleToolbar from "@/components/Console/ConsoleToolbar.vue";
|
|
116
|
+
import { TABLE_LAYOUT } from "@/constants/tableLayout";
|
|
130
117
|
|
|
131
118
|
const $autoscroll = ref(null);
|
|
119
|
+
const consolePageRef = ref(null);
|
|
120
|
+
const toolbarRef = ref(null);
|
|
132
121
|
const logLines = ref([]);
|
|
133
122
|
const ansii = new Filter();
|
|
134
123
|
const autoscrollToggled = ref(true);
|
|
@@ -139,6 +128,8 @@ const userScrolledUp = ref(false);
|
|
|
139
128
|
const scrollInterval = ref(null);
|
|
140
129
|
const isScrolling = ref(false);
|
|
141
130
|
const searchQuery = ref("");
|
|
131
|
+
const consoleHeight = ref(0);
|
|
132
|
+
let consoleResizeObserver = null;
|
|
142
133
|
|
|
143
134
|
// Optimized: Cache stripped versions to avoid regex on every filter
|
|
144
135
|
const logPlainTextCache = new Map();
|
|
@@ -173,8 +164,56 @@ const filteredCount = computed(() => {
|
|
|
173
164
|
const path = "/api/updates?type=console";
|
|
174
165
|
const url = (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + path;
|
|
175
166
|
|
|
176
|
-
const SCROLL_THRESHOLD =
|
|
167
|
+
const SCROLL_THRESHOLD = 100;
|
|
177
168
|
const SCROLL_AMOUNT = 100;
|
|
169
|
+
const MIN_CONSOLE_HEIGHT = 192;
|
|
170
|
+
|
|
171
|
+
const getTableHeightCap = (viewportHeight) => {
|
|
172
|
+
const isPWA = window.matchMedia("(display-mode: standalone)").matches;
|
|
173
|
+
const isMobile = window.innerWidth <= 768;
|
|
174
|
+
const isSmallMobile = window.innerWidth <= 480;
|
|
175
|
+
const extraBuffer = isPWA && isMobile ? 60 : 0;
|
|
176
|
+
const mobileEdgeBuffer = isMobile ? (isSmallMobile ? 20 : 12) : 0;
|
|
177
|
+
|
|
178
|
+
const availableHeight =
|
|
179
|
+
viewportHeight -
|
|
180
|
+
TABLE_LAYOUT.PROFILES.TOP_RESERVED_SPACE -
|
|
181
|
+
TABLE_LAYOUT.PROFILES.BOTTOM_BUFFER -
|
|
182
|
+
extraBuffer -
|
|
183
|
+
mobileEdgeBuffer;
|
|
184
|
+
const rowHeight = TABLE_LAYOUT.PROFILES.ROW_HEIGHT;
|
|
185
|
+
const minHeight = TABLE_LAYOUT.PROFILES.MIN_ROWS_TO_SHOW * rowHeight;
|
|
186
|
+
const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
|
|
187
|
+
return maxCompleteRows * rowHeight;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const consoleMainStyle = computed(() => {
|
|
191
|
+
if (!consoleHeight.value) return {};
|
|
192
|
+
const height = `${consoleHeight.value}px`;
|
|
193
|
+
return {
|
|
194
|
+
height,
|
|
195
|
+
maxHeight: height
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const updateConsoleHeight = () => {
|
|
200
|
+
const element = $autoscroll.value?.el;
|
|
201
|
+
if (!element) return;
|
|
202
|
+
|
|
203
|
+
const viewportHeight = window.visualViewport?.height || window.innerHeight;
|
|
204
|
+
const isMobile = window.innerWidth <= 768;
|
|
205
|
+
const isSmallMobile = window.innerWidth <= 480;
|
|
206
|
+
const baseBottomPadding = window.matchMedia("(display-mode: standalone)").matches ? 12 : 8;
|
|
207
|
+
const mobileEdgePadding = isMobile ? (isSmallMobile ? 16 : 10) : 0;
|
|
208
|
+
const bottomPadding = baseBottomPadding + mobileEdgePadding;
|
|
209
|
+
const availableHeight = Math.floor(viewportHeight - element.getBoundingClientRect().top - bottomPadding);
|
|
210
|
+
const tableHeightCap = getTableHeightCap(viewportHeight);
|
|
211
|
+
consoleHeight.value = Math.max(MIN_CONSOLE_HEIGHT, Math.min(availableHeight, tableHeightCap));
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const scheduleConsoleHeightUpdate = () => {
|
|
215
|
+
requestAnimationFrame(updateConsoleHeight);
|
|
216
|
+
};
|
|
178
217
|
|
|
179
218
|
const handleScroll = (event) => {
|
|
180
219
|
if (!autoscrollToggled.value) return;
|
|
@@ -184,6 +223,8 @@ const handleScroll = (event) => {
|
|
|
184
223
|
|
|
185
224
|
const isNearBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - SCROLL_THRESHOLD;
|
|
186
225
|
|
|
226
|
+
// Only mark as scrolled up if user deliberately scrolled away from bottom
|
|
227
|
+
// Ignore scroll events triggered by content additions
|
|
187
228
|
if (userScrolledUp.value === isNearBottom) {
|
|
188
229
|
userScrolledUp.value = !isNearBottom;
|
|
189
230
|
}
|
|
@@ -258,19 +299,28 @@ const stopScrolling = () => {
|
|
|
258
299
|
const autoScrollToBottom = () => {
|
|
259
300
|
if (!$autoscroll.value?.el || !autoscrollToggled.value) return;
|
|
260
301
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const targetScrollTop = element.scrollHeight - element.clientHeight;
|
|
302
|
+
const element = $autoscroll.value.el;
|
|
303
|
+
const targetScrollTop = element.scrollHeight - element.clientHeight;
|
|
304
|
+
const currentDistanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
265
305
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
306
|
+
// Only respect userScrolledUp if they're significantly away from bottom
|
|
307
|
+
// This prevents autoscroll from pausing due to content-triggered scroll events
|
|
308
|
+
if (currentDistanceFromBottom > SCROLL_THRESHOLD && userScrolledUp.value) {
|
|
309
|
+
return; // User has deliberately scrolled up
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Reset userScrolledUp if we're near bottom
|
|
313
|
+
if (currentDistanceFromBottom < SCROLL_THRESHOLD) {
|
|
314
|
+
userScrolledUp.value = false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (element.scrollTo && Math.abs(element.scrollTop - targetScrollTop) > 5) {
|
|
318
|
+
element.scrollTo({
|
|
319
|
+
top: targetScrollTop,
|
|
320
|
+
behavior: "smooth"
|
|
321
|
+
});
|
|
322
|
+
} else {
|
|
323
|
+
element.scrollTop = targetScrollTop;
|
|
274
324
|
}
|
|
275
325
|
};
|
|
276
326
|
|
|
@@ -358,6 +408,18 @@ watch([currentTaskLog, filteredLogs], () => {
|
|
|
358
408
|
onMounted(() => {
|
|
359
409
|
const socket = new WebsocketHeartbeatJs({ url, pingMsg: "ping" });
|
|
360
410
|
|
|
411
|
+
scheduleConsoleHeightUpdate();
|
|
412
|
+
requestAnimationFrame(scheduleConsoleHeightUpdate);
|
|
413
|
+
window.addEventListener("resize", scheduleConsoleHeightUpdate, { passive: true });
|
|
414
|
+
window.addEventListener("orientationchange", scheduleConsoleHeightUpdate, { passive: true });
|
|
415
|
+
window.visualViewport?.addEventListener("resize", scheduleConsoleHeightUpdate);
|
|
416
|
+
window.visualViewport?.addEventListener("scroll", scheduleConsoleHeightUpdate);
|
|
417
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
418
|
+
consoleResizeObserver = new ResizeObserver(scheduleConsoleHeightUpdate);
|
|
419
|
+
if (consolePageRef.value) consoleResizeObserver.observe(consolePageRef.value);
|
|
420
|
+
if (toolbarRef.value) consoleResizeObserver.observe(toolbarRef.value);
|
|
421
|
+
}
|
|
422
|
+
|
|
361
423
|
socket.onmessage = (event) => {
|
|
362
424
|
const msg = JSON.parse(event.data);
|
|
363
425
|
;
|
|
@@ -369,5 +431,11 @@ onMounted(() => {
|
|
|
369
431
|
|
|
370
432
|
onUnmounted(() => {
|
|
371
433
|
stopScrolling();
|
|
434
|
+
window.removeEventListener("resize", scheduleConsoleHeightUpdate);
|
|
435
|
+
window.removeEventListener("orientationchange", scheduleConsoleHeightUpdate);
|
|
436
|
+
window.visualViewport?.removeEventListener("resize", scheduleConsoleHeightUpdate);
|
|
437
|
+
window.visualViewport?.removeEventListener("scroll", scheduleConsoleHeightUpdate);
|
|
438
|
+
consoleResizeObserver?.disconnect();
|
|
439
|
+
consoleResizeObserver = null;
|
|
372
440
|
});
|
|
373
441
|
</script>
|
package/src/views/Editor.vue
CHANGED
|
@@ -102,11 +102,11 @@
|
|
|
102
102
|
</div>
|
|
103
103
|
</div>
|
|
104
104
|
</div>
|
|
105
|
-
<div class="hidden-scrollbars flex-1 overflow-y-auto overflow-x-hidden bg-dark-400"
|
|
105
|
+
<div class="hidden-scrollbars flex-1 overflow-y-auto overflow-x-hidden bg-dark-400">
|
|
106
106
|
<div
|
|
107
107
|
v-if="filterBuilder.filters.length"
|
|
108
108
|
ref="filtersListRef"
|
|
109
|
-
class="
|
|
109
|
+
class="flex flex-col gap-2 p-2">
|
|
110
110
|
<Filter
|
|
111
111
|
v-for="(f, i) in filterBuilder.filters"
|
|
112
112
|
v-show="doesFilterShow(f)"
|
|
@@ -568,9 +568,9 @@ watch(renderSeats, async () => {
|
|
|
568
568
|
|
|
569
569
|
/* Force filters list to be constrained and scrollable */
|
|
570
570
|
.hidden-scrollbars.flex-1.overflow-y-auto {
|
|
571
|
-
|
|
572
|
-
min-height: 150px;
|
|
571
|
+
flex: 1 1 0;
|
|
573
572
|
overflow-y: auto !important;
|
|
573
|
+
overflow-x: hidden !important;
|
|
574
574
|
}
|
|
575
575
|
|
|
576
576
|
/* SVG and Filters container responsive sizing */
|