Skip to content

Commit 68b4a1e

Browse files
committed
💄style: improve mobile navigation with fixed header and blur effects
1 parent 422051e commit 68b4a1e

File tree

14 files changed

+195
-171
lines changed

14 files changed

+195
-171
lines changed

app/src/components/BlinkoAi/aiChatBox.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ export const BlinkoChatBox = observer(({ shareMode = false }: { shareMode?: bool
304304

305305
return (
306306
<ScrollArea
307+
fixMobileTopBar
307308
ref={scrollAreaRef}
308309
onBottom={() => { }}
309310
className="h-full"

app/src/components/BlinkoCard/cardHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const CardHeader = observer(({ blinkoItem, blinko, isShareMode, isExpande
6161
variant='flat'
6262
size='sm'
6363
className='mr-2'
64-
onPress={() => {
64+
onPress={(e) => {
6565
window.history.back();
6666
}}
6767
>

app/src/components/BlinkoCard/expandContainer.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { eventBus } from "@/lib/event";
12
import { useIsIOS } from "@/lib/hooks";
23
import { motion } from "motion/react";
34
import { useEffect } from "react";
45
import { createPortal } from "react-dom";
6+
import { useMediaQuery } from "usehooks-ts";
57

68
interface ExpandableContainerProps {
79
isExpanded: boolean;
@@ -25,18 +27,33 @@ export const ExpandableContainer = ({ isExpanded, children, onClose, withoutBoxS
2527
} as const;
2628

2729
const isIOS = useIsIOS()
28-
2930
useEffect(() => {
3031
const handleKeyDown = (e: KeyboardEvent) => {
3132
if (e.key === 'Escape' && isExpanded) {
3233
onClose?.();
3334
}
3435
};
3536

37+
// Hide/show mobile navigation bars when expanded/collapsed
38+
const mobileHeader = document.querySelector('.blinko-mobile-header') as HTMLElement;
39+
const bottomBar = document.querySelector('.blinko-bottom-bar') as HTMLElement;
40+
41+
if (isExpanded) {
42+
if (mobileHeader) mobileHeader.style.display = 'none';
43+
if (bottomBar) bottomBar.style.display = 'none';
44+
} else {
45+
if (mobileHeader) mobileHeader.style.display = '';
46+
if (bottomBar) bottomBar.style.display = '';
47+
}
48+
3649
document.addEventListener('keydown', handleKeyDown);
3750
return () => {
3851
document.removeEventListener('keydown', handleKeyDown);
52+
// Restore navigation bars when component unmounts
53+
if (mobileHeader) mobileHeader.style.display = '';
54+
if (bottomBar) bottomBar.style.display = '';
3955
};
56+
4057
}, [isExpanded, onClose]);
4158

4259
if (isIOS) {

app/src/components/Common/ScrollArea/index.tsx

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,31 @@ import { useMediaQuery } from "usehooks-ts";
77
type IProps = {
88
style?: any;
99
className?: any;
10-
onBottom: () => void;
10+
onBottom?: () => void;
1111
onRefresh?: () => Promise<any>;
1212
children: any;
1313
pullDownThreshold?: number;
1414
maxPullDownDistance?: number;
15+
fixMobileTopBar?: boolean
1516
};
1617

1718
export type ScrollAreaHandles = {
1819
scrollToBottom: () => void;
1920
}
2021

21-
export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
22-
style,
23-
className,
24-
children,
22+
export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
23+
style,
24+
className,
25+
children,
2526
onBottom,
2627
onRefresh,
2728
pullDownThreshold = 60,
28-
maxPullDownDistance = 100
29+
maxPullDownDistance = 100,
30+
fixMobileTopBar = false
2931
}, ref) => {
3032
const scrollRef = useRef<HTMLDivElement>(null);
3133
const isPc = useMediaQuery('(min-width: 768px)');
32-
34+
3335
// Pull to refresh states
3436
const [pullDistance, setPullDistance] = useState(0);
3537
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -39,7 +41,7 @@ export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
3941
const startYRef = useRef(0);
4042
const canPullRef = useRef(true); // Initialize as true for initial state
4143
const currentInstanceRef = useRef(Math.random().toString(36)); // Unique identifier for this ScrollArea instance
42-
44+
4345
let debounceBottom;
4446
if (onBottom) {
4547
debounceBottom = _.debounce(onBottom!, 500, { leading: true, trailing: false });
@@ -57,7 +59,7 @@ export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
5759
if (bottom) {
5860
debounceBottom?.();
5961
}
60-
62+
6163
// Update can pull state
6264
canPullRef.current = target.scrollTop === 0;
6365
};
@@ -149,19 +151,19 @@ export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
149151
useEffect(() => {
150152
const divElement = scrollRef.current;
151153
if (!divElement) return;
152-
154+
153155
// Initialize canPull state
154156
canPullRef.current = divElement.scrollTop === 0;
155-
157+
156158
divElement.addEventListener("scroll", handleScroll);
157-
159+
158160
// Add pull-to-refresh listeners only if onRefresh exists AND device is mobile
159161
if (onRefresh && !isPc) {
160162
divElement.addEventListener('touchstart', handleTouchStart, { passive: false });
161163
divElement.addEventListener('touchmove', handleTouchMove, { passive: false });
162164
divElement.addEventListener('touchend', handleTouchEnd, { passive: true });
163165
}
164-
166+
165167
return () => {
166168
divElement.removeEventListener("scroll", handleScroll);
167169
if (onRefresh && !isPc) {
@@ -176,32 +178,32 @@ export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
176178
const pullProgress = Math.min(pullDistance / pullDownThreshold, 1);
177179
const arrowRotation = pullProgress * 180; // 0 to 180 degrees
178180
const isReadyToRefresh = pullDistance >= pullDownThreshold;
179-
181+
180182
const showRefreshIndicator = onRefresh && !isPc && (pullDistance > 0 || isRefreshing);
181-
const refreshText = isRefreshing
182-
? i18n.t('common.refreshing')
183-
: isReadyToRefresh
184-
? i18n.t('common.releaseToRefresh')
183+
const refreshText = isRefreshing
184+
? i18n.t('common.refreshing')
185+
: isReadyToRefresh
186+
? i18n.t('common.releaseToRefresh')
185187
: i18n.t('common.pullToRefresh');
186188

187189
// Arrow Icon Component
188190
const ArrowIcon = () => (
189-
<svg
190-
width="16"
191-
height="16"
192-
viewBox="0 0 24 24"
193-
fill="none"
191+
<svg
192+
width="16"
193+
height="16"
194+
viewBox="0 0 24 24"
195+
fill="none"
194196
className={`transition-transform duration-150 ${isDragging ? '' : 'duration-300'}`}
195-
style={{
197+
style={{
196198
transform: `rotate(${arrowRotation}deg)`,
197199
opacity: pullProgress
198200
}}
199201
>
200-
<path
201-
d="M12 5l0 14m-7-7l7-7 7 7"
202-
stroke="currentColor"
203-
strokeWidth="2"
204-
strokeLinecap="round"
202+
<path
203+
d="M12 5l0 14m-7-7l7-7 7 7"
204+
stroke="currentColor"
205+
strokeWidth="2"
206+
strokeLinecap="round"
205207
strokeLinejoin="round"
206208
/>
207209
</svg>
@@ -218,14 +220,13 @@ export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
218220
}}
219221
className={`${className} overflow-y-scroll overflow-x-hidden ${isPc ? '' : 'scrollbar-hide'} scroll-smooth scroll-area`}
220222
>
223+
{fixMobileTopBar && !isPc && <div className="h-16"></div>}
221224
{/* Pull to refresh indicator */}
222225
{showRefreshIndicator && (
223-
<div
224-
className={`flex items-center justify-center transition-all duration-150 ${
225-
isDragging ? '' : 'duration-300'
226-
} ${isReadyToRefresh ? 'text-primary' : 'text-gray-500'} ${
227-
isReadyToRefresh ? 'bg-primary/5' : 'bg-gray-50/80'
228-
}`}
226+
<div
227+
className={`flex items-center justify-center transition-all duration-150 ${isDragging ? '' : 'duration-300'
228+
} ${isReadyToRefresh ? 'text-primary' : 'text-gray-500'} ${isReadyToRefresh ? 'bg-primary/5' : 'bg-gray-50/80'
229+
}`}
229230
style={{
230231
height: `${pullDistance}px`,
231232
marginTop: `-${pullDistance}px`,
@@ -239,18 +240,17 @@ export const ScrollArea = observer(forwardRef<ScrollAreaHandles, IProps>(({
239240
) : (
240241
<ArrowIcon />
241242
)}
242-
<span
243-
className={`text-sm font-medium transition-all duration-200 ${
244-
isReadyToRefresh ? 'scale-105' : 'scale-100'
245-
}`}
243+
<span
244+
className={`text-sm font-medium transition-all duration-200 ${isReadyToRefresh ? 'scale-105' : 'scale-100'
245+
}`}
246246
style={{ opacity: Math.max(pullProgress, 0.6) }}
247247
>
248248
{refreshText}
249249
</span>
250250
</div>
251251
</div>
252252
)}
253-
253+
254254
{children}
255255
</div>
256256
);

app/src/components/Layout/MobileNavBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const MobileNavBar = observer(({ onItemClick }: MobileNavBarProps) => {
3636

3737
return (
3838
<motion.div
39-
className="h-[70px] flex w-full px-4 py-2 gap-2 bg-background block md:hidden overflow-hidden fixed bottom-0 z-50"
39+
className="blinko-bottom-bar h-[70px] flex w-full px-4 py-2 gap-2 bg-background block md:hidden overflow-hidden fixed bottom-0 z-50"
4040
animate={{ y: isVisible ? 0 : 100 }}
4141
transition={{
4242
type: "tween",

app/src/components/Layout/index.tsx

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import { BarSearchInput } from './BarSearchInput';
2424
import { BlinkoNotification } from '@/components/BlinkoNotification';
2525
import { AiStore } from '@/store/aiStore';
2626
import { useLocation, useSearchParams, Link } from 'react-router-dom';
27-
import { isDesktop } from '@/lib/tauriHelper';
28-
import { useSwipeable } from 'react-swipeable';
2927

3028
export const SideBarItem = 'p-2 flex flex-row items-center cursor-pointer gap-2 hover:bg-hover rounded-xl !transition-all';
3129

@@ -44,6 +42,13 @@ export const CommonLayout = observer(({ children, header }: { children?: React.R
4442
user.use();
4543
base.useInitApp();
4644

45+
const getFixedHeaderBackground = () => {
46+
if (document?.documentElement?.classList?.contains('dark')) {
47+
return '#00000080';
48+
}
49+
return '#ffffff80';
50+
};
51+
4752
useEffect(() => {
4853
if (isPc) setisOpen(false);
4954
}, [isPc]);
@@ -55,21 +60,6 @@ export const CommonLayout = observer(({ children, header }: { children?: React.R
5560
});
5661
}, []);
5762

58-
const swipeHandlers = useSwipeable({
59-
onSwipedRight: () => {
60-
if (!isPc) {
61-
setisOpen(true);
62-
}
63-
},
64-
onSwipedLeft: () => {
65-
if (!isPc) {
66-
setisOpen(false);
67-
}
68-
},
69-
trackMouse: false,
70-
swipeDuration: 500,
71-
delta: 50,
72-
});
7363

7464
if (!isClient) return <></>;
7565

@@ -103,16 +93,27 @@ export const CommonLayout = observer(({ children, header }: { children?: React.R
10393
{isPc && <Sidebar />}
10494

10595
<main
106-
{...swipeHandlers}
10796
id="page-wrap"
10897
style={{ width: isPc ? `calc(100% - ${base.sideBarWidth}px)` : '100%' }}
10998
className={`flex !transition-all duration-300 overflow-y-hidden w-full flex-col gap-y-1 bg-secondbackground`}
11099
>
111100
{/* nav bar */}
112-
<header className="relative flex md:h-16 md:min-h-16 h-14 min-h-14 items-center justify-between gap-2 rounded-medium px-2 md:px:4 pt-2 md:pb-2 overflow-hidden">
113-
<div className="hidden md:block absolute bottom-[20%] right-[5%] z-[0] h-[350px] w-[350px] overflow-hidden blur-3xl ">
101+
<header
102+
className="blinko-mobile-header relative flex md:h-16 md:min-h-16 h-14 min-h-14 items-center justify-between gap-2 px-2 md:px:4 pt-2 md:pb-2 overflow-hidden"
103+
style={!isPc ? {
104+
position: 'fixed',
105+
top: 0,
106+
borderRadius:'0 0 12px 12px',
107+
zIndex: 11,
108+
width: '100%',
109+
background: getFixedHeaderBackground(),
110+
backdropFilter: 'blur(10px)',
111+
WebkitBackdropFilter: 'blur(10px)'
112+
} : undefined}
113+
>
114+
{/* <div className="hidden md:block absolute bottom-[20%] right-[5%] z-[0] h-[350px] w-[350px] overflow-hidden blur-3xl ">
114115
<div className="w-full h-[100%] bg-[#9936e6] opacity-20" style={{ clipPath: 'circle(50% at 50% 50%)' }} />
115-
</div>
116+
</div> */}
116117
<div className="flex max-w-full items-center gap-2 md:p-2 w-full z-[1]">
117118
{!isPc && (
118119
<Button isIconOnly className="flex" size="sm" variant="light" onPress={() => setisOpen(!isOpen)}>
@@ -196,9 +197,11 @@ export const CommonLayout = observer(({ children, header }: { children?: React.R
196197
</div>
197198
{header}
198199
</header>
199-
{/* backdrop pt-6 -mt-6 to fix the editor tooltip position */}
200200

201-
<ScrollArea onBottom={() => { }} className="h-[calc(100%_-_70px)] !overflow-y-auto overflow-x-hidden mt-[-4px]">
201+
202+
203+
{/* backdrop pt-6 -mt-6 to fix the editor tooltip position */}
204+
<ScrollArea onBottom={() => { }} className={`${isPc ? 'h-[calc(100%_-_70px)]' : 'h-full'} !overflow-y-auto overflow-x-hidden mt-[-4px]`}>
202205
<div className="relative flex h-full w-full flex-col rounded-medium layout-container">
203206
<div className="hidden md:block absolute top-[-37%] right-[5%] z-[0] h-[350px] w-[350px] overflow-hidden blur-3xl ">
204207
<div className="w-full h-[356px] bg-[#9936e6] opacity-20" style={{ clipPath: 'circle(50% at 50% 50%)' }} />

app/src/pages/analytics.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TagDistributionChart } from "@/components/BlinkoAnalytics/TagDistributi
99
import dayjs from "dayjs"
1010
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@heroui/react"
1111
import { Icon } from '@/components/Common/Iconify/icons'
12+
import { ScrollArea } from '@/components/Common/ScrollArea'
1213

1314
const Analytics = observer(() => {
1415
const analyticsStore = RootStore.Get(AnalyticsStore)
@@ -33,7 +34,7 @@ const Analytics = observer(() => {
3334
const stats = analyticsStore.monthlyStats.value
3435

3536
return (
36-
<div className="p-6 space-y-6 mx-auto max-w-7xl">
37+
<ScrollArea onBottom={() => { }} fixMobileTopBar className="px-6 space-y-2 md:p-6 md:space-y-6 mx-auto max-w-7xl" >
3738
<div className="w-72">
3839
<Dropdown>
3940
<DropdownTrigger>
@@ -77,10 +78,12 @@ const Analytics = observer(() => {
7778
description={t('heatMapDescription')}
7879
/>
7980

80-
{stats?.tagStats && stats.tagStats.length > 0 && (
81-
<TagDistributionChart tagStats={stats.tagStats} />
82-
)}
83-
</div>
81+
{
82+
stats?.tagStats && stats.tagStats.length > 0 && (
83+
<TagDistributionChart tagStats={stats.tagStats} />
84+
)
85+
}
86+
</ScrollArea >
8487
)
8588
})
8689

app/src/pages/detail/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@ const Detail = observer(() => {
1212
const location = useLocation();
1313
const [searchParams] = useSearchParams();
1414
const blinko = RootStore.Get(BlinkoStore);
15-
15+
1616
useEffect(() => {
1717
if (searchParams.get('id')) {
1818
blinko.noteDetail.call({ id: Number(searchParams.get('id')) });
1919
}
2020
}, [location.pathname, searchParams.get('id'), blinko.updateTicker, blinko.forceQuery]);
2121

2222
return (
23-
<ScrollArea onBottom={() => {}}>
23+
<ScrollArea fixMobileTopBar>
2424
<div className="max-w-[800px] mx-auto p-4">
2525
<LoadingAndEmpty
2626
isLoading={blinko.noteDetail.loading.value}
2727
isEmpty={!blinko.noteDetail.value}
2828
/>
29-
29+
3030
{blinko.noteDetail.value && (
31-
<BlinkoCard
32-
blinkoItem={blinko.noteDetail.value}
31+
<BlinkoCard
32+
blinkoItem={blinko.noteDetail.value}
3333
defaultExpanded={false}
3434
glassEffect={false}
3535
/>

0 commit comments

Comments
 (0)