MIF_E31221353/resources/views/layouts/app.blade.php

1075 lines
34 KiB
PHP

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? 'Absensi' }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">
<style>
:root {
--font-sans: 'Plus Jakarta Sans', 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--bg: #030712;
--bg-alt: #060c1a;
--bg-soft: rgba(6, 12, 26, 0.78);
--card: rgba(11, 19, 40, 0.82);
--card-strong: rgba(13, 23, 50, 0.92);
--border: rgba(129, 140, 248, 0.26);
--border-soft: rgba(148, 163, 184, 0.16);
--border-strong: rgba(59, 130, 246, 0.3);
--text: #f8fafc;
--text-muted: #9ca3af;
--muted-chip: rgba(148, 163, 184, 0.16);
--primary: #6366f1;
--primary-strong: #2563eb;
--primary-soft: rgba(99, 102, 241, 0.12);
--danger: #ef4444;
--danger-accent: #f97316;
--success: #22c55e;
--warning: #f59e0b;
--header-height: 76px;
}
* {
box-sizing: border-box;
}
::selection {
background: rgba(99, 102, 241, 0.35);
color: #f8fafc;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font-sans);
background:
radial-gradient(circle at 20% -10%, rgba(59, 130, 246, 0.24), transparent 45%),
radial-gradient(circle at 85% 0%, rgba(129, 140, 248, 0.2), transparent 55%),
linear-gradient(160deg, #030712 0%, #020617 38%, #020819 100%);
color: var(--text);
color-scheme: dark;
}
header {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 32px;
min-height: var(--header-height);
backdrop-filter: blur(24px);
background: radial-gradient(120% 120% at 0% 0%, rgba(59, 130, 246, 0.16), transparent 55%), rgba(7, 13, 29, 0.84);
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
box-shadow: 0 24px 50px rgba(3, 7, 18, 0.55);
}
.header-left {
display: flex;
align-items: center;
gap: 18px;
}
header .brand {
font-weight: 700;
font-size: 21px;
letter-spacing: -0.015em;
display: flex;
align-items: center;
gap: 12px;
color: #e2e8f0;
}
header nav,
.header-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
header nav form {
margin: 0;
}
.sidebar-toggle {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
width: 46px;
height: 46px;
border-radius: 14px;
border: 1px solid rgba(99, 102, 241, 0.28);
background: linear-gradient(145deg, rgba(30, 41, 70, 0.96), rgba(15, 23, 42, 0.78));
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.45);
}
.sidebar-toggle span {
display: block;
width: 22px;
height: 3px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(226, 232, 240, 0.95), rgba(148, 163, 184, 0.8));
transition: transform 0.2s ease, opacity 0.2s ease;
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.25);
transform-origin: center;
}
.sidebar-toggle span + span {}
.sidebar-toggle:focus-visible {
outline: 3px solid rgba(99, 102, 241, 0.35);
outline-offset: 2px;
}
.sidebar-toggle:hover {
border-color: rgba(129, 140, 248, 0.65);
background: linear-gradient(145deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.45));
box-shadow: 0 18px 36px rgba(37, 99, 235, 0.28);
transform: translateY(-1px);
}
body.sidebar-open .sidebar-toggle span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
body.sidebar-open .sidebar-toggle span:nth-child(2) {
opacity: 0;
}
body.sidebar-open .sidebar-toggle span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(3, 7, 18, 0.58);
backdrop-filter: blur(6px);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
z-index: 55;
}
body.sidebar-open .sidebar-overlay {
opacity: 1;
pointer-events: auto;
}
body.sidebar-open {
overflow: hidden;
}
.layout {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 28px;
padding: 32px 36px 40px;
min-height: calc(100vh - 90px);
}
.layout.no-sidebar {
display: block;
padding: 32px;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(320px, 88vw);
padding: calc(var(--header-height) + 16px) 22px 32px;
border-radius: 0 24px 24px 0;
background: rgba(4, 10, 24, 0.94);
border: 1px solid rgba(129, 140, 248, 0.18);
box-shadow: 0 32px 60px rgba(3, 7, 18, 0.6);
backdrop-filter: blur(24px);
transform: translateX(-110%);
opacity: 0;
pointer-events: none;
z-index: 60;
overflow-y: auto;
transition: transform 0.28s ease, opacity 0.28s ease;
}
body.sidebar-open .sidebar {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.sidebar .menu {
list-style: none;
padding: 0;
margin: 0 0 30px;
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar .menu-heading {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: rgba(226, 232, 240, 0.58);
margin-bottom: 6px;
font-weight: 600;
}
.sidebar .menu li {
margin: 0;
}
.sidebar .menu a {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 16px;
color: var(--text);
text-decoration: none;
font-weight: 500;
letter-spacing: 0.01em;
border: 1px solid rgba(148, 163, 184, 0.08);
background: radial-gradient(circle at 0 50%, rgba(59, 130, 246, 0.12), transparent 55%), rgba(14, 23, 44, 0.65);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.1);
}
.sidebar .menu a::before {
content: "";
width: 10px;
height: 10px;
border-radius: 999px;
background: rgba(148, 163, 184, 0.4);
transition: background 0.2s ease, transform 0.2s ease;
}
.sidebar .menu a:hover {
transform: translateX(6px);
border-color: rgba(129, 140, 248, 0.3);
background: linear-gradient(135deg, rgba(37, 99, 235, 0.22), rgba(99, 102, 241, 0.18));
box-shadow: 0 18px 30px rgba(14, 23, 44, 0.45);
}
.sidebar .menu a:hover::before {
background: rgba(96, 165, 250, 0.75);
transform: scale(1.4);
}
.sidebar .menu a .menu-label {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar .menu a .menu-label small {
font-size: 11px;
color: rgba(203, 213, 225, 0.62);
letter-spacing: 0.04em;
}
.sidebar .menu a.active {
border-color: rgba(129, 140, 248, 0.55);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(56, 189, 248, 0.18));
box-shadow: 0 18px 32px rgba(37, 99, 235, 0.28), inset 0 0 0 1px rgba(148, 163, 184, 0.22);
}
.sidebar .menu a.active::before {
background: rgba(129, 140, 248, 0.95);
transform: scale(1.6);
box-shadow: 0 0 12px rgba(129, 140, 248, 0.55);
}
.sidebar .menu .notif-link {
position: relative;
}
.sidebar .menu .notif-link .notif-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
padding: 2px 6px;
border-radius: 999px;
background: linear-gradient(135deg, #f97316, #ef4444);
color: #fff7ed;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
}
.sidebar .menu .notif-link .notif-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #f97316;
}
.sidebar-users {
padding: 18px;
border-radius: 18px;
background: rgba(9, 16, 34, 0.82);
border: 1px solid rgba(129, 140, 248, 0.22);
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.28);
}
.sidebar-users-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
font-weight: 600;
color: #cbd5f5;
letter-spacing: 0.02em;
}
.sidebar-users-total {
background: rgba(59, 130, 246, 0.18);
color: #dbeafe;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
.sidebar-users-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar-users-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 14px;
background: rgba(15, 23, 42, 0.62);
border: 1px solid rgba(148, 163, 184, 0.18);
text-decoration: none;
color: inherit;
transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.sidebar-users-item:hover,
.sidebar-users-item:focus-visible {
border-color: rgba(99, 102, 241, 0.35);
background: rgba(15, 23, 42, 0.78);
transform: translateX(2px);
outline: none;
}
.sidebar-users-avatar {
width: 32px;
height: 32px;
border-radius: 999px;
background: linear-gradient(140deg, rgba(99, 102, 241, 0.6), rgba(14, 165, 233, 0.4));
color: #ecfeff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 13px;
}
.sidebar-users-info {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 12px;
color: #e2e8f0;
}
.sidebar-users-info span {
color: var(--text-muted);
font-size: 11px;
}
.sidebar-users-footer {
font-size: 11px;
color: var(--text-muted);
}
main {
flex: 1 1 auto;
min-width: 0;
padding: 32px 36px;
border-radius: 28px;
background: rgba(6, 12, 26, 0.6);
border: 1px solid rgba(148, 163, 184, 0.14);
box-shadow: 0 32px 48px rgba(4, 7, 18, 0.55);
backdrop-filter: blur(26px);
}
main > *:first-child {
margin-top: 0;
}
.card {
margin-bottom: 28px;
padding: 24px 26px;
border-radius: 20px;
background: var(--card);
border: 1px solid var(--border-soft);
box-shadow: 0 22px 46px rgba(6, 10, 24, 0.45);
}
.table-responsive {
width: 100%;
overflow-x: auto;
}
.card:last-child {
margin-bottom: 0;
}
.role-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(56, 189, 248, 0.22));
color: #f1f5f9;
font-weight: 600;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
border-radius: 12px;
border: 1px solid transparent;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.01em;
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background 0.18s ease;
background: rgba(15, 23, 42, 0.78);
color: var(--text);
text-decoration: none;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:focus-visible {
outline: 3px solid rgba(99, 102, 241, 0.35);
outline-offset: 2px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-strong));
border-color: rgba(99, 102, 241, 0.4);
color: #f8fafc;
box-shadow: 0 18px 30px rgba(79, 70, 229, 0.32);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger), var(--danger-accent));
border-color: rgba(239, 68, 68, 0.28);
color: #fff7ed;
box-shadow: 0 18px 30px rgba(249, 115, 22, 0.32);
}
.btn-outline {
background: transparent;
border-color: rgba(148, 163, 184, 0.32);
color: var(--text);
}
.btn-outline:hover {
background: rgba(15, 23, 42, 0.6);
border-color: rgba(129, 140, 248, 0.4);
}
.btn-ghost {
background: rgba(15, 23, 42, 0.3);
color: var(--text);
}
.btn-sm {
padding: 8px 12px;
border-radius: 10px;
font-size: 13px;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
th, td {
text-align: left;
padding: 12px 14px;
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
}
th {
color: #cbd5f5;
font-weight: 600;
letter-spacing: 0.02em;
font-size: 13px;
}
td {
color: var(--text);
font-size: 13px;
}
@media (max-width: 1280px) {
header {
padding: 16px 24px;
}
.layout {
padding: 24px 28px 32px;
}
main {
padding: 28px 28px;
}
}
@media (max-width: 1024px) {
.layout {
flex-direction: column;
gap: 18px;
padding: 20px 18px 28px;
}
.layout.no-sidebar {
padding: 20px 18px 28px;
}
main {
padding: 24px 20px;
}
body.sidebar-open .sidebar {
overflow-y: auto;
}
}
@media (min-width: 1025px) {
.sidebar {
width: 280px;
padding: calc(var(--header-height) + 28px) 26px 36px;
}
body.sidebar-open .sidebar-overlay {
opacity: 0;
pointer-events: none;
}
body.sidebar-open {
overflow: auto;
}
.layout {
padding-left: 0;
}
body.sidebar-open .layout {
padding-left: 260px;
transition: padding-left 0.28s ease;
}
}
@media (max-width: 900px) {
header {
padding: 14px 18px;
flex-wrap: wrap;
gap: 12px;
}
.header-actions {
justify-content: flex-end;
}
.header-left {
gap: 14px;
}
}
@media (max-width: 768px) {
header {
padding: 14px 16px;
}
header nav {
gap: 10px;
}
.layout {
padding: 18px 14px 26px;
}
main {
padding: 22px 16px;
}
.card {
padding: 22px 18px;
}
table {
min-width: 600px;
}
.card,
.sidebar-users,
.table-responsive,
.admin-user-attendance-list,
main > table,
.absensi-table-wrapper,
.report-table-wrapper,
.leave-table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 640px) {
header {
padding: 12px 14px;
}
.header-left {
gap: 12px;
}
header .brand {
font-size: 18px;
}
.btn.btn-sm {
padding: 8px 10px;
}
.card {
padding: 20px 16px;
}
main {
padding: 20px 14px;
}
.header-actions {
width: 100%;
justify-content: flex-end;
gap: 10px;
}
.header-actions form,
.header-actions .btn {
width: 100%;
}
.header-actions form button {
width: 100%;
}
}
@media (max-width: 480px) {
header {
padding: 12px;
}
.layout,
.layout.no-sidebar {
padding: 16px 12px 24px;
}
main {
padding: 18px 12px;
}
.card {
padding: 18px 14px;
}
}
</style>
<!-- Leaflet Maps Library -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
@stack('head')
</head>
<body>
<header>
<div class="header-left">
<button class="sidebar-toggle" type="button" aria-label="Toggle sidebar" data-sidebar-toggle>
<span></span>
<span></span>
<span></span>
</button>
<div class="brand">
<span style="display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; border-radius:12px; background:linear-gradient(135deg, rgba(99,102,241,0.65), rgba(37,99,235,0.5)); box-shadow:0 10px 22px rgba(37,99,235,0.28);">
<span style="font-weight:700; font-size:16px;">A</span>
</span>
Absensi
</div>
</div>
<nav class="header-actions">
@auth
<span class="role-pill">{{ auth()->user()->role ?? 'User' }}</span>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-danger btn-sm">Logout</button>
</form>
@endauth
@guest
<a href="{{ route('login') }}" class="btn btn-primary btn-sm">Login</a>
@endguest
</nav>
<div class="sidebar-overlay" data-sidebar-overlay></div>
</header>
<div class="layout {{ auth()->check() ? '' : 'no-sidebar' }}">
@auth
<aside class="sidebar">
@if(in_array(auth()->user()->role ?? 'pegawai', ['admin','atasan']))
<ul class="menu">
<li><a href="{{ route('admin.absensi.index') }}" class="{{ request()->is('admin/absensi*') ? 'active' : '' }}">Data Absensi</a></li>
<li><a href="{{ route('admin.barang_rusak.index') }}" class="{{ request()->is('admin/barang-rusak*') ? 'active' : '' }}">Laporan Barang Rusak</a></li>
<li><a href="{{ route('admin.cuti.index') }}" class="{{ request()->is('admin/cuti*') ? 'active' : '' }}">Pengajuan Cuti</a></li>
<li><a href="{{ route('admin.notifications.index') }}" class="{{ request()->is('admin/notifications*') ? 'active' : '' }}">Pemberitahuan</a></li>
<li><a href="{{ route('admin.users.index') }}" class="{{ request()->is('admin/users*') ? 'active' : '' }}">Daftar Pengguna</a></li>
</ul>
@php
$sidebarUsers = cache()->remember('sidebar_users_summary', 60, function () {
return [
'total' => \App\Models\User::count(),
'items' => \App\Models\User::select('id', 'name', 'username', 'role', 'attendance_enabled')
->orderBy('name')
->limit(6)
->get(),
];
});
@endphp
<div class="sidebar-users">
<div class="sidebar-users-header">
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<div>
<span>Pengguna Terdaftar</span>
<span class="sidebar-users-total">{{ $sidebarUsers['total'] }}</span>
</div>
</div>
</div>
<ul class="sidebar-users-list">
@foreach($sidebarUsers['items'] as $user)
@php
$initials = collect(explode(' ', trim($user->name)))->filter()->map(fn ($part) => mb_substr($part, 0, 1))->take(2)->implode('');
$initials = mb_strtoupper($initials ?: 'U');
@endphp
<li>
<div class="sidebar-users-item" style="display:flex;align-items:center;gap:12px;">
<a href="{{ route('admin.users.show', $user->id) }}" style="display:flex;align-items:center;gap:12px;text-decoration:none;color:inherit;flex:1;">
<span class="sidebar-users-avatar">{{ $initials }}</span>
<div class="sidebar-users-info" style="min-width:0;">
<strong style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px;">{{ $user->name }}</strong>
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px;">{{ $user->username ?? 'tanpa username' }} {{ strtoupper($user->role ?? '-') }}</span>
</div>
</a>
{{-- Tombol ON/OFF dan Hapus dipindahkan ke halaman Daftar Pengguna --}}
</div>
</li>
@endforeach
</ul>
<div class="sidebar-users-footer">Menampilkan {{ $sidebarUsers['items']->count() }} dari {{ $sidebarUsers['total'] }} pengguna.</div>
</div>
@else
@php
$unreadNotifications = auth()->user()->notifications()->unread()->count();
@endphp
<ul class="menu">
<li class="menu-heading">Navigasi Utama</li>
<li>
<a href="{{ route('user.absensi') }}" class="{{ request()->routeIs('user.absensi') ? 'active' : '' }}">
<span class="menu-label">
Absensi
<small>Kelola absensi harian Anda</small>
</span>
</a>
</li>
<li>
<a href="{{ route('barang-rusak.index') }}" class="{{ request()->routeIs('barang-rusak.*') ? 'active' : '' }}">
<span class="menu-label">
Barang Rusak
<small>Laporkan kerusakan dan pantau status</small>
</span>
</a>
</li>
<li>
<a href="{{ route('cuti.form') }}" class="{{ request()->routeIs('cuti.form') ? 'active' : '' }}">
<span class="menu-label">
Form Cuti
<small>Ajukan permohonan cuti</small>
</span>
</a>
</li>
<li>
<a href="{{ route('profile.index') }}" class="{{ request()->routeIs('profile.*') ? 'active' : '' }}">
<span class="menu-label">
Profil
<small>Perbarui informasi pribadi</small>
</span>
</a>
</li>
<li class="menu-heading" style="margin-top: 10px;">Komunikasi</li>
<li>
<a href="{{ route('notifications.index') }}" class="notif-link {{ request()->routeIs('notifications.*') ? 'active' : '' }}">
<span class="menu-label">
Pemberitahuan
<small>Kabar terbaru dari admin</small>
</span>
@if($unreadNotifications > 0)
<span class="notif-badge">{{ $unreadNotifications > 9 ? '9+' : $unreadNotifications }}</span>
@endif
</a>
</li>
</ul>
@endif
</aside>
@endauth
<main>
@yield('content')
</main>
</div>
<script>
(function () {
const body = document.body;
const toggle = document.querySelector('[data-sidebar-toggle]');
const overlay = document.querySelector('[data-sidebar-overlay]');
const sidebar = document.querySelector('.sidebar');
if (!toggle || !overlay || !sidebar) {
return;
}
const DESKTOP_BREAKPOINT = 1024;
const EDGE_GESTURE_START = 64;
const SWIPE_TRIGGER_DISTANCE = 60;
const MAX_VERTICAL_DRIFT = 90;
const applyScrollLock = () => {
if (window.innerWidth <= DESKTOP_BREAKPOINT && body.classList.contains('sidebar-open')) {
body.style.overflow = 'hidden';
} else {
body.style.overflow = '';
}
};
const openSidebar = () => {
if (body.classList.contains('sidebar-open')) {
applyScrollLock();
return;
}
body.classList.add('sidebar-open');
applyScrollLock();
};
const closeSidebar = () => {
if (!body.classList.contains('sidebar-open')) {
body.style.overflow = '';
return;
}
body.classList.remove('sidebar-open');
body.style.overflow = '';
};
const toggleSidebar = () => {
if (body.classList.contains('sidebar-open')) {
closeSidebar();
} else {
openSidebar();
}
};
toggle.addEventListener('click', (event) => {
event.stopPropagation();
toggleSidebar();
});
overlay.addEventListener('click', closeSidebar);
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
closeSidebar();
}
});
window.addEventListener('resize', () => {
applyScrollLock();
});
const sidebarLinks = sidebar.querySelectorAll('.menu a');
sidebarLinks.forEach((link) => {
link.addEventListener('click', () => {
if (window.innerWidth <= DESKTOP_BREAKPOINT) {
closeSidebar();
}
});
});
const shouldStartGesture = (event) => {
if (window.innerWidth > DESKTOP_BREAKPOINT) {
return false;
}
const open = body.classList.contains('sidebar-open');
if (!open) {
return event.clientX <= EDGE_GESTURE_START;
}
return sidebar.contains(event.target) || overlay.contains(event.target);
};
let pointerId = null;
let pointerActive = false;
let startX = 0;
let startY = 0;
const resetPointer = () => {
pointerId = null;
pointerActive = false;
startX = 0;
startY = 0;
};
const onPointerDown = (event) => {
if (!(event.pointerType === 'touch' || event.pointerType === 'pen')) {
return;
}
if (!shouldStartGesture(event)) {
resetPointer();
return;
}
pointerId = event.pointerId;
pointerActive = true;
startX = event.clientX;
startY = event.clientY;
};
const onPointerMove = (event) => {
if (!pointerActive || event.pointerId !== pointerId) {
return;
}
const deltaX = event.clientX - startX;
const deltaY = Math.abs(event.clientY - startY);
const open = body.classList.contains('sidebar-open');
if (deltaY > MAX_VERTICAL_DRIFT && Math.abs(deltaX) < deltaY) {
resetPointer();
return;
}
if (!open && deltaX > SWIPE_TRIGGER_DISTANCE) {
openSidebar();
resetPointer();
return;
}
if (open && deltaX < -SWIPE_TRIGGER_DISTANCE) {
closeSidebar();
resetPointer();
}
};
const onPointerEnd = (event) => {
if (event.pointerId === pointerId) {
resetPointer();
}
};
if (window.PointerEvent) {
document.addEventListener('pointerdown', onPointerDown);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerEnd);
document.addEventListener('pointercancel', onPointerEnd);
} else {
document.addEventListener('touchstart', (event) => {
if (event.touches.length !== 1) {
return;
}
const touch = event.touches[0];
if (!shouldStartGesture({ clientX: touch.clientX, target: event.target })) {
return;
}
pointerActive = true;
startX = touch.clientX;
startY = touch.clientY;
}, { passive: true });
document.addEventListener('touchmove', (event) => {
if (!pointerActive || event.touches.length !== 1) {
return;
}
const touch = event.touches[0];
const deltaX = touch.clientX - startX;
const deltaY = Math.abs(touch.clientY - startY);
const open = body.classList.contains('sidebar-open');
if (deltaY > MAX_VERTICAL_DRIFT && Math.abs(deltaX) < deltaY) {
resetPointer();
return;
}
if (!open && deltaX > SWIPE_TRIGGER_DISTANCE) {
openSidebar();
resetPointer();
return;
}
if (open && deltaX < -SWIPE_TRIGGER_DISTANCE) {
closeSidebar();
resetPointer();
}
}, { passive: true });
document.addEventListener('touchend', resetPointer);
document.addEventListener('touchcancel', resetPointer);
}
applyScrollLock();
})();
</script>
</body>
</html>