Initial release: JMAP Webmail Client

A modern, privacy-focused webmail client built with Next.js and the JMAP protocol.
Designed for Stalwart Mail Server.

Features:
- Full email operations (compose, reply, forward, threading)
- Real-time push notifications
- Dark/light theme support
- Mobile responsive design
- Keyboard shortcuts
- Drag-and-drop organization
- i18n (English/French)
- Security-first (external content blocked, HTML sanitization)
This commit is contained in:
Matthieu MALVACHE
2025-12-10 17:54:22 +01:00
committed by Matthieu MALVACHE
commit cf21a84263
79 changed files with 21821 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
"use client";
import { create } from "zustand";
export type ActiveView = "sidebar" | "list" | "viewer";
interface UIState {
// Mobile view state
activeView: ActiveView;
sidebarOpen: boolean;
// Device detection (hydrated client-side)
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
// Actions
setActiveView: (view: ActiveView) => void;
setSidebarOpen: (open: boolean) => void;
toggleSidebar: () => void;
setDeviceType: (isMobile: boolean, isTablet: boolean, isDesktop: boolean) => void;
// Navigation helpers
showEmailList: () => void;
showEmailViewer: () => void;
goBack: () => void;
}
export const useUIStore = create<UIState>((set, get) => ({
// Initial state (SSR-safe defaults)
activeView: "list",
sidebarOpen: false,
isMobile: false,
isTablet: false,
isDesktop: true,
// Actions
setActiveView: (view) => set({ activeView: view }),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setDeviceType: (isMobile, isTablet, isDesktop) =>
set({ isMobile, isTablet, isDesktop }),
// Navigation helpers for mobile
showEmailList: () => {
const { isMobile } = get();
if (isMobile) {
set({ activeView: "list", sidebarOpen: false });
}
},
showEmailViewer: () => {
const { isMobile } = get();
if (isMobile) {
set({ activeView: "viewer" });
}
},
goBack: () => {
const { activeView, isMobile } = get();
if (!isMobile) return;
if (activeView === "viewer") {
set({ activeView: "list" });
} else if (activeView === "list") {
set({ sidebarOpen: true });
}
},
}));