Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
220 lines
6.6 KiB
JavaScript
220 lines
6.6 KiB
JavaScript
export function getRangeStyles(direction, min, max) {
|
|
const styles = {
|
|
position: "absolute",
|
|
};
|
|
if (direction === "lr") {
|
|
styles.left = `${min}%`;
|
|
styles.right = `${max}%`;
|
|
}
|
|
else if (direction === "rl") {
|
|
styles.right = `${min}%`;
|
|
styles.left = `${max}%`;
|
|
}
|
|
else if (direction === "bt") {
|
|
styles.bottom = `${min}%`;
|
|
styles.top = `${max}%`;
|
|
}
|
|
else {
|
|
styles.top = `${min}%`;
|
|
styles.bottom = `${max}%`;
|
|
}
|
|
return styles;
|
|
}
|
|
export function getThumbStyles(direction, thumbPos) {
|
|
const styles = {
|
|
position: "absolute",
|
|
};
|
|
if (direction === "lr") {
|
|
styles.left = `${thumbPos}%`;
|
|
styles.translate = "-50% 0";
|
|
}
|
|
else if (direction === "rl") {
|
|
styles.right = `${thumbPos}%`;
|
|
styles.translate = "50% 0";
|
|
}
|
|
else if (direction === "bt") {
|
|
styles.bottom = `${thumbPos}%`;
|
|
styles.translate = "0 50%";
|
|
}
|
|
else {
|
|
styles.top = `${thumbPos}%`;
|
|
styles.translate = "0 -50%";
|
|
}
|
|
return styles;
|
|
}
|
|
export function getTickStyles(direction, tickPosition, offsetPercentage) {
|
|
const style = {
|
|
position: "absolute",
|
|
};
|
|
if (direction === "lr") {
|
|
style.left = `${tickPosition}%`;
|
|
style.translate = `${offsetPercentage}% 0`;
|
|
}
|
|
else if (direction === "rl") {
|
|
style.right = `${tickPosition}%`;
|
|
style.translate = `${-offsetPercentage}% 0`;
|
|
}
|
|
else if (direction === "bt") {
|
|
style.bottom = `${tickPosition}%`;
|
|
style.translate = `0 ${-offsetPercentage}%`;
|
|
}
|
|
else {
|
|
style.top = `${tickPosition}%`;
|
|
style.translate = `0 ${offsetPercentage}%`;
|
|
}
|
|
return style;
|
|
}
|
|
export function getTickLabelStyles(direction, tickPosition, labelPosition = "top") {
|
|
const style = {
|
|
position: "absolute",
|
|
};
|
|
if (direction === "lr" || direction === "rl") {
|
|
// Horizontal slider
|
|
style.left = direction === "lr" ? `${tickPosition}%` : undefined;
|
|
style.right = direction === "rl" ? `${tickPosition}%` : undefined;
|
|
style.translate = "-50% 0";
|
|
if (labelPosition === "top") {
|
|
style.bottom = "100%";
|
|
}
|
|
else if (labelPosition === "bottom") {
|
|
style.top = "100%";
|
|
}
|
|
}
|
|
else {
|
|
// Vertical slider - use same positioning as ticks
|
|
if (direction === "tb") {
|
|
style.top = `${tickPosition}%`;
|
|
}
|
|
else {
|
|
style.bottom = `${tickPosition}%`;
|
|
}
|
|
style.translate = "0 50%";
|
|
if (labelPosition === "left") {
|
|
style.right = "100%";
|
|
}
|
|
else if (labelPosition === "right") {
|
|
style.left = "100%";
|
|
}
|
|
}
|
|
return style;
|
|
}
|
|
export function getThumbLabelStyles(direction, thumbPosition, labelPosition = "top") {
|
|
const style = {
|
|
position: "absolute",
|
|
};
|
|
if (direction === "lr" || direction === "rl") {
|
|
// Horizontal slider
|
|
style.left = direction === "lr" ? `${thumbPosition}%` : undefined;
|
|
style.right = direction === "rl" ? `${thumbPosition}%` : undefined;
|
|
style.translate = "-50% 0";
|
|
if (labelPosition === "top") {
|
|
style.bottom = "100%";
|
|
}
|
|
else if (labelPosition === "bottom") {
|
|
style.top = "100%";
|
|
}
|
|
}
|
|
else {
|
|
// Vertical slider
|
|
if (direction === "tb") {
|
|
style.top = `${thumbPosition}%`;
|
|
}
|
|
else {
|
|
style.bottom = `${thumbPosition}%`;
|
|
}
|
|
style.translate = "0 -50%";
|
|
if (labelPosition === "left") {
|
|
style.right = "100%";
|
|
}
|
|
else if (labelPosition === "right") {
|
|
style.left = "100%";
|
|
}
|
|
}
|
|
return style;
|
|
}
|
|
/**
|
|
* Gets the number of decimal places in a number
|
|
*/
|
|
function getDecimalPlaces(num) {
|
|
if (Math.floor(num) === num)
|
|
return 0;
|
|
const str = num.toString();
|
|
if (str.indexOf(".") !== -1 && str.indexOf("e-") === -1) {
|
|
return str.split(".")[1].length;
|
|
}
|
|
else if (str.indexOf("e-") !== -1) {
|
|
const parts = str.split("e-");
|
|
return parseInt(parts[1], 10);
|
|
}
|
|
return 0;
|
|
}
|
|
/**
|
|
* Rounds a number to the specified number of decimal places
|
|
*/
|
|
function roundToPrecision(num, precision) {
|
|
const factor = Math.pow(10, precision);
|
|
return Math.round(num * factor) / factor;
|
|
}
|
|
/**
|
|
* Normalizes step to always be a sorted array of valid values within min/max range
|
|
*/
|
|
export function normalizeSteps(step, min, max) {
|
|
if (typeof step === "number") {
|
|
// generate regular steps - match original behavior exactly
|
|
const difference = max - min;
|
|
let count = Math.ceil(difference / step);
|
|
// Get precision from step to avoid floating point errors
|
|
const precision = getDecimalPlaces(step);
|
|
// Check if difference is divisible by step using integer arithmetic to avoid floating point errors
|
|
const factor = Math.pow(10, precision);
|
|
const intDifference = Math.round(difference * factor);
|
|
const intStep = Math.round(step * factor);
|
|
if (intDifference % intStep === 0) {
|
|
count++;
|
|
}
|
|
const steps = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const value = min + i * step;
|
|
// Round to the precision of the step to avoid floating point errors
|
|
const roundedValue = roundToPrecision(value, precision);
|
|
steps.push(roundedValue);
|
|
}
|
|
return steps;
|
|
}
|
|
return [...new Set(step)].filter((value) => value >= min && value <= max).sort((a, b) => a - b);
|
|
}
|
|
/**
|
|
* Snaps a value to the nearest step in a custom steps array
|
|
*/
|
|
export function snapValueToCustomSteps(value, steps) {
|
|
if (steps.length === 0)
|
|
return value;
|
|
// Find the closest step
|
|
let closest = steps[0];
|
|
let minDistance = Math.abs(value - closest);
|
|
for (const step of steps) {
|
|
const distance = Math.abs(value - step);
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
closest = step;
|
|
}
|
|
}
|
|
return closest;
|
|
}
|
|
/**
|
|
* Gets the next/previous step value for keyboard navigation
|
|
*/
|
|
export function getAdjacentStepValue(currentValue, steps, direction) {
|
|
const currentIndex = steps.indexOf(currentValue);
|
|
if (currentIndex === -1) {
|
|
// current value is not in steps, snap to nearest
|
|
return snapValueToCustomSteps(currentValue, steps);
|
|
}
|
|
if (direction === "next") {
|
|
return currentIndex < steps.length - 1 ? steps[currentIndex + 1] : currentValue;
|
|
}
|
|
else {
|
|
return currentIndex > 0 ? steps[currentIndex - 1] : currentValue;
|
|
}
|
|
}
|