@necrolab/dashboard 0.5.1 → 0.5.3
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/backend/api.js +26 -8
- package/index.html +5 -0
- package/package.json +1 -1
- package/public/manifest.json +7 -5
- package/public/reconnect-logo.png +0 -0
- package/src/App.vue +130 -1
- package/src/assets/css/components/toasts.scss +15 -6
- package/src/assets/css/main.scss +20 -19
- package/src/components/Auth/LoginForm.vue +75 -42
- package/src/components/Editors/Account/Account.vue +40 -1
- package/src/components/Editors/Profile/Profile.vue +40 -1
- package/src/components/Table/Table.vue +9 -2
- package/src/components/Tasks/Task.vue +42 -1
- package/src/components/Tasks/TaskView.vue +4 -8
- package/src/components/Tasks/Utilities.vue +9 -1
- package/src/components/Tasks/ViewTask.vue +2 -2
- package/src/components/ui/Modal.vue +4 -0
- package/src/components/ui/Navbar.vue +5 -5
- package/src/components/ui/ReconnectIndicator.vue +3 -3
- package/src/components/ui/controls/atomic/Dropdown.vue +23 -5
- package/src/views/Console.vue +35 -4
- package/src/views/Login.vue +7 -8
- package/src/views/Tasks.vue +4 -0
- package/vite.config.js +1 -1
package/backend/api.js
CHANGED
|
@@ -153,11 +153,17 @@ app.ws("/api/updates", async function (ws, req) {
|
|
|
153
153
|
setTimeout(() => send({ event: "init-axs-accounts", accounts: Bot.AXS.Accounts }));
|
|
154
154
|
|
|
155
155
|
ws.on("message", async (msg) => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
const msgStr = msg.toString();
|
|
157
|
+
if (msgStr === "ping") return ws.send("pong");
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(msgStr);
|
|
161
|
+
parsed.data.user = currentUser;
|
|
162
|
+
const res = await endpoints.handleWebsocketMessage(parsed);
|
|
163
|
+
if (res?.error) ws.send(JSON.stringify([{ event: "error", msg: res.error }]));
|
|
164
|
+
} catch (e) {
|
|
165
|
+
logger.Error("WebSocket message parse error:", e.message, "Message:", msgStr);
|
|
166
|
+
}
|
|
161
167
|
});
|
|
162
168
|
}
|
|
163
169
|
if (req.query.type === "console") {
|
|
@@ -194,14 +200,26 @@ app.post("/api/login", async (req, res) => {
|
|
|
194
200
|
const pwd = req.body.password;
|
|
195
201
|
const user = req.body.name;
|
|
196
202
|
const authResult = auth.loginToAccount(user, pwd);
|
|
197
|
-
if (authResult.token)
|
|
198
|
-
|
|
203
|
+
if (authResult.token) {
|
|
204
|
+
return res.cookie("auth", authResult.token, {
|
|
205
|
+
httpOnly: false,
|
|
206
|
+
sameSite: 'lax',
|
|
207
|
+
path: '/'
|
|
208
|
+
}).send(authResult);
|
|
209
|
+
} else {
|
|
210
|
+
return res.send(authResult);
|
|
211
|
+
}
|
|
199
212
|
});
|
|
200
213
|
|
|
201
214
|
app.post("/api/logout", async (req, res) => {
|
|
202
215
|
const token = req.cookies.auth;
|
|
203
216
|
const result = auth.invalidateAuthToken(token);
|
|
204
|
-
return res.cookie("auth", ""
|
|
217
|
+
return res.cookie("auth", "", {
|
|
218
|
+
httpOnly: false,
|
|
219
|
+
sameSite: 'lax',
|
|
220
|
+
path: '/',
|
|
221
|
+
maxAge: 0
|
|
222
|
+
}).send(result);
|
|
205
223
|
});
|
|
206
224
|
|
|
207
225
|
// ===== File editing =====
|
package/index.html
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
name="viewport"
|
|
8
8
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
9
9
|
/>
|
|
10
|
+
<!-- Lock iPhone to portrait orientation only -->
|
|
11
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
12
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
10
13
|
<meta name="description" content="Necro Lab - dashboard" />
|
|
14
|
+
<meta name="darkreader-lock" />
|
|
15
|
+
<meta name="color-scheme" content="dark" />
|
|
11
16
|
<title>Necro Lab - Dashboard</title>
|
|
12
17
|
<!-- DNS prefetch for external resources -->
|
|
13
18
|
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
package/package.json
CHANGED
package/public/manifest.json
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
{
|
|
6
6
|
"src": "/android-chrome-192x192.png?v=2",
|
|
7
7
|
"type": "image/png",
|
|
8
|
-
"sizes": "192x192"
|
|
8
|
+
"sizes": "192x192",
|
|
9
|
+
"purpose": "any"
|
|
9
10
|
},
|
|
10
11
|
{
|
|
11
|
-
"src": "/android-chrome-
|
|
12
|
+
"src": "/android-chrome-512x512.png?v=2",
|
|
12
13
|
"type": "image/png",
|
|
13
|
-
"sizes": "
|
|
14
|
-
"purpose": "
|
|
14
|
+
"sizes": "512x512",
|
|
15
|
+
"purpose": "maskable"
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
"src": "/android-chrome-512x512.png?v=2",
|
|
@@ -29,5 +30,6 @@
|
|
|
29
30
|
"display": "standalone",
|
|
30
31
|
"scope": "/",
|
|
31
32
|
"theme_color": "#2e2f32",
|
|
32
|
-
"description": "Necro Lab"
|
|
33
|
+
"description": "Necro Lab",
|
|
34
|
+
"orientation": "portrait"
|
|
33
35
|
}
|
|
Binary file
|
package/src/App.vue
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="layout">
|
|
3
|
+
<!-- iPhone Landscape Lock Overlay -->
|
|
4
|
+
<div v-if="!isIpadOS() && isIOS()" class="iphone-landscape-lock">
|
|
5
|
+
<div class="rotate-message">
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="rotate-icon">
|
|
7
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
8
|
+
</svg>
|
|
9
|
+
<p class="rotate-text">Please rotate your device to portrait mode</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
3
13
|
<transition name="fade">
|
|
4
14
|
<Splash v-if="isLoading" />
|
|
5
15
|
</transition>
|
|
@@ -88,6 +98,70 @@ function isIpadOS() {
|
|
|
88
98
|
return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
// Lock iPhone (not iPad) to portrait orientation only
|
|
102
|
+
function lockOrientationToPortrait() {
|
|
103
|
+
if (isIOS() && !isIpadOS()) {
|
|
104
|
+
// Try to lock orientation using Screen Orientation API
|
|
105
|
+
if (screen.orientation && screen.orientation.lock) {
|
|
106
|
+
screen.orientation.lock('portrait').catch(() => {
|
|
107
|
+
// Silently fail if not supported or in browser (vs PWA)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Also prevent landscape handling
|
|
112
|
+
const preventLandscape = () => {
|
|
113
|
+
if (window.innerWidth > window.innerHeight) {
|
|
114
|
+
// Force dimensions to portrait if somehow in landscape
|
|
115
|
+
document.documentElement.style.setProperty('width', window.innerHeight + 'px');
|
|
116
|
+
document.documentElement.style.setProperty('height', window.innerWidth + 'px');
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
window.addEventListener('orientationchange', preventLandscape);
|
|
121
|
+
window.addEventListener('resize', preventLandscape);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Run orientation lock on mount
|
|
126
|
+
lockOrientationToPortrait();
|
|
127
|
+
|
|
128
|
+
// Prevent pinch-to-zoom gestures
|
|
129
|
+
let lastTouchDistance = 0;
|
|
130
|
+
|
|
131
|
+
document.addEventListener('touchstart', (e) => {
|
|
132
|
+
if (e.touches.length > 1) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
}
|
|
135
|
+
}, { passive: false });
|
|
136
|
+
|
|
137
|
+
document.addEventListener('touchmove', (e) => {
|
|
138
|
+
if (e.touches.length > 1) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
}
|
|
141
|
+
}, { passive: false });
|
|
142
|
+
|
|
143
|
+
document.addEventListener('gesturestart', (e) => {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
}, { passive: false });
|
|
146
|
+
|
|
147
|
+
document.addEventListener('gesturechange', (e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
}, { passive: false });
|
|
150
|
+
|
|
151
|
+
document.addEventListener('gestureend', (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
}, { passive: false });
|
|
154
|
+
|
|
155
|
+
// Prevent double-tap zoom
|
|
156
|
+
let lastTouchEnd = 0;
|
|
157
|
+
document.addEventListener('touchend', (e) => {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
if (now - lastTouchEnd <= 300) {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
}
|
|
162
|
+
lastTouchEnd = now;
|
|
163
|
+
}, { passive: false });
|
|
164
|
+
|
|
91
165
|
// Handle orientation changes for iOS devices
|
|
92
166
|
const handleOrientationChange = () => {
|
|
93
167
|
if (isIOS()) {
|
|
@@ -653,7 +727,7 @@ watch(
|
|
|
653
727
|
const layout = computed(() => router.currentRoute.value.meta.layout);
|
|
654
728
|
</script>
|
|
655
729
|
<style lang="scss">
|
|
656
|
-
// Ultra bulletproof scroll prevention
|
|
730
|
+
// Ultra bulletproof scroll prevention and zoom prevention
|
|
657
731
|
html,
|
|
658
732
|
body {
|
|
659
733
|
overflow: hidden !important;
|
|
@@ -661,6 +735,10 @@ body {
|
|
|
661
735
|
width: 100% !important;
|
|
662
736
|
height: 100% !important;
|
|
663
737
|
touch-action: none !important;
|
|
738
|
+
// Prevent zoom on desktop
|
|
739
|
+
zoom: 1;
|
|
740
|
+
-moz-transform: scale(1);
|
|
741
|
+
-moz-transform-origin: 0 0;
|
|
664
742
|
}
|
|
665
743
|
|
|
666
744
|
#app {
|
|
@@ -671,6 +749,11 @@ body {
|
|
|
671
749
|
touch-action: none !important;
|
|
672
750
|
}
|
|
673
751
|
|
|
752
|
+
// Prevent any element from being zoomable
|
|
753
|
+
* {
|
|
754
|
+
touch-action: manipulation !important;
|
|
755
|
+
}
|
|
756
|
+
|
|
674
757
|
.dropdown {
|
|
675
758
|
position: relative;
|
|
676
759
|
display: inline-block;
|
|
@@ -691,6 +774,52 @@ body {
|
|
|
691
774
|
height: 100vh !important;
|
|
692
775
|
}
|
|
693
776
|
|
|
777
|
+
// iPhone landscape lock overlay
|
|
778
|
+
.iphone-landscape-lock {
|
|
779
|
+
display: none;
|
|
780
|
+
position: fixed;
|
|
781
|
+
inset: 0;
|
|
782
|
+
background: oklch(0.1822 0 0);
|
|
783
|
+
z-index: 99999;
|
|
784
|
+
align-items: center;
|
|
785
|
+
justify-content: center;
|
|
786
|
+
|
|
787
|
+
// Only show on iPhone (not iPad) in landscape
|
|
788
|
+
@media (max-device-width: 430px) and (orientation: landscape) {
|
|
789
|
+
display: flex;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.rotate-message {
|
|
793
|
+
text-align: center;
|
|
794
|
+
padding: 2rem;
|
|
795
|
+
|
|
796
|
+
.rotate-icon {
|
|
797
|
+
width: 64px;
|
|
798
|
+
height: 64px;
|
|
799
|
+
margin: 0 auto 1rem;
|
|
800
|
+
color: oklch(0.72 0.15 145);
|
|
801
|
+
animation: rotate-pulse 2s ease-in-out infinite;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.rotate-text {
|
|
805
|
+
color: oklch(0.9 0 0);
|
|
806
|
+
font-size: 1.125rem;
|
|
807
|
+
font-weight: 500;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
@keyframes rotate-pulse {
|
|
813
|
+
0%, 100% {
|
|
814
|
+
transform: rotate(0deg) scale(1);
|
|
815
|
+
opacity: 1;
|
|
816
|
+
}
|
|
817
|
+
50% {
|
|
818
|
+
transform: rotate(90deg) scale(1.1);
|
|
819
|
+
opacity: 0.8;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
694
823
|
.router-wrapper {
|
|
695
824
|
@apply pt-5;
|
|
696
825
|
transition: margin 0.25s;
|
|
@@ -4,22 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
.Toastify__toast-theme--colored.Toastify__toast--default,
|
|
6
6
|
.Toastify__toast-theme--light {
|
|
7
|
-
|
|
7
|
+
background: oklch(0.2046 0 0);
|
|
8
|
+
border: 2px solid oklch(0.2809 0 0);
|
|
9
|
+
color: oklch(0.90 0 0);
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
.Toastify__toast-icon {
|
|
11
|
-
background:
|
|
12
|
-
|
|
13
|
+
background: transparent;
|
|
14
|
+
width: 20px;
|
|
15
|
+
height: 20px;
|
|
16
|
+
|
|
17
|
+
svg {
|
|
18
|
+
width: 20px;
|
|
19
|
+
height: 20px;
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
.Toastify__close-button.Toastify__close-button--light {
|
|
16
24
|
margin-top: 1px;
|
|
17
25
|
padding-left: 0.1rem;
|
|
18
|
-
@apply
|
|
26
|
+
@apply flex border-solid w-6 h-6 items-center justify-center rounded-full;
|
|
27
|
+
border: 2px solid oklch(0.90 0 0);
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
.Toastify__close-button.Toastify__close-button--light svg {
|
|
22
|
-
color:
|
|
31
|
+
color: oklch(0.90 0 0);
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
.Toastify__close-button.Toastify__close-button--light svg path {
|
|
@@ -91,7 +100,7 @@
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
.Toastify__toast--success .Toastify__progress-bar {
|
|
94
|
-
|
|
103
|
+
background: oklch(0.72 0.15 145);
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
.Toastify__progress-bar {
|
package/src/assets/css/main.scss
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
NECRO DASHBOARD
|
|
2
|
+
NECRO DASHBOARD
|
|
3
3
|
Modular SCSS architecture with @use imports
|
|
4
4
|
========================================================================== */
|
|
5
5
|
|
|
@@ -47,14 +47,14 @@ img {
|
|
|
47
47
|
|
|
48
48
|
/* Global icon color consistency */
|
|
49
49
|
svg {
|
|
50
|
-
color: oklch(0.
|
|
50
|
+
color: oklch(0.9 0 0) !important;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/* For stroke icons (Eye, etc) - stroke on svg element */
|
|
54
54
|
svg[stroke] path,
|
|
55
55
|
svg[stroke] circle,
|
|
56
56
|
svg[stroke] line {
|
|
57
|
-
stroke: oklch(0.
|
|
57
|
+
stroke: oklch(0.9 0 0) !important;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/* For filled icons with explicit fill */
|
|
@@ -62,7 +62,7 @@ svg path[fill]:not([fill="none"]),
|
|
|
62
62
|
svg circle[fill]:not([fill="none"]),
|
|
63
63
|
svg rect[fill]:not([fill="none"]),
|
|
64
64
|
svg polygon[fill]:not([fill="none"]) {
|
|
65
|
-
fill: oklch(0.
|
|
65
|
+
fill: oklch(0.9 0 0) !important;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/* For filled icons without explicit fill attribute */
|
|
@@ -70,7 +70,7 @@ svg:not([stroke]):not([fill="none"]) path:not([fill]),
|
|
|
70
70
|
svg:not([stroke]):not([fill="none"]) circle:not([fill]),
|
|
71
71
|
svg:not([stroke]):not([fill="none"]) rect:not([fill]),
|
|
72
72
|
svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
73
|
-
fill: oklch(0.
|
|
73
|
+
fill: oklch(0.9 0 0) !important;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/* ==========================================================================
|
|
@@ -82,31 +82,32 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
.status-indicator {
|
|
85
|
-
@apply
|
|
85
|
+
@apply h-2 w-2 flex-shrink-0 rounded-full;
|
|
86
86
|
min-width: 4px;
|
|
87
87
|
min-height: 4px;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
.mobile-icons {
|
|
91
|
-
@apply
|
|
91
|
+
@apply ml-auto flex items-center gap-x-2 lg:hidden;
|
|
92
92
|
|
|
93
93
|
button {
|
|
94
|
-
@apply
|
|
94
|
+
@apply flex h-8 w-8 items-center justify-center rounded transition-all duration-150;
|
|
95
95
|
background-color: oklch(0.2046 0 0);
|
|
96
96
|
border: 2px solid oklch(0.2809 0 0);
|
|
97
97
|
color: oklch(0.82 0 0);
|
|
98
98
|
|
|
99
|
-
&:hover,
|
|
99
|
+
&:hover,
|
|
100
|
+
&:active {
|
|
100
101
|
border-color: oklch(0.72 0.15 145);
|
|
101
102
|
outline: 1px solid oklch(0.72 0.15 145);
|
|
102
103
|
outline-offset: 0;
|
|
103
|
-
color: oklch(0.
|
|
104
|
+
color: oklch(0.9 0 0);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
.loading-spinner {
|
|
109
|
-
@apply
|
|
110
|
+
@apply h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
/* ==========================================================================
|
|
@@ -114,7 +115,7 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
114
115
|
========================================================================== */
|
|
115
116
|
|
|
116
117
|
.label-override {
|
|
117
|
-
@apply flex items-center text-xs
|
|
118
|
+
@apply mb-2 flex items-center text-xs;
|
|
118
119
|
color: oklch(0.65 0 0);
|
|
119
120
|
|
|
120
121
|
svg {
|
|
@@ -128,10 +129,10 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
128
129
|
|
|
129
130
|
.task-switches {
|
|
130
131
|
h4 {
|
|
131
|
-
color: oklch(0.
|
|
132
|
+
color: oklch(0.9 0 0);
|
|
132
133
|
font-size: 0.8125rem;
|
|
133
134
|
font-weight: 500;
|
|
134
|
-
@apply
|
|
135
|
+
@apply mx-auto mb-2 flex items-center gap-x-2 text-center;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
.switch-wrapper {
|
|
@@ -141,7 +142,7 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
141
142
|
svg {
|
|
142
143
|
width: 15px !important;
|
|
143
144
|
height: 15px !important;
|
|
144
|
-
color: oklch(0.
|
|
145
|
+
color: oklch(0.9 0 0) !important;
|
|
145
146
|
margin-left: 0.25rem !important;
|
|
146
147
|
}
|
|
147
148
|
}
|
|
@@ -223,11 +224,11 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
223
224
|
@screen md {
|
|
224
225
|
.btn-primary,
|
|
225
226
|
.btn-secondary {
|
|
226
|
-
@apply
|
|
227
|
+
@apply px-3 py-1.5 text-sm;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
.btn-action {
|
|
230
|
-
@apply
|
|
231
|
+
@apply h-12 px-6 text-sm;
|
|
231
232
|
min-height: 48px;
|
|
232
233
|
}
|
|
233
234
|
|
|
@@ -241,7 +242,7 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
241
242
|
|
|
242
243
|
@screen mobile-portrait {
|
|
243
244
|
.btn-action {
|
|
244
|
-
@apply
|
|
245
|
+
@apply h-14 px-8 text-base;
|
|
245
246
|
min-height: 56px;
|
|
246
247
|
font-weight: 600;
|
|
247
248
|
border-radius: 8px;
|
|
@@ -257,7 +258,7 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
257
258
|
|
|
258
259
|
@screen mobile-landscape {
|
|
259
260
|
.btn-action {
|
|
260
|
-
@apply
|
|
261
|
+
@apply h-14 px-8 text-base;
|
|
261
262
|
min-height: 56px;
|
|
262
263
|
font-weight: 600;
|
|
263
264
|
}
|
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="form-section">
|
|
3
3
|
<!-- Username -->
|
|
4
|
-
<div class="input-
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
</
|
|
9
|
-
<div class="
|
|
10
|
-
<input
|
|
4
|
+
<div class="input-container mb-3">
|
|
5
|
+
<div class="flex items-center gap-2">
|
|
6
|
+
<ProfileIcon class="w-4 h-4" />
|
|
7
|
+
<span class="text-light-300 font-medium text-sm">Username</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex-1 flex items-center">
|
|
10
|
+
<input
|
|
11
|
+
type="text"
|
|
12
|
+
class="login-input"
|
|
13
|
+
v-model="user"
|
|
14
|
+
/>
|
|
11
15
|
</div>
|
|
12
16
|
</div>
|
|
13
17
|
<!-- Password -->
|
|
14
|
-
<div class="input-
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
</
|
|
19
|
-
<div class="
|
|
20
|
-
<input
|
|
18
|
+
<div class="input-container mb-4">
|
|
19
|
+
<div class="flex items-center gap-2">
|
|
20
|
+
<KeyIcon class="w-4 h-4" />
|
|
21
|
+
<span class="text-light-300 font-medium text-sm">Password</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex-1 flex items-center">
|
|
24
|
+
<input
|
|
25
|
+
type="password"
|
|
26
|
+
class="login-input"
|
|
27
|
+
v-model="password"
|
|
28
|
+
/>
|
|
21
29
|
</div>
|
|
22
30
|
</div>
|
|
23
31
|
<button
|
|
24
|
-
class="
|
|
32
|
+
class="login-btn mt-6 mx-auto"
|
|
25
33
|
@click="login()"
|
|
26
34
|
:disabled="buttonDisabled"
|
|
27
35
|
>
|
|
28
36
|
<span v-if="!buttonDisabled">Login</span>
|
|
37
|
+
<svg v-if="!buttonDisabled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
|
|
38
|
+
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M13.8 12H3"/>
|
|
39
|
+
</svg>
|
|
29
40
|
<div v-else class="loading-spinner"></div>
|
|
30
41
|
</button>
|
|
31
42
|
</div>
|
|
@@ -56,43 +67,65 @@ async function login() {
|
|
|
56
67
|
</script>
|
|
57
68
|
|
|
58
69
|
<style lang="scss" scoped>
|
|
59
|
-
.
|
|
60
|
-
@apply flex items-center
|
|
61
|
-
|
|
70
|
+
.login-btn {
|
|
71
|
+
@apply flex items-center justify-center gap-2 rounded-lg transition-all duration-150;
|
|
72
|
+
background: oklch(0.72 0.15 145);
|
|
73
|
+
border: 2px solid oklch(0.72 0.15 145);
|
|
74
|
+
color: oklch(1 0 0);
|
|
75
|
+
height: 3rem;
|
|
76
|
+
width: 12rem;
|
|
77
|
+
font-size: 0.9375rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
letter-spacing: 0.05em;
|
|
80
|
+
text-transform: uppercase;
|
|
81
|
+
|
|
82
|
+
&:hover:not(:disabled) {
|
|
83
|
+
background: oklch(0.68 0.15 145);
|
|
84
|
+
border-color: oklch(0.68 0.15 145);
|
|
85
|
+
transform: translateY(-1px);
|
|
86
|
+
box-shadow: 0 4px 12px oklch(0.72 0.15 145 / 0.3);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&:active:not(:disabled) {
|
|
90
|
+
transform: translateY(0);
|
|
91
|
+
box-shadow: 0 2px 4px oklch(0.72 0.15 145 / 0.2);
|
|
92
|
+
}
|
|
62
93
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
path {
|
|
67
|
-
fill: #e1e1e4 !important;
|
|
68
|
-
}
|
|
94
|
+
&:disabled {
|
|
95
|
+
opacity: 0.7;
|
|
96
|
+
cursor: not-allowed;
|
|
69
97
|
}
|
|
70
98
|
}
|
|
71
99
|
|
|
72
|
-
.input-
|
|
73
|
-
@apply bg-dark-
|
|
74
|
-
|
|
100
|
+
.input-container {
|
|
101
|
+
@apply text-white bg-dark-500 px-3 rounded-lg border-2 border-dark-550 flex items-center justify-between h-11;
|
|
102
|
+
overflow: visible;
|
|
103
|
+
transition: border-color 0.15s ease;
|
|
75
104
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
&:hover {
|
|
106
|
+
border-color: oklch(0.30 0 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&:focus-within {
|
|
110
|
+
border-color: oklch(0.72 0.15 145) !important;
|
|
111
|
+
outline: 1px solid oklch(0.72 0.15 145);
|
|
112
|
+
outline-offset: 0;
|
|
81
113
|
}
|
|
82
114
|
}
|
|
83
115
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
116
|
+
.login-input {
|
|
117
|
+
@apply w-full h-full text-sm text-white bg-transparent border-0 outline-none px-2 py-1;
|
|
118
|
+
|
|
119
|
+
&:focus {
|
|
120
|
+
@apply outline-none border-0 shadow-none bg-transparent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:hover:not(:focus) {
|
|
124
|
+
background: transparent;
|
|
92
125
|
}
|
|
93
126
|
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
&::placeholder {
|
|
128
|
+
color: oklch(0.50 0 0);
|
|
96
129
|
}
|
|
97
130
|
}
|
|
98
131
|
</style>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<Row
|
|
3
3
|
class="relative h-16 grid-cols-5 text-white md:grid-cols-7"
|
|
4
4
|
@click="ui.setOpenContextMenu('')"
|
|
5
|
-
@click.right.prevent="ui.setOpenContextMenu('')"
|
|
5
|
+
@click.right.prevent="ui.setOpenContextMenu('')"
|
|
6
|
+
@dblclick="handleDoubleClick"
|
|
7
|
+
@touchstart="handleTouchStart"
|
|
8
|
+
@touchend="handleTouchEnd">
|
|
6
9
|
<div class="col-span-3 flex lg:col-span-2">
|
|
7
10
|
<Checkbox
|
|
8
11
|
class="ml-0 mr-4"
|
|
@@ -189,4 +192,40 @@ const edit = () => {
|
|
|
189
192
|
ui.currentlyEditing = props.account;
|
|
190
193
|
ui.toggleModal("create-account");
|
|
191
194
|
};
|
|
195
|
+
|
|
196
|
+
// Double-click/tap selection
|
|
197
|
+
let lastTapTime = 0;
|
|
198
|
+
const DOUBLE_TAP_DELAY = 300; // ms
|
|
199
|
+
|
|
200
|
+
const handleDoubleClick = (event) => {
|
|
201
|
+
// Prevent if clicking on buttons or checkbox
|
|
202
|
+
if (event.target.closest('button') || event.target.closest('.checkbox')) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
ui.toggleAccountSelected(props.account.id);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleTouchStart = (event) => {
|
|
209
|
+
// Store touch time for double-tap detection
|
|
210
|
+
const currentTime = Date.now();
|
|
211
|
+
const tapGap = currentTime - lastTapTime;
|
|
212
|
+
|
|
213
|
+
if (tapGap < DOUBLE_TAP_DELAY && tapGap > 0) {
|
|
214
|
+
// Double-tap detected
|
|
215
|
+
if (!event.target.closest('button') && !event.target.closest('.checkbox')) {
|
|
216
|
+
event.preventDefault(); // Prevent zoom
|
|
217
|
+
ui.toggleAccountSelected(props.account.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
lastTapTime = currentTime;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleTouchEnd = (event) => {
|
|
225
|
+
// Prevent default to avoid potential zoom issues
|
|
226
|
+
// but only if not interacting with buttons/checkbox
|
|
227
|
+
if (event.target.closest('button') || event.target.closest('.checkbox')) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
192
231
|
</script>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<Row
|
|
3
3
|
class="relative grid-cols-7 text-white lg:grid-cols-8"
|
|
4
4
|
@click="ui.setOpenContextMenu('')"
|
|
5
|
-
@click.right.prevent="ui.setOpenContextMenu('')"
|
|
5
|
+
@click.right.prevent="ui.setOpenContextMenu('')"
|
|
6
|
+
@dblclick="handleDoubleClick"
|
|
7
|
+
@touchstart="handleTouchStart"
|
|
8
|
+
@touchend="handleTouchEnd">
|
|
6
9
|
<div class="col-span-3 flex lg:col-span-2">
|
|
7
10
|
<Checkbox
|
|
8
11
|
class="ml-0 mr-4"
|
|
@@ -216,4 +219,40 @@ const edit = () => {
|
|
|
216
219
|
ui.toggleModal("create-profile");
|
|
217
220
|
};
|
|
218
221
|
const deleteProfile = async () => await ui.deleteProfile(props.profile.id);
|
|
222
|
+
|
|
223
|
+
// Double-click/tap selection
|
|
224
|
+
let lastTapTime = 0;
|
|
225
|
+
const DOUBLE_TAP_DELAY = 300; // ms
|
|
226
|
+
|
|
227
|
+
const handleDoubleClick = (event) => {
|
|
228
|
+
// Prevent if clicking on buttons or checkbox
|
|
229
|
+
if (event.target.closest('button') || event.target.closest('.checkbox')) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
ui.toggleProfileSelected(props.profile.id);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleTouchStart = (event) => {
|
|
236
|
+
// Store touch time for double-tap detection
|
|
237
|
+
const currentTime = Date.now();
|
|
238
|
+
const tapGap = currentTime - lastTapTime;
|
|
239
|
+
|
|
240
|
+
if (tapGap < DOUBLE_TAP_DELAY && tapGap > 0) {
|
|
241
|
+
// Double-tap detected
|
|
242
|
+
if (!event.target.closest('button') && !event.target.closest('.checkbox')) {
|
|
243
|
+
event.preventDefault(); // Prevent zoom
|
|
244
|
+
ui.toggleProfileSelected(props.profile.id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lastTapTime = currentTime;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handleTouchEnd = (event) => {
|
|
252
|
+
// Prevent default to avoid potential zoom issues
|
|
253
|
+
// but only if not interacting with buttons/checkbox
|
|
254
|
+
if (event.target.closest('button') || event.target.closest('.checkbox')) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
219
258
|
</script>
|
|
@@ -7,15 +7,22 @@
|
|
|
7
7
|
.table-component {
|
|
8
8
|
@apply flex-col bg-clip-padding rounded relative box-border border border-dark-600 bg-dark-500;
|
|
9
9
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
10
|
-
overflow: auto !important;
|
|
10
|
+
overflow-x: auto !important;
|
|
11
|
+
overflow-y: auto !important;
|
|
11
12
|
overscroll-behavior: auto !important;
|
|
12
|
-
|
|
13
|
+
// Prevent pinch-to-zoom while allowing scrolling
|
|
14
|
+
touch-action: pan-x pan-y !important;
|
|
13
15
|
max-height: calc(100vh - 200px);
|
|
16
|
+
-webkit-overflow-scrolling: touch;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
.table-component > .grid {
|
|
17
20
|
@apply bg-dark-400;
|
|
18
21
|
border-bottom: 1px solid oklch(0.26 0 0);
|
|
22
|
+
// Only enforce min-width on desktop, allow mobile to fit screen
|
|
23
|
+
@media (min-width: 768px) {
|
|
24
|
+
min-width: 640px;
|
|
25
|
+
}
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
/* iPhone landscape mode table optimization */
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Row
|
|
2
|
+
<Row
|
|
3
|
+
class="relative grid-cols-10 gap-2 text-white lg:grid-cols-12"
|
|
4
|
+
@click="ui.setOpenContextMenu('')"
|
|
5
|
+
@dblclick="handleDoubleClick"
|
|
6
|
+
@touchstart="handleTouchStart"
|
|
7
|
+
@touchend="handleTouchEnd">
|
|
3
8
|
<div class="col-span-1 flex items-center justify-start lg:col-span-2">
|
|
4
9
|
<Checkbox
|
|
5
10
|
class="ml-2 mr-4 flex-shrink-0"
|
|
@@ -389,6 +394,42 @@ const props = defineProps({
|
|
|
389
394
|
const contextMenuPosition = ref({});
|
|
390
395
|
const contextMenuRef = ref(null);
|
|
391
396
|
|
|
397
|
+
// Double-click/tap selection
|
|
398
|
+
let lastTapTime = 0;
|
|
399
|
+
const DOUBLE_TAP_DELAY = 300; // ms
|
|
400
|
+
|
|
401
|
+
const handleDoubleClick = (event) => {
|
|
402
|
+
// Prevent if clicking on buttons or checkbox
|
|
403
|
+
if (event.target.closest('button') || event.target.closest('.checkbox')) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
ui.toggleTaskSelected(props.task.taskId);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const handleTouchStart = (event) => {
|
|
410
|
+
// Store touch time for double-tap detection
|
|
411
|
+
const currentTime = Date.now();
|
|
412
|
+
const tapGap = currentTime - lastTapTime;
|
|
413
|
+
|
|
414
|
+
if (tapGap < DOUBLE_TAP_DELAY && tapGap > 0) {
|
|
415
|
+
// Double-tap detected
|
|
416
|
+
if (!event.target.closest('button') && !event.target.closest('.checkbox')) {
|
|
417
|
+
event.preventDefault(); // Prevent zoom
|
|
418
|
+
ui.toggleTaskSelected(props.task.taskId);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
lastTapTime = currentTime;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const handleTouchEnd = (event) => {
|
|
426
|
+
// Prevent default to avoid potential zoom issues
|
|
427
|
+
// but only if not interacting with buttons/checkbox
|
|
428
|
+
if (event.target.closest('button') || event.target.closest('.checkbox')) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
392
433
|
// Handle right-click to position context menu
|
|
393
434
|
const handleRightClick = (event) => {
|
|
394
435
|
const menuWidth = 168; // w-42 = 10.5rem = 168px
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
<UpIcon v-if="ui.sortData.sortBy === 'eventId' && ui.sortData.reversed" class="ml-1" />
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
|
-
<div class="flex items-center justify-
|
|
17
|
+
<div class="col-span-2 flex items-center justify-start" v-once>
|
|
18
18
|
<TicketIcon class="mr-0 lg:mr-3" />
|
|
19
19
|
<h4 class="hidden lg:flex">Tickets</h4>
|
|
20
20
|
</div>
|
|
21
|
-
<div class="
|
|
21
|
+
<div class="col-span-5 md:col-span-4 lg:col-span-5 flex items-center justify-center" @click="ui.toggleSort('status')">
|
|
22
22
|
<StatusIcon class="mr-0 lg:mr-3" />
|
|
23
23
|
<h4 class="hidden lg:flex">Status</h4>
|
|
24
24
|
<DownIcon v-if="ui.sortData.sortBy === 'status' && !ui.sortData.reversed" class="ml-1" />
|
|
25
25
|
<UpIcon v-if="ui.sortData.sortBy === 'status' && ui.sortData.reversed" class="ml-1" />
|
|
26
26
|
</div>
|
|
27
|
-
<div class="
|
|
27
|
+
<div class="col-span-2 lg:col-span-3 flex items-center justify-center" v-once>
|
|
28
28
|
<ClickIcon class="mr-0 lg:mr-3" />
|
|
29
29
|
<h4 class="hidden lg:flex">Actions</h4>
|
|
30
30
|
</div>
|
|
@@ -58,9 +58,6 @@
|
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
</div>
|
|
61
|
-
|
|
62
|
-
<!-- View Task Modal -->
|
|
63
|
-
<ViewTask v-if="ui.activeModal === 'view-task' && ui.selectedTaskForView" :task="ui.selectedTaskForView" />
|
|
64
61
|
</div>
|
|
65
62
|
</template>
|
|
66
63
|
<style lang="scss" scoped>
|
|
@@ -102,7 +99,6 @@ import { computed, ref, onMounted, onUnmounted } from "vue";
|
|
|
102
99
|
import { Table, Header } from "@/components/Table";
|
|
103
100
|
import { EventIcon, TicketIcon, StatusIcon, ClickIcon, DownIcon, UpIcon, TasksIcon } from "@/components/icons";
|
|
104
101
|
import Task from "./Task.vue";
|
|
105
|
-
import ViewTask from "./ViewTask.vue";
|
|
106
102
|
import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
|
|
107
103
|
import { useUIStore } from "@/stores/ui";
|
|
108
104
|
|
|
@@ -192,7 +188,7 @@ const dynamicTableHeight = computed(() => {
|
|
|
192
188
|
const filtersAndStatsHeight = windowWidth.value >= 768
|
|
193
189
|
? (ui.queueStats.show ? 50 : 45) // Desktop: single row
|
|
194
190
|
: (ui.queueStats.show ? 130 : 90); // Mobile: stats row + stacked filters
|
|
195
|
-
const utilitiesHeight = windowWidth.value <= 768 ?
|
|
191
|
+
const utilitiesHeight = windowWidth.value <= 768 ? 180 : 160; // Increase mobile utilities space to prevent cutoff
|
|
196
192
|
const margins =
|
|
197
193
|
windowWidth.value >= 1024 ? 20 : windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 8 : 12;
|
|
198
194
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="grid grid-cols-1 gap-3 lg:grid-cols-1" v-if="ui.currentModule == 'TM'">
|
|
2
|
+
<div class="utilities-wrapper grid grid-cols-1 gap-3 lg:grid-cols-1" v-if="ui.currentModule == 'TM'">
|
|
3
3
|
<div class="lg:justify-self-end">
|
|
4
4
|
<h4 class="hidden lg:block text-white opacity-40 uppercase font-medium mb-1">Utils</h4>
|
|
5
5
|
<div class="flex gap-3 justify-between lg:justify-start">
|
|
@@ -23,6 +23,14 @@ import { useUIStore } from "@/stores/ui";
|
|
|
23
23
|
const ui = useUIStore();
|
|
24
24
|
</script>
|
|
25
25
|
<style lang="scss" scoped>
|
|
26
|
+
.utilities-wrapper {
|
|
27
|
+
// Add extra margin on mobile to prevent buttons from being cut off
|
|
28
|
+
@media (max-width: 768px) {
|
|
29
|
+
margin-top: 1rem;
|
|
30
|
+
margin-bottom: 1.5rem;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
.utility-btn {
|
|
27
35
|
height: 50px;
|
|
28
36
|
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
<div class="section-card mb-4 mt-4">
|
|
9
9
|
<h3 class="section-title">Task Overview</h3>
|
|
10
10
|
<div class="grid grid-cols-1 gap-3">
|
|
11
|
-
<div class="info-row
|
|
11
|
+
<div class="info-row" @click="copy(task.taskId)">
|
|
12
12
|
<span class="info-icon flex items-center justify-center text-base font-bold">#</span>
|
|
13
13
|
<span class="info-label">Task ID</span>
|
|
14
|
-
<span class="info-value">{{ task.taskId }}</span>
|
|
14
|
+
<span class="info-value copyable">{{ task.taskId }}</span>
|
|
15
15
|
</div>
|
|
16
16
|
<div class="info-row" @click="copy(task.eventId)">
|
|
17
17
|
<StadiumIcon class="info-icon" />
|
|
@@ -66,6 +66,8 @@ onClickOutside(target, (event) => {
|
|
|
66
66
|
backdrop-filter: blur(4px);
|
|
67
67
|
overflow-y: auto;
|
|
68
68
|
-webkit-overflow-scrolling: touch;
|
|
69
|
+
// Prevent zoom in modals
|
|
70
|
+
touch-action: pan-y !important;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
.component-modal {
|
|
@@ -73,6 +75,8 @@ onClickOutside(target, (event) => {
|
|
|
73
75
|
margin-bottom: 10rem;
|
|
74
76
|
overflow-y: visible;
|
|
75
77
|
@apply flex flex-col rounded-lg bg-dark-300 px-5 py-5;
|
|
78
|
+
// Prevent zoom in modal content
|
|
79
|
+
touch-action: pan-y !important;
|
|
76
80
|
|
|
77
81
|
.modal-header {
|
|
78
82
|
@apply flex font-bold text-white;
|
|
@@ -326,10 +326,10 @@ const toggleMenu = () => {
|
|
|
326
326
|
const logout = async () => {
|
|
327
327
|
ui.logger.Info("Logging out of session");
|
|
328
328
|
const json = await sendLogout();
|
|
329
|
-
if (json.error)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
329
|
+
if (json.error) ui.showError(json.error);
|
|
330
|
+
|
|
331
|
+
// Always clear profile and redirect, even if logout request failed
|
|
332
|
+
ui.setProfile({});
|
|
333
|
+
router.push("/login");
|
|
334
334
|
};
|
|
335
335
|
</script>
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
<script setup>
|
|
39
39
|
import { ref, onMounted, onUnmounted } from "vue";
|
|
40
|
-
import logoIcon from "/
|
|
40
|
+
import logoIcon from "/reconnect-logo.png";
|
|
41
41
|
|
|
42
42
|
const dotIndex = ref(0);
|
|
43
43
|
const progressWidth = ref(0);
|
|
@@ -119,7 +119,7 @@ const props = defineProps({
|
|
|
119
119
|
left: 0;
|
|
120
120
|
right: 0;
|
|
121
121
|
bottom: 0;
|
|
122
|
-
background:
|
|
122
|
+
background: oklch(0.1822 0 0 / 0.98);
|
|
123
123
|
backdrop-filter: blur(12px);
|
|
124
124
|
-webkit-backdrop-filter: blur(12px);
|
|
125
125
|
}
|
|
@@ -269,7 +269,7 @@ const props = defineProps({
|
|
|
269
269
|
left: -100%;
|
|
270
270
|
width: 100%;
|
|
271
271
|
height: 100%;
|
|
272
|
-
background: linear-gradient(90deg, transparent,
|
|
272
|
+
background: linear-gradient(90deg, transparent, oklch(1 0 0 / 0.2), transparent);
|
|
273
273
|
animation: shimmer 2s ease-in-out infinite;
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
class="dropdown-item"
|
|
21
21
|
:class="i !== 0 ? 'border-t border-dark-650' : ''"
|
|
22
22
|
v-for="(f, i) in !allowDefault ? props.options : ['', ...props.options]"
|
|
23
|
-
@
|
|
23
|
+
@mousedown.prevent.stop="chose(f)"
|
|
24
|
+
@touchstart.prevent.stop="chose(f)">
|
|
24
25
|
<span class="dropdown-item-text" :class="capitalize ? 'capitalize' : ''">
|
|
25
26
|
{{ f ? f : props.default }}
|
|
26
27
|
</span>
|
|
@@ -33,11 +34,10 @@
|
|
|
33
34
|
</template>
|
|
34
35
|
|
|
35
36
|
<script setup>
|
|
36
|
-
import { ref, computed, watch } from "vue";
|
|
37
|
+
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
|
37
38
|
import { DownIcon, CheckmarkIcon } from "@/components/icons";
|
|
38
39
|
import { useUIStore } from "@/stores/ui";
|
|
39
40
|
import { useDropdownPosition } from "@/composables/useDropdownPosition";
|
|
40
|
-
import { useClickOutside } from "@/composables/useClickOutside";
|
|
41
41
|
|
|
42
42
|
const ui = useUIStore();
|
|
43
43
|
|
|
@@ -78,10 +78,27 @@ const { menuStyle, updatePosition } = useDropdownPosition(dropdownRef, {
|
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// Improved click outside that accounts for teleported menu
|
|
82
|
+
const handleClickOutside = (event) => {
|
|
83
|
+
if (!opened.value) return;
|
|
84
|
+
|
|
85
|
+
// Check if click is outside both the dropdown trigger and the menu
|
|
86
|
+
const clickedDropdown = dropdownRef.value?.contains(event.target);
|
|
87
|
+
const clickedMenu = event.target.closest('.dropdown-menu-portal');
|
|
88
|
+
|
|
89
|
+
if (!clickedDropdown && !clickedMenu) {
|
|
83
90
|
ui.setCurrentDropdown("");
|
|
84
91
|
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
onMounted(() => {
|
|
95
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
96
|
+
document.addEventListener('touchstart', handleClickOutside);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
onUnmounted(() => {
|
|
100
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
101
|
+
document.removeEventListener('touchstart', handleClickOutside);
|
|
85
102
|
});
|
|
86
103
|
|
|
87
104
|
const toggleOpened = () => {
|
|
@@ -177,6 +194,7 @@ const chose = (f) => {
|
|
|
177
194
|
-webkit-overflow-scrolling: touch !important;
|
|
178
195
|
scrollbar-width: none;
|
|
179
196
|
-ms-overflow-style: none;
|
|
197
|
+
z-index: 1000;
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
.dropdown-menu-portal::-webkit-scrollbar {
|
package/src/views/Console.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="console-page">
|
|
3
3
|
<h4 class="mb-2 flex items-center gap-2 pt-5 text-sm font-bold text-white lg:mt-1">
|
|
4
4
|
Console
|
|
5
5
|
<ConsoleIcon />
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
|
|
95
95
|
<Smoothie
|
|
96
96
|
:weight="0.2"
|
|
97
|
-
class="console scrollable smooth-scroll overflow-y-auto overflow-x-
|
|
97
|
+
class="console scrollable smooth-scroll overflow-y-auto overflow-x-auto font-mono text-white"
|
|
98
98
|
style="min-height: 12rem !important"
|
|
99
99
|
ref="$autoscroll"
|
|
100
100
|
@wheel.stop
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
v-bind:key="`log-${index}`"
|
|
119
119
|
:style="{ '--index': index }"><code class="md:text-sm lg:text-base" v-html="line"></code></pre>
|
|
120
120
|
</Smoothie>
|
|
121
|
-
<div class="mt-
|
|
121
|
+
<div class="console-switches mt-4 mb-6 flex justify-between md:hidden">
|
|
122
122
|
<button
|
|
123
123
|
class="flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
|
|
124
124
|
<h3 class="text-sm text-white">Hide Monitors</h3>
|
|
@@ -138,13 +138,44 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</template>
|
|
140
140
|
<style lang="scss" scoped>
|
|
141
|
+
.console-page {
|
|
142
|
+
// Add generous bottom padding on mobile to prevent switch cutoff
|
|
143
|
+
@media (max-width: 768px) {
|
|
144
|
+
padding-bottom: 4rem;
|
|
145
|
+
margin-bottom: 2rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (max-width: 480px) and (orientation: portrait) {
|
|
149
|
+
padding-bottom: 6rem;
|
|
150
|
+
margin-bottom: 3rem;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.console-switches {
|
|
155
|
+
@media (max-width: 480px) and (orientation: portrait) {
|
|
156
|
+
margin-top: 1.5rem !important;
|
|
157
|
+
margin-bottom: 4rem !important;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
141
161
|
.console {
|
|
142
162
|
@apply relative rounded border-2 border-dark-550 bg-dark-400 p-2 lg:p-5;
|
|
143
163
|
height: calc(100vh - 18rem);
|
|
144
164
|
scrollbar-width: thin;
|
|
145
165
|
scrollbar-color: oklch(0.35 0 0) oklch(0.19 0 0);
|
|
146
166
|
|
|
147
|
-
|
|
167
|
+
// Use fixed height on mobile portrait to ensure switches are visible
|
|
168
|
+
@media (max-width: 768px) {
|
|
169
|
+
max-height: 60vh;
|
|
170
|
+
height: auto;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@media (max-width: 480px) and (orientation: portrait) {
|
|
174
|
+
max-height: 50vh;
|
|
175
|
+
height: auto;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@media (min-width: 769px) and (max-width: 1023px) {
|
|
148
179
|
height: calc(100vh - 16rem);
|
|
149
180
|
}
|
|
150
181
|
|
package/src/views/Login.vue
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="login-container" v-once>
|
|
3
3
|
<div class="login-card">
|
|
4
|
-
<div class="flex justify-center
|
|
5
|
-
<img src="@/assets/img/logo_trans.png" class="h-16 object-cover" alt="Logo:
|
|
4
|
+
<div class="mb-10 flex justify-center">
|
|
5
|
+
<img src="@/assets/img/logo_trans.png" class="mb-3 h-16 object-cover" alt="Logo: NecroLab" />
|
|
6
6
|
</div>
|
|
7
|
-
<h2 class="text-l text-white text-center font-bold mb-6">Please login to proceed</h2>
|
|
8
7
|
|
|
9
8
|
<LoginForm />
|
|
10
9
|
</div>
|
|
@@ -15,7 +14,7 @@ import LoginForm from "@/components/Auth/LoginForm.vue";
|
|
|
15
14
|
</script>
|
|
16
15
|
<style lang="scss" scoped>
|
|
17
16
|
.login-container {
|
|
18
|
-
@apply flex flex-col items-center
|
|
17
|
+
@apply flex min-h-screen flex-col items-center px-4;
|
|
19
18
|
margin-top: 3vh;
|
|
20
19
|
|
|
21
20
|
// Mobile devices
|
|
@@ -33,7 +32,7 @@ import LoginForm from "@/components/Auth/LoginForm.vue";
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
.login-card {
|
|
36
|
-
@apply
|
|
35
|
+
@apply rounded-lg border border-dark-650 bg-dark-400 shadow-xl;
|
|
37
36
|
backdrop-filter: blur(10px);
|
|
38
37
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
39
38
|
width: 100%;
|
|
@@ -52,7 +51,7 @@ import LoginForm from "@/components/Auth/LoginForm.vue";
|
|
|
52
51
|
margin: 0.25rem 0;
|
|
53
52
|
|
|
54
53
|
h2 {
|
|
55
|
-
@apply text-lg
|
|
54
|
+
@apply mb-2 text-lg;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
.flex.justify-center {
|
|
@@ -79,11 +78,11 @@ import LoginForm from "@/components/Auth/LoginForm.vue";
|
|
|
79
78
|
// Title responsive sizing
|
|
80
79
|
.login-card h2 {
|
|
81
80
|
@media (max-width: 480px) {
|
|
82
|
-
@apply text-lg
|
|
81
|
+
@apply mb-3 text-lg;
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
@media (orientation: landscape) {
|
|
86
|
-
@apply text-base
|
|
85
|
+
@apply mb-2 text-base;
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
</style>
|
package/src/views/Tasks.vue
CHANGED
|
@@ -126,6 +126,9 @@
|
|
|
126
126
|
<Utilities class="utilities-section" />
|
|
127
127
|
</div>
|
|
128
128
|
|
|
129
|
+
<!-- View Task Modal -->
|
|
130
|
+
<ViewTask v-if="ui.activeModal === 'view-task' && ui.selectedTaskForView" :task="ui.selectedTaskForView" />
|
|
131
|
+
|
|
129
132
|
<transition-group name="fade">
|
|
130
133
|
<CreateTaskTM
|
|
131
134
|
v-if="ui.currentModule == 'TM' && activeModal === 'create-task'"
|
|
@@ -234,6 +237,7 @@
|
|
|
234
237
|
import { computed, onMounted, ref } from "vue";
|
|
235
238
|
import { DesktopControls } from "@/components/Tasks/Controls";
|
|
236
239
|
import TaskView from "@/components/Tasks/TaskView.vue";
|
|
240
|
+
import ViewTask from "@/components/Tasks/ViewTask.vue";
|
|
237
241
|
import Utilities from "@/components/Tasks/Utilities.vue";
|
|
238
242
|
import CreateTaskTM from "@/components/Tasks/CreateTaskTM.vue";
|
|
239
243
|
import CreateTaskAXS from "@/components/Tasks/CreateTaskAXS.vue";
|
package/vite.config.js
CHANGED