feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
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.
This commit is contained in:
206
web/node_modules/runed/dist/utilities/scroll-state/scroll-state.svelte.js
generated
vendored
Normal file
206
web/node_modules/runed/dist/utilities/scroll-state/scroll-state.svelte.js
generated
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
import { AnimationFrames } from "../animation-frames/index.js";
|
||||
import { useDebounce } from "../use-debounce/index.js";
|
||||
import { useEventListener } from "../use-event-listener/index.js";
|
||||
import { onMount } from "svelte";
|
||||
import { extract } from "../extract/extract.svelte.js";
|
||||
import { noop } from "../../internal/utils/function.js";
|
||||
/**
|
||||
* We have to check if the scroll amount is close enough to some threshold in order to
|
||||
* more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
|
||||
* numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
|
||||
*/
|
||||
const ARRIVED_STATE_THRESHOLD_PIXELS = 1;
|
||||
/**
|
||||
* A reactive utility for tracking scroll position, direction,
|
||||
* and edge arrival states, while supporting programmatic scrolling.
|
||||
*
|
||||
* @see https://vueuse.org/useScroll for the inspiration behind this utility.
|
||||
* @param element
|
||||
* @param options
|
||||
*/
|
||||
export class ScrollState {
|
||||
#options;
|
||||
element = $derived(extract(this.#options.element));
|
||||
idle = $derived.by(() => extract(this.#options?.idle, 200));
|
||||
offset = $derived(extract(this.#options.offset, {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}));
|
||||
onScroll = $derived(this.#options.onScroll ?? noop);
|
||||
onStop = $derived(this.#options.onStop ?? noop);
|
||||
eventListenerOptions = $derived(this.#options.eventListenerOptions ?? {
|
||||
capture: false,
|
||||
passive: true,
|
||||
});
|
||||
behavior = $derived(extract(this.#options.behavior, "auto"));
|
||||
onError = $derived(this.#options.onError ??
|
||||
((e) => {
|
||||
console.error(e);
|
||||
}));
|
||||
/** State */
|
||||
internalX = $state(0);
|
||||
internalY = $state(0);
|
||||
// Use a get/set pair for x and y because we want to write the value to the refs
|
||||
// during a `scrollTo()` without firing additional `scrollTo()`s in the process.
|
||||
#x = $derived(this.internalX);
|
||||
get x() {
|
||||
return this.#x;
|
||||
}
|
||||
set x(v) {
|
||||
this.scrollTo(v, undefined);
|
||||
}
|
||||
#y = $derived(this.internalY);
|
||||
get y() {
|
||||
return this.#y;
|
||||
}
|
||||
set y(v) {
|
||||
this.scrollTo(undefined, v);
|
||||
}
|
||||
isScrolling = $state(false);
|
||||
arrived = $state({
|
||||
left: true,
|
||||
right: false,
|
||||
top: true,
|
||||
bottom: false,
|
||||
});
|
||||
directions = $state({
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
bottom: false,
|
||||
});
|
||||
progress = $state({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
constructor(options) {
|
||||
this.#options = options;
|
||||
useEventListener(() => this.element, "scroll", this.#onScrollHandler, this.eventListenerOptions);
|
||||
useEventListener(() => this.element, "scrollend", (e) => this.onScrollEnd(e), this.eventListenerOptions);
|
||||
onMount(() => {
|
||||
this.setArrivedState();
|
||||
});
|
||||
// overkill?
|
||||
new AnimationFrames(() => this.setArrivedState());
|
||||
}
|
||||
/**
|
||||
* Updates direction and edge arrival states based on the current scroll position.
|
||||
* Takes into account writing mode, flex direction, and RTL layouts.
|
||||
*/
|
||||
setArrivedState = () => {
|
||||
if (!window || !this.element)
|
||||
return;
|
||||
const el = (this.element?.document?.documentElement ||
|
||||
this.element?.documentElement ||
|
||||
this.element);
|
||||
const { display, flexDirection, direction } = getComputedStyle(el);
|
||||
const directionMultiplier = direction === "rtl" ? -1 : 1;
|
||||
const scrollLeft = el.scrollLeft;
|
||||
if (scrollLeft !== this.internalX) {
|
||||
this.directions.left = scrollLeft < this.internalX;
|
||||
this.directions.right = scrollLeft > this.internalX;
|
||||
}
|
||||
const left = scrollLeft * directionMultiplier <= (this.offset.left || 0);
|
||||
const right = scrollLeft * directionMultiplier + el.clientWidth >=
|
||||
el.scrollWidth - (this.offset.right || 0) - ARRIVED_STATE_THRESHOLD_PIXELS;
|
||||
if (display === "flex" && flexDirection === "row-reverse") {
|
||||
this.arrived.left = right;
|
||||
this.arrived.right = left;
|
||||
}
|
||||
else {
|
||||
this.arrived.left = left;
|
||||
this.arrived.right = right;
|
||||
}
|
||||
this.internalX = scrollLeft;
|
||||
let scrollTop = el.scrollTop;
|
||||
// patch for mobile compatible
|
||||
if (this.element === window.document && !scrollTop)
|
||||
scrollTop = window.document.body.scrollTop;
|
||||
if (scrollTop !== this.internalY) {
|
||||
this.directions.top = scrollTop < this.internalY;
|
||||
this.directions.bottom = scrollTop > this.internalY;
|
||||
}
|
||||
const top = scrollTop <= (this.offset.top || 0);
|
||||
const bottom = scrollTop + el.clientHeight >=
|
||||
el.scrollHeight - (this.offset.bottom || 0) - ARRIVED_STATE_THRESHOLD_PIXELS;
|
||||
/**
|
||||
* reverse columns and rows behave exactly the other way around,
|
||||
* bottom is treated as top and top is treated as the negative version of bottom
|
||||
*/
|
||||
if (display === "flex" && flexDirection === "column-reverse") {
|
||||
this.arrived.top = bottom;
|
||||
this.arrived.bottom = top;
|
||||
}
|
||||
else {
|
||||
this.arrived.top = top;
|
||||
this.arrived.bottom = bottom;
|
||||
}
|
||||
const height = el.scrollHeight - (this.offset.bottom || 0);
|
||||
this.progress.y = (scrollTop / (height - el.clientHeight)) * 100;
|
||||
const width = el.scrollWidth - (this.offset.left || 0);
|
||||
// Math.abs for rtl support
|
||||
this.progress.x = Math.abs((scrollLeft / (width - el.clientWidth)) * 100);
|
||||
this.internalY = scrollTop;
|
||||
};
|
||||
#onScrollHandler = (e) => {
|
||||
if (!window)
|
||||
return;
|
||||
this.setArrivedState();
|
||||
this.isScrolling = true;
|
||||
this.onScrollEndDebounced(e);
|
||||
this.onScroll(e);
|
||||
};
|
||||
/**
|
||||
* Programmatically scroll to a specific position.
|
||||
*/
|
||||
scrollTo(x, y) {
|
||||
if (!window)
|
||||
return;
|
||||
(this.element instanceof Document ? window.document.body : this.element)?.scrollTo({
|
||||
top: y ?? this.y,
|
||||
left: x ?? this.x,
|
||||
behavior: this.behavior,
|
||||
});
|
||||
const scrollContainer = this.element?.document?.documentElement ||
|
||||
this.element?.documentElement ||
|
||||
this.element;
|
||||
if (x != null)
|
||||
this.internalX = scrollContainer.scrollLeft;
|
||||
if (y != null)
|
||||
this.internalY = scrollContainer.scrollTop;
|
||||
}
|
||||
/**
|
||||
* Scrolls to the top of the element.
|
||||
*/
|
||||
scrollToTop() {
|
||||
this.scrollTo(undefined, 0);
|
||||
}
|
||||
/**
|
||||
* Scrolls to the bottom of the element.
|
||||
*/
|
||||
scrollToBottom() {
|
||||
if (!window)
|
||||
return;
|
||||
const scrollContainer = this.element?.document?.documentElement ||
|
||||
this.element?.documentElement ||
|
||||
this.element;
|
||||
if (!scrollContainer)
|
||||
return;
|
||||
this.scrollTo(undefined, scrollContainer.scrollHeight);
|
||||
}
|
||||
onScrollEnd = (e) => {
|
||||
// dedupe if support native scrollend event
|
||||
if (!this.isScrolling)
|
||||
return;
|
||||
this.isScrolling = false;
|
||||
this.directions.left = false;
|
||||
this.directions.right = false;
|
||||
this.directions.top = false;
|
||||
this.directions.bottom = false;
|
||||
this.onStop(e);
|
||||
};
|
||||
onScrollEndDebounced = useDebounce(this.onScrollEnd, () => this.idle);
|
||||
}
|
||||
Reference in New Issue
Block a user