Add per-table "hide chrome/headers when unfocused" clean-look option

New Settings checkbox: while a table isn't the selected canvas object,
its title bar, column letters and row numbers disappear entirely -- not
just visually, their reserved space collapses too, so the table shrinks
to just its cell grid. Clicking back into it (e.g. any cell) reselects
it and brings everything back.

TableObject.hideChromeUnfocused (store/types.ts) + setTableHideChromeUnfocused
(document.ts) + io/json.ts normalization, following the existing
per-object appearance-setting pattern. CanvasObject.vue gates its title
bar on a new showChrome computed and passes its existing `selected` prop
down to TableGrid.vue (previously only took tableId) so it can make the
same call for its own headers.

TableGrid.vue's HEAD_W (a plain constant) becomes a `headW` computed,
and the existing `headerH` computed now returns 0 for both whenever
hidden -- sufficient on its own since rowTop()/colLeft() already sum
those two values as their base offset and are the single source every
other position calculation in the file (sticky-frozen offsets, the
in-cell editor overlay, clip/fill marquees, scroll-into-view, row
virtualization) already goes through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-10 00:16:10 +02:00
co-authored by Claude Sonnet 5
parent c26b1dd857
commit 7646162767
8 changed files with 101 additions and 14 deletions
+18 -3
View File
@@ -221,6 +221,16 @@ function onResizeUp() {
window.removeEventListener('mouseup', onResizeUp);
}
/** A table with `hideChromeUnfocused` set shows its title bar (and, via the
* `selected` prop passed to TableGrid below, its row/column headers) only
* while it's the selected object -- a "clean look" presentation option
* ("Ohne Rahmen/Kopfzeilen, wenn nicht ausgewählt" in Settings). Every
* other object kind is unaffected -- always shown, today's behaviour. */
const showChrome = computed(() => {
const o = props.object;
return props.selected || !(o.kind === 'table' && o.hideChromeUnfocused);
});
const icon = computed(() => {
switch (props.object.kind) {
case 'table':
@@ -253,7 +263,7 @@ const style = computed(() => {
:style="style"
@mousedown="emit('select')"
>
<div class="chrome" @mousedown.stop="onMoveDown">
<div v-if="showChrome" class="chrome" @mousedown.stop="onMoveDown">
<input
v-if="renaming"
ref="nameInput"
@@ -277,8 +287,8 @@ const style = computed(() => {
</button>
</div>
<div class="content">
<TableGrid v-if="object.kind === 'table'" :table-id="object.id" />
<div class="content" :class="{ 'full-radius': !showChrome }">
<TableGrid v-if="object.kind === 'table'" :table-id="object.id" :selected="selected" />
<ChartView v-else-if="object.kind === 'chart'" :obj-id="object.id" />
<TextBox v-else-if="object.kind === 'text'" :obj-id="object.id" />
<ButtonView v-else-if="object.kind === 'button'" :obj-id="object.id" />
@@ -357,6 +367,11 @@ const style = computed(() => {
overflow: hidden;
border-radius: 0 0 10px 10px;
}
/* the chrome bar is what normally rounds the top corners -- without it
(hideChromeUnfocused, currently unfocused), .content fills the whole box */
.content.full-radius {
border-radius: 10px;
}
.canvas-object.flex .content {
flex: 1;
min-height: 0;
+8
View File
@@ -161,6 +161,14 @@ function resetDoc() {
<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">
+20 -9
View File
@@ -19,12 +19,11 @@ import { useKeyboardPointMode } from '../composables/useKeyboardPointMode';
import ContextMenu from './ContextMenu.vue';
import FormulaSuggest from './FormulaSuggest.vue';
const props = defineProps<{ tableId: string }>();
const props = defineProps<{ tableId: string; selected: boolean }>();
const store = useDocument();
const table = computed(() => store.tableById(props.tableId));
const HEAD_W = 44;
const DEFAULT_W = 104;
/** How far the in-cell editor may grow beyond the cell before it wraps/scrolls
* -- an approximation of "the visible display width/height" rather than
@@ -33,7 +32,18 @@ const EDIT_MAX_W = 560;
const EDIT_MAX_H = 320;
const colWidth = (c: number) => table.value?.colWidths[c] ?? DEFAULT_W;
const rowH = (r: number) => store.rowHeightOf(props.tableId, r);
const headerH = computed(() => table.value?.rowHeight ?? DEFAULT_ROW_H);
/** `hideChromeUnfocused` (Settings panel "Ohne Rahmen/Kopfzeilen, wenn nicht
* ausgewählt") hides the row/column headers -- not just visually, their
* reserved space collapses to 0 too -- whenever this table isn't the
* selected canvas object (CanvasObject.vue hides its title bar the same
* way, via its own `showChrome`). `headW`/`headerH` below are what
* actually implement the collapse: every position calculation in this file
* already goes through them (or through `rowTop`/`colLeft`, which do), so
* making them 0 when hidden is the entire fix -- cells simply start at
* (0,0) instead of past a header that isn't there. */
const showHeaders = computed(() => props.selected || table.value?.hideChromeUnfocused !== true);
const headW = computed(() => (showHeaders.value ? 44 : 0));
const headerH = computed(() => (showHeaders.value ? (table.value?.rowHeight ?? DEFAULT_ROW_H) : 0));
/** Prefix sums of row heights (`rowCumHeights[i]` = total height of rows
* `0..i-1`, excluding the header) -- lets `rowTop()` and the row
@@ -127,7 +137,7 @@ const scrollStyle = computed(() =>
/** px from the table's left edge to the start of column `c` (incl. row header). */
function colLeft(c: number): number {
return HEAD_W + (colCumWidths.value[c] ?? 0);
return headW.value + (colCumWidths.value[c] ?? 0);
}
/** The internal scroll element of a fixed table (null for auto-size tables). */
@@ -1284,8 +1294,8 @@ function onRowResizeUp() {
const totalWidth = computed(() => {
const t = table.value;
if (!t) return HEAD_W;
let w = HEAD_W;
if (!t) return headW.value;
let w = headW.value;
for (let c = 0; c < t.cols; c++) w += colWidth(c);
return w;
});
@@ -1320,10 +1330,10 @@ defineExpose({
<div class="tg-scroll" :style="scrollStyle" @scroll.passive="onScroll">
<table class="grid" :style="{ width: totalWidth + 'px' }">
<colgroup>
<col :style="{ width: HEAD_W + 'px' }" />
<col v-if="showHeaders" :style="{ width: headW + 'px' }" />
<col v-for="c in table.cols" :key="'c' + (c - 1)" :style="{ width: colWidth(c - 1) + 'px' }" />
</colgroup>
<thead>
<thead v-if="showHeaders">
<tr :style="{ height: headerH + 'px' }">
<th class="corner" @click="selectAll" />
<th
@@ -1344,6 +1354,7 @@ defineExpose({
<tr v-for="item in renderedRows" :key="item.key" :style="{ height: (item.kind === 'row' ? rowH(item.r) : item.height) + 'px' }">
<template v-if="item.kind === 'row'">
<th
v-if="showHeaders"
class="row-head"
:data-rowhead="item.r"
:class="{ hot: rowHot(item.r), 'frozen-row': item.r < frozenRows, 'freeze-edge-bottom': item.r === frozenRows - 1 }"
@@ -1388,7 +1399,7 @@ defineExpose({
<!-- a spacer standing in for the rows currently scrolled out of
view -- keeps the scrollbar's size/position correct without
those rows' cells actually existing in the DOM -->
<td v-else :colspan="table.cols + 1" class="row-spacer" />
<td v-else :colspan="showHeaders ? table.cols + 1 : table.cols" class="row-spacer" />
</tr>
</tbody>
</table>
+1
View File
@@ -341,6 +341,7 @@ function normaliseObject(o: SheetObject): SheetObject {
locale: localeOrUndef(o.locale),
bg: colorOrUndef(o.bg),
fg: colorOrUndef(o.fg),
hideChromeUnfocused: boolOrUndef(o.hideChromeUnfocused),
};
}
if (o.kind === 'text') {
+30
View File
@@ -2650,6 +2650,34 @@ describe('per-object background & text colour', () => {
});
});
describe('table "hide chrome when unfocused" (clean-look presentation option)', () => {
it('toggles hideChromeUnfocused and clears the field when turned off', () => {
const s = fresh();
const id = tableId(s, 'Umsatz');
expect(s.tableById(id)!.hideChromeUnfocused).toBeUndefined();
s.setTableHideChromeUnfocused(id, true);
expect(s.tableById(id)!.hideChromeUnfocused).toBe(true);
s.setTableHideChromeUnfocused(id, false);
expect(s.tableById(id)!.hideChromeUnfocused).toBeUndefined(); // "unset", not stored as false
});
it('survives a JSON round-trip; a non-boolean value on import is dropped', () => {
const s = fresh();
const id = tableId(s, 'Umsatz');
s.setTableHideChromeUnfocused(id, true);
const before = s.snapshot();
s.replaceDocument(parseDocument(serializeDocument(before)));
expect(s.snapshot()).toEqual(before);
expect(s.tableById(id)!.hideChromeUnfocused).toBe(true);
const doc = JSON.parse(serializeDocument(s.snapshot()));
const tbl = doc.sheets[0].objects.find((o: { id: string }) => o.id === id);
tbl.hideChromeUnfocused = 'yes';
s.replaceDocument(parseDocument(JSON.stringify(doc)));
expect(s.tableById(id)!.hideChromeUnfocused).toBeUndefined();
});
});
describe('full-fidelity JSON export — backgrounds, formats, sheet settings', () => {
/** Populate *every* persisted setting, then verify the export string carries
* it and a round-trip is byte-for-byte identical. */
@@ -2678,6 +2706,7 @@ describe('full-fidelity JSON export — backgrounds, formats, sheet settings', (
s.setObjectLocale(umsatzId, 'fr-FR');
s.setTableView(umsatzId, { mode: 'fixed', width: 520, height: 300, frozenRows: 1, frozenCols: 1 });
s.setTableRowHeight(umsatzId, 30);
s.setTableHideChromeUnfocused(umsatzId, true);
s.setRowHeight(umsatzId, 4, 52);
s.setColWidth(umsatzId, 0, 130);
s.setRangeFormat(umsatzId, { col: 1, row: 1 }, { col: 3, row: 4 }, {
@@ -2751,6 +2780,7 @@ describe('full-fidelity JSON export — backgrounds, formats, sheet settings', (
rowHeights: { '4': 52 },
colWidths: { '0': 130 },
view: { mode: 'fixed', width: 520, height: 300, frozenRows: 1, frozenCols: 1 },
hideChromeUnfocused: true,
});
expect(tbl.formats['1,1']).toEqual({
align: 'right',
+8
View File
@@ -895,6 +895,13 @@ function createStore() {
}
markDirty();
}
function setTableHideChromeUnfocused(tableId: string, hide: boolean) {
const t = tableById(tableId);
if (!t) return;
if (hide) t.hideChromeUnfocused = true;
else delete t.hideChromeUnfocused;
markDirty();
}
function setCsvDelimiter(d: ',' | ';') {
doc.csvDelimiter = d === ';' ? ';' : ',';
@@ -2944,6 +2951,7 @@ function createStore() {
selectObject,
setObjectBg,
setObjectTextColor,
setTableHideChromeUnfocused,
// table grid
tableById,
cellInput,
+7
View File
@@ -86,6 +86,13 @@ export interface TableObject {
bg?: string;
/** object default text colour override */
fg?: string;
/** when true, this table's title bar and row/column headers are only
* shown while it's the selected canvas object -- otherwise (including
* right after the document loads, before anything is selected) they're
* hidden entirely, not just visually: the table's footprint shrinks to
* just its cell grid. A presentation/"clean look" option; unset/false =
* always shown, today's behaviour. */
hideChromeUnfocused?: boolean;
}
/** A chart object placed on a sheet canvas. Freely positioned and resized. */