feat: add mobile visibility toggle for sidebar apps and update related components

This commit is contained in:
Linus Rath
2026-03-20 17:44:26 +01:00
parent 1e6f5e2c8c
commit 68e141b787
6 changed files with 61 additions and 24 deletions
+6 -3
View File
@@ -269,12 +269,13 @@ export default function CalendarPage() {
}, [closeDetail, openEditModal]); }, [closeDetail, openEditModal]);
const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => {
if (isMobile) return;
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
// Don't show hover popover if the sidebar is already open for this event // Don't show hover popover if the sidebar is already open for this event
if (showEventModal && editEvent?.id === event.id) return; if (showEventModal && editEvent?.id === event.id) return;
setDetailEvent(event); setDetailEvent(event);
setDetailAnchorRect(anchorRect); setDetailAnchorRect(anchorRect);
}, [showEventModal, editEvent]); }, [isMobile, showEventModal, editEvent]);
const handleHoverLeave = useCallback(() => { const handleHoverLeave = useCallback(() => {
hoverTimerRef.current = setTimeout(() => { hoverTimerRef.current = setTimeout(() => {
@@ -712,7 +713,7 @@ export default function CalendarPage() {
}; };
return ( return (
<div className="flex h-dvh bg-background overflow-hidden"> <div className={cn("flex h-dvh bg-background overflow-hidden", isMobile && "flex-col")}>
{/* Left Navigation Rail */} {/* Left Navigation Rail */}
{!isMobile && ( {!isMobile && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}> <div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
@@ -775,7 +776,7 @@ export default function CalendarPage() {
)} )}
{!inlineApp && ( {!inlineApp && (
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0 min-h-0">
<CalendarToolbar <CalendarToolbar
selectedDate={selectedDate} selectedDate={selectedDate}
viewMode={normalizedViewMode} viewMode={normalizedViewMode}
@@ -836,6 +837,7 @@ export default function CalendarPage() {
{/* Mobile Bottom Navigation */} {/* Mobile Bottom Navigation */}
{isMobile && ( {isMobile && (
<div className="shrink-0">
<NavigationRail <NavigationRail
orientation="horizontal" orientation="horizontal"
onManageApps={handleManageApps} onManageApps={handleManageApps}
@@ -843,6 +845,7 @@ export default function CalendarPage() {
onCloseInlineApp={closeInlineApp} onCloseInlineApp={closeInlineApp}
activeAppId={inlineApp?.id ?? null} activeAppId={inlineApp?.id ?? null}
/> />
</div>
)} )}
{detailEvent && detailAnchorRect && ( {detailEvent && detailAnchorRect && (
+1 -1
View File
@@ -544,7 +544,7 @@ export default function ContactsPage() {
}; };
return ( return (
<div className="flex h-dvh bg-background overflow-hidden"> <div className={cn("flex h-dvh bg-background overflow-hidden", isMobile && "flex-col")}>
{/* Navigation Rail - desktop only */} {/* Navigation Rail - desktop only */}
{!isMobile && ( {!isMobile && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}> <div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
+22 -11
View File
@@ -217,8 +217,8 @@ export function NavigationRail({
); );
})} })}
{/* Custom sidebar apps */} {/* Custom sidebar apps (per-app mobile visibility) */}
{sidebarApps.map((app) => { {sidebarApps.filter((app) => app.showOnMobile).map((app) => {
const AppIcon = lucideIcons[app.icon as keyof typeof lucideIcons] as LucideIcon | undefined; const AppIcon = lucideIcons[app.icon as keyof typeof lucideIcons] as LucideIcon | undefined;
const isActive = activeAppId === app.id; const isActive = activeAppId === app.id;
return ( return (
@@ -252,16 +252,27 @@ export function NavigationRail({
); );
})} })}
{/* Manage apps button */} {/* Settings */}
{onManageApps && ( <Link
<button href="/settings"
onClick={onManageApps} onClick={activeAppId ? () => onCloseInlineApp?.() : undefined}
className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] transition-colors duration-150 text-muted-foreground hover:text-foreground" className={cn(
> "flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px]",
<Plus className="w-5 h-5" /> "transition-colors duration-150",
<span className="text-[10px] font-medium leading-tight">{t("add_app")}</span> isSettingsActive
</button> ? "text-primary"
: "text-muted-foreground hover:text-foreground"
)} )}
aria-current={isSettingsActive ? "page" : undefined}
>
<div className="relative">
<Settings className="w-5 h-5" />
{isSettingsActive && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-4 h-0.5 rounded-full bg-primary" />
)}
</div>
<span className="text-[10px] font-medium leading-tight">{t("settings")}</span>
</Link>
</nav> </nav>
); );
} }
@@ -18,6 +18,7 @@ interface SidebarAppFormData {
url: string; url: string;
icon: string; icon: string;
openMode: "tab" | "inline"; openMode: "tab" | "inline";
showOnMobile: boolean;
} }
function AppForm({ function AppForm({
@@ -37,6 +38,7 @@ function AppForm({
url: app?.url || "", url: app?.url || "",
icon: app?.icon || "Globe", icon: app?.icon || "Globe",
openMode: app?.openMode || "tab", openMode: app?.openMode || "tab",
showOnMobile: app?.showOnMobile ?? false,
}); });
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
@@ -144,6 +146,25 @@ function AppForm({
</div> </div>
</div> </div>
<div className="flex items-center justify-between">
<label className="text-sm font-medium">{t("show_on_mobile")}</label>
<button
type="button"
onClick={() => setFormData({ ...formData, showOnMobile: !formData.showOnMobile })}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
formData.showOnMobile ? "bg-primary" : "bg-muted-foreground/30"
)}
>
<span
className={cn(
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
formData.showOnMobile ? "translate-x-4.5" : "translate-x-0.5"
)}
/>
</button>
</div>
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<Button type="button" variant="ghost" size="sm" onClick={onCancel}> <Button type="button" variant="ghost" size="sm" onClick={onCancel}>
{t("cancel")} {t("cancel")}
+2 -1
View File
@@ -137,7 +137,8 @@
"show_all": "All", "show_all": "All",
"no_icons_found": "No icons found", "no_icons_found": "No icons found",
"inline_badge": "Inline", "inline_badge": "Inline",
"tab_badge": "Tab" "tab_badge": "Tab",
"show_on_mobile": "Show on Mobile"
}, },
"email_list": { "email_list": {
"no_emails": "No messages found", "no_emails": "No messages found",
+1
View File
@@ -45,6 +45,7 @@ export interface SidebarApp {
url: string; url: string;
icon: string; // Lucide icon name (e.g. 'Globe', 'Rss') icon: string; // Lucide icon name (e.g. 'Globe', 'Rss')
openMode: 'tab' | 'inline'; // Open in new tab or embed inline openMode: 'tab' | 'inline'; // Open in new tab or embed inline
showOnMobile?: boolean; // Show in mobile bottom nav (default false)
} }
// Available color palette for keywords // Available color palette for keywords