Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# CLAUDE.md

## Project Overview

**Zero-State** is a Chrome Extension (Manifest V3) that replaces the New Tab page with a customizable link and notes organizer. Users can create hierarchical lists of links and notes, organized as trees. Built with TypeScript and VanJS.

- **Chrome Web Store**: [zero-state](https://chromewebstore.google.com/detail/zero-state/diloncejoolaamlldicgmhimamhecipj)
- **Repository**: https://github.com/nmai/zero-state

## Build & Development Commands

```bash
npm run build # Bundle with esbuild → dist/app.js (with sourcemaps)
npm run build-min # Bundle minified → dist/app.js
npm run watch # Watch mode for development
npm run pkg # Copy manifest, static assets, and dist into pkg/ for publishing
```

There is no test suite, linter, or CI/CD pipeline configured.

## Architecture

### Tech Stack

- **TypeScript** 5.8 with strict mode, targeting ES2019
- **VanJS** 1.5.3 — ultralight (~5kb) reactive UI framework
- **esbuild** — bundler
- **Chrome APIs** — `chrome.storage.sync`, `chrome.permissions`, `chrome.runtime`

### Source Layout

```
ts/
├── app.ts # Entry point: initialization, keyboard shortcuts, top-level render
├── core/
│ ├── types.ts # Interfaces (LinkNodeFlat, LinkNode, Settings) and FaviconProvider enum
│ ├── constants.ts # Storage keys, DOM classes, text icons, SVG loader, default settings
│ └── van.ts # VanJS re-exports (dependency isolation)
├── state/
│ ├── ui.state.ts # UI-only state (editMode, settingsMode, editingNode, footerMessages)
│ └── data.state.ts # Persistent data state (rawList, names, root tree, settings)
├── services/
│ ├── command.service.ts # Orchestrates state mutations + persistence (the main action layer)
│ ├── storage.service.ts # Chrome storage sync API wrapper (load/save)
│ ├── tree.service.ts # Builds tree structure from flat node array
│ ├── favicon.service.ts # Favicon provider management and caching
│ ├── theme.service.ts # Theme application (light/dark/system)
│ └── validator.service.ts # URL validation
└── components/
├── tree.component.ts # Tree rendering, node content, move/delete buttons
├── edit-form.component.ts # Add/edit form with validation
├── settings.component.ts # Settings modal
└── footer.component.ts # Footer with messaging/prompts
```

### Static Assets

```
static/
├── index.html # Single-page entry point
├── css/
│ ├── variables.css # Theme CSS custom properties (light/dark)
│ ├── base.css # Body, links, utility classes
│ ├── layout.css # Flexbox layout (row, col, main-content)
│ ├── buttons.css # Toggle, move, edit, close buttons
│ ├── tree.css # Tree lines, node styles, favicons, editable highlights
│ ├── modal.css # Settings modal and overlay
│ ├── forms.css # Edit form inputs, selects, actions
│ └── footer.css # Fixed footer bar
├── icons/
│ ├── svg/ # SVG icon files (edit, close, settings, link)
│ └── *.png # Extension icons at various sizes
└── json/
└── initial-data-2.0.0.json # Default data for new installs
```

### Data Flow

```
Chrome Storage ↔ StorageService ↔ CommandService ↔ DataState/UIState ↔ Components ↔ DOM
```

- Data is stored as a **flat array** of `LinkNodeFlat` objects with parent references
- `TreeService.buildTree()` converts flat data into a nested `LinkNode` tree for rendering
- `CommandService` is the single orchestration layer — components call it instead of directly touching storage or rebuilding the tree
- `UIState` holds transient UI state (edit mode, modals); `DataState` holds persistent data (list, settings)
- Changes sync across devices via `chrome.storage.sync`

### Chrome Storage Constraints

See `notes.md` for details. Key limits:
- 512 items max
- 100kb total across all keys
- 8kb per key
- All node data is stored under a single key (`links-v1`)

**Important**: The `FaviconProvider` enum string values (`'chrome'`, `'duck'`, `'gen'`, `'none'`) are persisted in storage. Never change these values — it would break settings for existing users.

## Code Conventions

### Naming

- **PascalCase** for classes: `StorageService`, `EditForm`, `TreeComponent`
- **camelCase** for methods and variables
- **UPPER_SNAKE_CASE** for constants: `CURRENT_LIST_VERSION`, `DOM_CLASSES`

### File Naming

- `.service.ts` — services (data/logic/orchestration layer)
- `.component.ts` — UI components
- `.state.ts` — state management

### Patterns

- Service classes use **static methods** (no instantiation)
- Components call `CommandService` for any action that mutates state + persists (never call `StorageService` directly from a component)
- Immutable state updates (spread operator for arrays/objects)
- `nameToIndexMap` cache in `DataState` for O(1) lookups by item name
- Optimistic updates with revert-on-failure pattern in `CommandService`
- SVG icons are loaded from files at init time via `loadSvgIcons()`, stored in `SVG_ICONS` map

### CSS

- CSS custom properties for theming (defined in `variables.css`)
- `light-dark()` CSS function with `prefers-color-scheme` media query
- Lowercase hyphenated class names (`tree-list`, `edit-node-btn`)
- One CSS file per concern (variables, layout, buttons, tree, modal, forms, footer)

## Key Features in Code

- **Tree visualization**: hierarchical lists with parent-child tree lines
- **Edit mode**: toggled via `[+]` button or backtick key
- **Task completion**: right-click to strikethrough (optional setting)
- **Favicon support**: multiple providers (Chrome cache, DuckDuckGo, generic icon)
- **Theme support**: light, dark, or system preference
- **Keyboard shortcuts**: Escape (close/exit), backtick (toggle edit mode)
- **Cross-device sync**: via Chrome storage sync API when user is signed in
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions static/css/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* ===== BASE STYLES ===== */
body {
color: var(--text-color);
background-color: var(--background-color);
font-size: 14px;
margin-left: 0;
margin-right: 0;
}

a {
text-decoration: none;
color: var(--link-color);
}

/* ===== UTILITY CLASSES ===== */
.display-none {
display: none;
}
91 changes: 91 additions & 0 deletions static/css/buttons.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* ===== TOGGLE & SETTINGS BUTTONS ===== */
#toggle-form-btn {
float: right;
}

#toggle-form-btn:hover, #settings-btn:hover {
color: var(--primary-button-hover-color);
transform: translateY(-1px);
}

#toggle-form-btn:active, #settings-btn:active {
transform: translateY(0);
}

/* ===== MOVE BUTTONS ===== */
.move-controls {
display: inline-block;
margin-right: 5px;
vertical-align: middle;
}

.move-btn {
display: block;
text-align: center;
width: 16px;
height: 16px;
line-height: 14px;
font-size: 12px;
margin: 1px 0;
border: 1px solid var(--border-color);
border-radius: 2px;
background-color: var(--secondary-bg-color);
}

.move-btn:hover {
background-color: var(--hover-bg-color);
}

.move-up {
margin-bottom: 0;
}

.move-down {
margin-top: 0;
}

/* ===== NODE EDIT BUTTON ===== */
.edit-node-btn {
margin-left: 8px;
display: inline-flex;
align-items: center;
opacity: 0.7;
transition: opacity 0.2s, transform 0.1s;
}

.edit-node-btn:hover {
opacity: 1;
transform: translateY(-1px);
}

/* ===== CLOSE BUTTONS ===== */
.close-settings-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: var(--secondary-bg-color);
transition: background-color 0.2s;
}

.close-settings-btn:hover {
background-color: var(--hover-bg-color);
}

.close-form-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: var(--hover-bg-color);
transition: background-color 0.2s, transform 0.1s;
}

.close-form-btn:hover {
background-color: var(--border-color);
transform: translateY(-1px);
}
Loading