fix: hydration issues caused by duplicate components on servers panel (#3753)

* fix: server stats icons

* fix: fix chart jumping

* refactor: iconComponent -> icon

* fix: panel hydration issues

* fix: apply requested changes
This commit is contained in:
IMB11 2025-06-11 22:30:24 +01:00 committed by GitHub
parent a3839461cf
commit f8fb23e05f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 156 additions and 168 deletions

View File

@ -1,80 +0,0 @@
<template>
<div
aria-hidden="true"
style="font-variant-numeric: tabular-nums"
class="pointer-events-none h-full w-full select-none"
>
<div class="flex flex-col gap-6">
<div class="flex flex-row items-center gap-6">
<div
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
>
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
</div>
<h3 class="relative z-10 text-base font-normal text-secondary">CPU usage</h3>
</div>
<CPUIcon class="absolute right-10 top-10" />
</div>
<div
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
>
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
</div>
<h3 class="relative z-10 text-base font-normal text-secondary">Memory usage</h3>
</div>
<DBIcon class="absolute right-10 top-10" />
</div>
<div
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
>
<div class="flex flex-row items-center gap-2">
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">0 B</h2>
</div>
<h3 class="relative z-10 text-base font-normal text-secondary">Storage usage</h3>
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
</div>
</div>
<div
class="relative flex h-full w-full flex-col gap-3 overflow-hidden rounded-2xl bg-bg-raised p-8"
>
<div class="experimental-styles-within flex flex-row items-center">
<div class="flex flex-row items-center gap-4">
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
</div>
</div>
<div class="relative w-full">
<input type="text" placeholder="Search logs" class="h-12 !w-full !pl-10 !pr-48" />
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
</div>
<div
class="console relative h-full min-h-[516px] w-full overflow-hidden rounded-xl bg-bg text-sm"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { CPUIcon, DBIcon, FolderOpenIcon, SearchIcon } from "@modrinth/assets";
</script>
<style scoped>
html.light-mode .console {
background: var(--color-bg);
}
html.dark-mode .console {
background: black;
}
html.oled-mode .console {
background: black;
}
</style>

View File

@ -7,16 +7,17 @@
type="text" type="text"
placeholder="Search logs" placeholder="Search logs"
class="h-12 !w-full !pl-10 !pr-48" class="h-12 !w-full !pl-10 !pr-48"
:disabled="loading"
@keydown.escape="clearSearch" @keydown.escape="clearSearch"
/> />
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" /> <SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
<ButtonStyled v-if="searchInput" @click="clearSearch"> <ButtonStyled v-if="searchInput && !loading" @click="clearSearch">
<button class="absolute right-2 top-1/2 -translate-y-1/2"> <button class="absolute right-2 top-1/2 -translate-y-1/2">
<XIcon class="h-5 w-5" /> <XIcon class="h-5 w-5" />
</button> </button>
</ButtonStyled> </ButtonStyled>
<span <span
v-if="pyroConsole.filteredOutput.value.length && searchInput" v-if="pyroConsole.filteredOutput.value.length && searchInput && !loading"
class="pointer-events-none absolute right-12 top-1/2 -translate-y-1/2 select-none whitespace-pre text-sm" class="pointer-events-none absolute right-12 top-1/2 -translate-y-1/2 select-none whitespace-pre text-sm"
> >
{{ pyroConsole.filteredOutput.value.length }} {{ pyroConsole.filteredOutput.value.length }}
@ -29,11 +30,13 @@
:class="[ :class="[
'terminal-font console relative z-[1] flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl px-1 text-sm transition-transform duration-300', 'terminal-font console relative z-[1] flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl px-1 text-sm transition-transform duration-300',
{ 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen }, { 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen },
{ 'pointer-events-none': loading },
]" ]"
:aria-hidden="loading"
tabindex="-1" tabindex="-1"
> >
<div <div
v-if="cosmetics.advancedRendering" v-if="cosmetics.advancedRendering && !loading"
class="progressive-gradient pointer-events-none absolute -bottom-6 left-0 z-[2] h-[10rem] w-full overflow-hidden rounded-xl" class="progressive-gradient pointer-events-none absolute -bottom-6 left-0 z-[2] h-[10rem] w-full overflow-hidden rounded-xl"
:style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`" :style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`"
aria-hidden="true" aria-hidden="true"
@ -47,7 +50,7 @@
/> />
</div> </div>
<div <div
v-else v-else-if="!loading"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-[2] h-[196px] w-full" class="pointer-events-none absolute bottom-0 left-0 right-0 z-[2] h-[196px] w-full"
:style=" :style="
bottomThreshold > 0 bottomThreshold > 0
@ -79,6 +82,7 @@
</div> </div>
<div data-pyro-terminal-scroll-root class="relative h-full w-full"> <div data-pyro-terminal-scroll-root class="relative h-full w-full">
<div <div
v-if="!loading"
ref="scrollbarTrack" ref="scrollbarTrack"
data-pyro-terminal-scrollbar-track data-pyro-terminal-scrollbar-track
class="absolute -right-1 bottom-16 top-4 z-[4] w-4 overflow-hidden" class="absolute -right-1 bottom-16 top-4 z-[4] w-4 overflow-hidden"
@ -118,7 +122,12 @@
class="scrollbar-none absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]" class="scrollbar-none absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]"
@scroll.passive="() => handleListScroll()" @scroll.passive="() => handleListScroll()"
> >
<div data-pyro-terminal-virtual-height-watcher :style="{ height: `${totalHeight}px` }"> <div v-if="loading" class="h-full w-full" />
<div
v-else
data-pyro-terminal-virtual-height-watcher
:style="{ height: `${totalHeight}px` }"
>
<ul <ul
class="m-0 list-none p-0" class="m-0 list-none p-0"
data-pyro-terminal-virtual-list data-pyro-terminal-virtual-list
@ -205,6 +214,7 @@
<slot /> <slot />
</div> </div>
<button <button
v-if="!loading"
data-pyro-fullscreen data-pyro-fullscreen
:label="isFullScreen ? 'Exit full screen' : 'Enter full screen'" :label="isFullScreen ? 'Exit full screen' : 'Enter full screen'"
class="experimental-styles-within absolute right-4 top-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95" class="experimental-styles-within absolute right-4 top-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
@ -217,7 +227,7 @@
<Transition name="fade"> <Transition name="fade">
<div <div
v-if="hasSelection || isSingleLineSelected" v-if="(hasSelection || isSingleLineSelected) && !loading"
class="absolute right-20 top-4 z-[3] flex flex-row items-center" class="absolute right-20 top-4 z-[3] flex flex-row items-center"
:class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }" :class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }"
> >
@ -247,7 +257,7 @@
<Transition name="scroll-to-bottom"> <Transition name="scroll-to-bottom">
<button <button
v-if="bottomThreshold > 0 && !isScrolledToBottom" v-if="bottomThreshold > 0 && !isScrolledToBottom && !loading"
data-pyro-scrolltobottom data-pyro-scrolltobottom
label="Scroll to bottom" label="Scroll to bottom"
class="scroll-to-bottom-btn experimental-styles-within absolute bottom-[4.5rem] right-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95" class="scroll-to-bottom-btn experimental-styles-within absolute bottom-[4.5rem] right-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
@ -298,6 +308,7 @@ const cosmetics = $cosmetics;
const props = defineProps<{ const props = defineProps<{
fullScreen: boolean; fullScreen: boolean;
loading?: boolean;
}>(); }>();
const BUFFER_SIZE = 5; const BUFFER_SIZE = 5;
@ -308,7 +319,7 @@ const SCROLL_END_DELAY = 150;
const progressiveBlurIterations = ref(8); const progressiveBlurIterations = ref(8);
const pyroConsole = usePyroConsole(); const pyroConsole = usePyroConsole();
const consoleOutput = pyroConsole.output; const consoleOutput = computed(() => (props.loading ? [] : pyroConsole.output.value));
const scrollContainer = ref<HTMLElement | null>(null); const scrollContainer = ref<HTMLElement | null>(null);

View File

@ -3,6 +3,8 @@
data-pyro-server-stats data-pyro-server-stats
style="font-variant-numeric: tabular-nums" style="font-variant-numeric: tabular-nums"
class="flex select-none flex-col items-center gap-6 md:flex-row" class="flex select-none flex-col items-center gap-6 md:flex-row"
:class="{ 'pointer-events-none': loading }"
:aria-hidden="loading"
> >
<div <div
v-for="(metric, index) in metrics" v-for="(metric, index) in metrics"
@ -18,7 +20,7 @@
<h3 class="flex items-center gap-2 text-base font-normal text-secondary"> <h3 class="flex items-center gap-2 text-base font-normal text-secondary">
{{ metric.title }} {{ metric.title }}
<IssuesIcon <IssuesIcon
v-if="metric.warning" v-if="metric.warning && !loading"
v-tooltip="metric.warning" v-tooltip="metric.warning"
class="size-5" class="size-5"
:style="{ color: 'var(--color-orange)' }" :style="{ color: 'var(--color-orange)' }"
@ -28,37 +30,47 @@
<div class="absolute -left-8 -top-4 h-28 w-56 rounded-full bg-bg-raised blur-lg" /> <div class="absolute -left-8 -top-4 h-28 w-56 rounded-full bg-bg-raised blur-lg" />
</div> </div>
<component :is="metric.icon" class="absolute right-10 top-10 z-10" /> <component
<ClientOnly> :is="metric.icon"
<VueApexCharts class="absolute right-10 top-10 z-10 size-8"
v-if="metric.showGraph" style="width: 2rem; height: 2rem"
type="area" />
height="142"
:options="getChartOptions(metric.warning)" <div class="chart-space absolute bottom-0 left-0 right-0">
:series="[{ name: metric.title, data: metric.data }]" <ClientOnly>
class="chart absolute bottom-0 left-0 right-0 w-full opacity-0" <VueApexCharts
/> v-if="metric.showGraph && !loading"
</ClientOnly> type="area"
height="142"
:options="getChartOptions(metric.warning, index)"
:series="[{ name: metric.title, data: metric.data }]"
class="chart"
:class="chartsReady.has(index) ? 'opacity-100' : 'opacity-0'"
/>
</ClientOnly>
</div>
</div> </div>
<NuxtLink <component
:to="`/servers/manage/${serverId}/files`" :is="loading ? 'div' : 'NuxtLink'"
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100" :to="loading ? undefined : `/servers/manage/${serverId}/files`"
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
:class="loading ? '' : 'transition-transform duration-100 hover:scale-105 active:scale-100'"
> >
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast"> <h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
{{ formatBytes(stats.storage_usage_bytes) }} {{ loading ? "0 B" : formatBytes(stats.storage_usage_bytes) }}
</h2> </h2>
</div> </div>
<h3 class="text-base font-normal text-secondary">Storage usage</h3> <h3 class="text-base font-normal text-secondary">Storage usage</h3>
<FolderOpenIcon class="absolute right-10 top-10 size-8" /> <FolderOpenIcon class="absolute right-10 top-10 size-8" />
</NuxtLink> </component>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, shallowRef } from "vue"; import { ref, computed, shallowRef } from "vue";
import { FolderOpenIcon, CPUIcon, DBIcon, IssuesIcon } from "@modrinth/assets"; import { FolderOpenIcon, CPUIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
import { useStorage } from "@vueuse/core"; import { useStorage } from "@vueuse/core";
import type { Stats } from "~/types/servers"; import type { Stats } from "~/types/servers";
@ -66,13 +78,28 @@ const route = useNativeRoute();
const serverId = route.params.id; const serverId = route.params.id;
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts")); const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
const chartsReady = ref(new Set<number>());
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, { const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
ramAsNumber: false, ramAsNumber: false,
}); });
const props = defineProps<{ data: Stats }>(); const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
loading: false,
});
const stats = shallowRef(props.data.current); const stats = shallowRef(
props.data?.current || {
cpu_percent: 0,
ram_usage_bytes: 0,
ram_total_bytes: 1, // Avoid division by zero
storage_usage_bytes: 0,
},
);
const onChartReady = (index: number) => {
chartsReady.value.add(index);
};
const formatBytes = (bytes: number) => { const formatBytes = (bytes: number) => {
const units = ["B", "KB", "MB", "GB"]; const units = ["B", "KB", "MB", "GB"];
@ -94,6 +121,29 @@ const updateGraphData = (arr: number[], newValue: number) => {
}; };
const metrics = computed(() => { const metrics = computed(() => {
if (props.loading) {
return [
{
title: "CPU usage",
value: "0.00%",
max: "100%",
icon: CPUIcon,
data: cpuData.value,
showGraph: false,
warning: null,
},
{
title: "Memory usage",
value: "0.00%",
max: "100%",
icon: DatabaseIcon,
data: ramData.value,
showGraph: false,
warning: null,
},
];
}
const ramPercent = Math.min( const ramPercent = Math.min(
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100, (stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
100, 100,
@ -119,7 +169,7 @@ const metrics = computed(() => {
? formatBytes(stats.value.ram_usage_bytes) ? formatBytes(stats.value.ram_usage_bytes)
: `${ramPercent.toFixed(2)}%`, : `${ramPercent.toFixed(2)}%`,
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%", max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
icon: DBIcon, icon: DatabaseIcon,
data: ramData.value, data: ramData.value,
showGraph: true, showGraph: true,
warning: ramPercent >= 90 ? "Memory usage is very high" : null, warning: ramPercent >= 90 ? "Memory usage is very high" : null,
@ -127,7 +177,7 @@ const metrics = computed(() => {
]; ];
}); });
const getChartOptions = (hasWarning: string | null) => ({ const getChartOptions = (hasWarning: string | null, index: number) => ({
chart: { chart: {
type: "area", type: "area",
animations: { enabled: false }, animations: { enabled: false },
@ -139,6 +189,10 @@ const getChartOptions = (hasWarning: string | null) => ({
top: 0, top: 0,
bottom: 0, bottom: 0,
}, },
events: {
mounted: () => onChartReady(index),
updated: () => onChartReady(index),
},
}, },
stroke: { curve: "smooth", width: 3 }, stroke: { curve: "smooth", width: 3 },
fill: { fill: {
@ -172,24 +226,26 @@ const getChartOptions = (hasWarning: string | null) => ({
}); });
watch( watch(
() => props.data.current, () => props.data?.current,
(newStats) => { (newStats) => {
stats.value = newStats; if (newStats) {
stats.value = newStats;
}
}, },
); );
</script> </script>
<style scoped> <style scoped>
.chart { .chart-space {
animation: fadeIn 0.2s ease-out 0.2s forwards; height: 142px;
width: calc(100% + 48px);
margin-left: -24px; margin-left: -24px;
margin-right: -24px; margin-right: -24px;
width: calc(100% + 48px) !important;
} }
@keyframes fadeIn { .chart {
to { width: 100% !important;
opacity: 1; height: 142px !important;
} transition: opacity 0.3s ease-out;
} }
</style> </style>

View File

@ -1,11 +1,7 @@
<template> <template>
<div <div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
v-if="isConnected && !isWsAuthIncorrect"
class="relative flex select-none flex-col gap-6"
data-pyro-server-manager-root
>
<div <div
v-if="inspectingError" v-if="inspectingError && isConnected && !isWsAuthIncorrect"
data-pyro-servers-inspecting-error data-pyro-servers-inspecting-error
class="flex justify-between rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast" class="flex justify-between rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
> >
@ -77,26 +73,34 @@
</ButtonStyled> </ButtonStyled>
</div> </div>
</div> </div>
<div class="flex flex-col-reverse gap-6 md:flex-col"> <div class="flex flex-col-reverse gap-6 md:flex-col">
<UiServersServerStats :data="stats" /> <UiServersServerStats
:data="isConnected && !isWsAuthIncorrect ? stats : undefined"
:loading="!isConnected || isWsAuthIncorrect"
/>
<div <div
class="relative flex h-[700px] w-full flex-col gap-3 overflow-hidden rounded-2xl border border-divider bg-bg-raised p-4 transition-all duration-300 ease-in-out md:p-8" class="relative flex h-[700px] w-full flex-col gap-3 overflow-hidden rounded-2xl border border-divider bg-bg-raised p-4 transition-all duration-300 ease-in-out md:p-8"
:class="{ 'border-0': !isConnected || isWsAuthIncorrect }"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2> <h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
<UiServersPanelServerStatus
<UiServersPanelServerStatus :state="serverPowerState" /> v-if="isConnected && !isWsAuthIncorrect"
:state="serverPowerState"
/>
</div> </div>
</div> </div>
<!-- <div class="flex flex-row items-center gap-2 text-sm font-medium">
<InfoIcon class="hidden sm:block" /> <UiServersPanelTerminal
Click and drag to select lines, then CMD+C to copy :full-screen="fullScreen"
</div> --> :loading="!isConnected || isWsAuthIncorrect"
<UiServersPanelTerminal :full-screen="fullScreen"> >
<div class="relative w-full px-4 pt-4"> <div class="relative w-full px-4 pt-4">
<ul <ul
v-if="suggestions.length" v-if="suggestions.length && isConnected && !isWsAuthIncorrect"
id="command-suggestions" id="command-suggestions"
ref="suggestionsList" ref="suggestionsList"
class="mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg" class="mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg"
@ -120,7 +124,7 @@
</ul> </ul>
<div class="relative flex items-center"> <div class="relative flex items-center">
<span <span
v-if="bestSuggestion" v-if="bestSuggestion && isConnected && !isWsAuthIncorrect"
class="pointer-events-none absolute left-[26px] transform select-none text-gray-400" class="pointer-events-none absolute left-[26px] transform select-none text-gray-400"
> >
<span class="ml-[23.5px] whitespace-pre">{{ <span class="ml-[23.5px] whitespace-pre">{{
@ -142,7 +146,7 @@
<TerminalSquareIcon class="ml-3 h-5 w-5" /> <TerminalSquareIcon class="ml-3 h-5 w-5" />
</div> </div>
<input <input
v-if="isServerRunning" v-if="isServerRunning && isConnected && !isWsAuthIncorrect"
v-model="commandInput" v-model="commandInput"
type="text" type="text"
placeholder="Send a command" placeholder="Send a command"
@ -168,21 +172,17 @@
</UiServersPanelTerminal> </UiServersPanelTerminal>
</div> </div>
</div> </div>
</div>
<UiServersOverviewLoading v-else-if="!isConnected && !isWsAuthIncorrect" /> <div
<div v-else-if="isWsAuthIncorrect" class="flex flex-col"> v-if="isWsAuthIncorrect"
<h2>Could not connect to the server.</h2> class="absolute inset-0 flex flex-col items-center justify-center bg-bg"
<p> >
An error occurred while attempting to connect to your server. Please try refreshing the page. <h2>Could not connect to the server.</h2>
(WebSocket Authentication Failed) <p>
</p> An error occurred while attempting to connect to your server. Please try refreshing the
</div> page. (WebSocket Authentication Failed)
<div v-else class="flex flex-col"> </p>
<h2>Could not connect to the server.</h2> </div>
<p>
An error occurred while attempting to connect to your server. Please try refreshing the page.
(No further information)
</p>
</div> </div>
</template> </template>

View File

@ -1 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cpu-icon lucide-cpu"><path d="M12 20v2"/><path d="M12 2v2"/><path d="M17 20v2"/><path d="M17 2v2"/><path d="M2 12h2"/><path d="M2 17h2"/><path d="M2 7h2"/><path d="M20 12h2"/><path d="M20 17h2"/><path d="M20 7h2"/><path d="M7 20v2"/><path d="M7 2v2"/><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="8" y="8" width="8" height="8" rx="1"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-cpu-icon lucide-cpu">
<path d="M12 20v2" />
<path d="M12 2v2" />
<path d="M17 20v2" />
<path d="M17 2v2" />
<path d="M2 12h2" />
<path d="M2 17h2" />
<path d="M2 7h2" />
<path d="M20 12h2" />
<path d="M20 17h2" />
<path d="M20 7h2" />
<path d="M7 20v2" />
<path d="M7 2v2" />
<rect x="4" y="4" width="16" height="16" rx="2" />
<rect x="8" y="8" width="8" height="8" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 648 B

View File

@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="absolute right-8 top-8 size-8"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"
/>
</svg>

Before

Width:  |  Height:  |  Size: 633 B

View File

@ -207,7 +207,6 @@ import _CubeIcon from './icons/cube.svg?component'
import _CloudIcon from './icons/cloud.svg?component' import _CloudIcon from './icons/cloud.svg?component'
import _CogIcon from './icons/cog.svg?component' import _CogIcon from './icons/cog.svg?component'
import _CPUIcon from './icons/cpu.svg?component' import _CPUIcon from './icons/cpu.svg?component'
import _DBIcon from './icons/db.svg?component'
import _LoaderIcon from './icons/loader.svg?component' import _LoaderIcon from './icons/loader.svg?component'
import _ImportIcon from './icons/import.svg?component' import _ImportIcon from './icons/import.svg?component'
import _TimerIcon from './icons/timer.svg?component' import _TimerIcon from './icons/timer.svg?component'
@ -438,7 +437,6 @@ export const CubeIcon = _CubeIcon
export const CloudIcon = _CloudIcon export const CloudIcon = _CloudIcon
export const CogIcon = _CogIcon export const CogIcon = _CogIcon
export const CPUIcon = _CPUIcon export const CPUIcon = _CPUIcon
export const DBIcon = _DBIcon
export const LoaderIcon = _LoaderIcon export const LoaderIcon = _LoaderIcon
export const ImportIcon = _ImportIcon export const ImportIcon = _ImportIcon
export const CardIcon = _CardIcon export const CardIcon = _CardIcon