- Help dialog (?, F1, Datei > Hilfe): user topics, all shortcuts and the formula functions with one search over everything. Topics and shortcuts are plain data in data/help.ts; the function list comes from codeReference.ts so new built-ins appear by themselves. - While a formula is edited, the grid outlines each referenced cell/range in the colour of its position in the text, and the outline can be dragged to move or resize the reference, as in Excel. Point-mode picks (click, drag, arrow keys) are outlined too, so keyboard picking is no longer blind. - Underline as a cell format, plus Mod+Shift+B/I/U toggles that switch on unless every selected cell already has the format. - A formula committed with open parentheses or an open text literal gets the missing closers appended; anything that still doesn't parse is left alone so the usual error shows instead of a guess. - Cells and tables with a fill but no text colour get dark or light text by WCAG contrast instead of the theme colour (no black on black).
766 lines
28 KiB
Vue
766 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 }" title="Fett (Strg+Umschalt+B)" @click="applyFmt({ bold: !selFmt.bold })"><b>F</b></button>
|
||
<button class="tgl" :class="{ on: selFmt.italic }" title="Kursiv (Strg+Umschalt+I)" @click="applyFmt({ italic: !selFmt.italic })"><i>K</i></button>
|
||
<button class="tgl" :class="{ on: selFmt.underline }" title="Unterstrichen (Strg+Umschalt+U)" @click="applyFmt({ underline: !selFmt.underline })"><u>U</u></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>
|