css refactor
This commit is contained in:
@@ -8,14 +8,10 @@
|
||||
-->
|
||||
<script>
|
||||
/** @type {Object} log - The log entry object */
|
||||
export let log;
|
||||
/** @type {boolean} showSource - Whether to show the source tag */
|
||||
export let showSource = true;
|
||||
let { log, showSource = true } = $props();
|
||||
|
||||
// [DEF:formatTime:Function]
|
||||
/** @PURPOSE Format ISO timestamp to HH:MM:SS */
|
||||
$: formattedTime = formatTime(log.timestamp);
|
||||
|
||||
function formatTime(timestamp) {
|
||||
if (!timestamp) return "";
|
||||
const date = new Date(timestamp);
|
||||
@@ -28,180 +24,77 @@
|
||||
}
|
||||
// [/DEF:formatTime:Function]
|
||||
|
||||
$: levelClass = getLevelClass(log.level);
|
||||
let formattedTime = $derived(formatTime(log.timestamp));
|
||||
|
||||
function getLevelClass(level) {
|
||||
switch (level?.toUpperCase()) {
|
||||
case "DEBUG":
|
||||
return "level-debug";
|
||||
case "INFO":
|
||||
return "level-info";
|
||||
case "WARNING":
|
||||
return "level-warning";
|
||||
case "ERROR":
|
||||
return "level-error";
|
||||
default:
|
||||
return "level-info";
|
||||
}
|
||||
}
|
||||
const levelStyles = {
|
||||
DEBUG: "text-log-debug bg-log-debug/15",
|
||||
INFO: "text-log-info bg-log-info/10",
|
||||
WARNING: "text-log-warning bg-log-warning/10",
|
||||
ERROR: "text-log-error bg-log-error/10",
|
||||
};
|
||||
|
||||
$: sourceClass = getSourceClass(log.source);
|
||||
const sourceStyles = {
|
||||
plugin: "bg-source-plugin/10 text-source-plugin",
|
||||
"superset-api": "bg-source-api/10 text-source-api",
|
||||
superset_api: "bg-source-api/10 text-source-api",
|
||||
git: "bg-source-git/10 text-source-git",
|
||||
system: "bg-source-system/10 text-source-system",
|
||||
};
|
||||
|
||||
function getSourceClass(source) {
|
||||
if (!source) return "source-default";
|
||||
return `source-${source.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
||||
}
|
||||
let levelClass = $derived(
|
||||
levelStyles[log.level?.toUpperCase()] || levelStyles.INFO,
|
||||
);
|
||||
let sourceClass = $derived(
|
||||
sourceStyles[log.source?.toLowerCase()] ||
|
||||
"bg-log-debug/15 text-terminal-text-subtle",
|
||||
);
|
||||
|
||||
$: hasProgress = log.metadata?.progress !== undefined;
|
||||
$: progressPercent = log.metadata?.progress || 0;
|
||||
let hasProgress = $derived(log.metadata?.progress !== undefined);
|
||||
let progressPercent = $derived(log.metadata?.progress || 0);
|
||||
</script>
|
||||
|
||||
<div class="log-row">
|
||||
<div
|
||||
class="py-2 px-3 border-b border-terminal-surface/60 transition-colors hover:bg-terminal-surface/50"
|
||||
>
|
||||
<!-- Meta line: time + level + source -->
|
||||
<div class="log-meta">
|
||||
<span class="log-time">{formattedTime}</span>
|
||||
<span class="log-level {levelClass}">{log.level || "INFO"}</span>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-mono text-[0.6875rem] text-terminal-text-muted shrink-0"
|
||||
>{formattedTime}</span
|
||||
>
|
||||
<span
|
||||
class="font-mono font-semibold uppercase text-[0.625rem] px-1.5 py-px rounded-sm tracking-wider shrink-0 {levelClass}"
|
||||
>{log.level || "INFO"}</span
|
||||
>
|
||||
{#if showSource && log.source}
|
||||
<span class="log-source {sourceClass}">{log.source}</span>
|
||||
<span
|
||||
class="text-[0.625rem] px-1.5 py-px rounded-sm shrink-0 {sourceClass}"
|
||||
>{log.source}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="log-message">{log.message}</div>
|
||||
<div
|
||||
class="font-mono text-[0.8125rem] leading-relaxed text-terminal-text break-words whitespace-pre-wrap"
|
||||
>
|
||||
{log.message}
|
||||
</div>
|
||||
|
||||
<!-- Progress bar (if applicable) -->
|
||||
{#if hasProgress}
|
||||
<div class="progress-container">
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" style="width: {progressPercent}%"></div>
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<div
|
||||
class="flex-1 h-1.5 bg-terminal-surface rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-primary to-purple-500 rounded-full transition-[width] duration-300 ease-out"
|
||||
style="width: {progressPercent}%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text">{progressPercent.toFixed(0)}%</span>
|
||||
<span class="font-mono text-[0.625rem] text-terminal-text-subtle shrink-0"
|
||||
>{progressPercent.toFixed(0)}%</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:LogEntryRow:Component] -->
|
||||
|
||||
<style>
|
||||
.log-row {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid rgba(30, 41, 59, 0.6);
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.log-row:hover {
|
||||
background-color: rgba(30, 41, 59, 0.5);
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-size: 0.6875rem;
|
||||
color: #475569;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.625rem;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 0.1875rem;
|
||||
letter-spacing: 0.03em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.level-debug {
|
||||
color: #64748b;
|
||||
background-color: rgba(100, 116, 139, 0.15);
|
||||
}
|
||||
|
||||
.level-info {
|
||||
color: #38bdf8;
|
||||
background-color: rgba(56, 189, 248, 0.1);
|
||||
}
|
||||
|
||||
.level-warning {
|
||||
color: #fbbf24;
|
||||
background-color: rgba(251, 191, 36, 0.1);
|
||||
}
|
||||
|
||||
.level-error {
|
||||
color: #f87171;
|
||||
background-color: rgba(248, 113, 113, 0.1);
|
||||
}
|
||||
|
||||
.log-source {
|
||||
font-size: 0.625rem;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 0.1875rem;
|
||||
background-color: rgba(100, 116, 139, 0.15);
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.source-plugin {
|
||||
background-color: rgba(34, 197, 94, 0.1);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.source-superset-api,
|
||||
.source-superset_api {
|
||||
background-color: rgba(168, 85, 247, 0.1);
|
||||
color: #c084fc;
|
||||
}
|
||||
|
||||
.source-git {
|
||||
background-color: rgba(249, 115, 22, 0.1);
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
.source-system {
|
||||
background-color: rgba(56, 189, 248, 0.1);
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: #cbd5e1;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
flex: 1;
|
||||
height: 0.375rem;
|
||||
background-color: #1e293b;
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
||||
border-radius: 9999px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-size: 0.625rem;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,14 +10,12 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
/** @type {string[]} availableSources - List of available source options */
|
||||
export let availableSources = [];
|
||||
/** @type {string} selectedLevel - Currently selected log level filter */
|
||||
export let selectedLevel = "";
|
||||
/** @type {string} selectedSource - Currently selected source filter */
|
||||
export let selectedSource = "";
|
||||
/** @type {string} searchText - Current search text */
|
||||
export let searchText = "";
|
||||
let {
|
||||
availableSources = [],
|
||||
selectedLevel = $bindable(""),
|
||||
selectedSource = $bindable(""),
|
||||
searchText = $bindable(""),
|
||||
} = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -63,13 +61,18 @@
|
||||
dispatch("filter-change", { level: "", source: "", search: "" });
|
||||
}
|
||||
|
||||
$: hasActiveFilters = selectedLevel || selectedSource || searchText;
|
||||
let hasActiveFilters = $derived(
|
||||
selectedLevel || selectedSource || searchText,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-controls">
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-3 py-2 bg-terminal-bg border-b border-terminal-surface"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
<select
|
||||
class="filter-select"
|
||||
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded px-2 py-[0.3125rem] text-xs cursor-pointer shrink-0 appearance-none pr-6 focus:outline-none focus:border-primary-ring"
|
||||
style="background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.375rem center;"
|
||||
value={selectedLevel}
|
||||
on:change={handleLevelChange}
|
||||
aria-label="Filter by level"
|
||||
@@ -80,7 +83,8 @@
|
||||
</select>
|
||||
|
||||
<select
|
||||
class="filter-select"
|
||||
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded px-2 py-[0.3125rem] text-xs cursor-pointer shrink-0 appearance-none pr-6 focus:outline-none focus:border-primary-ring"
|
||||
style="background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.375rem center;"
|
||||
value={selectedSource}
|
||||
on:change={handleSourceChange}
|
||||
aria-label="Filter by source"
|
||||
@@ -91,9 +95,9 @@
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="search-wrapper">
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<svg
|
||||
class="search-icon"
|
||||
class="absolute left-2 top-1/2 -translate-y-1/2 text-terminal-text-muted pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -107,7 +111,7 @@
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
class="w-full bg-terminal-surface text-terminal-text-bright border border-terminal-border rounded py-[0.3125rem] px-2 pl-7 text-xs placeholder:text-terminal-text-muted focus:outline-none focus:border-primary-ring"
|
||||
placeholder="Search..."
|
||||
value={searchText}
|
||||
on:input={handleSearchChange}
|
||||
@@ -118,7 +122,7 @@
|
||||
|
||||
{#if hasActiveFilters}
|
||||
<button
|
||||
class="clear-btn"
|
||||
class="flex items-center justify-center p-[0.3125rem] bg-transparent border border-terminal-border rounded text-terminal-text-subtle shrink-0 cursor-pointer transition-all hover:text-log-error hover:border-log-error hover:bg-log-error/10"
|
||||
on:click={clearFilters}
|
||||
aria-label="Clear filters"
|
||||
>
|
||||
@@ -137,98 +141,3 @@
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:LogFilterBar:Component] -->
|
||||
|
||||
<style>
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #0f172a;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
background-color: #1e293b;
|
||||
color: #94a3b8;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.3125rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.375rem center;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #475569;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background-color: #1e293b;
|
||||
color: #e2e8f0;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.3125rem 0.5rem 0.3125rem 1.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.3125rem;
|
||||
background: none;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.25rem;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
color: #f87171;
|
||||
border-color: #f87171;
|
||||
background-color: rgba(248, 113, 113, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,25 +12,21 @@
|
||||
@UX_STATE: AutoScroll -> Automatically scrolls to bottom on new logs
|
||||
-->
|
||||
<script>
|
||||
import { createEventDispatcher, onMount, afterUpdate } from "svelte";
|
||||
import { createEventDispatcher, onMount, tick } from "svelte";
|
||||
import LogFilterBar from "./LogFilterBar.svelte";
|
||||
import LogEntryRow from "./LogEntryRow.svelte";
|
||||
|
||||
/**
|
||||
* @PURPOSE Component properties and state.
|
||||
* @PRE logs is an array of LogEntry objects.
|
||||
*/
|
||||
export let logs = [];
|
||||
export let autoScroll = true;
|
||||
let { logs = [], autoScroll = $bindable(true) } = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let scrollContainer;
|
||||
let selectedSource = "all";
|
||||
let selectedLevel = "all";
|
||||
let searchText = "";
|
||||
let selectedSource = $state("all");
|
||||
let selectedLevel = $state("all");
|
||||
let searchText = $state("");
|
||||
|
||||
// Filtered logs based on current filters
|
||||
$: filteredLogs = filterLogs(logs, selectedLevel, selectedSource, searchText);
|
||||
let filteredLogs = $derived(
|
||||
filterLogs(logs, selectedLevel, selectedSource, searchText),
|
||||
);
|
||||
|
||||
function filterLogs(allLogs, level, source, search) {
|
||||
return allLogs.filter((log) => {
|
||||
@@ -52,17 +48,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Extract unique sources from logs
|
||||
$: availableSources = [...new Set(logs.map((l) => l.source).filter(Boolean))];
|
||||
let availableSources = $derived([
|
||||
...new Set(logs.map((l) => l.source).filter(Boolean)),
|
||||
]);
|
||||
|
||||
function handleFilterChange(event) {
|
||||
const { source, level, search } = event.detail;
|
||||
selectedSource = source || "all";
|
||||
selectedLevel = level || "all";
|
||||
searchText = search || "";
|
||||
console.log(
|
||||
`[TaskLogPanel][Action] Filter: level=${selectedLevel}, source=${selectedSource}, search=${searchText}`,
|
||||
);
|
||||
dispatch("filterChange", { source, level });
|
||||
}
|
||||
|
||||
@@ -77,8 +71,11 @@
|
||||
if (autoScroll) scrollToBottom();
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
scrollToBottom();
|
||||
// Use $effect instead of afterUpdate for runes mode
|
||||
$effect(() => {
|
||||
// Track filteredLogs length to trigger scroll
|
||||
filteredLogs.length;
|
||||
tick().then(scrollToBottom);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
@@ -86,14 +83,19 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="log-panel">
|
||||
<div class="flex flex-col h-full bg-terminal-bg overflow-hidden">
|
||||
<!-- Filter Bar -->
|
||||
<LogFilterBar {availableSources} on:filter-change={handleFilterChange} />
|
||||
|
||||
<!-- Log List -->
|
||||
<div bind:this={scrollContainer} class="log-list">
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
class="flex-1 overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
{#if filteredLogs.length === 0}
|
||||
<div class="empty-logs">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-12 px-4 text-terminal-border gap-3"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
@@ -109,7 +111,9 @@
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
<span>No logs available</span>
|
||||
<span class="text-[0.8125rem] text-terminal-text-muted"
|
||||
>No logs available</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
{#each filteredLogs as log}
|
||||
@@ -119,129 +123,27 @@
|
||||
</div>
|
||||
|
||||
<!-- Footer Stats -->
|
||||
<div class="log-footer">
|
||||
<span class="log-count">
|
||||
<div
|
||||
class="flex items-center justify-between py-1.5 px-3 border-t border-terminal-surface bg-terminal-bg"
|
||||
>
|
||||
<span class="font-mono text-[0.6875rem] text-terminal-text-muted">
|
||||
{filteredLogs.length}{filteredLogs.length !== logs.length
|
||||
? ` / ${logs.length}`
|
||||
: ""} entries
|
||||
</span>
|
||||
<button
|
||||
class="autoscroll-btn"
|
||||
class:active={autoScroll}
|
||||
class="flex items-center gap-1.5 bg-transparent border-none text-terminal-text-muted text-[0.6875rem] cursor-pointer py-px px-1.5 rounded transition-all hover:bg-terminal-surface hover:text-terminal-text-subtle
|
||||
{autoScroll ? 'text-terminal-accent' : ''}"
|
||||
on:click={toggleAutoScroll}
|
||||
aria-label="Toggle auto-scroll"
|
||||
>
|
||||
{#if autoScroll}
|
||||
<span class="pulse-dot"></span>
|
||||
<span
|
||||
class="inline-block w-[5px] h-[5px] rounded-full bg-terminal-accent animate-pulse"
|
||||
></span>
|
||||
{/if}
|
||||
Auto-scroll {autoScroll ? "on" : "off"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [/DEF:TaskLogPanel:Component] -->
|
||||
|
||||
<style>
|
||||
.log-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #0f172a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.log-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.log-list::-webkit-scrollbar-track {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.log-list::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #334155;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-logs span {
|
||||
font-size: 0.8125rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.log-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-top: 1px solid #1e293b;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
.log-count {
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-size: 0.6875rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.autoscroll-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #475569;
|
||||
font-size: 0.6875rem;
|
||||
cursor: pointer;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.autoscroll-btn:hover {
|
||||
background-color: #1e293b;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.autoscroll-btn.active {
|
||||
color: #22d3ee;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: #22d3ee;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user