mirror of
https://github.com/kunkunsh/kunkun-ext-neohtop.git
synced 2025-04-11 17:29:45 +00:00
extracting ProcessIcon component
This commit is contained in:
parent
d464775c4a
commit
73ada67432
@ -1,147 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
faThumbtack,
|
|
||||||
faInfoCircle,
|
|
||||||
faXmark,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import Fa from "svelte-fa";
|
|
||||||
import type { Process } from "$lib/types";
|
|
||||||
|
|
||||||
export let process: Process;
|
|
||||||
export let isPinned: boolean;
|
|
||||||
export let onTogglePin: (command: string) => void;
|
|
||||||
export let onShowDetails: (process: Process) => void;
|
|
||||||
export let onKillProcess: (process: Process) => void;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button
|
|
||||||
class="btn-action pin-btn"
|
|
||||||
class:pinned={isPinned}
|
|
||||||
on:click={() => onTogglePin(process.command)}
|
|
||||||
title={isPinned ? "Unpin" : "Pin"}
|
|
||||||
>
|
|
||||||
<Fa icon={faThumbtack} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn-action info-btn"
|
|
||||||
on:click={() => onShowDetails(process)}
|
|
||||||
title="Show Details"
|
|
||||||
>
|
|
||||||
<Fa icon={faInfoCircle} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn-action kill-btn"
|
|
||||||
on:click={() => onKillProcess(process)}
|
|
||||||
title="End Process"
|
|
||||||
>
|
|
||||||
<Fa icon={faXmark} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 6px;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0.1;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:hover::before {
|
|
||||||
opacity: 0.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn {
|
|
||||||
color: var(--sapphire);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn::before {
|
|
||||||
background: var(--sapphire);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn.pinned {
|
|
||||||
color: var(--blue);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn.pinned::before {
|
|
||||||
background: var(--blue);
|
|
||||||
opacity: 0.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn {
|
|
||||||
color: var(--lavender);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn::before {
|
|
||||||
background: var(--lavender);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-btn {
|
|
||||||
color: var(--red);
|
|
||||||
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-btn:hover {
|
|
||||||
color: var(--base);
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-btn:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn.pinned:active {
|
|
||||||
transform: rotate(45deg) translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:disabled:hover {
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:disabled::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -2,6 +2,7 @@
|
|||||||
import * as SimpleIcons from "simple-icons";
|
import * as SimpleIcons from "simple-icons";
|
||||||
|
|
||||||
export let processName: string;
|
export let processName: string;
|
||||||
|
export let size: number = 16;
|
||||||
|
|
||||||
function handleImageError(event: Event) {
|
function handleImageError(event: Event) {
|
||||||
const img = event.target as HTMLImageElement;
|
const img = event.target as HTMLImageElement;
|
||||||
@ -20,11 +21,20 @@
|
|||||||
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
|
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
|
||||||
|
|
||||||
if (companyIcon) {
|
if (companyIcon) {
|
||||||
return createSvgDataUrl(companyIcon);
|
// Use theme color instead of brand color
|
||||||
|
const color = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--text")
|
||||||
|
.trim();
|
||||||
|
const svg =
|
||||||
|
typeof companyIcon === "object" && "svg" in companyIcon
|
||||||
|
? companyIcon.svg
|
||||||
|
: "";
|
||||||
|
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
|
||||||
|
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to process name icon
|
// If no company icon found, fall back to original implementation
|
||||||
const cleanName = name
|
const cleanName = name
|
||||||
.replace(/\.(app|exe)$/i, "")
|
.replace(/\.(app|exe)$/i, "")
|
||||||
.replace(/[-_./\\]/g, " ")
|
.replace(/[-_./\\]/g, " ")
|
||||||
@ -35,18 +45,22 @@
|
|||||||
const formattedName =
|
const formattedName =
|
||||||
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
||||||
const iconKey = `si${formattedName}`;
|
const iconKey = `si${formattedName}`;
|
||||||
const simpleIcon =
|
let simpleIcon = SimpleIcons[iconKey as keyof typeof SimpleIcons];
|
||||||
SimpleIcons[iconKey as keyof typeof SimpleIcons] ||
|
|
||||||
SimpleIcons.siGhostery;
|
|
||||||
|
|
||||||
return createSvgDataUrl(simpleIcon);
|
// Default icon if no match found
|
||||||
}
|
if (!simpleIcon) {
|
||||||
|
simpleIcon = SimpleIcons.siGhostery;
|
||||||
|
}
|
||||||
|
|
||||||
function createSvgDataUrl(icon: any): string {
|
// Use theme color instead of brand color
|
||||||
const color = getComputedStyle(document.documentElement)
|
const color = getComputedStyle(document.documentElement)
|
||||||
.getPropertyValue("--text")
|
.getPropertyValue("--text")
|
||||||
.trim();
|
.trim();
|
||||||
const svg = typeof icon === "object" && "svg" in icon ? icon.svg : "";
|
|
||||||
|
const svg =
|
||||||
|
typeof simpleIcon === "object" && "svg" in simpleIcon
|
||||||
|
? simpleIcon.svg
|
||||||
|
: "";
|
||||||
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
|
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
|
||||||
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
|
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
|
||||||
}
|
}
|
||||||
@ -56,8 +70,8 @@
|
|||||||
class="process-icon"
|
class="process-icon"
|
||||||
src={getIconForProcess(processName)}
|
src={getIconForProcess(processName)}
|
||||||
alt=""
|
alt=""
|
||||||
height="16"
|
height={size}
|
||||||
width="16"
|
width={size}
|
||||||
on:error={handleImageError}
|
on:error={handleImageError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Process, Column } from "$lib/types";
|
|
||||||
import ProcessCell from "./cells/ProcessCell.svelte";
|
|
||||||
import ActionButtons from "./ActionButtons.svelte";
|
|
||||||
|
|
||||||
export let process: Process;
|
|
||||||
export let columns: Column[];
|
|
||||||
export let isPinned: boolean;
|
|
||||||
export let isHighUsage: boolean;
|
|
||||||
export let onTogglePin: (command: string) => void;
|
|
||||||
export let onShowDetails: (process: Process) => void;
|
|
||||||
export let onKillProcess: (process: Process) => void;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<tr class:high-usage={isHighUsage} class:pinned={isPinned}>
|
|
||||||
{#each columns.filter((col) => col.visible) as column}
|
|
||||||
<ProcessCell {process} field={column.id} format={column.format} />
|
|
||||||
{/each}
|
|
||||||
<td class="col-actions">
|
|
||||||
<ActionButtons
|
|
||||||
{process}
|
|
||||||
{isPinned}
|
|
||||||
{onTogglePin}
|
|
||||||
{onShowDetails}
|
|
||||||
{onKillProcess}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
tr:hover {
|
|
||||||
background-color: var(--surface0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.high-usage {
|
|
||||||
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.high-usage:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--red) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.pinned {
|
|
||||||
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.pinned:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-actions {
|
|
||||||
position: sticky;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
background: var(--base);
|
|
||||||
border-left: 1px solid var(--surface0);
|
|
||||||
width: 120px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,7 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
faThumbtack,
|
||||||
|
faInfoCircle,
|
||||||
|
faXmark,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Fa from "svelte-fa";
|
||||||
import type { Process, Column } from "$lib/types";
|
import type { Process, Column } from "$lib/types";
|
||||||
import TableHeader from "./TableHeader.svelte";
|
import ProcessIcon from "./ProcessIcon.svelte";
|
||||||
import ProcessRow from "./ProcessRow.svelte";
|
|
||||||
|
|
||||||
export let processes: Process[];
|
export let processes: Process[];
|
||||||
export let columns: Column[];
|
export let columns: Column[];
|
||||||
@ -13,23 +18,81 @@
|
|||||||
export let onTogglePin: (command: string) => void;
|
export let onTogglePin: (command: string) => void;
|
||||||
export let onShowDetails: (process: Process) => void;
|
export let onShowDetails: (process: Process) => void;
|
||||||
export let onKillProcess: (process: Process) => void;
|
export let onKillProcess: (process: Process) => void;
|
||||||
|
|
||||||
|
function getSortIndicator(field: keyof Process) {
|
||||||
|
if (sortConfig.field !== field) return "↕";
|
||||||
|
return sortConfig.direction === "asc" ? "↑" : "↓";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table>
|
||||||
<TableHeader {columns} {sortConfig} {onToggleSort} />
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{#each columns.filter((col) => col.visible) as column}
|
||||||
|
<th class="sortable" on:click={() => onToggleSort(column.id)}>
|
||||||
|
<div class="th-content">
|
||||||
|
{column.label}
|
||||||
|
<span
|
||||||
|
class="sort-indicator"
|
||||||
|
class:active={sortConfig.field === column.id}
|
||||||
|
>
|
||||||
|
{getSortIndicator(column.id)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each processes as process (process.pid)}
|
{#each processes as process (process.pid)}
|
||||||
<ProcessRow
|
<tr
|
||||||
{process}
|
class:high-usage={process.cpu_usage > 50 ||
|
||||||
{columns}
|
|
||||||
isPinned={pinnedProcesses.has(process.command)}
|
|
||||||
isHighUsage={process.cpu_usage > 50 ||
|
|
||||||
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
|
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
|
||||||
{onTogglePin}
|
class:pinned={pinnedProcesses.has(process.command)}
|
||||||
{onShowDetails}
|
>
|
||||||
{onKillProcess}
|
{#each columns.filter((col) => col.visible) as column}
|
||||||
/>
|
<td class="truncate">
|
||||||
|
{#if column.id === "name"}
|
||||||
|
<div class="name-cell">
|
||||||
|
<ProcessIcon processName={process.name} />
|
||||||
|
<span class="process-name">{process.name}</span>
|
||||||
|
</div>
|
||||||
|
{:else if column.format}
|
||||||
|
{@html column.format(process[column.id])}
|
||||||
|
{:else}
|
||||||
|
{process[column.id]}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
<td class="col-actions">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button
|
||||||
|
class="btn-action pin-btn"
|
||||||
|
class:pinned={pinnedProcesses.has(process.command)}
|
||||||
|
on:click={() => onTogglePin(process.command)}
|
||||||
|
title={pinnedProcesses.has(process.command) ? "Unpin" : "Pin"}
|
||||||
|
>
|
||||||
|
<Fa icon={faThumbtack} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-action info-btn"
|
||||||
|
on:click={() => onShowDetails(process)}
|
||||||
|
title="Show Details"
|
||||||
|
>
|
||||||
|
<Fa icon={faInfoCircle} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-action kill-btn"
|
||||||
|
on:click={() => onKillProcess(process)}
|
||||||
|
title="End Process"
|
||||||
|
>
|
||||||
|
<Fa icon={faXmark} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -83,4 +146,211 @@
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--mantle);
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--subtext0);
|
||||||
|
border-bottom: 1px solid var(--surface0);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-bottom: 1px solid var(--surface0);
|
||||||
|
color: var(--text);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: var(--surface0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncate {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-indicator {
|
||||||
|
color: var(--overlay0);
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-indicator.active {
|
||||||
|
color: var(--blue);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable:hover .sort-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.high-usage {
|
||||||
|
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.high-usage:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--red) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.pinned {
|
||||||
|
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.pinned:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child {
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 120px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-actions {
|
||||||
|
position: sticky;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--base);
|
||||||
|
border-left: 1px solid var(--surface0);
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.1;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:hover::before {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn {
|
||||||
|
color: var(--sapphire);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn::before {
|
||||||
|
background: var(--sapphire);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn.pinned {
|
||||||
|
color: var(--blue);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn.pinned::before {
|
||||||
|
background: var(--blue);
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-btn {
|
||||||
|
color: var(--lavender);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-btn::before {
|
||||||
|
background: var(--lavender);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-btn {
|
||||||
|
color: var(--red);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-btn:hover {
|
||||||
|
color: var(--base);
|
||||||
|
background: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-btn:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:hover {
|
||||||
|
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn.pinned:active {
|
||||||
|
transform: rotate(45deg) translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:disabled:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:disabled::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Process, Column } from "$lib/types";
|
|
||||||
|
|
||||||
export let columns: Column[];
|
|
||||||
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
|
|
||||||
export let onToggleSort: (field: keyof Process) => void;
|
|
||||||
|
|
||||||
function getSortIndicator(field: keyof Process) {
|
|
||||||
if (sortConfig.field !== field) return "↕";
|
|
||||||
return sortConfig.direction === "asc" ? "↑" : "↓";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{#each columns.filter((col) => col.visible) as column}
|
|
||||||
<th
|
|
||||||
class="sortable"
|
|
||||||
data-column={column.id}
|
|
||||||
on:click={() => onToggleSort(column.id)}
|
|
||||||
>
|
|
||||||
<div class="th-content">
|
|
||||||
{column.label}
|
|
||||||
<span
|
|
||||||
class="sort-indicator"
|
|
||||||
class:active={sortConfig.field === column.id}
|
|
||||||
>
|
|
||||||
{getSortIndicator(column.id)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: var(--mantle);
|
|
||||||
text-align: left;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--subtext0);
|
|
||||||
border-bottom: 1px solid var(--surface0);
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
th:last-child {
|
|
||||||
width: 120px;
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-indicator {
|
|
||||||
color: var(--overlay0);
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-indicator.active {
|
|
||||||
color: var(--blue);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable:hover .sort-indicator {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Column-specific widths */
|
|
||||||
th[data-column="name"] {
|
|
||||||
width: 200px;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="pid"] {
|
|
||||||
width: 70px;
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="status"] {
|
|
||||||
width: 90px;
|
|
||||||
min-width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="user"] {
|
|
||||||
width: 100px;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="cpu_usage"] {
|
|
||||||
width: 80px;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="memory_usage"],
|
|
||||||
th[data-column="virtual_memory"] {
|
|
||||||
width: 90px;
|
|
||||||
min-width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="disk_usage"] {
|
|
||||||
width: 100px;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="command"],
|
|
||||||
th[data-column="environ"] {
|
|
||||||
width: 200px;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="start_time"] {
|
|
||||||
width: 150px;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-column="run_time"] {
|
|
||||||
width: 100px;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,289 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Process } from "$lib/types";
|
|
||||||
import ProcessIcon from "../ProcessIcon.svelte";
|
|
||||||
|
|
||||||
export let process: Process;
|
|
||||||
export let field: keyof Process;
|
|
||||||
export let format: ((value: any) => string) | undefined = undefined;
|
|
||||||
|
|
||||||
function formatValue(value: any): string | undefined {
|
|
||||||
if (format) {
|
|
||||||
return format(value);
|
|
||||||
}
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<td class="truncate" data-column={field}>
|
|
||||||
{#if field === "name"}
|
|
||||||
<div class="name-cell">
|
|
||||||
<ProcessIcon processName={process.name} />
|
|
||||||
<span class="process-name">{process.name}</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
{formatValue(process[field])}
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.table-container {
|
|
||||||
flex: 1;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
/* Scrollbar styles */
|
|
||||||
scrollbar-width: thin; /* Firefox */
|
|
||||||
scrollbar-color: var(--surface2) var(--mantle); /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Webkit scrollbar styles (Chrome, Safari, Edge) */
|
|
||||||
.table-container::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container::-webkit-scrollbar-track {
|
|
||||||
background: var(--mantle);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--surface2);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--surface1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container::-webkit-scrollbar-corner {
|
|
||||||
background: var(--mantle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When both scrollbars are present, add some padding to prevent overlap */
|
|
||||||
.table-container::-webkit-scrollbar-corner {
|
|
||||||
background-color: var(--mantle);
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: max-content;
|
|
||||||
min-width: 100%;
|
|
||||||
table-layout: fixed;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: var(--mantle);
|
|
||||||
text-align: left;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--subtext0);
|
|
||||||
border-bottom: 1px solid var(--surface0);
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-bottom: 1px solid var(--surface0);
|
|
||||||
color: var(--text);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover {
|
|
||||||
background-color: var(--surface0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.truncate {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-indicator {
|
|
||||||
color: var(--overlay0);
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-indicator.active {
|
|
||||||
color: var(--blue);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable:hover .sort-indicator {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.high-usage {
|
|
||||||
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.high-usage:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--red) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.pinned {
|
|
||||||
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.pinned:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
th:last-child {
|
|
||||||
width: 120px;
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:last-child {
|
|
||||||
width: 120px;
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 120px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-actions {
|
|
||||||
position: sticky;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
background: var(--base);
|
|
||||||
border-left: 1px solid var(--surface0);
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 6px;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0.1;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:hover::before {
|
|
||||||
opacity: 0.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn {
|
|
||||||
color: var(--sapphire);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn::before {
|
|
||||||
background: var(--sapphire);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn.pinned {
|
|
||||||
color: var(--blue);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn.pinned::before {
|
|
||||||
background: var(--blue);
|
|
||||||
opacity: 0.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn {
|
|
||||||
color: var(--lavender);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn::before {
|
|
||||||
background: var(--lavender);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-btn {
|
|
||||||
color: var(--red);
|
|
||||||
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-btn:hover {
|
|
||||||
color: var(--base);
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kill-btn:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-btn.pinned:active {
|
|
||||||
transform: rotate(45deg) translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:disabled:hover {
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:disabled::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.process-icon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
x
Reference in New Issue
Block a user