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
2026-08-29 17:47:12 +02:00
2026-08-29 17:47:12 +02:00
2026-08-29 17:47:12 +02:00
2026-09-02 09:45:44 +02:00
2026-09-02 09:45:44 +02:00
2026-08-29 17:47:12 +02:00
2026-09-02 09:45:44 +02:00
2026-08-29 17:47:12 +02:00
2026-08-29 17:47:12 +02:00

Numbers-Studie — Tabellenkalkulation mit eigener Formel-Engine

Eine browserbasierte Tabellenkalkulation als Case- und Designstudie nach dem Vorbild von Apple Numbers. Zwei Dinge stehen im Mittelpunkt:

  1. Handwerk — eine von Grund auf selbst geschriebene, schlanke Formel-Engine (Tokenizer, Parser, AST, AbhĂ€ngigkeitsgraph, Neuberechnung, Zirkelbezug- Erkennung). Dokumentiert in src/engine/README.md.
  2. Produkt — eine polierte, Numbers-nahe OberflĂ€che: jedes Blatt ist eine freie FlĂ€che (Canvas), auf die man beliebig viele Tabellen- und Diagramm-Objekte setzt, verschiebt, skaliert und befĂŒllt.
npm install
npm run dev        # Vite-Devserver
npm run test       # Vitest-Suite (Engine + Store + IO), 237 Tests
npm run build      # Typecheck + Produktionsbuild
npm run lint       # ESLint (flat config)

Alles lĂ€uft rein clientseitig — kein Server, keine Netzwerk-Requests. Datei- I/O ĂŒber File API und Blob-URLs.


Das Canvas-Modell

Wie in Numbers ist ein Blatt kein einzelnes Endlos-Raster, sondern eine scroll- und zoombare FlÀche, die Objekte trÀgt:

  • Tabellen-Objekt — ein eigenes Gitter mit einem Namen, der pro Blatt eindeutig ist (siehe „Formelgrammatik" unten fĂŒr BezĂŒge ĂŒber mehrere BlĂ€tter mit demselben Tabellennamen). Frei positionierbar (Ziehen an der Kopfleiste). Zwei Darstellungen:
    • Automatisch — so groß wie die Zellen, alle Zeilen sichtbar; der SE-Griff skaliert die Spaltenbreiten proportional.
    • Feste GrĂ¶ĂŸe — fester Viewport, intern scrollbar, mit frei wĂ€hlbarer Zahl fixierter Kopfzeilen und fixierter Spalten (position: sticky gegen den Scroll-Container). Der SE-Griff zieht dann den Viewport auf. Spaltenbreite per Drag am Spaltenkopf (ab 24 px), Zeilenhöhe per Drag an der Unterkante des Zeilenkopfs (Standard + je Zeile).
  • Diagramm-Objekt — Chart.js (Balken/Linie), frei positionier- und skalierbar. Datenquelle = ein Tabellen-Objekt; jede ZellĂ€nderung aktualisiert es sofort.
  • Text-Objekt — ein formatierbares Textfeld als Zusatz zu Tabellen und Diagrammen. Doppelklick zum Bearbeiten; SchriftgrĂ¶ĂŸe, Fett/Kursiv, Ausrichtung, Text- und Hintergrundfarbe im Inspector; frei skalierbar.

Objekte kommen beim Anklicken in den Vordergrund, werden per Entf gelöscht (Diagramm/Text) bzw. ĂŒber das × der Kopfleiste, per Doppelklick auf den Namen umbenannt. Je Tabelle, Diagramm und Textfeld sind eigene Hintergrund- und Textfarbe einstellbar. Mehrere BlĂ€tter ĂŒber die Tabs unten (hinzufĂŒgen, umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt (Strg/Cmd + Mausrad, Zoom-Leiste oder Einstellungen).

Zellauswahl

  • Klick = eine Zelle, Shift+Klick / Ziehen = ein Rechteck. Das Ziehen ist positions- statt event-target-basiert (elementsFromPoint), ĂŒbersteht schnelle Bewegungen und das Verlassen der Tabelle, scrollt bei festen Tabellen an der Kante automatisch mit und heilt einen verlorenen mouseup selbst.
  • Kopf-Klick = ganze Spalte / Zeile, Ecke = alles.
  • Strg/Cmd+Klick = weitere Zelle hinzufĂŒgen (erneut = abwĂ€hlen), Strg/Cmd+Ziehen = weiteres Rechteck; auch am Spalten-/Zeilenkopf.
  • Die diskontinuierliche Mehrfachauswahl trĂ€gt alle folgenden Aktionen: Live-Aggregat (Summe/Mittel/Anzahl) in der Statusleiste, die Auswahl-Formatierung im Inspector und Entf (leert alle markierten Zellen).
  • Pfeiltasten / Enter kollabieren wieder auf eine Zelle. Die aktive Zelle (blauer Rahmen, Formelleiste, F2/Tippen zum Bearbeiten) ist stets die zuerst angeklickte/gesetzte Zelle einer Auswahl — sie bleibt beim Ziehen/Erweitern eines Bereichs stehen, statt dem Cursor zu folgen (Excel-/ Numbers-Verhalten). WĂ€hrend einer Tab/Enter-Dateneingabefolge im Bereich wandert sie mit dem Cursor mit.
  • Kopieren/Ausschneiden/EinfĂŒgen (Strg/Cmd+C/X/V, KontextmenĂŒ): ĂŒbertrĂ€gt Zellinhalte und Formatierung. EinfĂŒgen auf eine Auswahl, die ein exaktes Vielfaches der kopierten GrĂ¶ĂŸe ist, kachelt; eine kleinere oder Einzelzell-Auswahl verankert die kopierte FlĂ€che dort und lĂ€uft darĂŒber hinaus. Formeln werden dabei bezugsangepasst wie in Excel/Numbers — siehe „$-BezĂŒge sperren" unten; ein gestrichelter Rahmen markiert die Kopierquelle (mit eigenem Rahmen um deren erste Zelle, bleibt stehen, auch wenn die aktuelle Auswahl lĂ€ngst woanders ist), Esc verwirft den Puffer.
  • AusfĂŒllkĂ€stchen (das kleine Quadrat unten rechts an der Auswahl, wie in Excel/Numbers): ziehen wiederholt die Auswahl — Werte, Formeln (bezugsangepasst) und Formatierung — in die ĂŒberstrichenen Zellen. Nur eine Achse gleichzeitig (rein runter/hoch/rechts/links, je nachdem wohin weiter gezogen wird); ein mehrzeiliger/-spaltiger Ausgangsbereich wird zyklisch wiederholt, auch ĂŒber eine unvollstĂ€ndige letzte Wiederholung hinweg.

Tastaturnavigation (Excel-nah) — Fokus in der Tabelle:

Taste Verhalten
Pfeiltasten eine Zelle; Shift+Pfeil erweitert die Auswahl
Strg/Cmd+Pfeil zum Rand des Datenblocks springen (leer↔gefĂŒllt)
Tab / Shift+Tab rechts / links
Enter / Shift+Enter runter / hoch — nach einer Tab-Folge zurĂŒck zur Startspalte + eine Zeile
bei aktivem Bereich Tab/Enter rotieren den Cursor im Bereich (mit Umbruch), der Bereich bleibt markiert
Tippen / F2 Zelle editieren; Enter/Tab committen und ziehen weiter (auch im Bereich)
Pos1 / Strg+Pos1 / Ende / Strg+Ende Zeilenanfang / A1 / Zeilenende / letzte Zelle
Bild↑ / Bild↓ seitenweise (bei festen Tabellen nach Viewport-Höhe)
Strg/Cmd+C/X/V Kopieren / Ausschneiden / EinfĂŒgen (Zellen + Formatierung, bezugsangepasst)
Strg/Cmd+Z · Strg/Cmd+Y / Strg/Cmd+Shift+Z RĂŒckgĂ€ngig · Wiederholen (auch als Symbolleisten-Buttons ↶ ↷)
F4 (im Editor/in der Formelleiste) Bezug am Cursor sperren/zyklisch umschalten: A1 → $A$1 → A$1 → $A1 → A1; bei markiertem Text werden alle darin gefundenen BezĂŒge auf einmal umgeschaltet, jeder ausgehend vom eigenen Zustand
Alt+Enter bzw. Cmd+Enter (im Editor) Zeilenumbruch in der Zelle einfĂŒgen, statt zu committen — Cmd+Enter als Mac-Fallback, da ⌄ (Option) dort eine Compose-Taste ist und nicht ĂŒberall zuverlĂ€ssig als reines Modifier ankommt

Bei festen (scrollbaren) Tabellen wird die aktive Zelle bei jeder Navigation automatisch in den sichtbaren Bereich gescrollt (unter Beachtung fixierter Zeilen/Spalten).

RĂŒckgĂ€ngig/Wiederholen: dokumentweiter Undo-Stack (Snapshot-basiert, wie der bereits vorhandene JSON-Export/Import-Round-Trip — einfach und nachweisbar korrekt fĂŒr ein Dokument dieser GrĂ¶ĂŸe, statt eines Patch-/ Kommando-Logs). Reine Auswahl-/Cursor-Bewegungen zĂ€hlen nie als Schritt; eine schnelle Folge von Mutationen (ein Spalten-/Zeilen-Resize-Drag feuert bei jeder Mausbewegung) wird zu einem Schritt zusammengefasst, sofern sie innerhalb von 150 ms aufeinanderfolgen — knapp unter normalem Tipp-/Klick- Tempo, damit zwei tatsĂ€chlich getrennte schnelle Aktionen nicht verschmelzen, aber weit ĂŒber dem Abstand aufeinanderfolgender Drag-Ereignisse (~16–30 ms). Ein neuer Bearbeitungsschritt verwirft sofort einen etwaigen Wiederholen-Ast. „Auf Beispiel zurĂŒcksetzen" und JSON-Import „Ersetzen" starten eine neue Historie (das vorherige Dokument ist danach nicht mehr zurĂŒckholbar), Undo/ Redo selbst tun das nicht. Symbolleiste zeigt zwei Buttons (↶ ↷, deaktiviert wenn nichts zu tun ist); greift auch ohne Tabellenfokus, außer wĂ€hrend eine Texteingabe (Zell-Editor, Formelleiste, Blatt-Umbenennen, 
) den nativen Undo dieses Feldes benutzen soll.

In-Zellen-Editor: das Eingabefeld liegt als Overlay ĂŒber der Zelle (nicht in ihr) und wĂ€chst mit dem Inhalt — in der Breite bis zu einer angenĂ€herten „sichtbaren Anzeigebreite", danach umbrechend; in der Höhe mit Alt+Enter eingefĂŒgten ZeilenumbrĂŒchen, bis zu einem Maximum, danach intern scrollbar. Ein Zeilenumbruch bleibt auch nach dem Committen erhalten und wird in der Zelle als solcher dargestellt (umbrechend statt einzeiliger Ellipse), auf die Zeilenhöhe der Tabelle geklemmt.

Formelleiste (fx, oben): ein <textarea> statt eines einzeiligen Felds — dadurch umbricht eine lange Formel statt seitwĂ€rts wegzuscrollen, und an der unteren Kante lĂ€sst sie sich per Maus (natives resize: vertical) auf beliebig viele Zeilen aufziehen, um mehr auf einmal zu sehen. Alt+Enter bzw. Cmd+Enter, F4 und Esc verhalten sich wie im In-Zellen-Editor. Das Namensfeld links davon (ebenso das Pendant links in der Statuszeile, die mit den Blatt-Reitern eine gemeinsame Zeile am unteren Rand teilt) zeigt bei einer Mehrfachauswahl nicht nur die aktive Zelle, sondern den ganzen markierten Bereich (Umsatz::B2:E6), bei einer diskontinuierlichen Auswahl zusĂ€tzlich +N fĂŒr die weiteren Rechtecke.

BezĂŒge per Maus einfĂŒgen ("Point-Mode"): sobald der Editor (Zelle oder Formelleiste) eine Formel (=
) enthĂ€lt, fĂŒgt ein Klick auf eine Zelle — in derselben Tabelle oder einer ganz anderen, mit Name!-Qualifier bei Bedarf 'in Quotes' — deren Bezug an der Cursorposition ein; Ziehen fĂŒgt stattdessen einen Bereichsbezug (A1:C3) ein, live wachsend wĂ€hrend der Zug. Ein weiterer Klick/Zug ersetzt den zuletzt eingefĂŒgten Bezug, solange nichts dazwischen getippt wurde — jede Eingabe (ein ; als Argumenttrennzeichen ebenso wie jedes andere Zeichen) beendet dieses Fenster, der nĂ€chste Klick fĂŒgt dann einen neuen Bezug an. Ein Klick wĂ€hrend des Point-Mode Ă€ndert nie die Tabellen-/Objektauswahl im Hintergrund.

Benutzerdefinierte Funktionen

Der Button „𝑓 Funktionen" in der Symbolleiste öffnet die einzige Stelle, an der eigene, dokumentweite Funktionen angelegt, bearbeitet und gelöscht werden — kein JSON-Handedit, keine Konsole. Danach in jeder Zelle aufrufbar wie ein eingebauter Funktionsname: =MARGE(Umsatz!B2; Umsatz!C2).

  • Liste zeigt jede Funktion als NAME(param1; param2) — exakt die Aufruf-Syntax — mit Bearb. (öffnet dasselbe Formular, vorausgefĂŒllt) und 🗑 (löscht nach BestĂ€tigung).
  • Formular (Neu wie Bearbeiten): Name, eine dynamische Liste von Parameter-Feldern (+ Parameter, × zum Entfernen; auch 0 Parameter sind gĂŒltig — dann ist es eine benannte Konstante/Formel), und ein Formel-Feld optisch identisch zur Formelleiste — ohne fĂŒhrendes =. Ein Parametername verhĂ€lt sich im Funktionskörper wie eine Zelle mit eigenem Wert; er darf selbst nicht wie ein Zellbezug aussehen (A1, $B2 o. Ă€. sind ungĂŒltig). Ein Parameter kann auch ein Bereich sein, solange er im Funktionskörper direkt an eine bereichsfĂ€hige Funktion ĂŒbergeben wird — z. B. SPANNE(bereich) = MAX(bereich) - MIN(bereich), aufgerufen als =SPANNE(Umsatz!B2:E6); das Formular zeigt dieses Muster als Hinweis unter dem Formel-Feld. Wie bei jeder Formel ergibt ein Bereich außerhalb einer Funktionsargument-Position #VALUE!.
  • Validierung beim Speichern, mit Inline-Fehlermeldung direkt im Formular (kein Toast, nichts geht verloren): Name-Kollision mit einer eingebauten oder einer anderen benutzerdefinierten Funktion, ungĂŒltiger Name/Parameter, ein nicht parsender Formelkörper, und ein Funktions- Zyklus (A ruft B, B ruft A, auch ĂŒber mehrere Stufen) — wird abgelehnt, bevor je eine #ERR-Zelle entsteht.
  • Umbenennen/Löschen aktualisiert keine bestehenden Formeltexte — genau wie beim Umbenennen/Löschen einer Tabelle. Zellen, die die betroffene Funktion aufrufen, zeigen danach #NAME?.
  • Ein Aufruf wird beim Parsen der aufrufenden Zelle in ihre AST eingesetzt (Inlining, keine Laufzeit-Closures) — siehe src/engine/userFunctions.ts und die Architektur-Tabelle unten. Dadurch braucht Evaluator/Graph keine Änderung, und auch Zellen, die nur im Funktionskörper referenziert werden (nicht im Aufruf), lösen bei Änderung korrekt eine Neuberechnung aus.
  • VollstĂ€ndig undo-/redo-fĂ€hig und Teil des JSON-Exports/-Imports.

Mit Schleife: FOR-Code statt einer einzelnen Formel

Der „Art"-Umschalter im Formular bietet neben „Einfache Formel" (s. o.) auch „Mit Schleife" — echter Code statt eines einzelnen Ausdrucks, fĂŒr FĂ€lle, die eine einzelne Formel nicht ausdrĂŒcken kann: ĂŒber die Elemente eines Bereichs selbst iterieren (z. B. ein Produkt aller Zellen, nicht nur Summe/Mittel/Min/Max). Das Code-Feld akzeptiert eine Folge aus:

  • Zuweisungen — name = ausdruck.
  • FOR-Schleifen — FOR var IN bereich DO name = ausdruck; bereich ist ein Parameter, eine Zelle oder ein Bereich (kein berechneter Ausdruck).

— beliebig oft, beliebig gemischt, zuletzt RETURN ausdruck. Jeder eingefĂŒhrte Name (Zuweisungsziel oder Schleifenvariable) bleibt fĂŒr den restlichen Code sichtbar, auch nach dem Ende „seiner" Schleife (mit dem Wert der letzten Iteration) — flacher Namensraum, kein Block-Scoping. FOR/IN/DO/RETURN sind reservierte Wörter, nicht als Parameter-/ Variablenname erlaubt.

Beispiel — Produkt aller Elemente eines Bereichs (PRODUCT gibt es nicht als eingebaute Funktion, nur SUM/AVG/MIN/MAX/COUNT):

ergebnis = 1
FOR x IN bereich DO ergebnis = ergebnis * x
RETURN ergebnis

aufgerufen als =PRODUCT(Umsatz!B2:E6).

Keine echte Laufzeit-Schleife im Evaluator: Da die GrĂ¶ĂŸe eines Bereichs beim Aufruf bereits feststeht, wird eine FOR-Schleife beim Einsetzen des Funktionsaufrufs vollstĂ€ndig abgerollt — jede Iteration wird zu einem eigenen, ineinander verschachtelten Ausdrucks-Teilbaum (ein Links-Fold). Am Ende steht wieder eine ganz normale, statische Formel-AST — Evaluator und AbhĂ€ngigkeitsgraph sehen keinen Unterschied zu einer von Hand ausgeschriebenen Formel, Neuberechnung bei Änderung jeder im Bereich enthaltenen Zelle funktioniert automatisch korrekt. Kein Endlos-Risiko: mehr als insgesamt 10.000 abgerollte Zellen (ĂŒber alle Schleifen einer Funktion) ergeben #ERR statt zu hĂ€ngen. Details: src/engine/README.mds „Schleifen"-Abschnitt.

Zell-Formatierung

Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte ĂŒber den Kopf) → im Inspector unter Objekt · Auswahl: horizontale Ausrichtung (links/mitte/rechts), Zahlenformat (Automatisch, Ohne, Ganzzahl, 1/2 Nachkommastellen, Prozent 0/2 Stellen — kombiniert mit der Locale unten), Zell-Textfarbe, Zell-FĂŒllfarbe, Fett/Kursiv, „Formatierung entfernen". Auch exakte Spaltenbreite und Zeilenhöhe der Fokuszelle.

Ein Zahnrad neben der Überschrift öffnet eine linke Inspector-Schublade (SettingsPanel.vue), die die FlĂ€che zur Seite schiebt (auf schmalen Viewports ĂŒberlagert sie). Sie bĂŒndelt alle wichtigen Punkte:

  • Objekt (das gewĂ€hlte):
    • Tabelle → Name, Zeilen/Spalten (exakt), Darstellung, Viewport-GrĂ¶ĂŸe, fixierte Kopfzeilen/Spalten, Position, Zahlenformat-Locale, Standard- Zeilenhöhe, Tabellen-Hintergrund/-Textfarbe, und – bei aktiver Zellauswahl – die Auswahl-Formatierung (s. o.).
    • Diagramm → Name, Ansicht, Quelltabelle, GrĂ¶ĂŸe, Position, Zahlenformat-Locale, Hintergrund, Text-/Achsenfarbe.
    • Textfeld → Name, Text, SchriftgrĂ¶ĂŸe, Fett/Kursiv, Ausrichtung, Textfarbe, Hintergrund, GrĂ¶ĂŸe, Position.
  • Blatt: Name, Zahlenformat, Hintergrundfarbe, Schriftfarbe, Raster ein-/ausblenden (â†ș setzt eine Farbe auf den Theme-Wert zurĂŒck), neues Blatt, Blatt löschen.
  • Ansicht: Zoom-Regler (pro Blatt gespeichert).
  • Dokument: Erscheinungsbild (Auto/Hell/Dunkel), Zahlenformat (LĂ€ndereinstellung), CSV-Standard-Trennzeichen, „Auf Beispiel zurĂŒcksetzen".

Blatt-Erscheinungsbild

Jedes Blatt kann eine eigene Hintergrundfarbe (Canvas-FlĂ€che hinter den Objekten) und Schriftfarbe (Standard-Textfarbe der Gitter/Labels) haben und das Punkt-Raster ein- oder ausblenden. Die Farben ĂŒberschreiben die Theme-Variablen --bg / --text nur fĂŒr dieses Blatt; ohne Wert gilt das Theme. Der Canvas-Zoom wird ebenfalls pro Blatt gemerkt.

Alle diese Einstellungen sind Teil des Dokuments (DocumentState / Sheet) und werden damit in localStorage gespeichert und in .json exportiert – Round-Trip verlustfrei, ungĂŒltige Werte werden beim Import verworfen bzw. geklemmt.

Zahlenformat (LĂ€ndereinstellung)

Die Darstellung von Zahlen (Dezimal- und Tausendertrennzeichen) ist eine Kaskade: Objekt → Blatt → Dokument. Jede Ebene kann die darĂŒberliegende ĂŒberschreiben oder „erben" (leere Auswahl). Auswahl: Ohne Trennzeichen, de-DE, de-CH, en-US, en-GB, fr-FR, it-IT. Wirkt auf Zellwerte, das Live-Aggregat der Statusleiste und die Wertelabels der Diagramme. CSV-/XLSX- Export bleibt bewusst locale-neutral (.-Dezimal, keine Gruppierung) fĂŒr einen verlustfreien Round-Trip; der CSV-Import erkennt beide Schreibweisen ohnehin.

Das vorbefĂŒllte „Umsatz" ist bereits als feste, scrollbare Tabelle mit fixierter Kopfzeile und fixierter Spalte A angelegt, um das Feature zu zeigen.


Formelgrammatik (EBNF)

formula     = "=" expression ;

expression  = term { ("+" | "-") term } ;          (* links-assoziativ *)
term        = unary { ("*" | "/") unary } ;         (* links-assoziativ *)
unary       = ("+" | "-") unary
            | power ;
power       = primary [ "^" unary ] ;               (* rechts-assoziativ; bindet
                                                       stÀrker als unÀres Minus,
                                                       d.h. -2^2 = -(2^2) = -4    *)
primary     = NUMBER | STRING | "(" expression ")" | funcall | reference ;

funcall     = IDENT "(" [ expression { ";" expression } ] ")" ;

reference   = [ sheetQualifier ] [ qualifier ] CELL [ ":" CELL ] ;
qualifier   = (IDENT | QUOTED) ("!" | "::") ;       (* Tabellenname *)
sheetQualifier = (IDENT | QUOTED) "!" ;             (* Blattname -- nur erkannt,
                                                       wenn direkt gefolgt von
                                                       einem vollen "Tabelle::"
                                                       (3-teilige Form); ein
                                                       einzelnes "X!Y" ohne
                                                       folgendes "::" bleibt
                                                       die alte 2-teilige Form
                                                       (X = Tabelle)           *)

IDENT       = /[A-Za-z_][A-Za-z0-9_]*/ ;
QUOTED      = /' ( [^'] | '' )* '/ ;                (* Name mit Leerzeichen  *)
CELL        = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;

PrĂ€zedenz schwach → stark: + - < * / < unĂ€res + - < ^.

Der (innere) Qualifier ist der Tabellenname. ! und der Numbers-Stil :: sind dabei austauschbar und case-/whitespace-tolerant: Kosten!A2 = 'Kosten'!A2 = kosten!a2 = Kosten::A2.

Tabellennamen sind nur pro Blatt eindeutig, nicht dokumentweit — zwei verschiedene BlĂ€tter dĂŒrfen je eine Tabelle „Umsatz" haben (beim Anlegen/ Umbenennen wird nur gegen das eigene Blatt geprĂŒft). Ein unqualifizierter Bezug wie Umsatz!B2 (ohne Blattangabe) löst sich dann wie folgt auf:

  1. Gibt es die Tabelle nur auf einem Blatt (der Normalfall), wird sie genommen — unverĂ€ndert wie zuvor.
  2. Gibt es sie auf mehreren BlĂ€ttern, gewinnt die Tabelle auf demselben Blatt wie die Formel selbst (nicht: dem gerade angezeigten Blatt in der UI — das spielt fĂŒr die Formel keine Rolle).
  3. Bleibt es danach noch mehrdeutig, ergibt sich #REF — hier hilft die explizite 3-teilige Form Blatt!Tabelle::Zelle, z. B. =Analyse!Umsatz::B2 oder mit Leerzeichen ='Blatt 2'!Umsatz::B2, die immer genau die Tabelle auf dem genannten Blatt trifft, unabhĂ€ngig davon, wo die Formel selbst steht. Die 2-teilige Form (Umsatz!B2 / Umsatz::B2) bleibt fĂŒr jede bestehende Formel exakt gĂŒltig — die 3-teilige Form ist rein additiv.

Die Formel-Point-Mode-EinfĂŒgung (s. o.) erkennt eine mehrdeutige Zieltabelle selbst und fĂŒgt automatisch die volle 3-teilige Form ein, statt einer Formel, die versehentlich die falsche Tabelle trĂ€fe.

$-BezĂŒge sperren (relativ/gemischt/absolut): A1 (relativ) vs. $A1 / A$1 (Spalte bzw. Zeile gesperrt) vs. $A$1 (beide gesperrt). FĂŒr die Auswertung selbst ist $ irrelevant — sie ist rein positionsbasiert. Es zĂ€hlt beim Kopieren: eine gesperrte Achse bleibt exakt wie eingegeben stehen, eine ungesperrte verschiebt sich um das EinfĂŒge-Delta (siehe „Kopieren/ Ausschneiden/EinfĂŒgen" oben). Beim Ausschneiden gilt das nicht — dort verschiebt sich die referenzierte Zelle selbst, also folgt jede Referenz darauf, $ hin oder her. F4 schaltet den Bezug unter dem Cursor durch die vier Kombinationen.

Funktionen (case-insensitiv, Trenner ;): SUM, AVG (Alias AVERAGE), MIN, MAX, COUNT — plus alle ĂŒber „𝑓 Funktionen" angelegten benutzerdefinierten Funktionen (siehe „Benutzerdefinierte Funktionen" oben), die sich fĂŒr den Aufrufer identisch verhalten. Argumente gemischt: Einzelzellen, Bereiche (lokal oder Name!
), Zahl-Literale — z. B. =SUM(Umsatz!B2:E2; 100; A1).

Fehlermodell (propagiert an abhĂ€ngige Zellen): #REF (ungĂŒltiger/gelöschter Bezug, unbekannter Tabellen-/Blattname, oder ein unqualifizierter Tabellenname, der auf mehreren BlĂ€ttern existiert und auch ĂŒber die „gleiches Blatt wie die Formel"-Regel nicht eindeutig wird — siehe oben), #DIV/0!, #NAME? (unbekannte Funktion, eingebaut oder benutzerdefiniert), #ERR (Parse-Fehler; auch eine falsche Argumentanzahl bei einer benutzerdefinierten Funktion), #CIRC (Zirkelbezug, auch tabellenĂŒbergreifend), #VALUE! (Typfehler in Arithmetik — dokumentierte Erweiterung). Referenz auf leere Zelle = 0 in Arithmetik, von COUNT nicht gezĂ€hlt. Ein Zyklus zwischen benutzerdefinierten Funktionen selbst wird nicht ĂŒber #CIRC sichtbar — er wird schon beim Speichern der Funktion im UI abgelehnt (kein neuer Fehlercode nötig).


Architektur der Engine-Module

Framework-agnostisches ES-Modul unter src/engine/ — keine Formel-/Spreadsheet-Library. Kennt kein Vue, kein DOM. Die adressierbare Einheit ist eine Tabelle; der AbhĂ€ngigkeitsgraph lĂ€uft dokumentweit ĂŒber alle Tabellen aller BlĂ€tter.

Datei Aufgabe
lexer.ts Tokenizer, flacher Token-Strom (inkl. ! und ::).
parser.ts Recursive-Descent → AST; EBNF im Dateikopf; collectRefs().
evaluator.ts AST + EngineHost → Wert; Fehler-Propagierung.
graph.ts DependencyGraph: affected(), topoOrder(), Zirkel-DFS.
refs.ts A1 ↔ (col,row), Bereichs-Expansion.
functions.ts { NAME → fn(values) } fĂŒr SUM/AVG/MIN/MAX/COUNT.
userFunctions.ts Benutzerdefinierte Funktionen: expandUserFunctions() setzt einen Aufruf beim Parsen der aufrufenden Zelle als Kopie des (rekursiv expandierten, parametersubstituierten) Funktionskörpers in deren AST ein — kein Laufzeit-Closure, Evaluator/Graph sehen danach eine ganz normale AST.
workbook.ts Flache Tabellenliste + ASTs + Werte + Graph; inkrementelle Neuberechnung; implementiert EngineHost; verdrahtet userFunctions.ts direkt nach parseFormula().

Neuberechnung. setCell reparst eine Zelle → graph.affected(dirty) → graph.topoOrder() → Zyklus-Zellen #CIRC, Rest topologisch auswerten, nicht betroffene Zellen behalten ihren Wert.

Anbindung an Vue. src/store/document.ts ist das „eigene reactive()-Modul": es hĂ€lt eine Workbook-Instanz und einen recalcTick-Ref. Die UI liest store.cellValue(...), das recalcTick berĂŒhrt — nach jeder Neuberechnung wird der Tick erhöht, jede Formelzelle re-rendert also wie ein computed.


Selbst gebaut vs. Library

Selbst gebaut Library
Formel-Engine komplett (Lexer, Parser, AST, Evaluator, dokumentweiter AbhÀngigkeitsgraph, Topo-Sort, Zirkel-Erkennung, Funktionen) Vue 3 + Vite
Die Canvas: Objekte platzieren, verschieben, skalieren, Z-Order, Zoom Chart.js (Balken + Linie)
Das Raster je Tabelle: Auswahl (Klick/Shift/Drag), Tastaturnavigation, In-Zellen-Edit, Spaltenbreite per Drag xlsx (SheetJS) — nur .xlsx I/O, lazy geladen, nie zur Auswertung
Reaktiver Store / Dokumentmodell (v1 → v2 Migration) —
CSV-Parser + -Writer (RFC 4180, Quoting, Delimiter-Sniffing, Locale-Zahlen)
JSON-Serialisierung + Schema-Validierung
Sheet-Tabs (Drag-Sortierung), KontextmenĂŒ, Modals, Toasts, Theme

Bewusst nicht verwendet: Handsontable, AG Grid, Univer, Luckysheet, HyperFormula, formula-parser. @vueuse/core/splitpanes (in der Aufgabe als optionale Komfort-Libs genannt) wurden nach dem Umbau auf das Canvas-Modell nicht mehr gebraucht und wieder entfernt, um den AbhÀngigkeitsbaum ehrlich zu halten.


Import / Export

Alles clientseitig (Blob + URL.createObjectURL + <a download> + revoke).

Export

Format Inhalt
.json Natives Format, verlustfreier Round-Trip: alle BlĂ€tter, alle Tabellen-/Diagramm-Objekte mit Position & GrĂ¶ĂŸe, Rohformeln, Spaltenbreiten, Tab-Reihenfolge, aktives Objekt, Zellauswahl.
.csv Aktive Tabelle, berechnete Werte, RFC-4180-Quoting, Trennzeichen ,/; wÀhlbar, \r\n, optional UTF-8-BOM.
.xlsx Arbeitsmappe via SheetJS, ein Worksheet je Tabellen-Objekt. Formeln als echte Excel-Formeln (;→,, AVG→AVERAGE), immer mit gecachtem Wert.
.png GewĂ€hltes Diagramm-Objekt ĂŒber chart.toBase64Image().

Import

<input type="file"> und Drag & Drop auf die Canvas; .json .csv .xlsx.

  • .json → Dokument ersetzen (RĂŒckfrage) oder BlĂ€tter anhĂ€ngen. v1-Dokumente (altes Ein-Raster-Format) werden automatisch nach v2 migriert (je altes Blatt ein Blatt mit einer gleichnamigen Tabelle). Schema wird validiert; bei Fehler klare Meldung, kein Teil-Import.
  • .csv → „als neue Tabelle (aktuelles Blatt)" / „aktive Tabelle ab A1 ersetzen". Trennzeichen-Sniffing (+ Override). Locale-Erkennung (1.234,56 ↔ 1,234.56). Werte mit fĂŒhrendem = werden zu Formeln.
  • .xlsx → je Worksheet ein neues Blatt mit einer Tabelle.
  • Nach jedem Import: kompletter Reparse + Neuberechnung + Diagramm-Refresh. SchlĂ€gt ein Import fehl, bleibt das bestehende Dokument unverĂ€ndert.

Persistenz

Das gesamte Dokument wird (debounced) nach localStorage gespiegelt, robust gegen leeren/kaputten State — Fallback ist das Seed-Dokument. Ein v1-State im Storage wird beim Laden migriert. Button „ZurĂŒcksetzen" stellt das Seed-Dokument wieder her.

VorbefĂŒlltes Dokument

Blatt „Analyse" mit drei Tabellen-Objekten (Umsatz, Kosten, Übersicht) und einem Diagramm (Umsatz je Region), plus ein leeres Blatt „Blatt 2". „Übersicht" demonstriert tabellenĂŒbergreifende BezĂŒge und die vier Grundrechenarten: Übersicht!B2 = =SUM(Umsatz!B2:E2), D2 = =B2-C2, E2 = =D2/B2*100. Siehe src/data/seed.ts.

Hinweis zur Aufgabenstellung: Die Akzeptanzliste nennt „Übersicht!B2 = 6070". Die explizit angegebene Nord-Zeile ergibt jedoch 1200+1450+1600+1720 = 5970. Die Studie hĂ€lt sich an die explizite Tabelle; das demonstrierte Verhalten (Sofort-Propagierung durch Übersicht, „Gesamt" und das Diagramm bei Änderung von Umsatz!B2) ist davon unberĂŒhrt.

Tests

npm run test — Vitest, 237 Tests:

  • lexer / parser — PrĂ€zedenz & AssoziativitĂ€t (^ vor */ vor +-, ^ rechts, -2^2 = -(2^2)), lokale/qualifizierte/quoted BezĂŒge, ! und ::, Bereiche, ;-Argumente, Parse-Fehler.
  • refs ($-Sperrachsen, F4) — parseA1/toA1 runden relativ/absolut/ gemischt; cycleReferenceLocks durchlĂ€uft die vier Kombinationen, trifft bei Cursor auf einem Bereich die richtige Seite, liefert null ohne Treffer; bei echter Textauswahl schaltet es alle ĂŒberlappten BezĂŒge auf einmal um (jeden vom eigenen Zustand aus) und lĂ€sst Tabellen-Qualifier, die wie eine Zelle aussehen (AB12!A1), unangetastet.
  • evaluator — Arithmetik, leere Zelle = 0, alle Funktionen inkl. Alias, Cross-Table, Case-/::-Toleranz, jeder Fehlerfall, Fehler-Propagierung.
  • graph — affected, topologische Ordnung, Selbst-/2er-/lĂ€ngerer Zyklus.
  • workbook / store — Seed-Aggregate, Cross-Table-Summen, Edit-Ripple durch die ganze Kette, #REF nach Tabellen-/Blatt-Löschung, neues Tabellen-Objekt mit Cross-Sheet-Formel, resetToSeed, Live-Auswahl-Aggregat, Neuberechnungs-Reihenfolge, v1 → v2 Migration.
  • Tabellen-Ansicht — Seed-Umsatz ist fixiert/scrollbar; setTableView klemmt fixierte Zeilen/Spalten an die GittergrĂ¶ĂŸe; Verkleinern löscht Zellen außerhalb und klemmt nach; Ansichts-Einstellungen im JSON-Round-Trip.
  • Zahlenformat-Kaskade — cellDisplay nutzt Dokument-Locale; Blatt-Override schlĂ€gt Dokument, Objekt-Override schlĂ€gt Blatt; ZurĂŒcksetzen fĂ€llt die Kaskade hinab; Overrides im JSON-Round-Trip, ungĂŒltige Werte werden verworfen.
  • Blatt-Erscheinungsbild — Hintergrund/Schrift/Raster sind pro Blatt und löschbar; Zoom pro Blatt (folgt dem Tab); Erscheinungsbild + Zoom + CSV-Default im JSON-Round-Trip, kaputte Farb-/Zoom-Werte verworfen bzw. geklemmt.
  • Zell-Formatierung — per-Zelle Zahlenformat × Locale (inkl. Prozent); setRangeFormat ĂŒber einen Bereich, Löschen per Falsy/auto; selectionFormat meldet nur gemeinsame Werte; Zeilenhöhe (Standard + je Zeile), Spaltenbreite ab 24 px; Verkleinern pruned formats/rowHeights; alles im JSON-Round-Trip.
  • Text-Objekte — einfĂŒgen/bearbeiten/formatieren + Round-Trip; kaputtes Text-Objekt im Import wird abgelehnt, Dokument bleibt unverĂ€ndert.
  • Objekt-Farben — je Tabelle/Diagramm/Textfeld eigenes bg/fg, löschbar, im JSON-Round-Trip.
  • Mehrfachauswahl — addToSelection behĂ€lt frĂŒhere Rechtecke & verschiebt die aktive Zelle, erneutes Klicken schaltet eine 1×1-Auswahl ab, extendActiveRange wĂ€chst das jĂŒngste Rechteck; Aggregat & Formatierung ĂŒber alle Rechtecke; #-genaue Zellzahl; JSON-Round-Trip inkl. ranges, Klemmung beim Verkleinern; die aktive Zelle bleibt beim Erweitern eines Bereichs der Anker (erste Zelle), nicht die zuletzt erreichte.
  • Zwischenablage — Kopieren passt jede relative Referenz um das EinfĂŒge- Delta an, $-gesperrte Achsen bleiben stehen; EinfĂŒgen kachelt eine exakt passende grĂ¶ĂŸere Auswahl bzw. verankert/ĂŒberlĂ€uft eine kleinere; Formatierung wird mitkopiert und am Ziel ersetzt; Ausschneiden folgt trotz $ der verschobenen Zelle, lĂ€sst aber unbeteiligte und tabellenfremde Referenzen unangetastet und leert die Quelle; Esc verwirft den Puffer.
  • AusfĂŒllkĂ€stchen — fĂŒllt runter/rechts mit bezugsangepassten Formeln; ein mehrzeiliger/-spaltiger Quellbereich wiederholt sich zyklisch, auch bei einer unvollstĂ€ndigen letzten Wiederholung; fĂŒllt ebenso hoch/links; Formatierung wird mitkopiert.
  • RĂŒckgĂ€ngig/Wiederholen — Einzel-Edit rĂŒckgĂ€ng-/wiederholbar; mehrere Edits schrittweise in der richtigen Reihenfolge zurĂŒck; ein neuer Edit nach einem Undo verwirft den Wiederholen-Ast; reine AuswahlĂ€nderungen sind nie ein Schritt; deckt Kopieren/EinfĂŒgen, AusfĂŒllen und Formatierung ab (nicht nur setCellInput); „Auf Beispiel zurĂŒcksetzen" beginnt eine neue Historie.
  • Formel-Point-Mode — Klick fĂŒgt einen lokalen Bezug an der Cursorposition ein; ein zweiter Klick ohne Zwischen-Tippen ersetzt ihn, mit Tippen dazwischen hĂ€ngt er einen neuen an; Ziehen fĂŒgt/erweitert einen Bereichsbezug; ein Klick in eine andere Tabelle qualifiziert mit deren Namen (Quoting bei Leerzeichen); ohne Formel im Puffer greift nichts.
  • Benutzerdefinierte Funktionen — Engine-Ebene (userFunctions.spec.ts): Parametersubstitution, verschachtelte Aufrufe (eingebaut + benutzerdefiniert, auch bei kollidierenden Parameternamen zwischen den Scopes), Arity-Mismatch → #ERR, unbekannter Name → #NAME?, Rekursionstiefen-Sicherheitsnetz, AbhĂ€ngigkeits-Edges fĂŒr Zellen, die nur im Funktionskörper vorkommen (nicht im Aufruf) — inkl. korrekter Neuberechnungs-Reihenfolge; ein Bereichs- Parameter funktioniert in Funktionsargument-Position (inkl. Neuberechnung bei Änderung einer Zelle innerhalb des ĂŒbergebenen Bereichs) und ergibt #VALUE! außerhalb davon. Store-Ebene: Name-/ Parameter-Validierung, Kollision mit eingebauten/anderen Funktionen, Zyklus-Ablehnung beim Speichern (direkt und ĂŒber mehrere Stufen), Umbenennen/ Löschen lĂ€sst bestehende Aufrufe zu #NAME? werden, Undo/Redo einer FunktionsĂ€nderung, voller JSON-Round-Trip, customFunctions: [] bei v1→v2-Migration.
  • „Mit Schleife" (FOR-Code) — Parser-Ebene (parseFunctionBlock): eine Schleife (Akkumulator-Muster), mehrere unabhĂ€ngige Schleifen nacheinander, mehrere Zuweisungen vor/zwischen Schleifen, ein Name aus einer frĂŒheren Anweisung in einer spĂ€teren Schleife nutzbar; Fehler bei fehlendem RETURN, FOR ohne DO/IN, fehlendem =, Selbstbezug im eigenen Initialisierer. Engine-Ebene (compileBlock): korrektes Abrollen (ein Bereich → verschachtelte AusdrĂŒcke in Zellreihenfolge), ein einzelner Zellbezug als Quelle → genau eine Iteration, die Schleifenvariable/das letzte Zuweisungsziel bleiben danach mit ihrem letzten Wert sichtbar, verschachtelter Aufruf einer anderen Funktion im Schleifenkörper wird mit- expandiert, Arity-PrĂŒfung gilt auch fĂŒr „Mit Schleife"-Funktionen, eine FOR-Quelle, die nach Parametersubstitution kein Zell-/Bereichsbezug ist, wirft, mehr als 10.000 abgerollte Zellen wirft. Workbook-Integration: korrekter Wert und Neuberechnung bei Änderung einer Zelle innerhalb des ĂŒbergebenen Bereichs, Überschreitung des Abroll-Limits und eine ungĂŒltige FOR-Quelle ergeben #ERR statt eines Absturzes. Store-Ebene: reservierte Wörter (FOR/IN/DO/RETURN) als Parameter- oder lokaler Name abgelehnt, zellartiger lokaler Name abgelehnt, Wiederverwendung desselben lokalen Namens ĂŒber mehrere Anweisungen (das Akkumulator-Muster) ist keine Kollision, ein lokaler Name, der einen Parameter ĂŒberschattet, wird abgelehnt, eine berechnete FOR-Quelle wird abgelehnt, Funktions-Zyklus durch einen Schleifenkörper wird erkannt, Undo/Redo, kind im JSON- Round-Trip, ein Ă€lteres gespeichertes Dokument ohne kind-Feld defaultet auf 'expression'.
  • Blatt-Qualifier „Blatt!Tabelle::Zelle" — Parser-Ebene: die explizite 3-teilige Form (bar und quoted, in Bereichen/Funktionsargumenten), volle RĂŒckwĂ€rtskompatibilitĂ€t der 2-teiligen Form (X!Y, X::Y, auch ein zweites ! statt ::), Fehler bei fehlender Zelle nach ::. Workbook- Ebene: zwei gleichnamige Tabellen auf verschiedenen BlĂ€ttern lösen ein unqualifiziertes Tabelle!Zelle zum gleichen Blatt wie die Formel auf, die explizite Form trifft immer das genannte Blatt unabhĂ€ngig vom Formel-Standort, ein unbekanntes Blatt/eine weiterhin mehrdeutige Tabelle ergibt #REF, Neuberechnung folgt korrekt der Auflösung. Store- Ebene: Tabellennamen sind nur noch pro Blatt eindeutig (Anlegen/ Umbenennen prĂŒft nur gegen das eigene Blatt), Point-Mode fĂŒgt die volle 3-teilige Form ein, sobald ein Klick auf eine mehrdeutig benannte Tabelle auf einem anderen Blatt trifft.
  • IO — JSON verlustfreier Round-Trip (identisch), defekte/leere Datei → Fehler ohne Nebenwirkung, unbekannter Tabellenname → #REF, v1→v2 beim Import; CSV Übersicht-Export→Re-Import gleiche Zahlen, RFC-4180-Quoting, =1+2→3, Locale-Zahlen.

Bekannte Grenzen

  • Zeilen/Spalten nur am Rand. EinfĂŒgen/Löschen innerhalb eines Gitters (mit automatischem Umschreiben relativer BezĂŒge) ist bewusst nicht implementiert.
  • Objekt-BezĂŒge beim AnhĂ€ngen. Beim JSON-AnhĂ€ngen zweier Dokumente werden nur Blatt-/Objekt-IDs neu vergeben, nicht Tabellennamen. Kollidiert ein angehĂ€ngter Tabellenname mit einem bestehenden, gewinnt bei der Auflösung der erste Treffer. „Ersetzen" (der Round-Trip-Fall) ist davon nicht betroffen.
  • Kein Virtual Scrolling — „Feste GrĂ¶ĂŸe" scrollt intern, aber alle Zeilen bleiben im DOM (nur geclippt); sehr große Tabellen/Importe (viele Tausend Zeilen) werden trĂ€ge.
  • Große Dateien werden per setTimeout-Yield verarbeitet (UI bleibt reaktiv), kein echter Web Worker.
  • xlsx-Formel-Übernahme ist heuristisch (Separator-Swap unter Beachtung von Quotes); exotische Excel-Formeln fallen auf den Wert zurĂŒck.
  • Kein Text-Verkettungs-/Vergleichsoperator, keine benannten Bereiche (nur benutzerdefinierte Funktionen, keine benannten Zellen/Bereiche), kein Snapping der Canvas-Objekte.
  • Benutzerdefinierte Funktionen (Phase 1, siehe oben) bewusst ohne: AutovervollstĂ€ndigen des Funktionsnamens im Editor, Live-Vorschau des Ergebnisses im Bearbeiten-Dialog, variadische Parameter/Default-Werte, und automatisches NachfĂŒhren bestehender Aufrufe bei Umbenennung (Verhalten entspricht bewusst dem einer Tabellen-Umbenennung, s. o.).
  • „Mit Schleife"-Code bewusst ohne: verschachtelte Schleifen (FOR innerhalb einer DO-Klausel), bedingte Anweisungen (IF), echtes Block-Scoping (Namen bleiben absichtlich bis zum Ende des Codes sichtbar), frĂŒhzeitiges RETURN mitten in der Anweisungsfolge (nur am Ende erlaubt).

Projektstruktur

src/
  engine/           # Formel-Engine (Eigenbau) + Unit-Tests
  store/            # reaktiver Dokument-Store (v2 Canvas-Modell), Formatierung
  io/               # csv / json (Eigenbau, inkl. v1→v2), xlsx (SheetJS, lazy)
  components/       # AppToolbar, SettingsPanel, FormulaBar, SheetCanvas, CanvasObject,
                    #   TableGrid, ChartView, TextBox, SheetTabs, StatusBar, TheLogo, 

  numolus-logo.svg  # Marke: drei ĂŒberlappende Pop-Art-Quadrate + Schriftzug
  config/brand.ts   # Herausgeber-Metadaten (wicom.li) + Social-Media-Liste
  composables/      # Theme, Toasts
  data/seed.ts      # vorbefĂŒlltes Dokument
  styles/main.css   # Design-Tokens (Light/Dark)
S
Description
No description provided
Readme
3.3 MiB
Languages
TypeScript 73.9%
Vue 25.5%
HTML 0.3%
CSS 0.3%