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>
765 lines
28 KiB
Vue
765 lines
28 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* Left inspector drawer, opened from the gear button next to the title.
|
||
* Gathers every important setting in one place: the selected object (table
|
||
* sizing / frozen rows & columns / position, or chart), the active sheet, and
|
||
* document-wide options (theme, CSV default, reset), plus the canvas zoom.
|
||
*/
|
||
import { computed } from 'vue';
|
||
import { useDocument } from '../store/document';
|
||
import { useTheme } from '../composables/useTheme';
|
||
import { NUMBER_LOCALES, NUM_FORMATS } from '../store/format';
|
||
import type { CellFormat, HAlign } from '../store/types';
|
||
import { DEFAULT_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_SIZE } from '../store/types';
|
||
import ToolbarIcon from './ToolbarIcon.vue';
|
||
|
||
const store = useDocument();
|
||
const { mode: themeMode } = useTheme();
|
||
|
||
const open = computed(() => store.ui.settingsOpen);
|
||
const obj = computed(() => store.activeObject.value);
|
||
const table = computed(() =>
|
||
obj.value?.kind === 'table' && obj.value.view ? obj.value : null,
|
||
);
|
||
const chart = computed(() => (obj.value?.kind === 'chart' ? obj.value : null));
|
||
const text = computed(() => (obj.value?.kind === 'text' ? obj.value : null));
|
||
const button = computed(() => (obj.value?.kind === 'button' ? obj.value : null));
|
||
const sheet = computed(() => store.activeSheet.value);
|
||
|
||
const num = (e: Event) => Number((e.target as HTMLInputElement).value);
|
||
const str = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value;
|
||
const chk = (e: Event) => (e.target as HTMLInputElement).checked;
|
||
|
||
const tableList = computed(() =>
|
||
store.doc.sheets.flatMap((s) => s.objects).filter((o) => o.kind === 'table'),
|
||
);
|
||
|
||
// ---- cell-selection formatting (only when a table cell range is selected) ---
|
||
const sel = computed(() => store.doc.selection);
|
||
const selFmt = computed<CellFormat>(() => store.selectionFormat.value);
|
||
const hasCellSel = computed(() => !!sel.value && sel.value.tableId === table.value?.id);
|
||
const selFocus = computed(() => sel.value?.focus ?? { col: 0, row: 0 });
|
||
const focusColW = computed(() =>
|
||
table.value ? (table.value.colWidths[selFocus.value.col] ?? 104) : 104,
|
||
);
|
||
const focusRowH = computed(() =>
|
||
table.value ? store.rowHeightOf(table.value.id, selFocus.value.row) : 26,
|
||
);
|
||
function applyFmt(patch: Partial<Record<keyof CellFormat, unknown>>) {
|
||
store.applySelectionFormat(patch); // applies to every rectangle of the selection
|
||
}
|
||
function setAlign(a: HAlign) {
|
||
applyFmt({ align: selFmt.value.align === a ? null : a });
|
||
}
|
||
|
||
function close() {
|
||
store.ui.settingsOpen = false;
|
||
}
|
||
function resetDoc() {
|
||
if (confirm('Vorbefülltes Dokument wiederherstellen? Alle Änderungen gehen verloren.')) {
|
||
store.resetToSeed();
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<aside class="drawer" :class="{ open }" aria-label="Einstellungen">
|
||
<header>
|
||
<h2>Einstellungen</h2>
|
||
<button class="x" title="Schließen" @click="close">×</button>
|
||
</header>
|
||
|
||
<div class="body">
|
||
<!-- ===================== selected object ===================== -->
|
||
<section>
|
||
<h3>Objekt</h3>
|
||
|
||
<template v-if="table">
|
||
<label class="f">
|
||
<span>Name</span>
|
||
<input :value="table.name" @change="store.renameObject(table.id, str($event))" />
|
||
</label>
|
||
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Zeilen</span>
|
||
<input type="number" min="1" :value="table.rows" @change="store.setTableRows(table.id, num($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Spalten</span>
|
||
<input type="number" min="1" :value="table.cols" @change="store.setTableCols(table.id, num($event))" />
|
||
</label>
|
||
</div>
|
||
|
||
<label class="f">
|
||
<span>Darstellung</span>
|
||
<select :value="table.view.mode" @change="store.setTableView(table.id, { mode: str($event) === 'fixed' ? 'fixed' : 'auto' })">
|
||
<option value="auto">Automatisch – alle Zeilen zeigen</option>
|
||
<option value="fixed">Feste Größe – scrollbar</option>
|
||
</select>
|
||
</label>
|
||
|
||
<template v-if="table.view.mode === 'fixed'">
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Breite (px)</span>
|
||
<input type="number" min="200" step="10" :value="table.view.width" @change="store.setTableView(table.id, { width: num($event) })" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Höhe (px)</span>
|
||
<input type="number" min="120" step="10" :value="table.view.height" @change="store.setTableView(table.id, { height: num($event) })" />
|
||
</label>
|
||
</div>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Fixierte Kopfzeilen</span>
|
||
<input type="number" min="0" :max="table.rows - 1" :value="table.view.frozenRows" @change="store.setTableView(table.id, { frozenRows: num($event) })" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Fixierte Spalten</span>
|
||
<input type="number" min="0" :max="table.cols - 1" :value="table.view.frozenCols" @change="store.setTableView(table.id, { frozenCols: num($event) })" />
|
||
</label>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Position X</span>
|
||
<input type="number" min="0" step="4" :value="table.x" @change="store.moveObject(table.id, num($event), table.y)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Position Y</span>
|
||
<input type="number" min="0" step="4" :value="table.y" @change="store.moveObject(table.id, table.x, num($event))" />
|
||
</label>
|
||
</div>
|
||
|
||
<label class="f">
|
||
<span>Zahlenformat (Locale)</span>
|
||
<select :value="table.locale ?? ''" @change="store.setObjectLocale(table.id, str($event) || null)">
|
||
<option value="">— vom Blatt / Dokument</option>
|
||
<option v-for="l in NUMBER_LOCALES" :key="l.id" :value="l.id">{{ l.label }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="f">
|
||
<span>Zeilenhöhe – Standard (px)</span>
|
||
<input type="number" min="14" max="240" :value="table.rowHeight ?? 26" @change="store.setTableRowHeight(table.id, num($event))" />
|
||
</label>
|
||
|
||
<label class="f">
|
||
<span>Tabellen-Hintergrund</span>
|
||
<span class="color">
|
||
<input type="color" :value="table.bg ?? '#ffffff'" @input="store.setObjectBg(table.id, str($event))" />
|
||
<button class="mini" title="Standard" @click="store.setObjectBg(table.id, null)">↺</button>
|
||
<em v-if="!table.bg">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Tabellen-Textfarbe</span>
|
||
<span class="color">
|
||
<input type="color" :value="table.fg ?? '#1d1d1f'" @input="store.setObjectTextColor(table.id, str($event))" />
|
||
<button class="mini" title="Standard" @click="store.setObjectTextColor(table.id, null)">↺</button>
|
||
<em v-if="!table.fg">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f row-inline">
|
||
<input
|
||
type="checkbox"
|
||
:checked="!!table.hideChromeUnfocused"
|
||
@change="store.setTableHideChromeUnfocused(table.id, chk($event))"
|
||
/>
|
||
<span>Ohne Rahmen/Kopfzeilen, wenn nicht ausgewählt</span>
|
||
</label>
|
||
|
||
<!-- ---- current cell selection ---- -->
|
||
<div v-if="hasCellSel" class="subsection">
|
||
<h4>
|
||
Auswahl · {{ store.activeCellName.value }}
|
||
<template v-if="store.selectionAggregate.value.cells > 1">
|
||
· {{ store.selectionAggregate.value.cells }} Zellen
|
||
</template>
|
||
</h4>
|
||
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Spaltenbreite (px)</span>
|
||
<input type="number" min="24" max="800" :value="focusColW" @change="store.setColWidth(table.id, selFocus.col, num($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Zeilenhöhe (px)</span>
|
||
<input type="number" min="14" max="240" :value="focusRowH" @change="store.setRowHeight(table.id, selFocus.row, num($event))" />
|
||
</label>
|
||
</div>
|
||
|
||
<label class="f">
|
||
<span>Ausrichtung</span>
|
||
<span class="seg">
|
||
<button :class="{ on: selFmt.align === 'left' }" @click="setAlign('left')">links</button>
|
||
<button :class="{ on: selFmt.align === 'center' }" @click="setAlign('center')">mitte</button>
|
||
<button :class="{ on: selFmt.align === 'right' }" @click="setAlign('right')">rechts</button>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="f">
|
||
<span>Zahlenformat</span>
|
||
<select :value="selFmt.numFmt ?? 'auto'" @change="applyFmt({ numFmt: str($event) })">
|
||
<option v-for="nfmt in NUM_FORMATS" :key="nfmt.id" :value="nfmt.id">{{ nfmt.label }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="f row-inline">
|
||
<button class="tgl" :class="{ on: selFmt.bold }" @click="applyFmt({ bold: !selFmt.bold })"><b>F</b></button>
|
||
<button class="tgl" :class="{ on: selFmt.italic }" @click="applyFmt({ italic: !selFmt.italic })"><i>K</i></button>
|
||
</label>
|
||
|
||
<label class="f">
|
||
<span>Zell-Textfarbe</span>
|
||
<span class="color">
|
||
<input type="color" :value="selFmt.color ?? '#1d1d1f'" @input="applyFmt({ color: str($event) })" />
|
||
<button class="mini" title="Standard" @click="applyFmt({ color: null })">↺</button>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Zell-Füllfarbe</span>
|
||
<span class="color">
|
||
<input type="color" :value="selFmt.bg ?? '#ffffff'" @input="applyFmt({ bg: str($event) })" />
|
||
<button class="mini" title="Standard" @click="applyFmt({ bg: null })">↺</button>
|
||
</span>
|
||
</label>
|
||
|
||
<button @click="store.clearSelectionFormat()">Formatierung entfernen</button>
|
||
</div>
|
||
|
||
<button class="danger" @click="store.deleteObject(table.id)">Tabelle löschen</button>
|
||
</template>
|
||
|
||
<template v-else-if="chart">
|
||
<label class="f">
|
||
<span>Name</span>
|
||
<input :value="chart.name" @change="store.renameObject(chart.id, str($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Ansicht</span>
|
||
<select :value="chart.view" @change="store.setChartView(chart.id, str($event) === 'line' ? 'line' : 'bar')">
|
||
<option value="bar">Balken</option>
|
||
<option value="line">Linie</option>
|
||
</select>
|
||
</label>
|
||
<label class="f">
|
||
<span>Quelltabelle</span>
|
||
<select :value="chart.sourceTableId ?? ''" @change="store.setChartSource(chart.id, str($event))">
|
||
<option v-for="t in tableList" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||
</select>
|
||
</label>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Breite (px)</span>
|
||
<input type="number" min="200" step="10" :value="chart.width" @change="store.resizeChart(chart.id, num($event), chart.height)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Höhe (px)</span>
|
||
<input type="number" min="140" step="10" :value="chart.height" @change="store.resizeChart(chart.id, chart.width, num($event))" />
|
||
</label>
|
||
</div>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Position X</span>
|
||
<input type="number" min="0" step="4" :value="chart.x" @change="store.moveObject(chart.id, num($event), chart.y)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Position Y</span>
|
||
<input type="number" min="0" step="4" :value="chart.y" @change="store.moveObject(chart.id, chart.x, num($event))" />
|
||
</label>
|
||
</div>
|
||
<label class="f">
|
||
<span>Zahlenformat (Locale)</span>
|
||
<select :value="chart.locale ?? ''" @change="store.setObjectLocale(chart.id, str($event) || null)">
|
||
<option value="">— vom Blatt / Dokument</option>
|
||
<option v-for="l in NUMBER_LOCALES" :key="l.id" :value="l.id">{{ l.label }}</option>
|
||
</select>
|
||
</label>
|
||
<label class="f">
|
||
<span>Hintergrund</span>
|
||
<span class="color">
|
||
<input type="color" :value="chart.bg ?? '#ffffff'" @input="store.setObjectBg(chart.id, str($event))" />
|
||
<button class="mini" title="Standard" @click="store.setObjectBg(chart.id, null)">↺</button>
|
||
<em v-if="!chart.bg">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Text-/Achsenfarbe</span>
|
||
<span class="color">
|
||
<input type="color" :value="chart.fg ?? '#6e6e73'" @input="store.setObjectTextColor(chart.id, str($event))" />
|
||
<button class="mini" title="Standard" @click="store.setObjectTextColor(chart.id, null)">↺</button>
|
||
<em v-if="!chart.fg">Design</em>
|
||
</span>
|
||
</label>
|
||
<button class="danger" @click="store.deleteObject(chart.id)">Diagramm löschen</button>
|
||
</template>
|
||
|
||
<template v-else-if="text">
|
||
<label class="f">
|
||
<span>Name</span>
|
||
<input :value="text.name" @change="store.renameObject(text.id, str($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Text</span>
|
||
<textarea rows="4" :value="text.text" @change="store.setTextContent(text.id, str($event))" />
|
||
</label>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Schriftgröße (px)</span>
|
||
<input type="number" min="9" max="96" :value="text.fontSize" @change="store.setTextFormat(text.id, { fontSize: num($event) })" />
|
||
</label>
|
||
<label class="f row-inline" style="align-self: end; margin-bottom: 12px">
|
||
<button class="tgl" :class="{ on: text.bold }" @click="store.setTextFormat(text.id, { bold: !text.bold })"><b>F</b></button>
|
||
<button class="tgl" :class="{ on: text.italic }" @click="store.setTextFormat(text.id, { italic: !text.italic })"><i>K</i></button>
|
||
</label>
|
||
</div>
|
||
<label class="f">
|
||
<span>Ausrichtung</span>
|
||
<span class="seg">
|
||
<button :class="{ on: text.align === 'left' }" @click="store.setTextFormat(text.id, { align: 'left' })">links</button>
|
||
<button :class="{ on: text.align === 'center' }" @click="store.setTextFormat(text.id, { align: 'center' })">mitte</button>
|
||
<button :class="{ on: text.align === 'right' }" @click="store.setTextFormat(text.id, { align: 'right' })">rechts</button>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Textfarbe</span>
|
||
<span class="color">
|
||
<input type="color" :value="text.color ?? '#1d1d1f'" @input="store.setObjectTextColor(text.id, str($event))" />
|
||
<button class="mini" title="Standard" @click="store.setObjectTextColor(text.id, null)">↺</button>
|
||
<em v-if="!text.color">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Hintergrund</span>
|
||
<span class="color">
|
||
<input type="color" :value="text.bg ?? '#ffffff'" @input="store.setObjectBg(text.id, str($event))" />
|
||
<button class="mini" title="Ohne" @click="store.setObjectBg(text.id, null)">↺</button>
|
||
<em v-if="!text.bg">ohne</em>
|
||
</span>
|
||
</label>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Breite (px)</span>
|
||
<input type="number" min="80" step="10" :value="text.width" @change="store.resizeText(text.id, num($event), text.height)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Höhe (px)</span>
|
||
<input type="number" min="32" step="10" :value="text.height" @change="store.resizeText(text.id, text.width, num($event))" />
|
||
</label>
|
||
</div>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Position X</span>
|
||
<input type="number" min="0" step="4" :value="text.x" @change="store.moveObject(text.id, num($event), text.y)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Position Y</span>
|
||
<input type="number" min="0" step="4" :value="text.y" @change="store.moveObject(text.id, text.x, num($event))" />
|
||
</label>
|
||
</div>
|
||
<button class="danger" @click="store.deleteObject(text.id)">Textfeld löschen</button>
|
||
</template>
|
||
|
||
<template v-else-if="button">
|
||
<label class="f">
|
||
<span>Name</span>
|
||
<input :value="button.name" @change="store.renameObject(button.id, str($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Beschriftung</span>
|
||
<input :value="button.label" @change="store.setButtonLabel(button.id, str($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Makro</span>
|
||
<select :value="button.macroId ?? ''" @change="store.setButtonMacroId(button.id, str($event) || null)">
|
||
<option value="">— keins —</option>
|
||
<option v-for="m in store.doc.macros" :key="m.id" :value="m.id">{{ m.name }}</option>
|
||
</select>
|
||
</label>
|
||
<p v-if="store.doc.macros.length === 0" class="muted">
|
||
Noch kein Makro vorhanden — über „▶ Makros" in der Symbolleiste anlegen.
|
||
</p>
|
||
<label class="f">
|
||
<span>Textfarbe</span>
|
||
<span class="color">
|
||
<input type="color" :value="button.fg ?? '#0a84ff'" @input="store.setObjectTextColor(button.id, str($event))" />
|
||
<button class="mini" title="Standard" @click="store.setObjectTextColor(button.id, null)">↺</button>
|
||
<em v-if="!button.fg">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Hintergrund</span>
|
||
<span class="color">
|
||
<input type="color" :value="button.bg ?? '#e8f2ff'" @input="store.setObjectBg(button.id, str($event))" />
|
||
<button class="mini" title="Ohne" @click="store.setObjectBg(button.id, null)">↺</button>
|
||
<em v-if="!button.bg">Design</em>
|
||
</span>
|
||
</label>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Breite (px)</span>
|
||
<input type="number" min="60" step="10" :value="button.width" @change="store.resizeButton(button.id, num($event), button.height)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Höhe (px)</span>
|
||
<input type="number" min="28" step="10" :value="button.height" @change="store.resizeButton(button.id, button.width, num($event))" />
|
||
</label>
|
||
</div>
|
||
<div class="grid2">
|
||
<label class="f">
|
||
<span>Position X</span>
|
||
<input type="number" min="0" step="4" :value="button.x" @change="store.moveObject(button.id, num($event), button.y)" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Position Y</span>
|
||
<input type="number" min="0" step="4" :value="button.y" @change="store.moveObject(button.id, button.x, num($event))" />
|
||
</label>
|
||
</div>
|
||
<button class="danger" @click="store.deleteObject(button.id)">Button löschen</button>
|
||
</template>
|
||
|
||
<p v-else class="muted">Ein Objekt auf der Fläche anklicken, um es hier zu bearbeiten.</p>
|
||
</section>
|
||
|
||
<!-- ===================== sheet ===================== -->
|
||
<section>
|
||
<h3>Blatt</h3>
|
||
<label class="f">
|
||
<span>Name</span>
|
||
<input :value="sheet.name" @change="store.renameSheet(sheet.id, str($event))" />
|
||
</label>
|
||
<label class="f">
|
||
<span>Zahlenformat</span>
|
||
<select :value="sheet.locale ?? ''" @change="store.setSheetLocale(sheet.id, str($event) || null)">
|
||
<option value="">— vom Dokument ({{ store.doc.locale }})</option>
|
||
<option v-for="l in NUMBER_LOCALES" :key="l.id" :value="l.id">{{ l.label }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="f">
|
||
<span>Hintergrundfarbe</span>
|
||
<span class="color">
|
||
<input
|
||
type="color"
|
||
:value="sheet.background ?? '#ffffff'"
|
||
@input="store.setSheetBackground(sheet.id, str($event))"
|
||
/>
|
||
<button class="mini" title="Auf Standard zurücksetzen" @click="store.setSheetBackground(sheet.id, null)">↺</button>
|
||
<em v-if="!sheet.background">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Schriftfarbe</span>
|
||
<span class="color">
|
||
<input
|
||
type="color"
|
||
:value="sheet.foreground ?? '#1d1d1f'"
|
||
@input="store.setSheetForeground(sheet.id, str($event))"
|
||
/>
|
||
<button class="mini" title="Auf Standard zurücksetzen" @click="store.setSheetForeground(sheet.id, null)">↺</button>
|
||
<em v-if="!sheet.foreground">Design</em>
|
||
</span>
|
||
</label>
|
||
<label class="f row-inline">
|
||
<input
|
||
type="checkbox"
|
||
:checked="sheet.showGrid !== false"
|
||
@change="store.setSheetShowGrid(sheet.id, chk($event))"
|
||
/>
|
||
<span>Raster einblenden</span>
|
||
</label>
|
||
<label class="f">
|
||
<span>Rasterweite ({{ sheet.gridSize ?? DEFAULT_GRID_SIZE }} px)</span>
|
||
<input
|
||
type="range"
|
||
:min="MIN_GRID_SIZE"
|
||
:max="MAX_GRID_SIZE"
|
||
:value="sheet.gridSize ?? DEFAULT_GRID_SIZE"
|
||
title="Steuert auch, worauf Objekte beim Verschieben/Skalieren einrasten"
|
||
@input="store.setSheetGridSize(sheet.id, num($event))"
|
||
/>
|
||
</label>
|
||
|
||
<div class="row-btns">
|
||
<button @click="store.addSheet()">Neues Blatt</button>
|
||
<button class="danger" :disabled="store.doc.sheets.length <= 1" @click="store.deleteSheet(sheet.id)">
|
||
Blatt löschen
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== view ===================== -->
|
||
<section>
|
||
<h3>Ansicht</h3>
|
||
<label class="f">
|
||
<span>Zoom (dieses Blatt) · {{ Math.round(store.activeZoom.value * 100) }}%</span>
|
||
<input
|
||
type="range"
|
||
min="40"
|
||
max="200"
|
||
step="5"
|
||
:value="Math.round(store.activeZoom.value * 100)"
|
||
@input="store.setZoom(num($event) / 100)"
|
||
/>
|
||
</label>
|
||
</section>
|
||
|
||
<!-- ===================== document ===================== -->
|
||
<section>
|
||
<h3>Dokument</h3>
|
||
<label class="f">
|
||
<span>Erscheinungsbild</span>
|
||
<div class="seg">
|
||
<button
|
||
:class="{ on: themeMode === 'system' }"
|
||
title="Auto (folgt dem System)"
|
||
aria-label="Auto (folgt dem System)"
|
||
@click="themeMode = 'system'"
|
||
>
|
||
<ToolbarIcon name="auto" />
|
||
</button>
|
||
<button
|
||
:class="{ on: themeMode === 'light' }"
|
||
title="Hell"
|
||
aria-label="Hell"
|
||
@click="themeMode = 'light'"
|
||
>
|
||
<ToolbarIcon name="sun" />
|
||
</button>
|
||
<button
|
||
:class="{ on: themeMode === 'dark' }"
|
||
title="Dunkel"
|
||
aria-label="Dunkel"
|
||
@click="themeMode = 'dark'"
|
||
>
|
||
<ToolbarIcon name="moon" />
|
||
</button>
|
||
</div>
|
||
</label>
|
||
<label class="f">
|
||
<span>Zahlenformat (Ländereinstellung)</span>
|
||
<select :value="store.doc.locale" @change="store.setDocumentLocale(str($event))">
|
||
<option v-for="l in NUMBER_LOCALES" :key="l.id" :value="l.id">{{ l.label }}</option>
|
||
</select>
|
||
</label>
|
||
<label class="f">
|
||
<span>CSV-Trennzeichen (Standard)</span>
|
||
<select :value="store.doc.csvDelimiter" @change="store.setCsvDelimiter(str($event) === ';' ? ';' : ',')">
|
||
<option value=",">Komma ,</option>
|
||
<option value=";">Semikolon ;</option>
|
||
</select>
|
||
</label>
|
||
<button class="danger" @click="resetDoc">Auf Beispiel zurücksetzen</button>
|
||
</section>
|
||
</div>
|
||
</aside>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.drawer {
|
||
position: absolute;
|
||
z-index: 60;
|
||
top: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: 320px;
|
||
max-width: 86vw;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--surface);
|
||
border-right: 1px solid var(--border);
|
||
box-shadow: var(--shadow);
|
||
transform: translateX(-102%);
|
||
transition: transform 0.18s ease;
|
||
}
|
||
.drawer.open {
|
||
transform: none;
|
||
}
|
||
header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
header h2 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
}
|
||
.x {
|
||
border: 0;
|
||
background: transparent;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
color: var(--text-dim);
|
||
}
|
||
.body {
|
||
overflow-y: auto;
|
||
padding: 4px 14px 20px;
|
||
}
|
||
section {
|
||
padding: 14px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
section:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
h3 {
|
||
margin: 0 0 10px;
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--text-dim);
|
||
}
|
||
.subsection {
|
||
margin: 6px 0 12px;
|
||
padding: 10px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: var(--surface-2);
|
||
}
|
||
.subsection h4 {
|
||
margin: 0 0 10px;
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
font-weight: 600;
|
||
}
|
||
.tgl {
|
||
width: 30px;
|
||
height: 28px;
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
}
|
||
.tgl.on {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
border-color: var(--accent);
|
||
}
|
||
.f textarea {
|
||
font: inherit;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
padding: 6px 8px;
|
||
resize: vertical;
|
||
}
|
||
.f {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-bottom: 10px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
.f input,
|
||
.f select {
|
||
font: inherit;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
padding: 6px 8px;
|
||
}
|
||
.f input[type='range'] {
|
||
padding: 0;
|
||
background: transparent;
|
||
border: 0;
|
||
}
|
||
.f.row-inline {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.f.row-inline input {
|
||
width: auto;
|
||
}
|
||
.color {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.color input[type='color'] {
|
||
width: 42px;
|
||
height: 28px;
|
||
padding: 2px;
|
||
cursor: pointer;
|
||
}
|
||
.color .mini {
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
border-radius: 6px;
|
||
width: 26px;
|
||
height: 26px;
|
||
cursor: pointer;
|
||
color: var(--text-dim);
|
||
}
|
||
.color .mini:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
}
|
||
.color em {
|
||
font-style: normal;
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
}
|
||
.grid2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
.row-btns {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
button {
|
||
font: inherit;
|
||
font-size: 12.5px;
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
border-radius: 7px;
|
||
padding: 6px 12px;
|
||
cursor: pointer;
|
||
}
|
||
button:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
}
|
||
button:disabled {
|
||
opacity: 0.45;
|
||
cursor: not-allowed;
|
||
}
|
||
button.danger:hover {
|
||
border-color: var(--danger);
|
||
color: var(--danger);
|
||
}
|
||
.seg {
|
||
display: inline-flex;
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
overflow: hidden;
|
||
}
|
||
.seg button {
|
||
border: 0;
|
||
border-radius: 0;
|
||
padding: 5px 10px;
|
||
}
|
||
.seg button.on {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
}
|
||
.muted {
|
||
color: var(--text-dim);
|
||
font-size: 12px;
|
||
margin: 0;
|
||
}
|
||
</style>
|