2 Commits

4 changed files with 70 additions and 15 deletions
+2 -2
View File
@@ -8,12 +8,12 @@
href="https://assets.jdbnet.co.uk/projects/ssh.png"
/>
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0f1419" />
<meta name="theme-color" content="#0d1117" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<title>SSH</title>
+32 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Folder, Pencil, Trash2 } from "lucide-vue-next";
import { Folder, Pencil, Trash2, Radio } from "lucide-vue-next";
import {
api,
type HostRow,
@@ -37,6 +37,22 @@ const tabs = ref<{ id: string; hostId: number; label: string }[]>([]);
const activePanes = ref<string[]>([]);
const draggedTabId = ref<string | null>(null);
const broadcastMode = ref(false);
const tabRefs = ref<Record<string, any>>({});
function setTabRef(el: any, id: string) {
if (el) tabRefs.value[id] = el;
else delete tabRefs.value[id];
}
function handleBroadcast(data: string, sourceId: string) {
if (!broadcastMode.value) return;
for (const paneId of activePanes.value) {
if (paneId !== sourceId && tabRefs.value[paneId]) {
tabRefs.value[paneId].sendData(data);
}
}
}
function onTabDragStart(id: string) {
draggedTabId.value = id;
}
@@ -744,6 +760,19 @@ async function deleteIdentityRow(id: number) {
</div>
</div>
<div class="flex items-center gap-2">
<button
v-if="activePanes.length > 1"
type="button"
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex border transition-colors"
:class="broadcastMode ? 'border-accent bg-accent/10 text-accent' : 'border-slate-800 text-slate-400 hover:border-slate-700 hover:text-white'"
@click="broadcastMode = !broadcastMode"
title="Broadcast input to all visible terminals"
>
<span class="flex items-center gap-2">
<Radio class="h-3.5 w-3.5" />
Broadcast
</span>
</button>
<button
type="button"
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex"
@@ -1067,9 +1096,11 @@ async function deleteIdentityRow(id: number) {
class="flex-1 min-w-0 h-full"
>
<TabContent
:ref="(el) => setTabRef(el, t.id)"
:host-id="t.hostId"
:visible="activePanes.includes(t.id)"
:show-sftp="showSftpPanel"
@broadcast-data="(data: string) => handleBroadcast(data, t.id)"
/>
</div>
+19 -5
View File
@@ -17,6 +17,18 @@ const props = defineProps<{
showSftp: boolean;
}>();
const emit = defineEmits<{
(e: 'broadcast-data', data: string): void;
}>();
defineExpose({
sendData: (data: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data));
}
}
});
const termEl = ref<HTMLElement | null>(null);
const status = ref("Connecting…");
const connId = ref<string | null>(null);
@@ -79,12 +91,13 @@ onMounted(async () => {
term = new Terminal({
cursorBlink: true,
fontFamily: "IBM Plex Mono, monospace",
fontFamily: "DM Mono, ui-monospace, monospace",
fontSize: 14,
theme: {
background: "#0a0e12",
foreground: "#e2e8f0",
cursor: "#3d9aed",
background: "#0d1117",
foreground: "#e6edf3",
cursor: "#1ebe8a",
selectionBackground: "rgba(30, 190, 138, 0.3)",
},
});
fit = new FitAddon();
@@ -96,6 +109,7 @@ onMounted(async () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data));
}
emit("broadcast-data", data);
});
term.onResize(({ cols, rows }) => {
@@ -185,7 +199,7 @@ watch(
</div>
<div
ref="termEl"
class="h-full min-h-[320px] rounded-lg border border-slate-800 bg-[#0a0e12] p-1"
class="h-full min-h-[320px] rounded-lg border border-slate-800 bg-[#0d1117] p-1"
/>
</div>
<template v-if="showSftp">
+17 -7
View File
@@ -5,18 +5,28 @@ export default {
extend: {
colors: {
surface: {
DEFAULT: "#0f1419",
raised: "#151c24",
overlay: "#1a232e",
DEFAULT: "#0d1117",
raised: "#161b22",
overlay: "#21262d",
},
accent: {
DEFAULT: "#3d9aed",
muted: "#2a6fa3",
DEFAULT: "#1ebe8a",
muted: "#16966b",
},
slate: {
200: "#e6edf3",
300: "#c9d1d9",
400: "#8b949e",
500: "#6e7681",
600: "#484f58",
700: "#30363d",
800: "#21262d",
900: "#161b22",
},
},
fontFamily: {
sans: ["IBM Plex Sans", "system-ui", "sans-serif"],
mono: ["IBM Plex Mono", "ui-monospace", "monospace"],
sans: ["Space Grotesk", "system-ui", "sans-serif"],
mono: ["DM Mono", "ui-monospace", "monospace"],
},
},
},