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>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = defineProps<{ open: boolean; title: string; wide?: boolean }>();
|
||||
const emit = defineEmits<{ (e: 'close'): void }>();
|
||||
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') emit('close');
|
||||
}
|
||||
watch(
|
||||
() => props.open,
|
||||
(o) => {
|
||||
if (o) document.addEventListener('keydown', onKey);
|
||||
else document.removeEventListener('keydown', onKey);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="backdrop" @click.self="emit('close')">
|
||||
<div class="modal" :class="{ wide }" role="dialog" aria-modal="true">
|
||||
<header>
|
||||
<h2>{{ title }}</h2>
|
||||
<button class="x" @click="emit('close')">×</button>
|
||||
</header>
|
||||
<div class="body"><slot /></div>
|
||||
<footer><slot name="footer" /></footer>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 900;
|
||||
}
|
||||
.modal {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
/* never taller than the viewport: header/footer stay put, the body scrolls */
|
||||
max-height: calc(100dvh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.modal.wide {
|
||||
width: min(920px, calc(100vw - 32px));
|
||||
}
|
||||
header {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.x {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.body {
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* wide dialogs lay out one full-height child (e.g. an editor + side panel)
|
||||
that shrinks with the viewport and scrolls its own parts */
|
||||
.modal.wide .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
footer {
|
||||
flex: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
:slotted(button) {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
padding: 6px 14px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
:slotted(button.primary) {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user