fix: restore styling for local characters list (#152)

This commit is contained in:
guarzo
2025-02-07 12:15:20 -07:00
committed by GitHub
parent 6800be1bb6
commit f96cb01860
13 changed files with 457 additions and 332 deletions

View File

@@ -1,2 +1,3 @@
export * from './useSystemInfo'; export * from './useSystemInfo';
export * from './useGetOwnOnlineCharacters'; export * from './useGetOwnOnlineCharacters';
export * from './useElementWidth';

View File

@@ -0,0 +1,43 @@
import { useState, useLayoutEffect, RefObject } from 'react';
/**
* useElementWidth
*
* A custom hook that accepts a ref to an HTML element and returns its current width.
* It uses a ResizeObserver and window resize listener to update the width when necessary.
*
* @param ref - A RefObject pointing to an HTML element.
* @returns The current width of the element.
*/
export function useElementWidth<T extends HTMLElement>(ref: RefObject<T>): number {
const [width, setWidth] = useState<number>(0);
useLayoutEffect(() => {
const updateWidth = () => {
if (ref.current) {
const newWidth = ref.current.getBoundingClientRect().width;
if (newWidth > 0) {
setWidth(newWidth);
}
}
};
updateWidth(); // Initial measurement
const observer = new ResizeObserver(() => {
const id = setTimeout(updateWidth, 100);
return () => clearTimeout(id);
});
if (ref.current) {
observer.observe(ref.current);
}
window.addEventListener("resize", updateWidth);
return () => {
observer.disconnect();
window.removeEventListener("resize", updateWidth);
};
}, [ref]);
return width;
}

View File

@@ -1,16 +1,13 @@
import { useMemo, useRef } from 'react'; import { useMemo } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import clsx from 'clsx'; import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters';
import { LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api'; import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts'; import { UserPermission } from '@/hooks/Mapper/types/permissions';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { LocalCharactersList } from './components/LocalCharactersList'; import { LocalCharactersList } from './components/LocalCharactersList';
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters'; import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings'; import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
export const LocalCharacters = () => { export const LocalCharacters = () => {
const { const {
@@ -18,14 +15,12 @@ export const LocalCharacters = () => {
} = useMapRootState(); } = useMapRootState();
const [settings, setSettings] = useLocalCharacterWidgetSettings(); const [settings, setSettings] = useLocalCharacterWidgetSettings();
const [systemId] = selectedSystems; const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing'); const restrictOfflineShowing = useMapGetOption("restrict_offline_showing");
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]); const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
const showOffline = useMemo( const showOffline = useMemo(
() => !restrictOfflineShowing || isAdminOrManager, () => !restrictOfflineShowing || isAdminOrManager,
[isAdminOrManager, restrictOfflineShowing], [isAdminOrManager, restrictOfflineShowing]
); );
const sorted = useMemo(() => { const sorted = useMemo(() => {
@@ -42,7 +37,6 @@ export const LocalCharacters = () => {
if (!showOffline || !settings.showOffline) { if (!showOffline || !settings.showOffline) {
return filtered.filter(c => c.online); return filtered.filter(c => c.online);
} }
return filtered; return filtered;
}, [ }, [
characters, characters,
@@ -58,64 +52,18 @@ export const LocalCharacters = () => {
const isNotSelectedSystem = selectedSystems.length !== 1; const isNotSelectedSystem = selectedSystems.length !== 1;
const showList = sorted.length > 0 && selectedSystems.length === 1; const showList = sorted.length > 0 && selectedSystems.length === 1;
const ref = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(ref, 145);
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName); const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
return ( return (
<Widget <Widget
label={ label={
<div className="flex w-full items-center" ref={ref}> <LocalCharactersHeader
<div className="flex-shrink-0 select-none mr-2"> sortedCount={sorted.length}
Local{showList ? ` [${sorted.length}]` : ''} showList={showList}
</div> showOffline={showOffline}
<div className="flex-grow overflow-hidden"> settings={settings}
<LayoutEventBlocker className="flex items-center gap-2 justify-end"> setSettings={setSettings}
{showOffline && ( />
<WdTooltipWrapper content="Show offline characters in system">
<div className={clsx("min-w-0", { "max-w-[100px]": compact })}>
<WdCheckbox
size="xs"
labelSide="left"
label="Show offline"
value={settings.showOffline}
classNameLabel={clsx("whitespace-nowrap", { "truncate": compact })}
onChange={() =>
setSettings(prev => ({ ...prev, showOffline: !prev.showOffline }))
}
/>
</div>
</WdTooltipWrapper>
)}
{settings.compact && (
<WdTooltipWrapper content="Show ship name in compact rows">
<div className={clsx("min-w-0", { "max-w-[100px]": compact })}>
<WdCheckbox
size="xs"
labelSide="left"
label="Show ship name"
value={settings.showShipName}
classNameLabel={clsx("whitespace-nowrap", { "truncate": compact })}
onChange={() =>
setSettings(prev => ({ ...prev, showShipName: !prev.showShipName }))
}
/>
</div>
</WdTooltipWrapper>
)}
<span
className={clsx("w-4 h-4 cursor-pointer", {
"hero-bars-2": settings.compact,
"hero-bars-3": !settings.compact,
})}
onClick={() => setSettings(prev => ({ ...prev, compact: !prev.compact }))}
/>
</LayoutEventBlocker>
</div>
</div>
} }
> >
{isNotSelectedSystem && ( {isNotSelectedSystem && (
@@ -123,19 +71,17 @@ export const LocalCharacters = () => {
System is not selected System is not selected
</div> </div>
)} )}
{isNobodyHere && !isNotSelectedSystem && ( {isNobodyHere && !isNotSelectedSystem && (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm"> <div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
Nobody here Nobody here
</div> </div>
)} )}
{showList && ( {showList && (
<LocalCharactersList <LocalCharactersList
items={sorted} items={sorted}
itemSize={settings.compact ? 26 : 41} itemSize={settings.compact ? 26 : 41}
itemTemplate={itemTemplate} itemTemplate={itemTemplate}
containerClassName="w-full h-full overflow-x-hidden overflow-y-auto" containerClassName="w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none"
/> />
)} )}
</Widget> </Widget>

View File

@@ -1,4 +1,3 @@
// .VirtualScroller { .VirtualScroller {
// height: 100% !important; height: 100% !important;
// } }

View File

@@ -0,0 +1,91 @@
import React, { useRef } from 'react';
import clsx from 'clsx';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { LayoutEventBlocker, WdResponsiveCheckbox, WdDisplayMode } from '@/hooks/Mapper/components/ui-kit';
import { useElementWidth } from '@/hooks/Mapper/components/hooks';
interface LocalCharactersHeaderProps {
sortedCount: number;
showList: boolean;
showOffline: boolean;
settings: {
compact: boolean;
showOffline: boolean;
showShipName: boolean;
};
setSettings: (fn: (prev: any) => any) => void;
}
export const LocalCharactersHeader: React.FC<LocalCharactersHeaderProps> = ({
sortedCount,
showList,
showOffline,
settings,
setSettings,
}) => {
const headerRef = useRef<HTMLDivElement>(null);
const headerWidth = useElementWidth(headerRef) || 300;
const reservedWidth = 100;
const availableWidthForCheckboxes = Math.max(headerWidth - reservedWidth, 0);
let displayMode: WdDisplayMode = "full";
if (availableWidthForCheckboxes >= 150) {
displayMode = "full";
} else if (availableWidthForCheckboxes >= 100) {
displayMode = "abbr";
} else {
displayMode = "checkbox";
}
const compact = useMaxWidth(headerRef, 145);
return (
<div className="flex w-full items-center text-xs" ref={headerRef}>
<div className="flex-shrink-0 select-none mr-2">
Local{showList ? ` [${sortedCount}]` : ""}
</div>
<div className="flex-grow overflow-hidden">
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
<div className="flex items-center gap-2">
{showOffline && (
<WdResponsiveCheckbox
tooltipContent="Show offline characters in system"
size="xs"
labelFull="Show offline"
labelAbbreviated="Offline"
value={settings.showOffline}
onChange={() =>
setSettings((prev: any) => ({ ...prev, showOffline: !prev.showOffline }))
}
classNameLabel={clsx("whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300", { truncate: compact })}
displayMode={displayMode}
/>
)}
{settings.compact && (
<WdResponsiveCheckbox
tooltipContent="Show ship name in compact rows"
size="xs"
labelFull="Show ship name"
labelAbbreviated="Ship name"
value={settings.showShipName}
onChange={() =>
setSettings((prev: any) => ({ ...prev, showShipName: !prev.showShipName }))
}
classNameLabel={clsx("whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300", { truncate: compact })}
displayMode={displayMode}
/>
)}
</div>
<span
className={clsx("w-4 h-4 cursor-pointer", {
"hero-bars-2": settings.compact,
"hero-bars-3": !settings.compact,
})}
onClick={() => setSettings((prev: any) => ({ ...prev, compact: !prev.compact }))}
/>
</LayoutEventBlocker>
</div>
</div>
);
};

View File

@@ -15,7 +15,6 @@ export const SystemKills: React.FC = () => {
} = useMapRootState(); } = useMapRootState();
const [systemId] = selectedSystems || []; const [systemId] = selectedSystems || [];
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false); const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
const systemNameMap = useMemo(() => { const systemNameMap = useMemo(() => {
@@ -41,7 +40,9 @@ export const SystemKills: React.FC = () => {
const filteredKills = useMemo(() => { const filteredKills = useMemo(() => {
if (!settings.whOnly || !visible) return kills; if (!settings.whOnly || !visible) return kills;
return kills.filter(kill => { return kills.filter(kill => {
const system = systems.find(sys => sys.system_static_info.solar_system_id === kill.solar_system_id); const system = systems.find(
sys => sys.system_static_info.solar_system_id === kill.solar_system_id
);
if (!system) { if (!system) {
console.warn(`System with id ${kill.solar_system_id} not found.`); console.warn(`System with id ${kill.solar_system_id} not found.`);
return false; return false;
@@ -55,60 +56,62 @@ export const SystemKills: React.FC = () => {
<div className="flex flex-col flex-1 min-h-0"> <div className="flex flex-col flex-1 min-h-0">
<Widget <Widget
label={ label={
<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} /> <KillsHeader
systemId={systemId}
onOpenSettings={() => setSettingsDialogVisible(true)}
/>
} }
> >
{!isSubscriptionActive && ( <div className="relative h-full">
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm"> {!isSubscriptionActive ? (
Kills available with &#39;Active&#39; map subscription only (contact map administrators) <div className="absolute inset-0 flex items-center justify-center">
</div> <span className="select-none text-center text-stone-400/80 text-sm">
)} Kills available with &#39;Active&#39; map subscription only (contact map administrators)
{isSubscriptionActive && ( </span>
<> </div>
{isNothingSelected && ( ) : isNothingSelected ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm"> <div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
No system selected (or toggle Show all systems) No system selected (or toggle Show all systems)
</div> </span>
)} </div>
) : showLoading ? (
{!isNothingSelected && showLoading && ( <div className="absolute inset-0 flex items-center justify-center">
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm"> <span className="select-none text-center text-stone-400/80 text-sm">
Loading Kills... Loading Kills...
</div> </span>
)} </div>
) : error ? (
{!isNothingSelected && !showLoading && error && ( <div className="absolute inset-0 flex items-center justify-center">
<div className="w-full h-full flex justify-center items-center select-none text-center text-red-400 text-sm"> <span className="select-none text-center text-red-400 text-sm">
{error} {error}
</div> </span>
)} </div>
) : !filteredKills || filteredKills.length === 0 ? (
{!isNothingSelected && <div className="absolute inset-0 flex items-center justify-center">
!showLoading && <span className="select-none text-center text-stone-400/80 text-sm">
!error && No kills found
(!filteredKills || filteredKills.length === 0) && ( </span>
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm"> </div>
No kills found ) : (
</div> <div className="h-full overflow-y-auto">
)} <SystemKillsContent
key={settings.compact ? 'compact' : 'normal'}
{!isNothingSelected && !showLoading && !error && ( kills={filteredKills}
<div className="flex-1 flex flex-col overflow-y-auto"> systemNameMap={systemNameMap}
<SystemKillsContent compact={settings.compact}
key={settings.compact ? 'compact' : 'normal'} onlyOneSystem={!visible}
kills={filteredKills} />
systemNameMap={systemNameMap} </div>
compact={settings.compact} )}
onlyOneSystem={!visible} </div>
/>
</div>
)}
</>
)}
</Widget> </Widget>
</div> </div>
<KillsSettingsDialog visible={settingsDialogVisible} setVisible={setSettingsDialogVisible} /> <KillsSettingsDialog
visible={settingsDialogVisible}
setVisible={setSettingsDialogVisible}
/>
</div> </div>
); );
}; };

View File

@@ -1,52 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import { zkillLink } from '../helpers';
import classes from './SystemKillRow.module.scss';
interface AttackerRowSubInfoProps {
finalBlowCharId: number | null | undefined;
finalBlowCharName?: string;
attackerPortraitUrl: string | null;
finalBlowCorpId: number | null | undefined;
finalBlowCorpName?: string;
attackerCorpLogoUrl: string | null;
finalBlowAllianceId: number | null | undefined;
finalBlowAllianceName?: string;
attackerAllianceLogoUrl: string | null;
containerHeight?: number;
}
export const AttackerRowSubInfo: React.FC<AttackerRowSubInfoProps> = ({
finalBlowCharId = 0,
finalBlowCharName,
attackerPortraitUrl,
containerHeight = 8,
}) => {
if (!attackerPortraitUrl || finalBlowCharId === null || finalBlowCharId <= 0) {
return null;
}
const containerClass = `h-${containerHeight}`;
return (
<div className={clsx('flex items-start gap-1', containerClass)}>
<div className="relative shrink-0 w-auto h-full overflow-hidden">
<a
href={zkillLink('character', finalBlowCharId)}
target="_blank"
rel="noopener noreferrer"
className="block h-full"
>
<img
src={attackerPortraitUrl}
alt={finalBlowCharName || 'AttackerPortrait'}
className={clsx(classes.killRowImage, 'h-full w-auto object-contain')}
/>
</a>
</div>
</div>
);
};

View File

@@ -29,8 +29,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
}) => { }) => {
const { const {
killmail_id = 0, killmail_id = 0,
// Victim data
// Victim
victim_char_name = '', victim_char_name = '',
victim_alliance_ticker = '', victim_alliance_ticker = '',
victim_corp_ticker = '', victim_corp_ticker = '',
@@ -41,8 +40,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
victim_ship_type_id = 0, victim_ship_type_id = 0,
victim_corp_name = '', victim_corp_name = '',
victim_alliance_name = '', victim_alliance_name = '',
// Attacker data
// Attacker
final_blow_char_id = 0, final_blow_char_id = 0,
final_blow_char_name = '', final_blow_char_name = '',
final_blow_alliance_ticker = '', final_blow_alliance_ticker = '',
@@ -53,14 +51,12 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
final_blow_alliance_id = 0, final_blow_alliance_id = 0,
final_blow_ship_name = '', final_blow_ship_name = '',
final_blow_ship_type_id = 0, final_blow_ship_type_id = 0,
total_value = 0, total_value = 0,
kill_time = '', kill_time = '',
} = killDetails || {}; } = killDetails || {};
const attackerIsNpc = final_blow_char_id === 0; const attackerIsNpc = final_blow_char_id === 0;
const victimAffiliation = const victimAffiliation = victim_alliance_ticker || victim_corp_ticker || null;
victim_alliance_ticker || victim_corp_ticker || null;
const attackerAffiliation = attackerIsNpc const attackerAffiliation = attackerIsNpc
? '' ? ''
: final_blow_alliance_ticker || final_blow_corp_ticker || ''; : final_blow_alliance_ticker || final_blow_corp_ticker || '';
@@ -69,7 +65,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null; total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago'; const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
// Victim images, now also pulling victimShipUrl // Build victim images
const { const {
victimPortraitUrl, victimPortraitUrl,
victimCorpLogoUrl, victimCorpLogoUrl,
@@ -81,7 +77,8 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
victim_corp_id, victim_corp_id,
victim_alliance_id, victim_alliance_id,
}); });
// Attacker images
// Build attacker images
const { const {
attackerPortraitUrl, attackerPortraitUrl,
attackerCorpLogoUrl, attackerCorpLogoUrl,
@@ -92,7 +89,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
final_blow_alliance_id, final_blow_alliance_id,
}); });
// Primary corp/alliance logo for victim // Primary image for victim
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } = const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
getPrimaryLogoAndTooltip( getPrimaryLogoAndTooltip(
victimAllianceLogoUrl, victimAllianceLogoUrl,
@@ -102,7 +99,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
'Victim' 'Victim'
); );
// Primary image for attacker => NPC => ship, else corp/alliance // Primary image for attacker
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
getAttackerPrimaryImageAndTooltip( getAttackerPrimaryImageAndTooltip(
attackerIsNpc, attackerIsNpc,
@@ -119,34 +116,15 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
<div <div
className={clsx( className={clsx(
classes.killRowContainer, classes.killRowContainer,
'h-18 w-full justify-between items-start text-sm py-[4px]' 'w-full text-sm py-1 px-2',
'flex flex-col sm:flex-row'
)} )}
> >
{/* ---------------- Victim Side ---------------- */} <div className="w-full flex flex-col sm:flex-row items-start gap-2">
<div className="flex items-start gap-1 min-w-0 h-full"> {/* Victim Section */}
{victimShipUrl && ( <div className="flex items-start gap-1 min-w-0">
<div className="relative shrink-0 w-14 h-14 overflow-hidden"> {victimShipUrl && (
<a <div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={victimShipUrl}
alt="VictimShip"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
)}
{victimPrimaryImageUrl && (
<WdTooltipWrapper
content={victimPrimaryTooltip}
position={TooltipPosition.top}
>
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a <a
href={zkillLink('kill', killmail_id)} href={zkillLink('kill', killmail_id)}
target="_blank" target="_blank"
@@ -154,109 +132,136 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
className="block w-full h-full" className="block w-full h-full"
> >
<img <img
src={victimPrimaryImageUrl} src={victimShipUrl}
alt="VictimPrimaryLogo" alt="VictimShip"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')} className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
/> />
</a> </a>
</div> </div>
</WdTooltipWrapper> )}
)} {victimPrimaryImageUrl && (
<WdTooltipWrapper
<VictimRowSubInfo content={victimPrimaryTooltip}
victimCharName={victim_char_name} position={TooltipPosition.top}
victimCharacterId={victim_char_id} >
victimPortraitUrl={victimPortraitUrl} <div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
/> <a
href={zkillLink('kill', killmail_id)}
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden"> target="_blank"
<div className="truncate"> rel="noopener noreferrer"
<span className="font-semibold">{victim_char_name}</span> className="block w-full h-full"
{victimAffiliation && ( >
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span> <img
)} src={victimPrimaryImageUrl}
</div> alt="VictimPrimaryLogo"
<div className="truncate text-stone-300"> className={clsx(
{victim_ship_name} classes.killRowImage,
{killValueFormatted && ( 'w-full h-full object-contain'
<> )}
<span className="ml-1 text-stone-400">/</span> />
<span className="ml-1 text-green-400"> </a>
{killValueFormatted} </div>
</span> </WdTooltipWrapper>
</> )}
)} <VictimRowSubInfo
</div> victimCharName={victim_char_name}
<div className="truncate text-stone-400"> victimCharacterId={victim_char_id}
{!onlyOneSystem && systemName && <span>{systemName}</span>} victimPortraitUrl={victimPortraitUrl}
</div> />
</div> <div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
</div>
<div className="flex items-start gap-1 min-w-0 h-full">
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
{!attackerIsNpc && (
<div className="truncate font-semibold"> <div className="truncate font-semibold">
{final_blow_char_name} {victim_char_name}
{attackerAffiliation && ( {victimAffiliation && (
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span> <span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
)} )}
</div> </div>
)} <div className="truncate text-stone-300">
{!attackerIsNpc && final_blow_ship_name && ( {victim_ship_name}
<div className="truncate text-stone-300">{final_blow_ship_name}</div> {killValueFormatted && (
)} <>
<div className="truncate text-red-400">{killTimeAgo}</div> <span className="ml-1 text-stone-400">/</span>
</div> <span className="ml-1 text-green-400">{killValueFormatted}</span>
</>
{!attackerIsNpc && attackerPortraitUrl && final_blow_char_id && final_blow_char_id > 0 && ( )}
<div className="relative shrink-0 w-14 h-14 overflow-hidden"> </div>
<a <div className="truncate text-stone-400">
href={zkillLink('character', final_blow_char_id)} {!onlyOneSystem && systemName && <span>{systemName}</span>}
target="_blank" </div>
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={attackerPortraitUrl}
alt="AttackerPortrait"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div> </div>
)} </div>
{/* Attacker Section */}
{attackerPrimaryImageUrl && ( <div className="flex items-start gap-1 min-w-0 sm:ml-auto">
<WdTooltipWrapper <div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
content={attackerPrimaryTooltip} {!attackerIsNpc && (
position={TooltipPosition.top} <div className="truncate font-semibold">
> {final_blow_char_name}
<div className="relative shrink-0 w-14 h-14 overflow-hidden"> {attackerAffiliation && (
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
)}
</div>
)}
{!attackerIsNpc && final_blow_ship_name && (
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
)}
<div className="truncate text-red-400">{killTimeAgo}</div>
</div>
{(!attackerIsNpc && attackerPortraitUrl && final_blow_char_id > 0) && (
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
<a <a
href={zkillLink('kill', killmail_id)} href={zkillLink('character', final_blow_char_id)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block w-full h-full" className="block w-full h-full"
> >
<img <img
src={attackerPrimaryImageUrl} src={attackerPortraitUrl}
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'} alt="AttackerPortrait"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')} className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
/> />
{attackerSubscript && (
<span
className={clsx(
attackerSubscript.cssClass,
classes.attackerCountLabel
)}
>
{attackerSubscript.label}
</span>
)}
</a> </a>
</div> </div>
</WdTooltipWrapper> )}
)} {attackerPrimaryImageUrl && (
<WdTooltipWrapper
content={attackerPrimaryTooltip}
position={TooltipPosition.top}
>
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={attackerPrimaryImageUrl}
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
/>
{attackerSubscript && (
<span
className={clsx(
attackerSubscript.cssClass,
classes.attackerCountLabel
)}
>
{attackerSubscript.label}
</span>
)}
</a>
</div>
</WdTooltipWrapper>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,20 +1,22 @@
import React from 'react'; import React, { useRef } from 'react';
import clsx from 'clsx';
import { import {
LayoutEventBlocker, LayoutEventBlocker,
WdCheckbox, WdResponsiveCheckbox,
WdImgButton, WdImgButton,
TooltipPosition, TooltipPosition,
SystemView, WdDisplayMode,
} from '@/hooks/Mapper/components/ui-kit'; } from '@/hooks/Mapper/components/ui-kit';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings'; import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import { PrimeIcons } from 'primereact/api'; import { PrimeIcons } from 'primereact/api';
import { useElementWidth } from '@/hooks/Mapper/components/hooks';
interface KillsWidgetHeaderProps { interface KillsHeaderProps {
systemId?: string; systemId?: string;
onOpenSettings: () => void; onOpenSettings: () => void;
} }
export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpenSettings }) => { export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
const [settings, setSettings] = useKillsWidgetSettings(); const [settings, setSettings] = useKillsWidgetSettings();
const { showAll } = settings; const { showAll } = settings;
@@ -22,35 +24,48 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
setSettings(prev => ({ ...prev, showAll: !prev.showAll })); setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
}; };
const headerRef = useRef<HTMLDivElement>(null);
const headerWidth = useElementWidth(headerRef) || 300;
const reservedWidth = 100;
const availableWidth = Math.max(headerWidth - reservedWidth, 0);
let displayMode: WdDisplayMode = "full";
if (availableWidth >= 60) {
displayMode = "full";
} else {
displayMode = "abbr";
}
return ( return (
<div className="flex justify-between items-center text-xs w-full"> <div className="flex w-full items-center text-xs" ref={headerRef}>
<div className="flex items-center gap-1"> <div className="flex-shrink-0 select-none mr-2">
<div className="text-stone-400"> Kills{systemId && !showAll && ' in '}
Kills </div>
{systemId && !showAll && ' in '} <div className="flex-grow overflow-hidden">
</div> <LayoutEventBlocker className="flex items-center gap-2 justify-end">
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />} <div className="flex items-center gap-2">
<WdResponsiveCheckbox
tooltipContent="Show all systems"
size="xs"
labelFull="Show all systems"
labelAbbreviated="All"
value={showAll}
onChange={onToggleShowAllVisible}
classNameLabel={clsx("whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300")}
displayMode={displayMode}
/>
<WdImgButton
className={PrimeIcons.SLIDERS_H}
onClick={onOpenSettings}
tooltip={{
content: 'Open Kills Settings',
position: TooltipPosition.left,
}}
/>
</div>
</LayoutEventBlocker>
</div> </div>
<LayoutEventBlocker className="flex gap-2 items-center">
<WdCheckbox
size="xs"
labelSide="left"
label="Show all systems"
value={showAll}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={onToggleShowAllVisible}
/>
<WdImgButton
className={PrimeIcons.SLIDERS_H}
onClick={onOpenSettings}
tooltip={{
content: 'Open Kills Settings',
position: TooltipPosition.left,
}}
/>
</LayoutEventBlocker>
</div> </div>
); );
}; };

View File

@@ -1,4 +1,3 @@
// VictimSubRowInfo.tsx
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { zkillLink } from '../helpers'; import { zkillLink } from '../helpers';
@@ -15,13 +14,13 @@ export const VictimRowSubInfo: React.FC<VictimRowSubInfoProps> = ({
victimPortraitUrl, victimPortraitUrl,
victimCharName, victimCharName,
}) => { }) => {
if (!victimPortraitUrl || victimCharacterId === null || victimCharacterId <= 0) { if (!victimPortraitUrl || !victimCharacterId || victimCharacterId <= 0) {
return null; return null;
} }
return ( return (
<div className="flex items-start gap-1 h-14"> <div className="flex items-start gap-1">
<div className="relative shrink-0 w-14 h-14 overflow-hidden"> <div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
<a <a
href={zkillLink('character', victimCharacterId)} href={zkillLink('character', victimCharacterId)}
target="_blank" target="_blank"

View File

@@ -0,0 +1,72 @@
import React from 'react';
import clsx from 'clsx';
import { WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
/**
* Display modes for the responsive checkbox.
*
* - "full": show the full label (e.g. "Show offline" or "Show ship name")
* - "abbr": show the abbreviated label (e.g. "Offline" or "Ship name")
* - "checkbox": show only the checkbox (no text)
* - "hide": do not render the checkbox at all
*/
export type WdDisplayMode = "full" | "abbr" | "checkbox" | "hide";
export interface WdResponsiveCheckboxProps {
tooltipContent: string;
size: 'xs' | 'normal' | 'm';
labelFull: string;
labelAbbreviated: string;
value: boolean;
onChange: () => void;
classNameLabel?: string;
containerClassName?: string;
labelSide?: 'left' | 'right';
displayMode: WdDisplayMode;
}
export const WdResponsiveCheckbox: React.FC<WdResponsiveCheckboxProps> = ({
tooltipContent,
size,
labelFull,
labelAbbreviated,
value,
onChange,
classNameLabel,
containerClassName,
labelSide = 'left',
displayMode,
}) => {
if (displayMode === "hide") {
return null;
}
const label =
displayMode === "full"
? labelFull
: displayMode === "abbr"
? labelAbbreviated
: displayMode === "checkbox"
? ""
: labelFull;
const checkbox = (
<div className={clsx("min-w-0", containerClassName)}>
<WdCheckbox
size={size}
labelSide={labelSide}
label={label}
value={value}
classNameLabel={classNameLabel}
onChange={onChange}
/>
</div>
);
return tooltipContent ? (
<WdTooltipWrapper content={tooltipContent}>{checkbox}</WdTooltipWrapper>
) : (
checkbox
);
};

View File

@@ -0,0 +1 @@
export * from './WdResponsiveCheckbox';

View File

@@ -11,3 +11,5 @@ export * from './WdImgButton';
export * from './WdTooltip'; export * from './WdTooltip';
export * from './WdCheckbox'; export * from './WdCheckbox';
export * from './TimeAgo'; export * from './TimeAgo';
export * from './WdTooltipWrapper';
export * from './WdResponsiveCheckBox';