Files
wkhandClaude Opus 5.5 e14bada114 Add Rails backend: accounts via magic link, server documents, sharing
The app becomes a Rails 8 application; the Vue frontend moves to
app/frontend and is served through vite_rails.

- Without an account documents stay in the browser (IndexedDB). Sign-in
  is by magic link or the 6-digit code from the same mail; afterwards
  local documents can be uploaded into the account.
- Server documents (/d/<id>) with versioned saves and a single-editor
  lock per browser tab; others see them read-only and follow by polling.
- Sharing via link (works without an account) and via e-mail invitation,
  roles viewer/editor; the owner manages links and people.
- Store gains a read-only mode that blocks all content changes.
- Solid Queue, Cache and Cable share the one PostgreSQL database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:09:06 +02:00

163 lines
3.6 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/** Numbers-style bottom tab bar. Native HTML5 drag & drop for reordering. */
import { nextTick, ref } from 'vue';
import { useDocument } from '../store/document';
const store = useDocument();
const renaming = ref<string | null>(null);
const renameDraft = ref('');
const renameInput = ref<HTMLInputElement | null>(null);
let dragIndex = -1;
function startRename(id: string, name: string) {
renaming.value = id;
renameDraft.value = name;
nextTick(() => renameInput.value?.select());
}
function commitRename() {
if (renaming.value) store.renameSheet(renaming.value, renameDraft.value);
renaming.value = null;
}
function cancelRename() {
renaming.value = null;
}
function removeSheet(id: string, name: string, hasContent: boolean) {
if (store.doc.sheets.length <= 1) return;
if (hasContent && !confirm(`Blatt „${name}" mit allen Objekten löschen?`)) return;
store.deleteSheet(id);
}
function onDragStart(i: number) {
dragIndex = i;
}
function onDrop(i: number) {
if (dragIndex >= 0 && dragIndex !== i) store.moveSheet(dragIndex, i);
dragIndex = -1;
}
</script>
<template>
<div class="tabs">
<div class="tab-list">
<div
v-for="(s, i) in store.doc.sheets"
:key="s.id"
class="tab"
:class="{ active: s.id === store.doc.activeSheetId }"
draggable="true"
@click="store.setActiveSheet(s.id)"
@dblclick="startRename(s.id, s.name)"
@dragstart="onDragStart(i)"
@dragover.prevent
@drop="onDrop(i)"
>
<input
v-if="renaming === s.id"
ref="renameInput"
v-model="renameDraft"
class="rename"
@click.stop
@blur="commitRename"
@keydown.enter.prevent="commitRename"
@keydown.esc.prevent="cancelRename"
/>
<template v-else>
<span class="label">{{ s.name }}</span>
<button
v-if="store.doc.sheets.length > 1"
class="close"
title="Blatt löschen"
@click.stop="removeSheet(s.id, s.name, s.objects.length > 0)"
>
×
</button>
</template>
</div>
</div>
<button class="add" title="Blatt hinzufügen" @click="store.addSheet()">+</button>
</div>
</template>
<style scoped>
.tabs {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
flex: 1 1 auto;
min-width: 60px;
}
.tab-list {
display: flex;
gap: 2px;
overflow-x: auto;
scrollbar-width: thin;
min-width: 0;
}
.tab {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 8px 8px 0 0;
font-size: 12.5px;
color: var(--text-dim);
cursor: pointer;
white-space: nowrap;
user-select: none;
border: 1px solid transparent;
border-bottom: none;
}
.tab:hover {
background: var(--surface);
}
.tab.active {
background: var(--surface);
color: var(--text);
border-color: var(--border);
font-weight: 600;
}
.close {
border: 0;
background: transparent;
color: var(--text-dim);
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 0 2px;
border-radius: 4px;
opacity: 0;
}
.tab:hover .close,
.tab.active .close {
opacity: 1;
}
.close:hover {
background: var(--danger);
color: #fff;
}
.rename {
font: inherit;
border: 1px solid var(--accent);
border-radius: 4px;
padding: 1px 4px;
width: 96px;
}
.add {
border: 1px solid var(--border);
background: var(--surface);
border-radius: 8px;
width: 26px;
height: 26px;
cursor: pointer;
font-size: 15px;
color: var(--text-dim);
}
.add:hover {
border-color: var(--accent);
color: var(--accent);
}
</style>