update: tester submit struk

main
marszayn 2025-08-04 15:16:29 +07:00
parent b701c86656
commit a4b5c45312
10 changed files with 1446 additions and 127 deletions

View File

@ -4,26 +4,26 @@ using eSPJ.Models;
namespace eSPJ.Controllers.SpjDriverController; namespace eSPJ.Controllers.SpjDriverController;
[Route("")]
public class HomeController : Controller public class HomeController : Controller
{ {
private readonly ILogger<HomeController> _logger; private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger) public HomeController(ILogger<HomeController> logger)
{ {
_logger = logger; _logger = logger;
} }
[HttpGet("")]
public IActionResult Index() public IActionResult Index()
{ {
return View("~/Views/Admin/Transport/SpjDriver/Home/Index.cshtml"); return View("~/Views/Admin/Transport/SpjDriver/Home/Index.cshtml");
} }
[HttpGet("kosong")]
public IActionResult Privacy() public IActionResult Kosong()
{ {
return View("~/Views/Admin/Transport/SpjDriver/Home/Privacy.cshtml"); return View("~/Views/Admin/Transport/SpjDriver/Home/Kosong.cshtml");
} }
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() public IActionResult Error()
{ {

View File

@ -17,7 +17,7 @@ namespace eSPJ.Controllers.SpjDriverController
{ {
return View("~/Views/Admin/Transport/SpjDriver/Scan/Detail.cshtml"); return View("~/Views/Admin/Transport/SpjDriver/Scan/Detail.cshtml");
} }
[HttpGet("create")] [HttpGet("create")]
public IActionResult Create() public IActionResult Create()
{ {
@ -29,30 +29,19 @@ namespace eSPJ.Controllers.SpjDriverController
{ {
try try
{ {
// Validate barcode
if (string.IsNullOrEmpty(barcode)) if (string.IsNullOrEmpty(barcode))
{ {
TempData["Error"] = "Kode barcode tidak boleh kosong."; TempData["Error"] = "Kode barcode tidak boleh kosong.";
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
// Basic validation for SPJ barcode format
if (barcode.Length < 5) if (barcode.Length < 5)
{ {
TempData["Error"] = "Format kode SPJ tidak valid. Minimal 5 karakter."; TempData["Error"] = "Format kode SPJ tidak valid. Minimal 5 karakter.";
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
// Clean the barcode (remove any whitespace)
barcode = barcode.Trim(); barcode = barcode.Trim();
// TODO: Add your SPJ validation logic here
// For example:
// - Check if SPJ exists in database
// - Validate SPJ format according to your business rules
// - Check SPJ status (active, completed, etc.)
// Simulate SPJ lookup (replace with your actual database logic)
var spjData = await ValidateSpjCode(barcode); var spjData = await ValidateSpjCode(barcode);
if (spjData == null) if (spjData == null)
@ -61,20 +50,13 @@ namespace eSPJ.Controllers.SpjDriverController
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
// Success - redirect to detail page or next step
TempData["Success"] = $"SPJ '{barcode}' berhasil ditemukan!"; TempData["Success"] = $"SPJ '{barcode}' berhasil ditemukan!";
// Redirect to appropriate page based on your workflow
// For example, redirect to detail page:
return RedirectToAction("Index", "Detail", new { spjCode = barcode }); return RedirectToAction("Index", "Detail", new { spjCode = barcode });
// Or redirect to submission page:
// return RedirectToAction("Index", "Submit", new { spjCode = barcode });
} }
catch (Exception) catch (Exception)
{ {
// Log the error (add your logging here)
TempData["Error"] = "Terjadi kesalahan saat memproses scan. Silakan coba lagi."; TempData["Error"] = "Terjadi kesalahan saat memproses scan. Silakan coba lagi.";
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
@ -82,20 +64,10 @@ namespace eSPJ.Controllers.SpjDriverController
private async Task<SpjData?> ValidateSpjCode(string barcode) private async Task<SpjData?> ValidateSpjCode(string barcode)
{ {
// TODO: Implement your SPJ validation logic here
// This is just a sample implementation
try try
{ {
// Simulate database lookup await Task.Delay(100);
await Task.Delay(100); // Simulate async operation
// Example validation rules:
// 1. Check format (e.g., starts with "SPJ" or specific pattern)
// 2. Check if exists in database
// 3. Check status
// For demo purposes, accept codes that start with "SPJ"
if (barcode.ToUpper().StartsWith("SPJ")) if (barcode.ToUpper().StartsWith("SPJ"))
{ {
return new SpjData return new SpjData
@ -107,7 +79,6 @@ namespace eSPJ.Controllers.SpjDriverController
}; };
} }
// Return null if not found or invalid
return null; return null;
} }
catch catch
@ -116,7 +87,6 @@ namespace eSPJ.Controllers.SpjDriverController
} }
} }
// Sample model for SPJ data (replace with your actual model)
public class SpjData public class SpjData
{ {
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;

View File

@ -18,5 +18,43 @@ namespace eSPJ.Controllers.SpjDriverController
{ {
return View("~/Views/Admin/Transport/SpjDriver/Submit/Struk.cshtml"); return View("~/Views/Admin/Transport/SpjDriver/Submit/Struk.cshtml");
} }
[HttpPost("struk")]
public IActionResult ProcessStruk(string NomorStruk, string BeratMuatan)
{
try
{
// Validate inputs
if (string.IsNullOrEmpty(NomorStruk) || string.IsNullOrEmpty(BeratMuatan))
{
TempData["Error"] = "Nomor struk dan berat muatan harus diisi.";
return RedirectToAction("Struk");
}
if (NomorStruk.Length < 6)
{
TempData["Error"] = "Nomor struk minimal 6 digit.";
return RedirectToAction("Struk");
}
if (!decimal.TryParse(BeratMuatan, out decimal berat) || berat <= 0)
{
TempData["Error"] = "Berat muatan harus berupa angka yang valid.";
return RedirectToAction("Struk");
}
// Here you would normally save to database
// For now, just simulate success
TempData["Success"] = $"Struk berhasil disubmit! No: {NomorStruk}, Berat: {BeratMuatan} kg";
return RedirectToAction("Index", "Home");
}
catch (Exception)
{
TempData["Error"] = "Terjadi kesalahan saat memproses struk. Silakan coba lagi.";
return RedirectToAction("Struk");
}
}
} }
} }

View File

@ -0,0 +1,155 @@
@{
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
ViewData["Title"] = "History - DLH";
}
<div class="max-w-sm mx-auto bg-gray-50 min-h-screen">
<div class="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-4 py-4 sticky top-0 z-10 shadow-lg">
<div class="flex items-center justify-between">
<a href="@Url.Action("Index", "Home")" class="p-2 hover:bg-white/10 rounded-full transition-all duration-200">
<i class="w-5 h-5" data-lucide="chevron-left"></i>
</a>
<h1 class="text-lg font-bold">Riwayat Perjalanan</h1>
<div class="w-9"></div>
</div>
</div>
@{
var spjList = new[]
{
new {
Id = 1,
NoSpj = "SPJ/07-2025/PKM/000478",
Plat = "B 5678 ABC",
Kode = "JRC 007",
Tujuan = "Bantar Gebang",
Status = "In Progress",
Tanggal = "28 Jul 2025",
Waktu = "16:45"
},
new {
Id = 2,
NoSpj = "SPJ/07-2025/PKM/000476",
Plat = "B 9632 TOR",
Kode = "JRC 005",
Tujuan = "RDF Rorotan",
Status = "Completed",
Tanggal = "27 Jul 2025",
Waktu = "14:30"
},
new {
Id = 3,
NoSpj = "SPJ/07-2025/PKM/000477",
Plat = "B 1234 XYZ",
Kode = "JRC 006",
Tujuan = "RDF Pesanggarahan",
Status = "Completed",
Tanggal = "26 Jul 2025",
Waktu = "09:15"
},
new {
Id = 4,
NoSpj = "SPJ/07-2025/PKM/000479",
Plat = "B 9876 DEF",
Kode = "JRC 008",
Tujuan = "RDF Sunter",
Status = "Completed",
Tanggal = "25 Jul 2025",
Waktu = "11:20"
},
new {
Id = 5,
NoSpj = "SPJ/07-2025/PKM/000480",
Plat = "B 4321 GHI",
Kode = "JRC 009",
Tujuan = "Bantar Gebang",
Status = "Completed",
Tanggal = "24 Jul 2025",
Waktu = "08:45"
}
};
}
<div class="px-4 py-4 space-y-3">
@foreach (var spj in spjList)
{
<a href="@Url.Action("Details", "History", new { id = spj.Id })" class="block">
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 hover:shadow-lg hover:border-orange-200 transition-all duration-300 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-400 to-orange-500"></div>
<div class="flex items-start justify-between mb-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<div class="w-2 h-2 bg-orange-400 rounded-full"></div>
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">No. SPJ</span>
</div>
<div class="font-bold text-gray-900 text-sm">@spj.NoSpj</div>
</div>
<div class="flex flex-col items-end gap-1">
@if (spj.Status == "Completed")
{
<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs font-semibold flex items-center gap-1">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
Selesai
</span>
}
else
{
<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs font-semibold flex items-center gap-1">
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
Berlangsung
</span>
}
</div>
</div>
<div class="bg-gray-50 rounded-xl p-3 mb-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<i class="w-5 h-5 text-orange-600" data-lucide="car"></i>
</div>
<div>
<div class="font-bold text-gray-900 text-sm">@spj.Plat</div>
<div class="text-xs text-gray-500">@spj.Kode</div>
</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">@spj.Tanggal</div>
<div class="text-xs font-medium text-gray-700">@spj.Waktu</div>
</div>
</div>
</div>
<div class="flex items-center gap-3 pt-2 border-t border-gray-100">
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0">
<i class="w-4 h-4 text-green-600" data-lucide="map-pin"></i>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500 mb-1">Tujuan</div>
<div class="font-semibold text-gray-900 text-sm">@spj.Tujuan</div>
</div>
<div class="p-2 hover:bg-gray-100 rounded-full transition-colors">
<i class="w-4 h-4 text-gray-400" data-lucide="chevron-right"></i>
</div>
</div>
</div>
</a>
}
</div>
<!-- Bottom Navigation -->
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_Navigation.cshtml" />
<!-- Kalau butuh tampilan kosong (jika tidak ada data) -->
@* <div class="flex flex-col items-center justify-center py-16 px-4">
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<i class="w-12 h-12 text-gray-400" data-lucide="clock"></i>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Belum Ada Riwayat</h3>
<p class="text-gray-500 text-center text-sm">Riwayat perjalanan Anda akan muncul di sini setelah melakukan perjalanan pertama.</p>
</div> *@
</div>

View File

@ -0,0 +1,221 @@
@{
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
ViewData["Title"] = "Home Page";
}
<div class="container max-w-sm mx-auto bg-white min-h-screen">
<div class="absolute top-0 max-w-sm container mx-auto bg-orange-500 text-white rounded-br-[125px] h-[250px] flex flex-row justify-between items-start px-6 py-6 shadow-lg z-20">
<div class="flex flex-col">
<h1 class="text-md font-bold leading-tight text-white">Bonny Agung Putra</h1>
<p class="text-xs opacity-90 font-medium text-orange-100">Driver UPST</span></p>
<div class="mt-5 flex items-center gap-2">
<i class="w-4 h-4 text-white" data-lucide="map-pin"></i>
<span class="text-sm opacity-90">Lokasi Anda:</span>
</div>
<p id="userLocation" class="font-semibold text-xs tracking-wide cursor-pointer underline text-white hover:text-orange-200 transition">
Mendeteksi lokasi...
</p>
</div>
<div class="flex items-center justify-center">
<div class="relative flex flex-col items-center justify-center">
<div class="w-12 h-12 rounded-full border-3 border-white overflow-hidden shadow-md flex items-center justify-center cursor-pointer group" id="profileMenuButton">
<i class="w-8 h-8 text-white" data-lucide="user"></i>
</div>
<div id="profileMenuDropdown" class="absolute top-12 right-0 mt-2 w-32 bg-white rounded shadow-lg py-2 z-50 hidden">
<form method="post" asp-controller="Auth" asp-action="Logout">
<button type="submit" class="hover:cursor-pointer flex items-center gap-2 w-full text-left px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 transition rounded-md">
<i class="w-4 h-4" data-lucide="log-out"></i>
Logout
</button>
</form>
</div>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 mx-4 px-6 py-12 mt-40 rounded-3xl relative overflow-hidden shadow-2xl z-21 border border-slate-200">
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<div class="absolute top-4 right-8 w-6 h-6 bg-orange-300 rounded-full opacity-60 animate-bounce" style="animation-delay: 0.5s;"></div>
<div class="absolute top-12 left-12 w-4 h-4 bg-blue-400 rounded-full opacity-40 animate-pulse" style="animation-delay: 1s;"></div>
<div class="absolute bottom-8 left-8 w-5 h-5 bg-indigo-300 rounded-full opacity-50 animate-bounce" style="animation-delay: 1.5s;"></div>
<div class="absolute bottom-16 right-16 w-3 h-3 bg-slate-400 rounded-full opacity-30 animate-pulse" style="animation-delay: 2s;"></div>
</div>
<div class="relative z-10 text-center">
<div class="w-24 h-24 mx-auto mb-6 relative">
<div class="w-full h-full bg-gradient-to-br from-orange-400 to-orange-600 rounded-full flex items-center justify-center shadow-xl animate-pulse">
<div class="w-20 h-20 bg-white rounded-full flex items-center justify-center">
<i class="w-10 h-10 text-orange-500" data-lucide="clipboard-list"></i>
</div>
</div>
<div class="absolute inset-0 border-4 border-orange-300 rounded-full animate-ping opacity-30"></div>
</div>
<div class="space-y-4">
<h2 class="text-xl font-bold bg-gradient-to-r from-slate-700 to-slate-900 bg-clip-text text-transparent">
Belum Ada SPJ
</h2>
<p class="text-sm text-slate-600 leading-relaxed px-4 mb-2">
Anda belum memiliki <span class="font-semibold text-orange-600">Surat Perintah Jalan</span> yang aktif saat ini.
</p>
</div>
<div class="bg-white/70 backdrop-blur-sm border border-white/30 rounded-2xl p-5 mb-6 text-left">
<div class="space-y-2 text-xs text-slate-600">
<div class="flex items-start gap-2">
<div class="w-1 h-1 bg-orange-400 rounded-full mt-1.5 flex-shrink-0"></div>
<p>SPJ akan diterbitkan oleh admin sesuai jadwal kerja</p>
</div>
<div class="flex items-start gap-2">
<div class="w-1 h-1 bg-orange-400 rounded-full mt-1.5 flex-shrink-0"></div>
<p>Periksa koneksi internet dan aktifkan lokasi GPS</p>
</div>
</div>
</div>
<button id="refreshButton" class="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white px-6 py-3 rounded-2xl text-sm font-bold shadow-lg hover:shadow-xl transition-all duration-300 flex items-center gap-2 mx-auto transform hover:scale-105">
<i class="w-4 h-4" data-lucide="refresh-cw" id="refreshIcon"></i>
<span id="refreshText">Refresh Halaman</span>
</button>
</div>
</div>
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_NavigationAdmin.cshtml" />
</div>
<register-block dynamic-section="scripts" key="jsHomeKosong">
<style>
@@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
@@keyframes shimmer {
0% { background-position: -200px 0; }
100% { background-position: calc(200px + 100%) 0; }
}
.float-animation {
animation: float 3s ease-in-out infinite;
}
.shimmer {
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
background-size: 200px 100%;
animation: shimmer 2s infinite;
}
.refresh-spin {
animation: spin 1s linear infinite;
}
@@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const userLocationEl = document.getElementById("userLocation");
function reverseGeocode(lat, lng) {
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
.then(res => res.json())
.then(data => {
const address = data.display_name || `${lat}, ${lng}`;
userLocationEl.textContent = address;
localStorage.setItem("user_latitude", lat);
localStorage.setItem("user_longitude", lng);
localStorage.setItem("user_address", address);
})
.catch(() => {
userLocationEl.textContent = `${lat}, ${lng}`;
});
}
function getLocationUpdate() {
if ("geolocation" in navigator) {
userLocationEl.textContent = "Mendeteksi lokasi baru...";
navigator.geolocation.getCurrentPosition(
function (position) {
const lat = position.coords.latitude.toFixed(6);
const lng = position.coords.longitude.toFixed(6);
reverseGeocode(lat, lng);
},
function () {
userLocationEl.textContent = "Lokasi tidak diizinkan";
}
);
} else {
userLocationEl.textContent = "Browser tidak mendukung lokasi";
}
}
const savedAddress = localStorage.getItem("user_address");
if (savedAddress) {
userLocationEl.textContent = savedAddress;
} else {
getLocationUpdate();
}
userLocationEl.addEventListener("click", function () {
getLocationUpdate();
});
const refreshBtn = document.getElementById('refreshButton');
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
if (refreshBtn) {
refreshBtn.addEventListener("click", function() {
refreshIcon.style.animation = 'spin 1s linear infinite';
refreshText.textContent = 'Memuat...';
refreshBtn.disabled = true;
refreshBtn.style.opacity = '0.8';
setTimeout(() => {
location.reload();
}, 1000);
});
}
const mainIcon = document.querySelector('[data-lucide="clipboard-list"]');
if (mainIcon) {
mainIcon.closest('.w-24').classList.add('float-animation');
}
const mainCard = document.querySelector('.bg-gradient-to-br');
if (mainCard) {
mainCard.style.opacity = '0';
mainCard.style.transform = 'translateY(20px)';
mainCard.style.transition = 'all 0.6s ease-out';
setTimeout(() => {
mainCard.style.opacity = '1';
mainCard.style.transform = 'translateY(0)';
}, 100);
}
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const btn = document.getElementById("profileMenuButton");
const dropdown = document.getElementById("profileMenuDropdown");
btn.addEventListener("click", function (e) {
e.stopPropagation();
dropdown.classList.toggle("hidden");
});
document.addEventListener("click", function () {
dropdown.classList.add("hidden");
});
});
</script>
</register-block>

View File

@ -5,30 +5,6 @@
@section Styles { @section Styles {
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" /> <link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
<style>
/* Html5-QRCode specific styles */
#scanner-container video {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
border-radius: 8px;
}
#scanner-container canvas {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
border-radius: 8px;
}
/* Hide Html5-QRCode default UI elements */
#scanner-container select,
#scanner-container button {
display: none !important;
}
</style>
} }
@ -43,9 +19,7 @@
</div> </div>
</div> </div>
<!-- Scanner Section -->
<div class="p-4"> <div class="p-4">
<!-- Alert Messages -->
@if (TempData["Success"] != null) @if (TempData["Success"] != null)
{ {
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg"> <div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
@ -66,12 +40,9 @@
</div> </div>
} }
<!-- Camera Preview -->
<div class="scanner-container mb-4" style="height: 300px;"> <div class="scanner-container mb-4" style="height: 300px;">
<div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden"> <div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden">
<!-- Html5-QRCode will create video element here -->
<!-- Loading State -->
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10"> <div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
<div class="text-center text-white"> <div class="text-center text-white">
<div class="loading-spinner mx-auto mb-2"></div> <div class="loading-spinner mx-auto mb-2"></div>
@ -81,7 +52,6 @@
</div> </div>
</div> </div>
<!-- Scanner Controls -->
<div class="space-y-3 mb-4"> <div class="space-y-3 mb-4">
<button id="start-scanner" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner"> <button id="start-scanner" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner">
<i class="w-5 h-5 inline mr-2" data-lucide="camera"></i> <i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
@ -93,7 +63,6 @@
Hentikan Scan Hentikan Scan
</button> </button>
<!-- Camera Permission Info -->
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3"> <div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-start"> <div class="flex items-start">
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i> <i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
@ -109,7 +78,6 @@
</div> </div>
</div> </div>
<!-- Permission Denied Info -->
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3"> <div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
<div class="flex items-start"> <div class="flex items-start">
<i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i> <i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
@ -125,7 +93,6 @@
</div> </div>
</div> </div>
<!-- Scanning Tips -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm"> <div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
<div class="flex items-start"> <div class="flex items-start">
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i> <i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
@ -143,7 +110,6 @@
</div> </div>
</div> </div>
<!-- Manual Input Alternative -->
<div class="border-t pt-4"> <div class="border-t pt-4">
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3> <h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
<form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")"> <form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")">
@ -160,7 +126,6 @@
</form> </form>
</div> </div>
<!-- Scan Result -->
<div id="scan-result" class="hidden mt-4 p-4 bg-green-50 border border-green-200 rounded-lg scan-result-card"> <div id="scan-result" class="hidden mt-4 p-4 bg-green-50 border border-green-200 rounded-lg scan-result-card">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i> <i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
@ -177,7 +142,6 @@
</div> </div>
</div> </div>
<!-- Error Message -->
<div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg"> <div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<div class="flex items-center"> <div class="flex items-center">
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i> <i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
@ -185,16 +149,13 @@
</div> </div>
</div> </div>
</div> </div>
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_NavigationAdmin.cshtml" />
</div> </div>
<register-block dynamic-section="scripts" key="jsScan"> <register-block dynamic-section="scripts" key="jsScan">
<!-- Html5-QRCode Library -->
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script> <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
<!-- Fallback script loader -->
<script> <script>
// Check if library loaded, if not try alternative CDN
if (typeof Html5Qrcode === 'undefined') { if (typeof Html5Qrcode === 'undefined') {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js'; script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js';
@ -238,7 +199,6 @@
} }
checkBrowserSupport() { checkBrowserSupport() {
// Check if browser supports getUserMedia
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
this.startBtn.disabled = true; this.startBtn.disabled = true;
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Browser Tidak Didukung'; this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Browser Tidak Didukung';
@ -248,7 +208,6 @@
return; return;
} }
// Check if Html5Qrcode is loaded
if (typeof Html5Qrcode === 'undefined') { if (typeof Html5Qrcode === 'undefined') {
this.startBtn.disabled = true; this.startBtn.disabled = true;
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Library Tidak Dimuat'; this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Library Tidak Dimuat';
@ -258,7 +217,6 @@
return; return;
} }
// Check if page is served over HTTPS (required for camera access in production)
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
this.showError('Scanner barcode memerlukan koneksi HTTPS yang aman. Hubungi administrator sistem.'); this.showError('Scanner barcode memerlukan koneksi HTTPS yang aman. Hubungi administrator sistem.');
} }
@ -271,7 +229,6 @@
this.hideResult(); this.hideResult();
this.hidePermissionMessages(); this.hidePermissionMessages();
// Initialize Html5-QRCode scanner
await this.initializeHtml5QrCode(); await this.initializeHtml5QrCode();
this.isScanning = true; this.isScanning = true;
@ -287,23 +244,17 @@
async initializeHtml5QrCode() { async initializeHtml5QrCode() {
try { try {
// Show permission info
this.permissionInfo.classList.remove('hidden'); this.permissionInfo.classList.remove('hidden');
// Wait a moment to show the message
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
// Initialize Html5-QRCode
this.html5QrCode = new Html5Qrcode("scanner-container"); this.html5QrCode = new Html5Qrcode("scanner-container");
// Get available cameras - This will trigger permission request
const cameras = await Html5Qrcode.getCameras(); const cameras = await Html5Qrcode.getCameras();
if (cameras && cameras.length > 0) { if (cameras && cameras.length > 0) {
// Try to use back camera first, fallback to first available
let cameraId = cameras[0].id; let cameraId = cameras[0].id;
// Look for back camera
const backCamera = cameras.find(camera => const backCamera = cameras.find(camera =>
camera.label.toLowerCase().includes('back') || camera.label.toLowerCase().includes('back') ||
camera.label.toLowerCase().includes('rear') || camera.label.toLowerCase().includes('rear') ||
@ -314,14 +265,12 @@
cameraId = backCamera.id; cameraId = backCamera.id;
} }
// Start scanning with square QR code focus
await this.html5QrCode.start( await this.html5QrCode.start(
cameraId, cameraId,
{ {
fps: 10, // Frame per second fps: 10,
qrbox: function(viewfinderWidth, viewfinderHeight) { qrbox: function(viewfinderWidth, viewfinderHeight) {
// Make it square - use the smaller dimension let minEdgePercentage = 0.7;
let minEdgePercentage = 0.7; // 70% of the smaller edge
let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage); let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
return { return {
@ -329,15 +278,13 @@
height: qrboxSize height: qrboxSize
}; };
}, },
aspectRatio: 1.0, // Square ratio for QR codes aspectRatio: 1.0,
rememberLastUsedCamera: true rememberLastUsedCamera: true
}, },
(decodedText, decodedResult) => { (decodedText, decodedResult) => {
// Success callback
this.handleBarcodeDetected(decodedText, decodedResult); this.handleBarcodeDetected(decodedText, decodedResult);
}, },
(errorMessage) => { (errorMessage) => {
// Error callback (optional, can be ignored for scanning errors)
} }
); );
@ -377,19 +324,14 @@
} }
handleBarcodeDetected(decodedText, decodedResult) { handleBarcodeDetected(decodedText, decodedResult) {
// Validate the code (basic validation)
if (decodedText && decodedText.length >= 5) { if (decodedText && decodedText.length >= 5) {
// Add visual feedback
this.flashSuccess(); this.flashSuccess();
this.detectedCode = decodedText; this.detectedCode = decodedText;
this.showResult(decodedText); this.showResult(decodedText);
this.stopScanner(); // Auto stop after detection this.stopScanner();
// Play a success sound
this.playSuccessSound(); this.playSuccessSound();
// Haptic feedback if supported
this.vibrate(); this.vibrate();
} }
} }
@ -399,7 +341,6 @@
try { try {
await this.html5QrCode.stop(); await this.html5QrCode.stop();
} catch (error) { } catch (error) {
// Error stopping scanner
} }
this.isScanning = false; this.isScanning = false;
} }
@ -409,7 +350,6 @@
} }
flashSuccess() { flashSuccess() {
// Add a green flash overlay to indicate successful scan
const flash = document.createElement('div'); const flash = document.createElement('div');
flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg'; flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg';
flash.style.zIndex = '20'; flash.style.zIndex = '20';
@ -421,15 +361,13 @@
} }
vibrate() { vibrate() {
// Provide haptic feedback on mobile devices
if ('vibrate' in navigator) { if ('vibrate' in navigator) {
navigator.vibrate([200]); // Vibrate for 200ms navigator.vibrate([200]);
} }
} }
confirmScan() { confirmScan() {
if (this.detectedCode) { if (this.detectedCode) {
// Auto-fill manual input and submit
this.manualInput.value = this.detectedCode; this.manualInput.value = this.detectedCode;
this.manualForm.submit(); this.manualForm.submit();
} }
@ -441,12 +379,10 @@
this.hidePermissionMessages(); this.hidePermissionMessages();
this.detectedCode = null; this.detectedCode = null;
// Stop current scanner if running
if (this.isScanning && this.html5QrCode) { if (this.isScanning && this.html5QrCode) {
await this.stopScanner(); await this.stopScanner();
} }
// Wait a bit before restarting
setTimeout(() => { setTimeout(() => {
this.startScanner(); this.startScanner();
}, 500); }, 500);
@ -499,7 +435,6 @@
} }
playSuccessSound() { playSuccessSound() {
// Create a simple beep sound
try { try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator(); const oscillator = audioContext.createOscillator();
@ -517,14 +452,11 @@
oscillator.start(audioContext.currentTime); oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.2); oscillator.stop(audioContext.currentTime + 0.2);
} catch (e) { } catch (e) {
// Ignore audio errors
} }
} }
} }
// Initialize scanner when page loads
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Function to check if Html5Qrcode is loaded
function waitForLibrary() { function waitForLibrary() {
if (typeof Html5Qrcode !== 'undefined') { if (typeof Html5Qrcode !== 'undefined') {
new BarcodeScanner(); new BarcodeScanner();
@ -533,7 +465,6 @@
} }
} }
// Start checking
waitForLibrary(); waitForLibrary();
}); });
</script> </script>

View File

@ -0,0 +1,48 @@
<div class="fixed bottom-0 left-1/2 transform -translate-x-1/2 w-full max-w-sm z-99">
<div class="relative backdrop-blur-lg border border-gray-200/50 rounded-t-3xl shadow-xl overflow-hidden">
<div class="absolute -top-0 left-1/2 transform -translate-x-1/2 w-20 h-10">
<div class="w-full h-full bg-transparent relative">
<div class="absolute left-0 top-0 w-10 h-10 backdrop-blur-lg rounded-br-full"></div>
<div class="absolute right-0 top-0 w-10 h-10 backdrop-blur-lg rounded-bl-full"></div>
</div>
</div>
<!-- Navigation Content -->
<div class="flex justify-between items-center px-8 relative pt-6">
<!-- Home Button -->
<a href="@Url.Action("Kosong", "Home")" class="flex flex-col items-center gap-1 px-4 py-2 transition-all duration-300 hover:scale-105 group">
<div class="relative">
<i class="w-6 h-6 text-gray-400 group-hover:text-orange-500 transition-colors duration-300" data-lucide="home"></i>
<div class="absolute -bottom-0.5 left-1/2 transform -translate-x-1/2 w-0 h-0.5 bg-orange-500 group-hover:w-full transition-all duration-300"></div>
</div>
<span class="text-xs text-gray-600 group-hover:text-orange-500 font-medium transition-colors duration-300">Home</span>
</a>
<div class="w-12"></div>
<!-- Profile Button -->
<a href="@Url.Action("Index", "History")" class="flex flex-col items-center gap-1 px-4 py-2 transition-all duration-300 hover:scale-105 group">
<div class="relative">
<i class="w-6 h-6 text-gray-400 group-hover:text-orange-500 transition-colors duration-300" data-lucide="clipboard-check"></i>
<div class="absolute -bottom-0.5 left-1/2 transform -translate-x-1/2 w-0 h-0.5 bg-orange-500 group-hover:w-full transition-all duration-300"></div>
</div>
<span class="text-xs text-gray-600 group-hover:text-orange-500 font-medium transition-colors duration-300">History</span>
</a>
</div>
</div>
<!-- Center Submit -->
<div class="absolute -top-4 left-1/2 transform -translate-x-1/2">
<a href="@Url.Action("Index", "Scan")" id="odoBtn" class="hover:cursor-pointer w-14 h-14 bg-gradient-to-br from-orange-500 via-orange-400 to-orange-600 rounded-full shadow-xl flex items-center justify-center transition-all duration-300 hover:scale-110 hover:rotate-6 border-4 border-white ring-2 ring-orange-200">
<i class="w-6 h-6 text-white" data-lucide="camera"></i>
</a>
</div>
</div>
<div class="h-30"></div>
<register-block dynamic-section="scripts" key="jsNav">
</register-block>

View File

@ -3,6 +3,9 @@
ViewData["Title"] = "Submit Struk"; ViewData["Title"] = "Submit Struk";
} }
@section Styles {
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
}
<div class="max-w-sm mx-auto bg-white min-h-screen"> <div class="max-w-sm mx-auto bg-white min-h-screen">
<!-- Header with Orange Background --> <!-- Header with Orange Background -->
@ -16,13 +19,134 @@
</div> </div>
</div> </div>
<form action="/submit-struk" method="post" class="px-8 py-8 space-y-6 bg-white rounded-xl mt-2"> <div class="px-8 py-4">
<!-- Camera Scanner Section -->
<div class="mb-6">
<div class="flex flex-col items-center space-y-2 mb-4">
<div class="bg-orange-100 rounded-full p-3">
<i data-lucide="camera" class="w-7 h-7 text-orange-500"></i>
</div>
<h2 class="text-xl font-bold text-orange-500">Scan Struk Otomatis</h2>
<p class="text-sm text-gray-500 text-center">Arahkan kamera ke struk untuk membaca data secara otomatis.</p>
</div>
<!-- Scanner Container -->
<div class="scanner-container mb-4" style="height: 300px;">
<div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden">
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
<div class="text-center text-white">
<div class="loading-spinner mx-auto mb-2"></div>
<p class="text-sm">Memuat scanner...</p>
</div>
</div>
</div>
</div>
<!-- Scanner Controls -->
<div class="space-y-3 mb-4">
<button id="start-scanner" type="button" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner">
<i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
Mulai Scan Struk
</button>
<button id="stop-scanner" type="button" class="w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner hidden">
<i class="w-5 h-5 inline mr-2" data-lucide="camera-off"></i>
Hentikan Scan
</button>
<!-- Permission Messages -->
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-start">
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
<div class="text-blue-800 text-sm">
<p class="font-medium mb-1">📸 Meminta Akses Kamera...</p>
<p class="mb-2">Browser akan meminta izin akses kamera. Pastikan untuk:</p>
<ul class="text-xs space-y-1 list-disc list-inside">
<li>Klik tombol <strong>"Allow"</strong> atau <strong>"Izinkan"</strong></li>
<li>Jika popup tidak muncul, cek address bar browser</li>
<li>Pastikan kamera tidak sedang digunakan aplikasi lain</li>
</ul>
</div>
</div>
</div>
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
<div class="flex items-start">
<i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
<div class="text-red-800 text-sm">
<p class="font-medium mb-1">Akses Kamera Ditolak</p>
<p class="mb-2">Untuk menggunakan scanner, aktifkan akses kamera:</p>
<ol class="list-decimal list-inside space-y-1 text-xs">
<li>Klik ikon kunci/kamera di address bar browser</li>
<li>Pilih "Allow" atau "Izinkan" untuk kamera</li>
<li>Refresh halaman dan coba lagi</li>
</ol>
</div>
</div>
</div>
<!-- OCR Processing Message -->
<div id="ocr-processing" class="hidden bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div class="flex items-center">
<div class="loading-spinner-small mr-2"></div>
<span class="text-yellow-800 text-sm">Memproses teks dari struk...</span>
</div>
</div>
<!-- OCR Result -->
<div id="ocr-result" class="hidden bg-green-50 border border-green-200 rounded-lg p-3">
<div class="flex items-center mb-2">
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
<span class="text-green-800 font-medium">Data struk berhasil dibaca!</span>
</div>
<div class="text-green-700 text-sm space-y-1">
<p>Nomor Struk: <span id="detected-receipt-number" class="font-mono font-bold">-</span></p>
<p>Berat Muatan: <span id="detected-weight" class="font-mono font-bold">-</span> kg</p>
</div>
<div class="flex gap-2 mt-3">
<button id="apply-ocr-data" type="button" class="bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded-lg transition-colors text-sm">
Gunakan Data Ini
</button>
<button id="retry-ocr" type="button" class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-2 rounded-lg transition-colors text-sm">
Scan Ulang
</button>
</div>
</div>
<!-- Tips for scanning -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
<div class="flex items-start">
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
<div class="text-gray-700">
<p class="font-medium mb-1">Tips Scan Struk:</p>
<ul class="text-xs space-y-1">
<li>• Pastikan struk dalam pencahayaan yang cukup</li>
<li>• Letakkan struk rata tanpa lipatan</li>
<li>• Jaga jarak 15-30cm dari kamera</li>
<li>• Tunggu beberapa detik untuk proses OCR</li>
<li>• Periksa data yang terdeteksi sebelum submit</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Divider -->
<div class="flex items-center my-6">
<div class="flex-1 border-t border-gray-200"></div>
<span class="px-3 text-sm text-gray-500 bg-white">atau input manual</span>
<div class="flex-1 border-t border-gray-200"></div>
</div>
</div>
<form action="@Url.Action("ProcessStruk", "Submit")" method="post" class="px-8 py-4 space-y-6 bg-white">
<div class="flex flex-col items-center space-y-2"> <div class="flex flex-col items-center space-y-2">
<div class="bg-orange-100 rounded-full p-3"> <div class="bg-orange-100 rounded-full p-3">
<i data-lucide="file-text" class="w-7 h-7 text-orange-500"></i> <i data-lucide="edit-3" class="w-7 h-7 text-orange-500"></i>
</div> </div>
<h2 class="text-xl font-bold text-orange-500">Isi Data Struk</h2> <h2 class="text-xl font-bold text-orange-500">Input Manual</h2>
<p class="text-sm text-gray-500 text-center">Masukkan nomor struk dan berat muatan dengan benar.</p> <p class="text-sm text-gray-500 text-center">Masukkan nomor struk dan berat muatan secara manual.</p>
</div> </div>
<div> <div>
@ -65,11 +189,13 @@
</div> </div>
<register-block dynamic-section="scripts" key="jsSubmitStruk"> <register-block dynamic-section="scripts" key="jsSubmitStruk">
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@4.1.1/dist/tesseract.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const nomorStrukInput = document.getElementById('NomorStruk'); const nomorStrukInput = document.getElementById('NomorStruk');
const beratMuatanInput = document.getElementById('BeratMuatan'); const beratMuatanInput = document.getElementById('BeratMuatan');
// Input validation for manual entry
nomorStrukInput.addEventListener('input', function() { nomorStrukInput.addEventListener('input', function() {
this.value = this.value.replace(/[^0-9]/g, ''); this.value = this.value.replace(/[^0-9]/g, '');
}); });
@ -77,6 +203,396 @@
beratMuatanInput.addEventListener('input', function() { beratMuatanInput.addEventListener('input', function() {
this.value = this.value.replace(/[^0-9.]/g, ''); this.value = this.value.replace(/[^0-9.]/g, '');
}); });
// Initialize Receipt Scanner with OCR
class ReceiptScanner {
constructor() {
this.isScanning = false;
this.stream = null;
this.video = null;
this.canvas = null;
this.ctx = null;
this.detectedData = {
receiptNumber: '',
weight: ''
};
this.initializeElements();
this.bindEvents();
this.checkBrowserSupport();
}
initializeElements() {
this.startBtn = document.getElementById('start-scanner');
this.stopBtn = document.getElementById('stop-scanner');
this.loadingDiv = document.getElementById('loading-scanner');
this.ocrProcessing = document.getElementById('ocr-processing');
this.ocrResult = document.getElementById('ocr-result');
this.permissionInfo = document.getElementById('permission-info');
this.permissionDenied = document.getElementById('permission-denied');
this.applyDataBtn = document.getElementById('apply-ocr-data');
this.retryOcrBtn = document.getElementById('retry-ocr');
this.detectedReceiptSpan = document.getElementById('detected-receipt-number');
this.detectedWeightSpan = document.getElementById('detected-weight');
this.scannerContainer = document.getElementById('scanner-container');
}
bindEvents() {
this.startBtn.addEventListener('click', () => this.startScanner());
this.stopBtn.addEventListener('click', () => this.stopScanner());
this.applyDataBtn.addEventListener('click', () => this.applyDetectedData());
this.retryOcrBtn.addEventListener('click', () => this.retryOcr());
}
checkBrowserSupport() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
this.disableScanner('Browser Tidak Didukung');
return;
}
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
this.showWarning('Scanner berfungsi optimal dengan koneksi HTTPS yang aman.');
}
}
disableScanner(message) {
this.startBtn.disabled = true;
this.startBtn.innerHTML = `<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>${message}`;
this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
}
async startScanner() {
try {
this.showLoading();
this.hideMessages();
this.permissionInfo.classList.remove('hidden');
// Get camera access
this.stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
}
});
this.setupVideoElement();
this.isScanning = true;
this.startBtn.classList.add('hidden');
this.stopBtn.classList.remove('hidden');
this.hideLoading();
this.hideMessages();
// Start continuous capture for OCR
this.startContinuousCapture();
} catch (error) {
this.handleCameraError(error);
this.hideLoading();
}
}
setupVideoElement() {
// Create video element
this.video = document.createElement('video');
this.video.autoplay = true;
this.video.playsInline = true;
this.video.muted = true;
this.video.srcObject = this.stream;
// Style video element
this.video.className = 'w-full h-full object-cover rounded-lg';
// Clear container and add video
this.scannerContainer.innerHTML = '';
this.scannerContainer.appendChild(this.video);
// Create canvas for capture
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
}
startContinuousCapture() {
// Capture frame every 3 seconds for OCR processing
const captureInterval = setInterval(() => {
if (!this.isScanning) {
clearInterval(captureInterval);
return;
}
this.captureAndProcessFrame();
}, 3000);
}
async captureAndProcessFrame() {
if (!this.video || !this.canvas || !this.isScanning) return;
try {
// Set canvas size to video size
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
// Draw video frame to canvas
this.ctx.drawImage(this.video, 0, 0);
// Convert to blob for OCR
this.canvas.toBlob(async (blob) => {
await this.processImageWithOCR(blob);
}, 'image/jpeg', 0.8);
} catch (error) {
console.error('Error capturing frame:', error);
}
}
async processImageWithOCR(imageBlob) {
try {
this.showOcrProcessing();
const { data: { text } } = await Tesseract.recognize(
imageBlob,
'ind+eng',
{
logger: m => {
// Optional: show OCR progress
if (m.status === 'recognizing text') {
console.log(`OCR Progress: ${Math.round(m.progress * 100)}%`);
}
}
}
);
await this.extractDataFromText(text);
this.hideOcrProcessing();
} catch (error) {
console.error('OCR Error:', error);
this.hideOcrProcessing();
}
}
async extractDataFromText(text) {
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
let receiptNumber = '';
let weight = '';
// Patterns for receipt number detection
const receiptPatterns = [
/(?:no|nomor|receipt|struk|bon)[\s.:]*([0-9]{6,})/i,
/([0-9]{8,})/g, // Long number sequences
/(?:ticket|tiket)[\s.:]*([0-9]+)/i
];
// Patterns for weight detection
const weightPatterns = [
/(?:berat|weight|kg|kilogram)[\s.:]*([0-9]+(?:\.[0-9]+)?)/i,
/([0-9]+(?:\.[0-9]+)?)[\s]*(?:kg|kilogram)/i,
/(?:muatan|load)[\s.:]*([0-9]+(?:\.[0-9]+)?)/i
];
// Search for receipt number
for (const line of lines) {
for (const pattern of receiptPatterns) {
const match = line.match(pattern);
if (match && match[1] && match[1].length >= 6) {
receiptNumber = match[1];
break;
}
}
if (receiptNumber) break;
}
// Search for weight
for (const line of lines) {
for (const pattern of weightPatterns) {
const match = line.match(pattern);
if (match && match[1]) {
const weightValue = parseFloat(match[1]);
if (weightValue > 0 && weightValue < 100000) { // Reasonable weight range
weight = match[1];
break;
}
}
}
if (weight) break;
}
// If we found either piece of data, show results
if (receiptNumber || weight) {
this.detectedData.receiptNumber = receiptNumber;
this.detectedData.weight = weight;
this.showOcrResult();
this.playSuccessSound();
this.vibrate();
// Auto-stop scanning after successful detection
setTimeout(() => {
this.stopScanner();
}, 1000);
}
}
showOcrResult() {
this.detectedReceiptSpan.textContent = this.detectedData.receiptNumber || 'Tidak terdeteksi';
this.detectedWeightSpan.textContent = this.detectedData.weight || 'Tidak terdeteksi';
this.ocrResult.classList.remove('hidden');
}
applyDetectedData() {
if (this.detectedData.receiptNumber) {
nomorStrukInput.value = this.detectedData.receiptNumber;
nomorStrukInput.classList.add('auto-filled');
setTimeout(() => nomorStrukInput.classList.remove('auto-filled'), 2000);
}
if (this.detectedData.weight) {
beratMuatanInput.value = this.detectedData.weight;
beratMuatanInput.classList.add('auto-filled');
setTimeout(() => beratMuatanInput.classList.remove('auto-filled'), 2000);
}
this.hideMessages();
// Show success message
this.showSuccess('Data dari scan telah diisi ke form!');
}
async retryOcr() {
this.hideMessages();
this.detectedData = { receiptNumber: '', weight: '' };
if (this.isScanning) {
await this.stopScanner();
}
setTimeout(() => {
this.startScanner();
}, 500);
}
async stopScanner() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
this.isScanning = false;
this.startBtn.classList.remove('hidden');
this.stopBtn.classList.add('hidden');
this.hideLoading();
this.hideMessages();
// Reset scanner container
this.scannerContainer.innerHTML = `
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
<div class="text-center text-white">
<div class="loading-spinner mx-auto mb-2"></div>
<p class="text-sm">Memuat scanner...</p>
</div>
</div>
`;
this.loadingDiv = document.getElementById('loading-scanner');
}
handleCameraError(error) {
if (error.name === 'NotAllowedError') {
this.permissionDenied.classList.remove('hidden');
} else if (error.name === 'NotFoundError') {
this.showError('Kamera tidak ditemukan pada perangkat ini.');
} else if (error.name === 'NotReadableError') {
this.showError('Kamera sedang digunakan aplikasi lain. Tutup aplikasi lain dan coba lagi.');
} else {
this.showError('Gagal mengakses kamera: ' + error.message);
}
}
// Helper methods
showLoading() {
this.loadingDiv.classList.remove('hidden');
}
hideLoading() {
this.loadingDiv.classList.add('hidden');
}
showOcrProcessing() {
this.ocrProcessing.classList.remove('hidden');
}
hideOcrProcessing() {
this.ocrProcessing.classList.add('hidden');
}
hideMessages() {
this.permissionInfo.classList.add('hidden');
this.permissionDenied.classList.add('hidden');
this.ocrProcessing.classList.add('hidden');
this.ocrResult.classList.add('hidden');
}
showError(message) {
// Create temporary error message
const errorDiv = document.createElement('div');
errorDiv.className = 'bg-red-50 border border-red-200 rounded-lg p-3 mt-2';
errorDiv.innerHTML = `
<div class="flex items-center">
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
<span class="text-red-800 text-sm">${message}</span>
</div>
`;
this.scannerContainer.parentNode.insertBefore(errorDiv, this.scannerContainer.nextSibling);
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
showSuccess(message) {
// Create temporary success message
const successDiv = document.createElement('div');
successDiv.className = 'bg-green-50 border border-green-200 rounded-lg p-3 mt-2';
successDiv.innerHTML = `
<div class="flex items-center">
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
<span class="text-green-800 text-sm">${message}</span>
</div>
`;
this.scannerContainer.parentNode.insertBefore(successDiv, this.scannerContainer.nextSibling);
setTimeout(() => {
successDiv.remove();
}, 3000);
}
playSuccessSound() {
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'square';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.2);
} catch (e) {
// Silent fail
}
}
vibrate() {
if ('vibrate' in navigator) {
navigator.vibrate([200]);
}
}
}
// Initialize the receipt scanner
new ReceiptScanner();
}); });
</script> </script>
</register-block> </register-block>

View File

@ -1,4 +1,3 @@
/* Scanner specific styles */
.scanner-container { .scanner-container {
position: relative; position: relative;
background: #1a1a1a; background: #1a1a1a;
@ -47,15 +46,13 @@
} }
} }
/* Video element styling */
#video-preview { #video-preview {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transform: scaleX(-1); /* Mirror effect for better UX */ transform: scaleX(-1);
} }
/* Canvas overlay for QuaggaJS */
.drawingBuffer { .drawingBuffer {
position: absolute !important; position: absolute !important;
top: 0 !important; top: 0 !important;
@ -63,7 +60,6 @@
z-index: 5 !important; z-index: 5 !important;
} }
/* Button states */
.btn-scanner { .btn-scanner {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@ -72,7 +68,6 @@
transform: scale(0.98); transform: scale(0.98);
} }
/* Loading animation improvements */
.loading-spinner { .loading-spinner {
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white; border-top: 2px solid white;
@ -82,6 +77,15 @@
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.loading-spinner-small {
border: 2px solid rgba(245, 158, 11, 0.3);
border-top: 2px solid #f59e0b;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
}
@keyframes spin { @keyframes spin {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
@ -91,7 +95,6 @@
} }
} }
/* Result card styling */
.scan-result-card { .scan-result-card {
animation: slideInUp 0.3s ease-out; animation: slideInUp 0.3s ease-out;
} }
@ -107,7 +110,58 @@
} }
} }
/* Mobile optimizations */ /* OCR Processing Animation */
.ocr-processing {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* Receipt capture overlay */
.receipt-overlay {
position: absolute;
top: 10%;
left: 10%;
right: 10%;
bottom: 10%;
border: 2px dashed rgba(245, 158, 11, 0.8);
border-radius: 8px;
pointer-events: none;
z-index: 10;
}
.receipt-overlay::before {
content: "Arahkan ke teks struk";
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(245, 158, 11, 0.9);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
/* Form transition effects */
.form-section {
transition: all 0.3s ease;
}
.form-section.disabled {
opacity: 0.6;
pointer-events: none;
}
@media (max-width: 640px) { @media (max-width: 640px) {
.scanner-overlay { .scanner-overlay {
width: 200px; width: 200px;
@ -117,11 +171,74 @@
.scanner-container { .scanner-container {
height: 250px !important; height: 250px !important;
} }
.receipt-overlay::before {
font-size: 11px;
padding: 3px 6px;
}
} }
/* Dark mode support */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.scanner-container { .scanner-container {
background: #0a0a0a; background: #0a0a0a;
} }
} }
#scanner-container video {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
border-radius: 8px;
}
#scanner-container canvas {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
border-radius: 8px;
}
#scanner-container select,
#scanner-container button {
display: none !important;
}
/* Success feedback */
.success-flash {
position: absolute;
inset: 0;
background: rgba(34, 197, 94, 0.3);
border-radius: 8px;
z-index: 15;
animation: flash 0.3s ease-out;
}
@keyframes flash {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Input highlight when auto-filled */
.auto-filled {
background-color: rgba(34, 197, 94, 0.1) !important;
border-color: rgb(34, 197, 94) !important;
animation: highlight 1s ease-out;
}
@keyframes highlight {
0% {
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.3);
}
100% {
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
}
}

View File

@ -54,11 +54,15 @@
--color-blue-50: oklch(97% 0.014 254.604); --color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585); --color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128); --color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-400: oklch(70.7% 0.165 254.624);
--color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376); --color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638); --color-blue-800: oklch(42.4% 0.199 265.638);
--color-indigo-50: oklch(96.2% 0.018 272.314); --color-indigo-50: oklch(96.2% 0.018 272.314);
--color-indigo-100: oklch(93% 0.034 272.788);
--color-indigo-200: oklch(87% 0.065 274.039);
--color-indigo-300: oklch(78.5% 0.115 274.713);
--color-purple-50: oklch(97.7% 0.014 308.299); --color-purple-50: oklch(97.7% 0.014 308.299);
--color-purple-100: oklch(94.6% 0.033 307.174); --color-purple-100: oklch(94.6% 0.033 307.174);
--color-purple-400: oklch(71.4% 0.203 305.504); --color-purple-400: oklch(71.4% 0.203 305.504);
@ -69,8 +73,10 @@
--color-slate-200: oklch(92.9% 0.013 255.508); --color-slate-200: oklch(92.9% 0.013 255.508);
--color-slate-400: oklch(70.4% 0.04 256.788); --color-slate-400: oklch(70.4% 0.04 256.788);
--color-slate-500: oklch(55.4% 0.046 257.417); --color-slate-500: oklch(55.4% 0.046 257.417);
--color-slate-600: oklch(44.6% 0.043 257.281);
--color-slate-700: oklch(37.2% 0.044 257.287); --color-slate-700: oklch(37.2% 0.044 257.287);
--color-slate-800: oklch(27.9% 0.041 260.031); --color-slate-800: oklch(27.9% 0.041 260.031);
--color-slate-900: oklch(20.8% 0.042 265.755);
--color-gray-50: oklch(98.5% 0.002 247.839); --color-gray-50: oklch(98.5% 0.002 247.839);
--color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-100: oklch(96.7% 0.003 264.542);
--color-gray-200: oklch(92.8% 0.006 264.531); --color-gray-200: oklch(92.8% 0.006 264.531);
@ -84,6 +90,7 @@
--color-black: #000; --color-black: #000;
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--container-xs: 20rem;
--container-sm: 24rem; --container-sm: 24rem;
--text-xs: 0.75rem; --text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75); --text-xs--line-height: calc(1 / 0.75);
@ -95,6 +102,8 @@
--text-lg--line-height: calc(1.75 / 1.125); --text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem; --text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25); --text-xl--line-height: calc(1.75 / 1.25);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--font-weight-medium: 500; --font-weight-medium: 500;
--font-weight-semibold: 600; --font-weight-semibold: 600;
--font-weight-bold: 700; --font-weight-bold: 700;
@ -113,7 +122,9 @@
--ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--animate-spin: spin 1s linear infinite; --animate-spin: spin 1s linear infinite;
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
--blur-sm: 8px; --blur-sm: 8px;
--blur-lg: 16px; --blur-lg: 16px;
--default-transition-duration: 150ms; --default-transition-duration: 150ms;
@ -352,6 +363,12 @@
.right-4 { .right-4 {
right: calc(var(--spacing) * 4); right: calc(var(--spacing) * 4);
} }
.right-8 {
right: calc(var(--spacing) * 8);
}
.right-16 {
right: calc(var(--spacing) * 16);
}
.right-full { .right-full {
right: 100%; right: 100%;
} }
@ -373,6 +390,9 @@
.bottom-8 { .bottom-8 {
bottom: calc(var(--spacing) * 8); bottom: calc(var(--spacing) * 8);
} }
.bottom-16 {
bottom: calc(var(--spacing) * 16);
}
.bottom-50 { .bottom-50 {
bottom: calc(var(--spacing) * 50); bottom: calc(var(--spacing) * 50);
} }
@ -388,6 +408,12 @@
.left-1\/2 { .left-1\/2 {
left: calc(1/2 * 100%); left: calc(1/2 * 100%);
} }
.left-8 {
left: calc(var(--spacing) * 8);
}
.left-12 {
left: calc(var(--spacing) * 12);
}
.z-0 { .z-0 {
z-index: 0; z-index: 0;
} }
@ -568,6 +594,9 @@
.my-5 { .my-5 {
margin-block: calc(var(--spacing) * 5); margin-block: calc(var(--spacing) * 5);
} }
.my-6 {
margin-block: calc(var(--spacing) * 6);
}
.my-auto { .my-auto {
margin-block: auto; margin-block: auto;
} }
@ -637,6 +666,9 @@
.mt-1 { .mt-1 {
margin-top: calc(var(--spacing) * 1); margin-top: calc(var(--spacing) * 1);
} }
.mt-1\.5 {
margin-top: calc(var(--spacing) * 1.5);
}
.mt-2 { .mt-2 {
margin-top: calc(var(--spacing) * 2); margin-top: calc(var(--spacing) * 2);
} }
@ -655,6 +687,9 @@
.mt-8 { .mt-8 {
margin-top: calc(var(--spacing) * 8); margin-top: calc(var(--spacing) * 8);
} }
.mt-10 {
margin-top: calc(var(--spacing) * 10);
}
.mt-40 { .mt-40 {
margin-top: calc(var(--spacing) * 40); margin-top: calc(var(--spacing) * 40);
} }
@ -688,6 +723,9 @@
.mb-6 { .mb-6 {
margin-bottom: calc(var(--spacing) * 6); margin-bottom: calc(var(--spacing) * 6);
} }
.mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
.mb-auto { .mb-auto {
margin-bottom: auto; margin-bottom: auto;
} }
@ -748,6 +786,9 @@
.h-1 { .h-1 {
height: calc(var(--spacing) * 1); height: calc(var(--spacing) * 1);
} }
.h-1\.5 {
height: calc(var(--spacing) * 1.5);
}
.h-2 { .h-2 {
height: calc(var(--spacing) * 2); height: calc(var(--spacing) * 2);
} }
@ -790,6 +831,9 @@
.h-25 { .h-25 {
height: calc(var(--spacing) * 25); height: calc(var(--spacing) * 25);
} }
.h-28 {
height: calc(var(--spacing) * 28);
}
.h-30 { .h-30 {
height: calc(var(--spacing) * 30); height: calc(var(--spacing) * 30);
} }
@ -820,6 +864,12 @@
.w-0 { .w-0 {
width: calc(var(--spacing) * 0); width: calc(var(--spacing) * 0);
} }
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\.5 {
width: calc(var(--spacing) * 1.5);
}
.w-2 { .w-2 {
width: calc(var(--spacing) * 2); width: calc(var(--spacing) * 2);
} }
@ -865,6 +915,9 @@
.w-25 { .w-25 {
width: calc(var(--spacing) * 25); width: calc(var(--spacing) * 25);
} }
.w-28 {
width: calc(var(--spacing) * 28);
}
.w-32 { .w-32 {
width: calc(var(--spacing) * 32); width: calc(var(--spacing) * 32);
} }
@ -892,6 +945,9 @@
.max-w-sm { .max-w-sm {
max-width: var(--container-sm); max-width: var(--container-sm);
} }
.max-w-xs {
max-width: var(--container-xs);
}
.min-w-0 { .min-w-0 {
min-width: calc(var(--spacing) * 0); min-width: calc(var(--spacing) * 0);
} }
@ -940,6 +996,10 @@
--tw-translate-x: calc(var(--spacing) * -12); --tw-translate-x: calc(var(--spacing) * -12);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
} }
.translate-x-10 {
--tw-translate-x: calc(var(--spacing) * 10);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.translate-x-16 { .translate-x-16 {
--tw-translate-x: calc(var(--spacing) * 16); --tw-translate-x: calc(var(--spacing) * 16);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@ -952,6 +1012,10 @@
--tw-translate-y: calc(calc(1/2 * 100%) * -1); --tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
} }
.-translate-y-10 {
--tw-translate-y: calc(var(--spacing) * -10);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-16 { .-translate-y-16 {
--tw-translate-y: calc(var(--spacing) * -16); --tw-translate-y: calc(var(--spacing) * -16);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@ -969,6 +1033,12 @@
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
.animate-bounce {
animation: var(--animate-bounce);
}
.animate-ping {
animation: var(--animate-ping);
}
.animate-pulse { .animate-pulse {
animation: var(--animate-pulse); animation: var(--animate-pulse);
} }
@ -1145,6 +1215,9 @@
.rounded-2xl { .rounded-2xl {
border-radius: var(--radius-2xl); border-radius: var(--radius-2xl);
} }
.rounded-3xl {
border-radius: var(--radius-3xl);
}
.rounded-full { .rounded-full {
border-radius: calc(infinity * 1px); border-radius: calc(infinity * 1px);
} }
@ -1302,6 +1375,12 @@
.border-white { .border-white {
border-color: var(--color-white); border-color: var(--color-white);
} }
.border-white\/30 {
border-color: color-mix(in srgb, #fff 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
}
.border-yellow-200 { .border-yellow-200 {
border-color: var(--color-yellow-200); border-color: var(--color-yellow-200);
} }
@ -1323,6 +1402,9 @@
.bg-blue-100 { .bg-blue-100 {
background-color: var(--color-blue-100); background-color: var(--color-blue-100);
} }
.bg-blue-400 {
background-color: var(--color-blue-400);
}
.bg-blue-500 { .bg-blue-500 {
background-color: var(--color-blue-500); background-color: var(--color-blue-500);
} }
@ -1362,12 +1444,18 @@
.bg-green-600 { .bg-green-600 {
background-color: var(--color-green-600); background-color: var(--color-green-600);
} }
.bg-indigo-300 {
background-color: var(--color-indigo-300);
}
.bg-orange-50 { .bg-orange-50 {
background-color: var(--color-orange-50); background-color: var(--color-orange-50);
} }
.bg-orange-100 { .bg-orange-100 {
background-color: var(--color-orange-100); background-color: var(--color-orange-100);
} }
.bg-orange-300 {
background-color: var(--color-orange-300);
}
.bg-orange-400 { .bg-orange-400 {
background-color: var(--color-orange-400); background-color: var(--color-orange-400);
} }
@ -1389,6 +1477,9 @@
.bg-slate-100 { .bg-slate-100 {
background-color: var(--color-slate-100); background-color: var(--color-slate-100);
} }
.bg-slate-400 {
background-color: var(--color-slate-400);
}
.bg-transparent { .bg-transparent {
background-color: transparent; background-color: transparent;
} }
@ -1407,6 +1498,12 @@
background-color: color-mix(in oklab, var(--color-white) 10%, transparent); background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
} }
} }
.bg-white\/70 {
background-color: color-mix(in srgb, #fff 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
}
}
.bg-yellow-50 { .bg-yellow-50 {
background-color: var(--color-yellow-50); background-color: var(--color-yellow-50);
} }
@ -1439,6 +1536,10 @@
--tw-gradient-from: var(--color-blue-100); --tw-gradient-from: var(--color-blue-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.from-blue-500 {
--tw-gradient-from: var(--color-blue-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-blue-600 { .from-blue-600 {
--tw-gradient-from: var(--color-blue-600); --tw-gradient-from: var(--color-blue-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@ -1491,6 +1592,15 @@
--tw-gradient-from: var(--color-slate-100); --tw-gradient-from: var(--color-slate-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.from-slate-700 {
--tw-gradient-from: var(--color-slate-700);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.via-blue-50 {
--tw-gradient-via: var(--color-blue-50);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-orange-400 { .via-orange-400 {
--tw-gradient-via: var(--color-orange-400); --tw-gradient-via: var(--color-orange-400);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
@ -1505,6 +1615,10 @@
--tw-gradient-to: var(--color-blue-200); --tw-gradient-to: var(--color-blue-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.to-blue-600 {
--tw-gradient-to: var(--color-blue-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-emerald-600 { .to-emerald-600 {
--tw-gradient-to: var(--color-emerald-600); --tw-gradient-to: var(--color-emerald-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@ -1521,6 +1635,10 @@
--tw-gradient-to: var(--color-indigo-50); --tw-gradient-to: var(--color-indigo-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.to-indigo-100 {
--tw-gradient-to: var(--color-indigo-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-orange-50 { .to-orange-50 {
--tw-gradient-to: var(--color-orange-50); --tw-gradient-to: var(--color-orange-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@ -1577,6 +1695,14 @@
--tw-gradient-to: var(--color-slate-100); --tw-gradient-to: var(--color-slate-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.to-slate-200 {
--tw-gradient-to: var(--color-slate-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-slate-900 {
--tw-gradient-to: var(--color-slate-900);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-teal-50 { .to-teal-50 {
--tw-gradient-to: var(--color-teal-50); --tw-gradient-to: var(--color-teal-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@ -1585,6 +1711,9 @@
--tw-gradient-to: var(--color-yellow-50); --tw-gradient-to: var(--color-yellow-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.bg-clip-text {
background-clip: text;
}
.object-contain { .object-contain {
object-fit: contain; object-fit: contain;
} }
@ -1660,6 +1789,9 @@
.py-8 { .py-8 {
padding-block: calc(var(--spacing) * 8); padding-block: calc(var(--spacing) * 8);
} }
.py-12 {
padding-block: calc(var(--spacing) * 12);
}
.py-16 { .py-16 {
padding-block: calc(var(--spacing) * 16); padding-block: calc(var(--spacing) * 16);
} }
@ -1783,6 +1915,10 @@
.font-mono { .font-mono {
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-base { .text-base {
font-size: var(--text-base); font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height)); line-height: var(--tw-leading, var(--text-base--line-height));
@ -1935,12 +2071,18 @@
.text-slate-500 { .text-slate-500 {
color: var(--color-slate-500); color: var(--color-slate-500);
} }
.text-slate-600 {
color: var(--color-slate-600);
}
.text-slate-700 { .text-slate-700 {
color: var(--color-slate-700); color: var(--color-slate-700);
} }
.text-slate-800 { .text-slate-800 {
color: var(--color-slate-800); color: var(--color-slate-800);
} }
.text-transparent {
color: transparent;
}
.text-white { .text-white {
color: var(--color-white); color: var(--color-white);
} }
@ -1977,9 +2119,15 @@
.opacity-30 { .opacity-30 {
opacity: 30%; opacity: 30%;
} }
.opacity-40 {
opacity: 40%;
}
.opacity-50 { .opacity-50 {
opacity: 50%; opacity: 50%;
} }
.opacity-60 {
opacity: 60%;
}
.opacity-75 { .opacity-75 {
opacity: 75%; opacity: 75%;
} }
@ -1996,6 +2144,10 @@
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.shadow-2xl {
--tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-lg { .shadow-lg {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@ -2016,6 +2168,10 @@
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.ring {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-2 { .ring-2 {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@ -2092,6 +2248,16 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }
.transition-shadow {
transition-property: box-shadow;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-transform {
transition-property: transform, translate, scale, rotate;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-150 { .duration-150 {
--tw-duration: 150ms; --tw-duration: 150ms;
transition-duration: 150ms; transition-duration: 150ms;
@ -2104,6 +2270,10 @@
--tw-duration: 300ms; --tw-duration: 300ms;
transition-duration: 300ms; transition-duration: 300ms;
} }
.duration-500 {
--tw-duration: 500ms;
transition-duration: 500ms;
}
.ease-in-out { .ease-in-out {
--tw-ease: var(--ease-in-out); --tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out); transition-timing-function: var(--ease-in-out);
@ -2127,6 +2297,59 @@
} }
} }
} }
.group-hover\:rotate-180 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
rotate: 180deg;
}
}
}
.group-hover\:from-blue-100 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
--tw-gradient-from: var(--color-blue-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.group-hover\:from-orange-100 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
--tw-gradient-from: var(--color-orange-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.group-hover\:to-indigo-200 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
--tw-gradient-to: var(--color-indigo-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.group-hover\:to-orange-200 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
--tw-gradient-to: var(--color-orange-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.group-hover\:text-blue-600 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
color: var(--color-blue-600);
}
}
}
.group-hover\:text-blue-700 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
color: var(--color-blue-700);
}
}
}
.group-hover\:text-orange-500 { .group-hover\:text-orange-500 {
&:is(:where(.group):hover *) { &:is(:where(.group):hover *) {
@media (hover: hover) { @media (hover: hover) {
@ -2134,6 +2357,20 @@
} }
} }
} }
.group-hover\:text-orange-600 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
color: var(--color-orange-600);
}
}
}
.group-hover\:text-orange-700 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
color: var(--color-orange-700);
}
}
}
.group-hover\:opacity-100 { .group-hover\:opacity-100 {
&:is(:where(.group):hover *) { &:is(:where(.group):hover *) {
@media (hover: hover) { @media (hover: hover) {
@ -2175,6 +2412,13 @@
} }
} }
} }
.hover\:border-blue-200 {
&:hover {
@media (hover: hover) {
border-color: var(--color-blue-200);
}
}
}
.hover\:border-orange-200 { .hover\:border-orange-200 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@ -2259,6 +2503,13 @@
} }
} }
} }
.hover\:bg-slate-50 {
&:hover {
@media (hover: hover) {
background-color: var(--color-slate-50);
}
}
}
.hover\:bg-white\/10 { .hover\:bg-white\/10 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@ -2279,6 +2530,30 @@
} }
} }
} }
.hover\:bg-gradient-to-br {
&:hover {
@media (hover: hover) {
--tw-gradient-position: to bottom right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
}
}
.hover\:from-blue-50 {
&:hover {
@media (hover: hover) {
--tw-gradient-from: var(--color-blue-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.hover\:from-orange-50 {
&:hover {
@media (hover: hover) {
--tw-gradient-from: var(--color-orange-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.hover\:from-orange-600 { .hover\:from-orange-600 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@ -2287,6 +2562,22 @@
} }
} }
} }
.hover\:to-indigo-100 {
&:hover {
@media (hover: hover) {
--tw-gradient-to: var(--color-indigo-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.hover\:to-orange-100 {
&:hover {
@media (hover: hover) {
--tw-gradient-to: var(--color-orange-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
}
.hover\:to-orange-500 { .hover\:to-orange-500 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@ -2346,6 +2637,22 @@
} }
} }
} }
.hover\:shadow-md {
&:hover {
@media (hover: hover) {
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
}
.hover\:shadow-xl {
&:hover {
@media (hover: hover) {
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
}
.focus\:border-orange-500 { .focus\:border-orange-500 {
&:focus { &:focus {
border-color: var(--color-orange-500); border-color: var(--color-orange-500);
@ -2680,11 +2987,27 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes pulse { @keyframes pulse {
50% { 50% {
opacity: 0.5; opacity: 0.5;
} }
} }
@keyframes bounce {
0%, 100% {
transform: translateY(-25%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: none;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
@layer properties { @layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop { *, ::before, ::after, ::backdrop {