diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..d70c7a9 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,9 @@ +{ + "languages": { + "CSS": { + "language_servers": ["tailwindcss-language-server"], + "tab_size": 2, + "formatter": "auto", + }, + }, +} diff --git a/PROJECT_GUIDELINES.md b/PROJECT_GUIDELINES.md index f25d7e7..8ea0094 100644 --- a/PROJECT_GUIDELINES.md +++ b/PROJECT_GUIDELINES.md @@ -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) diff --git a/SCREENSHOT_AUTOMATION.md b/SCREENSHOT_AUTOMATION.md new file mode 100644 index 0000000..b223003 --- /dev/null +++ b/SCREENSHOT_AUTOMATION.md @@ -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 + +![Login page showing username and password fields](../images/user/login-page.webp) +``` + +## 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 diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 7229638..b2db201 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -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 diff --git a/docs/contributing/DEVELOPMENT.md b/docs/contributing/development.md similarity index 100% rename from docs/contributing/DEVELOPMENT.md rename to docs/contributing/development.md diff --git a/docs/index.md b/docs/index.md index 7d10383..5842c12 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/internal/docs/http_handler.go b/internal/docs/http_handler.go index 272f048..d670dfd 100644 --- a/internal/docs/http_handler.go +++ b/internal/docs/http_handler.go @@ -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") diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 401dae9..4e5d992 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -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(`
Login successful! Redirecting...
`, 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'; +`, 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) } diff --git a/scripts/verify-guidelines.sh b/scripts/verify-guidelines.sh index b1dba14..e3eb6c8 100755 --- a/scripts/verify-guidelines.sh +++ b/scripts/verify-guidelines.sh @@ -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: " 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 "- " + fi fi ############################################################################### diff --git a/tailwind.config.ts b/tailwind.config.ts index ec1f9e2..68c2cd0 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -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: [ diff --git a/templates/docs.templ b/templates/docs.templ index 3ad9fa2..740d185 100644 --- a/templates/docs.templ +++ b/templates/docs.templ @@ -4,41 +4,22 @@ import ( "fmt" ) -templ DocsLayout(nav Navigation, doc Document, user User) { +templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) { { doc.Title } - Bookhoard Documentation - - - - - - + + + + + + - + + @Header(user, currentPath)
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Documentation") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header(user, currentPath).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, section := range nav.Sections { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 76, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 57, Col: 28} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.Collapsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, item := range section.Items { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 85, Col: 40} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 85, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 65, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range section.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 74, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 74, Col: 61} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(doc.Breadcrumb) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 98, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(doc.TOC) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
On this page ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range doc.TOC { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + var templ_7745c5c3_Var15 templ.SafeURL + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor) if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 129, Col: 20} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 109, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline\" style=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 111, Col: 66} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 113, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -314,325 +344,7 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func DocsLayoutWithExplorer(nav Navigation, doc Document, user User, explorer APIExplorerData) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var16 := templ.GetChildren(ctx) - if templ_7745c5c3_Var16 == nil { - templ_7745c5c3_Var16 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 363, Col: 21} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " - Bookhoard Documentation
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, section := range nav.Sections { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 442, Col: 28} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if section.Collapsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if len(doc.Breadcrumb) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 483, Col: 89} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if len(doc.TOC) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
On this page ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, item := range doc.TOC { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 495, Col: 20} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = UnsafeHTML(doc.Content).ToComponent().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = APIExplorer(explorer).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/header.templ b/templates/header.templ index b5dd396..8ea2f07 100644 --- a/templates/header.templ +++ b/templates/header.templ @@ -1,7 +1,7 @@ package templates templ Header(user User, currentPath string) { -