From 31108977b98a30f221d1b03882d913168351f55f Mon Sep 17 00:00:00 2001 From: Wild Karl-Heinz Date: Wed, 2 Sep 2026 09:45:44 +0200 Subject: [PATCH] - before copy paste --- README.md | 25 ++- index.html | 42 ++++- numolus-logo.svg | 21 +++ package.json | 2 +- src/components/AppToolbar.vue | 159 +++++++++++++++-- src/components/SheetCanvas.vue | 19 ++- src/components/TableGrid.vue | 244 ++++++++++++++++++++++++--- src/components/TheLogo.vue | 46 +++++ src/config/brand.ts | 37 ++++ src/numolus-logo.svg | 11 ++ src/store/__tests__/document.spec.ts | 143 ++++++++++++++++ src/styles/main.css | 3 + 12 files changed, 708 insertions(+), 44 deletions(-) create mode 100644 numolus-logo.svg create mode 100644 src/components/TheLogo.vue create mode 100644 src/config/brand.ts create mode 100644 src/numolus-logo.svg diff --git a/README.md b/README.md index 5587915..7dba08c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt: ```bash npm install npm run dev # Vite-Devserver -npm run test # Vitest-Suite (Engine + Store + IO), 102 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 105 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -66,6 +66,23 @@ umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt - Pfeiltasten / Enter kollabieren wieder auf eine Zelle. Die aktive Zelle (Formelleiste) ist stets die zuletzt angeklickte. +**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) | + +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 @@ -268,7 +285,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 102 Tests: +`npm run test` — Vitest, 105 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -337,7 +354,9 @@ src/ 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, … + # 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) diff --git a/index.html b/index.html index 8ee4156..772ed01 100644 --- a/index.html +++ b/index.html @@ -4,11 +4,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Numolus · Tabellenkalkulation + Numolus · Tabellenkalkulation — wicom
diff --git a/numolus-logo.svg b/numolus-logo.svg new file mode 100644 index 0000000..be7c56c --- /dev/null +++ b/numolus-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + numolus + + + diff --git a/package.json b/package.json index 9edc7f2..f5c5441 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "description": "Browser-Tabellenkalkulation nach dem Vorbild von Apple Numbers – mit selbst geschriebener Formel-Engine.", "scripts": { - "dev": "vite", + "dev": "vite --port 3000", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "test": "vitest run", diff --git a/src/components/AppToolbar.vue b/src/components/AppToolbar.vue index 1c6ccef..6e73e51 100644 --- a/src/components/AppToolbar.vue +++ b/src/components/AppToolbar.vue @@ -4,6 +4,8 @@ import { useDocument } from '../store/document'; import { useTheme } from '../composables/useTheme'; import { useToasts } from '../composables/useToasts'; import BaseModal from './BaseModal.vue'; +import TheLogo from './TheLogo.vue'; +import { WICOM, WICOM_SOCIAL } from '../config/brand'; import { exportJson, exportActiveTableCsv, @@ -24,9 +26,12 @@ const { mode: themeMode, cycle: cycleTheme } = useTheme(); const toasts = useToasts(); const fileMenuOpen = ref(false); +const aboutOpen = ref(false); const busy = ref(false); const fileInput = ref(null); +const addr = `${WICOM.address.street}, ${WICOM.address.city}, ${WICOM.address.country}`; + const themeLabel = computed( () => ({ system: 'Auto', light: 'Hell', dark: 'Dunkel' })[themeMode.value], ); @@ -187,6 +192,13 @@ function onReset() { + + + +

+ Numolus — browserbasierte Tabellenkalkulation nach dem Vorbild von + Apple Numbers, mit selbst geschriebener Formel-Engine. Case- und Designstudie. +

+ +
+
Herausgeber
+
+ {{ WICOM.name }} + — {{ WICOM.descriptor }}
+ „{{ WICOM.tagline }}" · seit {{ WICOM.since }} +
+ +
Anschrift
+
{{ addr }}
+ +
Kontakt
+
+ {{ WICOM.email }}
+ {{ WICOM.phone }} +
+ +
Autor
+
{{ WICOM.author }}
+ +
Social Media
+
+ + wicom.li führt keine Social-Media-Profile. +
+
+ + +
diff --git a/src/config/brand.ts b/src/config/brand.ts new file mode 100644 index 0000000..77d031e --- /dev/null +++ b/src/config/brand.ts @@ -0,0 +1,37 @@ +/** + * Publisher / company metadata. + * Source: https://wicom.li (fetched 2026-08-29). + */ +export const WICOM = { + name: 'wicom', + legalName: 'wicom – Wild Karl-Heinz', + descriptor: 'Webentwicklung & digitale Lösungen', + tagline: 'Unsere Arbeiten – Ihr Erfolg', + since: 1997, + url: 'https://wicom.li', + email: 'office@wicom.li', + phone: '+41 79 446 2901', + phoneHref: 'tel:+41794462901', + address: { + street: 'Birkenweg 6', + city: 'FL-9490 Vaduz', + country: 'Liechtenstein', + }, + author: 'Karl-Heinz Wild', +} as const; + +/** One-line description of this app, used for `` / OG. */ +export const APP_DESCRIPTION = + 'Numolus — browserbasierte Tabellenkalkulation nach dem Vorbild von Apple Numbers, ' + + 'mit selbst geschriebener Formel-Engine. Eine Case- und Designstudie von wicom (Vaduz, FL).'; + +/** + * Social-media profiles of wicom. + * wicom.li lists none — add `{ label, url }` entries here and they appear in the + * header and the "Über"-Dialog automatically. + */ +export const WICOM_SOCIAL: ReadonlyArray<{ label: string; url: string }> = [ + // { label: 'LinkedIn', url: 'https://www.linkedin.com/company/…' }, + // { label: 'GitHub', url: 'https://github.com/…' }, + // { label: 'X', url: 'https://x.com/…' }, +]; diff --git a/src/numolus-logo.svg b/src/numolus-logo.svg new file mode 100644 index 0000000..dcaf0a4 --- /dev/null +++ b/src/numolus-logo.svg @@ -0,0 +1,11 @@ + + numolus + Drei am oberen Rand ausgerichtete, leicht überlappende Quadrate in kräftigen Pop-Art-Farben mit schwarzer Umrandung, darunter der Schriftzug numolus. + + + + + + numolus + diff --git a/src/store/__tests__/document.spec.ts b/src/store/__tests__/document.spec.ts index eedd922..02e8fb3 100644 --- a/src/store/__tests__/document.spec.ts +++ b/src/store/__tests__/document.spec.ts @@ -515,6 +515,149 @@ describe('per-object background & text colour', () => { }); }); +describe('full-fidelity JSON export — backgrounds, formats, sheet settings', () => { + /** Populate *every* persisted setting, then verify the export string carries + * it and a round-trip is byte-for-byte identical. */ + function loadEverything(s: DocumentStore) { + const analyse = s.doc.sheets[0].id; + const blatt2 = s.doc.sheets[1].id; + const umsatzId = tableId(s, 'Umsatz'); + const chartId = s.doc.sheets[0].objects.find((o) => o.kind === 'chart')!.id; + + // document + s.setDocumentLocale('en-GB'); + s.setCsvDelimiter(';'); + + // sheet ("Arbeitsblatt") settings — on both sheets + s.setSheetLocale(analyse, 'de-CH'); + s.setSheetBackground(analyse, '#eef2ff'); + s.setSheetForeground(analyse, '#101828'); + s.setSheetShowGrid(analyse, false); + s.setSheetZoom(analyse, 1.25); + s.renameSheet(blatt2, 'Notizen'); + s.setSheetBackground(blatt2, '#fff7ed'); + + // table object: colours + geometry + view + per-row/col + cell formats + s.setObjectBg(umsatzId, '#0b1220'); + s.setObjectTextColor(umsatzId, '#f8fafc'); + s.setObjectLocale(umsatzId, 'fr-FR'); + s.setTableView(umsatzId, { mode: 'fixed', width: 520, height: 300, frozenRows: 1, frozenCols: 1 }); + s.setTableRowHeight(umsatzId, 30); + s.setRowHeight(umsatzId, 4, 52); + s.setColWidth(umsatzId, 0, 130); + s.setRangeFormat(umsatzId, { col: 1, row: 1 }, { col: 3, row: 4 }, { + align: 'right', + valign: 'middle', + numFmt: 'pct2', + color: '#dc2626', + bg: '#fee2e2', + bold: true, + italic: true, + }); + + // chart object + s.setObjectBg(chartId, '#111827'); + s.setObjectTextColor(chartId, '#e5e7eb'); + s.setChartView(chartId, 'line'); + s.resizeChart(chartId, 600, 360); + + // text object with full formatting + const txtId = s.addText(analyse); + s.setTextContent(txtId, 'Bericht\nZeile 2'); + s.setTextFormat(txtId, { fontSize: 20, bold: true, italic: true, align: 'center' }); + s.setObjectTextColor(txtId, '#334155'); + s.setObjectBg(txtId, '#f1f5f9'); + + // multi-range selection + s.setSelection({ tableId: umsatzId, anchor: { col: 0, row: 0 }, focus: { col: 0, row: 0 } }); + s.addToSelection(umsatzId, 2, 2); + s.addToSelection(umsatzId, 4, 5); + + return { analyse, umsatzId, chartId, txtId }; + } + + it('the exported JSON text literally contains every setting', () => { + const s = fresh(); + loadEverything(s); + const json = serializeDocument(s.snapshot()); + const doc = JSON.parse(json); + + // document + expect(doc.locale).toBe('en-GB'); + expect(doc.csvDelimiter).toBe(';'); + + // sheet settings + const a = doc.sheets[0]; + expect(a).toMatchObject({ + locale: 'de-CH', + background: '#eef2ff', + foreground: '#101828', + showGrid: false, + zoom: 1.25, + }); + expect(doc.sheets[1]).toMatchObject({ name: 'Notizen', background: '#fff7ed' }); + + // table object + const tbl = a.objects.find((o: { kind: string }) => o.kind === 'table'); + expect(tbl).toMatchObject({ + bg: '#0b1220', + fg: '#f8fafc', + locale: 'fr-FR', + rowHeight: 30, + rowHeights: { '4': 52 }, + colWidths: { '0': 130 }, + view: { mode: 'fixed', width: 520, height: 300, frozenRows: 1, frozenCols: 1 }, + }); + expect(tbl.formats['1,1']).toEqual({ + align: 'right', + valign: 'middle', + numFmt: 'pct2', + color: '#dc2626', + bg: '#fee2e2', + bold: true, + italic: true, + }); + + // chart object + const cht = a.objects.find((o: { kind: string }) => o.kind === 'chart'); + expect(cht).toMatchObject({ bg: '#111827', fg: '#e5e7eb', view: 'line', width: 600, height: 360 }); + + // text object + const txt = a.objects.find((o: { kind: string }) => o.kind === 'text'); + expect(txt).toMatchObject({ + text: 'Bericht\nZeile 2', + fontSize: 20, + bold: true, + italic: true, + align: 'center', + color: '#334155', + bg: '#f1f5f9', + }); + + // multi-range selection + expect(doc.selection.ranges).toHaveLength(2); + }); + + it('export -> import is lossless for the fully-loaded document', () => { + const s = fresh(); + loadEverything(s); + const before = s.snapshot(); + s.replaceDocument(parseDocument(serializeDocument(before))); + expect(s.snapshot()).toEqual(before); + }); + + it('resetToSeed then re-import the exported file restores the exact state', () => { + const s = fresh(); + loadEverything(s); + const exported = serializeDocument(s.snapshot()); + const before = s.snapshot(); + s.resetToSeed(); + expect(s.snapshot()).not.toEqual(before); + s.replaceDocument(parseDocument(exported)); + expect(s.snapshot()).toEqual(before); + }); +}); + describe('v1 -> v2 migration', () => { it('turns each old sheet into a canvas sheet holding one table of the same name', async () => { const { migrateToV2 } = await import('../../io/json'); diff --git a/src/styles/main.css b/src/styles/main.css index f89ca87..0eb6c20 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -18,6 +18,7 @@ --accent-weak: rgba(10, 132, 255, 0.14); --accent-header: rgba(10, 132, 255, 0.22); --danger: #ff3b30; + --logo-keyline: #111111; --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 6px 20px rgba(0, 0, 0, 0.06); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08); --radius: 12px; @@ -44,6 +45,7 @@ --accent: #0a84ff; --accent-weak: rgba(10, 132, 255, 0.22); --accent-header: rgba(10, 132, 255, 0.32); + --logo-keyline: #f2f2f4; --shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 8px 28px rgba(0, 0, 0, 0.45); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); } @@ -61,6 +63,7 @@ --accent: #0a84ff; --accent-weak: rgba(10, 132, 255, 0.22); --accent-header: rgba(10, 132, 255, 0.32); + --logo-keyline: #f2f2f4; --shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 8px 28px rgba(0, 0, 0, 0.45); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); }