fix: add missing libraryService.js and fix two bugs
- Create missing src/services/libraryService.js (was imported but never created) - Fix readJSONFile import in tagIndexer.js (unused import) - Fix found.found?.libId typo in deleteDocument (double .found lookup) These were blocking the application from starting.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
* Rebuild and query the global tag index
|
* Rebuild and query the global tag index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readJSON, writeJSON, pathExists, listDir, readJSONFile } from '../utils/fsHelper.js';
|
import { readJSON, writeJSON, pathExists, listDir } from '../utils/fsHelper.js';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import config from '../config/index.js';
|
import config from '../config/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,6 @@ export class DocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const meta = readJSON(found.metaPath);
|
const meta = readJSON(found.metaPath);
|
||||||
deletePath(found.found?.libId ? join(this.librariesPath, found.libId, 'documents', docId) : null);
|
|
||||||
deletePath(this._docPath(found.libId, docId));
|
deletePath(this._docPath(found.libId, docId));
|
||||||
|
|
||||||
this.tagIndexer.removeDocument(docId);
|
this.tagIndexer.removeDocument(docId);
|
||||||
|
|||||||
273
src/services/libraryService.js
Normal file
273
src/services/libraryService.js
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
/**
|
||||||
|
* SimpleNote Web - Library Service
|
||||||
|
* Library CRUD with filesystem storage
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import config from '../config/index.js';
|
||||||
|
import { ensureDir, readJSON, writeJSON, pathExists, deletePath, listDir, isDirectory } from '../utils/fsHelper.js';
|
||||||
|
import { generateId } from '../utils/uuid.js';
|
||||||
|
import { NotFoundError, ValidationError } from '../utils/errors.js';
|
||||||
|
|
||||||
|
const LIBRARIES_DIR = 'libraries';
|
||||||
|
const LIBRARY_META_FILE = '.library.json';
|
||||||
|
|
||||||
|
export class LibraryService {
|
||||||
|
constructor(dataRoot = config.dataRoot) {
|
||||||
|
this.dataRoot = dataRoot;
|
||||||
|
this.librariesPath = join(dataRoot, LIBRARIES_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
_libPath(libId) {
|
||||||
|
return join(this.librariesPath, libId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_libMetaPath(libId) {
|
||||||
|
return join(this._libPath(libId), LIBRARY_META_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
_libDocumentsPath(libId) {
|
||||||
|
return join(this._libPath(libId), 'documents');
|
||||||
|
}
|
||||||
|
|
||||||
|
_libSubLibrariesPath(libId) {
|
||||||
|
return join(this._libPath(libId), 'sub-libraries');
|
||||||
|
}
|
||||||
|
|
||||||
|
_readLibMeta(libId) {
|
||||||
|
const meta = readJSON(this._libMetaPath(libId));
|
||||||
|
if (!meta) throw new NotFoundError('Library');
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findLibraryById(libId) {
|
||||||
|
const metaPath = this._libMetaPath(libId);
|
||||||
|
if (pathExists(metaPath)) {
|
||||||
|
return { id: libId, metaPath };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLibrary({ name, parentId = null }) {
|
||||||
|
if (!name || !name.trim()) {
|
||||||
|
throw new ValidationError('Library name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const libId = generateId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
// Verify parent exists
|
||||||
|
const parentMeta = this._readLibMeta(parentId);
|
||||||
|
const parentSubLibsPath = this._libSubLibrariesPath(parentId);
|
||||||
|
ensureDir(parentSubLibsPath);
|
||||||
|
|
||||||
|
const libMeta = {
|
||||||
|
id: libId,
|
||||||
|
name: name.trim(),
|
||||||
|
parentId,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const libPath = join(parentSubLibsPath, libId);
|
||||||
|
ensureDir(libPath);
|
||||||
|
ensureDir(join(libPath, 'documents'));
|
||||||
|
writeJSON(this._libMetaPath(libId), libMeta);
|
||||||
|
return libMeta;
|
||||||
|
} else {
|
||||||
|
ensureDir(this.librariesPath);
|
||||||
|
|
||||||
|
const libMeta = {
|
||||||
|
id: libId,
|
||||||
|
name: name.trim(),
|
||||||
|
parentId: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const libPath = this._libPath(libId);
|
||||||
|
ensureDir(libPath);
|
||||||
|
ensureDir(join(libPath, 'documents'));
|
||||||
|
writeJSON(this._libMetaPath(libId), libMeta);
|
||||||
|
return libMeta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRootLibraries() {
|
||||||
|
ensureDir(this.librariesPath);
|
||||||
|
|
||||||
|
const entries = listDir(this.librariesPath);
|
||||||
|
const libraries = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const libPath = join(this.librariesPath, entry);
|
||||||
|
if (!isDirectory(libPath)) continue;
|
||||||
|
|
||||||
|
const meta = readJSON(this._libMetaPath(entry));
|
||||||
|
if (!meta) continue;
|
||||||
|
|
||||||
|
const docCount = this._countDocuments(this._libDocumentsPath(entry));
|
||||||
|
libraries.push({
|
||||||
|
...meta,
|
||||||
|
documentCount: docCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return libraries;
|
||||||
|
}
|
||||||
|
|
||||||
|
_countDocuments(docsPath) {
|
||||||
|
if (!pathExists(docsPath)) return 0;
|
||||||
|
const entries = listDir(docsPath);
|
||||||
|
return entries.filter(e => {
|
||||||
|
const metaPath = join(docsPath, e, '.meta.json');
|
||||||
|
return pathExists(metaPath);
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLibrary(libId) {
|
||||||
|
const found = this._findLibraryById(libId);
|
||||||
|
if (!found) {
|
||||||
|
throw new NotFoundError('Library');
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = readJSON(this._libMetaPath(libId));
|
||||||
|
const docsPath = this._libDocumentsPath(libId);
|
||||||
|
const subLibsPath = this._libSubLibrariesPath(libId);
|
||||||
|
|
||||||
|
// List documents
|
||||||
|
const documents = [];
|
||||||
|
if (pathExists(docsPath)) {
|
||||||
|
const docEntries = listDir(docsPath);
|
||||||
|
for (const docEntry of docEntries) {
|
||||||
|
const docMetaPath = join(docsPath, docEntry, '.meta.json');
|
||||||
|
if (pathExists(docMetaPath)) {
|
||||||
|
const docMeta = readJSON(docMetaPath);
|
||||||
|
if (docMeta?.id) {
|
||||||
|
documents.push({
|
||||||
|
id: docMeta.id,
|
||||||
|
title: docMeta.title,
|
||||||
|
type: docMeta.type,
|
||||||
|
status: docMeta.status,
|
||||||
|
tags: docMeta.tags || [],
|
||||||
|
updatedAt: docMeta.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List sub-libraries
|
||||||
|
const subLibraries = [];
|
||||||
|
if (pathExists(subLibsPath)) {
|
||||||
|
const subEntries = listDir(subLibsPath);
|
||||||
|
for (const subEntry of subEntries) {
|
||||||
|
const subMetaPath = join(subLibsPath, subEntry, LIBRARY_META_FILE);
|
||||||
|
if (pathExists(subMetaPath)) {
|
||||||
|
const subMeta = readJSON(subMetaPath);
|
||||||
|
if (subMeta?.id) {
|
||||||
|
const subDocCount = this._countDocuments(join(subLibsPath, subEntry, 'documents'));
|
||||||
|
subLibraries.push({
|
||||||
|
id: subMeta.id,
|
||||||
|
name: subMeta.name,
|
||||||
|
documentCount: subDocCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
library: meta,
|
||||||
|
documents,
|
||||||
|
subLibraries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLibraryTree(libId) {
|
||||||
|
const found = this._findLibraryById(libId);
|
||||||
|
if (!found) {
|
||||||
|
throw new NotFoundError('Library');
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = readJSON(this._libMetaPath(libId));
|
||||||
|
|
||||||
|
const buildTree = (id) => {
|
||||||
|
const libMeta = readJSON(this._libMetaPath(id));
|
||||||
|
const docsPath = this._libDocumentsPath(id);
|
||||||
|
const subLibsPath = this._libSubLibrariesPath(id);
|
||||||
|
|
||||||
|
const documents = [];
|
||||||
|
if (pathExists(docsPath)) {
|
||||||
|
const docEntries = listDir(docsPath);
|
||||||
|
for (const docEntry of docEntries) {
|
||||||
|
const docMetaPath = join(docsPath, docEntry, '.meta.json');
|
||||||
|
if (pathExists(docMetaPath)) {
|
||||||
|
const docMeta = readJSON(docMetaPath);
|
||||||
|
if (docMeta?.id) {
|
||||||
|
documents.push({
|
||||||
|
id: docMeta.id,
|
||||||
|
title: docMeta.title,
|
||||||
|
type: docMeta.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subLibraries = [];
|
||||||
|
if (pathExists(subLibsPath)) {
|
||||||
|
const subEntries = listDir(subLibsPath);
|
||||||
|
for (const subEntry of subEntries) {
|
||||||
|
const subMetaPath = join(subLibsPath, subEntry, LIBRARY_META_FILE);
|
||||||
|
if (pathExists(subMetaPath)) {
|
||||||
|
subLibraries.push(buildTree(subEntry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: libMeta.id,
|
||||||
|
name: libMeta.name,
|
||||||
|
documents,
|
||||||
|
subLibraries,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return buildTree(libId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDocumentsInLibrary(libId) {
|
||||||
|
const found = this._findLibraryById(libId);
|
||||||
|
if (!found) {
|
||||||
|
throw new NotFoundError('Library');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.getLibrary(libId);
|
||||||
|
return { documents: result.documents, total: result.documents.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLibrary(libId) {
|
||||||
|
const found = this._findLibraryById(libId);
|
||||||
|
if (!found) {
|
||||||
|
throw new NotFoundError('Library');
|
||||||
|
}
|
||||||
|
|
||||||
|
const libPath = this._libPath(libId);
|
||||||
|
deletePath(libPath);
|
||||||
|
|
||||||
|
return { deleted: true, id: libId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let globalLibraryService = null;
|
||||||
|
|
||||||
|
export function getLibraryService(dataRoot = config.dataRoot) {
|
||||||
|
if (!globalLibraryService) {
|
||||||
|
globalLibraryService = new LibraryService(dataRoot);
|
||||||
|
}
|
||||||
|
return globalLibraryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LibraryService;
|
||||||
309
ui/SPEC.md
Normal file
309
ui/SPEC.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# SimpleNote Web - UI/UX Specification
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
### 1.1 Purpose
|
||||||
|
SimpleNote Web is the frontend interface for the SimpleNote document management system. It provides a clean, efficient way to browse, create, edit, and manage documents organized in nested libraries with tag-based filtering.
|
||||||
|
|
||||||
|
### 1.2 Target Users
|
||||||
|
- Technical users and agents creating/managing documentation
|
||||||
|
- Teams using CLI tools to push documents that need visual review
|
||||||
|
- Anyone preferring a Joplin-like experience with a modern dark-mode interface
|
||||||
|
|
||||||
|
### 1.3 Tech Stack
|
||||||
|
- **Framework**: Vanilla JS with modern ES modules (no framework dependency for simplicity)
|
||||||
|
- **Styling**: CSS custom properties for theming (dark/light modes)
|
||||||
|
- **Markdown**: `marked` library for rendering
|
||||||
|
- **Icons**: Lucide Icons (SVG-based, MIT licensed)
|
||||||
|
- **HTTP Client**: Native Fetch API
|
||||||
|
- **State**: Simple pub/sub pattern with localStorage for preferences
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Design Principles
|
||||||
|
|
||||||
|
### 2.1 Core Principles
|
||||||
|
1. **Content-First**: Documents are the hero; UI stays out of the way
|
||||||
|
2. **Keyboard-Friendly**: Common actions accessible via shortcuts
|
||||||
|
3. **Information Density**: Compact but readable; optimized for power users
|
||||||
|
4. **Progressive Disclosure**: Show details on demand, not all at once
|
||||||
|
5. **Dark Mode Primary**: Inspired by Mission Control dashboard aesthetics
|
||||||
|
|
||||||
|
### 2.2 Layout Architecture
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Header (Logo + Search + User Actions) │
|
||||||
|
├──────────────┬──────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ Sidebar │ Main Content Area │
|
||||||
|
│ (Library │ │
|
||||||
|
│ Tree + │ - Document List (Dashboard) │
|
||||||
|
│ Tags) │ - Document Viewer │
|
||||||
|
│ │ - Document Editor │
|
||||||
|
│ │ - Library Browser │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┴──────────────────────────────────────────────┤
|
||||||
|
│ Status Bar (optional: sync status, doc count) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Responsive Strategy
|
||||||
|
- **Desktop (>1024px)**: Full sidebar + content layout
|
||||||
|
- **Tablet (768-1024px)**: Collapsible sidebar, full content
|
||||||
|
- **Mobile (<768px)**: Bottom navigation, stacked layout, slide-over panels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Views Specification
|
||||||
|
|
||||||
|
### 3.1 Dashboard / Main View
|
||||||
|
|
||||||
|
**Purpose**: Central hub showing documents with filtering and navigation.
|
||||||
|
|
||||||
|
**Components**:
|
||||||
|
- **Header Bar**: Logo, global search input, theme toggle, settings
|
||||||
|
- **Sidebar (left)**:
|
||||||
|
- Library tree (collapsible, nested)
|
||||||
|
- Quick filters: All Documents, Recent, Favorites
|
||||||
|
- Tag cloud/list with counts
|
||||||
|
- **Main Content**:
|
||||||
|
- Toolbar: View toggle (list/grid), sort options, bulk actions
|
||||||
|
- Document cards/list with title, tags, type badge, date, status
|
||||||
|
- Quick actions: New Document, New Library buttons
|
||||||
|
|
||||||
|
**URL**: `/` or `/documents`
|
||||||
|
|
||||||
|
**Keyboard Shortcuts**:
|
||||||
|
- `Ctrl/Cmd + K`: Focus search
|
||||||
|
- `Ctrl/Cmd + N`: New document
|
||||||
|
- `Ctrl/Cmd + Shift + N`: New library
|
||||||
|
- `Escape`: Clear filters/search
|
||||||
|
|
||||||
|
### 3.2 Document Viewer
|
||||||
|
|
||||||
|
**Purpose**: Read-only view of a document with rendered markdown.
|
||||||
|
|
||||||
|
**Components**:
|
||||||
|
- **Header**: Back button, document title, action buttons (Edit, Export, Delete)
|
||||||
|
- **Metadata Panel** (collapsible sidebar or dropdown):
|
||||||
|
- Type badge (requirement/note/spec/general)
|
||||||
|
- Status badge (draft/approved/implemented)
|
||||||
|
- Priority indicator (high/medium/low with color)
|
||||||
|
- Tags as clickable pills
|
||||||
|
- Author
|
||||||
|
- Created/Updated timestamps
|
||||||
|
- **Content Area**: Rendered markdown with:
|
||||||
|
- Syntax highlighting for code blocks
|
||||||
|
- Styled tables
|
||||||
|
- Checkbox rendering for criteria lists
|
||||||
|
- Anchor links for headings
|
||||||
|
|
||||||
|
**URL**: `/documents/:id`
|
||||||
|
|
||||||
|
**Keyboard Shortcuts**:
|
||||||
|
- `E`: Edit document
|
||||||
|
- `X`: Export markdown
|
||||||
|
- `Delete/Backspace`: Delete (with confirmation)
|
||||||
|
- `Escape`: Back to list
|
||||||
|
|
||||||
|
### 3.3 Document Editor
|
||||||
|
|
||||||
|
**Purpose**: Create or edit documents with live preview.
|
||||||
|
|
||||||
|
**Components**:
|
||||||
|
- **Header**: Cancel/Save buttons, document title (editable)
|
||||||
|
- **Editor Toolbar**:
|
||||||
|
- Markdown formatting buttons (bold, italic, headers, lists, code, link)
|
||||||
|
- Insert template dropdown
|
||||||
|
- Preview toggle (split view or tabbed)
|
||||||
|
- **Frontmatter Panel**:
|
||||||
|
- Title input
|
||||||
|
- Type selector (dropdown)
|
||||||
|
- Status selector (dropdown)
|
||||||
|
- Priority selector (dropdown)
|
||||||
|
- Tags input (chip-style, autocomplete)
|
||||||
|
- Library selector (dropdown with tree view)
|
||||||
|
- **Editor Area**:
|
||||||
|
- Monaco-style textarea with markdown syntax highlighting
|
||||||
|
- OR CodeMirror/MarkText integration (future)
|
||||||
|
- **Preview Pane**: Live rendered markdown (toggleable)
|
||||||
|
- **Auto-save Indicator**: "Saved" / "Saving..." / "Unsaved changes"
|
||||||
|
|
||||||
|
**URL**: `/documents/:id/edit` or `/documents/new`
|
||||||
|
|
||||||
|
**Keyboard Shortcuts**:
|
||||||
|
- `Ctrl/Cmd + S`: Save
|
||||||
|
- `Ctrl/Cmd + B`: Bold
|
||||||
|
- `Ctrl/Cmd + I`: Italic
|
||||||
|
- `Ctrl/Cmd + K`: Insert link
|
||||||
|
- `Ctrl/Cmd + Shift + P`: Toggle preview
|
||||||
|
- `Escape`: Discard changes (with confirmation)
|
||||||
|
|
||||||
|
### 3.4 Library Browser
|
||||||
|
|
||||||
|
**Purpose**: Navigate and manage library hierarchy.
|
||||||
|
|
||||||
|
**Components**:
|
||||||
|
- **Breadcrumbs**: Path from root to current library
|
||||||
|
- **Library Tree** (sidebar): Expandable/collapsible nested view
|
||||||
|
- **Main Content**:
|
||||||
|
- Header: Current library name, document count
|
||||||
|
- Actions: New Document, New Sub-library, Rename, Delete
|
||||||
|
- Content list: Documents and sub-libraries mixed
|
||||||
|
- Drag-and-drop reordering (future)
|
||||||
|
|
||||||
|
**URL**: `/libraries/:id`
|
||||||
|
|
||||||
|
**Actions**:
|
||||||
|
- Click library → navigate to library view
|
||||||
|
- Click document → open viewer
|
||||||
|
- Right-click/long-press → context menu (Rename, Delete, Move)
|
||||||
|
- "+" buttons for creating new items
|
||||||
|
|
||||||
|
**Keyboard Shortcuts**:
|
||||||
|
- `Ctrl/Cmd + Shift + N`: New sub-library
|
||||||
|
- `R`: Rename selected
|
||||||
|
- `Delete`: Delete (with confirmation)
|
||||||
|
- `Escape`: Go to parent library
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Interaction Patterns
|
||||||
|
|
||||||
|
### 4.1 Navigation
|
||||||
|
- Sidebar persists across views (single-page app behavior)
|
||||||
|
- Breadcrumbs for deep library navigation
|
||||||
|
- Browser back/forward support via History API
|
||||||
|
|
||||||
|
### 4.2 Search
|
||||||
|
- Real-time filtering as user types (debounced 300ms)
|
||||||
|
- Search scope: title, content, tags
|
||||||
|
- Highlight matching terms in results
|
||||||
|
- Empty state: "No documents found"
|
||||||
|
|
||||||
|
### 4.3 Tag Filtering
|
||||||
|
- Click tag in sidebar → filter documents
|
||||||
|
- Click tag pill in document → filter by that tag
|
||||||
|
- Multiple tags = AND filter
|
||||||
|
- Active filters shown as removable chips
|
||||||
|
|
||||||
|
### 4.4 Confirmation Dialogs
|
||||||
|
- Delete operations: Modal with confirmation
|
||||||
|
- Unsaved changes: Modal asking to save/discard
|
||||||
|
- All modals: Escape to cancel, Enter to confirm (when safe)
|
||||||
|
|
||||||
|
### 4.5 Notifications/Toasts
|
||||||
|
- Success: Green, auto-dismiss 3s
|
||||||
|
- Error: Red, persistent until dismissed
|
||||||
|
- Info: Blue, auto-dismiss 5s
|
||||||
|
- Position: Bottom-right
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Data Flow
|
||||||
|
|
||||||
|
### 5.1 API Integration
|
||||||
|
All UI data comes from the REST API at `/api/v1`.
|
||||||
|
|
||||||
|
**Auth Flow**:
|
||||||
|
1. User enters token in settings or login page
|
||||||
|
2. Token stored in localStorage
|
||||||
|
3. All API requests include `Authorization: Bearer <token>` header
|
||||||
|
4. 401 response → clear token, show login
|
||||||
|
|
||||||
|
**Endpoints Used by UI**:
|
||||||
|
```
|
||||||
|
GET /api/v1/documents → Dashboard document list
|
||||||
|
GET /api/v1/documents/:id → Document viewer
|
||||||
|
POST /api/v1/documents → Create document
|
||||||
|
PUT /api/v1/documents/:id → Update document
|
||||||
|
DELETE /api/v1/documents/:id → Delete document
|
||||||
|
GET /api/v1/documents/:id/export → Export markdown
|
||||||
|
GET /api/v1/libraries → Library tree (root)
|
||||||
|
GET /api/v1/libraries/:id → Library contents
|
||||||
|
GET /api/v1/libraries/:id/tree → Full library tree
|
||||||
|
POST /api/v1/libraries → Create library
|
||||||
|
DELETE /api/v1/libraries/:id → Delete library
|
||||||
|
GET /api/v1/tags → Tag list with counts
|
||||||
|
GET /api/v1/tags/:tag/documents → Filtered documents
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 State Management
|
||||||
|
```javascript
|
||||||
|
// App state (simple pub/sub)
|
||||||
|
const state = {
|
||||||
|
documents: [],
|
||||||
|
libraries: [],
|
||||||
|
tags: [],
|
||||||
|
currentView: 'dashboard', // dashboard|viewer|editor|library
|
||||||
|
currentDocument: null,
|
||||||
|
currentLibrary: null,
|
||||||
|
filters: { tag: null, library: null, search: '' },
|
||||||
|
preferences: { theme: 'dark' }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Auto-save Implementation
|
||||||
|
```javascript
|
||||||
|
// Debounced auto-save during editing
|
||||||
|
let saveTimeout;
|
||||||
|
function onEditorChange(content) {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
ui.setSaving();
|
||||||
|
saveTimeout = setTimeout(() => {
|
||||||
|
api.updateDocument(id, { content }).then(() => {
|
||||||
|
ui.setSaved();
|
||||||
|
}).catch(() => {
|
||||||
|
ui.setError('Auto-save failed');
|
||||||
|
});
|
||||||
|
}, 2000); // 2 second debounce
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Accessibility
|
||||||
|
|
||||||
|
### 6.1 Requirements
|
||||||
|
- All interactive elements keyboard accessible
|
||||||
|
- Focus visible indicators (custom styled)
|
||||||
|
- ARIA labels on icons and non-text elements
|
||||||
|
- Color contrast ratio ≥ 4.5:1 (WCAG AA)
|
||||||
|
- Screen reader announcements for dynamic content
|
||||||
|
|
||||||
|
### 6.2 Keyboard Navigation
|
||||||
|
- Tab order follows visual layout
|
||||||
|
- Focus trapped in modals
|
||||||
|
- Skip-to-content link
|
||||||
|
- Arrow keys for tree/list navigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Error Handling
|
||||||
|
|
||||||
|
### 7.1 Network Errors
|
||||||
|
- Show toast notification
|
||||||
|
- Retry button for failed requests
|
||||||
|
- Offline indicator in status bar
|
||||||
|
|
||||||
|
### 7.2 Validation Errors
|
||||||
|
- Inline field errors in forms
|
||||||
|
- Red border on invalid fields
|
||||||
|
- Error message below field
|
||||||
|
|
||||||
|
### 7.3 Empty States
|
||||||
|
- No documents: Illustration + "Create your first document" CTA
|
||||||
|
- No search results: "No matches found" + clear filters button
|
||||||
|
- No libraries: "Create a library to organize your documents"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Future Enhancements (Out of Scope for v1)
|
||||||
|
|
||||||
|
- Drag-and-drop document organization
|
||||||
|
- Document versioning/history
|
||||||
|
- Collaborative editing
|
||||||
|
- Full-text search
|
||||||
|
- Document templates gallery
|
||||||
|
- Export to PDF/HTML
|
||||||
|
- Mobile native app
|
||||||
Reference in New Issue
Block a user