Merge branch 'main' of ssh://git.linuxhg.com:2222/Bookhoard/bookhoard
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"languages": {
|
||||
"CSS": {
|
||||
"language_servers": ["tailwindcss-language-server"],
|
||||
"tab_size": 2,
|
||||
"formatter": "auto",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -17,7 +17,10 @@
|
||||
- ❌ **NEVER modify backend/API for frontend features without user confirmation**
|
||||
- ❌ **NEVER use custom CSS** - TailwindCSS classes only
|
||||
- ❌ **NEVER use JavaScript** - convert all to TypeScript
|
||||
- ❌ **NEVER use object-oriented programming patterns in TypeScript** - avoid classes, inheritance, and OOP bloat; use functional/other paradigms
|
||||
- ❌ **NEVER use Object-Oriented Programming** (no classes, inheritance, or this-capture)
|
||||
- ✅ **DO use procedural/imperative style** as your default
|
||||
- ✅ **DO borrow functional techniques** when they simplify code
|
||||
- ✅ **DO avoid ideological purity** - the best paradigm is the one that fits the problem
|
||||
- ❌ **NEVER add new Dockerfiles without user confirmation
|
||||
- ❌ **NEVER fetch initial data via AJAX on page load** - use server-side rendering instead
|
||||
- ❌ **NEVER break progressive enhancement** - pages must work without JavaScript
|
||||
@@ -113,7 +116,10 @@ VERIFY → Compile successfully
|
||||
### Frontend & Styling
|
||||
- ✅ Always use **TailwindCSS classes** for all styling
|
||||
- ✅ Convert all JavaScript to **TypeScript**
|
||||
- ✅ Avoid OOP patterns - prefer functional/other paradigms
|
||||
- ✅ **Never use Object-Oriented Programming** (no classes, inheritance, or this-capture)
|
||||
- ✅ **Use procedural/imperative style** as your default
|
||||
- ✅ **Borrow functional techniques** when they simplify code
|
||||
- ✅ **Avoid ideological purity** - the best paradigm is the one that fits the problem
|
||||
- ✅ **Render initial data server-side** in Go templates for fast page loads
|
||||
- ✅ **Use JavaScript/HTMX for CRUD operations** (create, update, delete)
|
||||
- ✅ **Ensure progressive enhancement** - pages work without JavaScript
|
||||
@@ -207,7 +213,7 @@ VERIFY → Compile successfully
|
||||
- **Styling**: TailwindCSS (no custom CSS)
|
||||
- **Language**: TypeScript (no JavaScript)
|
||||
- **Templates**: HTMX with server-side rendering
|
||||
- **Patterns**: Functional/other (no OOP)
|
||||
- **Patterns**: Procedural/imperative with functional techniques where helpful (no OOP)
|
||||
|
||||
### Containerization
|
||||
- **Runtime**: Podman (not Docker)
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
# Screenshot Automation Plan for Bookhoard Documentation
|
||||
|
||||
> **Status**: Ready to implement when frontend is complete
|
||||
> **Last Updated**: 2026-02-03
|
||||
|
||||
This document outlines the complete plan for automatically generating screenshots for Bookhoard documentation using Playwright.
|
||||
|
||||
## Overview
|
||||
|
||||
Playwright will be used to:
|
||||
1. Navigate the running Bookhoard server
|
||||
2. Perform key user/admin workflows
|
||||
3. Capture screenshots at each step
|
||||
4. Save to `docs/images/` organized by documentation section
|
||||
5. Generate/update markdown files with proper image references
|
||||
|
||||
## Prerequisites (To Verify When Ready)
|
||||
|
||||
### Frontend Pages Complete
|
||||
Verify these pages are fully functional before starting:
|
||||
|
||||
- [ ] `/` - Login page
|
||||
- [ ] `/register` - Registration page
|
||||
- [ ] `/dashboard` - Main dashboard with library browsing
|
||||
- [ ] `/collections` - Collections management
|
||||
- [ ] `/devices` - Device management and registration
|
||||
- [ ] `/conflicts` - Sync conflict resolution
|
||||
- [ ] `/progress` - Reading progress tracking
|
||||
- [ ] `/analytics` - Usage analytics
|
||||
- [ ] `/queue` - Sync queue monitoring
|
||||
- [ ] `/admin` - Admin dashboard
|
||||
- [ ] `/admin/profile` - Profile settings
|
||||
- [ ] `/admin/library` - Library management
|
||||
- [ ] `/api-explorer` - API testing interface
|
||||
|
||||
### Test Environment Ready
|
||||
- [ ] Bookhoard server running on `http://localhost:8765`
|
||||
- [ ] Test database seeded with sample data (books, collections, devices)
|
||||
- [ ] Test admin account ready (username, password, role=admin)
|
||||
- [ ] Test regular user account ready (username, password, role=user)
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
```bash
|
||||
# Install Playwright (run in project root)
|
||||
npm init -y
|
||||
npm install -D @playwright/test
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
Project structure after setup:
|
||||
```
|
||||
bookhoard/
|
||||
├── docs/
|
||||
│ ├── images/ # Screenshots will be stored here
|
||||
│ │ ├── user/
|
||||
│ │ ├── admin/
|
||||
│ │ ├── devices/
|
||||
│ │ └── sync/
|
||||
│ └── *.md # Existing markdown files
|
||||
├── screenshots/ # Playwright test files
|
||||
│ ├── auth.spec.ts
|
||||
│ ├── user-workflows.spec.ts
|
||||
│ ├── admin-workflows.spec.ts
|
||||
│ ├── device-workflows.spec.ts
|
||||
│ ├── sync-workflows.spec.ts
|
||||
│ └── config.ts
|
||||
├── .env.screenshots # Environment configuration
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Create `.env.screenshots` in the project root:
|
||||
|
||||
```env
|
||||
# Bookhoard Server
|
||||
BASE_URL=http://localhost:8765
|
||||
|
||||
# Admin Account (for admin guide screenshots)
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=SecureAdminPass123!
|
||||
|
||||
# Regular User Account (for user guide screenshots)
|
||||
USER_EMAIL=user@example.com
|
||||
USER_USERNAME=user
|
||||
USER_PASSWORD=SecureUserPass456!
|
||||
```
|
||||
|
||||
## Questions to Ask When Ready
|
||||
|
||||
Before running the screenshot automation, ask the user:
|
||||
|
||||
### 1. Server Access
|
||||
**Q**: Where is the Bookhoard server running?
|
||||
- [ ] `localhost:8765` (default)
|
||||
- [ ] Custom port: `________`
|
||||
- [ ] Remote URL: `________`
|
||||
|
||||
### 2. Test Credentials
|
||||
**Q**: What credentials should Playwright use?
|
||||
|
||||
**Admin Account** (for admin guide screenshots):
|
||||
- Username: `________`
|
||||
- Password: `________`
|
||||
|
||||
**Regular User Account** (for user guide screenshots):
|
||||
- Username: `________`
|
||||
- Password: `________`
|
||||
|
||||
### 3. Screenshot Format
|
||||
**Q**: What format for screenshots?
|
||||
- [ ] **WebP** (recommended - modern, good compression)
|
||||
- [ ] PNG (highest quality, larger files)
|
||||
- [ ] JPEG (smaller files, compression artifacts)
|
||||
|
||||
### 4. Screenshot Dimensions
|
||||
**Q**: What viewport sizes for screenshots?
|
||||
- [ ] **Desktop**: 1920x1080 (full-width screenshots)
|
||||
- [ ] **Tablet**: 768x1024 (responsive documentation)
|
||||
- [ ] **Mobile**: 375x667 (mobile documentation)
|
||||
- [ ] All three sizes (comprehensive coverage)
|
||||
|
||||
### 5. Theme
|
||||
**Q**: What theme should screenshots use?
|
||||
- [ ] **Default** (Tokyo Night theme as seen in templates)
|
||||
- [ ] Light theme (if implemented)
|
||||
- [ ] Multiple themes (document theme switching)
|
||||
|
||||
### 6. Language
|
||||
**Q**: What language/region for the UI?
|
||||
- [ ] **English** (default)
|
||||
- [ ] Other: `________`
|
||||
|
||||
## Screenshot Plan by Documentation Section
|
||||
|
||||
### 1. User Guide Screenshots
|
||||
**File**: `docs/user/user-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `login-page.webp` | Login form with filled credentials | `/` - Login page |
|
||||
| `dashboard-overview.webp` | Main dashboard showing libraries | `/dashboard` |
|
||||
| `library-grid.webp` | Media items grid view | `/dashboard` → Click library |
|
||||
| `book-detail.webp` | Book detail view with metadata | `/dashboard` → Click book |
|
||||
| `search-results.webp` | Search in action | `/dashboard` → Type in search |
|
||||
| `filter-panel.webp` | Filter options expanded | `/dashboard` → Open filters |
|
||||
| `collections-list.webp` | Collections grid view | `/collections` |
|
||||
| `create-collection.webp` | New collection modal | `/collections` → Click "New Collection" |
|
||||
| `reading-progress.webp` | Progress tracking view | `/progress` |
|
||||
| `analytics-view.webp` | User analytics dashboard | `/analytics` |
|
||||
|
||||
**Estimated Screenshots**: ~10
|
||||
|
||||
### 2. Admin Guide Screenshots
|
||||
**File**: `docs/user/admin-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `admin-dashboard.webp` | Admin overview panel | `/admin` |
|
||||
| `user-management.webp` | User list with actions | `/admin` → Users section |
|
||||
| `add-user.webp` | Add new user form | `/admin` → Click "Add User" |
|
||||
| `library-settings.webp` | Library configuration | `/admin/library` |
|
||||
| `add-library.webp` | Create new library form | `/admin/library` → Click "Add Library" |
|
||||
| `analytics-admin.webp` | Admin analytics view | `/analytics` (admin view) |
|
||||
| `sync-queue.webp` | Sync queue monitoring | `/queue` |
|
||||
| `theme-settings.webp` | Theme selection interface | `/admin/profile` → Theme section |
|
||||
| `user-profile-edit.webp` | Edit user profile | `/admin/profile` |
|
||||
|
||||
**Estimated Screenshots**: ~9
|
||||
|
||||
### 3. Device Setup Screenshots
|
||||
**File**: `docs/user/devices/kobo-setup.md` and `koreader-setup.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `device-list.webp` | Device management page | `/devices` |
|
||||
| `add-device-modal.webp` | Add new device modal | `/devices` → Click "Add New Device" |
|
||||
| `device-form-kobo.webp` | Kobo device registration form | `/devices` → Select Kobo type |
|
||||
| `device-form-koreader.webp` | KOReader device registration form | `/devices` → Select KOReader type |
|
||||
| `device-qr-code.webp` | QR code for device approval | After device registration |
|
||||
| `device-approved.webp` | Device approved confirmation | After approving device |
|
||||
| `device-sync-settings.webp` | Sync configuration for device | `/devices` → Click device settings |
|
||||
| `shelf-mapping.webp` | Collection to shelf mapping | `/devices` → Click shelf mapping |
|
||||
| `sync-queue-item.webp` | Device sync in queue | `/queue` (device specific) |
|
||||
|
||||
**Estimated Screenshots**: ~9
|
||||
|
||||
### 4. Sync Guide Screenshots
|
||||
**File**: `docs/user/sync-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `sync-conflicts.webp` | Conflicts list view | `/conflicts` |
|
||||
| `conflict-resolution.webp` | Resolve conflict dialog | `/conflicts` → Click resolve |
|
||||
| `unlinked-books.webp` | Unlinked books list | `/unlinked-books` |
|
||||
| `book-linking.webp` | Link book to metadata | `/unlinked-books` → Click link |
|
||||
| `sync-success.webp` | Successful sync indicator | Any page after sync |
|
||||
|
||||
**Estimated Screenshots**: ~5
|
||||
|
||||
### 5. Settings Guide Screenshots
|
||||
**File**: `docs/user/settings-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `profile-overview.webp` | Profile settings page | `/admin/profile` |
|
||||
| `update-username.webp` | Username change form | `/admin/profile` |
|
||||
| `update-email.webp` | Email change form | `/admin/profile` |
|
||||
| `change-password.webp` | Password change form | `/admin/profile` |
|
||||
| `theme-selector.webp` | Theme selection dropdown | `/admin/profile` (if implemented) |
|
||||
|
||||
**Estimated Screenshots**: ~5
|
||||
|
||||
## Playwright Test Files
|
||||
|
||||
### `screenshots/config.ts` - Playwright Configuration
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './',
|
||||
fullyParallel: false,
|
||||
retries: 1,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:8765',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium-desktop',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: { width: 1920, height: 1080 }
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### `screenshots/auth.spec.ts` - Authentication Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Authentication Screenshots', () => {
|
||||
test('Login page', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/login-page.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('User login flow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/login-form-filled.webp',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/dashboard-after-login.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Admin login flow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin');
|
||||
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
|
||||
await page.goto('/admin');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/admin-dashboard.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Registration page', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/registration-page.webp',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
await page.fill('input[name="email"]', 'newuser@example.com');
|
||||
await page.fill('input[name="username"]', 'newuser');
|
||||
await page.fill('input[name="password"]', 'SecurePass123!');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/registration-form-filled.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### `screenshots/user-workflows.spec.ts` - User Guide Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('User Guide Screenshots', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Dashboard overview', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/dashboard-overview.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Library grid view', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.waitForSelector('#libraries-container', { timeout: 5000 });
|
||||
|
||||
const firstLibrary = page.locator('[data-library]').first();
|
||||
if (await firstLibrary.isVisible()) {
|
||||
await firstLibrary.click();
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 5000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/library-grid.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Search functionality', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.waitForSelector('#search-input', { timeout: 5000 });
|
||||
|
||||
await page.fill('#search-input', 'science');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/search-results.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Filter panel', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
const filterPanel = page.locator('.filter-panel details');
|
||||
if (await filterPanel.isVisible()) {
|
||||
await filterPanel.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/filter-panel-open.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Collections list', async ({ page }) => {
|
||||
await page.goto('/collections');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/collections-list.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Create collection modal', async ({ page }) => {
|
||||
await page.goto('/collections');
|
||||
|
||||
await page.click('button:has-text("New Collection"), button:has-text("Create Your First Collection")');
|
||||
|
||||
await page.waitForSelector('#create-modal', { state: 'visible', timeout: 5000 });
|
||||
|
||||
await page.fill('#collection-name', 'My Reading List');
|
||||
await page.fill('#collection-description', 'Books I want to read');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/create-collection-modal.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Reading progress view', async ({ page }) => {
|
||||
await page.goto('/progress');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/reading-progress.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Analytics view', async ({ page }) => {
|
||||
await page.goto('/analytics');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/analytics-view.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### `screenshots/admin-workflows.spec.ts` - Admin Guide Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Admin Guide Screenshots', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin');
|
||||
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Admin dashboard', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/admin-dashboard.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('User management', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
|
||||
const userSection = page.locator('[data-section="users"], text="Users"');
|
||||
if (await userSection.isVisible()) {
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/user-management.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Library management', async ({ page }) => {
|
||||
await page.goto('/admin/library');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/library-management.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Add library form', async ({ page }) => {
|
||||
await page.goto('/admin/library');
|
||||
|
||||
const addLibraryBtn = page.locator('button:has-text("Add Library"), button:has-text("Create Library")');
|
||||
if (await addLibraryBtn.isVisible()) {
|
||||
await addLibraryBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/add-library-form.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Profile settings', async ({ page }) => {
|
||||
await page.goto('/admin/profile');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/profile-settings.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Theme selector', async ({ page }) => {
|
||||
await page.goto('/admin/profile');
|
||||
|
||||
const themeSelect = page.locator('select[name="theme"], [data-theme-selector]');
|
||||
if (await themeSelect.isVisible()) {
|
||||
await themeSelect.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/theme-selector.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### `screenshots/device-workflows.spec.ts` - Device Setup Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Device Setup Screenshots', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Device list page', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/device-list.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Add device modal', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
|
||||
await page.click('button:has-text("Add New Device"), button:has-text("Add Your First Device")');
|
||||
|
||||
await page.waitForSelector('[data-modal="add-device"], #add-device-modal', { state: 'visible', timeout: 5000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/add-device-modal.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Kobo device form', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
|
||||
await page.click('button:has-text("Add New Device")');
|
||||
|
||||
await page.waitForSelector('[data-modal="add-device"]', { state: 'visible', timeout: 5000 });
|
||||
|
||||
const deviceTypeSelect = page.locator('select[name="device_type"]');
|
||||
if (await deviceTypeSelect.isVisible()) {
|
||||
await deviceTypeSelect.selectOption('kobo');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/device-form-kobo.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('KOReader device form', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
|
||||
await page.click('button:has-text("Add New Device")');
|
||||
|
||||
await page.waitForSelector('[data-modal="add-device"]', { state: 'visible', timeout: 5000 });
|
||||
|
||||
const deviceTypeSelect = page.locator('select[name="device_type"]');
|
||||
if (await deviceTypeSelect.isVisible()) {
|
||||
await deviceTypeSelect.selectOption('koreader');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/device-form-koreader.webp',
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Sync queue', async ({ page }) => {
|
||||
await page.goto('/queue');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/sync-queue.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### `screenshots/sync-workflows.spec.ts` - Sync Guide Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Sync Guide Screenshots', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Sync conflicts page', async ({ page }) => {
|
||||
await page.goto('/conflicts');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/sync/sync-conflicts.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('Unlinked books page', async ({ page }) => {
|
||||
await page.goto('/unlinked-books');
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/sync/unlinked-books.webp',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Running the Tests
|
||||
|
||||
### Run all tests
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### Run specific test file
|
||||
```bash
|
||||
npx playwright test auth.spec.ts
|
||||
```
|
||||
|
||||
### Run in headed mode (see browser)
|
||||
```bash
|
||||
npx playwright test --headed
|
||||
```
|
||||
|
||||
### Run with debug mode
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
## Markdown Update Strategy
|
||||
|
||||
### Option 1: Create New Markdown
|
||||
Generate fresh markdown files with embedded screenshots.
|
||||
|
||||
### Option 2: Update Existing Markdown
|
||||
Update existing markdown files by:
|
||||
1. Finding section headers
|
||||
2. Inserting screenshot references after relevant steps
|
||||
3. Using alt text to describe what's shown
|
||||
|
||||
**Example Markdown Update**:
|
||||
|
||||
```markdown
|
||||
### Step 1: Log In to Bookhoard
|
||||
|
||||
1. Open your browser and navigate to `http://localhost:8765`
|
||||
2. Enter your username and password
|
||||
3. Click the **Login** button
|
||||
|
||||

|
||||
```
|
||||
|
||||
## Workflow When Ready
|
||||
|
||||
### Step 1: Verify Frontend Complete
|
||||
- Check all pages listed in "Prerequisites" are working
|
||||
- Confirm no TODO placeholders in templates
|
||||
|
||||
### Step 2: Seed Test Data
|
||||
```bash
|
||||
# Add sample books, collections, devices
|
||||
# Create test admin and user accounts
|
||||
```
|
||||
|
||||
### Step 3: Set Up Playwright
|
||||
```bash
|
||||
npm install -D @playwright/test
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
### Step 4: Create Environment File
|
||||
Create `.env.screenshots` with the test credentials
|
||||
|
||||
### Step 5: Run Screenshot Scripts
|
||||
```bash
|
||||
# Run all screenshot workflows
|
||||
npx playwright test
|
||||
|
||||
# Or run specific suites
|
||||
npx playwright test auth.spec.ts
|
||||
npx playwright test user-workflows.spec.ts
|
||||
```
|
||||
|
||||
### Step 6: Review and Adjust
|
||||
- Manually review screenshots
|
||||
- Retake any that need adjustment
|
||||
- Update markdown files if needed
|
||||
|
||||
## Customizing for Your Implementation
|
||||
|
||||
When you're ready to run these, you may need to update:
|
||||
|
||||
1. **Selectors**: Update CSS selectors to match your actual HTML structure
|
||||
2. **Routes**: Ensure all routes match your router configuration
|
||||
3. **Wait conditions**: Adjust timeouts based on your app's performance
|
||||
4. **Modal IDs**: Update modal selectors to match your implementation
|
||||
|
||||
## Tips for Better Screenshots
|
||||
|
||||
1. **Wait for animations**: Add `page.waitForTimeout(500)` after clicking to let animations complete
|
||||
2. **Full page vs viewport**: Use `fullPage: true` for complete page screenshots
|
||||
3. **Hide scrollbars**: Use `page.evaluate(() => document.body.style.overflow = 'hidden')` for cleaner screenshots
|
||||
4. **Dark theme**: Most screenshots will use the default Tokyo Night theme
|
||||
5. **Clean test data**: Use consistent test data for reproducible screenshots
|
||||
|
||||
## Estimated Time Investment
|
||||
|
||||
| Task | Time |
|
||||
|------|------|
|
||||
| Install & configure Playwright | 15 min |
|
||||
| Seed test database | 30 min |
|
||||
| Write Playwright scripts | 2-3 hours |
|
||||
| Run screenshot automation | 10 min |
|
||||
| Review & retake screenshots | 30-60 min |
|
||||
| Update markdown files | 30-60 min |
|
||||
| **Total** | **4-6 hours** |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Visual Regression Testing**: Use Playwright to detect UI changes
|
||||
- **Multi-theme Screenshots**: Automatically capture all theme variations
|
||||
- **Multi-language Screenshots**: Capture screenshots in different locales
|
||||
- **Auto-update on Deploy**: Run screenshots as part of CI/CD on documentation updates
|
||||
|
||||
## References
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Playwright Screenshots API](https://playwright.dev/docs/screenshots)
|
||||
- [Bookhoard Frontend Templates](templates/)
|
||||
- [Bookhoard API Documentation](docs/developer/api/)
|
||||
|
||||
---
|
||||
|
||||
**Created by**: GLM (OpenCode)
|
||||
**Version**: 1.0
|
||||
**Ready for Implementation**: When frontend development is complete
|
||||
@@ -4,7 +4,7 @@ Welcome to the Bookhoard contributing documentation. This section contains guide
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
- **[Development Guide](DEVELOPMENT.md)** - Development workflow and architecture
|
||||
- **[Development Guide](Development.md)** - Development workflow and architecture
|
||||
- Architecture overview
|
||||
- Directory structure
|
||||
- Local development setup
|
||||
@@ -22,7 +22,7 @@ Welcome to the Bookhoard contributing documentation. This section contains guide
|
||||
|
||||
## 🤝 How to Contribute
|
||||
|
||||
We welcome contributions! Please see our [Development Guide](DEVELOPMENT.md) for information on:
|
||||
We welcome contributions! Please see our [Development Guide](Development.md) for information on:
|
||||
- Setting up your development environment
|
||||
- Understanding the codebase
|
||||
- Making pull requests
|
||||
|
||||
+5
-5
@@ -53,7 +53,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
|
||||
|
||||
**[Contributing Portal](contributing/contributing.md)** - Development workflow
|
||||
|
||||
- [Development Guide](contributing/DEVELOPMENT.md) - Architecture, setup, testing
|
||||
- [Development Guide](contributing/Development.md) - Architecture, setup, testing
|
||||
- [PROJECT_GUIDELINES.md](PROJECT_GUIDELINES.md) - Development rules and standards
|
||||
|
||||
---
|
||||
@@ -66,7 +66,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
|
||||
| **Set up a device** | [User Portal → Device Setup](user/user-guide.md) |
|
||||
| **Use the API** | [Developer Portal → API Docs](developer/development.md) |
|
||||
| **Deploy Bookhoard** | [Operations Portal → Troubleshooting](operations/troubleshooting.md) |
|
||||
| **Contribute code** | [Contributing Portal → Development Guide](contributing/DEVELOPMENT.md) |
|
||||
| **Contribute code** | [Contributing Portal → Development Guide](contributing/Development.md) |
|
||||
| **Understand sync** | [User Portal → Sync Guide](user/sync-guide.md) |
|
||||
|
||||
---
|
||||
@@ -84,7 +84,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
|
||||
| ...resolve conflicts? | [Sync Guide](user/sync-guide.md) - Managing Conflicts |
|
||||
| ...troubleshoot deployment? | [Troubleshooting Guide](operations/troubleshooting.md) |
|
||||
| ...use the API? | [API Reference](developer/api-reference.md) |
|
||||
| ...contribute code? | [Development Guide](contributing/DEVELOPMENT.md) |
|
||||
| ...contribute code? | [Development Guide](contributing/Development.md) |
|
||||
|
||||
### "Where is..."
|
||||
|
||||
@@ -116,7 +116,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
|
||||
1. Follow [README.md](../README.md) quick start
|
||||
2. Configure environment: [.env.example](../.env.example)
|
||||
3. Review [Troubleshooting Guide](operations/troubleshooting.md)
|
||||
4. Check [Development Guide](contributing/DEVELOPMENT.md) for performance tuning
|
||||
4. Check [Development Guide](contributing/Development.md) for performance tuning
|
||||
|
||||
---
|
||||
|
||||
@@ -126,7 +126,7 @@ When adding new features:
|
||||
|
||||
1. **User-facing features** → Update relevant User docs
|
||||
2. **API endpoints** → Update [API Reference](developer/api-reference.md) & split docs
|
||||
3. **Backend changes** → Update [Development Guide](contributing/DEVELOPMENT.md)
|
||||
3. **Backend changes** → Update [Development Guide](contributing/Development.md)
|
||||
4. **Deployment changes** → Update [Operations Portal](operations/operations.md)
|
||||
|
||||
Keep [PROJECT_GUIDELINES.md](PROJECT_GUIDELINES.md) in mind for documentation standards.
|
||||
|
||||
+116
-38
@@ -6,7 +6,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/templates"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@@ -58,18 +60,38 @@ func (h *HTTPHandler) ShowDocumentation(c echo.Context) error {
|
||||
// Get navigation
|
||||
nav := h.docs.BuildNavigation()
|
||||
|
||||
// Create empty user (docs are public, no auth required)
|
||||
user := templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
// Check if user is authenticated via middleware
|
||||
var user templates.User
|
||||
if userID := c.Get("user"); userID != nil {
|
||||
dbUser := userID.(database.Users)
|
||||
var theme string
|
||||
if dbUser.Theme.Valid {
|
||||
theme = dbUser.Theme.String
|
||||
} else {
|
||||
theme = "tokyo-night"
|
||||
}
|
||||
user = templates.User{
|
||||
ID: uuid.UUID(dbUser.ID.Bytes).String(),
|
||||
Username: dbUser.Username,
|
||||
Email: dbUser.Email,
|
||||
Role: dbUser.Role,
|
||||
Theme: theme,
|
||||
}
|
||||
} else {
|
||||
// Create empty user for unauthenticated users
|
||||
user = templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
}
|
||||
}
|
||||
|
||||
// Render documentation page
|
||||
var buf bytes.Buffer
|
||||
err = templates.DocsLayout(*nav, *doc, user).Render(c.Request().Context(), &buf)
|
||||
currentPath := c.Request().URL.Path
|
||||
err = templates.DocsLayout(*nav, *doc, user, currentPath).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
fmt.Printf("DOCS RENDER ERROR: %v\n", err)
|
||||
return c.HTML(http.StatusInternalServerError, "Failed to render page: "+err.Error())
|
||||
@@ -88,6 +110,7 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error
|
||||
|
||||
// Get endpoint data for the explorer
|
||||
endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath)
|
||||
_ = endpointInfo
|
||||
if err != nil {
|
||||
// Endpoint not found in explorer data, fall back to old behavior
|
||||
return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn)
|
||||
@@ -118,24 +141,38 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error
|
||||
// Get navigation
|
||||
nav := h.docs.BuildNavigation()
|
||||
|
||||
// Create empty user (docs are public)
|
||||
user := templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
// Check if user is authenticated via middleware
|
||||
var user templates.User
|
||||
if userID := c.Get("user"); userID != nil {
|
||||
dbUser := userID.(database.Users)
|
||||
var theme string
|
||||
if dbUser.Theme.Valid {
|
||||
theme = dbUser.Theme.String
|
||||
} else {
|
||||
theme = "tokyo-night"
|
||||
}
|
||||
user = templates.User{
|
||||
ID: uuid.UUID(dbUser.ID.Bytes).String(),
|
||||
Username: dbUser.Username,
|
||||
Email: dbUser.Email,
|
||||
Role: dbUser.Role,
|
||||
Theme: theme,
|
||||
}
|
||||
} else {
|
||||
// Create empty user for unauthenticated users
|
||||
user = templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
}
|
||||
}
|
||||
|
||||
// Create API explorer data
|
||||
explorerData := templates.APIExplorerData{
|
||||
Endpoint: *endpointInfo,
|
||||
IsLoggedIn: isLoggedIn,
|
||||
}
|
||||
|
||||
// Render API endpoint page with explorer
|
||||
// Render API endpoint page
|
||||
var buf bytes.Buffer
|
||||
err = templates.DocsLayoutWithExplorer(*nav, *doc, user, explorerData).Render(c.Request().Context(), &buf)
|
||||
currentPath := c.Request().URL.Path
|
||||
err = templates.DocsLayout(*nav, *doc, user, currentPath).Render(c.Request().Context(), &buf)
|
||||
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
||||
@@ -179,13 +216,33 @@ func (h *HTTPHandler) showLegacyAPIEndpoint(c echo.Context, endpointPath string,
|
||||
// Get navigation
|
||||
nav := h.docs.BuildNavigation()
|
||||
|
||||
// Create empty user
|
||||
user := templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
// Check if user is authenticated via middleware
|
||||
var user templates.User
|
||||
currentPath := c.Request().URL.Path
|
||||
if userID := c.Get("user"); userID != nil {
|
||||
dbUser := userID.(database.Users)
|
||||
var theme string
|
||||
if dbUser.Theme.Valid {
|
||||
theme = dbUser.Theme.String
|
||||
} else {
|
||||
theme = "tokyo-night"
|
||||
}
|
||||
user = templates.User{
|
||||
ID: uuid.UUID(dbUser.ID.Bytes).String(),
|
||||
Username: dbUser.Username,
|
||||
Email: dbUser.Email,
|
||||
Role: dbUser.Role,
|
||||
Theme: theme,
|
||||
}
|
||||
} else {
|
||||
// Create empty user for unauthenticated users
|
||||
user = templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
}
|
||||
}
|
||||
|
||||
// Render API endpoint page
|
||||
@@ -202,7 +259,7 @@ func (h *HTTPHandler) showLegacyAPIEndpoint(c echo.Context, endpointPath string,
|
||||
},
|
||||
Category: "API Reference",
|
||||
SourceFile: "api",
|
||||
}, user).Render(c.Request().Context(), &buf)
|
||||
}, user, currentPath).Render(c.Request().Context(), &buf)
|
||||
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
||||
@@ -221,12 +278,33 @@ func (h *HTTPHandler) APIHome(c echo.Context) error {
|
||||
endpoints := h.docs.GetAPIEndpoints()
|
||||
nav := h.docs.BuildNavigation()
|
||||
|
||||
user := templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
// Check if user is authenticated via middleware
|
||||
var user templates.User
|
||||
currentPath := c.Request().URL.Path
|
||||
if userID := c.Get("user"); userID != nil {
|
||||
dbUser := userID.(database.Users)
|
||||
var theme string
|
||||
if dbUser.Theme.Valid {
|
||||
theme = dbUser.Theme.String
|
||||
} else {
|
||||
theme = "tokyo-night"
|
||||
}
|
||||
user = templates.User{
|
||||
ID: uuid.UUID(dbUser.ID.Bytes).String(),
|
||||
Username: dbUser.Username,
|
||||
Email: dbUser.Email,
|
||||
Role: dbUser.Role,
|
||||
Theme: theme,
|
||||
}
|
||||
} else {
|
||||
// Create empty user for unauthenticated users
|
||||
user = templates.User{
|
||||
ID: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
Theme: "tokyo-night",
|
||||
}
|
||||
}
|
||||
|
||||
// Build API documentation content
|
||||
@@ -255,7 +333,7 @@ func (h *HTTPHandler) APIHome(c echo.Context) error {
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := templates.DocsLayout(*nav, *apiDoc, user).Render(c.Request().Context(), &buf)
|
||||
err := templates.DocsLayout(*nav, *apiDoc, user, currentPath).Render(c.Request().Context(), &buf)
|
||||
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
||||
|
||||
@@ -357,14 +357,18 @@ func (h *AuthHandler) Login(c echo.Context) error {
|
||||
}
|
||||
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
redirect := c.FormValue("redirect")
|
||||
if redirect == "" {
|
||||
redirect = "/bookshelf"
|
||||
}
|
||||
html := fmt.Sprintf(`<div class="text-green-500">Login successful! Redirecting...</div>
|
||||
<script>
|
||||
localStorage.setItem('token', '%s');
|
||||
localStorage.setItem('refreshToken', '%s');
|
||||
localStorage.setItem('user', JSON.stringify(%s));
|
||||
document.cookie = 'token=%s; path=/; max-age=3600';
|
||||
window.location.href = '/bookshelf';
|
||||
</script>`, accessToken, refreshToken, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s","first_name":"%s","last_name":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username, user.FirstName.String, user.LastName.String), accessToken)
|
||||
window.location.href = '%s';
|
||||
</script>`, accessToken, refreshToken, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s","first_name":"%s","last_name":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username, user.FirstName.String, user.LastName.String), accessToken, redirect)
|
||||
return c.HTML(http.StatusOK, html)
|
||||
}
|
||||
|
||||
|
||||
@@ -203,14 +203,23 @@ fi
|
||||
section "Frontend & Styling: TailwindCSS usage"
|
||||
|
||||
# GUIDELINE: Always use TailwindCSS classes for all styling
|
||||
# REQUIREMENT: Use local builds, not CDN (production-ready, faster, no external dependencies)
|
||||
echo "Checking for TailwindCSS usage in templates..."
|
||||
if grep -q "tailwindcss" templates/*.templ 2>/dev/null || grep -q "cdn.tailwindcss.com" templates/*.templ 2>/dev/null; then
|
||||
success_msg "TailwindCSS is being used in templates"
|
||||
|
||||
# Check for CDN usage (violates local-only requirement)
|
||||
if grep -q "cdn.tailwindcss.com" templates/*.templ 2>/dev/null; then
|
||||
error_msg "Found TailwindCSS CDN in templates (violation: use local /static/style.css instead)"
|
||||
grep -n "cdn.tailwindcss.com" templates/*.templ 2>/dev/null
|
||||
echo "Replace CDN script with: <link href=\"/static/style.css\" rel=\"stylesheet\">"
|
||||
else
|
||||
warning_msg "TailwindCSS not found in templates (custom CSS may be excessive)"
|
||||
echo "Expected patterns in templates:"
|
||||
echo "- tailwindcss in script src or href"
|
||||
echo "- cdn.tailwindcss.com in script tags"
|
||||
# Check for local build
|
||||
if grep -q "/static/style.css" templates/*.templ 2>/dev/null; then
|
||||
success_msg "TailwindCSS local build is being used in templates (correct approach)"
|
||||
else
|
||||
warning_msg "TailwindCSS not found in templates (custom CSS may be excessive)"
|
||||
echo "Expected pattern in templates:"
|
||||
echo "- <link href=\"/static/style.css\" rel=\"stylesheet\">"
|
||||
fi
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
|
||||
+33
-7
@@ -21,13 +21,39 @@ const config: Config = {
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
},
|
||||
'bg-primary': '#1a1b26',
|
||||
'bg-secondary': '#16161e',
|
||||
'text-primary': '#a9b1d6',
|
||||
'text-secondary': '#565f89',
|
||||
'accent': '#7aa2f7',
|
||||
'border': '#414868',
|
||||
}
|
||||
'bg-primary': 'var(--bg-primary)',
|
||||
'bg-secondary': 'var(--bg-secondary)',
|
||||
'text-primary': 'var(--text-primary)',
|
||||
'text-secondary': 'var(--text-secondary)',
|
||||
'accent': 'var(--accent)',
|
||||
'border': 'var(--border)',
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
invert: {
|
||||
css: {
|
||||
'--tw-prose-pre-bg': 'var(--bg-secondary)',
|
||||
code: {
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'code::before': {
|
||||
content: '""',
|
||||
},
|
||||
'code::after': {
|
||||
content: '""',
|
||||
},
|
||||
'pre code': {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
},
|
||||
pre: {
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
|
||||
+202
-68
@@ -4,41 +4,22 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{ doc.Title } - Bookhoard Documentation</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<link rel="stylesheet" href="/web/static/highlight-dark.min.css">
|
||||
<script src="/web/static/highlight.min.js"></script>
|
||||
<script src="/web/static/lunr.min.js"></script>
|
||||
<script src="/web/static/lunr-flex.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: {
|
||||
primary: '#1a1b26',
|
||||
secondary: '#24283b',
|
||||
},
|
||||
text: {
|
||||
primary: '#c0caf5',
|
||||
secondary: '#9aa5ce',
|
||||
},
|
||||
border: '#414868',
|
||||
accent: '#7aa2f7',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/highlight-dark.min.css">
|
||||
<script src="/static/highlight.min.js"></script>
|
||||
<script src="/static/lunr.min.js"></script>
|
||||
<script src="/static/lunr-flex.min.js"></script>
|
||||
<script src="/static/header.js"></script>
|
||||
</head>
|
||||
<body class="bg-background-primary text-text-primary font-sans antialiased">
|
||||
<body class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
|
||||
@Header(user, currentPath)
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80"
|
||||
@@ -54,7 +35,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto z-40 lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||
<!-- Search -->
|
||||
<div class="p-4 border-b border-border">
|
||||
<input
|
||||
@@ -62,7 +43,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm"
|
||||
placeholder="Search documentation..."
|
||||
id="docs-search"
|
||||
oninput="searchDocs(this.value)"
|
||||
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -118,8 +99,11 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
|
||||
<!-- Table of Contents -->
|
||||
if len(doc.TOC) > 0 {
|
||||
<div class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
||||
<strong class="block mb-2 text-text-primary font-semibold">On this page</strong>
|
||||
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
||||
<summary class="cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<strong class="text-text-primary font-semibold">On this page</strong>
|
||||
<span class="ml-2 text-text-secondary text-xs">▼</span>
|
||||
</summary>
|
||||
for _, item := range doc.TOC {
|
||||
<a
|
||||
href={ "#" + item.Anchor }
|
||||
@@ -129,7 +113,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
{ item.Title }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
<!-- Content -->
|
||||
@@ -162,14 +146,194 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
// Close sidebar when clicking main content on mobile
|
||||
document.querySelector('.main-content')?.addEventListener('click', () => {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
if (window.innerWidth < 1024) {
|
||||
sidebar.classList.remove('open');
|
||||
// Highlight current page in nav
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentPath = window.location.pathname;
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
if (item.getAttribute('href') === currentPath) {
|
||||
item.classList.add('text-accent', 'bg-accent/10', 'border-r-2', 'border-accent');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize syntax highlighting
|
||||
if (typeof hljs !== 'undefined') {
|
||||
hljs.highlightAll();
|
||||
|
||||
// Add copy buttons to all code blocks
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
const pre = block.parentElement;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative group';
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(pre);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
||||
button.textContent = 'Copy';
|
||||
button.onclick = async () => {
|
||||
await navigator.clipboard.writeText(block.textContent);
|
||||
button.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
button.textContent = 'Copy';
|
||||
}, 2000);
|
||||
};
|
||||
wrapper.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
// Add copy buttons to all code blocks
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
const pre = block.parentElement;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative group';
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(pre);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
||||
button.textContent = 'Copy';
|
||||
button.onclick = async () => {
|
||||
await navigator.clipboard.writeText(block.textContent);
|
||||
button.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
button.textContent = 'Copy';
|
||||
}, 2000);
|
||||
};
|
||||
wrapper.appendChild(button);
|
||||
});
|
||||
});
|
||||
|
||||
// Add copy buttons to all code blocks
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
const pre = block.parentElement;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative group';
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(pre);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
|
||||
button.textContent = 'Copy';
|
||||
button.onclick = async () => {
|
||||
await navigator.clipboard.writeText(block.textContent);
|
||||
button.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
button.textContent = 'Copy';
|
||||
}, 2000);
|
||||
};
|
||||
wrapper.appendChild(button);
|
||||
});
|
||||
|
||||
// Search state
|
||||
let idx;
|
||||
let searchDocs = [];
|
||||
|
||||
// Load index on page load
|
||||
fetch('/docs/search-index.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
idx = lunr(function() {
|
||||
this.use(lunr.flex);
|
||||
this.ref('id');
|
||||
this.field('title', {boost: 10});
|
||||
this.field('content', {boost: 1});
|
||||
data.forEach(doc => this.add(doc));
|
||||
});
|
||||
searchDocs = data;
|
||||
console.log('Search index loaded:', data.length, 'documents');
|
||||
})
|
||||
.catch(err => console.error('Failed to load search index:', err));
|
||||
|
||||
// Search input with debounce
|
||||
const searchInput = document.getElementById('docs-search');
|
||||
const searchResultsDiv = document.getElementById('search-results');
|
||||
|
||||
let debounceTimer;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
const query = e.target.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResultsDiv.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Search with fuzzy matching
|
||||
const results = idx.search(query, {
|
||||
fields: {title: {boost: 10}, content: 1},
|
||||
expand: true
|
||||
});
|
||||
|
||||
// Render results
|
||||
renderSearchResults(results, query);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Render search results
|
||||
function renderSearchResults(results, query) {
|
||||
if (results.length === 0) {
|
||||
searchResultsDiv.innerHTML = '<div class="text-center py-8 text-text-secondary">No results found</div>';
|
||||
searchResultsDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
searchResultsDiv.innerHTML = results.map(r => {
|
||||
const doc = searchDocs.find(d => d.id === r.ref);
|
||||
if (!doc) return '';
|
||||
|
||||
const snippet = getSnippet(doc.content, query);
|
||||
|
||||
return `
|
||||
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
|
||||
<div class="font-semibold mb-2">${doc.title}</div>
|
||||
<div class="text-sm text-text-secondary">${snippet}...</div>
|
||||
</a>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
searchResultsDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function getSnippet(content, query) {
|
||||
const words = query.toLowerCase().split(/\s+/);
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
for (const word of words) {
|
||||
const idx = contentLower.indexOf(word);
|
||||
if (idx !== -1) {
|
||||
const start = Math.max(0, idx - 50);
|
||||
const end = Math.min(content.length, idx + 100);
|
||||
return '...' + content.substring(start, end) + '...';
|
||||
}
|
||||
}
|
||||
return content.substring(0, 150) + '...';
|
||||
}
|
||||
|
||||
// Close search results when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchResultsDiv.contains(e.target) && e.target !== searchInput) {
|
||||
searchResultsDiv.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Close search on Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
searchResultsDiv.classList.add('hidden');
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Toggle sidebar on mobile
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
|
||||
// Highlight current page in nav
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentPath = window.location.pathname;
|
||||
@@ -353,33 +517,3 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ DocsLayoutWithExplorer(nav Navigation, doc Document, user User, explorer APIExplorerData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{ doc.Title } - Bookhoard Documentation</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<link rel="stylesheet" href="/web/static/highlight-dark.min.css">
|
||||
<script src="/web/static/highlight.min.js"></script>
|
||||
<script src="/web/static/lunr.min.js"></script>
|
||||
<script src="/web/static/lunr-flex.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: {
|
||||
primary: '#1a1b26',
|
||||
secondary: '#24283b',
|
||||
},
|
||||
text: {
|
||||
primary: '#c0caf5',
|
||||
secondary: '#9aa5ce',
|
||||
},
|
||||
border: '#414868',
|
||||
accent: '#7aa2f7',
|
||||
},
|
||||
|
||||
+175
-463
File diff suppressed because one or more lines are too long
+53
-21
@@ -1,7 +1,7 @@
|
||||
package templates
|
||||
|
||||
templ Header(user User, currentPath string) {
|
||||
<nav class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary)">
|
||||
<nav class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Left: App Title & Navigation -->
|
||||
@@ -104,28 +104,60 @@ templ Header(user User, currentPath string) {
|
||||
|
||||
<!-- User Icon with Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
|
||||
</button>
|
||||
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<div class="py-1">
|
||||
<a href="/settings" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Settings
|
||||
</a>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Admin Panel
|
||||
if user.ID != "" {
|
||||
<!-- LOGGED IN: Show user menu with logout -->
|
||||
<button onclick="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
|
||||
</button>
|
||||
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<div class="py-1">
|
||||
<a href="/settings" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Settings
|
||||
</a>
|
||||
}
|
||||
<div class="border-t my-1" style="border-color: var(--border);"></div>
|
||||
<button onclick="logout()" class="block w-full text-left px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary);">
|
||||
Logout
|
||||
</button>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Admin Panel
|
||||
</a>
|
||||
}
|
||||
<div class="border-t my-1" style="border-color: var(--border);"></div>
|
||||
<button onclick="logout()" class="block w-full text-left px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary);">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<!-- LOGGED OUT: Show login form -->
|
||||
<button onclick="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">Login</span>
|
||||
</button>
|
||||
<div id="user-menu" class="hidden absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50 p-4" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<form hx-post="/api/auth/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-3">
|
||||
<input type="hidden" name="redirect" value="{ currentPath }">
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required>
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2 rounded text-sm" style="background-color: var(--accent); color: white;">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div id="login-result"></div>
|
||||
<div class="border-t my-2" style="border-color: var(--border);"></div>
|
||||
<a href="/register" class="block text-center text-sm hover:opacity-80" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Don't have an account? Sign Up
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-17
File diff suppressed because one or more lines are too long
+40
-2
@@ -1,3 +1,4 @@
|
||||
/* Tailwind CSS directives - required for framework */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -15,8 +16,8 @@
|
||||
.theme-tokyo-night {
|
||||
--bg-primary: #1a1b26;
|
||||
--bg-secondary: #16161e;
|
||||
--text-primary: #a9b1d6;
|
||||
--text-secondary: #565f89;
|
||||
--text-primary: #c0caf5;
|
||||
--text-secondary: #c0caf5;
|
||||
--accent: #7aa2f7;
|
||||
--border: #414868;
|
||||
}
|
||||
@@ -143,6 +144,24 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Header component styles */
|
||||
.header-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Make header sticky only on docs pages */
|
||||
body:not(.page-docs) .header-nav {
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* Add class to body on docs pages */
|
||||
.page-docs .header-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border);
|
||||
@@ -184,4 +203,23 @@
|
||||
.priority-5 { background-color: #10b981; color: white; }
|
||||
.priority-7 { background-color: #6b7280; color: white; }
|
||||
.priority-10 { background-color: #9ca3af; color: white; }
|
||||
|
||||
/* Override highlight.js hardcoded colors for code blocks */
|
||||
.prose pre code.hljs {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.prose code.hljs {
|
||||
background-color: #1a1b26 !important;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
background-color: #16161e !important;
|
||||
}
|
||||
|
||||
.prose .hljs {
|
||||
background-color: transparent !important;
|
||||
color: #c0caf5 !important;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user