docs/config: restore current docs and package metadata, history stripped

README.md, docs/{architecture,css-classes,dev-workflow}.md, manifest.json,
package.json, package-lock.json, versions.json, .gitignore, LICENSE, and
tsconfig.json are re-added here with their current content only. Their
prior revision history is gone by design: an audit found 22 of README.md's
24 historical revisions still linked to handoff.md (a gitignored, never-
published file) — a dead reference that only ever disclosed the private
doc's existence, not its content, but was live across nearly the entire
project history until a recent cleanup. Since these are docs/metadata
rather than code, commit-by-commit revision history has no review value
here; only the current, already-audited content is kept. Code history
(everything else) is untouched.
This commit is contained in:
SVM0N 2026-07-16 14:03:23 +00:00
parent 6351d84e16
commit 4faff4588e
11 changed files with 2122 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
node_modules/
.DS_Store
Test/
# Ignore user data files at project root (sample/scratch copies).
# xlsx patterns kept post-migration in case someone drops one in for testing.
/*.csv
/*.xlsx
# Local deploy symlinks into personal vaults — never track (they leak
# machine paths and break on clone). npm run deploy follows them if present.
/datadeck
/datadeck-work
# Local-only synthetic demo data — the repo ships none; tests inline their fixtures.
/sample-data
# Personal dev tooling + session notes — local only, never published.
/local/
/handoff.md
/public_audit.md
# Claude Code local settings — machine-specific paths, never track.
.claude/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 SVM0N
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

282
README.md Normal file
View file

@ -0,0 +1,282 @@
# DataDeck
**Your CSV files, alive inside Obsidian.** DataDeck opens plain `.csv` files
as rich, editable views — cards, kanban, an editable table, charts with
best-fit lines and formula overlays, a habit dashboard, a tasks board, a
one-entry-at-a-time focus reader, and an interactive travel map. Everything
edits in place and writes straight back to the one canonical CSV file: no
database, no shadow copies, no lock-in. Your data stays a text file that
diffs, syncs, and outlives any tool.
![Chart view — a time series split by category with rolling-average smoothing](docs/img/chart-view.png)
<details>
<summary><b>More screenshots</b> — kanban, the tasks board, and the travel map</summary>
![Kanban view grouped by category with status subgroups](docs/img/kanban-view.png)
![Tasks board — sections per type, grouped by project, overdue flags](docs/img/tasks-view.png)
![Travel view — world choropleth with confirmed and photo-only countries](docs/img/travel-view.png)
</details>
## Highlights
- **Nine view modes**, auto-offered based on the columns in the file — a
books CSV gets a library, a habit log gets a dashboard, a tasks file gets
a project board, a travel log gets a world map.
- **Everything is editable** — click a cell, toggle a habit, drag nothing:
edits debounce-save back to the CSV. Notes-style columns render and edit
as **Markdown**.
- **Charts** like a tiny ggplot: scatter/line of any column pair, color-by
category (hue), size-by, per-series least-squares fits with R², `y = f(x)`
formula overlays, week/month bucketing, rolling-average smoothing, bar
aggregates for categorical axes, and one-click PNG export.
- **Embed blocks** put live views inside any note: `csv-view` (table /
cards / kanban), `csv-chart`, `csv-tasks` (a cross-file tasks board),
`csv-random` (quote of the day), and `csv-add` (a mobile-friendly entry
form).
- **Sync-safe saves** — if the file changed on disk while you were editing
(another device, another tab), the other version is stashed to `Archive/`
instead of being silently overwritten.
- **Works on mobile.** Compact toolbars, one-scroll modals, touch-friendly
pickers.
## Installation
Until DataDeck is in the community store:
- **Via [BRAT](https://github.com/TfTHacker/obsidian42-brat):** add
`SVM0N/datadeck` as a beta plugin.
- **Manual:** grab `main.js`, `manifest.json`, `styles.css`, and
`world-map.svg` from the latest
[release](https://github.com/SVM0N/datadeck/releases) into
`<vault>/.obsidian/plugins/datadeck/`, then enable **DataDeck** in
Settings → Community plugins.
> `world-map.svg` is loaded at runtime for the travel view — include it when
> installing manually.
## Quick start
1. Open any `.csv` file — DataDeck takes over the tab and picks a sensible
view from the columns it finds.
2. Switch views with the toolbar dropdown; **⚙ Config** sets per-file column
roles (title, category, status, notes, image…), column types (checkbox /
categorical / date), and the default view.
3. **+ Add** opens an entry form with the right control per column: dropdowns
for categorical columns, a date picker for dates, toggles for 0/1 habit
columns, a Markdown textarea for notes.
4. No CSV yet? Run a palette command: **Create tasks / travel / habit
tracker / chart file** scaffolds one preconfigured for its view.
## View modes
The toolbar dropdown offers only the modes that make sense for the file's
columns:
| Mode | Appears when… | What it shows |
|------|---------------|---------------|
| **Table** | always | Editable spreadsheet: resizable columns, click-to-sort headers, sticky header |
| **Cards** | any groupable column | A library grouped by category — status dots, star ratings, tags, cover images, per-file sort |
| **Kanban** | any groupable column | Columns by category (or any column via "Group by" — year columns bucket into decades), status subgroups, inline note editing |
| **Chart** | a numeric column (2+ rows) | Scatter/line of any column pair with hue, size-by, fits, formulas, smoothing, bucketing, and bar aggregates — see [Charts](#charts) |
| **Dashboard** | a date column | Habit tracker: daily toggles, progress chart, streaks, per-habit GitHub-style calendars |
| **Tasks** | due/priority columns, or task-like type values | Project board: rows grouped by project, split into Tasks / Idea / Note sections, click-to-toggle done, overdue flags |
| **Stats** | a category/status/rating/author column | Bar-chart insights: status breakdown, categories, rating histogram, entries per year — bars click through to the filtered library |
| **Focus** | any non-empty file | One entry at a time with big typography — built for quotes and vocabulary. ←/→ keys navigate |
| **Travel** | `country` + trip-date columns | World choropleth, trip timeline, per-country day totals, residency day-counters — see [Travel view](#travel-view) |
## Charts
Pick **Chart** in the toolbar (or embed a `csv-chart` block):
- **X / Y** — any numeric or date column; a date X renders a connected time
series, numeric pairs render as scatter dots.
- **Color** — ggplot-style hue: one colored series per value of a
categorical column.
- **Size** — a numeric column mapped to point radius (area-true sqrt scale).
- **Best fit** — per-series least-squares line with the equation and R²
(phrased as an per-day trend for date axes).
- **Smooth** — a 7-day rolling average drawn over faint raw dots.
- **By week / month** — bucket a time series with a sum / avg / count
aggregate ("km per week, one line per person").
- **Bar mode** — pick a *categorical* X and the chart becomes bars with a
count / sum / avg aggregate; hue makes grouped bars; comma-separated
columns (genres, tags) count once per value.
- **`y = f(x)` overlay** — type `2x^2 - 3x + 1` or `10 sin(x/2)`; parsed by
a small built-in evaluator (`+ - * / % ^`, parentheses, implicit
multiplication, `sin cos tan sqrt log ln exp abs floor ceil round min max
pow`, `pi`/`e`). No `eval`.
- **⬇ PNG** — saves the chart as an image next to the CSV.
## Embed blocks
Live views inside any note. All paths resolve like links: a bare name is a
sibling of the note, `../` walks up, and anything else is vault-relative.
### `csv-view` — table / cards / kanban in a note
````
```csv-view
file: ../movies.csv
mode: kanban ← table | cards | kanban (default: table)
height: 480 ← optional max content height in px
collapse: Removed, Done ← optional: card groups collapsed by default
```
````
Fully editable — inline cells, status chips, the entry expander, + Add, and
delete-with-undo all write back to the source CSV. Other open views of the
same file re-sync automatically.
### `csv-chart` — a chart (or pure function plot) in a note
````
```csv-chart
file: ../health.csv ← omit for a formula-only plot
x: date ← default: date column, else first numeric
y: weight ← default: first numeric column ≠ x
hue: person ← optional color-by column
size: effort ← optional numeric column → point radius
agg: sum ← aggregate for bar mode / bucket (count | sum | avg)
bucket: week ← date X only: week | month buckets
smooth: true ← date X only: rolling mean (true = 7 days, or a number)
fit: linear ← best-fit line(s) with equation + R²
formula: 0.5x + 2 ← y = f(x) overlay; the whole plot when there's no file
xmin: -10 ← formula-only domain (default -10 … 10)
xmax: 10
height: 280
```
````
Read-only; re-renders when the source CSV changes.
### `csv-tasks` — a cross-file tasks board
````
```csv-tasks
folder: Projects ← scan recursively for task-shaped CSVs ("/" = whole vault)
files: extra.csv ← and/or an explicit comma-separated list
height: 600
```
````
Merges task rows from many CSVs into one board. Columns map onto a canonical
set (Title / Type / Project / Priority / Due / Status / Notes) by name
aliases — `deadline`, `state`, `kind`, `area`… all resolve. A file without a
Project column uses its **basename** as the project, so one-CSV-per-project
folders just work. Every edit writes back to the row's source file;
right-click a row for "Open source".
### `csv-random` — a random entry (quote of the day)
````
```csv-random
file: ../quotes.csv
field: Quote ← optional; defaults to the first column
```
````
Re-rolls on every render and via its ↻ button — made for daily-note
templates.
### `csv-add` — an entry form in a note
````
```csv-add
file: ../habits.csv
```
````
A compact, grouped add/update form (date pre-filled with today, toggles for
habit columns, dropdowns with a custom escape hatch). On habit-tracker files
it pre-fills from the existing row for the picked date, so updating a day
never wipes what's already logged.
## Travel view
For a flat travel CSV with these columns:
```
date_entered,date_left,country,city,visa_status,notes,source,resolved
```
- `country` is ISO-2 (`US`, `FR`, …); `source` is `confirmed`, `inferred`
(e.g. photo-derived), or `conflict`.
- Confirmed countries render gold; photo-only countries blue; conflict rows
and photo rows overlapping a confirmed trip are excluded from counts.
- Click a country (map, table, or timeline) for a per-country trip panel.
- Blank/partial dates (`2022-06-??`) are allowed — visited-but-undated. A
blank `date_left` counts as an ongoing stay ("📍 Currently in …").
### Residency counters
Configurable day-gauges (e.g. Schengen 90/180) render in the travel view.
Each rule counts *days in the listed countries within a window (calendar
year / rolling N days / all time), minus exempt visa rows, against a
threshold*. Add rules in **Settings → DataDeck → Residency rules**. These
are **indicators, not legal or tax advice**.
## Anki sync
The **🎴 Anki** toolbar button pushes rows to
[Anki](https://apps.ankiweb.net/) as flashcards — great for quotes and
vocabulary files. One Basic note per row: the front is the title column (or
your ⚙ Config pick), everything else joins onto the back. Decks are named
after the file and re-syncs skip cards that already exist, so it's safe to
click repeatedly.
Requires the [AnkiConnect](https://ankiweb.net/shared/info/2055492159)
add-on and the Anki desktop app running; desktop-only (it talks to
`http://127.0.0.1:8765`).
## Commands
- **Add entry to current CSV** — the + Add form, hotkey-able.
- **Cycle view mode** — steps through the file's valid views.
- **Create tasks / travel / habit tracker / chart file** — scaffold a new
CSV preconfigured for that view.
## Privacy & network use
- All data stays in your vault. **No telemetry, no analytics, no network
calls** — with one exception: the optional Anki sync button talks to
AnkiConnect on `localhost` (`127.0.0.1:8765`) and only when you click it.
- Per-file settings live in the plugin's `data.json` inside your vault.
## Development
```bash
npm install
npm run build # bundle main.ts → main.js (esbuild)
npm run dev # watch build
npm run test:all # unit + jsdom view-smoke tests (no Obsidian needed)
npm run typecheck # tsc --noEmit
```
To deploy straight into your own vault while developing, point the plugin
at it once — `npm run deploy` copies the built assets into local symlinks
that you create (they're gitignored):
```bash
ln -s "<your-vault>/.obsidian/plugins/datadeck" datadeck
ln -s "<second-vault>/.obsidian/plugins/datadeck" datadeck-work # optional
npm run build:deploy
```
Vault-touching dev tooling reads the `OBSIDIAN_VAULT` environment variable
for your vault's path (no default).
`main.ts` holds the `CardView` lifecycle and plugin entry point; each view
renderer and feature lives in `src/` as a free function taking the view —
the same functions power the full-page views and the embed blocks. Tests run
the renderers against a jsdom DOM with Obsidian's DOM helpers polyfilled
(`test-support/`); every view has smoke coverage.
See [docs/](docs/) for architecture, CSS-class, and dev-workflow notes.
## License
[MIT](LICENSE)

245
docs/architecture.md Normal file
View file

@ -0,0 +1,245 @@
# Architecture
Reference doc — load when working on the plugin's data model, view classes, modals, or mobile dashboard generation. For CSS class names see [css-classes.md](css-classes.md); for build/test commands see [dev-workflow.md](dev-workflow.md).
## What this is
An Obsidian plugin that opens `.csv` files as a kanban, table, dashboard, library, stats, focus, or travel-map UI. Originally built for book-library and habit-tracking use cases, since generalized to any row/column CSV data. View modes:
- **Dashboard** — date-based habit tracking with chart, streaks, and stats (auto-detected when first column is dates).
- **Library** — grid of cards with filters (status, genre, search), collapsible genre sections, green-dot for done items, ratings, tags.
- **By Genre** — kanban grouped by category column, with status subgroups inside each column.
- **Table** — spreadsheet view with resizable columns; click a header to sort (asc → desc → off, numeric-aware, empties last). Sticky header: in table mode the content area gets `--no-yscroll` and `.csv-table-wrapper` becomes the y-scroller, giving `position: sticky` a scrollport.
- **Travel** — world choropleth + timeline + residency counters for travel-log CSVs. Countries are selectable (map shape / Countries-table row / timeline segment) → detail panel under the map with every trip there; timeline dims other countries while selected. Stats row includes Cities + Longest trip; a "📍 Currently in …" banner shows when today falls inside a confirmed trip (`currentStay` in travel-data, blank `date_left` = ongoing, partial dates excluded). Timeline years carry "Nd · M countries" summaries.
- **Stats** — pure-CSS bar charts (status / category / rating / year / top-author breakdowns); deliberately no Chart.js. Shown whenever the file has a chartable column (`hasStatsColumns`).
- **Focus** — one entry at a time with big typography and prev/random/next nav (←/→ keys); built for quote/dictionary files. Shown for any non-empty file.
Cards/Kanban are available whenever the file has *some* groupable column — per-file "Group by" pick → detected category column → auto-picked fallback (`effectiveGroupCol` in `view/kanban.ts`, scoring via `pickFallbackGroupCol` in `utils.ts`: skips notes/date/title and all-unique or single-value columns, prefers ~8 distinct values). Files like travel logs with no Category column still get Cards/Kanban grouped by e.g. country or city.
The plugin used to also handle `.xlsx` (via SheetJS lazy-loaded chunk + `_csv_helpers/` mirror for Dataview). That whole stack was retired in commit "SWITCH TO CSV AS MAIN" — a round-trip validator proved the migration lossless before cutover.
## FileView (not TextFileView)
Extends `FileView` directly. Historical reason: `TextFileView` decodes everything as UTF-8 before handing off — destroyed binary xlsx data when xlsx was still a registered extension. Now that the plugin is CSV-only, `TextFileView` would technically work, but `FileView` is left in place because the modal/view lifecycle is identical and switching is pure churn.
- **CSV read:** `vault.read()``parseCSV` (Papa wrapper in `src/utils.ts`)
- **CSV save:** `vault.modify()` with `Papa.unparse(...)`
All saves are debounced 600ms via `scheduleSave()``doSave()`. Edits commit to disk automatically — no explicit save button. Read/write failures surface to the user as `Notice`s, not silent `console.error`s.
## Data model
```typescript
interface CSVRow { [key: string]: string; }
// headers: string[] — column order preserved
// rows: CSVRow[] — all values stored as strings
```
Everything in memory. `onLoadFile` populates, `doSave` flushes.
## View modes
`type ViewMode = "kanban-genre" | "table" | "dashboard" | "library" | "travel" | "stats" | "focus"`. Switched via toolbar (only modes valid for the file's columns are shown). Full re-render on each switch.
## Module map (main.ts)
### Top-level
**`showSelectPicker(anchor, currentValue, allValues, onSelect, container, opts?)`** (in `src/utils.ts`)
Floating dropdown for select fields. `opts.multi` (used when `isMultiValueColName(h)` — Category/Genres/Tags/Themes/Topics by name) turns it into a toggle list: items check on/off, every toggle live-commits the comma-joined string via `onSelect`, the picker stays open until Done/Esc/outside-click, and comma-joined data values are split into individual options. Fixed-positioned below anchor. Search/filter, clear, "+ Add new", arrow-key navigation + Enter to commit. On desktop, dismisses on scroll/resize of any ancestor (capture-phase listener) and flips up when there isn't room below. On touch (`matchMedia("(pointer: coarse)")`) those listeners are skipped — focusing the search input on iOS pops the virtual keyboard which fires `resize`, which would dismiss the picker immediately.
### Classes
**`AddEntryModal extends Modal`**
"+ Add" toolbar button. Labeled form for every column. Notes → `<textarea>`, select cols → chip that opens `showSelectPicker`, everything else → `<input type="text">`. Requires at least one field filled.
**`NoteExpanderModal extends Modal`**
Wide modal (~780px) for viewing/editing a full entry. Three sections: header (title), fields bar (all non-notes columns, inline-editable), notes section (rendered markdown by default, click to enter edit). Works on a shallow copy; Cancel discards.
**`FileConfigModal extends Modal`**
"⚙ Columns" toolbar button. Dropdowns for category/status/notes/default-view + checkbox grid for habit-columns and `cardFields`. Saved to `settings.fileConfigs[filePath]`. The Default-view options are passed in from `availableModes(view)` (same source as the toolbar's view dropdown), so the picker offers exactly the modes this file can render; a saved mode the file can no longer render shows as "(no longer available)" so it can be cleared.
**`CardView extends FileView`**
| Method | Purpose |
|---|---|
| `onLoadFile(file)` | Reads CSV via `vault.read` + `parseCSV`, applies per-file default mode, calls `renderView()` |
| `doSave()` | `Papa.unparse``vault.modify` |
| `scheduleSave()` | 600ms debounce wrapper |
| `renderView()` | Clears and rebuilds entire `contentEl` |
| `renderToolbar(root)` | Mode buttons + search + ⚙ Columns + 💾 Backup + 🎴 Anki + + Add (phones collapse secondaries into ⋯) |
| `renderKanbanGenre/Table/Dashboard/Library/Stats/Focus` | The view renderers (free functions in `src/view/`) |
| `renderMarkdownInto(el, text)` | MarkdownRenderer wrapper tied to the view's render Component (used by Focus) |
| `renderKanbanCard(container, row, ...)` | Single kanban card |
| `renderSelectField/makeEditable` | Inline editors |
| `openNoteExpander/openAddModal` | Modal openers |
| `openOrCreateNotes(row)` | Creates/opens sidecar `.md` |
| `resolveCol(candidates)` | First header matching any candidate (case-insensitive) |
| `getNotesCol/getStatusCol/getCategoryCol/getDateCol` | Per-file override → fallback chain |
| `getFilteredRows()` | Rows matching current search query |
| `deleteWithUndo(row)` | Routes every delete path through a 6s Notice with Undo |
| `backupToArchive()` | Copies the current csv to `Archive/<basename>_YYYY-MM-DD.csv` (byte-identical, `readBinary``writeBinary`) |
The class constructor takes a typed `persistSettings: () => Promise<void>` callback. Previously `saveFileCfg()` reached the plugin via `(app as any).plugins.plugins["datadeck"]?.saveSettings()` — brittle and untyped. Now passed explicitly.
**`CardViewSettingTab extends PluginSettingTab`**
Global settings UI.
**`CardViewPlugin extends Plugin`**
Entry point. Registers view for `csv`, registers `csv-add`, `csv-refresh`, and `csv-random` (random entry card with ↻ re-roll, `src/random-block.ts`) code block processors, plus palette commands: `add-entry`, `cycle-view-mode` (→ `CardView.cycleMode`, modes from `availableModes` in `view/toolbar.ts`), and one `create-<id>` per entry in `FILE_TEMPLATES` (scaffold a new CSV with that view's canonical columns, pin its `defaultMode`, open it — `createTemplateFile`). `vault.on("rename")` / `vault.on("delete")` hooks migrate or drop `fileConfigs` keys so per-file config survives file moves (see `migrateFileConfigKey` in `src/utils.ts`).
Key methods:
- `renderAddEntryForm(source, el, ctx)` — renders the `csv-add` code block as a form. Parses `file:`, reads headers, auto-detects select fields (cols with ≤15 unique values), writes new entries directly to the file. For habit-shape files (file has a date col), pre-fills from existing row matching the date input → submit reads as "Update". Title flips to "Updating date" and submit button label flips to "Update" (vs "Add") when the date matches.
## Column auto-detection (fallback chains)
When no per-file override is set, columns are resolved by trying candidates in order against actual headers (case-insensitive). First match wins.
| Role | Candidates |
|---|---|
| Notes | Notes → Note → Summary → Review → Quote → Quotes → Comment → Comments → Description → Annotation |
| Category | Category → Categories → Genre → Genres → Type → Types → Tag → Tags → Topic → Topics → Subject → Section |
| Status | Status → State → Progress → Stage → Read |
| Title | Title → Name |
| Author/subtitle | Author → Authors → Director → Artist → Creator → By |
## Per-file configuration
```typescript
interface FileConfig {
categoryColumn?: string;
notesColumn?: string;
statusColumn?: string;
habitColumns?: string[]; // dashboard habit toggles
cardFields?: string[]; // fields surfaced on Library/Kanban cards
defaultMode?: ViewMode;
sortNewestFirst?: boolean; // table sort toggle
kanbanGroupCol?: string; // kanban "Group by" column (unset = category;
// year-like columns bucket into decades)
librarySort?: LibrarySort; // Cards-view sort: status|title|rating|year
}
```
Stored in `settings.fileConfigs[file.path]`. Accessed via `this.fileCfg` getter. Saved via `saveFileCfg(cfg)`. `undefined` fields fall back to the chain above. Keys follow file renames/deletes via vault hooks (see `migrateFileConfigKey` in `src/utils.ts`, 4 tests in `test-plugin-logic.mjs`).
**Habit columns** — if `habitColumns` is unset, auto-detected by scanning for columns with binary values (0/1, true/false, yes/no, empty). FileConfigModal has a checkbox grid to override.
**cardFields** — if unset, auto-detects author + year + rating + theme. Checkbox grid in FileConfigModal lets the user pick which fields to render on cards.
## Settings (`CardViewSettings`)
| Field | Default | Description |
|---|---|---|
| `defaultMode` | `"kanban-genre"` | Global default opening view |
| `notesColumns` | `["Notes","notes",...]` | Legacy list — superseded by `getNotesCol()` chain but kept for CSV fallback |
| `statusColumn` | `"status"` | Global status column name (overridden per-file) |
| `categoryColumn` | `"category"` | Global category column name (overridden per-file) |
| `notesSubfolder` | `"Notes"` | Subfolder for sidecar `.md` files relative to the data file |
| `columnWidths` | `{}` | Persisted table column widths in px, keyed by header name |
| `selectColumns` | `["Category","Type","Rating","Status",...]` | Columns that use dropdown picker instead of plain text input |
| `fileConfigs` | `{}` | Per-file overrides, keyed by vault-relative file path |
Persisted to `<vault>/.obsidian/plugins/datadeck/data.json`. Travels with the vault on iCloud sync.
## Notes system
### Inline notes (in the xlsx cell)
Any column resolved by `getNotesCol()` is the notes field.
- **Kanban card** — preview (120 chars, markdown stripped) is itself the click-to-edit affordance. Empty state shows hover-revealed "+ Add note". Scroll position saved before opening expander, restored via multi-layer approach (immediate + double rAF + setTimeout). Textarea focus uses `preventScroll: true`.
- **Table** — 2-line preview. Clicking the cell opens `NoteExpanderModal`.
### Sidecar notes file
"✚ Notes file" / "📄 Open notes file" button. Path: `<csv-folder>/<notesSubfolder>/<sanitized-title>.md`. Created with YAML frontmatter from all non-notes columns (serialized via Obsidian's `stringifyYaml` — hand-built `key: "value"` lines broke on backslashes/newlines in cell values) plus a `source` property wikilinking back to the CSV (skipped if a column is named "source"), + inline notes as seed content. Opens in new tab. `notesFileExists(row)` checks via `vault.getAbstractFileByPath()`.
## Dashboard view (date-based files)
Auto-selected when first column is detected as dates (YYYY-MM-DD pattern or named "date"/"day"/"datum").
- **Date navigator** — prev/next buttons, date dropdown, Today button
- **Habit toggles** — clickable ○/● for each habit column
- **Line chart** — habits completed per day (Chart.js, lazy-loaded)
- **Stats bar** — days logged, average per day, perfect days, current streak, best streak
- **Per-habit cards** — individual stats with progress bar; click expands a heatmap timeline
### Streak calculation
Streaks **break if a day is missed**. Algorithm checks consecutive rows are exactly 1 day apart. Current streak starts from today (or yesterday if today has no entry) and counts backwards only while dates are consecutive.
### Per-habit timeline
Heatmap grid: done (green), missed (red), no entry (grey). Month separators. Individual habit streak stats. Click card again or ✕ to close.
## Kanban by Genre
- Genres from splitting Category column on `,` — multi-genre entries appear in multiple columns
- Columns sorted alphabetically; column titles use `var(--text-accent)` to match Library headers
- Within each column, rows grouped by status with colored labels: `In progress` → blue, `Finished/Watched` → green, `Not started/Unwatched` → grey, `dropped` → red
- Status order: In progress → Finished → Not started (then any others)
- Right-click context menu: change status, open/create notes file, delete (uses `deleteWithUndo`)
- On mobile, CSS scroll-snap + `calc(100vw - 60px)` columns so swipe lands cleanly on the next column (desktop columns are 260300px and looked half-cut on phones)
- Single scrollbar per direction — kanban content area gets a `.csv-content-area--no-yscroll` modifier and column bodies fill viewport via `min-height: 0` (the flex-min-content trap), so the page doesn't double-scroll
## Chip truncation
Long non-select field values in kanban cards truncate at 40 chars with `…`. Full text on hover via `title=`. CSS enforces `max-width: 200px; text-overflow: ellipsis` on `.csv-chip-value` as a second layer.
## Mobile dashboard system (retired from the plugin)
The plugin's views are natively mobile-friendly, so the old DataviewJS dashboard generator (a "📱 Mobile" toolbar button that stamped `<folder>/Mobile/<basename>.md` from templates) was removed from the published plugin. The `csv-add` and `csv-refresh` blocks those dashboards used remain fully supported for back-compat, and the generator lives on as local (gitignored) tooling.
Design lesson worth keeping: both the `csv-add` write target and any external read target must point at the **same canonical CSV**. The pre-migration `_csv_helpers/` mirror (kept in sync via three separate code paths) is exactly the bug class to refuse if a future change tempts you toward a "secondary copy" of the data.
### csv-add code block
The plugin registers a `csv-add` markdown code block processor. Renders a labeled form for every column. Auto-detects column types from existing data: cols with ≤15 unique values become dropdowns with a "+ Custom" option. Writes directly to the CSV file. For habit-shape files (date col present), `syncFromExisting()` pre-fills the form when the date input matches an existing row; the card gets an `is-updating` class (subtle accent ring + tinted title) and the submit button label flips to "Update".
```csv-add
file: ../books.csv
```
File-path forms (resolved by `resolvePath` in `src/utils.ts`, 9 tests in `test-plugin-logic.mjs`):
- `books.csv` — sibling of the current note
- `../books.csv` / `../../foo.csv` — walked up from current folder (clamps at vault root)
- `Library/books.csv` — vault-relative
Generated dashboards always emit the `../` form so the data folder is portable — move the folder anywhere in the vault and dashboards still resolve.
### Three gotchas in the library template
- **CSV type coercion.** `dv.io.csv` parses with `dynamicTyping: true`, so a numeric-looking title like the book "1984" comes back as `Number`. Any string-only call (`.localeCompare`, `.toLowerCase`, `.split`) on a raw field must be wrapped in `String(...)`.
- **titleKey fallback.** Quotes/dictionary have no Title/Name column — the library template falls back through `Quote/Headline/Phrase`, then `headers[0]`.
- **Negative status values clutter the kanban.** `NEGATIVE_STATUS` filters out `no/not started/unwatched/unread/todo`. Affirmative finished values (`yes/watched/seen/finished/read`) render as a green dot on the title row, not a "Yes" chip.
## Module layout
```
main.ts # CardView lifecycle + shared helpers (col detection, notes,
# context menu, renderView dispatch, renderViewPreservingScroll,
# getFilteredRows, renderSelectField, backupToArchive, loadMapSvg)
# + CardViewPlugin entry point. ~770 lines.
src/
├── types.ts # Types, DEFAULT_SETTINGS, ResidencyRule, CARD_VIEW_TYPE
├── utils.ts # parseCSV (Papa wrapper), showSelectPicker, sanitizeFilename, titleCase,
│ # formatRatingForDisplay, resolvePath, migrateFileConfigKey, sortRowsByColumn
├── modals.ts # AddEntryModal, NoteExpanderModal, FileConfigModal, SearchModal, makeFieldInput
├── field-types.ts # column-type heuristics for the editor (pure, tested)
├── settings-tab.ts # CardViewSettingTab + residency-rule editor
├── add-entry-form.ts # the csv-add mobile entry form
├── travel-data.ts # analyzeTravel + country reference data (pure, tested)
├── residency.ts # evaluateResidency rule engine (pure, tested)
├── travel-view.ts # travel view renderer
└── view/
├── table.ts library.ts kanban.ts toolbar.ts dashboard.ts mobile.ts
├── stats.ts # bar-chart insights (pure DOM, no Chart.js) + hasStatsColumns/parseRating
├── tasks.ts # native CSV-backed Tasks/projects view (group-by-project)
├── anki.ts # 🎴 Anki toolbar button → AnkiConnect (127.0.0.1:8765) live push; desktop-only
└── focus.ts # one-entry-at-a-time reader (focusIndex/focusNavPending state on CardView)
```
**Renderer pattern.** Each view renderer is a free function `renderX(view, container)`
taking the `CardView` via a type-only import (no runtime cycle); the members they
reach are public on `CardView`. `dashboard.ts` owns the lazy `loadChart` so non-dashboard
sessions never load Chart.js.
**Test net.** `test-view-smoke.mjs` (+ `test-support/`) renders each view into jsdom
with Obsidian's `createEl`/etc. polyfilled onto `Element.prototype`; renderers are
driven by hand-built `view` stubs. `npm run test:view` / `test:all`. Add a renderer →
add a smoke case.
*(Optional future polish: a `ViewContext` interface so renderers depend on an explicit
contract instead of the whole `CardView` type. Not needed — current setup is type-safe.)*

155
docs/css-classes.md Normal file
View file

@ -0,0 +1,155 @@
# CSS class reference
Reference doc — load when editing `styles.css` or tracing a visual bug to its rule. For the component each class belongs to, see [architecture.md](architecture.md).
| Class | Where | Description |
|---|---|---|
| `.datadeck-root` | Root | Sets CSS vars, flex column layout |
| `.csv-toolbar` | Both | Top toolbar; `flex-wrap: wrap` so controls drop to a second row on narrow viewports |
| `.csv-toolbar-controls` | Both | Right-hand group; also wraps |
| `.csv-toolbar-title` | Both | Filename header; hidden on `max-width: 600px` (redundant with tab) |
| `.csv-mode-group` | Both | Wrapper around the view-mode dropdown (hidden while mobile search is expanded) |
| `.csv-mode-select` | Both | View-mode `<select>` (Travel/Dashboard/Cards/Kanban/Table/Focus/Stats, filtered by `availableModes`) |
| `.csv-cfg-btn` | Both | "⚙ Config", "💾 Backup", "🎴 Anki" buttons |
| `.csv-cfg-btn-secondary` | Both | Modifier on the three secondary buttons above — hidden on `max-width: 600px` (collapsed into ⋯ overflow) |
| `.csv-cfg-btn-overflow` | Both | ⋯ button shown only on `max-width: 600px`; click opens an Obsidian Menu with Columns / Mobile / Backup |
| `.csv-add-btn` | Both | "+ Add" button |
| `.csv-row-count` | Toolbar | Entry-count chip; hidden on mobile |
| `.csv-kanban-groupbar` | Kanban | "Group by" selector row above the board; `-label` for the caption span |
| `.csv-kanban-board` | Kanban | Horizontal flex container |
| `.csv-kanban-col` | Kanban | Single genre column; phone-scoped to `calc(100vw - 60px)` with scroll-snap |
| `.csv-kanban-col-header` | Kanban | Column title + count |
| `.csv-kanban-col-title` | Kanban | Uses `var(--text-accent)` to match Library headers |
| `.csv-kanban-status-group` | Kanban | Status subgroup within column |
| `.csv-kanban-status-label` | Kanban | Colored status pill |
| `.csv-kanban-card` | Kanban | Individual entry card |
| `.csv-kanban-card-btns` | Kanban | Button row (visible on hover) |
| `.csv-kanban-notes-preview` | Kanban | Plain-text note excerpt; `--empty` modifier hides it; itself click-to-edit |
| `.csv-kanban-notes-editor` | Kanban | Inline textarea wrapper (toggled via `display`) |
| `.csv-select-chip` | Both | Clickable dropdown chip; `.empty` = no value |
| `.csv-chip-value` | Both | Value span inside chip; `max-width: 200px`, ellipsis |
| `.csv-select-picker` | Both | Floating dropdown panel (fixed-positioned) |
| `.csv-picker-search` | Both | Picker search input |
| `.csv-picker-item` | Both | List item; `.active`/`--hover` = current; `.csv-picker-add` = new value |
| `.csv-table` | Table | Main table element |
| `.csv-table-notes-cell` | Table | Notes cell (relative-positioned for expand btn) |
| `.csv-table-expand-btn` | Table | "⤢" button, shown on row hover |
| `.csv-col-resize-handle` | Table | Drag handle on `<th>` right edge |
| `.csv-cell--clipped` | Table | Added at render time when a cell's content overflows its clamp; shows the bottom-fade |
| `.csv-content-area` | Both | Scroll container for view content |
| `.csv-content-area--no-yscroll` | Kanban | Modifier that hides outer y-scroll (kanban supplies its own) |
| `.csv-search-wrap` | Toolbar | Search bar container (`flex: 1` on mobile, `min-width: 0` so it can shrink) |
| `.csv-search-input` | Toolbar | Search input; desktop `width: 180px` (220px on focus); mobile `width: 100% !important` to defeat the focus expansion |
| `.csv-search-clear` | Toolbar | Clear search button (×) |
| `.csv-search-results` | Content | "Found X of Y entries" message |
| `.csv-add-modal` | Modal | Add entry modal content |
| `.csv-modal-form` | Modal | Scrollable form area |
| `.csv-modal-row` | Modal | Label + input pair |
| `.csv-modal-select` | Modal | `<select>` dropdown in FileConfigModal |
| `.csv-modal-checkbox-grid` | Modal | Habit column / cardFields selector grid |
| `.csv-modal-checkbox-label` | Modal | Checkbox + label; `.auto-detected` highlights auto-picked defaults |
| `.csv-modal-dup-hint` | Modal | Amber "already in this file" hint under the Add modal's title input (non-blocking) |
| `.csv-note-expander-modal` | Modal | Wide modal override (`min(780px, 90vw)`) |
| `.csv-expander-header` | Modal | Title row |
| `.csv-expander-fields` | Modal | Flex-wrap row of label/value field pairs |
| `.csv-expander-field-row` | Modal | Single label + value pair |
| `.csv-expander-field-label` | Modal | Uppercase faint label |
| `.csv-expander-field-value` | Modal | Clickable editable value; truncated with ellipsis |
| `.csv-expander-divider` | Modal | Notes section header (label only — Edit toggle removed; click-to-edit) |
| `.csv-expander-notes-label` | Modal | "Notes" column name label |
| `.csv-expander-rendered` | Modal | Markdown rendered view (itself click-to-edit) |
| `.csv-expander-editor` | Modal | Textarea editor view (toggled via `display`) |
| `.csv-expander-textarea` | Modal | Raw markdown textarea |
| `.csv-expander-footer` | Modal | Cancel + Save & close buttons |
| `.csv-dashboard` | Dashboard | Root container with max-width |
| `.csv-dash-nav` | Dashboard | Date navigator bar |
| `.csv-dash-nav-btn` | Dashboard | Prev/next arrow buttons |
| `.csv-dash-date-select` | Dashboard | Date dropdown |
| `.csv-dash-today-badge` | Dashboard | "Today" indicator pill |
| `.csv-dash-today-btn` | Dashboard | "Today" quick nav button |
| `.csv-dash-habits` | Dashboard | Habit toggles section |
| `.csv-dash-habits-grid` | Dashboard | Grid of habit toggle buttons |
| `.csv-dash-habit` | Dashboard | Single habit toggle; `.checked` variant |
| `.csv-dash-habit-check` | Dashboard | Clickable ○/● indicator |
| `.csv-dash-chart-section` | Dashboard | Chart container |
| `.csv-dash-stats-bar` | Dashboard | Stats summary row |
| `.csv-dash-cards-grid` | Dashboard | Per-habit stat cards grid |
| `.csv-dash-habit-card` | Dashboard | Individual habit stat card |
| `.csv-dash-habit-card-header` | Dashboard | Icon + name row |
| `.csv-dash-habit-icon` | Dashboard | Emoji icon for habit |
| `.csv-dash-habit-years` | Dashboard | Year badges (2024 · 2025 · 2026) |
| `.csv-dash-habit-progress` | Dashboard | Progress bar container |
| `.csv-dash-timeline-section` | Dashboard | Per-habit timeline container |
| `.csv-dash-timeline-grid` | Dashboard | Heatmap grid of day cells |
| `.csv-dash-timeline-cell` | Dashboard | Single day cell; `.done`, `.missed`, `.no-entry` |
| `.csv-dash-timeline-month` | Dashboard | Month label in timeline |
| `.csv-library-filters` | Library | Filters bar (flex, gap) |
| `.csv-library-filter-select` | Library | Status/genre/sort dropdowns (also reused by the kanban group-by select) |
| `.csv-library-search` | Library | Search input |
| `.csv-library-sections` | Library | Container for genre sections |
| `.csv-library-section` | Library | Collapsible `<details>` element |
| `.csv-library-section-header` | Library | Genre header with arrow + count |
| `.csv-library-grid` | Library | Card grid (auto-fill columns) |
| `.csv-library-card` | Library | Individual entry card |
| `.csv-library-card-title` | Library | Title with green dot |
| `.csv-library-done-dot` | Library | Green dot for watched/read |
| `.csv-library-card-meta` | Library | Author/year text |
| `.csv-library-card-rating` | Library | Star rating display |
| `.csv-library-card-tags` | Library | Tags container |
| `.csv-library-card-tag` | Library | Individual tag chip |
| `.csv-add-form` | Code block | Mobile add entry form container |
| `.csv-add-trigger` | Code block | Collapsed pill (re-opens the card) |
| `.csv-add-card` | Code block | Expanded grouped card; `.is-updating` adds blue accent ring + tinted title when the date matches an existing row |
| `.csv-add-card-header` / `-title` / `-close` | Code block | Header bar with × close |
| `.csv-add-rows` | Code block | Grouped row list with hairline separators |
| `.csv-add-row` | Code block | Single label/control row; variants: `-date`, `-toggle`, `-field`, `-custom`, `-notes` |
| `.csv-add-row-label` | Code block | Left-aligned field label |
| `.csv-add-row-control` | Code block | Right-aligned input/select |
| `.csv-add-row-textarea` | Code block | Notes textarea (stacked, full width) |
| `.csv-add-switch` / `-input` / `-track` | Code block | iOS-style switch for binary toggle rows |
| `.csv-add-submit` | Code block | Submit button (label flips between "Add" / "Update") |
| `.csv-add-error` | Code block | Error message styling |
| `.csv-refresh-btn` | Code block | Mobile `↻ refresh` button (subtle, no background) |
| `.csv-stats-wrap` | Stats | Centered column (max 720px) for all stats sections |
| `.csv-stats-overview` / `-chip` / `-chip-value` / `-chip-label` | Stats | Top row of stat tiles (entries, done %, avg rating…) — same visual family as travel-view stat tiles |
| `.csv-stats-section` / `-section-title` | Stats | One bar-chart block + its accent-colored uppercase title |
| `.csv-stats-bar-row` / `-label` / `-track` / `-fill` / `-count` | Stats | Grid bar row; fill modifiers `.is-done` (green), `.is-progress` (blue), `.is-dropped` (red), `.is-rating` (amber), default accent |
| `.csv-focus-wrap` | Focus | Centered column (max 640px), `tabindex=0` so ←/→ keys work; `outline: none` |
| `.csv-focus-card` | Focus | The single entry card |
| `.csv-focus-position` | Focus | "n / N" indicator |
| `.csv-focus-title` | Focus | Big title; `pre-wrap` so quote line breaks survive; `.is-clickable` when an expander is available |
| `.csv-focus-sub` / `-notes` / `-meta` | Focus | Author line, rendered-markdown notes body, chip row (reuses `.csv-kanban-chip`) |
| `.csv-focus-nav` / `-nav-btn` / `-nav-rand` | Focus | Prev / 🔀 / next button row; full-width touch targets on mobile |
| `.csv-th-sortable` | Table | Modifier on every `<th>`; pointer cursor + accent hover |
| `.csv-th-sort-indicator` | Table | ▲/▼ span inside the sorted column's `<th>` |
| `.csv-clear-filters-btn` | Library/Kanban/Focus | "Clear filters"/"Clear search" button in no-results empty states |
| `.csv-tv-now` / `-now-loc` / `-now-sub` | Travel | "📍 Currently in …" banner under the stats (gold-tinted; only when today is inside a confirmed trip) |
| `.cp-selected` | Travel | Accent stroke + brightness on the selected map country; overrides `.cp-tiny`'s transparent halo stroke |
| `.csv-tv-seg.is-dim` | Travel | Timeline segment dimmed to 0.18 opacity while another country is selected |
| `.csv-tv-row-click` / `.is-selected` | Travel | Clickable Countries-table rows; selected row gets the amber tint |
| `.csv-tv-detailwrap` / `.csv-tv-detail` | Travel | Country detail panel slot under the map / the gold-bordered panel card |
| `.csv-tv-detail-head` / `-flag` / `-titles` / `-name` / `-sub` / `-close` | Travel | Panel header: big flag, country name, totals line, ✕ button |
| `.csv-tv-tl-sub` | Travel | Per-year "Nd · M countries" summary next to the timeline year label |
| `.csv-picker-done` | Both | Done button at the bottom of a multi-select picker (raw Obsidian vars — picker mounts on body, outside the root's --csv-* scope) |
| `.csv-random-card` / `-text` / `-sub` / `-foot` / `-src` / `-btn` | Code block | csv-random quote card (also raw Obsidian vars — renders in regular notes) |
| `.csv-stats-bar-row.is-clickable` | Stats | Status/category bars that jump to the filtered library on click |
## Status colors
Status color variants: `.status-{slug}` where slug = value lowercased, spaces → `-`. Palette:
| Value | Color | CSS var |
|---|---|---|
| `finished` / `read` / `watched` / `seen` / `done` | green | `--csv-green`, `--csv-green-bg` |
| `in-progress` / `reading` / `watching` | blue | `--csv-blue`, `--csv-blue-bg` |
| `not-started` / `to-read` / `unwatched` / `unread` / `todo` / `no` | grey | `--background-modifier-border` + `--text-muted` |
| `dropped` | red | `--csv-red`, `--csv-red-bg` |
Theme palette in `:root` (Apple-muted iOS systemGreen/Blue/Red/Orange — confident-but-quiet on both light and dark themes):
```css
--csv-green: #30A14E; --csv-green-bg: rgba(52,199,89,0.13);
--csv-blue: #2E7CE6; --csv-blue-bg: rgba(0,122,255,0.13);
--csv-red: #D5443B; --csv-red-bg: rgba(255,59,48,0.12);
--csv-amber: #C18000; --csv-amber-bg: rgba(255,149,0,0.13);
```

167
docs/dev-workflow.md Normal file
View file

@ -0,0 +1,167 @@
# Dev workflow
Reference doc — load when running the dev loop, adding tests, or debugging build/deploy. For what the code does, see [architecture.md](architecture.md); for the CSS surface, see [css-classes.md](css-classes.md).
## Project structure
```
datadeck/
├── main.ts # CardView, Settings, Plugin
├── main.js # Compiled output — do not edit directly (minified,
│ # post-CSV migration; was much larger with SheetJS)
├── bench-load.mjs # Measures bundle parse/eval cost; run after big refactors
├── src/ # types, utils, modals, and one file per view mode
│ # under src/view/ (table, kanban, library, tasks, chart,
│ # dashboard, focus, stats, toolbar, anki)
├── styles.css # All plugin CSS
├── manifest.json # Obsidian plugin manifest (id: datadeck)
├── package.json # deps: chart.js, papaparse, esbuild, obsidian types
│ # (xlsx/SheetJS removed in "SWITCH TO CSV AS MAIN")
├── esbuild.config.mjs # Build config
├── tsconfig.json # TypeScript config
├── test-csv-parser.mjs # CSV parsing tests
├── test-plugin-logic.mjs # Plugin logic tests — sanitization, parsing, column
│ # resolution, round-trip, edge cases, resolvePath,
│ # migrateFileConfigKey, and more
├── test-view-smoke.mjs # DOM regression harness for view renderers
├── test-support/ # jsdom + Obsidian/Chart.js stubs for the smoke harness
├── migrate-xlsx-to-csv.mjs # One-shot migration script for anyone still on an xlsx-era
│ # install: writes canonical csv, archives xlsx to Archive/,
│ # rewrites fileConfigs keys
├── create-chart-test-data.mjs # Generates a synthetic Chart Test.csv fixture
├── create-world-map.mjs # Regenerates world-map.svg from source map data
├── world-map.svg # Static country-outline asset for the travel view
├── docs/ # Reference docs (architecture, css-classes, dev-workflow)
└── (local/, handoff.md) # gitignored personal dev tooling + session notes —
# not part of the published repo
```
## Live data location
Live data is any folder of CSVs in the vault. `Mobile/` and `Archive/` subfolders are created as needed by the plugin on first click of the respective buttons; `Archive/` also holds `*_pre-csv-migration.xlsx` originals preserved by the migration script and `* sync-conflict *.csv` stashes from the sync guard.
## Build & Deploy
```bash
npm install # Install dependencies
npm run build # Build main.js (minified, ~720 KB)
npm run build:deploy # Build and copy to Obsidian plugin folder
npm run dev # Watch mode (rebuild on changes, unminified, inline sourcemaps)
npm run deploy # Copy already-built files to Obsidian plugin folder
```
Watch mode is unminified with inline sourcemaps so DevTools in Obsidian stays readable during dev. Production builds (`npm run build`, `build:deploy`) minify.
Install in Obsidian: copy `main.js`, `manifest.json`, `styles.css` into `<vault>/.obsidian/plugins/datadeck/` and enable in Community plugins. The `datadeck/` folder in this repo is a symlink to that path, so `npm run deploy` (or `build:deploy`) updates the plugin in place. Reload with `npm run reload` (Obsidian CLI — see below) or Cmd+R in Obsidian.
## Testing
```bash
npm run test # Plugin logic tests (88)
npm run test:csv # CSV parser tests (6)
npm run test:all # All of the above (115 total)
npm run typecheck # TypeScript type checking
npm run check # Full check: typecheck + tests + build + deploy
```
### Test surface
- **CSV parsing** — CRLF, quotes, escaping, long fields, embedded newlines, Unicode, empty input.
- **Column resolution** — case-insensitive matching, fallback chains.
- **Round-trip serialization** — Papa unparse → parseCSV round-trip stability.
- **Title case, binary col detection, date col detection, search filtering.**
- **`findExistingRowByDate` / `mergeHabitEntry` / `sortByDate`** — habit-tracker write paths.
- **`formatRatingForDisplay`** (9) — empty / unrated / glyph-pass-through / numeric / text-mapped / out-of-range / unknown-on-rating-col / unknown-on-other-col.
- **`resolvePath`** (9) — sibling / vault-relative / `../` / `../../` / `./` / mixed / root-clamp.
- **`migrateFileConfigKey`** (4) — rename moves config, delete drops it, no-op for missing keys, caller-set new entry wins.
- **Mobile dashboard simulator** (21) — per-file no-throw + render assertions, movie-specific compact-grid/year/rating/theme checks, generic-table wrap, watched-dot count.
The mobile simulator runs the dataviewjs body against real CSVs via Papa Parse with `dynamicTyping: true`, so any drift between template and rendered output fails the test the same way it would fail in Obsidian.
## Active dev loop
```bash
npm run build:deploy && npm run test:all
npm run reload # hot-reload the plugin in the running Obsidian (no Cmd+R)
npm run errors # surface any runtime errors the reload produced
```
Mobile-dashboard regeneration/verification lives in the gitignored `local/` tooling (`OBSIDIAN_VAULT` env var required).
`npm run check:live` chains the whole thing: typecheck + tests + build + deploy + reload + error check.
## Obsidian CLI (driving the running app)
The `obsidian` CLI (ships with Obsidian ≥1.12, desktop must be running) can drive the live app — this replaces the manual "Cmd+R and click around" step for most verification. Full reference: `obsidian help` or https://help.obsidian.md/cli.
```bash
obsidian plugin:reload id=datadeck # reload after build:deploy (= npm run reload)
obsidian dev:errors # captured plugin errors (= npm run errors)
obsidian dev:console level=error # console output
obsidian dev:screenshot path=shot.png # screenshot the app for visual verification
obsidian dev:dom selector=".csv-card" text # inspect rendered DOM (CSS classes: css-classes.md)
obsidian dev:css selector=".csv-chip-value" prop=max-width
obsidian dev:mobile on # toggle mobile emulation (kanban scroll-snap etc.)
obsidian eval code="app.vault.getFiles().length"
```
Typical verify pass after a change: `npm run check:live`, then `dev:screenshot` / `dev:dom` against an open CSV view instead of eyeballing, and `dev:mobile on` to check the phone layout without a device. Commands target the most recently focused vault; pass `vault="<name>"` first if several are open.
## Bench
`node bench-load.mjs main.js` measures V8 parse cost on the bundle text + parse+top-level eval. 5-run averages. Current baseline:
| Stage | Size | Parse only | Parse+eval |
|---|---|---|---|
| Eager + unminified (historical) | 1338 KB | 2.6 ms | 3.4 ms |
| Lazy + unminified | 1533 KB | 2.0 ms | 0.8 ms |
| Lazy + minified (with SheetJS) | 720 KB | 1.5 ms | 0.9 ms |
| **CSV-only (current, post-migration)** | **297 KB** | **0.7 ms** | **0.5 ms** |
Chart.js is dynamic-imported (only initialises when dashboard renders). SheetJS is gone entirely — the CSV-only migration retired it. Run the bench after any refactor that touches imports/bundling.
## Settings persistence (data.json)
All plugin settings saved to:
```
<vault>/.obsidian/plugins/datadeck/data.json
```
Obsidian writes this automatically via `saveData()` / `loadData()`. Persists across sessions, restarts, and devices (travels with vault on iCloud sync). Example:
```json
{
"defaultMode": "kanban-genre",
"statusColumn": "status",
"categoryColumn": "category",
"notesSubfolder": "Notes",
"columnWidths": { "Name": 240, "Notes": 400 },
"selectColumns": ["Category", "Type", "Rating", "Status"],
"fileConfigs": {
"Library/books.csv": {
"categoryColumn": "Category",
"notesColumn": "Notes",
"statusColumn": "Status",
"defaultMode": "kanban-genre"
}
}
}
```
File-rename/delete hooks migrate `fileConfigs` keys automatically — old `Library/Data/...` entries in the user's live `data.json` may still be orphaned from before that fix; hand-edit if you want them back. The SWITCH TO CSV AS MAIN migration also rewrote `*.xlsx` keys to `*.csv` in-place.
## Manual test checklist
- Open `books.csv` → opens in By Genre view.
- Genre columns render with status subgroups (In progress / Finished / Not started).
- Hover card → buttons appear; click the note preview → textarea opens inline; blur saves and scroll position restored.
- Click "⤢ Expand" → NoteExpanderModal opens; fields bar shows all columns; text fields click-to-edit; select chips open picker; click rendered notes to edit; Cancel discards; Save & close commits.
- Click "+ Add" → AddEntryModal opens; all fields present; select chips work; submit adds row, notice shown.
- Click "⚙ Columns" → FileConfigModal opens; dropdowns show all headers; checkbox grid for habit cols + cardFields; save persists to data.json.
- Long chip values truncate with `…`; full text shown on hover.
- Switch to Table view; resize columns; click notes cell → NoteExpanderModal opens (no inline editing).
- Right-click kanban / library / table row → change status, delete (with undo).
- Delete a row → 6s Notice with Undo button → click Undo, row restored to original index.
- Click "💾 Backup" → `Archive/<basename>_<date>.csv` appears (byte-identical to source); same-day repeat refused.
- Verify csv saved correctly by reopening or checking mtime.

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "datadeck",
"name": "DataDeck",
"version": "1.6.0",
"minAppVersion": "1.4.0",
"description": "View and edit CSV files as cards, kanban, table, charts, a habit dashboard, a tasks board, a focus reader, or an interactive travel map. Charts with best-fit lines, formulas, and color-by splits. Notes columns render as Markdown.",
"author": "SVM0N",
"authorUrl": "https://github.com/SVM0N",
"isDesktopOnly": false
}

1167
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "datadeck",
"version": "1.6.0",
"description": "DataDeck: XLSX/CSV multi-view plugin for Obsidian",
"main": "main.js",
"scripts": {
"build": "node esbuild.config.mjs",
"dev": "node esbuild.config.mjs --watch",
"deploy": "cp main.js styles.css manifest.json world-map.svg datadeck/ && cp main.js styles.css manifest.json world-map.svg datadeck-work/",
"build:deploy": "npm run build && npm run deploy",
"test": "node test-plugin-logic.mjs",
"test:csv": "node test-csv-parser.mjs",
"test:view": "node test-view-smoke.mjs",
"test:all": "node test-csv-parser.mjs && node test-plugin-logic.mjs && node test-view-smoke.mjs",
"typecheck": "tsc --noEmit",
"check": "npm run typecheck && npm run test:all && npm run build:deploy",
"reload": "obsidian plugin:reload id=datadeck",
"errors": "obsidian dev:errors"
},
"devDependencies": {
"@types/node": "^18.0.0",
"esbuild": "^0.19.0",
"jsdom": "^29.1.1",
"obsidian": "latest",
"typescript": "^5.0.0"
},
"dependencies": {
"@types/papaparse": "^5.5.2",
"chart.js": "^4.5.1",
"papaparse": "^5.5.3"
}
}

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"allowJs": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"declaration": false,
"noEmit": true
},
"include": ["main.ts"]
}

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.6.0": "1.4.0"
}