ui refactoring

This commit is contained in:
Abdenasser 2024-11-13 03:16:45 +01:00
parent cb07a3bfcf
commit d464775c4a
21 changed files with 1309 additions and 894 deletions

View File

@ -1,430 +0,0 @@
<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 * as SimpleIcons from "simple-icons";
export let processes: Process[];
export let columns: Column[];
export let systemStats: { memory_total: number } | null;
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
export let pinnedProcesses: Set<string>;
export let onToggleSort: (field: keyof Process) => void;
export let onTogglePin: (command: string) => void;
export let onShowDetails: (process: Process) => void;
export let onKillProcess: (process: Process) => void;
function getSortIndicator(field: keyof Process) {
if (sortConfig.field !== field) return "↕";
return sortConfig.direction === "asc" ? "↑" : "↓";
}
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
img.src = getIconForProcess("default"); // Fallback to default icon
img.onerror = null; // Prevent infinite loop if default icon also fails
}
function getIconForProcess(name: string): string {
// First try with com.company.something pattern
if (name.startsWith("com.")) {
const companyName = name.replace(/^com\.([^.]+)\..*$/, "$1");
const formattedCompanyName =
companyName.charAt(0).toUpperCase() + companyName.slice(1);
const companyIconKey = `si${formattedCompanyName}`;
const companyIcon =
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
if (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)}`;
}
}
// If no company icon found, fall back to original implementation
const cleanName = name
.replace(/\.(app|exe)$/i, "")
.replace(/[-_./\\]/g, " ")
.split(" ")[0]
.trim()
.toLowerCase();
const formattedName =
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
const iconKey = `si${formattedName}`;
let simpleIcon = SimpleIcons[iconKey as keyof typeof SimpleIcons];
// Default icon if no match found
if (!simpleIcon) {
simpleIcon = SimpleIcons.siGhostery;
}
// Use theme color instead of brand color
const color = getComputedStyle(document.documentElement)
.getPropertyValue("--text")
.trim();
const svg =
typeof simpleIcon === "object" && "svg" in simpleIcon
? simpleIcon.svg
: "";
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
}
</script>
<div class="table-container">
<table>
<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>
{#each processes as process (process.pid)}
<tr
class:high-usage={process.cpu_usage > 50 ||
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
class:pinned={pinnedProcesses.has(process.command)}
>
{#each columns.filter((col) => col.visible) as column}
<td class="truncate">
{#if column.id === "name"}
<div class="name-cell">
<img
class="process-icon"
src={getIconForProcess(process.name)}
alt=""
height="16"
width="16"
on:error={handleImageError}
/>
<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}
</tbody>
</table>
</div>
<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>

View File

@ -1,461 +0,0 @@
<script lang="ts">
import Fa from "svelte-fa";
import {
faMicrochip,
faMemory,
faServer,
faNetworkWired,
faHardDrive,
} from "@fortawesome/free-solid-svg-icons";
import type { SystemStats } from "$lib/types";
import {
formatUptime,
formatMemorySize,
formatPercentage,
getUsageClass,
} from "$lib/utils";
export let systemStats: SystemStats | null = null;
$: memoryPercentage = systemStats
? (systemStats.memory_used / systemStats.memory_total) * 100
: 0;
function formatBytes(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
$: diskUsagePercentage = systemStats
? (systemStats.disk_used_bytes / systemStats.disk_total_bytes) * 100
: 0;
</script>
<div class="dashboard-stats">
{#if systemStats}
<div class="stats-layout">
<!-- CPU Panel -->
<div class="stat-panel">
<div class="panel-header">
<Fa icon={faMicrochip} />
<h3>CPU Usage</h3>
<div class="usage-pill">
{formatPercentage(
systemStats.cpu_usage.reduce((a, b) => a + b, 0) /
systemStats.cpu_usage.length,
)}
</div>
</div>
<div class="stats-content cpu-grid">
{#each systemStats.cpu_usage as usage, i}
<div class="stat-item with-progress">
<div class="progress-container">
<span class="label">Core {i}</span>
<div class="bar-container">
<div
class="usage-bar {getUsageClass(usage)}"
style="transform: translateX({usage - 100}%);"
></div>
</div>
<span class="value">{Math.round(usage)}%</span>
</div>
</div>
{/each}
</div>
</div>
<!-- Memory Panel -->
<div class="stat-panel">
<div class="panel-header">
<Fa icon={faMemory} />
<h3>Memory</h3>
<div class="usage-pill">{formatPercentage(memoryPercentage)}</div>
</div>
<div class="stats-content">
<div class="stat-item with-progress">
<div class="memory-progress-container">
<span class="label">Memory usage</span>
<div class="bar-container">
<div
class="usage-bar {getUsageClass(memoryPercentage)}"
style="transform: translateX({memoryPercentage - 100}%);"
></div>
</div>
<span class="value">{formatPercentage(memoryPercentage)}</span>
</div>
</div>
<div class="stat-item">
<span>Total</span>
<span>{formatMemorySize(systemStats.memory_total)}</span>
</div>
<div class="stat-item">
<span>Used</span>
<span>{formatMemorySize(systemStats.memory_used)}</span>
</div>
<div class="stat-item">
<span>Free</span>
<span>{formatMemorySize(systemStats.memory_free)}</span>
</div>
</div>
</div>
<!-- Storage Panel -->
<div class="stat-panel">
<div class="panel-header">
<Fa icon={faHardDrive} />
<h3>Storage</h3>
<div class="usage-pill">
{formatPercentage(diskUsagePercentage)}
</div>
</div>
<div class="stats-content">
<div class="stat-item">
<span>Total</span>
<span>{formatBytes(systemStats.disk_total_bytes)}</span>
</div>
<div class="stat-item">
<span>Used</span>
<span>{formatBytes(systemStats.disk_used_bytes)}</span>
</div>
<div class="stat-item">
<span>Free</span>
<span>{formatBytes(systemStats.disk_free_bytes)}</span>
</div>
</div>
</div>
<!-- System Panel -->
<div class="stat-panel">
<div class="panel-header">
<Fa icon={faServer} />
<h3>System</h3>
</div>
<div class="system-grid">
<div class="stat-item">
<span>Uptime</span>
<span>{formatUptime(systemStats.uptime)}</span>
</div>
<div class="stat-item">
<span>1m Load</span>
<span>{systemStats.load_avg[0].toFixed(2)}</span>
</div>
<div class="stat-item">
<span>5m Load</span>
<span>{systemStats.load_avg[1].toFixed(2)}</span>
</div>
<div class="stat-item">
<span>15m Load</span>
<span>{systemStats.load_avg[2].toFixed(2)}</span>
</div>
</div>
</div>
<!-- Network Panel -->
<div class="stat-panel">
<div class="panel-header">
<Fa icon={faNetworkWired} />
<h3>Network I/O</h3>
</div>
<div class="network-stats">
<div class="stat-item">
<span>↓ Receiving</span>
<span>{formatBytes(systemStats.network_rx_bytes)}/s</span>
</div>
<div class="stat-item">
<span>↑ Sending</span>
<span>{formatBytes(systemStats.network_tx_bytes)}/s</span>
</div>
</div>
</div>
</div>
{/if}
</div>
<style>
.dashboard-stats {
padding: 0.5rem;
overflow-x: auto;
}
.stats-layout {
display: flex;
gap: 0.75rem;
width: 100%;
}
.stat-panel {
flex: 1;
min-width: 0;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
/* box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); */
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface0);
flex-shrink: 0;
}
.panel-header h3 {
font-size: 0.8rem;
font-weight: 600;
margin: 0;
color: var(--text);
}
.panel-header :global(svg) {
color: var(--blue);
width: 0.8rem;
height: 0.8rem;
}
.usage-pill {
margin-left: auto;
background: var(--surface0);
padding: 0.15rem 0.5rem;
border-radius: 1rem;
font-size: 0.7rem;
font-weight: 500;
}
.cpu-layout {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
overflow-y: auto;
padding-right: 0.25rem;
}
.cpu-row {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.cpu-info {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
}
.bar-container {
height: 8px;
background: var(--surface0);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.bar-container.main-bar {
height: 6px;
margin-bottom: 0.5rem;
}
.usage-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
transition: transform 0.3s ease;
transform-origin: left;
}
.usage-bar.low {
background: var(--blue);
}
.usage-bar.medium {
background: var(--yellow);
}
.usage-bar.high {
background: var(--peach);
}
.usage-bar.critical {
background: var(--red);
}
.memory-grid,
.disk-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
align-content: start;
}
.system-grid,
.network-grid {
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
.load-stats {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.network-grid {
display: grid;
grid-template-columns: 1fr;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
line-height: 1.2;
margin: 0;
padding: 0;
}
.stat-item span:first-child {
color: var(--subtext0);
}
.stat-item span:last-child {
color: var(--text);
font-weight: 500;
}
.full-width {
grid-column: 1 / -1;
}
.network-stats {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stat-item.with-progress {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.stat-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
gap: 1rem;
}
.progress-container {
width: 100%;
display: grid;
grid-template-columns: 4rem 1fr 3rem;
align-items: center;
gap: 0.75rem;
font-size: 0.7rem;
}
.memory-progress-container {
width: 100%;
display: grid;
grid-template-columns: 5rem 1fr 2.5rem;
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
}
.memory-progress-container .label {
color: var(--subtext0);
white-space: nowrap;
}
.memory-progress-container .value {
color: var(--text);
text-align: right;
white-space: nowrap;
}
.progress-container .label {
color: var(--subtext0);
white-space: nowrap;
}
.progress-container .value {
color: var(--text);
text-align: right;
white-space: nowrap;
}
.bar-container {
height: 8px;
background: var(--surface0);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.usage-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
transition: transform 0.3s ease;
transform-origin: left;
}
.cpu-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem 2rem;
height: auto;
overflow: visible;
}
.progress-container {
width: 100%;
display: grid;
grid-template-columns: 2.5rem 1fr 2.5rem;
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
}
/* Make CPU panel take more space */
.stat-panel:first-child {
flex: 2;
}
/* Customize flex distribution for each panel */
.stat-panel:nth-child(1) {
flex: 2.5; /* CPU: more space for 2 columns */
}
.stat-panel:nth-child(2) {
flex: 2; /* Memory: more space for details */
}
.stat-panel:nth-child(3),
.stat-panel:nth-child(4),
.stat-panel:nth-child(5) {
flex: 0.8; /* Storage, System, and Network: less space */
min-width: 125px;
}
</style>

View File

@ -0,0 +1,147 @@
<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>

View File

@ -0,0 +1,70 @@
<script lang="ts">
import * as SimpleIcons from "simple-icons";
export let processName: string;
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
img.src = getIconForProcess("default");
img.onerror = null;
}
function getIconForProcess(name: string): string {
// First try with com.company.something pattern
if (name.startsWith("com.")) {
const companyName = name.replace(/^com\.([^.]+)\..*$/, "$1");
const formattedCompanyName =
companyName.charAt(0).toUpperCase() + companyName.slice(1);
const companyIconKey = `si${formattedCompanyName}`;
const companyIcon =
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
if (companyIcon) {
return createSvgDataUrl(companyIcon);
}
}
// Fall back to process name icon
const cleanName = name
.replace(/\.(app|exe)$/i, "")
.replace(/[-_./\\]/g, " ")
.split(" ")[0]
.trim()
.toLowerCase();
const formattedName =
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
const iconKey = `si${formattedName}`;
const simpleIcon =
SimpleIcons[iconKey as keyof typeof SimpleIcons] ||
SimpleIcons.siGhostery;
return createSvgDataUrl(simpleIcon);
}
function createSvgDataUrl(icon: any): string {
const color = getComputedStyle(document.documentElement)
.getPropertyValue("--text")
.trim();
const svg = typeof icon === "object" && "svg" in icon ? icon.svg : "";
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
}
</script>
<img
class="process-icon"
src={getIconForProcess(processName)}
alt=""
height="16"
width="16"
on:error={handleImageError}
/>
<style>
.process-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
</style>

View File

@ -0,0 +1,60 @@
<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>

View File

@ -0,0 +1,86 @@
<script lang="ts">
import type { Process, Column } from "$lib/types";
import TableHeader from "./TableHeader.svelte";
import ProcessRow from "./ProcessRow.svelte";
export let processes: Process[];
export let columns: Column[];
export let systemStats: { memory_total: number } | null;
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
export let pinnedProcesses: Set<string>;
export let onToggleSort: (field: keyof Process) => void;
export let onTogglePin: (command: string) => void;
export let onShowDetails: (process: Process) => void;
export let onKillProcess: (process: Process) => void;
</script>
<div class="table-container">
<table>
<TableHeader {columns} {sortConfig} {onToggleSort} />
<tbody>
{#each processes as process (process.pid)}
<ProcessRow
{process}
{columns}
isPinned={pinnedProcesses.has(process.command)}
isHighUsage={process.cpu_usage > 50 ||
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
{onTogglePin}
{onShowDetails}
{onKillProcess}
/>
{/each}
</tbody>
</table>
</div>
<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;
}
</style>

View File

@ -0,0 +1,136 @@
<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>

View File

@ -0,0 +1,289 @@
<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>

View File

@ -0,0 +1,6 @@
export { default as ProcessTable } from "./ProcessTable.svelte";
export { default as ProcessRow } from "./ProcessRow.svelte";
export { default as TableHeader } from "./TableHeader.svelte";
export { default as ProcessCell } from "./cells/ProcessCell.svelte";
export { default as ActionButtons } from "./ActionButtons.svelte";
export { default as ProcessIcon } from "./ProcessIcon.svelte";

View File

@ -0,0 +1,60 @@
<script lang="ts">
import { faMicrochip } from "@fortawesome/free-solid-svg-icons";
import PanelHeader from "./PanelHeader.svelte";
import ProgressBar from "./ProgressBar.svelte";
import { formatPercentage } from "$lib/utils";
export let cpuUsage: number[];
$: averageUsage = formatPercentage(
cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length,
);
</script>
<div class="stat-panel">
<PanelHeader icon={faMicrochip} title="CPU Usage" usageValue={averageUsage} />
<div class="stats-content cpu-grid">
{#each cpuUsage as usage, i}
<div class="stat-item with-progress">
<ProgressBar
label={`Core ${i}`}
value={usage}
labelWidth="2.5rem"
valueWidth="2.5rem"
/>
</div>
{/each}
</div>
</div>
<style>
.stat-panel {
flex: 2.5;
min-width: 0;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cpu-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem 2rem;
height: auto;
overflow: visible;
}
.stat-item.with-progress {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
</style>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { faMemory } from "@fortawesome/free-solid-svg-icons";
import PanelHeader from "./PanelHeader.svelte";
import ProgressBar from "./ProgressBar.svelte";
import StatItem from "./StatItem.svelte";
import { formatMemorySize, formatPercentage } from "$lib/utils";
export let memoryTotal: number;
export let memoryUsed: number;
export let memoryFree: number;
$: memoryPercentage = (memoryUsed / memoryTotal) * 100;
</script>
<div class="stat-panel">
<PanelHeader
icon={faMemory}
title="Memory"
usageValue={formatPercentage(memoryPercentage)}
/>
<div class="stats-content">
<div class="stat-item with-progress">
<ProgressBar
label="Memory usage"
value={memoryPercentage}
labelWidth="5rem"
valueWidth="2.5rem"
/>
</div>
<StatItem label="Total" value={formatMemorySize(memoryTotal)} />
<StatItem label="Used" value={formatMemorySize(memoryUsed)} />
<StatItem label="Free" value={formatMemorySize(memoryFree)} />
</div>
</div>
<style>
.stat-panel {
flex: 2;
min-width: 0;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stat-item.with-progress {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
</style>

View File

@ -0,0 +1,47 @@
<script lang="ts">
import { faNetworkWired } from "@fortawesome/free-solid-svg-icons";
import PanelHeader from "./PanelHeader.svelte";
import StatItem from "./StatItem.svelte";
export let networkRxBytes: number;
export let networkTxBytes: number;
function formatBytes(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
}
</script>
<div class="stat-panel">
<PanelHeader icon={faNetworkWired} title="Network I/O" />
<div class="network-stats">
<StatItem label="↓ Receiving" value={formatBytes(networkRxBytes)} />
<StatItem label="↑ Sending" value={formatBytes(networkTxBytes)} />
</div>
</div>
<style>
.stat-panel {
flex: 0.8;
min-width: 125px;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.network-stats {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
</style>

View File

@ -0,0 +1,50 @@
<script lang="ts">
import Fa from "svelte-fa";
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
export let icon: IconDefinition;
export let title: string;
export let usageValue: string | null = null;
</script>
<div class="panel-header">
<Fa {icon} />
<h3>{title}</h3>
{#if usageValue}
<div class="usage-pill">{usageValue}</div>
{/if}
</div>
<style>
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface0);
flex-shrink: 0;
}
.panel-header h3 {
font-size: 0.8rem;
font-weight: 600;
margin: 0;
color: var(--text);
}
.panel-header :global(svg) {
color: var(--blue);
width: 0.8rem;
height: 0.8rem;
}
.usage-pill {
margin-left: auto;
background: var(--surface0);
padding: 0.15rem 0.5rem;
border-radius: 1rem;
font-size: 0.7rem;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,80 @@
<script lang="ts">
export let label: string;
export let value: number;
export let labelWidth = "2.5rem";
export let valueWidth = "2.5rem";
function getUsageClass(usage: number): string {
if (usage > 90) return "critical";
if (usage > 75) return "high";
if (usage > 50) return "medium";
return "low";
}
</script>
<div
class="progress-container"
style="--label-width: {labelWidth}; --value-width: {valueWidth}"
>
<span class="label">{label}</span>
<div class="bar-container">
<div
class="usage-bar {getUsageClass(value)}"
style="transform: translateX({value - 100}%);"
/>
</div>
<span class="value">{Math.round(value)}%</span>
</div>
<style>
.progress-container {
width: 100%;
display: grid;
grid-template-columns: var(--label-width) 1fr var(--value-width);
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
}
.label {
color: var(--subtext0);
white-space: nowrap;
}
.value {
color: var(--text);
text-align: right;
white-space: nowrap;
}
.bar-container {
height: 8px;
background: var(--surface0);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.usage-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
transition: transform 0.3s ease;
transform-origin: left;
}
.usage-bar.low {
background: var(--blue);
}
.usage-bar.medium {
background: var(--yellow);
}
.usage-bar.high {
background: var(--peach);
}
.usage-bar.critical {
background: var(--red);
}
</style>

View File

@ -0,0 +1,30 @@
<script lang="ts">
export let label: string;
export let value: string;
</script>
<div class="stat-item">
<span>{label}</span>
<span>{value}</span>
</div>
<style>
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
line-height: 1.2;
margin: 0;
padding: 0;
}
.stat-item span:first-child {
color: var(--subtext0);
}
.stat-item span:last-child {
color: var(--text);
font-weight: 500;
}
</style>

View File

@ -0,0 +1,34 @@
<script lang="ts">
export let title: string;
export let flex = 1;
</script>
<div class="stat-panel" style="--flex: {flex}">
<h3 class="panel-title">{title}</h3>
<div class="panel-content">
<slot />
</div>
</div>
<style>
.stat-panel {
flex: var(--flex);
min-width: 125px;
background: var(--mantle);
border-radius: 8px;
padding: 12px;
}
.panel-title {
font-size: 12px;
font-weight: 600;
color: var(--subtext0);
margin: 0 0 12px 0;
}
.panel-content {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>

View File

@ -0,0 +1,50 @@
<script lang="ts">
import type { SystemStats } from "$lib/types";
import CpuPanel from "./CpuPanel.svelte";
import MemoryPanel from "./MemoryPanel.svelte";
import StoragePanel from "./StoragePanel.svelte";
import SystemPanel from "./SystemPanel.svelte";
import NetworkPanel from "./NetworkPanel.svelte";
export let systemStats: SystemStats | null = null;
</script>
<div class="dashboard-stats">
{#if systemStats}
<div class="stats-layout">
<CpuPanel cpuUsage={systemStats.cpu_usage} />
<MemoryPanel
memoryTotal={systemStats.memory_total}
memoryUsed={systemStats.memory_used}
memoryFree={systemStats.memory_free}
/>
<StoragePanel
diskTotalBytes={systemStats.disk_total_bytes}
diskUsedBytes={systemStats.disk_used_bytes}
diskFreeBytes={systemStats.disk_free_bytes}
/>
<SystemPanel uptime={systemStats.uptime} loadAvg={systemStats.load_avg} />
<NetworkPanel
networkRxBytes={systemStats.network_rx_bytes}
networkTxBytes={systemStats.network_tx_bytes}
/>
</div>
{/if}
</div>
<style>
.dashboard-stats {
padding: 0.5rem;
overflow-x: auto;
}
.stats-layout {
display: flex;
gap: 0.75rem;
width: 100%;
}
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { faHardDrive } from "@fortawesome/free-solid-svg-icons";
import PanelHeader from "./PanelHeader.svelte";
import StatItem from "./StatItem.svelte";
import { formatPercentage } from "$lib/utils";
export let diskTotalBytes: number;
export let diskUsedBytes: number;
export let diskFreeBytes: number;
function formatBytes(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
$: diskUsagePercentage = (diskUsedBytes / diskTotalBytes) * 100;
</script>
<div class="stat-panel">
<PanelHeader
icon={faHardDrive}
title="Storage"
usageValue={formatPercentage(diskUsagePercentage)}
/>
<div class="stats-content">
<StatItem label="Total" value={formatBytes(diskTotalBytes)} />
<StatItem label="Used" value={formatBytes(diskUsedBytes)} />
<StatItem label="Free" value={formatBytes(diskFreeBytes)} />
</div>
</div>
<style>
.stat-panel {
flex: 0.8;
min-width: 125px;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
</style>

View File

@ -0,0 +1,38 @@
<script lang="ts">
import { faServer } from "@fortawesome/free-solid-svg-icons";
import PanelHeader from "./PanelHeader.svelte";
import StatItem from "./StatItem.svelte";
import { formatUptime } from "$lib/utils";
export let uptime: number;
export let loadAvg: [number, number, number];
</script>
<div class="stat-panel">
<PanelHeader icon={faServer} title="System" />
<div class="system-grid">
<StatItem label="Uptime" value={formatUptime(uptime)} />
<StatItem label="1m Load" value={loadAvg[0].toFixed(2)} />
<StatItem label="5m Load" value={loadAvg[1].toFixed(2)} />
<StatItem label="15m Load" value={loadAvg[2].toFixed(2)} />
</div>
</div>
<style>
.stat-panel {
flex: 0.8;
min-width: 125px;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.system-grid {
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
</style>

View File

@ -0,0 +1,9 @@
export { default as StatsBar } from "./StatsBar.svelte";
export { default as CpuPanel } from "./CpuPanel.svelte";
export { default as MemoryPanel } from "./MemoryPanel.svelte";
export { default as StoragePanel } from "./StoragePanel.svelte";
export { default as SystemPanel } from "./SystemPanel.svelte";
export { default as NetworkPanel } from "./NetworkPanel.svelte";
export { default as PanelHeader } from "./PanelHeader.svelte";
export { default as ProgressBar } from "./ProgressBar.svelte";
export { default as StatItem } from "./StatItem.svelte";

View File

@ -1,9 +1,9 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { onMount, onDestroy } from "svelte";
import StatsBar from "$lib/components/StatsBar.svelte";
import { StatsBar } from "$lib/components/stats";
import ToolBar from "$lib/components/ToolBar.svelte";
import ProcessTable from "$lib/components/ProcessTable.svelte";
import ProcessTable from "$lib/components/process/ProcessTable.svelte";
import ProcessDetailsModal from "$lib/components/ProcessDetailsModal.svelte";
import KillProcessModal from "$lib/components/KillProcessModal.svelte";
import { formatMemorySize, formatStatus } from "$lib/utils";
@ -35,7 +35,7 @@
id: "status",
label: "Status",
visible: true,
format: formatStatus,
format: (v) => v,
},
{ id: "user", label: "User", visible: true },
{