9 Commits

Author SHA1 Message Date
Dodo 1270ce60fd refactor(tapops): modularize page code 2026-06-04 18:13:23 +07:00
Dodo 529693cd5f feat: implementasi custom date range picker dengan preset dan navigasi kalender
* Mengganti field input tanggal yang sudah ada dengan UI date range picker baru.
* Menambahkan popover kalender untuk memilih rentang tanggal dengan navigasi bulan.
* Menambahkan tombol preset untuk pilihan rentang tanggal umum secara cepat, seperti Hari Ini dan 7 Hari Terakhir.
* Memperbarui string lokalisasi untuk fitur date range picker baru.
* Melakukan refactor JavaScript untuk menangani pemilihan tanggal dan state management pada picker baru.
2026-06-04 17:28:18 +07:00
Dodo b0918c1bb9 fix(tapops): perbaiki overflow date input di mobile dengan layout kolom dan lebar fleksibel 2026-06-04 17:11:23 +07:00
Dodo 46d9b1677d fix(tapops): perlebar field tanggal dan tambah kembali tombol kalender dengan picker tersembunyi 2026-06-04 17:09:33 +07:00
Dodo 9c1de91a82 fix(tapops): ganti input date native dengan dd/mm/yyyy text input terpisah agar tahun max 4 digit 2026-06-04 17:06:11 +07:00
Dodo 5bb704ac88 fix(tapops): clamp year pada input dan change event agar validasi aktif saat mengetik 2026-06-04 16:58:05 +07:00
Dodo edab5db3d7 fix(tapops): batasi input tahun 4 digit dan tambah paginasi 6 dokumen per halaman
- Tambah atribut max/min pada date input agar tahun dibatasi 1000-9999, plus clamping via JS sebagai fallback lintas browser
- Tambah paginasi 6 dokumen per halaman dengan navigasi prev/next dan nomor halaman (ellipsis untuk banyak halaman)
- Reset ke halaman 1 saat filter, sort, search, atau rentang tanggal berubah
- Clamp halaman aktif ke rentang valid jika jumlah hasil berkurang setelah filter
- Tambah pesan "tidak ada hasil" saat filter aktif tidak menemukan berkas
- Semua DOM pagination dibangun via safe DOM API (tanpa innerHTML)
2026-06-04 16:52:54 +07:00
Dodo 00722bcfef fix(tapops): ganti mailto dengan Gmail compose URL dan tambah target blank
- Email button (Help Center & nav) sekarang membuka Gmail web compose di tab baru, berfungsi di semua OS termasuk Windows+Chrome tanpa perlu mail client
- WhatsApp button sekarang membuka wa.me di tab baru (tambah target="_blank")
2026-06-04 16:43:25 +07:00
Dodo c9d96eabe4 feat(tapops): add css modules, js scripts, and viewer page 2026-06-04 14:58:10 +07:00
52 changed files with 7009 additions and 1326 deletions
+123
View File
@@ -0,0 +1,123 @@
/* ===========================================
Base / Reset — scoped under .tapops-doc-root
Prevents contaminating Tailwind nav/footer.
=========================================== */
.tapops-doc-root *,
.tapops-doc-root *::before,
.tapops-doc-root *::after {
box-sizing: border-box;
}
/* Font is set on the root only (inherited), NOT on `*`.
A universal `.tapops-doc-root *` rule would tie Font Awesome's
`.fas` font-family in specificity and—loading later—override it,
blanking every icon. Inheritance loses to FA's explicit rule, so
icons render while normal text still gets Roboto. */
.tapops-doc-root {
font-family: 'Roboto', sans-serif;
color: var(--text-dark);
line-height: 1.6;
}
/* Form controls don't inherit font-family by default — opt them in.
These elements never hold Font Awesome glyphs, so this is icon-safe. */
.tapops-doc-root input,
.tapops-doc-root select,
.tapops-doc-root textarea,
.tapops-doc-root button {
font-family: inherit;
}
.tapops-doc-root a {
text-decoration: none;
color: inherit;
}
.tapops-doc-root img {
display: block;
}
/* --- Shared Section Styles --- */
.tapops-doc-root .section-header {
margin-bottom: 50px;
}
.tapops-doc-root .section-tag {
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 2px;
background-color: var(--secondary-gold);
color: var(--primary-blue);
display: inline-block;
padding: 6px 14px;
text-transform: uppercase;
margin-top: 10px;
}
.tapops-doc-root .section-title {
font-size: 2.5rem;
font-weight: 800;
color: var(--primary-blue);
letter-spacing: -1px;
line-height: 1.15;
}
/* --- Shared Button Styles --- */
.tapops-doc-root .btn-join,
.tapops-doc-root .btn-outline {
padding: 17px 30px;
font-weight: 800;
font-size: 0.9rem;
letter-spacing: 1px;
display: inline-flex;
align-items: center;
gap: 12px;
transition: all 0.3s ease;
}
.tapops-doc-root .btn-join {
background-color: var(--secondary-gold);
color: var(--primary-blue);
}
.tapops-doc-root .btn-outline {
border: 1px solid rgba(255, 255, 255, 0.18);
color: white;
}
.tapops-doc-root .btn-join:hover,
.tapops-doc-root .btn-outline:hover {
transform: translateY(-2px);
}
.tapops-doc-root .btn-join:hover {
background-color: #fff;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.25);
}
.tapops-doc-root .btn-outline:hover {
border-color: var(--secondary-gold);
color: var(--secondary-gold);
}
@media (max-width: 768px) {
.tapops-doc-root .btn-join,
.tapops-doc-root .btn-outline {
padding: 14px 24px;
font-size: 0.82rem;
width: 100%;
justify-content: center;
}
}
@media (max-width: 480px) {
.tapops-doc-root .btn-join,
.tapops-doc-root .btn-outline {
padding: 12px 20px;
font-size: 0.78rem;
letter-spacing: 0.5px;
}
}
+1
View File
@@ -0,0 +1 @@
@import url("repository/index.css");
+276
View File
@@ -0,0 +1,276 @@
/* ===========================================
Hero Section (Centered & Immersive) — Tapops
=========================================== */
.research-hero.immersive-hero {
background: radial-gradient(circle at center, #003150 0%, #001e35 100%);
color: white;
padding: 160px 5% 180px;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-bottom: 200px;
}
.research-hero::before {
content: '';
position: absolute;
width: 800px;
height: 800px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: conic-gradient(from 180deg at 50% 50%,
transparent 0deg,
rgba(254, 180, 1, 0.15) 180deg,
transparent 360deg);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #fff 0);
mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #fff 0);
border-radius: 50%;
pointer-events: none;
z-index: 1;
animation: rotate-orbit 30s linear infinite;
}
@keyframes rotate-orbit {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.hero-container-centered {
max-width: 900px;
position: relative;
z-index: 2;
width: 100%;
padding: 0 16px;
}
.hero-kicker {
font-size: 0.85rem;
letter-spacing: 4px;
font-weight: 800;
color: var(--secondary-gold);
text-transform: uppercase;
margin-bottom: 24px;
display: inline-block;
}
.hero-title {
font-size: 4.8rem;
font-weight: 800;
line-height: 1.05;
letter-spacing: -3px;
margin-bottom: 30px;
}
.hero-tagline {
font-size: 1.25rem;
line-height: 1.6;
opacity: 0.85;
margin-bottom: 45px;
}
.hero-actions {
display: flex;
justify-content: center;
gap: 20px;
}
/* --- Floating Insight Bar --- */
.hero-floating-stat-bar {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 25px 60px;
border-radius: 4px;
z-index: 3;
gap: 40px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.3);
max-width: calc(100% - 32px);
box-sizing: border-box;
}
.stat-bar-item {
display: flex;
flex-direction: column;
align-items: center;
white-space: nowrap;
}
.stat-bar-value {
font-size: 2.2rem;
font-weight: 800;
color: var(--secondary-gold);
line-height: 1.1;
}
.stat-bar-label {
font-size: 0.7rem;
letter-spacing: 1.5px;
text-transform: uppercase;
font-weight: 700;
opacity: 0.6;
margin-top: 4px;
}
.stat-bar-divider {
width: 1px;
height: 40px;
background: rgba(255, 255, 255, 0.1);
}
/* ===========================================
Hero Responsive
=========================================== */
@media (max-width: 992px) {
.research-hero.immersive-hero {
padding: 120px 5% 180px;
}
.hero-title {
font-size: 3.5rem;
letter-spacing: -2px;
}
.hero-tagline {
font-size: 1.1rem;
}
.hero-floating-stat-bar {
padding: 20px 32px;
gap: 24px;
}
.stat-bar-value {
font-size: 1.6rem;
}
}
@media (max-width: 768px) {
.research-hero.immersive-hero {
padding: 100px 16px 130px;
}
.hero-kicker {
font-size: 0.68rem;
letter-spacing: 2.5px;
margin-bottom: 16px;
}
.hero-title {
font-size: 2.2rem;
letter-spacing: -1px;
margin-bottom: 18px;
}
.hero-tagline {
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 28px;
}
.hero-actions {
flex-direction: column;
align-items: stretch;
gap: 12px;
width: 100%;
max-width: 320px;
margin: 0 auto;
}
.hero-floating-stat-bar {
width: 100%;
max-width: 320px;
padding: 18px 24px;
gap: 20px;
bottom: 30px;
}
.stat-bar-value {
font-size: 1.3rem;
}
.stat-bar-label {
font-size: 0.6rem;
letter-spacing: 1px;
}
.stat-bar-divider {
height: 30px;
}
.research-hero::before {
width: 400px;
height: 400px;
}
}
@media (max-width: 480px) {
.research-hero.immersive-hero {
padding: 80px 12px 125px;
}
.hero-kicker {
font-size: 0.6rem;
letter-spacing: 2px;
}
.hero-title {
font-size: 1.8rem;
letter-spacing: -0.5px;
margin-bottom: 14px;
}
.hero-tagline {
font-size: 0.82rem;
line-height: 1.45;
margin-bottom: 22px;
}
.hero-actions {
max-width: 280px;
}
.hero-floating-stat-bar {
flex-direction: row;
bottom: 20px;
gap: 16px;
padding: 14px 20px;
width: 100%;
max-width: 280px;
justify-content: space-evenly;
}
.stat-bar-divider {
width: 1px;
height: 24px;
}
.stat-bar-value {
font-size: 1.2rem;
}
.research-hero::before {
width: 280px;
height: 280px;
}
}
+286
View File
@@ -0,0 +1,286 @@
/* ===========================================
Navigation & Mega Menu Styles
Extracted from inline <style> in index.html
=========================================== */
/* ===== MOBILE NAV STABILITY FIX =====
Prevents logo bar from collapsing/shifting on Android when
address bar hides/shows and triggers viewport reflow.
===================================== */
#nav-wrapper {
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 50;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
will-change: box-shadow;
}
#nav-wrapper>div:first-child {
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
@media (max-width: 1023px) {
#nav-wrapper>div:first-child {
min-height: 0 !important;
max-height: none;
}
}
#pengumuman-bar {
z-index: 40;
}
#pengumuman-bar.is-stuck {
position: fixed;
left: 0;
right: 0;
z-index: 40;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Skip Link Styling */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #003150;
color: white;
padding: 8px 16px;
z-index: 100;
transition: top 0.3s;
}
.skip-link:focus {
top: 0;
}
/* Focus visible for better keyboard navigation */
:focus-visible {
outline: 3px solid #feb401;
outline-offset: 2px;
}
/* Mega Menu Styles */
.hero-pagination .swiper-pagination-bullet {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
display: inline-block;
margin: 0 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.hero-pagination .swiper-pagination-bullet-active {
background: rgba(255, 255, 255, 1);
width: 24px;
border-radius: 4px;
}
.mega-panel {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
position: absolute;
top: 100%;
left: 0;
right: 0;
}
.mega-panel.active {
opacity: 1;
visibility: visible;
}
/* Overlay transition */
#mega-overlay {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
#mega-overlay.active {
opacity: 1;
visibility: visible;
}
.mega-link-card {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
background: white;
transition: all 0.2s ease;
}
.mega-link-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
transform: translateY(-2px);
}
.mega-link-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
}
.nav-menu-btn.active {
background: rgba(255, 255, 255, 0.9);
color: #1e40af;
}
.nav-menu-btn:hover {
background: rgba(255, 255, 255, 0.8);
}
/* Mega menu positioning - no gap */
#main-nav {
position: relative;
}
#mega-menu-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
pointer-events: none;
}
#mega-menu-container .mega-panel {
pointer-events: auto;
}
/* Keep pointer bridge under nav to prevent hover loss */
.nav-menu-item {
position: relative;
}
.nav-menu-item::after {
content: '';
position: absolute;
bottom: -12px;
left: 0;
right: 0;
height: 12px;
}
/* ===== Luco Chatbot Animations ===== */
@keyframes slideInRight {
from {
transform: translateX(100px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeInUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes lucoPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
#luco-container {
animation: slideInRight 0.5s ease-out forwards;
}
#luco-bubble {
animation: fadeInUp 0.3s ease-out forwards;
width: min(18rem, calc(100vw - 2rem));
}
#luco-btn {
animation: lucoPulse 2s ease-in-out infinite;
}
#luco-btn:hover {
animation: none;
}
/* Typing indicator */
.typing-indicator span {
display: inline-block;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-6px);
}
}
/* Luco speech bubble arrow */
#luco-bubble .bubble-arrow {
position: absolute;
right: 24px;
bottom: -6px;
width: 12px;
height: 12px;
background: white;
transform: rotate(45deg);
border-right: 1px solid #e2e8f0;
border-bottom: 1px solid #e2e8f0;
}
+147
View File
@@ -0,0 +1,147 @@
/* --- Document Cards Grid --- */
.document-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.document-card {
background: white;
border: 1px solid #e2e8f0;
padding: 30px;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
border-radius: 4px;
}
.document-card:hover {
border-color: var(--secondary-gold);
box-shadow: 0 20px 40px rgba(0, 49, 80, 0.05);
transform: translateY(-5px);
}
.doc-type-icon {
font-size: 2.2rem;
margin-bottom: 20px;
}
.doc-type-pdf {
color: #ef4444;
}
.doc-type-docx {
color: #3b82f6;
}
.doc-title {
font-size: 1.15rem;
font-weight: 800;
color: var(--primary-blue);
margin-bottom: 12px;
line-height: 1.4;
}
.doc-meta {
display: flex;
gap: 8px;
margin-bottom: 25px;
flex-wrap: wrap;
}
.doc-badge {
font-size: 0.65rem;
font-weight: 800;
padding: 4px 10px;
background: #f1f5f9;
color: #64748b;
text-transform: uppercase;
}
.doc-badge.date {
background: hsl(200, 95%, 95%);
color: hsl(200, 95%, 30%);
}
/* --- Tag Badges --- */
.doc-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-bottom: 20px;
}
.doc-tags-icon {
font-size: 0.7rem;
color: #94a3b8;
margin-right: 2px;
}
.doc-tag {
font-size: 0.65rem;
font-weight: 700;
padding: 3px 10px;
background: hsl(43, 100%, 95%);
color: hsl(43, 80%, 35%);
border: 1px solid hsl(43, 80%, 85%);
border-radius: 20px;
text-transform: lowercase;
letter-spacing: 0.3px;
transition: all 0.2s ease;
}
.doc-tag:hover {
background: hsl(43, 80%, 88%);
border-color: hsl(43, 80%, 70%);
}
.doc-actions {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.btn-action-primary,
.btn-action-outline {
padding: 12px;
text-align: center;
text-decoration: none;
font-weight: 800;
font-size: 0.75rem;
letter-spacing: 0.5px;
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
border-radius: 4px;
width: 100%;
}
a.btn-action-primary {
background: var(--primary-blue);
color: white;
border: 1px solid var(--primary-blue);
}
a.btn-action-primary:hover {
background: var(--secondary-gold);
border-color: var(--secondary-gold);
color: var(--primary-blue);
}
a.btn-action-outline {
background: transparent;
color: var(--primary-blue);
border: 1px solid #e2e8f0;
}
a.btn-action-outline:hover {
border-color: var(--primary-blue);
background: #f8fafc;
}
+117
View File
@@ -0,0 +1,117 @@
/* --- Content Area & Search --- */
.repo-main-content {
min-height: 800px;
min-width: 0;
}
.repo-content-header {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 50px;
}
.header-title-area .section-title {
margin-bottom: 8px;
}
.section-subtitle {
font-size: 0.95rem;
color: #64748b;
font-weight: 400;
}
.repo-controls {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 24px;
background: white;
padding: 24px;
border-radius: 8px;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.search-container {
flex: 1;
min-width: 260px;
max-width: 500px;
}
.search-input-group {
position: relative;
width: 100%;
}
.search-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 1rem;
}
#doc-search {
width: 100%;
padding: 16px 16px 16px 50px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.95rem;
color: var(--primary-blue);
transition: all 0.3s ease;
}
#doc-search:focus {
outline: none;
}
/* --- Filter Controls --- */
.filter-controls {
display: flex;
gap: 16px;
align-items: flex-end;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.date-range-group {
min-width: 300px;
position: relative;
}
.control-label {
font-size: 0.7rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.select-wrapper {
position: relative;
min-width: 140px;
}
#sort-filter-wrapper {
position: relative;
min-width: 140px;
}
.select-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--primary-blue);
font-size: 0.8rem;
pointer-events: none;
}
@@ -0,0 +1,422 @@
/* --- Date Range Filter & Popover --- */
.date-picker-trigger {
width: 100%;
padding: 14px 18px;
background: white;
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 700;
color: var(--primary-blue);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.date-picker-trigger:hover {
border-color: var(--secondary-gold);
background: #fffdf5;
}
.date-picker-trigger:focus-visible {
outline: none;
border-color: var(--primary-blue);
box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08);
}
.date-picker-trigger .trigger-left-content {
display: flex;
align-items: center;
gap: 10px;
}
.date-picker-trigger .trigger-arrow {
font-size: 0.8rem;
transition: transform 0.25s ease;
color: var(--primary-blue);
}
.date-range-group.active .date-picker-trigger .trigger-arrow {
transform: rotate(180deg);
}
.date-range-group.active .date-picker-trigger {
border-color: var(--primary-blue);
box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08);
}
/* Backdrop */
.date-picker-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
z-index: 40;
}
/* Popover */
.date-picker-popover {
position: absolute;
top: calc(100% + 6px);
right: 0;
width: 630px;
background: white;
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
display: none;
flex-direction: column;
z-index: 45;
overflow: hidden;
}
.date-range-group.active .date-picker-popover {
display: flex;
}
/* Top Bar */
.date-picker-top-bar {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
background: #f8fafc;
display: flex;
justify-content: flex-end;
}
.date-display-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.date-display-field input {
width: 110px;
padding: 6px 10px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
color: var(--primary-blue);
text-align: center;
background: white;
}
.date-picker-separator {
color: #94a3b8;
font-weight: 700;
}
/* Body */
.date-picker-body {
display: flex;
background: white;
}
/* Calendar Pane */
.calendar-container {
flex: 1;
padding: 16px;
}
.calendar-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0;
}
.month-labels {
display: flex;
gap: 90px;
color: var(--primary-blue);
font-size: 0.95rem;
}
.month-label {
display: flex;
align-items: center;
gap: 6px;
font-weight: 700;
}
.year-select-wrapper {
position: relative;
display: inline-flex;
align-items: center;
border-radius: 4px;
transition: background 0.2s ease;
}
.year-select-wrapper:hover {
background: #f1f5f9;
}
.calendar-year-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
border: none;
padding: 2px 16px 2px 6px;
font-family: inherit;
font-size: 0.95rem;
font-weight: 700;
color: var(--primary-blue);
cursor: pointer;
outline: none;
}
.year-select-wrapper .select-arrow {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
font-size: 0.6rem;
color: var(--primary-blue);
pointer-events: none;
}
.nav-btn {
background: transparent;
border: none;
color: #64748b;
cursor: pointer;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.nav-btn:hover {
background: #f1f5f9;
color: var(--primary-blue);
}
.calendar-grids {
display: flex;
gap: 20px;
}
.calendar-grid-pane {
flex: 1;
}
.day-headers {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
font-size: 0.72rem;
font-weight: 700;
color: #94a3b8;
margin-bottom: 8px;
}
.days-container {
display: grid;
grid-template-columns: repeat(7, 1fr);
row-gap: 4px;
column-gap: 2px;
}
.day-cell {
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.82rem;
font-weight: 600;
color: #475569;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.day-cell:hover {
background: #f1f5f9;
color: var(--primary-blue);
}
.day-cell.empty {
pointer-events: none;
visibility: hidden;
}
.day-cell.disabled {
color: #cbd5e1;
pointer-events: none;
}
.day-cell.range-start {
background: var(--primary-blue) !important;
color: white !important;
border-radius: 4px 0 0 4px;
}
.day-cell.range-end {
background: var(--primary-blue) !important;
color: white !important;
border-radius: 0 4px 4px 0;
}
.day-cell.range-start.range-end {
border-radius: 4px !important;
}
.day-cell.range-between {
background: #eff6ff !important;
color: var(--primary-blue) !important;
border-radius: 0;
}
/* Presets Pane */
.presets-container {
width: 190px;
border-left: 1px solid var(--border-color);
padding: 16px;
background: white;
display: flex;
flex-direction: column;
gap: 12px;
}
.presets-title {
font-size: 0.7rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
display: block;
}
.presets-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 240px;
overflow-y: auto;
}
.preset-btn {
background: transparent;
border: none;
text-align: left;
padding: 8px 12px;
font-size: 0.8rem;
font-weight: 600;
color: #475569;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
font-family: inherit;
}
.preset-btn:hover {
background: #f8fafc;
color: var(--primary-blue);
}
.preset-btn.active {
background: #eff6ff;
color: var(--primary-blue);
}
.preset-btn.active::after {
content: '✓';
font-weight: 700;
color: var(--primary-blue);
font-size: 0.85rem;
}
/* Footer Action Buttons */
.date-picker-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 12px 16px;
border-top: 1px solid var(--border-color);
background: #f8fafc;
}
.action-btn {
padding: 8px 20px;
font-size: 0.85rem;
font-weight: 700;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.cancel-btn {
background: white;
border: 1px solid #cbd5e1;
color: #475569;
}
.cancel-btn:hover {
background: #f1f5f9;
border-color: #94a3b8;
color: #1e293b;
}
.apply-btn {
background: var(--primary-blue);
border: 1px solid var(--primary-blue);
color: white;
}
.apply-btn:hover {
background: #002238;
border-color: #002238;
}
.range-divider {
font-size: 0.7rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
padding: 0 4px;
}
.filter-select,
select#sort-filter {
width: 100%;
padding: 12px 12px 12px 36px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 700;
color: var(--primary-blue);
cursor: pointer;
appearance: none;
transition: all 0.2s ease;
}
.filter-select:hover,
select#sort-filter:hover {
border-color: var(--secondary-gold);
background: white;
}
.filter-select:focus,
select#sort-filter:focus {
outline: none;
}
+150
View File
@@ -0,0 +1,150 @@
/* --- Custom UI Dropdown --- */
.custom-dropdown {
position: relative;
width: 100%;
}
.custom-dropdown-trigger {
width: 100%;
padding: 14px 18px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 700;
color: var(--primary-blue);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.custom-dropdown-trigger:hover {
border-color: var(--secondary-gold);
background: #fffdf5;
}
.custom-dropdown-trigger:focus-visible {
outline: none;
border-color: var(--primary-blue);
box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08);
}
.custom-dropdown-trigger .trigger-arrow {
font-size: 0.8rem;
transition: transform 0.25s ease;
color: var(--primary-blue);
}
.custom-dropdown.active .custom-dropdown-trigger .trigger-arrow {
transform: rotate(180deg);
}
.custom-dropdown.active .custom-dropdown-trigger {
border-color: var(--primary-blue);
box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08);
}
.custom-dropdown-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
z-index: 100;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
max-height: 300px;
overflow-y: auto;
}
.custom-dropdown.active .custom-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.custom-dropdown-item {
padding: 12px 18px;
font-size: 0.88rem;
font-weight: 600;
color: #475569;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: space-between;
}
.custom-dropdown-item:hover {
background: #f1f5f9;
color: var(--primary-blue);
}
.custom-dropdown-item.active {
background: #f0f7ff;
color: var(--primary-blue);
font-weight: 700;
}
/* Tick mark for selected item */
.custom-dropdown-item.active::after {
content: "\f00c";
font-family: "Font Awesome 5 Free";
font-weight: 900;
font-size: 0.75rem;
color: var(--primary-blue);
}
/* --- Filter Skeleton Loading --- */
.filter-skeleton {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-skeleton-item {
height: 48px;
border-radius: 4px;
background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
}
.filter-skeleton-item.short {
width: 70%;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.sidebar-info-box {
background: #f1f5f9;
padding: 20px;
border-radius: 4px;
display: flex;
gap: 12px;
}
.sidebar-info-box i {
color: var(--primary-blue);
margin-top: 3px;
}
.sidebar-info-box p {
font-size: 0.82rem;
color: #64748b;
line-height: 1.5;
}
+7
View File
@@ -0,0 +1,7 @@
@import url("layout.css");
@import url("dropdown.css");
@import url("controls.css");
@import url("date-picker.css");
@import url("cards.css");
@import url("responsive.css");
@import url("pagination.css");
+81
View File
@@ -0,0 +1,81 @@
/* ===========================================
Document Hub & Dashboard Layout
=========================================== */
.documents-section {
padding: 120px 5%;
background: hsl(0, 0%, 100%);
}
.repo-dashboard-container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 280px 1fr;
gap: 40px;
align-items: start;
}
/* --- Sidebar Styling --- */
.repo-sidebar {
position: sticky;
top: 150px;
}
/* Mobile category dropdown wrapper (hidden on desktop) */
.mobile-category-control-group {
display: none !important;
}
.sidebar-title {
font-size: 0.75rem;
letter-spacing: 2px;
font-weight: 800;
color: #94a3b8;
margin-bottom: 20px;
text-transform: uppercase;
}
.document-filter-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 30px;
}
/* Desktop: show buttons, hide dropdown */
.filter-btn-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-select-group {
display: none;
}
.doc-filter-btn {
text-align: left;
padding: 14px 18px;
background: white;
border: 1px solid #e2e8f0;
font-size: 0.85rem;
font-weight: 700;
color: var(--primary-blue);
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
}
.doc-filter-btn.active {
background: var(--primary-blue);
color: white;
border-color: var(--primary-blue);
box-shadow: 0 4px 12px rgba(0, 49, 80, 0.15);
}
.doc-filter-btn:hover:not(.active) {
border-color: var(--secondary-gold);
background: #fffdf5;
}
+172
View File
@@ -0,0 +1,172 @@
/* ===========================================
Pagination
=========================================== */
.doc-pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px 0 0;
gap: 16px;
flex-wrap: wrap;
}
.pagination-info {
font-size: 0.75rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pagination-nav {
display: flex;
align-items: center;
gap: 6px;
}
.pagination-btn {
min-width: 40px;
height: 40px;
padding: 0 10px;
border: 1px solid #e2e8f0;
background: white;
color: var(--primary-blue);
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-family: inherit;
}
.pagination-btn:hover:not(.disabled):not(.active) {
border-color: var(--secondary-gold);
background: #fffdf5;
}
.pagination-btn.active {
background: var(--primary-blue);
border-color: var(--primary-blue);
color: white;
}
.pagination-btn.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.pagination-ellipsis {
min-width: 32px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-weight: 700;
font-size: 1rem;
letter-spacing: 1px;
}
@media (max-width: 768px) {
.doc-pagination {
flex-direction: column;
align-items: center;
gap: 12px;
}
}
@media (max-width: 480px) {
.pagination-btn {
min-width: 36px;
height: 36px;
font-size: 0.8rem;
}
.pagination-nav {
gap: 4px;
}
}
/* ===========================================
Responsive — Phone (≤480px)
=========================================== */
@media (max-width: 480px) {
.documents-section {
padding: 50px 12px;
}
.custom-dropdown-trigger {
padding: 12px 14px;
font-size: 0.85rem;
}
/* Single column cards */
.document-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.document-card {
padding: 18px;
}
.doc-title {
font-size: 0.95rem;
margin-bottom: 8px;
}
.doc-type-icon {
font-size: 1.5rem;
margin-bottom: 10px;
}
.doc-meta {
margin-bottom: 12px;
}
.doc-tags {
margin-bottom: 10px;
gap: 4px;
}
.doc-tag {
font-size: 0.6rem;
padding: 2px 8px;
}
.btn-action-primary,
.btn-action-outline {
padding: 10px;
font-size: 0.7rem;
}
.doc-actions {
gap: 8px;
}
.repo-controls {
padding: 12px;
gap: 12px;
}
#doc-search {
padding: 12px 12px 12px 40px;
font-size: 0.85rem;
}
.search-icon {
left: 14px;
font-size: 0.9rem;
}
.repo-content-header {
gap: 16px;
margin-bottom: 20px;
}
}
+233
View File
@@ -0,0 +1,233 @@
/* ===========================================
Responsive — Tablet (≤1024px)
=========================================== */
@media (max-width: 1024px) {
.repo-dashboard-container {
grid-template-columns: 1fr;
}
.repo-sidebar {
display: none;
}
.mobile-category-control-group {
display: flex !important;
flex-direction: column;
gap: 8px;
}
/* Mobile: show dropdown */
.filter-select-group {
display: block;
}
/* Skeleton single bar on mobile */
.filter-skeleton {
flex-direction: column;
}
.filter-skeleton-item {
height: 48px;
}
.filter-skeleton-item.short {
display: none;
}
.filter-skeleton-item:not(.short) ~ .filter-skeleton-item:not(.short) {
display: none;
}
.repo-controls {
flex-direction: column;
align-items: stretch;
}
.search-container {
max-width: 100%;
}
.filter-controls {
justify-content: flex-start;
}
.control-group {
flex: 1;
}
}
/* ===========================================
Responsive — Tablet / Large Phone (≤768px)
=========================================== */
@media (max-width: 768px) {
.documents-section {
padding: 80px 16px;
}
.repo-content-header {
gap: 20px;
margin-bottom: 30px;
}
.repo-controls {
padding: 16px;
gap: 16px;
}
#doc-search {
padding: 14px 14px 14px 44px;
font-size: 0.88rem;
}
.filter-controls {
flex-direction: column;
gap: 12px;
}
.control-group {
width: 100%;
}
.date-range-group {
min-width: 0;
position: relative;
}
body.date-picker-open {
overflow: hidden;
}
body.date-picker-open .date-picker-backdrop {
display: block;
z-index: 998;
}
.date-picker-popover {
/* Default mobile popover positioning inside media query is overridden below,
but let's define the base transitions or layouts. */
}
@media (max-width: 768px) {
.date-picker-popover {
position: fixed;
top: 50%;
left: 50%;
right: auto;
transform: translate(-50%, -50%);
width: calc(100% - 32px);
max-width: 380px;
max-height: 90vh;
overflow-y: auto;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
z-index: 999;
}
.date-picker-top-bar {
justify-content: center;
}
.calendar-grids {
gap: 0;
}
#right-calendar-pane {
display: none;
}
.month-labels {
gap: 0;
justify-content: center;
flex: 1;
}
#right-month-label {
display: none;
}
.date-picker-body {
flex-direction: column;
}
.presets-container {
width: 100%;
border-left: none;
border-top: 1px solid var(--border-color);
padding: 12px 16px;
}
.presets-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
max-height: none;
}
.preset-btn {
padding: 6px 10px;
font-size: 0.75rem;
}
}
.select-wrapper,
#sort-filter-wrapper {
min-width: 0;
width: 100%;
}
/* Cards: 2-column when space permits, else single */
.document-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
.document-card {
padding: 22px;
}
.document-card:hover {
transform: none;
}
.doc-type-icon {
font-size: 1.8rem;
margin-bottom: 14px;
}
.doc-title {
font-size: 1rem;
}
.doc-meta {
margin-bottom: 16px;
}
.doc-tags {
margin-bottom: 14px;
}
.repo-main-content {
min-height: 400px;
}
}
/* ===========================================
Responsive — Phone (≤480px)
=========================================== */
@media (max-width: 480px) {
.date-dd, .date-mm {
width: 26px;
}
.date-yyyy {
min-width: 38px;
}
.date-calendar-btn {
padding-left: 4px;
font-size: 0.82rem;
}
}
+45
View File
@@ -0,0 +1,45 @@
/* ===========================================
Responsive Breakpoints — Document Sections
=========================================== */
/* --- Tablet (≤768px) --- */
@media (max-width: 768px) {
.tapops-doc-root .section-title {
font-size: 1.8rem;
letter-spacing: -0.5px;
}
.tapops-doc-root .section-tag {
font-size: 0.68rem;
letter-spacing: 2px;
padding: 5px 12px;
}
.tapops-doc-root .section-header {
margin-bottom: 35px;
}
}
/* --- Phone (≤480px) --- */
@media (max-width: 480px) {
.tapops-doc-root .section-title {
font-size: 1.5rem;
letter-spacing: -0.5px;
}
.tapops-doc-root .section-tag {
font-size: 0.6rem;
letter-spacing: 1.5px;
padding: 4px 10px;
}
.tapops-doc-root .section-subtitle {
font-size: 0.82rem;
}
.tapops-doc-root .section-header {
margin-bottom: 28px;
}
}
+429
View File
@@ -0,0 +1,429 @@
/* ===========================================
Procedures (Modern Process Flow)
=========================================== */
.journey-section {
padding: 120px 5%;
background: #ffffff;
position: relative;
overflow: hidden;
}
.journey-container {
max-width: 1200px;
margin: 0 auto;
}
.process-flow-wrapper {
display: flex;
justify-content: space-between;
gap: 40px;
margin-top: 80px;
position: relative;
}
.process-step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
}
.step-visual {
position: relative;
margin-bottom: 30px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.step-icon-box {
width: 80px;
height: 80px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: var(--primary-blue);
box-shadow: 0 10px 25px rgba(0, 49, 80, 0.05);
transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
z-index: 2;
}
.process-step:hover .step-icon-box {
border-color: var(--secondary-gold);
color: var(--secondary-gold);
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(254, 180, 1, 0.15);
}
.step-number-badge {
position: absolute;
top: 0;
right: calc(50% - 38px);
width: 24px;
height: 24px;
background: var(--secondary-gold);
color: var(--primary-blue);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 800;
z-index: 3;
border: 2px solid white;
}
.step-connector {
position: absolute;
left: calc(50% + 50px);
top: 50%;
width: calc(100% - 100px);
height: 2px;
background: repeating-linear-gradient(to right, #e2e8f0, #e2e8f0 5px, transparent 5px, transparent 10px);
z-index: 1;
}
.step-content h3 {
font-size: 1.25rem;
color: var(--primary-blue);
font-weight: 800;
margin-bottom: 15px;
letter-spacing: -0.3px;
}
.step-content p {
font-size: 0.95rem;
color: #64748b;
line-height: 1.7;
max-width: 280px;
margin: 0 auto;
}
@media (max-width: 1024px) {
.process-flow-wrapper {
gap: 20px;
}
.step-connector {
display: none;
}
}
@media (max-width: 768px) {
.process-flow-wrapper {
flex-direction: column;
gap: 60px;
}
.process-step {
flex-direction: row;
text-align: left;
align-items: flex-start;
gap: 30px;
}
.step-visual {
width: 80px;
flex-shrink: 0;
margin-bottom: 0;
}
.step-number-badge {
right: 0;
}
.step-content p {
margin: 0;
max-width: 100%;
}
}
/* ===========================================
Help Center (Interaction Cards)
=========================================== */
.help-center-section {
padding: 100px 5%;
background: white;
}
.help-container {
max-width: 1100px;
margin: 0 auto;
}
.help-header-centered {
text-align: center;
margin-bottom: 60px;
}
.help-cards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
}
.interaction-card {
position: relative;
border-radius: 4px;
overflow: hidden;
padding: 2px;
}
.help-official {
background: linear-gradient(135deg, var(--primary-blue) 0%, #001e35 100%);
}
.help-urgent {
background: linear-gradient(135deg, var(--secondary-gold) 0%, #ffd54f 100%);
}
.card-inner {
background: white;
padding: 50px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
border-radius: 2px;
}
.help-official .card-inner {
border: 1px solid rgba(0, 49, 80, 0.05);
}
.card-icon {
width: 80px;
height: 80px;
background: #f8fafc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
margin-bottom: 30px;
color: var(--primary-blue);
}
.help-urgent .card-icon {
background: rgba(254, 180, 1, 0.1);
color: var(--secondary-gold);
}
.interaction-card h3 {
font-size: 1.5rem;
color: var(--primary-blue);
margin-bottom: 15px;
font-weight: 800;
}
.interaction-card p {
font-size: 1rem;
color: #64748b;
line-height: 1.6;
margin-bottom: 30px;
}
.card-checklist {
list-style: none;
margin-bottom: 40px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 0;
}
.card-checklist li {
font-size: 0.85rem;
font-weight: 700;
color: var(--primary-blue);
display: flex;
align-items: center;
gap: 8px;
}
.card-checklist li i {
color: #10b981;
}
a.btn-interaction,
a.btn-interaction-alt {
width: 100%;
padding: 18px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
font-size: 0.85rem;
text-decoration: none;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
a.btn-interaction {
background: var(--primary-blue);
color: white;
}
a.btn-interaction-alt {
background: var(--secondary-gold);
color: var(--primary-blue);
}
a.btn-interaction:hover {
background: #0f172a;
transform: translateY(-2px);
color: white;
}
a.btn-interaction-alt:hover {
background: #facc15;
transform: translateY(-2px);
color: var(--primary-blue);
}
@media (max-width: 768px) {
.journey-section {
padding: 80px 16px;
}
.help-center-section {
padding: 80px 16px;
}
.help-cards-grid {
grid-template-columns: 1fr;
}
.card-inner {
padding: 35px 24px;
}
.interaction-card h3 {
font-size: 1.25rem;
}
.interaction-card p {
font-size: 0.9rem;
}
.card-icon {
width: 64px;
height: 64px;
font-size: 1.6rem;
margin-bottom: 24px;
}
a.btn-interaction,
a.btn-interaction-alt {
padding: 14px;
font-size: 0.78rem;
letter-spacing: 1.5px;
}
.process-flow-wrapper {
margin-top: 50px;
}
.step-content h3 {
font-size: 1.1rem;
}
.step-content p {
font-size: 0.88rem;
}
}
@media (max-width: 480px) {
.journey-section {
padding: 50px 12px;
}
.help-center-section {
padding: 50px 12px;
}
.help-header-centered {
margin-bottom: 40px;
}
.card-inner {
padding: 28px 18px;
}
.card-icon {
width: 56px;
height: 56px;
font-size: 1.4rem;
margin-bottom: 20px;
}
.interaction-card h3 {
font-size: 1.1rem;
}
.interaction-card p {
font-size: 0.85rem;
margin-bottom: 20px;
}
.card-checklist {
margin-bottom: 28px;
gap: 8px;
}
.card-checklist li {
font-size: 0.78rem;
}
a.btn-interaction,
a.btn-interaction-alt {
padding: 12px;
font-size: 0.72rem;
letter-spacing: 1px;
}
.process-flow-wrapper {
margin-top: 35px;
gap: 40px;
}
.process-step {
gap: 20px;
}
.step-icon-box {
width: 60px;
height: 60px;
font-size: 1.4rem;
}
.step-content h3 {
font-size: 1rem;
}
.step-content p {
font-size: 0.82rem;
line-height: 1.6;
}
.step-number-badge {
width: 20px;
height: 20px;
font-size: 0.6rem;
}
}
+15
View File
@@ -0,0 +1,15 @@
/* ===========================================
Design Tokens — TAPOPS (Informatika UNTAN)
Overrides edo variable names with Tapops palette.
All document section CSS files inherit from here.
=========================================== */
:root {
--primary-blue: #003150; /* Tapops navy (was #1b3652) */
--secondary-gold: #feb401; /* Tapops yellow (was #f7b738) */
--text-dark: #1e293b; /* slate-800 (was #222) */
--text-light: #fff;
--bg-light: #f8fafc; /* slate-50 (was #f4f6f9) */
--border-color: #e2e8f0; /* slate-200 (was #eaeaea) */
--shadow: 0 18px 35px rgba(0, 49, 80, 0.08);
}
+234
View File
@@ -0,0 +1,234 @@
/* ===========================================
Viewer Page — PDF iframe + DOCX rendered preview
=========================================== */
.viewer-body {
margin: 0;
background: var(--bg-light);
font-family: 'Roboto', sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- Header --- */
.viewer-header {
background: var(--primary-blue);
color: var(--text-light);
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
box-shadow: var(--shadow);
flex-wrap: wrap;
position: sticky;
top: 0;
z-index: 10;
}
.viewer-header-left {
display: flex;
align-items: center;
gap: 20px;
flex: 1;
min-width: 0;
}
.viewer-title-group {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.viewer-title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-light);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.viewer-type-badge {
display: inline-block;
width: fit-content;
padding: 2px 10px;
background: var(--secondary-gold);
color: var(--primary-blue);
border-radius: 4px;
font-size: 0.7rem;
font-weight: 800;
letter-spacing: 0.5px;
}
/* --- Buttons --- */
.btn-back,
.btn-download,
.btn-back-mini {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: 6px;
font-weight: 700;
font-size: 0.85rem;
text-decoration: none;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn-back {
background: rgba(255, 255, 255, 0.1);
color: var(--text-light);
border: 1px solid rgba(255, 255, 255, 0.25);
}
.btn-back:hover {
background: rgba(255, 255, 255, 0.2);
}
.btn-download {
background: var(--secondary-gold);
color: var(--primary-blue);
}
.btn-download:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(254, 180, 1, 0.4);
}
.btn-back-mini {
background: var(--primary-blue);
color: var(--text-light);
margin-top: 12px;
}
.btn-back-mini:hover {
background: #001e35;
}
/* --- Content area --- */
.viewer-content {
flex: 1;
display: flex;
flex-direction: column;
background: #525659;
min-height: calc(100vh - 80px);
position: relative;
}
.viewer-pdf-frame {
flex: 1;
width: 100%;
border: none;
min-height: calc(100vh - 80px);
}
.viewer-docx-content {
background: white;
margin: 24px auto;
padding: 60px 80px;
max-width: 900px;
width: calc(100% - 48px);
box-sizing: border-box;
box-shadow: var(--shadow);
border-radius: 4px;
overflow-x: auto;
color: var(--text-dark);
line-height: 1.6;
font-family: 'Roboto', sans-serif;
}
/* docx-preview injects its own structure; override its defaults gently. */
.viewer-docx-content .docx-wrapper {
background: transparent !important;
padding: 0 !important;
}
.viewer-docx-content .docx {
box-shadow: none !important;
margin: 0 !important;
}
/* --- States --- */
.viewer-loading,
.viewer-error {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
color: var(--text-light);
font-size: 1rem;
font-weight: 600;
padding: 60px 20px;
text-align: center;
}
.viewer-loading i {
font-size: 3rem;
color: var(--secondary-gold);
}
.viewer-error i {
font-size: 3rem;
color: #ef4444;
}
/* --- Download Progress Bar --- */
.viewer-progress-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.1);
z-index: 5;
overflow: hidden;
}
.viewer-progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--secondary-gold), #ffd54f);
border-radius: 0 2px 2px 0;
transition: width 0.2s ease-out;
}
/* --- Responsive --- */
@media (max-width: 640px) {
.viewer-header {
padding: 12px 16px;
gap: 12px;
}
.viewer-header-left {
gap: 12px;
}
.viewer-title {
font-size: 0.95rem;
}
.viewer-docx-content {
padding: 24px;
margin: 12px;
width: calc(100% - 24px);
}
.btn-back,
.btn-download {
padding: 8px 14px;
font-size: 0.8rem;
}
}
+17 -1325
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
import { CounterAnimation } from './components/counter-animation.js';
import { AccessibilityToggleController } from './controllers/accessibility-toggle-controller.js';
import { DocumentRepository } from './controllers/document-repository.js';
import { MobileMenuController } from './controllers/mobile-menu-controller.js';
import { NavigationController } from './controllers/navigation-controller.js';
import { pageTemplate } from './templates/page-template.js';
class TapopsApp {
constructor(root = document.getElementById('tapops-page-root')) {
this.root = root;
}
init() {
if (!this.root) return;
this.root.innerHTML = pageTemplate();
window.applyLang?.();
window.applyTp?.(this.root);
new NavigationController().init();
new MobileMenuController().init();
new AccessibilityToggleController().init();
new CounterAnimation().init();
new DocumentRepository().init();
}
}
new TapopsApp().init();
@@ -0,0 +1,44 @@
export class CounterAnimation {
static DURATION_MS = 2000;
init() {
const counters = document.querySelectorAll('.counter-value[data-target]');
if (counters.length === 0) return;
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this._animate(entry.target);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.2 });
counters.forEach(c => observer.observe(c));
} else {
counters.forEach(c => this._animate(c));
}
}
_animate(el) {
const target = Number.parseInt((el.dataset.target || '0').replace(/[^0-9]/g, ''), 10);
if (Number.isNaN(target)) return;
CounterAnimation.animateValue(el, target, el.dataset.suffix || '');
}
// Reusable easing tween from 0 → target. Callable for stats whose
// value is only known after an async fetch (no data-target needed).
static animateValue(el, target, suffix = '') {
if (!el || Number.isNaN(target)) return;
const startTime = performance.now();
const tick = (now) => {
const progress = Math.min((now - startTime) / CounterAnimation.DURATION_MS, 1);
const eased = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.floor(eased * target) + suffix;
if (progress < 1) requestAnimationFrame(tick);
else el.textContent = target + suffix;
};
requestAnimationFrame(tick);
}
}
@@ -0,0 +1,175 @@
export class CustomDropdown {
constructor({ container, options, defaultValue, onChange, prefixIcon }) {
this.container = container;
this.options = options; // Array of { value, text, i18nKey }
this.onChange = onChange;
this.currentValue = defaultValue || options[0]?.value;
this.prefixIcon = prefixIcon; // optional class name like 'fas fa-sort'
this.isOpen = false;
this.elements = {};
this._build();
this._bindEvents();
}
_build() {
this.container.innerHTML = '';
this.container.classList.add('custom-dropdown');
// Trigger Button
const trigger = document.createElement('button');
trigger.className = 'custom-dropdown-trigger';
trigger.setAttribute('aria-haspopup', 'listbox');
trigger.setAttribute('aria-expanded', 'false');
trigger.type = 'button';
const triggerLeft = document.createElement('div');
triggerLeft.className = 'trigger-left-content';
triggerLeft.style.display = 'flex';
triggerLeft.style.alignItems = 'center';
triggerLeft.style.gap = '10px';
if (this.prefixIcon) {
const icon = document.createElement('i');
icon.className = this.prefixIcon;
triggerLeft.appendChild(icon);
}
const labelSpan = document.createElement('span');
labelSpan.className = 'trigger-label';
// Find default text
const defaultOpt = this.options.find(opt => opt.value === this.currentValue) || this.options[0];
labelSpan.textContent = defaultOpt ? defaultOpt.text : '';
if (defaultOpt?.i18nKey) {
labelSpan.setAttribute('data-tp-i18n', defaultOpt.i18nKey);
}
triggerLeft.appendChild(labelSpan);
trigger.appendChild(triggerLeft);
const arrowIcon = document.createElement('i');
arrowIcon.className = 'fas fa-chevron-down trigger-arrow';
trigger.appendChild(arrowIcon);
this.container.appendChild(trigger);
// Menu wrapper
const menu = document.createElement('div');
menu.className = 'custom-dropdown-menu';
menu.setAttribute('role', 'listbox');
// Populate options
this.options.forEach(opt => {
const item = document.createElement('div');
item.className = 'custom-dropdown-item' + (opt.value === this.currentValue ? ' active' : '');
item.setAttribute('role', 'option');
item.setAttribute('data-value', opt.value);
const itemText = document.createElement('span');
itemText.textContent = opt.text;
if (opt.i18nKey) {
itemText.setAttribute('data-tp-i18n', opt.i18nKey);
}
item.appendChild(itemText);
menu.appendChild(item);
});
this.container.appendChild(menu);
this.elements = { trigger, labelSpan, menu };
}
_bindEvents() {
// Toggle dropdown on trigger click
this.elements.trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Handle item selection
this.elements.menu.addEventListener('click', (e) => {
const item = e.target.closest('.custom-dropdown-item');
if (!item) return;
const value = item.getAttribute('data-value');
this.select(value);
this.close();
});
// Close when clicking outside
document.addEventListener('click', () => {
this.close();
});
// Close on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.close();
});
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
// Close other custom dropdowns
document.querySelectorAll('.custom-dropdown.active').forEach(dropdown => {
if (dropdown !== this.container) {
dropdown.classList.remove('active');
dropdown.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false');
}
});
this.isOpen = true;
this.container.classList.add('active');
this.elements.trigger.setAttribute('aria-expanded', 'true');
}
close() {
this.isOpen = false;
this.container.classList.remove('active');
this.elements.trigger.setAttribute('aria-expanded', 'false');
}
select(value) {
if (value === this.currentValue) return;
this.currentValue = value;
// Update active class in menu items
this.elements.menu.querySelectorAll('.custom-dropdown-item').forEach(item => {
const isCurrent = item.getAttribute('data-value') === value;
item.classList.toggle('active', isCurrent);
});
// Update trigger label
const selectedOpt = this.options.find(opt => opt.value === value);
if (selectedOpt) {
this.elements.labelSpan.textContent = selectedOpt.text;
if (selectedOpt.i18nKey) {
this.elements.labelSpan.setAttribute('data-tp-i18n', selectedOpt.i18nKey);
} else {
this.elements.labelSpan.removeAttribute('data-tp-i18n');
}
}
// Trigger callback
if (typeof this.onChange === 'function') {
this.onChange(value);
}
}
setValue(value) {
this.select(value);
}
getValue() {
return this.currentValue;
}
}
@@ -0,0 +1,663 @@
export class DateRangePicker {
constructor({ containerId, onChange, initialStartDate = '', initialEndDate = '' }) {
this.container = document.getElementById(containerId);
if (!this.container) return;
this.trigger = this.container.querySelector('#date-picker-trigger');
this.popover = this.container.querySelector('#date-picker-popover');
this.backdrop = this.container.querySelector('#date-picker-backdrop');
this.label = this.container.querySelector('#date-picker-label');
this.startDisplay = this.container.querySelector('#picker-start-display');
this.endDisplay = this.container.querySelector('#picker-end-display');
this.prevBtn = this.container.querySelector('#prev-month-btn');
this.nextBtn = this.container.querySelector('#next-month-btn');
this.leftMonthLabel = this.container.querySelector('#left-month-label');
this.rightMonthLabel = this.container.querySelector('#right-month-label');
this.leftMonthName = this.container.querySelector('#left-month-name');
this.rightMonthName = this.container.querySelector('#right-month-name');
this.leftYearSelect = this.container.querySelector('#left-year-select');
this.rightYearSelect = this.container.querySelector('#right-year-select');
this.leftPane = this.container.querySelector('#left-calendar-pane');
this.rightPane = this.container.querySelector('#right-calendar-pane');
this.leftDaysContainer = this.container.querySelector('#left-days-container');
this.rightDaysContainer = this.container.querySelector('#right-days-container');
this.cancelBtn = this.container.querySelector('#picker-cancel-btn');
this.applyBtn = this.container.querySelector('#picker-apply-btn');
this.onChange = onChange;
// Active dates (saved)
this.startDate = initialStartDate ? new Date(initialStartDate) : null;
this.endDate = initialEndDate ? new Date(initialEndDate) : null;
// Temporary dates (being edited)
this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
// Calendar view month/year
const today = new Date();
if (this.endDate) {
this.currentLeftMonth = this.endDate.getMonth();
this.currentLeftYear = this.endDate.getFullYear();
} else {
this.currentLeftMonth = today.getMonth();
this.currentLeftYear = today.getFullYear();
}
this.isOpen = false;
this._init();
}
_init() {
this._populateYearSelects();
this._bindEvents();
this._bindDisplayInputs();
this._updateTriggerLabel();
// Listen to language changes
document.addEventListener('languageChanged', () => {
if (this.isOpen) {
this._renderCalendars();
}
this._updateTriggerLabel();
});
}
_isEnglish() {
if (typeof window.getLang === 'function') {
const l = window.getLang();
if (l === 'id' || l === 'en') return l === 'en';
}
try {
const saved = localStorage.getItem('if_untan_lang');
if (saved === 'id' || saved === 'en') return saved === 'en';
} catch (e) {}
const nav = (navigator.language || 'id').toLowerCase();
return nav.startsWith('en');
}
_clearTime(date) {
if (!date) return null;
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
_datesEqual(d1, d2) {
if (!d1 || !d2) return false;
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
_getRightMonthYear() {
let m = this.currentLeftMonth + 1;
let y = this.currentLeftYear;
if (m > 11) {
m = 0;
y += 1;
}
return { month: m, year: y };
}
_navigateMonths(direction) {
this.currentLeftMonth += direction;
if (this.currentLeftMonth > 11) {
this.currentLeftMonth = 0;
this.currentLeftYear += 1;
} else if (this.currentLeftMonth < 0) {
this.currentLeftMonth = 11;
this.currentLeftYear -= 1;
}
this._renderCalendars();
}
_renderCalendars() {
const isEn = this._isEnglish();
const monthNames = isEn ?
['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] :
['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
const dayHeaders = isEn ?
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] :
['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
if (this.leftMonthName) {
this.leftMonthName.textContent = monthNames[this.currentLeftMonth];
}
if (this.leftYearSelect) {
this.leftYearSelect.value = this.currentLeftYear;
}
const rightMY = this._getRightMonthYear();
if (this.rightMonthName) {
this.rightMonthName.textContent = monthNames[rightMY.month];
}
if (this.rightYearSelect) {
this.rightYearSelect.value = rightMY.year;
}
const drawHeaders = (pane) => {
if (!pane) return;
const dayHeadersEl = pane.querySelector('.day-headers');
if (!dayHeadersEl) return;
dayHeadersEl.innerHTML = '';
dayHeaders.forEach(day => {
const span = document.createElement('span');
span.textContent = day;
dayHeadersEl.appendChild(span);
});
};
drawHeaders(this.leftPane);
drawHeaders(this.rightPane);
if (this.leftDaysContainer) {
this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear);
}
if (this.rightDaysContainer) {
this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year);
}
this._updateDisplayInputs();
this._updatePresetSelectionVisuals();
}
_populateDaysContainer(container, month, year) {
container.innerHTML = '';
const firstDay = new Date(year, month, 1);
const numDays = new Date(year, month + 1, 0).getDate();
let offset = (firstDay.getDay() + 6) % 7;
for (let i = 0; i < offset; i++) {
const cell = document.createElement('div');
cell.className = 'day-cell empty';
container.appendChild(cell);
}
const today = new Date();
today.setHours(23, 59, 59, 999);
for (let day = 1; day <= numDays; day++) {
const cell = document.createElement('div');
cell.className = 'day-cell';
cell.textContent = day;
const cellDate = new Date(year, month, day);
cellDate.setHours(0, 0, 0, 0);
if (cellDate > today) {
cell.classList.add('disabled');
} else {
if (this.tempStartDate && this.tempEndDate) {
const start = this._clearTime(this.tempStartDate);
const end = this._clearTime(this.tempEndDate);
if (this._datesEqual(cellDate, start)) {
cell.classList.add('range-start');
}
if (this._datesEqual(cellDate, end)) {
cell.classList.add('range-end');
}
if (cellDate > start && cellDate < end) {
cell.classList.add('range-between');
}
} else if (this.tempStartDate && this._datesEqual(cellDate, this._clearTime(this.tempStartDate))) {
cell.classList.add('range-start');
cell.classList.add('range-end');
}
cell.addEventListener('click', () => {
this._handleDayClick(cellDate);
});
}
container.appendChild(cell);
}
}
_handleDayClick(date) {
if (!this.tempStartDate || (this.tempStartDate && this.tempEndDate)) {
this.tempStartDate = new Date(date);
this.tempEndDate = null;
} else {
if (date < this.tempStartDate) {
this.tempStartDate = new Date(date);
this.tempEndDate = null;
} else {
this.tempEndDate = new Date(date);
}
}
this._renderCalendars();
}
_formatDateDisplay(date) {
if (!date) return '';
const d = new Date(date);
const dd = String(d.getDate()).padStart(2, '0');
const mm = String(d.getMonth() + 1).padStart(2, '0');
const yyyy = d.getFullYear();
return `${dd}/${mm}/${yyyy}`;
}
_updateDisplayInputs() {
if (this.startDisplay) {
this.startDisplay.value = this._formatDateDisplay(this.tempStartDate);
}
if (this.endDisplay) {
this.endDisplay.value = this._formatDateDisplay(this.tempEndDate);
}
}
_calculatePreset(presetName) {
const today = new Date();
today.setHours(0, 0, 0, 0);
let start = null;
let end = null;
switch (presetName) {
case 'today':
start = new Date(today);
end = new Date(today);
break;
case 'yesterday':
start = new Date(today);
start.setDate(today.getDate() - 1);
end = new Date(start);
break;
case 'last7':
end = new Date(today);
start = new Date(today);
start.setDate(today.getDate() - 6);
break;
case 'last14':
end = new Date(today);
start = new Date(today);
start.setDate(today.getDate() - 13);
break;
case 'last30':
end = new Date(today);
start = new Date(today);
start.setDate(today.getDate() - 29);
break;
case 'thisWeek':
end = new Date(today);
start = new Date(today);
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1);
start.setDate(diff);
break;
case 'lastWeek':
const temp = new Date(today);
const tempDay = temp.getDay();
const tempDiff = temp.getDate() - tempDay + (tempDay === 0 ? -6 : 1);
start = new Date(temp);
start.setDate(tempDiff - 7);
end = new Date(temp);
end.setDate(tempDiff - 1);
break;
case 'thisMonth':
start = new Date(today.getFullYear(), today.getMonth(), 1);
end = new Date(today);
break;
case 'lastMonth':
start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
end = new Date(today.getFullYear(), today.getMonth(), 0);
break;
case 'all':
default:
start = null;
end = null;
break;
}
return { start, end };
}
_handlePresetClick(presetName) {
const { start, end } = this._calculatePreset(presetName);
this.tempStartDate = start;
this.tempEndDate = end;
const today = new Date();
if (this.tempEndDate) {
this.currentLeftMonth = this.tempEndDate.getMonth();
this.currentLeftYear = this.tempEndDate.getFullYear();
} else {
this.currentLeftMonth = today.getMonth();
this.currentLeftYear = today.getFullYear();
}
this._renderCalendars();
}
_updatePresetSelectionVisuals() {
this.container.querySelectorAll('.preset-btn').forEach(btn => {
btn.classList.remove('active');
});
if (!this.tempStartDate && !this.tempEndDate) {
this.container.querySelector('[data-preset="all"]')?.classList.add('active');
return;
}
if (this.tempStartDate && this.tempEndDate) {
const activePreset = ['today', 'yesterday', 'last7', 'last14', 'last30', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth'].find(preset => {
const { start, end } = this._calculatePreset(preset);
return start && end &&
this._datesEqual(this.tempStartDate, start) &&
this._datesEqual(this.tempEndDate, end);
});
if (activePreset) {
this.container.querySelector(`[data-preset="${activePreset}"]`)?.classList.add('active');
}
}
}
_updateTriggerLabel() {
if (!this.startDate && !this.endDate) {
if (this.label) {
this.label.textContent = window.tpT?.('filter_all_time') || 'Semua Waktu';
this.label.setAttribute('data-tp-i18n', 'filter_all_time');
}
} else {
if (this.label) {
this.label.removeAttribute('data-tp-i18n');
const startFormatted = this._formatDateDisplay(this.startDate);
const endFormatted = this._formatDateDisplay(this.endDate);
if (this._datesEqual(this.startDate, this.endDate)) {
this.label.textContent = startFormatted;
} else {
this.label.textContent = `${startFormatted} - ${endFormatted}`;
}
}
}
}
_bindEvents() {
this.trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
this.popover.addEventListener('click', (e) => {
e.stopPropagation();
});
if (this.prevBtn) {
this.prevBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._navigateMonths(-1);
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._navigateMonths(1);
});
}
const presetsList = this.container.querySelector('.presets-list');
if (presetsList) {
presetsList.addEventListener('click', (e) => {
const btn = e.target.closest('.preset-btn');
if (!btn) return;
e.stopPropagation();
const preset = btn.getAttribute('data-preset');
this._handlePresetClick(preset);
});
}
if (this.cancelBtn) {
this.cancelBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._handleCancel();
});
}
if (this.applyBtn) {
this.applyBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._handleApply();
});
}
if (this.backdrop) {
this.backdrop.addEventListener('click', (e) => {
e.stopPropagation();
this._handleCancel();
});
}
document.addEventListener('click', () => {
if (this.isOpen) {
this._handleCancel();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this._handleCancel();
}
});
}
_bindDisplayInputs() {
const parseDate = (val) => {
const m = val.trim().match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/);
if (!m) return null;
const d = parseInt(m[1], 10);
const mon = parseInt(m[2], 10) - 1;
const y = parseInt(m[3], 10);
if (mon < 0 || mon > 11 || d < 1 || d > 31 || y < 1000 || y > 9999) return null;
const testDate = new Date(y, mon, d);
if (testDate.getFullYear() !== y || testDate.getMonth() !== mon || testDate.getDate() !== d) {
return null;
}
return testDate;
};
const setupInput = (input, isStart) => {
if (!input) return;
input.addEventListener('keydown', (e) => {
const CTRL_KEYS = new Set(['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter']);
if (!/^\d$/.test(e.key) && e.key !== '/' && e.key !== '-' && e.key !== '.' && !CTRL_KEYS.has(e.key)) {
e.preventDefault();
}
});
input.addEventListener('input', (e) => {
if (e.inputType && e.inputType.startsWith('delete')) {
return;
}
let val = input.value.replace(/[^\d]/g, '');
let formatted = val;
if (val.length > 2) {
formatted = val.substring(0, 2) + '/' + val.substring(2);
}
if (val.length > 4) {
formatted = val.substring(0, 2) + '/' + val.substring(2, 4) + '/' + val.substring(4, 8);
}
input.value = formatted;
const parsed = parseDate(formatted);
if (parsed) {
const today = new Date();
today.setHours(23, 59, 59, 999);
if (parsed <= today) {
if (isStart) {
this.tempStartDate = parsed;
if (this.tempEndDate && this.tempEndDate < this.tempStartDate) {
this.tempEndDate = new Date(this.tempStartDate);
}
} else {
this.tempEndDate = parsed;
if (this.tempStartDate && this.tempStartDate > this.tempEndDate) {
this.tempStartDate = new Date(this.tempEndDate);
}
}
this.currentLeftMonth = parsed.getMonth();
this.currentLeftYear = parsed.getFullYear();
const rightMY = this._getRightMonthYear();
const isEn = this._isEnglish();
const monthNames = isEn ?
['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] :
['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
if (this.leftMonthName) this.leftMonthName.textContent = monthNames[this.currentLeftMonth];
if (this.leftYearSelect) this.leftYearSelect.value = this.currentLeftYear;
if (this.rightMonthName) this.rightMonthName.textContent = monthNames[rightMY.month];
if (this.rightYearSelect) this.rightYearSelect.value = rightMY.year;
if (this.leftDaysContainer) this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear);
if (this.rightDaysContainer) this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year);
this._updatePresetSelectionVisuals();
}
}
});
input.addEventListener('blur', () => {
const parsed = parseDate(input.value);
if (!parsed) {
if (isStart) {
input.value = this._formatDateDisplay(this.tempStartDate);
} else {
input.value = this._formatDateDisplay(this.tempEndDate);
}
}
});
};
setupInput(this.startDisplay, true);
setupInput(this.endDisplay, false);
}
_populateYearSelects() {
if (!this.leftYearSelect || !this.rightYearSelect) return;
this.leftYearSelect.innerHTML = '';
this.rightYearSelect.innerHTML = '';
const startYear = 2020;
const endYear = new Date().getFullYear() + 4;
for (let y = startYear; y <= endYear; y++) {
const optLeft = document.createElement('option');
optLeft.value = y;
optLeft.textContent = y;
this.leftYearSelect.appendChild(optLeft);
const optRight = document.createElement('option');
optRight.value = y;
optRight.textContent = y;
this.rightYearSelect.appendChild(optRight);
}
this.leftYearSelect.addEventListener('change', () => {
this._handleYearChange(parseInt(this.leftYearSelect.value, 10), true);
});
this.rightYearSelect.addEventListener('change', () => {
this._handleYearChange(parseInt(this.rightYearSelect.value, 10), false);
});
}
_handleYearChange(year, isLeft) {
if (isLeft) {
this.currentLeftYear = year;
} else {
const rightMY = this._getRightMonthYear();
const rightMonth = rightMY.month;
if (rightMonth === 0) {
this.currentLeftYear = year - 1;
} else {
this.currentLeftYear = year;
}
}
this._renderCalendars();
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
document.querySelectorAll('.custom-dropdown.active').forEach(dd => {
dd.classList.remove('active');
dd.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false');
});
this.isOpen = true;
this.container.classList.add('active');
this.trigger.setAttribute('aria-expanded', 'true');
this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
const today = new Date();
if (this.endDate) {
this.currentLeftMonth = this.endDate.getMonth();
this.currentLeftYear = this.endDate.getFullYear();
} else {
this.currentLeftMonth = today.getMonth();
this.currentLeftYear = today.getFullYear();
}
this._renderCalendars();
if (window.innerWidth <= 768) {
document.body.classList.add('date-picker-open');
}
}
close() {
this.isOpen = false;
this.container.classList.remove('active');
this.trigger.setAttribute('aria-expanded', 'false');
document.body.classList.remove('date-picker-open');
}
_handleCancel() {
this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
this.close();
}
_handleApply() {
this.startDate = this.tempStartDate;
this.endDate = this.tempEndDate;
this._updateTriggerLabel();
this.close();
const startStr = this._getDateString(this.startDate);
const endStr = this._getDateString(this.endDate);
if (this.onChange) {
this.onChange(startStr, endStr);
}
}
_getDateString(date) {
if (!date) return '';
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
}
@@ -0,0 +1,73 @@
import { DirectusClient } from '../services/directus-client.js';
import { FileDownloader } from '../services/file-downloader.js';
import { DocumentFormatter } from '../utils/document-formatter.js';
export class DocumentCardView {
constructor(item) {
this.item = item;
this.title = item.nama_berkas;
this.tags = DocumentFormatter.parseTags(item.tag);
this.category = DocumentFormatter.categoryFromTags(this.tags);
this.fileType = DocumentFormatter.fileTypeFromName(this.title);
this.assetUrl = DirectusClient.assetUrl(item.file);
this.viewerUrl = DirectusClient.viewerUrl(item);
this.downloadName = DocumentFormatter.downloadFilename(this.title, this.fileType);
this.dateIso = item.tanggal_upload || '';
this.dateKey = this.dateIso ? this.dateIso.substring(0, 10) : '';
}
render() {
const card = document.createElement('article');
card.className = 'document-card';
card.dataset.date = this.dateKey;
const typeUpper = this.fileType.toUpperCase();
const typeClass = this.fileType === 'pdf' ? 'doc-type-pdf' : 'doc-type-docx';
const iconClass = this.fileType === 'pdf' ? 'fas fa-file-pdf' : 'fas fa-file-word';
const formattedDate = DocumentFormatter.formatDate(this.dateIso);
const safeTitle = DocumentFormatter.escapeHtml(this.title);
const safeViewerUrl = DocumentFormatter.escapeAttr(this.viewerUrl);
const safeAssetUrl = DocumentFormatter.escapeAttr(this.assetUrl);
// Build tag badges HTML.
const tagBadgesHtml = this.tags.length
? `<div class="doc-tags"><i class="fas fa-tags doc-tags-icon"></i>${this.tags.map(t => `<span class="doc-tag">${DocumentFormatter.escapeHtml(t)}</span>`).join('')}</div>`
: '';
card.innerHTML = `
<div class="doc-type-icon ${typeClass}"><i class="${iconClass}"></i></div>
<h3 class="doc-title">${safeTitle}</h3>
<div class="doc-meta">
<span class="doc-badge">${typeUpper}</span>
<span class="doc-badge date"><i class="far fa-calendar-alt"></i> ${formattedDate}</span>
</div>
${tagBadgesHtml}
<div class="doc-actions">
<a href="${safeViewerUrl}" class="btn-action-primary" target="_blank" rel="noopener"><span data-tp-i18n="card_open">Buka Berkas</span> <i class="fas fa-external-link-alt"></i></a>
<a href="${safeAssetUrl}" class="btn-action-outline"><span data-tp-i18n="card_download">Unduh Berkas</span> <i class="fas fa-download"></i></a>
</div>
`;
card.querySelector('.btn-action-outline').addEventListener('click', (e) => {
e.preventDefault();
FileDownloader.download(this.assetUrl, this.downloadName);
});
// Prefetch the asset when the user hovers over the card so the
// browser cache already has the file by the time the viewer loads.
let prefetched = false;
card.addEventListener('mouseenter', () => {
if (prefetched) return;
prefetched = true;
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = 'fetch';
link.crossOrigin = 'anonymous';
link.href = this.assetUrl;
document.head.appendChild(link);
}, { once: true });
return card;
}
}
@@ -0,0 +1,22 @@
export class AccessibilityToggleController {
constructor({
triggerSelector = '[data-accessibility-toggle]',
panelId = 'access-panel',
} = {}) {
this.triggers = Array.from(document.querySelectorAll(triggerSelector));
this.panel = document.getElementById(panelId);
}
init() {
if (this.triggers.length === 0) return;
this.triggers.forEach(trigger => {
trigger.addEventListener('click', () => this.toggle());
});
}
toggle() {
if (!this.panel) return;
this.panel.classList.toggle('hidden');
this.panel.classList.toggle('flex');
}
}
@@ -0,0 +1,466 @@
import { DirectusClient } from '../services/directus-client.js';
import { DocumentCardView } from '../components/document-card-view.js';
import { CustomDropdown } from '../components/custom-dropdown.js';
import { DateRangePicker } from '../components/date-range-picker.js';
import { CounterAnimation } from '../components/counter-animation.js';
export class DocumentRepository {
static PAGE_SIZE = 6;
constructor() {
this.gridContainer = document.getElementById('document-grid');
this.filterWrapper = document.getElementById('tag-filter-wrapper');
this.mobileFilterWrapper = document.getElementById('mobile-tag-filter-wrapper');
this.searchInput = document.getElementById('doc-search');
this.sortFilterWrapper = document.getElementById('sort-filter-wrapper');
// Hero stat values (filled from real API data after fetch).
this.statActiveEl = document.getElementById('stat-active-value');
this.statUpdatesEl = document.getElementById('stat-updates-value');
this.cards = [];
this.paginationContainer = null;
this.state = {
filter: 'all', // 'all' or a specific tag string
search: '',
startDate: '',
endDate: '',
sort: 'newest',
page: 1,
};
}
async init() {
if (!this.gridContainer) return;
this._bindControls();
this._initPagination();
try {
const items = await DirectusClient.fetchPublished('berkas');
this._renderCards(items);
this._buildCategoryFilters(items);
this._renderStats(items);
this._applySort();
this._applyFilters();
} catch (err) {
console.error(err);
this._renderError();
this._renderStatsError();
}
}
_initPagination() {
const pag = document.createElement('div');
pag.id = 'doc-pagination';
pag.className = 'doc-pagination';
this.gridContainer.insertAdjacentElement('afterend', pag);
this.paginationContainer = pag;
}
// Days within which an upload counts as a "recent update".
static RECENT_DAYS = 30;
// Populate the hero stats from real data:
// Dokumen Aktif → number of published documents
// Pembaruan Terbaru → uploads within the last RECENT_DAYS days
_renderStats(items) {
const active = items.length;
const windowMs = DocumentRepository.RECENT_DAYS * 24 * 60 * 60 * 1000;
const now = Date.now();
const recent = items.filter(item => {
if (!item.tanggal_upload) return false;
const t = new Date(item.tanggal_upload).getTime();
return !Number.isNaN(t) && now - t <= windowMs;
}).length;
CounterAnimation.animateValue(this.statActiveEl, active);
CounterAnimation.animateValue(this.statUpdatesEl, recent);
}
_renderStatsError() {
if (this.statActiveEl) this.statActiveEl.textContent = '—';
if (this.statUpdatesEl) this.statUpdatesEl.textContent = '—';
}
/**
* Fixed category definitions for the sidebar filter.
* Each has a key, an i18n key for bilingual labels, and a fallback label.
*/
static CATEGORIES = [
{ key: 'all', i18n: 'filter_all', label: 'Semua Berkas' },
{ key: 'skripsi', i18n: 'filter_skripsi', label: 'Panduan Skripsi' },
{ key: 'akademik', i18n: 'filter_akademik', label: 'Kalender Akademik' },
{ key: 'praktik', i18n: 'filter_praktik', label: 'Kerja Praktik' },
{ key: 'umum', i18n: 'filter_umum', label: 'Formulir Umum' },
];
/**
* Build category filter controls:
* Desktop: sidebar buttons (hidden on mobile via CSS)
* Mobile: a <select> dropdown (hidden on desktop via CSS)
* Both stay in sync through a shared _setFilter() helper.
*/
_buildCategoryFilters(items) {
if (!this.filterWrapper) return;
// Clear skeleton placeholder.
this.filterWrapper.innerHTML = '';
// ── Desktop buttons ──
const btnGroup = document.createElement('div');
btnGroup.className = 'filter-btn-group';
DocumentRepository.CATEGORIES.forEach(({ key, i18n, label }) => {
const isActive = key === 'all';
const btn = document.createElement('button');
btn.className = 'doc-filter-btn' + (isActive ? ' active' : '');
btn.dataset.filter = key;
btn.textContent = (window.tpT?.(i18n)) || label;
btn.setAttribute('data-tp-i18n', i18n);
btn.addEventListener('click', () => this._setFilter(key));
btnGroup.appendChild(btn);
});
this.filterWrapper.appendChild(btnGroup);
// ── Mobile Custom UI Dropdown ──
if (this.mobileFilterWrapper) {
const dropdownOptions = DocumentRepository.CATEGORIES.map(({ key, i18n, label }) => ({
value: key,
i18nKey: i18n,
text: (window.tpT?.(i18n)) || label
}));
this._mobileDropdown = new CustomDropdown({
container: this.mobileFilterWrapper,
options: dropdownOptions,
defaultValue: this.state.filter,
onChange: (value) => this._setFilter(value)
});
}
// Store refs for syncing.
this._filterBtnGroup = btnGroup;
// Re-translate if i18n is active.
window.applyTp?.(this.filterWrapper);
}
/**
* Shared filter setter — keeps buttons + dropdown in sync.
*/
_setFilter(key) {
this.state.filter = key;
this.state.page = 1;
// Sync buttons.
if (this._filterBtnGroup) {
this._filterBtnGroup.querySelectorAll('.doc-filter-btn')
.forEach(b => b.classList.toggle('active', b.dataset.filter === key));
}
// Sync custom dropdown.
if (this._mobileDropdown && this._mobileDropdown.getValue() !== key) {
this._mobileDropdown.setValue(key);
}
this._applyFilters();
}
_bindControls() {
this.searchInput?.addEventListener('input', (e) => {
this.state.search = e.target.value.toLowerCase().trim();
this.state.page = 1;
this._applyFilters();
});
// Initialize Custom Date Range Picker
const datePickerGroup = document.getElementById('date-range-control-group');
if (datePickerGroup) {
this.dateRangePicker = new DateRangePicker({
containerId: 'date-range-control-group',
onChange: (startDate, endDate) => {
this.state.startDate = startDate;
this.state.endDate = endDate;
this.state.page = 1;
this._applyFilters();
}
});
window.applyTp?.(datePickerGroup);
}
if (this.sortFilterWrapper) {
const sortOptions = [
{ value: 'newest', i18nKey: 'sort_newest', text: (window.tpT?.('sort_newest')) || 'Terbaru' },
{ value: 'oldest', i18nKey: 'sort_oldest', text: (window.tpT?.('sort_oldest')) || 'Terlama' },
{ value: 'az', i18nKey: 'sort_az', text: (window.tpT?.('sort_az')) || 'Nama A-Z' }
];
this._sortDropdown = new CustomDropdown({
container: this.sortFilterWrapper,
options: sortOptions,
defaultValue: this.state.sort,
prefixIcon: 'fas fa-sort-amount-down',
onChange: (value) => {
this.state.sort = value;
this.state.page = 1;
this._applySort();
this._applyFilters();
}
});
window.applyTp?.(this.sortFilterWrapper);
}
}
_renderCards(items) {
this.gridContainer.innerHTML = '';
if (items.length === 0) {
this._renderState('repo-empty', 'fas fa-folder-open', '#cbd5e1', '#64748b',
'state_empty', 'Belum ada dokumen yang tersedia di repositori.');
return;
}
this.cards = items.map(item => {
const view = new DocumentCardView(item);
const element = view.render();
this.gridContainer.appendChild(element);
return { view, element };
});
// Translate the freshly-rendered card labels (Buka/Unduh Berkas).
window.applyTp?.(this.gridContainer);
}
_renderError() {
this._renderState('repo-error', 'fas fa-exclamation-triangle', '#f87171', '#ef4444',
'state_error', 'Gagal memuat berkas. Silakan periksa koneksi internet Anda atau coba lagi nanti.');
}
// Build the empty/error placeholder via safe DOM APIs (no innerHTML).
// `i18nKey` lets the Tapops i18n layer re-translate the message on toggle.
_renderState(className, iconClass, iconColor, textColor, i18nKey, message) {
this.cards = [];
this.gridContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = className;
Object.assign(wrapper.style, {
gridColumn: '1 / -1',
textAlign: 'center',
padding: '60px 20px',
color: textColor,
fontWeight: '700',
fontSize: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '15px',
});
const icon = document.createElement('i');
icon.className = iconClass;
Object.assign(icon.style, { fontSize: '3rem', color: iconColor });
const span = document.createElement('span');
if (i18nKey) span.setAttribute('data-tp-i18n', i18nKey);
span.textContent = (window.tpT?.(i18nKey)) || message;
wrapper.appendChild(icon);
wrapper.appendChild(span);
this.gridContainer.appendChild(wrapper);
}
_applyFilters() {
const matched = this.cards.filter(({ view }) => this._cardMatches(view));
const total = matched.length;
// Handle zero-results state when cards exist but none match the filter.
this._toggleNoResultsState(total === 0 && this.cards.length > 0);
if (total === 0) {
this.cards.forEach(({ element }) => { element.style.display = 'none'; });
this._renderPagination(0, 0);
return;
}
const totalPages = Math.ceil(total / DocumentRepository.PAGE_SIZE);
// Clamp page to valid range after filter changes.
this.state.page = Math.min(Math.max(this.state.page, 1), totalPages);
const start = (this.state.page - 1) * DocumentRepository.PAGE_SIZE;
const pageSet = new Set(
matched.slice(start, start + DocumentRepository.PAGE_SIZE).map(c => c.element)
);
this.cards.forEach(({ element }) => {
const show = pageSet.has(element);
element.style.display = show ? 'flex' : 'none';
if (show) {
setTimeout(() => {
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
}, 10);
} else {
element.style.opacity = '0';
element.style.transform = 'translateY(10px)';
}
});
this._renderPagination(totalPages, total);
}
_toggleNoResultsState(show) {
const existing = this.gridContainer.querySelector('.repo-no-results');
if (show && !existing) {
const wrapper = document.createElement('div');
wrapper.className = 'repo-no-results';
Object.assign(wrapper.style, {
gridColumn: '1 / -1',
textAlign: 'center',
padding: '60px 20px',
color: '#64748b',
fontWeight: '700',
fontSize: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '15px',
});
const icon = document.createElement('i');
icon.className = 'fas fa-search';
Object.assign(icon.style, { fontSize: '3rem', color: '#cbd5e1' });
const span = document.createElement('span');
span.setAttribute('data-tp-i18n', 'state_no_results');
span.textContent = (window.tpT?.('state_no_results')) || 'Tidak ada berkas yang cocok dengan filter saat ini.';
wrapper.appendChild(icon);
wrapper.appendChild(span);
this.gridContainer.appendChild(wrapper);
} else if (!show && existing) {
existing.remove();
}
}
_renderPagination(totalPages, total) {
if (!this.paginationContainer) return;
this.paginationContainer.innerHTML = '';
if (total === 0 || totalPages <= 1) return;
const current = this.state.page;
const start = (current - 1) * DocumentRepository.PAGE_SIZE + 1;
const end = Math.min(current * DocumentRepository.PAGE_SIZE, total);
const info = document.createElement('span');
info.className = 'pagination-info';
info.textContent = `${start}${end} dari ${total} berkas`;
this.paginationContainer.appendChild(info);
const nav = document.createElement('nav');
nav.className = 'pagination-nav';
nav.setAttribute('aria-label', 'Navigasi halaman dokumen');
const prev = document.createElement('button');
prev.className = 'pagination-btn' + (current <= 1 ? ' disabled' : '');
prev.disabled = current <= 1;
prev.setAttribute('aria-label', 'Halaman sebelumnya');
const prevIcon = document.createElement('i');
prevIcon.className = 'fas fa-chevron-left';
prev.appendChild(prevIcon);
prev.addEventListener('click', () => {
if (this.state.page > 1) { this.state.page--; this._applyFilters(); this._scrollToGrid(); }
});
nav.appendChild(prev);
this._paginationRange(current, totalPages).forEach(p => {
if (p === '...') {
const el = document.createElement('span');
el.className = 'pagination-ellipsis';
el.setAttribute('aria-hidden', 'true');
el.textContent = '…';
nav.appendChild(el);
} else {
const btn = document.createElement('button');
btn.className = 'pagination-btn pagination-number' + (p === current ? ' active' : '');
btn.textContent = p;
btn.setAttribute('aria-label', `Halaman ${p}`);
if (p === current) btn.setAttribute('aria-current', 'page');
btn.addEventListener('click', () => {
this.state.page = p; this._applyFilters(); this._scrollToGrid();
});
nav.appendChild(btn);
}
});
const next = document.createElement('button');
next.className = 'pagination-btn' + (current >= totalPages ? ' disabled' : '');
next.disabled = current >= totalPages;
next.setAttribute('aria-label', 'Halaman berikutnya');
const nextIcon = document.createElement('i');
nextIcon.className = 'fas fa-chevron-right';
next.appendChild(nextIcon);
next.addEventListener('click', () => {
if (this.state.page < totalPages) { this.state.page++; this._applyFilters(); this._scrollToGrid(); }
});
nav.appendChild(next);
this.paginationContainer.appendChild(nav);
}
// Returns page numbers and '...' ellipsis placeholders for the given range.
_paginationRange(current, total) {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages = [1];
const lo = Math.max(2, current - 1);
const hi = Math.min(total - 1, current + 1);
if (lo > 2) pages.push('...');
for (let i = lo; i <= hi; i++) pages.push(i);
if (hi < total - 1) pages.push('...');
pages.push(total);
return pages;
}
_scrollToGrid() {
document.getElementById('repository')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
_cardMatches(view) {
const { filter, search } = this.state;
const matchesCategory = filter === 'all' || view.category === filter;
const matchesTitle = view.title.toLowerCase().includes(search);
const matchesTags = search
? view.tags.some(t => t.toLowerCase().includes(search))
: false;
const matchesSearch = matchesTitle || matchesTags;
return matchesCategory && matchesSearch && this._dateMatches(view.dateKey);
}
_dateMatches(dateKey) {
const { startDate, endDate } = this.state;
if (!dateKey) return !startDate && !endDate;
const cardDate = new Date(dateKey);
if (startDate && cardDate < new Date(startDate)) return false;
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
if (cardDate > end) return false;
}
return true;
}
_applySort() {
const { sort } = this.state;
this.cards.sort((a, b) => {
if (sort === 'newest') return b.view.dateKey.localeCompare(a.view.dateKey);
if (sort === 'oldest') return a.view.dateKey.localeCompare(b.view.dateKey);
if (sort === 'az') return a.view.title.toLowerCase().localeCompare(b.view.title.toLowerCase());
return 0;
});
this.cards.forEach(({ element }) => this.gridContainer.appendChild(element));
}
}
@@ -0,0 +1,29 @@
export class MobileMenuController {
constructor({
buttonId = 'mobile-menu-button',
menuId = 'mobile-menu',
closeButtonId = 'close-mobile-menu',
} = {}) {
this.button = document.getElementById(buttonId);
this.menu = document.getElementById(menuId);
this.closeButton = document.getElementById(closeButtonId);
}
init() {
if (!this.button || !this.menu) return;
this.button.addEventListener('click', () => this.toggle());
this.closeButton?.addEventListener('click', () => this.close());
}
toggle() {
const isOpen = !this.menu.classList.contains('-translate-x-full');
this.menu.classList.toggle('-translate-x-full');
this.button.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
}
close() {
this.menu.classList.add('-translate-x-full');
this.button?.setAttribute('aria-expanded', 'false');
}
}
@@ -0,0 +1,88 @@
export class NavigationController {
constructor({
menuSelector = '[data-menu]',
overlayId = 'mega-overlay',
} = {}) {
this.menuItems = Array.from(document.querySelectorAll(menuSelector));
this.overlay = document.getElementById(overlayId);
this.activeMenu = null;
this.menuMap = {
tentang: 'mega-tentang',
program: 'mega-program',
sdm: 'mega-sdm',
fasilitas: 'mega-fasilitas',
karya: 'mega-karya',
kontak: 'mega-kontak',
};
}
init() {
if (this.menuItems.length === 0 || !this.overlay) return;
this.menuItems.forEach(item => {
const menuId = item.getAttribute('data-menu');
const button = item.querySelector('.nav-menu-btn');
if (!button) return;
button.setAttribute('aria-haspopup', 'true');
button.setAttribute('aria-expanded', 'false');
button.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
if (this.activeMenu === menuId) {
this.closeMenus();
} else {
this.openMenu(menuId);
}
});
});
this.overlay.addEventListener('click', () => this.closeMenus());
document.addEventListener('click', event => this._handleOutsideClick(event));
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && this.activeMenu) this.closeMenus();
});
}
openMenu(menuId) {
const targetPanel = document.getElementById(this.menuMap[menuId]);
if (!targetPanel) return;
Object.values(this.menuMap).forEach(panelId => {
document.getElementById(panelId)?.classList.remove('active');
});
targetPanel.classList.add('active');
this.overlay.classList.add('active');
this._setButtonState(menuId);
this.activeMenu = menuId;
}
closeMenus() {
Object.values(this.menuMap).forEach(panelId => {
document.getElementById(panelId)?.classList.remove('active');
});
this.overlay.classList.remove('active');
this._setButtonState(null);
this.activeMenu = null;
}
_setButtonState(menuId) {
this.menuItems.forEach(item => {
const isActive = item.getAttribute('data-menu') === menuId;
const button = item.querySelector('.nav-menu-btn');
const chevron = item.querySelector('.fa-chevron-down');
button?.classList.toggle('active', isActive);
button?.setAttribute('aria-expanded', isActive ? 'true' : 'false');
if (chevron) chevron.style.transform = isActive ? 'rotate(180deg)' : '';
});
}
_handleOutsideClick(event) {
const clickedInsideNav = event.target.closest('#main-nav');
const clickedInsidePanel = event.target.closest('.mega-panel');
if (!clickedInsideNav && !clickedInsidePanel && this.activeMenu) this.closeMenus();
}
}
+208
View File
@@ -0,0 +1,208 @@
/**
* Tapops-local i18n — bilingual ID/EN for the document-repository sections.
*
* The shared assets/global/localization.js owns the nav/footer translations
* and exposes window.getLang() plus a `languageChanged` event. Its internal
* `translations` object is not extendable from here, so this layer keeps its
* own dictionary under a separate `data-tp-i18n*` attribute namespace. The
* global applyLang() ignores those attributes, and this script ignores the
* global `data-i18n*` ones — no collisions.
*
* Exposes window.applyTp() and window.tpT(key) so main.js can translate the
* dynamically-rendered document cards and state messages too.
*/
(function () {
const dict = {
id: {
// Hero
hero_kicker: "Digital Administrative Hub",
hero_title: "Administrasi Akademik Digital",
hero_tagline: "Satu pintu menuju semua dokumen penting. Akses terpusat menuju repositori dokumen akademik Program Studi Informatika Universitas Tanjungpura untuk mahasiswa dan staf.",
hero_btn_search: "Cari Dokumen",
hero_btn_flow: "ALUR PENGAJUAN",
stat_active: "Dokumen Aktif",
stat_updates: "Pembaruan Terbaru",
// Repository — sidebar
sidebar_category: "KATEGORI BERKAS",
filter_all: "Semua Berkas",
filter_skripsi: "Panduan Skripsi",
filter_akademik: "Kalender Akademik",
filter_praktik: "Kerja Praktik",
filter_umum: "Formulir Umum",
sidebar_info: "Pastikan mengunduh versi terbaru untuk menjamin validitas berkas pengajuan.",
// Repository — controls
repo_title: "Eksplorasi Dokumen Prodi",
repo_subtitle: "Temukan berkas administrasi dan akademik terbaru Program Studi Informatika.",
search_ph: "Cari nama berkas atau kata kunci...",
label_period: "Periode Unggahan",
range_divider: "s/d",
label_sort: "Urutkan",
sort_newest: "Terbaru",
sort_oldest: "Terlama",
sort_az: "Nama A-Z",
loading: "Menghubungkan ke repositori berkas...",
filter_all_time: "Semua Waktu",
presets_title: "RENTANG WAKTU",
preset_today: "Hari Ini",
preset_yesterday: "Kemarin",
preset_last_7_days: "7 Hari Terakhir",
preset_last_14_days: "14 Hari Terakhir",
preset_last_30_days: "30 Hari Terakhir",
preset_this_week: "Minggu Ini",
preset_last_week: "Minggu Lalu",
preset_this_month: "Bulan Ini",
preset_last_month: "Bulan Lalu",
btn_cancel: "Batal",
btn_update: "Perbarui",
// Repository — dynamic (cards + states, used by main.js)
card_open: "Buka Berkas",
card_download: "Unduh Berkas",
state_empty: "Belum ada dokumen yang tersedia di repositori.",
state_error: "Gagal memuat berkas. Silakan periksa koneksi internet Anda atau coba lagi nanti.",
// Procedures
proc_title: "Tata Cara Pengajuan Berkas",
proc_tag: "Alur Pengajuan",
proc_s1_h: "Unduh Formulir",
proc_s1_p: "Pilih kategori pengajuan Anda, lalu unduh formulir versi terbaru. Pastikan kode revisi pada formulir sesuai dengan yang tertera di halaman ini.",
proc_s2_h: "Lengkapi Formulir",
proc_s2_p: "Isi setiap kolom dengan lengkap dan teliti. Sertakan seluruh dokumen pendukung yang diminta sebelum pengiriman.",
proc_s3_h: "Kirim Berkas",
proc_s3_p: "Kirimkan formulir beserta dokumen pendukung melalui email resmi atau WhatsApp admin prodi. Berkas yang lengkap akan langsung diproses oleh admin prodi.",
// Help Center
help_title: "Butuh Bantuan Lebih Lanjut?",
help_desc: "Tim admin kami siap membantu Anda melalui kanal komunikasi resmi di bawah ini.",
help_email_title: "Korespondensi Resmi (Email)",
help_email_desc: "Gunakan untuk permohonan form khusus non-publik atau administrasi formal lainnya.",
help_email_c1: "Respon Maks. 24 Jam",
help_email_c2: "Pelayanan Formal",
help_email_btn: "HUBUNGI ADMIN",
help_wa_title: "Quick Support (WhatsApp)",
help_wa_desc: "Konsultasi cepat alur birokrasi atau pelaporan kendala akses pada berkas repositori.",
help_wa_c1: "Respon Instan Jam Kerja",
help_wa_c2: "Konsultasi Teknis",
help_wa_btn: "CHAT WHATSAPP",
},
en: {
// Hero
hero_kicker: "Digital Administrative Hub",
hero_title: "Digital Academic Administration",
hero_tagline: "One gateway to every important document. A centralized access point to the academic document repository of the Informatics Department, Universitas Tanjungpura, for students and staff.",
hero_btn_search: "Search Documents",
hero_btn_flow: "SUBMISSION FLOW",
stat_active: "Active Documents",
stat_updates: "Recent Updates",
// Repository — sidebar
sidebar_category: "DOCUMENT CATEGORY",
filter_all: "All Files",
filter_skripsi: "Thesis Guides",
filter_akademik: "Academic Calendar",
filter_praktik: "Internship",
filter_umum: "General Forms",
sidebar_info: "Always download the latest version to ensure your submission files remain valid.",
// Repository — controls
repo_title: "Explore Department Documents",
repo_subtitle: "Find the latest administrative and academic files of the Informatics Department.",
search_ph: "Search file name or keyword...",
label_period: "Upload Period",
range_divider: "to",
label_sort: "Sort by",
sort_newest: "Newest",
sort_oldest: "Oldest",
sort_az: "Name A-Z",
loading: "Connecting to the file repository...",
filter_all_time: "All Time",
presets_title: "DATE RANGE",
preset_today: "Today",
preset_yesterday: "Yesterday",
preset_last_7_days: "Last 7 Days",
preset_last_14_days: "Last 14 Days",
preset_last_30_days: "Last 30 Days",
preset_this_week: "This Week",
preset_last_week: "Last Week",
preset_this_month: "This Month",
preset_last_month: "Last Month",
btn_cancel: "Cancel",
btn_update: "Update",
// Repository — dynamic (cards + states, used by main.js)
card_open: "Open File",
card_download: "Download File",
state_empty: "No documents are available in the repository yet.",
state_error: "Failed to load files. Please check your internet connection or try again later.",
// Procedures
proc_title: "Document Submission Procedure",
proc_tag: "Submission Flow",
proc_s1_h: "Download the Form",
proc_s1_p: "Choose your submission category, then download the latest version of the form. Make sure the revision code on the form matches the one shown on this page.",
proc_s2_h: "Complete the Form",
proc_s2_p: "Fill in every field completely and carefully. Include all required supporting documents before submitting.",
proc_s3_h: "Submit the Document",
proc_s3_p: "Send the form along with the supporting documents via the official email or the department admin's WhatsApp. Complete submissions are processed immediately by the department admin.",
// Help Center
help_title: "Need Further Assistance?",
help_desc: "Our admin team is ready to help you through the official communication channels below.",
help_email_title: "Official Correspondence (Email)",
help_email_desc: "Use this for requests of non-public special forms or other formal administration.",
help_email_c1: "Response within 24 Hours",
help_email_c2: "Formal Service",
help_email_btn: "CONTACT ADMIN",
help_wa_title: "Quick Support (WhatsApp)",
help_wa_desc: "Quick consultation on bureaucratic flow or reporting access issues with repository files.",
help_wa_c1: "Instant Response in Work Hours",
help_wa_c2: "Technical Consultation",
help_wa_btn: "CHAT ON WHATSAPP",
},
};
function currentLang() {
if (typeof window.getLang === 'function') {
const l = window.getLang();
if (l === 'id' || l === 'en') return l;
}
try {
const saved = localStorage.getItem('if_untan_lang');
if (saved === 'id' || saved === 'en') return saved;
} catch (e) { /* localStorage unavailable */ }
const nav = (navigator.language || 'id').toLowerCase();
return nav.startsWith('en') ? 'en' : 'id';
}
function tpT(key) {
const lang = currentLang();
return (dict[lang] && dict[lang][key]) || dict.id[key] || null;
}
function applyTp(root) {
const scope = root || document;
scope.querySelectorAll('[data-tp-i18n]').forEach(el => {
const val = tpT(el.getAttribute('data-tp-i18n'));
if (val != null) el.textContent = val;
});
scope.querySelectorAll('[data-tp-i18n-placeholder]').forEach(el => {
const val = tpT(el.getAttribute('data-tp-i18n-placeholder'));
if (val != null) el.placeholder = val;
});
}
// Expose for main.js (dynamic cards/states).
window.tpT = tpT;
window.applyTp = applyTp;
// Re-translate when the shared toggle switches language.
document.addEventListener('languageChanged', () => applyTp());
// Initial pass (runs after localization.js's own DOMContentLoaded handler,
// so getLang() already reflects the resolved language).
document.addEventListener('DOMContentLoaded', () => applyTp());
})();
+1
View File
@@ -0,0 +1 @@
import './app.js';
@@ -0,0 +1,16 @@
export class PaginationModel {
static range(current, total) {
if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1);
const pages = [1];
const low = Math.max(2, current - 1);
const high = Math.min(total - 1, current + 1);
if (low > 2) pages.push('...');
for (let page = low; page <= high; page += 1) pages.push(page);
if (high < total - 1) pages.push('...');
pages.push(total);
return pages;
}
}
@@ -0,0 +1,31 @@
import { DocumentFormatter } from '../utils/document-formatter.js';
export class DirectusClient {
static API_BASE = 'https://api.ifuntanhub.dev';
static itemsUrl(collection) {
return `${this.API_BASE}/items/${collection}?limit=-1`;
}
static assetUrl(uuid) {
return `${this.API_BASE}/assets/${uuid}`;
}
static viewerUrl(item) {
const type = DocumentFormatter.fileTypeFromName(item.nama_berkas);
const params = new URLSearchParams({
id: item.file,
name: item.nama_berkas,
type,
});
return `viewer.html?${params.toString()}`;
}
static async fetchPublished(collection) {
const response = await fetch(this.itemsUrl(collection));
if (!response.ok) throw new Error(`Gagal memuat koleksi ${collection}.`);
const body = await response.json();
const items = body.data || [];
return items.filter(item => item.status === 'published');
}
}
@@ -0,0 +1,26 @@
export class FileDownloader {
static async download(url, filename) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Gagal mengunduh berkas.');
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
this._click(blobUrl, filename);
URL.revokeObjectURL(blobUrl);
} catch (err) {
console.warn('Unduh via Blob gagal, beralih ke parameter Directus:', err);
const fallbackUrl = url.includes('?') ? `${url}&download` : `${url}?download`;
this._click(fallbackUrl, filename);
}
}
static _click(href, filename) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
@@ -0,0 +1,65 @@
export function footerTemplate() {
return ` <footer class="bg-untan-navy text-white pt-16 relative overflow-hidden mt-0">
<div class="container mx-auto px-4 lg:px-8 relative z-10 flex flex-col items-center justify-center text-center">
<h3 class="text-xs font-bold text-slate-300 uppercase tracking-widest mb-4" data-i18n="footer_follow_us">
Ikuti Kami</h3>
<div class="flex items-center justify-center gap-5 sm:gap-6 mb-10">
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-untan-yellow transition-colors"><i
class="fab fa-instagram text-xl sm:text-2xl"></i></a>
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-untan-yellow transition-colors"><i
class="fab fa-youtube text-xl sm:text-2xl"></i></a>
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-untan-yellow transition-colors"><i
class="fab fa-facebook-f text-xl sm:text-2xl"></i></a>
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-untan-yellow transition-colors"><i
class="fab fa-twitter text-xl sm:text-2xl"></i></a>
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-untan-yellow transition-colors"><i
class="fab fa-linkedin-in text-xl sm:text-2xl"></i></a>
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-untan-yellow transition-colors"><i
class="fab fa-tiktok text-xl sm:text-2xl"></i></a>
</div>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6 mb-8 text-left">
<img src="https://upload.wikimedia.org/wikipedia/id/0/03/Lambang_Universitas_Tanjungpura.png"
class="h-16 sm:h-20 shrink-0" alt="Logo UNTAN">
<div
class="border-t sm:border-t-0 sm:border-l-2 border-white/30 pt-4 sm:pt-0 sm:pl-5 py-1 flex flex-col justify-center leading-tight">
<h2 class="text-2xl sm:text-3xl font-bold font-sans tracking-widest uppercase mb-1">INFORMATIKA</h2>
<h2 class="text-xs sm:text-sm font-normal text-slate-200 tracking-widest uppercase mt-0.5">
UNIVERSITAS TANJUNGPURA</h2>
</div>
</div>
<div class="mb-5 text-sm sm:text-base text-slate-100 max-w-2xl font-light">
<p class="flex flex-col sm:flex-row items-center justify-center gap-2 mb-1">
<i class="fas fa-map-marker-alt text-untan-yellow"></i>
Jl. Prof. Dr. H. Hadari Nawawi, Bansir Laut, Kec. Pontianak Tenggara,
</p>
<p>Kota Pontianak, Kalimantan Barat 78124</p>
</div>
<div
class="text-xs sm:text-sm text-slate-200 flex flex-wrap justify-center items-center gap-x-3 gap-y-2 mb-12 font-light tracking-wide">
<span>E: info@informatika.untan.ac.id</span>
<span class="hidden sm:inline opacity-50">|</span>
<span>P: +62 851-7165-5161</span>
<span class="hidden sm:inline opacity-50">|</span>
<span>F: +62(561) 739637</span>
<span class="hidden sm:inline opacity-50">|</span>
<span>WA: +62 851-7165-5161</span>
</div>
</div>
<div class="w-full relative z-0 mt-4 pointer-events-none flex justify-center">
<img src="../../assets/global/footer.png" alt="Footer Illustration" class="w-full h-auto object-cover object-bottom"
style="object-position: bottom;">
</div>
</footer>`;
}
@@ -0,0 +1,278 @@
export function mainContentTemplate() {
return `<main id="main-content" class="tapops-doc-root pt-[72px] lg:pt-[160px]">
<!-- ═══════════════════════════════════════
HERO
═══════════════════════════════════════ -->
<section class="research-hero immersive-hero">
<div class="hero-container-centered">
<span class="hero-kicker" data-tp-i18n="hero_kicker">Digital Administrative Hub</span>
<h2 class="hero-title" data-tp-i18n="hero_title">Administrasi Akademik Digital</h2>
<p class="hero-tagline" data-tp-i18n="hero_tagline">
Satu pintu menuju semua dokumen penting. Akses terpusat menuju repositori dokumen akademik Program Studi
Informatika Universitas Tanjungpura untuk mahasiswa dan staf.
</p>
<div class="hero-actions">
<a href="#repository" class="btn-join"><span data-tp-i18n="hero_btn_search">Cari Dokumen</span> <i class="fas fa-search"></i></a>
<a href="#procedures" class="btn-outline"><span data-tp-i18n="hero_btn_flow">ALUR PENGAJUAN</span> <i class="fas fa-chevron-right"></i></a>
</div>
</div>
<div class="hero-floating-stat-bar">
<div class="stat-bar-item">
<span class="stat-bar-value" id="stat-active-value">—</span>
<span class="stat-bar-label" data-tp-i18n="stat_active">Dokumen Aktif</span>
</div>
<div class="stat-bar-divider"></div>
<div class="stat-bar-item">
<span class="stat-bar-value" id="stat-updates-value">—</span>
<span class="stat-bar-label" data-tp-i18n="stat_updates">Pembaruan Terbaru</span>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════
DOCUMENT REPOSITORY
═══════════════════════════════════════ -->
<section id="repository" class="documents-section">
<div class="repo-dashboard-container">
<!-- Sidebar Filter -->
<aside class="repo-sidebar">
<div class="sidebar-sticky">
<h3 class="sidebar-title" data-tp-i18n="sidebar_category">KATEGORI BERKAS</h3>
<div class="document-filter-wrapper" id="tag-filter-wrapper">
<!-- Filter buttons are built dynamically from API tags by main.js -->
<div class="filter-skeleton">
<div class="filter-skeleton-item"></div>
<div class="filter-skeleton-item short"></div>
<div class="filter-skeleton-item"></div>
<div class="filter-skeleton-item short"></div>
</div>
</div>
<div class="sidebar-info-box">
<i class="fas fa-info-circle"></i>
<p data-tp-i18n="sidebar_info">Pastikan mengunduh versi terbaru untuk menjamin validitas berkas pengajuan.</p>
</div>
</div>
</aside>
<!-- Main Document Area (div, not main — only one <main> per page) -->
<div class="repo-main-content">
<div class="repo-content-header">
<div class="header-title-area">
<h2 class="section-title" data-tp-i18n="repo_title">Eksplorasi Dokumen Prodi</h2>
<p class="section-subtitle" data-tp-i18n="repo_subtitle">Temukan berkas administrasi dan akademik terbaru Program Studi
Informatika.</p>
</div>
<div class="repo-controls">
<div class="search-container">
<div class="search-input-group">
<i class="fas fa-search search-icon"></i>
<input type="text" id="doc-search" data-tp-i18n-placeholder="search_ph" placeholder="Cari nama berkas atau kata kunci...">
</div>
</div>
<div class="filter-controls">
<div class="control-group date-range-group" id="date-range-control-group">
<span class="control-label" data-tp-i18n="label_period">Periode Unggahan</span>
<button type="button" id="date-picker-trigger" class="date-picker-trigger">
<div class="trigger-left-content">
<i class="far fa-calendar-alt"></i>
<span id="date-picker-label" data-tp-i18n="filter_all_time">Semua Waktu</span>
</div>
<i class="fas fa-chevron-down trigger-arrow"></i>
</button>
<div id="date-picker-backdrop" class="date-picker-backdrop"></div>
<div id="date-picker-popover" class="date-picker-popover">
<div class="date-picker-top-bar">
<div class="date-display-inputs">
<div class="date-display-field">
<input type="text" id="picker-start-display" placeholder="DD/MM/YYYY">
</div>
<span class="date-picker-separator">-</span>
<div class="date-display-field">
<input type="text" id="picker-end-display" placeholder="DD/MM/YYYY">
</div>
</div>
</div>
<div class="date-picker-body">
<div class="calendar-container">
<div class="calendar-nav">
<button type="button" id="prev-month-btn" class="nav-btn"><i class="fas fa-chevron-left"></i></button>
<div class="month-labels">
<div id="left-month-label" class="month-label">
<span id="left-month-name">Mei</span>
<div class="year-select-wrapper">
<select id="left-year-select" class="calendar-year-select"></select>
<i class="fas fa-chevron-down select-arrow"></i>
</div>
</div>
<div id="right-month-label" class="month-label">
<span id="right-month-name">Juni</span>
<div class="year-select-wrapper">
<select id="right-year-select" class="calendar-year-select"></select>
<i class="fas fa-chevron-down select-arrow"></i>
</div>
</div>
</div>
<button type="button" id="next-month-btn" class="nav-btn"><i class="fas fa-chevron-right"></i></button>
</div>
<div class="calendar-grids">
<div class="calendar-grid-pane" id="left-calendar-pane">
<div class="day-headers"></div>
<div class="days-container" id="left-days-container"></div>
</div>
<div class="calendar-grid-pane" id="right-calendar-pane">
<div class="day-headers"></div>
<div class="days-container" id="right-days-container"></div>
</div>
</div>
</div>
<div class="presets-container">
<span class="presets-title" data-tp-i18n="presets_title">RENTANG WAKTU</span>
<div class="presets-list">
<button type="button" class="preset-btn" data-preset="all" data-tp-i18n="filter_all_time">Semua Waktu</button>
<button type="button" class="preset-btn" data-preset="today" data-tp-i18n="preset_today">Hari Ini</button>
<button type="button" class="preset-btn" data-preset="yesterday" data-tp-i18n="preset_yesterday">Kemarin</button>
<button type="button" class="preset-btn" data-preset="last7" data-tp-i18n="preset_last_7_days">7 Hari Terakhir</button>
<button type="button" class="preset-btn" data-preset="last14" data-tp-i18n="preset_last_14_days">14 Hari Terakhir</button>
<button type="button" class="preset-btn" data-preset="last30" data-tp-i18n="preset_last_30_days">30 Hari Terakhir</button>
<button type="button" class="preset-btn" data-preset="thisWeek" data-tp-i18n="preset_this_week">Minggu Ini</button>
<button type="button" class="preset-btn" data-preset="lastWeek" data-tp-i18n="preset_last_week">Minggu Lalu</button>
<button type="button" class="preset-btn" data-preset="thisMonth" data-tp-i18n="preset_this_month">Bulan Ini</button>
<button type="button" class="preset-btn" data-preset="lastMonth" data-tp-i18n="preset_last_month">Bulan Lalu</button>
</div>
</div>
</div>
<div class="date-picker-footer">
<button type="button" id="picker-cancel-btn" class="action-btn cancel-btn" data-tp-i18n="btn_cancel">Batal</button>
<button type="button" id="picker-apply-btn" class="action-btn apply-btn" data-tp-i18n="btn_update">Perbarui</button>
</div>
</div>
</div>
<div class="control-group">
<span class="control-label" data-tp-i18n="label_sort">Urutkan</span>
<div id="sort-filter-wrapper"></div>
</div>
<div class="control-group mobile-category-control-group">
<span class="control-label" data-tp-i18n="sidebar_category">Kategori Berkas</span>
<div id="mobile-tag-filter-wrapper"></div>
</div>
</div>
</div>
</div>
<div class="document-grid" id="document-grid">
<div class="repo-loading" style="grid-column: 1 / -1; text-align: center; padding: 60px 20px; color: var(--primary-blue); font-weight: 700; font-size: 1.1rem; display: flex; flex-direction: column; align-items: center; gap: 15px;">
<i class="fas fa-circle-notch fa-spin" style="font-size: 2.5rem; color: var(--secondary-gold);"></i>
<span data-tp-i18n="loading">Menghubungkan ke repositori berkas...</span>
</div>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════
PROCEDURES
═══════════════════════════════════════ -->
<section id="procedures" class="journey-section">
<div class="journey-container">
<div class="section-header centered">
<h2 class="section-title" data-tp-i18n="proc_title">Tata Cara Pengajuan Berkas</h2>
<span class="section-tag" data-tp-i18n="proc_tag">Alur Pengajuan</span>
</div>
<div class="process-flow-wrapper">
<!-- Step 01 -->
<div class="process-step">
<div class="step-visual">
<div class="step-icon-box"><i class="fas fa-file-download"></i></div>
<div class="step-number-badge">01</div>
<div class="step-connector"></div>
</div>
<div class="step-content">
<h3 data-tp-i18n="proc_s1_h">Unduh Formulir</h3>
<p data-tp-i18n="proc_s1_p">Pilih kategori pengajuan Anda, lalu unduh formulir versi terbaru. Pastikan kode revisi pada formulir sesuai dengan yang tertera di halaman ini.</p>
</div>
</div>
<!-- Step 02 -->
<div class="process-step">
<div class="step-visual">
<div class="step-icon-box"><i class="fas fa-edit"></i></div>
<div class="step-number-badge">02</div>
<div class="step-connector"></div>
</div>
<div class="step-content">
<h3 data-tp-i18n="proc_s2_h">Lengkapi Formulir</h3>
<p data-tp-i18n="proc_s2_p">Isi setiap kolom dengan lengkap dan teliti. Sertakan seluruh dokumen pendukung yang diminta sebelum pengiriman.</p>
</div>
</div>
<!-- Step 03 -->
<div class="process-step">
<div class="step-visual">
<div class="step-icon-box"><i class="fas fa-paper-plane"></i></div>
<div class="step-number-badge">03</div>
</div>
<div class="step-content">
<h3 data-tp-i18n="proc_s3_h">Kirim Berkas</h3>
<p data-tp-i18n="proc_s3_p">Kirimkan formulir beserta dokumen pendukung melalui email resmi atau WhatsApp admin prodi. Berkas yang lengkap akan langsung diproses oleh admin prodi.</p>
</div>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════
HELP CENTER
═══════════════════════════════════════ -->
<section id="help" class="help-center-section">
<div class="help-container">
<div class="help-header-centered">
<h2 class="section-title" data-tp-i18n="help_title">Butuh Bantuan Lebih Lanjut?</h2>
<p data-tp-i18n="help_desc">Tim admin kami siap membantu Anda melalui kanal komunikasi resmi di bawah ini.</p>
</div>
<div class="help-cards-grid">
<div class="interaction-card help-official">
<div class="card-inner">
<div class="card-icon"><i class="fas fa-envelope-open-text"></i></div>
<h3 data-tp-i18n="help_email_title">Korespondensi Resmi (Email)</h3>
<p data-tp-i18n="help_email_desc">Gunakan untuk permohonan form khusus non-publik atau administrasi formal lainnya.</p>
<ul class="card-checklist">
<li><i class="fas fa-check"></i> <span data-tp-i18n="help_email_c1">Respon Maks. 24 Jam</span></li>
<li><i class="fas fa-check"></i> <span data-tp-i18n="help_email_c2">Pelayanan Formal</span></li>
</ul>
<a href="https://mail.google.com/mail/?view=cm&fs=1&to=admin@informatika.untan.ac.id&su=Layanan%20Administrasi%20%26%20CS%20Informatika%20UNTAN&body=Halo%20Admin%20Prodi%20Informatika%20UNTAN%2C%0A%0ASaya%20ingin%20mengajukan%20permohonan%20%2F%20melaporkan%20kendala%20terkait%20administrasi%20akademik.%20Berikut%20detail%20informasi%20saya%3A%0A%0ANama%3A%20%5BNama%20Lengkap%20Anda%5D%0ANIM%3A%20%5BNIM%20Anda%5D%0AKategori%20Layanan%3A%20%5BTata%20Cara%20Pengajuan%20%2F%20Kendala%20Teknis%20%26%20CS%5D%0A%0ADetail%20Masalah%20%2F%20Pengajuan%3A%0A%5BTuliskan%20pertanyaan%2C%20detail%20pengajuan%2C%20atau%20kendala%20Anda%20di%20sini...%5D%0A%0ATerima%20kasih." target="_blank" rel="noopener noreferrer" class="btn-interaction"><span data-tp-i18n="help_email_btn">HUBUNGI ADMIN</span> <i class="fas fa-paper-plane"></i></a>
</div>
</div>
<div class="interaction-card help-urgent">
<div class="card-inner">
<div class="card-icon"><i class="fab fa-whatsapp"></i></div>
<h3 data-tp-i18n="help_wa_title">Quick Support (WhatsApp)</h3>
<p data-tp-i18n="help_wa_desc">Konsultasi cepat alur birokrasi atau pelaporan kendala akses pada berkas repositori.</p>
<ul class="card-checklist">
<li><i class="fas fa-check"></i> <span data-tp-i18n="help_wa_c1">Respon Instan Jam Kerja</span></li>
<li><i class="fas fa-check"></i> <span data-tp-i18n="help_wa_c2">Konsultasi Teknis</span></li>
</ul>
<a href="https://wa.me/6285171655161?text=Halo%20Admin%20Prodi%20Informatika%20UNTAN%2C%0A%0ASaya%20ingin%20mengajukan%20permohonan%20%2F%20melaporkan%20kendala%20terkait%20administrasi%20akademik.%20Berikut%20detail%20informasi%20saya%3A%0A%0ANama%3A%20%5BNama%20Lengkap%20Anda%5D%0ANIM%3A%20%5BNIM%20Anda%5D%0AKategori%20Layanan%3A%20%5BTata%20Cara%20Pengajuan%20%2F%20Kendala%20Teknis%20%26%20CS%5D%0A%0ADetail%20Masalah%20%2F%20Pengajuan%3A%0A%5BTuliskan%20pertanyaan%2C%20detail%20pengajuan%2C%20atau%20kendala%20Anda%20di%20sini...%5D%0A%0ATerima%20kasih." target="_blank" rel="noopener noreferrer" class="btn-interaction-alt"><span data-tp-i18n="help_wa_btn">CHAT WHATSAPP</span> <i class="fab fa-whatsapp"></i></a>
</div>
</div>
</div>
</div>
</section>
</main>`;
}
@@ -0,0 +1,843 @@
export function navigationTemplate() {
return ` <a href="#main-content" class="skip-link" data-i18n="skip_to_content">Langsung ke Konten Utama</a>
<div id="nav-wrapper"
class="fixed top-0 left-0 right-0 z-50 transition-shadow duration-300 shadow-sm border-b border-slate-200 will-change-auto">
<div class="bg-untan-navy text-white py-3 lg:py-4 relative overflow-hidden" style="min-height:0;">
<div class="absolute inset-0 w-full overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute inset-0 opacity-[0.02] font-mono text-[10px] md:text-[12px] text-white select-none leading-snug whitespace-pre-wrap break-all text-right"
style="overflow:hidden;max-height:100%;">
01101001 01101110 01100110 01101111 01110010 01101101 01100001 01110100 01101001
01101011 01100001 00100000 01110101 01101110 01110100 01100001 01101110 00100000
01100010 01101001 01110011 01100001 01101001 01101110 01100110 01101111 01110010
01101101 01100001 01110100 01101001 01101011 01100001 00100000 01110101 01101110
01110100 01100001 01101110 00100000 01100010 01101001 01110011 01100001 01010101
01001110 01010100 01000001 01001110 00100000 00110010 00110000 00110010 00110101
00100000 01100110 01110101 01110100 01110101 01110010 01100101 00100000 01110010
01100101 01100001 01100100 01111001 01101001 01101110 01100110 01101111 01110010
01101101 01100001 01110100 01101001 01101011 01100001 00100000 01110101 01101110
</div>
<div class="absolute inset-0 bg-gradient-to-r from-untan-navy via-untan-navy/80 to-transparent"></div>
</div>
<div class="container mx-auto px-4 lg:px-6 flex justify-between items-center relative z-10">
<a href="../../index.html" class="flex items-center gap-4 group">
<div class="relative w-12 h-12 lg:w-14 lg:h-14 flex items-center justify-center shrink-0">
<img src="https://upload.wikimedia.org/wikipedia/id/0/03/Lambang_Universitas_Tanjungpura.png"
alt="Logo Universitas Tanjungpura - Lambang resmi UNTAN"
class="w-full h-full object-contain transition-transform duration-500 group-hover:scale-105 group-hover:rotate-3 relative z-10">
</div>
<div class="border-l-2 border-white/30 pl-4 lg:pl-5 py-1 flex flex-col justify-center">
<span
class="text-lg lg:text-2xl font-bold tracking-widest text-white leading-none font-sans drop-shadow-sm mb-1 uppercase">INFORMATIKA</span>
<span
class="text-[10px] lg:text-sm font-normal tracking-[0.15em] lg:tracking-[0.3em] text-slate-200 mt-0.5 opacity-90 drop-shadow-sm uppercase font-sans">UNIVERSITAS
TANJUNGPURA</span>
</div>
</a>
<div class="hidden lg:flex items-center gap-5">
<div class="flex items-center gap-3 text-sm">
<a href="https://satu.untan.ac.id" target="_blank"
class="hover:text-untan-yellow transition-colors font-medium">SATU UNTAN</a>
</div>
<div class="pl-5 border-l border-white/20 flex items-center gap-3">
<button
class="language-toggle-btn text-xs hover:text-untan-yellow transition-colors flex items-center gap-1">
<span class="font-bold">ID</span> <span class="text-white/40">|</span> EN
</button>
<button
class="w-8 h-8 flex items-center justify-center rounded-sm hover:bg-white/10 transition-colors"
aria-label="Pengaturan aksesibilitas" data-accessibility-toggle>
<i class="fas fa-universal-access" aria-hidden="true"></i>
</button>
<button
class="w-8 h-8 flex items-center justify-center rounded-sm hover:bg-white/10 transition-colors"
aria-label="Cari konten">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
</div>
</div>
<button id="mobile-menu-button"
class="lg:hidden text-xl text-white w-9 h-9 flex items-center justify-center rounded-sm hover:bg-white/10 transition-colors"
aria-label="Buka menu navigasi" aria-expanded="false" aria-controls="mobile-menu">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
</div>
</div>
<nav class="bg-[#feb401] shadow-md hidden lg:block border-t border-white/20 relative" id="main-nav"
aria-label="Menu navigasi utama">
<div class="container mx-auto px-6">
<div class="flex items-center justify-center w-full gap-1 py-2" role="menubar">
<!-- Menu: Tentang Kami -->
<div class="nav-menu-item relative" data-menu="tentang">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_about">Tentang Kami</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
<!-- Menu: Program -->
<div class="nav-menu-item relative" data-menu="program">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_program">Program</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
<!-- Menu: SDM -->
<div class="nav-menu-item relative" data-menu="sdm">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_sdm">SDM</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
<!-- Menu: Fasilitas -->
<div class="nav-menu-item relative" data-menu="fasilitas">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_facilities">Fasilitas</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
<!-- Menu: Karya & Agenda -->
<div class="nav-menu-item relative" data-menu="karya">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_works">Karya & Agenda</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
<!-- Menu: Kontak -->
<div class="nav-menu-item relative" data-menu="kontak">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_contact">Kontak</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
</div>
</div>
<!-- MEGA MENU PANELS (Inside nav for proper positioning) -->
<div id="mega-menu-container" class="absolute left-0 right-0 top-full z-50">
<!-- Mega Menu: Tentang Kami -->
<div id="mega-tentang" class="mega-panel bg-white border-b-4 border-untan-navy shadow-2xl">
<div class="container mx-auto px-6 py-8">
<div class="grid grid-cols-12 gap-8">
<!-- Left: Featured -->
<div
class="col-span-4 bg-gradient-to-br from-untan-navy to-slate-800 rounded-2xl p-6 text-white relative overflow-hidden">
<div
class="absolute top-0 right-0 w-32 h-32 bg-untan-yellow/10 rounded-full -mr-16 -mt-16">
</div>
<div class="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full -ml-12 -mb-12">
</div>
<div class="relative z-10">
<span
class="inline-block px-3 py-1 bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-wider rounded-full mb-4"
data-i18n="mega_about_title">Tentang Kami</span>
<h3 class="text-2xl font-bold mb-3">Mengenal Informatika UNTAN</h3>
<p class="text-slate-300 text-sm mb-6 leading-relaxed" data-i18n="mega_about_desc">
Terakreditasi Unggul, mencetak lulusan kompeten di bidang teknologi informasi
sejak 1993.</p>
<a href="content.html?type=berita"
class="inline-flex items-center gap-2 text-untan-yellow font-semibold text-sm hover:gap-3 transition-all">
Selengkapnya <i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
<!-- Right: Links -->
<div class="col-span-8 grid grid-cols-2 gap-6">
<a href="content.html?type=berita" class="mega-link-card group">
<div
class="mega-link-icon bg-amber-50 text-amber-600 group-hover:bg-amber-600 group-hover:text-white">
<i class="fas fa-landmark"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue transition-colors"
data-i18n="link_history">Sejarah & Tonggak</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_history_desc">Perjalanan
panjang membangun generasi digital</p>
</div>
</a>
<a href="content.html?type=berita" class="mega-link-card group">
<div
class="mega-link-icon bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white">
<i class="fas fa-users"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue transition-colors"
data-i18n="link_org">Struktur Organisasi</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_org_desc">Pimpinan dan
jajaran pengelola</p>
</div>
</a>
<a href="profil.html" class="mega-link-card group">
<div
class="mega-link-icon bg-emerald-50 text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white">
<i class="fas fa-bullseye"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue transition-colors"
data-i18n="link_vision">Visi & Misi</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_vision_desc">Arah dan
tujuan pengembangan prodi</p>
</div>
</a>
<a href="content.html?type=berita" class="mega-link-card group">
<div
class="mega-link-icon bg-purple-50 text-purple-600 group-hover:bg-purple-600 group-hover:text-white">
<i class="fas fa-handshake"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue transition-colors"
data-i18n="link_partnership">Kerjasama & MoU</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_partnership_desc">Mitra
industri dan akademik</p>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Mega Menu: Program -->
<div id="mega-program" class="mega-panel bg-white border-b-4 border-untan-navy shadow-2xl">
<div class="container mx-auto px-6 py-8">
<div class="grid grid-cols-12 gap-8">
<!-- Left: Featured -->
<div
class="col-span-4 bg-gradient-to-br from-blue-600 to-indigo-700 rounded-2xl p-6 text-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-40 h-40 bg-white/10 rounded-full -mr-20 -mt-20">
</div>
<div class="relative z-10">
<span
class="inline-block px-3 py-1 bg-white/20 text-white text-[10px] font-bold uppercase tracking-wider rounded-full mb-4">Program
Akademik</span>
<h3 class="text-2xl font-bold mb-3" data-i18n="mega_program_obe">Kurikulum Berbasis
OBE</h3>
<p class="text-blue-100 text-sm mb-6 leading-relaxed">Outcome-Based Education dengan
4 jalur konsentrasi: AI, Software Engineering, Network, dan Information Systems.
</p>
<a href="subject-and-courses.html"
class="inline-flex items-center gap-2 bg-white text-blue-600 px-4 py-2 rounded-lg font-semibold text-sm hover:bg-blue-50 transition-all"
data-i18n="btn_krs_sim">
<i class="fas fa-play-circle"></i> Simulasi KRS
</a>
</div>
</div>
<!-- Right: Links Grid -->
<div class="col-span-8">
<div class="grid grid-cols-3 gap-4">
<a href="subject-and-courses.html" class="mega-link-card group">
<div
class="mega-link-icon bg-indigo-50 text-indigo-600 group-hover:bg-indigo-600 group-hover:text-white">
<i class="fas fa-graduation-cap"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_academic">Akademik</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_academic_desc">
Jadwal, KRS, dan kalender</p>
</div>
</a>
<a href="content.html?type=berita" class="mega-link-card group">
<div
class="mega-link-icon bg-yellow-50 text-yellow-600 group-hover:bg-yellow-600 group-hover:text-white">
<i class="fas fa-award"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_accreditation">Akreditasi</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_accred_desc">Status
Unggul BAN-PT</p>
</div>
</a>
<a href="graduate-profile.html" class="mega-link-card group">
<div
class="mega-link-icon bg-green-50 text-green-600 group-hover:bg-green-600 group-hover:text-white">
<i class="fas fa-user-graduate"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_graduate">Profil Lulusan</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_graduate_desc">
Kompetensi & karir</p>
</div>
</a>
<a href="subject-and-courses.html" class="mega-link-card group">
<div
class="mega-link-icon bg-rose-50 text-rose-600 group-hover:bg-rose-600 group-hover:text-white">
<i class="fas fa-book-open"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_courses">Mata Kuliah</h4>
<p class="text-xs text-slate-500 mt-1">144 SKS kurikulum</p>
</div>
</a>
<a href="academic-guidelines.html" class="mega-link-card group">
<div
class="mega-link-icon bg-cyan-50 text-cyan-600 group-hover:bg-cyan-600 group-hover:text-white">
<i class="fas fa-file-alt"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_guideline">Panduan</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_guideline_desc">Buku
panduan akademik</p>
</div>
</a>
<a href="content.html?type=berita" class="mega-link-card group">
<div
class="mega-link-icon bg-orange-50 text-orange-600 group-hover:bg-orange-600 group-hover:text-white">
<i class="fas fa-chart-bar"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_statistics">Informatika Angka</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_statistics_desc">
Statistik & data</p>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Mega Menu: SDM -->
<div id="mega-sdm" class="mega-panel bg-white border-b-4 border-untan-navy shadow-2xl">
<div class="container mx-auto px-6 py-8">
<div class="grid grid-cols-12 gap-8">
<!-- Left: Featured Dosen -->
<div
class="col-span-5 bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl p-6 text-white relative overflow-hidden">
<div class="absolute inset-0 opacity-10">
<div class="absolute top-4 right-4 w-20 h-20 border-2 border-white rounded-full">
</div>
<div class="absolute bottom-4 left-4 w-16 h-16 border-2 border-white rounded-full">
</div>
</div>
<div class="relative z-10">
<span
class="inline-block px-3 py-1 bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-wider rounded-full mb-4"
data-i18n="mega_sdm_title">Sumber Daya Manusia</span>
<h3 class="text-2xl font-bold mb-3" data-i18n="mega_sdm_desc">Tim Pengajar
Profesional</h3>
<p class="text-slate-300 text-sm mb-4 leading-relaxed">30+ dosen berkualifikasi
S2/S3 dari universitas terkemuka dalam dan luar negeri.</p>
<div class="flex gap-4 mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-untan-yellow">15+</div>
<div class="text-[10px] text-slate-400 uppercase">Doktor</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-untan-yellow">20+</div>
<div class="text-[10px] text-slate-400 uppercase">Magister</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-untan-yellow">5</div>
<div class="text-[10px] text-slate-400 uppercase">Guru Besar</div>
</div>
</div>
<a href="people-based-on-kk.html"
class="inline-flex items-center gap-2 text-untan-yellow font-semibold text-sm hover:gap-3 transition-all"
data-i18n="btn_all_lecturers">
Lihat Semua Dosen <i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
<!-- Right: KK Groups -->
<div class="col-span-7">
<h4 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4"
data-i18n="label_expertise">Kelompok Keahlian</h4>
<div class="grid grid-cols-2 gap-4">
<a href="people-based-on-kk.html"
class="group bg-gradient-to-r from-purple-50 to-white border border-purple-100 rounded-xl p-5 hover:shadow-lg hover:border-purple-300 transition-all">
<div class="flex items-start gap-4">
<div
class="w-12 h-12 bg-purple-600 rounded-xl flex items-center justify-center text-white text-xl group-hover:scale-110 transition-transform">
<i class="fas fa-brain"></i>
</div>
<div>
<h5 class="font-bold text-slate-800 mb-1">KK Kecerdasan Buatan</h5>
<p class="text-xs text-slate-500">Machine Learning, Computer Vision, NLP
</p>
<span class="inline-block mt-2 text-xs text-purple-600 font-semibold">8
Dosen <i class="fas fa-chevron-right ml-1"></i></span>
</div>
</div>
</a>
<a href="people-based-on-kk.html"
class="group bg-gradient-to-r from-blue-50 to-white border border-blue-100 rounded-xl p-5 hover:shadow-lg hover:border-blue-300 transition-all">
<div class="flex items-start gap-4">
<div
class="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center text-white text-xl group-hover:scale-110 transition-transform">
<i class="fas fa-code"></i>
</div>
<div>
<h5 class="font-bold text-slate-800 mb-1">KK Rekayasa Perangkat Lunak
</h5>
<p class="text-xs text-slate-500">Software Engineering, DevOps, Testing
</p>
<span class="inline-block mt-2 text-xs text-blue-600 font-semibold">10
Dosen <i class="fas fa-chevron-right ml-1"></i></span>
</div>
</div>
</a>
<a href="people-based-on-kk.html"
class="group bg-gradient-to-r from-emerald-50 to-white border border-emerald-100 rounded-xl p-5 hover:shadow-lg hover:border-emerald-300 transition-all">
<div class="flex items-start gap-4">
<div
class="w-12 h-12 bg-emerald-600 rounded-xl flex items-center justify-center text-white text-xl group-hover:scale-110 transition-transform">
<i class="fas fa-network-wired"></i>
</div>
<div>
<h5 class="font-bold text-slate-800 mb-1">KK Jaringan & Keamanan</h5>
<p class="text-xs text-slate-500">Network, Cybersecurity, Cloud</p>
<span class="inline-block mt-2 text-xs text-emerald-600 font-semibold">7
Dosen <i class="fas fa-chevron-right ml-1"></i></span>
</div>
</div>
</a>
<a href="people-based-on-kk.html"
class="group bg-gradient-to-r from-orange-50 to-white border border-orange-100 rounded-xl p-5 hover:shadow-lg hover:border-orange-300 transition-all">
<div class="flex items-start gap-4">
<div
class="w-12 h-12 bg-orange-600 rounded-xl flex items-center justify-center text-white text-xl group-hover:scale-110 transition-transform">
<i class="fas fa-database"></i>
</div>
<div>
<h5 class="font-bold text-slate-800 mb-1">KK Sistem Informasi</h5>
<p class="text-xs text-slate-500">Data Science, GIS, Business Intel</p>
<span class="inline-block mt-2 text-xs text-orange-600 font-semibold">6
Dosen <i class="fas fa-chevron-right ml-1"></i></span>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Mega Menu: Fasilitas -->
<div id="mega-fasilitas" class="mega-panel bg-white border-b-4 border-untan-navy shadow-2xl">
<div class="container mx-auto px-6 py-8">
<div class="grid grid-cols-4 gap-6">
<a href="content.html?type=rubrik"
class="group relative bg-gradient-to-br from-cyan-500 to-blue-600 rounded-2xl p-6 text-white overflow-hidden hover:shadow-xl transition-all">
<div class="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12">
</div>
<i class="fas fa-flask text-4xl mb-4 opacity-80"></i>
<h4 class="text-lg font-bold mb-2" data-i18n="mega_lab">Laboratorium</h4>
<p class="text-sm text-cyan-100 mb-4" data-i18n="mega_lab_desc">8 lab modern dengan
peralatan terkini</p>
<span
class="inline-flex items-center gap-1 text-xs font-semibold opacity-80 group-hover:opacity-100 group-hover:gap-2 transition-all"
data-i18n="btn_explore">
Jelajahi <i class="fas fa-arrow-right"></i>
</span>
</a>
<a href="content.html?type=rubrik"
class="group relative bg-gradient-to-br from-violet-500 to-purple-600 rounded-2xl p-6 text-white overflow-hidden hover:shadow-xl transition-all">
<div class="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12">
</div>
<i class="fas fa-server text-4xl mb-4 opacity-80"></i>
<h4 class="text-lg font-bold mb-2" data-i18n="mega_dc">Data Center</h4>
<p class="text-sm text-violet-100 mb-4" data-i18n="mega_dc_desc">Server & infrastruktur
cloud lokal</p>
<span
class="inline-flex items-center gap-1 text-xs font-semibold opacity-80 group-hover:opacity-100 group-hover:gap-2 transition-all"
data-i18n="btn_explore">
Jelajahi <i class="fas fa-arrow-right"></i>
</span>
</a>
<a href="content.html?type=rubrik"
class="group relative bg-gradient-to-br from-emerald-500 to-teal-600 rounded-2xl p-6 text-white overflow-hidden hover:shadow-xl transition-all">
<div class="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12">
</div>
<i class="fas fa-book-reader text-4xl mb-4 opacity-80"></i>
<h4 class="text-lg font-bold mb-2" data-i18n="mega_eresources">E-Resources</h4>
<p class="text-sm text-emerald-100 mb-4" data-i18n="mega_eresources_desc">Akses jurnal &
database ilmiah</p>
<span
class="inline-flex items-center gap-1 text-xs font-semibold opacity-80 group-hover:opacity-100 group-hover:gap-2 transition-all"
data-i18n="btn_explore">
Jelajahi <i class="fas fa-arrow-right"></i>
</span>
</a>
<a href="ifuntanhub.html"
class="group relative bg-gradient-to-br from-slate-700 to-slate-900 rounded-2xl p-6 text-white overflow-hidden hover:shadow-xl transition-all">
<div
class="absolute top-0 right-0 w-24 h-24 bg-untan-yellow/20 rounded-full -mr-12 -mt-12">
</div>
<i class="fas fa-cloud text-4xl mb-4 opacity-80"></i>
<h4 class="text-lg font-bold mb-2">IFUNTANHUB</h4>
<p class="text-sm text-slate-300 mb-4" data-i18n="mega_hub_desc">Platform PaaS untuk
mahasiswa</p>
<span
class="inline-flex items-center gap-1 text-xs font-semibold text-untan-yellow group-hover:gap-2 transition-all">
Launch <i class="fas fa-external-link-alt"></i>
</span>
</a>
</div>
</div>
</div>
<!-- Mega Menu: Karya & Agenda -->
<div id="mega-karya" class="mega-panel bg-white border-b-4 border-untan-navy shadow-2xl">
<div class="container mx-auto px-6 py-8">
<div class="grid grid-cols-12 gap-8">
<!-- Left: Research Highlight -->
<div class="col-span-5">
<div
class="bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl p-6 text-white mb-4">
<span
class="inline-block px-3 py-1 bg-white/20 text-white text-[10px] font-bold uppercase tracking-wider rounded-full mb-3"
data-i18n="mega_research_title">Riset Unggulan</span>
<h3 class="text-xl font-bold mb-2" data-i18n="mega_sinta_title">Indeks SINTA Terbaik
</h3>
<p class="text-amber-100 text-sm mb-4" data-i18n="mega_sinta_desc">Publikasi dosen
terindeks Scopus, WoS, dan jurnal nasional terakreditasi.</p>
<a href="content.html?type=berita"
class="inline-flex items-center gap-2 bg-white text-orange-600 px-4 py-2 rounded-lg font-semibold text-sm hover:bg-orange-50 transition-all"
data-i18n="btn_publications">
Lihat Publikasi
</a>
</div>
<a href="content.html?type=berita"
class="block bg-slate-50 border border-slate-200 rounded-xl p-4 hover:bg-slate-100 hover:border-slate-300 transition-all">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center text-red-600">
<i class="fas fa-calendar-alt"></i>
</div>
<div>
<h5 class="font-bold text-slate-800 text-sm" data-i18n="link_info_agenda">
Info & Agenda</h5>
<p class="text-xs text-slate-500" data-i18n="link_info_agenda_desc">
Pengumuman dan jadwal kegiatan</p>
</div>
<i class="fas fa-chevron-right text-slate-400 ml-auto"></i>
</div>
</a>
</div>
<!-- Right: Grid -->
<div class="col-span-7 grid grid-cols-2 gap-4">
<a href="karya.html" class="mega-link-card group">
<div
class="mega-link-icon bg-indigo-50 text-indigo-600 group-hover:bg-indigo-600 group-hover:text-white">
<i class="fas fa-layer-group"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue">Semua Karya
</h4>
<p class="text-xs text-slate-500 mt-1">Lihat seluruh karya dalam satu halaman
</p>
</div>
</a>
<a href="karya.html?filter=penelitian" class="mega-link-card group">
<div
class="mega-link-icon bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white">
<i class="fas fa-microscope"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_research">Riset</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_research_desc">Penelitian
dosen dan mahasiswa</p>
</div>
</a>
<a href="karya.html?filter=pkm" class="mega-link-card group">
<div
class="mega-link-icon bg-green-50 text-green-600 group-hover:bg-green-600 group-hover:text-white">
<i class="fas fa-hands-helping"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_community">Pengabdian</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_community_desc">Program
pengabdian masyarakat</p>
</div>
</a>
<a href="content.html?type=rubrik" class="mega-link-card group">
<div
class="mega-link-icon bg-amber-50 text-amber-600 group-hover:bg-amber-600 group-hover:text-white">
<i class="fas fa-trophy"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_achievements">Prestasi</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_achievements_desc">
Pencapaian dosen & mahasiswa</p>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Mega Menu: Kontak -->
<div id="mega-kontak" class="mega-panel bg-white border-b-4 border-untan-navy shadow-2xl">
<div class="container mx-auto px-6 py-8">
<div class="grid grid-cols-12 gap-8">
<!-- Left: Contact Info -->
<div class="col-span-5 bg-untan-navy rounded-2xl p-6 text-white">
<h3 class="text-xl font-bold mb-4" data-i18n="mega_contact_title">Hubungi Kami</h3>
<div class="space-y-4">
<div class="flex items-start gap-3">
<div
class="w-10 h-10 bg-white/10 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-map-marker-alt text-untan-yellow"></i>
</div>
<div>
<p class="text-sm text-slate-300">Jl. Prof. Dr. H. Hadari Nawawi, Bansir
Laut, Pontianak Tenggara</p>
<p class="text-xs text-slate-400 mt-1">Kalimantan Barat 78124</p>
</div>
</div>
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-white/10 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fab fa-whatsapp text-untan-yellow"></i>
</div>
<div>
<p class="text-sm text-white font-medium">+62 851-7165-5161</p>
<p class="text-xs text-slate-400">Senin - Jumat, 08:00 - 16:00</p>
</div>
</div>
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-white/10 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-envelope text-untan-yellow"></i>
</div>
<div>
<p class="text-sm text-white font-medium">info@informatika.untan.ac.id</p>
</div>
</div>
</div>
</div>
<!-- Right: Quick Links & Social -->
<div class="col-span-7">
<div class="grid grid-cols-2 gap-4 mb-6">
<a href="content.html?type=berita" class="mega-link-card group">
<div
class="mega-link-icon bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white">
<i class="fas fa-id-card"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_official_contact">Kontak Resmi</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_contact_desc">
Direktori kontak staf</p>
</div>
</a>
<a href="https://mail.google.com/mail/?view=cm&fs=1&to=info@informatika.untan.ac.id" target="_blank" rel="noopener noreferrer" class="mega-link-card group">
<div
class="mega-link-icon bg-green-50 text-green-600 group-hover:bg-green-600 group-hover:text-white">
<i class="fas fa-comment-dots"></i>
</div>
<div>
<h4 class="font-bold text-slate-800 group-hover:text-untan-blue"
data-i18n="link_feedback">Saran & Masukan</h4>
<p class="text-xs text-slate-500 mt-1" data-i18n="link_feedback_desc">Kirim
feedback Anda</p>
</div>
</a>
</div>
<div class="bg-slate-50 rounded-xl p-5">
<h4 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4"
data-i18n="follow_us">Ikuti Kami</h4>
<div class="flex gap-3">
<a href="https://instagram.com/informatika.untan" target="_blank"
class="w-12 h-12 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center text-white hover:scale-110 transition-transform">
<i class="fab fa-instagram text-xl"></i>
</a>
<a href="#"
class="w-12 h-12 bg-gradient-to-br from-red-500 to-red-600 rounded-xl flex items-center justify-center text-white hover:scale-110 transition-transform">
<i class="fab fa-youtube text-xl"></i>
</a>
<a href="#"
class="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white hover:scale-110 transition-transform">
<i class="fab fa-facebook-f text-xl"></i>
</a>
<a href="#"
class="w-12 h-12 bg-gradient-to-br from-sky-400 to-sky-500 rounded-xl flex items-center justify-center text-white hover:scale-110 transition-transform">
<i class="fab fa-twitter text-xl"></i>
</a>
<a href="#"
class="w-12 h-12 bg-gradient-to-br from-blue-600 to-blue-700 rounded-xl flex items-center justify-center text-white hover:scale-110 transition-transform">
<i class="fab fa-linkedin-in text-xl"></i>
</a>
<a href="#"
class="w-12 h-12 bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl flex items-center justify-center text-white hover:scale-110 transition-transform">
<i class="fab fa-tiktok text-xl"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- End mega-menu-container -->
</nav>
<!-- Overlay for mega menu -->
<div id="mega-overlay" class="fixed inset-0 bg-black/30 backdrop-blur-sm z-30"></div>
</div>
<div id="mobile-menu"
class="fixed inset-0 bg-slate-900 z-[60] transform -translate-x-full lg:hidden flex flex-col transition-transform duration-300"
role="dialog" aria-modal="true" aria-label="Menu navigasi mobile">
<div class="p-4 flex justify-between items-center border-b border-white/10 bg-untan-navy">
<div class="flex items-center gap-4">
<span class="font-bold text-lg text-white font-serif">Menu Utama</span>
<button
class="language-toggle-btn px-2 py-1 rounded-sm border border-white/20 text-xs text-white hover:bg-white/10 transition-colors flex items-center gap-2"
aria-label="Ganti bahasa">
<span class="font-bold">ID</span> <span class="text-white/40" aria-hidden="true">|</span> EN
</button>
<button
class="w-8 h-8 rounded-sm bg-white/10 hover:bg-white/20 flex items-center justify-center text-white transition-colors"
aria-label="Pengaturan aksesibilitas" data-accessibility-toggle>
<i class="fas fa-universal-access" aria-hidden="true"></i>
</button>
</div>
<button id="close-mobile-menu"
class="w-8 h-8 rounded-sm bg-white/10 flex items-center justify-center text-white"
aria-label="Tutup menu navigasi">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto p-5 space-y-4 bg-slate-50">
<a href="../../index.html" class="block text-base font-bold text-slate-800 py-1 border-b border-slate-200"
data-i18n="nav_home">Beranda</a>
<!-- Tentang Kami -->
<div class="pt-2">
<span class="text-xs font-bold text-slate-400 uppercase font-serif" data-i18n="nav_about">Tentang
Kami</span>
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_history">Sejarah & Tonggak</a>
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_org">Struktur Organisasi</a>
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_vision">Visi & Misi</a>
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_partnership">Kerjasama & MoU</a>
</div>
</div>
<!-- Program -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif"
data-i18n="nav_program">Program</span>
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
<a href="subject-and-courses.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_academic">Akademik</a>
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_accreditation">Akreditasi</a>
<a href="graduate-profile.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_graduate">Profil Lulusan</a>
<a href="subject-and-courses.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_courses">Mata Kuliah</a>
<a href="academic-guidelines.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_guideline">Panduan Akademik</a>
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_statistics">Informatika Angka</a>
</div>
</div>
<!-- SDM -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif" data-i18n="nav_sdm">SDM</span>
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
<a href="people-based-on-kk.html"
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue font-medium"
data-i18n="btn_all_lecturers">Semua Dosen</a>
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="kk_ai">Kecerdasan Buatan</a>
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="kk_se">Rekayasa Perangkat Lunak</a>
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="kk_network">Jaringan & Keamanan</a>
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="kk_is">Sistem Informasi</a>
</div>
</div>
<!-- Fasilitas -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif"
data-i18n="nav_facilities">Fasilitas</span>
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="mega_lab">Laboratorium</a>
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="mega_dc">Data Center</a>
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="mega_eresources">E-Resources</a>
<a href="ifuntanhub.html"
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue">IFUNTANHUB</a>
</div>
</div>
<!-- Karya & Agenda -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif" data-i18n="nav_works">Karya &
Agenda</span>
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
<a href="karya.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_all_karya">Semua Karya</a>
<a href="karya.html?filter=penelitian"
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_research">Riset</a>
<a href="karya.html?filter=pkm" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_community">Pengabdian Masyarakat</a>
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_achievements">Prestasi</a>
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_info_agenda">Info & Agenda</a>
</div>
</div>
<!-- Kontak -->
<div class="pt-2 border-t border-slate-200 pb-8">
<span class="text-xs font-bold text-slate-400 uppercase font-serif"
data-i18n="nav_contact">Kontak</span>
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_official_contact">Kontak Resmi</a>
<a href="https://mail.google.com/mail/?view=cm&fs=1&to=info@informatika.untan.ac.id" target="_blank" rel="noopener noreferrer"
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
data-i18n="link_feedback">Saran & Masukan</a>
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue flex items-center gap-2">
<i class="fab fa-instagram text-pink-500"></i> Instagram
</a>
<a href="#"
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue flex items-center gap-2">
<i class="fab fa-youtube text-red-500"></i> YouTube
</a>
</div>
</div>
</div>
</div>`;
}
@@ -0,0 +1,7 @@
import { navigationTemplate } from './navigation-template.js';
import { mainContentTemplate } from './main-content-template.js';
import { footerTemplate } from './footer-template.js';
export function pageTemplate() {
return [navigationTemplate(), mainContentTemplate(), footerTemplate()].join('');
}
@@ -0,0 +1,112 @@
export class DateRangeCalculator {
static clearTime(date) {
if (!date) return null;
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
static datesEqual(left, right) {
if (!left || !right) return false;
return left.getFullYear() === right.getFullYear()
&& left.getMonth() === right.getMonth()
&& left.getDate() === right.getDate();
}
static calculatePreset(presetName, todayInput = new Date()) {
const today = this.clearTime(todayInput);
let start = null;
let end = null;
switch (presetName) {
case 'today':
start = new Date(today);
end = new Date(today);
break;
case 'yesterday':
start = new Date(today);
start.setDate(today.getDate() - 1);
end = new Date(start);
break;
case 'last7':
end = new Date(today);
start = new Date(today);
start.setDate(today.getDate() - 6);
break;
case 'last14':
end = new Date(today);
start = new Date(today);
start.setDate(today.getDate() - 13);
break;
case 'last30':
end = new Date(today);
start = new Date(today);
start.setDate(today.getDate() - 29);
break;
case 'thisWeek': {
end = new Date(today);
start = new Date(today);
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1);
start.setDate(diff);
break;
}
case 'lastWeek': {
const temp = new Date(today);
const day = temp.getDay();
const diff = temp.getDate() - day + (day === 0 ? -6 : 1);
start = new Date(temp);
start.setDate(diff - 7);
end = new Date(temp);
end.setDate(diff - 1);
break;
}
case 'thisMonth':
start = new Date(today.getFullYear(), today.getMonth(), 1);
end = new Date(today);
break;
case 'lastMonth':
start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
end = new Date(today.getFullYear(), today.getMonth(), 0);
break;
case 'all':
default:
start = null;
end = null;
break;
}
return { start, end };
}
static formatDisplay(date) {
if (!date) return '';
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
return `${day}/${month}/${d.getFullYear()}`;
}
static parseDisplayDate(value) {
const match = String(value || '').trim().match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/);
if (!match) return null;
const day = Number.parseInt(match[1], 10);
const month = Number.parseInt(match[2], 10) - 1;
const year = Number.parseInt(match[3], 10);
if (month < 0 || month > 11 || day < 1 || day > 31 || year < 1000 || year > 9999) return null;
const date = new Date(year, month, day);
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
return date;
}
static toDateKey(date) {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
}
@@ -0,0 +1,71 @@
export class DocumentFormatter {
static MONTHS_ID = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
static CATEGORY_RULES = [
{ key: 'skripsi', match: ['skripsi', 'tugas akhir', 'ta'] },
{ key: 'akademik', match: ['akademik', 'kalender', 'kaldik'] },
{ key: 'praktik', match: ['praktik', 'praktek', 'kerja praktik', 'kp'] },
];
static formatDate(iso) {
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = this.MONTHS_ID[date.getMonth()];
return `${day} ${month} ${date.getFullYear()}`;
}
static categoryFromTags(tags) {
if (!tags || tags.length === 0) return 'umum';
const lower = tags.map(tag => tag.toLowerCase());
for (const rule of this.CATEGORY_RULES) {
if (rule.match.some(match => lower.includes(match))) return rule.key;
}
return 'umum';
}
static fileTypeFromName(name) {
const lower = (name || '').toLowerCase();
if (lower.endsWith('.docx') || lower.endsWith('.doc')) return 'docx';
return 'pdf';
}
static downloadFilename(name, type) {
const ext = `.${type}`;
return name.toLowerCase().endsWith(ext) ? name : `${name}${ext}`;
}
static escapeHtml(str) {
if (str == null) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
static escapeAttr(str) {
return this.escapeHtml(str);
}
static parseTags(raw) {
if (!raw) return [];
const str = String(raw).trim();
if (!str) return [];
if (str.startsWith('[')) {
try {
const arr = JSON.parse(str);
if (Array.isArray(arr)) {
return arr.map(tag => String(tag).trim()).filter(Boolean);
}
} catch {
// Fall through to comma-separated parsing.
}
}
return str.split(',').map(tag => tag.trim()).filter(Boolean);
}
}
+1
View File
@@ -0,0 +1 @@
import './viewer/viewer-app.js';
+130
View File
@@ -0,0 +1,130 @@
import { ScriptLoader } from './script-loader.js';
import { ViewerApi } from './viewer-api.js';
import { ViewerProgressBar } from './viewer-progress-bar.js';
export class FileViewer {
constructor() {
const params = new URLSearchParams(window.location.search);
this.id = params.get('id');
this.name = params.get('name') || 'Dokumen';
this.type = (params.get('type') || 'pdf').toLowerCase();
this.titleEl = document.getElementById('viewer-title');
this.typeBadgeEl = document.getElementById('viewer-type');
this.downloadEl = document.getElementById('viewer-download');
this.contentEl = document.getElementById('viewer-content');
this.assetUrl = this.id ? ViewerApi.assetUrl(this.id) : null;
}
async init() {
if (!this.id) {
this._renderError('Parameter berkas tidak ditemukan di URL.');
return;
}
document.title = `${this.name} — Pratinjau Dokumen`;
this.titleEl.textContent = this.name;
this.typeBadgeEl.textContent = this.type.toUpperCase();
this._setupDownload();
try {
if (this.type === 'docx') {
await this._renderDocx();
} else {
this._renderPdf();
}
} catch (err) {
console.error(err);
this._renderError('Gagal memuat pratinjau berkas. Silakan unduh berkas untuk membuka secara lokal.');
}
}
_setupDownload() {
const ext = `.${this.type}`;
const filename = this.name.toLowerCase().endsWith(ext) ? this.name : `${this.name}${ext}`;
this.downloadEl.removeAttribute('href');
this.downloadEl.style.cursor = 'pointer';
this.downloadEl.addEventListener('click', async event => {
event.preventDefault();
const originalHtml = this.downloadEl.innerHTML;
this.downloadEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Mengunduh…';
this.downloadEl.style.pointerEvents = 'none';
try {
const blob = await ViewerApi.fetchBlob(this.assetUrl);
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
} catch (err) {
console.warn('Blob download failed, falling back:', err);
window.open(`${this.assetUrl}?download`, '_blank');
} finally {
this.downloadEl.innerHTML = originalHtml;
this.downloadEl.style.pointerEvents = '';
}
});
}
_renderPdf() {
const iframe = document.createElement('iframe');
iframe.className = 'viewer-pdf-frame';
iframe.title = this.name;
iframe.src = this.assetUrl;
this.contentEl.replaceChildren(iframe);
}
async _renderDocx() {
const progressBar = new ViewerProgressBar(this.contentEl);
const [blob] = await Promise.all([
ViewerApi.fetchBlob(this.assetUrl, ratio => progressBar.update(ratio)),
this._loadDocxLibraries(),
]);
progressBar.finish();
if (!window.docx || typeof window.docx.renderAsync !== 'function') {
throw new Error('Library docx-preview belum dimuat.');
}
const container = document.createElement('div');
container.className = 'viewer-docx-content';
this.contentEl.replaceChildren(container);
await window.docx.renderAsync(blob, container, null, {
inWrapper: true,
ignoreWidth: false,
ignoreHeight: false,
});
}
async _loadDocxLibraries() {
if (window.docx) return;
await ScriptLoader.load('https://unpkg.com/jszip@3.10.1/dist/jszip.min.js');
await ScriptLoader.load('https://unpkg.com/docx-preview@0.3.5/dist/docx-preview.js');
}
_renderError(message) {
const wrapper = document.createElement('div');
wrapper.className = 'viewer-error';
const icon = document.createElement('i');
icon.className = 'fas fa-exclamation-triangle';
const span = document.createElement('span');
span.textContent = message;
const back = document.createElement('a');
back.href = 'index.html#repository';
back.className = 'btn-back-mini';
back.textContent = 'Kembali ke Repositori';
wrapper.append(icon, span, back);
this.contentEl.replaceChildren(wrapper);
}
}
+11
View File
@@ -0,0 +1,11 @@
export class ScriptLoader {
static load(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Gagal memuat skrip: ${src}`));
document.head.appendChild(script);
});
}
}
+31
View File
@@ -0,0 +1,31 @@
export class ViewerApi {
static API_BASE = 'https://api.ifuntanhub.dev';
static assetUrl(uuid) {
return `${this.API_BASE}/assets/${uuid}`;
}
static async fetchBlob(url, onProgress) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Gagal mengambil berkas (HTTP ${response.status}).`);
const contentLength = response.headers.get('Content-Length');
if (onProgress && contentLength && response.body) {
const total = Number.parseInt(contentLength, 10);
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
onProgress(received / total);
}
return new Blob(chunks);
}
return response.blob();
}
}
+8
View File
@@ -0,0 +1,8 @@
import { FileViewer } from './file-viewer.js';
import { ViewerPrefetch } from './viewer-prefetch.js';
ViewerPrefetch.fromLocation();
document.addEventListener('DOMContentLoaded', () => {
new FileViewer().init();
});
@@ -0,0 +1,16 @@
import { ViewerApi } from './viewer-api.js';
export class ViewerPrefetch {
static fromLocation(location = window.location) {
const params = new URLSearchParams(location.search);
const id = params.get('id');
if (!id) return;
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = 'fetch';
link.crossOrigin = 'anonymous';
link.href = ViewerApi.assetUrl(id);
document.head.appendChild(link);
}
}
@@ -0,0 +1,19 @@
export class ViewerProgressBar {
constructor(contentEl) {
this.bar = document.createElement('div');
this.bar.className = 'viewer-progress-bar';
this.fill = document.createElement('div');
this.fill.className = 'viewer-progress-fill';
this.bar.appendChild(this.fill);
contentEl.prepend(this.bar);
}
update(ratio) {
this.fill.style.width = `${Math.min(Math.round(ratio * 100), 100)}%`;
}
finish() {
this.fill.style.width = '100%';
setTimeout(() => this.bar.remove(), 300);
}
}
@@ -0,0 +1,37 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { DateRangeCalculator } from '../js/utils/date-range-calculator.js';
import { PaginationModel } from '../js/models/pagination-model.js';
test('calculates date presets using an injected current day', () => {
const today = new Date('2026-06-04T12:00:00+07:00');
const last7 = DateRangeCalculator.calculatePreset('last7', today);
assert.equal(DateRangeCalculator.toDateKey(last7.start), '2026-05-29');
assert.equal(DateRangeCalculator.toDateKey(last7.end), '2026-06-04');
const thisMonth = DateRangeCalculator.calculatePreset('thisMonth', today);
assert.equal(DateRangeCalculator.toDateKey(thisMonth.start), '2026-06-01');
assert.equal(DateRangeCalculator.toDateKey(thisMonth.end), '2026-06-04');
const all = DateRangeCalculator.calculatePreset('all', today);
assert.equal(all.start, null);
assert.equal(all.end, null);
});
test('formats and parses dd/mm/yyyy date input safely', () => {
const date = new Date(2026, 5, 4);
assert.equal(DateRangeCalculator.formatDisplay(date), '04/06/2026');
assert.equal(DateRangeCalculator.toDateKey(date), '2026-06-04');
assert.equal(DateRangeCalculator.parseDisplayDate('04/06/2026')?.getFullYear(), 2026);
assert.equal(DateRangeCalculator.parseDisplayDate('31/02/2026'), null);
assert.equal(DateRangeCalculator.parseDisplayDate('04/06/10000'), null);
});
test('creates stable compact pagination ranges', () => {
assert.deepEqual(PaginationModel.range(1, 4), [1, 2, 3, 4]);
assert.deepEqual(PaginationModel.range(1, 10), [1, 2, '...', 10]);
assert.deepEqual(PaginationModel.range(5, 10), [1, '...', 4, 5, 6, '...', 10]);
assert.deepEqual(PaginationModel.range(10, 10), [1, '...', 9, 10]);
});
@@ -0,0 +1,31 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { DocumentFormatter } from '../js/utils/document-formatter.js';
test('parses comma-separated and JSON string tags', () => {
assert.deepEqual(DocumentFormatter.parseTags('akademik, panduan, 2026'), [
'akademik',
'panduan',
'2026',
]);
assert.deepEqual(DocumentFormatter.parseTags('["kerja praktik","kp"]'), [
'kerja praktik',
'kp',
]);
});
test('maps parsed tags to fixed repository categories', () => {
assert.equal(DocumentFormatter.categoryFromTags(['TA']), 'skripsi');
assert.equal(DocumentFormatter.categoryFromTags(['Kalender']), 'akademik');
assert.equal(DocumentFormatter.categoryFromTags(['kp']), 'praktik');
assert.equal(DocumentFormatter.categoryFromTags(['surat']), 'umum');
assert.equal(DocumentFormatter.categoryFromTags([]), 'umum');
});
test('derives file type and safe download filename from document name', () => {
assert.equal(DocumentFormatter.fileTypeFromName('Panduan Akademik.pdf'), 'pdf');
assert.equal(DocumentFormatter.fileTypeFromName('Form KP.DOCX'), 'docx');
assert.equal(DocumentFormatter.downloadFilename('Form KP', 'docx'), 'Form KP.docx');
assert.equal(DocumentFormatter.downloadFilename('Kalender.pdf', 'pdf'), 'Kalender.pdf');
});
@@ -0,0 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const indexHtml = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const viewerHtml = readFileSync(new URL('../viewer.html', import.meta.url), 'utf8');
test('Tapops index uses module scripts and no inline behavior', () => {
assert.match(indexHtml, /<script type="module" src="js\/app\.js"><\/script>/);
assert.doesNotMatch(indexHtml, /<script>\s*\(/);
assert.doesNotMatch(indexHtml, /\sonclick=/);
});
test('Tapops viewer uses module scripts and no inline behavior', () => {
assert.match(viewerHtml, /<script type="module" src="js\/viewer\/viewer-app\.js"><\/script>/);
assert.doesNotMatch(viewerHtml, /<script>\s*\(/);
});
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pratinjau Dokumen — TAPOPS | Informatika UNTAN</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://api.ifuntanhub.dev" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha384-iw3OoTErCYJJB9mCa8LNS2hbsQ7M3C0EpIsO/H5+EGAkPGc6rk+V8i04oW/K5xq0"
crossorigin="anonymous">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/viewer.css">
</head>
<body class="viewer-body">
<header class="viewer-header">
<div class="viewer-header-left">
<a href="index.html#repository" class="btn-back">
<i class="fas fa-arrow-left"></i> Kembali ke Repositori
</a>
<div class="viewer-title-group">
<h1 id="viewer-title" class="viewer-title">Memuat dokumen…</h1>
<span id="viewer-type" class="viewer-type-badge"></span>
</div>
</div>
<div class="viewer-header-right">
<a id="viewer-download" class="btn-download" href="#" download>
<i class="fas fa-download"></i> Unduh Berkas
</a>
</div>
</header>
<main id="viewer-content" class="viewer-content">
<div class="viewer-loading">
<i class="fas fa-circle-notch fa-spin"></i>
<span>Memuat pratinjau berkas…</span>
</div>
</main>
<script type="module" src="js/viewer/viewer-app.js"></script>
</body>
</html>
-1
View File
@@ -917,7 +917,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+2
View File
@@ -2,10 +2,12 @@
"name": "web-student-safe",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Student-safe static page assignment kit for Informatika UNTAN",
"scripts": {
"dev": "vite",
"build": "vite build",
"test:tapops": "node --test groups/Tapops/tests/*.test.mjs",
"preview": "vite preview"
},
"devDependencies": {
+654
View File
@@ -0,0 +1,654 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
vite:
specifier: ^7.1.0
version: 7.3.3
packages:
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@rollup/rollup-android-arm-eabi@4.60.4':
resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.60.4':
resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.60.4':
resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.60.4':
resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.60.4':
resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.60.4':
resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.60.4':
resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.4':
resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.4':
resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.4':
resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.4':
resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.4':
resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.4':
resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.4':
resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.4':
resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.4':
resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.4':
resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.4':
resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.4':
resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.4':
resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.60.4':
resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.60.4':
resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.60.4':
resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.60.4':
resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.60.4':
resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
engines: {node: '>=18'}
hasBin: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.60.4:
resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
vite@7.3.3:
resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
snapshots:
'@esbuild/aix-ppc64@0.27.7':
optional: true
'@esbuild/android-arm64@0.27.7':
optional: true
'@esbuild/android-arm@0.27.7':
optional: true
'@esbuild/android-x64@0.27.7':
optional: true
'@esbuild/darwin-arm64@0.27.7':
optional: true
'@esbuild/darwin-x64@0.27.7':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
optional: true
'@esbuild/freebsd-x64@0.27.7':
optional: true
'@esbuild/linux-arm64@0.27.7':
optional: true
'@esbuild/linux-arm@0.27.7':
optional: true
'@esbuild/linux-ia32@0.27.7':
optional: true
'@esbuild/linux-loong64@0.27.7':
optional: true
'@esbuild/linux-mips64el@0.27.7':
optional: true
'@esbuild/linux-ppc64@0.27.7':
optional: true
'@esbuild/linux-riscv64@0.27.7':
optional: true
'@esbuild/linux-s390x@0.27.7':
optional: true
'@esbuild/linux-x64@0.27.7':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
optional: true
'@esbuild/netbsd-x64@0.27.7':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
optional: true
'@esbuild/openbsd-x64@0.27.7':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
optional: true
'@esbuild/sunos-x64@0.27.7':
optional: true
'@esbuild/win32-arm64@0.27.7':
optional: true
'@esbuild/win32-ia32@0.27.7':
optional: true
'@esbuild/win32-x64@0.27.7':
optional: true
'@rollup/rollup-android-arm-eabi@4.60.4':
optional: true
'@rollup/rollup-android-arm64@4.60.4':
optional: true
'@rollup/rollup-darwin-arm64@4.60.4':
optional: true
'@rollup/rollup-darwin-x64@4.60.4':
optional: true
'@rollup/rollup-freebsd-arm64@4.60.4':
optional: true
'@rollup/rollup-freebsd-x64@4.60.4':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.60.4':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.60.4':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.60.4':
optional: true
'@rollup/rollup-linux-arm64-musl@4.60.4':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.60.4':
optional: true
'@rollup/rollup-linux-loong64-musl@4.60.4':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.60.4':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.60.4':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.60.4':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.60.4':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.60.4':
optional: true
'@rollup/rollup-linux-x64-gnu@4.60.4':
optional: true
'@rollup/rollup-linux-x64-musl@4.60.4':
optional: true
'@rollup/rollup-openbsd-x64@4.60.4':
optional: true
'@rollup/rollup-openharmony-arm64@4.60.4':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.60.4':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.60.4':
optional: true
'@rollup/rollup-win32-x64-gnu@4.60.4':
optional: true
'@rollup/rollup-win32-x64-msvc@4.60.4':
optional: true
'@types/estree@1.0.8': {}
esbuild@0.27.7:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
'@esbuild/android-arm': 0.27.7
'@esbuild/android-arm64': 0.27.7
'@esbuild/android-x64': 0.27.7
'@esbuild/darwin-arm64': 0.27.7
'@esbuild/darwin-x64': 0.27.7
'@esbuild/freebsd-arm64': 0.27.7
'@esbuild/freebsd-x64': 0.27.7
'@esbuild/linux-arm': 0.27.7
'@esbuild/linux-arm64': 0.27.7
'@esbuild/linux-ia32': 0.27.7
'@esbuild/linux-loong64': 0.27.7
'@esbuild/linux-mips64el': 0.27.7
'@esbuild/linux-ppc64': 0.27.7
'@esbuild/linux-riscv64': 0.27.7
'@esbuild/linux-s390x': 0.27.7
'@esbuild/linux-x64': 0.27.7
'@esbuild/netbsd-arm64': 0.27.7
'@esbuild/netbsd-x64': 0.27.7
'@esbuild/openbsd-arm64': 0.27.7
'@esbuild/openbsd-x64': 0.27.7
'@esbuild/openharmony-arm64': 0.27.7
'@esbuild/sunos-x64': 0.27.7
'@esbuild/win32-arm64': 0.27.7
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fsevents@2.3.3:
optional: true
nanoid@3.3.12: {}
picocolors@1.1.1: {}
picomatch@4.0.4: {}
postcss@8.5.15:
dependencies:
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.60.4:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.60.4
'@rollup/rollup-android-arm64': 4.60.4
'@rollup/rollup-darwin-arm64': 4.60.4
'@rollup/rollup-darwin-x64': 4.60.4
'@rollup/rollup-freebsd-arm64': 4.60.4
'@rollup/rollup-freebsd-x64': 4.60.4
'@rollup/rollup-linux-arm-gnueabihf': 4.60.4
'@rollup/rollup-linux-arm-musleabihf': 4.60.4
'@rollup/rollup-linux-arm64-gnu': 4.60.4
'@rollup/rollup-linux-arm64-musl': 4.60.4
'@rollup/rollup-linux-loong64-gnu': 4.60.4
'@rollup/rollup-linux-loong64-musl': 4.60.4
'@rollup/rollup-linux-ppc64-gnu': 4.60.4
'@rollup/rollup-linux-ppc64-musl': 4.60.4
'@rollup/rollup-linux-riscv64-gnu': 4.60.4
'@rollup/rollup-linux-riscv64-musl': 4.60.4
'@rollup/rollup-linux-s390x-gnu': 4.60.4
'@rollup/rollup-linux-x64-gnu': 4.60.4
'@rollup/rollup-linux-x64-musl': 4.60.4
'@rollup/rollup-openbsd-x64': 4.60.4
'@rollup/rollup-openharmony-arm64': 4.60.4
'@rollup/rollup-win32-arm64-msvc': 4.60.4
'@rollup/rollup-win32-ia32-msvc': 4.60.4
'@rollup/rollup-win32-x64-gnu': 4.60.4
'@rollup/rollup-win32-x64-msvc': 4.60.4
fsevents: 2.3.3
source-map-js@1.2.1: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
vite@7.3.3:
dependencies:
esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.15
rollup: 4.60.4
tinyglobby: 0.2.16
optionalDependencies:
fsevents: 2.3.3
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: set this to true or false