Move scroll animations to Rust/WASM, enable Hasura, and move bookmarks to home sidebar
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m7s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m7s
Port AutoScroll and Headline scroll logic from Vue/JS to Rust compiled to WASM via wasm-pack. Add multi-stage Docker build for WASM compilation, Vite WASM plugins, and top-level await for WASM init. Enable Hasura service in docker-compose. Move bookmarks from a separate route to an inline sidebar component on the home page. Fix ToggleHeader click propagation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,55 +1,22 @@
|
||||
<script setup>
|
||||
import { onMounted, useTemplateRef, onUnmounted } from "vue";
|
||||
import { HeadlineScroller } from "@/wasm/stp_wasm.js";
|
||||
|
||||
const container = useTemplateRef("container");
|
||||
const item1 = useTemplateRef("item1");
|
||||
|
||||
let offset = 0;
|
||||
let cachedWidth = 0;
|
||||
|
||||
let rafId;
|
||||
|
||||
const speed = 0.5; // pixels per frame
|
||||
|
||||
function measureWidth() {
|
||||
const ctnr = container.value;
|
||||
const it1 = item1.value;
|
||||
if (ctnr && it1) {
|
||||
cachedWidth = Math.max(ctnr.offsetWidth, it1.scrollWidth);
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
const ctnr = container.value;
|
||||
if (!ctnr || cachedWidth === 0) {
|
||||
rafId = requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
|
||||
offset -= speed;
|
||||
|
||||
if (offset <= -cachedWidth) {
|
||||
offset += cachedWidth;
|
||||
}
|
||||
|
||||
ctnr.style.transform = `translateX(${offset}px)`;
|
||||
|
||||
rafId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let resizeObserver;
|
||||
let scroller = null;
|
||||
|
||||
onMounted(() => {
|
||||
measureWidth();
|
||||
rafId = requestAnimationFrame(animate);
|
||||
|
||||
resizeObserver = new ResizeObserver(measureWidth);
|
||||
resizeObserver.observe(container.value);
|
||||
if (!container.value || !item1.value) return;
|
||||
scroller = new HeadlineScroller(container.value, item1.value);
|
||||
scroller.start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId);
|
||||
resizeObserver?.disconnect();
|
||||
scroller?.destroy();
|
||||
scroller?.free();
|
||||
scroller = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const handleClick = () => {
|
||||
class="pointer-events-none"
|
||||
:model-value="props.modelValue"
|
||||
@update:model-value="updateValue"
|
||||
@click.stop
|
||||
ref="toggleButtonRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,108 +1,26 @@
|
||||
<template>
|
||||
<div ref="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" class="overflow-y-auto">
|
||||
<div ref="container" class="overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTemplateRef, onMounted, onBeforeUnmount } from "vue";
|
||||
import { AutoScroller } from "@/wasm/stp_wasm.js";
|
||||
|
||||
const container = useTemplateRef("container");
|
||||
|
||||
const SPEED = 0.0005; // % per frame
|
||||
const PAUSE = 2000; // ms at top/bottom
|
||||
|
||||
let pos = 0;
|
||||
let direction = 1; // 1 = down, -1 = up
|
||||
let hovered = false;
|
||||
let rafId = null;
|
||||
let pauseTimeoutId = null;
|
||||
let cachedScrollHeight = 0;
|
||||
|
||||
function measureScrollHeight() {
|
||||
const el = container.value;
|
||||
if (el) cachedScrollHeight = el.scrollHeight;
|
||||
}
|
||||
|
||||
function stopLoop() {
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
if (pauseTimeoutId !== null) {
|
||||
clearTimeout(pauseTimeoutId);
|
||||
pauseTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startLoop() {
|
||||
stopLoop();
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
hovered = true;
|
||||
stopLoop();
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
hovered = false;
|
||||
const el = container.value;
|
||||
if (el && cachedScrollHeight > 0) {
|
||||
pos = el.scrollTop / cachedScrollHeight;
|
||||
}
|
||||
startLoop();
|
||||
}
|
||||
|
||||
function schedulePause(callback) {
|
||||
stopLoop();
|
||||
pauseTimeoutId = setTimeout(callback, PAUSE);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
rafId = null;
|
||||
const el = container.value;
|
||||
if (hovered) return;
|
||||
|
||||
if (!el || cachedScrollHeight === 0) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
const reachedBottom = pos >= 1;
|
||||
const reachedTop = pos <= 0;
|
||||
|
||||
if (reachedBottom) {
|
||||
pos = 0.999;
|
||||
direction = -1;
|
||||
schedulePause(startLoop);
|
||||
return;
|
||||
} else if (reachedTop && direction === -1) {
|
||||
pos = 0.001;
|
||||
direction = 1;
|
||||
schedulePause(startLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
pos += direction * SPEED;
|
||||
|
||||
el.scrollTop = pos * cachedScrollHeight;
|
||||
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
let resizeObserver;
|
||||
let scroller = null;
|
||||
|
||||
onMounted(() => {
|
||||
measureScrollHeight();
|
||||
schedulePause(startLoop);
|
||||
|
||||
resizeObserver = new ResizeObserver(measureScrollHeight);
|
||||
resizeObserver.observe(container.value);
|
||||
if (!container.value) return;
|
||||
scroller = new AutoScroller(container.value);
|
||||
scroller.start();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopLoop();
|
||||
resizeObserver?.disconnect();
|
||||
scroller?.destroy();
|
||||
scroller?.free();
|
||||
scroller = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,9 @@ import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import "./assets/styles.css";
|
||||
import init from "@/wasm/stp_wasm.js";
|
||||
|
||||
await init();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
||||
@@ -34,11 +34,6 @@ const router = createRouter({
|
||||
component: () => import("@/views/admin/Admin.vue"),
|
||||
meta: { requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "bookmarks",
|
||||
name: "bookmarks",
|
||||
component: () => import("@/views/home/bookmarks/Bookmarks.vue"),
|
||||
},
|
||||
{
|
||||
path: "shrines",
|
||||
name: "shrine links",
|
||||
|
||||
@@ -21,6 +21,7 @@ import Favorites from "./Favorites.vue";
|
||||
import Gym2 from "./Gym2.vue";
|
||||
import Consumption from "./Consumption.vue";
|
||||
import Steam from "./Steam.vue";
|
||||
import Bookmarks from "./bookmarks/Bookmarks.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -61,6 +62,7 @@ import Steam from "./Steam.vue";
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<Steam class="steam-sidebar sidebar-cell" />
|
||||
<Bookmarks class="bookmarks-sidebar sidebar-cell" />
|
||||
<Chat
|
||||
class="chat-sidebar flex-1 min-h-0 chat-home sidebar-cell"
|
||||
/>
|
||||
@@ -180,7 +182,8 @@ import Steam from "./Steam.vue";
|
||||
}
|
||||
|
||||
.commits-sidebar,
|
||||
.steam-sidebar {
|
||||
.steam-sidebar,
|
||||
.bookmarks-sidebar {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import LinkTable from "@/components/util/LinkTable.vue";
|
||||
import Header from "@/components/text/Header.vue";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
@@ -16,18 +17,30 @@ const groupedBookmarks = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="items-center flex flex-col">
|
||||
<div
|
||||
class="a4page-portrait bdr-1 flex flex-row flex-wrap overflow-x-auto gap-1"
|
||||
>
|
||||
<div class="w-full h-fit">
|
||||
<LinkTable
|
||||
class="flex flex-col flex-wrap"
|
||||
v-for="group in groupedBookmarks"
|
||||
:title="group[0]"
|
||||
:items="group[1]"
|
||||
/>
|
||||
</div>
|
||||
<div class="bookmarks-wrapper">
|
||||
<Header class="text-left">Bookmarks</Header>
|
||||
<div class="bookmarks-scroll">
|
||||
<LinkTable
|
||||
v-for="group in groupedBookmarks"
|
||||
:key="group[0]"
|
||||
:title="group[0]"
|
||||
:items="group[1]"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bookmarks-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bookmarks-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user