Fixes console page responsiveness, allowing proper stretch on different viewports
This commit is contained in:
@@ -34,6 +34,9 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
|
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
|
||||||
const rgbaRef = useRef<Uint8ClampedArray | null>(null);
|
const rgbaRef = useRef<Uint8ClampedArray | null>(null);
|
||||||
|
|
||||||
|
const tmpCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const tmpCtxRef = useRef<CanvasRenderingContext2D | null>(null);
|
||||||
|
|
||||||
const framebufferRef = useRef({ width: 0, height: 0 });
|
const framebufferRef = useRef({ width: 0, height: 0 });
|
||||||
|
|
||||||
const [state, setState] = useState<VncConnectionState>("IDLE");
|
const [state, setState] = useState<VncConnectionState>("IDLE");
|
||||||
@@ -45,12 +48,49 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
const [lastFrameAt, setLastFrameAt] = useState<string | null>(null);
|
const [lastFrameAt, setLastFrameAt] = useState<string | null>(null);
|
||||||
|
|
||||||
const buildWebSocketUrl = useCallback(() => {
|
const buildWebSocketUrl = useCallback(() => {
|
||||||
if (!accessToken) {
|
return accessToken ? appendAccessToken(websocketUrl, accessToken) : websocketUrl;
|
||||||
return websocketUrl;
|
}, [websocketUrl, accessToken]);
|
||||||
|
|
||||||
|
const renderScaledFrame = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const tmpCanvas = tmpCanvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas || !tmpCanvas) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const displayWidth = Math.max(1, Math.round(rect.width));
|
||||||
|
const displayHeight = Math.max(1, Math.round(rect.height));
|
||||||
|
|
||||||
|
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
||||||
|
canvas.width = displayWidth;
|
||||||
|
canvas.height = displayHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return appendAccessToken(websocketUrl, accessToken);
|
if (!ctxRef.current) {
|
||||||
}, [websocketUrl, accessToken]);
|
ctxRef.current = canvas.getContext("2d", {
|
||||||
|
alpha: false,
|
||||||
|
desynchronized: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = ctxRef.current;
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
tmpCanvas,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
tmpCanvas.width,
|
||||||
|
tmpCanvas.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clearFrame = useCallback(() => {
|
const clearFrame = useCallback(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -64,6 +104,8 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
|
|
||||||
ctxRef.current = null;
|
ctxRef.current = null;
|
||||||
rgbaRef.current = null;
|
rgbaRef.current = null;
|
||||||
|
tmpCanvasRef.current = null;
|
||||||
|
tmpCtxRef.current = null;
|
||||||
framebufferRef.current = { width: 0, height: 0 };
|
framebufferRef.current = { width: 0, height: 0 };
|
||||||
|
|
||||||
setFrameSize({ width: 0, height: 0 });
|
setFrameSize({ width: 0, height: 0 });
|
||||||
@@ -96,63 +138,66 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const drawFrame = useCallback((buffer: ArrayBuffer) => {
|
const drawFrame = useCallback(
|
||||||
const canvas = canvasRef.current;
|
(buffer: ArrayBuffer) => {
|
||||||
if (!canvas) return;
|
if (buffer.byteLength <= 8) return;
|
||||||
|
|
||||||
if (buffer.byteLength <= 8) return;
|
const view = new DataView(buffer);
|
||||||
|
const width = view.getInt32(0);
|
||||||
|
const height = view.getInt32(4);
|
||||||
|
|
||||||
const view = new DataView(buffer);
|
if (!width || !height || width <= 0 || height <= 0) return;
|
||||||
const width = view.getInt32(0);
|
|
||||||
const height = view.getInt32(4);
|
|
||||||
|
|
||||||
console.log("[VNC] drawFrame", {
|
const pixels = new Uint8ClampedArray(buffer, 8);
|
||||||
byteLength: buffer.byteLength,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!width || !height || width <= 0 || height <= 0) return;
|
if (
|
||||||
|
framebufferRef.current.width !== width ||
|
||||||
|
framebufferRef.current.height !== height
|
||||||
|
) {
|
||||||
|
framebufferRef.current = { width, height };
|
||||||
|
setFrameSize({ width, height });
|
||||||
|
}
|
||||||
|
|
||||||
const pixels = new Uint8ClampedArray(buffer, 8);
|
if (!rgbaRef.current || rgbaRef.current.length !== width * height * 4) {
|
||||||
framebufferRef.current = { width, height };
|
rgbaRef.current = new Uint8ClampedArray(width * height * 4);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ctxRef.current) {
|
const rgba = rgbaRef.current;
|
||||||
ctxRef.current = canvas.getContext("2d", {
|
|
||||||
alpha: false,
|
|
||||||
desynchronized: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = ctxRef.current;
|
for (let index = 0; index < pixels.length; index += 4) {
|
||||||
if (!ctx) return;
|
rgba[index] = pixels[index + 2];
|
||||||
|
rgba[index + 1] = pixels[index + 1];
|
||||||
|
rgba[index + 2] = pixels[index];
|
||||||
|
rgba[index + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
if (canvas.width !== width || canvas.height !== height) {
|
if (!tmpCanvasRef.current) {
|
||||||
canvas.width = width;
|
tmpCanvasRef.current = document.createElement("canvas");
|
||||||
canvas.height = height;
|
}
|
||||||
rgbaRef.current = new Uint8ClampedArray(width * height * 4);
|
|
||||||
setFrameSize({ width, height });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rgbaRef.current || rgbaRef.current.length !== width * height * 4) {
|
const tmpCanvas = tmpCanvasRef.current;
|
||||||
rgbaRef.current = new Uint8ClampedArray(width * height * 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rgba = rgbaRef.current;
|
if (tmpCanvas.width !== width || tmpCanvas.height !== height) {
|
||||||
|
tmpCanvas.width = width;
|
||||||
|
tmpCanvas.height = height;
|
||||||
|
tmpCtxRef.current = tmpCanvas.getContext("2d", {
|
||||||
|
alpha: false,
|
||||||
|
desynchronized: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < pixels.length; index += 4) {
|
const tmpCtx = tmpCtxRef.current;
|
||||||
rgba[index] = pixels[index + 2];
|
if (!tmpCtx) return;
|
||||||
rgba[index + 1] = pixels[index + 1];
|
|
||||||
rgba[index + 2] = pixels[index];
|
|
||||||
rgba[index + 3] = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageData = ctx.createImageData(width, height);
|
const imageData = tmpCtx.createImageData(width, height);
|
||||||
imageData.data.set(rgba);
|
imageData.data.set(rgba);
|
||||||
|
tmpCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
renderScaledFrame();
|
||||||
setLastFrameAt(new Date().toLocaleTimeString());
|
setLastFrameAt(new Date().toLocaleTimeString());
|
||||||
}, []);
|
},
|
||||||
|
[renderScaledFrame],
|
||||||
|
);
|
||||||
|
|
||||||
const connect = useCallback(
|
const connect = useCallback(
|
||||||
async (input?: Partial<ConnectVncInput>) => {
|
async (input?: Partial<ConnectVncInput>) => {
|
||||||
@@ -246,19 +291,15 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data instanceof ArrayBuffer) {
|
if (event.data instanceof ArrayBuffer) {
|
||||||
console.log("[VNC] binary ArrayBuffer", event.data.byteLength);
|
|
||||||
drawFrame(event.data);
|
drawFrame(event.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data instanceof Blob) {
|
if (event.data instanceof Blob) {
|
||||||
console.log("[VNC] binary Blob", event.data.size);
|
|
||||||
event.data.arrayBuffer().then(drawFrame);
|
event.data.arrayBuffer().then(drawFrame);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("[VNC] unknown binary payload", event.data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onerror = () => {
|
socket.onerror = () => {
|
||||||
@@ -310,28 +351,8 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
const containerAspect = rect.width / rect.height;
|
const relativeX = (clientX - rect.left) / rect.width;
|
||||||
const frameAspect = framebuffer.width / framebuffer.height;
|
const relativeY = (clientY - rect.top) / rect.height;
|
||||||
|
|
||||||
let renderedWidth: number;
|
|
||||||
let renderedHeight: number;
|
|
||||||
let offsetX: number;
|
|
||||||
let offsetY: number;
|
|
||||||
|
|
||||||
if (containerAspect > frameAspect) {
|
|
||||||
renderedHeight = rect.height;
|
|
||||||
renderedWidth = rect.height * frameAspect;
|
|
||||||
offsetX = rect.left + (rect.width - renderedWidth) / 2;
|
|
||||||
offsetY = rect.top;
|
|
||||||
} else {
|
|
||||||
renderedWidth = rect.width;
|
|
||||||
renderedHeight = rect.width / frameAspect;
|
|
||||||
offsetX = rect.left;
|
|
||||||
offsetY = rect.top + (rect.height - renderedHeight) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativeX = (clientX - offsetX) / renderedWidth;
|
|
||||||
const relativeY = (clientY - offsetY) / renderedHeight;
|
|
||||||
|
|
||||||
const clampedX = Math.max(0, Math.min(1, relativeX));
|
const clampedX = Math.max(0, Math.min(1, relativeX));
|
||||||
const clampedY = Math.max(0, Math.min(1, relativeY));
|
const clampedY = Math.max(0, Math.min(1, relativeY));
|
||||||
@@ -350,6 +371,21 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
[sendClick],
|
[sendClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
renderScaledFrame();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(canvas);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [renderScaledFrame]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
closeSocket();
|
closeSocket();
|
||||||
@@ -375,4 +411,4 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
|||||||
connected: state === "CONNECTED" || state === "FIRST_FRAME",
|
connected: state === "CONNECTED" || state === "FIRST_FRAME",
|
||||||
connecting: state === "CONNECTING_WS" || state === "CONNECTING_VNC",
|
connecting: state === "CONNECTING_WS" || state === "CONNECTING_VNC",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,19 +30,17 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
defaultHost: runtimeConfig.vnc.defaultHost,
|
defaultHost: runtimeConfig.vnc.defaultHost,
|
||||||
defaultPort: runtimeConfig.vnc.defaultPort,
|
defaultPort: runtimeConfig.vnc.defaultPort,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
const connectionLabel = getConnectionLabel(vnc.state);
|
const connectionLabel = getConnectionLabel(vnc.state);
|
||||||
const hasFrame = vnc.frameSize.width > 0 && vnc.frameSize.height > 0;
|
const hasFrame = vnc.frameSize.width > 0 && vnc.frameSize.height > 0;
|
||||||
const frameAspectRatio = hasFrame
|
|
||||||
? `${vnc.frameSize.width} / ${vnc.frameSize.height}`
|
|
||||||
: "800 / 480";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isDark ? "h-full min-h-0 text-slate-100" : "h-full min-h-0 text-slate-950"}>
|
<div className={isDark ? "h-full min-h-0 text-slate-100" : "h-full min-h-0 text-slate-950"}>
|
||||||
<section className="grid h-full min-h-0 gap-4 xl:grid-cols-[300px_minmax(0,1fr)]">
|
<section className="grid h-full min-h-0 gap-3 lg:grid-cols-[240px_minmax(0,1fr)] 2xl:grid-cols-[300px_minmax(0,1fr)]">
|
||||||
<aside className={panelClass(isDark)}>
|
<aside className={panelClass(isDark)}>
|
||||||
<div className="mb-5">
|
<div className="mb-3">
|
||||||
<p className="text-xs font-black uppercase tracking-[0.20em] text-slate-500">
|
<p className="text-[10px] font-black uppercase tracking-[0.20em] text-slate-500">
|
||||||
ACESSO REMOTO
|
ACESSO REMOTO
|
||||||
</p>
|
</p>
|
||||||
<h2 className={panelTitleClass(isDark)}>
|
<h2 className={panelTitleClass(isDark)}>
|
||||||
@@ -50,17 +48,15 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<StatusCard
|
||||||
<StatusCard
|
theme={theme}
|
||||||
theme={theme}
|
icon={<Wifi className="h-4 w-4" />}
|
||||||
icon={<Wifi className="h-4 w-4" />}
|
title="Estado da sessão"
|
||||||
title="Estado da sessão"
|
value={connectionLabel}
|
||||||
value={connectionLabel}
|
color={vnc.connected ? "green" : vnc.connecting ? "blue" : "purple"}
|
||||||
color={vnc.connected ? "green" : vnc.connecting ? "blue" : "purple"}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 space-y-4">
|
<div className="mt-3 space-y-3">
|
||||||
<Field
|
<Field
|
||||||
theme={theme}
|
theme={theme}
|
||||||
label="Host"
|
label="Host"
|
||||||
@@ -113,20 +109,20 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={vnc.connecting}
|
disabled={vnc.connecting}
|
||||||
onClick={() => (vnc.connected ? vnc.disconnect() : vnc.connect())}
|
onClick={() => (vnc.connected ? vnc.disconnect() : vnc.connect())}
|
||||||
className="mt-5 flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-[5px] bg-[#4FD1C5] text-sm font-black text-[#031014] transition hover:bg-[#5FE1D5] disabled:cursor-not-allowed disabled:opacity-50"
|
className="mt-3 flex h-9 w-full cursor-pointer items-center justify-center gap-2 rounded-[5px] bg-[#4FD1C5] text-xs font-black text-[#031014] transition hover:bg-[#5FE1D5] disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Plug className="h-4 w-4" />
|
<Plug className="h-4 w-4" />
|
||||||
{vnc.connected ? "Desligar sessão" : vnc.connecting ? "A ligar..." : "Iniciar sessão"}
|
{vnc.connected ? "Desligar sessão" : vnc.connecting ? "A ligar..." : "Iniciar sessão"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{vnc.error && (
|
{vnc.error && (
|
||||||
<div className="mt-4 flex gap-3 rounded-[5px] border border-red-500/25 bg-red-500/10 p-4 text-xs font-semibold leading-5 text-red-200">
|
<div className="mt-3 flex gap-2 rounded-[5px] border border-red-500/25 bg-red-500/10 p-3 text-xs font-semibold leading-5 text-red-200">
|
||||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
<span>{vnc.error}</span>
|
<span>{vnc.error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 space-y-3">
|
<div className="mt-4 hidden space-y-3 2xl:block">
|
||||||
<SmallInfo theme={theme} icon={<ShieldCheck className="h-4 w-4" />} label="Modo" value="LAN direta" />
|
<SmallInfo theme={theme} icon={<ShieldCheck className="h-4 w-4" />} label="Modo" value="LAN direta" />
|
||||||
<SmallInfo theme={theme} icon={<Wrench className="h-4 w-4" />} label="Controlador" value={vnc.host} />
|
<SmallInfo theme={theme} icon={<Wrench className="h-4 w-4" />} label="Controlador" value={vnc.host} />
|
||||||
<SmallInfo theme={theme} icon={<MousePointerClick className="h-4 w-4" />} label="Interação" value="Cliques ativos" />
|
<SmallInfo theme={theme} icon={<MousePointerClick className="h-4 w-4" />} label="Interação" value="Cliques ativos" />
|
||||||
@@ -134,12 +130,12 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className={`${panelClass(isDark)} min-w-0 p-0`}>
|
<section className={`${panelClass(isDark)} min-w-0 p-0`}>
|
||||||
<header className="flex h-14 shrink-0 items-center justify-between border-b border-white/10 px-5">
|
<header className="flex h-12 shrink-0 items-center justify-between border-b border-white/10 px-4">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h2 className={panelTitleClass(isDark)}>
|
<h2 className={panelTitleClass(isDark)}>
|
||||||
Ecrã do controlador
|
Ecrã do controlador
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-0.5 text-xs font-semibold text-slate-500">
|
<p className="mt-0.5 truncate text-xs font-semibold text-slate-500">
|
||||||
{vnc.lastFrameAt ? `Último frame: ${vnc.lastFrameAt}` : "A aguardar imagem da consola"}
|
{vnc.lastFrameAt ? `Último frame: ${vnc.lastFrameAt}` : "A aguardar imagem da consola"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,12 +143,12 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
|
|
||||||
<div className="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-black">
|
<div className="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-black">
|
||||||
{!hasFrame && (
|
{!hasFrame && (
|
||||||
<div className="flex max-w-md flex-col items-center text-center">
|
<div className="flex max-w-md flex-col items-center px-6 text-center">
|
||||||
<div className="grid h-16 w-16 place-items-center rounded-[5px] border border-sky-400/20 bg-sky-400/10">
|
<div className="grid h-14 w-14 place-items-center rounded-[5px] border border-sky-400/20 bg-sky-400/10">
|
||||||
<Monitor className="h-8 w-8 text-[#4FD1C5]" />
|
<Monitor className="h-7 w-7 text-[#4FD1C5]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="mt-5 text-xl font-black tracking-[-0.04em] text-white">
|
<h3 className="mt-4 text-lg font-black tracking-[-0.04em] text-white">
|
||||||
{vnc.state === "DISCONNECTED" ? "Sessão terminada" : "Consola indisponível"}
|
{vnc.state === "DISCONNECTED" ? "Sessão terminada" : "Consola indisponível"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 text-sm leading-6 text-slate-400">
|
<p className="mt-2 text-sm leading-6 text-slate-400">
|
||||||
@@ -174,12 +170,7 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={vnc.canvasRef}
|
ref={vnc.canvasRef}
|
||||||
className="pointer-events-none block max-h-full max-w-full bg-black object-contain [image-rendering:pixelated]"
|
className="pointer-events-none block h-full w-full bg-black [image-rendering:pixelated]"
|
||||||
style={{
|
|
||||||
aspectRatio: frameAspectRatio,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,14 +182,14 @@ export function ConsolePage({ theme }: ConsolePageProps) {
|
|||||||
|
|
||||||
function panelClass(isDark: boolean) {
|
function panelClass(isDark: boolean) {
|
||||||
return isDark
|
return isDark
|
||||||
? `${RADIUS} flex h-full min-h-0 flex-col border border-white/10 bg-[#071421] p-3 shadow-[0_14px_34px_rgba(0,0,0,0.22)]`
|
? `${RADIUS} flex h-full min-h-0 flex-col border border-white/10 bg-[#071421] p-2 shadow-[0_14px_34px_rgba(0,0,0,0.22)]`
|
||||||
: `${RADIUS} flex h-full min-h-0 flex-col border border-slate-200 bg-white p-3 shadow-[0_10px_26px_rgba(15,23,42,0.06)]`;
|
: `${RADIUS} flex h-full min-h-0 flex-col border border-slate-200 bg-white p-2 shadow-[0_10px_26px_rgba(15,23,42,0.06)]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelTitleClass(isDark: boolean) {
|
function panelTitleClass(isDark: boolean) {
|
||||||
return isDark
|
return isDark
|
||||||
? "text-base font-black text-slate-100"
|
? "text-sm font-black text-slate-100"
|
||||||
: "text-base font-black text-slate-950";
|
: "text-sm font-black text-slate-950";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionLabel(state: VncConnectionState) {
|
function getConnectionLabel(state: VncConnectionState) {
|
||||||
@@ -245,14 +236,14 @@ function Field({
|
|||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? "text-[11px] font-black uppercase tracking-[0.20em] text-[#7F8CA3]"
|
? "text-[10px] font-black uppercase tracking-[0.18em] text-[#7F8CA3]"
|
||||||
: "text-[11px] font-black uppercase tracking-[0.20em] text-slate-500"
|
: "text-[10px] font-black uppercase tracking-[0.18em] text-slate-500"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="relative mt-2">
|
<div className="relative mt-1.5">
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -260,8 +251,8 @@ function Field({
|
|||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? `${RADIUS} h-10 w-full border border-[#263247] bg-[#07101B] px-3 ${rightElement ? "pr-11" : ""} text-sm font-bold text-white outline-none transition placeholder:text-[#526074] focus:border-[#4FD1C5] disabled:cursor-not-allowed disabled:opacity-60`
|
? `${RADIUS} h-9 w-full border border-[#263247] bg-[#07101B] px-3 ${rightElement ? "pr-11" : ""} text-sm font-bold text-white outline-none transition placeholder:text-[#526074] focus:border-[#4FD1C5] disabled:cursor-not-allowed disabled:opacity-60`
|
||||||
: `${RADIUS} h-10 w-full border border-slate-200 bg-white px-3 ${rightElement ? "pr-11" : ""} text-sm font-bold text-[#0F172A] outline-none transition placeholder:text-slate-400 focus:border-[#0F766E] disabled:cursor-not-allowed disabled:opacity-60`
|
: `${RADIUS} h-9 w-full border border-slate-200 bg-white px-3 ${rightElement ? "pr-11" : ""} text-sm font-bold text-[#0F172A] outline-none transition placeholder:text-slate-400 focus:border-[#0F766E] disabled:cursor-not-allowed disabled:opacity-60`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -308,13 +299,13 @@ function StatusCard({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? `${RADIUS} min-w-0 border border-[#263247] bg-[#111A2B] p-3`
|
? `${RADIUS} min-w-0 border border-[#263247] bg-[#111A2B] p-2`
|
||||||
: `${RADIUS} min-w-0 border border-[#D7DEE8] bg-[#F8FAFC] p-3`
|
: `${RADIUS} min-w-0 border border-[#D7DEE8] bg-[#F8FAFC] p-2`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={`grid h-10 w-10 shrink-0 place-items-center ${RADIUS} ${colors[color]}`}
|
className={`grid h-9 w-9 shrink-0 place-items-center ${RADIUS} ${colors[color]}`}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
@@ -323,8 +314,8 @@ function StatusCard({
|
|||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? "truncate text-[10px] font-bold uppercase tracking-[0.18em] text-[#7F8CA3]"
|
? "truncate text-[9px] font-bold uppercase tracking-[0.16em] text-[#7F8CA3]"
|
||||||
: "truncate text-[10px] font-bold uppercase tracking-[0.18em] text-slate-500"
|
: "truncate text-[9px] font-bold uppercase tracking-[0.16em] text-slate-500"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -333,8 +324,8 @@ function StatusCard({
|
|||||||
<h3
|
<h3
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? "mt-1 truncate text-sm font-black text-white"
|
? "mt-0.5 truncate text-sm font-black text-white"
|
||||||
: "mt-1 truncate text-sm font-black text-[#0F172A]"
|
: "mt-0.5 truncate text-sm font-black text-[#0F172A]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
@@ -394,4 +385,4 @@ function SmallInfo({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConsolePage;
|
export default ConsolePage;
|
||||||
Reference in New Issue
Block a user