diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000..4082fed --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,32 @@ +dolt/ +embeddeddolt/ +proxieddb/ +backup/ +bd.sock +bd.sock.startlock +sync-state.json +last-touched +.exclusive-lock +daemon.* +push-state.json +*.lock +.beads-credential-key +.local_version +proxied_server_client_info.json +redirect +.sync.lock +export-state/ +export-state.json +last_pull +ephemeral.sqlite3* +dolt-server.* +dolt-pprof/ +*.corrupt.backup/ +.env +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm +db.sqlite +bd.db diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..700e266 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1 @@ +sync.remote: "git+ssh://git@github.com/kakulukia/bitfeed.git" diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..5d0943a --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,7 @@ +{ + "database": "dolt", + "backend": "dolt", + "dolt_mode": "embedded", + "dolt_database": "bitfeed", + "project_id": "451af4ca-8ba0-4001-b42c-321c0623977e" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c0f942f..85ed3e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,21 @@ TIMESTAMP.txt + +# Local agent/tool state +.agents/ +.claude/ +.codex/ +.codegraph/ +AGENTS.md +CLAUDE.md + +# Beads / Dolt files +.dolt/ +*.db +.beads-credential-key +.beads/proxieddb/ +.beads/README.md +.beads/hooks/ +.beads/interactions.jsonl + +# bv (beads viewer) local config and caches +.bv/ diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..3fc358a --- /dev/null +++ b/.mise.toml @@ -0,0 +1,11 @@ +[tasks."server:compile"] +description = "Compile the server with the Dockerfile toolchain, Elixir 1.11 / OTP 23" +run = "container build --tag bitfeed-server:local server" + +[tasks."server:test"] +description = "Run server tests inside the Elixir 1.11 / OTP 23 container toolchain" +run = "server/scripts/test-elixir11-container.sh" + +[tasks."server:test-mempool"] +description = "Run the mempool regression test inside the Elixir 1.11 / OTP 23 container toolchain" +run = "server/scripts/test-elixir11-container.sh test/mempool_test.exs" diff --git a/client/package-lock.json b/client/package-lock.json index 364275e..5f362a3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitfeed-client", - "version": "2.2.1", + "version": "2.3.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bitfeed-client", - "version": "2.2.1", + "version": "2.3.4", "dependencies": { "@babel/core": "^7.16.5", "@babel/preset-env": "^7.16.5", @@ -37,6 +37,9 @@ "svelte": "^3.44.3", "svelte-preprocess": "^4.10.1", "svelte-select": "^4.4.7" + }, + "devDependencies": { + "@rollup/plugin-json": "^4.1.0" } }, "node_modules/@babel/code-frame": { @@ -1595,6 +1598,19 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/@rollup/plugin-json": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.0.8" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.1.tgz", @@ -2521,6 +2537,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -3757,19 +3787,6 @@ "rollup": "^2.0.0" } }, - "node_modules/rollup-plugin-terser/node_modules/acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", - "optional": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/rollup-plugin-terser/node_modules/source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", @@ -5398,6 +5415,15 @@ "integrity": "sha512-x0qpNXxbmGa9Jnl4OX89AORPe2T/a4DqNK69BGRnEdaPKq6MdiUXSTam/eCkF5DxkQGcRcPq0L4vzr/E3q4mVA==", "requires": {} }, + "@rollup/plugin-json": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.8" + } + }, "@rollup/plugin-node-resolve": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.1.tgz", @@ -6125,6 +6151,12 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7088,13 +7120,6 @@ "terser": "^5.0.0" }, "dependencies": { - "acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", - "optional": true, - "peer": true - }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", diff --git a/client/package.json b/client/package.json index 710e91c..99a9996 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,8 @@ "scripts": { "build": "rollup -c", "dev": "rollup -c -w", - "start": "sirv public --single" + "start": "sirv public --single", + "start:proxy": "node scripts/umbrel-dev-server.mjs" }, "dependencies": { "@babel/core": "^7.16.5", diff --git a/client/scripts/umbrel-dev-server.mjs b/client/scripts/umbrel-dev-server.mjs new file mode 100644 index 0000000..a1f1840 --- /dev/null +++ b/client/scripts/umbrel-dev-server.mjs @@ -0,0 +1,94 @@ +import fs from 'node:fs' +import http from 'node:http' +import https from 'node:https' +import net from 'node:net' +import path from 'node:path' +import tls from 'node:tls' +import { fileURLToPath } from 'node:url' + +const port = Number(process.env.PORT || 5001) +const host = process.env.HOST || '127.0.0.1' +const targetUrl = (value, fallbackProtocol) => + new URL(value.includes('://') ? value : `${fallbackProtocol}://${value}`) +const wsTarget = targetUrl(process.env.BITFEED_WS_TARGET || 'wss://bits.monospace.live', 'ws') +const apiTarget = targetUrl(process.env.BITFEED_API_TARGET || 'https://bits.monospace.live', 'https') +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../public/build') + +const types = { + '.css': 'text/css', + '.html': 'text/html', + '.ico': 'image/x-icon', + '.js': 'text/javascript', + '.json': 'application/json', + '.map': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.webmanifest': 'application/manifest+json', + '.xml': 'application/xml', +} + +function sendFile(res, file) { + res.writeHead(200, { + 'content-type': types[path.extname(file)] || 'application/octet-stream', + 'cache-control': 'no-store', + }) + fs.createReadStream(file).pipe(res) +} + +function serveStatic(req, res) { + const pathname = decodeURIComponent(new URL(req.url, `http://${req.headers.host}`).pathname) + const candidate = path.resolve(root, `.${pathname}`) + const file = candidate.startsWith(root) && fs.existsSync(candidate) && fs.statSync(candidate).isFile() + ? candidate + : path.join(root, 'index.html') + sendFile(res, file) +} + +function proxyApi(req, res) { + const upstreamUrl = new URL(req.url, apiTarget) + const client = upstreamUrl.protocol === 'https:' ? https : http + const upstream = client.request(upstreamUrl, { + method: req.method, + headers: { ...req.headers, host: upstreamUrl.host }, + }, upstreamRes => { + const headers = { ...upstreamRes.headers } + delete headers.connection + delete headers['transfer-encoding'] + res.writeHead(upstreamRes.statusCode || 502, headers) + upstreamRes.pipe(res) + }) + upstream.on('error', error => { + res.writeHead(502, { 'content-type': 'text/plain' }) + res.end(error.message) + }) + req.pipe(upstream) +} + +const server = http.createServer((req, res) => { + if (req.url.startsWith('/api/')) proxyApi(req, res) + else serveStatic(req, res) +}) + +server.on('upgrade', (req, socket, head) => { + if (!req.url.startsWith('/ws/')) return socket.destroy() + const upstreamPort = Number(wsTarget.port || (wsTarget.protocol === 'wss:' ? 443 : 80)) + const writeUpgrade = () => { + upstream.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`) + for (const [name, value] of Object.entries({ ...req.headers, host: wsTarget.host })) { + upstream.write(`${name}: ${value}\r\n`) + } + upstream.write('\r\n') + if (head.length) upstream.write(head) + socket.pipe(upstream).pipe(socket) + } + const upstream = wsTarget.protocol === 'wss:' + ? tls.connect({ host: wsTarget.hostname, port: upstreamPort, servername: wsTarget.hostname }, writeUpgrade) + : net.connect({ host: wsTarget.hostname, port: upstreamPort }, writeUpgrade) + upstream.on('error', () => socket.destroy()) +}) + +server.listen(port, host, () => { + console.log(`Bitfeed dev server: http://${host}:${port}`) + console.log(` /ws -> ${wsTarget.origin}`) + console.log(` /api -> ${apiTarget.origin}`) +}) diff --git a/client/src/components/BlockInfo.svelte b/client/src/components/BlockInfo.svelte index dd8943d..634b01a 100644 --- a/client/src/components/BlockInfo.svelte +++ b/client/src/components/BlockInfo.svelte @@ -5,10 +5,10 @@ import { createEventDispatcher } from 'svelte' import Icon from '../components/Icon.svelte' import closeIcon from '../assets/icon/cil-x-circle.svg' - import { shortBtcFormat, longBtcFormat, dateFormat, numberFormat } from '../utils/format.js' - import { exchangeRates, settings, blocksEnabled, latestBlockHeight, blockTransitionDirection, loading, freezeResize, pageWidth, pageHeight } from '../stores.js' + import { shortBtcFormat, longBtcFormat, dateFormat, numberFormat, feeRateFormat } from '../utils/format.js' + import { exchangeRates, settings, blocksEnabled, latestBlockHeight, blockTransitionDirection, loading, freezeResize, fullscreenActive, pageWidth, pageHeight, overlay, explorerBlock, urlPath } from '../stores.js' import { formatCurrency } from '../utils/fx.js' - import { searchBlockHeight } from '../utils/search.js' + import { fetchBlockByHeight } from '../utils/search.js' const dispatch = createEventDispatcher() @@ -17,8 +17,15 @@ export let block export let visible const newBlockDelay = 2000 + const blockNavigationAnimationMs = 2700 // 200ms delay + 2000ms duration + 500ms jitter + const blockNavigationQueueDelayMs = 500 let restoring = false let formattedBlockValue = '' + let navigationQueue = [] + let navigationRunning = false + let activeNavigationHeight = null + let navigationAnimationUntil = 0 + let escapePending = false let compactView let landscape @@ -117,7 +124,7 @@ function formatFee (n) { if (n) { - return numberFormat.format(n.toFixed(2)) + return feeRateFormat.format(n) } else return '0' } @@ -130,25 +137,113 @@ } } - async function explorePrevBlock (e) { - e.preventDefault() - if (!$loading && block) { - loading.increment() - await searchBlockHeight(block.height - 1) + async function loadNavigationBlock (height) { + loading.increment() + try { + return await fetchBlockByHeight(height) + } catch (error) { + console.log('error fetching block ', error) + return null + } finally { loading.decrement() } } - async function exploreNextBlock (e) { - e.preventDefault() - if (!$loading && block) { - if (block.height + 1 < $latestBlockHeight) { - loading.increment() - await searchBlockHeight(block.height + 1) - loading.decrement() - } else { - dispatch('quitExploring') + function queueBlockNavigation (direction) { + if (!block || escapePending) return + + const lastQueued = navigationQueue[navigationQueue.length - 1] + const fromHeight = lastQueued ? lastQueued.height : (activeNavigationHeight != null ? activeNavigationHeight : block.height) + const height = fromHeight + (direction === 'prev' ? -1 : 1) + if (height < 0 || height > $latestBlockHeight) return + + navigationQueue.push({ + height, + blockPromise: height === $latestBlockHeight ? null : loadNavigationBlock(height) + }) + runNavigationQueue() + } + + function queueEscape () { + navigationQueue = [] + escapePending = true + runNavigationQueue() + } + + async function runNavigationQueue () { + if (navigationRunning) return + navigationRunning = true + + try { + while (navigationQueue.length || escapePending) { + if (escapePending) { + const remainingAnimationMs = navigationAnimationUntil - Date.now() + if (remainingAnimationMs > 0) await new Promise(resolve => setTimeout(resolve, remainingAnimationMs)) + escapePending = false + navigationAnimationUntil = 0 + hideBlock() + break + } + + const navigation = navigationQueue.shift() + activeNavigationHeight = navigation.height + const isLatest = navigation.height === $latestBlockHeight + const nextBlock = isLatest ? null : await (navigation.blockPromise || loadNavigationBlock(navigation.height)) + + if (escapePending) { + activeNavigationHeight = null + continue + } + + if (isLatest) { + dispatch('quitExploring') + } else if (nextBlock && nextBlock.id) { + urlPath.set(`/block/height/${navigation.height}`) + overlay.set(null) + explorerBlock.set(nextBlock) + } else { + activeNavigationHeight = null + continue + } + + navigationAnimationUntil = Date.now() + blockNavigationAnimationMs + await new Promise(resolve => setTimeout(resolve, blockNavigationQueueDelayMs)) + activeNavigationHeight = null } + } finally { + activeNavigationHeight = null + navigationRunning = false + if (navigationQueue.length || escapePending) runNavigationQueue() + } + } + + function explorePrevBlock (e) { + e.preventDefault() + queueBlockNavigation('prev') + } + + function exploreNextBlock (e) { + e.preventDefault() + queueBlockNavigation('next') + } + + function isEditableTarget (target) { + if (!target) return false + const tag = target.tagName + return target.isContentEditable || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' + } + + function handleKeydown (e) { + if (!visible || !block || !$blocksEnabled || $overlay || e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || isEditableTarget(e.target)) return + if (e.key === 'Escape') { + e.preventDefault() + queueEscape() + } else if (e.key === 'ArrowLeft' && hasPrevBlock) { + e.preventDefault() + queueBlockNavigation('prev') + } else if (e.key === 'ArrowRight' && hasNextBlock) { + e.preventDefault() + queueBlockNavigation('next') } } @@ -167,6 +262,8 @@ cursor: pointer; pointer-events: all; font-size: 1.2em; + opacity: var(--block-control-opacity, 1); + transition: opacity 250ms, top 1000ms, bottom 1000ms, left 1000ms, right 1000ms, margin 1000ms, transform 1000ms; &.standalone { display: none; @@ -183,7 +280,7 @@ .block-info { position: absolute; - bottom: calc(100% + 0.25rem); + bottom: calc(100% + 1.15rem); left: 50%; min-width: 100%; transform: translateX(-50%); @@ -191,6 +288,7 @@ color: var(--palette-x); font-size: 1rem; + transition: top 1000ms, bottom 1000ms, left 1000ms, right 1000ms, transform 1000ms; @media (max-width: 360px) { font-size: 4.4vw; @@ -234,7 +332,7 @@ margin: 0; padding: 0; cursor: pointer; - margin-top: -5px; + transform: translateY(-.7rem); } &:first-child { @@ -249,14 +347,16 @@ .explore-button { position: absolute; bottom: 10%; - padding: .75em; + padding: .5em; pointer-events: all; + opacity: var(--block-control-opacity, 1); + transition: opacity 250ms, bottom 1000ms, left 1000ms, right 1000ms; &.prev { - right: 100% + right: calc(100% + .7rem); } &.next { - left: 100%; + left: calc(100% + .7rem); } .chevron { @@ -291,11 +391,12 @@ .block-info.landscape { bottom: unset; left: unset; - top: 0; - right: 100%; - padding-right: .5rem; + top: -4px; + right: calc(100% + 1.2rem); + padding-right: 0; - min-width: 0; + min-width: 13rem; + text-align: right; transform: translateX(0); .data-row { @@ -310,7 +411,7 @@ } .data-field { - white-space: wrap; + white-space: nowrap; margin-left: 0; margin-right: 5px; @@ -323,31 +424,40 @@ } } } - .standalone.landscape.close-button { display: block; position: absolute; - bottom: 100%; - left: 100%; - margin: 5px; + top: -2px; + bottom: unset; + left: calc(100% + 1.15rem); + margin: 0; } - .standalone.tinyscreen.close-button { top: 0; bottom: unset; margin-top: 0; } + + .block-info-container.ambient-mode { + .close-button, + .explore-button { + opacity: 0; + pointer-events: none; + } + } + + {#key transitionDirection} {#each ((block != null && visible && $blocksEnabled) ? [block] : []) as block (block.id)} -
+
{#if block.height == $latestBlockHeight}Latest {/if}Block: { numberFormat.format(block.height) } - +
{ formatDateTime(block.time) } @@ -370,7 +480,7 @@
{#if block.height == $latestBlockHeight}Latest {/if}Block: { numberFormat.format(block.height) } - +
{ formatDateTime(block.time) } diff --git a/client/src/components/MempoolLegend.svelte b/client/src/components/MempoolLegend.svelte index eb16150..f8fbc99 100644 --- a/client/src/components/MempoolLegend.svelte +++ b/client/src/components/MempoolLegend.svelte @@ -74,8 +74,8 @@ function formatBytes (bytes) { function formatValue (value) { const btc = value / 100000000 - const str = (btc < 1) ? btc.toPrecision(1) : `${Math.floor(btc)}` - const units = str.split('.')[0].length + const str = (btc < 1) ? btc.toLocaleString(undefined, { maximumSignificantDigits: 1 }) : numberFormat.format(Math.floor(btc)) + const units = str.split(/[,.]/)[0].length const padded = str.padEnd(3 + units, ' ').padStart(6, ' ') return padded } diff --git a/client/src/components/PriceChartBackground.svelte b/client/src/components/PriceChartBackground.svelte new file mode 100644 index 0000000..bad8b77 --- /dev/null +++ b/client/src/components/PriceChartBackground.svelte @@ -0,0 +1,143 @@ + + + + +{#if mode !== 'none' && path} + +{/if} diff --git a/client/src/components/SearchTab.svelte b/client/src/components/SearchTab.svelte index 45918b9..60faf93 100644 --- a/client/src/components/SearchTab.svelte +++ b/client/src/components/SearchTab.svelte @@ -9,7 +9,7 @@ import CrossIcon from '../assets/icon/cil-x.svg' import AddressIcon from '../assets/icon/cil-wallet.svg' import TxIcon from '../assets/icon/cil-arrow-circle-right.svg' import { matchQuery } from '../utils/search.js' -import { highlight, newHighlightQuery, highlightingFull, freezeResize } from '../stores.js' +import { highlight, newHighlightQuery, removeHighlightQuery, focusTx, highlightingFull, freezeResize } from '../stores.js' import { hlToHex, highlightA, highlightB, highlightC, highlightD, highlightE } from '../utils/color.js' const highlightColors = [highlightA, highlightB, highlightC, highlightD, highlightE] @@ -53,6 +53,13 @@ $: { $newHighlightQuery = null } } +$: { + if ($removeHighlightQuery) { + const index = watchlist.findIndex(watched => watched.txid === $removeHighlightQuery) + if (index >= 0) remove(index) + $removeHighlightQuery = null + } +} $: { $highlight = matchedQuery ? [ ...watchlist, matchedQuery ] : watchlist } @@ -150,6 +157,10 @@ function searchSubmit (e) { return false } +function focusWatched (watched) { + if (watched && watched.txid) $focusTx = watched.txid +} + let freezeTimeout function focusIn(e) { if (freezeTimeout) clearTimeout(freezeTimeout) @@ -249,6 +260,10 @@ async function focusOut(e) { border-color: var(--palette-e); } } + + .watched.focusable { + cursor: pointer; + } } @@ -270,6 +285,8 @@ async function focusOut(e) { {#each watchlist as watched, index (watched.colorIndex)}
focusWatched(watched)} transition:fade={{ duration: 200 }} animate:flip={{ duration: 200 }} > @@ -277,7 +294,7 @@ async function focusOut(e) {
{ watched.value } -
remove(index)} title="Remove from watchlist"> +
remove(index)} title="Remove from watchlist">
diff --git a/client/src/components/Settings.svelte b/client/src/components/Settings.svelte index b054714..ed9cccc 100644 --- a/client/src/components/Settings.svelte +++ b/client/src/components/Settings.svelte @@ -12,8 +12,8 @@ function toggle(setting) { } function onChange(setting, value) { - $settings[setting] = value - analytics.trackEvent('settings', setting, $settings[setting]) + settings.set({ ...$settings, [setting]: value }) + analytics.trackEvent('settings', setting, value) } const currencyOptions = Object.keys(currencies).map(code => { @@ -24,6 +24,12 @@ const currencyOptions = Object.keys(currencies).map(code => { } }) +const priceChartOptions = [ + { value: 'none', label: 'Off', tags: ['none', 'off'] }, + { value: '1d', label: '1D', tags: ['day', '24h'] }, + { value: '30d', label: '30D', tags: ['month', '30 days'] } +] + let settingConfig = { showNetworkStatus: { label: 'Network Status', @@ -53,9 +59,19 @@ let settingConfig = { trueLabel: 'fee rate', valueType: 'bool' }, + showGhostTrails: { + label: 'Ghost Trails', + valueType: 'bool' + }, showSearch: { label: 'Search Bar', valueType: 'bool' + }, + priceChartMode: { + label: 'Price Chart', + type: 'dropdown', + valueType: 'string', + options: priceChartOptions } } $: { diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte index 64aaf81..a732ae0 100644 --- a/client/src/components/Sidebar.svelte +++ b/client/src/components/Sidebar.svelte @@ -1,4 +1,5 @@ -
+ +{#if $fullscreenActive} + +{/if} diff --git a/client/src/components/SidebarMenuItem.svelte b/client/src/components/SidebarMenuItem.svelte index 6fc8177..05d334c 100644 --- a/client/src/components/SidebarMenuItem.svelte +++ b/client/src/components/SidebarMenuItem.svelte @@ -87,6 +87,7 @@ async function focusOut(e) { { label } {:else if type === 'dropdown'} + { label }