218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
# 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 (0–11) 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`.
|