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>
124 lines
3.3 KiB
Vue
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>
|