fix(ui): keep workspace and admin usable on narrow screens
Admin sidebar becomes a dismissible drawer below md; the chat side panel gains a scrim and Escape close; the global search overlay reserves a gutter in every leading bar and collapses to an icon below lg; fixed grids and table cells now wrap or scroll instead of overflowing.
ZhenYi committed
Modified admin/src/app/layout.tsx admin/src/app/layout.tsx +41 −9 View admin/src/app/layout.tsx
@@ -13,7 +13,9 @@
1313 Sun,
1414 Users,
1515} from 'lucide-react'
16import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom'
16import { useEffect, useState } from 'react'
17import { Menu, X } from 'lucide-react'
18import { NavLink, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom'
1719import { Cpu } from 'lucide-react'
1820import { useSession } from '@/app/session'
1921import { useTheme } from '@/app/theme'
@@ -48,7 +50,12 @@
4850 const { me, loading, logout } = useSession()
4951 const { dark, toggle } = useTheme()
5052 const navigate = useNavigate()
53 const location = useLocation()
54 const [menuOpen, setMenuOpen] = useState(false)
5155
56 // The drawer belongs to the current page only: any navigation closes it.
57 useEffect(() => { setMenuOpen(false) }, [location.pathname])
58
5259 if (loading) {
5360 return <Loading label="正在校验管理会话…" />
5461 }
@@ -61,10 +68,30 @@
6168
6269 return (
6370 <div className="flex min-h-full">
64 <aside className="hidden w-56 shrink-0 flex-col border-r border-slate-200 bg-white p-3 dark:border-slate-800 dark:bg-slate-950 md:flex">
65 <div className="mb-4 px-2">
66 <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">GitData</div>
67 <div className="text-xs text-slate-500">管理后台</div>
71 {menuOpen ? (
72 <button
73 type="button"
74 aria-label="关闭导航"
75 tabIndex={-1}
76 onClick={() => setMenuOpen(false)}
77 className="fixed inset-0 z-40 bg-slate-900/40 md:hidden"
78 />
79 ) : null}
80 <aside
81 className={`fixed inset-y-0 left-0 z-50 flex w-56 shrink-0 flex-col overflow-y-auto border-r border-slate-200 bg-white p-3 transition-transform duration-200 dark:border-slate-800 dark:bg-slate-950 md:static md:z-auto md:translate-x-0 ${
82 menuOpen ? 'translate-x-0' : 'invisible -translate-x-full md:visible md:translate-x-0'
83 }`}
84 >
85 <div className="mb-4 flex items-start justify-between gap-2 px-2">
86 <div>
87 <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">GitData</div>
88 <div className="text-xs text-slate-500">管理后台</div>
89 </div>
90 <span className="md:hidden">
91 <Button variant="ghost" size="sm" title="关闭导航" onClick={() => setMenuOpen(false)}>
92 <X size={16} />
93 </Button>
94 </span>
6895 </div>
6996 <nav className="flex flex-1 flex-col gap-1">
7097 {NAV_ITEMS.map((item) => (
@@ -91,12 +118,17 @@
91118 </aside>
92119
93120 <div className="flex min-w-0 flex-1 flex-col">
94 <header className="flex items-center justify-between gap-3 border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950">
95 <div className="flex items-center gap-2">
96 <span className="font-medium text-slate-800 dark:text-slate-100">{me.display_name}</span>
121 <header className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950">
122 <div className="flex min-w-0 items-center gap-2">
123 <span className="md:hidden">
124 <Button variant="ghost" size="sm" title="打开导航" onClick={() => setMenuOpen(true)}>
125 <Menu size={18} />
126 </Button>
127 </span>
128 <span className="truncate font-medium text-slate-800 dark:text-slate-100">{me.display_name}</span>
97129 <Badge tone={me.role === 'super_admin' ? 'danger' : 'info'}>{roleLabel(me.role)}</Badge>
98130 </div>
99 <div className="flex items-center gap-2">
131 <div className="flex flex-wrap items-center gap-2">
100132 <Button variant="ghost" size="sm" title="切换主题" onClick={toggle}>
101133 {dark ? <Sun size={16} /> : <Moon size={16} />}
102134 </Button>
Modified admin/src/pages/dashboard.tsx admin/src/pages/dashboard.tsx +4 −4 View admin/src/pages/dashboard.tsx
@@ -14,7 +14,7 @@
1414}) {
1515 return (
1616 <Card title={name}>
17 <div className="flex items-center justify-between">
17 <div className="flex flex-wrap items-center justify-between gap-2">
1818 <Badge tone={dependency.ok ? 'success' : 'danger'}>{dependency.ok ? '正常' : '异常'}</Badge>
1919 <span className="text-xs text-slate-500">
2020 {dependency.latency_ms === null ? '-' : `${dependency.latency_ms} ms`}
@@ -123,10 +123,10 @@
123123
124124 return (
125125 <div className="space-y-4">
126 <div className="flex items-center justify-between">
127 <div>
126 <div className="flex flex-wrap items-center justify-between gap-2">
127 <div className="min-w-0">
128128 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">系统状态</h1>
129 <p className="text-xs text-slate-500">
129 <p className="break-words text-xs text-slate-500">
130130 adminhost v{status.process.version} · 运行 {formatUptime(status.process.uptime_secs)} · 更新于{' '}
131131 {formatDateTime(dashboard.generated_at)}
132132 </p>
Modified admin/src/shared/ui.tsx admin/src/shared/ui.tsx +3 −3 View admin/src/shared/ui.tsx
@@ -54,7 +54,7 @@
5454}) {
5555 return (
5656 <section
57 className={`rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900 ${className ?? ''}`}
57 className={`min-w-0 rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900 ${className ?? ''}`}
5858 >
5959 {(title || action) && (
6060 <header className="mb-3 flex items-center justify-between gap-3">
@@ -78,7 +78,7 @@
7878 info: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
7979 }
8080 return (
81 <span className={`inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${tones[tone]}`}>
81 <span className={`inline-flex items-center whitespace-nowrap rounded px-1.5 py-0.5 text-xs font-medium ${tones[tone]}`}>
8282 {children}
8383 </span>
8484 )
@@ -223,7 +223,7 @@
223223 <div className="overflow-x-auto">
224224 <table className="w-full min-w-[640px] border-collapse text-sm">
225225 <thead>
226 <tr className="border-b border-slate-200 text-left text-xs uppercase tracking-wide text-slate-500 dark:border-slate-700 dark:text-slate-400">
226 <tr className="border-b border-slate-200 text-left text-xs uppercase tracking-wide text-slate-500 dark:border-slate-700 dark:text-slate-400 [&>th]:whitespace-nowrap">
227227 {columns.map((column) => (
228228 <th key={column.key} className={`px-2 py-2 font-medium ${column.className ?? ''}`}>
229229 {column.title}
Modified web/src/app/styles/workspace.css web/src/app/styles/workspace.css +17 −1 View web/src/app/styles/workspace.css
@@ -53,6 +53,19 @@
5353 */
5454.workspace-main :is(.shell-top-bar, .chat-header) { transition: padding-left var(--nav-motion) var(--nav-ease); }
5555.navigation-collapsed .workspace-main :is(.shell-top-bar, .chat-header) { padding-left: 64px; }
56/*
57 * The global search box is a fixed overlay pinned to the top-right of the
58 * window, so every bar that can lead the content column reserves a gutter for
59 * it — otherwise it sits on top of the bar's own actions. Below `lg` the
60 * overlay collapses to its icon, so the gutter stays small enough that the bar
61 * keeps a usable title.
62 */
63@media (min-width: 768px) {
64 .workspace-main :is(.shell-top-bar, .chat-header) { padding-right: 64px; }
65}
66@media (min-width: 1024px) {
67 .workspace-main :is(.shell-top-bar, .chat-header) { padding-right: calc(clamp(200px, 22vw, 300px) + 28px); }
68}
5669.workspace-mobile-bar, .workspace-drawer-close, .workspace-scrim { display: none; }
5770.workspace-skip { position: fixed; left: 12px; top: -80px; z-index: 100; padding: 10px 16px; border-radius: 10px; color: white; background: var(--shell-action); }
5871.workspace-skip:focus { top: 12px; }
@@ -149,7 +162,9 @@
149162.repo-toolbar-refs { display: flex; min-width: 0; flex-shrink: 0; flex-wrap: wrap; gap: 8px; }
150163.repo-toolbar-clone { display: flex; flex-shrink: 0; align-items: center; gap: 8px; }
151164.repo-toolbar-clone.empty .repo-overview-toggle { margin-left: auto; }
152.repo-toolbar-tabs { display: flex; min-width: 0; align-items: center; padding-left: 12px; border-left: 1px solid var(--shell-hairline); }
165.repo-toolbar-tabs { display: flex; min-width: 0; max-width: 100%; align-items: center; overflow-x: auto; padding-left: 12px; border-left: 1px solid var(--shell-hairline); overscroll-behavior-x: contain; scrollbar-width: none; }
166.repo-toolbar-tabs::-webkit-scrollbar { display: none; }
167.repo-toolbar-tabs > nav { flex: 0 0 auto; }
153168.repo-content-layout { display: flex; flex: 1; min-height: 0; gap: 24px; padding: 16px 24px 24px; background: var(--shell-field); }
154169.repo-content-layout > aside { width: 260px; }
155170.repo-overview-toggle { display: none; }
@@ -205,6 +220,7 @@
205220 .board-toolbar { padding: 12px 16px; }
206221 .board-canvas { padding: 16px; }
207222 .repo-toolbar { padding: 10px 16px; }
223 .repo-toolbar-refs { flex: 1 1 100%; min-width: 0; }
208224 .repo-toolbar-clone { max-width: 100%; flex-wrap: wrap; margin-left: 0; }
209225 .repo-content-layout { padding: 12px 16px 16px; }
210226 .repo-content-layout.overview-open { flex-direction: column; overflow-y: auto; }
Modified web/src/features/chat/ui/chat-polish.css web/src/features/chat/ui/chat-polish.css +8 −2 View web/src/features/chat/ui/chat-polish.css
@@ -95,14 +95,16 @@
9595.chat-popover { box-shadow: 0 14px 40px rgb(24 28 31 / 16%); }
9696.chat-popover button { cursor: pointer; }
9797
98.chat-panel-scrim { display: none; }
9899@media (max-width: 1200px) {
100 .chat-panel-scrim { display: block; position: fixed; inset: 0; z-index: 24; background: rgb(18 20 22 / 40%); }
99101 .chat-side-panel { position: fixed; z-index: 25; right: 0; top: 0; bottom: 0; width: min(380px, 88vw); box-shadow: -18px 0 48px rgb(24 28 31 / 18%); }
100102}
101103@media (max-width: 760px) {
102104 .chat-header { min-height: 68px; padding: 12px 16px; }
103105 .chat-channel-icon { width: 34px; height: 34px; border-radius: 10px; }
104 .chat-header-actions > button, .chat-search-action { width: 34px; height: 34px; }
105 .chat-header-actions > button:nth-child(n + 4) { display: none; }
106 .chat-header-actions { gap: 1px; }
107 .chat-header-actions > button, .chat-search-action { width: 32px; height: 32px; }
106108 .chat-message-list { padding: 6px 8px 12px; }
107109 .chat-message-row { gap: 10px; padding-left: 8px; padding-right: 8px; }
108110 .chat-message-toolbar { right: 8px; opacity: 1; pointer-events: auto; }
@@ -111,6 +113,10 @@
111113 .chat-composer-help { display: none; }
112114 .chat-side-panel { width: 100%; }
113115}
116@media (max-width: 420px) {
117 .chat-header { gap: 8px; padding: 10px 12px; }
118 .chat-header-actions > button, .chat-search-action { width: 30px; height: 30px; }
119}
114120@media (prefers-reduced-motion: reduce) {
115121 .chat-message-toolbar, .chat-send-button { transition: none; }
116122}
Modified web/src/features/chat/ui/chat-room.tsx web/src/features/chat/ui/chat-room.tsx +16 −0 View web/src/features/chat/ui/chat-room.tsx
@@ -355,6 +355,19 @@
355355 [dmBusy, onDmOpened, viewerId],
356356 )
357357
358 // Narrow screens turn the panel into a full-height overlay; a scrim is the
359 // only affordance that lets a tap outside dismiss it again.
360 const closePanel = () => onStateChange({ panel: null, threadId: undefined, query: undefined })
361
362 useEffect(() => {
363 if (!state.panel) return
364 const onKey = (event: KeyboardEvent) => {
365 if (event.key === 'Escape') onStateChange({ panel: null, threadId: undefined, query: undefined })
366 }
367 window.addEventListener('keydown', onKey)
368 return () => window.removeEventListener('keydown', onKey)
369 }, [state.panel, onStateChange])
370
358371 const header = (
359372 <header className="chat-header">
360373 <span className="chat-channel-icon" aria-hidden={!dm}>
@@ -635,6 +648,8 @@
635648 )}
636649 </section>
637650 {state.panel && !roomGone ? (
651 <>
652 <button type="button" className="chat-panel-scrim" aria-label={t('chat.panel.close')} tabIndex={-1} onClick={closePanel} />
638653 <aside className="chat-side-panel flex w-[320px] shrink-0 flex-col border-l border-shell-hairline bg-shell-sidebar xl:w-[380px]" aria-label={t('chat.room.details')}>
639654 {state.panel === 'thread' ? (
640655 thread ? (
@@ -709,6 +724,7 @@
709724 />
710725 )}
711726 </aside>
727 </>
712728 ) : null}
713729
714730 <RoomSettingsModal
Modified web/src/features/search/ui/global-search-command.tsx web/src/features/search/ui/global-search-command.tsx +3 −3 View web/src/features/search/ui/global-search-command.tsx
@@ -112,12 +112,12 @@
112112 <button
113113 type="button"
114114 onClick={() => setOpen(true)}
115 className="fixed right-4 top-3 z-40 hidden h-9 w-[clamp(200px,22vw,300px)] items-center gap-2 rounded-lg border border-shell-hairline bg-shell-surface px-3 text-[12px] text-shell-ink-muted shadow-sm transition-colors hover:bg-shell-field md:flex"
115 className="fixed right-4 top-3 z-40 hidden h-9 w-9 items-center justify-center gap-2 rounded-lg border border-shell-hairline bg-shell-surface px-0 text-[12px] text-shell-ink-muted shadow-sm transition-colors hover:bg-shell-field md:flex lg:w-[clamp(200px,22vw,300px)] lg:justify-start lg:px-3"
116116 aria-label={t('search.command.open')}
117117 >
118118 <Search className="h-4 w-4 shrink-0" aria-hidden />
119 <span className="flex-1 truncate text-left">{t('search.command.placeholder')}</span>
120 <kbd className="shrink-0 rounded border border-shell-hairline bg-shell-field px-1.5 py-0.5 font-mono text-[10px]">⌘K</kbd>
119 <span className="hidden flex-1 truncate text-left lg:block">{t('search.command.placeholder')}</span>
120 <kbd className="hidden shrink-0 rounded border border-shell-hairline bg-shell-field px-1.5 py-0.5 font-mono text-[10px] lg:block">⌘K</kbd>
121121 </button>
122122 {open ? (
123123 <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/35 p-4 pt-[10vh]" onMouseDown={() => setOpen(false)}>
Modified web/src/features/space-management/ui/import-repo-modal.tsx web/src/features/space-management/ui/import-repo-modal.tsx +1 −1 View web/src/features/space-management/ui/import-repo-modal.tsx
@@ -281,7 +281,7 @@
281281 onChange={event => setToken(event.target.value)}
282282 />
283283 </div>
284 <div className="grid grid-cols-2 gap-3">
284 <div className="grid gap-3 sm:grid-cols-2">
285285 <div>
286286 <label className={modalLabelClass} htmlFor="import-name">
287287 {t('space.import.localName')}
Modified web/src/features/user-settings/ui/sections/account-section.tsx web/src/features/user-settings/ui/sections/account-section.tsx +1 −1 View web/src/features/user-settings/ui/sections/account-section.tsx
@@ -76,7 +76,7 @@
7676 <Field label={t('settings.account.username')} hint={t('settings.account.usernameHint')}>
7777 <input className={inputCls} value={username} onChange={e => setUsername(e.target.value.toLowerCase())} maxLength={32} />
7878 </Field>
79 <div className="grid max-w-[700px] grid-cols-3 gap-3">
79 <div className="grid max-w-[700px] gap-3 sm:grid-cols-3">
8080 <Field label={t('settings.account.language')}>
8181 <LanguagePicker
8282 className={`${inputCls} max-w-none`}
Modified web/src/features/user-settings/ui/sections/profile-section.tsx web/src/features/user-settings/ui/sections/profile-section.tsx +1 −1 View web/src/features/user-settings/ui/sections/profile-section.tsx
@@ -45,7 +45,7 @@
4545 onChange={e => set('bio', e.target.value)}
4646 />
4747 </Field>
48 <div className="grid max-w-[700px] grid-cols-2 gap-3">
48 <div className="grid max-w-[700px] gap-3 sm:grid-cols-2">
4949 <Field label={t('userSettings.profile.website')}><input className={`${inputCls} max-w-none`} value={form.website} onChange={e => set('website', e.target.value)} /></Field>
5050 <Field label={t('userSettings.profile.company')}><input className={`${inputCls} max-w-none`} value={form.company} onChange={e => set('company', e.target.value)} /></Field>
5151 <Field label={t('userSettings.profile.location')}><input className={`${inputCls} max-w-none`} value={form.location} onChange={e => set('location', e.target.value)} /></Field>