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,7 +1,17 @@
|
||||
# Stage 1: Build WASM from Rust
|
||||
FROM rust:slim AS wasm-builder
|
||||
RUN rustup target add wasm32-unknown-unknown \
|
||||
&& cargo install wasm-pack
|
||||
WORKDIR /wasm
|
||||
COPY crates/stp_wasm/ crates/stp_wasm/
|
||||
RUN wasm-pack build crates/stp_wasm --target web --out-dir ../../src/wasm
|
||||
|
||||
# Stage 2: Build Vue frontend
|
||||
FROM node:22-slim
|
||||
RUN apt-get update && apt-get install -y make git && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
CMD ["sh", "-c", "npm run build -- --outDir /output --emptyOutDir"]
|
||||
COPY --from=wasm-builder /wasm/src/wasm/ src/wasm/
|
||||
CMD ["sh", "-c", "npx vite build --outDir /output --emptyOutDir"]
|
||||
|
||||
@@ -3,6 +3,9 @@ name = "stp_wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3.85"
|
||||
wasm-bindgen = "0.2.108"
|
||||
@@ -10,6 +13,13 @@ web-sys = { version = "0.3.85", features = [
|
||||
"console",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"Window",
|
||||
"Animation",
|
||||
"CssStyleDeclaration",
|
||||
"ResizeObserver",
|
||||
"ResizeObserverEntry",
|
||||
"ResizeObserverSize",
|
||||
"EventTarget",
|
||||
"MouseEvent",
|
||||
] }
|
||||
|
||||
259
vue/crates/stp_wasm/src/auto_scroll.rs
Normal file
259
vue/crates/stp_wasm/src/auto_scroll.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
const SPEED: f64 = 0.0005; // % per frame
|
||||
const PAUSE: i32 = 2000; // ms at top/bottom
|
||||
|
||||
struct Inner {
|
||||
el: HtmlElement,
|
||||
pos: f64,
|
||||
direction: f64, // 1.0 = down, -1.0 = up
|
||||
hovered: bool,
|
||||
cached_scroll_height: f64,
|
||||
raf_id: Option<i32>,
|
||||
pause_timeout_id: Option<i32>,
|
||||
resize_observer: Option<web_sys::ResizeObserver>,
|
||||
// closures kept alive
|
||||
tick_closure: Option<Closure<dyn FnMut()>>,
|
||||
resize_closure: Option<Closure<dyn FnMut(js_sys::Array)>>,
|
||||
mouseenter_closure: Option<Closure<dyn FnMut()>>,
|
||||
mouseleave_closure: Option<Closure<dyn FnMut()>>,
|
||||
start_after_pause_closure: Option<Closure<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct AutoScroller {
|
||||
inner: Rc<RefCell<Inner>>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn measure_scroll_height(&mut self) {
|
||||
self.cached_scroll_height = self.el.scroll_height() as f64;
|
||||
}
|
||||
|
||||
fn stop_loop(&mut self) {
|
||||
let window = web_sys::window().unwrap();
|
||||
if let Some(id) = self.raf_id.take() {
|
||||
window.cancel_animation_frame(id).ok();
|
||||
}
|
||||
if let Some(id) = self.pause_timeout_id.take() {
|
||||
window.clear_timeout_with_handle(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_loop(inner: &Rc<RefCell<Inner>>) {
|
||||
{
|
||||
let mut s = inner.borrow_mut();
|
||||
s.stop_loop();
|
||||
}
|
||||
schedule_tick(inner);
|
||||
}
|
||||
|
||||
fn schedule_tick(inner: &Rc<RefCell<Inner>>) {
|
||||
let inner_clone = Rc::clone(inner);
|
||||
|
||||
// Create a fresh tick closure each frame
|
||||
let closure = Closure::once(move || {
|
||||
tick(&inner_clone);
|
||||
});
|
||||
|
||||
let mut s = inner.borrow_mut();
|
||||
let window = web_sys::window().unwrap();
|
||||
let id = window
|
||||
.request_animation_frame(closure.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
s.raf_id = Some(id);
|
||||
// Store the closure to keep it alive until the frame fires
|
||||
s.tick_closure = Some(closure);
|
||||
}
|
||||
|
||||
fn tick(inner: &Rc<RefCell<Inner>>) {
|
||||
let should_continue;
|
||||
{
|
||||
let mut s = inner.borrow_mut();
|
||||
s.raf_id = None;
|
||||
|
||||
if s.hovered {
|
||||
return;
|
||||
}
|
||||
|
||||
if s.cached_scroll_height == 0.0 {
|
||||
drop(s);
|
||||
schedule_tick(inner);
|
||||
return;
|
||||
}
|
||||
|
||||
let reached_bottom = s.pos >= 1.0;
|
||||
let reached_top = s.pos <= 0.0;
|
||||
|
||||
if reached_bottom {
|
||||
s.pos = 0.999;
|
||||
s.direction = -1.0;
|
||||
drop(s);
|
||||
schedule_pause(inner);
|
||||
return;
|
||||
} else if reached_top && s.direction == -1.0 {
|
||||
s.pos = 0.001;
|
||||
s.direction = 1.0;
|
||||
drop(s);
|
||||
schedule_pause(inner);
|
||||
return;
|
||||
}
|
||||
|
||||
s.pos += s.direction * SPEED;
|
||||
s.el.set_scroll_top((s.pos * s.cached_scroll_height) as i32);
|
||||
should_continue = true;
|
||||
}
|
||||
|
||||
if should_continue {
|
||||
schedule_tick(inner);
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_pause(inner: &Rc<RefCell<Inner>>) {
|
||||
{
|
||||
let mut s = inner.borrow_mut();
|
||||
s.stop_loop();
|
||||
}
|
||||
|
||||
let inner_clone = Rc::clone(inner);
|
||||
let closure = Closure::once(move || {
|
||||
start_loop(&inner_clone);
|
||||
});
|
||||
|
||||
let mut s = inner.borrow_mut();
|
||||
let window = web_sys::window().unwrap();
|
||||
let id = window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
closure.as_ref().unchecked_ref(),
|
||||
PAUSE,
|
||||
)
|
||||
.unwrap();
|
||||
s.pause_timeout_id = Some(id);
|
||||
s.start_after_pause_closure = Some(closure);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl AutoScroller {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(el: HtmlElement) -> AutoScroller {
|
||||
let inner = Rc::new(RefCell::new(Inner {
|
||||
el,
|
||||
pos: 0.0,
|
||||
direction: 1.0,
|
||||
hovered: false,
|
||||
cached_scroll_height: 0.0,
|
||||
raf_id: None,
|
||||
pause_timeout_id: None,
|
||||
resize_observer: None,
|
||||
tick_closure: None,
|
||||
resize_closure: None,
|
||||
mouseenter_closure: None,
|
||||
mouseleave_closure: None,
|
||||
start_after_pause_closure: None,
|
||||
}));
|
||||
|
||||
// Set up mouseenter listener
|
||||
{
|
||||
let inner_clone = Rc::clone(&inner);
|
||||
let closure = Closure::wrap(Box::new(move || {
|
||||
let mut s = inner_clone.borrow_mut();
|
||||
s.hovered = true;
|
||||
s.stop_loop();
|
||||
}) as Box<dyn FnMut()>);
|
||||
let s = inner.borrow();
|
||||
s.el
|
||||
.add_event_listener_with_callback("mouseenter", closure.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
drop(s);
|
||||
inner.borrow_mut().mouseenter_closure = Some(closure);
|
||||
}
|
||||
|
||||
// Set up mouseleave listener
|
||||
{
|
||||
let inner_clone = Rc::clone(&inner);
|
||||
let closure = Closure::wrap(Box::new(move || {
|
||||
{
|
||||
let mut s = inner_clone.borrow_mut();
|
||||
s.hovered = false;
|
||||
if s.cached_scroll_height > 0.0 {
|
||||
s.pos = s.el.scroll_top() as f64 / s.cached_scroll_height;
|
||||
}
|
||||
}
|
||||
start_loop(&inner_clone);
|
||||
}) as Box<dyn FnMut()>);
|
||||
let s = inner.borrow();
|
||||
s.el
|
||||
.add_event_listener_with_callback("mouseleave", closure.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
drop(s);
|
||||
inner.borrow_mut().mouseleave_closure = Some(closure);
|
||||
}
|
||||
|
||||
AutoScroller { inner }
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
// Measure initial scroll height
|
||||
self.inner.borrow_mut().measure_scroll_height();
|
||||
|
||||
// Set up resize observer
|
||||
let inner_clone = Rc::clone(&self.inner);
|
||||
let resize_closure = Closure::wrap(Box::new(move |_entries: js_sys::Array| {
|
||||
inner_clone.borrow_mut().measure_scroll_height();
|
||||
}) as Box<dyn FnMut(js_sys::Array)>);
|
||||
|
||||
let observer =
|
||||
web_sys::ResizeObserver::new(resize_closure.as_ref().unchecked_ref()).unwrap();
|
||||
|
||||
// Clone the element ref and drop the borrow before calling observe(),
|
||||
// because observe() can fire the resize callback synchronously,
|
||||
// which would conflict with an active borrow.
|
||||
let el_clone = self.inner.borrow().el.clone();
|
||||
observer.observe(&el_clone);
|
||||
|
||||
{
|
||||
let mut s = self.inner.borrow_mut();
|
||||
s.resize_observer = Some(observer);
|
||||
s.resize_closure = Some(resize_closure);
|
||||
}
|
||||
|
||||
// Start with a pause then begin scrolling
|
||||
schedule_pause(&self.inner);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
let mut s = self.inner.borrow_mut();
|
||||
s.stop_loop();
|
||||
|
||||
if let Some(observer) = s.resize_observer.take() {
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
if let Some(ref closure) = s.mouseenter_closure {
|
||||
s.el
|
||||
.remove_event_listener_with_callback(
|
||||
"mouseenter",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
if let Some(ref closure) = s.mouseleave_closure {
|
||||
s.el
|
||||
.remove_event_listener_with_callback(
|
||||
"mouseleave",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
s.mouseenter_closure = None;
|
||||
s.mouseleave_closure = None;
|
||||
s.resize_closure = None;
|
||||
s.tick_closure = None;
|
||||
s.start_after_pause_closure = None;
|
||||
}
|
||||
}
|
||||
135
vue/crates/stp_wasm/src/headline.rs
Normal file
135
vue/crates/stp_wasm/src/headline.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
const SPEED: f64 = 0.5; // pixels per frame
|
||||
|
||||
struct Inner {
|
||||
container: HtmlElement,
|
||||
item1: HtmlElement,
|
||||
offset: f64,
|
||||
cached_width: f64,
|
||||
raf_id: Option<i32>,
|
||||
resize_observer: Option<web_sys::ResizeObserver>,
|
||||
// closures kept alive
|
||||
animate_closure: Option<Closure<dyn FnMut()>>,
|
||||
resize_closure: Option<Closure<dyn FnMut(js_sys::Array)>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct HeadlineScroller {
|
||||
inner: Rc<RefCell<Inner>>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn measure_width(&mut self) {
|
||||
let container_width = self.container.offset_width() as f64;
|
||||
let item_width = self.item1.scroll_width() as f64;
|
||||
self.cached_width = container_width.max(item_width);
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_frame(inner: &Rc<RefCell<Inner>>) {
|
||||
let inner_clone = Rc::clone(inner);
|
||||
|
||||
let closure = Closure::once(move || {
|
||||
animate(&inner_clone);
|
||||
});
|
||||
|
||||
let mut s = inner.borrow_mut();
|
||||
let window = web_sys::window().unwrap();
|
||||
let id = window
|
||||
.request_animation_frame(closure.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
s.raf_id = Some(id);
|
||||
s.animate_closure = Some(closure);
|
||||
}
|
||||
|
||||
fn animate(inner: &Rc<RefCell<Inner>>) {
|
||||
{
|
||||
let mut s = inner.borrow_mut();
|
||||
s.raf_id = None;
|
||||
|
||||
if s.cached_width == 0.0 {
|
||||
drop(s);
|
||||
schedule_frame(inner);
|
||||
return;
|
||||
}
|
||||
|
||||
s.offset -= SPEED;
|
||||
|
||||
if s.offset <= -s.cached_width {
|
||||
s.offset += s.cached_width;
|
||||
}
|
||||
|
||||
let transform = format!("translateX({}px)", s.offset);
|
||||
s.container.style().set_property("transform", &transform).ok();
|
||||
}
|
||||
|
||||
schedule_frame(inner);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl HeadlineScroller {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(container: HtmlElement, item1: HtmlElement) -> HeadlineScroller {
|
||||
let inner = Rc::new(RefCell::new(Inner {
|
||||
container,
|
||||
item1,
|
||||
offset: 0.0,
|
||||
cached_width: 0.0,
|
||||
raf_id: None,
|
||||
resize_observer: None,
|
||||
animate_closure: None,
|
||||
resize_closure: None,
|
||||
}));
|
||||
|
||||
HeadlineScroller { inner }
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
// Measure initial width
|
||||
self.inner.borrow_mut().measure_width();
|
||||
|
||||
// Set up resize observer
|
||||
let inner_clone = Rc::clone(&self.inner);
|
||||
let resize_closure = Closure::wrap(Box::new(move |_entries: js_sys::Array| {
|
||||
inner_clone.borrow_mut().measure_width();
|
||||
}) as Box<dyn FnMut(js_sys::Array)>);
|
||||
|
||||
let observer =
|
||||
web_sys::ResizeObserver::new(resize_closure.as_ref().unchecked_ref()).unwrap();
|
||||
|
||||
// Clone the element ref and drop the borrow before calling observe(),
|
||||
// because observe() can fire the resize callback synchronously,
|
||||
// which would conflict with an active borrow.
|
||||
let container_clone = self.inner.borrow().container.clone();
|
||||
observer.observe(&container_clone);
|
||||
|
||||
{
|
||||
let mut s = self.inner.borrow_mut();
|
||||
s.resize_observer = Some(observer);
|
||||
s.resize_closure = Some(resize_closure);
|
||||
}
|
||||
|
||||
// Start animation loop
|
||||
schedule_frame(&self.inner);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
let mut s = self.inner.borrow_mut();
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
if let Some(id) = s.raf_id.take() {
|
||||
window.cancel_animation_frame(id).ok();
|
||||
}
|
||||
|
||||
if let Some(observer) = s.resize_observer.take() {
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
s.animate_closure = None;
|
||||
s.resize_closure = None;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
mod auto_scroll;
|
||||
mod headline;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct BadApplePlayer {
|
||||
is_playing: bool,
|
||||
}
|
||||
pub use auto_scroll::AutoScroller;
|
||||
pub use headline::HeadlineScroller;
|
||||
|
||||
349
vue/package-lock.json
generated
349
vue/package-lock.json
generated
@@ -25,7 +25,9 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-vue-devtools": "^8.0.3"
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vite-plugin-wasm": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -1024,6 +1026,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-virtual": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
|
||||
"integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
@@ -1349,6 +1369,293 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz",
|
||||
"integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.26"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.15.24",
|
||||
"@swc/core-darwin-x64": "1.15.24",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.24",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.24",
|
||||
"@swc/core-linux-arm64-musl": "1.15.24",
|
||||
"@swc/core-linux-ppc64-gnu": "1.15.24",
|
||||
"@swc/core-linux-s390x-gnu": "1.15.24",
|
||||
"@swc/core-linux-x64-gnu": "1.15.24",
|
||||
"@swc/core-linux-x64-musl": "1.15.24",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.24",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.24",
|
||||
"@swc/core-win32-x64-msvc": "1.15.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz",
|
||||
"integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz",
|
||||
"integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz",
|
||||
"integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz",
|
||||
"integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz",
|
||||
"integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-ppc64-gnu": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz",
|
||||
"integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-s390x-gnu": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz",
|
||||
"integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz",
|
||||
"integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz",
|
||||
"integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz",
|
||||
"integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz",
|
||||
"integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz",
|
||||
"integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.26",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
|
||||
"integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/wasm": {
|
||||
"version": "1.15.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.15.24.tgz",
|
||||
"integrity": "sha512-vFjzOE8dhJcfeTbM4+HO9Qy58IINV0ysqStAgw81uds+KqCeUDM9huN+SZ5lWZ6U+5nf8VcZoEw5N81xMtAidg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
@@ -3518,6 +3825,20 @@
|
||||
"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
|
||||
"license": "(WTFPL OR MIT)"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
@@ -3661,6 +3982,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite-plugin-top-level-await": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz",
|
||||
"integrity": "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@swc/core": "^1.12.14",
|
||||
"@swc/wasm": "^1.12.14",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": ">=2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-vue-devtools": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.5.tgz",
|
||||
@@ -3736,6 +4073,16 @@
|
||||
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-wasm": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz",
|
||||
"integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:wasm": "wasm-pack build crates/stp_wasm --target web --out-dir ../../src/wasm",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "npm run build:wasm && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -29,6 +30,8 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-vue-devtools": "^8.0.3"
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vite-plugin-wasm": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { defineConfig } from "vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueDevTools from "vite-plugin-vue-devtools";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -11,6 +13,8 @@ export default defineConfig({
|
||||
vue(),
|
||||
...(process.env.NODE_ENV !== "production" ? [vueDevTools()] : []),
|
||||
tailwindcss(),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user