thornhill-tv/AGENTS.md
2026-07-23 19:58:22 +02:00

218 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md — Thornhill TV
Guidance for AI agents and contributors working in this repository.
## What this project is
**Thornhill TV** is a desktop Electron app with a Vue 3 frontend. It presents a scrollable “space” of feed tiles and a year/month timeline overlay, controlled primarily by the keyboard. The UI is dark-themed, full-viewport, and framed as a TV-like surface rather than a conventional document app.
At a high level:
- **Electron** owns the window, native theme, optional CORS bypass, and packaging.
- **Vue 3** owns the interactive UI (space grid + timeline).
- **Vite** serves the UI in development and bundles it for production.
- **electron-builder** produces platform installers (AppImage / NSIS / DMG).
Package metadata: `thornhill-tv` `0.1.0`, MIT, entry `start.js`.
## Architecture overview
```
┌─────────────────────────────────────────────────────────────┐
│ Node process (start.js dual role) │
│ │
│ yarn start → start.js as Node │
│ ├─ Vite dev server (src-vue, random free port) │
│ └─ spawn(electron, start.js) with PORT env │
│ │
│ start.js as Electron main │
│ ├─ BrowserWindow + preload (src-electron/preload.js) │
│ ├─ config.js → window chrome / CORS / title │
│ └─ loadURL(localhost:PORT) | loadFile(dist/index.html) │
└────────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Renderer (Vue 3) │
│ App.vue │
│ ├─ Space.vue → grid of Feed.vue tiles (scrollable) │
│ └─ Timeline.vue → year / month navigator (Tab toggle) │
└─────────────────────────────────────────────────────────────┘
```
### Dual-entry bootstrap (`start.js`)
`start.js` is both the npm `main` and the Electron entry. Behavior depends on what `require('electron')` returns:
| Context | `require('electron')` | Behavior |
| --- | --- | --- |
| Plain Node (`yarn start`) | Path string to the Electron binary | Start Vite on an ephemeral port, spawn Electron with `PORT` set, exit when the child exits |
| Electron main process | Electron API object | Create the window, apply config, load the UI |
This avoids a separate “main” vs “launcher” file.
### Dev vs packaged load path
- **Development:** renderer loads `http://localhost:${PORT}/` from Vite. Vue DevTools (`nhdogjmejiglipccpnnnanhbledajbpd`) is installed when unpackaged. Detached DevTools open and stay active.
- **Packaged:** renderer loads `./dist/index.html`. DevTools are closed after open.
Native theme is forced to dark (`electron.nativeTheme.themeSource = 'dark'`).
### Build pipeline (`build.mjs`)
1. Delete `./dist` and `./build` if present.
2. Vite build of `src-vue``../dist` with `base: './'` (file://-safe relative assets).
3. `electron-builder` for the **current host platform only**:
- Linux → AppImage (x64)
- Windows → NSIS (x64)
- macOS → DMG
4. Output under `./build`; build resources from `src-electron/assets`.
5. Packaged file set is derived from the repo root listing, excluding `.git`, tooling files, and most `.gitignore` entries (but keeping `dist`, `node_modules`, and `config.js` in the packaged set when present).
### Configuration (`config.js`)
Runtime config is **required** as `./config.js` (gitignored). Copy from `config.example.js`:
| Key | Type | Role |
| --- | --- | --- |
| `isSpellcheckEnabled` | boolean | BrowserWindow spellcheck |
| `isTitleBarEnabled` | boolean | Native frame / title bar (`frame`) |
| `isMenuBarEnabled` | boolean | Menu bar visibility |
| `isCorsEnabled` | boolean | When `false`, strips real CORS headers and injects `*` allow-origin/methods/headers on all responses (useful for embedding remote media in feeds) |
| `title` | string | Window / HTML title (falls back to package name) |
Agents changing window behavior should update `config.example.js` and document new keys here; never commit secrets in `config.js`.
### Preload (`src-electron/preload.js`)
Currently a stub (`console.log`). The preload path is wired in `BrowserWindow` `webPreferences.preload`. Prefer exposing a narrow `contextBridge` API here if the renderer needs Node/Electron capabilities; do not enable `nodeIntegration` unless explicitly required.
## Directory map
```
thornhill-tv/
├── AGENTS.md # This file
├── package.json # Scripts, deps, Electron main = start.js
├── yarn.lock
├── start.js # Dev launcher + Electron main
├── build.mjs # Vite production build + electron-builder
├── config.example.js # Template for config.js
├── config.js # Local overrides (gitignored)
├── src-electron/
│ ├── preload.js # Preload script for the renderer
│ └── assets/
│ └── icon.png # App / builder icon
├── src-vue/
│ ├── index.html # Shell: destyle.css, dark color-scheme, Apple font
│ ├── main.js # createApp(App) → .App
│ ├── static/fonts/
│ │ └── Apple.woff2
│ └── components/
│ ├── App.vue # Layout + Tab focus switch
│ ├── Space.vue # Feed grid + arrow-key pan
│ ├── Timeline.vue # Year/month bar
│ └── Space/
│ └── Feed.vue # Single feed tile (placeholder color)
├── dist/ # Vite output (gitignored)
└── build/ # electron-builder output (gitignored)
```
## UI features and interaction model
### Layout (`App.vue`)
- Full viewport CSS grid: `1fr` main area + `125px` bottom strip for the timeline.
- Two siblings: `Space` and `Timeline`.
- **Tab** (on keyup): toggles `isTimelineActive`, focuses either Timeline or Space. Tab keydown is always prevented (no browser focus cycle).
- Timeline receives `:is-active` to animate open/closed.
### Space — feed mosaic (`Space.vue`)
- Renders `feedCount` (currently **100**) `Feed` children in a CSS grid.
- Grid dimensions: `columnCount = floor(sqrt(feedCount))`, `rowCount = ceil(feedCount / columnCount)` so the mosaic stays roughly square.
- Tile size via CSS variables: `--feed-width: 400px`, `--feed-height: 300px`, `--feed-gap: 50px`. Outer `Space` overflows hidden; the inner `Space__Feeds` is larger and is panned by scrolling the host.
- **Arrow keys** while Space is focused: continuous pan via `requestAnimationFrame` loops (`SCROLL_STEP = 25` px per frame). Separate flags for X/Y so diagonal motion works. Keyup clears the corresponding axis flag.
- Host is `tabindex="1"` so it can receive keyboard focus.
### Feed tile (`Space/Feed.vue`)
- Placeholder: random hex background color per instance.
- Intended extension point for real video / stream / channel content.
### Timeline (`Timeline.vue`)
- Collapsed by default (`max-height: 0`); when `Timeline--active`, expands with a 0.25s transition over a semi-transparent black bar.
- Years: current year 5 through current year + 1 (seven slots). Default `activeYear` index is `5` (current year).
- Months: single-letter labels `J F M A M J J A S O N D`. Month row appears only when `activeMonth` is a number.
- Keyboard (on keyup, when Timeline focused):
- **Up:** leave month mode (`activeMonth = undefined`).
- **Down:** enter month mode (current calendar month if active year is current; otherwise month `0`).
- **Left / Right:** move month (011) or year index within bounds.
- Active year/month use opacity (0.5 inactive, 1 active). Uses the custom `Apple` font face.
### Visual / CSS conventions
- Global reset: `destyle.css`.
- `color-scheme: dark`; `:focus-visible { outline: none }` (keyboard focus is app-managed).
- Prefer local `<style>` in SFCs; layout uses CSS grid and custom properties rather than a CSS framework.
- Do not introduce Inter/Roboto/system defaults for branded chrome; the project already ships `Apple.woff2` for timeline type.
## Scripts and workflow
| Command | Purpose |
| --- | --- |
| `yarn` / `yarn install` | Install dependencies |
| `yarn start` | Vite + Electron (development) |
| `yarn build` | Production Vite build + electron-builder for this OS |
Prerequisites for agents:
1. Ensure `config.js` exists (`cp config.example.js config.js`) before `start` or `build`.
2. Use **Yarn** (lockfile is `yarn.lock`); do not switch package managers without an explicit request.
3. Do not commit `config.js`, `dist/`, `build/`, or `node_modules/`.
## Dependency roles
| Package | Role |
| --- | --- |
| `electron` | Desktop shell |
| `electron-builder` | Installers |
| `electron-devtools-installer` | Vue DevTools in unpackaged runs |
| `vite`, `@vitejs/plugin-vue`, `vite-plugin-html` | Dev server + HTML title injection + production bundle |
| `vue` | UI framework (Options API via `defineComponent`) |
| `destyle.css` | CSS reset |
| `dayjs` | Declared dependency; not yet used in source — prefer it for any new date logic to match the package intent |
## Conventions for code changes
### Prefer existing patterns
- Vue components use **Options API** (`defineComponent`, `data`, `methods`, `mounted` / `beforeUnmount`). Match that style unless migrating the whole tree.
- Keyboard handling is done with DOM listeners on the component root (or `window` in `App`), not Vue `@keydown` templates — keep that consistent for focus-scoped controls.
- Event listener cleanup belongs in `beforeUnmount`.
- Keep Electron concerns in `start.js` / `src-electron`; keep UI in `src-vue`.
### Safe extension points
- **Real feeds:** flesh out `Feed.vue` (and possibly props from `Space.vue`) without changing the grid math unless product requirements demand it.
- **Timeline ↔ Space coupling:** today Timeline selection does not filter feeds; wiring year/month to feed data is a natural next feature.
- **IPC / APIs:** add through `preload.js` + `contextBridge`, then consume from Vue.
- **Config toggles:** extend `config.example.js` and destructure in `start.js`.
### Avoid unless asked
- Enabling `nodeIntegration` or disabling `contextIsolation`.
- Committing generated `dist/` / `build/` artifacts.
- Cross-compiling all platforms in one build (current builder only targets `process.platform`).
- Rewriting the dual-role `start.js` without a clear migration plan — Vite port discovery and Electron spawn are tightly coupled.
### Testing / verification
There is no automated test suite. After UI or Electron changes:
1. `yarn start` and verify Tab focus switching, Space arrow panning, Timeline year/month navigation.
2. For packaging changes, `yarn build` on the relevant OS and smoke-test the artifact under `build/`.
## Mental model for agents
Treat Thornhill TV as a **keyboard-first media wall**: a large pannable grid of feeds plus a temporal navigator. Changes should preserve full-viewport immersion, dark theme, and arrow/Tab navigation. Electron is a thin host; almost all product behavior lives under `src-vue/components`.