# 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), 127 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 unter dem Cursor sperren/zyklisch umschalten: `A1` → `$A$1` → `A$1` → `$A1` → `A1` | Bei **festen (scrollbaren) Tabellen** wird die aktive Zelle bei jeder Navigation automatisch in den sichtbaren Bereich gescrollt (unter Beachtung fixierter Zeilen/Spalten). ### 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, 127 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; `cycleReferenceLock` durchläuft die vier Kombinationen, trifft bei Cursor auf einem Bereich die richtige Seite, liefert `null` ohne Treffer. - **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) ```