mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
* fix: avoid content shift on home page load and in the script editor logs pane The tutorial banner rendered by default and was removed once an API round-trip resolved that it should not show, jumping everything below it up by 58px on every home page load. It now caches the last resolved state in localStorage and paints that first, so the first frame already matches what the sync concludes; a device with nothing cached stays hidden until the sync answers. The logs header spinner was an unsized lucide icon (24px) where the settled state renders a 12px Timer, so the row grew 7px while a job was queued and shrank back when it started, shoving the log body down and up again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the tutorial banner hidden when dismissed mid-sync The banner is interactive while the initial tutorial-progress request is still in flight, so a dismiss or a skip can land before the sync resolves. The continuation then overwrote the user's choice and brought the banner back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: pin the result placeholder row height across the spinner swap Sizing the spinner to the font size still left it 6px short of the text-sm line box it replaces, so the row contracted instead of growing. Pin the height on the container so it holds in both states and tracks the root font size. Also assign state before persisting it, and collapse the duplicated rationale above the banner cache. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop the test panel splitpanes resting one header too tall The panes carried `!max-h-[calc(100%-{...}px)]`, but the arbitrary value is built by string interpolation so Tailwind never emitted a rule for it: the class was inert and the computed max-height was `none`. The panes then took their 100% height, ignoring the header row above them, and overflowed the column by exactly the header. Flex only applied the shrink transiently, so a reflow during a run snapped the whole logs & result region up ~12px and back. min-h-0 lets flex size the panes to the space that is actually left, which is what the clamp was reaching for and is correct for the debug and bottom layouts too, without their hardcoded 83/43/0 pixel guesses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
675 lines
19 KiB
Svelte
675 lines
19 KiB
Svelte
<script lang="ts" module>
|
|
import { untrack } from 'svelte'
|
|
const s3LogPrefixes = [
|
|
'[windmill] Previous logs have been saved to object storage at logs/',
|
|
'[windmill] Previous logs have been saved to disk at logs/',
|
|
'[windmill] No object storage set in instance settings. Previous logs have been saved to disk at logs/'
|
|
]
|
|
|
|
// Only search within first N characters to avoid expensive full-content scans
|
|
const S3_LOG_SEARCH_LIMIT = 2000
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { ClipboardCopy, Download, Expand, Loader2, Timer, Cpu } from 'lucide-svelte'
|
|
import { Button, Drawer, DrawerContent } from './common'
|
|
import { copyToClipboard } from '$lib/utils'
|
|
import { base } from '$lib/base'
|
|
import { withExternalDomain } from '$lib/externalDomain'
|
|
import { downloadViaClient, shouldDownloadViaClient } from '$lib/utils/downloadFile'
|
|
import { appendViewToken } from '$lib/viewToken'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { AnsiUp } from 'ansi_up'
|
|
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
|
|
import { JobService } from '$lib/gen'
|
|
import { WM_LOGS_SKIPPED } from '$lib/consts'
|
|
import { isReplaying } from './recording/offlineReplay.svelte'
|
|
import Tooltip from './Tooltip.svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import QueuePosition from './QueuePosition.svelte'
|
|
|
|
interface Props {
|
|
content: string | undefined
|
|
isLoading: boolean
|
|
duration?: number | undefined
|
|
mem?: number | undefined
|
|
wrapperClass?: string
|
|
jobId?: string | undefined
|
|
tag: string | undefined
|
|
small?: boolean
|
|
drawerOpen?: boolean
|
|
noMaxH?: boolean
|
|
noAutoScroll?: boolean
|
|
download?: boolean
|
|
customEmptyMessage?: string
|
|
tagLabel?: string
|
|
noPadding?: boolean
|
|
navigationId?: string
|
|
/** Called once after we resolve a WM_LOGS_SKIPPED sentinel by fetching the
|
|
* full job. Use this to write the real logs back into the source of truth
|
|
* (e.g. flowStateStore) so subsequent mounts don't refetch. */
|
|
onLogsResolved?: (logs: string) => void
|
|
}
|
|
|
|
let {
|
|
content,
|
|
isLoading,
|
|
duration = undefined,
|
|
mem = undefined,
|
|
wrapperClass = '',
|
|
jobId = undefined,
|
|
tag,
|
|
small = false,
|
|
drawerOpen = $bindable(false),
|
|
noMaxH = false,
|
|
noAutoScroll = false,
|
|
download = true,
|
|
customEmptyMessage = 'No logs are available yet',
|
|
tagLabel = undefined,
|
|
noPadding = false,
|
|
navigationId = undefined,
|
|
onLogsResolved
|
|
}: Props = $props()
|
|
|
|
// @ts-ignore
|
|
const ansi_up = $state(new AnsiUp())
|
|
|
|
ansi_up.use_classes = true
|
|
|
|
let scroll = $state(true)
|
|
let preEl: HTMLElement | null = $state(null)
|
|
|
|
// let downloadStartUrl: string | undefined = undefined
|
|
|
|
let LOG_INC = 10000
|
|
let LOG_LIMIT = $state(LOG_INC)
|
|
|
|
let lastJobId = $state(untrack(() => jobId))
|
|
|
|
let loadedFromObjectStore = $state('')
|
|
|
|
// A replayed job only exists inside the recording: its logs are whatever was
|
|
// captured, so every path that would go ask the backend for more has to stay
|
|
// shut. `jobId` still comes through for the reset-on-change bookkeeping.
|
|
let replaying = $derived(isReplaying())
|
|
|
|
// `content` is the WM_LOGS_SKIPPED sentinel when the job was fetched with
|
|
// no_logs=true. If an older in-memory value accidentally has real bytes
|
|
// concatenated after the sentinel, treat that as skipped too and refetch.
|
|
let isLogsSkipped = $derived((content ?? '').startsWith(WM_LOGS_SKIPPED))
|
|
let resolvedSkippedLogs: string | undefined = $state(undefined)
|
|
let fetchedSkippedJobId: string | undefined = $state(undefined)
|
|
let effectiveContent = $derived(isLogsSkipped ? resolvedSkippedLogs : content)
|
|
let resolvingSkippedLogs = $derived(
|
|
isLogsSkipped && !!jobId && !replaying && resolvedSkippedLogs === undefined
|
|
)
|
|
|
|
$effect(() => {
|
|
if (!isLogsSkipped || !jobId || replaying || fetchedSkippedJobId === jobId) {
|
|
return
|
|
}
|
|
const id = jobId
|
|
fetchedSkippedJobId = id
|
|
untrack(() => {
|
|
JobService.getJob({ workspace: $workspaceStore ?? '', id })
|
|
.then((j) => {
|
|
if (fetchedSkippedJobId === id) {
|
|
const logs = (j as { logs?: string })['logs'] ?? ''
|
|
resolvedSkippedLogs = logs
|
|
onLogsResolved?.(logs)
|
|
}
|
|
})
|
|
.catch((e) => console.error('Failed to resolve skipped logs', e))
|
|
})
|
|
})
|
|
|
|
function findPrefixInfo(
|
|
truncateContent: string
|
|
): { prefixIndex: number; position: number } | undefined {
|
|
// Quick check - [windmill] is rare, so bail early in the common case
|
|
// This is much faster than doing 3 long string indexOf searches
|
|
const windmillPos = truncateContent.indexOf('[windmill]')
|
|
if (windmillPos === -1 || windmillPos >= S3_LOG_SEARCH_LIMIT) {
|
|
return undefined
|
|
}
|
|
|
|
// Found [windmill] marker, now determine which specific prefix matches
|
|
for (let i = 0; i < s3LogPrefixes.length; i++) {
|
|
const prefix = s3LogPrefixes[i]
|
|
if (
|
|
truncateContent.length >= windmillPos + prefix.length &&
|
|
truncateContent.startsWith(prefix, windmillPos)
|
|
) {
|
|
return { prefixIndex: i, position: windmillPos }
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function findStartUrl(
|
|
truncateContent: string,
|
|
prefixInfo: { prefixIndex: number; position: number } | undefined
|
|
) {
|
|
if (!prefixInfo) {
|
|
return undefined
|
|
}
|
|
|
|
const { prefixIndex, position } = prefixInfo
|
|
const prefix = s3LogPrefixes[prefixIndex]
|
|
const startOfPath = position + prefix.length
|
|
const endOfLine = truncateContent.indexOf('\n', startOfPath)
|
|
|
|
return truncateContent.substring(startOfPath, endOfLine === -1 ? undefined : endOfLine)
|
|
}
|
|
|
|
function splitAtWindmillLine(
|
|
content: string,
|
|
prefixInfo: { prefixIndex: number; position: number }
|
|
): { before: string; after: string } {
|
|
const { prefixIndex, position } = prefixInfo
|
|
const prefix = s3LogPrefixes[prefixIndex]
|
|
|
|
// Find line boundaries
|
|
const lineStart = position > 0 && content[position - 1] === '\n' ? position - 1 : position
|
|
const pathStart = position + prefix.length
|
|
const lineEnd = content.indexOf('\n', pathStart)
|
|
|
|
if (lineEnd === -1) {
|
|
// [windmill] line is at the end
|
|
return { before: content.substring(0, lineStart), after: '' }
|
|
}
|
|
|
|
return {
|
|
before: content.substring(0, lineStart),
|
|
after: content.substring(lineEnd)
|
|
}
|
|
}
|
|
|
|
function tooltipText(prefixInfo: { prefixIndex: number; position: number } | undefined) {
|
|
if (prefixInfo == undefined) {
|
|
return 'No path/file detected to download from'
|
|
} else if (prefixInfo.prefixIndex == 0) {
|
|
return 'Download the previous logs from the instance configured object store'
|
|
} else if (prefixInfo.prefixIndex == 1) {
|
|
return 'Attempt to download the logs from disk. Assume there is a shared disk between the workers and the server at /tmp/windmill/logs. Upgrade to EE to use an object store such as S3 instead of a shared volume.'
|
|
} else if (prefixInfo.prefixIndex == 2) {
|
|
return 'Attempt to download the logs from disk. Assume there is a shared disk between the workers and the server at /tmp/windmill/logs. Since you are on EE, you can alternatively use an object store such as S3 configured in the instance settings instead of a shared volume..'
|
|
}
|
|
}
|
|
|
|
function truncateContent(
|
|
jobContent: string | undefined,
|
|
loadedFromObjectStore: string,
|
|
limit: number
|
|
) {
|
|
let content = loadedFromObjectStore + (jobContent ?? '')
|
|
if (content.length > limit) {
|
|
return content.substring(content.length - limit)
|
|
}
|
|
return content
|
|
}
|
|
|
|
export function scrollToBottom() {
|
|
scroll && setTimeout(() => preEl?.scroll({ top: preEl?.scrollHeight, behavior: 'smooth' }), 100)
|
|
}
|
|
|
|
let logViewer: Drawer | undefined = $state()
|
|
|
|
async function getStoreLogs() {
|
|
if (downloadStartUrl) {
|
|
scroll = false
|
|
let res = (await JobService.getLogFileFromStore({
|
|
workspace: $workspaceStore ?? '',
|
|
path: downloadStartUrl
|
|
})) as string
|
|
LOG_LIMIT += Math.min(LOG_INC, res.length)
|
|
loadedFromObjectStore = res + loadedFromObjectStore
|
|
let newC = truncateContent(effectiveContent, loadedFromObjectStore, LOG_LIMIT)
|
|
LOG_LIMIT -= newC.indexOf('\n') + 1
|
|
} else {
|
|
console.error('No file detected to download from')
|
|
}
|
|
}
|
|
|
|
function showMoreTruncate(len: number) {
|
|
scroll = false
|
|
LOG_LIMIT += LOG_INC
|
|
let newC = truncateContent(effectiveContent, loadedFromObjectStore, LOG_LIMIT)
|
|
let newlineIndex = newC.indexOf('\n') + 1
|
|
if (newlineIndex < LOG_INC / 2) {
|
|
LOG_LIMIT -= newlineIndex
|
|
}
|
|
}
|
|
$effect.pre(() => {
|
|
if (jobId !== lastJobId) {
|
|
lastJobId = jobId
|
|
loadedFromObjectStore = ''
|
|
LOG_LIMIT = LOG_INC
|
|
scroll = true
|
|
resolvedSkippedLogs = undefined
|
|
fetchedSkippedJobId = undefined
|
|
}
|
|
})
|
|
let logsApiPath = $derived(appendViewToken(`/w/${$workspaceStore}/jobs_u/get_logs/${jobId}`))
|
|
let downloadHref = $derived(withExternalDomain(`${base}/api${logsApiPath}`))
|
|
let downloadName = $derived(`windmill_logs_${jobId}.txt`)
|
|
let truncatedContent = $derived(
|
|
truncateContent(effectiveContent, loadedFromObjectStore, LOG_LIMIT)
|
|
)
|
|
// The "Show more..." button behind this reads the rest of the logs out of the
|
|
// instance's object store, which a replay has no access to; leaving `prefixInfo`
|
|
// unset renders the `[windmill]` line as the plain log line it is.
|
|
let prefixInfo = $derived(replaying ? undefined : findPrefixInfo(truncatedContent))
|
|
let downloadStartUrl = $derived(findStartUrl(truncatedContent, prefixInfo))
|
|
$effect.pre(() => {
|
|
truncatedContent && scrollToBottom()
|
|
})
|
|
|
|
// When [windmill] line is NOT at start, split into before/after to render button inline
|
|
let splitHtml = $derived.by(() => {
|
|
if (prefixInfo == undefined || prefixInfo.position === 0) {
|
|
return undefined
|
|
}
|
|
const { before, after } = splitAtWindmillLine(truncatedContent, prefixInfo)
|
|
return {
|
|
before: ansi_up.ansi_to_html(before),
|
|
after: ansi_up.ansi_to_html(after)
|
|
}
|
|
})
|
|
|
|
// Only compute html when splitHtml won't be used (avoids wasteful ansi_to_html call)
|
|
let html = $derived.by(() => {
|
|
if (splitHtml) {
|
|
// splitHtml is active - skip expensive computation
|
|
return ''
|
|
}
|
|
if (prefixInfo == undefined) {
|
|
// No [windmill] line - return full content
|
|
return ansi_up.ansi_to_html(truncatedContent)
|
|
}
|
|
// [windmill] at start - strip the line and return the rest
|
|
const { after } = splitAtWindmillLine(truncatedContent, prefixInfo)
|
|
return ansi_up.ansi_to_html(after)
|
|
})
|
|
</script>
|
|
|
|
<Drawer bind:this={logViewer} bind:open={drawerOpen} size="900px">
|
|
<DrawerContent title="Expanded Logs" on:close={logViewer.closeDrawer}>
|
|
{#snippet actions()}
|
|
{#if jobId && download}
|
|
{#if shouldDownloadViaClient()}
|
|
<Button
|
|
on:click={() => downloadViaClient(logsApiPath, downloadName)}
|
|
color="light"
|
|
size="xs"
|
|
startIcon={{
|
|
icon: Download
|
|
}}
|
|
>
|
|
Download
|
|
</Button>
|
|
{:else}
|
|
<Button
|
|
href={downloadHref}
|
|
download={downloadName}
|
|
color="light"
|
|
size="xs"
|
|
startIcon={{
|
|
icon: Download
|
|
}}
|
|
>
|
|
Download
|
|
</Button>
|
|
{/if}
|
|
{/if}
|
|
|
|
<Button
|
|
on:click={() => copyToClipboard(effectiveContent)}
|
|
color="light"
|
|
size="xs"
|
|
startIcon={{
|
|
icon: ClipboardCopy
|
|
}}
|
|
>
|
|
Copy to clipboard
|
|
</Button>
|
|
{/snippet}
|
|
<div>
|
|
<pre
|
|
class="bg-surface-secondary text-primary text-xs w-full p-2 whitespace-pre-wrap border rounded-md"
|
|
>{#if resolvingSkippedLogs}<Loader2
|
|
class="animate-spin"
|
|
size={14}
|
|
/>{:else if effectiveContent}{@const len =
|
|
(effectiveContent?.length ?? 0) +
|
|
(loadedFromObjectStore?.length ?? 0)}{#if splitHtml}{@html splitHtml.before}<button
|
|
onclick={getStoreLogs}
|
|
>Show more... <Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
|
|
>{@html splitHtml.after}{:else if downloadStartUrl}<button onclick={getStoreLogs}
|
|
>Show more... <Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
|
|
><br
|
|
/>{@html html}{:else if len > LOG_LIMIT}(truncated to the last {LOG_LIMIT} characters)...<br
|
|
/><button onclick={() => showMoreTruncate(len)}>Show more..</button><br
|
|
/>{@html html}{:else}{@html html}{/if}{:else if isLoading}Waiting for job to start...{:else}No logs are available yet{/if}</pre
|
|
>
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<div class="w-full h-full {wrapperClass}">
|
|
<div class="w-full h-full relative">
|
|
<div
|
|
class="w-full h-full bg-surface-secondary flex flex-col {noMaxH ? '' : 'max-h-screen'}"
|
|
data-nav-id={navigationId}
|
|
>
|
|
<div
|
|
class="flex gap-2 ml-2 {small ? 'py-1' : 'py-2'} border-b overflow-x-auto overflow-y-hidden"
|
|
>
|
|
{#if isLoading}
|
|
<div class="flex gap-2 items-center">
|
|
<!-- Same size as the Timer icon this swaps out for: an unsized (24px) spinner makes
|
|
this header taller than the settled state and shoves the logs down while queued -->
|
|
<Loader2 size={small ? 10 : 12} class="animate-spin" />
|
|
{#if tag}
|
|
<div class="flex flex-row items-center gap-1">
|
|
<div class="text-secondary text-2xs">{tagLabel ?? 'tag'}: {tag}</div>
|
|
<NoWorkerWithTagWarning {tagLabel} {tag} />
|
|
</div>
|
|
{/if}
|
|
{#if jobId && !replaying}
|
|
<QueuePosition {jobId} />
|
|
{/if}
|
|
</div>
|
|
{:else if duration}
|
|
<span
|
|
class={twMerge(
|
|
'flex items-center gap-1 text-secondary dark:text-gray-400',
|
|
small ? '!text-2xs' : '!text-xs'
|
|
)}
|
|
title="Duration"
|
|
>
|
|
<Timer size={small ? 10 : 12} />
|
|
{duration}ms
|
|
</span>
|
|
{/if}
|
|
{#if mem}
|
|
<span
|
|
class={twMerge(
|
|
'flex items-center gap-1 text-secondary dark:text-gray-400',
|
|
small ? '!text-2xs' : '!text-xs'
|
|
)}
|
|
title="Memory peak"
|
|
>
|
|
<Cpu size={small ? 10 : 12} />
|
|
{(mem / 1024).toPrecision(4)}MB
|
|
</span>
|
|
{/if}
|
|
<div class="flex gap-2 justify-end flex-1">
|
|
{#if jobId && download}
|
|
<div class="flex items-center">
|
|
{#if shouldDownloadViaClient()}
|
|
<button
|
|
class="text-primary pb-0.5"
|
|
onclick={() => downloadViaClient(logsApiPath, downloadName)}
|
|
><Download size="14" />
|
|
</button>
|
|
{:else}
|
|
<a
|
|
class="text-primary pb-0.5"
|
|
target="_blank"
|
|
href={downloadHref}
|
|
download={downloadName}
|
|
><Download size="14" />
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<button onclick={logViewer.openDrawer}><Expand size="12" /></button>
|
|
{#if !noAutoScroll}
|
|
<label
|
|
class="pr-2 text-2xs flex gap-2 font-normal text-primary items-center whitespace-nowrap"
|
|
>
|
|
auto-scroll
|
|
<input class="windmillapp" type="checkbox" bind:checked={scroll} />
|
|
</label>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<pre
|
|
bind:this={preEl}
|
|
class={twMerge(
|
|
'whitespace-pre break-words w-full flex-1 overflow-auto',
|
|
small ? '!text-2xs' : '!text-xs',
|
|
noPadding ? '' : 'p-2'
|
|
)}
|
|
>{#if resolvingSkippedLogs}<span class="flex p-2"
|
|
><Loader2 class="animate-spin" size={14} /></span
|
|
>{:else if effectiveContent}{@const len =
|
|
(effectiveContent?.length ?? 0) +
|
|
(loadedFromObjectStore?.length ?? 0)}{#if splitHtml}<span>{@html splitHtml.before}</span
|
|
><button onclick={getStoreLogs}
|
|
>Show more... <Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
|
|
><span>{@html splitHtml.after}</span>{:else if downloadStartUrl}<button
|
|
onclick={getStoreLogs}
|
|
>Show more... <Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
|
|
><br /><span>{@html html}</span>{:else if len > LOG_LIMIT}<button
|
|
onclick={() => showMoreTruncate(len)}>Show more..</button
|
|
> ({LOG_LIMIT}/{len} chars)<br /><span>{@html html}</span>{:else}<span
|
|
>{@html html}</span
|
|
>{/if}{:else if !isLoading}<span>{customEmptyMessage}</span>{/if}</pre
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style global>
|
|
/* Foreground colors */
|
|
.ansi-black-fg {
|
|
color: rgb(0, 0, 0);
|
|
}
|
|
.ansi-red-fg {
|
|
color: rgb(187, 0, 0);
|
|
}
|
|
.ansi-green-fg {
|
|
color: rgb(0, 187, 0);
|
|
}
|
|
.ansi-yellow-fg {
|
|
color: rgb(187, 187, 0);
|
|
}
|
|
.ansi-blue-fg {
|
|
color: rgb(0, 0, 187);
|
|
}
|
|
.ansi-magenta-fg {
|
|
color: rgb(187, 0, 187);
|
|
}
|
|
.ansi-cyan-fg {
|
|
color: rgb(0, 187, 187);
|
|
}
|
|
.ansi-white-fg {
|
|
color: rgb(255, 255, 255);
|
|
}
|
|
|
|
.ansi-bright-black-fg {
|
|
color: rgb(85, 85, 85);
|
|
}
|
|
.ansi-bright-red-fg {
|
|
color: rgb(255, 85, 85);
|
|
}
|
|
.ansi-bright-green-fg {
|
|
color: rgb(0, 255, 0);
|
|
}
|
|
.ansi-bright-yellow-fg {
|
|
color: rgb(255, 255, 85);
|
|
}
|
|
.ansi-bright-blue-fg {
|
|
color: rgb(85, 85, 255);
|
|
}
|
|
.ansi-bright-magenta-fg {
|
|
color: rgb(255, 85, 255);
|
|
}
|
|
.ansi-bright-cyan-fg {
|
|
color: rgb(85, 255, 255);
|
|
}
|
|
.ansi-bright-white-fg {
|
|
color: rgb(255, 255, 255);
|
|
}
|
|
|
|
/* Background colors */
|
|
.ansi-black-bg {
|
|
background-color: rgb(0, 0, 0);
|
|
}
|
|
.ansi-red-bg {
|
|
background-color: rgb(187, 0, 0);
|
|
}
|
|
.ansi-green-bg {
|
|
background-color: rgb(0, 187, 0);
|
|
}
|
|
.ansi-yellow-bg {
|
|
background-color: rgb(187, 187, 0);
|
|
}
|
|
.ansi-blue-bg {
|
|
background-color: rgb(0, 0, 187);
|
|
}
|
|
.ansi-magenta-bg {
|
|
background-color: rgb(187, 0, 187);
|
|
}
|
|
.ansi-cyan-bg {
|
|
background-color: rgb(0, 187, 187);
|
|
}
|
|
.ansi-white-bg {
|
|
background-color: rgb(255, 255, 255);
|
|
}
|
|
|
|
.ansi-bright-black-bg {
|
|
background-color: rgb(85, 85, 85);
|
|
}
|
|
.ansi-bright-red-bg {
|
|
background-color: rgb(255, 85, 85);
|
|
}
|
|
.ansi-bright-green-bg {
|
|
background-color: rgb(0, 255, 0);
|
|
}
|
|
.ansi-bright-yellow-bg {
|
|
background-color: rgb(255, 255, 85);
|
|
}
|
|
.ansi-bright-blue-bg {
|
|
background-color: rgb(85, 85, 255);
|
|
}
|
|
.ansi-bright-magenta-bg {
|
|
background-color: rgb(255, 85, 255);
|
|
}
|
|
.ansi-bright-cyan-bg {
|
|
background-color: rgb(85, 255, 255);
|
|
}
|
|
.ansi-bright-white-bg {
|
|
background-color: rgb(255, 255, 255);
|
|
}
|
|
|
|
/* Foreground colors for dark mode (Nord theme) */
|
|
.dark .ansi-black-fg {
|
|
color: rgb(46, 52, 64);
|
|
}
|
|
.dark .ansi-red-fg {
|
|
color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-green-fg {
|
|
color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-yellow-fg {
|
|
color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-blue-fg {
|
|
color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-magenta-fg {
|
|
color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-cyan-fg {
|
|
color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-white-fg {
|
|
color: rgb(216, 222, 233);
|
|
}
|
|
|
|
.dark .ansi-bright-black-fg {
|
|
color: rgb(67, 76, 94);
|
|
}
|
|
.dark .ansi-bright-red-fg {
|
|
color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-bright-green-fg {
|
|
color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-bright-yellow-fg {
|
|
color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-bright-blue-fg {
|
|
color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-bright-magenta-fg {
|
|
color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-bright-cyan-fg {
|
|
color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-bright-white-fg {
|
|
color: rgb(229, 233, 240);
|
|
}
|
|
|
|
/* Background colors for dark mode (Nord theme) */
|
|
.dark .ansi-black-bg {
|
|
background-color: rgb(46, 52, 64);
|
|
}
|
|
.dark .ansi-red-bg {
|
|
background-color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-green-bg {
|
|
background-color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-yellow-bg {
|
|
background-color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-blue-bg {
|
|
background-color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-magenta-bg {
|
|
background-color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-cyan-bg {
|
|
background-color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-white-bg {
|
|
background-color: rgb(216, 222, 233);
|
|
}
|
|
|
|
.dark .ansi-bright-black-bg {
|
|
background-color: rgb(67, 76, 94);
|
|
}
|
|
.dark .ansi-bright-red-bg {
|
|
background-color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-bright-green-bg {
|
|
background-color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-bright-yellow-bg {
|
|
background-color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-bright-blue-bg {
|
|
background-color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-bright-magenta-bg {
|
|
background-color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-bright-cyan-bg {
|
|
background-color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-bright-white-bg {
|
|
background-color: rgb(229, 233, 240);
|
|
}
|
|
|
|
[data-nav-id]:focus-visible {
|
|
outline: none;
|
|
outline-offset: 0;
|
|
}
|
|
</style>
|