Extract Vue frontend into separate container and add stp_wasm crate
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m58s

Move Vue app from nginx/vue/ to top-level vue/ with its own Dockerfile,
update docker-compose configs and nginx proxy to serve from the new
container, and add initial Rust WASM crate (stp_wasm). Also fix .gitignore
to exclude Rust target/ directories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 16:40:45 +00:00
parent 2b84730126
commit d3d3269d49
182 changed files with 215 additions and 34 deletions

View File

@@ -0,0 +1,7 @@
<template>
<div class="w-full border-b border-primary">
<h1 class="pl-2 m-0">
<slot />
</h1>
</div>
</template>

View File

@@ -0,0 +1,85 @@
<script setup>
import { onMounted, useTemplateRef, onUnmounted } from "vue";
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;
onMounted(() => {
measureWidth();
rafId = requestAnimationFrame(animate);
resizeObserver = new ResizeObserver(measureWidth);
resizeObserver.observe(container.value);
});
onUnmounted(() => {
cancelAnimationFrame(rafId);
resizeObserver?.disconnect();
});
</script>
<template>
<div class="root">
<div class="container" ref="container">
<div ref="item1">
<slot />
</div>
<div>
<slot />
</div>
</div>
</div>
</template>
<style scoped>
.root {
overflow: hidden;
}
.container {
width: fit-content;
height: fit-content;
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
/* Each column fits its content */
overflow-x: visible;
will-change: transform;
gap: 10em;
}
</style>

View File

@@ -0,0 +1,39 @@
<script setup>
import { computed } from "vue";
const props = defineProps({
href: { type: String, default: "" },
to: { type: String, default: "" },
target: { type: String, default: undefined },
rel: { type: String, default: undefined },
});
const computedRel = computed(() => {
if (props.rel !== undefined) return props.rel;
if (props.target === "_blank") return "noopener noreferrer";
return undefined;
});
</script>
<template>
<RouterLink v-if="to" :to="to" class="inline-link">
<slot />
</RouterLink>
<a v-else :href="href" :target="target" :rel="computedRel" class="inline-link">
<slot />
</a>
</template>
<style scoped>
.inline-link {
color: var(--primary);
font-weight: bold;
font-style: italic;
text-decoration: none;
transition: color 0.15s ease;
}
.inline-link:hover {
color: var(--tertiary);
}
</style>

View File

@@ -0,0 +1,38 @@
<script setup>
import { computed } from "vue";
const props = defineProps({
href: { type: String, default: "" },
to: { type: String, default: "" },
target: { type: String, default: undefined },
rel: { type: String, default: undefined },
bare: { type: Boolean, default: false },
});
const computedRel = computed(() => {
if (props.rel !== undefined) return props.rel;
if (props.target === "_blank") return "noopener noreferrer";
return undefined;
});
</script>
<template>
<RouterLink v-if="to" :to="to" :class="{ link: !bare }">
<slot />
</RouterLink>
<a v-else :href="href" :target="target" :rel="computedRel" :class="{ link: !bare }">
<slot />
</a>
</template>
<style scoped>
.link {
color: var(--primary);
text-decoration: none;
transition: color 0.15s ease;
}
.link:hover {
color: var(--tertiary);
}
</style>

View File

@@ -0,0 +1,5 @@
<template>
<p class="p-1">
<slot />
</p>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { ref } from "vue";
import ToggleButton from "@/components/input/ToggleButton.vue";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue"]);
const toggleButtonRef = ref(null);
const updateValue = (newValue) => {
emit("update:modelValue", newValue);
};
const handleClick = () => {
toggleButtonRef.value?.$el?.click();
};
</script>
<template>
<div
class="w-full border-b border-primary cursor-pointer"
@click="handleClick"
>
<h1 class="pl-2 m-0">
<slot />
</h1>
<ToggleButton
class="pointer-events-none"
:model-value="props.modelValue"
@update:model-value="updateValue"
ref="toggleButtonRef"
/>
</div>
</template>