remove: app
@@ -1,9 +0,0 @@
|
||||
node_modules
|
||||
charts
|
||||
dist
|
||||
dev-dist
|
||||
.DS_Store
|
||||
visualizer
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
.wrangler
|
||||
@@ -1,17 +0,0 @@
|
||||
# Satonomics - App
|
||||
|
||||
## Description
|
||||
|
||||
A web app to view the generated datasets in various charts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Install `node`
|
||||
- Install `pnpm`
|
||||
- If using `cloudflare`, add cache rule to bypass the cache for `/sw.js`
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
pnpm deploy-prod
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
/* /index.html
|
||||
@@ -1,372 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>kibō</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A better, FOSS, Bitcoin-only, self-hostable Glassnode"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="theme-color" content="#0c0a09" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="196x196"
|
||||
href="/assets/favicon-196.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/assets/apple-icon-180.png" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2048-2732.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2732-2048.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1668-2388.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2388-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1536-2048.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2048-1536.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1488-2266.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2266-1488.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1640-2360.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2360-1640.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1668-2224.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2224-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1620-2160.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2160-1620.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1290-2796.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2796-1290.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1179-2556.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2556-1179.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1284-2778.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2778-1284.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1170-2532.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2532-1170.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1125-2436.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2436-1125.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1242-2688.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2688-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-828-1792.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1792-828.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1242-2208.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2208-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-750-1334.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1334-750.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-640-1136.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1136-640.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2048-2732.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2732-2048.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1668-2388.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2388-1668.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1536-2048.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2048-1536.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1488-2266.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2266-1488.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1640-2360.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2360-1640.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1668-2224.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2224-1668.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1620-2160.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2160-1620.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1290-2796.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2796-1290.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1179-2556.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2556-1179.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1284-2778.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2778-1284.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1170-2532.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2532-1170.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1125-2436.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2436-1125.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1242-2688.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2688-1242.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-828-1792.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1792-828.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1242-2208.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2208-1242.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-750-1334.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1334-750.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-640-1136.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1136-640.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "satonomics",
|
||||
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
|
||||
"version": "0.4.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "($npm_execpath outdated || read -p \"Press enter to ignore...\") && vite --host",
|
||||
"check": "tsc --noEmit --skipLibCheck --pretty",
|
||||
"build": "$npm_execpath run check && vite build",
|
||||
"check-watch": "$npm_execpath check --watch",
|
||||
"format": "prettier --write './src'",
|
||||
"prod": "$npm_execpath run build && vite preview --host",
|
||||
"pages-prod": "pnpm build && pnpm wrangler pages deploy ./dist",
|
||||
"pages-dev": "pnpm build && pnpm wrangler pages deploy --branch dev ./dist",
|
||||
"assets": "pnpm pwa-asset-generator ../assets/logo-icon.svg ./public/assets --index ./index.html --manifest ./public/manifest.webmanifest --icon-only --favicon --background #ffffe3 --padding \"min(15vh, 15vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ../assets/logo-icon.svg ./public/assets --index ./index.html --splash-only --background #10100e --padding \"min(33vh, 33vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ../assets/logo-icon.svg ./public/assets --index ./index.html --splash-only --dark-mode --background #10100e --padding \"min(33vh, 33vw)\" --path-override \"/assets\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@leeoniya/ufuzzy": "^1.0.14",
|
||||
"@solid-primitives/event-listener": "^2.3.3",
|
||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||
"@solid-primitives/resize-observer": "^2.0.26",
|
||||
"lean-qr": "^2.3.4",
|
||||
"lightweight-charts": "^4.2.0",
|
||||
"solid-js": "^1.8.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@iconify-json/tabler": "^1.1.120",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"pwa-asset-generator": "^6.3.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-icons": "^0.19.2",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.71.0"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @type {import("prettier").Options} */
|
||||
export default {
|
||||
plugins: [
|
||||
'@ianvs/prettier-plugin-sort-imports',
|
||||
'prettier-plugin-tailwindcss', // MUST come last
|
||||
],
|
||||
|
||||
tailwindFunctions: ['classList'],
|
||||
|
||||
importOrder: ['<THIRD_PARTY_MODULES>', '', '^/?(~|src)/', '', '^[./]'],
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="black">
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1,17 +0,0 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="white">
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "Satonomics",
|
||||
"short_name": "Satonomics",
|
||||
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#0c0a09",
|
||||
"background_color": "#0c0a09",
|
||||
"lang": "en",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -1,293 +0,0 @@
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { standalone } from "../env";
|
||||
import { createDatasets } from "../scripts/datasets";
|
||||
import { createPresets } from "../scripts/presets";
|
||||
import { createUserConfig } from "../scripts/user/config";
|
||||
import { sleep } from "../scripts/utils/sleep";
|
||||
import {
|
||||
readBooleanFromStorage,
|
||||
saveToStorage,
|
||||
} from "../scripts/utils/storage";
|
||||
import { readBooleanURLParam, writeURLParam } from "../scripts/utils/urlParams";
|
||||
import { webSockets } from "../scripts/ws";
|
||||
import { classPropToString } from "../solid/classes";
|
||||
import { ChartFrame } from "./frames/chart";
|
||||
import { FoldersFrame } from "./frames/folders";
|
||||
import { HistoryFrame } from "./frames/history";
|
||||
import { SettingsFrame } from "./frames/settings";
|
||||
import { StripDesktop, StripMobile } from "./strip";
|
||||
import { Update } from "./update";
|
||||
|
||||
const LOCAL_STORAGE_BAR_KEY = "bar-width";
|
||||
const LOCAL_STORAGE_FULLSCREEN = "fullscrenn";
|
||||
|
||||
export const INPUT_PRESET_SEARCH_ID = "input-search-preset";
|
||||
|
||||
export function App() {
|
||||
const tabFocused = createRWS(true);
|
||||
const qrcode = createRWS("");
|
||||
const dark = createRWS(false);
|
||||
|
||||
const userConfig = createUserConfig({
|
||||
dark,
|
||||
});
|
||||
|
||||
const fullscreen = createRWS(
|
||||
readBooleanURLParam(LOCAL_STORAGE_FULLSCREEN) ||
|
||||
readBooleanFromStorage(LOCAL_STORAGE_FULLSCREEN) ||
|
||||
false,
|
||||
);
|
||||
|
||||
const windowWidth = createRWS(window.innerWidth);
|
||||
const windowWidth60p = createMemo(() => windowWidth() * 0.6);
|
||||
const windowResizeCallback = () => {
|
||||
windowWidth.set(window.innerWidth);
|
||||
};
|
||||
window.addEventListener("resize", windowResizeCallback);
|
||||
onCleanup(() => window.removeEventListener("resize", windowResizeCallback));
|
||||
|
||||
const windowSizeIsAtLeastMedium = createMemo(() => windowWidth() >= 768);
|
||||
|
||||
const minBarWidth = 384;
|
||||
const barWidth = createRWS(
|
||||
Number(localStorage.getItem(LOCAL_STORAGE_BAR_KEY)) || minBarWidth,
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
localStorage.setItem(LOCAL_STORAGE_BAR_KEY, String(barWidth()));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (fullscreen()) {
|
||||
writeURLParam(LOCAL_STORAGE_FULLSCREEN, "true");
|
||||
saveToStorage(LOCAL_STORAGE_FULLSCREEN, fullscreen());
|
||||
} else {
|
||||
writeURLParam(LOCAL_STORAGE_FULLSCREEN, undefined);
|
||||
saveToStorage(LOCAL_STORAGE_FULLSCREEN, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const _selectedFrame = createRWS<FrameName>("Chart");
|
||||
|
||||
const selectedFrame = createMemo(() =>
|
||||
windowSizeIsAtLeastMedium() && _selectedFrame() === "Chart"
|
||||
? "Folders"
|
||||
: _selectedFrame(),
|
||||
);
|
||||
|
||||
const presets = createPresets();
|
||||
|
||||
const resizingBarStart = createRWS<number | undefined>(undefined);
|
||||
const resizingBarWidth = createRWS<number>(0);
|
||||
|
||||
createEffect(
|
||||
() => {
|
||||
if (!windowSizeIsAtLeastMedium() && presets.selected()) {
|
||||
_selectedFrame.set("Chart");
|
||||
}
|
||||
},
|
||||
{
|
||||
deffer: true,
|
||||
},
|
||||
);
|
||||
|
||||
const datasets = createDatasets();
|
||||
|
||||
onMount(() => {
|
||||
webSockets.openAll();
|
||||
|
||||
createEffect(() => {
|
||||
const latest = webSockets.liveKrakenCandle.latest();
|
||||
|
||||
if (latest) {
|
||||
const close = latest.close;
|
||||
|
||||
console.log("close:", close);
|
||||
|
||||
document.title = `${latest.close.toLocaleString("en-us")} | Satonomics`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const documentVisibilityChange = () =>
|
||||
tabFocused.set(document.visibilityState === "visible");
|
||||
document.addEventListener("visibilitychange", documentVisibilityChange);
|
||||
onCleanup(() =>
|
||||
document.removeEventListener("visibilitychange", documentVisibilityChange),
|
||||
);
|
||||
|
||||
const documentOnKeyDown = async (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case "Escape": {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
_selectedFrame.set("Chart");
|
||||
|
||||
break;
|
||||
}
|
||||
case "/": {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
_selectedFrame.set("Search");
|
||||
|
||||
await sleep(50);
|
||||
|
||||
document.getElementById(INPUT_PRESET_SEARCH_ID)?.focus();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", documentOnKeyDown);
|
||||
onCleanup(() => document.removeEventListener("keydown", documentOnKeyDown));
|
||||
|
||||
const SearchFrame = lazy(() =>
|
||||
import("./frames/search").then((d) => ({
|
||||
default: d.SearchFrame,
|
||||
})),
|
||||
);
|
||||
|
||||
const Qrcode = lazy(() =>
|
||||
import("./qrcode").then((d) => ({
|
||||
default: d.Qrcode,
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class="relative h-dvh"
|
||||
style={{
|
||||
"user-select": resizingBarStart() !== undefined ? "none" : undefined,
|
||||
}}
|
||||
onMouseMove={(event) => {
|
||||
const startingClientX = resizingBarStart();
|
||||
|
||||
if (startingClientX !== undefined) {
|
||||
barWidth.set(
|
||||
Math.min(
|
||||
Math.max(
|
||||
resizingBarWidth() + event.clientX - startingClientX,
|
||||
minBarWidth,
|
||||
),
|
||||
windowWidth60p(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => resizingBarStart.set(undefined)}
|
||||
onMouseLeave={() => resizingBarStart.set(undefined)}
|
||||
onTouchEnd={() => resizingBarStart.set(undefined)}
|
||||
onTouchCancel={() => resizingBarStart.set(undefined)}
|
||||
>
|
||||
<Show when={qrcode()}>
|
||||
<Qrcode qrcode={qrcode} />
|
||||
</Show>
|
||||
|
||||
<Update />
|
||||
|
||||
<div class="flex size-full flex-col md:flex-row">
|
||||
<Show when={!windowSizeIsAtLeastMedium() || !fullscreen()}>
|
||||
<div
|
||||
class={classPropToString([
|
||||
standalone && "border-t md:border-t-0",
|
||||
"flex h-full flex-col overflow-hidden md:flex-row md:shadow-md md:short:hidden",
|
||||
])}
|
||||
>
|
||||
<div class="hidden flex-col gap-2 border-r px-3 py-4 backdrop-blur-sm md:flex">
|
||||
<StripDesktop
|
||||
selected={selectedFrame}
|
||||
setSelected={_selectedFrame.set}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex h-full min-h-0"
|
||||
style={{
|
||||
...(windowSizeIsAtLeastMedium()
|
||||
? {
|
||||
"min-width": `${minBarWidth}px`,
|
||||
width: `${barWidth()}px`,
|
||||
"max-width": `${windowWidth60p()}px`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<Show when={!windowSizeIsAtLeastMedium()}>
|
||||
<ChartFrame
|
||||
presets={presets}
|
||||
hide={() => selectedFrame() !== "Chart"}
|
||||
qrcode={qrcode}
|
||||
standalone={false}
|
||||
datasets={datasets}
|
||||
dark={dark}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<FoldersFrame presets={presets} selectedFrame={selectedFrame} />
|
||||
<SearchFrame presets={presets} selectedFrame={selectedFrame} />
|
||||
<HistoryFrame presets={presets} selectedFrame={selectedFrame} />
|
||||
<SettingsFrame
|
||||
selectedFrame={selectedFrame}
|
||||
appTheme={userConfig.settings.appTheme}
|
||||
backgroundMode={userConfig.settings.background.mode}
|
||||
backgroundOpacity={userConfig.settings.background.opacity}
|
||||
/>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 z-10 h-6 w-full bg-gradient-to-b from-transparent to-[var(--background-color)]" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={classPropToString([
|
||||
standalone && "pb-6",
|
||||
"flex justify-between gap-3 border-t p-2 sm:justify-around md:hidden short:hidden",
|
||||
])}
|
||||
>
|
||||
<StripMobile
|
||||
selected={selectedFrame}
|
||||
setSelected={_selectedFrame.set}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* <Show when={!fullscreen()}>
|
||||
<div
|
||||
class="mx-[3px] my-8 hidden w-[6px] cursor-col-resize items-center justify-center rounded-full opacity-0 hover:opacity-50 md:block short:hidden"
|
||||
onMouseDown={(event) => {
|
||||
if (resizingBarStart() === undefined) {
|
||||
resizingBarStart.set(event.clientX);
|
||||
resizingBarWidth.set(barWidth());
|
||||
}
|
||||
}}
|
||||
onTouchStart={(event) => {
|
||||
if (resizingBarStart() === undefined) {
|
||||
resizingBarStart.set(event.touches[0].clientX);
|
||||
resizingBarWidth.set(barWidth());
|
||||
}
|
||||
}}
|
||||
onDblClick={() => {
|
||||
barWidth.set(0);
|
||||
}}
|
||||
/>
|
||||
</Show> */}
|
||||
|
||||
<Show when={windowSizeIsAtLeastMedium()}>
|
||||
<div class="flex min-w-0 flex-1 border-l">
|
||||
<ChartFrame
|
||||
standalone={true}
|
||||
presets={presets}
|
||||
qrcode={qrcode}
|
||||
fullscreen={fullscreen}
|
||||
datasets={datasets}
|
||||
dark={dark}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import { touchScreen } from "/src/env";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
const texts = [
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
|
||||
"stay humble, stack sats",
|
||||
"21 million",
|
||||
"cold storage",
|
||||
"utxo",
|
||||
"satoshi nakamoto",
|
||||
"hodl",
|
||||
`don't trust, verify`,
|
||||
"zap",
|
||||
"₿itcoin",
|
||||
"lightning",
|
||||
"nostr",
|
||||
"freedom tech",
|
||||
"2008/10/31",
|
||||
"2009/01/03",
|
||||
"2010/05/22",
|
||||
"hodl!",
|
||||
"Hal Finney",
|
||||
"Vote for better money",
|
||||
"gradually then suddenly",
|
||||
"timechain",
|
||||
"self custody",
|
||||
"be your own bank",
|
||||
"resistance money",
|
||||
"foss",
|
||||
"permissionless",
|
||||
"great reset",
|
||||
"orange pill",
|
||||
"borderless",
|
||||
"anonymous",
|
||||
"nyknyc",
|
||||
"low time preference",
|
||||
"absolute scarcity",
|
||||
"time is scarce",
|
||||
"ride or die",
|
||||
"cypherpunk",
|
||||
"we like the coin",
|
||||
"money for enemies",
|
||||
"trustless",
|
||||
"sustainable",
|
||||
"discriminationless",
|
||||
];
|
||||
|
||||
export function Background({
|
||||
mode,
|
||||
opacity,
|
||||
focused,
|
||||
}: {
|
||||
mode: SL<"Scroll" | "Static">;
|
||||
opacity: SL<{ text: string; value: number }>;
|
||||
focused: Accessor<boolean>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class="absolute h-full w-full overflow-hidden will-change-auto"
|
||||
style={{
|
||||
opacity: opacity.selected().value,
|
||||
}}
|
||||
>
|
||||
<div class="-m-[2rem] -space-y-1 overflow-hidden md:-m-[1rem]">
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div class="absolute h-full w-full opacity-15 mix-blend-multiply">
|
||||
<Noise />
|
||||
</div> */}
|
||||
<div class="absolute h-full w-full opacity-15 mix-blend-hard-light">
|
||||
<Noise />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Line({
|
||||
mode,
|
||||
focused,
|
||||
}: {
|
||||
mode: SL<"Scroll" | "Static">;
|
||||
focused: Accessor<boolean>;
|
||||
}) {
|
||||
const shuffled = shuffle(texts).slice(0, 21);
|
||||
const joined = shuffled.join(". ");
|
||||
|
||||
return (
|
||||
<div class="select-none whitespace-nowrap">
|
||||
<TextWrapper mode={mode} focused={focused} joined={joined} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextWrapper({
|
||||
joined,
|
||||
mode,
|
||||
focused,
|
||||
}: {
|
||||
mode: SL<"Scroll" | "Static">;
|
||||
focused: Accessor<boolean>;
|
||||
joined: string;
|
||||
}) {
|
||||
const p = createRWS(undefined as HTMLParagraphElement | undefined);
|
||||
|
||||
const seconds = createRWS(joined.length * 2);
|
||||
|
||||
const wasOnceOn = createRWS(false);
|
||||
|
||||
createEffect(() => {
|
||||
if (!wasOnceOn() && mode.selected() === "Scroll") {
|
||||
wasOnceOn.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Bug in Safari iOS, not sure where else, works perfectly on Mac OS though
|
||||
if (!touchScreen) {
|
||||
onMount(() => {
|
||||
seconds.set(Math.round(p()!.clientWidth / 15));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={p.set}
|
||||
class="inline-block px-2 text-[4dvh] font-black uppercase leading-none"
|
||||
style={{
|
||||
...(wasOnceOn()
|
||||
? {
|
||||
animation: `marquee ${seconds()}s linear infinite`,
|
||||
"animation-play-state":
|
||||
focused() && mode.selected() === "Scroll"
|
||||
? "running"
|
||||
: "paused",
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{joined} {wasOnceOn() ? joined : undefined}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function shuffle<T>([...arr]: T[]): T[] {
|
||||
let m = arr.length;
|
||||
|
||||
while (m) {
|
||||
const i = Math.floor(Math.random() * m--);
|
||||
[arr[m], arr[i]] = [arr[i], arr[m]];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function Noise() {
|
||||
return (
|
||||
<svg
|
||||
class="size-full"
|
||||
viewBox="0 0 210 210"
|
||||
preserveAspectRatio="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<filter id="noiseFilter">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="4"
|
||||
numOctaves="6"
|
||||
stitchTiles="stitch"
|
||||
/>
|
||||
</filter>
|
||||
|
||||
<rect width="100%" height="100%" filter="url(#noiseFilter)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
|
||||
export function Box({
|
||||
flex = true,
|
||||
absolute,
|
||||
padded = true,
|
||||
spaced = true,
|
||||
children,
|
||||
dark,
|
||||
classes,
|
||||
}: {
|
||||
flex?: boolean;
|
||||
absolute?: "top" | "bottom";
|
||||
padded?: boolean;
|
||||
spaced?: boolean;
|
||||
dark?: boolean;
|
||||
classes?: string;
|
||||
} & ParentProps) {
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
absolute
|
||||
? [
|
||||
"absolute inset-x-0",
|
||||
absolute === "top" ? "top-0" : "pointer-events-none bottom-0",
|
||||
]
|
||||
: "relative",
|
||||
classes,
|
||||
])}
|
||||
>
|
||||
<div
|
||||
class={classPropToString([
|
||||
"pointer-events-auto relative overflow-hidden rounded-full border shadow-md",
|
||||
])}
|
||||
style={{
|
||||
"background-color": "var(--background-color)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classPropToString([
|
||||
flex && "flex w-full",
|
||||
spaced && "space-x-2",
|
||||
padded && "p-1.5",
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { random } from "/src/scripts/utils/math/random";
|
||||
|
||||
export function Button({
|
||||
onClick,
|
||||
children,
|
||||
}: { onClick: VoidFunction } & ParentProps) {
|
||||
return (
|
||||
<button
|
||||
class="group flex w-full flex-1 items-center justify-center rounded-lg px-2 py-1.5 hover:bg-orange-200/20 active:scale-95"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonRandomChart({ presets }: { presets: Presets }) {
|
||||
return (
|
||||
<button
|
||||
class="inline-flex rounded-md bg-orange-700 bg-opacity-80 px-1.5 py-0.5 font-medium hover:bg-opacity-100 active:scale-95"
|
||||
onClick={() => {
|
||||
const randomPreset = random(presets.list);
|
||||
if (randomPreset) {
|
||||
presets.select(randomPreset);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Open a random chart
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Button } from "./button";
|
||||
|
||||
export function Actions({
|
||||
presets,
|
||||
fullscreen,
|
||||
qrcode,
|
||||
}: {
|
||||
presets: Presets;
|
||||
qrcode: RWS<string>;
|
||||
fullscreen?: RWS<boolean>;
|
||||
}) {
|
||||
const ButtonShare = lazy(() =>
|
||||
import("./buttonShare").then((d) => ({ default: d.ButtonShare })),
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="flex space-x-1 p-1.5">
|
||||
<Show when={fullscreen}>
|
||||
{(fullscreen) => (
|
||||
<Button
|
||||
title="Toggle fullscreen"
|
||||
icon={() =>
|
||||
fullscreen()()
|
||||
? IconTablerLayoutSidebarLeftExpand
|
||||
: IconTablerLayoutSidebarRightExpand
|
||||
}
|
||||
onClick={() => {
|
||||
fullscreen().set((b) => !b);
|
||||
}}
|
||||
classes="hidden md:block"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<ButtonShare qrcode={qrcode} />
|
||||
|
||||
<Button
|
||||
title="Favorite"
|
||||
colors={() =>
|
||||
presets.selected().isFavorite()
|
||||
? "text-amber-500 bg-amber-500/15 hover:bg-amber-500/30"
|
||||
: ""
|
||||
}
|
||||
icon={() =>
|
||||
presets.selected().isFavorite()
|
||||
? IconTablerStarFilled
|
||||
: IconTablerStar
|
||||
}
|
||||
onClick={() => presets.selected().isFavorite.set((b) => !b)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
|
||||
export function Button({
|
||||
title,
|
||||
icon,
|
||||
colors,
|
||||
onClick,
|
||||
disabled,
|
||||
classes,
|
||||
}: {
|
||||
title: string;
|
||||
icon: () => ValidComponent;
|
||||
colors?: () => string;
|
||||
onClick: VoidFunction;
|
||||
disabled?: () => boolean;
|
||||
classes?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
title={title}
|
||||
disabled={disabled?.()}
|
||||
class={classPropToString([
|
||||
colors?.() || (disabled?.() ? "" : "hover:bg-orange-200/15"),
|
||||
!disabled?.() && "group",
|
||||
classes,
|
||||
"flex-none rounded-lg p-2 disabled:opacity-50",
|
||||
])}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Dynamic
|
||||
component={icon()}
|
||||
class="size-[1.125rem] group-active:scale-90"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Button } from "./button";
|
||||
|
||||
export function ButtonShare({ qrcode }: { qrcode: RWS<string> }) {
|
||||
return (
|
||||
<Button
|
||||
title="Share"
|
||||
icon={() => IconTablerShare}
|
||||
onClick={() =>
|
||||
import("lean-qr").then(({ generate }) =>
|
||||
qrcode.set(() =>
|
||||
generate(document.location.href).toDataURL({
|
||||
on: [0xff, 0xff, 0xff, 0xff],
|
||||
off: [0x00, 0x00, 0x00, 0x00],
|
||||
padX: 0,
|
||||
padY: 0,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
import { requestIdleCallbackPossible } from "/src/env";
|
||||
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
|
||||
import { createChart } from "/src/scripts/lightweightCharts/create";
|
||||
import { createSeriesGroup } from "/src/scripts/lightweightCharts/group";
|
||||
import { setMinMaxMarkers } from "/src/scripts/lightweightCharts/markers";
|
||||
import {
|
||||
debouncedUpdateVisiblePriceSeriesType,
|
||||
updateVisiblePriceSeriesType,
|
||||
} from "/src/scripts/lightweightCharts/price";
|
||||
import {
|
||||
initTimeScale,
|
||||
setInitialTimeRange,
|
||||
} from "/src/scripts/lightweightCharts/time";
|
||||
import { setWhitespace } from "/src/scripts/lightweightCharts/whitespace";
|
||||
import { SeriesType } from "/src/scripts/presets/enums";
|
||||
import { colors } from "/src/scripts/utils/colors";
|
||||
import { debounce } from "/src/scripts/utils/debounce";
|
||||
import { createSL } from "/src/scripts/utils/selectableList/static";
|
||||
import { webSockets } from "/src/scripts/ws";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { RadioGroup } from "../../settings";
|
||||
|
||||
export function Chart({
|
||||
activeDatasets,
|
||||
activeIds,
|
||||
charts,
|
||||
chartsDrawn,
|
||||
dark,
|
||||
datasets,
|
||||
exactRange,
|
||||
firstChartSetter,
|
||||
index,
|
||||
lastActiveIndex,
|
||||
lastChartIndex,
|
||||
legendSetter,
|
||||
preset: presetAccessor,
|
||||
priceSeriesType,
|
||||
seriesConfigs,
|
||||
seriesCount,
|
||||
}: {
|
||||
activeDatasets: ReadWriteSignal<ResourceDataset<any, any>[]>;
|
||||
activeIds: RWS<number[]>;
|
||||
charts: ReadWriteSignal<
|
||||
{
|
||||
chart: RWS<IChartApi | undefined>;
|
||||
whitespace: RWS<ISeriesApiAny | undefined>;
|
||||
}[]
|
||||
>;
|
||||
chartsDrawn: Accessor<ReadWriteSignal<boolean>[]>;
|
||||
dark: Accessor<boolean>;
|
||||
datasets: Datasets;
|
||||
exactRange: ReadWriteSignal<TimeRange>;
|
||||
firstChartSetter: Setter<IChartApi | undefined>;
|
||||
index: Accessor<number>;
|
||||
lastActiveIndex: Accessor<number | undefined>;
|
||||
lastChartIndex: Accessor<number>;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
preset: Accessor<Preset>;
|
||||
priceSeriesType: ReadWriteSignal<PriceSeriesType>;
|
||||
seriesConfigs: SeriesConfig[];
|
||||
seriesCount: Accessor<number>;
|
||||
}) {
|
||||
const div = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
const chartIndex = index();
|
||||
|
||||
const isDrawn = chartsDrawn()[chartIndex];
|
||||
const isLastDrawn = createMemo(
|
||||
() => chartsDrawn().findLastIndex((drawn) => drawn()) === chartIndex,
|
||||
);
|
||||
|
||||
const chartPriceModeKey = `chart-price-mode-${chartIndex}` as const;
|
||||
const chartPriceMode = createSL(["Linear", "Log"] as const, {
|
||||
saveable: {
|
||||
key: chartPriceModeKey,
|
||||
mode: "localStorage",
|
||||
},
|
||||
defaultValue: chartIndex === 0 ? "Log" : "Linear",
|
||||
});
|
||||
|
||||
createEffect(
|
||||
on([div, () => charts()[chartIndex]], ([div, chartConfig]) => {
|
||||
if (!div || !chartConfig) return;
|
||||
|
||||
const preset = presetAccessor();
|
||||
const scale = preset.scale;
|
||||
|
||||
const chart = createChart({
|
||||
scale,
|
||||
element: div,
|
||||
dark,
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
console.log("chart: undefined");
|
||||
return;
|
||||
}
|
||||
|
||||
const whitespace = setWhitespace(chart, scale);
|
||||
|
||||
batch(() => {
|
||||
chartConfig.chart.set(chart);
|
||||
chartConfig.whitespace.set(whitespace);
|
||||
|
||||
if (chartIndex === 0) {
|
||||
firstChartSetter(chart);
|
||||
}
|
||||
});
|
||||
|
||||
const range = exactRange();
|
||||
|
||||
setInitialTimeRange({ chart, range });
|
||||
|
||||
if (chartIndex === 0) {
|
||||
initTimeScale({
|
||||
scale,
|
||||
chart,
|
||||
activeIds,
|
||||
exactRange,
|
||||
});
|
||||
|
||||
if (range) {
|
||||
updateVisiblePriceSeriesType({
|
||||
scale,
|
||||
chart,
|
||||
priceSeriesType,
|
||||
timeRange: range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const chartLegend: SeriesLegend[] = [];
|
||||
|
||||
onCleanup(() => {
|
||||
chartLegend.length = 0;
|
||||
});
|
||||
|
||||
const markerCallback = () =>
|
||||
setMinMaxMarkers({
|
||||
scale,
|
||||
visibleRange: exactRange(),
|
||||
legendList: chartLegend,
|
||||
dark,
|
||||
activeIds: activeIds,
|
||||
});
|
||||
|
||||
const debouncedSetMinMaxMarkers = requestIdleCallbackPossible
|
||||
? () => requestIdleCallback(markerCallback)
|
||||
: debounce(
|
||||
markerCallback,
|
||||
seriesCount() * 10 + scale === "date" ? 50 : 100,
|
||||
);
|
||||
|
||||
createEffect(on([exactRange, dark], debouncedSetMinMaxMarkers));
|
||||
|
||||
if (chartIndex === 0) {
|
||||
function createPriceSeries(seriesType: PriceSeriesType) {
|
||||
const datasetPath: AnyDatasetPath = `${scale}-to-price`;
|
||||
|
||||
const dataset = datasets.getOrImport(scale, datasetPath);
|
||||
|
||||
// Don't trigger reactivity by design
|
||||
activeDatasets().push(dataset);
|
||||
|
||||
const title = "Price";
|
||||
|
||||
let seriesConfig: SeriesConfig;
|
||||
|
||||
if (seriesType === "Candlestick") {
|
||||
seriesConfig = {
|
||||
datasetPath,
|
||||
title,
|
||||
seriesType: SeriesType.Candlestick,
|
||||
};
|
||||
} else {
|
||||
seriesConfig = {
|
||||
datasetPath,
|
||||
title,
|
||||
color: colors.white,
|
||||
};
|
||||
}
|
||||
|
||||
const priceSeries = createSeriesGroup({
|
||||
scale,
|
||||
datasets,
|
||||
index: -1,
|
||||
activeIds,
|
||||
seriesConfig,
|
||||
chart,
|
||||
chartLegend,
|
||||
lastActiveIndex,
|
||||
preset,
|
||||
disabled: () => priceSeriesType() !== seriesType,
|
||||
debouncedSetMinMaxMarkers,
|
||||
dark,
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const latest = webSockets.liveKrakenCandle.latest();
|
||||
|
||||
if (!latest) return;
|
||||
|
||||
const index = chunkIdToIndex(scale, latest.year);
|
||||
|
||||
const series = priceSeries.seriesList.at(index)?.();
|
||||
|
||||
series?.update(latest);
|
||||
});
|
||||
|
||||
return priceSeries;
|
||||
}
|
||||
|
||||
const priceCandlestickLegend = createPriceSeries("Candlestick");
|
||||
const priceLineLegend = createPriceSeries("Line");
|
||||
|
||||
createEffect(() => {
|
||||
priceCandlestickLegend.visible.set(priceLineLegend.visible());
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
priceLineLegend.visible.set(priceCandlestickLegend.visible());
|
||||
});
|
||||
}
|
||||
|
||||
[...seriesConfigs].reverse().forEach((seriesConfig, index) => {
|
||||
const dataset = datasets.getOrImport(scale, seriesConfig.datasetPath);
|
||||
|
||||
// Don't trigger reactivity by design
|
||||
activeDatasets().push(dataset);
|
||||
|
||||
createSeriesGroup({
|
||||
scale,
|
||||
datasets,
|
||||
activeIds,
|
||||
index,
|
||||
seriesConfig,
|
||||
chartLegend,
|
||||
chart,
|
||||
preset,
|
||||
lastActiveIndex,
|
||||
debouncedSetMinMaxMarkers,
|
||||
dark,
|
||||
});
|
||||
});
|
||||
|
||||
chartLegend.forEach((legend) => {
|
||||
createEffect(on(legend.visible, debouncedSetMinMaxMarkers));
|
||||
});
|
||||
|
||||
legendSetter((l) => {
|
||||
for (let i = 0; i < chartLegend.length; i++) {
|
||||
l.splice(0, 0, chartLegend[i]);
|
||||
}
|
||||
return l;
|
||||
});
|
||||
|
||||
createEffect(() =>
|
||||
isDrawn.set(() => chartLegend.some((legend) => legend.drawn())),
|
||||
);
|
||||
|
||||
createEffect(() =>
|
||||
chart.timeScale().applyOptions({
|
||||
visible: isLastDrawn(),
|
||||
}),
|
||||
);
|
||||
|
||||
createEffect(() =>
|
||||
chart.priceScale("right").applyOptions({
|
||||
mode: chartPriceMode.selected() === "Linear" ? 0 : 1,
|
||||
}),
|
||||
);
|
||||
|
||||
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
|
||||
if (!logicalRange) return;
|
||||
|
||||
// Must be the chart with the visible timeScale
|
||||
if (chartIndex === lastChartIndex()) {
|
||||
debouncedUpdateVisiblePriceSeriesType({
|
||||
scale,
|
||||
chart,
|
||||
logicalRange,
|
||||
priceSeriesType,
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let otherChartIndex = 0;
|
||||
otherChartIndex <= lastChartIndex();
|
||||
otherChartIndex++
|
||||
) {
|
||||
if (chartIndex !== otherChartIndex) {
|
||||
const chart = charts()[otherChartIndex].chart();
|
||||
|
||||
chart?.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.subscribeCrosshairMove(({ time, sourceEvent }) => {
|
||||
// Don't override crosshair position from scroll event
|
||||
if (time && !sourceEvent) return;
|
||||
|
||||
for (
|
||||
let otherChartIndex = 0;
|
||||
otherChartIndex <= lastChartIndex();
|
||||
otherChartIndex++
|
||||
) {
|
||||
const { whitespace: _whitespace, chart: _otherChart } =
|
||||
charts()[otherChartIndex];
|
||||
|
||||
const otherChart = _otherChart();
|
||||
const whitespace = _whitespace();
|
||||
|
||||
if (otherChart && whitespace && chartIndex !== otherChartIndex) {
|
||||
if (time) {
|
||||
otherChart.setCrosshairPosition(NaN, time, whitespace);
|
||||
} else {
|
||||
// No time when mouse goes outside the chart
|
||||
otherChart.clearCrosshairPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger reactivity now
|
||||
activeDatasets.set((l) => l);
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: isLastDrawn() ? "100%" : "calc(100% - 62px)",
|
||||
}}
|
||||
class={classPropToString([
|
||||
isDrawn()
|
||||
? [
|
||||
"max-h-full",
|
||||
// isLastDrawn() ? "mb-[-2px]"
|
||||
]
|
||||
: "max-h-0",
|
||||
"relative h-full min-h-0 w-full cursor-crosshair",
|
||||
])}
|
||||
>
|
||||
<div ref={div.set} class="size-full" />
|
||||
|
||||
<Show when={isDrawn()}>
|
||||
<div class="pointer-events-none absolute left-0 top-0 z-10 flex items-center space-x-2 px-6 text-xs">
|
||||
<span>
|
||||
{chartIndex === 0
|
||||
? ("US Dollars" satisfies Unit)
|
||||
: presetAccessor().unit}
|
||||
</span>
|
||||
<span class="off">—</span>
|
||||
<RadioGroup size="xs" title={chartPriceModeKey} sl={chartPriceMode} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
|
||||
import {
|
||||
getInitialTimeRange,
|
||||
setActiveIds,
|
||||
} from "/src/scripts/lightweightCharts/time";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Chart } from "./chart";
|
||||
|
||||
export function Charts({
|
||||
firstChartSetter,
|
||||
preset,
|
||||
datasets,
|
||||
legendSetter,
|
||||
dark,
|
||||
activeIds,
|
||||
}: {
|
||||
firstChartSetter: Setter<IChartApi | undefined>;
|
||||
preset: Accessor<Preset>;
|
||||
datasets: Datasets;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
dark: Accessor<boolean>;
|
||||
activeIds: RWS<number[]>;
|
||||
}) {
|
||||
const scale = createMemo(() => preset().scale);
|
||||
const exactRange = createRWS(getInitialTimeRange(scale()));
|
||||
const priceSeriesType = createRWS<PriceSeriesType>("Candlestick");
|
||||
const activeDatasets = createRWS([] as ResourceDataset<any, any>[], {
|
||||
equals: false,
|
||||
});
|
||||
const chartSeriesConfigs = createRWS([] as SeriesConfig[][], {
|
||||
equals: false,
|
||||
});
|
||||
const charts = createRWS(
|
||||
[] as {
|
||||
chart: RWS<IChartApi | undefined>;
|
||||
whitespace: RWS<ISeriesApiAny | undefined>;
|
||||
}[],
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const lastChartIndex = createMemo(() => chartSeriesConfigs().length - 1);
|
||||
const seriesCount = createMemo(() =>
|
||||
chartSeriesConfigs().reduce(
|
||||
(acc, l) => (acc += l.length),
|
||||
1, // Because of price series
|
||||
),
|
||||
);
|
||||
const lastActiveIndex = createMemo(() => {
|
||||
const last = activeIds().at(-1);
|
||||
return last !== undefined
|
||||
? chunkIdToIndex(preset().scale, last)
|
||||
: undefined;
|
||||
});
|
||||
const chartsDrawn = createMemo(() =>
|
||||
chartSeriesConfigs().map((_) => createRWS(true)),
|
||||
);
|
||||
|
||||
createEffect(
|
||||
on([activeIds, activeDatasets], ([ids, activeDatasets]) => {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
for (let j = 0; j < activeDatasets.length; j++) {
|
||||
activeDatasets[j].fetch(id);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
createEffect(
|
||||
on(preset, (preset) => {
|
||||
const scale = preset.scale;
|
||||
|
||||
exactRange.set(getInitialTimeRange(scale));
|
||||
|
||||
chartSeriesConfigs.set(
|
||||
[preset.top || [], preset.bottom].flatMap((list) =>
|
||||
list ? [list] : [],
|
||||
),
|
||||
);
|
||||
|
||||
charts.set(() =>
|
||||
new Array(chartSeriesConfigs().length).fill(undefined).map(() => ({
|
||||
chart: createRWS(undefined as IChartApi | undefined),
|
||||
whitespace: createRWS(undefined as ISeriesApiAny | undefined),
|
||||
})),
|
||||
);
|
||||
|
||||
setActiveIds({
|
||||
exactRange: exactRange(),
|
||||
activeIds,
|
||||
});
|
||||
|
||||
legendSetter(() => []);
|
||||
}),
|
||||
);
|
||||
|
||||
onCleanup(() => {
|
||||
firstChartSetter(undefined);
|
||||
|
||||
charts().map(({ chart, whitespace }) => {
|
||||
chart()?.remove();
|
||||
chart.set(undefined);
|
||||
whitespace.set(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<For
|
||||
each={chartSeriesConfigs().filter(
|
||||
(configs, index) => index === 0 || configs.length !== 0,
|
||||
)}
|
||||
>
|
||||
{(seriesConfigs, index) => (
|
||||
<Chart
|
||||
activeDatasets={activeDatasets}
|
||||
activeIds={activeIds}
|
||||
charts={charts}
|
||||
chartsDrawn={chartsDrawn}
|
||||
dark={dark}
|
||||
datasets={datasets}
|
||||
exactRange={exactRange}
|
||||
firstChartSetter={firstChartSetter}
|
||||
index={index}
|
||||
lastActiveIndex={lastActiveIndex}
|
||||
lastChartIndex={lastChartIndex}
|
||||
legendSetter={legendSetter}
|
||||
preset={preset}
|
||||
priceSeriesType={priceSeriesType}
|
||||
seriesConfigs={seriesConfigs}
|
||||
seriesCount={seriesCount}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
);
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Scrollable } from "../../scrollable";
|
||||
|
||||
const transparency = "44";
|
||||
|
||||
export function Legend({
|
||||
scale,
|
||||
legend: legendList,
|
||||
dark,
|
||||
activeIds,
|
||||
}: {
|
||||
scale: Accessor<ResourceScale>;
|
||||
legend: Accessor<SeriesLegend[]>;
|
||||
dark: Accessor<boolean>;
|
||||
activeIds: Accessor<number[]>;
|
||||
}) {
|
||||
const hovered = createRWS<SeriesLegend | undefined>(undefined);
|
||||
|
||||
let toggle = false;
|
||||
|
||||
return (
|
||||
<Scrollable classes="items-center gap-7">
|
||||
<For each={legendList()}>
|
||||
{(legend) => {
|
||||
createEffect(() => {
|
||||
const range = activeIds();
|
||||
|
||||
for (let i = 0; i < range.length; i++) {
|
||||
const id = range[i];
|
||||
|
||||
const initialColors = {} as any;
|
||||
const darkenColors = {} as any;
|
||||
|
||||
const chunkIndex = chunkIdToIndex(scale(), id);
|
||||
|
||||
const series = legend.seriesList.at(chunkIndex)?.();
|
||||
|
||||
if (!series) return;
|
||||
|
||||
const seriesOptions = series.options();
|
||||
|
||||
if (!seriesOptions) continue;
|
||||
|
||||
Object.entries(seriesOptions).forEach(([k, v]) => {
|
||||
if (k.toLowerCase().includes("color") && v) {
|
||||
if (typeof v === "string" && !v.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
|
||||
v = (v as string).substring(0, 7);
|
||||
initialColors[k] = v;
|
||||
darkenColors[k] = `${v}${transparency}`;
|
||||
} else if (k === "lastValueVisible" && v) {
|
||||
initialColors[k] = true;
|
||||
darkenColors[k] = false;
|
||||
}
|
||||
});
|
||||
|
||||
createEffect((wasHovering: boolean) => {
|
||||
const hoveredLegend = hovered();
|
||||
const hovering = !!hovered();
|
||||
|
||||
if (wasHovering === hovering) {
|
||||
return hovering;
|
||||
}
|
||||
|
||||
if (hoveredLegend) {
|
||||
if (hoveredLegend.title !== legend.title) {
|
||||
series.applyOptions(darkenColors);
|
||||
}
|
||||
} else {
|
||||
series.applyOptions(initialColors);
|
||||
}
|
||||
|
||||
return hovering;
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
|
||||
let previousClickTime: number = 0;
|
||||
|
||||
return (
|
||||
<Show when={!legend.disabled()}>
|
||||
<div class="flex flex-none items-center space-x-1.5">
|
||||
<button
|
||||
title="Click to toggle, double click to focus"
|
||||
onMouseEnter={() => legend.visible() && hovered.set(legend)}
|
||||
onMouseLeave={() => hovered.set(undefined)}
|
||||
onTouchStart={() => legend.visible() && hovered.set(legend)}
|
||||
onTouchEnd={() => hovered.set(undefined)}
|
||||
onClick={() => {
|
||||
const currentClickTime = new Date().getTime();
|
||||
|
||||
if (currentClickTime - previousClickTime > 300) {
|
||||
legend.visible.set((visible) => !visible);
|
||||
} else {
|
||||
legendList().forEach((_legend) => {
|
||||
if (_legend.title != legend.title) {
|
||||
_legend.visible.set(toggle);
|
||||
}
|
||||
});
|
||||
|
||||
legend.visible.set(true);
|
||||
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
previousClickTime = currentClickTime;
|
||||
|
||||
if (legend.visible()) {
|
||||
hovered.set(legend);
|
||||
} else {
|
||||
hovered.set(undefined);
|
||||
}
|
||||
}}
|
||||
class="flex flex-none items-center space-x-1.5 active:scale-[0.975]"
|
||||
>
|
||||
<span
|
||||
class="flex size-3 flex-col overflow-hidden rounded-full"
|
||||
style={{
|
||||
opacity: legend.visible() ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<For
|
||||
each={
|
||||
Array.isArray(legend.color)
|
||||
? legend.color.map((c) => c(dark))
|
||||
: [legend.color(dark)]
|
||||
}
|
||||
>
|
||||
{(color) => (
|
||||
<span
|
||||
class="w-full flex-1"
|
||||
style={{
|
||||
"background-color": color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
<span
|
||||
class="text-sm font-medium decoration-wavy decoration-[1.5px]"
|
||||
style={{
|
||||
"text-decoration-line": !legend.visible()
|
||||
? "line-through"
|
||||
: undefined,
|
||||
"text-decoration-color": "var(--color)",
|
||||
color: !legend.visible() ? "var(--off-color)" : undefined,
|
||||
}}
|
||||
>
|
||||
{legend.title}
|
||||
</span>
|
||||
</button>
|
||||
<Show when={legend.dataset.url}>
|
||||
{(url) => (
|
||||
<a
|
||||
title="Dataset"
|
||||
class="inline-flex size-4 flex-col overflow-hidden active:scale-[0.975]"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
href={url()}
|
||||
target={
|
||||
url()?.startsWith("/") || url()?.startsWith("http")
|
||||
? "_blank"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<IconTablerDownload />
|
||||
</a>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
import { GENESIS_DAY } from "/src/scripts/lightweightCharts/whitespace";
|
||||
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Box } from "../../box";
|
||||
import { Scrollable } from "../../scrollable";
|
||||
|
||||
const DELAY = 1;
|
||||
const MULTIPLIER = DELAY / 1000;
|
||||
const LEFT = -1;
|
||||
const RIGHT = 1;
|
||||
|
||||
export function TimeScale({
|
||||
scale,
|
||||
firstChart,
|
||||
}: {
|
||||
scale: Accessor<ResourceScale>;
|
||||
firstChart: RWS<IChartApi | undefined>;
|
||||
}) {
|
||||
const today = new Date();
|
||||
|
||||
const disabled = createMemo(() => !firstChart());
|
||||
|
||||
const scrollDirection = createRWS(0);
|
||||
|
||||
const timeScale = createMemo(() => {
|
||||
const chart = firstChart();
|
||||
if (!chart) return undefined;
|
||||
return chart.timeScale();
|
||||
});
|
||||
|
||||
let interval: number | undefined;
|
||||
|
||||
function createScrollLoop() {
|
||||
clearInterval(interval);
|
||||
const direction = scrollDirection();
|
||||
if (!direction) return;
|
||||
|
||||
// @ts-ignore
|
||||
interval = setInterval(() => {
|
||||
const time = timeScale();
|
||||
|
||||
if (!time) return;
|
||||
|
||||
const range = time.getVisibleLogicalRange();
|
||||
|
||||
if (!range) return;
|
||||
|
||||
const speed = (range.to - range.from) * MULTIPLIER * direction;
|
||||
|
||||
// @ts-ignore
|
||||
range.from += speed;
|
||||
// @ts-ignore
|
||||
range.to += speed;
|
||||
|
||||
time.setVisibleLogicalRange(range);
|
||||
}, DELAY);
|
||||
}
|
||||
|
||||
onCleanup(() => clearInterval(interval));
|
||||
|
||||
return (
|
||||
<Box padded={false} spaced={false} classes="short:hidden text-sm">
|
||||
<div class="flex items-center p-1.5">
|
||||
<Button
|
||||
square
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
scrollDirection.set((v) => (v === LEFT ? 0 : LEFT));
|
||||
createScrollLoop();
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={scrollDirection() === LEFT}
|
||||
fallback={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path d="M8.5 4.75a.75.75 0 0 0-1.107-.66l-6 3.25a.75.75 0 0 0 0 1.32l6 3.25a.75.75 0 0 0 1.107-.66V8.988l5.393 2.921A.75.75 0 0 0 15 11.25v-6.5a.75.75 0 0 0-1.107-.66L8.5 7.013V4.75Z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
<IconTablerPlayerPauseFilled class="size-5" />
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mr-2 border-l" />
|
||||
<Scrollable classes="space-x-2">
|
||||
<Switch>
|
||||
<Match when={scale() === "date"}>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ scale: scale(), timeScale })}
|
||||
>
|
||||
All Time
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 7 })
|
||||
}
|
||||
>
|
||||
1 Week
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 30 })
|
||||
}
|
||||
>
|
||||
1 Month
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 3 * 30 })
|
||||
}
|
||||
>
|
||||
3 Months
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 6 * 30 })
|
||||
}
|
||||
>
|
||||
6 Months
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
scale: scale(),
|
||||
timeScale,
|
||||
days: Math.ceil(
|
||||
(today.getTime() -
|
||||
new Date(`${today.getUTCFullYear()}-01-01`).getTime()) /
|
||||
ONE_DAY_IN_MS,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Year To Date
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 365 })
|
||||
}
|
||||
>
|
||||
1 Year
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 2 * 365 })
|
||||
}
|
||||
>
|
||||
2 Years
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 4 * 365 })
|
||||
}
|
||||
>
|
||||
4 Years
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 8 * 365 })
|
||||
}
|
||||
>
|
||||
8 Years
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(
|
||||
new Date().getFullYear() - new Date("2009-01-01").getFullYear(),
|
||||
)
|
||||
.fill(0)
|
||||
.map((_, index) => index + 2009)
|
||||
.reverse()}
|
||||
>
|
||||
{(year) => (
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, year })
|
||||
}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={scale() === "height"}>
|
||||
<Button minWidth disabled={() => true} onClick={() => {}}>
|
||||
24h
|
||||
</Button>
|
||||
<Button minWidth disabled={() => true} onClick={() => {}}>
|
||||
48h
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(9)
|
||||
.fill(0)
|
||||
.flatMap((_, i) => [i, i + 0.5])
|
||||
.reverse()}
|
||||
>
|
||||
{(i) => (
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
scale: scale(),
|
||||
timeScale,
|
||||
range: {
|
||||
from: i * 100_000,
|
||||
to: (i + 0.5) * 100_000,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{`${100 * (i + 0.5)}k`}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Scrollable>
|
||||
<div class="ml-2 border-l" />
|
||||
<div class="flex items-center p-1.5">
|
||||
<Button
|
||||
square
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
scrollDirection.set((v) => (v === RIGHT ? 0 : RIGHT));
|
||||
createScrollLoop();
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={scrollDirection() === RIGHT}
|
||||
fallback={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path d="M2.53 3.956A1 1 0 0 0 1 4.804v6.392a1 1 0 0 0 1.53.848l5.113-3.196c.16-.1.279-.233.357-.383v2.73a1 1 0 0 0 1.53.849l5.113-3.196a1 1 0 0 0 0-1.696L9.53 3.956A1 1 0 0 0 8 4.804v2.731a.992.992 0 0 0-.357-.383L2.53 3.956Z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
<IconTablerPlayerPauseFilled class="size-5" />
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({
|
||||
onClick,
|
||||
disabled,
|
||||
children,
|
||||
minWidth,
|
||||
square,
|
||||
}: ParentProps & {
|
||||
onClick: VoidFunction;
|
||||
disabled?: Accessor<boolean>;
|
||||
minWidth?: boolean;
|
||||
square?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
class={classPropToString([
|
||||
minWidth && "min-w-20",
|
||||
!disabled?.() && "active:scale-95",
|
||||
"flex-shrink-0 flex-grow whitespace-nowrap p-1.5 font-medium",
|
||||
])}
|
||||
onClick={onClick}
|
||||
disabled={disabled?.()}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function setTimeScale({
|
||||
timeScale,
|
||||
scale,
|
||||
days,
|
||||
year,
|
||||
range,
|
||||
}: {
|
||||
timeScale: Accessor<ITimeScaleApi<Time> | undefined>;
|
||||
scale: ResourceScale;
|
||||
days?: number;
|
||||
year?: number;
|
||||
range?: { from: number; to: number };
|
||||
}) {
|
||||
if (scale === "date") {
|
||||
let from = new Date();
|
||||
let to = new Date();
|
||||
|
||||
if (year) {
|
||||
from = new Date(`${year}-01-01`);
|
||||
to = new Date(`${year}-12-31`);
|
||||
} else if (days) {
|
||||
from.setDate(from.getUTCDate() - days);
|
||||
} else {
|
||||
from = new Date(GENESIS_DAY);
|
||||
}
|
||||
|
||||
timeScale()?.setVisibleRange({
|
||||
from: (from.getTime() / 1000) as Time,
|
||||
to: (to.getTime() / 1000) as Time,
|
||||
});
|
||||
} else if (scale === "height") {
|
||||
if (range) {
|
||||
timeScale()?.setVisibleRange({
|
||||
from: range.from as Time,
|
||||
to: range.to as Time,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export function Title({ presets }: { presets: Presets }) {
|
||||
return (
|
||||
<div
|
||||
class="flex-0 -mx-6 -mb-4 flex items-center overflow-x-auto px-6 pb-4 pt-1"
|
||||
style={{
|
||||
"scrollbar-width": "thin",
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 whitespace-nowrap">
|
||||
<h1 class="text-lg font-bold md:text-xl">{presets.selected().title}</h1>
|
||||
<h3 class="off">{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createWasIdleAccessor } from "/src/solid/idle";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Box } from "../box";
|
||||
import { Actions } from "./components/actions";
|
||||
import { Legend } from "./components/legend";
|
||||
import { TimeScale } from "./components/timeScale";
|
||||
import { Title } from "./components/title";
|
||||
|
||||
export function ChartFrame({
|
||||
presets,
|
||||
datasets,
|
||||
hide,
|
||||
qrcode,
|
||||
standalone,
|
||||
fullscreen,
|
||||
dark,
|
||||
}: {
|
||||
presets: Presets;
|
||||
hide?: Accessor<boolean>;
|
||||
qrcode: RWS<string>;
|
||||
datasets: Datasets;
|
||||
fullscreen?: RWS<boolean>;
|
||||
dark: Accessor<boolean>;
|
||||
standalone: boolean;
|
||||
}) {
|
||||
const legend = createRWS<SeriesLegend[]>([], { equals: false });
|
||||
|
||||
const firstChart = createRWS<IChartApi | undefined>(undefined);
|
||||
|
||||
const scale = createMemo(() => presets.selected().scale);
|
||||
|
||||
const activeIds = createRWS([] as number[], { equals: false });
|
||||
|
||||
const wasIdle = createWasIdleAccessor();
|
||||
|
||||
const Charts = lazy(() =>
|
||||
import("./components/charts").then((d) => ({ default: d.Charts })),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
"frame flex size-full min-h-0 flex-1 flex-col",
|
||||
])}
|
||||
style={{
|
||||
display: (hide ? hide() : false) ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<Title presets={presets} />
|
||||
|
||||
<div class="border-t" />
|
||||
|
||||
<Legend legend={legend} scale={scale} activeIds={activeIds} dark={dark} />
|
||||
|
||||
<div class="!mt-4 flex min-h-0 flex-1 flex-col">
|
||||
<div class="relative -ml-6 -mr-8 flex min-h-0 flex-1 flex-col pb-2">
|
||||
<div class="pointer-events-none absolute inset-x-0 top-0 z-10 h-6 w-full flex-none bg-gradient-to-t from-transparent to-[var(--background-color)]" />
|
||||
<Show when={wasIdle()}>
|
||||
<Charts
|
||||
firstChartSetter={firstChart.set}
|
||||
datasets={datasets}
|
||||
legendSetter={legend.set}
|
||||
preset={presets.selected}
|
||||
dark={dark}
|
||||
activeIds={activeIds}
|
||||
/>
|
||||
</Show>
|
||||
<div class="pointer-events-none absolute bottom-9 right-0 z-10 h-6 w-[80px] flex-none bg-gradient-to-b from-transparent to-[var(--background-color)]" />
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 z-10 h-full w-6 flex-none bg-gradient-to-r from-[var(--background-color)] from-5% to-transparent" />
|
||||
</div>
|
||||
|
||||
<TimeScale firstChart={firstChart} scale={scale} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export function Counter({
|
||||
count,
|
||||
name,
|
||||
setRef,
|
||||
}: {
|
||||
count: () => number;
|
||||
name: string;
|
||||
setRef?: Setter<HTMLDivElement | undefined>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
ref={setRef}
|
||||
class="text-orange-100/75"
|
||||
style={{
|
||||
"border-style": count() ? "dashed" : "none",
|
||||
}}
|
||||
>
|
||||
Counted{" "}
|
||||
<span class="font-medium text-orange-400/75">
|
||||
{count().toLocaleString("en-us")}
|
||||
</span>{" "}
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Line } from "../../line";
|
||||
|
||||
export function File({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
active,
|
||||
depth,
|
||||
onClick,
|
||||
favorite,
|
||||
visited,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: JSXElement;
|
||||
active: Accessor<boolean>;
|
||||
depth: number;
|
||||
onClick: VoidFunction;
|
||||
favorite: Accessor<boolean>;
|
||||
visited: Accessor<boolean>;
|
||||
}) {
|
||||
const tail = createMemo(() =>
|
||||
favorite() ? (
|
||||
// <span class="p-1">
|
||||
<IconTablerStarFilled class="orange size-3" />
|
||||
) : // </span>
|
||||
!visited() ? (
|
||||
<span class="ml-1.5 rounded-full bg-orange-500 p-[3px] text-transparent" />
|
||||
) : undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<Line
|
||||
id={id}
|
||||
depth={depth}
|
||||
active={active}
|
||||
name={name}
|
||||
icon={() => icon}
|
||||
onClick={onClick}
|
||||
tail={tail}
|
||||
/>
|
||||
);
|
||||
}
|
||||