Commit Graph
62 Commits
Author SHA1 Message Date
wkhandClaude Opus 5.5 873cd50eac Store multiple documents in IndexedDB with a document list
Documents now live in an IndexedDB library instead of a single
localStorage slot: each has its own ?doc=<id> URL, a title (editable in
the toolbar, used for export file names) and appears in Datei ›
Dokumente (open, rename, duplicate, delete). New/sample/import create
new documents instead of replacing the open one. Tabs stay in step via
BroadcastChannel. The old localStorage document is migrated once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:48:49 +02:00
wkhandClaude Opus 5.5 8e974cc870 Shift+click on column/row headers extends the selection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:10:46 +02:00
wkhandClaude Opus 5.5 83f365ce3f Show the "numolus" wordmark only when the toolbar has room for it
Instead of fixed breakpoints (wicom hidden below 720px, the wordmark below
520px -- while the toolbar already wrapped to two lines much earlier), the
wordmark and "wicom" are shown only while the whole toolbar still fits on
one line with them. Measured with a ResizeObserver; the wordmark's width is
remembered while hidden, so it doesn't flip-flop at the threshold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:38:14 +02:00
wkhandClaude Opus 5.5 a0680c595e Save and open documents as .num (zipped JSON)
Datei › Speichern writes a .num file: a ZIP (fflate) laid out like
OpenDocument -- an uncompressed "mimetype" entry first
(application/x-numolus), meta.json (format version, ISO 8601 creation
time) and document.json, the same JSON the .json export writes. Opening a
.num (file dialog or drag & drop) unpacks it and offers the same
replace/append choice as a .json. Non-ZIPs, foreign ZIPs, a missing
document.json and a newer format version fail with a clear message.

The zip code is loaded on first use, like xlsx, so the main bundle does
not grow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:33:05 +02:00
wkhandClaude Opus 5.5 452f13e774 Whole-column and whole-row references (B:B, A:C, 1:1, 2:5)
Formulas accept B:B / A:C / $B:$B and 1:1 / 2:5 / $3:$3, also qualified
(Umsatz!B:B, Umsatz::1:1, 'Blatt 2'!Kosten::A:C). The open end is clamped
to the table's current size wherever cells are read (evaluation, the
dependency graph, SETVALUE targets, FOR sources), and typing past the
table's end now rebuilds the workbook so such a reference covers the new
cells. The Name Box selects the whole column/row.

The token-level rewriters handle the new shape: copy/fill shifts B:B only
sideways and 1:1 only up/down ($ stays), column insert/delete moves or
#REFs B:B like a range (rows leave it alone, and vice versa), and a table
rename rewrites its qualifier.

Also fixes renaming a table referenced with a quoted name ('Tabelle 1'!B2):
the rewriter replaced only as many characters as the unquoted name has,
leaving a stray quote (=Daten1'!B2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:30:08 +02:00
wkhandClaude Opus 5.5 c4d2712903 Durations past 24 hours and the basic date/time functions
- New cell format "Dauer" ([h]:mm): hours keep counting past 24 (37:30).
  A formula left on "Automatisch" whose numeric inputs are all times or
  durations (=SUM over working times, =end-start) displays as a duration.
  Typing 24 or more hours (37:30) enters a duration.
- "Datum + Zeit" shows a bare time of day (< 1 day) without the epoch
  date 30.12.1899.
- New functions: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY,
  ISOWEEKNUM, WEEKNUM, EDATE, EOMONTH, DAYS, DATEDIF, NETWORKDAYS,
  WORKDAY, DATEVALUE, TIMEVALUE. Where Excel defaults to US conventions,
  ISO 8601 is the default: weeks start Monday (WEEKDAY -> Monday = 1),
  WEEKNUM is the ISO week. Excel's other types stay available.
- Command reference and README document all of it; the README test
  count is updated to 553.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:21:48 +02:00
wkhandClaude Opus 5.5 604794118b Keep dialogs within the viewport height
BaseModal is now capped at the viewport height: header and footer stay
visible and the body scrolls. Wide dialogs make their body a flex column
so the macro/function editor fills it and its reference list scrolls on
its own on small screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:12:44 +02:00
wkhandClaude Opus 5.5 9a39648421 Searchable command reference in the macro and function editors
Both editors get a side panel listing everything that can go into the
code: search (incl. German synonyms), category chips, expandable entries
with description and a complete example, and "Einfügen" to put a snippet
or the example at the caret. The list adapts to the editor -- macros see
write actions, GET/POST/JSONGET and the document's sheets/tables; "Mit
Schleife" functions see FOR/IF/RETURN; single formulas only operators and
functions; function editors also list their own parameters.

The catalog lives in src/data/codeReference.ts (built-in functions derived
from BUILTIN_SIGNATURES). Tests check that every write action is
documented, that function editors never offer write/network commands, and
that every example parses in each context it is shown in.

BaseModal gains a `wide` prop for the two-column edit layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:12:29 +02:00
wkhandClaude Opus 5.5 ac09df1482 Close the Datei menu on Escape or a click outside it
Listens in the capture phase while the menu is open, so canvas and grid
handlers that stop propagation can't keep it open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:12:05 +02:00
wkhandClaude Opus 5.5 5a79ddee79 Start with an empty document; offer Datei › Neu and Datei › Muster
A first start (nothing in localStorage) now opens a blank document -- one
sheet "Blatt 1" with an empty "Tabelle 1" -- instead of the pre-filled
sample. The file menu gets "Neu" (blank document) and "Muster ·
Umsatzanalyse" (the former seed), both behind a confirm and both starting
a fresh undo history. The toolbar's "Zurücksetzen" button is gone; its job
is now Datei › Muster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:11:51 +02:00
wkhandClaude Opus 5.5 86d03346e8 Rename project from numbers-study to numolus
- package.json / package-lock.json: package name "numolus"
- README: new title, drop the "Case- und Designstudie" framing, update
  test count (331 -> 528)
- localStorage keys renamed to numolus:doc:v2 / numolus:theme; the old
  numbers-study:* keys are still read as a fallback so existing documents
  and theme choices survive the rename

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:51:18 +02:00
wkh 3fb3e340ba Update numolus logo SVGs and add Affinity Designer source files
Simplify the logo mark to three overlapping squares without the wordmark, and check in the .afdesign source files and export JSON used to produce it.
2026-09-28 01:41:23 +02:00
wkhandClaude Sonnet 5 56bb68ccd0 Show the theme selector as icons instead of text
The toolbar's theme-cycling button ("Theme: Auto") and the Settings
panel's Auto/Hell/Dunkel segmented control now use icons -- a sun, a
moon, and a half-filled circle for "follows system" -- drawn in this
project's existing hand-drawn SVG icon set (ToolbarIcon.vue) rather than
a new icon system or emoji. Tooltips/aria-labels keep the full text for
accessibility; the toolbar button's click-to-cycle behavior is
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 00:17:14 +02:00
wkhandClaude Sonnet 5 7646162767 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>
2026-09-10 00:16:10 +02:00
wkhandClaude Sonnet 5 c26b1dd857 Fix lexer rejecting non-ASCII letters in identifiers (table/sheet names)
isWordStart only accepted ASCII A-Z/a-z, so tokenizing any identifier
containing an umlaut or other accented letter threw outright. Since a
table/sheet name is used unquoted as a formula qualifier whenever it has
no spaces, this silently made any such name unreferenceable from
elsewhere -- including "Übersicht", the seed document's own demo table:
=Übersicht!B2 written in a different table has always thrown, nothing
in the existing test suite ever referenced it by name to catch it.

Found via the new Name Box (jumpToNameInput): "Übersicht::B4" reported
"invalid reference" even though the bare table name alone worked fine
(that path never touches the parser). Fixed at the lexer level rather
than working around it in the Name Box, since real formulas hit the
same gap. isWordStart now accepts any Unicode letter (\p{L}), a strict
superset of the old ASCII-only rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:54:18 +02:00
wkhandClaude Sonnet 5 06fea34f82 Make the Name Box interactive: jump-to-reference + table/sheet/named-range autocomplete
The top-left Name Box was a plain read-only display of the current
selection ("Umsatz::B2:E6"). It's now an editable input that navigates
on Enter, using the same reference syntax a formula would: a bare cell
or range on the active table, a table/sheet-qualified reference
(Tabelle::A1, Blatt!Tabelle::A1:B5), a named range's name, or -- beyond
what's valid formula syntax -- a bare table name (selects the whole
table) or bare sheet name (switches to it).

Autocompletes against named ranges, table names, and sheet names as you
type (Tab accepts the highlighted suggestion, click jumps there
directly); an unresolvable entry reports via a toast and leaves the text
selected for a quick retry rather than silently reverting.

document.ts gains two new functions (jumpToNameInput, nameBoxSuggestions)
built on the existing parseFormula/resolveTableName/resolveNamedRange
machinery already used elsewhere in the store -- no engine changes.
FormulaSuggest.vue's prop type was loosened so the same dropdown
component serves both this and the existing formula function-name
autocomplete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:54:01 +02:00
wkhandClaude Sonnet 5 d0500fcaf5 Snap-to-grid for moving/resizing canvas objects, plus a per-sheet grid size
Sheet.gridSize (4-200px, default 24, configurable in Settings -> Blatt)
now doubles as both the visible dot-grid's spacing and the snap
increment while dragging to move any canvas object, or free/viewport-
resizing a chart/text/button/fixed-viewport table. An "Automatisch"
table's resize is left unsnapped on top of this -- it already
discretizes to row/col boundaries by construction.

Also fixes the visible grid not actually lining up with real snap
positions once zoomed: the dot pattern's background-size now scales
with zoom like the objects themselves already did, instead of staying a
fixed screen-pixel size regardless of zoom level.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:33:39 +02:00
wkhandClaude Sonnet 5 5f75b9c298 Keyboard "point mode": arrow keys pick cell references while editing a formula
Mirrors the existing mouse point mode (click/drag a grid cell to insert
its reference) without leaving the keyboard. Arrow keys only take over
when the caret sits right after a token that expects a new operand --
the leading '=', an operator, '(', or the ';' argument separator, same
as Excel/Numbers -- so editing an existing formula by hand still works
everywhere else. Shift+Arrow extends to a range, matching the existing
mouse-drag behavior; a fresh chain always starts one cell from the cell
being edited.

New composable (src/composables/useKeyboardPointMode.ts) needed no store
changes: store.pointClick/pointDrag already do the right thing given
target coordinates. Wired into both TableGrid.vue's in-cell editor and
FormulaBar.vue's top formula field, which already share one
registerFormulaEditor interface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:33:03 +02:00
wkh a623adf9cc scrolling and size of canvas 2026-09-09 23:13:46 +02:00
wkhandClaude Sonnet 5 1c5e57ceb4 Fix 50k-row edit performance: replace deep-watch undo/persistence with explicit dirty-marking
The debounced undo-history and localStorage persistence were driven by
watch(() => doc, ..., { deep: true }) -- Vue's deep watch re-traverses
the entire reactive object graph on every single mutation just to
re-establish tracking, an O(document size) cost per edit regardless of
what actually changed. Measured on a 50k-row table: one setCellInput
call went from ~450ms to <1ms with the deep watch disabled.

Both watches are replaced by scheduleSave()/markDirty(), called
explicitly by every content-mutating store function (mirroring the
existing bump()/recalcTick convention), with pure selection/navigation
functions calling scheduleSave() only so cursor movement still never
becomes an undo step. applyDocumentState (undo/redo/replaceDocument)
schedules persistence but never marks history dirty, since those
callers already reconcile history state themselves -- this also makes
the old applyingHistory guard flag dead code, now removed.

setCellInput gets an O(1) same-value no-op guard (the highest-frequency
mutator) so retyping an unchanged value creates no history noise; the
remaining no-op safety net (a real fingerprint diff) still lives in
commitHistoryStep, now running once per debounced burst instead of per
keystroke.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:20:27 +02:00
wkhandClaude Sonnet 5 47c99bbeb6 Tabellen-Umbenennung schreibt Referenzen dokumentweit um
Bisher speicherte eine Formel den Tabellennamen nur als Text im
Qualifier ("Umsatz!B2") – beim Umbenennen der Tabelle wurde nirgendwo
danach gesucht, sodass jede andere Formel, die noch den alten Namen
nannte, beim nächsten Neuberechnen #REF zeigte.

renameObject() ruft jetzt vor der eigentlichen Umbenennung eine neue
renameDocumentRefs()/rewriteRefsForRename() auf, die dieselbe
Token-Splice-Technik wiederverwendet, die für das dokumentweite
Umschreiben bei Zeilen-/Spalten-Einfügen bereits existiert: jede Formel
im Dokument wird tokenisiert, jeder Qualifier über die echte
wb.resolveTableName-Auflösung (nicht per String-Vergleich) auf die
umbenannte Tabelle geprüft, und nur die Tabellen-Namens-Textspanne
ersetzt – bei der 3-teiligen "Blatt!Tabelle::Zelle"-Form bleibt der
Blatt-Teil unangetastet, ein neuer Name mit Leerzeichen wird korrekt
gequotet. Umbenennen + Umschreiben zählen als ein Undo-Schritt.

Bewusst auf Tabellenzellen begrenzt, wie schon beim Zeilen-/Spalten-
Umschreiben (keine Makro-Aktionen, keine Funktionskörper).

Live per Playwright verifiziert: "Umsatz" per Doppelklick zu "Verkauf"
umbenannt, =SUM(Umsatz!B2:E2) in einer anderen Tabelle wurde automatisch
zu =SUM(Verkauf!B2:E2), Wert blieb unverändert (kein #REF).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 12:10:23 +02:00
wkhandClaude Sonnet 5 3ae3d81066 Makros: GET/POST-Anfragen mit JSON-Ergebnis und FOR-Iteration
Erweitert die Makro-Sprache um Netzwerkzugriff: "x = GET(url)"/
"x = POST(url; body)" binden die geparste JSON-Antwort an eine Variable;
FOR item IN x DO ... END iteriert, wenn x ein JSON-Array ist, über dessen
Elemente; JSONGET(wert; pfad) liest ein Feld heraus (punktgetrennt,
Zahl = Array-Index) und funktioniert – anders als GET/POST – überall in
einem Ausdruck verschachtelt, nicht nur als ganze Zuweisung.

Da ein echter Netzwerkaufruf nicht synchron laufen kann, ohne den Tab
einzufrieren, bekommt der Makro-Interpreter einen separaten asynchronen
Zweig (execBlockAsync/execStatementAsync); der bestehende synchrone
Interpreter, den auch gewöhnliche "Mit Schleife"-Funktionen mitten in der
synchronen Neuberechnung nutzen, bleibt unangetastet. Netzwerkaufrufe
werden pro Makro einmalig mit Zielname bestätigt (dauerhaft gemerkt); die
bestehende "ein Lauf = ein Undo-Schritt"-Garantie, die bislang auf
Synchronität beruhte, wird jetzt explizit wiederhergestellt statt sich
weiter auf die Zeit-Koaleszenz zu verlassen.

Zwei echte Lücken erst beim eigenen Live-Test gefunden und behoben: (1)
JSONGET direkt in eine Aktion verschachtelt (statt erst einer Variable
zugewiesen) scheiterte zunächst an #NAME?; (2) JSONGET(...) direkt als
FOR-Quelle (ohne Zwischenvariable) scheiterte sowohl an der
Speicher-Zeit-Prüfung als auch zur Laufzeit, da substitute() einen
Funktionsaufruf nie auswertet.

Live gegen eine neue same-origin-Fixture (public/test-fixtures/sample.json)
verifiziert: echter GET, echter Bestätigungsdialog, FOR/JSONGET über die
echte Antwort, kein erneuter Dialog beim zweiten Lauf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 11:10:19 +02:00
wkhandClaude Sonnet 5 a59fda25b0 Makros: UI (Button-Objekt, Verwaltungspanel, Symbolleiste)
Zweiter von zwei Commits (Fundament in e670f13). Neues, frei platzierbares
Button-Objekt auf der Fläche, das per Klick ein zugewiesenes Makro
ausführt (Zuweisung im Inspektor oder per Rechtsklick direkt am Button).
Neues "▶ Makros"-Panel (Liste + Formular, dritter "▶ Ausführen"-Button pro
Zeile) für Anlegen/Bearbeiten/Löschen/Testlauf ohne Canvas-Button.

Beim Live-Testen gefunden und behoben: ADDTABLE/ADDSHEET navigieren als
Nebeneffekt zur eingefügten Tabelle/zum eingefügten Blatt (richtig für
einen Symbolleisten-Klick, überraschend für einen Makro-Lauf im
Hintergrund) — runMacro() sichert und stellt jetzt aktives Blatt/Objekt/
Auswahl nach dem Lauf wieder her. Nebenbei zwei bestehende
"impliziter-Else"-Fehler behoben, die mit einer vierten Objektart zu
echten Fehlerquellen geworden wären (io/json.ts normaliseObject,
CanvasObject.vue Render-Dispatch).

Vollständig live per Playwright End-zu-Ende verifiziert: Makro über das
echte Formular angelegt, per Listen-Button und per Button-Objekt
ausgeführt, Rechtsklick-Zuweisung geprüft, ein absichtlich fehlerhaftes
Makro zeigt einen Fehler-Toast statt abzustürzen — keine Konsolenfehler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 10:07:56 +02:00
wkhandClaude Sonnet 5 e670f13077 Makros: Engine- und Store-Fundament (Zellen/Zeilen/Spalten/Tabellen/Blätter per Skript)
Erster von zwei Commits (UI folgt separat): neue, explizit ausgelöste
Makro-Sprache — dieselbe block-mode-Grammatik wie bei "Mit Schleife"-
Funktionen (FOR/IF/RETURN), plus ein neues Schreib-Vokabular (SETVALUE,
INSERTROW/DELETEROW/INSERTCOL/DELETECOL, ADDTABLE/RENAMETABLE/DELETETABLE,
ADDSHEET/RENAMESHEET/DELETESHEET), adressiert mit der bestehenden
Blatt!Tabelle::Zelle-Syntax.

Bewusst außerhalb der normalen Neuberechnung: ein Makro läuft nie aus einer
Zellformel heraus, sondern nur explizit getriggert (store.runMacro), und
mutiert ausschließlich über die bereits vorhandenen Store-Primitiven
(setCellInput, insertRow, addTable, addSheet, …) — Workbook/EngineHost/
DependencyGraph bleiben unverändert rein lesend. Ein ganzer synchroner
Makro-Lauf wird automatisch zu einem einzigen Undo-Schritt.

50 neue Tests (Engine + Store), inkl. eines gezielten Regressionstests für
die zentrale Korrektheits-Fallgrube: eine strukturelle Aktion mitten im
Lauf ersetzt die Workbook-Instanz komplett, jeder Host-Zugriff muss daher
frisch aufgelöst werden statt eine Referenz zu cachen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 09:35:02 +02:00
wkhandClaude Sonnet 5 c7b316de1b Favicon neu gestaltet: eigenständige, bei 16px lesbare Bildmarke
Das bisherige Favicon teilte sich seine Geometrie mit der breiten
Symbolleisten-Wortmarke (drei zunehmend breitere Balken samt dicker
Konturlinie) — bei 16x16 verschmilzt das zu einem kaum erkennbaren
schwarzen Fleck. Neue, eigenständige quadratische Bildmarke: drei
gleich große, diagonal kaskadierende Quadrate in den Markenfarben, ohne
Konturlinie (die bei Favicon-Größe ohnehin nur verschwimmt) — bleibt auch
bei 16px klar als rot/blau/gelb erkennbar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 08:03:45 +02:00
wkhandClaude Sonnet 5 10efd6af2f Symbolleiste professioneller gestaltet: echte SVG-Icons statt Unicode/Emoji
Neue ToolbarIcon.vue (hand-gezeichnete Strich-Icons: Einstellungen, Rückgängig/
Wiederholen, Tabelle/Diagramm/Text einfügen, Benannte Bereiche) ersetzt die
bisherigen ⚙ ↶ ↷ ▦ ▤ ¶ 🏷 Glyphen. Buttons sind jetzt in dezente,
segmentierte Gruppen ("Pills") statt einzeln umrandeter Kästchen mit dünnen
Trennstrichen gefasst, mit Ghost-Stil in Ruhe und Hover-/Aktiv-Zuständen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 07:59:42 +02:00
wkhandClaude Sonnet 5 40adaa2e1c Ctrl/Cmd+A: zweistufige Auswahl (zusammenhängender Bereich, dann alles Befüllte)
Erster Druck markiert den zusammenhängenden Bereich um die aktive Zelle
(Excel-Verhalten, iterative Rechteck-Expansion bis zur ersten leeren Zeile/
Spalte oder dem Tabellenrand); ein zweiter, unmittelbar folgender Druck
erweitert auf alle befüllten Zellen der Tabelle. Der Zyklus erkennt sich
selbst über einen Abgleich der aktuellen Auswahl mit der zuletzt gesetzten
und setzt sich bei jeder anderen Aktion dazwischen zurück.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:38:44 +02:00
wkhandClaude Sonnet 5 3594b12615 Größenänderungs-Verhalten der Tabellenmodi vertauscht
Der SE-Griff-Effekt war genau falsch herum zugeordnet: "Automatisch"-
Tabellen (Größe folgt immer dem Inhalt) sollen beim Ziehen die Zeilen-/
Spaltenzahl ändern, "Feste Größe"-Tabellen (expliziter, unabhängig
wählbarer Scroll-Viewport) sollen unverändert nur den Pixel-Viewport
ändern.

- "Automatisch": Ziehen zeigt ein gestricheltes Vorschau-Rechteck (die
  eigentliche Zeilen-/Spaltenänderung ist teuer -- voller Workbook-Rebuild
  -- und committet daher erst beim Loslassen, nicht live). Wachsen fügt
  leere Zeilen/Spalten hinzu, Schrumpfen entfernt sie nie über echten
  Zellinhalt hinweg (gleicher Inhaltsschutz wie zuvor, nur am richtigen
  Modus).
- "Feste Größe": zurück auf reines Pixel-Viewport-Resize, unabhängig von
  der Zeilen-/Spaltenzahl -- das ursprüngliche Verhalten.
- Die dadurch tot gewordene alte "Automatisch"-Funktion (proportionale
  Spaltenbreiten-Skalierung beim Ziehen) entfernt (scaleTableColumns),
  keine andere Stelle rief sie noch auf.

vue-tsc/eslint/build sauber, Playwright-live-verifiziert (Automatisch:
Wachsen 12×8→19×11, hartes Schrumpfen klemmt auf 6×5 mit erhaltenem
Inhalt; Feste Größe: Zeilen-/Spaltenzahl bleibt bei Viewport-Resize
unverändert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:26:54 +02:00
wkhandClaude Sonnet 5 8a7fafcd7a Meta-Beschreibung: Toolbar-Branding an "Case-/Designstudie"-Entfernung angleichen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:19:47 +02:00
wkhandClaude Sonnet 5 f45fbab945 Interaktives Abhängigkeiten-Panel (Vorgänger/Nachfolger, andockbar)
Rechtsklick auf eine Zelle → "🕸 Abhängigkeiten anzeigen" öffnet ein an der
rechten Seite angedocktes Panel (wie das Einstellungen-Panel links) mit den
direkten Vorgängern (was die Zelle liest) und Nachfolgern (was sie liest),
auch über Tabellen-/Blattgrenzen hinweg. Ein Klick auf einen Eintrag
navigiert wirklich dorthin: wählt die echte Zelle im Dokument aus, wechselt
bei Bedarf das Blatt-Tab, und das Panel zentriert sich neu — Blatt-Tabs,
Canvas, Formel- und Statusleiste bleiben dabei die ganze Zeit sichtbar und
bedienbar (kein Vollbild-Dialog). Jeder Eintrag zeigt immer den Blattnamen,
nie nur implizit. Zirkelbezüge werden rot markiert.

Neu in der Engine: Workbook.graphPrecedents/graphDependents, dünne
öffentliche Wrapper um den bereits vorhandenen DependencyGraph. Im Store:
cellGraphSelf/cellGraphNeighbors (Anreicherung für die Anzeige) sowie
openDependencyGraph/closeDependencyGraph (dokumentweiter UI-State, ruft
intern das bestehende selectCell auf).

424 Tests (Engine-/Store-Ebene vollständig abgedeckt), vue-tsc/eslint/build
sauber. Das Panel selbst nur live per Playwright verifiziert (kein
Komponenten-Test-Setup vorhanden, in README dokumentiert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:19:42 +02:00
wkhandClaude Sonnet 5 6a62dccedd Meta-Beschreibung: "Case-/Designstudie"-Framing entfernt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:19:15 +02:00
wkhandClaude Sonnet 5 76587c1cee Größenänderung einer "Feste Größe"-Tabelle steuert Zeilen-/Spaltenzahl
Der SE-Griff einer Tabelle im "Feste Größe"-Modus ändert nicht mehr nur
den scrollbaren Pixel-Viewport, sondern direkt die Zeilen-/Spaltenzahl:
Vergrößern fügt leere Zeilen/Spalten hinzu, Verkleinern entfernt sie —
aber nie über echten Zellinhalt hinweg (klemmt auf die kleinste Größe,
die den vorhandenen Inhalt noch vollständig zeigt). Live-Vorschau während
des Ziehens bleibt der günstige reine Viewport-Resize (kein Workbook-
Rebuild pro Mausbewegung); die eigentliche Zeilen-/Spaltenänderung
committet erst beim Loslassen. Reine UI-Änderung (CanvasObject.vue),
nutzt die bestehenden setTableRows/setTableCols weiter.

Playwright-live-verifiziert, kein Komponenten-Test-Setup vorhanden
(README dokumentiert die Lücke).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 22:50:27 +02:00
wkhandClaude Sonnet 5 aef8f1349c AND/OR/NOT, Fehler-Literale und Zeilen/Spalten einfügen/löschen
- AND(...)/OR(...)/NOT(...) als gewöhnliche Funktionen (kein Kurzschluss-
  Sonderfall wie IF/XLOOKUP nötig, werten wie in echtem Excel alle
  Argumente eager aus). Dabei die bisher dreifach duplizierte
  Wahrheits-Prüfung zu functions.ts#isTruthy() zusammengezogen, jetzt
  von FUNCTIONS.IF, evalIf() und dem Block-IF-Statement gemeinsam genutzt.
  Schließt "kein AND/OR für zusammengesetzte Bedingungen" aus den
  Bekannten Grenzen, auch nutzbar in "Mit Schleife"-IF-Bedingungen.

- Neue Grammatik-Ergänzung: ErrorLiteralNode. Jeder der sieben Fehlercodes
  (#REF, #DIV/0!, #NAME?, #ERR, #CIRC, #VALUE!, #N/A) lässt sich jetzt
  direkt in eine Formel tippen (=IF(A1=0;#N/A;A1)) und wertet unbedingt zu
  genau diesem Fehler aus.

- Zeilen/Spalten einfügen/löschen mitten im Gitter (Rechtsklick auf einen
  Zeilen-/Spaltenkopf), nicht nur am Rand wie bisher: Zellinhalte, Formate
  und Zeilenhöhen/Spaltenbreiten verschieben sich innerhalb der Tabelle,
  und jede Formel im gesamten Dokument, die die Tabelle referenziert (auch
  über Tabelle!Zelle/Blatt!Tabelle::Zelle-Qualifier aus anderen Tabellen),
  wird automatisch mitgezogen. Ein Bereich, der die Stelle überspannt,
  wächst/schrumpft entsprechend; eine Referenz, die exakt die gelöschte
  Zeile/Spalte trifft, wird zu einem echten #REF-Literal im Formeltext
  (nutzt die neue ErrorLiteralNode-Grammatik). $-Sperren bleiben dabei
  bewusst wirkungslos (anders als beim Kopieren), unveränderte Referenzen
  bleiben byte-identisch. Token-Level-Splice wie shiftFormula (Kopieren/
  Ausschneiden), keine AST-Neuserialisierung.

406 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 22:34:33 +02:00
wkhandClaude Sonnet 5 2c54537427 Text-Vergleich, &-Verkettung und benannte Bereiche
- Vergleichsoperatoren (= <> > < >= <=) unterscheiden jetzt Text- von
  Zahlenvergleich statt jede Seite zwangsweise durch toNumber() zu
  schicken (=A1="Nord" gab vorher #VALUE! statt 1); ermöglicht
  Text-Kriterien in selbstgebauten COUNTIF/SUMIF/AVERAGEIF-Funktionen
  (neue README-Sektion "SUMIF/COUNTIF/AVERAGEIF nachbauen").
- Neuer &-Operator (Excel-Präzedenz zwischen Vergleich und +/-) plus
  CONCAT/CONCATENATE als Funktions-Äquivalent.
- Benannte Bereiche: neuer Toolbar-Button "🏷" verwaltet dokumentweite
  Namen für eine feste Zelle/einen festen Bereich (z. B. Umsatz_Q1statt
  Umsatz!B2:B6), unqualifiziert in jeder Top-Level-Formel nutzbar.
  Live gegen die Tabellen-ID aufgelöst (übersteht Tabellen-Umbenennung),
  bewusst nicht innerhalb einer benutzerdefinierten Funktion selbst
  nutzbar (Aufrufer können sie aber als Argument übergeben).
- Nebenbei: COUNT2s Autovervollständigungs-Hint korrigiert (zeigte
  "COUNT(...)" statt "COUNT2(...)").

374 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 21:59:47 +02:00
wkh 9015a991aa adding count2 2026-09-03 08:49:20 +02:00
wkhandClaude Sonnet 5 0b7602b874 Funktions-Autovervollständigung in der Formelbearbeitung
Beim Tippen eines Funktionsnamens (nach "=", einem Operator, "(" oder
";" -- nicht bei einem Zellbezug wie A1) zeigt sowohl die Formelleiste
als auch der In-Zellen-Editor jetzt ein Dropdown mit passenden
eingebauten UND eigenen Funktionen samt Kurzsignatur. ↑/↓ navigieren,
Tab/Enter/Klick übernehmen (fügt "NAME(" ein), Esc schließt nur das
Dropdown, ohne die Bearbeitung abzubrechen.

- engine/refs.ts: neue reine Funktion currentIdentifierAtCaret() --
  strukturell wie das schon vorhandene cycleReferenceLocks (=
  abstreifen, tokenize, am Cursor nachsehen), aber nur der Text bis
  zum Cursor wird tokenisiert und nur das letzte Token zählt; ein
  IDENT, das bis an den Cursor reicht und nicht wie ein Zellbezug
  aussieht, ist der Funktionsname. tokenize() hängt ein EOF-Token an
  -- muss beim Suchen des "letzten Tokens" herausgefiltert werden
  (von den eigenen Tests aufgedeckt).
- engine/functions.ts: BUILTIN_SIGNATURES (Name -> Kurzsignatur je
  eingebauter Funktion), gegen FUNCTIONS' Keys testabgesichert, damit
  eine künftige neue Funktion ohne Eintrag hier nicht still im
  Dropdown fehlt.
- store/document.ts: functionSuggestions(prefix) kombiniert eingebaute
  + dokumenteigene Funktionen, case-insensitiv gefiltert, sortiert.
- composables/useFormulaAutocomplete.ts: pro Editor-Instanz eigener
  State (suggestions/activeIndex), orchestriert die drei Bausteine
  oben; die Übernahme eines Vorschlags läuft über dieselbe
  replace(start,end,value)-Mechanik, die Point-Mode für den Bezug-
  Einfügeklick schon nutzt -- keine neue Einfüge-Infrastruktur nötig.
- components/FormulaSuggest.vue: geteiltes, rein präsentationelles
  Dropdown (position:absolute; top:100% relativ zum Editor-Wrapper,
  kein Caret-Pixel-Tracking).
- FormulaBar.vue + TableGrid.vue: gleiches Einbindungsmuster in
  beiden Editoren (Wrapper bekommt position:relative, onInput ruft
  autocomplete.update(), onKeydown fängt Pfeiltasten/Tab/Enter/Esc
  vor der bestehenden Logik ab, wenn Vorschläge offen sind, onBlur
  schließt das Dropdown).

Tests: 13 neue (refs.spec.ts: 7 Fälle für currentIdentifierAtCaret;
functions.spec.ts neu: Signatur-Tabelle deckungsgleich mit FUNCTIONS;
document.spec.ts: functionSuggestions Filter/Mischung/Sortierung).
318 -> 331 Tests, alle grün. vue-tsc/eslint/build sauber. Live per
Playwright verifiziert in Formelleiste UND In-Zellen-Editor: Filterung,
Pfeiltasten-Navigation, Tab-Übernahme, Maus-Klick-Übernahme, Esc
schließt nur das Dropdown, eigene Funktion (MARGE) erscheint neben
eingebauten (MAX) korrekt einsortiert.

READMEs aktualisiert (neuer Abschnitt "Funktions-Autovervollständigung",
Tastaturtabelle, Testliste, Bekannte Grenzen -- dabei eine veraltete
Aussage korrigiert, die Autovervollständigung für benutzerdefinierte
Funktionen noch als "bewusst fehlend" auflistete).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 08:43:57 +02:00
wkhandClaude Sonnet 5 552f1cfae8 XLOOKUP als Formel, plus überspringbare optionale Argumente (;;)
Neue eingebaute Funktion XLOOKUP(Suchwert; Suchbereich; Rückgabebereich;
[WennNichtGefunden]; [Vergleichsmodus]; [Suchmodus]).

- Grammatik-Erweiterung (allgemein, nicht XLOOKUP-spezifisch): eine leere
  Argumentstelle zwischen zwei ";" (oder vor der schließenden ")") parst
  jetzt als neuer OmittedNode statt eines Parse-Fehlers -- Excels
  "dieses optionale Argument auslassen"-Idiom, z. B.
  XLOOKUP(x;a;b;;-1). Wertet überall wie eine leere Zelle aus (null);
  expand()/substitute() (userFunctions.ts) behandeln ihn als Blatt,
  collectRefs() ignoriert ihn stillschweigend.
- evaluator.ts: XLOOKUP wird -- wie IF -- vor dem generischen eager/
  flatten-Dispatch abgefangen (evalXLookup()), aus zwei Gründen: (1)
  WennNichtGefunden muss lazy ausgewertet werden (Kurzschluss wie bei
  IF), (2) Suchbereich/Rückgabebereich dürfen nicht in eine gemeinsame
  flache Werteliste geraten -- der generische Pfad würde die Grenze
  zwischen beiden Bereichen zerstören. Eigene xlookupEquals()
  (case-insensitiv/getrimmt für Text, kein Zahl-Text-Crossmatch) statt
  des `=`-Operators, der über toNumber() koerziert und nie auf Text
  träfe. Vergleichsmodus -1/1 (exakt/nächstkleiner/-größer, numerisch),
  Suchmodus 1/-1 (Richtung); Excels Platzhalter- und Binärsuch-Modi
  bewusst nicht unterstützt.
- functions.ts: Platzhalter-Eintrag für Namensreservierung (analog IF).
- types.ts: neuer Fehlercode #N/A (kein Treffer, kein
  WennNichtGefunden) -- dokumentierte Erweiterung, wie zuvor #VALUE!.
- collectRefs() brauchte keine Änderung -- sein generischer case 'func'
  läuft schon konservativ durch alle Argumente.

Tests: 22 neue (parser.spec.ts: OmittedNode leading/middle/trailing;
evaluator.spec.ts: SUM(1;;3)-Smoke plus 15 XLOOKUP-Fälle; workbook.spec.ts:
Neuberechnung bei Änderung in Such- ODER Rückgabebereich, keine bei
unbeteiligten Zellen; document.spec.ts: Namenskollision). 298 -> 318
Tests, alle grün. vue-tsc/eslint/build sauber. Live per Playwright
verifiziert (Treffer, #N/A, WennNichtGefunden, Kurzschluss-Beweis,
übersprungenes Argument für Vergleichsmodus, Neuberechnung nach Umbenennen
einer Suchbereich-Zelle inkl. nachträglich ausgelöstem #DIV/0! im vorher
nicht ausgewerteten WennNichtGefunden-Zweig).

READMEs aktualisiert (Funktionen, Fehlermodell, neue Abschnitte "Leere
Argumentstelle" und "Nachschlagen (XLOOKUP)" in engine/README.md,
Bekannte Grenzen, Testzahl; EBNF-Grammatik in parser.ts kommentiert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 08:21:11 +02:00
wkhandClaude Sonnet 5 af01aa5e53 Locale-unabhängiges ISO-Datumsformat als Zellformat
Neues CellNumFmt 'dateiso' im Zahlenformat-Dropdown ("Datum (ISO) ·
2026-09-02"): zeigt ein Datum immer als YYYY-MM-DD, unabhängig von der
aktiven Locale (Zelle/Blatt/Dokument) -- anders als "Datum", das der
jeweiligen Locale folgt. Nützlich für export-nahe/sortierbare
Datumsspalten.

- store/types.ts: CellNumFmt um 'dateiso' erweitert.
- store/format.ts: die manuelle YYYY-MM-DD-Formatierung, die bisher nur
  für das 'plain'-Pseudo-Locale galt, in isoDateString() extrahiert und
  von einem neuen 'dateiso'-Fall in formatValue() wiederverwendet.
  NUM_FORMATS bekam den neuen Eintrag -- das Zahlenformat-Dropdown
  (SettingsPanel.vue) und die JSON-Validierung (isCellNumFmt) sind
  bereits generisch über NUM_FORMATS, keine Änderung nötig.

Tests: formatValue mit 'dateiso' über mehrere Locales (inkl. 'plain').
297 -> 298 Tests, alle grün. vue-tsc/eslint/build sauber. Live per
Playwright verifiziert: Zelle auf "Datum (ISO)" umgestellt zeigt
2026-09-02 und bleibt das auch nach Wechsel der Dokument-Locale auf
en-US, während eine andere Zelle ohne dieses Format der Locale folgt
(09/02/2026).

READMEs aktualisiert (Zahlenformat-Liste, "Datum & Zeit"-Abschnitt in
engine/README.md, Testzahl).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 23:31:18 +02:00
wkhandClaude Sonnet 5 e6cb18ab3c Datum- & Zeit-Formatierung: Serial-Zahlen, Auto-Erkennung, DATE/TIME/TODAY/NOW
Wie in jeder Tabellenkalkulation ist ein Datum intern einfach eine Zahl
("Serial": Tage seit 1899-12-30, Uhrzeit als Bruchteil eines Tages) --
Datums-Arithmetik (=fällig - rechnungsdatum, =rechnungsdatum + 30)
funktioniert dadurch automatisch, ganz ohne Änderung an Operatoren.

- Neues Modul engine/dates.ts: dateToSerial/timeSerial (Bausteine für
  DATE()/TIME(), Überlauf wie Excel erlaubt), todaySerial/nowSerial
  (lokales Kalenderdatum), serialToUtcDate (Anzeige), parseDateTimeLiteral
  (Eingabe-Erkennung -- bewusst nur ISO/Punkt-Daten + Uhrzeit, KEINE
  Schrägstrich-Daten, da Tag-/Monat-zuerst über die unterstützten Locales
  nicht eindeutig auflösbar ist; ein zu einer ungültigen/rollierten
  Kalenderdatum führender Kandidat wird strikt verworfen statt lautlos
  zu überlaufen).
- workbook.ts: literalValue() erkennt Datum/Zeit vor dem Zahl-Test und
  liefert direkt den Serial.
- functions.ts: neue eingebaute Funktionen DATE(jahr;monat;tag),
  TIME(std;min;sek), TODAY(), NOW() -- TODAY/NOW sind bewusst nicht
  volatil (dokumentierte Grenze: diese Engine rechnet rein
  vorgänger-getrieben neu, ohne Timer).
- store/types.ts + format.ts: CellNumFmt um 'date'/'time'/'datetime'
  erweitert; formatValue rendert über Intl.DateTimeFormat (UTC-Zeitzone,
  pro Locale), 'plain' bekommt eine manuelle, locale-neutrale
  Formatierung statt der Browser-Locale.
- store/document.ts: setCellInput erkennt eine getippte Datums-/
  Zeit-Eingabe automatisch und setzt das passende Zellformat -- wie
  Excel/Numbers --, überschreibt aber nie ein bereits explizit gesetztes
  Format.
- Zahlenformat-Dropdown (SettingsPanel) und JSON-Validierung
  (isCellNumFmt) brauchten keine Änderung -- beide sind bereits generisch
  über NUM_FORMATS.

Tests: 32 neue (dates.spec.ts neu; workbook.spec.ts: literalValue +
DATE/TIME/TODAY/NOW-Formeln inkl. Datums-Arithmetik; format.spec.ts neu:
formatValue über mehrere Locales; document.spec.ts: Auto-Format beim
Eintippen, überschreibt nie ein gesetztes Format, Namenskollision mit
den neuen Funktionen). 265 -> 297 Tests, alle grün. vue-tsc/eslint/build
sauber. Live per Playwright verifiziert (Auto-Erkennung, Formel-Ergebnisse
ohne Auto-Format, Datums-Arithmetik, Schrägstrich-Daten bleiben Text).

READMEs aktualisiert (Funktionen, Zahlenformat, neuer "Datum & Zeit"-
Abschnitt in engine/README.md, Bekannte Grenzen, Testzahl; nebenbei eine
veraltete Aussage zu Vergleichsoperatoren in "Bekannte Grenzen" korrigiert
-- die gibt es seit der IF-Funktion bereits).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 23:24:39 +02:00
wkhandClaude Sonnet 5 ec85c371e8 IF-Funktion als Formel: echte Kurzschluss-Auswertung
Neue eingebaute Funktion IF(Bedingung; Dann; Sonst?), nutzbar in jeder
gewöhnlichen Zellformel (nicht nur in "Mit Schleife"-Funktionskörpern).

- functions.ts: IF als Platzhalter-Eintrag registriert, nur damit
  hasFunction('IF') greift (Namensreservierung -- ein Benutzer kann
  keine gleichnamige eigene Funktion mehr anlegen) und als defensiver
  Fallback. Trägt einen Kommentar, der auf die echte Implementierung
  verweist.
- evaluator.ts: die echte, kurzschließende Auswertung sitzt in
  evalScalar()s case 'func', VOR dem generischen eager-args-Pfad.
  evalIf() wertet nur die Bedingung immer aus, danach ausschließlich
  den genommenen Zweig -- der andere Zweig wird nie evaluiert, ein
  Fehler darin (z. B. =IF(B1=0; 0; A1/B1)) schlägt also nie durch.
  Gleiche Wahrheits-Konvention wie überall sonst im Engine (Zahl != 0
  bzw. nicht-leerer Text = wahr).
- collectRefs() bleibt unverändert: sein generischer case 'func' läuft
  bereits konservativ durch alle Argumente, unabhängig vom zur
  Laufzeit genommenen Zweig -- exakt wie Excels IF().

Tests: 8 neue (evaluator.spec.ts: beide Zweige, 2-/3-Argument-Form,
Kurzschluss-Beweis, Fehler in der Bedingung, Arity-Fehler, Vergleiche
als Bedingung; document.spec.ts: Namenskollision mit eingebautem IF).
257 -> 265 Tests, alle grün. vue-tsc/eslint/build sauber. Live per
Playwright verifiziert (inkl. Kurzschluss- und Neuberechnungsverhalten).

READMEs aktualisiert (Funktionen-Liste, Auswertungsmodell, Testzahl).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 23:00:45 +02:00
wkhandClaude Sonnet 5 3fd5a84701 Move zoom control into the status bar, right end
Was a floating pill pinned to the bottom-right corner of the canvas
(SheetCanvas.vue's .zoom-ctl); now lives as the last element of the
status bar (StatusBar.vue), after a thin divider, in the same combined
bottom row as the sheet tabs. Same three actions (−, current % as a
button that resets to 100%, +), same store.activeZoom/setZoom.
Ctrl/Cmd+scroll-to-zoom on the canvas is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 22:46:25 +02:00
wkhandClaude Sonnet 5 01ea18106c Real control flow for "Mit Schleife" functions: DO/END, IF/ELSE/END, RETURN anywhere
Replaces the previous FOR-only, single-statement-body, static-unrolling
design with genuine control flow: FOR var IN source DO <block> END loop
bodies with any number of statements (including nested loops/IFs),
IF cond THEN <block> [ELSE <block>] END branching (nestable in any
block), and RETURN as an ordinary statement usable anywhere -- it
aborts execution immediately with its value, including breaking out of
a loop early.

This required a real, if small, architecture change: an IF branch or an
early RETURN depends on actual cell values, which the previous "unroll
everything into one static formula at parse time" approach can't
resolve (loop *counts* are knowable up front from a range's size;
branch *outcomes* aren't). A kind: 'block' call is now inlined only as
far as resolving nested function calls (expandBlock, parse-time, same
inside-out principle as before) and wrapped in a new BlockCallNode;
its own control flow is interpreted at evaluation time by a small
tree-walking interpreter added to evaluator.ts (execBlock/execStatement,
a thrown ReturnSignal for early exit), reusing userFunctions.ts's
substitute() (now exported) to resolve parameter/local-variable names
against the current environment. Dependency tracking stays fully
correct and conservative: collectRefs() walks into a BlockCallNode's
block (new collectRefsInBlock), so a cell referenced only in a branch
that doesn't happen to run is still a tracked precedent -- same
principle as an ordinary IF() in any spreadsheet.

Also added: comparison operators (= <> > < >= <=, result 1/0) as a new,
loosest-binding precedence level in the *general* expression grammar
(not just inside a block's IF) -- needed for IF conditions to be useful
at all. A definition-time static check (blockAlwaysReturns) rejects a
function at save time when it's not obviously guaranteed to reach
RETURN on every path (an IF without an ELSE, or one where only one
branch returns); the interpreter's own runtime "ended without RETURN"
-> #ERR remains the actual enforcement for cases the static check can't
see (e.g. a loop over an empty range).

Nested loops (FOR inside FOR) and nested IFs work as an emergent
property of the recursive statement grammar -- verified with dedicated
tests, not just assumed.

257 tests passing (30 net new/rewritten for this change), vue-tsc/
eslint/build clean. Verified live in the running app: a FIRSTOVER(range;
threshold) function using a FOR loop with a nested IF and an early
RETURN, confirmed it returns the first match, confirmed changing a
cell *past* the match point doesn't affect the result while changing
the match point itself does, and confirmed a function without a
guaranteed RETURN is rejected inline at save time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 22:41:55 +02:00
wkhandClaude Sonnet 5 6daea9f956 Add FOR-loop "code" bodies to user-defined functions (kind: 'block')
A custom function body could previously only be a single expression --
no way to iterate over a range's elements to build something like
PRODUCT (multiply every cell together), since none of the built-ins
(SUM/AVG/MIN/MAX/COUNT) do that and a body can only compose existing
functions, not define a new reduction. This adds a second body kind,
'block': a sequence of `name = expression` assignments and
`FOR var IN source DO name = expression` loops, ending in
`RETURN expression`. Flat scope -- every introduced name stays visible
for the rest of the body, including after its own loop, holding its
last iteration's value.

No runtime loop in the evaluator: a loop's source size is already known
once the caller's real argument is substituted in, so
userFunctions.ts's new compileBlock() unrolls every FOR loop into a
single nested expression (a left fold) at the same parse-time inlining
point Phase 1 already used -- the result is exactly the same shape of
static AST as any other inlined function call, so evaluator.ts and
graph.ts need no changes, dependency edges on every unrolled cell are
picked up automatically, and a bounded MAX_UNROLL_STEPS (10,000) keeps
it crash/hang-free. parser.ts's parseFunctionBlock() parses the new
grammar (a purely additive EQUALS lexer token; FOR/IN/DO/RETURN are
plain case-matched IDENTs, not new reserved tokens) into a BlockNode
kept outside the existing Node union, so no existing switch needs a
new case.

Store: CustomFunction/FunctionDef gain `kind: 'expression' | 'block'`
(defaulted for every function saved before this existed, including via
JSON import). validateCustomFunction validates the new grammar's local/
loop-variable names the same way as parameters (not cell-shaped, no
reserved words, no shadowing a parameter -- but reusing the same local
across statements, the accumulator pattern, is correctly not a
collision) and that every FOR source is structurally a parameter, cell,
or range. UI: a "𝑓 Funktionen" form toggle between "Einfache Formel"
and "Mit Schleife", with its own placeholder/hint (a PRODUCT example)
and a small badge on block-kind rows in the list.

237 tests passing (34 new), vue-tsc/eslint/build clean. Verified live
in the running app: defined PRODUCT via the loop UI, called
=PRODUCT(Umsatz!B2:E6), confirmed the correct value and recalculation
on a change to a cell inside the range, and an inline rejection for an
invalid FOR source at save time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 21:58:59 +02:00
wkhandClaude Sonnet 5 7340307a70 Add a sheet qualifier: "Blatt!Tabelle::Zelle", table names unique per sheet
Table names were document-unique; now they're unique per sheet only --
two different sheets may each hold a table called "Umsatz". A plain,
unqualified reference (Umsatz!B2 / Umsatz::B2, unchanged from before)
resolves to the table on the SAME sheet as the formula referencing it
when the name is otherwise ambiguous, falling back to the single
document-wide match when it isn't ambiguous at all -- exactly as
before for every existing formula. The new explicit 3-part form
(Analyse!Umsatz::B2, quoted where a name has spaces) always targets
the named sheet's table regardless of where the formula itself lives,
for the case that's still genuinely ambiguous.

Engine: parser.ts's afterFirstQualifierToken() disambiguates via
lookahead -- "X!Y::" (a name, then "::") is the new 3-part sheet+table
form; a bare "X!Y" keeps its original single-qualifier meaning, so
every formula written before this exists still parses identically.
CellNode/RangeNode gain an optional sheetQualifier field.
Workbook.resolveTableName(name, sheetName?) implements the resolution
policy; TableModel gains sheetName. precedentsOf() now also sets
homeTableId (previously only evaluateCell did), so graph edges use the
same same-sheet-preference resolution an evaluation would.

Store: table name uniqueness checks (addTable, renameObject) are now
scoped to the target sheet instead of the whole document. Point-mode's
qualifyRef() inserts the full "Blatt!Tabelle::" form only when the
clicked table's name is genuinely ambiguous and lives on a different
sheet than the editor; otherwise the simple form, unchanged.

200 tests passing (19 new), vue-tsc and eslint clean, verified live in
the running app: renamed a Blatt-2 table to the same name as Analyse's
seed "Umsatz", confirmed an unqualified reference resolves per-sheet,
an explicit sheet-qualified one always targets the named sheet, and
the original sheet's own formulas are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 16:16:21 +02:00
wkhandClaude Sonnet 5 fdde4ed5a7 Show a range-handling pattern in the function-editing UI
A user function parameter already worked as a range when passed
straight into a range-capable call (SUM/AVG/MIN/MAX/another user
function) -- expandUserFunctions splices the caller's RangeNode in
unchanged, so it behaves exactly like a normal formula (and #VALUE!
outside a function-argument position, also exactly like normal). Added
2 tests confirming this (incl. recalculation on a change inside the
passed range) and a hint under the Formel field in
CustomFunctionsModal.vue showing the pattern (SPANNE(bereich) =
MAX(bereich) - MIN(bereich)), since nothing surfaced that this was
possible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:59:56 +02:00
wkhandClaude Sonnet 5 1046b22e96 Show the selected range, not just the active cell, in status/formula bar
New store computed selectionRangeName: for a single cell it's identical
to activeCellName ("Umsatz::B2"), for a multi-cell selection it shows
the full primary rectangle ("Umsatz::B2:E6"), and a discontiguous
multi-selection gets a "+N" suffix for the further rectangles.
activeCellName itself is untouched (still anchor-only, since that's what
a formula/edit actually targets) -- only the formula bar's name field and
the status bar's range label switch to the new computed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:49:35 +02:00
wkhandClaude Sonnet 5 85a8687020 Merge sheet tabs and status bar into one row
SheetTabs and StatusBar were two stacked full-width bars, each with its
own background/border. Wrapped them in a shared .bottom-bar flex row in
App.vue instead: the tab list shrinks and scrolls internally to make
room, the status bar is pinned to the right and scrolls internally
rather than wrapping, so the combined row stays a single line even at
narrow (375px) viewports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:43:48 +02:00
wkhandClaude Sonnet 5 bcc0e8252e Toolbar: insert/functions buttons as icon-only pictograms
Tabelle/Diagramm/Text/Funktionen now show only their glyph (▦ ▤ ¶ 𝑓,
matching the icons already used for object kinds in CanvasObject.vue)
instead of a text label, with title + aria-label tooltips for
accessibility. New shared .icon-btn style for the compact square shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:41:15 +02:00
wkhandClaude Sonnet 5 c3fde283c8 Add user-defined functions, editable exclusively via a new UI
Document-wide, named formula functions (e.g. MARGE(revenue; cost) =
(revenue - cost) / revenue) callable from any cell like a built-in
(=MARGE(Umsatz!B2; Umsatz!C2)). Created, edited and deleted only through
a new "𝑓 Funktionen" toolbar button -> modal (list + shared edit form),
never via JSON handedit.

Engine: a call is inlined into the caller's AST at parse time
(src/engine/userFunctions.ts's expandUserFunctions), so evaluator.ts and
graph.ts need no changes -- dependencies on cells referenced only inside
a function body (not in the call) are picked up for free. parser.ts
gains an optional paramNames argument to parseFormula() producing a new
ParamNode leaf, used only when parsing a function body.

Store: addCustomFunction/updateCustomFunction/deleteCustomFunction with
inline (not toast) validation -- name/parameter shape, no collision with
a built-in or another function, the body parses, and a definition-time
cycle check across the whole call graph. Renaming/deleting a function
mirrors table rename/delete: existing calls are not rewritten, they
become #NAME? on the next rebuild. Fully undo/redo-able and part of the
JSON document (customFunctions: [], migrated for v1 and older v2 docs).

178 tests passing (32 new), vue-tsc and eslint clean. Manually verified
end-to-end in the running app via Playwright: define/call/recalculate,
rename -> #NAME?, a closing cycle rejected inline at save, undo restores
the function library.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:37:04 +02:00
wkhandClaude Sonnet 5 fb5915d5be Add formula "point mode": click/drag a cell to insert its reference
Excel/Numbers-style reference picking: while editing a formula (buffer
starts with "="), a click on any grid cell -- in the same table or a
completely different one -- inserts its reference at the caret instead of
changing the selection; a drag inserts/grows a range reference live as you
drag. A further click/drag before typing anything replaces the just-inserted
reference; typing anything (";" to separate SUM() arguments included, but
not special-cased -- any character ends the window, same as Excel) makes the
next click append a fresh one instead.

- document.ts: whichever editor is open (in-cell editor or formula bar)
  registers a small handle (home tableId + read/replace the buffer);
  pointClick()/pointDrag() insert or replace a reference into it, qualifying
  with `Name!` (quoted if needed) when the clicked table differs from the
  editor's own. formulaPointModeActive() gates it on the buffer being a
  formula; noteFormulaTyped() (wired to real typing, not the programmatic
  inserts) ends the "replace" window.
- TableGrid.vue: onCellMouseDown checks point mode before its normal
  selection/edit handling, tracking a drag the same way normal selection
  does; dblclick during point mode no longer hijacks into editing that cell.
- FormulaBar.vue: registers on focus, same click/drag targeting works from
  there without disturbing the grid's own selection.

Verified interactively against the dev server with Playwright, including a
real bug the store-level tests (a mocked editor handle, no real DOM) missed
entirely: preventDefault() alone kept focus on the editor, but the mousedown
still bubbled to CanvasObject.vue's own handler, which selects/brings the
clicked table to front -- closing the very editor being pointed into. Fixed
with stopPropagation() alongside it. After that fix: same-table and
cross-table click/drag both insert correctly-qualified references, a second
click replaces before typing and appends after, and the live selection/
active object are untouched by a point-mode click.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 14:41:27 +02:00