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,92 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* A formatted text box object. Formatting (size, bold, italic, alignment,
|
||||
* text/background colour) lives on the object and is edited in the inspector;
|
||||
* double-click here edits the text itself.
|
||||
*/
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import { useDocument } from '../store/document';
|
||||
|
||||
const props = defineProps<{ objId: string }>();
|
||||
const store = useDocument();
|
||||
|
||||
const obj = computed(() => {
|
||||
const o = store.objectById(props.objId);
|
||||
return o && o.kind === 'text' ? o : undefined;
|
||||
});
|
||||
|
||||
const editing = ref(false);
|
||||
const draft = ref('');
|
||||
const area = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
function beginEdit() {
|
||||
if (!obj.value) return;
|
||||
draft.value = obj.value.text;
|
||||
editing.value = true;
|
||||
nextTick(() => area.value?.focus());
|
||||
}
|
||||
function commit() {
|
||||
if (obj.value) store.setTextContent(obj.value.id, draft.value);
|
||||
editing.value = false;
|
||||
}
|
||||
function cancel() {
|
||||
editing.value = false;
|
||||
}
|
||||
|
||||
const boxStyle = computed(() => {
|
||||
const o = obj.value;
|
||||
if (!o) return {};
|
||||
return {
|
||||
fontSize: o.fontSize + 'px',
|
||||
fontWeight: o.bold ? '600' : '400',
|
||||
fontStyle: o.italic ? 'italic' : 'normal',
|
||||
textAlign: o.align,
|
||||
color: o.color || 'var(--text)',
|
||||
background: o.bg || 'transparent',
|
||||
} as Record<string, string>;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="obj" class="textbox" :style="boxStyle" @dblclick="beginEdit">
|
||||
<textarea
|
||||
v-if="editing"
|
||||
ref="area"
|
||||
v-model="draft"
|
||||
class="edit"
|
||||
spellcheck="false"
|
||||
@blur="commit"
|
||||
@keydown.esc.prevent="cancel"
|
||||
@keydown.enter.exact.stop
|
||||
@mousedown.stop
|
||||
/>
|
||||
<div v-else class="view">{{ obj.text || 'Doppelklick zum Bearbeiten' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.textbox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 10px;
|
||||
overflow: auto;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.view {
|
||||
min-height: 100%;
|
||||
}
|
||||
.edit {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
outline: none;
|
||||
resize: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user