Skip to content

Commit 3db1b15

Browse files
make topbar badges responsive and fix server health badges showing on unrelated dialogs (#6291)
## Summary Implemented responsive topbar badges with three display modes (full, compact, icon-only) using Tailwind breakpoints and PrimeVue Popover interactions. https://github.com/user-attachments/assets/57912253-b1b5-4a68-953e-0be942ff09c4 ## Changes - **What**: Replaced hardcoded 880px breakpoint with [Tailwind breakpoints](https://tailwindcss.com/docs/responsive-design) via [@vueuse/core](https://vueuse.org/core/useBreakpoints/) - `xl (≥1280px)`: Full display (icon + label + text) - `lg (≥1024px)`: Compact (icon + label, click for popover) - `<lg (<1024px)`: Icon-only (icon/label/dot, click for popover) - **What**: Added `CloudBadge` component to isolate static "Comfy Cloud BETA" badge from runtime store badges - **What**: Added `backgroundColor` prop to support different contexts (topbar vs dialog backgrounds) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6291-make-topbar-badges-responsive-and-fix-server-health-badges-showing-on-unrelated-dialogs-2986d73d365081d294e5c9a7af1aafb2) by [Unito](https://www.unito.io)
1 parent d9e6298 commit 3db1b15

File tree

6 files changed

+436
-13
lines changed

6 files changed

+436
-13
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<template>
2+
<TopbarBadge
3+
:badge="cloudBadge"
4+
:display-mode="displayMode"
5+
:reverse-order="reverseOrder"
6+
:no-padding="noPadding"
7+
:background-color="backgroundColor"
8+
/>
9+
</template>
10+
11+
<script setup lang="ts">
12+
import { computed } from 'vue'
13+
14+
import { t } from '@/i18n'
15+
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
16+
17+
import TopbarBadge from './TopbarBadge.vue'
18+
19+
withDefaults(
20+
defineProps<{
21+
displayMode?: 'full' | 'compact' | 'icon-only'
22+
reverseOrder?: boolean
23+
noPadding?: boolean
24+
backgroundColor?: string
25+
}>(),
26+
{
27+
displayMode: 'full',
28+
reverseOrder: false,
29+
noPadding: false,
30+
backgroundColor: 'var(--comfy-menu-bg)'
31+
}
32+
)
33+
34+
const cloudBadge = computed<TopbarBadgeType>(() => ({
35+
label: t('g.beta'),
36+
text: 'Comfy Cloud'
37+
}))
38+
</script>
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { mount } from '@vue/test-utils'
2+
import Popover from 'primevue/popover'
3+
import PrimeVue from 'primevue/config'
4+
import Tooltip from 'primevue/tooltip'
5+
import { describe, expect, it } from 'vitest'
6+
import { createI18n } from 'vue-i18n'
7+
8+
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
9+
10+
import TopbarBadge from './TopbarBadge.vue'
11+
12+
const i18n = createI18n({
13+
legacy: false,
14+
locale: 'en',
15+
messages: {
16+
en: {}
17+
}
18+
})
19+
20+
describe('TopbarBadge', () => {
21+
const exampleBadge: TopbarBadgeType = {
22+
text: 'Test Badge',
23+
label: 'BETA',
24+
variant: 'info'
25+
}
26+
27+
const mountTopbarBadge = (
28+
badge: Partial<TopbarBadgeType> = {},
29+
displayMode: 'full' | 'compact' | 'icon-only' = 'full'
30+
) => {
31+
return mount(TopbarBadge, {
32+
global: {
33+
plugins: [PrimeVue, i18n],
34+
directives: { tooltip: Tooltip },
35+
components: { Popover }
36+
},
37+
props: {
38+
badge: { ...exampleBadge, ...badge },
39+
displayMode
40+
}
41+
})
42+
}
43+
44+
describe('full display mode', () => {
45+
it('renders all badge elements (icon, label, text)', () => {
46+
const wrapper = mountTopbarBadge(
47+
{
48+
text: 'Comfy Cloud',
49+
label: 'BETA',
50+
icon: 'pi pi-cloud'
51+
},
52+
'full'
53+
)
54+
55+
expect(wrapper.find('.pi-cloud').exists()).toBe(true)
56+
expect(wrapper.text()).toContain('BETA')
57+
expect(wrapper.text()).toContain('Comfy Cloud')
58+
})
59+
60+
it('renders without icon when not provided', () => {
61+
const wrapper = mountTopbarBadge(
62+
{
63+
text: 'Test',
64+
label: 'NEW'
65+
},
66+
'full'
67+
)
68+
69+
expect(wrapper.find('i').exists()).toBe(false)
70+
expect(wrapper.text()).toContain('NEW')
71+
expect(wrapper.text()).toContain('Test')
72+
})
73+
})
74+
75+
describe('compact display mode', () => {
76+
it('renders icon and label but not text', () => {
77+
const wrapper = mountTopbarBadge(
78+
{
79+
text: 'Hidden Text',
80+
label: 'BETA',
81+
icon: 'pi pi-cloud'
82+
},
83+
'compact'
84+
)
85+
86+
expect(wrapper.find('.pi-cloud').exists()).toBe(true)
87+
expect(wrapper.text()).toContain('BETA')
88+
expect(wrapper.text()).not.toContain('Hidden Text')
89+
})
90+
91+
it('opens popover on click', async () => {
92+
const wrapper = mountTopbarBadge(
93+
{
94+
text: 'Full Text',
95+
label: 'ALERT'
96+
},
97+
'compact'
98+
)
99+
100+
const clickableArea = wrapper.find('[class*="flex h-full"]')
101+
await clickableArea.trigger('click')
102+
103+
const popover = wrapper.findComponent(Popover)
104+
expect(popover.exists()).toBe(true)
105+
})
106+
})
107+
108+
describe('icon-only display mode', () => {
109+
it('renders only icon', () => {
110+
const wrapper = mountTopbarBadge(
111+
{
112+
text: 'Hidden Text',
113+
label: 'BETA',
114+
icon: 'pi pi-cloud'
115+
},
116+
'icon-only'
117+
)
118+
119+
expect(wrapper.find('.pi-cloud').exists()).toBe(true)
120+
expect(wrapper.text()).not.toContain('BETA')
121+
expect(wrapper.text()).not.toContain('Hidden Text')
122+
})
123+
124+
it('renders label when no icon provided', () => {
125+
const wrapper = mountTopbarBadge(
126+
{
127+
text: 'Hidden Text',
128+
label: 'NEW'
129+
},
130+
'icon-only'
131+
)
132+
133+
expect(wrapper.text()).toContain('NEW')
134+
expect(wrapper.text()).not.toContain('Hidden Text')
135+
})
136+
})
137+
138+
describe('badge variants', () => {
139+
it('applies error variant styles', () => {
140+
const wrapper = mountTopbarBadge(
141+
{
142+
text: 'Error Message',
143+
label: 'ERROR',
144+
variant: 'error'
145+
},
146+
'full'
147+
)
148+
149+
expect(wrapper.find('.bg-danger-100').exists()).toBe(true)
150+
expect(wrapper.find('.text-danger-100').exists()).toBe(true)
151+
})
152+
153+
it('applies warning variant styles', () => {
154+
const wrapper = mountTopbarBadge(
155+
{
156+
text: 'Warning Message',
157+
label: 'WARN',
158+
variant: 'warning'
159+
},
160+
'full'
161+
)
162+
163+
expect(wrapper.find('.bg-warning-100').exists()).toBe(true)
164+
expect(wrapper.find('.text-warning-100').exists()).toBe(true)
165+
})
166+
167+
it('uses default error icon for error variant', () => {
168+
const wrapper = mountTopbarBadge(
169+
{
170+
text: 'Error',
171+
variant: 'error'
172+
},
173+
'full'
174+
)
175+
176+
expect(wrapper.find('.pi-exclamation-circle').exists()).toBe(true)
177+
})
178+
179+
it('uses default warning icon for warning variant', () => {
180+
const wrapper = mountTopbarBadge(
181+
{
182+
text: 'Warning',
183+
variant: 'warning'
184+
},
185+
'full'
186+
)
187+
188+
expect(wrapper.find('.pi-exclamation-triangle').exists()).toBe(true)
189+
})
190+
})
191+
192+
describe('popover', () => {
193+
it('includes popover component in compact and icon-only modes', () => {
194+
const compactWrapper = mountTopbarBadge({}, 'compact')
195+
const iconOnlyWrapper = mountTopbarBadge({}, 'icon-only')
196+
const fullWrapper = mountTopbarBadge({}, 'full')
197+
198+
expect(compactWrapper.findComponent(Popover).exists()).toBe(true)
199+
expect(iconOnlyWrapper.findComponent(Popover).exists()).toBe(true)
200+
expect(fullWrapper.findComponent(Popover).exists()).toBe(false)
201+
})
202+
})
203+
204+
describe('edge cases', () => {
205+
it('handles badge with only text', () => {
206+
const wrapper = mountTopbarBadge(
207+
{
208+
text: 'Simple Badge'
209+
},
210+
'full'
211+
)
212+
213+
expect(wrapper.text()).toContain('Simple Badge')
214+
expect(wrapper.find('i').exists()).toBe(false)
215+
})
216+
217+
it('handles custom icon override', () => {
218+
const wrapper = mountTopbarBadge(
219+
{
220+
text: 'Custom',
221+
variant: 'error',
222+
icon: 'pi pi-custom-icon'
223+
},
224+
'full'
225+
)
226+
227+
expect(wrapper.find('.pi-custom-icon').exists()).toBe(true)
228+
expect(wrapper.find('.pi-exclamation-circle').exists()).toBe(false)
229+
})
230+
})
231+
})

0 commit comments

Comments
 (0)