feat(ui): pro badge, icon button animations, splitpanes nav fixes (#5009)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-09-05 13:08:03 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 3168aa0b85
commit e9b2f7065c
24 changed files with 262 additions and 26 deletions
+1
View File
@@ -195,6 +195,7 @@ declare module 'vue' {
'Ph:stack': typeof import('~icons/ph/stack')['default'] 'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default'] 'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
Popup: typeof import('./components/Popup.vue')['default'] Popup: typeof import('./components/Popup.vue')['default']
ProBadge: typeof import('./components/common/ProBadge.vue')['default']
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default'] RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default'] RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default'] 'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default']
+1 -1
View File
@@ -5,7 +5,7 @@
@closed="releaseSeen = mostRecent?.tag ?? config.version" @closed="releaseSeen = mostRecent?.tag ?? config.version"
> >
<template #trigger> <template #trigger>
<mdi:announcement class="size-6 -rotate-12" /> <mdi:announcement class="icon-wiggle size-6 -rotate-12" />
<template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag"> <template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag">
<span class="bg-red absolute top-0 right-px size-2 animate-ping rounded-full opacity-75"></span> <span class="bg-red absolute top-0 right-px size-2 animate-ping rounded-full opacity-75"></span>
<span class="bg-red absolute top-0 right-px size-2 rounded-full"></span> <span class="bg-red absolute top-0 right-px size-2 rounded-full"></span>
+1 -1
View File
@@ -3,7 +3,7 @@
<template #trigger> <template #trigger>
<div class="relative"> <div class="relative">
<mdi:cloud <mdi:cloud
class="size-6" class="icon-float size-6"
:class=" :class="
!cloudConfig !cloudConfig
? 'text-base-content/40' ? 'text-base-content/40'
+2 -2
View File
@@ -58,14 +58,14 @@
</div> </div>
<div class="join max-md:hidden"> <div class="join max-md:hidden">
<button <button
class="btn join-item btn-xs md:btn-sm" class="icon-btn btn join-item btn-xs md:btn-sm"
:class="statMode === 'chart' ? 'btn-active' : 'btn-ghost'" :class="statMode === 'chart' ? 'btn-active' : 'btn-ghost'"
@click="statMode = 'chart'" @click="statMode = 'chart'"
> >
<mdi:chart-bar /> <mdi:chart-bar />
</button> </button>
<button <button
class="btn join-item btn-xs md:btn-sm" class="icon-btn btn join-item btn-xs md:btn-sm"
:class="statMode === 'progress' ? 'btn-active' : 'btn-ghost'" :class="statMode === 'progress' ? 'btn-active' : 'btn-ghost'"
@click="statMode = 'progress'" @click="statMode = 'progress'"
> >
@@ -1,6 +1,6 @@
<template> <template>
<div class="dropdown dropdown-end dropdown-hover z-20"> <div class="dropdown dropdown-end dropdown-hover z-20">
<label tabindex="0" class="btn btn-ghost btn-sm relative w-8 gap-0 px-0 md:gap-0.5"> <label tabindex="0" class="icon-btn btn btn-ghost btn-sm relative w-8 gap-0 px-0 md:gap-0.5">
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" /> <carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" /> <carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
<span <span
@@ -1,6 +1,6 @@
<template> <template>
<div class="@container flex min-w-0 flex-1 items-center gap-1.5 md:gap-2"> <div class="@container flex min-w-0 flex-1 items-center gap-1.5 md:gap-2">
<label class="swap swap-rotate size-4"> <label class="icon-btn swap swap-rotate size-4">
<input type="checkbox" v-model="pinned" /> <input type="checkbox" v-model="pinned" />
<carbon:star-filled class="swap-on text-secondary" /> <carbon:star-filled class="swap-on text-secondary" />
<carbon:star class="swap-off" /> <carbon:star class="swap-off" />
+1 -1
View File
@@ -18,7 +18,7 @@
:placeholder="placeholderCopy" :placeholder="placeholderCopy"
/> />
<form method="dialog" class="flex"> <form method="dialog" class="flex">
<button v-if="isMobile" class="text-base-content/50 hover:text-base-content"> <button v-if="isMobile" class="icon-btn text-base-content/50 hover:text-base-content">
<mdi:close class="size-5" /> <mdi:close class="size-5" />
</button> </button>
<button v-else> <button v-else>
+2 -2
View File
@@ -10,7 +10,7 @@
data-testid="notifications" data-testid="notifications"
class="btn btn-circle btn-sm" class="btn btn-circle btn-sm"
> >
<mdi:bell class="size-6" /> <mdi:bell class="icon-ring size-6" />
</router-link> </router-link>
<CloudPopover v-if="config.enableCloud" /> <CloudPopover v-if="config.enableCloud" />
@@ -21,7 +21,7 @@
data-testid="settings" data-testid="settings"
class="btn btn-circle btn-sm" class="btn btn-circle btn-sm"
> >
<mdi:cog class="size-6" /> <mdi:cog class="icon-spin size-6" />
</router-link> </router-link>
<dropdown class="dropdown-end" data-testid="user-menu" v-if="config.user"> <dropdown class="dropdown-end" data-testid="user-menu" v-if="config.user">
+2 -2
View File
@@ -30,13 +30,13 @@
Raw JSON Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage"> <UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage">
<button class="swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }"> <button class="icon-btn swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" /> <mdi:check class="swap-on" />
<material-symbols:content-copy class="swap-off" /> <material-symbols:content-copy class="swap-off" />
</button> </button>
</UseClipboard> </UseClipboard>
<button class="outline-hidden" @click="downloadJSON()" title="Download JSON"> <button class="icon-btn outline-hidden" @click="downloadJSON()" title="Download JSON">
<material-symbols:download /> <material-symbols:download />
</button> </button>
</div> </div>
@@ -1,6 +1,6 @@
<template> <template>
<div class="dropdown dropdown-end dropdown-hover z-20"> <div class="dropdown dropdown-end dropdown-hover z-20">
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5"> <label tabindex="0" class="icon-btn btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" /> <carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" /> <carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
</label> </label>
+1 -1
View File
@@ -29,7 +29,7 @@
<div class="mr-16 text-right" v-if="!historical"> <div class="mr-16 text-right" v-if="!historical">
<transition name="fade"> <transition name="fade">
<button <button
class="btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm transition-colors" class="icon-btn btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm transition-colors"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''" :class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()" @click="scrollToBottom()"
v-show="scrollContext.paused" v-show="scrollContext.paused"
+2 -1
View File
@@ -3,7 +3,8 @@
<h1> <h1>
<router-link :to="{ name: '/' }" class="flex w-full items-center gap-2.5 overflow-hidden text-4xl font-thin"> <router-link :to="{ name: '/' }" class="flex w-full items-center gap-2.5 overflow-hidden text-4xl font-thin">
<Logo class="h-11 w-11 shrink-0" /> <Logo class="h-11 w-11 shrink-0" />
Dozzle <span class="truncate">Dozzle</span>
<ProBadge />
</router-link> </router-link>
<small class="mt-4 block text-sm font-light" v-if="hostname">{{ hostname }}</small> <small class="mt-4 block text-sm font-light" v-if="hostname">{{ hostname }}</small>
+1 -1
View File
@@ -4,7 +4,7 @@
:href="container.url" :href="container.url"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-base-content/40 hover:text-primary inline-flex shrink-0 items-center gap-1 transition-colors" class="icon-btn text-base-content/40 hover:text-primary inline-flex shrink-0 items-center gap-1 transition-colors"
:title="label" :title="label"
:aria-label="label" :aria-label="label"
@click.stop @click.stop
+1 -1
View File
@@ -3,7 +3,7 @@
<button <button
tabindex="0" tabindex="0"
role="button" role="button"
class="text-base-content/25 hover:text-primary shrink-0 cursor-pointer transition-colors" class="icon-btn text-base-content/25 hover:text-primary shrink-0 transition-colors"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
> >
+2 -1
View File
@@ -2,8 +2,9 @@
<nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-30 w-full border-b" data-testid="navigation"> <nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-30 w-full border-b" data-testid="navigation">
<div class="px-4 py-2"> <div class="px-4 py-2">
<div class="flex items-center"> <div class="flex items-center">
<router-link :to="{ name: '/' }"> <router-link :to="{ name: '/' }" class="flex items-center gap-1.5">
<Logo class="h-10" /> <Logo class="h-10" />
<ProBadge />
</router-link> </router-link>
<div class="ml-auto flex items-center gap-2"> <div class="ml-auto flex items-center gap-2">
+78
View File
@@ -0,0 +1,78 @@
<template>
<a
v-if="isPro"
:href="cloudUrl"
target="_blank"
rel="noreferrer noopener"
class="pro-badge status-pill status-pill-secondary"
:title="$t('cloud.pro-badge')"
data-testid="pro-badge"
@click.stop
>
PRO
</a>
</template>
<script lang="ts" setup>
const cloudUrl = __CLOUD_URL__;
const { isPro, cloudConfig, ensureCloudStatus } = useCloudConfig();
// The badge can mount before the shared cloud config resolves, so ask again
// once it lands. `ensureCloudStatus` is a no-op when the status is already in.
watch(
() => cloudConfig.value?.linked,
() => ensureCloudStatus(),
{ immediate: true },
);
</script>
<style scoped>
@reference "@/main.css";
/* Rides on the app's status-pill idiom (mono, uppercase, tracked, thin border)
* in the logo's gold, and only trims it down to sit beside the wordmark. The
* right padding drops the tracking added after the final letter so the text
* stays optically centered. */
.pro-badge {
@apply border-secondary/50 relative shrink-0 overflow-hidden rounded-[3px] py-px pl-[0.3rem] text-[0.6875rem] leading-[1.25] font-bold;
padding-right: calc(0.3rem - 0.1em);
transition:
border-color 150ms ease-out,
background-color 150ms ease-out;
}
.pro-badge:hover {
@apply border-secondary/80 bg-secondary/20;
}
/* A single narrow glint, in the accent color rather than white so it stays
* crisp on both themes. Long pause between passes keeps it a sheen. */
.pro-badge::after {
content: "";
@apply pointer-events-none absolute inset-0;
background: linear-gradient(
105deg,
transparent 44%,
color-mix(in oklab, var(--color-secondary) 55%, transparent) 50%,
transparent 56%
);
transform: translateX(-160%);
animation: pro-shine 5s ease-in-out infinite;
}
@keyframes pro-shine {
0%,
78% {
transform: translateX(-160%);
}
100% {
transform: translateX(160%);
}
}
@media (prefers-reduced-motion: reduce) {
.pro-badge::after {
animation: none;
}
}
</style>
+3 -3
View File
@@ -5,7 +5,7 @@
<div class="absolute right-0 flex items-center gap-3"> <div class="absolute right-0 flex items-center gap-3">
<button <button
v-if="!isMobile" v-if="!isMobile"
class="hover:text-base-content/60 cursor-pointer outline-hidden" class="icon-btn hover:text-base-content/60 outline-hidden"
type="button" type="button"
:title="maximized ? $t('drawer.restore') : $t('drawer.maximize')" :title="maximized ? $t('drawer.restore') : $t('drawer.maximize')"
:aria-label="maximized ? $t('drawer.restore') : $t('drawer.maximize')" :aria-label="maximized ? $t('drawer.restore') : $t('drawer.maximize')"
@@ -15,10 +15,10 @@
<mdi:arrow-expand v-else /> <mdi:arrow-expand v-else />
</button> </button>
<form method="dialog"> <form method="dialog">
<button v-if="isMobile" class="cursor-pointer"> <button v-if="isMobile" class="icon-btn">
<mdi:close /> <mdi:close />
</button> </button>
<button v-else class="swap hover:swap-active cursor-pointer outline-hidden"> <button v-else class="icon-btn swap hover:swap-active outline-hidden">
<mdi:keyboard-esc class="swap-off" /> <mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" /> <mdi:close class="swap-on" />
</button> </button>
+13
View File
@@ -47,6 +47,17 @@ async function fetchCloudStatus() {
} }
} }
// Dedupes concurrent/repeat status fetches so several components (nav popover,
// pro badge) can ask for the status without each firing its own request.
let pendingStatus: Promise<void> | null = null;
async function ensureCloudStatus() {
if (!config.enableCloud || !cloudConfig.value?.linked || cloudStatus.value) return;
pendingStatus ??= fetchCloudStatus().finally(() => (pendingStatus = null));
return pendingStatus;
}
const isPro = computed(() => cloudStatus.value?.plan.name.toLowerCase() === "pro");
function clearCloudState() { function clearCloudState() {
cloudConfig.value = null; cloudConfig.value = null;
cloudStatus.value = null; cloudStatus.value = null;
@@ -59,9 +70,11 @@ export function useCloudConfig() {
cloudStatus, cloudStatus,
cloudStatusError, cloudStatusError,
isLoadingCloudStatus, isLoadingCloudStatus,
isPro,
initialLoad, initialLoad,
fetchCloudConfig, fetchCloudConfig,
fetchCloudStatus, fetchCloudStatus,
ensureCloudStatus,
clearCloudState, clearCloudState,
}; };
} }
+35 -4
View File
@@ -2,10 +2,10 @@
<div> <div>
<MobileMenu v-if="isMobile && !forceMenuHidden" @search="showFuzzySearch"></MobileMenu> <MobileMenu v-if="isMobile && !forceMenuHidden" @search="showFuzzySearch"></MobileMenu>
<Splitpanes @resized="onResized($event)"> <Splitpanes @resized="onResized($event)">
<Pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav && !forceMenuHidden"> <Pane min-size="10" :size="menuWidth" v-if="navVisible">
<SidePanel /> <SidePanel />
</Pane> </Pane>
<Pane min-size="10" :size="100 - menuWidth"> <Pane min-size="10" :size="navVisible ? 100 - menuWidth : 100">
<Splitpanes> <Splitpanes>
<Pane class="router-view min-h-screen"> <Pane class="router-view min-h-screen">
<router-view></router-view> <router-view></router-view>
@@ -72,6 +72,14 @@ const { open, openSearch: showFuzzySearch, closeSearch } = useFuzzySearch();
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const forceMenuHidden = ref(searchParams.has("hideMenu")); const forceMenuHidden = ref(searchParams.has("hideMenu"));
// splitpanes only reads a pane's `size` as its "given size" when the pane first
// registers, and its initial layout is only exact when every pane declares one.
// So both panes keep an explicit size, and this one tells the content pane to
// claim the whole width while the nav pane is unmounted — otherwise the two
// panes would together still claim 100% + menuWidth once the nav came back, and
// splitpanes would shrink the nav to its min-size to compensate.
const navVisible = computed(() => !isMobile.value && !collapseNav.value && !forceMenuHidden.value);
watch(open, () => { watch(open, () => {
if (open.value) { if (open.value) {
modal.value?.showModal(); modal.value?.showModal();
@@ -87,7 +95,11 @@ onKeyStroke("k", (e) => {
} }
}); });
function onResized({ panes }: { panes: { size: number }[] }) { // splitpanes also emits `resized` for its own relayouts (the collapse toggle
// adding/removing the nav pane, a remount). Those carry neither `event` nor
// `index`, so only a width the user actually dragged to gets persisted.
function onResized({ panes, event, index }: { panes: { size: number }[]; event?: Event; index?: number }) {
if (event === undefined && index === undefined) return;
if (panes.length == 2) { if (panes.length == 2) {
menuWidth.value = Math.min(panes[0].size, 50); menuWidth.value = Math.min(panes[0].size, 50);
} }
@@ -98,7 +110,26 @@ function onResized({ panes }: { panes: { size: number }[] }) {
@reference "@/main.css"; @reference "@/main.css";
:deep(.splitpanes--vertical > .splitpanes__splitter) { :deep(.splitpanes--vertical > .splitpanes__splitter) {
@apply bg-base-100 hover:bg-secondary min-w-[5px]; @apply bg-base-100 relative min-w-[5px];
transition: background-color 150ms ease-out;
}
/* Grab zone reaches a few pixels past the visible divider so the drag is
* easier to start without making the divider itself any wider. */
:deep(.splitpanes--vertical > .splitpanes__splitter)::before {
content: "";
@apply absolute inset-y-0 -right-1 -left-1 z-20;
}
:deep(.splitpanes--vertical > .splitpanes__splitter):hover,
:deep(.splitpanes--dragging.splitpanes--vertical > .splitpanes__splitter) {
@apply bg-secondary;
}
@media (prefers-reduced-motion: reduce) {
:deep(.splitpanes--vertical > .splitpanes__splitter) {
transition: none;
}
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
+110
View File
@@ -274,9 +274,119 @@ body {
.status-pill-primary { .status-pill-primary {
@apply text-primary border-primary/30 bg-primary/10; @apply text-primary border-primary/30 bg-primary/10;
} }
.status-pill-secondary {
@apply text-secondary border-secondary/30 bg-secondary/10;
}
.status-pill-warning { .status-pill-warning {
@apply text-warning border-warning/30 bg-warning/10; @apply text-warning border-warning/30 bg-warning/10;
} }
.status-pill-error { .status-pill-error {
@apply text-error border-error/30 bg-error/10; @apply text-error border-error/30 bg-error/10;
} }
/* --- Icon button motion ---------------------------------------------------
* Icon-only buttons get a small spring on hover and a press-in on click. The
* transform sits on the glyph rather than the button so the circle and border
* are left alone. This includes daisyUI `swap` icons: swap-rotate animates the
* standalone `rotate` property, which composes with `transform` rather than
* fighting it (swap-flip, which does use `transform`, is unused here).
*
* `btn-circle` and `btn-square` are icon-only by definition; `.icon-btn` opts
* in a bare <button>/<a> or a daisyUI button that is icon-only without being
* one of those shapes.
*
* Everything composes through custom properties instead of competing
* `transform` declarations, so a per-icon flavour below only has to set a
* variable and never has to out-specify the generic hover rule. */
:is(.btn-circle, .btn-square, .icon-btn) :where(svg, img) {
--icon-scale: 1;
--icon-rotate: 0deg;
--icon-lift: 0px;
transform: translateY(var(--icon-lift)) rotate(var(--icon-rotate)) scale(var(--icon-scale));
/* Includes the standalone rotate/scale/translate properties so a Tailwind
* `rotate-180` toggle on the glyph keeps animating rather than snapping, and
* opacity so this rule winning over daisyUI's `.swap > *` does not kill the
* swap crossfade. */
transition-property: transform, rotate, scale, translate, opacity;
transition-duration: 200ms;
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}
.btn-circle,
.btn-square,
.icon-btn {
@apply cursor-pointer;
}
:is(.btn-circle, .btn-square, .icon-btn):hover :where(svg, img) {
--icon-scale: 1.15;
}
:is(.btn-circle, .btn-square, .icon-btn):active :where(svg, img) {
--icon-scale: 0.92;
}
/* Per-icon flavours, opted into by the icon itself. */
.btn:hover .icon-spin {
--icon-rotate: 90deg;
}
.btn:hover .icon-float {
--icon-lift: -2px;
}
/* The two animated flavours restate the hover scale in their keyframes so they
* settle at the same size the generic hover rule would have given them. */
.btn:hover .icon-wiggle {
animation: icon-wiggle 600ms ease-in-out;
}
.btn:hover .icon-ring {
animation: icon-ring 700ms ease-in-out;
transform-origin: top center;
}
@keyframes icon-wiggle {
0%,
100% {
transform: rotate(0deg) scale(1.15);
}
25% {
transform: rotate(-14deg) scale(1.15);
}
60% {
transform: rotate(10deg) scale(1.15);
}
80% {
transform: rotate(-5deg) scale(1.15);
}
}
@keyframes icon-ring {
0%,
100% {
transform: rotate(0deg) scale(1.15);
}
15% {
transform: rotate(14deg) scale(1.15);
}
35% {
transform: rotate(-12deg) scale(1.15);
}
55% {
transform: rotate(8deg) scale(1.15);
}
75% {
transform: rotate(-4deg) scale(1.15);
}
}
@media (prefers-reduced-motion: reduce) {
:is(.btn-circle, .btn-square, .icon-btn) :where(svg, img),
.btn:hover .icon-wiggle,
.btn:hover .icon-ring {
animation: none !important;
transform: none !important;
transition: none !important;
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
<section> <section>
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold">{{ $t("label.host-count", { count: Object.keys(hosts).length }) }}</h2> <h2 class="text-lg font-semibold">{{ $t("label.host-count", { count: Object.keys(hosts).length }) }}</h2>
<button @click="hostsCollapsed = !hostsCollapsed" class="btn btn-ghost btn-sm"> <button @click="hostsCollapsed = !hostsCollapsed" class="icon-btn btn btn-ghost btn-sm">
<mdi:chevron-down :class="{ 'rotate-180': !hostsCollapsed }" class="transition-transform" /> <mdi:chevron-down :class="{ 'rotate-180': !hostsCollapsed }" class="transition-transform" />
</button> </button>
</div> </div>
@@ -17,7 +17,7 @@
<h2 class="text-lg font-semibold"> <h2 class="text-lg font-semibold">
{{ $t("label.container", { count: dashboardContainers.length }) }} {{ $t("label.container", { count: dashboardContainers.length }) }}
</h2> </h2>
<button @click="containersCollapsed = !containersCollapsed" class="btn btn-ghost btn-sm"> <button @click="containersCollapsed = !containersCollapsed" class="icon-btn btn btn-ghost btn-sm">
<mdi:chevron-down :class="{ 'rotate-180': !containersCollapsed }" class="transition-transform" /> <mdi:chevron-down :class="{ 'rotate-180': !containersCollapsed }" class="transition-transform" />
</button> </button>
</div> </div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+1
View File
@@ -457,6 +457,7 @@ cloud:
link-instance: Link instance link-instance: Link instance
relink-instance: Re-link instance relink-instance: Re-link instance
connected: Connected connected: Connected
pro-badge: Dozzle Cloud Pro
plan: Plan plan: Plan
usage: Events this period usage: Events this period
dashboard: Dashboard dashboard: Dashboard