General
Email Viewer code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/email-viewer.blade.php
@props([
'subject' => null,
'from' => null,
'fromEmail' => null,
'to' => [],
'cc' => [],
'date' => null,
'labels' => [],
'labelColors' => [],
'attachments' => [],
'backHref' => null,
'backLabel' => 'Back',
'emptyTitle' => 'Select a message',
'emptyDescription' => 'Choose an email from the list to read it here.',
])
@php
/*
| Email reading pane for mailbox layouts. Shows headers, coloured labels, a
| message body slot, attachment chips and a default reply toolbar.
|
| from/to/cc accept strings or ['name' => '...', 'email' => '...'] arrays.
| labels accept strings or ['name' => '...', 'color' => '...'] arrays.
| labelColors: map of label name to variant (gray, brand, green, red, amber, blue).
| attachments: [['name' => '...', 'size' => '...', 'type' => 'pdf'], ...]
|
| Usage:
| <x-ui.email-viewer
| subject="Q3 launch timeline"
| :from="['name' => 'Ada Lovelace', 'email' => 'ada@example.com']"
| :to="[['name' => 'You', 'email' => 'you@example.com']]"
| date="Today, 10:42 AM"
| :labels="['Work', 'Launch']"
| :label-colors="['Work' => 'blue', 'Launch' => 'amber']"
| :attachments="[['name' => 'timeline.pdf', 'size' => '420 KB', 'type' => 'pdf']]"
| >
| <p>Email body copy…</p>
| </x-ui.email-viewer>
*/
$labelVariants = ['gray', 'brand', 'green', 'red', 'amber', 'blue'];
$resolvedLabels = collect($labels)
->map(function (mixed $label) use ($labelColors, $labelVariants): array {
if (is_array($label)) {
$name = (string) ($label['name'] ?? $label['label'] ?? '');
$color = (string) ($label['color'] ?? $labelColors[$name] ?? 'gray');
} else {
$name = (string) $label;
$color = (string) ($labelColors[$name] ?? 'gray');
}
if (! in_array($color, $labelVariants, true)) {
$color = 'gray';
}
return ['name' => $name, 'color' => $color];
})
->filter(fn (array $label): bool => $label['name'] !== '')
->values()
->all();
$formatParticipant = function (mixed $participant): array {
if (is_string($participant)) {
return ['name' => $participant, 'email' => null];
}
if (! is_array($participant)) {
return ['name' => 'Unknown', 'email' => null];
}
return [
'name' => $participant['name'] ?? $participant['email'] ?? 'Unknown',
'email' => $participant['email'] ?? null,
];
};
$fromParticipant = $from !== null ? $formatParticipant($from) : null;
$toParticipants = collect($to)->map($formatParticipant)->values()->all();
$ccParticipants = collect($cc)->map($formatParticipant)->values()->all();
$formatAddressList = function (array $participants): string {
return collect($participants)
->map(function (array $participant): string {
if ($participant['email']) {
return $participant['name'] !== $participant['email']
? "{$participant['name']} <{$participant['email']}>"
: $participant['email'];
}
return $participant['name'];
})
->implode(', ');
};
$attachmentIcons = [
'pdf' => 'file-pdf',
'image' => 'image',
'spreadsheet' => 'file-xls',
'archive' => 'file-zip',
'document' => 'file-doc',
'code' => 'file-code',
'text' => 'file-text',
];
$hasMessage = filled($subject) || $fromParticipant !== null || ! $slot->isEmpty();
@endphp
<aside {{ $attributes->merge(['class' => 'flex min-h-0 flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900']) }}>
@if (! $hasMessage)
<div class="flex h-full min-h-48 flex-1 flex-col items-center justify-center px-6 py-12 text-center">
<span class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500">
<x-ui.icon name="envelope-open" weight="duotone" size="2xl" />
</span>
<p class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">{{ $emptyTitle }}</p>
<p class="mt-1 max-w-sm text-sm text-gray-500 dark:text-gray-400">{{ $emptyDescription }}</p>
</div>
@else
<div class="border-b border-gray-200 px-4 py-4 dark:border-gray-800 sm:px-5">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="flex min-w-0 items-start gap-3">
@if ($backHref)
<a
href="{{ $backHref }}"
class="mt-0.5 inline-flex shrink-0 items-center justify-center rounded-lg p-2 text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
aria-label="{{ $backLabel }}"
>
<x-ui.icon name="arrow-left" weight="bold" size="lg" />
</a>
@endif
<div class="min-w-0 flex-1">
<h2 class="text-base font-semibold text-gray-900 sm:text-lg dark:text-white">{{ $subject ?? 'No subject' }}</h2>
@if ($resolvedLabels !== [])
<div class="mt-2 flex flex-wrap gap-1.5">
@foreach ($resolvedLabels as $label)
<x-ui.badge :variant="$label['color']" size="sm" dot>{{ $label['name'] }}</x-ui.badge>
@endforeach
</div>
@endif
</div>
</div>
@if ($date)
<p class="shrink-0 text-xs text-gray-400 sm:text-right dark:text-gray-500">{{ $date }}</p>
@endif
</div>
</div>
<div class="space-y-3 border-b border-gray-200 px-4 py-4 text-sm dark:border-gray-800 sm:px-5">
@if ($fromParticipant)
<div class="flex flex-col gap-1 sm:flex-row sm:gap-3">
<span class="w-10 shrink-0 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">From</span>
<div class="min-w-0">
<p class="font-medium text-gray-900 dark:text-white">{{ $fromParticipant['name'] }}</p>
@if ($fromParticipant['email'])
<p class="break-all text-gray-500 dark:text-gray-400">{{ $fromParticipant['email'] }}</p>
@endif
</div>
</div>
@endif
@if ($toParticipants !== [])
<div class="flex flex-col gap-1 sm:flex-row sm:gap-3">
<span class="w-10 shrink-0 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">To</span>
<p class="min-w-0 break-words text-gray-700 dark:text-gray-300">{{ $formatAddressList($toParticipants) }}</p>
</div>
@endif
@if ($ccParticipants !== [])
<div class="flex flex-col gap-1 sm:flex-row sm:gap-3">
<span class="w-10 shrink-0 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Cc</span>
<p class="min-w-0 break-words text-gray-700 dark:text-gray-300">{{ $formatAddressList($ccParticipants) }}</p>
</div>
@endif
</div>
<div class="flex items-center gap-2 overflow-x-auto border-b border-gray-200 px-4 py-3 dark:border-gray-800 sm:flex-wrap sm:px-5">
@isset($actions)
{{ $actions }}
@else
<x-ui.icon-button icon="arrow-bend-up-left" label="Reply" size="sm" class="shrink-0 sm:hidden" />
<x-ui.button type="button" size="sm" icon="arrow-bend-up-left" class="hidden shrink-0 sm:inline-flex">Reply</x-ui.button>
<x-ui.icon-button icon="arrow-bend-double-up-left" label="Reply all" size="sm" variant="outline" class="shrink-0 sm:hidden" />
<x-ui.button type="button" variant="outline" size="sm" icon="arrow-bend-double-up-left" class="hidden shrink-0 sm:inline-flex">Reply all</x-ui.button>
<x-ui.icon-button icon="arrow-bend-up-right" label="Forward" size="sm" variant="outline" class="shrink-0 sm:hidden" />
<x-ui.button type="button" variant="outline" size="sm" icon="arrow-bend-up-right" class="hidden shrink-0 sm:inline-flex">Forward</x-ui.button>
<x-ui.dropdown align="right" width="48">
<x-slot:trigger>
<x-ui.icon-button icon="dots-three" label="More actions" size="sm" variant="outline" class="shrink-0" />
</x-slot:trigger>
<x-ui.dropdown.item icon="archive">Archive</x-ui.dropdown.item>
<x-ui.dropdown.item icon="tag">Add label</x-ui.dropdown.item>
<x-ui.dropdown.item icon="trash" variant="danger">Delete</x-ui.dropdown.item>
</x-ui.dropdown>
@endisset
</div>
<div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 py-5 sm:px-5">
<div class="space-y-4 text-sm leading-7 text-gray-700 dark:text-gray-300">
{{ $slot }}
</div>
@if ($attachments !== [])
<div class="mt-6 border-t border-gray-200 pt-5 dark:border-gray-800">
<p class="mb-3 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
{{ count($attachments) }} attachment{{ count($attachments) === 1 ? '' : 's' }}
</p>
<div class="grid gap-2 sm:grid-cols-2">
@foreach ($attachments as $attachment)
@php
$type = $attachment['type'] ?? 'file';
$icon = $attachmentIcons[$type] ?? 'file';
@endphp
<a
href="{{ $attachment['href'] ?? '#' }}"
class="flex items-center gap-3 rounded-xl border border-gray-200 bg-gray-50 px-3 py-3 transition hover:border-gray-300 hover:bg-white dark:border-gray-800 dark:bg-gray-950/40 dark:hover:border-gray-700 dark:hover:bg-gray-900"
>
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-white text-gray-500 ring-1 ring-gray-200 dark:bg-gray-900 dark:text-gray-300 dark:ring-gray-700">
<x-ui.icon :name="$icon" weight="fill" size="lg" />
</span>
<span class="min-w-0">
<span class="block truncate text-sm font-medium text-gray-900 dark:text-white">{{ $attachment['name'] ?? 'Attachment' }}</span>
@if (! empty($attachment['size']))
<span class="block text-xs text-gray-500 dark:text-gray-400">{{ $attachment['size'] }}</span>
@endif
</span>
</a>
@endforeach
</div>
</div>
@endif
</div>
@isset($footer)
<div class="border-t border-gray-200 px-5 py-4 dark:border-gray-800">
{{ $footer }}
</div>
@endisset
@endif
</aside>