mirror of
https://github.com/bagusindrayana/ews-concept-new.git
synced 2026-06-08 09:45:34 +00:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
083cf23dbf | ||
|
|
91f96952e3 |
45 changed files with 980 additions and 4301 deletions
26
LICENSE
26
LICENSE
|
|
@ -1,26 +0,0 @@
|
|||
MIT License (Modified)
|
||||
|
||||
Copyright (c) 2026 Bagus Indrayana
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
1. If the Software (including any part of the code or data) is modified by the
|
||||
user or deployer, the requirement to include the above copyright notice
|
||||
and this permission notice is waived.
|
||||
2. If the Software is used or deployed in its original form without any
|
||||
modifications to the code or data, the above copyright notice and this
|
||||
permission notice MUST be included in all copies, and credit must be given
|
||||
to the original author or original repository (https://github.com/bagusindrayana/ews-concept-new).
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -85,7 +85,3 @@ This application relies on two distinct external data sources for real-time oper
|
|||
- **Purpose**: A WebSocket server that broadcasts structured alert data regarding recent earthquakes, parameters (magnitude, depth, location), and potential tsunami warnings.
|
||||
- **Usage**: Triggers the UI popups, updates the recent earthquake list, and displays alert banners.
|
||||
- **Environment Variable**: `PUBLIC_SOCKET_DATA_URL` (default: `ws://localhost:8081`)
|
||||
|
||||
|
||||
## Support Me!
|
||||
[](https://sociabuzz.com/bagusindrayana/tribe)
|
||||
|
|
|
|||
131
convert_bad_apple.py
Normal file
131
convert_bad_apple.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
import json
|
||||
import argparse
|
||||
import sys
|
||||
import math
|
||||
|
||||
def distance(p1, p2):
|
||||
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])
|
||||
|
||||
def sort_points_nearest_neighbor(points):
|
||||
"""
|
||||
Mengurutkan titik-titik agar menjadi satu garis yang menyambung (TSP approximation).
|
||||
Sangat krusial untuk rendering tipe oscilloscope/XY agar garis tidak melompat-lompat berantakan.
|
||||
"""
|
||||
if not points:
|
||||
return []
|
||||
|
||||
unvisited = points.copy()
|
||||
current = unvisited.pop(0)
|
||||
sorted_points = [current]
|
||||
|
||||
while unvisited:
|
||||
nearest_idx = 0
|
||||
min_dist = distance(current, unvisited[0])
|
||||
for i in range(1, len(unvisited)):
|
||||
dist = distance(current, unvisited[i])
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
nearest_idx = i
|
||||
|
||||
current = unvisited.pop(nearest_idx)
|
||||
sorted_points.append(current)
|
||||
|
||||
return sorted_points
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Convert black & white video to XY JSON for Waveform Chart")
|
||||
parser.add_argument("input", help="Path to input video (e.g. bad_apple.mp4)")
|
||||
parser.add_argument("output", help="Path to output JSON (e.g. bad_apple_frames.json)")
|
||||
parser.add_argument("--fps", type=int, default=15, help="Target FPS to extract (lower is easier on browser memory)")
|
||||
parser.add_argument("--max-points", type=int, default=400, help="Max points per frame (lower = faster render, higher = detailed)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
cap = cv2.VideoCapture(args.input)
|
||||
if not cap.isOpened():
|
||||
print(f"Error: Could not open video {args.input}")
|
||||
sys.exit(1)
|
||||
|
||||
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
if video_fps <= 0:
|
||||
video_fps = 30
|
||||
|
||||
frame_skip = max(1, int(video_fps / args.fps))
|
||||
|
||||
frames_data = []
|
||||
frame_count = 0
|
||||
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
print(f"Processing video: {width}x{height} @ {video_fps}fps")
|
||||
print(f"Targeting: {args.fps}fps, Max {args.max_points} points/frame")
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
if frame_count % frame_skip == 0:
|
||||
# 1. Convert to graysale
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# 2. Thresholding (Pastikan benar-benar hitam/putih murni)
|
||||
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# 3. Cari garis tepi (Contours)
|
||||
# Menggunakan RETR_LIST agar lubang (seperti apel hitam di background putih) ikut terbaca
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
frame_points = []
|
||||
screen_area = width * height
|
||||
for contour in contours:
|
||||
# Abaikan batas kotak frame layar penuh saat warnanya terbalik
|
||||
if cv2.contourArea(contour) < screen_area * 0.95:
|
||||
for point in contour:
|
||||
x, y = point[0]
|
||||
frame_points.append((x, y))
|
||||
|
||||
# 4. Batasi jumlah titik agar web tidak hang (Downsampling)
|
||||
if len(frame_points) > args.max_points:
|
||||
step = len(frame_points) / args.max_points
|
||||
frame_points = [frame_points[int(i * step)] for i in range(args.max_points)]
|
||||
|
||||
# 5. Sortir titik-titik (Algoritma Jalur Terpendek)
|
||||
# supaya "benang" yang digambar di Svelte tidak ruwet/melompat jauh
|
||||
if len(frame_points) > 0:
|
||||
frame_points = sort_points_nearest_neighbor(frame_points)
|
||||
|
||||
# 6. Normalisasi ke rentang nx, ny (-1.0 sampai 1.0)
|
||||
normalized_points = []
|
||||
for (x, y) in frame_points:
|
||||
nx = (x / width) * 2.0 - 1.0
|
||||
|
||||
# Koordinat Canvas Y itu dari Atas(0) ke Bawah(Height).
|
||||
# Sumbu Math/Grafik Bawah(-1) ke Atas(1). Di versi terbaru chart,
|
||||
# Y positif ny sudah menuju ke bawah, jadi tidak perlu di-invert lagi.
|
||||
ny = (y / height) * 2.0 - 1.0
|
||||
|
||||
normalized_points.append({"nx": round(nx, 4), "ny": round(ny, 4)})
|
||||
|
||||
# Hanya simpan frame jika ada point-nya
|
||||
frames_data.append(normalized_points)
|
||||
|
||||
if len(frames_data) % 100 == 0:
|
||||
print(f"Processed {len(frames_data)} target frames...")
|
||||
|
||||
frame_count += 1
|
||||
|
||||
cap.release()
|
||||
|
||||
print(f"Saving {len(frames_data)} frames to {args.output}...")
|
||||
with open(args.output, 'w') as f:
|
||||
# Tulis JSON serapat mungkin (tanpa indent) agar file lebih kecil
|
||||
json.dump(frames_data, f, separators=(',', ':'))
|
||||
|
||||
print(f"Done! The file size is ready for consumption by WaveformChart.svelte.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
package.json
13
package.json
|
|
@ -1,9 +1,8 @@
|
|||
{
|
||||
"name": "ews-concept-new",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
|
@ -13,7 +12,6 @@
|
|||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/svelte": "^5.2.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-cloudflare": "^7.2.8",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
|
|
@ -23,24 +21,23 @@
|
|||
"@types/mapbox-gl": "^3.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@ubermanu/sveltekit-websocket": "^0.3.3",
|
||||
"devalue": ">=5.6.4",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"undici": ">=7.24.0",
|
||||
"vite": "^7.3.1"
|
||||
"devalue": ">=5.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@turf/turf": "^6.5",
|
||||
"fast-xml-parser": ">=5.5.7",
|
||||
"html-to-image": "^1.11.13",
|
||||
"fast-xml-parser": ">=5.5.6",
|
||||
"luxon": "^3.4.4",
|
||||
"mapbox-gl": "^3.2.0",
|
||||
"mapbox-gl-animated-popup": "^0.4.0",
|
||||
"seisplotjs": "^3.2.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"svelte-highlight": "^7.9.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"ws": "^8.19.0"
|
||||
|
|
|
|||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
|
|
@ -18,11 +18,8 @@ importers:
|
|||
specifier: ^6.5
|
||||
version: 6.5.0
|
||||
fast-xml-parser:
|
||||
specifier: '>=5.5.7'
|
||||
version: 5.5.7
|
||||
html-to-image:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
specifier: '>=5.5.6'
|
||||
version: 5.5.6
|
||||
luxon:
|
||||
specifier: ^3.4.4
|
||||
version: 3.7.2
|
||||
|
|
@ -36,7 +33,7 @@ importers:
|
|||
specifier: ^3.2.1
|
||||
version: 3.2.1
|
||||
socket.io-client:
|
||||
specifier: ^4.8.1
|
||||
specifier: ^4.7.5
|
||||
version: 4.8.3
|
||||
svelte-highlight:
|
||||
specifier: ^7.9.0
|
||||
|
|
@ -48,9 +45,6 @@ importers:
|
|||
specifier: ^8.19.0
|
||||
version: 8.19.0
|
||||
devDependencies:
|
||||
'@iconify/svelte':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(svelte@5.53.10)
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.10)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))
|
||||
|
|
@ -308,14 +302,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@iconify/svelte@5.2.1':
|
||||
resolution: {integrity: sha512-zHmsIPmnIhGd5gc95bNN5FL+GifwMZv7M2rlZEpa7IXYGFJm/XGHdWf6PWQa6OBoC+R69WyiPO9NAj5wjfjbow==}
|
||||
peerDependencies:
|
||||
svelte: '>5.0.0'
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1466,8 +1452,8 @@ packages:
|
|||
fast-xml-builder@1.1.4:
|
||||
resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
|
||||
|
||||
fast-xml-parser@5.5.7:
|
||||
resolution: {integrity: sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==}
|
||||
fast-xml-parser@5.5.6:
|
||||
resolution: {integrity: sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==}
|
||||
hasBin: true
|
||||
|
||||
fdir@6.5.0:
|
||||
|
|
@ -1544,9 +1530,6 @@ packages:
|
|||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2254,13 +2237,6 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@iconify/svelte@5.2.1(svelte@5.53.10)':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
svelte: 5.53.10
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
|
|
@ -3776,7 +3752,7 @@ snapshots:
|
|||
dependencies:
|
||||
path-expression-matcher: 1.1.3
|
||||
|
||||
fast-xml-parser@5.5.7:
|
||||
fast-xml-parser@5.5.6:
|
||||
dependencies:
|
||||
fast-xml-builder: 1.1.4
|
||||
path-expression-matcher: 1.1.3
|
||||
|
|
@ -3858,8 +3834,6 @@ snapshots:
|
|||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
@import "./lib/styles/animations.css";
|
||||
@import "./lib/styles/components.css";
|
||||
@import "./lib/styles/variants.css";
|
||||
/* @import "./lib/styles/status.css";
|
||||
@import "./lib/styles/hex-grid.css"; */
|
||||
@import "./lib/styles/status.css";
|
||||
@import "./lib/styles/hex-grid.css";
|
||||
|
||||
/* refactor code */
|
||||
/* @import "./lib/styles/stripe-bar.css";
|
||||
@import "./lib/styles/hex-shape.css";
|
||||
@import "./lib/styles/components/RibLayout.css"; */
|
||||
@import "./lib/styles/stripe-bar.css";
|
||||
@import "./lib/styles/hex-shape.css";
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import "../styles/components/Card.css";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -15,10 +14,10 @@
|
|||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="ews-card {className}" class:open>
|
||||
<div class="ews-card bordered {className}" class:open>
|
||||
{#if title}
|
||||
<div
|
||||
class="ews-card-header"
|
||||
class="ews-card-header bordered-bottom"
|
||||
onclick={() => {
|
||||
open = !open;
|
||||
onToggle?.();
|
||||
|
|
@ -27,11 +26,11 @@
|
|||
{@render title()}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ews-card-content">
|
||||
<div class="ews-card-content p-1 lg:p-2 custom-scrollbar">
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if footer}
|
||||
<div class="ews-card-footer">
|
||||
<div class="ews-card-footer bordered-top">
|
||||
{@render footer()}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import StripeBar from "./StripeBar.svelte";
|
||||
import InfiniteScroll from "./InfiniteScroll.svelte";
|
||||
|
||||
interface GempaBumiAlertProps {
|
||||
magnitudo?: number;
|
||||
|
|
@ -54,7 +52,7 @@
|
|||
class="warning-black opacity-0 blink animation-fast animation-delay-2"
|
||||
></div>
|
||||
<div class="flex flex-col font-bold text-center text-black">
|
||||
<span class="text-xl">WARNING</span>
|
||||
<span class="text-xl">PERINGATAN</span>
|
||||
<span class="text-xs">Gempa Bumi Terdeteksi</span>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -105,53 +103,16 @@
|
|||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="absolute top-0">
|
||||
<StripeBar loop={true} reverse={true} duration={20}></StripeBar>
|
||||
</div>
|
||||
<div class="absolute bottom-0">
|
||||
<StripeBar loop={true} duration={20}></StripeBar>
|
||||
</div> -->
|
||||
|
||||
<div class="flex w-full absolute top-0 left-0 right-0" style="z-index: 2;">
|
||||
<StripeBar className="my-2 " size="100px"></StripeBar>
|
||||
<StripeBar className="my-2 -scale-x-100 " size="100px"></StripeBar>
|
||||
<div
|
||||
class="absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center text-center"
|
||||
>
|
||||
<div class="p-1 bg-black rounded-lg w-full">
|
||||
<div class="bordered-red bg-black p-2 w-full text-primary">
|
||||
<InfiniteScroll speed={100} gap={48}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col text-center px-4">
|
||||
<b class="text-3xl" style="line-height: 0.8;">EARTHQUAKE</b>
|
||||
</div>
|
||||
{/snippet}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stripe top-0">
|
||||
<div class="stripe-wrapper">
|
||||
<div class="stripe-bar loop-stripe-reverse"></div>
|
||||
<div class="stripe-bar loop-stripe-reverse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full absolute bottom-0 left-0 right-0"
|
||||
style="z-index: 2;"
|
||||
>
|
||||
<StripeBar className="my-2 " size="100px"></StripeBar>
|
||||
<StripeBar className="my-2 -scale-x-100 " size="100px"></StripeBar>
|
||||
<div
|
||||
class="absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center text-center"
|
||||
>
|
||||
<div class="p-1 bg-black rounded-lg w-full">
|
||||
<div class="bordered-red bg-black p-2 w-full text-primary">
|
||||
<InfiniteScroll speed={100} gap={48} direction="right">
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col text-center px-4">
|
||||
<b class="text-3xl" style="line-height: 0.8;">EARTHQUAKE</b>
|
||||
</div>
|
||||
{/snippet}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stripe bottom-0">
|
||||
<div class="stripe-wrapper">
|
||||
<div class="stripe-bar loop-stripe"></div>
|
||||
<div class="stripe-bar loop-stripe"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import "../styles/components/HexGrid.css";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
|
|
@ -8,7 +7,7 @@
|
|||
variant = "pointy",
|
||||
hexWidth,
|
||||
hexHeight,
|
||||
gap = 4,
|
||||
gap = 4
|
||||
}: {
|
||||
children: Snippet;
|
||||
className?: string;
|
||||
|
|
@ -39,12 +38,10 @@
|
|||
|
||||
if (!isFlat) {
|
||||
// Pointy (Variant 1)
|
||||
const rowOffsetTop = gap + -20;
|
||||
const rowOffsetTop = -14;
|
||||
const itemFullWidth = w + gap;
|
||||
|
||||
let maxCols = Math.floor(
|
||||
(containerWidth + gap) / itemFullWidth,
|
||||
);
|
||||
let maxCols = Math.floor((containerWidth + gap) / itemFullWidth);
|
||||
if (maxCols < 1) maxCols = 1;
|
||||
|
||||
let isOffset = false;
|
||||
|
|
@ -81,18 +78,21 @@
|
|||
|
||||
let totalHeight = 0;
|
||||
if (currentCol > 0) {
|
||||
totalHeight = currentRow * (h + rowOffsetTop) + h;
|
||||
totalHeight =
|
||||
currentRow * (h + rowOffsetTop) + h;
|
||||
} else {
|
||||
totalHeight = (currentRow - 1) * (h + rowOffsetTop) + h;
|
||||
totalHeight =
|
||||
(currentRow - 1) * (h + rowOffsetTop) +
|
||||
h;
|
||||
}
|
||||
node.style.height = `${totalHeight}px`;
|
||||
|
||||
} else {
|
||||
// Flat (Variant 2)
|
||||
const colAdvanceX = w * 0.75 + gap;
|
||||
const rowAdvanceY = h + gap;
|
||||
|
||||
let maxCols =
|
||||
Math.floor((containerWidth - w) / colAdvanceX) + 1;
|
||||
let maxCols = Math.floor((containerWidth - w) / colAdvanceX) + 1;
|
||||
if (containerWidth < w) maxCols = 1;
|
||||
|
||||
let currentCol = 0;
|
||||
|
|
@ -149,6 +149,6 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="ews-hex-honeycomb {className}" use:honeycombLayout>
|
||||
<div class="hex-honeycomb {className}" use:honeycombLayout>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import "../styles/components/HexShape.css";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -22,14 +21,11 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="ews-hex-shape {flatTop ? 'flat-top' : ''} {color} {clipContent
|
||||
class="hex-shape {flatTop ? 'flat-top' : ''} {color} {clipContent
|
||||
? 'clip-content'
|
||||
: ''} {className}"
|
||||
: ''} flex flex-col justify-center items-center {className}"
|
||||
>
|
||||
<div
|
||||
class="inner-content"
|
||||
style={`--ews-hex-padding: ${paddingContent}px;`}
|
||||
>
|
||||
<div class="inner-content" style={`--hex-padding: ${paddingContent}px;`}>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
/** Gap antar item (px) */
|
||||
gap?: number;
|
||||
/** Kecepatan scroll: pixel per detik */
|
||||
speed?: number;
|
||||
/** Arah scroll: 'left' = normal, 'right' = reverse */
|
||||
direction?: "left" | "right";
|
||||
/** Pause saat hover */
|
||||
pauseOnHover?: boolean;
|
||||
/** Class tambahan untuk wrapper luar */
|
||||
className?: string;
|
||||
children?: import("svelte").Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
gap = 24,
|
||||
speed = 80,
|
||||
direction = "left",
|
||||
pauseOnHover = false,
|
||||
className = "",
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
let containerEl: HTMLDivElement;
|
||||
let trackEl: HTMLDivElement;
|
||||
let animFrame: number;
|
||||
let offset = 0;
|
||||
let cloneCount = $state(1);
|
||||
let singleWidth = 0;
|
||||
let lastTime: number | null = null;
|
||||
let paused = false;
|
||||
|
||||
/**
|
||||
* Hitung berapa kali child perlu di-clone agar track selalu lebih lebar dari container,
|
||||
* termasuk gap antar item.
|
||||
*/
|
||||
function calcClones() {
|
||||
if (!containerEl || !trackEl) return;
|
||||
const containerW = containerEl.getBoundingClientRect().width;
|
||||
// Ambil lebar satu "set" original items (slot pertama)
|
||||
const firstSet = trackEl.querySelector<HTMLElement>(".scroll-set");
|
||||
if (!firstSet) return;
|
||||
singleWidth = firstSet.getBoundingClientRect().width + gap;
|
||||
// Kloning minimal agar total lebar > 2x container (agar seamless loop)
|
||||
const needed = Math.ceil((containerW * 2) / singleWidth) + 1;
|
||||
cloneCount = Math.max(needed, 2);
|
||||
}
|
||||
|
||||
function tick(ts: number) {
|
||||
if (!paused) {
|
||||
if (lastTime !== null) {
|
||||
const dt = (ts - lastTime) / 1000;
|
||||
offset += speed * dt;
|
||||
if (singleWidth > 0 && offset >= singleWidth) {
|
||||
offset -= singleWidth;
|
||||
}
|
||||
}
|
||||
lastTime = ts;
|
||||
} else {
|
||||
lastTime = ts; // reset agar tidak jump saat resume
|
||||
}
|
||||
|
||||
if (trackEl) {
|
||||
const sign = direction === "right" ? 1 : -1;
|
||||
trackEl.style.transform = `translateX(${sign * offset}px)`;
|
||||
}
|
||||
animFrame = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Hitung clone setelah render awal
|
||||
calcClones();
|
||||
// Tunggu $state update (clones dirender), lalu hitung ulang
|
||||
requestAnimationFrame(() => {
|
||||
calcClones();
|
||||
animFrame = requestAnimationFrame(tick);
|
||||
});
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
calcClones();
|
||||
offset = 0;
|
||||
lastTime = null;
|
||||
});
|
||||
ro.observe(containerEl);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animFrame);
|
||||
ro.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="infinite-scroll-container {className}"
|
||||
bind:this={containerEl}
|
||||
onmouseenter={() => { if (pauseOnHover) paused = true; }}
|
||||
onmouseleave={() => { if (pauseOnHover) paused = false; }}
|
||||
>
|
||||
<div
|
||||
class="infinite-scroll-track"
|
||||
bind:this={trackEl}
|
||||
style="gap: {gap}px;"
|
||||
>
|
||||
<!--
|
||||
Render (1 + cloneCount) sets: index 0 adalah original,
|
||||
sisanya adalah clone agar container selalu penuh.
|
||||
-->
|
||||
{#each { length: 1 + cloneCount } as _, i}
|
||||
<div class="scroll-set" aria-hidden={i > 0 ? "true" : undefined}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.infinite-scroll-container {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.infinite-scroll-track {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.scroll-set {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -23,21 +23,21 @@
|
|||
networks = [
|
||||
{
|
||||
id: "network-1",
|
||||
name: "NET 1",
|
||||
name: "Network 1",
|
||||
active_channel: 2,
|
||||
inactive_channel: 14,
|
||||
total_channel: 16,
|
||||
},
|
||||
{
|
||||
id: "network-2",
|
||||
name: "NET 2",
|
||||
name: "Network 2",
|
||||
active_channel: 22,
|
||||
inactive_channel: 7,
|
||||
total_channel: 29,
|
||||
},
|
||||
{
|
||||
id: "network-3",
|
||||
name: "NET 3",
|
||||
name: "Network 3",
|
||||
active_channel: 12,
|
||||
inactive_channel: 5,
|
||||
total_channel: 17,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import StripeBar from "./StripeBar.svelte";
|
||||
|
||||
let {
|
||||
show = $bindable(false),
|
||||
|
|
@ -24,14 +23,15 @@
|
|||
{#if show}
|
||||
<div class="settings-modal-overlay" onclick={close} role="presentation">
|
||||
<div
|
||||
class="settings-modal ews-card ews-card-red {variant === 'large'
|
||||
? '!w-11/12 !max-w-4xl'
|
||||
: ''}"
|
||||
class="settings-modal ews-card bordered-red {variant === 'large' ? '!w-11/12 !max-w-4xl' : ''}"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="ews-card-header bordered-red-bottom overflow-hidden">
|
||||
<StripeBar></StripeBar>
|
||||
<div class="stripe-wrapper">
|
||||
<div class="stripe-bar loop-stripe-reverse anim-duration-20"></div>
|
||||
<div class="stripe-bar loop-stripe-reverse anim-duration-20"></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 right-0 flex justify-between items-center px-3"
|
||||
>
|
||||
|
|
@ -45,11 +45,7 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ews-card-content {variant === 'large'
|
||||
? 'p-4'
|
||||
: 'p-1 lg:p-2 p-4'} {contentClass}"
|
||||
>
|
||||
<div class="ews-card-content {variant === 'large' ? 'p-4' : 'p-1 lg:p-2 p-4'} {contentClass}">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
low: number;
|
||||
high: number;
|
||||
}
|
||||
|
||||
let { min, max, step, low = $bindable(), high = $bindable() }: Props = $props();
|
||||
|
||||
let sliderElement: HTMLDivElement;
|
||||
|
||||
function handlePointerDown(e: PointerEvent, type: 'low' | 'high') {
|
||||
const moveHandler = (moveEvent: PointerEvent) => {
|
||||
const rect = sliderElement.getBoundingClientRect();
|
||||
let percentage = (moveEvent.clientX - rect.left) / rect.width;
|
||||
percentage = Math.max(0, Math.min(1, percentage));
|
||||
|
||||
let newValue = min + percentage * (max - min);
|
||||
newValue = Math.round(newValue / step) * step;
|
||||
|
||||
if (type === 'low') {
|
||||
low = Math.min(newValue, high - step);
|
||||
} else {
|
||||
high = Math.max(newValue, low + step);
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
window.removeEventListener('pointermove', moveHandler);
|
||||
window.removeEventListener('pointerup', upHandler);
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', moveHandler);
|
||||
window.addEventListener('pointerup', upHandler);
|
||||
|
||||
// Immediate move to click position
|
||||
moveHandler(e);
|
||||
}
|
||||
|
||||
let lowPercent = $derived(((low - min) / (max - min)) * 100);
|
||||
let highPercent = $derived(((high - min) / (max - min)) * 100);
|
||||
</script>
|
||||
|
||||
<div class="range-slider-container">
|
||||
<div bind:this={sliderElement} class="range-slider-track">
|
||||
<div
|
||||
class="range-slider-fill"
|
||||
style="left: {lowPercent}%; right: {100 - highPercent}%"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="range-slider-handle"
|
||||
style="left: {lowPercent}%"
|
||||
onpointerdown={(e) => handlePointerDown(e, 'low')}
|
||||
role="slider"
|
||||
aria-valuenow={low}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="handle-glow"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="range-slider-handle"
|
||||
style="left: {highPercent}%"
|
||||
onpointerdown={(e) => handlePointerDown(e, 'high')}
|
||||
role="slider"
|
||||
aria-valuenow={high}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="handle-glow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.range-slider-container {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.range-slider-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 165, 0, 0.1);
|
||||
border: 1px solid rgba(255, 165, 0, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.range-slider-fill {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: var(--orange);
|
||||
box-shadow: 0 0 10px var(--orange);
|
||||
}
|
||||
|
||||
.range-slider-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: black;
|
||||
border: 2px solid var(--orange);
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.range-slider-handle:hover {
|
||||
transform: translate(-50%, -50%) rotate(45deg) scale(1.2);
|
||||
}
|
||||
|
||||
.handle-glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--orange);
|
||||
opacity: 0.3;
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.range-slider-handle:active {
|
||||
background: var(--orange);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import "../styles/components/RibLayout.css";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
|
|
@ -8,31 +7,13 @@
|
|||
nodeContent,
|
||||
connectorContent,
|
||||
getHref,
|
||||
maxBranches,
|
||||
}: {
|
||||
items: any[];
|
||||
maxBranches?: number;
|
||||
nodeContent: Snippet<
|
||||
[
|
||||
any,
|
||||
{
|
||||
side: "left" | "right";
|
||||
branchIndex: number;
|
||||
index: number;
|
||||
delay: number;
|
||||
},
|
||||
]
|
||||
[any, { side: "left" | "right"; branchIndex: number; index: number; delay: number }]
|
||||
>;
|
||||
connectorContent?: Snippet<
|
||||
[
|
||||
any,
|
||||
{
|
||||
side: "left" | "right";
|
||||
branchIndex: number;
|
||||
index: number;
|
||||
delay: number;
|
||||
},
|
||||
]
|
||||
[any, { side: "left" | "right"; branchIndex: number; index: number; delay: number }]
|
||||
>;
|
||||
getHref?: (item: any) => string;
|
||||
} = $props();
|
||||
|
|
@ -41,15 +22,10 @@
|
|||
let windowWidth = $state(0);
|
||||
|
||||
function getBranchCount(width: number): number {
|
||||
let count = 5;
|
||||
if (width < 768) count = 1;
|
||||
else if (width < 1024) count = 2;
|
||||
else if (width < 1300) count = 4;
|
||||
|
||||
if (maxBranches !== undefined) {
|
||||
return Math.min(count, maxBranches);
|
||||
}
|
||||
return count;
|
||||
if (width < 768) return 1;
|
||||
if (width < 1024) return 2;
|
||||
if (width < 1300) return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
|
|
@ -81,76 +57,62 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="ews-rib-layout">
|
||||
<div
|
||||
class="inline-flex h-auto justify-center gap-4 w-full px-4 overflow-none relative"
|
||||
>
|
||||
{#each chunkedItems as branchItems, branchIndex}
|
||||
<div class="ews-rib-layout__branch">
|
||||
<div class="relative py-4 lg:py-10 flex flex-col gap-4">
|
||||
<!-- Central Spine -->
|
||||
<div
|
||||
class="ews-rib-layout__spine line-central"
|
||||
class="absolute h-auto left-1/2 top-0 bottom-0 w-1 bg-primary transform -translate-x-1/2 z-0 line-central"
|
||||
style="animation-delay: {branchIndex * 200}ms;"
|
||||
></div>
|
||||
|
||||
<!-- Iterate in pairs essentially by grouping them two by two -->
|
||||
<div class="ews-rib-layout__grid">
|
||||
<div class="grid grid-cols-2 relative z-10">
|
||||
{#each branchItems as item, index}
|
||||
{@const side = index % 2 === 0 ? "left" : "right"}
|
||||
{@const delay = (branchIndex + 1) * (index + 1) * 10}
|
||||
|
||||
|
||||
<svelte:element
|
||||
this={getHref ? "a" : "div"}
|
||||
href={getHref?.(item)}
|
||||
class="ews-rib-layout__node {side === 'left'
|
||||
? 'ews-rib-layout__node--left node'
|
||||
: 'ews-rib-layout__node--right node-flip'}"
|
||||
class="{side === 'left'
|
||||
? 'flex flex-grow justify-end items-center relative pr-0 col-start-1 node'
|
||||
: 'flex justify-start items-center relative pl-0 col-start-2 w-auto node-flip'}"
|
||||
>
|
||||
{#if side === "left"}
|
||||
<div class="ews-rib-layout__node-content parent-node">
|
||||
<div class="relative flex parent-node">
|
||||
{@render nodeContent(item, { side, branchIndex, index, delay })}
|
||||
</div>
|
||||
<div
|
||||
class="ews-rib-layout__connector-wrapper ews-rib-layout__connector-wrapper--left line"
|
||||
>
|
||||
<div class="w-24 flex justify-end relative line">
|
||||
<div
|
||||
class="ews-rib-layout__connector-line line-node"
|
||||
class="h-[2px] w-24 bg-primary z-0 line-node"
|
||||
style="animation-delay: {delay}ms;"
|
||||
></div>
|
||||
{#if connectorContent}
|
||||
<div
|
||||
class="ews-rib-layout__connector-text ews-rib-layout__connector-text--left fade-in animation-delay-5"
|
||||
class="font-bold text-xs uppercase absolute left-2 z-10 text-left top-1 fade-in animation-delay-5 text-primary"
|
||||
>
|
||||
{@render connectorContent(item, {
|
||||
side,
|
||||
branchIndex,
|
||||
index,
|
||||
delay,
|
||||
})}
|
||||
{@render connectorContent(item, { side, branchIndex, index, delay })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ews-rib-layout__connector-wrapper ews-rib-layout__connector-wrapper--right"
|
||||
>
|
||||
<div class="w-24 flex justify-start relative">
|
||||
<div
|
||||
class="ews-rib-layout__connector-line line-node"
|
||||
class="h-[2px] w-24 bg-primary z-0 line-node"
|
||||
style="animation-delay: {delay}ms;"
|
||||
></div>
|
||||
{#if connectorContent}
|
||||
<div
|
||||
class="ews-rib-layout__connector-text ews-rib-layout__connector-text--right fade-in animation-delay-5"
|
||||
class="font-bold text-xs uppercase absolute z-10 right-2 text-right top-1 fade-in animation-delay-5 text-primary"
|
||||
>
|
||||
{@render connectorContent(item, {
|
||||
side,
|
||||
branchIndex,
|
||||
index,
|
||||
delay,
|
||||
})}
|
||||
{@render connectorContent(item, { side, branchIndex, index, delay })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="ews-rib-layout__node-content ews-rib-layout__node-content--right parent-node flip"
|
||||
>
|
||||
<div class="relative flex parent-node flip">
|
||||
{@render nodeContent(item, { side, branchIndex, index, delay })}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { serialStore } from "$lib/stores/serialStore";
|
||||
import { fade } from "svelte/transition";
|
||||
import StripeBar from "./StripeBar.svelte";
|
||||
|
||||
let status = $derived($serialStore.status);
|
||||
let error = $derived($serialStore.error);
|
||||
|
||||
const statusProps = {
|
||||
connected: {
|
||||
color: "green",
|
||||
label: "LINKED",
|
||||
loop: true,
|
||||
reverse: false,
|
||||
},
|
||||
disconnected: {
|
||||
color: "",
|
||||
label: "LINK ESP32",
|
||||
loop: false,
|
||||
reverse: false,
|
||||
},
|
||||
unsupported: {
|
||||
color: "red",
|
||||
label: "NO SERIAL",
|
||||
loop: false,
|
||||
reverse: false,
|
||||
},
|
||||
connecting: {
|
||||
color: "blue",
|
||||
label: "LINKING...",
|
||||
loop: true,
|
||||
reverse: true,
|
||||
},
|
||||
};
|
||||
|
||||
async function handleConnect() {
|
||||
if (status === "connected") {
|
||||
await serialStore.disconnect();
|
||||
} else {
|
||||
await serialStore.connect();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-start no-snapshot">
|
||||
<button
|
||||
onclick={handleConnect}
|
||||
disabled={status === "unsupported" || status === "connecting"}
|
||||
class="ews-btn ews-btn-primary w-full"
|
||||
>
|
||||
{statusProps[status].label}
|
||||
</button>
|
||||
|
||||
{#if status === "connected"}
|
||||
<button
|
||||
onclick={() => serialStore.testConnection()}
|
||||
class="ews-btn ews-btn-danger w-full"
|
||||
>
|
||||
TEST BEEP / HEARTBEAT
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div
|
||||
transition:fade
|
||||
class="bg-red-950/80 border border-red-500/50 text-red-400 px-3 py-1 text-[10px] font-mono uppercase"
|
||||
>
|
||||
ERROR: {error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import "../styles/components/StripeBar.css";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -25,20 +24,20 @@
|
|||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div style="overflow: hidden;" class={className}>
|
||||
<div class="overflow-hidden {className}">
|
||||
<div
|
||||
class="ews-stripe-wrapper {orientation}"
|
||||
class="stripe-wrapper {orientation}"
|
||||
style="{orientation == 'vertical' ? 'width' : 'height'}: {size};"
|
||||
>
|
||||
<div
|
||||
class="ews-stripe-bar {color} {orientation} {loop
|
||||
class="stripe-bar {color} {orientation} {loop
|
||||
? 'loop-stripe'
|
||||
: ''}{orientation ? '-' + orientation : ''} {reverse
|
||||
? 'reverse'
|
||||
: ''} anim-duration-{duration}"
|
||||
></div>
|
||||
<div
|
||||
class="ews-stripe-bar {color} {orientation} {loop
|
||||
class="stripe-bar {color} {orientation} {loop
|
||||
? 'loop-stripe'
|
||||
: ''}{orientation ? '-' + orientation : ''} {reverse
|
||||
? 'reverse'
|
||||
|
|
|
|||
|
|
@ -1,718 +0,0 @@
|
|||
<script module lang="ts">
|
||||
export type ThreadTone = "normal" | "danger" | "muted";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
// import { fade } from "svelte/transition";
|
||||
|
||||
type ThreadVariant = "spine" | "threaded";
|
||||
|
||||
interface ThreadItem {
|
||||
id: string;
|
||||
label: string;
|
||||
level?: number;
|
||||
tone?: ThreadTone;
|
||||
collapsed?: boolean;
|
||||
children?: ThreadItem[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items?: ThreadItem[];
|
||||
variant?: ThreadVariant;
|
||||
className?: string;
|
||||
nodeWidth?: number;
|
||||
rowHeight?: number;
|
||||
indent?: number;
|
||||
tone?: "neutral" | "primary" | "danger";
|
||||
gap?: number;
|
||||
expandable?: boolean;
|
||||
animated?: boolean;
|
||||
collapseAll?: boolean;
|
||||
onToggle?: (id: string, collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
const defaultItemsByVariant: Record<ThreadVariant, ThreadItem[]> = {
|
||||
spine: [
|
||||
{
|
||||
id: "spine-1",
|
||||
label: "Primary Alert Message",
|
||||
level: 1,
|
||||
tone: "normal",
|
||||
},
|
||||
{
|
||||
id: "spine-2",
|
||||
label: "Operator Verification Note",
|
||||
level: 2,
|
||||
tone: "muted",
|
||||
},
|
||||
{
|
||||
id: "spine-3",
|
||||
label: "Follow-up Action Detail",
|
||||
level: 3,
|
||||
tone: "muted",
|
||||
},
|
||||
],
|
||||
threaded: [
|
||||
{
|
||||
id: "thread-1",
|
||||
label: "Primary Alert Message",
|
||||
level: 1,
|
||||
tone: "normal",
|
||||
children: [
|
||||
{
|
||||
id: "thread-1-1",
|
||||
label: "Operator Verification Note",
|
||||
level: 2,
|
||||
tone: "muted",
|
||||
},
|
||||
{
|
||||
id: "thread-1-2",
|
||||
label: "Follow-up Action Detail",
|
||||
level: 3,
|
||||
tone: "muted",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "thread-2",
|
||||
label: "Secondary Discussion",
|
||||
level: 1,
|
||||
tone: "muted",
|
||||
children: [
|
||||
{
|
||||
id: "thread-2-1",
|
||||
label: "Nested Reply A",
|
||||
level: 2,
|
||||
tone: "normal",
|
||||
},
|
||||
{
|
||||
id: "thread-2-2",
|
||||
label: "Nested Reply B",
|
||||
level: 2,
|
||||
tone: "muted",
|
||||
children: [
|
||||
{
|
||||
id: "thread-2-2-1",
|
||||
label: "Deep Nested Reply",
|
||||
level: 3,
|
||||
tone: "muted",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "thread-3",
|
||||
label: "Final Comment",
|
||||
level: 1,
|
||||
tone: "muted",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let {
|
||||
items = [],
|
||||
variant = "spine",
|
||||
className = "",
|
||||
nodeWidth = 280,
|
||||
rowHeight = 42,
|
||||
indent = 21,
|
||||
tone = "primary",
|
||||
gap = 16,
|
||||
expandable = true,
|
||||
animated = true,
|
||||
collapseAll = false,
|
||||
onToggle,
|
||||
}: Props = $props();
|
||||
|
||||
let toggledIds = $state(new Set<string>());
|
||||
let currentToggleId: string | null = $state(null);
|
||||
|
||||
function isNodeCollapsed(id: string): boolean {
|
||||
if (!expandable) {
|
||||
return false;
|
||||
}
|
||||
const defaultCollapsed = !collapseAll;
|
||||
const toggled = toggledIds.has(id);
|
||||
return defaultCollapsed ? !toggled : toggled;
|
||||
}
|
||||
|
||||
function toggleCollapse(id: string) {
|
||||
currentToggleId = id;
|
||||
const newToggled = new Set(toggledIds);
|
||||
if (newToggled.has(id)) {
|
||||
newToggled.delete(id);
|
||||
} else {
|
||||
newToggled.add(id);
|
||||
}
|
||||
toggledIds = newToggled;
|
||||
onToggle?.(id, isNodeCollapsed(id));
|
||||
}
|
||||
|
||||
function flattenItems(
|
||||
itemList: ThreadItem[],
|
||||
parentCollapsed = false,
|
||||
): (ThreadItem & { parentCollapsed?: boolean })[] {
|
||||
const result: (ThreadItem & { parentCollapsed?: boolean })[] = [];
|
||||
for (const item of itemList) {
|
||||
const isCollapsed = isNodeCollapsed(item.id) || parentCollapsed;
|
||||
result.push({ ...item, parentCollapsed });
|
||||
if (item.children && item.children.length > 0 && !isCollapsed) {
|
||||
result.push(...flattenItems(item.children, isCollapsed));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function hasChildren(item: ThreadItem): boolean {
|
||||
return !!(item.children && item.children.length > 0);
|
||||
}
|
||||
|
||||
function getChildCount(item: ThreadItem): number {
|
||||
if (!item.children || item.children.length === 0) return 0;
|
||||
let count = item.children.length;
|
||||
for (const child of item.children) {
|
||||
count += getChildCount(child);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const rowStep = $derived(rowHeight + gap);
|
||||
const rootX = $derived(indent);
|
||||
|
||||
const resolvedItems = $derived.by(() =>
|
||||
items.length > 0 ? items : defaultItemsByVariant[variant],
|
||||
);
|
||||
|
||||
const normalizedItems = $derived.by(() => {
|
||||
const items = resolvedItems.map((item, index) => ({
|
||||
id: item.id || `threaded-item-${index + 1}`,
|
||||
label: item.label || `Item ${index + 1}`,
|
||||
level: Math.max(1, Math.floor(item.level ?? 1)),
|
||||
tone: item.tone ?? ("muted" as ThreadTone),
|
||||
collapsed: item.collapsed ?? false,
|
||||
children: item.children,
|
||||
}));
|
||||
return flattenItems(items);
|
||||
});
|
||||
|
||||
const maxLevel = $derived.by(() =>
|
||||
normalizedItems.reduce((acc, item) => Math.max(acc, item.level ?? 1), 1),
|
||||
);
|
||||
|
||||
const canvasHeight = $derived.by(() => {
|
||||
if (normalizedItems.length === 0) return rowHeight;
|
||||
return normalizedItems.length * rowStep - gap;
|
||||
});
|
||||
|
||||
const canvasWidth = $derived.by(() => getNodeX(maxLevel) + nodeWidth + 8);
|
||||
|
||||
function getDepthX(depth: number) {
|
||||
if (depth <= 0) return rootX;
|
||||
return rootX + depth * indent;
|
||||
}
|
||||
|
||||
function getNodeX(level: number) {
|
||||
return getDepthX(level);
|
||||
}
|
||||
|
||||
function getRowCenter(index: number) {
|
||||
return index * rowStep + rowHeight / 2;
|
||||
}
|
||||
|
||||
function getHorizontalStartX(level: number) {
|
||||
if (variant === "spine") return rootX;
|
||||
return getDepthX(level - 1);
|
||||
}
|
||||
|
||||
function getThreadedVerticalX(currentLevel: number, nextLevel: number) {
|
||||
if (nextLevel > currentLevel) return getDepthX(currentLevel);
|
||||
if (nextLevel === currentLevel) return getDepthX(currentLevel - 1);
|
||||
return getDepthX(nextLevel - 1);
|
||||
}
|
||||
|
||||
function getParentIndex(index: number) {
|
||||
const currentLevel = normalizedItems[index].level ?? 0;
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if ((normalizedItems[i].level ?? 0) < currentLevel) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getChildIndex(parentId: string, childId: string): number {
|
||||
const parentIndex = normalizedItems.findIndex((i) => i.id === parentId);
|
||||
if (parentIndex === -1) return -1;
|
||||
const childIndex = normalizedItems[parentIndex].children?.findIndex(
|
||||
(i) => i.id === childId,
|
||||
);
|
||||
if (childIndex === -1) return -1;
|
||||
return childIndex ?? -1;
|
||||
}
|
||||
|
||||
function animateLine(
|
||||
node: SVGLineElement,
|
||||
params = { duration: 350, delay: 0 },
|
||||
) {
|
||||
const length = Math.hypot(
|
||||
(node.x2?.baseVal?.value ?? 0) - (node.x1?.baseVal?.value ?? 0),
|
||||
(node.y2?.baseVal?.value ?? 0) - (node.y1?.baseVal?.value ?? 0),
|
||||
);
|
||||
|
||||
node.style.strokeDasharray = String(length);
|
||||
node.style.strokeDashoffset = String(length);
|
||||
node.style.opacity = "0";
|
||||
|
||||
const anim = node.animate(
|
||||
[
|
||||
{ strokeDashoffset: length, opacity: 0 },
|
||||
{ strokeDashoffset: 0, opacity: 1 },
|
||||
],
|
||||
{
|
||||
duration: params.duration,
|
||||
delay: params.delay,
|
||||
easing: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
fill: "forwards",
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
anim.cancel();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function animatePath(
|
||||
node: SVGPathElement,
|
||||
params = { duration: 350, delay: 0 },
|
||||
) {
|
||||
if (!animated) {
|
||||
return;
|
||||
}
|
||||
const targetD = node.getAttribute("data-target-d");
|
||||
if (targetD) {
|
||||
const timeOut = setTimeout(() => {
|
||||
node.setAttribute("d", targetD);
|
||||
}, params.delay);
|
||||
return {
|
||||
destroy() {
|
||||
clearTimeout(timeOut);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const length = node.getTotalLength();
|
||||
|
||||
node.style.strokeDasharray = String(length);
|
||||
node.style.strokeDashoffset = String(length);
|
||||
node.style.opacity = "0";
|
||||
|
||||
const anim = node.animate(
|
||||
[
|
||||
{ strokeDashoffset: length, opacity: 0 },
|
||||
{ strokeDashoffset: 0, opacity: 1 },
|
||||
],
|
||||
{
|
||||
duration: params.duration,
|
||||
delay: params.delay,
|
||||
easing: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
fill: "forwards",
|
||||
},
|
||||
);
|
||||
|
||||
const timeOut = setTimeout(() => {
|
||||
node.classList.add("smooth-line");
|
||||
node.removeAttribute("style");
|
||||
}, params.delay + params.duration);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
anim.cancel();
|
||||
clearTimeout(timeOut);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ews-threaded-comments {tone} {className}">
|
||||
<div class="threaded-scroll">
|
||||
<div
|
||||
class="threaded-canvas"
|
||||
style:height={`${canvasHeight}px`}
|
||||
style:width={`100%`}
|
||||
>
|
||||
<svg
|
||||
class="threaded-connectors"
|
||||
height="100%"
|
||||
width="100%"
|
||||
viewBox={`0 0 100% ${canvasHeight}`}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#if normalizedItems.length > 1 && variant === "spine"}
|
||||
<path
|
||||
class="connector-line vertical-main smooth-line"
|
||||
d={`M 2,${rowHeight / 2} L 2,${(normalizedItems.length - 1) * rowStep + rowHeight / 2}`}
|
||||
/>
|
||||
{#each normalizedItems as item, index (`${item.id}-horizontal`)}
|
||||
{@const toggleIndex = normalizedItems.findIndex(
|
||||
(i) => i.id === currentToggleId,
|
||||
)}
|
||||
{@const parentIndex = getParentIndex(index)}
|
||||
{@const childIndex =
|
||||
getParentIndex(index) > -1
|
||||
? getChildIndex(normalizedItems[parentIndex].id, item.id)
|
||||
: 0}
|
||||
{@const delay =
|
||||
50 * Math.max(parentIndex + childIndex - toggleIndex, 1)}
|
||||
<path
|
||||
class="connector-line horizontal-1 {!animated
|
||||
? 'smooth-line'
|
||||
: ''}"
|
||||
d={`M 2 ${getRowCenter(index)} H ${getNodeX(item.level ?? 1) + 2}`}
|
||||
data-level={item.level}
|
||||
fill="none"
|
||||
use:animatePath={{
|
||||
duration: 200,
|
||||
delay: delay,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if variant === "threaded"}
|
||||
<path
|
||||
class="connector-line vertical-main smooth-line"
|
||||
d={`M 2,${rowHeight / 2} L 2,${(normalizedItems.length - 1) * rowStep + rowHeight / 2}`}
|
||||
/>
|
||||
|
||||
{#each normalizedItems as item, index (`${item.id}-vertical`)}
|
||||
{#if index > 0 && getParentIndex(index) >= 0}
|
||||
{@const toggleIndex = normalizedItems.findIndex(
|
||||
(i) => i.id === currentToggleId,
|
||||
)}
|
||||
{@const parentIndex = getParentIndex(index)}
|
||||
{@const childIndex =
|
||||
getParentIndex(index) > -1
|
||||
? getChildIndex(normalizedItems[parentIndex].id, item.id)
|
||||
: 0}
|
||||
{@const delay =
|
||||
50 * Math.max(parentIndex + childIndex - toggleIndex, 1)}
|
||||
<path
|
||||
d="M {((item.level ?? 1) - 1) * rootX + 2}, {getRowCenter(
|
||||
index,
|
||||
) -
|
||||
rowStep * (index - getParentIndex(index))} L {((item.level ??
|
||||
1) -
|
||||
1) *
|
||||
rootX +
|
||||
2}, {getRowCenter(index + 1) - rowStep}"
|
||||
fill="none"
|
||||
class="connector-line vertical-2 {!animated
|
||||
? 'smooth-line'
|
||||
: ''}"
|
||||
use:animatePath={{
|
||||
duration: 200,
|
||||
delay: delay,
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each normalizedItems as item, index (`${item.id}-horizontal`)}
|
||||
{@const toggleIndex = normalizedItems.findIndex(
|
||||
(i) => i.id === currentToggleId,
|
||||
)}
|
||||
|
||||
{@const parentIndex = getParentIndex(index)}
|
||||
{@const childIndex =
|
||||
getParentIndex(index) > -1
|
||||
? getChildIndex(normalizedItems[parentIndex].id, item.id)
|
||||
: 0}
|
||||
{@const delay =
|
||||
100 * Math.max(parentIndex + childIndex - toggleIndex, 1)}
|
||||
|
||||
<path
|
||||
class="connector-line horizontal-2 {!animated
|
||||
? 'smooth-line'
|
||||
: ''}"
|
||||
d={`M ${((item.level ?? 1) - 1) * rootX + 2} ${getRowCenter(index)} H ${getNodeX(item.level ?? 1) + 2}`}
|
||||
data-level={item.level}
|
||||
fill="none"
|
||||
use:animatePath={{
|
||||
duration: 200,
|
||||
delay: delay,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</svg>
|
||||
|
||||
{#each normalizedItems as item, index (item.id)}
|
||||
{@const isCollapsed = isNodeCollapsed(item.id)}
|
||||
{@const hasKids = hasChildren(item)}
|
||||
{@const childCount = getChildCount(item)}
|
||||
<div
|
||||
class="threaded-node-wrap"
|
||||
style:top={`${index * rowStep}px`}
|
||||
style:left={`${getNodeX(item.level ?? 1)}px`}
|
||||
style:height={`${rowHeight}px`}
|
||||
style:right={"0"}
|
||||
class:collapsed={isCollapsed}
|
||||
class:has-children={hasKids && expandable}
|
||||
>
|
||||
<div
|
||||
class="threaded-node {item.tone}"
|
||||
class:is-collapsed={isCollapsed}
|
||||
>
|
||||
{#if expandable && hasKids}
|
||||
<button
|
||||
class="expand-toggle"
|
||||
onclick={() => toggleCollapse(item.id)}
|
||||
aria-label={isCollapsed ? "Expand" : "Collapse"}
|
||||
>
|
||||
<span class="toggle-icon" class:collapsed={isCollapsed}>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M4.5 5.5l3.5 3.5 3.5-3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
{#if isCollapsed}
|
||||
<span class="child-count">{childCount}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<span class="node-label">{item.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ews-threaded-comments {
|
||||
--line-color: rgba(var(--glow-rgb), 0.85);
|
||||
--line-glow: rgba(var(--glow-rgb), 0.5);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ews-threaded-comments.neutral {
|
||||
--line-color: rgba(226, 231, 236, 0.92);
|
||||
--line-glow: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ews-threaded-comments.primary {
|
||||
--line-color: rgba(var(--glow-rgb), 0.88);
|
||||
--line-glow: rgba(var(--glow-rgb), 0.45);
|
||||
}
|
||||
|
||||
.ews-threaded-comments.danger {
|
||||
--line-color: rgba(var(--danger-glow-rgb), 0.9);
|
||||
--line-glow: rgba(var(--danger-glow-rgb), 0.45);
|
||||
}
|
||||
|
||||
.threaded-scroll {
|
||||
width: 100%;
|
||||
/* overflow-x: auto;
|
||||
overflow-y: hidden; */
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.threaded-canvas {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
transition:
|
||||
height 0.25s ease,
|
||||
width 0.25s ease;
|
||||
}
|
||||
|
||||
.threaded-connectors {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.connector-line {
|
||||
stroke: var(--line-color);
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 0 2px var(--line-glow));
|
||||
/* transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); */
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
|
||||
/* stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: draw 1s linear forwards; */
|
||||
}
|
||||
|
||||
.connector-line.smooth-line {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes draw {
|
||||
to {
|
||||
stroke-dashoffset: 0; /* Animate the offset to zero to reveal the path */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.threaded-node-wrap {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s ease,
|
||||
top 0.25s ease,
|
||||
left 0.25s ease;
|
||||
}
|
||||
|
||||
.threaded-node {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(36, 36, 36, 0.98),
|
||||
rgba(62, 62, 62, 0.98)
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.65);
|
||||
color: #f4f4f4;
|
||||
}
|
||||
|
||||
.threaded-node.normal {
|
||||
color: var(--text-color);
|
||||
border-color: rgba(var(--glow-rgb), 0.55);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(var(--glow-rgb), 0.18),
|
||||
0 0 8px rgba(var(--glow-rgb), 0.12);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--glow-rgb), 0.1),
|
||||
rgba(var(--glow-rgb), 0.1)
|
||||
);
|
||||
}
|
||||
|
||||
.threaded-node.danger {
|
||||
color: var(--danger-text-color);
|
||||
border-color: rgba(var(--danger-glow-rgb), 0.7);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(var(--danger-glow-rgb), 0.2),
|
||||
0 0 8px rgba(var(--danger-glow-rgb), 0.14);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--danger-glow-rgb), 0.1),
|
||||
rgba(var(--danger-glow-rgb), 0.1)
|
||||
);
|
||||
}
|
||||
|
||||
.threaded-node.muted {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.expand-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px;
|
||||
margin-right: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-icon svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.toggle-icon.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.child-count {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
padding: 0 4px;
|
||||
background: rgba(var(--glow-rgb), 0.3);
|
||||
border-radius: 8px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* .threaded-node-wrap.collapsed {
|
||||
opacity: 0.5;
|
||||
} */
|
||||
|
||||
.threaded-node.is-collapsed {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ews-threaded-comments:not(.primary) .expand-toggle {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.ews-threaded-comments.neutral .child-count {
|
||||
background: rgba(226, 231, 236, 0.2);
|
||||
}
|
||||
|
||||
.ews-threaded-comments.danger .child-count {
|
||||
background: rgba(var(--danger-glow-rgb), 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.threaded-node {
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import mapboxgl from "mapbox-gl";
|
||||
import AnimatedPopup from 'mapbox-gl-animated-popup';
|
||||
import type { InfoGempa } from '$lib/types';
|
||||
import { createGempaPopupHTML } from "$lib/utils/mapUtils";
|
||||
|
||||
type TitikGempaSetting = {
|
||||
map: mapboxgl.Map;
|
||||
|
|
@ -153,15 +152,30 @@ export class TitikGempa {
|
|||
|
||||
renderPopup() {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.innerHTML = createGempaPopupHTML({
|
||||
id: this.id,
|
||||
mag: this.mag,
|
||||
depth: this.depth,
|
||||
time: new Date(this.infoGempa.time!).toLocaleString(),
|
||||
lat: this.infoGempa.lat,
|
||||
lng: this.infoGempa.lng,
|
||||
place: this.infoGempa.place ?? "-",
|
||||
});
|
||||
placeholder.innerHTML = `
|
||||
<div class="ews-card bordered-red min-h-48 min-w-48 whitespace-pre-wrap">
|
||||
<div class="ews-card-header bordered-red-bottom">
|
||||
<div class="overflow-hidden">
|
||||
<div class="stripe-wrapper"><div class="stripe-bar loop-stripe-reverse anim-duration-20"></div><div class="stripe-bar loop-stripe-reverse anim-duration-20"></div></div>
|
||||
<div class="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center">
|
||||
<p class="p-1 bg-black font-bold text-xs text-glow">EARTHQUAKE</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ews-card-content p-1 lg:p-2 custom-scrollbar">
|
||||
${this.mag ? `<table class="w-full">
|
||||
<tbody>
|
||||
<tr><td class="flex">Magnitudo</td><td class="text-right break-words pl-2">${Number(this.mag).toFixed(1)}</td></tr>
|
||||
<tr><td class="flex">Kedalaman</td><td class="text-right break-words pl-2">${this.depth}</td></tr>
|
||||
<tr><td class="flex">Waktu</td><td class="text-right break-words pl-2">${new Date(this.infoGempa.time!).toLocaleString()}</td></tr>
|
||||
<tr><td class="flex">Lokasi (Lat,Lng)</td><td class="text-right break-words pl-2">${this.infoGempa.lat} , ${this.infoGempa.lng}</td></tr>
|
||||
</tbody>
|
||||
</table>` : ''}
|
||||
${this.setting?.description != null && this.setting?.description != '' ? `<hr><p class="mt-1 text-xs">${this.setting?.description}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`.trim()
|
||||
.replace(/>\s+</g, "><");
|
||||
|
||||
if (this.gemaMarker) {
|
||||
const popup = new AnimatedPopup({
|
||||
|
|
@ -286,7 +300,7 @@ export class TitikGempa {
|
|||
if (this.setting?.map != null) {
|
||||
this.setting.map.flyTo({
|
||||
center: [this.infoGempa.lng, this.infoGempa.lat],
|
||||
zoom: 8
|
||||
zoom: 6
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export class TitikTsunami {
|
|||
if (this.setting?.map != null) {
|
||||
this.setting.map.flyTo({
|
||||
center: [this.infoTsunami.lng, this.infoTsunami.lat],
|
||||
zoom: 8
|
||||
zoom: 6
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,49 +134,45 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-11/12 md:w-3/4 overflow-hidden bg-black relative rounded flex justify-center items-center opacity-0 show-pop-up animation-delay-2"
|
||||
class="w-3/4 overflow-hidden bg-black relative rounded flex justify-center items-center opacity-0 show-pop-up animation-delay-2"
|
||||
>
|
||||
<div
|
||||
class="absolute w-full h-2 m-auto top-0 left-0 right-0 overflow-hidden"
|
||||
>
|
||||
<StripeBar color="red" loop={true} className="w-full h-2"
|
||||
></StripeBar>
|
||||
<div class="w-2 h-full stripe-bar-red stripe-animation"></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute w-full h-2 m-auto bottom-0 left-0 right-0 overflow-hidden"
|
||||
>
|
||||
<StripeBar
|
||||
color="red"
|
||||
loop={true}
|
||||
reverse={true}
|
||||
className="w-full h-2"
|
||||
></StripeBar>
|
||||
<div
|
||||
class="w-2 h-full stripe-bar-red stripe-animation-reverse"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute w-2 h-full m-auto top-0 bottom-0 left-0 overflow-hidden"
|
||||
>
|
||||
<StripeBar
|
||||
color="red"
|
||||
orientation="vertical"
|
||||
reverse={true}
|
||||
loop={true}
|
||||
className="w-2 h-full"
|
||||
></StripeBar>
|
||||
<div
|
||||
class="w-2 h-full stripe-bar-red-vertical loop-stripe-vertical-reverse"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute w-2 h-full m-auto top-0 bottom-0 right-0 overflow-hidden"
|
||||
>
|
||||
<StripeBar
|
||||
color="red"
|
||||
orientation="vertical"
|
||||
loop={true}
|
||||
className="w-2 h-full"
|
||||
></StripeBar>
|
||||
<div
|
||||
class="w-2 h-full stripe-bar-red-vertical loop-stripe-vertical"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-full h-full p-6">
|
||||
<div class="bordered-red p-2 text-center w-full mb-2">
|
||||
<div class="overflow-hidden relative">
|
||||
<StripeBar loop={true} reverse={true} duration={20}></StripeBar>
|
||||
<div class="stripe-wrapper">
|
||||
<div
|
||||
class="stripe-bar loop-stripe-reverse anim-duration-20"
|
||||
></div>
|
||||
<div
|
||||
class="stripe-bar loop-stripe-reverse anim-duration-20"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center"
|
||||
>
|
||||
|
|
@ -184,11 +180,17 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ews-card ews-card-red w-full h-auto">
|
||||
<div class="ews-card bordered-red w-full h-auto">
|
||||
<div class="ews-card-header bordered-red-bottom">
|
||||
<div class="overflow-hidden relative">
|
||||
<StripeBar loop={true} reverse={true} duration={20}
|
||||
></StripeBar>
|
||||
<div class="stripe-wrapper">
|
||||
<div
|
||||
class="stripe-bar loop-stripe-reverse anim-duration-20"
|
||||
></div>
|
||||
<div
|
||||
class="stripe-bar loop-stripe-reverse anim-duration-20"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
<script context="module" lang="ts">
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
// Data point structure
|
||||
export interface DataPoint {
|
||||
t: number; // timestamp in MS
|
||||
v: number; // amplitude value
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import "../styles/components/WaveformChart.css";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let waveformData: DataPoint[] = [];
|
||||
|
||||
|
|
@ -30,10 +27,6 @@
|
|||
export let timeWindowMs: number = 20000;
|
||||
export let timeOffsetMs: number = 0;
|
||||
|
||||
// Horizontal (X-axis / time) zoom limits in milliseconds
|
||||
export const MIN_TIME_WINDOW_MS: number = 2000; // 2 seconds minimum
|
||||
export const MAX_TIME_WINDOW_MS: number = 300000; // 5 minutes maximum
|
||||
|
||||
// Timezone
|
||||
export let selectedTimezone: number = 0; // 0: UTC, 7: WIB, 8: WITA, 9: WIT
|
||||
|
||||
|
|
@ -41,24 +34,12 @@
|
|||
export let isDemoPsychoMode: boolean = false;
|
||||
export let psychoPoints: { nx: number; ny: number }[] = [];
|
||||
|
||||
// Historical mode: set both to non-zero to activate
|
||||
// When active, live loop pauses and X-axis is fixed to this range
|
||||
export let historicalStartMs: number = 0;
|
||||
export let historicalEndMs: number = 0;
|
||||
|
||||
// Method to resume viewing live data
|
||||
export function resumeLive() {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
|
||||
// Jump to the end of the historical range (right edge = endTime)
|
||||
export function jumpToHistoricalEnd() {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
draw();
|
||||
}
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let container: HTMLDivElement;
|
||||
|
|
@ -67,11 +48,6 @@
|
|||
let height = 600;
|
||||
|
||||
let frozenLatestTime = 0;
|
||||
|
||||
// Automatically freeze live mode if timeOffsetMs is increased externally
|
||||
$: if (timeOffsetMs > 0 && frozenLatestTime === 0 && historicalStartMs === 0) {
|
||||
frozenLatestTime = Date.now();
|
||||
}
|
||||
let isDragging = false;
|
||||
let lastMouseX = 0;
|
||||
let initialPinchDistance = 0;
|
||||
|
|
@ -95,66 +71,8 @@
|
|||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const isHistoricalMode = historicalStartMs > 0 && historicalEndMs > 0;
|
||||
const isMobile = width < 768;
|
||||
const leftPadding = isMobile ? 40 : 70;
|
||||
const drawWidth = width - leftPadding;
|
||||
|
||||
// CTRL + Scroll → Horizontal Zoom (X-axis / Time Window)
|
||||
if (e.ctrlKey && e.deltaY !== 0) {
|
||||
const oldWindow = timeWindowMs;
|
||||
const zoomFactor = e.deltaY > 0 ? 1.15 : 1 / 1.15; // zoom out / zoom in
|
||||
const newWindow = Math.min(
|
||||
MAX_TIME_WINDOW_MS,
|
||||
Math.max(MIN_TIME_WINDOW_MS, oldWindow * zoomFactor)
|
||||
);
|
||||
|
||||
// Anchor zoom to mouse cursor X position on the time axis
|
||||
const mouseXOnCanvas = e.offsetX - leftPadding;
|
||||
const cursorRatio = Math.max(0, Math.min(1, mouseXOnCanvas / drawWidth));
|
||||
|
||||
// Determine the time under the cursor before zoom
|
||||
let rightEdgeBefore: number;
|
||||
if (isHistoricalMode) {
|
||||
rightEdgeBefore = historicalEndMs - timeOffsetMs;
|
||||
} else {
|
||||
const latestTime =
|
||||
timeOffsetMs > 0 && frozenLatestTime > 0 ? frozenLatestTime : Date.now();
|
||||
rightEdgeBefore = latestTime - timeOffsetMs;
|
||||
}
|
||||
const leftEdgeBefore = rightEdgeBefore - oldWindow;
|
||||
const timeUnderCursor = leftEdgeBefore + cursorRatio * oldWindow;
|
||||
|
||||
// After zoom, the right edge must remain so that timeUnderCursor stays at cursorRatio
|
||||
// newRightEdge = timeUnderCursor + (1 - cursorRatio) * newWindow
|
||||
const newRightEdge = timeUnderCursor + (1 - cursorRatio) * newWindow;
|
||||
|
||||
timeWindowMs = newWindow;
|
||||
|
||||
if (isHistoricalMode) {
|
||||
timeOffsetMs = historicalEndMs - newRightEdge;
|
||||
const maxOffset = (historicalEndMs - historicalStartMs) - newWindow;
|
||||
if (timeOffsetMs < 0) timeOffsetMs = 0;
|
||||
if (maxOffset > 0 && timeOffsetMs > maxOffset) timeOffsetMs = maxOffset;
|
||||
} else {
|
||||
// Freeze the live clock at the current "right edge" reference point
|
||||
if (frozenLatestTime === 0 && timeOffsetMs === 0) {
|
||||
frozenLatestTime = Date.now();
|
||||
}
|
||||
const refTime = frozenLatestTime > 0 ? frozenLatestTime : Date.now();
|
||||
timeOffsetMs = refTime - newRightEdge;
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
return; // Don't run other scroll handlers on CTRL+scroll
|
||||
}
|
||||
|
||||
// Vertical Scroll (Y-Axis Zoom) — no modifier key
|
||||
if (e.deltaY !== 0 && !e.shiftKey && !e.ctrlKey) {
|
||||
// Vertical Scroll (Y-Axis Zoom)
|
||||
if (e.deltaY !== 0 && !e.shiftKey) {
|
||||
const zoomStep = zoomLevel * 0.1; // 10% change
|
||||
if (e.deltaY < 0) {
|
||||
zoomLevel = Math.min(MAX_ZOOM, zoomLevel + zoomStep);
|
||||
|
|
@ -166,7 +84,7 @@
|
|||
// Horizontal Scroll with Shift key or Trackpad horizontal scroll (Pan Time)
|
||||
// Also allow horizontal wheel events (deltaX)
|
||||
if (e.deltaX !== 0 || (e.deltaY !== 0 && e.shiftKey)) {
|
||||
if (!isHistoricalMode && timeOffsetMs === 0) {
|
||||
if (timeOffsetMs === 0) {
|
||||
frozenLatestTime = Date.now();
|
||||
}
|
||||
|
||||
|
|
@ -175,16 +93,10 @@
|
|||
const msPerPixel = timeWindowMs / width;
|
||||
timeOffsetMs += delta * msPerPixel * 2;
|
||||
|
||||
// Prevent panning into the future (live) or past the start (historical)
|
||||
if (isHistoricalMode) {
|
||||
const maxOffset = (historicalEndMs - historicalStartMs) - timeWindowMs;
|
||||
if (timeOffsetMs < 0) timeOffsetMs = 0;
|
||||
if (maxOffset > 0 && timeOffsetMs > maxOffset) timeOffsetMs = maxOffset;
|
||||
} else {
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
// Prevent panning into the future
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,25 +117,16 @@
|
|||
|
||||
// Map pixel drag to time change
|
||||
const msPerPixel = timeWindowMs / width;
|
||||
const isHistoricalMode = historicalStartMs > 0 && historicalEndMs > 0;
|
||||
|
||||
// Moving right (dx > 0) means going back in time (increasing timeOffset)
|
||||
if (!isHistoricalMode && timeOffsetMs === 0 && dx > 0) {
|
||||
if (timeOffsetMs === 0 && dx > 0) {
|
||||
frozenLatestTime = Date.now();
|
||||
}
|
||||
|
||||
// Drag left = positive dx = go earlier in time (increase offset)
|
||||
timeOffsetMs -= dx * msPerPixel;
|
||||
timeOffsetMs += dx * msPerPixel;
|
||||
|
||||
if (isHistoricalMode) {
|
||||
const maxOffset = (historicalEndMs - historicalStartMs) - timeWindowMs;
|
||||
if (timeOffsetMs < 0) timeOffsetMs = 0;
|
||||
if (maxOffset > 0 && timeOffsetMs > maxOffset) timeOffsetMs = maxOffset;
|
||||
} else {
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
|
||||
draw();
|
||||
|
|
@ -258,24 +161,16 @@
|
|||
lastMouseX = e.touches[0].clientX;
|
||||
|
||||
const msPerPixel = timeWindowMs / width;
|
||||
const isHistoricalMode = historicalStartMs > 0 && historicalEndMs > 0;
|
||||
|
||||
if (!isHistoricalMode && timeOffsetMs === 0 && dx > 0) {
|
||||
if (timeOffsetMs === 0 && dx > 0) {
|
||||
frozenLatestTime = Date.now();
|
||||
}
|
||||
|
||||
// Drag right (dx > 0) = scroll into the past (increase offset)
|
||||
timeOffsetMs -= dx * msPerPixel;
|
||||
timeOffsetMs += dx * msPerPixel;
|
||||
|
||||
if (isHistoricalMode) {
|
||||
const maxOffset = (historicalEndMs - historicalStartMs) - timeWindowMs;
|
||||
if (timeOffsetMs < 0) timeOffsetMs = 0;
|
||||
if (maxOffset > 0 && timeOffsetMs > maxOffset) timeOffsetMs = maxOffset;
|
||||
} else {
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
if (timeOffsetMs <= 0) {
|
||||
timeOffsetMs = 0;
|
||||
frozenLatestTime = 0;
|
||||
}
|
||||
|
||||
draw();
|
||||
|
|
@ -336,25 +231,15 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Determine right/left edge: historical mode uses fixed range, live mode uses Date.now()
|
||||
const isHistoricalMode = historicalStartMs > 0 && historicalEndMs > 0;
|
||||
// Use real-time clock for smooth sliding instead of snapping to data points
|
||||
const latestTime =
|
||||
timeOffsetMs > 0 && frozenLatestTime > 0
|
||||
? frozenLatestTime
|
||||
: Date.now();
|
||||
|
||||
let rightEdgeTime: number;
|
||||
let leftEdgeTime: number;
|
||||
|
||||
if (isHistoricalMode) {
|
||||
// In historical mode, the right edge starts at endTime and user scrolls left
|
||||
rightEdgeTime = historicalEndMs - timeOffsetMs;
|
||||
leftEdgeTime = rightEdgeTime - timeWindowMs;
|
||||
} else {
|
||||
// Live mode: smooth sliding clock
|
||||
const latestTime =
|
||||
timeOffsetMs > 0 && frozenLatestTime > 0
|
||||
? frozenLatestTime
|
||||
: Date.now();
|
||||
rightEdgeTime = latestTime - timeOffsetMs;
|
||||
leftEdgeTime = rightEdgeTime - timeWindowMs;
|
||||
}
|
||||
// The right edge of the screen represents (latestTime - timeOffsetMs)
|
||||
const rightEdgeTime = latestTime - timeOffsetMs;
|
||||
const leftEdgeTime = rightEdgeTime - timeWindowMs;
|
||||
|
||||
const isMobile = width < 768;
|
||||
const leftPadding = isMobile ? 40 : 70;
|
||||
|
|
@ -427,7 +312,7 @@
|
|||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Bottom Ruler (Time/Ticks) — Adaptive intervals
|
||||
// Bottom Ruler (Time/Ticks)
|
||||
ctx.strokeStyle = axesColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
|
@ -448,71 +333,40 @@
|
|||
|
||||
const pixelsPerMs = drawWidth / timeWindowMs;
|
||||
|
||||
// ── Adaptive tick intervals ──────────────────────────────────────────
|
||||
// Each entry: [windowThreshold_ms, majorInterval_ms, minorInterval_ms, labelFormat]
|
||||
// labelFormat: 'hms' = HH:MM:SS, 'hm' = HH:MM, 'hmd' = HH:MM + date
|
||||
type LabelFmt = 'hms' | 'hm';
|
||||
const tickTable: [number, number, number, LabelFmt][] = [
|
||||
[ 3_000, 500, 100, 'hms'], // < 3 s → major 0.5s, minor 0.1s
|
||||
[ 8_000, 1_000, 250, 'hms'], // < 8 s → major 1s, minor 0.25s
|
||||
[ 20_000, 2_000, 500, 'hms'], // < 20 s → major 2s, minor 0.5s
|
||||
[ 45_000, 5_000, 1_000, 'hms'], // < 45 s → major 5s, minor 1s
|
||||
[ 90_000, 10_000, 2_000, 'hms'], // < 90 s → major 10s, minor 2s
|
||||
[ 180_000, 20_000, 5_000, 'hms'], // < 3 min → major 20s, minor 5s
|
||||
[ 360_000, 60_000, 15_000, 'hms'], // < 6 min → major 1min, minor 15s
|
||||
[ 900_000, 120_000, 30_000, 'hms'], // < 15 min→ major 2min, minor 30s
|
||||
[ 1_800_000, 300_000, 60_000, 'hm' ], // < 30 min→ major 5min, minor 1min
|
||||
[ 3_600_000, 600_000,120_000, 'hm' ], // < 1 hr → major 10min,minor 2min
|
||||
[ 7_200_000,1_200_000,300_000,'hm' ], // < 2 hr → major 20min,minor 5min
|
||||
[ 18_000_000,3_600_000,900_000,'hm' ], // < 5 hr → major 1hr, minor 15min
|
||||
];
|
||||
// Find the first integer second within our visible window
|
||||
const startSecond = Math.floor(leftEdgeTime / 1000);
|
||||
const endSecond = Math.ceil(rightEdgeTime / 1000);
|
||||
|
||||
let majorMs = 3_600_000; // default ≥ 5 hr
|
||||
let minorMs = 900_000;
|
||||
let labelFmt: LabelFmt = 'hm';
|
||||
for (let sec = startSecond; sec <= endSecond; sec++) {
|
||||
const timeAtTick = sec * 1000;
|
||||
const x = leftPadding + (timeAtTick - leftEdgeTime) * pixelsPerMs;
|
||||
|
||||
for (const [threshold, maj, min, fmt] of tickTable) {
|
||||
if (timeWindowMs < threshold) {
|
||||
majorMs = maj;
|
||||
minorMs = min;
|
||||
labelFmt = fmt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: format a timestamp for the label
|
||||
function formatLabel(ms: number): string {
|
||||
const tzOffsetMs = selectedTimezone * 60 * 60 * 1000;
|
||||
const d = new Date(ms + tzOffsetMs);
|
||||
const hh = String(d.getUTCHours()).padStart(2, '0');
|
||||
const mm = String(d.getUTCMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getUTCSeconds()).padStart(2, '0');
|
||||
return labelFmt === 'hms' ? `${hh}:${mm}:${ss}` : `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
// Draw minor ticks (no label)
|
||||
const minorStart = Math.floor(leftEdgeTime / minorMs) * minorMs;
|
||||
for (let t = minorStart; t <= rightEdgeTime; t += minorMs) {
|
||||
// Skip positions that will be covered by a major tick
|
||||
if (t % majorMs === 0) continue;
|
||||
const x = leftPadding + (t - leftEdgeTime) * pixelsPerMs;
|
||||
if (x >= leftPadding && x <= width) {
|
||||
let tickLen = 6;
|
||||
// Every 10 seconds is a major tick
|
||||
if (sec % 10 === 0) {
|
||||
tickLen = 20;
|
||||
let timeLabel = formatTimeStr(timeAtTick);
|
||||
ctx.fillText(timeLabel, x, drawHeight + 22);
|
||||
} else if (sec % 5 === 0) {
|
||||
tickLen = 12;
|
||||
} else if (sec % 1 === 0) {
|
||||
tickLen = 6;
|
||||
}
|
||||
|
||||
ctx.moveTo(x, drawHeight);
|
||||
ctx.lineTo(x, drawHeight + 8);
|
||||
ctx.lineTo(x, drawHeight + tickLen);
|
||||
|
||||
// Minor ticks every 0.25 sec
|
||||
for (let minor = 1; minor < 4; minor++) {
|
||||
const minorX = x + minor * 0.25 * 1000 * pixelsPerMs;
|
||||
if (minorX > leftPadding && minorX <= width) {
|
||||
ctx.moveTo(minorX, drawHeight);
|
||||
ctx.lineTo(minorX, drawHeight + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw major ticks + labels
|
||||
const majorStart = Math.floor(leftEdgeTime / majorMs) * majorMs;
|
||||
for (let t = majorStart; t <= rightEdgeTime; t += majorMs) {
|
||||
const x = leftPadding + (t - leftEdgeTime) * pixelsPerMs;
|
||||
if (x >= leftPadding && x <= width) {
|
||||
ctx.moveTo(x, drawHeight);
|
||||
ctx.lineTo(x, drawHeight + 18);
|
||||
ctx.fillText(formatLabel(t), x, drawHeight + 20);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw the waveform line
|
||||
|
|
@ -674,11 +528,7 @@
|
|||
window.addEventListener("touchcancel", handleTouchEnd);
|
||||
|
||||
function updateLoop() {
|
||||
const isHistoricalMode = historicalStartMs > 0 && historicalEndMs > 0;
|
||||
// In historical mode, we don't need continuous re-draws based on clock;
|
||||
// draw() is called on user interaction. But we still run the loop
|
||||
// at a low rate so panning/dragging still works smoothly.
|
||||
if (!isDragging || isHistoricalMode) {
|
||||
if (!isDragging) {
|
||||
draw();
|
||||
}
|
||||
animId = requestAnimationFrame(updateLoop);
|
||||
|
|
@ -709,8 +559,12 @@
|
|||
// although updateLoop is already calling draw() repeatedly.
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="ews-waveform-chart-wrapper">
|
||||
<div bind:this={container} class="w-full h-full relative cursor-crosshair">
|
||||
<!-- Slot for putting overlays on top of canvas if needed -->
|
||||
<slot></slot>
|
||||
<canvas bind:this={canvas} class="ews-waveform-chart-canvas"></canvas>
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
class="absolute inset-0 block w-full h-full touch-none"
|
||||
style="z-index: 0;"
|
||||
></canvas>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,111 +15,6 @@ export class WaveformService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw miniseed ArrayBuffer dari HTTP response (tanpa skip WebSocket prefix).
|
||||
* @param expectedStartMs - Hint waktu awal (Unix ms) untuk fallback jika header gagal diparsing.
|
||||
* @param skipTrim - Jika true, buffer tidak di-trim (gunakan untuk data historis besar).
|
||||
*/
|
||||
public processMiniseedRaw(
|
||||
mseedDataBuffer: ArrayBuffer,
|
||||
nominalSampleRateMs: number = 10,
|
||||
expectedStartMs?: number,
|
||||
skipTrim: boolean = false
|
||||
) {
|
||||
if (!this.seis) {
|
||||
console.warn("WaveformService not initialized with seisplotjs, ignoring data.");
|
||||
return;
|
||||
}
|
||||
|
||||
const records = this.seis.miniseed.parseDataRecords(mseedDataBuffer);
|
||||
const newPoints: DataPoint[] = [];
|
||||
|
||||
// Used to chain records sequentially when header timestamps can't be trusted
|
||||
let cumulativeMs: number | null = expectedStartMs ?? null;
|
||||
|
||||
// Reasonable range: 1970 ~ 10 years from now (to reject Date.now()-like fallback noise)
|
||||
const MIN_VALID_TS = 0;
|
||||
const MAX_VALID_TS = Date.now() + 365 * 24 * 60 * 60 * 1000 * 10;
|
||||
|
||||
records.forEach((r: any) => {
|
||||
const samples = r.decompress();
|
||||
if (!samples || samples.length === 0) return;
|
||||
|
||||
const mean = samples.reduce((sum: number, v: number) => sum + v, 0) / samples.length;
|
||||
const demeaned = samples.map((v: number) => v - mean);
|
||||
|
||||
let msPerSample = nominalSampleRateMs;
|
||||
if (r.header && r.header.sampleRate) {
|
||||
msPerSample = 1000 / r.header.sampleRate;
|
||||
}
|
||||
|
||||
// Try to extract start timestamp from header; accept only if it's in valid historical range
|
||||
let startTimeMs: number | null = null;
|
||||
if (r.header && r.header.start) {
|
||||
try {
|
||||
let parsed: number;
|
||||
const h = r.header.start;
|
||||
if (typeof h.toMillis === 'function') {
|
||||
parsed = h.toMillis();
|
||||
} else if (typeof h.toEpochMilli === 'function') {
|
||||
parsed = h.toEpochMilli();
|
||||
} else if (typeof h.toJSDate === 'function') {
|
||||
parsed = h.toJSDate().getTime();
|
||||
} else {
|
||||
parsed = h.valueOf();
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed === 'number' &&
|
||||
!isNaN(parsed) &&
|
||||
parsed > MIN_VALID_TS &&
|
||||
parsed < MAX_VALID_TS
|
||||
) {
|
||||
// Extra sanity: if expectedStartMs provided, parsed should be close to it (within ±3 hours)
|
||||
if (expectedStartMs !== undefined) {
|
||||
const diff = Math.abs(parsed - expectedStartMs);
|
||||
if (diff < 3 * 60 * 60 * 1000) {
|
||||
startTimeMs = parsed;
|
||||
}
|
||||
} else {
|
||||
startTimeMs = parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse miniseed header start time:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: chain from previous record end OR expectedStartMs
|
||||
if (startTimeMs === null) {
|
||||
if (cumulativeMs !== null) {
|
||||
startTimeMs = cumulativeMs;
|
||||
} else {
|
||||
startTimeMs = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < demeaned.length; i++) {
|
||||
newPoints.push({
|
||||
t: startTimeMs! + i * msPerSample,
|
||||
v: demeaned[i],
|
||||
});
|
||||
}
|
||||
|
||||
cumulativeMs = startTimeMs! + demeaned.length * msPerSample;
|
||||
});
|
||||
|
||||
if (skipTrim) {
|
||||
// Add without trimming (historical data can span hours)
|
||||
for (let i = 0; i < newPoints.length; i++) {
|
||||
this.buffer.push(newPoints[i]);
|
||||
}
|
||||
this.buffer.sort((a, b) => a.t - b.t);
|
||||
} else {
|
||||
this.addPoints(newPoints);
|
||||
}
|
||||
}
|
||||
|
||||
public processMiniseed(mseedDataBuffer: ArrayBuffer, nominalSampleRateMs: number = 10) {
|
||||
if (!this.seis) {
|
||||
console.warn("WaveformService not initialized with seisplotjs, ignoring data.");
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
import { writable, get } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import { demoStore } from './demoStore';
|
||||
|
||||
export type SerialStatus = 'connected' | 'disconnected' | 'unsupported' | 'connecting';
|
||||
|
||||
interface SerialState {
|
||||
status: SerialStatus;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function createSerialStore() {
|
||||
const { subscribe, set, update } = writable<SerialState>({
|
||||
status: browser && 'serial' in navigator ? 'disconnected' : 'unsupported',
|
||||
error: null
|
||||
});
|
||||
|
||||
let port: SerialPort | null = null;
|
||||
let writer: WritableStreamDefaultWriter | null = null;
|
||||
|
||||
async function connect() {
|
||||
if (!browser || !('serial' in navigator)) return;
|
||||
|
||||
update(s => ({ ...s, status: 'connecting', error: null }));
|
||||
|
||||
try {
|
||||
port = await navigator.serial.requestPort();
|
||||
await port.open({
|
||||
baudRate: 115200,
|
||||
dataTerminalReady: true,
|
||||
requestToSend: true
|
||||
});
|
||||
writer = port.writable?.getWriter() || null;
|
||||
|
||||
update(s => ({ ...s, status: 'connected' }));
|
||||
|
||||
port.addEventListener('error', () => {
|
||||
console.error('Serial port error:');
|
||||
});
|
||||
|
||||
// Listen for port disconnection
|
||||
port.addEventListener('disconnect', () => {
|
||||
disconnect();
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Serial connection error:', err);
|
||||
update(s => ({ ...s, status: 'disconnected', error: err.message }));
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (writer) {
|
||||
await writer.releaseLock();
|
||||
writer = null;
|
||||
}
|
||||
if (port) {
|
||||
await port.close();
|
||||
port = null;
|
||||
}
|
||||
update(s => ({ ...s, status: 'disconnected' }));
|
||||
}
|
||||
|
||||
async function sendData(data: any) {
|
||||
if (!writer) return;
|
||||
|
||||
try {
|
||||
const encoder = new TextEncoder();
|
||||
const jsonString = JSON.stringify(data) + '\n';
|
||||
console.log(jsonString);
|
||||
await writer.write(encoder.encode(jsonString));
|
||||
} catch (err) {
|
||||
console.error('Failed to send data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-subscribe to demoStore for earthquake alerts
|
||||
if (browser) {
|
||||
demoStore.subscribe(state => {
|
||||
if (state.gempaAlert) {
|
||||
sendData({
|
||||
type: 'GEMPA',
|
||||
id: state.gempaAlert.id,
|
||||
mag: state.gempaAlert.mag,
|
||||
// depth: state.gempaAlert.depth,
|
||||
// mmi: state.gempaAlert.mmi,
|
||||
// place: state.gempaAlert.place
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
await sendData({ type: 'TEST', message: 'HEARTBEAT' });
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
connect,
|
||||
disconnect,
|
||||
sendData,
|
||||
testConnection
|
||||
};
|
||||
}
|
||||
|
||||
export const serialStore = createSerialStore();
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
:root {
|
||||
--orange: #fa0;
|
||||
--red: #e60908;
|
||||
--red: #f23;
|
||||
--glow-rgb: 255, 170, 0;
|
||||
--fill-color: #fa0;
|
||||
--text-color: #fa0;
|
||||
--danger-fill-color: #e60908;
|
||||
--danger-fill-color: #f23;
|
||||
--danger-glow-rgb: 255, 0, 0;
|
||||
--danger-text-color: #e60908;
|
||||
--danger-text-color: #f23;
|
||||
--gutter-size: 8px;
|
||||
--border-width: 3px;
|
||||
color-scheme: dark;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -37,10 +35,4 @@ body {
|
|||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
|
@ -388,7 +388,7 @@
|
|||
--border-color: rgba(var(--glow-rgb));
|
||||
border-radius: var(--gutter-size);
|
||||
border-style: solid;
|
||||
border-width: var(--border-width);
|
||||
border-width: 3px;
|
||||
border-color: var(--border-color);
|
||||
/* box-shadow:
|
||||
inset 0 0 0 1px var(--border-glow-color),
|
||||
|
|
@ -399,7 +399,7 @@
|
|||
/* color: var(--text-color); */
|
||||
/* --border-glow-color: rgba(var(--glow-rgb)); */
|
||||
border-color: unset;
|
||||
border-bottom: var(--border-width) solid var(--border-color);
|
||||
border-bottom: 3px solid var(--border-color);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -408,7 +408,7 @@
|
|||
/* color: var(--text-color); */
|
||||
/* --border-glow-color: rgba(var(--glow-rgb)); */
|
||||
border-color: unset;
|
||||
border-top: var(--border-width) solid var(--border-color);
|
||||
border-top: 3px solid var(--border-color);
|
||||
}
|
||||
|
||||
.bordered-red {
|
||||
|
|
@ -423,7 +423,7 @@
|
|||
--border-color: rgba(var(--danger-glow-rgb));
|
||||
border-radius: var(--gutter-size);
|
||||
border-style: solid;
|
||||
border-width: var(--border-width);
|
||||
border-width: 3px;
|
||||
border-color: var(--border-color);
|
||||
|
||||
}
|
||||
|
|
@ -432,7 +432,7 @@
|
|||
/* color: var(--danger-text-color); */
|
||||
/* --border-glow-color: rgba(var(--danger-glow-rgb)); */
|
||||
border-color: unset;
|
||||
border-bottom: var(--border-width) solid var(--border-color);
|
||||
border-bottom: 3px solid var(--border-color);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -441,9 +441,82 @@
|
|||
/* color: var(--danger-text-color); */
|
||||
/* --border-glow-color: rgba(var(--danger-glow-rgb)); */
|
||||
border-color: unset;
|
||||
border-top: var(--border-width) solid var(--border-color);
|
||||
border-top: 3px solid var(--border-color);
|
||||
}
|
||||
|
||||
.ews-card {
|
||||
background-color: black;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.ews-card-header {
|
||||
padding: 6px;
|
||||
color: var(--orange);
|
||||
position: relative;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
}
|
||||
|
||||
.ews-card-header .ews-card-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ews-card-header button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ews-card-footer {
|
||||
padding: 6px;
|
||||
/* border-top: 3px var(--red) solid; */
|
||||
color: var(--orange);
|
||||
position: relative;
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
}
|
||||
|
||||
.ews-card-footer .ews-card-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* .ews-card-content {
|
||||
padding: unset;
|
||||
} */
|
||||
|
||||
.ews-card-content tbody {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.ews-card-float {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-content {
|
||||
display: block;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ews-card-close-button {
|
||||
font-size: 24px;
|
||||
color: #e60003;
|
||||
padding: 2px 4px;
|
||||
background-color: black !important;
|
||||
right: 10px !important;
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
.parallelogram {
|
||||
height: 30px;
|
||||
|
|
@ -541,19 +614,19 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.-striped {
|
||||
--stripe-color: var(--danger-fill-color);
|
||||
--stripe-size: 15px;
|
||||
.-stripeed {
|
||||
--stripee-color: var(--danger-fill-color);
|
||||
--stripee-size: 15px;
|
||||
--glow-color: rgba(var(--danger-glow-rgb), 0.8);
|
||||
--glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--glow-color) calc(-1 * var(--glow-size)),
|
||||
var(--stripe-color) 0,
|
||||
var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripe-size)),
|
||||
var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)));
|
||||
var(--stripee-color) 0,
|
||||
var(--stripee-color) calc(var(--stripee-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripee-size)),
|
||||
var(--glow-color) calc(2 * var(--stripee-size) - var(--glow-size)));
|
||||
}
|
||||
|
||||
.-blink {
|
||||
|
|
@ -573,6 +646,31 @@
|
|||
padding-top: var(--label-gutter-size);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ews-card-float .ews-card-content {
|
||||
height: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.ews-card-float.open .ews-card-content {
|
||||
height: unset;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* .ews-card-float {
|
||||
margin: auto;
|
||||
right: 0.25rem;
|
||||
left: 0.25rem;
|
||||
} */
|
||||
|
||||
.ews-card-float .ews-card-header {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
.ews-card-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
width: 20px;
|
||||
|
|
@ -805,12 +903,8 @@
|
|||
transparent 50%,
|
||||
rgba(255, 0, 0, 0.1) 50%);
|
||||
background-size: 100% 4px;
|
||||
/* animation: scanline 8s linear infinite; */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scanline.animate::after {
|
||||
animation: scanline 8s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
|
|
@ -978,62 +1072,4 @@ table tr td {
|
|||
|
||||
button {
|
||||
text-box: trim-both cap alphabetic;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ews-title.internal .text.-characters {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.ews-title.badge .text.-characters {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.ews-title.internal .decal {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Custom Range Slider */
|
||||
.custom-range {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 165, 0, 0.2);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--orange);
|
||||
border: 2px solid black;
|
||||
border-radius: 0; /* Square/Diamond look for EWS */
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 10px rgba(var(--glow-rgb), 0.5);
|
||||
transition: all 0.2s ease;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.custom-range::-webkit-slider-thumb:hover {
|
||||
transform: rotate(45deg) scale(1.2);
|
||||
box-shadow: 0 0 15px rgba(var(--glow-rgb), 0.8);
|
||||
}
|
||||
|
||||
.custom-range::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--orange);
|
||||
border: 2px solid black;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 10px rgba(var(--glow-rgb), 0.5);
|
||||
transition: all 0.2s ease;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
.ews-card {
|
||||
--ews-card-color: var(--orange, #fa0);
|
||||
--ews-card-radius: var(--gutter-size, 8px);
|
||||
--ews-card-border-width: 3px;
|
||||
background-color: black;
|
||||
transition: 0.3s;
|
||||
|
||||
border-radius: var(--ews-card-radius);
|
||||
border-style: solid;
|
||||
border-width: var(--ews-card-border-width);
|
||||
border-color: var(--ews-card-color);
|
||||
}
|
||||
|
||||
.ews-card.ews-card-red {
|
||||
--ews-card-color: var(--red, #e60908);
|
||||
}
|
||||
|
||||
.ews-card-header {
|
||||
padding: 6px;
|
||||
color: var(--ews-card-color);
|
||||
position: relative;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
border-bottom: var(--ews-card-border-width) solid var(--ews-card-color);
|
||||
}
|
||||
|
||||
.ews-card-header .ews-card-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ews-card-header button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ews-card-footer {
|
||||
padding: 6px;
|
||||
color: var(--ews-card-color);
|
||||
position: relative;
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
border-top: var(--ews-card-border-width) solid var(--ews-card-color);
|
||||
}
|
||||
|
||||
.ews-card-footer .ews-card-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ews-card-content {
|
||||
color: var(--ews-card-color);
|
||||
}
|
||||
|
||||
|
||||
.ews-card-content::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
background-color: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.ews-card-content::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.ews-card-content::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
.ews-card-content tbody {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.ews-card-float {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-content {
|
||||
display: block;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ews-card-close-button {
|
||||
font-size: 24px;
|
||||
color: #e60003;
|
||||
padding: 2px 4px;
|
||||
background-color: black !important;
|
||||
right: 10px !important;
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.ews-card {
|
||||
--ews-card-border-width: 1px;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-content {
|
||||
display: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.ews-card-float.open .ews-card-content {
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.ews-card-float {
|
||||
margin: auto;
|
||||
right: 0.25rem;
|
||||
left: 0.25rem;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-header {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
.ews-card-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
/* =============================================
|
||||
HEXAGONAL GRID STYLES
|
||||
============================================= */
|
||||
|
||||
/* --- Shared hex clip-path (flat-top orientation) --- */
|
||||
.ews-hex-clip {
|
||||
clip-path: polygon(24.96% 100%,
|
||||
0% 50%,
|
||||
24.96% 0%,
|
||||
74.87% 0%,
|
||||
99.84% 50%,
|
||||
74.87% 100%);
|
||||
}
|
||||
|
||||
/* --- Shared hex clip-path (pointy-top / rotated 90°) --- */
|
||||
.ews-hex-clip-pointy {
|
||||
clip-path: polygon(0% 25.13%,
|
||||
50% 0%,
|
||||
100% 25.13%,
|
||||
100% 74.87%,
|
||||
50% 100%,
|
||||
0% 74.87%);
|
||||
}
|
||||
|
||||
/* ---- 1. Basic Flat Hex Grid ---- */
|
||||
.ews-hex-grid-flat {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
aspect-ratio: 584 / 507;
|
||||
clip-path: polygon(24.96% 100%, 0% 50%, 24.96% 0%, 74.87% 0%, 99.84% 50%, 74.87% 100%);
|
||||
background-color: rgba(255, 170, 0, 0.08);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.25s ease, transform 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
clip-path: polygon(24.96% 100%, 0% 50%, 24.96% 0%, 74.87% 0%, 99.84% 50%, 74.87% 100%);
|
||||
background-color: rgba(255, 170, 0, 0.04);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat:hover {
|
||||
background-color: rgba(255, 170, 0, 0.18);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat.ews-hex-danger {
|
||||
background-color: rgba(255, 34, 51, 0.15);
|
||||
box-shadow: 0 0 12px 2px rgba(255, 34, 51, 0.3);
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat.ews-hex-danger::before {
|
||||
background-color: rgba(255, 34, 51, 0.06);
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat.ews-hex-warn {
|
||||
background-color: rgba(255, 170, 0, 0.15);
|
||||
box-shadow: 0 0 10px 2px rgba(255, 170, 0, 0.25);
|
||||
}
|
||||
|
||||
.ews-hex-cell-flat.ews-hex-safe {
|
||||
background-color: rgba(0, 200, 80, 0.12);
|
||||
box-shadow: 0 0 8px 1px rgba(0, 200, 80, 0.2);
|
||||
}
|
||||
|
||||
.ews-hex-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
color: var(--orange);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- 2. Honeycomb Offset Grid ---- */
|
||||
.ews-hex-honeycomb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.ews-hex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ews-hex-row-offset {
|
||||
margin-left: calc(72px / 2 + 2px);
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
.ews-hex-hive {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 83px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.ews-hex-hive.bg-hex {
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
display: unset;
|
||||
transition: unset;
|
||||
background-color: transparent;
|
||||
/* height: unset; */
|
||||
|
||||
background-image: url('data:image/svg+xml,<svg width="115" height="133" viewBox="0 0 115 133" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z" fill="%23E60003"/><path d="M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z" fill="black"/><path d="M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z" fill="%23E60003"/></svg>');
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.bg-hex.yellow {
|
||||
background-image: url('data:image/svg+xml,<svg width="115" height="133" viewBox="0 0 115 133" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z" fill="%23fa0"/><path d="M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z" fill="black"/><path d="M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z" fill="%23fa0"/></svg>');
|
||||
}
|
||||
|
||||
.ews-hex-hive.bg-hex-flat {
|
||||
clip-path: polygon(24.96% 100%, 0% 50%, 24.96% 0%, 74.87% 0%, 99.84% 50%, 74.87% 100%);
|
||||
display: unset;
|
||||
transition: unset;
|
||||
background-color: transparent;
|
||||
|
||||
background-image: url('data:image/svg+xml,<svg width="584" height="507" viewBox="0 0 584 507" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z" fill="%23E60003"/><path d="M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z" fill="black"/><path d="M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z" fill="%23E60003"/></svg>');
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.ews-hex-hive.flat {
|
||||
clip-path: polygon(24.96% 100%, 0% 50%, 24.96% 0%, 74.87% 0%, 99.84% 50%, 74.87% 100%);
|
||||
display: unset;
|
||||
transition: unset;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.bg-hex-flat.yellow {
|
||||
background-image: url('data:image/svg+xml,<svg width="584" height="507" viewBox="0 0 584 507" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z" fill="%23fa0"/><path d="M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z" fill="black"/><path d="M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z" fill="%23fa0"/></svg>');
|
||||
}
|
||||
|
||||
.ews-hex-hive.ews-hex-danger {
|
||||
background-color: rgba(255, 34, 51, 0.18);
|
||||
filter: drop-shadow(0 0 8px rgba(255, 34, 51, 0.5));
|
||||
}
|
||||
|
||||
.ews-hex-hive.ews-hex-warn {
|
||||
background-color: rgba(255, 170, 0, 0.18);
|
||||
filter: drop-shadow(0 0 6px rgba(255, 170, 0, 0.4));
|
||||
}
|
||||
|
||||
.ews-hex-hive-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
text-align: center;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
/* ---- 3. Animated Status Hex Cells ---- */
|
||||
.ews-hex-status-cell {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 104px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
background-color: rgba(255, 170, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.ews-hex-status-cell::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
background: transparent;
|
||||
border: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ews-hex-status-cell:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.ews-hex-status-cell.ews-hex-danger {
|
||||
background-color: rgba(255, 34, 51, 0.12);
|
||||
filter: drop-shadow(0 0 10px rgba(255, 34, 51, 0.45));
|
||||
}
|
||||
|
||||
.ews-hex-status-cell.ews-hex-warn {
|
||||
background-color: rgba(255, 170, 0, 0.12);
|
||||
filter: drop-shadow(0 0 8px rgba(255, 170, 0, 0.4));
|
||||
}
|
||||
|
||||
.ews-hex-status-cell.ews-hex-caution {
|
||||
background-color: rgba(255, 255, 0, 0.08);
|
||||
filter: drop-shadow(0 0 6px rgba(255, 255, 0, 0.25));
|
||||
}
|
||||
|
||||
.ews-hex-status-cell.ews-hex-safe {
|
||||
background-color: rgba(0, 200, 80, 0.08);
|
||||
filter: drop-shadow(0 0 6px rgba(0, 200, 80, 0.2));
|
||||
}
|
||||
|
||||
.ews-hex-status-cell.ews-hex-pulse {
|
||||
animation: hexPulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes hexPulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 34, 51, 0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 22px rgba(255, 34, 51, 0.85));
|
||||
}
|
||||
}
|
||||
|
||||
.ews-hex-status-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 2px;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
/* ---- 4. Hex with Strip Decoration ---- */
|
||||
.ews-hex-stripe-cell {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 127px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(255, 170, 0, 0.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.ews-hex-stripe-cell:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.ews-hex-stripe-cell.ews-hex-danger {
|
||||
background-color: rgba(255, 34, 51, 0.08);
|
||||
}
|
||||
|
||||
.ews-hex-stripe-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ews-hex-stripe-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 1px;
|
||||
color: var(--orange);
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
padding: 6px 10px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
width: 88%;
|
||||
height: 88%;
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
.ews-hex-shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 0.866 / 1;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='115' height='133' viewBox='0 0 115 133' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z' fill='%23E60003'/%3E%3Cpath d='M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z' fill='black'/%3E%3Cpath d='M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z' fill='%23E60003'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
--polygon-shape: polygon(0% 25.13%,
|
||||
/* top-left point */
|
||||
50% 0%,
|
||||
/* top center point */
|
||||
100% 25.13%,
|
||||
/* top-right point */
|
||||
100% 74.87%,
|
||||
/* bottom-right point */
|
||||
50% 100%,
|
||||
/* bottom center point */
|
||||
0% 74.87%
|
||||
/* bottom-left point */
|
||||
);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.ews-hex-shape.clip-content {
|
||||
overflow: hidden;
|
||||
clip-path: var(--polygon-shape);
|
||||
}
|
||||
|
||||
.ews-hex-shape.clip-content .inner-content {
|
||||
--ews-hex-padding: 10px;
|
||||
width: calc(100% - var(--ews-hex-padding));
|
||||
height: calc(100% - var(--ews-hex-padding));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
clip-path: var(--polygon-shape);
|
||||
}
|
||||
|
||||
.ews-hex-shape.flat-top {
|
||||
aspect-ratio: 1.1547 / 1;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='584' height='507' viewBox='0 0 584 507' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z' fill='%23E60003'/%3E%3Cpath d='M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z' fill='black'/%3E%3Cpath d='M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z' fill='%23E60003'/%3E%3C/svg%3E%0A");
|
||||
--polygon-shape: polygon(24.96% 100%,
|
||||
/* 145.77/584, 507/507 */
|
||||
0% 50%,
|
||||
/* 0/584, 253.5/507 */
|
||||
24.96% 0%,
|
||||
/* 145.77/584, 0/507 */
|
||||
74.87% 0%,
|
||||
/* 437.28/584, 0/507 */
|
||||
99.84% 50%,
|
||||
/* 583.05/584, 253.5/507 */
|
||||
74.87% 100%
|
||||
/* 437.28/584, 507/507 */
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
.ews-hex-shape.orange {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='115' height='133' viewBox='0 0 115 133' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z' fill='%23fa0'/%3E%3Cpath d='M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z' fill='black'/%3E%3Cpath d='M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z' fill='%23fa0'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.ews-hex-shape.orange.flat-top {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='584' height='507' viewBox='0 0 584 507' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z' fill='%23fa0'/%3E%3Cpath d='M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z' fill='black'/%3E%3Cpath d='M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z' fill='%23fa0'/%3E%3C/svg%3E%0A");
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
.ews-rib-layout {
|
||||
display: inline-flex;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ews-rib-layout__branch {
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.ews-rib-layout__branch {
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ews-rib-layout__spine {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.25rem;
|
||||
transform: translateX(-50%);
|
||||
z-index: 0;
|
||||
background-color: var(--orange);
|
||||
}
|
||||
|
||||
.ews-rib-layout__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node--left {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0;
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node--right {
|
||||
justify-content: flex-start;
|
||||
padding-left: 0;
|
||||
grid-column-start: 2;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-wrapper {
|
||||
width: 6rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-wrapper--left {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-wrapper--right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-line {
|
||||
height: 2px;
|
||||
width: 6rem;
|
||||
z-index: 0;
|
||||
background-color: var(--orange);
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-text {
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
z-index: 10;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-text--left {
|
||||
left: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-text--right {
|
||||
right: 0.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ews-rib-node {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M500 0H37.5414L-1.63437e-05 100H462.459L500 0Z" fill="%2300FF80"/><path d="M168.5 0H37.1618L30.5 19H161.838L168.5 0Z" fill="%23FC8416"/></svg>');
|
||||
background-image: var(--bg-url);
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
width: 6rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.parent-node {
|
||||
transform: translateX(0px);
|
||||
rotate: -21deg !important;
|
||||
transition: all 0.2s ease-in-out;
|
||||
margin-left: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.parent-node.flip {
|
||||
rotate: 21deg !important;
|
||||
}
|
||||
|
||||
.node:hover .parent-node {
|
||||
cursor: pointer;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ews-rib-node.danger {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M500 0H37.5414L-1.63437e-05 100H462.459L500 0Z" fill="%23E60003"/><path d="M168.5 0H37.1618L30.5 19H161.838L168.5 0Z" fill="%23FC8416"/></svg>');
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ews-rib-node.flip {
|
||||
margin-left: -0.5rem;
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H462.459L500 100H37.5415L0 0Z" fill="%2300FF80"/><path d="M0 0H131.338L138 19H6.66178L0 0Z" fill="%23FC8416"/></svg>');
|
||||
}
|
||||
|
||||
.node-flip:hover .parent-node {
|
||||
cursor: pointer;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.ews-rib-node.flip.danger {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H462.459L500 100H37.5415L0 0Z" fill="%23E60003"/><path d="M0 0H131.338L138 19H6.66178L0 0Z" fill="%23FC8416"/></svg>');
|
||||
}
|
||||
|
||||
|
||||
.ews-rib-node.slide-fade-in {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
animation: slideFadeIn 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
|
||||
.ews-rib-node-flip.slide-fade-in {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
animation: slideFadeInFlip 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
|
||||
/* Slide and fade in animation */
|
||||
.slide-fade-in {
|
||||
animation: slideFadeIn 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-fade-in-flip {
|
||||
animation: slideFadeInFlip 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideFadeInFlip {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.line-node {
|
||||
width: 0%;
|
||||
animation: slideWidth 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
|
||||
@keyframes slideWidth {
|
||||
0% {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: calc(var(--spacing) * 24);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.line-central {
|
||||
height: 0%;
|
||||
animation: slideHeight 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideHeight {
|
||||
0% {
|
||||
height: 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/* Strip Bar Styles */
|
||||
.ews-stripe-wrapper {
|
||||
width: max(200vw, 2000px);
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ews-stripe-wrapper.vertical {
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.ews-stripe-bar {
|
||||
width: max(200vw, 2000px);
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0px !important;
|
||||
margin-left: 0px !important;
|
||||
/* margin-bottom: -5px; */
|
||||
--ews-ews-stripe-color: var(--orange, #fa0);
|
||||
--ews-ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 94, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-ews-stripe-color) 0,
|
||||
var(--ews-ews-stripe-color) calc(var(--ews-ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-ews-stripe-size) - var(--ews-glow-size)));
|
||||
/* background-size: var(--background-width) var(--background-height); */
|
||||
background-size: 47px 47px;
|
||||
}
|
||||
|
||||
.ews-stripe-bar.red {
|
||||
--ews-ews-stripe-color: var(--red, #e60908);
|
||||
--ews-ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 17, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
}
|
||||
|
||||
.ews-stripe-bar.vertical {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.ews-stripe-bar-red {
|
||||
width: max(200vw, 2000px);
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
/* margin-bottom: -5px; */
|
||||
--ews-ews-stripe-color: var(--red, #e60908);
|
||||
--ews-ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 17, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-ews-stripe-color) 0,
|
||||
var(--ews-ews-stripe-color) calc(var(--ews-ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-ews-stripe-size) - var(--ews-glow-size)));
|
||||
}
|
||||
|
||||
.ews-stripe-bar-vertical {
|
||||
height: max(2000px, 200vh);
|
||||
transform: translate3d(0, 0, 0);
|
||||
--ews-ews-stripe-color: var(--orange, #fa0);
|
||||
--ews-ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 94, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-ews-stripe-color) 0,
|
||||
var(--ews-ews-stripe-color) calc(var(--ews-ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-ews-stripe-size) - var(--ews-glow-size)));
|
||||
}
|
||||
|
||||
.ews-stripe-bar-red-vertical {
|
||||
height: max(2000px, 200vh);
|
||||
transform: translate3d(0, 0, 0);
|
||||
--ews-ews-stripe-color: var(--red, #e60908);
|
||||
--ews-ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 17, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-ews-stripe-color) 0,
|
||||
var(--ews-ews-stripe-color) calc(var(--ews-ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-ews-stripe-size) - var(--ews-glow-size)));
|
||||
}
|
||||
|
||||
.ews-stripe-wrapper-vertical {
|
||||
height: max(200vh, 2000px);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ews-stripe {
|
||||
background-color: black;
|
||||
width: 100vw;
|
||||
border-top: 1px solid var(--red, #e60908);
|
||||
border-bottom: 1px solid var(--red, #e60908);
|
||||
position: fixed;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
.ews-waveform-chart-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.ews-waveform-chart-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
|
@ -26,25 +26,25 @@
|
|||
margin-right: 0px !important;
|
||||
margin-left: 0px !important;
|
||||
/* margin-bottom: -5px; */
|
||||
--stripe-color: var(--orange);
|
||||
--stripe-size: 15px;
|
||||
--stripee-color: var(--orange);
|
||||
--stripee-size: 15px;
|
||||
--glow-color: rgba(255, 94, 0, 0.8);
|
||||
--glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--glow-color) calc(-1 * var(--glow-size)),
|
||||
var(--stripe-color) 0,
|
||||
var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripe-size)),
|
||||
var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)));
|
||||
var(--stripee-color) 0,
|
||||
var(--stripee-color) calc(var(--stripee-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripee-size)),
|
||||
var(--glow-color) calc(2 * var(--stripee-size) - var(--glow-size)));
|
||||
/* background-size: var(--background-width) var(--background-height); */
|
||||
background-size: 47px 47px;
|
||||
}
|
||||
|
||||
.stripe-bar.red {
|
||||
--stripe-color: var(--red);
|
||||
--stripe-size: 15px;
|
||||
--stripee-color: var(--red);
|
||||
--stripee-size: 15px;
|
||||
--glow-color: rgba(255, 17, 0, 0.8);
|
||||
--glow-size: 3px;
|
||||
}
|
||||
|
|
@ -63,52 +63,52 @@
|
|||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
/* margin-bottom: -5px; */
|
||||
--stripe-color: var(--red);
|
||||
--stripe-size: 15px;
|
||||
--stripee-color: var(--red);
|
||||
--stripee-size: 15px;
|
||||
--glow-color: rgba(255, 17, 0, 0.8);
|
||||
--glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--glow-color) calc(-1 * var(--glow-size)),
|
||||
var(--stripe-color) 0,
|
||||
var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripe-size)),
|
||||
var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)));
|
||||
var(--stripee-color) 0,
|
||||
var(--stripee-color) calc(var(--stripee-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripee-size)),
|
||||
var(--glow-color) calc(2 * var(--stripee-size) - var(--glow-size)));
|
||||
}
|
||||
|
||||
.stripe-bar-vertical {
|
||||
height: max(2000px, 200vh);
|
||||
transform: translate3d(0, 0, 0);
|
||||
--stripe-color: var(--orange);
|
||||
--stripe-size: 15px;
|
||||
--stripee-color: var(--orange);
|
||||
--stripee-size: 15px;
|
||||
--glow-color: rgba(255, 94, 0, 0.8);
|
||||
--glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
var(--glow-color) calc(-1 * var(--glow-size)),
|
||||
var(--stripe-color) 0,
|
||||
var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripe-size)),
|
||||
var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)));
|
||||
var(--stripee-color) 0,
|
||||
var(--stripee-color) calc(var(--stripee-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripee-size)),
|
||||
var(--glow-color) calc(2 * var(--stripee-size) - var(--glow-size)));
|
||||
}
|
||||
|
||||
.stripe-bar-red-vertical {
|
||||
height: max(2000px, 200vh);
|
||||
transform: translate3d(0, 0, 0);
|
||||
--stripe-color: var(--red);
|
||||
--stripe-size: 15px;
|
||||
--stripee-color: var(--red);
|
||||
--stripee-size: 15px;
|
||||
--glow-color: rgba(255, 17, 0, 0.8);
|
||||
--glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
var(--glow-color) calc(-1 * var(--glow-size)),
|
||||
var(--stripe-color) 0,
|
||||
var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripe-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripe-size)),
|
||||
var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)));
|
||||
var(--stripee-color) 0,
|
||||
var(--stripee-color) calc(var(--stripee-size) - var(--glow-size) / 2),
|
||||
var(--glow-color) calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(var(--stripee-size) + var(--glow-size) / 2),
|
||||
transparent calc(2 * var(--stripee-size)),
|
||||
var(--glow-color) calc(2 * var(--stripee-size) - var(--glow-size)));
|
||||
}
|
||||
|
||||
.stripe-wrapper-vertical {
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@
|
|||
/* Textarea variant */
|
||||
.ews-textarea {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color-scheme: dark;
|
||||
color: var(--text-color);
|
||||
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
|
|
@ -215,7 +214,6 @@
|
|||
/* Select variant */
|
||||
.ews-select {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color-scheme: dark;
|
||||
color: var(--text-color);
|
||||
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
export interface Snapshot {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
time: string;
|
||||
place: string;
|
||||
mag: number | string;
|
||||
imageBase64: string;
|
||||
}
|
||||
|
||||
const DB_NAME = "ews-snapshots-db";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "snapshots";
|
||||
|
||||
export function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
||||
}
|
||||
};
|
||||
request.onsuccess = (event) => {
|
||||
resolve((event.target as IDBOpenDBRequest).result);
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
reject((event.target as IDBOpenDBRequest).error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveSnapshot(snapshot: Snapshot): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], "readwrite");
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.put(snapshot);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSnapshots(): Promise<Snapshot[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], "readonly");
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => {
|
||||
const results = request.result as Snapshot[];
|
||||
results.sort((a, b) => b.timestamp - a.timestamp); // newest first
|
||||
resolve(results);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSnapshot(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], "readwrite");
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
|
@ -10,12 +10,11 @@ export function createGempaPopupHTML(data: {
|
|||
time: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
place?: string;
|
||||
}): string {
|
||||
return `
|
||||
<div class="ews-card ews-card-red min-h-48 min-w-48 whitespace-pre-wrap" data-id="${data.id}">
|
||||
<div class="ews-card bordered-red min-h-48 min-w-48 whitespace-pre-wrap" data-id="${data.id}">
|
||||
<div class="ews-card-header bordered-red-bottom overflow-hidden">
|
||||
<div class="ews-stripe-wrapper " style="height: 30px;"><div class="ews-stripe-bar red loop-stripe reverse anim-duration-20"></div> <div class="ews-stripe-bar red loop-stripe reverse anim-duration-20"></div></div>
|
||||
<div class="stripe-wrapper"><div class="stripe-bar-red loop-stripe-reverse anim-duration-20"></div><div class="stripe-bar-red loop-stripe-reverse anim-duration-20"></div></div>
|
||||
<div class="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center">
|
||||
<p class="p-1 bg-black font-bold text-xs ews-title">EARTHQUAKE</p>
|
||||
</div>
|
||||
|
|
@ -24,7 +23,6 @@ export function createGempaPopupHTML(data: {
|
|||
<table class="w-full">
|
||||
<tbody>
|
||||
<tr><td class="flex">Magnitudo</td><td class="text-right break-words pl-2">${Number(data.mag).toFixed(1)}</td></tr>
|
||||
<tr><td class="flex">Place</td><td class="text-right break-words pl-2">${data.place}</td></tr>
|
||||
<tr><td class="flex">Kedalaman</td><td class="text-right break-words pl-2">${data.depth}</td></tr>
|
||||
<tr><td class="flex">Waktu</td><td class="text-right break-words pl-2">${data.time}</td></tr>
|
||||
<tr><td class="flex">Lokasi (Lat,Lng)</td><td class="text-right break-words pl-2">${data.lat} , ${data.lng}</td></tr>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="backgroundline absolute inset-0 pointer-events-none z-10"></div>
|
||||
<div class="no-snapshot scanline fixed inset-0 pointer-events-none z-10"></div>
|
||||
<div class="scanline fixed inset-0 pointer-events-none z-10"></div>
|
||||
{@render children()}
|
||||
|
||||
{#if $demoStore.gempaAlert}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
159
src/routes/bad-apple/+page.svelte
Normal file
159
src/routes/bad-apple/+page.svelte
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import WaveformChart from "$lib/components/WaveformChart.svelte";
|
||||
|
||||
let frames: any[] = [];
|
||||
let currentFrameIndex = 0;
|
||||
let isPlaying = false;
|
||||
let isLoading = true;
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
// Props for WaveformChart
|
||||
let waveformChart: any;
|
||||
let psychoPoints: { nx: number; ny: number }[] = [];
|
||||
|
||||
// Target FPS used during extraction
|
||||
const TARGET_FPS = 15;
|
||||
const MS_PER_FRAME = 1000 / TARGET_FPS;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch("/bad_apple_frames_v4.json");
|
||||
if (response.ok) {
|
||||
frames = await response.json();
|
||||
isLoading = false;
|
||||
|
||||
// Set initial frame
|
||||
if (frames.length > 0) {
|
||||
psychoPoints = frames[0];
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch frames:", response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load Bad Apple frames:", error);
|
||||
}
|
||||
});
|
||||
|
||||
function togglePlay() {
|
||||
if (isPlaying) {
|
||||
isPlaying = false;
|
||||
if (interval) clearInterval(interval);
|
||||
} else {
|
||||
if (frames.length === 0) return;
|
||||
isPlaying = true;
|
||||
|
||||
// Restart if at end
|
||||
if (currentFrameIndex >= frames.length - 1) {
|
||||
currentFrameIndex = 0;
|
||||
}
|
||||
|
||||
interval = setInterval(() => {
|
||||
if (currentFrameIndex < frames.length) {
|
||||
psychoPoints = frames[currentFrameIndex];
|
||||
currentFrameIndex++;
|
||||
} else {
|
||||
isPlaying = false;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, MS_PER_FRAME);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Bad Apple!! - Oscilloscope Mode</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="w-full min-h-screen bg-black flex flex-col items-center justify-center p-4 lg:p-8 font-mono relative"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-6 w-full">
|
||||
<div class="w-full max-w-7xl flex flex-col bordered p-1 grow gap-3">
|
||||
<div
|
||||
class="relative w-full h-[75vh] border-b-4 border-l-4 bg-black overflow-hidden flex flex-col items-center"
|
||||
style="border-bottom-color: #fa0; border-left-color: #fa0;"
|
||||
>
|
||||
<!-- Top Left -->
|
||||
<div
|
||||
class="absolute right-2 lg:right-0 lg:top-6 left-4 lg:left-24 pointer-events-none z-5 max-w-100 flex flex-col justify-center items-end lg:items-start gap-1"
|
||||
>
|
||||
<div
|
||||
class="rounded-sm bordered text-3xl bg-black/60 shadow-lg h-10 text-center flex justify-center items-center px-1"
|
||||
>
|
||||
<div class="font-bold md:text-3xl uppercase ews-title">
|
||||
PSYCHOGRAPHIC DISPLAY
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="font-bold mt-1 tracking-widest text-sm md:text-xl ews-title"
|
||||
>
|
||||
Phase 4 Link
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Right -->
|
||||
<div
|
||||
class="absolute bottom-16 lg:bottom-auto top-auto lg:top-18 md:top-4 right-2 lg:right-16 pointer-events-none flex flex-col items-end z-5"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg p-1 inline-block bg-black/60 shadow-lg"
|
||||
>
|
||||
<div class="bordered px-3 py-1 flex flex-col text-3xl">
|
||||
<div
|
||||
class="font-bold text-[10px] md:text-sm tracking-widest leading-none mb-1 ews-title"
|
||||
>
|
||||
STATION:
|
||||
</div>
|
||||
<div
|
||||
class="font-bold text-2xl md:text-4xl tracking-widest leading-none text-right ews-title"
|
||||
>
|
||||
BAD APPLE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WaveformChart
|
||||
bind:this={waveformChart}
|
||||
waveformData={[]}
|
||||
timeWindowMs={20000}
|
||||
selectedTimezone={0}
|
||||
isDemoPsychoMode={true}
|
||||
{psychoPoints}
|
||||
backgroundColor="#000000"
|
||||
gridColor="#33cc55"
|
||||
axesColor="#fa0"
|
||||
waveformColor="#ff9900"
|
||||
psychoWaveformColor="#ff9900"
|
||||
emptyTextColor="#fa0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-2">
|
||||
<button
|
||||
class="px-10 py-3 bg-white text-black font-bold uppercase tracking-widest text-lg rounded hover:bg-gray-200 transition-all shadow-[0_0_15px_rgba(255,255,255,0.3)] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={togglePlay}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isPlaying ? "PAUSE" : "PLAY VIDEO"}
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-3 bg-transparent border-2 border-white/50 text-white font-bold uppercase tracking-widest text-lg rounded hover:bg-white hover:text-black transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={() => {
|
||||
currentFrameIndex = 0;
|
||||
if (frames.length > 0) psychoPoints = frames[0];
|
||||
if (isPlaying) togglePlay();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
RESTART
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -10,8 +10,6 @@
|
|||
import { PUBLIC_WEBSOCKET_URL } from "$env/static/public";
|
||||
import { WaveformService } from "$lib/services/WaveformService";
|
||||
import HexGrid from "$lib/components/HexGrid.svelte";
|
||||
import HexShape from "$lib/components/HexShape.svelte";
|
||||
import StripeBar from "$lib/components/StripeBar.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
let waveformChart: any;
|
||||
|
|
@ -135,328 +133,6 @@
|
|||
let isSettingsOpen = false;
|
||||
let selectedTimezone = 0; // 0: UTC, 7: WIB, 8: WITA, 9: WIT
|
||||
|
||||
// Historical Data Mode
|
||||
let startTimeInput: string = ""; // datetime-local format: "2026-04-02T00:00"
|
||||
let endTimeInput: string = "";
|
||||
let isHistoricalMode: boolean = false;
|
||||
let isLoadingHistory: boolean = false;
|
||||
let historyError: string = "";
|
||||
let historicalStartMs: number = 0;
|
||||
let historicalEndMs: number = 0;
|
||||
|
||||
// Local MiniSEED file upload
|
||||
let miniseedFileInput: HTMLInputElement;
|
||||
let isLoadingMiniseed: boolean = false;
|
||||
let miniseedError: string = "";
|
||||
let miniseedFileName: string = "";
|
||||
|
||||
// Web Serial ESP32
|
||||
let isEspConnected = false;
|
||||
let espSerialPort: any;
|
||||
|
||||
class LineBreakTransformer {
|
||||
chunks: string;
|
||||
constructor() {
|
||||
this.chunks = "";
|
||||
}
|
||||
|
||||
transform(chunk: string, controller: any) {
|
||||
this.chunks += chunk;
|
||||
const lines = this.chunks.split("\n");
|
||||
this.chunks = lines.pop() || "";
|
||||
lines.forEach((line) => controller.enqueue(line.trim()));
|
||||
}
|
||||
|
||||
flush(controller: any) {
|
||||
if (this.chunks) {
|
||||
controller.enqueue(this.chunks.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
let targetP1 = -1;
|
||||
let targetP2 = -1;
|
||||
let targetP3 = -1;
|
||||
|
||||
let currentP1 = -1;
|
||||
let currentP2 = -1;
|
||||
let currentP3 = -1;
|
||||
|
||||
let lerpAnimFrame: number;
|
||||
|
||||
const LERP_SPEED = 0.1; // Tingkat kehalusan, semakin kecil makin halus
|
||||
|
||||
function lerpLoop() {
|
||||
if (!isEspConnected) return;
|
||||
|
||||
if (targetP1 !== -1 && currentP1 !== -1) {
|
||||
currentP1 += (targetP1 - currentP1) * LERP_SPEED;
|
||||
zoomLevel = MIN_ZOOM + (currentP1 / 4095) * (MAX_ZOOM - MIN_ZOOM);
|
||||
}
|
||||
|
||||
if (targetP2 !== -1 && currentP2 !== -1) {
|
||||
currentP2 += (targetP2 - currentP2) * LERP_SPEED;
|
||||
timeWindowMs = 2000 + (currentP2 / 4095) * (300000 - 2000);
|
||||
}
|
||||
|
||||
if (targetP3 !== -1 && currentP3 !== -1) {
|
||||
currentP3 += (targetP3 - currentP3) * LERP_SPEED;
|
||||
|
||||
// Snap ke 0 ketika target sudah pasti 0 agar resumeLive memicu
|
||||
if (targetP3 === 0 && currentP3 < 1) {
|
||||
currentP3 = 0;
|
||||
}
|
||||
|
||||
if (currentP3 === 0) {
|
||||
if (timeOffsetMs !== 0) {
|
||||
timeOffsetMs = 0;
|
||||
if (waveformChart) waveformChart.resumeLive();
|
||||
}
|
||||
} else {
|
||||
timeOffsetMs = (currentP3 / 4095) * 300000;
|
||||
}
|
||||
}
|
||||
|
||||
lerpAnimFrame = requestAnimationFrame(lerpLoop);
|
||||
}
|
||||
|
||||
async function connectEsp32() {
|
||||
if (!browser) return;
|
||||
try {
|
||||
if ("serial" in navigator) {
|
||||
if (!espSerialPort) {
|
||||
espSerialPort = await (
|
||||
navigator as any
|
||||
).serial.requestPort();
|
||||
await espSerialPort.open({ baudRate: 115200 });
|
||||
isEspConnected = true;
|
||||
logMessages += `${new Date().toLocaleString()} : ESP32 Serial Connected\n`;
|
||||
targetP1 = -1;
|
||||
targetP2 = -1;
|
||||
targetP3 = -1;
|
||||
currentP1 = -1;
|
||||
currentP2 = -1;
|
||||
currentP3 = -1;
|
||||
if (lerpAnimFrame) cancelAnimationFrame(lerpAnimFrame);
|
||||
lerpAnimFrame = requestAnimationFrame(lerpLoop);
|
||||
readEsp32Serial();
|
||||
} else {
|
||||
// Close the port if already connected
|
||||
await espSerialPort.close();
|
||||
espSerialPort = null;
|
||||
isEspConnected = false;
|
||||
if (lerpAnimFrame) cancelAnimationFrame(lerpAnimFrame);
|
||||
logMessages += `${new Date().toLocaleString()} : ESP32 Serial Disconnected\n`;
|
||||
}
|
||||
} else {
|
||||
console.error("Web Serial API not supported in this browser.");
|
||||
logMessages += `${new Date().toLocaleString()} : Web Serial API not supported.\n`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error connecting to ESP32:", err);
|
||||
logMessages += `${new Date().toLocaleString()} : ESP32 Connection Error.\n`;
|
||||
}
|
||||
}
|
||||
|
||||
async function readEsp32Serial() {
|
||||
if (!espSerialPort) return;
|
||||
const textDecoder = new TextDecoderStream();
|
||||
const readableStreamClosed = espSerialPort.readable.pipeTo(
|
||||
textDecoder.writable,
|
||||
);
|
||||
const reader = textDecoder.readable
|
||||
.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
||||
.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
if (
|
||||
data.p1 !== undefined &&
|
||||
data.p2 !== undefined &&
|
||||
data.p3 !== undefined
|
||||
) {
|
||||
let p1Val = data.p1 < 50 ? 0 : data.p1;
|
||||
let p2Val = data.p2 < 50 ? 0 : data.p2;
|
||||
let p3Val = data.p3 < 50 ? 0 : data.p3;
|
||||
|
||||
targetP1 = p1Val;
|
||||
targetP2 = p2Val;
|
||||
targetP3 = p3Val;
|
||||
|
||||
if (currentP1 === -1) currentP1 = p1Val;
|
||||
if (currentP2 === -1) currentP2 = p2Val;
|
||||
if (currentP3 === -1) currentP3 = p3Val;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore JSON parse errors or partial lines
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Serial read error", err);
|
||||
} finally {
|
||||
isEspConnected = false;
|
||||
espSerialPort = null;
|
||||
if (lerpAnimFrame) cancelAnimationFrame(lerpAnimFrame);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistoricalData() {
|
||||
if (!startTimeInput || !endTimeInput) {
|
||||
historyError = "Start Time dan End Time harus diisi.";
|
||||
return;
|
||||
}
|
||||
|
||||
// datetime-local gives "YYYY-MM-DDTHH:MM" or "YYYY-MM-DDTHH:MM:SS" (local time, no Z)
|
||||
// We treat user input as UTC — append 'Z' to force UTC parsing
|
||||
const toUTC = (dt: string) => {
|
||||
const s = dt.length === 16 ? dt + ":00" : dt; // ensure HH:MM:SS
|
||||
return new Date(s + "Z").getTime();
|
||||
};
|
||||
|
||||
const startMs = toUTC(startTimeInput);
|
||||
const endMs = toUTC(endTimeInput);
|
||||
|
||||
if (isNaN(startMs) || isNaN(endMs) || endMs <= startMs) {
|
||||
historyError = "Range waktu tidak valid.";
|
||||
return;
|
||||
}
|
||||
|
||||
historyError = "";
|
||||
isLoadingHistory = true;
|
||||
|
||||
// Format ISO for FDSN API (already UTC since startMs is UTC based)
|
||||
const startISO = new Date(startMs).toISOString();
|
||||
const endISO = new Date(endMs).toISOString();
|
||||
|
||||
const network = data.networkCode ?? "GE";
|
||||
const station = data.stationCode ?? "LUWI";
|
||||
const channel = selectedChannel
|
||||
? selectedChannel["@attributes"].code
|
||||
: "BHZ";
|
||||
|
||||
const url = `https://geofon.gfz.de/fdsnws/dataselect/1/query?starttime=${encodeURIComponent(startISO)}&endtime=${encodeURIComponent(endISO)}&nodata=404&network=${network}&station=${station}&channel=${channel}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
historyError = `Gagal fetch data: HTTP ${response.status}`;
|
||||
isLoadingHistory = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
// Clear buffer and load historical data
|
||||
// Pass startMs as hint for timestamp fallback, skipTrim=true so 1-hour+ data isn't cut
|
||||
waveformService.clearBuffer();
|
||||
waveformService.processMiniseedRaw(
|
||||
arrayBuffer,
|
||||
nominalSampleRateMs,
|
||||
startMs,
|
||||
true,
|
||||
);
|
||||
dataBuffer = waveformService.getBuffer();
|
||||
|
||||
if (dataBuffer.length === 0) {
|
||||
historyError =
|
||||
"Data kosong atau tidak ada data pada rentang waktu ini.";
|
||||
isLoadingHistory = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use USER's requested range for historicalStartMs/EndMs
|
||||
// (don't use actualEnd — it may be Date.now() if timestamp parsing failed)
|
||||
historicalStartMs = startMs;
|
||||
historicalEndMs = endMs;
|
||||
isHistoricalMode = true;
|
||||
timeOffsetMs = 0; // Start at right edge (endTime)
|
||||
|
||||
// Jump chart to end of historical range
|
||||
if (waveformChart) {
|
||||
waveformChart.jumpToHistoricalEnd();
|
||||
}
|
||||
|
||||
const actualStart = dataBuffer[0].t;
|
||||
const actualEnd = dataBuffer[dataBuffer.length - 1].t;
|
||||
logMessages += `${new Date().toLocaleString()} : Historical data loaded (${dataBuffer.length} pts | actual: ${new Date(actualStart).toISOString()} ~ ${new Date(actualEnd).toISOString()})\n`;
|
||||
isSettingsOpen = false;
|
||||
} catch (err: any) {
|
||||
historyError = `Error: ${err.message}`;
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoadingHistory = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearHistoricalMode() {
|
||||
waveformService.clearBuffer();
|
||||
dataBuffer = waveformService.getBuffer();
|
||||
historicalStartMs = 0;
|
||||
historicalEndMs = 0;
|
||||
isHistoricalMode = false;
|
||||
timeOffsetMs = 0;
|
||||
historyError = "";
|
||||
miniseedFileName = "";
|
||||
miniseedError = "";
|
||||
if (waveformChart) waveformChart.resumeLive();
|
||||
logMessages += `${new Date().toLocaleString()} : Back to live mode\n`;
|
||||
}
|
||||
|
||||
async function loadMiniseedFile(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const file = input.files[0];
|
||||
miniseedFileName = file.name;
|
||||
miniseedError = "";
|
||||
isLoadingMiniseed = true;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
waveformService.clearBuffer();
|
||||
// No expectedStartMs hint — let header timestamps drive it
|
||||
waveformService.processMiniseedRaw(
|
||||
arrayBuffer,
|
||||
nominalSampleRateMs,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
dataBuffer = waveformService.getBuffer();
|
||||
|
||||
if (dataBuffer.length === 0) {
|
||||
miniseedError =
|
||||
"Tidak ada data yang dapat dibaca dari file ini.";
|
||||
isLoadingMiniseed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const actualStart = dataBuffer[0].t;
|
||||
const actualEnd = dataBuffer[dataBuffer.length - 1].t;
|
||||
|
||||
historicalStartMs = actualStart;
|
||||
historicalEndMs = actualEnd;
|
||||
isHistoricalMode = true;
|
||||
timeOffsetMs = 0;
|
||||
|
||||
if (waveformChart) waveformChart.jumpToHistoricalEnd();
|
||||
|
||||
logMessages += `${new Date().toLocaleString()} : MiniSEED file loaded "${file.name}" (${dataBuffer.length} pts | ${new Date(actualStart).toISOString()} ~ ${new Date(actualEnd).toISOString()})\n`;
|
||||
} catch (err: any) {
|
||||
miniseedError = `Error: ${err.message}`;
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoadingMiniseed = false;
|
||||
// Reset file input so the same file can be re-loaded
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function loadDataStation(network: string, station: string) {
|
||||
const url = `https://geofon.gfz-potsdam.de/fdsnws/station/1/query?network=${network}&station=${station}&level=response&format=xml`;
|
||||
|
||||
|
|
@ -564,8 +240,7 @@
|
|||
|
||||
ws.onmessage = (e) => {
|
||||
const dataBufferIncoming = e.data;
|
||||
// Jangan proses data live saat historical mode aktif
|
||||
if (isDemoMode || isDemoPsychoMode || isHistoricalMode) {
|
||||
if (isDemoMode || isDemoPsychoMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -621,19 +296,6 @@
|
|||
<div
|
||||
class="min-h-screen px-1 lg:px-0 py-1 lg:py-8 flex flex-col justify-center overflow-hidden font-mono relative gap-2"
|
||||
>
|
||||
<div
|
||||
class="flex no-snapshot fixed right-2 translate-y-0 top-2 left-0 right-0 m-auto flex-row justify-center items-center z-5 gap-2 pointer-events-none"
|
||||
style="width:fit-content"
|
||||
>
|
||||
<a
|
||||
class="ews-btn ews-btn-primary scale-75 md:scale-100 pointer-events-auto"
|
||||
href="/">HOME</a
|
||||
>
|
||||
<a
|
||||
class="ews-btn ews-btn-primary scale-75 md:scale-100 pointer-events-auto"
|
||||
href="/status-ui">STATION STATUS</a
|
||||
>
|
||||
</div>
|
||||
<div class="w-full flex flex-col-reverse lg:flex-row gap-2 justify-center">
|
||||
{#if stationData != null && stationData.Network.Station != undefined && !Array.isArray(stationData.Network.Station)}
|
||||
<div class="flex flex-col gap-4 w-auto h-full items-stretch">
|
||||
|
|
@ -644,9 +306,9 @@
|
|||
</p>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
<div class="w-full flex flex-row gap-2 p-1 lg:p-2">
|
||||
<div class="w-full flex flex-row gap-2">
|
||||
<div
|
||||
class="badge ews-title text-3xl bordered flex justify-between w-full"
|
||||
class="badge ews-title text-3xl bordered flex justify-between mb-2 w-full"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center justify-between p-1"
|
||||
|
|
@ -671,7 +333,7 @@
|
|||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bordered p-1 lg:p-2 w-full">
|
||||
<div class="bordered p-2 w-full">
|
||||
<table class="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
|
@ -729,137 +391,67 @@
|
|||
</p>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
<div class="p-1 lg:p-2 w-full">
|
||||
<HexGrid variant="flat">
|
||||
{#each listChannel as channel, channelIndex (channel["@attributes"]["code"])}
|
||||
<div
|
||||
class="ews-hex-hive flat opacity-0 show-pop-up"
|
||||
<HexGrid variant="flat">
|
||||
{#each listChannel as channel, channelIndex (channel["@attributes"]["code"])}
|
||||
<div
|
||||
class="w-full h-full {channel['@attributes']
|
||||
.endDate == ''
|
||||
? 'yellow glow-orange-small'
|
||||
: 'glow-red-small'}"
|
||||
>
|
||||
<button
|
||||
class="w-full h-full cursor-pointer hex-hive bg-hex-flat opacity-0 show-pop-up {selectedChannel !=
|
||||
undefined &&
|
||||
selectedChannel != null &&
|
||||
selectedChannel['@attributes'].code ==
|
||||
channel['@attributes']['code']
|
||||
? 'blink blink-fast'
|
||||
: ''} {channel['@attributes']
|
||||
.endDate == undefined ||
|
||||
channel['@attributes'].endDate == ''
|
||||
? 'yellow '
|
||||
: ' '}"
|
||||
style="animation-delay: {Math.min(
|
||||
channelIndex * 50,
|
||||
1000,
|
||||
)}ms; width: 83px; height: 72px;"
|
||||
on:click={() => {
|
||||
selectedChannel = channel;
|
||||
const request = {
|
||||
net: data.networkCode ?? "GE",
|
||||
sta: data.stationCode ?? "GSI",
|
||||
cha: selectedChannel[
|
||||
"@attributes"
|
||||
].code,
|
||||
};
|
||||
if (
|
||||
ws &&
|
||||
ws.readyState === WebSocket.OPEN
|
||||
) {
|
||||
ws.send(
|
||||
JSON.stringify(request),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HexShape
|
||||
clipContent={true}
|
||||
color={channel["@attributes"]
|
||||
.endDate == "" ||
|
||||
channel["@attributes"].endDate ==
|
||||
undefined
|
||||
? "orange"
|
||||
: "red"}
|
||||
className={selectedChannel !=
|
||||
undefined &&
|
||||
selectedChannel != null &&
|
||||
selectedChannel["@attributes"]
|
||||
.code ==
|
||||
channel["@attributes"]["code"]
|
||||
? "blink blink-fast"
|
||||
: ""}
|
||||
<div
|
||||
class="w-full h-full flex justify-center items-center text-black text-center"
|
||||
>
|
||||
<button
|
||||
class="w-full h-full cursor-pointer"
|
||||
on:click={() => {
|
||||
selectedChannel = channel;
|
||||
const request = {
|
||||
net:
|
||||
data.networkCode ??
|
||||
"GE",
|
||||
sta:
|
||||
data.stationCode ??
|
||||
"GSI",
|
||||
cha: selectedChannel[
|
||||
"@attributes"
|
||||
].code,
|
||||
};
|
||||
if (
|
||||
ws &&
|
||||
ws.readyState ===
|
||||
WebSocket.OPEN
|
||||
) {
|
||||
ws.send(
|
||||
JSON.stringify(
|
||||
request,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex justify-center items-center text-black text-center"
|
||||
>
|
||||
{channel["@attributes"][
|
||||
"code"
|
||||
]}
|
||||
</div>
|
||||
</button>
|
||||
</HexShape>
|
||||
</div>
|
||||
{/each}
|
||||
</HexGrid>
|
||||
</div>
|
||||
{channel["@attributes"]["code"]}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</HexGrid>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
<!-- Hidden file input for miniseed upload -->
|
||||
<input
|
||||
bind:this={miniseedFileInput}
|
||||
type="file"
|
||||
accept=".mseed,.miniseed,.ms,application/octet-stream"
|
||||
class="hidden"
|
||||
on:change={loadMiniseedFile}
|
||||
/>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<button
|
||||
class="ews-btn ews-btn-primary"
|
||||
on:click={() => (isSettingsOpen = true)}
|
||||
>SETTING</button
|
||||
>
|
||||
<button
|
||||
id="load-miniseed-btn"
|
||||
class="ews-btn ews-btn-danger {isLoadingMiniseed
|
||||
? 'opacity-60 cursor-not-allowed'
|
||||
: 'cursor-pointer'}"
|
||||
on:click={() =>
|
||||
!isLoadingMiniseed &&
|
||||
miniseedFileInput.click()}
|
||||
disabled={isLoadingMiniseed}
|
||||
title="Load local MiniSEED file"
|
||||
>
|
||||
{#if isLoadingMiniseed}
|
||||
<span class="animate-spin inline-block mr-1"
|
||||
>⟳</span
|
||||
> LOADING...
|
||||
{:else}
|
||||
LOAD MINISEED
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
id="connect-esp32-btn"
|
||||
class="ews-btn {isEspConnected
|
||||
? 'ews-btn-danger'
|
||||
: 'ews-btn-primary'} cursor-pointer"
|
||||
on:click={connectEsp32}
|
||||
>
|
||||
{isEspConnected
|
||||
? "ESP32 CONNECTED"
|
||||
: "CONNECT ESP32"}
|
||||
</button>
|
||||
{#if miniseedFileName && !isLoadingMiniseed}
|
||||
<div
|
||||
class="text-xs text-center truncate px-1"
|
||||
style="color: #fa0; opacity: 0.75;"
|
||||
title={miniseedFileName}
|
||||
>
|
||||
📼 {miniseedFileName}
|
||||
</div>
|
||||
{/if}
|
||||
{#if miniseedError}
|
||||
<div
|
||||
class="text-xs text-red-400 text-center px-1 break-words"
|
||||
>
|
||||
{miniseedError}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Card>
|
||||
|
|
@ -951,8 +543,6 @@
|
|||
{selectedTimezone}
|
||||
{isDemoPsychoMode}
|
||||
{psychoPoints}
|
||||
{historicalStartMs}
|
||||
{historicalEndMs}
|
||||
backgroundColor="#000000"
|
||||
gridColor="#33cc55"
|
||||
axesColor="#fa0"
|
||||
|
|
@ -979,18 +569,7 @@
|
|||
<div
|
||||
class="flex flex-col lg:flex-row items-center gap-0 lg:gap-4 h-4"
|
||||
>
|
||||
{#if isHistoricalMode}
|
||||
<span class="text-yellow-400 text-xs font-bold"
|
||||
>HISTORICAL MODE</span
|
||||
>
|
||||
<button
|
||||
class="bg-orange-950 border hover:bg-orange-800 text-white text-xs lg:text-md px-1 lg:px-3 py-0 lg:py-1 rounded cursor-pointer transition-colors"
|
||||
style="border-color: #fa0;"
|
||||
on:click={clearHistoricalMode}
|
||||
>
|
||||
Back to Live
|
||||
</button>
|
||||
{:else if timeOffsetMs > 0}
|
||||
{#if timeOffsetMs > 0}
|
||||
<button
|
||||
class="bg-orange-950 border hover:bg-orange-800 text-white text-xs lg:text-md px-1 lg:px-3 py-0 lg:py-1 rounded cursor-pointer transition-colors"
|
||||
style="border-color: #fa0;"
|
||||
|
|
@ -1029,79 +608,7 @@
|
|||
{/snippet}
|
||||
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-6 w-full p-1 lg:p-2">
|
||||
<!-- Historical Data Filter -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<p
|
||||
class="text-orange-500 font-bold uppercase tracking-widest text-sm mb-1"
|
||||
>
|
||||
Historical Data (FDSN DataSelect):
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label
|
||||
for="hist-start-time"
|
||||
class="text-orange-400 text-xs uppercase tracking-widest"
|
||||
>Start Time (UTC)</label
|
||||
>
|
||||
<input
|
||||
id="hist-start-time"
|
||||
type="datetime-local"
|
||||
bind:value={startTimeInput}
|
||||
class="bg-black border border-orange-700 text-orange-300 px-2 py-1 text-sm w-full focus:outline-none focus:border-orange-400"
|
||||
style="color-scheme: dark;"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label
|
||||
for="hist-end-time"
|
||||
class="text-orange-400 text-xs uppercase tracking-widest"
|
||||
>End Time (UTC)</label
|
||||
>
|
||||
<input
|
||||
id="hist-end-time"
|
||||
type="datetime-local"
|
||||
bind:value={endTimeInput}
|
||||
class="bg-black border border-orange-700 text-orange-300 px-2 py-1 text-sm w-full focus:outline-none focus:border-orange-400"
|
||||
style="color-scheme: dark;"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
{#if historyError}
|
||||
<p class="text-red-400 text-xs">
|
||||
{historyError}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="ews-btn ews-btn-primary flex-1 flex items-center justify-center gap-2"
|
||||
on:click={loadHistoricalData}
|
||||
disabled={isLoadingHistory}
|
||||
>
|
||||
{#if isLoadingHistory}
|
||||
<span class="animate-spin">⟳</span> Memuat...
|
||||
{:else}
|
||||
Load Historical Data
|
||||
{/if}
|
||||
</button>
|
||||
{#if isHistoricalMode}
|
||||
<button
|
||||
class="border border-orange-700 hover:bg-orange-950 text-orange-400 text-sm px-3 py-1"
|
||||
on:click={() => {
|
||||
clearHistoricalMode();
|
||||
isSettingsOpen = false;
|
||||
}}
|
||||
>
|
||||
✕ Clear
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-orange-900 my-2" />
|
||||
|
||||
<div class="flex flex-col gap-6 w-full p-2">
|
||||
<!-- Timezone Settings -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<p
|
||||
|
|
@ -1138,12 +645,18 @@
|
|||
class="cursor-pointer p-0 b-0 overflow-hidden flex items-center justify-center bordered p-1"
|
||||
on:click={toggleDemoData}
|
||||
>
|
||||
<StripeBar
|
||||
reverse={true}
|
||||
loop={isDemoMode}
|
||||
duration={20}
|
||||
></StripeBar>
|
||||
|
||||
<div class="stripe-wrapper">
|
||||
<div
|
||||
class="stripe-bar anim-duration-20 {isDemoMode
|
||||
? 'loop-stripe-reverse'
|
||||
: ''}"
|
||||
></div>
|
||||
<div
|
||||
class="stripe-bar anim-duration-20 {isDemoMode
|
||||
? 'loop-stripe-reverse'
|
||||
: ''}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="absolute bg-black ews-label px-2 py-1"
|
||||
>⚠ DEMO DATA</span
|
||||
>
|
||||
|
|
@ -1153,12 +666,18 @@
|
|||
class="cursor-pointer p-0 b-0 overflow-hidden flex items-center justify-center bordered p-1 mt-2"
|
||||
on:click={toggleDemoPsycho}
|
||||
>
|
||||
<StripeBar
|
||||
reverse={true}
|
||||
color="red"
|
||||
loop={isDemoPsychoMode}
|
||||
duration={20}
|
||||
></StripeBar>
|
||||
<div class="stripe-wrapper">
|
||||
<div
|
||||
class="stripe-bar-red anim-duration-20 {isDemoPsychoMode
|
||||
? 'loop-stripe-reverse'
|
||||
: ''}"
|
||||
></div>
|
||||
<div
|
||||
class="stripe-bar-red anim-duration-20 {isDemoPsychoMode
|
||||
? 'loop-stripe-reverse'
|
||||
: ''}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="absolute bg-black ews-label px-2 py-1"
|
||||
>⚠ DEMO PSYCHOGRAPHIC DATA</span
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,16 +7,12 @@
|
|||
import type { TitikTsunami } from "$lib/components/TitikTsunami";
|
||||
import HexGrid from "$lib/components/HexGrid.svelte";
|
||||
|
||||
// import Highlight from "svelte-highlight";
|
||||
// import css from "svelte-highlight/languages/css";
|
||||
// import vbscriptHtml from "svelte-highlight/languages/vbscript-html";
|
||||
import Highlight from "svelte-highlight";
|
||||
import css from "svelte-highlight/languages/css";
|
||||
import vbscriptHtml from "svelte-highlight/languages/vbscript-html";
|
||||
import StripeBar from "$lib/components/StripeBar.svelte";
|
||||
import InfiniteScroll from "$lib/components/InfiniteScroll.svelte";
|
||||
import HexShape from "$lib/components/HexShape.svelte";
|
||||
import MentalToxicityLevel from "$lib/components/MentalToxicityLevel.svelte";
|
||||
import ThreadedComments, {
|
||||
type ThreadTone,
|
||||
} from "$lib/components/ThreadedComments.svelte";
|
||||
|
||||
let showGempaBumiAlert = $state(false);
|
||||
let showTsunamiAlert = $state(false);
|
||||
|
|
@ -52,103 +48,13 @@
|
|||
{ label: "ZONA F", val: "5.9", warn: true },
|
||||
{ label: "ZONA G", val: "8.1", danger: true },
|
||||
];
|
||||
|
||||
const threadedSpineItems: {
|
||||
id: string;
|
||||
label: string;
|
||||
level: number;
|
||||
tone: ThreadTone;
|
||||
}[] = [
|
||||
{ id: "spine-1", label: "MAIN THREAD TOPIC", level: 1, tone: "danger" },
|
||||
{ id: "spine-2", label: "FOLLOW-UP COMMENT", level: 2, tone: "normal" },
|
||||
{ id: "spine-3", label: "NESTED DETAIL NOTE", level: 3, tone: "normal" },
|
||||
];
|
||||
|
||||
const threadedNestedItems: {
|
||||
id: string;
|
||||
label: string;
|
||||
level: number;
|
||||
tone: ThreadTone;
|
||||
children?: typeof threadedNestedItems;
|
||||
}[] = [
|
||||
{
|
||||
id: "threaded-1",
|
||||
label: "MAIN THREAD TOPIC",
|
||||
level: 1,
|
||||
tone: "danger",
|
||||
children: [
|
||||
{
|
||||
id: "threaded-1-1",
|
||||
label: "REPLY CHILD A",
|
||||
level: 2,
|
||||
tone: "normal",
|
||||
children: [
|
||||
{
|
||||
id: "threaded-1-1-1",
|
||||
label: "DEEP NESTED REPLY",
|
||||
level: 3,
|
||||
tone: "normal",
|
||||
children: [
|
||||
{
|
||||
id: "threaded-1-1-1-1",
|
||||
label: "DEEP DEEP NESTED REPLY",
|
||||
level: 4,
|
||||
tone: "muted",
|
||||
},
|
||||
{
|
||||
id: "threaded-1-1-1-2",
|
||||
label: "DEEP DEEP NESTED REPLY",
|
||||
level: 4,
|
||||
tone: "muted",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "threaded-1-2",
|
||||
label: "REPLY CHILD B",
|
||||
level: 2,
|
||||
tone: "muted",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "threaded-2",
|
||||
label: "SECOND COMMENT",
|
||||
level: 1,
|
||||
tone: "normal",
|
||||
children: [
|
||||
{
|
||||
id: "threaded-2-1",
|
||||
label: "REPLY TO SECOND",
|
||||
level: 2,
|
||||
tone: "normal",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "threaded-3",
|
||||
label: "THIRD COMMENT",
|
||||
level: 1,
|
||||
tone: "muted",
|
||||
},
|
||||
];
|
||||
|
||||
let lastToggledId = $state<string | null>(null);
|
||||
let lastToggledState = $state<boolean | null>(null);
|
||||
|
||||
function handleToggle(id: string, collapsed: boolean) {
|
||||
lastToggledId = id;
|
||||
lastToggledState = collapsed;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Showcase UI Components</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="p-8 min-h-screen max-w-4xl mx-auto text-xs">
|
||||
<div class="p-8 min-h-screen w-4xl mx-auto text-xs">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2">Showcase UI Components</h1>
|
||||
</div>
|
||||
|
|
@ -181,11 +87,6 @@
|
|||
<StripeBar color="red" loop={true} reverse={true} duration={20}
|
||||
></StripeBar>
|
||||
</div>
|
||||
|
||||
<p>Flip</p>
|
||||
<div class="h-[30px]">
|
||||
<StripeBar className="my-2 -scale-x-100"></StripeBar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
|
|
@ -212,7 +113,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CARD DEMO -->
|
||||
<section>
|
||||
<h2 class="text-xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
|
|
@ -295,30 +195,34 @@
|
|||
</div>
|
||||
<div class="flex gap-2 w-full justify-center items-center">
|
||||
<div class="">
|
||||
<HexShape clipContent={true} color="orange" className="h-[100px]">
|
||||
<StripeBar
|
||||
className="bg-black"
|
||||
loop={true}
|
||||
reverse={true}
|
||||
duration={20}
|
||||
color="red"
|
||||
></StripeBar>
|
||||
</HexShape>
|
||||
<div
|
||||
class="hex-shape orange flat-top clip-content h-[100px] flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="inner-content">
|
||||
<StripeBar
|
||||
className="bg-black"
|
||||
color="red"
|
||||
loop={true}
|
||||
reverse={true}
|
||||
duration={20}
|
||||
></StripeBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<HexShape
|
||||
clipContent={true}
|
||||
flatTop={false}
|
||||
color="orange"
|
||||
className="h-[100px]"
|
||||
<div
|
||||
class="hex-shape orange clip-content h-[100px] flex flex-col justify-center items-center"
|
||||
>
|
||||
<StripeBar
|
||||
className="bg-black"
|
||||
loop={true}
|
||||
color="red"
|
||||
duration={20}
|
||||
></StripeBar>
|
||||
</HexShape>
|
||||
<div class="inner-content">
|
||||
<StripeBar
|
||||
className="bg-black"
|
||||
color="red"
|
||||
loop={true}
|
||||
reverse={true}
|
||||
duration={20}
|
||||
></StripeBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="long-hex h-[100px]"></div>
|
||||
|
|
@ -330,121 +234,42 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="col-span-1 md:col-span-2 lg:col-span-3">
|
||||
<h2 class="text-xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
Infinite Scroll
|
||||
</h2>
|
||||
|
||||
<!-- Demo 1: Ticker bar with emergency notices -->
|
||||
<div class="flex w-full relative mb-4">
|
||||
<StripeBar className="my-2 " size="200px"></StripeBar>
|
||||
<StripeBar className="my-2 -scale-x-100 " size="200px"></StripeBar>
|
||||
<div
|
||||
class="absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center text-center"
|
||||
>
|
||||
<div class="p-1 bg-black rounded-lg w-full">
|
||||
<div class="bordered-red bg-black p-2 w-full text-primary">
|
||||
<InfiniteScroll speed={60} gap={48}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col text-center px-4">
|
||||
<span class="text-xs">CONDITION: RED</span>
|
||||
<b class="text-4xl" style="line-height: 0.8;">EMERGENCY</b>
|
||||
<span class="text-xs">CODE: 102</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo 2: Variasi speed & direction -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs mb-1">speed=40 (slow), gap=32px</p>
|
||||
<div class="bordered p-2">
|
||||
<InfiniteScroll speed={40} gap={32}>
|
||||
{#snippet children()}
|
||||
<span class="ews-text text-sm px-4">⬡ SEISMIC ALERT</span>
|
||||
<span class="ews-text danger text-sm px-4">⚠ AWAS GEMPA</span>
|
||||
<span class="ews-text text-sm px-4">⬡ ZONE: SUMATERA</span>
|
||||
<span class="ews-text danger text-sm px-4">⚠ MAG 7.2</span>
|
||||
{/snippet}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs mb-1">
|
||||
speed=120 (fast), direction=right
|
||||
</p>
|
||||
<div class="bordered p-2">
|
||||
<InfiniteScroll speed={120} gap={32} direction="right">
|
||||
{#snippet children()}
|
||||
<span class="ews-text-digital text-sm px-4"
|
||||
>STATION: BDG-01</span
|
||||
>
|
||||
<span class="ews-text-digital text-sm px-4">DEPTH: 10km</span>
|
||||
<span class="ews-text-digital text-sm px-4">LAT: -6.208</span>
|
||||
<span class="ews-text-digital text-sm px-4">LNG: 106.845</span>
|
||||
{/snippet}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs mb-1">
|
||||
pauseOnHover, speed=80, gap=64px — single large item
|
||||
</p>
|
||||
<div class="bordered-red p-2">
|
||||
<InfiniteScroll speed={80} gap={64} pauseOnHover={true}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col text-center px-2">
|
||||
<span class="text-xs text-gray-400">HOVER TO PAUSE</span>
|
||||
<b class="text-2xl ews-text danger">⚠ TSUNAMI WARNING</b>
|
||||
<span class="text-xs">EVAKUASI SEGERA</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HEXAGONAL GRID -->
|
||||
<section class="col-span-1 md:col-span-2 lg:col-span-3">
|
||||
<h2 class="text-xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
Hexagonal Grid
|
||||
</h2>
|
||||
|
||||
<div class="w-full flex flex-col md:flex-row gap-2">
|
||||
<!-- Honeycomb Offset Grid -->
|
||||
<div class="basis-0 flex-1">
|
||||
<p class="text-gray-500 text-xs mb-3">Honeycomb Offset Grid</p>
|
||||
<HexGrid gap={0}>
|
||||
{#each { length: 30 } as _, i}
|
||||
<div class="ews-hex-hive">
|
||||
<HexShape clipContent={true} flatTop={false}>
|
||||
{i}
|
||||
</HexShape>
|
||||
</div>
|
||||
{/each}
|
||||
</HexGrid>
|
||||
</div>
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="w-full flex gap-2">
|
||||
<!-- Honeycomb Offset Grid -->
|
||||
<div class="basis-0 flex-1">
|
||||
<p class="text-gray-500 text-xs mb-3">Honeycomb Offset Grid</p>
|
||||
<HexGrid>
|
||||
{#each { length: 30 } as _, i}
|
||||
<div class="hex-hive bg-hex">
|
||||
<HexShape clipContent={true} flatTop={false}>
|
||||
{i}
|
||||
</HexShape>
|
||||
</div>
|
||||
{/each}
|
||||
</HexGrid>
|
||||
</div>
|
||||
|
||||
<div class="basis-0 flex-1">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Honeycomb Variant 2 Offset Grid
|
||||
</p>
|
||||
<HexGrid variant="flat">
|
||||
{#each { length: 30 } as _, i}
|
||||
<div class="hex-hive flat">
|
||||
<HexShape clipContent={true}>
|
||||
{i}
|
||||
</HexShape>
|
||||
</div>
|
||||
{/each}
|
||||
</HexGrid>
|
||||
<div class="basis-0 flex-1">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Honeycomb Variant 2 Offset Grid
|
||||
</p>
|
||||
<HexGrid variant="flat">
|
||||
{#each { length: 30 } as _, i}
|
||||
<div class="hex-hive bg-hex-flat">
|
||||
<HexShape clipContent={true}>
|
||||
{i}
|
||||
</HexShape>
|
||||
</div>
|
||||
{/each}
|
||||
</HexGrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -463,11 +288,7 @@
|
|||
<div class="text">MAG</div>
|
||||
</div>
|
||||
<div class="decal">
|
||||
<StripeBar
|
||||
className="w-full h-full"
|
||||
size={"100%"}
|
||||
orientation="vertical"
|
||||
></StripeBar>
|
||||
<div class="w-full h-full stripe-bar vertical"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -479,12 +300,7 @@
|
|||
<div class="text">MAG</div>
|
||||
</div>
|
||||
<div class="decal">
|
||||
<StripeBar
|
||||
className="w-full h-full"
|
||||
size={"100%"}
|
||||
orientation="vertical"
|
||||
color="red"
|
||||
></StripeBar>
|
||||
<div class="w-full h-full stripe-bar red vertical"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -497,12 +313,9 @@
|
|||
<div class="text">MAG</div>
|
||||
</div>
|
||||
<div class="decal">
|
||||
<StripeBar
|
||||
className="w-full h-full"
|
||||
size={"100%"}
|
||||
orientation="vertical"
|
||||
loop={true}
|
||||
></StripeBar>
|
||||
<div
|
||||
class="w-full h-full stripe-bar vertical loop-stripe-vertical-reverse anim-duration-10"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -514,13 +327,9 @@
|
|||
<div class="text">MAG</div>
|
||||
</div>
|
||||
<div class="decal">
|
||||
<StripeBar
|
||||
className="w-full h-full"
|
||||
size={"100%"}
|
||||
orientation="vertical"
|
||||
color="red"
|
||||
loop={true}
|
||||
></StripeBar>
|
||||
<div
|
||||
class="w-full h-full stripe-bar red vertical loop-stripe-vertical anim-duration-10"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -586,7 +395,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs mb-1">ews-text danger</p>
|
||||
<p class="ews-text danger text-lg">EARLY WARNING</p>
|
||||
<p class="ews-text danger text-lg">PERINGATAN DINI</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs mb-1">ews-text</p>
|
||||
|
|
@ -682,7 +491,7 @@
|
|||
<div>
|
||||
<p class="text-gray-500 text-xs mb-1">ews-select danger</p>
|
||||
<select class="ews-select danger">
|
||||
<option>LEVEL WARNING</option>
|
||||
<option>LEVEL PERINGATAN</option>
|
||||
<option>SIAGA</option>
|
||||
<option>WASPADA</option>
|
||||
<option>AWAS</option>
|
||||
|
|
@ -856,95 +665,6 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- THREADED COMMENTS / NESTED LIST -->
|
||||
<section class="col-span-1 md:col-span-2 lg:col-span-3">
|
||||
<h2 class="text-xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
Threaded Comments / Nested List
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="p-3">
|
||||
<p class="text-gray-500 text-xs mb-3">Variation 1 — Spine</p>
|
||||
<ThreadedComments
|
||||
variant="spine"
|
||||
items={threadedSpineItems}
|
||||
animated={false}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="text-gray-500 text-xs mb-3">Variation 2 — Threaded</p>
|
||||
<ThreadedComments
|
||||
variant="threaded"
|
||||
items={threadedSpineItems}
|
||||
animated={false}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Variation 3 — Spine (Expandable)
|
||||
</p>
|
||||
<ThreadedComments
|
||||
variant="spine"
|
||||
items={threadedNestedItems}
|
||||
tone="danger"
|
||||
expandable={true}
|
||||
animated={false}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Variation 4 — Spine (Animated)
|
||||
</p>
|
||||
<ThreadedComments
|
||||
variant="spine"
|
||||
items={threadedNestedItems}
|
||||
expandable={true}
|
||||
animated={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Variation 5 — Threaded (Expandable)
|
||||
</p>
|
||||
<ThreadedComments
|
||||
variant="threaded"
|
||||
items={threadedNestedItems}
|
||||
tone="danger"
|
||||
expandable={true}
|
||||
animated={false}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Variation 6 — Threaded (Animated)
|
||||
</p>
|
||||
<ThreadedComments
|
||||
variant="threaded"
|
||||
items={threadedNestedItems}
|
||||
expandable={true}
|
||||
animated={true}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3 col-span-1 lg:col-span-2">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Variation 7 — Threaded (With Toggle Callback)
|
||||
</p>
|
||||
<div class="mb-2 text-xs text-gray-400">
|
||||
Last toggled: {lastToggledId ?? "none"} — {lastToggledState === null
|
||||
? ""
|
||||
: lastToggledState
|
||||
? "collapsed"
|
||||
: "expanded"}
|
||||
</div>
|
||||
<ThreadedComments
|
||||
variant="threaded"
|
||||
items={threadedNestedItems}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MENTAL TOXICITY LEVEL -->
|
||||
<section class="col-span-1 md:col-span-2 lg:col-span-3">
|
||||
<h2 class="text-xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
|
|
@ -978,7 +698,7 @@
|
|||
{#if showTsunamiAlert}
|
||||
<!-- Tsunami Alert takes up the full screen and has animations -->
|
||||
<div class="fixed inset-0 z-50 pointer-events-auto">
|
||||
<TsunamiAlert infoTsunami={dummyTsunami.infoTsunami} />
|
||||
<TsunamiAlert alertTsunami={dummyTsunami} />
|
||||
<button
|
||||
class="absolute top-4 right-4 z-[60] bg-black text-white px-4 py-2"
|
||||
onclick={() => (showTsunamiAlert = false)}
|
||||
|
|
|
|||
|
|
@ -227,20 +227,7 @@
|
|||
class="min-h-screen py-1 md:py-4 flex flex-col items-center overflow-x-hidden overflow-y-auto font-mono"
|
||||
>
|
||||
<div
|
||||
class="flex no-snapshot fixed right-2 translate-y-0 top-2 left-0 right-0 m-auto flex-row justify-center items-center z-5 gap-2 pointer-events-none"
|
||||
style="width:fit-content"
|
||||
>
|
||||
<a
|
||||
class="ews-btn ews-btn-primary scale-75 md:scale-100 pointer-events-auto"
|
||||
href="/">HOME</a
|
||||
>
|
||||
<a
|
||||
class="ews-btn ews-btn-primary scale-75 md:scale-100 pointer-events-auto"
|
||||
href="/status-ui">STATION STATUS</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="mb-2 text-center p-2 z-10 w-full bordered flex justify-center items-center relative show-pop-up mt-6"
|
||||
class="mb-2 text-center p-2 z-10 w-full bordered flex justify-center items-center relative show-pop-up"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<StripeBar loop={true} duration={20} color="red"></StripeBar>
|
||||
|
|
@ -262,14 +249,14 @@
|
|||
{#snippet title()}
|
||||
<h1>NETWORK CHANNEL STATUS</h1>
|
||||
{/snippet}
|
||||
<div class="overflow-y-auto h-[80vh]">
|
||||
<div class="overflow-y-auto max-h-[700px]">
|
||||
<MentalToxicityLevel networks={networkStats} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div class="w-full lg:w-2/3 bordered relative overflow-hidden">
|
||||
<div
|
||||
class="w-full h-[80vh] bg-black rounded"
|
||||
class="w-full h-[700px] bg-black rounded"
|
||||
bind:this={mapContainer}
|
||||
></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
onMount(() => {
|
||||
// https://geofon.bmkg.go.id/fdsnws/station/1/
|
||||
// URL GEOFON (tanpa format=text agar mengembalikan XML)
|
||||
const url = `https://geofon.gfz-potsdam.de/fdsnws/station/1/query?${mapStore.urlParams}&level=station`;
|
||||
const url =
|
||||
`https://geofon.gfz-potsdam.de/fdsnws/station/1/query?${mapStore.urlParams}&level=station`;
|
||||
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
|
|
@ -90,21 +91,7 @@
|
|||
class="min-h-screen py-1 md:py-4 flex flex-col items-center overflow-x-hidden overflow-y-auto font-mono"
|
||||
>
|
||||
<div
|
||||
class="flex no-snapshot fixed right-2 translate-y-0 top-2 left-0 right-0 m-auto flex-row justify-center items-center z-5 gap-2 pointer-events-none"
|
||||
style="width:fit-content"
|
||||
>
|
||||
<a
|
||||
class="ews-btn ews-btn-primary scale-75 md:scale-100 pointer-events-auto"
|
||||
href="/">HOME</a
|
||||
>
|
||||
<a
|
||||
class="ews-btn ews-btn-primary scale-75 md:scale-100 pointer-events-auto"
|
||||
href="/status-map">STATION MAP</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-2 text-center p-2 z-10 w-full bordered flex justify-center items-center relative show-pop-up mt-6"
|
||||
class="mb-2 text-center p-2 z-10 w-full bordered flex justify-center items-center relative show-pop-up"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<StripeBar loop={true} duration={20} color="red"></StripeBar>
|
||||
|
|
@ -127,14 +114,15 @@
|
|||
getHref={(item: any) =>
|
||||
`/realtime?networkCode=${item.networkCode}&stationCode=${item.stationCode}`}
|
||||
>
|
||||
{#snippet nodeContent(
|
||||
item: any,
|
||||
{ side, delay }: { side: string; delay: number },
|
||||
)}
|
||||
{#snippet nodeContent(item: any, { side, delay }: { side: string; delay: number })}
|
||||
<div
|
||||
class="slide-fade-in ews-rib-node {side === 'right'
|
||||
? 'flip'
|
||||
: ''} {item.type === 'danger' ? 'danger' : ''}"
|
||||
class="slide-fade-in {side === 'left' ? 'status-node' : 'status-node-flip'} {item.type ===
|
||||
'danger'
|
||||
? 'danger'
|
||||
: ''} w-24 h-6 flex flex-glow flex-col items-center justify-center relative mt-6 {side ===
|
||||
'left'
|
||||
? '-mr-2'
|
||||
: '-ml-2'} z-5 text-black text-xs font-bold"
|
||||
style="animation-delay: {delay}ms;"
|
||||
></div>
|
||||
{/snippet}
|
||||
|
|
|
|||
1
static/bad_apple_frames_v3.json
Normal file
1
static/bad_apple_frames_v3.json
Normal file
File diff suppressed because one or more lines are too long
1
static/bad_apple_frames_v4.json
Normal file
1
static/bad_apple_frames_v4.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue