OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
No files matched your search
@@ -0,0 +1,721 @@
|
||||
/* ============================================================================
|
||||
admin-theme.css — Overlay tema OPEN-ISP untuk Admin Panel
|
||||
Dimuat SETELAH admin.css. TIDAK mengubah admin.css / view manapun.
|
||||
Seluruh aturan dibungkus @media screen agar print stylesheet admin aman.
|
||||
========================================================================== */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap');
|
||||
|
||||
@media screen {
|
||||
|
||||
/* ── TOKEN DARK (OPEN-ISP) ───────────────────────────────────────────── */
|
||||
:root {
|
||||
--brand: #ADFF2F;
|
||||
--brand-2: #9CE62A;
|
||||
--brand-ink: #0A0D14;
|
||||
|
||||
--bg: #0A0D14;
|
||||
--bg2: #111622;
|
||||
--bg3: #1F293D;
|
||||
--border: rgba(242, 245, 241, 0.10);
|
||||
--card-border: var(--border);
|
||||
/* Token yang dipakai sebagian halaman admin (mis. acs.ejs/GenieACS Pro) */
|
||||
--card-bg: var(--bg2);
|
||||
--sub-bg: var(--bg3);
|
||||
--sidebar-bg: var(--bg2);
|
||||
--hover-bg: rgba(173, 255, 47, 0.06);
|
||||
--card-color: var(--primary);
|
||||
--text: #F2F5F1;
|
||||
--muted: #8A96A6;
|
||||
|
||||
--primary: #ADFF2F;
|
||||
--pdim: rgba(173, 255, 47, 0.14);
|
||||
|
||||
--success: #34D399;
|
||||
--sdim: rgba(52, 211, 153, 0.14);
|
||||
--warning: #FBBF24;
|
||||
--wdim: rgba(251, 191, 36, 0.14);
|
||||
--danger: #FB7185;
|
||||
--ddim: rgba(251, 113, 133, 0.14);
|
||||
--info: #38BDF8;
|
||||
--idim: rgba(56, 189, 248, 0.14);
|
||||
|
||||
--on-primary: #0A0D14; /* teks di atas latar --primary */
|
||||
--overlay-bg: rgba(17, 22, 34, 0.72);
|
||||
}
|
||||
|
||||
/* ── BASE / BACKGROUND ───────────────────────────────────────────────── */
|
||||
body {
|
||||
background-color: var(--bg) !important;
|
||||
background-image:
|
||||
radial-gradient(rgba(255, 255, 255, 0.11) 1.4px, transparent 1.4px),
|
||||
radial-gradient(130% 130% at 50% 42%, transparent 38%, rgba(0, 0, 0, 0.55) 100%),
|
||||
radial-gradient(1100px 620px at 8% -14%, rgba(173, 255, 47, 0.10), transparent 58%),
|
||||
radial-gradient(950px 540px at 102% -6%, rgba(56, 189, 248, 0.085), transparent 55%),
|
||||
radial-gradient(1100px 640px at 50% 122%, rgba(139, 92, 246, 0.075), transparent 60%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018), transparent 30%),
|
||||
linear-gradient(180deg, #0C1017 0%, #080A0F 100%) !important;
|
||||
background-size: 22px 22px, auto, auto, auto, auto, auto, auto !important;
|
||||
background-attachment: fixed !important;
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.mw { background: transparent; }
|
||||
|
||||
/* ── SIDEBAR ─────────────────────────────────────────────────────────── */
|
||||
.sidebar {
|
||||
background: linear-gradient(180deg, rgba(173, 255, 47, 0.035), transparent 180px), var(--bg2) !important;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.sb-brand { border-bottom: 1px solid var(--border); }
|
||||
.sb-icon {
|
||||
background: linear-gradient(135deg, var(--brand), #6fae12) !important;
|
||||
color: var(--brand-ink) !important;
|
||||
box-shadow: 0 6px 16px rgba(173, 255, 47, 0.28);
|
||||
border-radius: 11px;
|
||||
}
|
||||
.sb-name { color: var(--text); letter-spacing: -0.01em; }
|
||||
.sb-item { border-radius: 0 10px 10px 0; }
|
||||
.sb-item:hover { background: rgba(255, 255, 255, 0.045); }
|
||||
.sb-item.active {
|
||||
color: var(--primary) !important;
|
||||
background: var(--pdim);
|
||||
border-left-color: var(--primary);
|
||||
box-shadow: inset 0 0 0 1px rgba(173, 255, 47, 0.12);
|
||||
}
|
||||
.sb-item.active i { color: var(--primary); }
|
||||
|
||||
/* ── TOPBAR ──────────────────────────────────────────────────────────── */
|
||||
.topbar {
|
||||
background: rgba(17, 22, 34, 0.72) !important;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tb-title { font-weight: 600; }
|
||||
.tb-title i { color: var(--primary); }
|
||||
.adm-badge {
|
||||
background: var(--pdim);
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(173, 255, 47, 0.22);
|
||||
}
|
||||
.realtime-bar { color: var(--muted); }
|
||||
|
||||
/* ── STAT CARDS ──────────────────────────────────────────────────────── */
|
||||
.stat-grid { gap: 16px; }
|
||||
.sc {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 55%),
|
||||
var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 18px 18px 16px;
|
||||
}
|
||||
.sc:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(173, 255, 47, 0.28);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.sc::before { height: 3px; opacity: 0.9; }
|
||||
.sc.p::before { background: linear-gradient(90deg, var(--brand), #d4ff7a); }
|
||||
.sc.s::before { background: linear-gradient(90deg, #34D399, #6ee7b7); }
|
||||
.sc.w::before { background: linear-gradient(90deg, #FBBF24, #fcd34d); }
|
||||
.sc.d::before { background: linear-gradient(90deg, #FB7185, #fda4af); }
|
||||
.sc.i::before { background: linear-gradient(90deg, #38BDF8, #7dd3fc); }
|
||||
.sc-icon { opacity: 0.16; font-size: 30px; }
|
||||
.sc-label { color: var(--muted); font-weight: 600; }
|
||||
.sc-val { font-family: 'Space Grotesk', 'Inter', sans-serif; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.sc-sub { color: var(--muted); }
|
||||
|
||||
/* ── CARDS ───────────────────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
}
|
||||
.card-hd { border-bottom: 1px solid var(--border); }
|
||||
.card-hd h6 { font-weight: 600; letter-spacing: -0.01em; }
|
||||
.card-hd h6 i { color: var(--primary); }
|
||||
|
||||
/* ── TABLE ───────────────────────────────────────────────────────────── */
|
||||
.dtbl th {
|
||||
background: var(--bg3);
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.dtbl td { border-bottom: 1px solid var(--border); }
|
||||
.dtbl tbody tr:hover td { background: rgba(173, 255, 47, 0.035); }
|
||||
.dtbl a { color: var(--primary); }
|
||||
|
||||
/* ── BADGES ──────────────────────────────────────────────────────────── */
|
||||
.badge { font-weight: 700; letter-spacing: 0.01em; }
|
||||
.bp { background: var(--pdim); color: var(--primary); }
|
||||
|
||||
/* ── BUTTONS ─────────────────────────────────────────────────────────── */
|
||||
.btn { border-radius: 10px; font-weight: 600; }
|
||||
.btn-p {
|
||||
background: linear-gradient(135deg, var(--brand), var(--brand-2)) !important;
|
||||
color: var(--brand-ink) !important;
|
||||
box-shadow: 0 6px 16px rgba(173, 255, 47, 0.22);
|
||||
}
|
||||
.btn-p:hover { opacity: 1; filter: brightness(1.05); }
|
||||
.btn-p i, .btn-p span { color: var(--brand-ink); }
|
||||
.btn-g {
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.btn-g:hover { background: rgba(255, 255, 255, 0.09); }
|
||||
|
||||
/* ── FORMS ───────────────────────────────────────────────────────────── */
|
||||
.fc, .fs {
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.fc:focus, .fs:focus {
|
||||
border-color: rgba(173, 255, 47, 0.65);
|
||||
box-shadow: 0 0 0 3px var(--pdim);
|
||||
}
|
||||
.fl { color: var(--muted); font-weight: 600; }
|
||||
.fs option { background: var(--bg2); color: var(--text); }
|
||||
|
||||
/* ── MODAL ───────────────────────────────────────────────────────────── */
|
||||
.mo { background: rgba(4, 7, 12, 0.72); backdrop-filter: blur(6px); }
|
||||
.mb {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.mh, .mf { border-color: var(--border); }
|
||||
.mt { font-weight: 600; }
|
||||
|
||||
/* ── ALERT / TABS / MISC ─────────────────────────────────────────────── */
|
||||
.alert { border-radius: 12px; }
|
||||
.tab-bar { background: var(--bg3); border: 1px solid var(--border); border-radius: 12px; }
|
||||
.tab-btn { border-radius: 9px; font-weight: 600; }
|
||||
.tab-btn.active { background: var(--pdim) !important; color: var(--primary) !important; box-shadow: none; }
|
||||
.mtab.active, .tab.active { background: var(--primary) !important; color: var(--brand-ink) !important; }
|
||||
.donut-num { font-family: 'Space Grotesk', 'Inter', sans-serif; }
|
||||
.donut-lbl { color: var(--muted); }
|
||||
|
||||
/* ── TOMBOL/TAB "TERPILIH" (latar = --primary) → teks kontras ───────── */
|
||||
.wa-tab.active, .mtab.active, .tab.active, .view-toggle-btn.active,
|
||||
.bind-tag.active, .btn-action:hover, .chat-user-avatar, .msg-bubble.outbound {
|
||||
color: var(--on-primary) !important;
|
||||
}
|
||||
[style*="var(--primary)"][style*="color:white"],
|
||||
[style*="var(--primary)"][style*="color: white"],
|
||||
[style*="var(--primary)"][style*="color:#fff"],
|
||||
[style*="var(--primary)"][style*="color: #fff"] {
|
||||
color: var(--on-primary) !important;
|
||||
}
|
||||
|
||||
/* ══ HALAMAN MAP (komponen terang hardcode → token tema) ═════════════ */
|
||||
#map { background: var(--bg3) !important; border-color: var(--border) !important; }
|
||||
.map-toolbar { background: var(--bg2) !important; border-color: var(--border) !important; box-shadow: none !important; }
|
||||
.msc { background: var(--bg2) !important; border-color: var(--border) !important; box-shadow: none !important; }
|
||||
.msc-info .val { color: var(--text) !important; }
|
||||
.msc-info .lbl { color: var(--muted) !important; }
|
||||
.map-search-input { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.map-search-input:focus { background: var(--bg2) !important; border-color: var(--primary) !important; }
|
||||
.map-search-icon, .map-search-clear { color: var(--muted) !important; }
|
||||
.map-search-dropdown { background: var(--bg2) !important; border-color: var(--border) !important; box-shadow: 0 12px 30px rgba(0,0,0,.4) !important; }
|
||||
.ms-header { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--muted) !important; }
|
||||
.ms-item { border-color: var(--border) !important; }
|
||||
.ms-item:hover, .ms-item.selected { background: var(--pdim) !important; }
|
||||
.ms-item-name { color: var(--text) !important; }
|
||||
.ms-item-meta, .ms-empty { color: var(--muted) !important; }
|
||||
.map-pill-btn { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.map-pill-btn:hover { background: var(--bg2) !important; border-color: var(--primary) !important; }
|
||||
.data-tabs-panel { background: var(--bg2) !important; border-color: var(--border) !important; box-shadow: none !important; }
|
||||
.data-tabs-nav { background: var(--bg3) !important; border-color: var(--border) !important; }
|
||||
.tab-btn { color: var(--muted) !important; }
|
||||
.tab-btn:hover { color: var(--text) !important; background: var(--pdim) !important; }
|
||||
.tab-btn.active { background: var(--bg2) !important; color: var(--primary) !important; border-color: var(--border) !important; }
|
||||
.tab-badge { background: var(--bg3) !important; color: var(--text) !important; }
|
||||
.tab-btn.active .tab-badge { background: var(--pdim) !important; color: var(--primary) !important; }
|
||||
.table-search-input { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.table-search-input:focus { background: var(--bg2) !important; border-color: var(--primary) !important; }
|
||||
.table-wrap { border-color: var(--border) !important; }
|
||||
.data-table th, .data-table td { border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.data-table th { background: var(--bg3) !important; color: var(--muted) !important; }
|
||||
.data-table tr:hover td { background: var(--pdim) !important; }
|
||||
#picker-map, #edit-picker-map, #cable-drawing-map { background: var(--bg3) !important; border-color: var(--border) !important; }
|
||||
/* Leaflet popup pelanggan */
|
||||
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--bg2) !important; color: var(--text) !important; box-shadow: 0 12px 30px rgba(0,0,0,.45) !important; }
|
||||
.cp-head, .cp-traffic { border-color: var(--border) !important; }
|
||||
.cp-name, .cp-traffic-head, .cp-chart-label { color: var(--text) !important; }
|
||||
.cp-meta-item, .cp-meta-item i { color: var(--muted) !important; }
|
||||
.cp-chip { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.cp-chart { background: var(--bg3) !important; border-color: var(--border) !important; }
|
||||
.cp-overlay { background: var(--overlay-bg) !important; color: var(--muted) !important; }
|
||||
.cp-avatar { color: var(--text) !important; }
|
||||
|
||||
/* ══ HALAMAN OLTS (komponen terang hardcode → token tema) ════════════ */
|
||||
.stat-card-v2 { background: var(--bg2) !important; border-color: var(--border) !important; box-shadow: none !important; }
|
||||
.stat-card-v2 .details h3 { color: var(--text) !important; }
|
||||
.stat-card-v2 .details p { color: var(--muted) !important; }
|
||||
.system-info-bar { background: var(--bg3) !important; border-color: var(--border) !important; }
|
||||
.info-item { color: var(--muted) !important; }
|
||||
.olt-selector-container { background: var(--bg2) !important; border-color: var(--border) !important; }
|
||||
.custom-select-v2 { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.onu-main-card { background: var(--bg2) !important; border-color: var(--border) !important; box-shadow: none !important; }
|
||||
.onu-main-card .card-header { background: var(--bg3) !important; border-color: var(--border) !important; }
|
||||
.onu-table-v2 th { background: var(--bg3) !important; color: var(--muted) !important; border-color: var(--border) !important; }
|
||||
.onu-table-v2 td { color: var(--text) !important; border-color: var(--border) !important; }
|
||||
.onu-table-v2 tr:hover { background: var(--pdim) !important; }
|
||||
.btn-action-v2 { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--muted) !important; }
|
||||
.btn-action-v2:hover { background: var(--bg2) !important; color: var(--text) !important; border-color: var(--primary) !important; }
|
||||
.empty-state { color: var(--muted) !important; }
|
||||
.loading-overlay { background: var(--overlay-bg) !important; }
|
||||
|
||||
/* ══ PROMO-SLIDES: modal edit (style di body, butuh spesifisitas lebih) ═ */
|
||||
.modal-wrapper .modal-content { background: var(--bg2) !important; color: var(--text) !important; border: 1px solid var(--border); }
|
||||
.modal-wrapper .modal-header, .modal-wrapper .modal-footer { border-color: var(--border) !important; }
|
||||
.modal-wrapper .modal-header h5 { color: var(--text) !important; }
|
||||
.modal-wrapper .modal-close { color: var(--muted) !important; }
|
||||
.modal-wrapper .modal-close:hover { color: var(--text) !important; }
|
||||
.modal-wrapper .fl { color: var(--text) !important; }
|
||||
.modal-wrapper .hint { color: var(--muted) !important; }
|
||||
.modal-wrapper .fc { background: var(--bg3) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
||||
.modal-wrapper .fc:focus { border-color: var(--primary) !important; box-shadow: 0 0 0 3px var(--pdim) !important; }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
LIGHT THEME — token (spesifisitas tinggi agar menang atas admin.css)
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
:root[data-theme="light"], :root.light-theme,
|
||||
html[data-theme="light"], html.light-theme,
|
||||
body[data-theme="light"], body.light-theme {
|
||||
--bg: #E8EDE5;
|
||||
--bg2: #FFFFFF;
|
||||
--bg3: #DEE4DA;
|
||||
--border: rgba(16, 20, 15, 0.15);
|
||||
--card-border: rgba(16, 20, 15, 0.15);
|
||||
--hover-bg: rgba(92, 154, 10, 0.08);
|
||||
--text: #141A12;
|
||||
--muted: #52604C;
|
||||
|
||||
--primary: #5C9A0A;
|
||||
--pdim: rgba(92, 154, 10, 0.12);
|
||||
|
||||
--success: #0F9D6B;
|
||||
--sdim: rgba(15, 157, 107, 0.12);
|
||||
--warning: #C2740A;
|
||||
--wdim: rgba(194, 116, 10, 0.12);
|
||||
--danger: #DE3B5C;
|
||||
--ddim: rgba(222, 59, 92, 0.12);
|
||||
--info: #0A7BC2;
|
||||
--idim: rgba(10, 123, 194, 0.12);
|
||||
|
||||
--on-primary: #FFFFFF;
|
||||
--overlay-bg: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
/* ── LIGHT: base ─────────────────────────────────────────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) body,
|
||||
:is(html[data-theme="light"], html.light-theme) body.light-theme {
|
||||
background-color: var(--bg) !important;
|
||||
background-image:
|
||||
radial-gradient(rgba(16, 20, 15, 0.17) 1.4px, transparent 1.4px),
|
||||
radial-gradient(130% 130% at 50% 42%, transparent 38%, rgba(16, 20, 15, 0.20) 100%),
|
||||
radial-gradient(1100px 620px at 8% -14%, rgba(92, 154, 10, 0.10), transparent 58%),
|
||||
radial-gradient(950px 540px at 102% -6%, rgba(10, 123, 194, 0.08), transparent 55%),
|
||||
radial-gradient(1100px 640px at 50% 122%, rgba(124, 92, 246, 0.07), transparent 60%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.35), transparent 30%),
|
||||
linear-gradient(180deg, #E8EDE5 0%, #DEE4DA 100%) !important;
|
||||
background-size: 22px 22px, auto, auto, auto, auto, auto, auto !important;
|
||||
background-attachment: fixed !important;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── LIGHT: shell ────────────────────────────────────────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) .sidebar {
|
||||
background: linear-gradient(180deg, rgba(173, 255, 47, 0.16), transparent 180px), var(--bg2) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-icon {
|
||||
background: linear-gradient(135deg, var(--brand), #7cc61a) !important;
|
||||
color: var(--brand-ink) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-name,
|
||||
:is(html[data-theme="light"], html.light-theme) .tb-title,
|
||||
:is(html[data-theme="light"], html.light-theme) .card-hd h6,
|
||||
:is(html[data-theme="light"], html.light-theme) .sc-val { color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-item { color: #4b5563 !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-item:hover { background: rgba(0, 0, 0, 0.04) !important; color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-item.active {
|
||||
color: var(--primary) !important;
|
||||
background: var(--pdim) !important;
|
||||
border-left-color: var(--primary);
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-item.active i { color: var(--primary); }
|
||||
:is(html[data-theme="light"], html.light-theme) .sb-item.logout { color: var(--danger) !important; }
|
||||
/* Biarkan pola/dot latar body terlihat (admin.css memaksa #f8fafc pada .mw/.page) */
|
||||
:is(html[data-theme="light"], html.light-theme) .mw,
|
||||
:is(html[data-theme="light"], html.light-theme) .page,
|
||||
:is(html[data-theme="light"], html.light-theme) .main-content,
|
||||
:is(html[data-theme="light"], html.light-theme) .content-wrapper { background: transparent !important; }
|
||||
|
||||
:is(html[data-theme="light"], html.light-theme) .topbar {
|
||||
background: rgba(255, 255, 255, 0.82) !important;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .tb-title i,
|
||||
:is(html[data-theme="light"], html.light-theme) .card-hd h6 i { color: var(--primary) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .adm-badge {
|
||||
color: var(--primary) !important;
|
||||
border-color: rgba(77, 124, 15, 0.25) !important;
|
||||
}
|
||||
|
||||
/* ── LIGHT: surfaces ─────────────────────────────────────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) .sc,
|
||||
:is(html[data-theme="light"], html.light-theme) .card,
|
||||
:is(html[data-theme="light"], html.light-theme) .mb { background: var(--bg2) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .sc:hover { box-shadow: 0 10px 30px rgba(10, 13, 20, 0.10); }
|
||||
:is(html[data-theme="light"], html.light-theme) .fc,
|
||||
:is(html[data-theme="light"], html.light-theme) .fs { background: var(--bg3) !important; color: var(--text) !important; border-color: var(--border) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .fc:focus,
|
||||
:is(html[data-theme="light"], html.light-theme) .fs:focus { border-color: rgba(77, 124, 15, 0.55) !important; box-shadow: 0 0 0 3px var(--pdim) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .btn-g { background: rgba(0, 0, 0, 0.045) !important; color: var(--text) !important; border-color: var(--border) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .btn-g:hover { background: rgba(0, 0, 0, 0.08) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .dtbl th { background: var(--bg3) !important; color: var(--muted) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .dtbl td { color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .dtbl tbody tr:hover td { background: rgba(120, 160, 0, 0.06) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .tab-bar { background: var(--bg3) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .tab-btn.active { background: var(--pdim) !important; color: var(--primary) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .badge.bp { color: var(--primary) !important; }
|
||||
|
||||
/* ── LIGHT: inline DARK backgrounds (kartu hardcode) ─────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="#0f172a"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="#1e293b"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="#0b132b"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="#111827"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="#0b1220"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(15,23,42"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(15, 23, 42"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(30,41,59"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(30, 41, 59"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(17,24,39"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(17, 24, 39"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(51,65,85"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="rgba(51, 65, 85"] {
|
||||
background: #FFFFFF !important;
|
||||
}
|
||||
|
||||
/* ── LIGHT: inline terang → gelap (teks) ─────────────────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#f8fafc"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color: #f8fafc"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#f1f5f9"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color: #f1f5f9"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#e2e8f0"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color: #e2e8f0"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:white"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color: white"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#fff"] {
|
||||
color: #0A0D14 !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#94a3b8"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color: #94a3b8"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#cbd5e1"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#64748b"] {
|
||||
color: #475569 !important;
|
||||
}
|
||||
/* aksen terang → versi gelap */
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#60a5fa"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#38bdf8"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#93c5fd"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#bfdbfe"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#e0e7ff"] { color: #1d4ed8 !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#4ade80"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#86efac"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#bbf7d0"] { color: #15803d !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#a5b4fc"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#818cf8"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#c7d2fe"] { color: #4338ca !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#fde68a"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#fbbf24"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#fcd34d"] { color: #b45309 !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#fecaca"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#fca5a5"],
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:#f87171"] { color: #dc2626 !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) [style*="color:var(--primary)"] { color: var(--primary) !important; }
|
||||
|
||||
/* table-dark (mis. OLT SNMP) */
|
||||
:is(html[data-theme="light"], html.light-theme) .table-dark,
|
||||
:is(html[data-theme="light"], html.light-theme) .table-dark > tbody > tr > td,
|
||||
:is(html[data-theme="light"], html.light-theme) .table-dark > tbody > tr > th,
|
||||
:is(html[data-theme="light"], html.light-theme) .table-dark > thead > tr > th {
|
||||
background: transparent !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) pre,
|
||||
:is(html[data-theme="light"], html.light-theme) code { color: var(--text) !important; }
|
||||
|
||||
/* ── LIGHT: halaman ROUTERS (class hardcode) ─────────────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-card {
|
||||
background: #FFFFFF !important;
|
||||
border-color: var(--border) !important;
|
||||
box-shadow: 0 10px 30px rgba(10, 13, 20, 0.08) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-card:hover {
|
||||
border-color: rgba(77, 124, 15, 0.45) !important;
|
||||
box-shadow: 0 14px 34px rgba(10, 13, 20, 0.14) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-icon {
|
||||
color: #4d7c0f !important;
|
||||
background: linear-gradient(135deg, rgba(173, 255, 47, 0.30), rgba(173, 255, 47, 0.12)) !important;
|
||||
border-color: rgba(120, 160, 0, 0.35) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-name { color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-host { color: var(--muted) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-status-pill.online { color: #15803d !important; background: rgba(34, 197, 94, 0.12) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-status-pill.offline { color: #dc2626 !important; background: rgba(239, 68, 68, 0.10) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-metrics {
|
||||
background: var(--bg3) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-metric-head { color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-progress { background: rgba(10, 13, 20, 0.08) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-spec-box {
|
||||
background: var(--bg3) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-spec-val { color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-spec-lbl { color: var(--muted) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .rc-actions { border-top-color: var(--border) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .modal-tab-btn {
|
||||
background: var(--bg3) !important;
|
||||
color: var(--muted) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) .modal-tab-btn:hover { background: #e2e8e0 !important; color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .modal-tab-btn.active {
|
||||
background: linear-gradient(135deg, var(--brand), var(--brand-2)) !important;
|
||||
color: var(--brand-ink) !important;
|
||||
border-color: var(--brand-2) !important;
|
||||
}
|
||||
|
||||
/* ── LIGHT: expense categories ───────────────────────────────────────── */
|
||||
:is(html[data-theme="light"], html.light-theme) .cat-card { background: var(--bg2) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .cat-card:hover { border-color: var(--border) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .cat-name { color: var(--text) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .cat-desc { color: var(--muted) !important; }
|
||||
:is(html[data-theme="light"], html.light-theme) .icon-box { background: rgba(10, 13, 20, 0.05) !important; }
|
||||
|
||||
}
|
||||
|
||||
/* ── MOBILE: fix overlap sidebar drawer vs bottom-nav (tab bawah sidebar terpotong) ── */
|
||||
@media screen and (max-width: 768px) {
|
||||
.sidebar { height: 100dvh; }
|
||||
.sb-nav { padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px)) !important; }
|
||||
.sidebar.open ~ .bottom-nav { display: none !important; }
|
||||
}
|
||||
|
||||
|
||||
/* ── Grup menu → tab bar (sidebar ringkas) ── */
|
||||
.grp-tabs{display:flex;gap:8px;margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:10px;overflow-x:auto;-webkit-overflow-scrolling:touch;white-space:nowrap}
|
||||
.grp-tab{padding:8px 14px;border-radius:8px;color:var(--muted);transition:all .2s;font-weight:700;text-decoration:none;flex-shrink:0;display:inline-flex;align-items:center;gap:6px}
|
||||
.grp-tab:hover{background:rgba(255,255,255,.06);color:var(--text)}
|
||||
.grp-tab.active{background:var(--primary);color:#0A0D14}
|
||||
@media(max-width:768px){.grp-tabs{font-size:13px}.grp-tab{padding:6px 10px;font-size:12px}}
|
||||
|
||||
/* Section switcher (grup menu) */
|
||||
.grp-nav{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding:6px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg2,#111622);width:fit-content;max-width:100%}
|
||||
.grp-nav-title{font-size:12px;font-weight:800;color:var(--muted);display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
|
||||
.grp-nav-select{background:transparent;border:none;color:var(--text);font-weight:800;font-size:13px;cursor:pointer;outline:none;max-width:60vw}
|
||||
.grp-nav-select option{background:var(--bg2,#111622);color:var(--text)}
|
||||
|
||||
/* Sidebar accordion (grup menu) */
|
||||
.sb-item.sb-parent .sb-caret{margin-left:auto;font-size:12px;opacity:.6;transition:transform .2s}
|
||||
.sb-item.sb-parent.open .sb-caret{transform:rotate(180deg)}
|
||||
.sb-sub{display:flex;flex-direction:column}
|
||||
.sb-item.sb-child{padding-left:34px;font-size:12.5px}
|
||||
.sb-item.sb-child i{font-size:13px;opacity:.85}
|
||||
|
||||
|
||||
/* admin scroll fix */
|
||||
html, body { height: 100dvh; min-height: 100dvh; }
|
||||
/* .mw = area konten; jadikan block-scroll yang andal (bukan flex-column) */
|
||||
.mw { display: block !important; height: 100dvh; overflow-y: auto !important; overflow-x: auto; }
|
||||
.mw > .page, .page { flex: 0 0 auto; }
|
||||
@media (max-width: 768px) { .mw { padding-bottom: 70px; } }
|
||||
/* modal: satu scroll saja (hindari nested-scroll yang mengunci) */
|
||||
.mo .mb { max-height: 92vh; overflow-y: auto; }
|
||||
.mo .mb .mbody { max-height: none !important; overflow: visible !important; }
|
||||
|
||||
/* == UI HARDENING (audit ui-ux.md) - jangan hapus marker == */
|
||||
@media screen {
|
||||
/* #2 gambar & media responsif (admin) */
|
||||
.mw img, .mo img, .card img, .alert img { max-width: 100%; height: auto; }
|
||||
/* #2 anti-overlap: teks panjang tidak meluber */
|
||||
.sb-name, .sb-sub, .tb-title, .card-hd h6, .alert { overflow-wrap: break-word; } /* break-word: cegah teks vertikal (min-content bukan 1 huruf) */
|
||||
.dtbl td { overflow-wrap: break-word; } /* tabel: break-word agar kolom tidak menyusut jadi 1 huruf */
|
||||
/* #2 flex/grid child tidak meluber ke luar parent */
|
||||
.card-hd > *, .srow > *, .filter-bar > *, .form-row > *, .form-row2 > *, .form-row3 > * { min-width: 0; }
|
||||
/* #1 tipografi fluid (metrik & judul) */
|
||||
.sc-val { font-size: clamp(20px, 1.4vw + 14px, 30px); }
|
||||
.tb-title { font-size: clamp(13px, 0.55vw + 11px, 17px); }
|
||||
/* #4 tinggi fluid aman (mobile browser UI) */
|
||||
.sidebar { height: 100vh; height: 100dvh; }
|
||||
}
|
||||
|
||||
/* == DROPDOWN AKSI (kolom aksi tabel) == */
|
||||
.act{position:relative;display:inline-block;text-align:left}
|
||||
.act-btn{width:34px;height:34px;border-radius:9px;border:1px solid var(--border);background:var(--bg3);color:var(--text);cursor:pointer;display:inline-grid;place-items:center;font-size:17px;line-height:1;transition:.15s}
|
||||
.act-btn:hover{border-color:var(--primary);color:var(--primary)}
|
||||
.act.open .act-btn{border-color:var(--primary);color:var(--primary)}
|
||||
.act-menu{position:absolute;right:0;top:calc(100% + 6px);min-width:240px;max-width:290px;background:var(--bg2);border:1px solid var(--border);border-radius:12px;box-shadow:0 18px 44px rgba(0,0,0,.55);padding:6px;z-index:1300;display:none}
|
||||
.act.open .act-menu{display:block}
|
||||
.act-menu.up{top:auto;bottom:calc(100% + 6px)}
|
||||
.act-menu.fixed{position:fixed;right:auto;top:auto;bottom:auto;z-index:99999;display:block;background:var(--bg2);border:1px solid var(--border);box-shadow:0 18px 44px rgba(0,0,0,.55)}
|
||||
.act-item{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:9px 10px;border-radius:9px;background:transparent;border:none;color:var(--text);font-size:12.5px;font-weight:600;font-family:inherit;line-height:1.2;cursor:pointer;text-decoration:none;white-space:normal}
|
||||
.act-item i{font-size:15px;width:18px;text-align:center;color:var(--muted);flex:0 0 auto}
|
||||
.act-item:hover{background:rgba(255,255,255,.06)}
|
||||
.act-item:hover i{color:var(--text)}
|
||||
.act-item.primary i{color:var(--primary)} .act-item.success i{color:var(--success)}
|
||||
.act-item.warn i{color:var(--warning)} .act-item.danger{color:var(--danger)} .act-item.danger i{color:var(--danger)}
|
||||
.act-sep{height:1px;background:var(--border);margin:5px 4px}
|
||||
html[data-theme="light"] .act-menu{background:#ffffff}
|
||||
html[data-theme="light"] .act-item:hover{background:#f1f5f9}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
UNIFIKASI TOMBOL TAB (semua varian) — ditempel paling akhir agar
|
||||
menang atas gaya bawaan tiap halaman & admin-theme sebelumnya.
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
@media screen {
|
||||
:is(.tab-bar,.grp-tabs,.mtabs,.wa-tabs,.inv-tabs,.set-tabs,.mt-tabs,.au-tabs,.tabs){
|
||||
display:flex !important;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
flex-wrap:nowrap !important;
|
||||
overflow-x:auto;
|
||||
overflow-y:hidden;
|
||||
white-space:nowrap;
|
||||
padding:0 0 2px 0 !important;
|
||||
margin:0 0 14px 0 !important;
|
||||
background:none !important;
|
||||
border:none !important;
|
||||
border-bottom:0 !important;
|
||||
border-radius:0 !important;
|
||||
-webkit-overflow-scrolling:touch;
|
||||
scrollbar-width:none;
|
||||
}
|
||||
:is(.tab-bar,.grp-tabs,.mtabs,.wa-tabs,.inv-tabs,.set-tabs,.mt-tabs,.au-tabs,.tabs)::-webkit-scrollbar{ display:none; }
|
||||
|
||||
:is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a){
|
||||
display:inline-flex !important;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:6px;
|
||||
padding:7px 13px !important;
|
||||
border-radius:9px !important;
|
||||
background:var(--bg3) !important;
|
||||
color:var(--muted) !important;
|
||||
border:1px solid var(--border) !important;
|
||||
font-weight:600 !important;
|
||||
font-size:13px !important;
|
||||
line-height:1.2;
|
||||
text-decoration:none !important;
|
||||
cursor:pointer;
|
||||
white-space:nowrap;
|
||||
transition:background .15s,color .15s,border-color .15s;
|
||||
flex:0 0 auto;
|
||||
}
|
||||
:is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a) i{ color:inherit !important; }
|
||||
|
||||
:is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a):hover{ color:var(--text) !important; border-color:var(--primary) !important; background:var(--bg3) !important; }
|
||||
|
||||
:is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active, :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active:hover{
|
||||
background:var(--primary) !important;
|
||||
color:var(--on-primary,#0A0D14) !important;
|
||||
border-color:var(--primary) !important;
|
||||
box-shadow:none !important;
|
||||
}
|
||||
|
||||
:is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active .tab-badge,
|
||||
:is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active .mt-tab-badge{ background:rgba(0,0,0,.18) !important; color:inherit !important; }
|
||||
}
|
||||
html[data-theme="light"] :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active, html.light-theme :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active{ color:#ffffff !important; }
|
||||
html[data-theme="light"] :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active .tab-badge, html[data-theme="light"] :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active .mt-tab-badge{ background:rgba(255,255,255,.22) !important; }
|
||||
|
||||
|
||||
/* ── Kolom cari (filter bar) jangan menyusut kekecilan ── */
|
||||
@media screen {
|
||||
.srow > .sinput, .filter-bar > .sinput { min-width: 240px !important; }
|
||||
}
|
||||
/* Mobile: kolom cari full width, TINGGI auto (jangan ikut flex-basis -> ikon tidak nyasar) */
|
||||
@media(max-width:576px){
|
||||
.sinput, .srow > .sinput, .filter-bar > .sinput {
|
||||
flex: 0 0 auto !important;
|
||||
height: auto !important;
|
||||
min-width: 0 !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
/* jaga ikon kaca pembesar tetap sejajar di dalam kotak */
|
||||
.sinput { position: relative !important; }
|
||||
.sinput i { position: absolute !important; top: 50% !important; transform: translateY(-50%) !important; }
|
||||
.sinput input { padding-left: 34px !important; }
|
||||
}
|
||||
|
||||
/* ── Highlight kolom cari saat shortcut "/" ditekan ── */
|
||||
@keyframes adminSearchPulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(173,255,47,.55); }
|
||||
100% { box-shadow: 0 0 0 8px rgba(173,255,47,0); }
|
||||
}
|
||||
.admin-search-flash { border-radius: 8px; animation: adminSearchPulse .9s ease-out; }
|
||||
.admin-search-flash input, .admin-search-flash .fc { outline: 2px solid #ADFF2F !important; outline-offset: 1px; }
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
FIX: TOMBOL TAB AKTIF KONSISTEN (light mode)
|
||||
Menang atas style lokal halaman (radius-settings biru, payroll pale,
|
||||
onu-provision underline biru) -> semua seragam solid --primary + teks putih.
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
@media screen {
|
||||
:is(html[data-theme="light"], html.light-theme) :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a,.view-toggle-btn).active,
|
||||
:is(html[data-theme="light"], html.light-theme) :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a,.view-toggle-btn).active:hover {
|
||||
background: var(--primary) !important;
|
||||
color: #ffffff !important;
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active i {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
:is(html[data-theme="light"], html.light-theme) :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active .tab-badge,
|
||||
:is(html[data-theme="light"], html.light-theme) :is(.tab-btn,.mtab,.grp-tab,.wa-tab,.au-tab,.set-tab,.mt-tab,.inv-tab,.tabs > .tab,.tab-bar > button,.tab-bar > a).active .mt-tab-badge {
|
||||
background: rgba(255,255,255,.25) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* ── FLUID + ANTI-OVERLAP (ui-ux.md) ───────────────────────────────────── */
|
||||
/* Tinggi viewport dinamis (mobile browser chrome) */
|
||||
body { height: 100dvh; }
|
||||
.mw { height: 100vh; height: 100dvh; }
|
||||
|
||||
/* Grid/flex children tidak meluber keluar kontainer */
|
||||
.stat-grid > *, .settings-grid > *, .form-row > *, .form-row2 > *, .form-row3 > *,
|
||||
.dash-main-grid > *, .quick-actions-grid > *, .sidebar-settings-save-grid > *,
|
||||
.card-hd > *, .row > * { min-width: 0; }
|
||||
|
||||
/* Header kartu boleh wrap saat sempit (bukan tumpang tindih) */
|
||||
.card-hd { flex-wrap: wrap; gap: 8px 12px; }
|
||||
|
||||
/* Teks/tautan panjang tidak menembus sel & kartu.
|
||||
PENTING: gunakan break-word (BUKAN anywhere) — "anywhere" membuat min-content sel = 1 huruf
|
||||
sehingga kolom tabel menyusut & teks jadi vertikal di layar sempit (1024px dsb). */
|
||||
table td, .dtbl td, .table td, .tbl td, .sc-val, .sc-label, .sc-sub, .card-bd { overflow-wrap: break-word; }
|
||||
|
||||
/* Modal: cegah min-content 1 huruf (teks vertikal) di header & body modal */
|
||||
.mo .mbody, .mo .mh .mt { overflow-wrap: break-word; }
|
||||
|
||||
/* Tipografi fluid untuk elemen utama (clamp) */
|
||||
.page-title { font-size: clamp(17px, 16px + 0.6vw, 22px); }
|
||||
.sc-val { font-size: clamp(17px, 16px + 0.7vw, 22px); line-height: 1.15; }
|
||||
.card-hd { font-size: clamp(12.5px, 12px + 0.3vw, 14px); }
|
||||
}
|
||||
@@ -0,0 +1,895 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg2: #161b22;
|
||||
--bg3: #21262d;
|
||||
--border: rgba(240,246,252,0.1);
|
||||
--text: #e6edf3;
|
||||
--muted: #7d8590;
|
||||
--primary: #6366f1;
|
||||
--pdim: rgba(99,102,241,0.15);
|
||||
--success: #3fb950;
|
||||
--sdim: rgba(63,185,80,0.15);
|
||||
--warning: #d29922;
|
||||
--wdim: rgba(210,153,34,0.15);
|
||||
--danger: #f85149;
|
||||
--ddim: rgba(248,81,73,0.15);
|
||||
--info: #58a6ff;
|
||||
--idim: rgba(88,166,255,0.15);
|
||||
--panel2: #1c222c;
|
||||
--sw: 240px;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;font-size:14px}
|
||||
|
||||
/* ── SIDEBAR ── */
|
||||
.sidebar{width:var(--sw);height:100vh;background:var(--bg2);border-right:1px solid var(--border);position:fixed;left:0;top:0;display:flex;flex-direction:column;z-index:100;overflow-y:auto;scrollbar-width:thin}
|
||||
.sb-brand{padding:18px 16px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--border)}
|
||||
.sb-icon{width:38px;height:38px;background:linear-gradient(135deg,#6366f1,#818cf8);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
|
||||
.sb-name{font-size:13px;font-weight:700;line-height:1.3;color:var(--text)}
|
||||
.sb-sub{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
||||
.sb-nav{flex:1;padding:12px 0 40px}
|
||||
.sb-section{font-size:10px;font-weight:600;color:var(--muted);letter-spacing:1px;text-transform:uppercase;padding:14px 16px 5px}
|
||||
.sb-item{display:flex;align-items:center;gap:10px;padding:9px 16px;color:var(--muted);text-decoration:none;font-size:13px;font-weight:500;border-left:3px solid transparent;transition:all .2s;margin:1px 0}
|
||||
.sb-item:hover{color:var(--text);background:rgba(255,255,255,.04)}
|
||||
.sb-item.active{color:var(--primary);background:var(--pdim);border-left-color:var(--primary)}
|
||||
.sb-item.logout{color:var(--danger)}
|
||||
.sb-item.logout:hover{background:var(--ddim)}
|
||||
.sb-item i{font-size:15px;width:18px;text-align:center}
|
||||
|
||||
/* ── MAIN ── */
|
||||
.mw{margin-left:var(--sw);flex:1;height:100vh;overflow-y:auto;overflow-x:auto;display:flex;flex-direction:column}
|
||||
.topbar{background:var(--bg2);border-bottom:1px solid var(--border);padding:13px 22px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:50}
|
||||
.tb-title{font-size:15px;font-weight:600;display:flex;align-items:center;gap:8px}
|
||||
.tb-title i{color:var(--primary)}
|
||||
.tb-right{display:flex;align-items:center;gap:10px}
|
||||
.adm-badge{background:var(--pdim);color:var(--primary);padding:4px 12px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.page{padding:22px;flex:1}
|
||||
|
||||
/* ── STAT GRID ── */
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:22px}
|
||||
.sc{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:18px;position:relative;overflow:hidden;transition:transform .2s}
|
||||
.sc:hover{transform:translateY(-2px)}
|
||||
.sc::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
||||
.sc.p::before{background:linear-gradient(90deg,#6366f1,#818cf8)}
|
||||
.sc.s::before{background:linear-gradient(90deg,#3fb950,#4ade80)}
|
||||
.sc.w::before{background:linear-gradient(90deg,#d29922,#f59e0b)}
|
||||
.sc.d::before{background:linear-gradient(90deg,#f85149,#fb7185)}
|
||||
.sc.i::before{background:linear-gradient(90deg,#58a6ff,#38bdf8)}
|
||||
.sc-icon{position:absolute;top:14px;right:14px;font-size:26px;opacity:.12}
|
||||
.sc-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
|
||||
.sc-val{font-size:26px;font-weight:700;line-height:1}
|
||||
.sc-sub{font-size:11px;color:var(--muted);margin-top:3px}
|
||||
|
||||
/* ── CARD ── */
|
||||
.card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-bottom:18px}
|
||||
.card-hd{padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
||||
.card-hd h6{font-size:13px;font-weight:600;display:flex;align-items:center;gap:7px;margin:0}
|
||||
.card-hd h6 i{color:var(--primary)}
|
||||
.card-bd{padding:18px}
|
||||
|
||||
/* ── TABLE ── */
|
||||
.tbl-wrap{overflow-x:auto}
|
||||
.dtbl{width:100%;border-collapse:collapse;font-size:13px}
|
||||
.dtbl th{background:var(--bg3);padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;white-space:nowrap}
|
||||
.dtbl td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||||
.dtbl tr:hover td{background:rgba(255,255,255,.02)}
|
||||
.dtbl tr:last-child td{border-bottom:none}
|
||||
.dtbl .num{text-align:right}
|
||||
|
||||
/* ── BADGES ── */
|
||||
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.bs{background:var(--sdim);color:var(--success)}
|
||||
.bd{background:var(--ddim);color:var(--danger)}
|
||||
.bw{background:var(--wdim);color:var(--warning)}
|
||||
.bp{background:var(--pdim);color:var(--primary)}
|
||||
.bm{background:rgba(125,133,144,.12);color:var(--muted)}
|
||||
|
||||
/* ── BUTTONS ── */
|
||||
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;border:none;transition:all .2s;text-decoration:none;font-family:inherit}
|
||||
.btn:hover{opacity:.85;transform:translateY(-1px)}
|
||||
.btn-p{background:var(--primary);color:#fff}
|
||||
.btn-s{background:var(--success);color:#fff}
|
||||
.btn-d{background:var(--danger);color:#fff}
|
||||
.btn-w{background:var(--warning);color:#fff}
|
||||
.btn-g{background:rgba(255,255,255,.06);color:var(--text);border:1px solid var(--border)}
|
||||
.btn-sm{padding:5px 10px;font-size:12px}
|
||||
.btn-icon{padding:6px 8px;border-radius:7px}
|
||||
|
||||
/* ── FORMS ── */
|
||||
.fg{margin-bottom:14px}
|
||||
.fl{display:block;font-size:11px;font-weight:500;color:var(--muted);margin-bottom:5px;text-transform:uppercase;letter-spacing:.4px}
|
||||
.fc,.fs{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:9px 13px;color:var(--text);font-size:13px;font-family:inherit;outline:none;transition:border-color .2s}
|
||||
.fc:focus,.fs:focus{border-color:var(--primary);box-shadow:0 0 0 3px var(--pdim)}
|
||||
.fc::placeholder{color:var(--muted)}
|
||||
.fs option{background:var(--bg2)}
|
||||
textarea.fc{resize:vertical;min-height:75px}
|
||||
.form-row, .form-row2, .form-row3 {display:grid;gap:14px}
|
||||
.form-row{grid-template-columns:1fr 1fr}
|
||||
.form-row2{grid-template-columns:1fr 1fr}
|
||||
.form-row3{grid-template-columns:1fr 1fr 1fr}
|
||||
.settings-grid {display:grid;grid-template-columns:1fr 1fr;gap:24px}
|
||||
.settings-span-2 {grid-column:span 2}
|
||||
.sidebar-settings-save-grid {display:grid;grid-template-columns:minmax(260px,420px) 1fr;gap:12px;align-items:end}
|
||||
|
||||
/* ── SEARCH ROW ── */
|
||||
.srow{display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap}
|
||||
.sinput{position:relative;flex:1;min-width:180px}
|
||||
.sinput input{padding-left:34px}
|
||||
.sinput i{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:14px;pointer-events:none}
|
||||
|
||||
/* ── MODAL ── */
|
||||
.mo{display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;align-items:center;justify-content:center}
|
||||
.mo.show{display:flex}
|
||||
.mb{background:var(--bg2);border:1px solid var(--border);border-radius:14px;width:90%;max-width:520px;max-height:90vh;overflow-y:auto}
|
||||
.mh{padding:18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
||||
.mt{font-size:14px;font-weight:600}
|
||||
.mc{background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer;line-height:1}
|
||||
.mc:hover{color:var(--text)}
|
||||
.mbody{padding:18px}
|
||||
.mf{padding:14px 18px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px}
|
||||
|
||||
/* ── ALERT ── */
|
||||
.alert{padding:11px 15px;border-radius:8px;margin-bottom:14px;font-size:13px;display:flex;align-items:center;gap:8px}
|
||||
.alert-s{background:var(--sdim);color:var(--success);border:1px solid rgba(63,185,80,.3)}
|
||||
.alert-d{background:var(--ddim);color:var(--danger);border:1px solid rgba(248,81,73,.3)}
|
||||
.alert-w{background:var(--wdim);color:var(--warning);border:1px solid rgba(210,153,34,.3)}
|
||||
.alert-i{background:var(--idim);color:var(--info);border:1px solid rgba(88,166,255,.3)}
|
||||
|
||||
/* ── MISC ── */
|
||||
.currency::before{content:'Rp '}
|
||||
.text-right{text-align:right}
|
||||
.text-center{text-align:center}
|
||||
.text-muted{color:var(--muted)}
|
||||
.fw6{font-weight:600}
|
||||
.mb16{margin-bottom:16px}
|
||||
.flex{display:flex}.gap{gap:10px}.ai-c{align-items:center}.jc-sb{justify-content:space-between}
|
||||
.text-danger{color:var(--danger) !important}
|
||||
.text-success{color:var(--success) !important}
|
||||
.text-warning{color:var(--warning) !important}
|
||||
|
||||
/* ── DASHBOARD RESPONSIVE GRIDS ── */
|
||||
.dash-main-grid{display:grid;grid-template-columns:280px 1fr;gap:18px;margin-top:4px}
|
||||
.quick-actions-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
||||
|
||||
@media(max-width:768px){
|
||||
.dash-main-grid{grid-template-columns:1fr}
|
||||
.quick-actions-grid{grid-template-columns:repeat(2,1fr)}
|
||||
}
|
||||
|
||||
/* ── MOBILE FIXES ── */
|
||||
*{-webkit-tap-highlight-color:transparent}
|
||||
html,body{width:100%}
|
||||
|
||||
.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:90;backdrop-filter:blur(2px)}
|
||||
.sidebar-overlay.show{display:block}
|
||||
.sb-close{display:none}
|
||||
.hb-menu{display:none;background:none;border:none;color:var(--text);font-size:20px;cursor:pointer;padding:5px;line-height:1}
|
||||
|
||||
/* ── BOTTOM NAV ── */
|
||||
.bottom-nav{
|
||||
display:none;
|
||||
position:fixed;bottom:0;left:0;right:0;
|
||||
width:100%;
|
||||
background:var(--bg2);
|
||||
border-top:1px solid var(--border);
|
||||
z-index:300;
|
||||
padding:6px 0 calc(6px + env(safe-area-inset-bottom));
|
||||
gap:0;
|
||||
backdrop-filter:blur(20px);
|
||||
-webkit-backdrop-filter:blur(20px);
|
||||
}
|
||||
.bn-item{
|
||||
flex:1;display:flex;flex-direction:column;align-items:center;
|
||||
gap:3px;padding:6px 4px;
|
||||
color:var(--muted);text-decoration:none;
|
||||
font-size:9px;font-weight:600;letter-spacing:.3px;
|
||||
text-transform:uppercase;transition:color .2s;border:none;background:none;cursor:pointer;
|
||||
}
|
||||
.bn-item i{font-size:20px;line-height:1}
|
||||
.bn-item.active{color:var(--primary)}
|
||||
.bn-item:hover{color:var(--text)}
|
||||
|
||||
@media(max-width:992px){
|
||||
:root { --sw: 0px; }
|
||||
.sidebar{width:260px;transform:translateX(-100%);transition:transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);box-shadow: 10px 0 30px rgba(0,0,0,0.5);}
|
||||
.sidebar.open{transform:translateX(0);z-index:400}
|
||||
.sidebar.open ~ .bottom-nav{display:none !important}
|
||||
.mw{margin-left:0}
|
||||
.sb-close{display:flex}
|
||||
.hb-menu{display:block}
|
||||
.stat-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.settings-grid{grid-template-columns:1fr;gap:16px}
|
||||
.settings-span-2{grid-column:span 1}
|
||||
}
|
||||
|
||||
@media(max-width:768px){
|
||||
.bottom-nav{display:flex}
|
||||
.mw{padding-bottom:70px}
|
||||
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.filter-bar{gap:8px}
|
||||
.form-row, .form-row2, .form-row3{grid-template-columns:1fr}
|
||||
.sidebar-settings-save-grid{grid-template-columns:1fr;align-items:stretch}
|
||||
}
|
||||
|
||||
@media(max-width:576px){
|
||||
.stat-grid{grid-template-columns:1fr 1fr}
|
||||
.topbar{padding:10px 14px}
|
||||
.page{padding:12px}
|
||||
.tb-title{font-size:13px}
|
||||
.adm-badge{display:none}
|
||||
.filter-bar{flex-direction:column;align-items:stretch}
|
||||
.filter-bar .sinput,.filter-bar select{max-width:none !important;width:100% !important}
|
||||
/* table font smaller on phone */
|
||||
.dtbl{font-size:12px}
|
||||
.dtbl th,.dtbl td{padding:9px 10px}
|
||||
/* modal full width */
|
||||
.mb{width:96%;border-radius:16px 16px 0 0;position:fixed;bottom:0;left:0;right:0;max-height:92vh;margin:0 auto}
|
||||
.mo.show{align-items:flex-end}
|
||||
}
|
||||
|
||||
/* ── LIGHT THEME COMPREHENSIVE OVERRIDES ── */
|
||||
:root[data-theme="light"],
|
||||
html[data-theme="light"],
|
||||
html.light-theme,
|
||||
body.light-theme,
|
||||
[data-theme="light"] {
|
||||
--bg: #f8fafc;
|
||||
--bg2: #ffffff;
|
||||
--bg3: #f1f5f9;
|
||||
--border: rgba(0, 0, 0, 0.12);
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--primary: #4f46e5;
|
||||
--pdim: rgba(79, 70, 229, 0.12);
|
||||
--success: #16a34a;
|
||||
--sdim: rgba(22, 163, 74, 0.12);
|
||||
--warning: #d97706;
|
||||
--wdim: rgba(217, 119, 6, 0.12);
|
||||
--danger: #dc2626;
|
||||
--ddim: rgba(220, 38, 38, 0.12);
|
||||
--info: #0284c7;
|
||||
--idim: rgba(2, 132, 199, 0.12);
|
||||
--panel2: #f8fafc;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body,
|
||||
html[data-theme="light"] .mw,
|
||||
html[data-theme="light"] .page,
|
||||
html[data-theme="light"] .main-content,
|
||||
html[data-theme="light"] .content-wrapper,
|
||||
html[data-theme="light"] main {
|
||||
background-color: #f8fafc !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .sidebar,
|
||||
html[data-theme="light"] .topbar,
|
||||
html[data-theme="light"] .card,
|
||||
html[data-theme="light"] .mb,
|
||||
html[data-theme="light"] .mb-card,
|
||||
html[data-theme="light"] .bottom-nav,
|
||||
html[data-theme="light"] .modal-content,
|
||||
html[data-theme="light"] .modal-body,
|
||||
html[data-theme="light"] .modal-header,
|
||||
html[data-theme="light"] .modal-footer,
|
||||
html[data-theme="light"] .sc,
|
||||
html[data-theme="light"] .sc-card,
|
||||
html[data-theme="light"] .stat-card,
|
||||
html[data-theme="light"] .stat-card-v2,
|
||||
html[data-theme="light"] .box {
|
||||
background-color: #ffffff !important;
|
||||
color: #0f172a !important;
|
||||
border-color: rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .card-hd,
|
||||
html[data-theme="light"] .mh,
|
||||
html[data-theme="light"] .mf,
|
||||
html[data-theme="light"] .sb-brand,
|
||||
html[data-theme="light"] .topbar {
|
||||
border-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .sb-name,
|
||||
html[data-theme="light"] .tb-title,
|
||||
html[data-theme="light"] .card-hd h6,
|
||||
html[data-theme="light"] .mt,
|
||||
html[data-theme="light"] h1, html[data-theme="light"] h2, html[data-theme="light"] h3,
|
||||
html[data-theme="light"] h4, html[data-theme="light"] h5, html[data-theme="light"] h6,
|
||||
html[data-theme="light"] .sc-val,
|
||||
html[data-theme="light"] strong,
|
||||
html[data-theme="light"] b {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .fc,
|
||||
html[data-theme="light"] .fs,
|
||||
html[data-theme="light"] input,
|
||||
html[data-theme="light"] select,
|
||||
html[data-theme="light"] textarea,
|
||||
html[data-theme="light"] .form-control,
|
||||
html[data-theme="light"] .form-select {
|
||||
background-color: #ffffff !important;
|
||||
color: #0f172a !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] select option {
|
||||
background-color: #ffffff !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .dtbl,
|
||||
html[data-theme="light"] .table,
|
||||
html[data-theme="light"] table {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .dtbl th,
|
||||
html[data-theme="light"] .table th,
|
||||
html[data-theme="light"] table th {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #475569 !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .dtbl td,
|
||||
html[data-theme="light"] .table td,
|
||||
html[data-theme="light"] table td {
|
||||
background-color: #ffffff !important;
|
||||
color: #0f172a !important;
|
||||
border-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .dtbl tr:hover td,
|
||||
html[data-theme="light"] .table tr:hover td,
|
||||
html[data-theme="light"] table tr:hover td {
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .router-selector,
|
||||
html[data-theme="light"] .filter-bar {
|
||||
background-color: #f1f5f9 !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .btn-g {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .sb-item {
|
||||
color: #475569 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .sb-item:hover {
|
||||
color: #0f172a !important;
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .sb-item.active {
|
||||
color: #4f46e5 !important;
|
||||
background-color: rgba(79, 70, 229, 0.1) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .mo,
|
||||
html[data-theme="light"] .modal-backdrop {
|
||||
background: rgba(15, 23, 42, 0.5) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] pre,
|
||||
html[data-theme="light"] code {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
UNIVERSAL PRINT STYLESHEET (ADMIN & MANAGEMENT PORTALS)
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
@media print {
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 10mm 12mm 12mm 12mm;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
background: transparent !important;
|
||||
color: #000000 !important;
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: auto !important;
|
||||
min-height: auto !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
background: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Arial, sans-serif !important;
|
||||
font-size: 10pt !important;
|
||||
line-height: 1.35 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Hide Navigation, Sidebars, Action Buttons & Modals */
|
||||
.sidebar,
|
||||
.topbar,
|
||||
.hb-menu,
|
||||
.btn,
|
||||
.btn-p,
|
||||
.btn-s,
|
||||
.btn-d,
|
||||
.btn-w,
|
||||
.btn-g,
|
||||
.btn-icon,
|
||||
.srow,
|
||||
.sinput,
|
||||
.search-box,
|
||||
.pagination,
|
||||
.alert,
|
||||
.mo,
|
||||
.modal,
|
||||
.modal-backdrop,
|
||||
.no-print,
|
||||
.router-selector,
|
||||
.filter-bar,
|
||||
.filter-group,
|
||||
.nav-tabs,
|
||||
.tab-buttons,
|
||||
.floating-btn,
|
||||
.action-col,
|
||||
th.action-col,
|
||||
td.action-col,
|
||||
th:last-child:not(.print-keep),
|
||||
td:last-child:not(.print-keep),
|
||||
.card-hd .btn,
|
||||
.card-hd a,
|
||||
.adm-badge,
|
||||
.leaflet-control-container,
|
||||
#picker-map,
|
||||
form select,
|
||||
form input[type="text"]:not(.print-val),
|
||||
form button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Main Wrapper Expansion */
|
||||
.mw {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Card & Containers */
|
||||
.card {
|
||||
background: #ffffff !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: none !important;
|
||||
margin-bottom: 14px !important;
|
||||
page-break-inside: avoid !important;
|
||||
break-inside: avoid !important;
|
||||
}
|
||||
|
||||
.card-hd {
|
||||
padding: 10px 14px !important;
|
||||
background: #f8fafc !important;
|
||||
border-bottom: 1.5px solid #cbd5e1 !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
.card-hd h6 {
|
||||
font-size: 11pt !important;
|
||||
font-weight: bold !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
.card-bd {
|
||||
padding: 12px 14px !important;
|
||||
}
|
||||
|
||||
/* Stat Grids */
|
||||
.stat-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(4, 1fr) !important;
|
||||
gap: 8px !important;
|
||||
margin-bottom: 14px !important;
|
||||
page-break-inside: avoid !important;
|
||||
break-inside: avoid !important;
|
||||
}
|
||||
|
||||
.sc {
|
||||
background: #ffffff !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 10px !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.sc::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sc-val {
|
||||
font-size: 14pt !important;
|
||||
font-weight: bold !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
.sc-label {
|
||||
font-size: 8.5pt !important;
|
||||
color: #475569 !important;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.tbl-wrap {
|
||||
overflow: visible !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table, .dtbl, .table {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
font-size: 8.5pt !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table th, .dtbl th, .table th {
|
||||
background: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
padding: 6px 8px !important;
|
||||
font-size: 8pt !important;
|
||||
font-weight: bold !important;
|
||||
text-transform: uppercase !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
table td, .dtbl td, .table td {
|
||||
background: #ffffff !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
padding: 5px 8px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
table thead, .dtbl thead {
|
||||
display: table-header-group !important;
|
||||
}
|
||||
|
||||
table tr, .dtbl tr {
|
||||
page-break-inside: avoid !important;
|
||||
break-inside: avoid !important;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) td, .dtbl tr:nth-child(even) td {
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
/* Badges & Status */
|
||||
.badge, .status-badge {
|
||||
border: 1px solid #475569 !important;
|
||||
background: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
font-size: 7.5pt !important;
|
||||
font-weight: bold !important;
|
||||
padding: 1px 6px !important;
|
||||
border-radius: 3px !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Page Break Helpers */
|
||||
.page-break {
|
||||
page-break-before: always !important;
|
||||
break-before: page !important;
|
||||
}
|
||||
|
||||
.avoid-break {
|
||||
page-break-inside: avoid !important;
|
||||
break-inside: avoid !important;
|
||||
}
|
||||
|
||||
/* Auto Print Header */
|
||||
.print-only {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.print-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
Bootstrap-utility shim — app hanya memuat bootstrap-icons, bukan
|
||||
Bootstrap CSS. Ini melengkapi utilitas yang dipakai di view.
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.d-flex{display:flex}.d-inline-flex{display:inline-flex}.d-block{display:block}.d-inline{display:inline}
|
||||
.flex-column{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}
|
||||
.justify-content-between{justify-content:space-between}.justify-content-center{justify-content:center}.justify-content-end{justify-content:flex-end}.justify-content-start{justify-content:flex-start}
|
||||
.align-items-center{align-items:center}.align-items-start{align-items:flex-start}.align-items-end{align-items:flex-end}
|
||||
.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:16px}
|
||||
.m-0{margin:0}.mt-1{margin-top:4px}.mt-2{margin-top:8px}.mt-3{margin-top:16px}
|
||||
.mb-0{margin-bottom:0}.mb-1{margin-bottom:4px}.mb-2{margin-bottom:8px}.mb-3{margin-bottom:16px}.mb-4{margin-bottom:24px}
|
||||
.me-1{margin-right:4px}.me-2{margin-right:8px}.me-3{margin-right:16px}
|
||||
.ms-1{margin-left:4px}.ms-2{margin-left:8px}.ms-3{margin-left:16px}
|
||||
.mx-auto{margin-left:auto;margin-right:auto}
|
||||
.p-2{padding:8px}.p-3{padding:16px}
|
||||
.px-2{padding-left:8px;padding-right:8px}.px-3{padding-left:16px;padding-right:16px}
|
||||
.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:4px;padding-bottom:4px}.py-2{padding-top:8px;padding-bottom:8px}
|
||||
.w-100{width:100%}
|
||||
.fw-bold,.font-weight-bold{font-weight:700}
|
||||
.text-start{text-align:left}.text-end{text-align:right}
|
||||
.text-primary{color:var(--primary)!important}.text-info{color:var(--info)!important}
|
||||
.row{display:flex;flex-wrap:wrap}
|
||||
.col-4{flex:0 0 33.3333%;max-width:33.3333%}.col-6{flex:0 0 50%;max-width:50%}.col-12{flex:0 0 100%;max-width:100%}
|
||||
@media(max-width:560px){.col-6{flex:0 0 100%;max-width:100%}}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
Modal global — rapi & responsif (berlaku untuk semua .mo)
|
||||
Header & footer sticky, isi (.mbody) scroll, mobile = bottom-sheet.
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.mo .mb{display:flex;flex-direction:column;overflow-x:hidden}
|
||||
.mo .mh{position:sticky;top:0;z-index:6;background:var(--bg2);gap:10px;flex-wrap:nowrap;align-items:center}
|
||||
.mo .mh .mt{line-height:1.3;min-width:0;flex:1;overflow-wrap:anywhere;display:flex;align-items:center;gap:8px}
|
||||
.mo .mh .mc{flex:0 0 auto;width:34px;height:34px;border-radius:9px;display:grid;place-items:center}
|
||||
.mo .mh .mc:hover{background:var(--bg3);color:var(--text)}
|
||||
.mo .mf{position:sticky;bottom:0;z-index:6;background:var(--bg2);flex-wrap:wrap}
|
||||
.mo .mbody{overflow-wrap:anywhere}
|
||||
.mo .mbody img{max-width:100%;height:auto}
|
||||
.mo .mb .card{box-shadow:none}
|
||||
|
||||
@media(max-width:768px){
|
||||
.mo{align-items:flex-end}
|
||||
.mo .mb{width:100%;max-width:100% !important;border-radius:18px 18px 0 0;max-height:94vh;padding-bottom:env(safe-area-inset-bottom,0)}
|
||||
.mo .mh{border-radius:18px 18px 0 0;padding:15px 16px}
|
||||
.mo .mbody{padding:14px 16px}
|
||||
.mo .mf{padding:12px 16px;flex-direction:column;align-items:stretch;gap:8px;border-radius:0}
|
||||
.mo .mf form{width:100%;display:block}
|
||||
.mo .mf .btn,.mo .mf button,.mo .mf a{width:100%;justify-content:center;text-align:center}
|
||||
.mo .mh .mt{font-size:13.5px}
|
||||
}
|
||||
|
||||
/* modal: hilangkan gap sticky footer (safe-area dipindah ke footer) */
|
||||
.mo .mb{padding:0}
|
||||
@media(max-width:768px){ .mo .mf{padding:12px 16px calc(12px + env(safe-area-inset-bottom,0))} }
|
||||
|
||||
/* paksa: tidak ada padding pada kontainer modal (footer flush ke dasar) */
|
||||
.mo .mb{padding:0 !important}
|
||||
@media(max-width:768px){ .mo .mb{padding-bottom:0 !important} .mo .mf{padding:12px 16px calc(12px + env(safe-area-inset-bottom,0)) !important} }
|
||||
|
||||
|
||||
/* ══ Bagian A: teks panjang & tabel di mobile ═══════════════════════
|
||||
- .dtbl td: break-word (bukan anywhere) → kolom tak menyusut 1 huruf
|
||||
- tabel mobile: min-width + wrapper scroll horizontal (geser kanan-kiri)
|
||||
- kolom kode/status/angka: nowrap agar tidak pecah
|
||||
- kartu KPI: beri ruang ikon + angka panjang tidak terpotong
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
.dtbl td{overflow-wrap:break-word}
|
||||
.dtbl td code,.dtbl td .badge,.dtbl td.num,.dtbl td .mono,.dtbl td .text-nowrap{white-space:nowrap}
|
||||
.dtbl th{white-space:nowrap}
|
||||
|
||||
.sc{padding-right:46px}
|
||||
.sc-val{overflow-wrap:break-word;line-height:1.15}
|
||||
|
||||
@media(max-width:768px){
|
||||
.dtbl{min-width:680px}
|
||||
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.tbl-wrap::-webkit-scrollbar{height:8px}
|
||||
.tbl-wrap::-webkit-scrollbar-thumb{background:var(--border);border-radius:8px}
|
||||
}
|
||||
@media(max-width:576px){
|
||||
.sc{padding:16px 42px 16px 16px}
|
||||
.sc-icon{font-size:22px;top:12px;right:12px}
|
||||
.sc-val{font-size:22px}
|
||||
.dtbl{min-width:640px}
|
||||
}
|
||||
|
||||
|
||||
/* Bagian A lanjutan: semua wrapper tabel + min-width (geser horizontal) */
|
||||
.tbl-wrap,.table-container,.table-responsive,.table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
@media(max-width:768px){
|
||||
.dtbl,
|
||||
.tbl-wrap > table,
|
||||
.table-container > table,
|
||||
.table-responsive > table,
|
||||
.table-wrap > table{min-width:680px}
|
||||
}
|
||||
|
||||
|
||||
/* Bagian A lanjutan: topbar & kartu KPI di mobile */
|
||||
@media(max-width:768px){
|
||||
.topbar{flex-wrap:wrap;gap:8px 10px;row-gap:8px}
|
||||
.tb-title{min-width:0;flex:1 1 100%;order:-1}
|
||||
.tb-right{flex-wrap:wrap;justify-content:flex-start;gap:8px;flex:1 1 100%}
|
||||
.realtime-bar{display:none}
|
||||
.stat-grid,.stat-grid-3,.stat-grid-4{grid-template-columns:repeat(2,1fr) !important}
|
||||
}
|
||||
|
||||
|
||||
/* ══ Bagian B: kolom pertama sticky saat tabel digeser (mobile) ══════ */
|
||||
@media(max-width:768px){
|
||||
.dtbl th:first-child, .dtbl td:first-child{
|
||||
position:sticky; left:0; z-index:3;
|
||||
background:var(--bg2);
|
||||
box-shadow:1px 0 0 var(--border);
|
||||
}
|
||||
.dtbl th:first-child{ background:var(--bg3); z-index:4; }
|
||||
.dtbl tr:hover td:first-child{ background:var(--bg2); }
|
||||
/* tabel di dalam wrapper khusus (non-.dtbl) */
|
||||
.tbl-wrap > table th:first-child, .tbl-wrap > table td:first-child{
|
||||
position:sticky; left:0; z-index:3; background:var(--bg2); box-shadow:1px 0 0 var(--border);
|
||||
}
|
||||
}
|
||||
html[data-theme="light"] .dtbl td:first-child,
|
||||
html.light-theme .dtbl td:first-child{ background:#fff; }
|
||||
html[data-theme="light"] .dtbl th:first-child,
|
||||
html.light-theme .dtbl th:first-child{ background:#f1f5f9; }
|
||||
|
||||
/* Bagian B2: Pelanggan -> sembunyikan # , sticky kolom Nama */
|
||||
@media(max-width:768px){
|
||||
.stick-name-mobile th:first-child, .stick-name-mobile td:first-child{ display:none !important; }
|
||||
.stick-name-mobile th:nth-child(2), .stick-name-mobile td:nth-child(2){
|
||||
position:sticky; left:0; z-index:3; background:var(--bg2); box-shadow:1px 0 0 var(--border);
|
||||
}
|
||||
.stick-name-mobile th:nth-child(2){ background:var(--bg3); z-index:4; }
|
||||
}
|
||||
html[data-theme="light"] .stick-name-mobile td:nth-child(2),html.light-theme .stick-name-mobile td:nth-child(2){background:#fff}
|
||||
|
||||
|
||||
/* ══ Bagian B3: sticky kolom pertama utk tabel non-.dtbl (ACS, Routers live) ══ */
|
||||
@media(max-width:768px){
|
||||
.table-container > table > thead > tr > th:first-child,
|
||||
.table-container > table > tbody > tr > td:first-child,
|
||||
.mt-tablewrap > table > thead > tr > th:first-child,
|
||||
.mt-tablewrap > table > tbody > tr > td:first-child{
|
||||
position:sticky !important; left:0; z-index:3;
|
||||
background:var(--bg2); box-shadow:1px 0 0 var(--border);
|
||||
}
|
||||
.table-container > table > thead > tr > th:first-child,
|
||||
.mt-tablewrap > table > thead > tr > th:first-child{ background:var(--bg3); z-index:4; }
|
||||
.mt-tablewrap > table{ min-width:680px; }
|
||||
}
|
||||
html[data-theme="light"] .table-container > table > tbody > tr > td:first-child,
|
||||
html.light-theme .table-container > table > tbody > tr > td:first-child{ background:#fff; }
|
||||
html[data-theme="light"] .table-container > table > thead > tr > th:first-child,
|
||||
html.light-theme .table-container > table > thead > tr > th:first-child{ background:#f1f5f9; }
|
||||
|
||||
|
||||
/* ══ Sticky cell: latar SOLID (juga saat hover/highlight) ══════════════
|
||||
Hover baris = rgba(173,255,47,.035) di atas --bg2 → warna solid #161e22
|
||||
(terang: rgba(120,160,0,.06) di atas #fff → #f7f9f0). Sel sticky harus
|
||||
opak agar konten di bawahnya tidak tembus. */
|
||||
:root{ --row-hover-bg:#161e22; }
|
||||
html[data-theme="light"], html.light-theme, [data-theme="light"], body.light-theme{ --row-hover-bg:#f7f9f0; }
|
||||
|
||||
@media(max-width:768px){
|
||||
.dtbl tbody tr:hover > td:first-child,
|
||||
.dtbl tbody tr:hover > td:first-child,
|
||||
.stick-name-mobile tbody tr:hover > td:nth-child(2),
|
||||
.tbl-wrap > table > tbody > tr:hover > td:first-child,
|
||||
.table-container > table > tbody > tr:hover > td:first-child,
|
||||
.mt-tablewrap > table > tbody > tr:hover > td:first-child{
|
||||
background: var(--row-hover-bg) !important;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
}
|
||||
/* desktop juga: sel sticky (mis. Pelanggan nama) solid saat hover */
|
||||
.stick-name-mobile tbody tr:hover > td:nth-child(2){ background: var(--row-hover-bg) !important; }
|
||||
|
||||
|
||||
/* ══ Baris tercentang (checkbox baris pertama) → warna beda ═══════════
|
||||
Dark: lebih TERANG (2–3x). Light: lebih GELAP. Termasuk sel sticky. */
|
||||
:root{ --row-checked-bg:#2f3b4a; } /* lebih terang dari --bg2 #161b22 */
|
||||
html[data-theme="light"], html.light-theme, [data-theme="light"], body.light-theme{ --row-checked-bg:#d7dfe7; } /* lebih gelap dari #fff */
|
||||
|
||||
.dtbl tbody tr:has(> td:first-child input[type="checkbox"]:checked) > td,
|
||||
.tbl-wrap > table > tbody > tr:has(> td:first-child input[type="checkbox"]:checked) > td,
|
||||
.table-container > table > tbody > tr:has(> td:first-child input[type="checkbox"]:checked) > td,
|
||||
.table-responsive > table > tbody > tr:has(> td:first-child input[type="checkbox"]:checked) > td,
|
||||
.table-wrap > table > tbody > tr:has(> td:first-child input[type="checkbox"]:checked) > td{
|
||||
background: var(--row-checked-bg) !important;
|
||||
}
|
||||
/* pastikan sel sticky ikut (menang atas aturan sticky/hover) */
|
||||
.dtbl tbody tr:has(> td:first-child input[type="checkbox"]:checked) > td:first-child,
|
||||
.tbl-wrap > table > tbody > tr:has(> td:first-child input[type="checkbox"]:checked) > td:first-child,
|
||||
.table-container > table > tbody > tr:has(> td:first-child input[type="checkbox"]:checked) > td:first-child{
|
||||
background: var(--row-checked-bg) !important;
|
||||
}
|
||||
|
||||
|
||||
/* ══ Topbar mobile: tinggi KONSISTEN (judul 1 baris + aksi 1 baris scroll) ══ */
|
||||
@media(max-width:768px){
|
||||
.topbar{
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
column-gap: 10px;
|
||||
padding: 9px 14px;
|
||||
align-items: center;
|
||||
min-height: 0;
|
||||
}
|
||||
.topbar .tb-title{
|
||||
flex: 1 1 100%;
|
||||
order: -1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.topbar .tb-title .hb-menu{ flex: 0 0 auto; }
|
||||
.topbar .tb-right{
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 2px;
|
||||
min-height: 0;
|
||||
}
|
||||
.topbar .tb-right::-webkit-scrollbar{ display: none; }
|
||||
.topbar .tb-right > *{ flex: 0 0 auto; }
|
||||
.topbar .realtime-bar{ display: none; }
|
||||
}
|
||||
|
||||
|
||||
/* Topbar mobile: paksa 1 baris & tinggi seragam antar halaman */
|
||||
@media(max-width:768px){
|
||||
.topbar .tb-title{ min-height:34px; }
|
||||
.topbar .tb-right{
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
}
|
||||
.topbar .tb-right::-webkit-scrollbar{ display:none; }
|
||||
.topbar .tb-right > *{ flex: 0 0 auto; margin: 0; }
|
||||
/* seragamkan tinggi elemen aksi (tombol, select/router-selector, badge) */
|
||||
.topbar .tb-right .btn,
|
||||
.topbar .tb-right .sn-badge,
|
||||
.topbar .tb-right .router-selector{ height: 34px !important; box-sizing: border-box; display: inline-flex; align-items: center; }
|
||||
.topbar .tb-right .router-selector{ padding-top: 0 !important; padding-bottom: 0 !important; }
|
||||
.topbar .tb-right .fs{ height: 30px; box-sizing: border-box; padding-top: 0; padding-bottom: 0; }
|
||||
}
|
||||
|
||||
|
||||
/* ══ ACS (GenieACS) header & tab: rapi di mobile ══ */
|
||||
.acs-box .tab-bar{ padding:14px 14px 0 !important; margin:0 0 4px 0 !important; }
|
||||
@media(max-width:768px){
|
||||
.acs-head{ flex-wrap:wrap !important; gap:10px; margin-bottom:16px !important; }
|
||||
.acs-head .page-title{ font-size:22px !important; line-height:1.15; width:100%; }
|
||||
.acs-head > div:last-child{ display:flex; flex-wrap:wrap; gap:8px; width:100%; }
|
||||
.acs-head > div:last-child .btn{ flex:1 1 auto; justify-content:center; }
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
auth.css — Halaman autentikasi & publik (login, register, OTP, cek tagihan)
|
||||
Memakai token/warna dari landing.css (dimuat lebih dulu).
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── LAYOUT LOGIN ── */
|
||||
.auth-hero{padding:clamp(18px,3vw,42px) 0;min-height:calc(100vh - var(--nav-h));min-height:calc(100dvh - var(--nav-h));display:flex;align-items:center}
|
||||
.auth-hero > .container{width:min(1180px,92vw);margin-inline:auto}
|
||||
.auth-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(24px,4vw,56px);align-items:center}
|
||||
.auth-copy h1{font-size:clamp(30px,4.6vw,50px);margin:16px 0 12px}
|
||||
.auth-copy h1 .hl{color:var(--primary)}
|
||||
.auth-copy p.lead{color:var(--muted);font-size:clamp(15px,1.6vw,17px);max-width:52ch}
|
||||
.auth-points{list-style:none;padding:0;margin:22px 0 0;display:grid;gap:11px}
|
||||
.auth-points li{display:flex;gap:10px;color:var(--muted);font-size:14.5px}
|
||||
.auth-points li i{color:var(--primary);margin-top:3px}
|
||||
.auth-stats{display:flex;flex-wrap:wrap;gap:26px;margin-top:30px;padding-top:24px;border-top:1px solid var(--border)}
|
||||
.auth-stats .st b{display:block;font-family:var(--font-display);font-size:22px;line-height:1}
|
||||
.auth-stats .st span{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
|
||||
|
||||
/* ── KARTU LOGIN ── */
|
||||
.auth-card{background:var(--glass-2);backdrop-filter:blur(14px);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:clamp(20px,2.6vw,28px);box-shadow:var(--shadow-3);position:relative;overflow:hidden;max-width:430px;margin:0 auto;width:100%}
|
||||
.auth-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;
|
||||
background:linear-gradient(90deg,var(--primary),transparent 70%)}
|
||||
.auth-logo{width:56px;height:56px;border-radius:16px;display:grid;place-items:center;margin:0 auto 10px;
|
||||
background:var(--pdim);border:1px solid var(--primary-glow)}
|
||||
.auth-logo img{width:32px;height:32px;object-fit:contain}
|
||||
.auth-card h2{font-size:19px;text-align:center}
|
||||
.auth-card .sub{text-align:center;color:var(--muted);font-size:13px;margin-top:3px}
|
||||
|
||||
/* ── FORM ── */
|
||||
.field{margin-top:14px}
|
||||
.field label{display:flex;align-items:center;gap:7px;font-size:11.5px;font-weight:700;text-transform:uppercase;
|
||||
letter-spacing:.05em;color:var(--muted);margin-bottom:6px}
|
||||
.field .wrap{position:relative;display:flex;align-items:center}
|
||||
.field .wrap > i.lead-ic{position:absolute;left:14px;color:var(--muted);font-size:16px;pointer-events:none}
|
||||
.inp{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:12px 14px 12px 42px;
|
||||
color:var(--text);font:inherit;font-size:15px;outline:none;transition:border-color .18s var(--ease),box-shadow .18s var(--ease),background .18s var(--ease)}
|
||||
.inp::placeholder{color:var(--muted)}
|
||||
.inp:hover{border-color:var(--primary-glow)}
|
||||
.inp:focus{border-color:var(--primary);box-shadow:var(--ring);background:var(--bg2)}
|
||||
.hint-line{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:12px;margin-top:8px;justify-content:center}
|
||||
|
||||
.btn-auth{width:100%;margin-top:16px;padding:13px;font-size:15px;border-radius:12px}
|
||||
|
||||
/* ── ALERT ── */
|
||||
.auth-alert{display:flex;align-items:center;gap:9px;padding:11px 14px;border-radius:11px;font-size:13px;margin-top:14px;border:1px solid transparent;font-weight:600}
|
||||
.auth-alert.err{background:var(--ddim);color:var(--danger);border-color:var(--danger-glow)}
|
||||
.auth-alert.ok{background:var(--sdim);color:var(--success);border-color:var(--success-glow)}
|
||||
.auth-alert.info{background:var(--idim);color:var(--info);border-color:rgba(56,189,248,.3)}
|
||||
|
||||
/* ── QUICK TOOLS ── */
|
||||
.quick-tools{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
|
||||
.qtool{display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 10px;border-radius:12px;
|
||||
background:var(--ghost);border:1px solid var(--border);color:var(--text);font-size:12.5px;font-weight:600;
|
||||
text-decoration:none;transition:transform .18s var(--ease),border-color .18s var(--ease),background .18s var(--ease)}
|
||||
.qtool:hover{transform:translateY(-2px);border-color:var(--primary-glow);background:var(--ghost-h)}
|
||||
.qtool i{font-size:20px;color:var(--primary)}
|
||||
|
||||
.auth-foot{margin-top:16px;padding-top:14px;border-top:1px solid var(--border);text-align:center;font-size:13px;color:var(--muted)}
|
||||
.auth-foot a{font-weight:700}
|
||||
|
||||
/* anchor di halaman ini jangan tertutup header */
|
||||
#packages,#features,#login-section{scroll-margin-top:84px}
|
||||
|
||||
/* ── Sembunyikan statistik pada mode aplikasi (PWA/standalone) ── */
|
||||
html.is-pwa .auth-stats{display:none!important}
|
||||
@media(display-mode:standalone){ .auth-stats{display:none!important} }
|
||||
|
||||
/* ── FORM UMUM (register, dll) ── */
|
||||
.form-card{background:var(--glass-2);border:1px solid var(--border);border-radius:var(--radius);padding:clamp(20px,3vw,30px);box-shadow:var(--shadow-2)}
|
||||
.form-grid{display:grid;gap:14px}
|
||||
.form-grid.cols-2{grid-template-columns:1fr 1fr}
|
||||
.inp-plain{border-radius:12px;padding:12px 14px}
|
||||
textarea.inp,textarea.inp-plain{padding-left:14px;min-height:84px;resize:vertical}
|
||||
select.inp,select.inp-plain{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16' fill='%238A96A6'%3E%3Cpath d='M1.5 5.5 8 12l6.5-6.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:34px}
|
||||
.form-note{font-size:11.5px;color:var(--muted);margin-top:5px;line-height:1.4}
|
||||
.reg-wrap{padding:clamp(24px,4vw,52px) 0;display:flex;justify-content:center}
|
||||
.reg-wrap .form-card{width:100%;max-width:560px}
|
||||
.reg-head{text-align:center;margin-bottom:20px}
|
||||
.reg-head .auth-logo{margin-bottom:12px}
|
||||
.reg-head h1{font-size:22px}
|
||||
.reg-head p{color:var(--muted);font-size:13px;margin-top:3px}
|
||||
.loc-btns{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.chip-note{font-size:11.5px;color:var(--muted);margin-top:8px}
|
||||
|
||||
/* ── MODAL (auth) ── */
|
||||
.mo{display:none;position:fixed;inset:0;background:var(--overlay-bg);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:1000;align-items:center;justify-content:center;padding:16px}
|
||||
.mo.show{display:flex;animation:fadein .18s var(--ease)}
|
||||
.mb{background:var(--bg2);border:1px solid var(--border);border-radius:16px;width:100%;max-width:660px;max-height:90vh;overflow:auto;color:var(--text);box-shadow:var(--shadow-3)}
|
||||
.mh{padding:16px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
||||
.mh h3{font-size:15px;font-family:var(--font-display)}
|
||||
.mc{background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;line-height:1}
|
||||
.mc:hover{color:var(--danger)}
|
||||
.mbody{padding:18px}
|
||||
.mf{padding:14px 18px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px}
|
||||
#picker-map{height:320px;width:100%;border-radius:12px;overflow:hidden;background:var(--bg3)}
|
||||
|
||||
/* ── CEK TAGIHAN / PEMBAYARAN ── */
|
||||
.bill-wrap{padding:clamp(20px,3vw,40px) 0;display:flex;justify-content:center}
|
||||
.bill-card{width:100%;max-width:800px;background:var(--glass-2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-2);overflow:hidden}
|
||||
.bill-head{padding:18px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;background:var(--ghost)}
|
||||
.bill-head h1{font-size:17px}
|
||||
.bill-head .sub{color:var(--muted);font-size:12.5px}
|
||||
.bill-body{padding:20px}
|
||||
.search-row{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end}
|
||||
.search-row .field{flex:1;min-width:220px;margin-top:0}
|
||||
.btbl{width:100%;border-collapse:collapse;font-size:13px;color:var(--text)}
|
||||
.btbl th{padding:10px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);background:var(--ghost);border-bottom:1px solid var(--border);white-space:nowrap}
|
||||
.btbl td{padding:11px 12px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||||
.btbl tr:hover td{background:var(--ghost)}
|
||||
.btbl .num{text-align:right;font-variant-numeric:tabular-nums}
|
||||
.btbl tr:last-child td{border-bottom:none}
|
||||
.bill-sub{color:var(--muted);font-size:12px}
|
||||
.sec-title{font-weight:700;margin:18px 0 4px;display:flex;align-items:center;gap:8px}
|
||||
.sec-title i{color:var(--primary)}
|
||||
.srch-wrap{overflow-x:auto}
|
||||
.inv-radio{width:18px;height:18px;accent-color:var(--primary);cursor:pointer}
|
||||
.pay-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}
|
||||
@media(max-width:430px){.pay-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.pay-scroll{max-height:230px;overflow:auto;padding-right:4px}
|
||||
.pay-radio{position:absolute;opacity:0;pointer-events:none}
|
||||
.pay-item{display:flex;align-items:center;gap:12px;padding:12px;background:var(--ghost);border:1px solid var(--border);border-radius:14px;cursor:pointer;transition:transform .2s var(--ease),border-color .2s var(--ease),box-shadow .2s var(--ease);height:100%;min-width:0;overflow:hidden}
|
||||
.pay-item:hover{transform:translateY(-1px);border-color:var(--primary-glow)}
|
||||
.pay-radio:checked + .pay-item{border-color:var(--primary);box-shadow:0 0 0 1px var(--primary-glow)}
|
||||
.pay-icon{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;background:var(--bg);border:1px solid var(--border);flex:0 0 auto;overflow:hidden;font-size:19px;color:var(--primary)}
|
||||
.pay-logo{width:100%;height:100%;object-fit:contain;padding:7px;background:#fff}
|
||||
.pay-info{display:flex;flex-direction:column;min-width:0;flex:1}
|
||||
.pay-name{font-size:13.5px;font-weight:700;color:var(--text);word-break:break-word;line-height:1.25}
|
||||
.pay-sub{display:flex;flex-wrap:wrap;gap:5px;font-size:11px;color:var(--muted);margin-top:4px;min-width:0}
|
||||
.pay-chip{padding:2px 8px;border-radius:999px;border:1px solid var(--border);font-size:11px;font-weight:600;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
|
||||
.pay-chip.off{background:var(--ddim);color:var(--danger);border-color:var(--danger-glow)}
|
||||
@media(max-width:576px){
|
||||
.pay-item{flex-direction:column;align-items:center;text-align:center;gap:8px}
|
||||
.pay-info{align-items:center}
|
||||
.pay-sub{justify-content:center}
|
||||
.pay-name{font-size:13px}
|
||||
}
|
||||
.chk{display:flex;align-items:flex-start;gap:9px;font-size:13px;color:var(--muted);margin:14px 0}
|
||||
.chk input{width:18px;height:18px;accent-color:var(--primary);margin-top:2px;flex:0 0 auto}
|
||||
.stack{display:grid;gap:10px}
|
||||
.auth-alert.warn{background:var(--wdim);color:var(--warning);border-color:var(--warning-glow)}
|
||||
.bill-foot{margin-top:20px;padding-top:14px;border-top:1px solid var(--border);color:var(--muted);font-size:12px}
|
||||
|
||||
/* ── OTP ── */
|
||||
.otp-wrap{padding:clamp(24px,4vw,52px) 0;display:flex;justify-content:center}
|
||||
.otp-card{width:100%;max-width:420px;background:var(--glass-2);backdrop-filter:blur(14px);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-3);padding:clamp(22px,3vw,32px);text-align:center}
|
||||
.otp-icon{width:64px;height:64px;border-radius:18px;display:grid;place-items:center;margin:0 auto 14px;background:var(--pdim);border:1px solid var(--primary-glow);color:var(--primary);font-size:28px}
|
||||
.otp-card h1{font-size:20px}
|
||||
.otp-card .sub{color:var(--muted);font-size:13.5px;margin-top:6px}
|
||||
.otp-input{letter-spacing:16px;font-size:28px;font-weight:800;text-align:center;font-family:var(--font-mono);padding-left:16px;padding-right:6px}
|
||||
.otp-card form{margin-top:18px}
|
||||
.otp-back{margin-top:16px;font-size:13px}
|
||||
|
||||
/* ── VOUCHER ── */
|
||||
.vform{display:grid;grid-template-columns:1.1fr .9fr;gap:18px}
|
||||
@media(max-width:820px){.vform{grid-template-columns:1fr}}
|
||||
.voucher-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
|
||||
@media(max-width:576px){.voucher-grid{grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px}}
|
||||
.voucher-radio{position:absolute;opacity:0;pointer-events:none}
|
||||
.voucher-card{display:block;border:1px solid var(--border);background:var(--ghost);border-radius:14px;padding:12px;cursor:pointer;transition:transform .18s var(--ease),border-color .18s var(--ease),box-shadow .18s var(--ease)}
|
||||
.voucher-card:hover{transform:translateY(-1px);border-color:var(--primary-glow)}
|
||||
.voucher-radio:checked + .voucher-card{border-color:var(--primary);box-shadow:0 0 0 1px var(--primary-glow)}
|
||||
.voucher-title{font-weight:800;font-size:14px;display:flex;justify-content:space-between;gap:10px;align-items:center}
|
||||
.voucher-price{color:var(--primary);font-weight:800;white-space:nowrap}
|
||||
.voucher-meta{color:var(--muted);font-size:12px;margin-top:5px}
|
||||
.voucher-code{background:var(--bg);border:1px dashed var(--primary-glow);border-radius:12px;padding:12px;margin-top:12px}
|
||||
.voucher-code .row{display:flex;justify-content:space-between;gap:10px;padding:3px 0;font-size:14px}
|
||||
.voucher-code .row b{font-family:var(--font-mono);color:var(--text);letter-spacing:.5px}
|
||||
.label-cap{display:block;font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:7px}
|
||||
|
||||
/* ── PWA BANNER / MODAL ── */
|
||||
.pwa-banner{position:fixed;left:14px;right:14px;bottom:14px;z-index:200;display:none;align-items:center;gap:12px;
|
||||
background:var(--glass-2);backdrop-filter:blur(14px);border:1px solid var(--primary-glow);border-radius:14px;
|
||||
padding:12px 14px;box-shadow:var(--shadow-3)}
|
||||
.pwa-banner.show{display:flex}
|
||||
.pwa-banner .txt{flex:1;font-size:13px}
|
||||
.pwa-banner .txt b{display:block;font-size:13.5px}
|
||||
.pwa-banner .txt span{color:var(--muted);font-size:12px}
|
||||
|
||||
/* ── RESPONSIVE ── */
|
||||
@media(max-width:900px){
|
||||
.auth-grid{grid-template-columns:1fr}
|
||||
.auth-hero{min-height:auto;display:block}
|
||||
.auth-card{order:-1;margin-bottom:16px}
|
||||
.auth-copy{padding-top:0;display:flex;flex-direction:column}
|
||||
/* statistik (Pilihan Paket · Dukungan · Fiber Optic) tampil tepat di bawah kartu */
|
||||
.auth-stats{order:-1;margin:0 0 22px;padding:0;border-top:none;justify-content:center;gap:24px}
|
||||
.auth-stats .st{text-align:center}
|
||||
.auth-card{margin-inline:auto}
|
||||
}
|
||||
@media(max-width:560px){
|
||||
.form-grid.cols-2{grid-template-columns:1fr}
|
||||
.quick-tools{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
|
||||
/* ── Rapatkan teks (line-height) agar tidak terlalu renggang ── */
|
||||
body{line-height:1.42}
|
||||
.auth-copy h1{line-height:1.12;margin:14px 0 10px}
|
||||
.auth-copy p.lead{line-height:1.5;font-size:clamp(14.5px,1.55vw,16.5px)}
|
||||
.auth-points{gap:8px;margin-top:18px}
|
||||
.auth-points li{line-height:1.4}
|
||||
.auth-stats{margin-top:24px;padding-top:18px}
|
||||
.auth-card .sub{line-height:1.35}
|
||||
.hint-line{line-height:1.35}
|
||||
.section-head{max-width:620px;margin-bottom:clamp(22px,3vw,34px)}
|
||||
.section-head p{line-height:1.5;margin-top:9px}
|
||||
.feature h3{margin-bottom:5px}
|
||||
.feature p{line-height:1.45}
|
||||
.price-card li{line-height:1.4}
|
||||
.foot-links{gap:7px}
|
||||
.foot-links a{line-height:1.4}
|
||||
|
||||
/* == UI HARDENING (audit ui-ux.md) - jangan hapus marker == */
|
||||
img { max-width: 100%; height: auto; }
|
||||
@@ -0,0 +1,209 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
dashboard.css — Dashboard Pelanggan (PWA)
|
||||
Dipakai setelah landing.css + auth.css (token & komponen dasar dari sana).
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── APP BAR ── */
|
||||
.app-bar{position:sticky;top:0;z-index:100;background:var(--glass);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid var(--border)}
|
||||
.app-bar .inner{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 0;flex-wrap:wrap}
|
||||
.app-brand{display:flex;align-items:center;gap:9px;font-family:var(--font-display);font-weight:700;font-size:16px}
|
||||
.app-brand img{width:28px;height:28px;border-radius:8px}
|
||||
.app-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
|
||||
.chip-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:10px;border:1px solid var(--border);background:var(--ghost);color:var(--text);font-size:12.5px;font-weight:700;cursor:pointer;text-decoration:none;transition:.18s var(--ease)}
|
||||
.chip-btn:hover{border-color:var(--primary-glow);transform:translateY(-1px)}
|
||||
.chip-btn.ok{border-color:var(--success-glow);color:var(--success)}
|
||||
.chip-btn.danger{border-color:var(--danger-glow);color:var(--danger)}
|
||||
.chip-btn.wallet{border-color:var(--success-glow);color:var(--success)}
|
||||
|
||||
.status-pill{display:inline-flex;align-items:center;gap:7px;padding:5px 12px;border-radius:999px;font-size:11.5px;font-weight:700;border:1px solid var(--border)}
|
||||
.status-pill.online{background:var(--sdim);color:var(--success);border-color:var(--success-glow)}
|
||||
.status-pill.offline,.status-pill.suspended{background:var(--ddim);color:var(--danger);border-color:var(--danger-glow)}
|
||||
.pulse-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
||||
.pulse-dot.green{background:var(--success);animation:pulse-dot 1.8s infinite}
|
||||
.pulse-dot.red{background:var(--danger);animation:pulse-dot2 1.8s infinite}
|
||||
@keyframes pulse-dot2{0%{box-shadow:0 0 0 0 rgba(251,113,133,.55)}70%{box-shadow:0 0 0 7px rgba(251,113,133,0)}100%{box-shadow:0 0 0 0 rgba(251,113,133,0)}}
|
||||
|
||||
.notif-bell-btn{position:relative;width:38px;height:38px;display:grid;place-items:center;border-radius:10px;border:1px solid var(--border);background:var(--ghost);color:var(--text);cursor:pointer;font-size:16px}
|
||||
.notif-bell-btn:hover{border-color:var(--primary-glow);color:var(--primary)}
|
||||
.notif-badge-count{position:absolute;top:-5px;right:-5px;min-width:18px;height:18px;padding:0 4px;border-radius:999px;background:var(--danger);color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center}
|
||||
|
||||
/* ── LAYOUT ── */
|
||||
.dash-wrap{padding:18px 0 96px}
|
||||
.dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||
@media(max-width:860px){.dash-grid{grid-template-columns:1fr}}
|
||||
|
||||
/* ── PROMO (auto-rotate) ── */
|
||||
.promo-rot{position:relative;border-radius:var(--radius);overflow:hidden;border:1px solid var(--border);box-shadow:var(--shadow-2);background:var(--glass-2);margin-bottom:18px;min-height:130px}
|
||||
.promo-rot img{width:100%;aspect-ratio:16/6;object-fit:cover;display:block}
|
||||
.promo-rot a{display:block}
|
||||
.promo-rot .welcome{aspect-ratio:16/6;min-height:130px;display:flex;align-items:center;justify-content:center;gap:10px;color:#0A0D14;font-family:var(--font-display);font-weight:700;font-size:clamp(15px,2.4vw,20px);background:linear-gradient(135deg,var(--primary),#7BC21F)}
|
||||
.promo-rot .dots{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:6px}
|
||||
.promo-rot .dots span{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.5)}
|
||||
.promo-rot .dots span.on{background:var(--primary)}
|
||||
|
||||
/* ── HERO ── */
|
||||
.hero-banner{position:relative;border-radius:var(--radius);padding:18px;margin-bottom:16px;background:linear-gradient(135deg,var(--pdim),transparent 60%),var(--glass-2);border:1px solid var(--border);overflow:hidden}
|
||||
.hero-banner h3{font-size:clamp(17px,2.6vw,22px)}
|
||||
.id-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;background:var(--pdim);border:1px solid var(--primary-glow);color:var(--primary);font-size:11px;font-weight:700}
|
||||
|
||||
/* ── BILL HERO ── */
|
||||
.bill-hero-card{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;border-radius:var(--radius);padding:16px 18px;margin-bottom:16px;border:1px solid var(--danger-glow);background:linear-gradient(135deg,var(--ddim),transparent 65%),var(--glass-2)}
|
||||
.bill-hero-card.paid{border-color:var(--success-glow);background:linear-gradient(135deg,var(--sdim),transparent 65%),var(--glass-2)}
|
||||
.bill-hero-card .bico{width:52px;height:52px;border-radius:16px;display:grid;place-items:center;font-size:24px;flex:0 0 auto}
|
||||
.bill-hero-card.unpaid .bico{background:var(--wdim);color:var(--warning);border:1px solid var(--warning-glow)}
|
||||
.bill-hero-card .bico{background:var(--sdim);color:var(--success);border:1px solid var(--success-glow)}
|
||||
.bill-hero-card h5{font-family:var(--font-display);font-size:20px;margin:0}
|
||||
|
||||
/* ── ACTION GRID ── */
|
||||
.action-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:18px}
|
||||
@media(max-width:760px){.action-grid{grid-template-columns:repeat(3,1fr)}}
|
||||
.action-item{display:flex;flex-direction:column;align-items:center;gap:8px;padding:14px 8px;border-radius:14px;background:var(--glass-2);border:1px solid var(--border);cursor:pointer;text-decoration:none;transition:transform .18s var(--ease),border-color .18s var(--ease)}
|
||||
.action-item:hover{transform:translateY(-3px);border-color:var(--primary-glow)}
|
||||
.action-icon{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;font-size:20px;background:var(--ghost);color:var(--primary)}
|
||||
.action-icon.ai-ssid{color:var(--info)} .action-icon.ai-pass{color:var(--success)}
|
||||
.action-icon.ai-reboot{color:var(--warning)} .action-icon.ai-pay{color:var(--primary)}
|
||||
.action-icon.ai-ppob{color:#c084fc} .action-icon.ai-ticket{color:var(--danger)}
|
||||
.action-label{font-size:11.5px;font-weight:600;color:var(--text);text-align:center;line-height:1.25}
|
||||
|
||||
/* ── CARD (glass) ── */
|
||||
.glass-card{background:var(--glass-2);backdrop-filter:blur(10px);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-1);overflow:hidden;height:100%}
|
||||
.glass-header{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--border);background:var(--card-hd-bg)}
|
||||
.glass-header h5{font-size:14px;display:flex;align-items:center;gap:8px}
|
||||
.glass-body{padding:16px}
|
||||
.badge-soft{display:inline-flex;align-items:center;padding:3px 9px;border-radius:8px;font-size:10.5px;font-weight:700;background:var(--pdim);color:var(--primary);border:1px solid var(--primary-glow)}
|
||||
|
||||
/* ── STAT CARDS ── */
|
||||
.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
||||
.stat-card{background:var(--ghost);border:1px solid var(--border);border-radius:12px;padding:12px;text-align:center}
|
||||
.stat-icon{width:38px;height:38px;margin:0 auto 8px;border-radius:11px;display:grid;place-items:center;font-size:17px;background:var(--pdim);color:var(--primary)}
|
||||
.stat-icon.online{background:var(--sdim);color:var(--success)} .stat-icon.offline{background:var(--ddim);color:var(--danger)}
|
||||
.stat-icon.activity{background:var(--idim);color:var(--info)} .stat-icon.people{background:rgba(192,132,252,.14);color:#c084fc}
|
||||
.stat-label{font-size:10.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
|
||||
.stat-value{font-weight:800;font-size:14px;color:var(--text);margin-top:2px;word-break:break-word}
|
||||
|
||||
/* ── WIFI BOX ── */
|
||||
.wifi-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;padding:14px}
|
||||
.wifi-row{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}
|
||||
.wifi-label{font-size:12px;font-weight:700;display:flex;align-items:center;gap:6px}
|
||||
.wifi-val{font-weight:800;font-size:15px;color:var(--text);word-break:break-word}
|
||||
.link-btn{background:none;border:none;color:var(--primary);font-weight:700;font-size:12px;cursor:pointer;padding:2px 4px}
|
||||
.link-btn:hover{text-decoration:underline}
|
||||
|
||||
/* ── DEVICE LIST ── */
|
||||
.dev-list{display:flex;flex-direction:column;gap:8px;max-height:190px;overflow-y:auto}
|
||||
.dev-item{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:9px 11px;border-radius:10px;background:var(--ghost);border:1px solid var(--border);font-size:12px}
|
||||
.dev-empty{text-align:center;padding:12px;border-radius:10px;background:var(--ghost);color:var(--muted);font-size:12px}
|
||||
|
||||
/* ── TRAFFIC GAUGE ── */
|
||||
.traffic-gauge{position:relative;display:inline-block;margin-top:8px}
|
||||
.traffic-gauge canvas{max-width:100%}
|
||||
.traffic-gauge-center{position:absolute;left:0;right:0;bottom:18px;text-align:center}
|
||||
.traffic-gauge-value{font-family:var(--font-display);font-weight:700;font-size:22px;line-height:1}
|
||||
.traffic-gauge-unit{font-size:11px;color:var(--muted)}
|
||||
.traffic-gauge-scale{display:flex;justify-content:space-between;font-size:10px;color:var(--muted);margin-top:-6px}
|
||||
|
||||
/* ── SECTION TITLE ── */
|
||||
.sec-title{display:flex;align-items:center;gap:8px;font-family:var(--font-display);font-weight:700;font-size:15px;margin:0 0 12px}
|
||||
.sec-title i{color:var(--primary)}
|
||||
|
||||
/* ── INVOICE CARD (mobile) ── */
|
||||
.inv-card{background:var(--ghost);border:1px solid var(--border);border-radius:14px;padding:14px;margin-bottom:12px}
|
||||
.inv-card .inv-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
||||
.inv-card .inv-name{font-weight:700;color:var(--text)}
|
||||
.inv-card .inv-amt{font-family:var(--font-display);font-weight:800;font-size:18px;color:var(--primary)}
|
||||
.inv-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||||
|
||||
/* ── NOTIF ITEM ── */
|
||||
.notif-item{padding:12px;border-radius:12px;border:1px solid var(--border);background:var(--ghost);margin-bottom:10px}
|
||||
.notif-item.unread{border-color:var(--primary-glow);background:var(--pdim)}
|
||||
.notif-item .n-title{font-weight:700;font-size:13px}
|
||||
.notif-item .n-msg{color:var(--muted);font-size:12px;margin-top:3px}
|
||||
.notif-item .n-time{color:var(--muted);font-size:10.5px;margin-top:5px}
|
||||
|
||||
/* ── ACCORDION (details) ── */
|
||||
.acc{background:var(--glass-2);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:14px;overflow:hidden}
|
||||
.acc summary{list-style:none;cursor:pointer;padding:15px 16px;font-weight:700;font-size:14px;display:flex;justify-content:space-between;align-items:center;gap:10px}
|
||||
.acc summary::-webkit-details-marker{display:none}
|
||||
.acc summary .chev{transition:transform .25s var(--ease);color:var(--primary)}
|
||||
.acc[open] summary .chev{transform:rotate(180deg)}
|
||||
.acc .acc-body{padding:0 16px 16px}
|
||||
|
||||
/* ── BOTTOM NAV (partial kompatibel) ── */
|
||||
.customer-bottom-nav{display:none} /* diatur partial */
|
||||
|
||||
/* ── INFO LAYANAN ── */
|
||||
.svc-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
|
||||
@media(max-width:860px){.svc-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.svc-card{display:flex;gap:12px;align-items:center;padding:14px;border-radius:14px;background:var(--glass-2);border:1px solid var(--border);min-width:0}
|
||||
.svc-ic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;font-size:19px;background:var(--pdim);color:var(--primary);flex:0 0 auto}
|
||||
.svc-tx{min-width:0}
|
||||
.svc-tx > span{display:block;font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
|
||||
.svc-tx b{display:block;font-size:14.5px;color:var(--text);margin-top:2px}
|
||||
.svc-tx b.ok{color:var(--success)} .svc-tx b.warn{color:var(--warning)} .svc-tx b.off{color:var(--danger)}
|
||||
.svc-tx small{display:block;font-size:11px;color:var(--muted);margin-top:2px;word-break:break-word}
|
||||
|
||||
/* ── ONU TIDAK DITEMUKAN ── */
|
||||
.onu-missing{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border-radius:14px;margin-bottom:16px;background:var(--wdim);border:1px solid var(--warning-glow);color:var(--text)}
|
||||
.onu-missing i{color:var(--warning);font-size:20px;margin-top:2px}
|
||||
.onu-missing b{display:block;font-size:14px}
|
||||
.onu-missing span{display:block;font-size:12.5px;color:var(--muted);margin-top:3px}
|
||||
|
||||
/* ── FLOATING THEME TOGGLE ── */
|
||||
.theme-fab{position:fixed;right:16px;bottom:84px;z-index:1060;width:48px;height:48px;border-radius:50%;display:grid;place-items:center;border:1px solid var(--primary-glow);background:var(--glass-2);color:var(--primary);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);box-shadow:var(--shadow-2);cursor:pointer;font-size:20px;transition:transform .18s var(--ease),box-shadow .2s var(--ease)}
|
||||
.theme-fab:hover{transform:translateY(-2px);box-shadow:0 10px 26px var(--primary-glow)}
|
||||
@media(min-width:992px){.theme-fab{bottom:20px}}
|
||||
|
||||
/* ── BILL HERO MENCOLOK ── */
|
||||
.bill-hero-card{position:relative;overflow:hidden}
|
||||
.bill-hero-card.unpaid{border-color:rgba(251,191,36,0.55);background:linear-gradient(135deg,rgba(251,191,36,0.20),rgba(251,191,36,0.04) 62%),var(--bg2);box-shadow:0 0 0 1px rgba(251,191,36,0.30),0 16px 38px rgba(251,191,36,0.16)}
|
||||
.bill-hero-card.unpaid::after{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--warning),#FDE68A)}
|
||||
.bill-hero-card.paid{border-color:var(--success-glow);background:linear-gradient(135deg,rgba(52,211,153,0.14),rgba(52,211,153,0.03) 62%),var(--bg2)}
|
||||
.bill-hero-card.paid::after{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--success),#6EE7B7)}
|
||||
.bill-hero-card h5{font-size:clamp(21px,3.2vw,28px);letter-spacing:-0.01em}
|
||||
.bill-hero-card.unpaid .bico{background:var(--wdim);color:var(--warning);border:1px solid var(--warning-glow)}
|
||||
.lbtn-lg{padding:14px 22px;font-size:15px;border-radius:12px}
|
||||
|
||||
/* ── POPUP ONU ── */
|
||||
.onu-pop-ic{width:66px;height:66px;border-radius:50%;margin:0 auto 12px;display:grid;place-items:center;font-size:30px;background:var(--wdim);color:var(--warning);border:1px solid var(--warning-glow)}
|
||||
|
||||
/* ── NOTIFIKASI (desain lebih jelas) ── */
|
||||
.notif-item{position:relative;padding:14px 14px 14px 16px;border-radius:12px;border:1px solid var(--border);background:var(--bg);margin-bottom:10px;border-left:4px solid var(--info)}
|
||||
.notif-item.unread{border-color:var(--primary-glow);background:var(--pdim);border-left-color:var(--primary)}
|
||||
.notif-item .n-title{font-weight:700;font-size:13.5px;display:flex;align-items:center;gap:8px;color:var(--text)}
|
||||
.notif-item .n-msg{color:var(--muted);font-size:12.5px;margin-top:6px;line-height:1.5}
|
||||
.notif-item .n-time{color:var(--muted);font-size:10.5px;margin-top:8px;display:inline-flex;align-items:center;gap:5px}
|
||||
|
||||
/* ── COPY ID & CHIP ONU ── */
|
||||
.copy-mini{background:none;border:none;color:var(--primary);cursor:pointer;padding:1px 4px;font-size:12px;line-height:1;border-radius:6px}
|
||||
.copy-mini:hover{color:var(--primary-dark);background:var(--ghost)}
|
||||
.onu-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;background:var(--wdim);border:1px solid var(--warning-glow);color:var(--warning);font-size:11.5px;font-weight:700;cursor:pointer}
|
||||
.onu-chip:hover{filter:brightness(1.08)}
|
||||
|
||||
/* ── POPUP STATUS WIFI ── */
|
||||
.wifi-status-ic{width:66px;height:66px;border-radius:50%;margin:6px auto 12px;display:grid;place-items:center;font-size:30px}
|
||||
.wifi-status-ic.loading{background:var(--pdim);color:var(--primary)}
|
||||
.wifi-status-ic.success{background:var(--sdim);color:var(--success)}
|
||||
.wifi-status-ic.error{background:var(--ddim);color:var(--danger)}
|
||||
.spin{animation:spin 1s linear infinite;display:inline-block}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* ── TOAST ── */
|
||||
.toast-wrap{position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:1200;display:flex;flex-direction:column;gap:8px;width:min(420px,92vw);pointer-events:none}
|
||||
.toast{display:flex;align-items:center;gap:9px;padding:12px 14px;border-radius:12px;background:var(--glass-2);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);box-shadow:var(--shadow-2);font-size:13px;font-weight:600;color:var(--text);animation:toastin .25s var(--ease);pointer-events:auto}
|
||||
.toast i{font-size:17px}
|
||||
.toast.success{border-color:var(--success-glow)} .toast.success i{color:var(--success)}
|
||||
.toast.error{border-color:var(--danger-glow)} .toast.error i{color:var(--danger)}
|
||||
.toast.info i{color:var(--info)}
|
||||
.toast.out{opacity:0;transform:translateY(-6px)}
|
||||
@keyframes toastin{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* ── PEMBEDA: KARTU/INFO ↔ TOMBOL/TOGGLE ── */
|
||||
/* Kartu/info: permukaan tenang, bukan tombol */
|
||||
.glass-card, .svc-card, .stat-card{ box-shadow: var(--shadow-1); }
|
||||
.svc-card{ border-left: 3px solid var(--primary-glow); }
|
||||
/* Tombol/toggle/tile aksi: tegas, aksen lime, jelas bisa ditekan */
|
||||
.action-item{ background: var(--ghost); border: 1px solid var(--primary-glow); }
|
||||
.action-item .action-icon{ background: var(--pdim); color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary-glow); }
|
||||
.action-item:hover{ background: var(--pdim); border-color: var(--primary); transform: translateY(-3px); box-shadow: 0 12px 26px rgba(0,0,0,.28); }
|
||||
.chip-btn, .lbtn{ cursor: pointer; }
|
||||
.chip-btn:active, .lbtn:active, .action-item:active{ transform: translateY(0) scale(.98); }
|
||||
@@ -0,0 +1,85 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
isolated.css — Halaman Isolir (redirect MikroTik)
|
||||
Dipakai setelah landing.css + auth.css (token dasar).
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
.iso-wrap{padding:20px 0 44px;display:flex;flex-direction:column;align-items:center}
|
||||
.iso-card{width:100%;max-width:540px;background:var(--glass-2);backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-2);padding:22px;position:relative;overflow:hidden}
|
||||
.iso-card::before{content:'';position:absolute;top:0;left:0;right:0;height:4px}
|
||||
.iso-card.iso-danger::before{background:linear-gradient(90deg,var(--danger),#FDA4AF)}
|
||||
.iso-card.iso-success::before{background:linear-gradient(90deg,var(--success),#6EE7B7)}
|
||||
|
||||
.iso-brand{display:flex;align-items:center;gap:10px;justify-content:center;margin-bottom:16px}
|
||||
.iso-brand img{height:34px;width:34px;border-radius:9px}
|
||||
.iso-brand .t{font-family:var(--font-display);font-weight:700;font-size:18px;letter-spacing:.01em}
|
||||
|
||||
.iso-badge{display:inline-flex;align-items:center;gap:7px;padding:6px 13px;border-radius:999px;font-size:12px;font-weight:700}
|
||||
.iso-badge.danger{background:var(--ddim);color:var(--danger);border:1px solid var(--danger-glow)}
|
||||
.iso-badge.success{background:var(--sdim);color:var(--success);border:1px solid var(--success-glow)}
|
||||
.iso-card h1{font-size:clamp(20px,3.2vw,26px);margin:14px 0 8px}
|
||||
.iso-sub{color:var(--muted);font-size:14px;line-height:1.55}
|
||||
|
||||
.iso-info{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:12px 14px;margin:16px 0}
|
||||
.iso-info .row{display:flex;justify-content:space-between;gap:10px;padding:5px 0;font-size:13.5px}
|
||||
.iso-info .row .label{color:var(--muted)}
|
||||
.iso-info .row .val{font-weight:700;color:var(--text);text-align:right;word-break:break-word}
|
||||
|
||||
.iso-sec{display:flex;align-items:center;gap:8px;font-weight:700;font-size:14px;margin:18px 0 10px}
|
||||
.iso-sec i{color:var(--primary)}
|
||||
|
||||
.iso-inv{border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:12px;background:var(--ghost)}
|
||||
.iso-inv .top{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
|
||||
.iso-inv .period{font-size:13px;font-weight:700;color:var(--text)}
|
||||
.iso-inv .id{font-size:11.5px;color:var(--muted);margin-top:2px}
|
||||
.iso-inv .amt{font-family:var(--font-display);font-weight:800;font-size:24px;color:var(--primary);margin:12px 0}
|
||||
|
||||
.iso-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:13px;border-radius:12px;font-weight:700;font-size:14px;cursor:pointer;border:1px solid transparent;text-decoration:none;transition:transform .16s var(--ease),filter .16s var(--ease),border-color .16s var(--ease)}
|
||||
.iso-btn:active{transform:translateY(1px)}
|
||||
.iso-btn.primary{background:var(--primary);color:var(--on-primary);box-shadow:0 6px 20px var(--primary-glow)}
|
||||
.iso-btn.primary:hover{filter:brightness(1.05)}
|
||||
.iso-btn.success{background:var(--success);color:#04231a}
|
||||
.iso-btn.success:hover{filter:brightness(1.05)}
|
||||
.iso-btn.ghost{background:var(--ghost);color:var(--text);border-color:var(--border)}
|
||||
.iso-btn.ghost:hover{border-color:var(--primary-glow)}
|
||||
.iso-btn.wa{background:rgba(37,211,102,.14);color:#25D366;border-color:rgba(37,211,102,.3)}
|
||||
.iso-btn.wa:hover{background:rgba(37,211,102,.22)}
|
||||
.iso-space{margin-top:10px}
|
||||
.iso-card form > .iso-btn{margin-top:14px}
|
||||
.iso-inv .iso-btn.primary{margin-top:4px}
|
||||
.iso-error{color:var(--danger);font-size:12.5px;margin-top:8px}
|
||||
.iso-foot{color:var(--muted);font-size:12px;text-align:center;margin-top:22px;line-height:1.55}
|
||||
|
||||
/* ── MODAL QRIS ── */
|
||||
.iso-mo{display:none;position:fixed;inset:0;background:var(--overlay-bg);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);z-index:1100;align-items:center;justify-content:center;padding:16px}
|
||||
.iso-mo.active{display:flex;animation:fadein .18s var(--ease)}
|
||||
.iso-mc{background:var(--bg2);border:1px solid var(--border);border-radius:18px;width:100%;max-width:420px;padding:24px 20px;box-shadow:var(--shadow-3);text-align:center;position:relative;max-height:92vh;overflow:auto}
|
||||
.iso-mc-close{position:absolute;top:12px;right:14px;background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer;line-height:1}
|
||||
.iso-mc-close:hover{color:var(--danger)}
|
||||
.iso-mc .gt{display:flex;align-items:center;justify-content:center;gap:6px;color:var(--primary);font-size:13px;font-weight:800;text-transform:uppercase}
|
||||
.iso-mc .per{font-size:12px;color:var(--muted);margin-top:3px}
|
||||
.iso-qr{border-radius:14px;background:#fff;padding:10px;margin:14px auto;width:230px;max-width:82%}
|
||||
.iso-qr img{width:100%;display:block}
|
||||
.iso-amt-box{background:var(--bg);border:1px solid var(--primary-glow);border-radius:12px;padding:12px;margin-top:12px}
|
||||
.iso-amt-box .l{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
|
||||
.iso-amt-box .v{font-family:var(--font-display);font-weight:800;font-size:26px;color:var(--primary)}
|
||||
.iso-unique{display:inline-block;font-size:11px;font-weight:700;color:var(--warning);background:var(--wdim);border:1px solid var(--warning-glow);border-radius:999px;padding:3px 10px;margin-top:8px}
|
||||
.iso-poll{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:14px;color:var(--muted);font-size:12.5px}
|
||||
.spin-pulse{width:16px;height:16px;border-radius:50%;background:var(--primary);animation:pulse-dot 1.4s infinite;flex:0 0 auto}
|
||||
.iso-success-ic{font-size:58px;color:var(--success);display:block;margin-bottom:10px}
|
||||
.iso-mc h2{font-size:22px;color:var(--success)}
|
||||
.iso-actions{display:flex;gap:8px;margin-top:14px}
|
||||
.iso-actions .iso-btn{flex:1}
|
||||
.iso-help{font-size:11.5px;color:var(--muted);margin-top:14px;line-height:1.5}
|
||||
|
||||
/* ── DONASI ── */
|
||||
.preset-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
||||
.preset-btn{padding:10px 6px;border-radius:10px;border:1px solid var(--border);background:var(--ghost);color:var(--text);font-weight:700;font-size:13px;cursor:pointer;transition:.15s var(--ease)}
|
||||
.preset-btn:hover{border-color:var(--primary-glow)}
|
||||
.preset-btn.active{background:var(--pdim);border-color:var(--primary);color:var(--primary)}
|
||||
.activation-card{border:1px solid var(--border);border-radius:14px;padding:16px;margin-top:16px;background:var(--ghost)}
|
||||
.code-box{display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--bg);border:1px dashed var(--primary-glow);border-radius:12px;padding:12px;margin:10px 0;flex-wrap:wrap}
|
||||
.code-text{font-family:var(--font-mono);font-weight:800;font-size:18px;letter-spacing:1px;color:var(--primary)}
|
||||
.step-item{display:flex;gap:10px;font-size:13px;color:var(--muted);margin-bottom:6px}
|
||||
.step-num{flex:0 0 auto;width:20px;height:20px;border-radius:50%;background:var(--pdim);color:var(--primary);font-size:11px;font-weight:800;display:grid;place-items:center;margin-top:1px}
|
||||
.heart-badge{width:60px;height:60px;border-radius:50%;margin:0 auto 12px;display:grid;place-items:center;font-size:26px;background:var(--ddim);color:var(--danger);border:1px solid var(--danger-glow)}
|
||||
.donate-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px}
|
||||
@media(max-width:820px){.donate-grid{grid-template-columns:1fr}}
|
||||
@@ -0,0 +1,346 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
landing.css — Halaman Depan (Landing Page) Billing RT/RW
|
||||
Mandiri (tidak memuat ui.css) agar bisa scroll normal & tetap ringan.
|
||||
Tema: Halogen — Dark #0A0D14 + Lime #ADFF2F. Font: Space Grotesk + Plus Jakarta Sans.
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root{
|
||||
color-scheme: dark;
|
||||
--font-sans:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
--font-display:'Space Grotesk','Plus Jakarta Sans',sans-serif;
|
||||
--font-mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
|
||||
|
||||
--bg:#0A0D14; --bg2:#111622; --bg3:#1F293D;
|
||||
--text:#F2F5F1; --muted:#8A96A6;
|
||||
--border:rgba(242,245,241,0.10);
|
||||
--primary:#ADFF2F; --primary-dark:#9CE62A; --on-primary:#0A0D14;
|
||||
--primary-glow:rgba(173,255,47,0.35); --pdim:rgba(173,255,47,0.14);
|
||||
--success:#34D399; --warning:#FBBF24; --danger:#FB7185; --info:#38BDF8;
|
||||
|
||||
--glass:rgba(17,22,34,0.72);
|
||||
--glass-2:linear-gradient(180deg,rgba(23,29,42,0.80),rgba(14,18,26,0.86));
|
||||
--ghost:rgba(242,245,241,0.06); --ghost-h:rgba(242,245,241,0.11);
|
||||
|
||||
--page-1:#0C1017; --page-2:#080A0F;
|
||||
--dot:rgba(255,255,255,0.11); --dot-size:1.4px; --dot-gap:22px;
|
||||
--glow-lime:rgba(173,255,47,0.10); --glow-cyan:rgba(56,189,248,0.085); --glow-violet:rgba(139,92,246,0.075);
|
||||
--vignette:rgba(0,0,0,0.55); --sheen:rgba(255,255,255,0.018);
|
||||
|
||||
--radius:16px; --radius-sm:10px; --pill:999px;
|
||||
--shadow-1:0 1px 2px rgba(0,0,0,.3); --shadow-2:0 10px 30px rgba(0,0,0,.4); --shadow-3:0 22px 60px rgba(0,0,0,.5);
|
||||
--ring:0 0 0 3px rgba(173,255,47,.28);
|
||||
--ease:cubic-bezier(.4,0,.2,1);
|
||||
--nav-h:66px;
|
||||
}
|
||||
:root[data-theme="light"],html[data-theme="light"]{
|
||||
color-scheme:light;
|
||||
--bg:#E8EDE5; --bg2:#FFFFFF; --bg3:#DEE4DA;
|
||||
--text:#141A12; --muted:#52604C; --border:rgba(16,20,15,0.15);
|
||||
--primary:#5C9A0A; --primary-dark:#4A7D08; --on-primary:#FFFFFF;
|
||||
--primary-glow:rgba(92,154,10,0.30); --pdim:rgba(92,154,10,0.12);
|
||||
--success:#0F9D6B; --warning:#C2740A; --danger:#DE3B5C; --info:#0A7BC2;
|
||||
--glass:rgba(255,255,255,0.82); --glass-2:linear-gradient(180deg,rgba(255,255,255,0.92),rgba(255,255,255,0.96));
|
||||
--ghost:rgba(16,20,15,0.05); --ghost-h:rgba(16,20,15,0.09);
|
||||
--page-1:#E8EDE5; --page-2:#DEE4DA;
|
||||
--dot:rgba(16,20,15,0.17);
|
||||
--glow-lime:rgba(92,154,10,0.10); --glow-cyan:rgba(10,123,194,0.08); --glow-violet:rgba(124,92,246,0.07);
|
||||
--vignette:rgba(16,20,15,0.20); --sheen:rgba(255,255,255,0.35);
|
||||
--shadow-1:0 1px 2px rgba(16,20,15,.10); --shadow-2:0 10px 30px rgba(16,20,15,.13); --shadow-3:0 22px 60px rgba(16,20,15,.18);
|
||||
--ring:0 0 0 3px rgba(92,154,10,.22);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
|
||||
body{
|
||||
margin:0;font-family:var(--font-sans);color:var(--text);line-height:1.5;
|
||||
background-color:var(--bg);
|
||||
background-image:
|
||||
radial-gradient(var(--dot) var(--dot-size),transparent var(--dot-size)),
|
||||
radial-gradient(130% 130% at 50% 42%,transparent 38%,var(--vignette) 100%),
|
||||
radial-gradient(1100px 620px at 8% -14%,var(--glow-lime),transparent 58%),
|
||||
radial-gradient(950px 540px at 102% -6%,var(--glow-cyan),transparent 55%),
|
||||
radial-gradient(1100px 640px at 50% 122%,var(--glow-violet),transparent 60%),
|
||||
linear-gradient(180deg,var(--sheen),transparent 30%),
|
||||
linear-gradient(180deg,var(--page-1) 0%,var(--page-2) 100%);
|
||||
background-size:var(--dot-gap) var(--dot-gap),auto,auto,auto,auto,auto,auto;
|
||||
background-attachment:fixed;
|
||||
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;overflow-x:hidden;
|
||||
}
|
||||
h1,h2,h3,h4{font-family:var(--font-display);letter-spacing:-.02em;line-height:1.15;margin:0}
|
||||
p{margin:0}
|
||||
a{color:var(--primary);text-decoration:none}
|
||||
img{max-width:100%;display:block}
|
||||
.container{width:min(1180px,92vw);margin-inline:auto}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.muted{color:var(--muted)}
|
||||
.center{text-align:center}
|
||||
|
||||
/* ── BUTTONS ── */
|
||||
.lbtn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 22px;border-radius:12px;
|
||||
font:inherit;font-weight:700;font-size:14px;cursor:pointer;border:1px solid transparent;text-decoration:none;white-space:nowrap;
|
||||
transition:transform .15s var(--ease),box-shadow .2s var(--ease),background .2s var(--ease),border-color .2s var(--ease),color .2s var(--ease)}
|
||||
.lbtn:active{transform:translateY(1px)}
|
||||
.lbtn:focus-visible{outline:none;box-shadow:var(--ring)}
|
||||
.lbtn-primary{background:var(--primary);color:var(--on-primary);box-shadow:0 6px 22px var(--primary-glow)}
|
||||
.lbtn-primary:hover{background:var(--primary-dark);color:var(--on-primary);transform:translateY(-2px);box-shadow:0 10px 30px var(--primary-glow)}
|
||||
.lbtn-ghost{background:var(--ghost);color:var(--text);border-color:var(--border)}
|
||||
.lbtn-ghost:hover{background:var(--ghost-h);border-color:var(--primary-glow);transform:translateY(-2px)}
|
||||
.lbtn-wa{background:rgba(37,211,102,.14);color:#25D366;border-color:rgba(37,211,102,.3)}
|
||||
.lbtn-wa:hover{background:rgba(37,211,102,.22);transform:translateY(-2px)}
|
||||
.lbtn-sm{padding:8px 14px;font-size:13px;border-radius:10px}
|
||||
.lbtn-block{width:100%}
|
||||
|
||||
/* ── HEADER ── */
|
||||
.site-header{position:sticky;top:0;z-index:100;background:var(--glass);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
|
||||
border-bottom:1px solid var(--border)}
|
||||
.site-header .inner{display:flex;align-items:center;justify-content:space-between;gap:16px;height:var(--nav-h)}
|
||||
.brand{display:flex;align-items:center;gap:10px}
|
||||
.brand img,.brand .bmark{height:30px;width:30px;border-radius:8px;object-fit:contain}
|
||||
.brand .bname{font-family:var(--font-display);font-weight:700;font-size:18px;letter-spacing:.02em}
|
||||
.nav{display:flex;align-items:center;gap:4px}
|
||||
.nav a{padding:9px 13px;border-radius:9px;color:var(--muted);font-size:14px;font-weight:600;transition:color .18s var(--ease),background .18s var(--ease)}
|
||||
.nav a:hover{color:var(--text);background:var(--ghost)}
|
||||
.header-actions{display:flex;align-items:center;gap:8px}
|
||||
.icon-btn{width:38px;height:38px;display:grid;place-items:center;border-radius:10px;border:1px solid var(--border);background:var(--ghost);color:var(--text);cursor:pointer;font-size:16px;transition:.2s var(--ease)}
|
||||
.icon-btn:hover{border-color:var(--primary-glow);color:var(--primary)}
|
||||
.hamburger{display:none}
|
||||
.mobile-menu{display:none}
|
||||
.hide-sm{display:inline-flex}
|
||||
.logo-on-light{display:none}
|
||||
html[data-theme="light"] .logo-on-dark{display:none}
|
||||
html[data-theme="light"] .logo-on-light{display:block}
|
||||
|
||||
/* ── HERO ── */
|
||||
.hero{padding:clamp(36px,6vw,80px) 0 20px}
|
||||
.hero .grid{display:grid;grid-template-columns:1.18fr .82fr;gap:clamp(24px,4vw,56px);align-items:center}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:var(--pill);background:var(--pdim);
|
||||
border:1px solid var(--primary-glow);color:var(--primary);font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
|
||||
.hero h1{font-size:clamp(32px,5.4vw,58px);margin:18px 0 14px}
|
||||
.hero h1 .hl{color:var(--primary)}
|
||||
.hero p.lead{font-size:clamp(15px,1.7vw,18px);color:var(--muted);max-width:56ch}
|
||||
.cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:26px}
|
||||
.hero-stats{display:flex;flex-wrap:wrap;gap:26px;margin-top:34px;padding-top:26px;border-top:1px solid var(--border)}
|
||||
.hero-stats .st b{display:block;font-family:var(--font-display);font-size:clamp(22px,3vw,30px);line-height:1}
|
||||
.hero-stats .st span{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
|
||||
.hero-media{position:relative;max-width:420px;margin-left:auto;width:100%}
|
||||
.hero-media img{border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow-2);width:100%;aspect-ratio:4/3;max-height:330px;object-fit:cover}
|
||||
.hero-media::after{content:'';position:absolute;inset:0;border-radius:var(--radius);background:linear-gradient(180deg,transparent 55%,rgba(8,10,15,.45));pointer-events:none}
|
||||
.hero-media .float-card{position:absolute;left:-14px;bottom:-18px;z-index:2;background:var(--glass-2);backdrop-filter:blur(12px);
|
||||
border:1px solid var(--border);border-radius:14px;padding:12px 16px;box-shadow:var(--shadow-2);display:flex;align-items:center;gap:10px}
|
||||
.hero-media .float-card i{color:var(--primary);font-size:20px}
|
||||
.hero-media .float-card b{font-size:14px}
|
||||
.hero-media .float-card span{display:block;font-size:11px;color:var(--muted)}
|
||||
|
||||
/* ── SECTIONS ── */
|
||||
.section{padding:clamp(48px,7vw,86px) 0}
|
||||
.section-head{max-width:640px;margin:0 auto clamp(28px,4vw,44px);text-align:center}
|
||||
.section-head .eyebrow{margin-bottom:14px}
|
||||
.section-head h2{font-size:clamp(26px,3.6vw,38px)}
|
||||
.section-head p{color:var(--muted);margin-top:12px;font-size:15px}
|
||||
.card-surface{background:var(--glass-2);backdrop-filter:blur(10px);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-1)}
|
||||
|
||||
/* FEATURES */
|
||||
.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px}
|
||||
.feature{padding:24px;border-radius:var(--radius);background:var(--glass-2);border:1px solid var(--border);transition:transform .2s var(--ease),border-color .2s var(--ease),box-shadow .2s var(--ease)}
|
||||
.feature:hover{transform:translateY(-4px);border-color:var(--primary-glow);box-shadow:var(--shadow-2)}
|
||||
.feature .ficon{width:48px;height:48px;border-radius:13px;display:grid;place-items:center;font-size:22px;color:var(--primary);
|
||||
background:var(--pdim);border:1px solid var(--primary-glow);margin-bottom:14px}
|
||||
.feature h3{font-size:17px;margin-bottom:7px}
|
||||
.feature p{color:var(--muted);font-size:14px}
|
||||
|
||||
/* PRICING */
|
||||
.pricing-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:18px;align-items:stretch}
|
||||
.price-card{display:flex;flex-direction:column;padding:26px 22px;border-radius:var(--radius);background:var(--glass-2);
|
||||
border:1px solid var(--border);position:relative;transition:transform .2s var(--ease),border-color .2s var(--ease),box-shadow .2s var(--ease)}
|
||||
.price-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-2)}
|
||||
.price-card.featured{border-color:var(--primary);box-shadow:0 0 0 1px var(--primary-glow),var(--shadow-2)}
|
||||
.price-card .tag{position:absolute;top:-11px;left:50%;transform:translateX(-50%);background:var(--primary);color:var(--on-primary);
|
||||
font-size:11px;font-weight:700;padding:4px 12px;border-radius:var(--pill);letter-spacing:.04em;text-transform:uppercase}
|
||||
.price-card h3{font-size:16px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;font-family:var(--font-sans);font-weight:700}
|
||||
.price-card .speed{font-family:var(--font-display);font-size:38px;font-weight:700;line-height:1;margin:12px 0 2px}
|
||||
.price-card .speed small{font-size:15px;color:var(--muted);font-weight:600}
|
||||
.price-card .price{color:var(--muted);font-size:14px;margin-bottom:16px}
|
||||
.price-card .price b{color:var(--text);font-size:20px;font-family:var(--font-display)}
|
||||
.price-card ul{list-style:none;padding:0;margin:0 0 20px;display:grid;gap:9px}
|
||||
.price-card li{display:flex;gap:9px;font-size:13.5px;color:var(--muted)}
|
||||
.price-card li i{color:var(--primary);margin-top:2px}
|
||||
.price-card .lbtn{margin-top:auto}
|
||||
|
||||
/* STEPS */
|
||||
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;counter-reset:s}
|
||||
.step{padding:24px;border-radius:var(--radius);background:var(--glass-2);border:1px solid var(--border);position:relative}
|
||||
.step .num{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;font-family:var(--font-display);font-weight:700;
|
||||
font-size:18px;color:var(--on-primary);background:var(--primary);margin-bottom:14px;box-shadow:0 0 18px var(--primary-glow)}
|
||||
.step h3{font-size:16px;margin-bottom:6px}
|
||||
.step p{color:var(--muted);font-size:13.5px}
|
||||
|
||||
/* AREA / MAP */
|
||||
.area-wrap{display:grid;grid-template-columns:1fr 1fr;gap:22px;align-items:stretch}
|
||||
.area-list{display:flex;flex-wrap:wrap;gap:9px;align-content:flex-start}
|
||||
.chip{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:var(--pill);background:var(--ghost);
|
||||
border:1px solid var(--border);font-size:13px;color:var(--text)}
|
||||
.chip i{color:var(--primary);font-size:13px}
|
||||
#coverage-map{min-height:320px;border-radius:var(--radius);border:1px solid var(--border);overflow:hidden;background:var(--bg2)}
|
||||
.map-fallback{display:grid;place-items:center;height:100%;min-height:320px;color:var(--muted);text-align:center;padding:20px}
|
||||
|
||||
/* TESTIMONIALS */
|
||||
.testi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px}
|
||||
.testi{padding:22px;border-radius:var(--radius);background:var(--glass-2);border:1px solid var(--border)}
|
||||
.testi .stars{color:var(--primary);font-size:13px;margin-bottom:10px;letter-spacing:2px}
|
||||
.testi p{font-size:14px;color:var(--muted);margin-bottom:16px}
|
||||
.testi .who{display:flex;align-items:center;gap:11px}
|
||||
.testi .av{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;background:var(--pdim);color:var(--primary);
|
||||
font-weight:700;font-family:var(--font-display)}
|
||||
.testi .who b{font-size:14px;display:block}
|
||||
.testi .who span{font-size:12px;color:var(--muted)}
|
||||
|
||||
/* FAQ */
|
||||
.faq{max-width:800px;margin-inline:auto;display:grid;gap:12px}
|
||||
.faq details{background:var(--glass-2);border:1px solid var(--border);border-radius:14px;padding:0 18px;transition:border-color .2s var(--ease)}
|
||||
.faq details[open]{border-color:var(--primary-glow)}
|
||||
.faq summary{list-style:none;cursor:pointer;padding:16px 0;font-weight:600;font-size:15px;display:flex;justify-content:space-between;gap:14px;align-items:center}
|
||||
.faq summary::-webkit-details-marker{display:none}
|
||||
.faq summary .chev{color:var(--primary);transition:transform .25s var(--ease)}
|
||||
.faq details[open] summary .chev{transform:rotate(180deg)}
|
||||
.faq .ans{padding:0 0 16px;color:var(--muted);font-size:14px}
|
||||
|
||||
/* ADS / IKLAN */
|
||||
.adsbox{border:1.5px dashed var(--border);border-radius:var(--radius);padding:34px 24px;text-align:center;background:var(--glass)}
|
||||
.adsbox .badge-soon{display:inline-flex;align-items:center;gap:7px;background:var(--pdim);border:1px solid var(--primary-glow);
|
||||
color:var(--primary);padding:6px 14px;border-radius:var(--pill);font-size:12px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}
|
||||
.adsbox h3{margin:14px 0 6px;font-size:19px}
|
||||
.adsbox p{color:var(--muted);font-size:14px;max-width:52ch;margin:0 auto}
|
||||
|
||||
/* CTA BAND */
|
||||
.cta-band{border-radius:var(--radius);padding:clamp(30px,5vw,54px);text-align:center;
|
||||
background:linear-gradient(135deg,var(--pdim),transparent 60%),var(--glass-2);border:1px solid var(--primary-glow)}
|
||||
.cta-band h2{font-size:clamp(24px,3.4vw,34px)}
|
||||
.cta-band p{color:var(--muted);margin:12px auto 24px;max-width:54ch}
|
||||
.cta-band .cta-row{justify-content:center}
|
||||
|
||||
/* FOOTER */
|
||||
.site-footer{border-top:1px solid var(--border);margin-top:40px;padding:48px 0 30px;background:var(--glass)}
|
||||
.foot-grid{display:grid;grid-template-columns:1.3fr 1fr 1fr 1.1fr;gap:26px}
|
||||
.foot-grid h4{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:14px;font-family:var(--font-sans);font-weight:700}
|
||||
.foot-links{display:grid;gap:9px}
|
||||
.foot-links a{color:var(--muted);font-size:14px}
|
||||
.foot-links a:hover{color:var(--primary)}
|
||||
.foot-contact{display:grid;gap:12px}
|
||||
.foot-contact .ci{display:flex;gap:10px;font-size:14px;color:var(--muted)}
|
||||
.foot-contact .ci i{color:var(--primary);margin-top:3px}
|
||||
.foot-bottom{margin-top:34px;padding-top:20px;border-top:1px solid var(--border);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:13px;color:var(--muted)}
|
||||
|
||||
/* REVEAL (aktif hanya bila JS ada agar konten tetap tampil tanpa JS) */
|
||||
.js .reveal{opacity:0;transform:translateY(18px);transition:opacity .6s var(--ease),transform .6s var(--ease)}
|
||||
.js .reveal.in{opacity:1;transform:none}
|
||||
|
||||
/* ── ANIMASI ── */
|
||||
/* stagger masuk untuk kartu-kartu */
|
||||
.feature-grid .reveal:nth-child(2),.pricing-grid .reveal:nth-child(2),.testi-grid .reveal:nth-child(2),.steps .reveal:nth-child(2){transition-delay:.06s}
|
||||
.feature-grid .reveal:nth-child(3),.pricing-grid .reveal:nth-child(3),.testi-grid .reveal:nth-child(3),.steps .reveal:nth-child(3){transition-delay:.12s}
|
||||
.feature-grid .reveal:nth-child(4),.pricing-grid .reveal:nth-child(4),.testi-grid .reveal:nth-child(4){transition-delay:.18s}
|
||||
.feature-grid .reveal:nth-child(5),.pricing-grid .reveal:nth-child(5){transition-delay:.24s}
|
||||
.feature-grid .reveal:nth-child(6),.pricing-grid .reveal:nth-child(6){transition-delay:.30s}
|
||||
|
||||
/* teks highlight hero berdenyut halus */
|
||||
.hero h1 .hl{animation:hlglow 2.8s ease-in-out infinite}
|
||||
@keyframes hlglow{0%,100%{text-shadow:0 0 16px var(--primary-glow)}50%{text-shadow:0 0 34px var(--primary-glow)}}
|
||||
|
||||
/* kartu melayang pada media hero */
|
||||
@keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-9px)}}
|
||||
.hero-media .float-card{animation:floaty 4.5s ease-in-out infinite}
|
||||
.hero-media .float-card i{animation:pulse-ic 2.2s ease-in-out infinite}
|
||||
@keyframes pulse-ic{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
|
||||
/* kilau pada tombol utama */
|
||||
.lbtn-primary{position:relative;overflow:hidden}
|
||||
.lbtn-primary::after{content:'';position:absolute;top:0;left:-130%;width:55%;height:100%;
|
||||
background:linear-gradient(90deg,transparent,rgba(255,255,255,.5),transparent);transform:skewX(-20deg);animation:shinebtn 3.4s ease-in-out infinite}
|
||||
@keyframes shinebtn{0%{left:-130%}55%,100%{left:150%}}
|
||||
|
||||
/* titik berdenyut pada eyebrow */
|
||||
.eyebrow i{animation:pulse-ic 2s ease-in-out infinite}
|
||||
|
||||
/* header bayangan saat di-scroll */
|
||||
.site-header.scrolled{box-shadow:0 8px 30px rgba(0,0,0,.35)}
|
||||
:root[data-theme="light"] .site-header.scrolled{box-shadow:0 8px 30px rgba(16,20,15,.12)}
|
||||
|
||||
/* ikon fitur berputar saat hover */
|
||||
.feature:hover .ficon{transform:rotate(-6deg) scale(1.06);transition:transform .3s var(--ease)}
|
||||
.feature .ficon{transition:transform .3s var(--ease)}
|
||||
|
||||
/* chip area muncul bertahap */
|
||||
@keyframes chipin{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:none}}
|
||||
.area-list .chip{animation:chipin .5s var(--ease) both}
|
||||
.area-list .chip{animation-delay:calc(var(--i,0)*0.05s)}
|
||||
|
||||
/* FAQ jawaban */
|
||||
.faq details[open] .ans{animation:fadein .3s var(--ease)}
|
||||
@keyframes fadein{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* angka statistik */
|
||||
.hero-stats .st b{transition:color .3s var(--ease)}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media(max-width:900px){
|
||||
.hide-sm{display:none!important}
|
||||
.hero .grid{grid-template-columns:1fr}
|
||||
.hero-media{order:-1;max-width:520px;margin-inline:auto}
|
||||
.hero-media img{max-height:280px}
|
||||
.area-wrap{grid-template-columns:1fr}
|
||||
.foot-grid{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
@media(max-width:720px){
|
||||
.nav{display:none}
|
||||
.hamburger{display:grid}
|
||||
.mobile-menu{display:block;overflow:hidden;max-height:0;transition:max-height .3s var(--ease)}
|
||||
.mobile-menu.open{max-height:420px;border-top:1px solid var(--border)}
|
||||
.mobile-menu .inner{display:grid;gap:2px;padding:12px 0 18px}
|
||||
.mobile-menu a{padding:12px 10px;border-radius:10px;color:var(--text);font-weight:600}
|
||||
.mobile-menu a:hover{background:var(--ghost)}
|
||||
.foot-grid{grid-template-columns:1fr}
|
||||
.hero-media .float-card{left:8px;bottom:-14px}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){
|
||||
*{animation:none!important;transition:none!important}
|
||||
.reveal{opacity:1;transform:none}
|
||||
}
|
||||
|
||||
/* PROMO / IKLAN (sinkron dengan promo_slides) */
|
||||
.promo-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
|
||||
.promo-card{display:block;position:relative;border-radius:var(--radius);overflow:hidden;border:1px solid var(--border);background:var(--glass-2);box-shadow:var(--shadow-1);transition:transform .2s var(--ease),border-color .2s var(--ease),box-shadow .2s var(--ease)}
|
||||
.promo-card:hover{transform:translateY(-4px);border-color:var(--primary-glow);box-shadow:var(--shadow-2)}
|
||||
.promo-card img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}
|
||||
.promo-cap{padding:12px 14px}
|
||||
.promo-cap b{display:block;font-size:14px;color:var(--text)}
|
||||
.promo-cap span{color:var(--muted);font-size:12.5px;display:block;margin-top:3px}
|
||||
@media(max-width:480px){.promo-grid{grid-template-columns:1fr}}
|
||||
|
||||
/* ═══ SERVICE NOTICE (Gangguan Masal / Perbaikan Sistem) ═══ */
|
||||
.service-notice{display:flex;gap:14px;align-items:flex-start;padding:16px 18px;border-radius:16px;margin:0 0 18px;border:1px solid var(--border);background:var(--glass-2);position:relative;overflow:hidden}
|
||||
.service-notice::before{content:"";position:absolute;top:0;bottom:0;left:0;width:4px;background:var(--primary)}
|
||||
.service-notice .sn-ico{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;font-size:21px;flex:0 0 auto;background:var(--pdim);color:var(--primary)}
|
||||
.service-notice .sn-main{min-width:0}
|
||||
.service-notice .sn-title{font-weight:800;font-family:var(--font-display);font-size:14.5px;color:var(--text);letter-spacing:.2px}
|
||||
.service-notice .sn-text{font-size:13.5px;color:var(--muted);margin-top:3px;line-height:1.5}
|
||||
.service-notice .sn-eta{font-size:12.5px;margin-top:9px;color:var(--text);display:flex;flex-wrap:wrap;gap:5px;align-items:center}
|
||||
.service-notice .sn-eta .sn-since{color:var(--muted)}
|
||||
.service-notice[data-tone="danger"]{border-color:rgba(251,113,133,.38)}
|
||||
.service-notice[data-tone="danger"]::before{background:var(--danger)}
|
||||
.service-notice[data-tone="danger"] .sn-ico{background:rgba(251,113,133,.16);color:var(--danger)}
|
||||
html[data-theme="light"] .service-notice[data-tone="danger"] .sn-ico{background:rgba(222,59,92,.14)}
|
||||
@media(max-width:480px){.service-notice{padding:14px}.service-notice .sn-ico{width:38px;height:38px;font-size:18px}}
|
||||
|
||||
/* == UI HARDENING (audit playwright) - header responsif == */
|
||||
.brand{min-width:0}
|
||||
.brand .bname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.header-actions{flex:0 0 auto;gap:6px}
|
||||
@media(max-width:900px){
|
||||
.nav{display:none}
|
||||
.hamburger{display:grid}
|
||||
.site-header .inner{gap:10px}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
.header-actions .lbtn-ghost{display:none}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* page-loader.css — Animasi saat berpindah halaman (progress bar + spinner).
|
||||
Netral terhadap tema (pakai CSS var landing.css bila ada, fallback bila tidak). */
|
||||
#pgbar{
|
||||
position:fixed;top:0;left:0;height:3px;width:0;z-index:2147483000;opacity:0;pointer-events:none;
|
||||
background:linear-gradient(90deg,var(--primary,#ADFF2F),#7cc61a);
|
||||
box-shadow:0 0 12px rgba(173,255,47,.6);
|
||||
transition:width .25s ease,opacity .3s ease;
|
||||
}
|
||||
#pgbar.on{opacity:1}
|
||||
#pgload{
|
||||
position:fixed;inset:0;z-index:2147482999;display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(10,13,20,.66);
|
||||
backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
|
||||
opacity:0;pointer-events:none;transition:opacity .25s ease;
|
||||
}
|
||||
#pgload.on{opacity:1;pointer-events:auto}
|
||||
#pgload .pgbox{display:flex;flex-direction:column;align-items:center;gap:14px}
|
||||
#pgload .pgspin{
|
||||
width:46px;height:46px;border-radius:50%;
|
||||
border:3px solid rgba(242,245,241,.18);border-top-color:var(--primary,#ADFF2F);
|
||||
animation:pgspin .8s linear infinite;
|
||||
}
|
||||
#pgload .pgtxt{
|
||||
font:700 11px/1.2 system-ui,-apple-system,'Segoe UI',sans-serif;
|
||||
letter-spacing:.14em;text-transform:uppercase;color:var(--muted,#8A96A6);
|
||||
}
|
||||
[data-theme="light"] #pgload{background:rgba(232,237,229,.78)}
|
||||
[data-theme="light"] #pgload .pgspin{border-color:rgba(20,26,18,.15);border-top-color:var(--primary,#5C9A0A)}
|
||||
[data-theme="light"] #pgload .pgtxt{color:#52604C}
|
||||
@media print{ #pgbar,#pgload{display:none !important} }
|
||||
@keyframes pgspin{to{transform:rotate(360deg)}}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
pages.css — Halaman konten (Tentang, Kontak, Privasi, S&K)
|
||||
Dipakai setelah landing.css + auth.css.
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
.page-hero{padding:clamp(30px,5.5vw,64px) 0 8px;text-align:center}
|
||||
.page-hero h1{font-size:clamp(27px,4.4vw,44px)}
|
||||
.page-hero p{color:var(--muted);font-size:16px;margin-top:12px;max-width:60ch;margin-inline:auto}
|
||||
.prose{max-width:900px;margin:22px auto 0;background:var(--glass-2);backdrop-filter:blur(10px);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-2);padding:clamp(22px,4vw,46px)}
|
||||
.prose h2,.prose h3,.prose h4{color:var(--primary);display:flex;align-items:center;gap:10px;margin:28px 0 8px;font-size:20px}
|
||||
.prose h2:first-child,.prose h3:first-child,.prose h4:first-child{margin-top:0}
|
||||
.prose p,.prose li{font-size:15.5px;line-height:1.75;color:var(--text)}
|
||||
.prose p{margin:12px 0}
|
||||
.prose ul{margin:12px 0 12px 20px;padding:0}
|
||||
.prose ul li{margin-bottom:8px}
|
||||
.prose strong{color:var(--text);font-weight:700}
|
||||
.prose .lead{font-size:17px}
|
||||
.prose-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:22px 0}
|
||||
.pcard{background:var(--ghost);border:1px solid var(--border);border-radius:14px;padding:20px;text-align:center}
|
||||
.pcard i{font-size:30px;color:var(--primary);display:block;margin-bottom:12px}
|
||||
.pcard h5{font-size:15px;font-weight:700;margin-bottom:6px}
|
||||
.pcard p{color:var(--muted);font-size:13.5px;margin:0}
|
||||
.contact-list{display:grid;gap:14px;margin-top:8px}
|
||||
.contact-item{display:flex;align-items:center;gap:16px;padding:18px;border-radius:14px;background:var(--ghost);border:1px solid var(--border);text-decoration:none;color:inherit;transition:transform .18s var(--ease),border-color .18s var(--ease)}
|
||||
.contact-item:hover{transform:translateY(-2px);border-color:var(--primary-glow)}
|
||||
.contact-ic{width:52px;height:52px;border-radius:13px;display:grid;place-items:center;font-size:22px;background:var(--pdim);color:var(--primary);flex:0 0 auto}
|
||||
.contact-item h6{font-size:15px;font-weight:700;margin:0 0 2px}
|
||||
.contact-item p{color:var(--muted);font-size:13.5px;margin:0;word-break:break-word}
|
||||
.page-foot{text-align:center;color:var(--muted);font-size:13px;padding:28px 0 10px}
|
||||
@@ -0,0 +1,134 @@
|
||||
/* portal-theme.css — override Bootstrap ke design system (dark + lime)
|
||||
Dipakai pada portal teknisi (dan halaman lama yang masih memakai Bootstrap). */
|
||||
[data-bs-theme="dark"]{
|
||||
color-scheme: dark;
|
||||
--bs-body-bg: var(--bg);
|
||||
--bs-body-color: var(--text);
|
||||
--bs-border-color: var(--border);
|
||||
--bs-secondary-color: var(--muted);
|
||||
--bs-emphasis-color: var(--text);
|
||||
--bs-link-color: var(--primary);
|
||||
--bs-link-hover-color: var(--primary-dark);
|
||||
}
|
||||
body{ background-color: var(--bg) !important; color: var(--text) !important; }
|
||||
|
||||
.card, .offcanvas, .list-group-item, .accordion-item, .dropdown-menu{
|
||||
background: var(--bg2, #111622) !important; border-color: var(--border) !important; color: var(--text) !important;
|
||||
}
|
||||
.modal-header, .modal-footer, .accordion-button{ border-color: var(--border) !important; background: transparent !important; color: var(--text) !important; }
|
||||
.accordion-button:not(.collapsed){ background: var(--pdim) !important; color: var(--primary) !important; }
|
||||
|
||||
.form-control, .form-select{
|
||||
background: var(--bg) !important; border-color: var(--border) !important; color: var(--text) !important;
|
||||
}
|
||||
.form-control::placeholder{ color: var(--muted) !important; }
|
||||
.form-control:focus, .form-select:focus{ border-color: var(--primary) !important; box-shadow: var(--ring) !important; }
|
||||
.form-select option{ background: var(--bg2); color: var(--text); }
|
||||
.input-group-text{ background: var(--bg) !important; border-color: var(--border) !important; color: var(--muted) !important; }
|
||||
|
||||
.btn-primary{ --bs-btn-bg: var(--primary); --bs-btn-border-color: var(--primary); --bs-btn-color: var(--on-primary); --bs-btn-hover-bg: var(--primary-dark); --bs-btn-hover-border-color: var(--primary-dark); --bs-btn-hover-color: var(--on-primary); --bs-btn-active-bg: var(--primary-dark); --bs-btn-disabled-bg: var(--bg3); --bs-btn-disabled-border-color: var(--border); }
|
||||
.btn-success{ --bs-btn-bg: var(--success); --bs-btn-border-color: var(--success); --bs-btn-color: #04231a; --bs-btn-hover-bg: #2bb988; --bs-btn-hover-border-color: #2bb988; --bs-btn-hover-color: #04231a; }
|
||||
.btn-outline-light{ --bs-btn-color: var(--text); --bs-btn-border-color: var(--border); --bs-btn-hover-bg: var(--ghost); --bs-btn-hover-border-color: var(--primary-glow); --bs-btn-hover-color: var(--text); }
|
||||
.btn-outline-primary{ --bs-btn-color: var(--primary); --bs-btn-border-color: var(--primary-glow); --bs-btn-hover-bg: var(--pdim); --bs-btn-hover-color: var(--primary); --bs-btn-hover-border-color: var(--primary); }
|
||||
.btn-outline-info{ --bs-btn-color: var(--info); --bs-btn-border-color: var(--info); --bs-btn-hover-bg: var(--idim); --bs-btn-hover-color: var(--info); }
|
||||
.btn-outline-warning{ --bs-btn-color: var(--warning); --bs-btn-border-color: var(--warning); --bs-btn-hover-bg: var(--wdim); --bs-btn-hover-color: var(--warning); }
|
||||
.btn-outline-success{ --bs-btn-color: var(--success); --bs-btn-border-color: var(--success); --bs-btn-hover-bg: var(--sdim); --bs-btn-hover-color: var(--success); }
|
||||
.btn-secondary{ --bs-btn-bg: var(--bg3); --bs-btn-border-color: var(--border); --bs-btn-color: var(--text); }
|
||||
|
||||
.badge.bg-primary{ background: var(--primary) !important; color: var(--on-primary) !important; }
|
||||
.badge.bg-success{ background: var(--success) !important; color: #04231a !important; }
|
||||
.badge.bg-danger{ background: var(--danger) !important; color: #2b0714 !important; }
|
||||
.badge.bg-warning{ background: var(--warning) !important; color: #2b1c00 !important; }
|
||||
.badge.bg-info{ background: var(--info) !important; color: #04222e !important; }
|
||||
.badge.bg-secondary{ background: var(--bg3) !important; color: var(--muted) !important; }
|
||||
|
||||
.text-secondary{ color: var(--muted) !important; }
|
||||
.text-muted{ color: var(--muted) !important; }
|
||||
.text-primary{ color: var(--primary) !important; }
|
||||
.text-success{ color: var(--success) !important; }
|
||||
.text-warning{ color: var(--warning) !important; }
|
||||
.text-danger{ color: var(--danger) !important; }
|
||||
.text-info{ color: var(--info) !important; }
|
||||
.text-light, .text-white{ color: var(--text) !important; }
|
||||
.bg-dark{ background: var(--bg) !important; }
|
||||
.bg-primary{ background: var(--primary) !important; }
|
||||
.border-secondary, .border-top, .border-bottom, .border{ border-color: var(--border) !important; }
|
||||
.table{ --bs-table-bg: transparent; --bs-table-color: var(--text); --bs-table-border-color: var(--border); color: var(--text); }
|
||||
.table thead th{ color: var(--muted) !important; border-color: var(--border) !important; }
|
||||
.table td, .table th{ border-color: var(--border) !important; }
|
||||
|
||||
.alert{ border-radius: 12px; border: 1px solid transparent !important; }
|
||||
.alert-danger{ background: var(--ddim) !important; color: var(--danger) !important; border-color: var(--danger-glow) !important; }
|
||||
.alert-success{ background: var(--sdim) !important; color: var(--success) !important; border-color: var(--success-glow) !important; }
|
||||
.alert-info{ background: var(--idim) !important; color: var(--info) !important; border-color: rgba(56,189,248,.3) !important; }
|
||||
.alert-warning{ background: var(--wdim) !important; color: var(--warning) !important; border-color: var(--warning-glow) !important; }
|
||||
|
||||
.modal-backdrop{ background: rgba(0,0,0,.6); }
|
||||
.nav-tabs, .nav-pills{ border-color: var(--border); }
|
||||
.nav-tabs .nav-link{ color: var(--muted); }
|
||||
.nav-tabs .nav-link.active{ color: var(--primary) !important; background: transparent !important; border-color: var(--border) var(--border) transparent !important; }
|
||||
|
||||
/* ── LIGHT MODE ── */
|
||||
html[data-theme="light"]{
|
||||
--bg:#E8EDE5; --bg2:#FFFFFF; --bg3:#DEE4DA;
|
||||
--panel:#FFFFFF; --panel2:#F6F8F4; --panel2b:#F6F8F4;
|
||||
--text:#141A12; --muted:#52604C; --border:rgba(16,20,15,.15);
|
||||
--glass-2:linear-gradient(180deg,#FFFFFF,#FFFFFF);
|
||||
}
|
||||
html[data-theme="light"] body{ background-color: var(--bg) !important; color: var(--text) !important; }
|
||||
html[data-theme="light"] .card,
|
||||
html[data-theme="light"] .t-card,
|
||||
html[data-theme="light"] .p-card,
|
||||
html[data-theme="light"] .cardx,
|
||||
html[data-theme="light"] .accordion-item,
|
||||
html[data-theme="light"] .list-group-item,
|
||||
html[data-theme="light"] .dropdown-menu{ background: var(--bg2) !important; color: var(--text) !important; border-color: var(--border) !important; }
|
||||
html[data-theme="light"] .form-control,
|
||||
html[data-theme="light"] .form-select{ background: var(--bg) !important; color: var(--text) !important; border-color: var(--border) !important; }
|
||||
html[data-theme="light"] .input-group-text{ background: var(--bg3) !important; color: var(--muted) !important; }
|
||||
html[data-theme="light"] .text-secondary,
|
||||
html[data-theme="light"] .text-muted{ color: var(--muted) !important; }
|
||||
html[data-theme="light"] .bg-dark{ background: var(--bg3) !important; }
|
||||
html[data-theme="light"] .border-secondary,
|
||||
html[data-theme="light"] .border-top,
|
||||
html[data-theme="light"] .border-bottom,
|
||||
html[data-theme="light"] .border{ border-color: var(--border) !important; }
|
||||
html[data-theme="light"] .leaflet-popup-content-wrapper,
|
||||
html[data-theme="light"] .leaflet-popup-tip{ background: var(--bg2) !important; color: var(--text) !important; }
|
||||
|
||||
/* collector: topbar sticky + bottom nav light + chip/pill agen */
|
||||
.collector-top{position:sticky;top:0;z-index:950;background:var(--bg);border-bottom:1px solid var(--border);padding:8px 0;margin-bottom:12px}
|
||||
html[data-theme="light"] .bn{ background: rgba(255,255,255,.95) !important; border-top-color: var(--border) !important; }
|
||||
html[data-theme="light"] .bni{ background: rgba(0,0,0,.03) !important; color: var(--muted) !important; border-color: var(--border) !important; }
|
||||
html[data-theme="light"] .bni.active{ background: var(--pdim) !important; color: var(--primary) !important; border-color: var(--primary-glow) !important; }
|
||||
html[data-theme="light"] .bni.active i{ color: var(--primary) !important; }
|
||||
html[data-theme="light"] .chip.b{ color:#4338ca !important; }
|
||||
html[data-theme="light"] .chip.r{ color:#b91c1c !important; }
|
||||
html[data-theme="light"] .chip.g{ color:#15803d !important; }
|
||||
html[data-theme="light"] .pill{ color: var(--text) !important; }
|
||||
|
||||
/* agent: warna berbeda tiap KPI Ringkasan */
|
||||
.kpi .k:nth-child(1) .val{ color: var(--primary); }
|
||||
.kpi .k:nth-child(2) .val{ color: var(--info); }
|
||||
.kpi .k:nth-child(3) .val{ color: var(--success); }
|
||||
.kpi .k:nth-child(4) .val{ color: var(--warning); }
|
||||
|
||||
/* agent: bottom nav (5 item) — susun vertikal agar tidak terpotong */
|
||||
.bn-wrap{ gap: 6px !important; }
|
||||
.bn-item{ flex-direction: column !important; gap: 3px !important; font-size: 10px !important; padding: 8px 4px !important; min-height: 46px; }
|
||||
.bn-item i{ font-size: 17px !important; }
|
||||
|
||||
/* agent/collector: topbar & bottom-nav mengikuti tema */
|
||||
html[data-theme="light"] .topbar{ background: rgba(255,255,255,.9) !important; border-bottom-color: var(--border) !important; }
|
||||
html[data-theme="light"] .bottom-nav{ background: rgba(255,255,255,.95) !important; border-top-color: var(--border) !important; }
|
||||
html[data-theme="light"] .bn-item{ border-color: var(--border) !important; color: var(--muted) !important; }
|
||||
html[data-theme="light"] .bn-item.active{ border-color: var(--primary-glow) !important; background: var(--pdim) !important; color: var(--primary) !important; }
|
||||
html[data-theme="light"] .bn-item.active i, html[data-theme="light"] .bn-item i{ color: var(--muted) !important; }
|
||||
html[data-theme="light"] .bn-item.active i{ color: var(--primary) !important; }
|
||||
html[data-theme="light"] .brand .icon{ background: var(--pdim) !important; border-color: var(--primary-glow) !important; color: var(--primary) !important; }
|
||||
|
||||
/* agent: kartu metode pembayaran (QRIS/Transfer) di light mode */
|
||||
html[data-theme="light"] .payment-name-agent{ color: var(--text) !important; }
|
||||
html[data-theme="light"] .payment-card-agent{ background: var(--bg) !important; border-color: var(--border) !important; }
|
||||
html[data-theme="light"] .payment-radio-agent:checked + .payment-card-agent{ background: var(--pdim) !important; border-color: var(--primary) !important; }
|
||||
html[data-theme="light"] .payment-fee-agent{ color: var(--muted) !important; }
|
||||
@@ -0,0 +1,61 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
portal.css — Portal Teknisi / Agen / Kolektor (tema baru)
|
||||
Dipakai setelah landing.css + auth.css.
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
.portal-body{background-color:var(--bg);color:var(--text);padding-top:64px;padding-bottom:84px;min-height:100vh;min-height:100dvh}
|
||||
|
||||
.portal-topnav{position:fixed;top:0;left:0;right:0;height:60px;background:var(--glass);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 16px;z-index:1000}
|
||||
.portal-topnav .t{font-family:var(--font-display);font-weight:700;font-size:17px;color:var(--text);display:flex;align-items:center;gap:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.portal-topnav .actions{display:flex;gap:6px;align-items:center}
|
||||
.portal-topnav .nav-btn{background:none;border:none;color:var(--text);font-size:19px;padding:7px;cursor:pointer;border-radius:9px;text-decoration:none;line-height:1;display:inline-flex;align-items:center}
|
||||
.portal-topnav .nav-btn:hover{background:var(--ghost);color:var(--primary)}
|
||||
.portal-topnav .lang-btn{font-size:12px;font-weight:700;padding:5px 8px}
|
||||
|
||||
.portal-bottomnav{position:fixed;bottom:0;left:0;right:0;height:66px;background:var(--glass);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-top:1px solid var(--border);display:flex;justify-content:space-around;align-items:center;z-index:1000;padding-bottom:env(safe-area-inset-bottom)}
|
||||
.portal-nav-item{display:flex;flex-direction:column;align-items:center;gap:2px;color:var(--muted);text-decoration:none;font-size:10.5px;font-weight:600;flex:1}
|
||||
.portal-nav-item i{font-size:21px}
|
||||
.portal-nav-item.active{color:var(--primary)}
|
||||
|
||||
.p-card{background:var(--bg2, #111622);border:1px solid var(--border);border-radius:16px;padding:15px;margin-bottom:14px}
|
||||
/* alias kompatibilitas halaman lama */
|
||||
.t-card{background:var(--bg2, #111622);border:1px solid var(--border);border-radius:16px;padding:15px;margin-bottom:14px;color:var(--text)}
|
||||
.p-card h6{font-size:14px;font-weight:700}
|
||||
.p-stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px}
|
||||
.p-stat{background:var(--glass-2);border:1px solid var(--border);border-radius:16px;padding:15px;text-align:center}
|
||||
.p-stat .val{font-family:var(--font-display);font-size:27px;font-weight:800;margin-top:5px}
|
||||
.p-stat .lbl{font-size:12px;color:var(--muted)}
|
||||
.p-sec{font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:0 0 12px;display:flex;align-items:center;gap:7px}
|
||||
.p-sec i{color:var(--primary)}
|
||||
|
||||
.badge-status{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:999px;font-size:11px;font-weight:700;border:1px solid transparent}
|
||||
.badge-status.badge-open{background:var(--ddim);color:var(--danger);border-color:var(--danger-glow)}
|
||||
.badge-status.badge-in_progress{background:var(--wdim);color:var(--warning);border-color:var(--warning-glow)}
|
||||
.badge-status.badge-resolved{background:var(--sdim);color:var(--success);border-color:var(--success-glow)}
|
||||
|
||||
.ticket-msg{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:10px 12px;font-size:13px;margin:10px 0}
|
||||
.ticket-msg .subj{font-weight:700;color:var(--primary)}
|
||||
.p-img{width:100%;height:56px;object-fit:cover;border:1px solid var(--primary-glow);border-radius:8px}
|
||||
.p-empty{text-align:center;padding:40px 16px;color:var(--muted)}
|
||||
.p-empty i{font-size:40px;display:block;margin-bottom:10px;opacity:.5}
|
||||
.p-list{display:flex;flex-direction:column;gap:10px}
|
||||
|
||||
.p-field{margin-bottom:14px}
|
||||
.p-field label{display:block;font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:6px}
|
||||
.p-input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:11px;padding:12px 14px;color:var(--text);font-size:14px;font-family:inherit;outline:none}
|
||||
.p-input:focus{border-color:var(--primary);box-shadow:var(--ring)}
|
||||
.p-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px;border-radius:12px;font-weight:700;font-size:14px;cursor:pointer;border:1px solid transparent;text-decoration:none;transition:.16s var(--ease)}
|
||||
.p-btn.primary{background:var(--primary);color:var(--on-primary)}
|
||||
.p-btn.success{background:var(--success);color:#04231a}
|
||||
.p-btn.ghost{background:var(--ghost);color:var(--text);border-color:var(--border)}
|
||||
.p-btn.sm{padding:8px 12px;font-size:12.5px;width:auto}
|
||||
.p-grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
@media(max-width:480px){.p-grid2{grid-template-columns:1fr}}
|
||||
.p-sub{font-size:13px;font-weight:700;margin:18px 0 10px;display:flex;align-items:center;gap:8px}
|
||||
.p-sub i{color:var(--primary)}
|
||||
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px}
|
||||
.stat-box{background:var(--bg2, #111622);border:1px solid var(--border);border-radius:16px;padding:15px}
|
||||
.stat-box .val{font-family:var(--font-display);font-size:24px;font-weight:800}
|
||||
.stat-box .lbl{font-size:11.5px;color:var(--muted)}
|
||||
|
||||
/* == UI HARDENING (audit ui-ux.md) - jangan hapus marker == */
|
||||
img { max-width: 100%; height: auto; }
|
||||
@@ -0,0 +1,13 @@
|
||||
/* service-notice-badge.css — badge mode status layanan di topbar admin */
|
||||
.sn-badge{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;font-size:12px;font-weight:800;text-decoration:none;white-space:nowrap;border:1px solid transparent;line-height:1.1;transition:filter .15s ease,transform .15s ease}
|
||||
.sn-badge i{font-size:13px;line-height:1}
|
||||
.sn-badge:hover{filter:brightness(1.12);text-decoration:none}
|
||||
.sn-badge-primary{background:rgba(173,255,47,.14);color:#ADFF2F;border-color:rgba(173,255,47,.42)}
|
||||
.sn-badge-danger{background:rgba(251,113,133,.16);color:#FB7185;border-color:rgba(251,113,133,.45)}
|
||||
.sn-badge-normal{background:rgba(148,163,184,.14);color:var(--muted,#94a3b8);border-color:rgba(148,163,184,.3)}
|
||||
.sn-badge-pulse{animation:snPulse 2s ease-in-out infinite}
|
||||
@keyframes snPulse{0%,100%{box-shadow:0 0 0 0 rgba(251,113,133,.45)}50%{box-shadow:0 0 0 6px rgba(251,113,133,0)}}
|
||||
html[data-theme="light"] .sn-badge-primary{color:#4A7D08;background:rgba(92,154,10,.12);border-color:rgba(92,154,10,.35)}
|
||||
html[data-theme="light"] .sn-badge-danger{color:#DE3B5C;background:rgba(222,59,92,.12);border-color:rgba(222,59,92,.35)}
|
||||
html[data-theme="light"] .sn-badge-normal{color:#52604C;background:rgba(82,96,76,.10);border-color:rgba(82,96,76,.25)}
|
||||
@media(max-width:640px){.sn-badge{padding:5px 9px;font-size:11px;gap:5px;flex:0 0 auto}.sn-badge i{font-size:12px}}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* theme-toggle.css — tombol switch tema global (floating) */
|
||||
.theme-fab{
|
||||
position:fixed;right:16px;bottom:84px;z-index:2200;
|
||||
width:48px;height:48px;border-radius:50%;
|
||||
display:grid;place-items:center;
|
||||
border:1px solid var(--primary-glow, rgba(173,255,47,.35));
|
||||
background:var(--glass-2, rgba(17,22,34,.9));
|
||||
color:var(--primary, #ADFF2F);
|
||||
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
|
||||
box-shadow:0 6px 20px rgba(0,0,0,.35);
|
||||
cursor:pointer;font-size:20px;line-height:1;
|
||||
transition:transform .18s ease, box-shadow .2s ease;
|
||||
}
|
||||
.theme-fab:hover{transform:translateY(-2px);box-shadow:0 10px 26px var(--primary-glow, rgba(173,255,47,.35))}
|
||||
.theme-fab:active{transform:translateY(0)}
|
||||
@media(min-width:992px){.theme-fab{bottom:20px}}
|
||||
|
After Width: | Height: | Size: 983 B |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="7.600287mm"
|
||||
height="12.464276mm"
|
||||
viewBox="0 0 7.600287 12.464276"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"
|
||||
transform="translate(-113.79598,40.042711)"><path
|
||||
style="fill:#adff2f;fill-opacity:1;stroke-width:0.107258"
|
||||
d="m 117.43273,-30.438919 c -0.0226,-0.0792 -0.0735,-0.12528 -0.15839,-0.14315 -0.0413,-0.009 -0.0682,-0.004 -0.34805,0.0597 -1.47242,0.33549 -1.88059,0.43074 -1.95084,0.45524 -0.37389,0.13041 -0.69556,0.43438 -0.837,0.79096 -0.0653,0.16459 -0.0919,0.27905 -0.1039,0.44732 -0.0103,0.14374 0.003,1.03974 0.0158,1.09725 0.0201,0.0876 0.10465,0.15439 0.19386,0.15315 0.0487,-6.8e-4 2.06305,-0.45574 2.20754,-0.4987 0.37802,-0.11239 0.69013,-0.3792 0.86377,-0.73841 0.078,-0.16133 0.12189,-0.3269 0.13537,-0.51049 0.0116,-0.15842 -0.003,-1.05984 -0.0182,-1.11285 z"
|
||||
id="path100-9" /><path
|
||||
style="fill:#adff2f;fill-opacity:1;stroke-width:0.107258"
|
||||
d="m 119.40184,-33.794399 c -0.0171,-0.0656 -0.0403,-0.1077 -0.0872,-0.15845 -0.0809,-0.0875 -0.18066,-0.13298 -0.2883,-0.13148 -0.0459,6.4e-4 -0.3994,0.0909 -1.71297,0.43757 -0.91033,0.24022 -1.90622,0.50272 -2.21308,0.58333 -0.47734,0.12538 -0.56937,0.15224 -0.63705,0.18588 -0.23931,0.11896 -0.40467,0.33103 -0.46464,0.59587 -0.0119,0.0527 -0.0132,0.15063 -0.009,0.69347 0.003,0.39869 0.009,0.65655 0.015,0.69826 0.0211,0.13815 0.11792,0.25928 0.24723,0.30935 0.12485,0.0483 -0.0512,0.0892 2.38545,-0.55375 1.2025,-0.31729 2.20825,-0.58477 2.23499,-0.5944 0.29448,-0.10596 0.51406,-0.38172 0.55179,-0.69295 0.0129,-0.10592 -0.007,-1.31325 -0.0222,-1.3727 z"
|
||||
id="path99-3" /><path
|
||||
style="fill:#adff2f;fill-opacity:1;stroke-width:0.107258"
|
||||
d="m 121.36387,-37.360589 c -0.0367,-0.10809 -0.13203,-0.20366 -0.24506,-0.24555 -0.0327,-0.0121 -0.0676,-0.0165 -0.12283,-0.0155 -0.0714,0.001 -0.31757,0.0649 -3.30307,0.85287 -1.77423,0.46831 -3.24993,0.85988 -3.27931,0.87016 -0.17336,0.0607 -0.34583,0.20438 -0.43963,0.36633 -0.0479,0.0827 -0.0965,0.22099 -0.10978,0.31201 -0.005,0.0369 -0.005,0.30203 4.2e-4,0.70386 0.01,0.69923 0.01,0.69659 0.0613,0.78202 0.0419,0.0693 0.0919,0.11476 0.16642,0.15129 0.0626,0.0307 0.0784,0.0345 0.14945,0.0363 0.0792,0.002 0.0893,-6e-4 3.304,-0.84794 1.77349,-0.46749 3.25097,-0.85892 3.28329,-0.86985 0.26085,-0.0882 0.45982,-0.30176 0.5429,-0.58262 l 0.0243,-0.0821 -0.008,-0.69013 c -0.007,-0.64713 -0.009,-0.69331 -0.0248,-0.74121 z"
|
||||
id="path98-6" /><path
|
||||
style="fill:#adff2f;fill-opacity:1;stroke-width:0.107258"
|
||||
d="m 121.37687,-36.428379 c -0.0336,0.10907 -0.1263,0.20726 -0.23813,0.25229 -0.0323,0.013 -0.0671,0.0184 -0.12234,0.0189 -0.0714,6.7e-4 -0.31926,-0.056 -3.32556,-0.76046 -1.7866,-0.41866 -3.27264,-0.76895 -3.3023,-0.7784 -0.17498,-0.0558 -0.35139,-0.19466 -0.44967,-0.35394 -0.0502,-0.0813 -0.10267,-0.2182 -0.11844,-0.30882 -0.006,-0.0367 -0.0136,-0.30177 -0.0192,-0.7036 -0.01,-0.69924 -0.01,-0.69658 0.0394,-0.78343 0.04,-0.0704 0.0887,-0.11727 0.16213,-0.15587 0.0617,-0.0324 0.0774,-0.0367 0.14838,-0.0405 0.0791,-0.004 0.0893,-0.002 3.32636,0.75551 1.78583,0.41786 3.27365,0.76795 3.30626,0.77798 0.26321,0.0809 0.46806,0.28882 0.55893,0.56725 l 0.0265,0.0814 0.0117,0.69007 c 0.011,0.64708 0.0107,0.69329 -0.004,0.74162 z"
|
||||
id="path98-3-06" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 96" width="360" height="96" role="img" aria-label="OPEN-ISP">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#ADFF2F"/>
|
||||
<stop offset="100%" stop-color="#8EDB1C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="88" height="88" rx="22" fill="#0A0D14" stroke="#2A3350" stroke-width="2"/>
|
||||
<path d="M30 34 h36 M30 46 h26 M30 58 h36" stroke="url(#g)" stroke-width="7" stroke-linecap="round" fill="none"/>
|
||||
<text x="108" y="58" font-family="'Space Grotesk', 'Inter', system-ui, sans-serif" font-size="34" font-weight="700" fill="#FFFFFF">OPEN<tspan fill="#ADFF2F">-ISP</tspan></text>
|
||||
<text x="110" y="78" font-family="'Inter', system-ui, sans-serif" font-size="12" letter-spacing="2" fill="#8A94A6">ISP BILLING SYSTEM</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#6366f1"/>
|
||||
<stop offset="1" stop-color="#3b82f6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="512" height="512" rx="112" fill="url(#g)"/>
|
||||
<rect x="44" y="44" width="424" height="424" rx="96" fill="rgba(15,23,42,0.18)"/>
|
||||
<path d="M84 96c56-36 124-56 202-56 82 0 150 22 202 58v70c-60-44-128-66-202-66-72 0-140 22-202 66V96Z" fill="rgba(255,255,255,0.10)"/>
|
||||
<text x="256" y="338" text-anchor="middle" font-family="system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif" font-size="300" font-weight="900" fill="rgba(0,0,0,0.20)">A</text>
|
||||
<text x="256" y="330" text-anchor="middle" font-family="system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif" font-size="300" font-weight="900" fill="#ffffff">A</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 943 B |
@@ -0,0 +1,246 @@
|
||||
/* =========================================================================
|
||||
* OPEN-ISP admin — Global search shortcuts + Reset Filter
|
||||
* -------------------------------------------------------------------------
|
||||
* - Tekan "/" : fokus ke kolom pencarian (baris filter / modal terbuka)
|
||||
* - Tekan "Enter" : jalankan pencarian (submit form filter)
|
||||
* - Tombol "Reset" : otomatis ditambahkan ke tiap baris filter:
|
||||
* • form filter GET -> bersihkan & muat ulang tanpa query
|
||||
* • baris filter JS -> bersihkan & trigger ulang filter (keyup/change)
|
||||
* Non-invasif: tidak mengubah view apa pun, dimuat lewat injeksi /admin.
|
||||
* ========================================================================= */
|
||||
(function () {
|
||||
'use strict';
|
||||
if (window.__adminSearchInit) return;
|
||||
window.__adminSearchInit = true;
|
||||
|
||||
var NAME_SEL = 'input[name="search"], input[name="q"], input[name="keyword"], input[type="search"]';
|
||||
var FILTER_WRAP = '.srow, .filter-bar';
|
||||
var FILTER_EXTRA = 'select[name="status"], select[name="month"], select[name="year"], select[name="area"], select[name="router_id"], select[name="package_id"], select[name="collector_id"]';
|
||||
|
||||
/* ---------- util ---------- */
|
||||
function isEditable(el) {
|
||||
if (!el) return false;
|
||||
var t = (el.tagName || '').toLowerCase();
|
||||
if (t === 'textarea' || t === 'select') return true;
|
||||
if (t === 'input') {
|
||||
var ty = (el.type || 'text').toLowerCase();
|
||||
return ['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file', 'hidden'].indexOf(ty) === -1;
|
||||
}
|
||||
return el.isContentEditable === true;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || el.disabled || el.readOnly) return false;
|
||||
if (el.offsetParent === null) {
|
||||
var r = el.getBoundingClientRect();
|
||||
return r.width > 0 && r.height > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSearchLike(el) {
|
||||
if (!el || el.tagName !== 'INPUT') return false;
|
||||
if (el.matches('.sinput input, ' + NAME_SEL)) return true;
|
||||
if (el.closest('.sinput')) return true;
|
||||
var ph = (el.getAttribute('placeholder') || '').toLowerCase();
|
||||
if (/cari|search|telusur|temukan|find/.test(ph)) return true;
|
||||
var h = (el.getAttribute('onkeyup') || '') + (el.getAttribute('oninput') || '');
|
||||
if (h && /filter|search|cari/i.test(h)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
var mods = document.querySelectorAll('.mo.show, .modal.show');
|
||||
return mods.length ? mods[mods.length - 1] : null;
|
||||
}
|
||||
|
||||
function searchInputs() {
|
||||
var nodes = document.querySelectorAll('input');
|
||||
var out = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (isSearchLike(nodes[i]) && isVisible(nodes[i])) out.push(nodes[i]);
|
||||
}
|
||||
var modal = openModal();
|
||||
out.sort(function (a, b) {
|
||||
function score(x) {
|
||||
if (modal && modal.contains(x)) return 0;
|
||||
if (x.closest(FILTER_WRAP)) return 1;
|
||||
if (x.closest('.sinput')) return 2;
|
||||
return 3;
|
||||
}
|
||||
return score(a) - score(b);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function firstSearch() { return searchInputs()[0] || null; }
|
||||
|
||||
function flash(el) {
|
||||
var w = el.closest('.sinput') || el.parentElement || el;
|
||||
if (!w || !w.classList) return;
|
||||
w.classList.add('admin-search-flash');
|
||||
setTimeout(function () { w.classList.remove('admin-search-flash'); }, 900);
|
||||
}
|
||||
|
||||
function submitForm(form) {
|
||||
if (!form) return;
|
||||
if (typeof form.requestSubmit === 'function') {
|
||||
try { form.requestSubmit(); return; } catch (e) { /* fallback */ }
|
||||
}
|
||||
form.submit();
|
||||
}
|
||||
|
||||
/* ---------- keyboard shortcut ---------- */
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.defaultPrevented || e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
var el = document.activeElement;
|
||||
var typing = isEditable(el);
|
||||
|
||||
// Enter di kolom pencarian -> jalankan pencarian
|
||||
if (e.key === 'Enter' && typing && el && el.tagName === 'INPUT') {
|
||||
var isSearch = !!(el.closest('.sinput') || isSearchLike(el));
|
||||
var form = el.form || el.closest('form');
|
||||
if (isSearch && form) {
|
||||
e.preventDefault();
|
||||
submitForm(form);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// "/" -> fokus kolom pencarian (hanya saat tidak sedang mengetik)
|
||||
if ((e.key === '/' || e.key === 'Slash' || e.code === 'Slash') && !typing) {
|
||||
var inp = firstSearch();
|
||||
if (inp) {
|
||||
e.preventDefault();
|
||||
inp.focus();
|
||||
try { inp.setSelectionRange(inp.value.length, inp.value.length); } catch (err) {}
|
||||
flash(inp);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
/* ---------- helpers reset ---------- */
|
||||
function baseAction(form) {
|
||||
var a = form.getAttribute('action') || location.pathname || '/';
|
||||
return (a.split('?')[0]) || location.pathname || '/';
|
||||
}
|
||||
|
||||
function clearField(el) {
|
||||
var ty = (el.type || '').toLowerCase();
|
||||
if (ty === 'hidden') return;
|
||||
if (ty === 'checkbox' || ty === 'radio') { el.checked = el.defaultChecked; return; }
|
||||
if (el.tagName === 'SELECT') {
|
||||
var idx = 0;
|
||||
for (var o = 0; o < el.options.length; o++) if (el.options[o].defaultSelected) { idx = o; break; }
|
||||
el.selectedIndex = idx;
|
||||
return;
|
||||
}
|
||||
el.value = '';
|
||||
}
|
||||
|
||||
function fireEvents(el) {
|
||||
try { el.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
|
||||
try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
|
||||
try { el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: '', code: '' })); } catch (e) {}
|
||||
}
|
||||
|
||||
function makeResetButton(scope, onClick) {
|
||||
var btn = document.createElement('a');
|
||||
btn.href = 'javascript:void(0)';
|
||||
btn.className = 'btn btn-g btn-sm';
|
||||
btn.setAttribute('data-admin-reset', '1');
|
||||
btn.setAttribute('title', 'Reset semua filter');
|
||||
btn.innerHTML = '<i class="bi bi-arrow-counterclockwise"></i> Reset';
|
||||
btn.addEventListener('click', function (ev) { ev.preventDefault(); onClick(); });
|
||||
return btn;
|
||||
}
|
||||
|
||||
/* ---------- A) form filter GET ---------- */
|
||||
function isFilterForm(f) {
|
||||
if (!f) return false;
|
||||
if (f.closest('.mo, .modal')) return false; // jangan sentuh form di modal
|
||||
if ((f.getAttribute('method') || 'get').toLowerCase() !== 'get') return false;
|
||||
if (f.matches(FILTER_WRAP)) return true;
|
||||
if (f.querySelector('.sinput, ' + NAME_SEL + ', ' + FILTER_EXTRA)) return true;
|
||||
var ins = f.querySelectorAll('input');
|
||||
for (var i = 0; i < ins.length; i++) if (isSearchLike(ins[i])) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasReset(f) {
|
||||
var ctrls = f.querySelectorAll('a, button');
|
||||
for (var i = 0; i < ctrls.length; i++) {
|
||||
var c = ctrls[i];
|
||||
if (c.hasAttribute('data-admin-reset')) continue;
|
||||
if (c.tagName === 'BUTTON' && (c.getAttribute('type') || '').toLowerCase() === 'reset') return true;
|
||||
if (c.hasAttribute('data-reset')) return true;
|
||||
if ((c.textContent || '').toLowerCase().indexOf('reset') !== -1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setupFormReset(form) {
|
||||
if (form.querySelector('[data-admin-reset]') || hasReset(form)) return;
|
||||
form.appendChild(makeResetButton(form, function () {
|
||||
var fields = form.querySelectorAll('input, select');
|
||||
for (var i = 0; i < fields.length; i++) clearField(fields[i]);
|
||||
location.href = baseAction(form);
|
||||
}));
|
||||
}
|
||||
|
||||
/* ---------- B) baris filter client-side ---------- */
|
||||
function clientScope(el) {
|
||||
return el.closest(FILTER_WRAP) || el.closest('.sinput') || null;
|
||||
}
|
||||
|
||||
function setupClientReset(scope) {
|
||||
if (!scope || scope.hasAttribute('data-admin-reset-scope')) return;
|
||||
scope.setAttribute('data-admin-reset-scope', '1');
|
||||
// jangan dobel bila sudah ada tombol reset di dalam/ sekitar
|
||||
var near = scope.querySelector('[data-admin-reset]') ||
|
||||
(scope.parentElement && scope.parentElement.querySelector('[data-admin-reset]'));
|
||||
if (near || hasReset(scope)) return;
|
||||
|
||||
var btn = makeResetButton(scope, function () {
|
||||
var fields = scope.querySelectorAll('input, select');
|
||||
for (var i = 0; i < fields.length; i++) { clearField(fields[i]); fireEvents(fields[i]); }
|
||||
});
|
||||
|
||||
if (scope.matches(FILTER_WRAP)) {
|
||||
scope.appendChild(btn);
|
||||
} else if (scope.parentElement) {
|
||||
scope.insertAdjacentElement('afterend', btn);
|
||||
} else {
|
||||
scope.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// A) form filter GET
|
||||
var forms = document.querySelectorAll('form');
|
||||
for (var i = 0; i < forms.length; i++) if (isFilterForm(forms[i])) setupFormReset(forms[i]);
|
||||
|
||||
// B) baris filter client-side (input di luar form)
|
||||
var ins = document.querySelectorAll('input');
|
||||
for (var j = 0; j < ins.length; j++) {
|
||||
var el = ins[j];
|
||||
if (!isSearchLike(el)) continue;
|
||||
if (el.closest('form')) continue; // ditangani bagian A
|
||||
if (el.closest('.mo, .modal')) continue; // jangan di modal
|
||||
var sc = clientScope(el);
|
||||
if (sc) setupClientReset(sc);
|
||||
}
|
||||
|
||||
// hint tooltip
|
||||
var inputs = searchInputs();
|
||||
for (var k = 0; k < inputs.length; k++) {
|
||||
if (!inputs[k].getAttribute('title')) inputs[k].setAttribute('title', 'Tekan "/" untuk fokus, "Enter" untuk mencari');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setup);
|
||||
} else {
|
||||
setup();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Membersihkan Service Worker yang salah terdaftar untuk area /admin.
|
||||
Service Worker yang benar HANYA untuk portal pelanggan (scope /customer/).
|
||||
File ini di-inject otomatis ke setiap halaman admin. */
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||||
regs.forEach(function (r) {
|
||||
if (!r.scope || r.scope.indexOf('/customer/') === -1) {
|
||||
r.unregister().catch(function () {});
|
||||
}
|
||||
});
|
||||
}).catch(function () {});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/* admin-theme-sync.js — Menjaga konsistensi tema admin (dark/light).
|
||||
PENTING: observer TIDAK boleh menulis ulang `data-theme` (bisa memicu loop). */
|
||||
(function () {
|
||||
var KEY = 'app-theme';
|
||||
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
|
||||
|
||||
// Sinkronkan atribut turunan berdasarkan data-theme saat ini (tanpa menulis data-theme).
|
||||
function syncAttrs() {
|
||||
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||
var bs = isLight ? 'light' : 'dark';
|
||||
if (document.documentElement.getAttribute('data-bs-theme') !== bs) document.documentElement.setAttribute('data-bs-theme', bs);
|
||||
if (document.documentElement.classList.contains('light-theme') !== isLight) document.documentElement.classList.toggle('light-theme', isLight);
|
||||
if (document.body && document.body.classList.contains('light-theme') !== isLight) document.body.classList.toggle('light-theme', isLight);
|
||||
try { if (document.documentElement.style.colorScheme !== bs) document.documentElement.style.colorScheme = bs; } catch (e) {}
|
||||
}
|
||||
|
||||
function applyTheme(t) {
|
||||
t = t || read() || 'dark';
|
||||
if (document.documentElement.getAttribute('data-theme') !== t) document.documentElement.setAttribute('data-theme', t);
|
||||
syncAttrs();
|
||||
return t;
|
||||
}
|
||||
|
||||
window.__applyAdminTheme = applyTheme;
|
||||
applyTheme();
|
||||
|
||||
// Observer hanya menyinkronkan atribut turunan (AMAN, tanpa loop).
|
||||
try { new MutationObserver(syncAttrs).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); } catch (e) {}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () { applyTheme(); });
|
||||
window.addEventListener('pageshow', function () { applyTheme(); });
|
||||
|
||||
if (typeof window.toggleAppTheme !== 'function') {
|
||||
window.toggleAppTheme = function () {
|
||||
var cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
||||
try { localStorage.setItem(KEY, cur); } catch (e) {}
|
||||
applyTheme(cur);
|
||||
if (typeof window.updateThemeToggleIcons === 'function') window.updateThemeToggleIcons(cur);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target && e.target.closest ? e.target.closest('.theme-toggle-btn') : null;
|
||||
if (!btn) return;
|
||||
if (btn.dataset.themeDelegate === '1') return;
|
||||
btn.dataset.themeDelegate = '1';
|
||||
if (!btn.hasAttribute('onclick')) { e.preventDefault(); window.toggleAppTheme(); }
|
||||
}, true);
|
||||
})();
|
||||
|
||||
// ── Dropdown Aksi (kebab menu) ──
|
||||
// Menu di-"portal" ke <body> saat terbuka, lalu diposisikan fixed relatif tombol.
|
||||
// Ini menghilangkan SEMUA pengaruh ancestor (overflow tabel/kartu, stacking, dsb)
|
||||
// sehingga menu selalu solid & tampil utuh di mana pun (termasuk baris Lunas).
|
||||
var _actOpen = null, _actMenu = null, _actHome = null;
|
||||
function actCloseAll() {
|
||||
if (_actMenu) {
|
||||
try { _actMenu.classList.remove('fixed', 'up'); _actMenu.style.left = ''; _actMenu.style.top = ''; } catch (e) {}
|
||||
try {
|
||||
if (_actHome && _actHome.parent) _actHome.parent.insertBefore(_actMenu, _actHome.next);
|
||||
else document.body.appendChild(_actMenu);
|
||||
} catch (e) {}
|
||||
}
|
||||
document.querySelectorAll('.act.open').forEach(function (x) { x.classList.remove('open'); });
|
||||
_actOpen = null; _actMenu = null; _actHome = null;
|
||||
}
|
||||
function actPlace(btn, menu) {
|
||||
menu.style.left = '0px'; menu.style.top = '0px';
|
||||
var w = menu.offsetWidth, h = menu.offsetHeight;
|
||||
var br = btn.getBoundingClientRect();
|
||||
var vw = window.innerWidth, vh = window.innerHeight, pad = 8, gap = 6;
|
||||
var left = br.right - w; // rata kanan ke tombol
|
||||
if (left + w > vw - pad) left = vw - pad - w;
|
||||
if (left < pad) left = pad;
|
||||
var top = br.bottom + gap; // default: di bawah
|
||||
if (top + h > vh - pad) top = br.top - gap - h; // tak cukup -> ke atas
|
||||
if (top < pad) top = pad;
|
||||
menu.style.left = Math.round(left) + 'px';
|
||||
menu.style.top = Math.round(top) + 'px';
|
||||
}
|
||||
window.actToggle = function (e, btn) {
|
||||
try { if (e) { e.preventDefault(); e.stopPropagation(); } } catch (_) {}
|
||||
var a = btn && btn.closest ? btn.closest('.act') : null; if (!a) return;
|
||||
var wasOpen = a.classList.contains('open');
|
||||
actCloseAll();
|
||||
if (!wasOpen) {
|
||||
a.classList.add('open');
|
||||
var menu = a.querySelector('.act-menu');
|
||||
if (!menu) return;
|
||||
_actOpen = a; _actMenu = menu;
|
||||
_actHome = { parent: menu.parentNode, next: menu.nextSibling };
|
||||
document.body.appendChild(menu); // pindah ke body
|
||||
menu.classList.add('fixed');
|
||||
actPlace(btn, menu);
|
||||
}
|
||||
};
|
||||
// klik di luar menu -> tutup; klik di dalam menu -> biarkan aksi jalan dulu, tutup setelahnya
|
||||
document.addEventListener('click', function (ev) {
|
||||
if (_actMenu && ev.target && _actMenu.contains(ev.target)) { setTimeout(actCloseAll, 0); return; }
|
||||
actCloseAll();
|
||||
});
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') actCloseAll(); });
|
||||
window.addEventListener('scroll', actCloseAll, true);
|
||||
window.addEventListener('resize', actCloseAll);
|
||||
@@ -0,0 +1,185 @@
|
||||
/* admin-toast.js — Notifikasi bertema untuk seluruh admin.
|
||||
* - toast(...) : popup kecil (opsional, untuk info ringan)
|
||||
* - notify(...) : OVERLAY layar blur + ikon + teks (untuk hasil aksi)
|
||||
* - notifyLoading(...) : OVERLAY blur + spinner (proses berjalan)
|
||||
* Otomatis:
|
||||
* - banner flash (.alert[data-flash]) -> overlay notify
|
||||
* - window.alert() -> overlay notify
|
||||
* - submit form POST -> notifyLoading (blur + spinner)
|
||||
*/
|
||||
(function () {
|
||||
if (window.__adminNotify) return;
|
||||
window.__adminNotify = true;
|
||||
|
||||
var CSS = [
|
||||
/* ---- overlay ---- */
|
||||
'.nf-ov{position:fixed;inset:0;z-index:2147483000;display:none;align-items:center;justify-content:center;padding:20px;background:rgba(4,7,12,.5);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);opacity:0;transition:opacity .25s ease}',
|
||||
'.nf-ov.show{display:flex;opacity:1}',
|
||||
'.nf-card{background:var(--bg2,#161b22);border:1px solid var(--border,rgba(255,255,255,.14));border-radius:18px;padding:32px 26px;max-width:min(92vw,410px);width:100%;text-align:center;box-shadow:0 26px 70px rgba(0,0,0,.6);transform:scale(.9);transition:transform .3s cubic-bezier(.2,.9,.3,1.25)}',
|
||||
'.nf-ov.show .nf-card{transform:scale(1)}',
|
||||
'.nf-spin{width:56px;height:56px;margin:0 auto 16px;border:4px solid var(--pdim,rgba(173,255,47,.2));border-top-color:var(--primary,#ADFF2F);border-radius:50%;animation:nfspin .8s linear infinite}',
|
||||
'@keyframes nfspin{to{transform:rotate(360deg)}}',
|
||||
'.nf-ic{font-size:60px;line-height:1;display:block;margin:0 auto 12px;animation:nfpop .35s cubic-bezier(.2,.9,.3,1.4)}',
|
||||
'@keyframes nfpop{0%{transform:scale(.4);opacity:0}100%{transform:scale(1);opacity:1}}',
|
||||
'.nf-success{color:var(--success,#3fb950)} .nf-error{color:var(--danger,#f85149)} .nf-warning{color:var(--warning,#d29922)} .nf-info{color:var(--info,#58a6ff)}',
|
||||
'.nf-title{font-size:17px;font-weight:800;margin-bottom:7px;color:var(--text,#e6edf3)}',
|
||||
'.nf-msg{font-size:13.5px;color:var(--muted,#7d8590);line-height:1.55;max-height:46vh;overflow:auto;word-break:break-word}',
|
||||
'.nf-msg b,.nf-msg code{color:var(--text,#e6edf3)}',
|
||||
'.nf-close{margin-top:18px;background:var(--pdim,rgba(173,255,47,.14));color:var(--primary,#ADFF2F);border:1px solid var(--primary,#ADFF2F);border-radius:10px;padding:8px 18px;font-weight:700;font-size:13px;cursor:pointer;font-family:inherit}',
|
||||
'.nf-close:hover{filter:brightness(1.1)}',
|
||||
'html[data-theme="light"] .nf-card,html.light-theme .nf-card{background:#fff;border-color:#e2e8f0}',
|
||||
'html[data-theme="light"] .nf-title,html.light-theme .nf-title{color:#0f172a}',
|
||||
'html[data-theme="light"] .nf-msg,html.light-theme .nf-msg{color:#475569}',
|
||||
'html[data-theme="light"] .nf-ov,html.light-theme .nf-ov{background:rgba(226,232,240,.55)}',
|
||||
/* ---- toast kecil ---- */
|
||||
'.toast-wrap{position:fixed;top:16px;right:16px;z-index:2147482900;display:flex;flex-direction:column;gap:10px;max-width:min(92vw,430px);pointer-events:none}',
|
||||
'.toast{pointer-events:auto;display:flex;gap:11px;align-items:flex-start;background:var(--bg2,#161b22);border:1px solid var(--border,rgba(255,255,255,.14));border-left:4px solid var(--info,#58a6ff);border-radius:12px;padding:12px 14px;box-shadow:0 16px 44px rgba(0,0,0,.5);color:var(--text,#e6edf3);transform:translateX(120%);opacity:0;transition:transform .28s cubic-bezier(.2,.8,.2,1),opacity .28s;font-size:13px;line-height:1.4}',
|
||||
'.toast.show{transform:translateX(0);opacity:1}',
|
||||
'.toast-ic{font-size:18px;line-height:1.15;margin-top:1px;flex:0 0 auto}',
|
||||
'.toast-body{flex:1;min-width:0}.toast-title{font-weight:700;margin-bottom:2px}.toast-msg{opacity:.94;word-break:break-word}',
|
||||
'.toast-x{margin-left:6px;background:transparent;border:none;color:var(--muted,#7d8590);font-size:19px;line-height:1;cursor:pointer;padding:0 2px}',
|
||||
'.toast-success{border-left-color:var(--success,#3fb950)} .toast-success .toast-ic{color:var(--success,#3fb950)}',
|
||||
'.toast-error{border-left-color:var(--danger,#f85149)} .toast-error .toast-ic{color:var(--danger,#f85149)}',
|
||||
'.toast-warning{border-left-color:var(--warning,#d29922)} .toast-warning .toast-ic{color:var(--warning,#d29922)}',
|
||||
'.toast-info{border-left-color:var(--info,#58a6ff)} .toast-info .toast-ic{color:var(--info,#58a6ff)}',
|
||||
/* ---- mobile ---- */
|
||||
'@media(max-width:520px){.nf-ov{padding:14px;align-items:center}.nf-card{padding:26px 18px;border-radius:16px;max-width:100%}.nf-ic{font-size:50px}.nf-title{font-size:15.5px}.nf-msg{font-size:13px;max-height:52vh}.toast-wrap{top:10px;right:10px;left:10px;max-width:none}}'
|
||||
].join('\n');
|
||||
|
||||
function ensureCss() {
|
||||
if (document.getElementById('adminNotifyCss')) return;
|
||||
var st = document.createElement('style'); st.id = 'adminNotifyCss'; st.textContent = CSS; document.head.appendChild(st);
|
||||
}
|
||||
|
||||
var ICONS = { success: 'bi-check-circle-fill', error: 'bi-x-octagon-fill', warning: 'bi-exclamation-triangle-fill', info: 'bi-info-circle-fill' };
|
||||
|
||||
function autoDur(type, text, base) {
|
||||
var len = String(text || '').length;
|
||||
var b = base || (type === 'error' ? 2800 : type === 'warning' ? 2400 : 2000);
|
||||
return Math.min(b + Math.floor(len / 60) * 900, 12000);
|
||||
}
|
||||
|
||||
/* ================= TOAST (kecil) ================= */
|
||||
function toastWrap() {
|
||||
ensureCss();
|
||||
var w = document.getElementById('adminToastWrap');
|
||||
if (!w) { w = document.createElement('div'); w.id = 'adminToastWrap'; w.className = 'toast-wrap'; document.body.appendChild(w); }
|
||||
return w;
|
||||
}
|
||||
window.toast = function (type, title, message, opts) {
|
||||
try {
|
||||
if (type && typeof type === 'object') { opts = type; type = opts.type; title = opts.title; message = opts.message || opts.text; }
|
||||
type = String(type || 'info'); if (!ICONS[type]) type = 'info';
|
||||
opts = opts || {};
|
||||
var w = toastWrap();
|
||||
var el = document.createElement('div');
|
||||
el.className = 'toast toast-' + type;
|
||||
el.innerHTML = '<i class="bi ' + ICONS[type] + ' toast-ic"></i><div class="toast-body">' + (title ? '<div class="toast-title">' + title + '</div>' : '') + '<div class="toast-msg">' + (message == null ? '' : message) + '</div></div><button class="toast-x">×</button>';
|
||||
w.appendChild(el);
|
||||
requestAnimationFrame(function () { el.classList.add('show'); });
|
||||
var dur = opts.duration || autoDur(type, (title || '') + ' ' + (message || ''));
|
||||
var t = setTimeout(close, dur);
|
||||
function close() { el.classList.remove('show'); setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 280); }
|
||||
el.querySelector('.toast-x').addEventListener('click', function () { clearTimeout(t); close(); });
|
||||
el.addEventListener('mouseenter', function () { clearTimeout(t); });
|
||||
el.addEventListener('mouseleave', function () { t = setTimeout(close, 2200); });
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
/* ================= OVERLAY (blur + ikon) ================= */
|
||||
var _ovTimer = null;
|
||||
function ov() {
|
||||
ensureCss();
|
||||
var el = document.getElementById('nfOv');
|
||||
if (!el) {
|
||||
el = document.createElement('div'); el.id = 'nfOv'; el.className = 'nf-ov';
|
||||
el.innerHTML = '<div class="nf-card" role="alertdialog" aria-live="assertive"><div class="nf-spin"></div><i class="bi nf-ic"></i><div class="nf-title"></div><div class="nf-msg"></div><button type="button" class="nf-close">Tutup</button></div>';
|
||||
document.body.appendChild(el);
|
||||
el.querySelector('.nf-close').addEventListener('click', hideOv);
|
||||
el.addEventListener('click', function (e) { if (e.target === el) hideOv(); });
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') hideOv(); });
|
||||
}
|
||||
return el;
|
||||
}
|
||||
function hideOv() { var el = document.getElementById('nfOv'); if (!el) return; clearTimeout(_ovTimer); el.classList.remove('show'); }
|
||||
window.closeNotify = hideOv;
|
||||
|
||||
window.notifyLoading = function (text, title) {
|
||||
var el = ov();
|
||||
el.querySelector('.nf-spin').style.display = '';
|
||||
var ic = el.querySelector('.nf-ic'); ic.style.display = 'none'; ic.className = 'bi nf-ic';
|
||||
var t = el.querySelector('.nf-title'); if (title) { t.style.display = ''; t.textContent = title; } else { t.style.display = 'none'; t.textContent = ''; }
|
||||
el.querySelector('.nf-msg').innerHTML = text || '';
|
||||
el.querySelector('.nf-close').style.display = 'none';
|
||||
clearTimeout(_ovTimer);
|
||||
el.classList.add('show');
|
||||
return { loading: window.notifyLoading, done: function (ty, ti, ms) { window.notify(ty, ti, ms); }, close: hideOv };
|
||||
};
|
||||
|
||||
window.notify = function (type, title, message, opts) {
|
||||
if (type && typeof type === 'object') { opts = type; type = opts.type; title = opts.title; message = opts.message || opts.text; }
|
||||
type = String(type || 'info'); if (!ICONS[type]) type = 'info';
|
||||
opts = opts || {};
|
||||
var el = ov();
|
||||
el.querySelector('.nf-spin').style.display = 'none';
|
||||
var ic = el.querySelector('.nf-ic'); ic.style.display = ''; ic.className = 'bi ' + ICONS[type] + ' nf-ic nf-' + type;
|
||||
var t = el.querySelector('.nf-title'); if (title) { t.style.display = ''; t.textContent = title; } else { t.style.display = 'none'; t.textContent = ''; }
|
||||
el.querySelector('.nf-msg').innerHTML = (message == null ? '' : message);
|
||||
el.querySelector('.nf-close').style.display = '';
|
||||
clearTimeout(_ovTimer);
|
||||
el.classList.add('show');
|
||||
var dur = opts.duration != null ? opts.duration : autoDur(type, (title || '') + ' ' + (message || ''));
|
||||
if (dur > 0) _ovTimer = setTimeout(hideOv, dur);
|
||||
return el;
|
||||
};
|
||||
|
||||
/* ================= integrasi otomatis ================= */
|
||||
function flashType(cls) {
|
||||
if (cls.indexOf('alert-s') !== -1) return 'success';
|
||||
if (cls.indexOf('alert-d') !== -1) return 'error';
|
||||
if (cls.indexOf('alert-w') !== -1) return 'warning';
|
||||
return 'info';
|
||||
}
|
||||
function convertFlash() {
|
||||
var list = document.querySelectorAll('.alert[data-flash]');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var type = flashType(a.className || '');
|
||||
var html = a.innerHTML.replace(/^\s*<i[^>]*>\s*<\/i>\s*/i, '').trim();
|
||||
a.style.display = 'none';
|
||||
(function (tp, h) {
|
||||
var title = tp === 'success' ? 'Berhasil' : tp === 'error' ? 'Gagal' : tp === 'warning' ? 'Perhatian' : 'Info';
|
||||
// tampilkan overlay langsung (aksi sudah selesai setelah redirect)
|
||||
window.notify(tp, title, h);
|
||||
})(type, html);
|
||||
}
|
||||
}
|
||||
window.__adminFlashToNotify = convertFlash;
|
||||
|
||||
var _nativeAlert = window.alert;
|
||||
window.alert = function (msg) {
|
||||
try {
|
||||
var t = String(msg == null ? '' : msg);
|
||||
var low = t.toLowerCase();
|
||||
var isErr = /gagal|error|tidak |invalid|tidak valid|kosong|forbidden|ditolak|harus|wajib|salah|exception|kedaluwarsa|tidak ditemukan|not found|401|403|500/.test(low);
|
||||
var type = isErr ? 'error' : 'success';
|
||||
window.notify(type, isErr ? 'Gagal' : 'Berhasil', t.replace(/\n/g, '<br>'));
|
||||
} catch (e) { try { _nativeAlert(msg); } catch (_) { } }
|
||||
};
|
||||
|
||||
// form POST → tampilkan loading blur
|
||||
document.addEventListener('submit', function (e) {
|
||||
try {
|
||||
var f = e.target;
|
||||
if (!f || f.tagName !== 'FORM') return;
|
||||
if ((f.getAttribute('method') || 'get').toLowerCase() !== 'post') return;
|
||||
if (f.getAttribute('target') === '_blank') return;
|
||||
if (f.hasAttribute('data-no-loading')) return;
|
||||
if (e.defaultPrevented) return;
|
||||
window.notifyLoading(f.getAttribute('data-loading') || 'Menyimpan perubahan…', 'Mohon tunggu');
|
||||
} catch (_) { }
|
||||
}, false);
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', convertFlash);
|
||||
else convertFlash();
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
/* page-loader.js — Animasi saat berpindah halaman.
|
||||
- Muncul saat klik link (navigasi same-origin) / submit form.
|
||||
- Sembunyi saat halaman baru selesai dimuat atau saat PWA resume (bfcache).
|
||||
- Fallback timer: bila tak jadi navigasi (mis. AJAX), loader otomatis disembunyikan. */
|
||||
(function () {
|
||||
var bar = null, ov = null, tShow = null, tBar = null, tFallback = null, w = 0;
|
||||
|
||||
function build() {
|
||||
if (bar || !document.body) return;
|
||||
bar = document.createElement('div'); bar.id = 'pgbar';
|
||||
ov = document.createElement('div'); ov.id = 'pgload';
|
||||
ov.innerHTML = '<div class="pgbox"><div class="pgspin"></div><div class="pgtxt">Memuat…</div></div>';
|
||||
document.body.appendChild(bar);
|
||||
document.body.appendChild(ov);
|
||||
}
|
||||
|
||||
function startBar() {
|
||||
build(); if (!bar) return;
|
||||
w = 8; bar.classList.add('on'); bar.style.width = '8%';
|
||||
clearInterval(tBar);
|
||||
tBar = setInterval(function () { w = Math.min(w + Math.max(2, (90 - w) * 0.12), 90); bar.style.width = w.toFixed(1) + '%'; }, 220);
|
||||
}
|
||||
|
||||
function show(delay, fallbackMs) {
|
||||
build();
|
||||
clearTimeout(tShow); clearTimeout(tFallback);
|
||||
tShow = setTimeout(function () { startBar(); if (ov) ov.classList.add('on'); }, delay == null ? 150 : delay);
|
||||
if (fallbackMs) tFallback = setTimeout(hide, fallbackMs);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clearTimeout(tShow); clearTimeout(tFallback); clearInterval(tBar);
|
||||
if (bar) { bar.style.width = '100%'; bar.classList.remove('on'); }
|
||||
if (ov) ov.classList.remove('on');
|
||||
setTimeout(function () { if (bar) bar.style.width = '0'; }, 300);
|
||||
}
|
||||
|
||||
function samePage(u) {
|
||||
try { return u.origin === location.origin && u.pathname === location.pathname && u.search === location.search; } catch (e) { return false; }
|
||||
}
|
||||
function eligible(a) {
|
||||
if (!a) return false;
|
||||
if (a.hasAttribute('download') || a.target === '_blank' || a.hasAttribute('data-no-loader')) return false;
|
||||
var href = a.getAttribute('href');
|
||||
if (!href || href.charAt(0) === '#') return false;
|
||||
if (/^(mailto:|tel:|javascript:|blob:|data:)/i.test(href)) return false;
|
||||
var u; try { u = new URL(a.href, location.href); } catch (e) { return false; }
|
||||
if (u.origin !== location.origin) return false;
|
||||
if (samePage(u) && !u.hash) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
|
||||
if (eligible(a)) show(150, 12000); // navigasi nyata → fallback lama
|
||||
}, true);
|
||||
|
||||
document.addEventListener('submit', function (e) {
|
||||
if (e.defaultPrevented) return;
|
||||
var f = e.target;
|
||||
if (!f || f.hasAttribute('data-no-loader') || f.target === '_blank') return;
|
||||
// Bisa jadi AJAX → fallback lebih pendek; bila navigasi nyata, dokumen berganti & timer hilang
|
||||
show(120, 2500);
|
||||
}, true);
|
||||
|
||||
window.addEventListener('pageshow', hide);
|
||||
window.addEventListener('load', hide);
|
||||
document.addEventListener('DOMContentLoaded', function () { if (bar) hide(); });
|
||||
})();
|
||||
@@ -0,0 +1,63 @@
|
||||
/* service-notice-badge.js — badge mode status layanan di topbar admin.
|
||||
SELALU tampil; yang berbeda hanya label & warna (Normal / Gamas / Perbaikan Sistem).
|
||||
Data disuntik server-side via window.__serviceNotice. Non-invasif: tidak mengubah view. */
|
||||
(function () {
|
||||
var n = window.__serviceNotice;
|
||||
if (!n || !n.mode) return;
|
||||
|
||||
function labelOf() {
|
||||
if (n.badgeLabel) return n.badgeLabel;
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'Gamas';
|
||||
if (n.mode === 'PERBAIKAN') return 'Perbaikan Sistem';
|
||||
return 'Normal';
|
||||
}
|
||||
function iconOf() {
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'bi-exclamation-triangle-fill';
|
||||
if (n.mode === 'PERBAIKAN') return 'bi-tools';
|
||||
return 'bi-check-circle-fill';
|
||||
}
|
||||
function clsOf() {
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'sn-badge-danger sn-badge-pulse';
|
||||
if (n.mode === 'PERBAIKAN') return 'sn-badge-primary';
|
||||
return 'sn-badge-normal';
|
||||
}
|
||||
function titleOf() {
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'Status layanan: Gangguan Masal' + (n.estimateText ? ' \u00b7 estimasi selesai ' + n.estimateText : '');
|
||||
if (n.mode === 'PERBAIKAN') return 'Status layanan: Perbaikan Sistem' + (n.estimateText ? ' \u00b7 estimasi selesai ' + n.estimateText : '');
|
||||
return 'Status layanan: Normal (tidak ada gangguan)';
|
||||
}
|
||||
|
||||
function makeBadge() {
|
||||
var a = document.createElement('a');
|
||||
a.href = '/admin/service-notice';
|
||||
a.setAttribute('data-notice-badge', '1');
|
||||
a.className = 'sn-badge ' + clsOf();
|
||||
a.title = titleOf() + ' \u2014 klik untuk ubah';
|
||||
a.innerHTML = '<i class="bi ' + iconOf() + '"></i><span>' + labelOf() + '</span>';
|
||||
return a;
|
||||
}
|
||||
|
||||
function place() {
|
||||
var existing = document.querySelector('[data-notice-badge]');
|
||||
if (existing) { existing.replaceWith(makeBadge()); return true; }
|
||||
var target = document.querySelector('.topbar .tb-right') || document.querySelector('.tb-right');
|
||||
if (!target) return false;
|
||||
target.insertBefore(makeBadge(), target.firstChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (place()) return;
|
||||
var tries = 0;
|
||||
var t = setInterval(function () {
|
||||
if (place() || ++tries > 20) clearInterval(t);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
window.addEventListener('pageshow', function () { setTimeout(place, 100); });
|
||||
})();
|
||||
@@ -0,0 +1,61 @@
|
||||
/* theme-toggle.js — switch tema global (portal & pelanggan).
|
||||
- Sinkronkan data-theme + data-bs-theme + class .light-theme + color-scheme
|
||||
- Pakai #topbarThemeBtn / .theme-fab yang sudah ada, jika tidak buat floating.
|
||||
- MutationObserver: bila skrip lain mengubah data-theme, atribut lain ikut sinkron (live).
|
||||
- pageshow/visibilitychange: menyegarkan saat PWA resume / bfcache. */
|
||||
(function () {
|
||||
var KEY = 'landing-theme';
|
||||
function current() { return document.documentElement.getAttribute('data-theme') || 'dark'; }
|
||||
function syncIcon() {
|
||||
var isLight = current() === 'light';
|
||||
var btn = document.getElementById('topbarThemeBtn');
|
||||
if (btn) { var i = btn.querySelector('i'); if (i) i.className = isLight ? 'bi bi-moon-stars' : 'bi bi-sun'; }
|
||||
}
|
||||
function sync() {
|
||||
var isLight = current() === 'light';
|
||||
var bs = isLight ? 'light' : 'dark';
|
||||
if (document.documentElement.getAttribute('data-bs-theme') !== bs) document.documentElement.setAttribute('data-bs-theme', bs);
|
||||
if (document.documentElement.classList.contains('light-theme') !== isLight) document.documentElement.classList.toggle('light-theme', isLight);
|
||||
if (document.body && document.body.classList.contains('light-theme') !== isLight) document.body.classList.toggle('light-theme', isLight);
|
||||
try { if (document.documentElement.style.colorScheme !== bs) document.documentElement.style.colorScheme = bs; } catch (e) {}
|
||||
syncIcon();
|
||||
}
|
||||
function apply(t) {
|
||||
t = t || (function () { try { return localStorage.getItem(KEY); } catch (e) { return null; } })() || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
sync();
|
||||
return t;
|
||||
}
|
||||
window.applyTheme = apply;
|
||||
window.toggleTheme = function () {
|
||||
var cur = current() === 'dark' ? 'light' : 'dark';
|
||||
try { localStorage.setItem(KEY, cur); } catch (e) {}
|
||||
apply(cur);
|
||||
try { document.body && document.body.getBoundingClientRect(); } catch (e) {} // paksa repaint
|
||||
};
|
||||
apply();
|
||||
|
||||
// Bila skrip lain (mis. toggleDashTheme) mengubah data-theme, sinkronkan atribut lain
|
||||
try { new MutationObserver(sync).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); } catch (e) {}
|
||||
|
||||
function bind(el) {
|
||||
if (!el || el.dataset.themeBound || el.hasAttribute('onclick')) return;
|
||||
el.dataset.themeBound = '1';
|
||||
el.addEventListener('click', function (e) { e.preventDefault(); window.toggleTheme(); });
|
||||
}
|
||||
function mount() {
|
||||
var top = document.getElementById('topbarThemeBtn');
|
||||
if (top) { bind(top); syncIcon(); return; }
|
||||
var fab = document.querySelector('.theme-fab');
|
||||
if (fab) { bind(fab); return; }
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button'; b.className = 'theme-fab';
|
||||
b.setAttribute('aria-label', 'Ganti tema'); b.setAttribute('title', 'Mode Gelap / Terang');
|
||||
b.innerHTML = '<i class="bi bi-circle-half"></i>';
|
||||
b.addEventListener('click', window.toggleTheme);
|
||||
document.body.appendChild(b);
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount); else mount();
|
||||
window.addEventListener('pageshow', function () { apply(); });
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) sync(); });
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('app-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
if (savedTheme === 'light') {
|
||||
document.body?.classList.add('light-theme');
|
||||
}
|
||||
})();
|
||||
|
||||
function toggleAppTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
if (document.body) {
|
||||
document.body.classList.toggle('light-theme', newTheme === 'light');
|
||||
}
|
||||
localStorage.setItem('app-theme', newTheme);
|
||||
updateThemeToggleIcons(newTheme);
|
||||
}
|
||||
|
||||
function updateThemeToggleIcons(theme) {
|
||||
const icons = document.querySelectorAll('.theme-toggle-icon');
|
||||
const btns = document.querySelectorAll('.theme-toggle-btn');
|
||||
icons.forEach(icon => {
|
||||
if (theme === 'light') {
|
||||
icon.className = 'bi bi-sun-fill theme-toggle-icon';
|
||||
icon.style.color = '#f59e0b';
|
||||
} else {
|
||||
icon.className = 'bi bi-moon-stars-fill theme-toggle-icon';
|
||||
icon.style.color = '#fbbf24';
|
||||
}
|
||||
});
|
||||
btns.forEach(btn => {
|
||||
btn.setAttribute('title', theme === 'light' ? 'Ganti ke Mode Gelap (Dark)' : 'Ganti ke Mode Terang (Light)');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const theme = localStorage.getItem('app-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
if (theme === 'light') {
|
||||
document.body?.classList.add('light-theme');
|
||||
}
|
||||
updateThemeToggleIcons(theme);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "OPEN-ISP",
|
||||
"short_name": "OPEN-ISP",
|
||||
"description": "Internet fiber optic cepat, stabil, dan tanpa batas untuk rumah dan usaha.",
|
||||
"start_url": "/customer/login?source=pwa",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"background_color": "#0A0D14",
|
||||
"theme_color": "#0A0D14",
|
||||
"categories": ["business", "utilities"],
|
||||
"icons": [
|
||||
{ "src": "/img/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/img/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/img/icons/icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
||||
{ "src": "/img/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "OPEN-ISP",
|
||||
"short_name": "OPEN-ISP",
|
||||
"description": "Portal pelanggan OPEN-ISP — tagihan, pembayaran, dan pengaturan WiFi.",
|
||||
"start_url": "/customer/login?source=pwa",
|
||||
"scope": "/customer/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#0A0D14",
|
||||
"theme_color": "#0A0D14",
|
||||
"icons": [
|
||||
{ "src": "/img/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/img/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/img/icons/icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
||||
{ "src": "/img/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Offline</title>
|
||||
<meta name="theme-color" content="#161A21">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{
|
||||
min-height:100dvh;display:flex;align-items:center;justify-content:center;
|
||||
background:radial-gradient(1200px 600px at 50% -10%,#1F2420,#090d16 60%);
|
||||
color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
padding:24px;text-align:center;
|
||||
}
|
||||
.wrap{max-width:420px;width:100%}
|
||||
.icon{
|
||||
width:96px;height:96px;margin:0 auto 20px;border-radius:28px;
|
||||
background:linear-gradient(135deg,rgba(99,102,241,.25),rgba(6,182,212,.15));
|
||||
border:1px solid rgba(99,102,241,.35);
|
||||
display:flex;align-items:center;justify-content:center;font-size:44px;
|
||||
}
|
||||
h1{font-size:20px;font-weight:800;margin-bottom:8px;color:#fff}
|
||||
p{font-size:14px;line-height:1.6;color:#8A96A6;margin-bottom:20px}
|
||||
.box{
|
||||
background:rgba(17,24,39,.8);border:1px solid rgba(99,102,241,.22);
|
||||
border-radius:20px;padding:18px;text-align:left;margin-bottom:20px;
|
||||
}
|
||||
.row{display:flex;justify-content:space-between;gap:12px;font-size:13px;padding:8px 0;border-bottom:1px solid rgba(148,163,184,.12)}
|
||||
.row:last-child{border-bottom:0}
|
||||
.row span{color:#8A96A6}
|
||||
.row b{color:#fff;font-weight:700}
|
||||
.btn{
|
||||
display:flex;align-items:center;justify-content:center;gap:8px;
|
||||
min-height:48px;width:100%;border-radius:14px;border:0;cursor:pointer;
|
||||
background:linear-gradient(135deg,#ADFF2F,#3b82f6);color:#fff;
|
||||
font-size:15px;font-weight:800;text-decoration:none;box-shadow:0 8px 22px rgba(59,130,246,.35);
|
||||
}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#f87171;margin-right:6px;
|
||||
animation:pulse 1.4s ease-in-out infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||
@media (prefers-reduced-motion:reduce){.dot{animation:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="icon">📡</div>
|
||||
<h1>Tidak ada koneksi internet</h1>
|
||||
<p><span class="dot"></span>Koneksi ke server terputus. Aplikasi akan otomatis memuat kembali data saat jaringan kembali normal.</p>
|
||||
|
||||
<div class="box">
|
||||
<div class="row"><span>Status</span><b>Offline</b></div>
|
||||
<div class="row"><span>Data tagihan</span><b>Perlu online</b></div>
|
||||
<div class="row"><span>Pemakaian trafik</span><b>Perlu online</b></div>
|
||||
<div class="row"><span>Tagihan & tiket</span><b>Tersimpan lokal</b></div>
|
||||
</div>
|
||||
|
||||
<a class="btn" href="/customer/dashboard">🔄 Coba Lagi</a>
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('online', function () { location.reload(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Speed Test</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: #0b1220; color: #e2e8f0; }
|
||||
html[data-theme="light"] body { background: #F2F5F1; color: #161A21; }
|
||||
.wrap { max-width: 760px; margin: 0 auto; padding: 18px; }
|
||||
.head { text-align: center; margin-bottom: 6px; }
|
||||
.head h1 { font-size: 18px; margin: 0 0 2px; font-weight: 800; }
|
||||
.head p { margin: 0; font-size: 12px; color: #8A96A6; }
|
||||
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 16px; }
|
||||
.cell { background: rgba(30, 41, 59, .6); border: 1px solid rgba(148, 163, 184, .2); border-radius: 14px; padding: 16px; text-align: center; }
|
||||
html[data-theme="light"] .cell { background: #fff; border-color: #e2e8f0; }
|
||||
.val { font-size: 28px; font-weight: 800; letter-spacing: -.5px; }
|
||||
.lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: #8A96A6; margin-top: 2px; }
|
||||
.val.dl { color: #22d3ee; } .val.ul { color: #a78bfa; } .val.pingv { color: #34d399; } .val.jit { color: #fbbf24; }
|
||||
.bar { height: 10px; border-radius: 999px; background: rgba(148, 163, 184, .2); overflow: hidden; margin-top: 16px; }
|
||||
.bar > i { display: block; height: 100%; width: 0%; background: linear-gradient(90deg, #ADFF2F, #22d3ee); transition: width .2s; }
|
||||
.btn { margin-top: 16px; width: 100%; padding: 15px; border: none; border-radius: 12px; font-weight: 800; color: #fff; background: linear-gradient(135deg, #ADFF2F, #059669); cursor: pointer; font-size: 15px; }
|
||||
.btn:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.status { margin-top: 10px; font-size: 13px; color: #8A96A6; text-align: center; min-height: 18px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<h1><i>⚡</i> Speed Test</h1>
|
||||
<p>Dari perangkat Anda ke server kami</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="cell"><div class="val pingv" id="ping">-</div><div class="lbl">Ping (ms)</div></div>
|
||||
<div class="cell"><div class="val jit" id="jitter">-</div><div class="lbl">Jitter (ms)</div></div>
|
||||
<div class="cell"><div class="val dl" id="dl">-</div><div class="lbl">Download (Mbps)</div></div>
|
||||
<div class="cell"><div class="val ul" id="ul">-</div><div class="lbl">Upload (Mbps)</div></div>
|
||||
</div>
|
||||
<div class="bar"><i id="prog"></i></div>
|
||||
<button class="btn" id="startBtn">Mulai Tes</button>
|
||||
<div class="status" id="status"> </div>
|
||||
</div>
|
||||
|
||||
<script src="speedtest.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
document.documentElement.setAttribute('data-theme', localStorage.getItem('app-theme') || 'dark');
|
||||
} catch (e) {}
|
||||
|
||||
var el = function (id) { return document.getElementById(id); };
|
||||
var st = new Speedtest();
|
||||
st.setParameter('url_dl', '/speedtest/garbage.php');
|
||||
st.setParameter('url_ul', '/speedtest/empty.php');
|
||||
st.setParameter('url_ping', '/speedtest/empty.php');
|
||||
st.setParameter('url_getIp', '/speedtest/getIP.php');
|
||||
st.setParameter('telemetry_level', 'disabled');
|
||||
st.setParameter('time_dl', 12);
|
||||
st.setParameter('time_ul', 12);
|
||||
|
||||
var running = false;
|
||||
var labels = { 0: 'Menyiapkan...', 1: 'Mengukur Download...', 2: 'Mengukur Ping / Jitter...', 3: 'Mengukur Upload...', 4: 'Selesai', 5: 'Dibatalkan' };
|
||||
|
||||
st.onupdate = function (data) {
|
||||
if (!data) return;
|
||||
if (data.dlStatus) el('dl').textContent = (Math.round(data.dlStatus * 100) / 100);
|
||||
if (data.ulStatus) el('ul').textContent = (Math.round(data.ulStatus * 100) / 100);
|
||||
if (data.pingStatus) el('ping').textContent = (Math.round(data.pingStatus * 100) / 100);
|
||||
if (data.jitterStatus) el('jitter').textContent = (Math.round(data.jitterStatus * 100) / 100);
|
||||
var p = 0;
|
||||
if (data.testState === 1) p = (data.dlProgress || 0) * 45;
|
||||
else if (data.testState === 2) p = 45 + (data.pingProgress || 0) * 10;
|
||||
else if (data.testState === 3) p = 55 + (data.ulProgress || 0) * 45;
|
||||
else if (data.testState >= 4) p = 100;
|
||||
el('prog').style.width = p + '%';
|
||||
el('status').textContent = labels[data.testState] || '';
|
||||
};
|
||||
|
||||
st.onend = function (aborted) {
|
||||
running = false;
|
||||
el('startBtn').disabled = false;
|
||||
el('startBtn').textContent = 'Uji Lagi';
|
||||
el('status').textContent = aborted ? 'Tes dibatalkan' : 'Tes selesai';
|
||||
if (!aborted) el('prog').style.width = '100%';
|
||||
};
|
||||
|
||||
el('startBtn').addEventListener('click', function () {
|
||||
if (running) return;
|
||||
running = true;
|
||||
this.disabled = true;
|
||||
this.textContent = 'Mengukur...';
|
||||
el('dl').textContent = el('ul').textContent = el('ping').textContent = el('jitter').textContent = '-';
|
||||
el('prog').style.width = '0%';
|
||||
st.start();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
LibreSpeed - Main
|
||||
by Federico Dossena
|
||||
https://github.com/librespeed/speedtest/
|
||||
GNU LGPLv3 License
|
||||
*/
|
||||
|
||||
/*
|
||||
This is the main interface between your webpage and the speed test.
|
||||
It hides the speed test web worker to the page, and provides many convenient functions to control the test.
|
||||
|
||||
The best way to learn how to use this is to look at the basic example, but here's some documentation.
|
||||
|
||||
To initialize the test, create a new Speedtest object:
|
||||
let s=new Speedtest();
|
||||
Now you can think of this as a finite state machine. These are the states (use getState() to see them):
|
||||
- 0: here you can change the speed test settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events.
|
||||
- 1: here you can add test points. You only need to do this if you want to use multiple test points.
|
||||
A server is defined as an object like this:
|
||||
{
|
||||
name: "User friendly name",
|
||||
server:"http://yourBackend.com/", <---- URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol
|
||||
dlURL:"garbage.php" <----- path to garbage.php or its replacement on the server
|
||||
ulURL:"empty.php" <----- path to empty.php or its replacement on the server
|
||||
pingURL:"empty.php" <----- path to empty.php or its replacement on the server. This is used to ping the server by this selector
|
||||
getIpURL:"getIP.php" <----- path to getIP.php or its replacement on the server
|
||||
}
|
||||
While in state 1, you can only add test points, you cannot change the test settings. When you're done, use selectServer(callback) to select the test point with the lowest ping. This is asynchronous, when it's done, it will call your callback function and move to state 2. Calling setSelectedServer(server) will manually select a server and move to state 2.
|
||||
- 2: test point selected, ready to start the test. Use start() to begin, this will move to state 3
|
||||
- 3: test running. Here, your onupdate event callback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your onupdate function, with the following items:
|
||||
- dlStatus: download speed in Mbit/s
|
||||
- ulStatus: upload speed in Mbit/s
|
||||
- pingStatus: ping in ms
|
||||
- jitterStatus: jitter in ms
|
||||
- dlProgress: progress of the download test as a float 0-1
|
||||
- ulProgress: progress of the upload test as a float 0-1
|
||||
- pingProgress: progress of the ping/jitter test as a float 0-1
|
||||
- testState: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted)
|
||||
- clientIp: IP address of the client performing the test (and optionally ISP and distance)
|
||||
At the end of the test, the onend function will be called, with a boolean specifying whether the test was aborted or if it ended normally.
|
||||
The test can be aborted at any time with abort().
|
||||
At the end of the test, it will move to state 4
|
||||
- 4: test finished. You can run it again by calling start() if you want.
|
||||
*/
|
||||
|
||||
function Speedtest() {
|
||||
this._serverList = []; //when using multiple points of test, this is a list of test points
|
||||
this._selectedServer = null; //when using multiple points of test, this is the selected server
|
||||
this._settings = {}; //settings for the speed test worker
|
||||
this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
|
||||
console.log(
|
||||
"LibreSpeed by Federico Dossena v6.3.0 - https://github.com/librespeed/speedtest"
|
||||
);
|
||||
}
|
||||
|
||||
Speedtest.prototype = {
|
||||
constructor: Speedtest,
|
||||
/**
|
||||
* Returns the state of the test: 0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
|
||||
*/
|
||||
getState: function() {
|
||||
return this._state;
|
||||
},
|
||||
/**
|
||||
* Change one of the test settings from their defaults.
|
||||
* - parameter: string with the name of the parameter that you want to set
|
||||
* - value: new value for the parameter
|
||||
*
|
||||
* Invalid values or nonexistent parameters will be ignored by the speed test worker.
|
||||
*/
|
||||
setParameter: function(parameter, value) {
|
||||
if (this._state == 3)
|
||||
throw "You cannot change the test settings while running the test";
|
||||
this._settings[parameter] = value;
|
||||
if(parameter === "telemetry_extra"){
|
||||
this._originalExtra=this._settings.telemetry_extra;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Used internally to check if a server object contains all the required elements.
|
||||
* Also fixes the server URL if needed.
|
||||
*/
|
||||
_checkServerDefinition: function(server) {
|
||||
try {
|
||||
if (typeof server.name !== "string")
|
||||
throw "Name string missing from server definition (name)";
|
||||
if (typeof server.server !== "string")
|
||||
throw "Server address string missing from server definition (server)";
|
||||
if (server.server.charAt(server.server.length - 1) != "/")
|
||||
server.server += "/";
|
||||
if (server.server.indexOf("//") == 0)
|
||||
server.server = location.protocol + server.server;
|
||||
if (typeof server.dlURL !== "string")
|
||||
throw "Download URL string missing from server definition (dlURL)";
|
||||
if (typeof server.ulURL !== "string")
|
||||
throw "Upload URL string missing from server definition (ulURL)";
|
||||
if (typeof server.pingURL !== "string")
|
||||
throw "Ping URL string missing from server definition (pingURL)";
|
||||
if (typeof server.getIpURL !== "string")
|
||||
throw "GetIP URL string missing from server definition (getIpURL)";
|
||||
} catch (e) {
|
||||
throw "Invalid server definition";
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Add a test point (multiple points of test)
|
||||
* server: the server to be added as an object. Must contain the following elements:
|
||||
* {
|
||||
* name: "User friendly name",
|
||||
* server:"http://yourBackend.com/", URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol
|
||||
* dlURL:"garbage.php" path to garbage.php or its replacement on the server
|
||||
* ulURL:"empty.php" path to empty.php or its replacement on the server
|
||||
* pingURL:"empty.php" path to empty.php or its replacement on the server. This is used to ping the server by this selector
|
||||
* getIpURL:"getIP.php" path to getIP.php or its replacement on the server
|
||||
* }
|
||||
*/
|
||||
addTestPoint: function(server) {
|
||||
this._checkServerDefinition(server);
|
||||
if (this._state == 0) this._state = 1;
|
||||
if (this._state != 1) throw "You can't add a server after server selection";
|
||||
this._settings.mpot = true;
|
||||
this._serverList.push(server);
|
||||
},
|
||||
/**
|
||||
* Same as addTestPoint, but you can pass an array of servers
|
||||
*/
|
||||
addTestPoints: function(list) {
|
||||
for (let i = 0; i < list.length; i++) this.addTestPoint(list[i]);
|
||||
},
|
||||
/**
|
||||
* Load a JSON server list from URL (multiple points of test)
|
||||
* url: the url where the server list can be fetched. Must be an array with objects containing the following elements:
|
||||
* {
|
||||
* "name": "User friendly name",
|
||||
* "server":"http://yourBackend.com/", URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol
|
||||
* "dlURL":"garbage.php" path to garbage.php or its replacement on the server
|
||||
* "ulURL":"empty.php" path to empty.php or its replacement on the server
|
||||
* "pingURL":"empty.php" path to empty.php or its replacement on the server. This is used to ping the server by this selector
|
||||
* "getIpURL":"getIP.php" path to getIP.php or its replacement on the server
|
||||
* }
|
||||
* result: callback to be called when the list is loaded correctly. An array with the loaded servers will be passed to this function, or null if it failed
|
||||
*/
|
||||
loadServerList: function(url,result) {
|
||||
if (this._state == 0) this._state = 1;
|
||||
if (this._state != 1) throw "You can't add a server after server selection";
|
||||
this._settings.mpot = true;
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.onload = function(){
|
||||
try{
|
||||
const servers=JSON.parse(xhr.responseText);
|
||||
for(let i=0;i<servers.length;i++){
|
||||
this._checkServerDefinition(servers[i]);
|
||||
}
|
||||
this.addTestPoints(servers);
|
||||
result(servers);
|
||||
}catch(e){
|
||||
result(null);
|
||||
}
|
||||
}.bind(this);
|
||||
xhr.onerror = function(){result(null);}
|
||||
xhr.open("GET",url);
|
||||
xhr.send();
|
||||
},
|
||||
/**
|
||||
* Returns the selected server (multiple points of test)
|
||||
*/
|
||||
getSelectedServer: function() {
|
||||
if (this._state < 2 || this._selectedServer == null)
|
||||
throw "No server is selected";
|
||||
return this._selectedServer;
|
||||
},
|
||||
/**
|
||||
* Manually selects one of the test points (multiple points of test)
|
||||
*/
|
||||
setSelectedServer: function(server) {
|
||||
this._checkServerDefinition(server);
|
||||
if (this._state == 3)
|
||||
throw "You can't select a server while the test is running";
|
||||
this._selectedServer = server;
|
||||
this._state = 2;
|
||||
},
|
||||
/**
|
||||
* Automatically selects a server from the list of added test points. The server with the lowest ping will be chosen. (multiple points of test)
|
||||
* The process is asynchronous and the passed result callback function will be called when it's done, then the test can be started.
|
||||
*/
|
||||
selectServer: function(result) {
|
||||
if (this._state != 1) {
|
||||
if (this._state == 0) throw "No test points added";
|
||||
if (this._state == 2) throw "Server already selected";
|
||||
if (this._state >= 3)
|
||||
throw "You can't select a server while the test is running";
|
||||
}
|
||||
if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true;
|
||||
/*this function goes through a list of servers. For each server, the ping is measured, then the server with the function selected is called with the best server, or null if all the servers were down.
|
||||
*/
|
||||
const select = function(serverList, selected) {
|
||||
//pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong.
|
||||
const PING_TIMEOUT = 2000;
|
||||
let USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers
|
||||
if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) {
|
||||
//IE11 doesn't support XHR timeout
|
||||
USE_PING_TIMEOUT = false;
|
||||
}
|
||||
const ping = function(url, rtt) {
|
||||
url += (url.match(/\?/) ? "&" : "?") + "cors=true";
|
||||
let xhr = new XMLHttpRequest();
|
||||
let t = new Date().getTime();
|
||||
xhr.onload = function() {
|
||||
if (xhr.responseText.length == 0) {
|
||||
//we expect an empty response
|
||||
let instspd = new Date().getTime() - t; //rough timing estimate
|
||||
try {
|
||||
//try to get more accurate timing using performance API
|
||||
let p = performance.getEntriesByName(url);
|
||||
p = p[p.length - 1];
|
||||
let d = p.responseStart - p.requestStart;
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {}
|
||||
rtt(instspd);
|
||||
} else rtt(-1);
|
||||
}.bind(this);
|
||||
xhr.onerror = function() {
|
||||
rtt(-1);
|
||||
}.bind(this);
|
||||
xhr.open("GET", url);
|
||||
if (USE_PING_TIMEOUT) {
|
||||
try {
|
||||
xhr.timeout = PING_TIMEOUT;
|
||||
xhr.ontimeout = xhr.onerror;
|
||||
} catch (e) {}
|
||||
}
|
||||
xhr.send();
|
||||
}.bind(this);
|
||||
|
||||
//this function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters. At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong.
|
||||
const PINGS = 3, //up to 3 pings are performed, unless the server is down...
|
||||
SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold
|
||||
const checkServer = function(server, done) {
|
||||
let i = 0;
|
||||
server.pingT = -1;
|
||||
if (
|
||||
location.protocol === "https:" &&
|
||||
server.server.substring(0, 7).toLowerCase() === "http://"
|
||||
)
|
||||
done();
|
||||
else {
|
||||
const nextPing = function() {
|
||||
if (i++ == PINGS) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
ping(
|
||||
server.server + server.pingURL,
|
||||
function(t) {
|
||||
if (t >= 0) {
|
||||
if (t < server.pingT || server.pingT == -1) server.pingT = t;
|
||||
if (t < SLOW_THRESHOLD) nextPing();
|
||||
else done();
|
||||
} else done();
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this);
|
||||
nextPing();
|
||||
}
|
||||
}.bind(this);
|
||||
//check servers in list, one by one
|
||||
let i = 0;
|
||||
const done = function() {
|
||||
let bestServer = null;
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
if (
|
||||
serverList[i].pingT != -1 &&
|
||||
(bestServer == null || serverList[i].pingT < bestServer.pingT)
|
||||
)
|
||||
bestServer = serverList[i];
|
||||
}
|
||||
selected(bestServer);
|
||||
}.bind(this);
|
||||
const nextServer = function() {
|
||||
if (i == serverList.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
checkServer(serverList[i++], nextServer);
|
||||
}.bind(this);
|
||||
nextServer();
|
||||
}.bind(this);
|
||||
|
||||
//parallel server selection
|
||||
const CONCURRENCY = 6;
|
||||
let serverLists = [];
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
serverLists[i] = [];
|
||||
}
|
||||
for (let i = 0; i < this._serverList.length; i++) {
|
||||
serverLists[i % CONCURRENCY].push(this._serverList[i]);
|
||||
}
|
||||
let completed = 0;
|
||||
let bestServer = null;
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
select(
|
||||
serverLists[i],
|
||||
function(server) {
|
||||
if (server != null) {
|
||||
if (bestServer == null || server.pingT < bestServer.pingT)
|
||||
bestServer = server;
|
||||
}
|
||||
completed++;
|
||||
if (completed == CONCURRENCY) {
|
||||
this._selectedServer = bestServer;
|
||||
this._state = 2;
|
||||
if (result) result(bestServer);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Starts the test.
|
||||
* During the test, the onupdate(data) callback function will be called periodically with data from the worker.
|
||||
* At the end of the test, the onend(aborted) function will be called with a boolean telling you if the test was aborted or if it ended normally.
|
||||
*/
|
||||
start: function() {
|
||||
if (this._state == 3) throw "Test already running";
|
||||
this.worker = new Worker("speedtest_worker.js?r=" + Math.random());
|
||||
this.worker.onmessage = function(e) {
|
||||
if (e.data === this._prevData) return;
|
||||
else this._prevData = e.data;
|
||||
const data = JSON.parse(e.data);
|
||||
try {
|
||||
if (this.onupdate) this.onupdate(data);
|
||||
} catch (e) {
|
||||
console.error("Speedtest onupdate event threw exception: " + e);
|
||||
}
|
||||
if (data.testState >= 4) {
|
||||
clearInterval(this.updater);
|
||||
this._state = 4;
|
||||
try {
|
||||
if (this.onend) this.onend(data.testState == 5);
|
||||
} catch (e) {
|
||||
console.error("Speedtest onend event threw exception: " + e);
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
this.updater = setInterval(
|
||||
function() {
|
||||
this.worker.postMessage("status");
|
||||
}.bind(this),
|
||||
200
|
||||
);
|
||||
if (this._state == 1)
|
||||
throw "When using multiple points of test, you must call selectServer before starting the test";
|
||||
if (this._state == 2) {
|
||||
this._settings.url_dl =
|
||||
this._selectedServer.server + this._selectedServer.dlURL;
|
||||
this._settings.url_ul =
|
||||
this._selectedServer.server + this._selectedServer.ulURL;
|
||||
this._settings.url_ping =
|
||||
this._selectedServer.server + this._selectedServer.pingURL;
|
||||
this._settings.url_getIp =
|
||||
this._selectedServer.server + this._selectedServer.getIpURL;
|
||||
if (typeof this._originalExtra !== "undefined") {
|
||||
this._settings.telemetry_extra = JSON.stringify({
|
||||
server: this._selectedServer.name,
|
||||
extra: this._originalExtra
|
||||
});
|
||||
} else
|
||||
this._settings.telemetry_extra = JSON.stringify({
|
||||
server: this._selectedServer.name
|
||||
});
|
||||
}
|
||||
this._state = 3;
|
||||
this.worker.postMessage("start " + JSON.stringify(this._settings));
|
||||
},
|
||||
/**
|
||||
* Aborts the test while it's running.
|
||||
*/
|
||||
abort: function() {
|
||||
if (this._state < 3) throw "You cannot abort a test that's not started yet";
|
||||
if (this._state < 4) this.worker.postMessage("abort");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,734 @@
|
||||
/*
|
||||
LibreSpeed - Worker
|
||||
by Federico Dossena
|
||||
https://github.com/librespeed/speedtest/
|
||||
GNU LGPLv3 License
|
||||
*/
|
||||
|
||||
// data reported to main thread
|
||||
let testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort
|
||||
let dlStatus = ""; // download speed in megabit/s with 2 decimal digits
|
||||
let ulStatus = ""; // upload speed in megabit/s with 2 decimal digits
|
||||
let pingStatus = ""; // ping in milliseconds with 2 decimal digits
|
||||
let jitterStatus = ""; // jitter in milliseconds with 2 decimal digits
|
||||
let clientIp = ""; // client's IP address as reported by getIP.php
|
||||
let dlProgress = 0; //progress of download test 0-1
|
||||
let ulProgress = 0; //progress of upload test 0-1
|
||||
let pingProgress = 0; //progress of ping+jitter test 0-1
|
||||
let testId = null; //test ID (sent back by telemetry if used, null otherwise)
|
||||
|
||||
let log = ""; //telemetry log
|
||||
function tlog(s) {
|
||||
if (settings.telemetry_level >= 2) {
|
||||
log += Date.now() + ": " + s + "\n";
|
||||
}
|
||||
}
|
||||
function tverb(s) {
|
||||
if (settings.telemetry_level >= 3) {
|
||||
log += Date.now() + ": " + s + "\n";
|
||||
}
|
||||
}
|
||||
function twarn(s) {
|
||||
if (settings.telemetry_level >= 2) {
|
||||
log += Date.now() + " WARN: " + s + "\n";
|
||||
}
|
||||
console.warn(s);
|
||||
}
|
||||
|
||||
// test settings. can be overridden by sending specific values with the start command
|
||||
let settings = {
|
||||
mpot: false, //set to true when in MPOT mode
|
||||
test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay
|
||||
time_ul_max: 15, // max duration of upload test in seconds
|
||||
time_dl_max: 15, // max duration of download test in seconds
|
||||
time_auto: true, // if set to true, tests will take less time on faster connections
|
||||
time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill)
|
||||
time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase)
|
||||
count_ping: 10, // number of pings to perform in ping test
|
||||
url_dl: "backend/garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file
|
||||
url_ul: "backend/empty.php", // path to an empty file, used for upload test. must be relative to this js file
|
||||
url_ping: "backend/empty.php", // path to an empty file, used for ping test. must be relative to this js file
|
||||
url_getIp: "backend/getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
|
||||
getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address
|
||||
getIp_ispInfo_distance: "km", //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work
|
||||
xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active)
|
||||
xhr_ulMultistream: 3, // number of upload streams to use (can be different if enable_quirks is active)
|
||||
xhr_multistreamDelay: 300, //how much concurrent requests should be delayed
|
||||
xhr_ignoreErrors: 1, // 0=fail on errors, 1=attempt to restart a stream if it fails, 2=ignore all errors
|
||||
xhr_dlUseBlob: false, // if set to true, it reduces ram usage but uses the hard drive (useful with large garbagePhp_chunkSize and/or high xhr_dlMultistream)
|
||||
xhr_ul_blob_megabytes: 20, //size in megabytes of the upload blobs sent in the upload test (forced to 4 on chrome mobile)
|
||||
garbagePhp_chunkSize: 100, // size of chunks sent by garbage.php (can be different if enable_quirks is active)
|
||||
enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command
|
||||
ping_allowPerformanceApi: true, // if enabled, the ping test will attempt to calculate the ping more precisely using the Performance API. Currently works perfectly in Chrome, badly in Edge, and not at all in Firefox. If Performance API is not supported or the result is obviously wrong, a fallback is provided.
|
||||
overheadCompensationFactor: 1.06, //can be changed to compensate for transport overhead. (see doc.md for some other values)
|
||||
useMebibits: false, //if set to true, speed will be reported in mebibits/s instead of megabits/s
|
||||
telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results and timing) 3=debug (results+log)
|
||||
url_telemetry: "results/telemetry.php", // path to the script that adds telemetry data to the database
|
||||
telemetry_extra: "", //extra data that can be passed to the telemetry through the settings
|
||||
forceIE11Workaround: false //when set to true, it will force the IE11 upload test on all browsers. Debug only
|
||||
};
|
||||
|
||||
let xhr = null; // array of currently active xhr requests
|
||||
let interval = null; // timer used in tests
|
||||
let test_pointer = 0; //pointer to the next test to run inside settings.test_order
|
||||
|
||||
/*
|
||||
this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator
|
||||
*/
|
||||
function url_sep(url) {
|
||||
return url.match(/\?/) ? "&" : "?";
|
||||
}
|
||||
|
||||
/*
|
||||
listener for commands from main thread to this worker.
|
||||
commands:
|
||||
-status: returns the current status as a JSON string containing testState, dlStatus, ulStatus, pingStatus, clientIp, jitterStatus, dlProgress, ulProgress, pingProgress
|
||||
-abort: aborts the current test
|
||||
-start: starts the test. optionally, settings can be passed as JSON.
|
||||
example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"}
|
||||
*/
|
||||
this.addEventListener("message", function(e) {
|
||||
const params = e.data.split(" ");
|
||||
if (params[0] === "status") {
|
||||
// return status
|
||||
postMessage(
|
||||
JSON.stringify({
|
||||
testState: testState,
|
||||
dlStatus: dlStatus,
|
||||
ulStatus: ulStatus,
|
||||
pingStatus: pingStatus,
|
||||
clientIp: clientIp,
|
||||
jitterStatus: jitterStatus,
|
||||
dlProgress: dlProgress,
|
||||
ulProgress: ulProgress,
|
||||
pingProgress: pingProgress,
|
||||
testId: testId
|
||||
})
|
||||
);
|
||||
}
|
||||
if (params[0] === "start" && testState === -1) {
|
||||
// start new test
|
||||
testState = 0;
|
||||
try {
|
||||
// parse settings, if present
|
||||
let s = {};
|
||||
try {
|
||||
const ss = e.data.substring(5);
|
||||
if (ss) s = JSON.parse(ss);
|
||||
} catch (e) {
|
||||
twarn("Error parsing custom settings JSON. Please check your syntax");
|
||||
}
|
||||
//copy custom settings
|
||||
for (let key in s) {
|
||||
if (typeof settings[key] !== "undefined") settings[key] = s[key];
|
||||
else twarn("Unknown setting ignored: " + key);
|
||||
}
|
||||
const ua = navigator.userAgent;
|
||||
// quirks for specific browsers. apply only if not overridden. more may be added in future releases
|
||||
if (settings.enable_quirks || (typeof s.enable_quirks !== "undefined" && s.enable_quirks)) {
|
||||
if (/Firefox.(\d+\.\d+)/i.test(ua)) {
|
||||
if (typeof s.ping_allowPerformanceApi === "undefined") {
|
||||
// ff performance API sucks
|
||||
settings.ping_allowPerformanceApi = false;
|
||||
}
|
||||
}
|
||||
if (/Edge.(\d+\.\d+)/i.test(ua)) {
|
||||
if (typeof s.xhr_dlMultistream === "undefined") {
|
||||
// edge more precise with 3 download streams
|
||||
settings.xhr_dlMultistream = 3;
|
||||
}
|
||||
}
|
||||
if (/Chrome.(\d+)/i.test(ua) && !!self.fetch) {
|
||||
if (typeof s.xhr_dlMultistream === "undefined") {
|
||||
// chrome more precise with 5 streams
|
||||
settings.xhr_dlMultistream = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (/Edge.(\d+\.\d+)/i.test(ua)) {
|
||||
if (typeof s.forceIE11Workaround === "undefined") {
|
||||
//Edge 15 introduced a bug that causes onprogress events to not get fired, we have to use the "small chunks" workaround that reduces accuracy
|
||||
settings.forceIE11Workaround = true;
|
||||
}
|
||||
}
|
||||
if (/PlayStation 4.(\d+\.\d+)/i.test(ua)) {
|
||||
if (typeof s.forceIE11Workaround === "undefined") {
|
||||
//PS4 browser has the same bug as IE11/Edge
|
||||
settings.forceIE11Workaround = true;
|
||||
}
|
||||
}
|
||||
if (/Chrome.(\d+)/i.test(ua) && /Android|iPhone|iPad|iPod|Windows Phone/i.test(ua)) {
|
||||
//cheap af
|
||||
//Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes
|
||||
settings.xhr_ul_blob_megabytes = 4;
|
||||
}
|
||||
//telemetry_level has to be parsed and not just copied
|
||||
if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level
|
||||
//transform test_order to uppercase, just in case
|
||||
settings.test_order = settings.test_order.toUpperCase();
|
||||
} catch (e) {
|
||||
twarn("Possible error in custom test settings. Some settings might not have been applied. Exception: " + e);
|
||||
}
|
||||
// run the tests
|
||||
tverb(JSON.stringify(settings));
|
||||
test_pointer = 0;
|
||||
let iRun = false,
|
||||
dRun = false,
|
||||
uRun = false,
|
||||
pRun = false;
|
||||
const runNextTest = function() {
|
||||
if (testState == 5) return;
|
||||
if (test_pointer >= settings.test_order.length) {
|
||||
//test is finished
|
||||
if (settings.telemetry_level > 0)
|
||||
sendTelemetry(function(id) {
|
||||
testState = 4;
|
||||
if (id != null) testId = id;
|
||||
});
|
||||
else testState = 4;
|
||||
return;
|
||||
}
|
||||
switch (settings.test_order.charAt(test_pointer)) {
|
||||
case "I":
|
||||
{
|
||||
test_pointer++;
|
||||
if (iRun) {
|
||||
runNextTest();
|
||||
return;
|
||||
} else iRun = true;
|
||||
getIp(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "D":
|
||||
{
|
||||
test_pointer++;
|
||||
if (dRun) {
|
||||
runNextTest();
|
||||
return;
|
||||
} else dRun = true;
|
||||
testState = 1;
|
||||
dlTest(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "U":
|
||||
{
|
||||
test_pointer++;
|
||||
if (uRun) {
|
||||
runNextTest();
|
||||
return;
|
||||
} else uRun = true;
|
||||
testState = 3;
|
||||
ulTest(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "P":
|
||||
{
|
||||
test_pointer++;
|
||||
if (pRun) {
|
||||
runNextTest();
|
||||
return;
|
||||
} else pRun = true;
|
||||
testState = 2;
|
||||
pingTest(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "_":
|
||||
{
|
||||
test_pointer++;
|
||||
setTimeout(runNextTest, 1000);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
test_pointer++;
|
||||
}
|
||||
};
|
||||
runNextTest();
|
||||
}
|
||||
if (params[0] === "abort") {
|
||||
// abort command
|
||||
if (testState >= 4) return;
|
||||
tlog("manually aborted");
|
||||
clearRequests(); // stop all xhr activity
|
||||
runNextTest = null;
|
||||
if (interval) clearInterval(interval); // clear timer if present
|
||||
if (settings.telemetry_level > 1) sendTelemetry(function() {});
|
||||
testState = 5; //set test as aborted
|
||||
dlStatus = "";
|
||||
ulStatus = "";
|
||||
pingStatus = "";
|
||||
jitterStatus = "";
|
||||
clientIp = "";
|
||||
dlProgress = 0;
|
||||
ulProgress = 0;
|
||||
pingProgress = 0;
|
||||
}
|
||||
});
|
||||
// stops all XHR activity, aggressively
|
||||
function clearRequests() {
|
||||
tverb("stopping pending XHRs");
|
||||
if (xhr) {
|
||||
for (let i = 0; i < xhr.length; i++) {
|
||||
try {
|
||||
xhr[i].onprogress = null;
|
||||
xhr[i].onload = null;
|
||||
xhr[i].onerror = null;
|
||||
} catch (e) {}
|
||||
try {
|
||||
xhr[i].upload.onprogress = null;
|
||||
xhr[i].upload.onload = null;
|
||||
xhr[i].upload.onerror = null;
|
||||
} catch (e) {}
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {}
|
||||
try {
|
||||
delete xhr[i];
|
||||
} catch (e) {}
|
||||
}
|
||||
xhr = null;
|
||||
}
|
||||
}
|
||||
// gets client's IP using url_getIp, then calls the done function
|
||||
let ipCalled = false; // used to prevent multiple accidental calls to getIp
|
||||
let ispInfo = ""; //used for telemetry
|
||||
function getIp(done) {
|
||||
tverb("getIp");
|
||||
if (ipCalled) return;
|
||||
else ipCalled = true; // getIp already called?
|
||||
let startT = new Date().getTime();
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
clientIp = data.processedString;
|
||||
ispInfo = data.rawIspInfo;
|
||||
} catch (e) {
|
||||
clientIp = xhr.responseText;
|
||||
ispInfo = "";
|
||||
}
|
||||
done();
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
tlog("getIp failed, took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
};
|
||||
xhr.open("GET", settings.url_getIp + url_sep(settings.url_getIp) + (settings.mpot ? "cors=true&" : "") + (settings.getIp_ispInfo ? "isp=true" + (settings.getIp_ispInfo_distance ? "&distance=" + settings.getIp_ispInfo_distance + "&" : "&") : "&") + "r=" + Math.random(), true);
|
||||
xhr.send();
|
||||
}
|
||||
// download test, calls done function when it's over
|
||||
let dlCalled = false; // used to prevent multiple accidental calls to dlTest
|
||||
function dlTest(done) {
|
||||
tverb("dlTest");
|
||||
if (dlCalled) return;
|
||||
else dlCalled = true; // dlTest already called?
|
||||
let totLoaded = 0.0, // total number of loaded bytes
|
||||
startT = new Date().getTime(), // timestamp when test was started
|
||||
bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
|
||||
graceTimeDone = false, //set to true after the grace time is past
|
||||
failed = false; // set to true if a stream fails
|
||||
xhr = [];
|
||||
// function to create a download stream. streams are slightly delayed so that they will not end at the same time
|
||||
const testStream = function(i, delay) {
|
||||
setTimeout(
|
||||
function() {
|
||||
if (testState !== 1) return; // delayed stream ended up starting after the end of the download test
|
||||
tverb("dl test stream started " + i + " " + delay);
|
||||
let prevLoaded = 0; // number of bytes loaded last time onprogress was called
|
||||
let x = new XMLHttpRequest();
|
||||
xhr[i] = x;
|
||||
xhr[i].onprogress = function(event) {
|
||||
tverb("dl stream progress event " + i + " " + event.loaded);
|
||||
if (testState !== 1) {
|
||||
try {
|
||||
x.abort();
|
||||
} catch (e) {}
|
||||
} // just in case this XHR is still running after the download test
|
||||
// progress event, add number of new loaded bytes to totLoaded
|
||||
const loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
|
||||
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
|
||||
totLoaded += loadDiff;
|
||||
prevLoaded = event.loaded;
|
||||
}.bind(this);
|
||||
xhr[i].onload = function() {
|
||||
// the large file has been loaded entirely, start again
|
||||
tverb("dl stream finished " + i);
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {} // reset the stream data to empty ram
|
||||
testStream(i, 0);
|
||||
}.bind(this);
|
||||
xhr[i].onerror = function() {
|
||||
// error
|
||||
tverb("dl stream failed " + i);
|
||||
if (settings.xhr_ignoreErrors === 0) failed = true; //abort
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {}
|
||||
delete xhr[i];
|
||||
if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream
|
||||
}.bind(this);
|
||||
// send xhr
|
||||
try {
|
||||
if (settings.xhr_dlUseBlob) xhr[i].responseType = "blob";
|
||||
else xhr[i].responseType = "arraybuffer";
|
||||
} catch (e) {}
|
||||
xhr[i].open("GET", settings.url_dl + url_sep(settings.url_dl) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random() + "&ckSize=" + settings.garbagePhp_chunkSize, true); // random string to prevent caching
|
||||
xhr[i].send();
|
||||
}.bind(this),
|
||||
1 + delay
|
||||
);
|
||||
}.bind(this);
|
||||
// open streams
|
||||
for (let i = 0; i < settings.xhr_dlMultistream; i++) {
|
||||
testStream(i, settings.xhr_multistreamDelay * i);
|
||||
}
|
||||
// every 200ms, update dlStatus
|
||||
interval = setInterval(
|
||||
function() {
|
||||
tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)"));
|
||||
const t = new Date().getTime() - startT;
|
||||
if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000);
|
||||
if (t < 200) return;
|
||||
if (!graceTimeDone) {
|
||||
if (t > 1000 * settings.time_dlGraceTime) {
|
||||
if (totLoaded > 0) {
|
||||
// if the connection is so slow that we didn't get a single chunk yet, do not reset
|
||||
startT = new Date().getTime();
|
||||
bonusT = 0;
|
||||
totLoaded = 0.0;
|
||||
}
|
||||
graceTimeDone = true;
|
||||
}
|
||||
} else {
|
||||
const speed = totLoaded / (t / 1000.0);
|
||||
if (settings.time_auto) {
|
||||
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
|
||||
const bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
dlStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits
|
||||
if ((t + bonusT) / 1000.0 > settings.time_dl_max || failed) {
|
||||
// test is over, stop streams and timer
|
||||
if (failed || isNaN(dlStatus)) dlStatus = "Fail";
|
||||
clearRequests();
|
||||
clearInterval(interval);
|
||||
dlProgress = 1;
|
||||
tlog("dlTest: " + dlStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}
|
||||
}.bind(this),
|
||||
200
|
||||
);
|
||||
}
|
||||
// upload test, calls done function when it's over
|
||||
let ulCalled = false; // used to prevent multiple accidental calls to ulTest
|
||||
function ulTest(done) {
|
||||
tverb("ulTest");
|
||||
if (ulCalled) return;
|
||||
else ulCalled = true; // ulTest already called?
|
||||
// garbage data for upload test
|
||||
let r = new ArrayBuffer(1048576);
|
||||
const maxInt = Math.pow(2, 32) - 1;
|
||||
try {
|
||||
r = new Uint32Array(r);
|
||||
for (let i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
|
||||
} catch (e) {}
|
||||
let req = [];
|
||||
let reqsmall = [];
|
||||
for (let i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r);
|
||||
req = new Blob(req);
|
||||
r = new ArrayBuffer(262144);
|
||||
try {
|
||||
r = new Uint32Array(r);
|
||||
for (let i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
|
||||
} catch (e) {}
|
||||
reqsmall.push(r);
|
||||
reqsmall = new Blob(reqsmall);
|
||||
const testFunction = function() {
|
||||
let totLoaded = 0.0, // total number of transmitted bytes
|
||||
startT = new Date().getTime(), // timestamp when test was started
|
||||
bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
|
||||
graceTimeDone = false, //set to true after the grace time is past
|
||||
failed = false; // set to true if a stream fails
|
||||
xhr = [];
|
||||
// function to create an upload stream. streams are slightly delayed so that they will not end at the same time
|
||||
const testStream = function(i, delay) {
|
||||
setTimeout(
|
||||
function() {
|
||||
if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test
|
||||
tverb("ul test stream started " + i + " " + delay);
|
||||
let prevLoaded = 0; // number of bytes transmitted last time onprogress was called
|
||||
let x = new XMLHttpRequest();
|
||||
xhr[i] = x;
|
||||
let ie11workaround;
|
||||
if (settings.forceIE11Workaround) ie11workaround = true;
|
||||
else {
|
||||
try {
|
||||
xhr[i].upload.onprogress;
|
||||
ie11workaround = false;
|
||||
} catch (e) {
|
||||
ie11workaround = true;
|
||||
}
|
||||
}
|
||||
if (ie11workaround) {
|
||||
// IE11 workaround: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections
|
||||
xhr[i].onload = xhr[i].onerror = function() {
|
||||
tverb("ul stream progress event (ie11wa)");
|
||||
totLoaded += reqsmall.size;
|
||||
testStream(i, 0);
|
||||
};
|
||||
xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching
|
||||
try {
|
||||
xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway)
|
||||
} catch (e) {}
|
||||
//No Content-Type header in MPOT branch because it triggers bugs in some browsers
|
||||
xhr[i].send(reqsmall);
|
||||
} else {
|
||||
// REGULAR version, no workaround
|
||||
xhr[i].upload.onprogress = function(event) {
|
||||
tverb("ul stream progress event " + i + " " + event.loaded);
|
||||
if (testState !== 3) {
|
||||
try {
|
||||
x.abort();
|
||||
} catch (e) {}
|
||||
} // just in case this XHR is still running after the upload test
|
||||
// progress event, add number of new loaded bytes to totLoaded
|
||||
const loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
|
||||
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
|
||||
totLoaded += loadDiff;
|
||||
prevLoaded = event.loaded;
|
||||
}.bind(this);
|
||||
xhr[i].upload.onload = function() {
|
||||
// body uploaded, but response not yet available.
|
||||
// xhr.onload below handles the full lifecycle.
|
||||
}.bind(this);
|
||||
xhr[i].upload.onerror = function() {
|
||||
tverb("ul stream failed " + i);
|
||||
if (settings.xhr_ignoreErrors === 0) failed = true;
|
||||
try { x.abort(); } catch (e) {}
|
||||
delete xhr[i];
|
||||
if (settings.xhr_ignoreErrors === 1) testStream(i, 0);
|
||||
}.bind(this);
|
||||
xhr[i].onload = function() {
|
||||
// response complete — check status on captured x, not xhr[i]
|
||||
if (x.status >= 200 && x.status < 300) {
|
||||
tverb("ul stream finished " + i);
|
||||
testStream(i, 0);
|
||||
} else {
|
||||
tverb("ul stream failed with HTTP " + x.status + " " + i);
|
||||
if (settings.xhr_ignoreErrors === 0) failed = true;
|
||||
try { x.abort(); } catch (e) {}
|
||||
delete xhr[i];
|
||||
if (settings.xhr_ignoreErrors === 1) testStream(i, 0);
|
||||
}
|
||||
}.bind(this);
|
||||
// send xhr
|
||||
xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching
|
||||
try {
|
||||
xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway)
|
||||
} catch (e) {}
|
||||
//No Content-Type header in MPOT branch because it triggers bugs in some browsers
|
||||
xhr[i].send(req);
|
||||
}
|
||||
}.bind(this),
|
||||
delay
|
||||
);
|
||||
}.bind(this);
|
||||
// open streams
|
||||
for (let i = 0; i < settings.xhr_ulMultistream; i++) {
|
||||
testStream(i, settings.xhr_multistreamDelay * i);
|
||||
}
|
||||
// every 200ms, update ulStatus
|
||||
interval = setInterval(
|
||||
function() {
|
||||
tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)"));
|
||||
const t = new Date().getTime() - startT;
|
||||
if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000);
|
||||
if (t < 200) return;
|
||||
if (!graceTimeDone) {
|
||||
if (t > 1000 * settings.time_ulGraceTime) {
|
||||
if (totLoaded > 0) {
|
||||
// if the connection is so slow that we didn't get a single chunk yet, do not reset
|
||||
startT = new Date().getTime();
|
||||
bonusT = 0;
|
||||
totLoaded = 0.0;
|
||||
}
|
||||
graceTimeDone = true;
|
||||
}
|
||||
} else {
|
||||
const speed = totLoaded / (t / 1000.0);
|
||||
if (settings.time_auto) {
|
||||
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
|
||||
const bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits
|
||||
if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) {
|
||||
// test is over, stop streams and timer
|
||||
if (failed || isNaN(ulStatus)) ulStatus = "Fail";
|
||||
clearRequests();
|
||||
clearInterval(interval);
|
||||
ulProgress = 1;
|
||||
tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}
|
||||
}.bind(this),
|
||||
200
|
||||
);
|
||||
}.bind(this);
|
||||
if (settings.mpot) {
|
||||
tverb("Sending POST request before performing upload test");
|
||||
xhr = [];
|
||||
xhr[0] = new XMLHttpRequest();
|
||||
xhr[0].onload = xhr[0].onerror = function() {
|
||||
tverb("POST request sent, starting upload test");
|
||||
testFunction();
|
||||
}.bind(this);
|
||||
xhr[0].open("POST", settings.url_ul);
|
||||
xhr[0].send();
|
||||
} else testFunction();
|
||||
}
|
||||
// ping+jitter test, function done is called when it's over
|
||||
let ptCalled = false; // used to prevent multiple accidental calls to pingTest
|
||||
function pingTest(done) {
|
||||
tverb("pingTest");
|
||||
if (ptCalled) return;
|
||||
else ptCalled = true; // pingTest already called?
|
||||
const startT = new Date().getTime(); //when the test was started
|
||||
let prevT = null; // last time a pong was received
|
||||
let ping = 0.0; // current ping value
|
||||
let jitter = 0.0; // current jitter value
|
||||
let i = 0; // counter of pongs received
|
||||
let prevInstspd = 0; // last ping time, used for jitter calculation
|
||||
xhr = [];
|
||||
// ping function
|
||||
const doPing = function() {
|
||||
tverb("ping");
|
||||
pingProgress = i / settings.count_ping;
|
||||
prevT = new Date().getTime();
|
||||
xhr[0] = new XMLHttpRequest();
|
||||
xhr[0].onload = function() {
|
||||
// pong
|
||||
tverb("pong");
|
||||
if (i === 0) {
|
||||
prevT = new Date().getTime(); // first pong
|
||||
} else {
|
||||
let instspd = new Date().getTime() - prevT;
|
||||
if (settings.ping_allowPerformanceApi) {
|
||||
try {
|
||||
//try to get accurate performance timing using performance api
|
||||
let p = performance.getEntries();
|
||||
p = p[p.length - 1];
|
||||
let d = p.responseStart - p.requestStart;
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {
|
||||
//if not possible, keep the estimate
|
||||
tverb("Performance API not supported, using estimate");
|
||||
}
|
||||
}
|
||||
//noticed that some browsers randomly have 0ms ping
|
||||
if (instspd < 1) instspd = prevInstspd;
|
||||
if (instspd < 1) instspd = 1;
|
||||
const instjitter = Math.abs(instspd - prevInstspd);
|
||||
if (i === 1) ping = instspd;
|
||||
/* first ping, can't tell jitter yet*/ else {
|
||||
if (instspd < ping) ping = instspd; // update ping, if the instant ping is lower
|
||||
if (i === 2) jitter = instjitter;
|
||||
//discard the first jitter measurement because it might be much higher than it should be
|
||||
else jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2; // update jitter, weighted average. spikes in ping values are given more weight.
|
||||
}
|
||||
prevInstspd = instspd;
|
||||
}
|
||||
pingStatus = ping.toFixed(2);
|
||||
jitterStatus = jitter.toFixed(2);
|
||||
i++;
|
||||
tverb("ping: " + pingStatus + " jitter: " + jitterStatus);
|
||||
if (i < settings.count_ping) doPing();
|
||||
else {
|
||||
// more pings to do?
|
||||
pingProgress = 1;
|
||||
tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}.bind(this);
|
||||
xhr[0].onerror = function() {
|
||||
// a ping failed, cancel test
|
||||
tverb("ping failed");
|
||||
if (settings.xhr_ignoreErrors === 0) {
|
||||
//abort
|
||||
pingStatus = "Fail";
|
||||
jitterStatus = "Fail";
|
||||
clearRequests();
|
||||
tlog("ping test failed, took " + (new Date().getTime() - startT) + "ms");
|
||||
pingProgress = 1;
|
||||
done();
|
||||
}
|
||||
if (settings.xhr_ignoreErrors === 1) doPing(); //retry ping
|
||||
if (settings.xhr_ignoreErrors === 2) {
|
||||
//ignore failed ping
|
||||
i++;
|
||||
if (i < settings.count_ping) doPing();
|
||||
else {
|
||||
// more pings to do?
|
||||
pingProgress = 1;
|
||||
tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
// send xhr
|
||||
xhr[0].open("GET", settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching
|
||||
xhr[0].send();
|
||||
}.bind(this);
|
||||
doPing(); // start first ping
|
||||
}
|
||||
// telemetry
|
||||
function sendTelemetry(done) {
|
||||
if (settings.telemetry_level < 1) return;
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
try {
|
||||
const parts = xhr.responseText.split(" ");
|
||||
if (parts[0] == "id") {
|
||||
try {
|
||||
let id = parts[1];
|
||||
done(id);
|
||||
} catch (e) {
|
||||
done(null);
|
||||
}
|
||||
} else done(null);
|
||||
} catch (e) {
|
||||
done(null);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
console.log("TELEMETRY ERROR " + xhr.status);
|
||||
done(null);
|
||||
};
|
||||
xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true);
|
||||
const telemetryIspInfo = {
|
||||
processedString: clientIp,
|
||||
rawIspInfo: typeof ispInfo === "object" ? ispInfo : ""
|
||||
};
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append("ispinfo", JSON.stringify(telemetryIspInfo));
|
||||
fd.append("dl", dlStatus);
|
||||
fd.append("ul", ulStatus);
|
||||
fd.append("ping", pingStatus);
|
||||
fd.append("jitter", jitterStatus);
|
||||
fd.append("log", settings.telemetry_level > 1 ? log : "");
|
||||
fd.append("extra", settings.telemetry_extra);
|
||||
xhr.send(fd);
|
||||
} catch (ex) {
|
||||
const postData = "extra=" + encodeURIComponent(settings.telemetry_extra) + "&ispinfo=" + encodeURIComponent(JSON.stringify(telemetryIspInfo)) + "&dl=" + encodeURIComponent(dlStatus) + "&ul=" + encodeURIComponent(ulStatus) + "&ping=" + encodeURIComponent(pingStatus) + "&jitter=" + encodeURIComponent(jitterStatus) + "&log=" + encodeURIComponent(settings.telemetry_level > 1 ? log : "");
|
||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
xhr.send(postData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/* Service Worker — Portal Pelanggan OPEN-ISP
|
||||
Strategi: HTML & CSS/JS = network-first (selalu ambil terbaru saat online),
|
||||
gambar & manifest & font = cache-first (jarang berubah).
|
||||
Naikkan CACHE_NAME bila perlu purge (v4 → v5 → ...). */
|
||||
const CACHE_NAME = 'customer-pwa-v7';
|
||||
const PRECACHE_URLS = [
|
||||
// Aset inti (network-first tapi disimpan untuk offline)
|
||||
'/css/auth.css',
|
||||
'/css/landing.css',
|
||||
'/css/dashboard.css',
|
||||
'/css/pages.css',
|
||||
'/css/theme-toggle.css',
|
||||
'/js/theme-toggle.js',
|
||||
// Ikon & manifest PWA
|
||||
'/img/icons/icon-192.png',
|
||||
'/img/icons/icon-512.png',
|
||||
'/img/icons/icon-maskable-192.png',
|
||||
'/img/icons/icon-maskable-512.png',
|
||||
'/img/icons/apple-touch-icon.png',
|
||||
'/manifest.webmanifest',
|
||||
'/manifest-landing.webmanifest',
|
||||
// Halaman shell untuk fallback offline
|
||||
'/offline.html',
|
||||
'/customer/login',
|
||||
'/customer/register'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) =>
|
||||
// Tambah satu-per-satu agar satu URL gagal tidak membatalkan semuanya
|
||||
Promise.all(
|
||||
PRECACHE_URLS.map((url) =>
|
||||
cache.add(url).catch(() => null)
|
||||
)
|
||||
)
|
||||
).catch(() => {})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(keys.map((k) => (k === CACHE_NAME ? null : caches.delete(k))))
|
||||
),
|
||||
self.clients.claim()
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
const res = await fetch(request);
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
if (res && res.ok) cache.put(request, res.clone()).catch(() => {});
|
||||
return res;
|
||||
}
|
||||
|
||||
async function networkFirst(request, fallbackUrl) {
|
||||
try {
|
||||
const res = await fetch(request, { cache: 'no-cache' });
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
if (res && res.ok) cache.put(request, res.clone()).catch(() => {});
|
||||
return res;
|
||||
} catch (e) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
if (fallbackUrl) {
|
||||
const fallback = await caches.match(fallbackUrl);
|
||||
if (fallback) return fallback;
|
||||
}
|
||||
return new Response('Offline', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const req = event.request;
|
||||
if (req.method !== 'GET') return;
|
||||
|
||||
const url = new URL(req.url);
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
const path = url.pathname;
|
||||
|
||||
// version.txt → selalu network (mekanisme deteksi update)
|
||||
if (path === '/version.txt') return;
|
||||
|
||||
// Navigasi halaman pelanggan → network-first
|
||||
if (req.mode === 'navigate') {
|
||||
if (path.startsWith('/customer/')) event.respondWith(networkFirst(req, '/customer/login'));
|
||||
else event.respondWith(networkFirst(req));
|
||||
return;
|
||||
}
|
||||
|
||||
// CSS & JS → network-first agar perubahan desain langsung terlihat
|
||||
if (path.startsWith('/css/') || path.startsWith('/js/')) {
|
||||
event.respondWith(networkFirst(req));
|
||||
return;
|
||||
}
|
||||
|
||||
// Gambar, ikon, manifest, font → cache-first
|
||||
if (
|
||||
path.startsWith('/img/') ||
|
||||
path.startsWith('/fonts/') ||
|
||||
path === '/manifest.webmanifest' ||
|
||||
path === '/manifest-landing.webmanifest'
|
||||
) {
|
||||
event.respondWith(cacheFirst(req));
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(networkFirst(req));
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data === 'SKIP_WAITING') self.skipWaiting();
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
|
||||
* Copyright 2019-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
|
||||
*/@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?dd67030699838ea613ee6dbda90effa6") format("woff2"),url("fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\fLine truncated
|
||||
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slLine truncated
|
||||
//# sourceMappingURL=bootstrap.bundle.min.js.map
|
||||
@@ -0,0 +1,6 @@
|
||||
@charset "UTF-8";/*!
|
||||
* Bootstrap v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherLine truncated
|
||||
/*# sourceMappingURL=bootstrap.min.css.map */
|
||||