Files
numolus/app/frontend/App.vue
T
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

124 lines
3.3 KiB
Vue

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import AppToolbar from './components/AppToolbar.vue';
import FormulaBar from './components/FormulaBar.vue';
import SheetCanvas from './components/SheetCanvas.vue';
import SheetTabs from './components/SheetTabs.vue';
import StatusBar from './components/StatusBar.vue';
import SettingsPanel from './components/SettingsPanel.vue';
import DependencyGraphView from './components/DependencyGraphView.vue';
import { useToasts } from './composables/useToasts';
import { useDocument } from './store/document';
const toolbar = ref<InstanceType<typeof AppToolbar> | null>(null);
const { toasts, dismiss } = useToasts();
const store = useDocument();
function onCanvasFiles(files: FileList) {
if (files[0]) toolbar.value?.handleFile(files[0]);
}
/** Fallback Strg/Cmd+Z/Y when no table grid has focus (e.g. an empty canvas
* click) -- TableGrid.vue handles it directly (and correctly defers to a
* cell editor's own native undo) whenever a table *does* have focus. Skips
* entirely while focus sits in any other text field (rename-sheet, CSV
* import, a colour input, ...), so their own native undo keeps working. */
function isTextEditable(el: Element | null): boolean {
if (!el) return false;
const tag = el.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || (el as HTMLElement).isContentEditable;
}
function onGlobalKeydown(e: KeyboardEvent) {
if (!(e.ctrlKey || e.metaKey)) return;
const k = e.key.toLowerCase();
if (k !== 'z' && k !== 'y') return;
if (isTextEditable(document.activeElement)) return;
e.preventDefault();
if (k === 'y' || (k === 'z' && e.shiftKey)) store.redo();
else store.undo();
}
onMounted(() => window.addEventListener('keydown', onGlobalKeydown));
onBeforeUnmount(() => window.removeEventListener('keydown', onGlobalKeydown));
</script>
<template>
<div class="app">
<AppToolbar ref="toolbar" />
<FormulaBar />
<main class="workspace">
<SheetCanvas @files="onCanvasFiles" />
<SettingsPanel />
<DependencyGraphView />
</main>
<div class="bottom-bar">
<SheetTabs />
<StatusBar />
</div>
<div class="toasts">
<div
v-for="t in toasts"
:key="t.id"
class="toast"
:class="t.kind"
@click="dismiss(t.id)"
>
{{ t.message }}
</div>
</div>
</div>
</template>
<style scoped>
.app {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
}
.bottom-bar {
display: flex;
align-items: center;
background: var(--surface-2);
border-top: 1px solid var(--border);
}
.workspace {
position: relative;
flex: 1;
min-height: 0;
margin: 10px;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: var(--surface);
box-shadow: var(--shadow);
}
.toasts {
position: fixed;
right: 16px;
bottom: 16px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 1200;
}
.toast {
padding: 10px 14px;
border-radius: var(--radius-sm);
background: var(--surface);
border: 1px solid var(--border);
box-shadow: var(--shadow);
font-size: 13px;
cursor: pointer;
max-width: 320px;
}
.toast.success {
border-color: var(--series-2);
}
.toast.error {
border-color: var(--danger);
}
</style>