Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions openless-all/app/src/components/FloatingShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ import {
} from '../lib/providerSetup';
import { type SettingsSectionId } from './SettingsModal';
import { MobileMoreSheet } from './MobileMoreSheet';
import { MobileStyleSheet } from './MobileStyleSheet';
import { subItemLabelKey } from '../lib/navLabels';
import { useMobileLayout } from '../lib/useMobileLayout';
import { useAppState, type AppTab } from '../state/useAppState';

const MORE_TAB_IDS: AppTab[] = ['vocab', 'translation', 'selectionAsk'];
const STYLE_TAB_IDS: AppTab[] = ['style', 'marketplace'];

/** macOS 上侧栏顶部需让开原生红绿灯的高度(红绿灯竖直落在 ~6–22px)。 */
const MAC_TRAFFIC_LIGHT_CLEARANCE = 30;
Expand Down Expand Up @@ -71,13 +74,6 @@ const NAV_TREE: NavNode[] = [
{ kind: 'group', key: 'tools', icon: 'selectionAsk', children: [{ id: 'selectionAsk' }, { id: 'translation' }] },
];

/** 分组标题的 i18n key(nav.group.<key>)。子项标题:style 组用 nav.polishMode/nav.marketplace,
* 其余子项复用 nav.<id>。 */
function subItemLabelKey(id: AppTab): string {
if (id === 'style') return 'nav.polishMode';
return `nav.${id}`;
}

interface FloatingShellProps {
os?: OS;
initialTab?: AppTab;
Expand All @@ -101,6 +97,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
const [providerPromptOpen, setProviderPromptOpen] = useState(false);
const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false);
const [moreOpen, setMoreOpen] = useState(false);
const [styleOpen, setStyleOpen] = useState(false);

// tab 切换的 cross-fade:旧页 blur+fade out(180ms),结束后挂载新页(走 ol-page-slide enter)。
// displayTab 是实际渲染的 tab,currentTab 是用户点中的目标 tab。
Expand Down Expand Up @@ -181,6 +178,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
setSettingsInitialSection(section);
setSettingsOpen(true);
setMoreOpen(false);
setStyleOpen(false);
};

// ⌘, 打开设置页面
Expand Down Expand Up @@ -210,6 +208,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
? t('shell.footer.settings')
: t(subItemLabelKey(currentTab));
const moreTabActive = MORE_TAB_IDS.includes(currentTab);
const styleTabActive = STYLE_TAB_IDS.includes(currentTab);

return (
// 不再为 macOS 红绿灯预留顶部 28px 空条(用户反馈「块上方多一条丑横条」):
Expand Down Expand Up @@ -467,12 +466,31 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
currentTab={currentTab}
moreOpen={moreOpen}
moreTabActive={moreTabActive}
styleOpen={styleOpen}
styleTabActive={styleTabActive}
settingsOpen={settingsOpen}
onSelectTab={id => {
setMoreOpen(false);
setStyleOpen(false);
setCurrentTab(id);
}}
onOpenStyle={() => {
setMoreOpen(false);
setStyleOpen(true);
}}
onOpenMore={() => {
setStyleOpen(false);
setMoreOpen(true);
}}
/>
<MobileStyleSheet
open={styleOpen}
currentTab={currentTab}
onClose={() => setStyleOpen(false)}
onSelectTab={id => {
setStyleOpen(false);
setCurrentTab(id);
}}
onOpenMore={() => setMoreOpen(true)}
/>
<MobileMoreSheet
open={moreOpen}
Expand Down Expand Up @@ -571,7 +589,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
const MOBILE_BOTTOM_TABS: Array<{ id: AppTab; icon: string }> = [
{ id: 'overview', icon: 'overview' },
{ id: 'history', icon: 'history' },
{ id: 'style', icon: 'style' },
];

function MobileTopBar({
Expand Down Expand Up @@ -646,19 +663,26 @@ function MobileBottomNav({
currentTab,
moreOpen,
moreTabActive,
styleOpen,
styleTabActive,
settingsOpen,
onSelectTab,
onOpenStyle,
onOpenMore,
}: {
currentTab: AppTab;
moreOpen: boolean;
moreTabActive: boolean;
styleOpen: boolean;
styleTabActive: boolean;
settingsOpen: boolean;
onSelectTab: (tab: AppTab) => void;
onOpenStyle: () => void;
onOpenMore: () => void;
}) {
const { t } = useTranslation();
const moreActive = moreOpen || moreTabActive;
const styleActive = !settingsOpen && (styleOpen || styleTabActive);

return (
<nav
Expand Down Expand Up @@ -692,6 +716,15 @@ function MobileBottomNav({
</button>
);
})}
<button
type="button"
onClick={onOpenStyle}
className={styleActive ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
style={mobileNavBtnStyle}
>
<Icon name="style" size={18} />
<span style={{ fontSize: 10.5, fontWeight: styleActive ? 600 : 500 }}>{t('nav.group.style')}</span>
</button>
<button
type="button"
onClick={onOpenMore}
Expand Down
115 changes: 115 additions & 0 deletions openless-all/app/src/components/MobileStyleSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from './Icon';
import { subItemLabelKey } from '../lib/navLabels';
import type { AppTab } from '../state/useAppState';

const STYLE_TABS: Array<{ id: AppTab; icon: string }> = [
{ id: 'style', icon: 'style' },
{ id: 'marketplace', icon: 'sparkle' },
];

interface MobileStyleSheetProps {
open: boolean;
currentTab: AppTab;
onClose: () => void;
onSelectTab: (tab: AppTab) => void;
}

export function MobileStyleSheet({
open,
currentTab,
onClose,
onSelectTab,
}: MobileStyleSheetProps) {
const { t } = useTranslation();
if (!open) return null;

return (
<div
onClick={onClose}
style={{
position: 'absolute',
inset: 0,
zIndex: 60,
background: 'rgba(15,17,22,0.32)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
animation: 'ol-mobile-sheet-backdrop 0.2s var(--ol-motion-soft)',
}}
>
<div
onClick={e => e.stopPropagation()}
style={{
background: 'var(--ol-surface)',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
border: '0.5px solid var(--ol-line)',
padding: '12px 12px calc(12px + env(safe-area-inset-bottom, 0px))',
boxShadow: '0 -8px 32px -8px rgba(15,17,22,0.18)',
animation: 'ol-mobile-sheet-up 0.26s var(--ol-motion-spring)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px 12px' }}>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--ol-ink)' }}>{t('nav.group.style')}</span>
<button
type="button"
onClick={onClose}
aria-label={t('common.close')}
style={iconBtnStyle}
>
<Icon name="close" size={16} />
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{STYLE_TABS.map(item => {
const active = currentTab === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => {
onSelectTab(item.id);
onClose();
}}
className={active ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
style={rowBtnStyle}
>
<Icon name={item.icon} size={16} />
<span style={{ flex: 1 }}>{t(subItemLabelKey(item.id))}</span>
</button>
);
})}
</div>
</div>
</div>
);
}

const iconBtnStyle: CSSProperties = {
width: 32,
height: 32,
border: 0,
borderRadius: 999,
background: 'transparent',
color: 'var(--ol-ink-3)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'default',
};

const rowBtnStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 14px',
borderRadius: 10,
border: 0,
background: 'transparent',
fontFamily: 'inherit',
fontSize: 14,
cursor: 'default',
textAlign: 'left',
};
7 changes: 7 additions & 0 deletions openless-all/app/src/lib/navLabels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { AppTab } from '../state/useAppState';

/** 分组子项标题的 i18n key:style → nav.polishMode,其余 nav.<id>。 */
export function subItemLabelKey(id: AppTab): string {
if (id === 'style') return 'nav.polishMode';
return `nav.${id}`;
}
Loading