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()
|
const container = getMenuContainer()
|
||||||
injectionMap.set(target, container)
|
injectionMap.set(target, container)
|
||||||
target.before(container)
|
getNavMenuInsertionTarget(target).before(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selector = '[data-testid="accounts-profile-button"]'
|
const selector = '[data-testid="accounts-profile-button"]'
|
||||||
@@ -99,3 +99,10 @@ function getMenuContainer() {
|
|||||||
render(<Menu container={container} />, container)
|
render(<Menu container={container} />, container)
|
||||||
return 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);
|
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 {
|
.ce-card {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
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 '../style.css'
|
||||||
import './Dialog.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 }) {
|
function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -59,6 +114,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
|||||||
|
|
||||||
const width = useWindowResize(() => window.innerWidth)
|
const width = useWindowResize(() => window.innerWidth)
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768
|
||||||
|
const isCollapsedSidebar = useCollapsedSidebar(container, isMobile)
|
||||||
const Portal = isMobile ? 'div' : HoverCard.Portal
|
const Portal = isMobile ? 'div' : HoverCard.Portal
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,8 +134,11 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
|||||||
>
|
>
|
||||||
<HoverCard.Trigger>
|
<HoverCard.Trigger>
|
||||||
<MenuItem
|
<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')}
|
text={t('ExportHelper')}
|
||||||
|
ariaLabel={t('ExportHelper')}
|
||||||
icon={IconArrowRightFromBracket}
|
icon={IconArrowRightFromBracket}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
@@ -210,7 +269,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
|||||||
</HoverCard.Content>
|
</HoverCard.Content>
|
||||||
</Portal>
|
</Portal>
|
||||||
</HoverCard.Root>
|
</HoverCard.Root>
|
||||||
<Divider />
|
{!isCollapsedSidebar && <Divider />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ export interface MenuItemProps {
|
|||||||
successText?: string
|
successText?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
title?: string
|
title?: string
|
||||||
|
ariaLabel?: string
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: (() => boolean) | (() => Promise<boolean>)
|
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 [loading, setLoading] = useState(false)
|
||||||
const [succeed, setSucceed] = useState(false)
|
const [succeed, setSucceed] = useState(false)
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onTouchStart={handleClick}
|
onTouchStart={handleClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
aria-label={ariaLabel}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
@@ -63,7 +65,9 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
|
|||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
{Icon && <Icon />}
|
{Icon && <Icon />}
|
||||||
{(succeed && successText) ? successText : text}
|
<span className="ce-menu-item-text">
|
||||||
|
{(succeed && successText) ? successText : text}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user