Skip to content

Commit eb941a0

Browse files
committed
chore(release): bump version to 0.7.0 and UI fixes
1 parent da7405d commit eb941a0

File tree

13 files changed

+698
-124
lines changed

13 files changed

+698
-124
lines changed

client/src/App.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,13 @@ function AppShell(): JSX.Element {
343343
startedOnNoteCard = !!(el && el.closest && el.closest('.note-card'));
344344
} catch {}
345345

346+
// Also: avoid treating note drags as sidebar opens. If a note swap-drag is
347+
// active (or pending activation), never allow the edge swipe to open the drawer.
348+
let draggingInProgress = false;
349+
try { draggingInProgress = !!document.documentElement.classList && (document.documentElement.classList.contains('is-note-swap-dragging') || document.documentElement.classList.contains('is-note-swap-dragging-pending')); } catch {}
350+
346351
// Only arm the gesture if we started in a relevant region.
347-
// Also: avoid treating note drags as sidebar opens.
348-
const canStartOpen = (!sidebarDrawerOpen && x <= OPEN_ZONE_MAX_X && (!startedOnNoteCard || x <= NOTE_CARD_OPEN_EDGE_PX));
352+
const canStartOpen = (!sidebarDrawerOpen && x <= OPEN_ZONE_MAX_X && !draggingInProgress && (!startedOnNoteCard || x <= NOTE_CARD_OPEN_EDGE_PX));
349353
const canStartClose = (sidebarDrawerOpen && x <= DRAWER_REGION_PX);
350354
if (!canStartOpen && !canStartClose) return false;
351355

client/src/authContext.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
185185
const effectiveToken = token || localStorage.getItem('fn_token');
186186
if (!effectiveToken) throw new Error('Not authenticated');
187187
const data = await uploadMyPhoto(effectiveToken, dataUrl);
188-
if (data && data.user) setUser(data.user);
188+
if (data && data.user) {
189+
setUser(data.user);
190+
try {
191+
window.dispatchEvent(new CustomEvent('freemannotes:user-photo-updated', { detail: { user: data.user } }));
192+
} catch {}
193+
}
189194
}
190195

191196
async function updateMe(payload: any) {

client/src/components/Header.tsx

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { useAuth } from "../authContext";
55

66
export default function Header({ onToggleSidebar, searchQuery, onSearchChange, viewMode = 'cards', onToggleViewMode }: { onToggleSidebar?: () => void, searchQuery?: string, onSearchChange?: (q: string) => void, viewMode?: 'cards' | 'list-1' | 'list-2', onToggleViewMode?: () => void }) {
77
const [showPrefs, setShowPrefs] = useState(false);
8+
const [mobileCompact, setMobileCompact] = useState(false);
9+
const [avatarKey, setAvatarKey] = useState<number>(() => Date.now());
810
const { user } = useAuth();
911
const theme = (() => { try { return useTheme(); } catch { return { effective: 'dark' } as any; } })();
1012
const nextViewMode = viewMode === 'cards' ? 'list-1' : (viewMode === 'list-1' ? 'list-2' : 'cards');
@@ -13,8 +15,142 @@ export default function Header({ onToggleSidebar, searchQuery, onSearchChange, v
1315

1416
// dropdown removed; preferences open via avatar click
1517

18+
useEffect(() => {
19+
const onPhoto = (e: any) => {
20+
try { setAvatarKey(Date.now()); } catch {}
21+
};
22+
window.addEventListener('freemannotes:user-photo-updated', onPhoto as EventListener);
23+
return () => window.removeEventListener('freemannotes:user-photo-updated', onPhoto as EventListener);
24+
}, []);
25+
26+
useEffect(() => {
27+
const root = document.documentElement;
28+
if (showPrefs) {
29+
root.classList.add('is-preferences-open');
30+
try { window.dispatchEvent(new CustomEvent('freemannotes:mobile-add/close')); } catch {}
31+
} else {
32+
root.classList.remove('is-preferences-open');
33+
}
34+
return () => {
35+
try { root.classList.remove('is-preferences-open'); } catch {}
36+
};
37+
}, [showPrefs]);
38+
39+
useEffect(() => {
40+
let rafId: number | null = null;
41+
const root = document.documentElement;
42+
let lastY = 0;
43+
let compact = false;
44+
let upAccum = 0;
45+
let downAccum = 0;
46+
let lastToggleAt = 0;
47+
48+
const ENTER_Y = 72;
49+
const EXIT_Y = 26;
50+
const MIN_DELTA = 1.25;
51+
const ENTER_ACCUM = 14;
52+
const EXIT_ACCUM = 14;
53+
const TOGGLE_COOLDOWN_MS = 220;
54+
55+
const isPhoneLike = () => {
56+
try {
57+
const mq = window.matchMedia;
58+
const touchLike = !!(mq && (mq('(pointer: coarse)').matches || mq('(any-pointer: coarse)').matches));
59+
const vw = (window.visualViewport && typeof window.visualViewport.width === 'number') ? window.visualViewport.width : window.innerWidth;
60+
const vh = (window.visualViewport && typeof window.visualViewport.height === 'number') ? window.visualViewport.height : window.innerHeight;
61+
const shortSide = Math.min(vw, vh);
62+
return touchLike && shortSide <= 600;
63+
} catch {
64+
return false;
65+
}
66+
};
67+
68+
const getScrollTop = () => {
69+
const el = document.querySelector('.main-area') as HTMLElement | null;
70+
if (el) return Math.max(0, el.scrollTop || 0);
71+
return Math.max(0, window.scrollY || 0);
72+
};
73+
74+
const applyCompact = (next: boolean) => {
75+
if (compact === next) return;
76+
compact = next;
77+
lastToggleAt = Date.now();
78+
upAccum = 0;
79+
downAccum = 0;
80+
setMobileCompact(next);
81+
root.classList.toggle('mobile-header-compact', next);
82+
};
83+
84+
const evaluate = () => {
85+
if (!isPhoneLike()) {
86+
if (compact) applyCompact(false);
87+
return;
88+
}
89+
const y = getScrollTop();
90+
const dy = y - lastY;
91+
lastY = y;
92+
93+
// Ignore micro-jitter from inertial/bounce scrolling.
94+
if (Math.abs(dy) < MIN_DELTA) return;
95+
96+
if (dy > 0) {
97+
downAccum += dy;
98+
upAccum = 0;
99+
} else {
100+
upAccum += -dy;
101+
downAccum = 0;
102+
}
103+
104+
if ((Date.now() - lastToggleAt) < TOGGLE_COOLDOWN_MS) return;
105+
106+
// Enter compact mode only after passing threshold + clear downward intent.
107+
if (!compact && y >= ENTER_Y && downAccum >= ENTER_ACCUM) {
108+
applyCompact(true);
109+
return;
110+
}
111+
112+
// Exit compact mode near top or after clear upward intent.
113+
if (compact && (y <= EXIT_Y || upAccum >= EXIT_ACCUM)) {
114+
applyCompact(false);
115+
}
116+
};
117+
118+
const onScroll = () => {
119+
if (rafId != null) return;
120+
rafId = window.requestAnimationFrame(() => {
121+
rafId = null;
122+
evaluate();
123+
});
124+
};
125+
126+
const onResize = () => {
127+
lastY = getScrollTop();
128+
evaluate();
129+
};
130+
131+
const mainArea = document.querySelector('.main-area') as HTMLElement | null;
132+
lastY = getScrollTop();
133+
evaluate();
134+
mainArea?.addEventListener('scroll', onScroll, { passive: true });
135+
window.addEventListener('scroll', onScroll, { passive: true });
136+
window.addEventListener('resize', onResize);
137+
try { window.visualViewport?.addEventListener('resize', onResize); } catch {}
138+
139+
return () => {
140+
if (rafId != null) {
141+
try { window.cancelAnimationFrame(rafId); } catch {}
142+
}
143+
mainArea?.removeEventListener('scroll', onScroll);
144+
window.removeEventListener('scroll', onScroll);
145+
window.removeEventListener('resize', onResize);
146+
try { window.visualViewport?.removeEventListener('resize', onResize); } catch {}
147+
try { root.classList.remove('mobile-header-compact'); } catch {}
148+
setMobileCompact(false);
149+
};
150+
}, []);
151+
16152
return (
17-
<header className="app-header">
153+
<header className={`app-header${mobileCompact ? ' app-header--mobile-compact' : ''}`}>
18154
<div className="header-left">
19155
<button
20156
type="button"
@@ -77,7 +213,7 @@ export default function Header({ onToggleSidebar, searchQuery, onSearchChange, v
77213
{user ? (
78214
<div className="header-avatar-wrap">
79215
{ (user as any).userImageUrl ? (
80-
<img src={(user as any).userImageUrl} alt="User" className="avatar" style={{ width: 33, height: 33, borderRadius: '50%', objectFit: 'cover', cursor: 'pointer' }} onClick={() => setShowPrefs(true)} />
216+
<img key={avatarKey} src={(user as any).userImageUrl} alt="User" className="avatar" style={{ width: 33, height: 33, borderRadius: '50%', objectFit: 'cover', cursor: 'pointer' }} onClick={() => setShowPrefs(true)} />
81217
) : (
82218
<div className="avatar" style={{ width: 33, height: 33, borderRadius: '50%', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }} onClick={() => setShowPrefs(true)}>{(user.name && user.email ? (user.name || user.email)[0] : '')}</div>
83219
) }

client/src/components/ImageLightbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export default function ImageLightbox({ url, onClose }: { url: string; onClose:
141141
<div
142142
className="lightbox-backdrop"
143143
onClick={onClose}
144-
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 }}
144+
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10120 }}
145145
>
146146
<div
147147
className="lightbox-content"

0 commit comments

Comments
 (0)