# 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`](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. ```bash npm install npm run dev # Vite-Devserver npm run test # Vitest-Suite (Engine + Store + IO), 132 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 dokumentweit eindeutigem Namen. 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) | | **`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). **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. ### 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. ### Einstellungen (⚙, links neben dem Titel) Ein Zahnrad neben der Überschrift öffnet eine linke Inspector-Schublade ([`SettingsPanel.vue`](src/components/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) ```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 = [ qualifier ] CELL [ ":" CELL ] ; qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname *) 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 Qualifier ist der **Tabellenname** (nicht der Blattname). `!` und der Numbers-Stil `::` sind austauschbar. Tabellennamen werden dokumentweit eindeutig gehalten und **case-/whitespace-tolerant** aufgelöst: `Kosten!A2` = `'Kosten'!A2` = `kosten!a2` = `Kosten::A2`. **`$`-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`. 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 oder unbekannter Tabellenname), `#DIV/0!`, `#NAME?` (unbekannte Funktion), `#ERR` (Parse-Fehler), `#CIRC` (Zirkelbezug, auch tabellenübergreifend), `#VALUE!` (Typfehler in Arithmetik — *dokumentierte Erweiterung*). Referenz auf leere Zelle = `0` in Arithmetik, von `COUNT` nicht gezählt. --- ## Architektur der Engine-Module Framework-agnostisches ES-Modul unter [`src/engine/`](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. | | `workbook.ts` | Flache Tabellenliste + ASTs + Werte + Graph; inkrementelle Neuberechnung; implementiert `EngineHost`. | **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`](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` + `` + 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 `` **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`](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, 132 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. - **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, kein Kopieren/Einfügen von Zellbereichen, keine Undo-Historie, kein Snapping der Canvas-Objekte. ## 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) ```