Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion app/Services/ImapMailboxService.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,27 @@ public function getMessageCount(string $folderPath): array

try {
$folder = $client->getFolder($folderPath);
$info = $folder->examine();

// prefer one STATUS round trip over EXAMINE plus an UNSEEN search.
try {
$status = $folder->status();

if (is_array($status) && (
array_key_exists('messages', $status)
|| array_key_exists('unseen', $status)
|| array_key_exists('uidvalidity', $status)
)) {
return [
'total' => (int) ($status['messages'] ?? 0),
'unread' => (int) ($status['unseen'] ?? 0),
'uidvalidity' => $status['uidvalidity'] ?? null,
];
}
} catch (\Throwable) {
// fall back for servers that do not support STATUS reliably.
}

$info = $folder->examine();
$unread = $folder->query()
->unseen()
->setFetchBody(false)
Expand Down
6 changes: 6 additions & 0 deletions docs/OPENMAIL_2.0.0_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ Acceptance: security signals include evidence and limitations; rendering and URL

Acceptance: every shared-mail action is authorized server-side; internal notes cannot enter outbound MIME; memberships and audit events are tested for cross-user access.

## Premium UX execution slice

The premium UX initiative is implemented incrementally. Its first slice establishes a consistent branded application bar, a more deliberate mailbox canvas and sidebar, clearer inbox hierarchy, refined message-reader and composer surfaces, and reduced-motion-aware micro-interactions. This visual foundation does not claim that the planned three-pane desktop workspace or every accessibility/browser acceptance gate is complete.

Next slices must deliver the unified desktop conversation pane, stronger responsive navigation, keyboard/focus and screen-reader verification, and measured performance improvements. Keep the Laravel/Livewire/Tailwind architecture, self-hosting constraints, privacy controls, and existing mail behavior intact.

## Workstream 5 — UX, PWA, and accessibility (P1)

- Unified three-pane desktop workspace and adaptive mobile layouts.
Expand Down
42 changes: 42 additions & 0 deletions docs/OPENMAIL_PREMIUM_UX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# OpenMail Premium UX

## Product intent

OpenMail should feel like a calm, polished, dependable email workspace: clear hierarchy, fast feedback, low visual noise, accessible controls, and careful treatment of private mail. The visual system should support real work rather than imitate another mail client's brand.

## Visual principles

- Keep Coral Red (#CF4234) as the signature accent, reserved for primary actions, selected navigation, unread cues, and focus.
- Use warm light surfaces and deep charcoal dark surfaces with the same semantic tokens in both themes.
- Prefer restrained borders, subtle elevation, 8–16px control radii, consistent spacing, and readable typography.
- Keep inbox rows information-dense but easy to scan; distinguish unread state through type weight and a restrained accent rail.
- Use motion only for feedback and context. Honor prefers-reduced-motion.
- Avoid decorative gradients, heavy glass effects, excessive cards, and large animations that compete with message content.

## UX workstreams

1. **Foundation** — shared shell, design tokens, typography, control sizing, semantic focus styles, consistent light/dark treatment.
2. **Inbox** — clearer folder hierarchy, scanning rhythm, selection and bulk-action states, skeletons, errors, and empty states.
3. **Reader** — comfortable reading width, clear sender/recipient/date hierarchy, predictable attachment treatment, safe remote-image controls, and thread navigation.
4. **Composer** — focused layout, clear recipient chips, attachment affordances, visible draft-save state, keyboard support, and send/undo feedback.
5. **Workspace** — three-pane desktop view, adaptive mobile navigation, command palette and shortcuts, accessible focus management.
6. **Perceived performance** — avoid layout shifts; show immediate optimistic feedback only when rollback is possible; instrument IMAP/SMTP and database waits; never hide real server failures.
7. **Verification** — browser checks at mobile/tablet/desktop widths, keyboard-only paths, screen-reader labels, no horizontal overflow, no console errors, and regression tests for mail operations.

## Implemented foundation slice

- Aligned the global application bar to a 56px workspace header and the Coral Red brand token.
- Refined mailbox content spacing and increased the desktop folder-sidebar width for easier scanning.
- Strengthened inbox heading and status hierarchy, primary compose actions, search-field sizing, unread row cues, and surface separation.
- Refined the message reader and composer surfaces while preserving existing Livewire actions and full-screen behavior on mobile.
- Added restrained hover/focus feedback and reduced-motion handling without adding a frontend dependency.

## Acceptance criteria for subsequent slices

- Every primary action provides a clear loading, success, or error state.
- The inbox and message reader remain usable at 360px wide without horizontal scrolling.
- Keyboard focus is visible and follows visual order; dialogs trap and restore focus correctly.
- Theme and density preferences remain consistent across routes and authenticated views.
- Visual changes do not change authorization, message sanitization, remote-image consent, attachment validation, Bcc privacy, or SMTP outcome semantics.
- Run npm run build, the PHP/Pest suite, Pint, and npm run test:browser where a configured browser environment is available.
- Measure mailbox render and IMAP/SMTP timings before claiming performance improvements.
210 changes: 210 additions & 0 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1220,3 +1220,213 @@ select.glass-input {
font-weight: 500;
letter-spacing: 0.01em;
}


/* ============================================
premium UX foundation
keep the interface calm, fast, and content-first.
============================================ */
.app-topbar {
min-height: 3.5rem;
background: color-mix(in srgb, var(--color-surface-raised) 94%, transparent);
border-color: var(--color-border);
-webkit-backdrop-filter: saturate(140%) blur(14px);
backdrop-filter: saturate(140%) blur(14px);
}
.brand-mark {
box-shadow: 0 2px 5px rgb(207 66 52 / 0.20);
transition: transform 160ms ease, box-shadow 160ms ease;
}
.brand-link:hover .brand-mark {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgb(207 66 52 / 0.24);
}
.mailbox-workspace { isolation: isolate; background: var(--color-surface); }
.mailbox-main-content {
background: radial-gradient(ellipse at 92% 0%, rgb(207 66 52 / 0.035), transparent 30rem), var(--color-surface);
}
.mailbox-content-scroll { scrollbar-gutter: stable; }
.premium-folder-sidebar { background: var(--color-surface-raised); }
.premium-compose-sidebar-action {
min-height: 2.75rem;
box-shadow: 0 2px 5px rgb(207 66 52 / 0.12);
}
.premium-compose-sidebar-action:hover {
box-shadow: 0 5px 12px rgb(207 66 52 / 0.18);
transform: translateY(-1px);
}
.mailbox-page-header { border-color: var(--color-border); }
.mailbox-unread-count {
display: inline-flex;
align-items: center;
border: 1px solid rgb(207 66 52 / 0.18);
border-radius: 999px;
background: var(--color-primary-subtle);
padding: 0.3rem 0.65rem;
white-space: nowrap;
}
.mailbox-total-count {
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-surface-raised);
padding: 0.3rem 0.65rem;
white-space: nowrap;
}
.mailbox-primary-action {
min-height: 2.5rem;
box-shadow: 0 2px 5px rgb(207 66 52 / 0.14);
transition: background-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
}
.mailbox-primary-action:hover {
box-shadow: 0 5px 12px rgb(207 66 52 / 0.2);
transform: translateY(-1px);
}
.mailbox-search-container input,
.mailbox-search-container [data-openmail-search-input] {
min-height: 2.5rem;
border-radius: 0.75rem;
font-size: 0.8125rem;
box-shadow: inset 0 1px 1px rgb(28 25 23 / 0.015);
}
.mailbox-search-container input:focus,
.mailbox-search-container [data-openmail-search-input]:focus {
box-shadow: 0 0 0 3px rgb(207 66 52 / 0.10);
}
.mailbox-message-list { border-color: var(--color-border); box-shadow: var(--shadow-sm); }
.mailbox-message-list .message-row-card {
min-height: 3.4rem;
padding: 0.55rem 0.75rem;
border-color: var(--color-border-subtle);
transition: background-color 140ms ease, box-shadow 140ms ease;
}
.mailbox-message-list .message-row-card:hover { background-color: var(--color-hover); }
.mailbox-message-list .message-row-card.message-row-unread { box-shadow: inset 3px 0 0 var(--color-primary); }
.mailbox-message-list .message-row-avatar { overflow: hidden; }
.mailbox-message-list a:focus-visible,
.premium-message-viewer a:focus-visible,
.premium-composer button:focus-visible,
.premium-composer input:focus-visible,
.premium-composer textarea:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.premium-message-viewer > div:first-child { min-height: 2.75rem; }
.premium-reader-surface { box-shadow: var(--shadow-sm); }
.premium-reader-surface .email-body-content { border-top: 1px solid var(--color-border-subtle); }
.premium-reader-surface .email-body-content pre { overflow-wrap: anywhere; }
.premium-composer { box-shadow: 0 24px 70px rgb(0 0 0 / 0.20); }
.premium-composer > div:first-child { border-color: var(--color-border); }
.premium-composer input:not([type="checkbox"]):not([type="radio"]),
.premium-composer textarea,
.premium-composer select { border-radius: 0.75rem; min-height: 2.5rem; }
.premium-composer .tiptap-toolbar-container { background: var(--color-surface-sunken); }
.premium-composer .tiptap-editor { min-height: 16rem; }
:where(.dark) .app-topbar { background: color-mix(in srgb, var(--color-surface-raised) 92%, transparent); }
:where(.dark) .mailbox-main-content {
background: radial-gradient(ellipse at 92% 0%, rgb(248 113 104 / 0.045), transparent 30rem), var(--color-surface);
}
@media (max-width: 639px) {
.mailbox-page-header { align-items: stretch; gap: 0.875rem; padding-bottom: 1rem; }
.mailbox-page-header h1 { font-size: 1.55rem; }
.mailbox-search-container { flex-basis: 100%; }
.mailbox-message-list .message-row-card { padding: 0.65rem 0.625rem; }
.premium-reader-surface .text-right.tabular-nums { max-width: 7rem; white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
.brand-mark, .mailbox-primary-action, .premium-compose-sidebar-action, .mailbox-message-list .message-row-card { transition: none; }
.brand-link:hover .brand-mark, .mailbox-primary-action:hover, .premium-compose-sidebar-action:hover { transform: none; }
}


/* selected-message workspace: navigation sidebar + message list + reader. */
.mailbox-three-pane {
display: grid;
grid-template-columns: minmax(320px, 37%) minmax(0, 1fr);
gap: 0;
width: calc(100% + 3.5rem);
height: calc(100dvh - 7rem);
min-height: 32rem;
margin: -1.75rem;
overflow: hidden;
background: var(--color-surface);
}
.mailbox-three-pane-list {
min-width: 0;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
background: var(--color-surface-raised);
border-right: 1px solid var(--color-border);
}
.mailbox-three-pane-list > [wire\:id] { min-width: 0; }
.mailbox-three-pane-list .mailbox-page-header {
display: flex;
align-items: stretch;
flex-direction: column;
gap: 0.875rem;
margin: 0;
padding: 1.25rem;
border-bottom: 1px solid var(--color-border);
}
.mailbox-three-pane-list .mailbox-page-header h1 { font-size: 1.4rem; }
.mailbox-three-pane-list .mailbox-page-header > div:last-child {
display: flex;
align-items: center;
flex-wrap: wrap;
width: 100%;
gap: 0.5rem;
}
.mailbox-three-pane-list .mailbox-search-container {
flex: 1 0 100%;
order: 3;
width: 100%;
}
.mailbox-three-pane-list .mailbox-message-list {
border: 0;
border-radius: 0;
box-shadow: none;
}
.mailbox-three-pane-list .message-row-avatar { display: none; }
.mailbox-three-pane-list .message-row-card { align-items: flex-start; padding: 0.75rem 0.875rem; }
.mailbox-three-pane-list .message-row-card > a {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.2rem;
margin-left: 0.5rem;
}
.mailbox-three-pane-list .message-row-card > a > div:first-child {
width: 100%;
min-width: 0;
}
.mailbox-three-pane-list .message-row-card > a > div:first-child > div { min-width: 0; }
.mailbox-three-pane-list .message-row-card > a > div:nth-child(2) { width: 100%; }
.mailbox-three-pane-reader {
min-width: 0;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.5rem;
background: var(--color-surface);
}
.mailbox-three-pane-reader .premium-message-viewer {
width: 100%;
max-width: none;
margin: 0;
}
.mailbox-three-pane-reader .premium-reader-surface { box-shadow: var(--shadow-sm); }

@media (min-width: 640px) and (max-width: 1023px) {
.mailbox-three-pane { width: calc(100% + 2.5rem); height: calc(100dvh - 6rem); margin: -1.25rem; }
}
@media (max-width: 639px) {
.mailbox-three-pane { width: calc(100% + 1.5rem); height: calc(100dvh - 5rem); min-height: 0; margin: -0.75rem; }
}
@media (max-width: 1023px) {
.mailbox-three-pane { display: block; overflow: auto; }
.mailbox-three-pane-list { display: none; }
.mailbox-three-pane-reader { min-height: 100%; overflow: visible; padding: 1rem; }
}
@media (prefers-reduced-motion: reduce) {
.premium-compose-sidebar-action:hover { transform: none; }
}
12 changes: 6 additions & 6 deletions resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#f45b55">
<meta name="theme-color" content="#cf4234">
<meta name="application-name" content="OpenMail">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
Expand Down Expand Up @@ -58,19 +58,19 @@ class="pointer-events-none fixed inset-0 z-[9999] opacity-0 transition-opacity d
aria-hidden="true"></div>

{{-- Application header — minimal, recedes behind content --}}
<nav class="bg-surface-raised border-b border-border sticky top-0 z-30 w-full shrink-0">
<nav class="app-topbar bg-surface-raised border-b border-border sticky top-0 z-30 w-full shrink-0">
<div class="w-full px-2.5 sm:px-6 lg:px-8">
<div class="flex justify-between h-11 items-center gap-1.5 sm:gap-4">
<div class="flex justify-between h-14 items-center gap-1.5 sm:gap-4">
{{-- Left: Brand & Navigation --}}
<div class="flex items-center gap-1.5 sm:gap-3 min-w-0">
@hasSection('mobile-nav-toggle')
@yield('mobile-nav-toggle')
@endif
<a href="{{ route('mailbox') }}" wire:navigate class="flex items-center gap-1.5 sm:gap-2 group min-w-0">
<a href="{{ route('mailbox') }}" wire:navigate class="brand-link flex items-center gap-2 sm:gap-2.5 group min-w-0">
{{-- Logo mark — simple envelope, no color background --}}
<svg class="w-5 h-5 text-primary shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.75">
<span class="brand-mark flex h-8 w-8 items-center justify-center rounded-xl bg-primary shadow-sm"><svg class="h-4 w-4 text-white shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</svg></span>
<span class="text-sm font-semibold text-ink tracking-tight group-hover:text-primary transition-colors duration-150 truncate">
{{ config('app.name', 'OpenMail') }}
</span>
Expand Down
8 changes: 4 additions & 4 deletions resources/views/layouts/mailbox.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class="lg:hidden p-1 -ml-1 text-ink-secondary hover:text-ink hover:bg-hover roun
@endsection

@section('content')
<div class="flex-1 flex overflow-hidden w-full h-[calc(100vh-2.75rem)] min-h-[calc(100dvh-2.75rem)] relative"
<div class="mailbox-workspace flex-1 flex overflow-hidden w-full h-[calc(100vh-3.5rem)] min-h-[calc(100dvh-3.5rem)] relative"
x-data="{
sidebarOpen: false,
closeSidebar() {
Expand Down Expand Up @@ -42,7 +42,7 @@ class="lg:hidden fixed inset-0 z-40 bg-black/40 backdrop-blur-xs transition-opac
{{-- Unified Responsive Sidebar: Desktop Static + Mobile Slide-over Drawer --}}
<aside
class="fixed inset-y-0 left-0 z-50 w-[280px] max-w-[85vw] bg-surface-raised border-r border-border shadow-xl flex flex-col h-full min-h-0
lg:static lg:z-auto lg:w-64 lg:shadow-none lg:flex lg:translate-x-0 transition-transform duration-200 ease-in-out"
lg:static lg:z-auto lg:w-[280px] lg:shadow-none lg:flex lg:translate-x-0 transition-transform duration-200 ease-in-out"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
role="dialog"
:aria-modal="sidebarOpen ? 'true' : 'false'"
Expand Down Expand Up @@ -74,8 +74,8 @@ class="fixed inset-y-0 left-0 z-50 w-[280px] max-w-[85vw] bg-surface-raised bord
</aside>

{{-- Main content area --}}
<section class="flex-1 flex flex-col min-w-0 bg-surface h-full min-h-0 overflow-hidden">
<div class="flex-1 overflow-y-auto scrollbar-thin p-3 sm:p-5 min-h-0">
<section class="mailbox-main-content flex-1 flex flex-col min-w-0 bg-surface h-full min-h-0 overflow-hidden">
<div class="mailbox-content-scroll flex-1 overflow-y-auto scrollbar-thin p-3 sm:p-5 lg:p-7 min-h-0">
@yield('mailbox-content')
{{ $slot ?? '' }}
</div>
Expand Down
6 changes: 3 additions & 3 deletions resources/views/livewire/mailbox/composer.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class="fixed inset-0 z-40"
>
{{-- Backdrop --}}
<div
class="fixed inset-0 bg-black/40 transition-opacity"
class="fixed inset-0 bg-black/45 backdrop-blur-sm transition-opacity"
@click="closeWithConfirm()"
aria-hidden="true"
></div>
Expand All @@ -191,7 +191,7 @@ class="fixed inset-0 flex items-center justify-center p-0 sm:p-4 md:p-5"
@click.outside="closeWithConfirm()"
>
<div
class="bg-surface-raised rounded-none sm:rounded border-0 sm:border border-border shadow-xl max-w-4xl w-full h-[100dvh] sm:h-auto sm:max-h-[92vh] flex flex-col overflow-hidden animate-fade-in"
class="premium-composer bg-surface-raised rounded-none sm:rounded-2xl border-0 sm:border border-border shadow-xl max-w-5xl w-full h-[100dvh] sm:h-auto sm:max-h-[92vh] flex flex-col overflow-hidden animate-fade-in"
@click.outside.stop
>
{{-- Header --}}
Expand Down Expand Up @@ -254,7 +254,7 @@ class="p-1 text-ink-tertiary hover:text-ink rounded hover:bg-hover transition-co
</div>

{{-- Form Body --}}
<form class="flex-1 overflow-y-auto p-3 sm:p-5 space-y-3" @submit.prevent="send()">
<form class="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4" @submit.prevent="send()">
{{-- Recipient Fields --}}
<div class="space-y-2">
{{-- To Field + CC/BCC Toggles --}}
Expand Down
Loading
Loading