mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-09 15:10:17 +00:00
## Summary Enable `better-tailwindcss/no-deprecated-classes` lint rule and auto-fix all 103 violations across 65 files. First PR in a stacked series for #9300. ## Changes - **What**: Replace deprecated Tailwind v3 classes with v4 equivalents: - `rounded` → `rounded-sm` (85) - `flex-shrink-0` → `shrink-0` (16) - `flex-grow` → `grow` (2) - Enable `no-deprecated-classes` as `'error'` in eslint config - Update one test asserting on `'rounded'` class string ## Review Focus Mechanical auto-fix PR — all changes produced by `eslint --fix`. No visual or behavioral changes (Tailwind v4 aliases these classes identically). Fixes #9300 (partial — 1 of 3 rules) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9417-fix-enable-no-deprecated-classes-tailwind-lint-rule-31a6d73d3650819eaef4cf8ad84fb186) by [Unito](https://www.unito.io) Co-authored-by: Alexander Brown <drjkl@comfy.org>
838 lines
21 KiB
Vue
838 lines
21 KiB
Vue
<template>
|
|
<div
|
|
class="help-center-menu flex flex-col items-start gap-1"
|
|
role="menu"
|
|
:aria-label="$t('help.helpCenterMenu')"
|
|
>
|
|
<!-- Main Menu Items -->
|
|
<div class="w-full">
|
|
<nav class="flex w-full flex-col gap-2" role="menubar">
|
|
<button
|
|
v-for="menuItem in menuItems"
|
|
v-show="menuItem.visible !== false"
|
|
:key="menuItem.key"
|
|
type="button"
|
|
class="help-menu-item"
|
|
:class="{ 'more-item': menuItem.key === 'more' }"
|
|
role="menuitem"
|
|
@click="menuItem.action"
|
|
@mouseenter="onMenuItemHover(menuItem.key, $event)"
|
|
@mouseleave="onMenuItemLeave(menuItem.key)"
|
|
>
|
|
<div class="help-menu-icon-container">
|
|
<div class="help-menu-icon">
|
|
<component
|
|
:is="menuItem.icon"
|
|
v-if="typeof menuItem.icon === 'object'"
|
|
:size="16"
|
|
/>
|
|
<i v-else :class="menuItem.icon" />
|
|
</div>
|
|
<div v-if="menuItem.showRedDot" class="menu-red-dot" />
|
|
</div>
|
|
<span class="menu-label">{{ menuItem.label }}</span>
|
|
<i
|
|
v-if="menuItem.showExternalIcon"
|
|
class="icon-[lucide--external-link] text-primary w-4 h-4 ml-auto"
|
|
/>
|
|
<i
|
|
v-if="menuItem.key === 'more'"
|
|
class="pi pi-chevron-right ml-auto"
|
|
/>
|
|
</button>
|
|
</nav>
|
|
<div
|
|
class="flex h-4 flex-col items-center justify-between self-stretch p-2"
|
|
>
|
|
<div class="w-full border-b border-interface-menu-stroke" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- More Submenu -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="isSubmenuVisible"
|
|
ref="submenuRef"
|
|
class="more-submenu"
|
|
:style="submenuStyle"
|
|
@mouseenter="onSubmenuHover"
|
|
@mouseleave="onSubmenuLeave"
|
|
>
|
|
<template
|
|
v-for="submenuItem in moreMenuItem?.items"
|
|
:key="submenuItem.key"
|
|
>
|
|
<div
|
|
v-if="submenuItem.type === 'divider'"
|
|
v-show="submenuItem.visible !== false"
|
|
class="submenu-divider"
|
|
/>
|
|
<button
|
|
v-else
|
|
v-show="submenuItem.visible !== false"
|
|
type="button"
|
|
class="help-menu-item submenu-item"
|
|
role="menuitem"
|
|
@click="submenuItem.action"
|
|
>
|
|
<span class="menu-label">{{ submenuItem.label }}</span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- What's New Section -->
|
|
<section
|
|
v-if="showVersionUpdates"
|
|
class="w-full"
|
|
data-testid="whats-new-section"
|
|
>
|
|
<h3
|
|
class="section-description flex items-center gap-2.5 self-stretch px-8 pt-2 pb-2"
|
|
>
|
|
{{ $t('helpCenter.whatsNew') }}
|
|
</h3>
|
|
|
|
<!-- Release Items -->
|
|
<div
|
|
v-if="hasReleases"
|
|
role="group"
|
|
:aria-label="$t('help.recentReleases')"
|
|
>
|
|
<article
|
|
v-for="release in releaseStore.recentReleases"
|
|
:key="release.id || release.version"
|
|
class="release-menu-item flex h-12 min-h-6 cursor-pointer items-center gap-2 self-stretch rounded-sm p-2 transition-colors hover:bg-interface-menu-component-surface-hovered"
|
|
role="button"
|
|
tabindex="0"
|
|
@click="onReleaseClick(release)"
|
|
@keydown.enter="onReleaseClick(release)"
|
|
@keydown.space.prevent="onReleaseClick(release)"
|
|
>
|
|
<i class="help-menu-icon icon-[lucide--package]" aria-hidden="true" />
|
|
<div class="release-content">
|
|
<span class="release-title">
|
|
{{
|
|
$t('g.releaseTitle', {
|
|
package: 'Comfy',
|
|
version: release.version
|
|
})
|
|
}}
|
|
</span>
|
|
<time class="release-date" :datetime="release.published_at">
|
|
<span class="normal-state">
|
|
{{ formatReleaseDate(release.published_at) }}
|
|
</span>
|
|
<span class="hover-state">
|
|
{{ $t('helpCenter.clickToLearnMore') }}
|
|
</span>
|
|
</time>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div
|
|
v-else-if="releaseStore.isLoading"
|
|
class="help-menu-item"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
<i class="pi pi-spin pi-spinner help-menu-icon" aria-hidden="true" />
|
|
<span>{{ $t('helpCenter.loadingReleases') }}</span>
|
|
</div>
|
|
|
|
<!-- No Releases State -->
|
|
<div v-else class="help-menu-item" role="status">
|
|
<i class="pi pi-info-circle help-menu-icon" aria-hidden="true" />
|
|
<span>{{ $t('helpCenter.noRecentReleases') }}</span>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import type { CSSProperties, Component } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
|
import { useExternalLink } from '@/composables/useExternalLink'
|
|
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
|
|
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { electronAPI } from '@/utils/envUtil'
|
|
import { formatVersionAnchor } from '@/utils/formatUtil'
|
|
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
|
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
|
|
|
// Types
|
|
interface MenuItem {
|
|
key: string
|
|
icon?: string | Component
|
|
label?: string
|
|
action?: () => void
|
|
visible?: boolean
|
|
type?: 'item' | 'divider'
|
|
items?: MenuItem[]
|
|
showRedDot?: boolean
|
|
showExternalIcon?: boolean
|
|
}
|
|
|
|
// Constants
|
|
const TIME_UNITS = {
|
|
MINUTE: 60 * 1000,
|
|
HOUR: 60 * 60 * 1000,
|
|
DAY: 24 * 60 * 60 * 1000,
|
|
WEEK: 7 * 24 * 60 * 60 * 1000,
|
|
MONTH: 30 * 24 * 60 * 60 * 1000,
|
|
YEAR: 365 * 24 * 60 * 60 * 1000
|
|
} as const
|
|
|
|
const SUBMENU_CONFIG = {
|
|
DELAY_MS: 100,
|
|
OFFSET_PX: 8,
|
|
Z_INDEX: 10001
|
|
} as const
|
|
|
|
// Composables
|
|
const { t } = useI18n()
|
|
const toast = useToast()
|
|
const { staticUrls, buildDocsUrl } = useExternalLink()
|
|
const releaseStore = useReleaseStore()
|
|
const commandStore = useCommandStore()
|
|
const settingStore = useSettingStore()
|
|
const telemetry = useTelemetry()
|
|
|
|
// Track when help center was opened
|
|
const openedAt = ref(Date.now())
|
|
|
|
// Emits
|
|
const emit = defineEmits<{
|
|
close: []
|
|
}>()
|
|
|
|
// State
|
|
const isSubmenuVisible = ref(false)
|
|
const submenuRef = ref<HTMLElement | null>(null)
|
|
const submenuStyle = ref<CSSProperties>({})
|
|
let hoverTimeout: number | null = null
|
|
|
|
// Computed
|
|
const hasReleases = computed(() => releaseStore.releases.length > 0)
|
|
const showVersionUpdates = computed(() =>
|
|
settingStore.get('Comfy.Notification.ShowVersionUpdates')
|
|
)
|
|
|
|
// Use conflict acknowledgment state from composable
|
|
const { shouldShowRedDot: shouldShowManagerRedDot } =
|
|
useConflictAcknowledgment()
|
|
const { isNewManagerUI } = useManagerState()
|
|
|
|
const moreItems = computed<MenuItem[]>(() => {
|
|
const allMoreItems: MenuItem[] = [
|
|
{
|
|
key: 'desktop-guide',
|
|
type: 'item',
|
|
label: t('helpCenter.desktopUserGuide'),
|
|
visible: isDesktop,
|
|
action: () => {
|
|
trackResourceClick('docs', true)
|
|
openExternalLink(
|
|
buildDocsUrl('/installation/desktop', {
|
|
includeLocale: true,
|
|
platform: true
|
|
})
|
|
)
|
|
emit('close')
|
|
}
|
|
},
|
|
{
|
|
key: 'dev-tools',
|
|
type: 'item',
|
|
label: t('helpCenter.openDevTools'),
|
|
visible: isDesktop,
|
|
action: () => {
|
|
openDevTools()
|
|
emit('close')
|
|
}
|
|
},
|
|
{
|
|
key: 'divider-1',
|
|
type: 'divider',
|
|
visible: isDesktop
|
|
},
|
|
{
|
|
key: 'reinstall',
|
|
type: 'item',
|
|
label: t('helpCenter.reinstall'),
|
|
visible: isDesktop,
|
|
action: () => {
|
|
onReinstall()
|
|
emit('close')
|
|
}
|
|
}
|
|
]
|
|
|
|
// Filter for visible items only
|
|
return allMoreItems.filter((item) => item.visible !== false)
|
|
})
|
|
|
|
const hasVisibleMoreItems = computed(() => {
|
|
return !!moreItems.value.length
|
|
})
|
|
|
|
const moreMenuItem = computed(() =>
|
|
menuItems.value.find((item) => item.key === 'more')
|
|
)
|
|
|
|
const menuItems = computed<MenuItem[]>(() => {
|
|
const items: MenuItem[] = [
|
|
{
|
|
key: 'feedback',
|
|
type: 'item',
|
|
icon: 'icon-[lucide--clipboard-pen]',
|
|
label: t('helpCenter.feedback'),
|
|
action: () => {
|
|
trackResourceClick('help_feedback', false)
|
|
void commandStore.execute('Comfy.ContactSupport')
|
|
emit('close')
|
|
}
|
|
},
|
|
{
|
|
key: 'help',
|
|
type: 'item',
|
|
icon: 'icon-[lucide--message-circle-question]',
|
|
label: t('helpCenter.help'),
|
|
action: () => {
|
|
trackResourceClick('help_feedback', false)
|
|
void commandStore.execute('Comfy.ContactSupport')
|
|
emit('close')
|
|
}
|
|
},
|
|
{
|
|
key: 'docs',
|
|
type: 'item',
|
|
icon: 'icon-[lucide--book-open]',
|
|
label: t('helpCenter.docs'),
|
|
showExternalIcon: true,
|
|
action: () => {
|
|
trackResourceClick('docs', true)
|
|
const path = isCloud ? '/get_started/cloud' : '/'
|
|
openExternalLink(buildDocsUrl(path, { includeLocale: true }))
|
|
emit('close')
|
|
}
|
|
},
|
|
{
|
|
key: 'discord',
|
|
type: 'item',
|
|
icon: 'pi pi-discord',
|
|
label: 'Discord',
|
|
showExternalIcon: true,
|
|
action: () => {
|
|
trackResourceClick('discord', true)
|
|
openExternalLink(staticUrls.discord)
|
|
emit('close')
|
|
}
|
|
},
|
|
{
|
|
key: 'github',
|
|
type: 'item',
|
|
icon: 'icon-[lucide--github]',
|
|
label: t('helpCenter.github'),
|
|
showExternalIcon: true,
|
|
action: () => {
|
|
trackResourceClick('github', true)
|
|
openExternalLink(staticUrls.github)
|
|
emit('close')
|
|
}
|
|
}
|
|
]
|
|
|
|
// Extension manager - only in non-cloud distributions
|
|
if (!isCloud) {
|
|
items.push({
|
|
key: 'manager',
|
|
type: 'item',
|
|
icon: PuzzleIcon,
|
|
label: t('helpCenter.managerExtension'),
|
|
showRedDot: shouldShowManagerRedDot.value,
|
|
action: async () => {
|
|
trackResourceClick('manager', false)
|
|
await useManagerState().openManager({
|
|
initialTab: ManagerTab.All,
|
|
showToastOnLegacyError: false
|
|
})
|
|
emit('close')
|
|
}
|
|
})
|
|
}
|
|
// Update ComfyUI - only for non-desktop, non-cloud with new manager UI
|
|
if (!isDesktop && !isCloud && isNewManagerUI.value) {
|
|
items.push({
|
|
key: 'update-comfyui',
|
|
type: 'item',
|
|
icon: 'icon-[lucide--download]',
|
|
label: t('helpCenter.updateComfyUI'),
|
|
action: () => {
|
|
onUpdateComfyUI()
|
|
emit('close')
|
|
}
|
|
})
|
|
}
|
|
|
|
items.push({
|
|
key: 'more',
|
|
type: 'item',
|
|
icon: '',
|
|
label: t('helpCenter.more'),
|
|
visible: hasVisibleMoreItems.value,
|
|
action: () => {}, // No action for more item
|
|
items: moreItems.value
|
|
})
|
|
|
|
return items
|
|
})
|
|
|
|
// Utility Functions
|
|
const trackResourceClick = (
|
|
resourceType:
|
|
| 'docs'
|
|
| 'discord'
|
|
| 'github'
|
|
| 'help_feedback'
|
|
| 'manager'
|
|
| 'release_notes',
|
|
isExternal: boolean
|
|
): void => {
|
|
telemetry?.trackHelpResourceClicked({
|
|
resource_type: resourceType,
|
|
is_external: isExternal,
|
|
source: 'help_center'
|
|
})
|
|
}
|
|
|
|
const openExternalLink = (url: string): void => {
|
|
window.open(url, '_blank', 'noopener,noreferrer')
|
|
}
|
|
|
|
const clearHoverTimeout = (): void => {
|
|
if (hoverTimeout) {
|
|
clearTimeout(hoverTimeout)
|
|
hoverTimeout = null
|
|
}
|
|
}
|
|
|
|
const calculateSubmenuPosition = (button: HTMLElement): CSSProperties => {
|
|
const rect = button.getBoundingClientRect()
|
|
const submenuWidth = 210 // Width defined in CSS
|
|
|
|
// Get actual submenu height if available, otherwise estimate based on visible item count
|
|
const visibleItemCount =
|
|
moreMenuItem.value?.items?.filter((item) => item.visible !== false)
|
|
.length || 0
|
|
const estimatedHeight = visibleItemCount * 48 + 16 // ~48px per item + padding
|
|
const submenuHeight = submenuRef.value?.offsetHeight || estimatedHeight
|
|
|
|
// Get viewport dimensions
|
|
const viewportWidth = window.innerWidth
|
|
const viewportHeight = window.innerHeight
|
|
|
|
// Calculate basic position (aligned with button)
|
|
let top = rect.top
|
|
let left = rect.right + SUBMENU_CONFIG.OFFSET_PX
|
|
|
|
// Check if submenu would overflow viewport on the right
|
|
if (left + submenuWidth > viewportWidth) {
|
|
// Position submenu to the left of the button instead
|
|
left = rect.left - submenuWidth - SUBMENU_CONFIG.OFFSET_PX
|
|
}
|
|
|
|
// Check if submenu would overflow viewport at the bottom
|
|
if (top + submenuHeight > viewportHeight) {
|
|
// Position submenu above the button, aligned to bottom
|
|
top = Math.max(
|
|
SUBMENU_CONFIG.OFFSET_PX, // Minimum distance from top of viewport
|
|
rect.bottom - submenuHeight
|
|
)
|
|
}
|
|
|
|
// Ensure submenu doesn't go above viewport
|
|
if (top < SUBMENU_CONFIG.OFFSET_PX) {
|
|
top = SUBMENU_CONFIG.OFFSET_PX
|
|
}
|
|
|
|
top -= 8
|
|
|
|
return {
|
|
position: 'fixed',
|
|
top: `${top}px`,
|
|
left: `${left}px`,
|
|
zIndex: SUBMENU_CONFIG.Z_INDEX
|
|
}
|
|
}
|
|
|
|
const formatReleaseDate = (dateString?: string): string => {
|
|
if (!dateString) return 'date'
|
|
|
|
const date = new Date(dateString)
|
|
const now = new Date()
|
|
const diffTime = Math.abs(now.getTime() - date.getTime())
|
|
|
|
const timeUnits = [
|
|
{ unit: TIME_UNITS.YEAR, key: 'yearsAgo' },
|
|
{ unit: TIME_UNITS.MONTH, key: 'monthsAgo' },
|
|
{ unit: TIME_UNITS.WEEK, key: 'weeksAgo' },
|
|
{ unit: TIME_UNITS.DAY, key: 'daysAgo' },
|
|
{ unit: TIME_UNITS.HOUR, key: 'hoursAgo' },
|
|
{ unit: TIME_UNITS.MINUTE, key: 'minutesAgo' }
|
|
]
|
|
|
|
for (const { unit, key } of timeUnits) {
|
|
const value = Math.floor(diffTime / unit)
|
|
if (value > 0) {
|
|
return t(`g.relativeTime.${key}`, { count: value })
|
|
}
|
|
}
|
|
|
|
return t('g.relativeTime.now')
|
|
}
|
|
|
|
// Event Handlers
|
|
const onMenuItemHover = async (
|
|
key: string,
|
|
event: MouseEvent
|
|
): Promise<void> => {
|
|
if (key !== 'more' || !moreMenuItem.value?.items) return
|
|
|
|
// Don't show submenu if all items are hidden
|
|
const hasVisibleItems = moreMenuItem.value.items.some(
|
|
(item) => item.visible !== false
|
|
)
|
|
if (!hasVisibleItems) return
|
|
|
|
clearHoverTimeout()
|
|
|
|
const moreButton = event.currentTarget as HTMLElement
|
|
|
|
// Calculate initial position before showing submenu
|
|
submenuStyle.value = calculateSubmenuPosition(moreButton)
|
|
|
|
// Show submenu with correct position
|
|
isSubmenuVisible.value = true
|
|
|
|
// After submenu is rendered, refine position if needed
|
|
await nextTick()
|
|
if (submenuRef.value) {
|
|
submenuStyle.value = calculateSubmenuPosition(moreButton)
|
|
}
|
|
}
|
|
|
|
const onMenuItemLeave = (key: string): void => {
|
|
if (key !== 'more') return
|
|
|
|
hoverTimeout = window.setTimeout(() => {
|
|
isSubmenuVisible.value = false
|
|
}, SUBMENU_CONFIG.DELAY_MS)
|
|
}
|
|
|
|
const onSubmenuHover = (): void => {
|
|
clearHoverTimeout()
|
|
}
|
|
|
|
const onSubmenuLeave = (): void => {
|
|
isSubmenuVisible.value = false
|
|
}
|
|
|
|
const openDevTools = (): void => {
|
|
if (isDesktop) {
|
|
electronAPI().openDevTools()
|
|
}
|
|
}
|
|
|
|
const onReinstall = (): void => {
|
|
if (isDesktop) {
|
|
void electronAPI().reinstall()
|
|
}
|
|
}
|
|
|
|
const onUpdateComfyUI = async (): Promise<void> => {
|
|
const { updateComfyUI, rebootComfyUI, error } = useComfyManagerService()
|
|
|
|
toast.add({
|
|
severity: 'info',
|
|
summary: t('helpCenter.updateComfyUIStarted'),
|
|
detail: t('helpCenter.updateComfyUIStartedDetail'),
|
|
life: 3000
|
|
})
|
|
|
|
try {
|
|
const result = await updateComfyUI({ is_stable: true })
|
|
|
|
if (result === null || error.value) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: t('g.error'),
|
|
detail: error.value || t('helpCenter.updateComfyUIFailed'),
|
|
life: 5000
|
|
})
|
|
return
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: t('helpCenter.updateComfyUISuccess'),
|
|
detail: t('helpCenter.updateComfyUISuccessDetail'),
|
|
life: 3000
|
|
})
|
|
|
|
await rebootComfyUI()
|
|
} catch (err) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: t('g.error'),
|
|
detail: err instanceof Error ? err.message : t('g.unknownError'),
|
|
life: 5000
|
|
})
|
|
}
|
|
}
|
|
|
|
const onReleaseClick = (release: ReleaseNote): void => {
|
|
trackResourceClick('release_notes', true)
|
|
void releaseStore.handleShowChangelog(release.version)
|
|
const versionAnchor = formatVersionAnchor(release.version)
|
|
const changelogUrl = `${buildDocsUrl('/changelog', { includeLocale: true })}#${versionAnchor}`
|
|
openExternalLink(changelogUrl)
|
|
emit('close')
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(async () => {
|
|
telemetry?.trackHelpCenterOpened({ source: 'sidebar' })
|
|
if (!hasReleases.value) {
|
|
await releaseStore.fetchReleases()
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
const timeSpentSeconds = Math.round((Date.now() - openedAt.value) / 1000)
|
|
telemetry?.trackHelpCenterClosed({ time_spent_seconds: timeSpentSeconds })
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.help-center-menu {
|
|
width: 256px;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
background: var(--interface-menu-surface);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 0.1);
|
|
border: 1px solid var(--interface-menu-stroke);
|
|
padding: 12px 8px;
|
|
position: relative;
|
|
}
|
|
|
|
.help-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
height: 32px;
|
|
min-height: 24px;
|
|
padding: 8px;
|
|
gap: 8px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
font-size: 0.9rem;
|
|
color: var(--text-primary);
|
|
text-align: left;
|
|
}
|
|
|
|
.help-menu-item:hover {
|
|
background-color: var(--interface-menu-component-surface-hovered);
|
|
}
|
|
|
|
.help-menu-item:focus,
|
|
.help-menu-item:focus-visible {
|
|
outline: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.help-menu-icon-container {
|
|
position: relative;
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.help-menu-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
font-size: 16px;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.help-menu-icon svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.menu-red-dot {
|
|
position: absolute;
|
|
top: -2px;
|
|
right: -2px;
|
|
width: 8px;
|
|
height: 8px;
|
|
background: #ff3b30;
|
|
border-radius: 50%;
|
|
border: 1.5px solid var(--p-content-background);
|
|
z-index: 1;
|
|
}
|
|
|
|
.menu-label {
|
|
flex: 1;
|
|
}
|
|
|
|
.more-item {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.section-description {
|
|
color: var(--text-secondary);
|
|
font-family: var(--font-inter);
|
|
font-size: 12px;
|
|
font-style: normal;
|
|
font-weight: 700;
|
|
line-height: normal;
|
|
margin: 0;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.release-menu-item {
|
|
position: relative;
|
|
}
|
|
|
|
.release-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.release-title {
|
|
font-size: 0.9rem;
|
|
line-height: 1.2;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.release-date {
|
|
height: 16px;
|
|
color: var(--text-secondary);
|
|
font-family: var(--font-inter);
|
|
font-size: 12px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 1;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.release-date .hover-state {
|
|
display: none;
|
|
}
|
|
|
|
.release-menu-item:hover .release-date .normal-state,
|
|
.release-menu-item:focus-within .release-date .normal-state {
|
|
display: none;
|
|
}
|
|
|
|
.release-menu-item:hover .release-date .hover-state,
|
|
.release-menu-item:focus-within .release-date .hover-state {
|
|
display: inline;
|
|
}
|
|
|
|
/* Submenu Styles */
|
|
.more-submenu {
|
|
width: 210px;
|
|
padding: 12px 8px;
|
|
background: var(--interface-menu-surface);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--interface-menu-stroke);
|
|
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 0.1);
|
|
overflow: hidden;
|
|
transition: opacity 0.15s ease-out;
|
|
}
|
|
|
|
.submenu-item {
|
|
padding: 8px;
|
|
height: 32px;
|
|
min-height: 24px;
|
|
border-radius: 4px;
|
|
color: var(--text-primary);
|
|
font-size: 0.9rem;
|
|
font-weight: inherit;
|
|
line-height: inherit;
|
|
}
|
|
|
|
.submenu-item:hover {
|
|
background-color: var(--interface-menu-component-surface-hovered);
|
|
}
|
|
|
|
.submenu-item:focus,
|
|
.submenu-item:focus-visible {
|
|
outline: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.submenu-divider {
|
|
height: 1px;
|
|
background: var(--interface-menu-stroke);
|
|
margin: 4px 0;
|
|
}
|
|
|
|
/* Scrollbar Styling */
|
|
.help-center-menu::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.help-center-menu::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.help-center-menu::-webkit-scrollbar-thumb {
|
|
background: var(--interface-menu-stroke);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.help-center-menu::-webkit-scrollbar-thumb:hover {
|
|
background: var(--text-secondary);
|
|
}
|
|
|
|
/* Reduced Motion */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.help-menu-item {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|