@@ -59,6 +59,11 @@ import Footer from '../components/Footer.astro';
5959 </div >
6060 </div >
6161
62+ <!-- Host order hint -->
63+ <p class =" order-hint text-muted" id =" order-hint" hidden >
64+ Arraste os cards para reordenar. A ordem importa — o SSH usa o primeiro Host que combinar.
65+ </p >
66+
6267 <!-- Host entries container -->
6368 <section id =" hosts-container" ></section >
6469
@@ -294,6 +299,59 @@ import Footer from '../components/Footer.astro';
294299 gap: 0.5rem;
295300 }
296301
302+ /* Order hint */
303+ .order-hint {
304+ font-size: 0.75rem;
305+ margin-bottom: 0.5rem;
306+ }
307+
308+ /* Drag handle */
309+ .host-card__drag {
310+ cursor: grab;
311+ color: var(--text-muted);
312+ font-size: 0.9rem;
313+ padding: 0 0.25rem;
314+ flex-shrink: 0;
315+ user-select: none;
316+ }
317+
318+ .host-card__drag:active {
319+ cursor: grabbing;
320+ }
321+
322+ .host-card--dragging {
323+ opacity: 0.4;
324+ border-style: dashed;
325+ }
326+
327+ .host-card--drag-over {
328+ border-color: var(--accent);
329+ box-shadow: var(--glow-sm);
330+ }
331+
332+ /* ProxyJump chain */
333+ .proxy-chain {
334+ display: flex;
335+ align-items: center;
336+ gap: 0.35rem;
337+ flex-wrap: wrap;
338+ margin-top: 0.5rem;
339+ padding: 0.4rem 0.6rem;
340+ background: var(--bg-secondary);
341+ border: 1px solid var(--border);
342+ border-radius: var(--radius);
343+ font-size: 0.75rem;
344+ }
345+
346+ .proxy-chain__node {
347+ color: var(--accent);
348+ font-weight: 700;
349+ }
350+
351+ .proxy-chain__arrow {
352+ color: var(--text-muted);
353+ }
354+
297355 @media (max-width: 640px) {
298356 .actions-bar {
299357 flex-direction: column;
@@ -328,12 +386,14 @@ import Footer from '../components/Footer.astro';
328386 hostsContainer.innerHTML = '';
329387 emptyState.hidden = hosts.length > 0;
330388 outputSection.hidden = hosts.length === 0;
389+ $<HTMLElement>('order-hint').hidden = hosts.length < 2;
331390
332391 for (const host of hosts) {
333392 hostsContainer.appendChild(createHostCard(host, false));
334393 }
335394
336395 updateOutput();
396+ updateProxyChains();
337397 }
338398
339399 function updateOutput() {
@@ -350,9 +410,12 @@ import Footer from '../components/Footer.astro';
350410 const title = entry.host || 'novo-host';
351411 const subtitle = [entry.user, entry.hostName].filter(Boolean).join('@') || '';
352412
413+ card.draggable = true;
414+
353415 card.innerHTML = `
354416 <div class="host-card__header">
355417 <div class="host-card__header-left">
418+ <span class="host-card__drag" title="Arraste para reordenar">☰</span>
356419 <span class="host-card__chevron">▶</span>
357420 <span class="host-card__title">Host ${title}</span>
358421 <span class="host-card__subtitle">${esc(subtitle)}</span>
@@ -386,6 +449,7 @@ import Footer from '../components/Footer.astro';
386449 <input type =" text" data-field =" proxyJump" value =" ${esc(entry.proxyJump)}" placeholder =" bastion" />
387450 </div >
388451 </div >
452+ <div class =" proxy-chain" data-proxy-chain =" ${entry.id}" hidden ></div >
389453 <button class =" advanced-toggle" data-toggle =" ${entry.id}" >+ Opções avançadas</button >
390454 <div class =" advanced-fields form-grid" hidden data-advanced =" ${entry.id}" >
391455 <div class =" form-group" >
@@ -471,6 +535,7 @@ import Footer from '../components/Footer.astro';
471535 titleEl .textContent = ` Host ${entry .host || ' novo-host' } ` ;
472536 subtitleEl .textContent = [entry .user , entry .hostName ].filter (Boolean ).join (' @' );
473537 updateOutput ();
538+ if (field === ' proxyJump' ) updateProxyChains ();
474539 };
475540 input .addEventListener (' input' , handler );
476541 input .addEventListener (' change' , handler );
@@ -522,6 +587,90 @@ import Footer from '../components/Footer.astro';
522587 return value .replace (/ "/ g , ' "' ).replace (/ </ g , ' <' ).replace (/ >/ g , ' >' );
523588 }
524589
590+ // --- ProxyJump chain resolver ---
591+ function resolveChain(entry: SshHostEntry): string[] {
592+ const chain: string [] = [' Você' ];
593+ const visited = new Set <string >();
594+ let jump = entry .proxyJump ;
595+
596+ while (jump ) {
597+ if (visited .has (jump )) break ; // circular
598+ visited .add (jump );
599+ chain .push (jump );
600+ const jumpHost = hosts .find (h => h .host === jump );
601+ jump = jumpHost ?.proxyJump ;
602+ }
603+
604+ chain .push (entry .host || ' destino' );
605+ return chain ;
606+ }
607+
608+ function updateProxyChains() {
609+ for (const entry of hosts ) {
610+ const el = document .querySelector (` [data-proxy-chain="${entry .id }"] ` ) as HTMLElement | null ;
611+ if (! el ) continue ;
612+
613+ if (! entry .proxyJump ) {
614+ el .hidden = true ;
615+ continue ;
616+ }
617+
618+ const chain = resolveChain (entry );
619+ el .hidden = false ;
620+ el .innerHTML = chain
621+ .map ((node , i ) => {
622+ const nodeHtml = ` <span class="proxy-chain__node">${esc (node )}</span> ` ;
623+ return i < chain .length - 1
624+ ? ` ${nodeHtml } <span class="proxy-chain__arrow">→</span> `
625+ : nodeHtml ;
626+ })
627+ .join (' ' );
628+ }
629+ }
630+
631+ // --- Drag and drop ---
632+ let draggedId: string | null = null;
633+
634+ hostsContainer.addEventListener('dragstart', (e) => {
635+ const card = (e .target as HTMLElement ).closest (' .host-card' ) as HTMLElement | null ;
636+ if (! card ) return ;
637+ draggedId = card .dataset .id ! ;
638+ card .classList .add (' host-card--dragging' );
639+ if (e .dataTransfer ) e .dataTransfer .effectAllowed = ' move' ;
640+ } );
641+
642+ hostsContainer.addEventListener('dragend', (e) => {
643+ const card = (e .target as HTMLElement ).closest (' .host-card' ) as HTMLElement | null ;
644+ if (card ) card .classList .remove (' host-card--dragging' );
645+ hostsContainer .querySelectorAll (' .host-card--drag-over' ).forEach (c => c .classList .remove (' host-card--drag-over' ));
646+ draggedId = null ;
647+ } );
648+
649+ hostsContainer.addEventListener('dragover', (e) => {
650+ e .preventDefault ();
651+ const card = (e .target as HTMLElement ).closest (' .host-card' ) as HTMLElement | null ;
652+ if (! card || card .dataset .id === draggedId ) return ;
653+ hostsContainer .querySelectorAll (' .host-card--drag-over' ).forEach (c => c .classList .remove (' host-card--drag-over' ));
654+ card .classList .add (' host-card--drag-over' );
655+ } );
656+
657+ hostsContainer.addEventListener('drop', (e) => {
658+ e .preventDefault ();
659+ const targetCard = (e .target as HTMLElement ).closest (' .host-card' ) as HTMLElement | null ;
660+ if (! targetCard || ! draggedId ) return ;
661+
662+ const targetId = targetCard .dataset .id ! ;
663+ if (targetId === draggedId ) return ;
664+
665+ const fromIdx = hosts .findIndex (h => h .id === draggedId );
666+ const toIdx = hosts .findIndex (h => h .id === targetId );
667+ if (fromIdx === - 1 || toIdx === - 1 ) return ;
668+
669+ const [moved] = hosts .splice (fromIdx , 1 );
670+ hosts .splice (toIdx , 0 , moved );
671+ render ();
672+ } );
673+
525674 // --- Add Host ---
526675 $<HTMLButtonElement >('btn-add-host').addEventListener('click', () => {
527676 const newHost = createEmptyHost ();
0 commit comments