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>
72 lines
1.8 KiB
Vue
72 lines
1.8 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Generic name/function autocomplete dropdown -- used both for a formula
|
|
* editor's function-name autocomplete (see useFormulaAutocomplete.ts) and
|
|
* the Name Box's table/sheet/named-range autocomplete (see document.ts's
|
|
* `nameBoxSuggestions()`), anything shaped like {name, hint}. Purely
|
|
* presentational -- positions itself just under whatever it's rendered
|
|
* inside of (that parent needs `position: relative`); no caret-pixel
|
|
* tracking.
|
|
*/
|
|
defineProps<{ items: { name: string; hint: string }[]; activeIndex: number }>();
|
|
const emit = defineEmits<{ (e: 'pick', index: number): void }>();
|
|
</script>
|
|
|
|
<template>
|
|
<ul class="formula-suggest" role="listbox">
|
|
<li
|
|
v-for="(item, i) in items"
|
|
:key="item.name"
|
|
role="option"
|
|
:aria-selected="i === activeIndex"
|
|
:class="{ on: i === activeIndex }"
|
|
@mousedown.prevent="emit('pick', i)"
|
|
>
|
|
<span class="name">{{ item.name }}</span>
|
|
<span class="hint">{{ item.hint }}</span>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.formula-suggest {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
z-index: 30;
|
|
margin: 2px 0 0;
|
|
padding: 4px;
|
|
min-width: 220px;
|
|
max-width: 420px;
|
|
max-height: 220px;
|
|
overflow-y: auto;
|
|
list-style: none;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
font-size: 12px;
|
|
}
|
|
.formula-suggest li {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
padding: 5px 8px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
.formula-suggest li.on {
|
|
background: var(--accent-weak);
|
|
}
|
|
.name {
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
}
|
|
.hint {
|
|
color: var(--text-dim);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
</style>
|