bitview: reorg part 1

This commit is contained in:
nym21
2025-09-22 18:43:53 +02:00
parent aad34c4d52
commit 5b6ce5d8ee
23 changed files with 2945 additions and 3013 deletions
File diff suppressed because it is too large Load Diff
+120
View File
@@ -0,0 +1,120 @@
/**
* @import { Accessor } from "../packages/solidjs-signals/wrapper";
*/
const globalComputedStyle = getComputedStyle(window.document.documentElement);
/**
* @param {Accessor<boolean>} dark
*/
export function createColors(dark) {
/**
* @param {string} color
*/
function getColor(color) {
return globalComputedStyle.getPropertyValue(`--${color}`);
}
function red() {
return getColor("red");
}
function orange() {
return getColor("orange");
}
function amber() {
return getColor("amber");
}
function yellow() {
return getColor("yellow");
}
function avocado() {
return getColor("avocado");
}
function lime() {
return getColor("lime");
}
function green() {
return getColor("green");
}
function emerald() {
return getColor("emerald");
}
function teal() {
return getColor("teal");
}
function cyan() {
return getColor("cyan");
}
function sky() {
return getColor("sky");
}
function blue() {
return getColor("blue");
}
function indigo() {
return getColor("indigo");
}
function violet() {
return getColor("violet");
}
function purple() {
return getColor("purple");
}
function fuchsia() {
return getColor("fuchsia");
}
function pink() {
return getColor("pink");
}
function rose() {
return getColor("rose");
}
function gray() {
return getColor("gray");
}
/**
* @param {string} property
*/
function getLightDarkValue(property) {
const value = globalComputedStyle.getPropertyValue(property);
const [light, _dark] = value.slice(11, -1).split(", ");
return dark() ? _dark : light;
}
function textColor() {
return getLightDarkValue("--color");
}
function borderColor() {
return getLightDarkValue("--border-color");
}
return {
default: textColor,
gray,
border: borderColor,
red,
orange,
amber,
yellow,
avocado,
lime,
green,
emerald,
teal,
cyan,
sky,
blue,
indigo,
violet,
purple,
fuchsia,
pink,
rose,
};
}
/**
* @typedef {ReturnType<typeof createColors>} Colors
* @typedef {Colors["orange"]} Color
* @typedef {keyof Colors} ColorName
*/
+28
View File
@@ -0,0 +1,28 @@
/**
* @param {string} id
*/
function getElementById(id) {
const element = window.document.getElementById(id);
if (!element) throw `Element with id = "${id}" should exist`;
return element;
}
export default {
head: window.document.getElementsByTagName("head")[0],
body: window.document.body,
main: getElementById("main"),
aside: getElementById("aside"),
asideLabel: getElementById("aside-selector-label"),
navLabel: getElementById(`nav-selector-label`),
searchLabel: getElementById(`search-selector-label`),
search: getElementById("search"),
nav: getElementById("nav"),
searchInput: /** @type {HTMLInputElement} */ (getElementById("search-input")),
searchResults: getElementById("search-results"),
selectors: getElementById("frame-selectors"),
style: getComputedStyle(window.document.documentElement),
charts: getElementById("charts"),
table: getElementById("table"),
explorer: getElementById("explorer"),
simulation: getElementById("simulation"),
};
+24
View File
@@ -0,0 +1,24 @@
const localhost = window.location.hostname === "localhost";
const standalone =
"standalone" in window.navigator && !!window.navigator.standalone;
const userAgent = navigator.userAgent.toLowerCase();
const isChrome = userAgent.includes("chrome");
const safari = userAgent.includes("safari");
const safariOnly = safari && !isChrome;
const macOS = userAgent.includes("mac os");
const iphone = userAgent.includes("iphone");
const ipad = userAgent.includes("ipad");
const ios = iphone || ipad;
export default {
standalone,
userAgent,
isChrome,
safari,
safariOnly,
macOS,
iphone,
ipad,
ios,
localhost,
};
@@ -0,0 +1,371 @@
import { createPartialOptions } from "./partial";
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {Signals} args.signals
* @param {Env} args.env
* @param {Utilities} args.utils
* @param {VecIdToIndexes} args.vecIdToIndexes
* @param {Pools} args.pools
* @param {Signal<string | null>} args.qrcode
*/
export function initOptions({
colors,
signals,
env,
utils,
qrcode,
vecIdToIndexes,
pools,
}) {
const LS_SELECTED_KEY = `selected_path`;
const urlPath_ = window.document.location.pathname
.split("/")
.filter((v) => v);
const urlPath = urlPath_.length ? urlPath_ : undefined;
const savedPath = /** @type {string[]} */ (
JSON.parse(utils.storage.read(LS_SELECTED_KEY) || "[]") || []
).filter((v) => v);
console.log(savedPath);
/** @type {Signal<Option>} */
const selected = signals.createSignal(/** @type {any} */ (undefined));
const partialOptions = createPartialOptions({
env,
colors,
vecIdToIndexes,
pools,
});
/** @type {Option[]} */
const list = [];
const parent = signals.createSignal(/** @type {HTMLElement | null} */ (null));
/**
* @param {AnyFetchedSeriesBlueprint[]} [arr]
*/
function arrayToRecord(arr = []) {
return (arr || []).reduce((record, blueprint) => {
if (env.localhost && !(blueprint.key in vecIdToIndexes)) {
throw Error(`${blueprint.key} not recognized`);
}
const unit = blueprint.unit ?? utils.vecidToUnit(blueprint.key);
record[unit] ??= [];
record[unit].push(blueprint);
return record;
}, /** @type {Record<Unit, AnyFetchedSeriesBlueprint[]>} */ ({}));
}
/**
* @param {Option} option
*/
function selectOption(option) {
utils.url.pushHistory(option.path);
utils.url.resetParams(option);
utils.storage.write(LS_SELECTED_KEY, JSON.stringify(option.path));
selected.set(option);
}
/**
* @param {Object} args
* @param {Option} args.option
* @param {string} args.frame
* @param {Signal<string | null>} args.qrcode
* @param {string} [args.name]
*/
function createOptionElement({ option, frame, name, qrcode }) {
const title = option.title;
if (option.kind === "url") {
const href = option.url();
if (option.qrcode) {
return utils.dom.createButtonElement({
inside: option.name,
title,
onClick: () => {
qrcode.set(option.url);
},
});
} else {
return utils.dom.createAnchorElement({
href,
blank: true,
text: option.name,
title,
});
}
} else {
return utils.dom.createAnchorElement({
href: `/${option.path.join("/")}`,
title,
text: name || option.name,
onClick: () => {
selectOption(option);
},
});
}
}
/** @type {Option | undefined} */
let savedOption;
/**
* @param {PartialOptionsTree} partialTree
* @param {Accessor<HTMLElement | null>} parent
* @param {string[] | undefined} parentPath
* @returns {Accessor<number>}
*/
function recursiveProcessPartialTree(
partialTree,
parent,
parentPath = [],
depth = 0,
) {
/** @type {Accessor<number>[]} */
const listForSum = [];
const ul = signals.createMemo(
// @ts_ignore
(_previous) => {
const previous = /** @type {HTMLUListElement | null} */ (_previous);
previous?.remove();
const _parent = parent();
if (_parent) {
if ("open" in _parent && !_parent.open) {
throw "Set accesor to null instead";
}
const ul = window.document.createElement("ul");
_parent.append(ul);
return ul;
} else {
return null;
}
},
null,
);
partialTree.forEach((anyPartial, partialIndex) => {
const renderLi = signals.createSignal(true);
const li = signals.createMemo((_previous) => {
const previous = _previous;
previous?.remove();
const _ul = ul();
if (renderLi() && _ul) {
const li = window.document.createElement("li");
utils.dom.insertElementAtIndex(_ul, li, partialIndex);
return li;
} else {
return null;
}
}, /** @type {HTMLLIElement | null} */ (null));
if ("tree" in anyPartial) {
/** @type {Omit<OptionsGroup, keyof PartialOptionsGroup>} */
const groupAddons = {};
Object.assign(anyPartial, groupAddons);
const passedDetails = signals.createSignal(
/** @type {HTMLDivElement | HTMLDetailsElement | null} */ (null),
);
const serName = utils.stringToId(anyPartial.name);
const path = [...parentPath, serName];
const childOptionsCount = recursiveProcessPartialTree(
anyPartial.tree,
passedDetails,
path,
depth + 1,
);
listForSum.push(childOptionsCount);
signals.createEffect(li, (li) => {
if (!li) {
passedDetails.set(null);
return;
}
signals.createEffect(selected, (selected) => {
if (
path.length <= selected.path.length &&
path.every((v, i) => selected.path.at(i) === v)
) {
li.dataset.highlight = "";
} else {
delete li.dataset.highlight;
}
});
const details = window.document.createElement("details");
details.dataset.name = serName;
li.appendChild(details);
const summary = window.document.createElement("summary");
details.append(summary);
summary.append(anyPartial.name);
const supCount = window.document.createElement("sup");
summary.append(supCount);
signals.createEffect(childOptionsCount, (childOptionsCount) => {
supCount.innerHTML = childOptionsCount.toLocaleString("en-us");
});
details.addEventListener("toggle", () => {
const open = details.open;
if (open) {
passedDetails.set(details);
} else {
passedDetails.set(null);
}
});
});
function createRenderLiEffect() {
signals.createEffect(childOptionsCount, (count) => {
renderLi.set(!!count);
});
}
createRenderLiEffect();
} else {
const option = /** @type {Option} */ (anyPartial);
const name = option.name;
const path = [...parentPath, utils.stringToId(option.name)];
if ("kind" in anyPartial && anyPartial.kind === "explorer") {
Object.assign(
option,
/** @satisfies {ExplorerOption} */ ({
kind: anyPartial.kind,
path,
name,
title: option.title,
}),
);
} else if ("kind" in anyPartial && anyPartial.kind === "table") {
Object.assign(
option,
/** @satisfies {TableOption} */ ({
kind: anyPartial.kind,
path,
name,
title: option.title,
}),
);
} else if ("kind" in anyPartial && anyPartial.kind === "simulation") {
Object.assign(
option,
/** @satisfies {SimulationOption} */ ({
kind: anyPartial.kind,
path,
name,
title: anyPartial.title,
}),
);
} else if ("url" in anyPartial) {
Object.assign(
option,
/** @satisfies {UrlOption} */ ({
kind: "url",
path,
name,
title: name,
qrcode: !!anyPartial.qrcode,
url: anyPartial.url,
}),
);
} else {
const title = option.title || option.name;
Object.assign(
option,
/** @satisfies {ChartOption} */ ({
kind: "chart",
name,
title,
path,
top: arrayToRecord(anyPartial.top),
bottom: arrayToRecord(anyPartial.bottom),
}),
);
}
list.push(option);
if (urlPath) {
const sameAsURLPath =
urlPath.length === path.length &&
urlPath.every((val, i) => val === path[i]);
if (sameAsURLPath) {
selected.set(option);
}
} else if (savedPath) {
const sameAsSavedPath =
savedPath.length === path.length &&
savedPath.every((val, i) => val === path[i]);
if (sameAsSavedPath) {
savedOption = option;
}
}
signals.createEffect(li, (li) => {
if (!li) {
return;
}
signals.createEffect(selected, (selected) => {
if (selected === option) {
li.dataset.highlight = "";
} else {
delete li.dataset.highlight;
}
});
const element = createOptionElement({
option,
frame: "nav",
qrcode,
});
li.append(element);
});
listForSum.push(() => 1);
}
});
return signals.createMemo(() =>
listForSum.reduce((acc, s) => acc + s(), 0),
);
}
recursiveProcessPartialTree(partialOptions, parent);
if (!selected()) {
const option =
savedOption || list.find((option) => option.kind === "chart");
if (option) {
selected.set(option);
}
}
return {
selected,
list,
tree: /** @type {OptionsTree} */ (partialOptions),
parent,
createOptionElement,
selectOption,
};
}
/** @typedef {ReturnType<typeof initOptions>} Options */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
/**
* @param {Signals} signals
*/
export function createWebSockets(signals) {
/**
* @template T
* @param {(callback: (value: T) => void) => WebSocket} creator
*/
function createWebsocket(creator) {
let ws = /** @type {WebSocket | null} */ (null);
const live = signals.createSignal(false);
const latest = signals.createSignal(/** @type {T | null} */ (null));
function reinitWebSocket() {
if (!ws || ws.readyState === ws.CLOSED) {
console.log("ws: reinit");
resource.open();
}
}
function reinitWebSocketIfDocumentNotHidden() {
!window.document.hidden && reinitWebSocket();
}
const resource = {
live,
latest,
open() {
ws = creator((value) => latest.set(() => value));
ws.addEventListener("open", () => {
console.log("ws: open");
live.set(true);
});
ws.addEventListener("close", () => {
console.log("ws: close");
live.set(false);
});
window.document.addEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
);
window.document.addEventListener("online", reinitWebSocket);
},
close() {
ws?.close();
window.document.removeEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
);
window.document.removeEventListener("online", reinitWebSocket);
live.set(false);
ws = null;
},
};
return resource;
}
/**
* @param {(candle: CandlestickData) => void} callback
*/
function krakenCandleWebSocketCreator(callback) {
const ws = new WebSocket("wss://ws.kraken.com/v2");
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
method: "subscribe",
params: {
channel: "ohlc",
symbol: ["BTC/USD"],
interval: 1440,
},
}),
);
});
ws.addEventListener("message", (message) => {
const result = JSON.parse(message.data);
if (result.channel !== "ohlc") return;
const { interval_begin, open, high, low, close } = result.data.at(-1);
/** @type {CandlestickData} */
const candle = {
// index: -1,
time: new Date(interval_begin).valueOf() / 1000,
open: Number(open),
high: Number(high),
low: Number(low),
close: Number(close),
};
candle && callback({ ...candle });
});
return ws;
}
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
const kraken1dCandle = createWebsocket((callback) =>
krakenCandleWebSocketCreator(callback),
);
kraken1dCandle.open();
signals.createEffect(kraken1dCandle.latest, (latest) => {
if (latest) {
const close = latest.close;
console.log("close:", close);
window.document.title = `${latest.close.toLocaleString("en-us")} | ${
window.location.host
}`;
}
});
return {
kraken1dCandle,
};
}
/** @typedef {ReturnType<typeof createWebSockets>} WebSockets */