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
+13 -10
View File
@@ -269,12 +269,13 @@ export default function CalendarPage() {
}, [closeDetail, openEditModal]);
const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => {
if (isMobile) return;
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
// Don't show hover popover if the sidebar is already open for this event
if (showEventModal && editEvent?.id === event.id) return;
setDetailEvent(event);
setDetailAnchorRect(anchorRect);
}, [showEventModal, editEvent]);
}, [isMobile, showEventModal, editEvent]);
const handleHoverLeave = useCallback(() => {
hoverTimerRef.current = setTimeout(() => {
@@ -712,7 +713,7 @@ export default function CalendarPage() {
};
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 */}
{!isMobile && (
<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 && (
<div className="flex flex-col flex-1 min-w-0">
<div className="flex flex-col flex-1 min-w-0 min-h-0">
<CalendarToolbar
selectedDate={selectedDate}
viewMode={normalizedViewMode}
@@ -836,13 +837,15 @@ export default function CalendarPage() {
{/* Mobile Bottom Navigation */}
{isMobile && (
<NavigationRail
orientation="horizontal"
onManageApps={handleManageApps}
onInlineApp={handleInlineApp}
onCloseInlineApp={closeInlineApp}
activeAppId={inlineApp?.id ?? null}
/>
<div className="shrink-0">
<NavigationRail
orientation="horizontal"
onManageApps={handleManageApps}
onInlineApp={handleInlineApp}
onCloseInlineApp={closeInlineApp}
activeAppId={inlineApp?.id ?? null}
/>
</div>
)}
{detailEvent && detailAnchorRect && (
+1 -1
View File
@@ -544,7 +544,7 @@ export default function ContactsPage() {
};
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 */}
{!isMobile && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
+23 -12
View File
@@ -217,8 +217,8 @@ export function NavigationRail({
);
})}
{/* Custom sidebar apps */}
{sidebarApps.map((app) => {
{/* Custom sidebar apps (per-app mobile visibility) */}
{sidebarApps.filter((app) => app.showOnMobile).map((app) => {
const AppIcon = lucideIcons[app.icon as keyof typeof lucideIcons] as LucideIcon | undefined;
const isActive = activeAppId === app.id;
return (
@@ -252,16 +252,27 @@ export function NavigationRail({
);
})}
{/* Manage apps button */}
{onManageApps && (
<button
onClick={onManageApps}
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"
>
<Plus className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight">{t("add_app")}</span>
</button>
)}
{/* Settings */}
<Link
href="/settings"
onClick={activeAppId ? () => onCloseInlineApp?.() : undefined}
className={cn(
"flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px]",
"transition-colors duration-150",
isSettingsActive
? "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>
);
}
@@ -18,6 +18,7 @@ interface SidebarAppFormData {
url: string;
icon: string;
openMode: "tab" | "inline";
showOnMobile: boolean;
}
function AppForm({
@@ -37,6 +38,7 @@ function AppForm({
url: app?.url || "",
icon: app?.icon || "Globe",
openMode: app?.openMode || "tab",
showOnMobile: app?.showOnMobile ?? false,
});
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -144,6 +146,25 @@ function AppForm({
</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">
<Button type="button" variant="ghost" size="sm" onClick={onCancel}>
{t("cancel")}
+2 -1
View File
@@ -137,7 +137,8 @@
"show_all": "All",
"no_icons_found": "No icons found",
"inline_badge": "Inline",
"tab_badge": "Tab"
"tab_badge": "Tab",
"show_on_mobile": "Show on Mobile"
},
"email_list": {
"no_emails": "No messages found",
+1
View File
@@ -45,6 +45,7 @@ export interface SidebarApp {
url: string;
icon: string; // Lucide icon name (e.g. 'Globe', 'Rss')
openMode: 'tab' | 'inline'; // Open in new tab or embed inline
showOnMobile?: boolean; // Show in mobile bottom nav (default false)
}
// Available color palette for keywords