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 './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 { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import clsx from 'clsx';
import { LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { UserPermission } from '@/hooks/Mapper/types/permissions';
import { LocalCharactersList } from './components/LocalCharactersList';
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
export const LocalCharacters = () => {
const {
@@ -18,14 +15,12 @@ export const LocalCharacters = () => {
} = useMapRootState();
const [settings, setSettings] = useLocalCharacterWidgetSettings();
const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const restrictOfflineShowing = useMapGetOption("restrict_offline_showing");
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
const showOffline = useMemo(
() => !restrictOfflineShowing || isAdminOrManager,
[isAdminOrManager, restrictOfflineShowing],
[isAdminOrManager, restrictOfflineShowing]
);
const sorted = useMemo(() => {
@@ -42,7 +37,6 @@ export const LocalCharacters = () => {
if (!showOffline || !settings.showOffline) {
return filtered.filter(c => c.online);
}
return filtered;
}, [
characters,
@@ -58,64 +52,18 @@ export const LocalCharacters = () => {
const isNotSelectedSystem = 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);
return (
<Widget
label={
<div className="flex w-full items-center" ref={ref}>
<div className="flex-shrink-0 select-none mr-2">
Local{showList ? ` [${sorted.length}]` : ''}
</div>
<div className="flex-grow overflow-hidden">
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
{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>
<LocalCharactersHeader
sortedCount={sorted.length}
showList={showList}
showOffline={showOffline}
settings={settings}
setSettings={setSettings}
/>
}
>
{isNotSelectedSystem && (
@@ -123,19 +71,17 @@ export const LocalCharacters = () => {
System is not selected
</div>
)}
{isNobodyHere && !isNotSelectedSystem && (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
Nobody here
</div>
)}
{showList && (
<LocalCharactersList
items={sorted}
itemSize={settings.compact ? 26 : 41}
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>

View File

@@ -1,4 +1,3 @@
// .VirtualScroller {
// height: 100% !important;
// }
.VirtualScroller {
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();
const [systemId] = selectedSystems || [];
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
const systemNameMap = useMemo(() => {
@@ -41,7 +40,9 @@ export const SystemKills: React.FC = () => {
const filteredKills = useMemo(() => {
if (!settings.whOnly || !visible) return kills;
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) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
@@ -55,60 +56,62 @@ export const SystemKills: React.FC = () => {
<div className="flex flex-col flex-1 min-h-0">
<Widget
label={
<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />
<KillsHeader
systemId={systemId}
onOpenSettings={() => setSettingsDialogVisible(true)}
/>
}
>
{!isSubscriptionActive && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
Kills available with &#39;Active&#39; map subscription only (contact map administrators)
</div>
)}
{isSubscriptionActive && (
<>
{isNothingSelected && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
<div className="relative h-full">
{!isSubscriptionActive ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
Kills available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
) : isNothingSelected ? (
<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)
</div>
)}
{!isNothingSelected && showLoading && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
</span>
</div>
) : showLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
Loading Kills...
</div>
)}
{!isNothingSelected && !showLoading && error && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-red-400 text-sm">
</span>
</div>
) : error ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-red-400 text-sm">
{error}
</div>
)}
{!isNothingSelected &&
!showLoading &&
!error &&
(!filteredKills || filteredKills.length === 0) && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
No kills found
</div>
)}
{!isNothingSelected && !showLoading && !error && (
<div className="flex-1 flex flex-col overflow-y-auto">
<SystemKillsContent
key={settings.compact ? 'compact' : 'normal'}
kills={filteredKills}
systemNameMap={systemNameMap}
compact={settings.compact}
onlyOneSystem={!visible}
/>
</div>
)}
</>
)}
</span>
</div>
) : !filteredKills || filteredKills.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
No kills found
</span>
</div>
) : (
<div className="h-full overflow-y-auto">
<SystemKillsContent
key={settings.compact ? 'compact' : 'normal'}
kills={filteredKills}
systemNameMap={systemNameMap}
compact={settings.compact}
onlyOneSystem={!visible}
/>
</div>
)}
</div>
</Widget>
</div>
<KillsSettingsDialog visible={settingsDialogVisible} setVisible={setSettingsDialogVisible} />
<KillsSettingsDialog
visible={settingsDialogVisible}
setVisible={setSettingsDialogVisible}
/>
</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 {
killmail_id = 0,
// Victim
// Victim data
victim_char_name = '',
victim_alliance_ticker = '',
victim_corp_ticker = '',
@@ -41,8 +40,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
victim_ship_type_id = 0,
victim_corp_name = '',
victim_alliance_name = '',
// Attacker
// Attacker data
final_blow_char_id = 0,
final_blow_char_name = '',
final_blow_alliance_ticker = '',
@@ -53,14 +51,12 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
final_blow_alliance_id = 0,
final_blow_ship_name = '',
final_blow_ship_type_id = 0,
total_value = 0,
kill_time = '',
} = killDetails || {};
const attackerIsNpc = final_blow_char_id === 0;
const victimAffiliation =
victim_alliance_ticker || victim_corp_ticker || null;
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker || null;
const attackerAffiliation = attackerIsNpc
? ''
: 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;
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
// Victim images, now also pulling victimShipUrl
// Build victim images
const {
victimPortraitUrl,
victimCorpLogoUrl,
@@ -81,7 +77,8 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
victim_corp_id,
victim_alliance_id,
});
// Attacker images
// Build attacker images
const {
attackerPortraitUrl,
attackerCorpLogoUrl,
@@ -92,7 +89,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
final_blow_alliance_id,
});
// Primary corp/alliance logo for victim
// Primary image for victim
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
@@ -102,7 +99,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
'Victim'
);
// Primary image for attacker => NPC => ship, else corp/alliance
// Primary image for attacker
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
@@ -119,34 +116,15 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
<div
className={clsx(
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="flex items-start gap-1 min-w-0 h-full">
{victimShipUrl && (
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a
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">
<div className="w-full flex flex-col sm:flex-row items-start gap-2">
{/* Victim Section */}
<div className="flex items-start gap-1 min-w-0">
{victimShipUrl && (
<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"
@@ -154,109 +132,136 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
className="block w-full h-full"
>
<img
src={victimPrimaryImageUrl}
alt="VictimPrimaryLogo"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
src={victimShipUrl}
alt="VictimShip"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
/>
</a>
</div>
</WdTooltipWrapper>
)}
<VictimRowSubInfo
victimCharName={victim_char_name}
victimCharacterId={victim_char_id}
victimPortraitUrl={victimPortraitUrl}
/>
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
<div className="truncate">
<span className="font-semibold">{victim_char_name}</span>
{victimAffiliation && (
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
)}
</div>
<div className="truncate text-stone-300">
{victim_ship_name}
{killValueFormatted && (
<>
<span className="ml-1 text-stone-400">/</span>
<span className="ml-1 text-green-400">
{killValueFormatted}
</span>
</>
)}
</div>
<div className="truncate text-stone-400">
{!onlyOneSystem && systemName && <span>{systemName}</span>}
</div>
</div>
</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 && (
)}
{victimPrimaryImageUrl && (
<WdTooltipWrapper
content={victimPrimaryTooltip}
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={victimPrimaryImageUrl}
alt="VictimPrimaryLogo"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
/>
</a>
</div>
</WdTooltipWrapper>
)}
<VictimRowSubInfo
victimCharName={victim_char_name}
victimCharacterId={victim_char_id}
victimPortraitUrl={victimPortraitUrl}
/>
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
<div className="truncate font-semibold">
{final_blow_char_name}
{attackerAffiliation && (
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
{victim_char_name}
{victimAffiliation && (
<span className="ml-1 text-stone-400">/ {victimAffiliation}</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 && final_blow_char_id > 0 && (
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a
href={zkillLink('character', final_blow_char_id)}
target="_blank"
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 className="truncate text-stone-300">
{victim_ship_name}
{killValueFormatted && (
<>
<span className="ml-1 text-stone-400">/</span>
<span className="ml-1 text-green-400">{killValueFormatted}</span>
</>
)}
</div>
<div className="truncate text-stone-400">
{!onlyOneSystem && systemName && <span>{systemName}</span>}
</div>
</div>
)}
{attackerPrimaryImageUrl && (
<WdTooltipWrapper
content={attackerPrimaryTooltip}
position={TooltipPosition.top}
>
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
</div>
{/* Attacker Section */}
<div className="flex items-start gap-1 min-w-0 sm:ml-auto">
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
{!attackerIsNpc && (
<div className="truncate font-semibold">
{final_blow_char_name}
{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
href={zkillLink('kill', killmail_id)}
href={zkillLink('character', final_blow_char_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')}
src={attackerPortraitUrl}
alt="AttackerPortrait"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
/>
{attackerSubscript && (
<span
className={clsx(
attackerSubscript.cssClass,
classes.attackerCountLabel
)}
>
{attackerSubscript.label}
</span>
)}
</a>
</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>
);

View File

@@ -1,20 +1,22 @@
import React from 'react';
import React, { useRef } from 'react';
import clsx from 'clsx';
import {
LayoutEventBlocker,
WdCheckbox,
WdResponsiveCheckbox,
WdImgButton,
TooltipPosition,
SystemView,
WdDisplayMode,
} from '@/hooks/Mapper/components/ui-kit';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import { PrimeIcons } from 'primereact/api';
import { useElementWidth } from '@/hooks/Mapper/components/hooks';
interface KillsWidgetHeaderProps {
interface KillsHeaderProps {
systemId?: string;
onOpenSettings: () => void;
}
export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpenSettings }) => {
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
const [settings, setSettings] = useKillsWidgetSettings();
const { showAll } = settings;
@@ -22,35 +24,48 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
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 (
<div className="flex justify-between items-center text-xs w-full">
<div className="flex items-center gap-1">
<div className="text-stone-400">
Kills
{systemId && !showAll && ' in '}
</div>
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
<div className="flex w-full items-center text-xs" ref={headerRef}>
<div className="flex-shrink-0 select-none mr-2">
Kills{systemId && !showAll && ' in '}
</div>
<div className="flex-grow overflow-hidden">
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
<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>
<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>
);
};

View File

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