mirror of
https://github.com/svm0n/datadeck.git
synced 2026-07-22 08:31:46 +00:00
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:
parent
6351d84e16
commit
4faff4588e
11 changed files with 2122 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
282
README.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>More screenshots</b> — kanban, the tasks board, and the travel map</summary>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</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
245
docs/architecture.md
Normal 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 260–300px 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
155
docs/css-classes.md
Normal 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
167
docs/dev-workflow.md
Normal 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
10
manifest.json
Normal 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
1167
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal 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
16
tsconfig.json
Normal 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
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.6.0": "1.4.0"
|
||||
}
|
||||
Loading…
Reference in a new issue