mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-07-23 09:00:51 -05:00
fix: improve collapsed sidebar exporter UX
- Render the sidebar trigger as an icon-only button when collapsed
- Insert outside the profile trigger wrapper to avoid native tooltip overlap
- Add an accessible label while hiding collapsed trigger text
This commit is contained in:
@@ -29,7 +29,7 @@ function main() {
|
||||
|
||||
const container = getMenuContainer()
|
||||
injectionMap.set(target, container)
|
||||
target.before(container)
|
||||
getNavMenuInsertionTarget(target).before(container)
|
||||
}
|
||||
|
||||
const selector = '[data-testid="accounts-profile-button"]'
|
||||
@@ -99,3 +99,10 @@ function getMenuContainer() {
|
||||
render(<Menu container={container} />, container)
|
||||
return container
|
||||
}
|
||||
|
||||
function getNavMenuInsertionTarget(target: Element) {
|
||||
const wrapper = target.parentElement
|
||||
if (!wrapper || wrapper.children.length !== 1) return target
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
@@ -54,6 +54,37 @@ html {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
.ce-nav-trigger {
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
color: var(--ce-text-primary);
|
||||
}
|
||||
|
||||
.ce-nav-trigger .ce-menu-item-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ce-nav-trigger-collapsed {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 auto 0.5rem;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary, var(--ce-text-primary));
|
||||
}
|
||||
|
||||
.ce-nav-trigger-collapsed:hover {
|
||||
background-color: var(--sidebar-surface-secondary, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.ce-nav-trigger-collapsed .ce-menu-item-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ce-card {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
@@ -18,6 +18,61 @@ import { SettingDialog } from './SettingDialog'
|
||||
import '../style.css'
|
||||
import './Dialog.css'
|
||||
|
||||
function useCollapsedSidebar(container: HTMLDivElement, isMobile: boolean) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setIsCollapsed(false)
|
||||
return
|
||||
}
|
||||
|
||||
let frame = 0
|
||||
const observed = new Set<Element>()
|
||||
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update)
|
||||
|
||||
function sidebarElement() {
|
||||
return container.closest('nav, aside, [aria-label="Sidebar"], [data-testid="sidebar"]')
|
||||
?? container.parentElement
|
||||
}
|
||||
|
||||
function observe(element: Element | null | undefined) {
|
||||
if (!observer || !element || observed.has(element)) return
|
||||
observer.observe(element)
|
||||
observed.add(element)
|
||||
}
|
||||
|
||||
function update() {
|
||||
cancelAnimationFrame(frame)
|
||||
frame = requestAnimationFrame(() => {
|
||||
const parentWidth = container.parentElement?.getBoundingClientRect().width ?? 0
|
||||
const sidebarWidth = sidebarElement()?.getBoundingClientRect().width ?? parentWidth
|
||||
const nextCollapsed = (parentWidth > 0 && parentWidth < 96)
|
||||
|| (sidebarWidth > 0 && sidebarWidth < 96)
|
||||
|
||||
setIsCollapsed(nextCollapsed)
|
||||
container.toggleAttribute('data-ce-sidebar-collapsed', nextCollapsed)
|
||||
observe(container.parentElement)
|
||||
observe(sidebarElement())
|
||||
})
|
||||
}
|
||||
|
||||
observe(container.parentElement)
|
||||
observe(sidebarElement())
|
||||
update()
|
||||
|
||||
window.addEventListener('resize', update)
|
||||
return () => {
|
||||
cancelAnimationFrame(frame)
|
||||
window.removeEventListener('resize', update)
|
||||
observer?.disconnect()
|
||||
container.removeAttribute('data-ce-sidebar-collapsed')
|
||||
}
|
||||
}, [container, isMobile])
|
||||
|
||||
return isCollapsed
|
||||
}
|
||||
|
||||
function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -59,6 +114,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
|
||||
const width = useWindowResize(() => window.innerWidth)
|
||||
const isMobile = width < 768
|
||||
const isCollapsedSidebar = useCollapsedSidebar(container, isMobile)
|
||||
const Portal = isMobile ? 'div' : HoverCard.Portal
|
||||
|
||||
return (
|
||||
@@ -78,8 +134,11 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
>
|
||||
<HoverCard.Trigger>
|
||||
<MenuItem
|
||||
className="border-0 ms-2 me-1.5 mb-2"
|
||||
className={isCollapsedSidebar
|
||||
? 'ce-nav-trigger ce-nav-trigger-collapsed'
|
||||
: 'ce-nav-trigger border-0 ms-2 me-1.5 mb-2'}
|
||||
text={t('ExportHelper')}
|
||||
ariaLabel={t('ExportHelper')}
|
||||
icon={IconArrowRightFromBracket}
|
||||
onClick={() => {
|
||||
setOpen(true)
|
||||
@@ -210,7 +269,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
</HoverCard.Content>
|
||||
</Portal>
|
||||
</HoverCard.Root>
|
||||
<Divider />
|
||||
{!isCollapsedSidebar && <Divider />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ export interface MenuItemProps {
|
||||
successText?: string
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
ariaLabel?: string
|
||||
className?: string
|
||||
onClick?: (() => boolean) | (() => Promise<boolean>)
|
||||
}
|
||||
|
||||
export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = false, title, icon: Icon, onClick, className }) => {
|
||||
export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = false, title, ariaLabel, icon: Icon, onClick, className }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [succeed, setSucceed] = useState(false)
|
||||
|
||||
@@ -52,6 +53,7 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
|
||||
onClick={handleClick}
|
||||
onTouchStart={handleClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
title={title}
|
||||
>
|
||||
{loading
|
||||
@@ -63,7 +65,9 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
|
||||
: (
|
||||
<>
|
||||
{Icon && <Icon />}
|
||||
{(succeed && successText) ? successText : text}
|
||||
<span className="ce-menu-item-text">
|
||||
{(succeed && successText) ? successText : text}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user