Improve docs search UX, dash cleanup, and colored modality tool icons (#338)

* feat(docs): improve search UX

- Hide the 'Search by Pagefind' branding in the search dialog footer
- Replace the 'No results found.' message shown before any query with a
  friendly hint (detected via empty-input :placeholder-shown state)
- Tune placeholder, empty-state, and results-heading copy; show a few
  sub-section matches per result (pageResultCount)

* docs: replace double-dash em-dash substitute with single dash

Swept prose ' -- ' to ' - ' and numeric ranges (e.g. 2--20 to 2-20)
across the documentation. CLI flags and code blocks left untouched.

* fix(landing): show colored category icons on modality tool pages

The /tools/<modality>/ pages rendered bare name+description cards with no
icon or color. Port the colored, category-tinted icon card from /tools/
so modality pages match the main catalog (icon, tint, 'Learn more').
This commit is contained in:
SnapOtter
2026-06-24 12:06:31 +08:00
committed by GitHub
parent deace744c9
commit 607c24895b
20 changed files with 182 additions and 73 deletions
+8 -1
View File
@@ -49,7 +49,14 @@ export default defineConfig({
vite: { vite: {
plugins: [ plugins: [
pagefindPlugin(), pagefindPlugin({
btnPlaceholder: "Search",
placeholder: "Search tools, guides, and the API…",
emptyText: "No matches found. Try a different term or check the spelling.",
heading: "{{searchResult}} results",
// Show a few sub-section matches per page so deep-linked headings surface.
pageResultCount: 3,
}),
llmstxt({ llmstxt({
domain: "https://docs.snapotter.com", domain: "https://docs.snapotter.com",
customLLMsTxtTemplate: `# {title} customLLMsTxtTemplate: `# {title}
+1
View File
@@ -4,6 +4,7 @@ import Layout from "./Layout.vue";
import "./vars.css"; import "./vars.css";
import "./fonts.css"; import "./fonts.css";
import "./github-stars.css"; import "./github-stars.css";
import "./search.css";
export default { export default {
extends: DefaultTheme, extends: DefaultTheme,
+58
View File
@@ -0,0 +1,58 @@
/**
* Customizations for the Pagefind-powered search dialog
* (vitepress-plugin-pagefind, rendered via vue-command-palette `algolia` theme).
*
* Two goals:
* 1. Hide the "Search by Pagefind" branding in the dialog footer.
* 2. Replace the misleading "No results found." text shown the instant the
* dialog opens (before the user has typed anything) with a friendly hint.
* We can't change the component's markup, so we detect an empty query via
* the input's `:placeholder-shown` state and restyle the empty slot.
*/
/* 1. Drop the Pagefind logo/attribution in the footer.
The keyboard-shortcut hints (to select / navigate / close) stay. */
.algolia .command-palette-logo {
display: none;
}
/* 2a. Before anything is typed (input still shows its placeholder), turn the
empty slot into a centered hint with an icon instead of "No results found." */
.algolia
[command-dialog-wrapper]:has([command-input]:placeholder-shown)
[command-empty=""] {
flex-direction: column;
gap: 10px;
height: auto;
min-height: 160px;
/* Hide the component's default empty text without removing it from the DOM. */
font-size: 0;
}
.algolia
[command-dialog-wrapper]:has([command-input]:placeholder-shown)
[command-empty=""]::before {
content: "";
width: 28px;
height: 28px;
background-color: var(--vp-c-brand-1);
/* magnifying-glass mask so the icon picks up the brand (Otter Orange) */
-webkit-mask: var(--search-hint-icon) center / contain no-repeat;
mask: var(--search-hint-icon) center / contain no-repeat;
opacity: 0.9;
}
.algolia
[command-dialog-wrapper]:has([command-input]:placeholder-shown)
[command-empty=""]::after {
content: "Search 157 tools, guides, and the API reference";
font-size: 14px;
line-height: 1.4;
color: var(--vp-c-text-2);
max-width: 320px;
text-align: center;
}
:root {
--search-hint-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z' stroke='black' fill='none' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
+2 -2
View File
@@ -6,7 +6,7 @@ description: AI engine reference with all local ML tools. Background removal, up
The `@snapotter/ai` package bridges Node.js to a **persistent Python sidecar** for all ML operations. The dispatcher process stays alive between requests for fast warm-start performance. GPU is auto-detected at startup and used when available. The `@snapotter/ai` package bridges Node.js to a **persistent Python sidecar** for all ML operations. The dispatcher process stays alive between requests for fast warm-start performance. GPU is auto-detected at startup and used when available.
19 Python sidecar AI tools across four modalities (image, audio, video, document), plus 2 tools with optional AI capabilities. All models run locally -- no internet required after initial model download. 19 Python sidecar AI tools across four modalities (image, audio, video, document), plus 2 tools with optional AI capabilities. All models run locally - no internet required after initial model download.
## Architecture ## Architecture
@@ -406,7 +406,7 @@ An additional analysis endpoint is available at `POST /api/v1/tools/image/image-
### Content-Aware Resize (Seam Carving) ### Content-Aware Resize (Seam Carving)
**Tool route:** `content-aware-resize` **Tool route:** `content-aware-resize`
**Engine:** Go `caire` binary (not Python -- no GPU benefit) **Engine:** Go `caire` binary (not Python - no GPU benefit)
Intelligently resizes images by removing low-energy seams, preserving important content. Intelligently resizes images by removing low-energy seams, preserving important content.
+23 -23
View File
@@ -65,11 +65,11 @@ Live demo, per-tool landing pages, and a batch of polish fixes.
### New features ### New features
- **Live demo** -- [demo.snapotter.com](https://demo.snapotter.com) lets people try SnapOtter without installing anything. - **Live demo** - [demo.snapotter.com](https://demo.snapotter.com) lets people try SnapOtter without installing anything.
- **Tools index page** -- Browse all 50+ tools at `/tools` with search and category filters. - **Tools index page** - Browse all 50+ tools at `/tools` with search and category filters.
- **50+ SEO landing pages** -- Every tool now has a dedicated landing page with FAQs, use cases, and comparison tables. - **50+ SEO landing pages** - Every tool now has a dedicated landing page with FAQs, use cases, and comparison tables.
- **Background preview** -- Before-after slider shows a checkered background behind transparent images. - **Background preview** - Before-after slider shows a checkered background behind transparent images.
- **Strong password generator** -- One-click button in the Add Members form. - **Strong password generator** - One-click button in the Add Members form.
### Bug fixes ### Bug fixes
@@ -110,28 +110,28 @@ Five new tools, a full image editor, SSO login, 20 languages. Probably should ha
### New features ### New features
- **Image editor** -- Layers, brushes, shapes, adjustments, filters, curves, keyboard shortcuts. Runs in your browser, processes on your hardware. - **Image editor** - Layers, brushes, shapes, adjustments, filters, curves, keyboard shortcuts. Runs in your browser, processes on your hardware.
- **OIDC / SSO authentication** -- Login with Google, GitHub, Okta, or any OpenID Connect provider. Set a few env vars and your team uses their existing accounts. - **OIDC / SSO authentication** - Login with Google, GitHub, Okta, or any OpenID Connect provider. Set a few env vars and your team uses their existing accounts.
- **Meme generator** -- 100 built-in templates with text rendering via opentype.js. Or upload your own image. - **Meme generator** - 100 built-in templates with text rendering via opentype.js. Or upload your own image.
- **Beautify** -- Drop a screenshot in, get a polished image out. Device frames (macOS, Windows, browser), shadows, gradients, social media presets. - **Beautify** - Drop a screenshot in, get a polished image out. Device frames (macOS, Windows, browser), shadows, gradients, social media presets.
- **Color blindness simulation** -- Preview how images look with protanopia, deuteranopia, tritanopia, and other color vision deficiencies. - **Color blindness simulation** - Preview how images look with protanopia, deuteranopia, tritanopia, and other color vision deficiencies.
- **PNG transparency fixer** -- Detects fake-transparent PNGs and fixes them with BiRefNet HR-matting. Optional watermark removal via LaMa inpainting. - **PNG transparency fixer** - Detects fake-transparent PNGs and fixes them with BiRefNet HR-matting. Optional watermark removal via LaMa inpainting.
- **AI canvas expand** -- Extend image boundaries with AI fill. Three quality tiers (fast, balanced, quality) depending on how much GPU time you want to trade. - **AI canvas expand** - Extend image boundaries with AI fill. Three quality tiers (fast, balanced, quality) depending on how much GPU time you want to trade.
- **20 languages** -- Arabic, Chinese (Simplified/Traditional), Czech, Dutch, French, German, Hindi, Indonesian, Italian, Japanese, Korean, Polish, Portuguese, Russian, Spanish, Thai, Turkish, Ukrainian, Vietnamese. RTL works for Arabic. - **20 languages** - Arabic, Chinese (Simplified/Traditional), Czech, Dutch, French, German, Hindi, Indonesian, Italian, Japanese, Korean, Polish, Portuguese, Russian, Spanish, Thai, Turkish, Ukrainian, Vietnamese. RTL works for Arabic.
- **URL import** -- Paste URLs into the dropzone or bulk-import from a list. Server-side fetch with SSRF protection. - **URL import** - Paste URLs into the dropzone or bulk-import from a list. Server-side fetch with SSRF protection.
- **Multi-file eraser** -- Draw erase masks across multiple images, process them all with one click. Strokes persist per-image. - **Multi-file eraser** - Draw erase masks across multiple images, process them all with one click. Strokes persist per-image.
- **Pipeline import/export** -- Save tool chains as JSON, share them with others. - **Pipeline import/export** - Save tool chains as JSON, share them with others.
- **17 new camera RAW formats** via exiftool, plus QOI, JP2, EPS, DDS, CUR, DPX, FITS, PPM/PGM/PBM, SVGZ, and APNG input. New output codecs for BMP, ICO, JP2, QOI. AVIF, TIFF, GIF, JXL, and PSD export recovered from a previously lost branch. - **17 new camera RAW formats** via exiftool, plus QOI, JP2, EPS, DDS, CUR, DPX, FITS, PPM/PGM/PBM, SVGZ, and APNG input. New output codecs for BMP, ICO, JP2, QOI. AVIF, TIFF, GIF, JXL, and PSD export recovered from a previously lost branch.
### Improvements ### Improvements
- **Image enhancement** -- Replaced the old pipeline with CLAHE + normalise + gamma. New Deep Enhance toggle uses the AI model for more aggressive results. - **Image enhancement** - Replaced the old pipeline with CLAHE + normalise + gamma. New Deep Enhance toggle uses the AI model for more aggressive results.
- **Restore photo** -- Scratch detection rewritten with 8-angle Otsu filtering. LaMa inpainting now runs at native resolution. - **Restore photo** - Scratch detection rewritten with 8-angle Otsu filtering. LaMa inpainting now runs at native resolution.
- **Exotic formats everywhere** -- OCR, image-to-PDF, favicon generator, composition, stitch, and vectorize all decode HEIC, RAW, PSD now. - **Exotic formats everywhere** - OCR, image-to-PDF, favicon generator, composition, stitch, and vectorize all decode HEIC, RAW, PSD now.
- **Compress** -- Target-size tolerance tightened from 5% to 1%. Target size is the default mode. Added stepper buttons and KB/MB unit selector. - **Compress** - Target-size tolerance tightened from 5% to 1%. Target size is the default mode. Added stepper buttons and KB/MB unit selector.
- **Sentry cleanup** -- 644 non-actionable events filtered. Real errors now handled properly. - **Sentry cleanup** - 644 non-actionable events filtered. Real errors now handled properly.
- **GPU detection** -- Better diagnostics for containers where CUDA is present but nvidia-smi is not. - **GPU detection** - Better diagnostics for containers where CUDA is present but nvidia-smi is not.
- **Auth-disabled mode** -- Anonymous user is seeded in the DB with admin role. API keys, pipelines, and user files no longer break on FK constraints. - **Auth-disabled mode** - Anonymous user is seeded in the DB with admin role. API keys, pipelines, and user files no longer break on FK constraints.
- **2,705+ new tests** across unit, integration, and E2E. - **2,705+ new tests** across unit, integration, and E2E.
### Bug fixes ### Bug fixes
+4 -4
View File
@@ -51,14 +51,14 @@ Shared TypeScript types, constants (like `APP_VERSION` and tool definitions), an
A Fastify v5 server exposing 157 tool routes across five modalities (image, video, audio, document, data) that handles: A Fastify v5 server exposing 157 tool routes across five modalities (image, video, audio, document, data) that handles:
- File uploads, temporary workspace management, and persistent file storage - File uploads, temporary workspace management, and persistent file storage
- User file library with version chains (`user_files` table) -- each processed result links back to its source file and records which tool was applied, with auto-generated thumbnails for the Files page - User file library with version chains (`user_files` table) - each processed result links back to its source file and records which tool was applied, with auto-generated thumbnails for the Files page
- Tool execution (routes each tool request to the image engine or AI bridge) - Tool execution (routes each tool request to the image engine or AI bridge)
- Pipeline orchestration (chaining multiple tools sequentially) - Pipeline orchestration (chaining multiple tools sequentially)
- Batch processing with concurrency control via BullMQ job queues (pools: image, media, ai, docs, system) - Batch processing with concurrency control via BullMQ job queues (pools: image, media, ai, docs, system)
- User authentication, RBAC (admin/user roles with a full permission set), API key management, and rate limiting - User authentication, RBAC (admin/user roles with a full permission set), API key management, and rate limiting
- Teams management -- admin-only CRUD; users are assigned to a team via the `team` field on their profile - Teams management - admin-only CRUD; users are assigned to a team via the `team` field on their profile
- Runtime settings -- a key-value store in the `settings` table that controls `disabledTools`, `enableExperimentalTools`, `loginAttemptLimit`, and other operational knobs without redeploying - Runtime settings - a key-value store in the `settings` table that controls `disabledTools`, `enableExperimentalTools`, `loginAttemptLimit`, and other operational knobs without redeploying
- Custom branding -- logo upload endpoint; the uploaded image is stored at `data/branding/logo.png` and served to the frontend - Custom branding - logo upload endpoint; the uploaded image is stored at `data/branding/logo.png` and served to the frontend
- Swagger/OpenAPI documentation at `/api/docs` - Swagger/OpenAPI documentation at `/api/docs`
- Serving the built frontend as a SPA in production - Serving the built frontend as a SPA in production
+4 -4
View File
@@ -142,7 +142,7 @@ volumes:
The Docker Compose stack uses four volumes: The Docker Compose stack uses four volumes:
- `/data` (app) -- AI models, Python venv, and user files. Mount this to keep uploaded files and installed AI bundles across restarts. - `/data` (app) - AI models, Python venv, and user files. Mount this to keep uploaded files and installed AI bundles across restarts.
- `/tmp/workspace` (app) -- Temporary storage for files being processed. This can be ephemeral, but mounting it avoids filling up the container's writable layer. - `/tmp/workspace` (app) - Temporary storage for files being processed. This can be ephemeral, but mounting it avoids filling up the container's writable layer.
- `SnapOtter-pgdata` (postgres) -- PostgreSQL data directory. This holds all relational data (users, settings, pipelines, jobs, audit log). Back up via `pg_dump` or volume snapshot. - `SnapOtter-pgdata` (postgres) - PostgreSQL data directory. This holds all relational data (users, settings, pipelines, jobs, audit log). Back up via `pg_dump` or volume snapshot.
- `SnapOtter-redisdata` (redis) -- Redis append-only file for durable job queues. - `SnapOtter-redisdata` (redis) - Redis append-only file for durable job queues.
+6 -6
View File
@@ -11,7 +11,7 @@ See [Docker Image](./docker-tags) for GPU setup, Docker Compose examples, and ve
## Quick Start (CPU) ## Quick Start (CPU)
```yaml ```yaml
# docker-compose.yml -- Copy this file and run: docker compose up -d # docker-compose.yml - Copy this file and run: docker compose up -d
services: services:
SnapOtter: SnapOtter:
image: snapotter/snapotter:latest # or ghcr.io/snapotter-hq/snapotter:latest image: snapotter/snapotter:latest # or ghcr.io/snapotter-hq/snapotter:latest
@@ -94,7 +94,7 @@ services:
start_period: 10s start_period: 10s
volumes: volumes:
SnapOtter-data: # Named volume -- Docker manages permissions automatically SnapOtter-data: # Named volume - Docker manages permissions automatically
SnapOtter-workspace: SnapOtter-workspace:
SnapOtter-pgdata: SnapOtter-pgdata:
SnapOtter-redisdata: SnapOtter-redisdata:
@@ -113,7 +113,7 @@ The app is then available at `http://localhost:1349`.
For NVIDIA GPU acceleration on AI tools (background removal, upscaling, face enhancement, OCR): For NVIDIA GPU acceleration on AI tools (background removal, upscaling, face enhancement, OCR):
```yaml ```yaml
# docker-compose-gpu.yml -- Requires: NVIDIA GPU + nvidia-container-toolkit # docker-compose-gpu.yml - Requires: NVIDIA GPU + nvidia-container-toolkit
# Install toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html # Install toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
services: services:
SnapOtter: SnapOtter:
@@ -511,9 +511,9 @@ Note: Cloudflare has a 100 MB upload limit on free plans. Set `MAX_UPLOAD_SIZE_M
The GitHub repository has three workflows: The GitHub repository has three workflows:
- **ci.yml** -- Runs automatically on every push and PR. Lints, typechecks, tests, builds, and validates the Docker image (without pushing). - **ci.yml** - Runs automatically on every push and PR. Lints, typechecks, tests, builds, and validates the Docker image (without pushing).
- **release.yml** -- Triggered manually via `workflow_dispatch`. Runs semantic-release to create a version tag and GitHub release, then builds a multi-arch Docker image (amd64 + arm64) and pushes to Docker Hub (`snapotter/snapotter`) and GitHub Container Registry (`ghcr.io/snapotter-hq/snapotter`). - **release.yml** - Triggered manually via `workflow_dispatch`. Runs semantic-release to create a version tag and GitHub release, then builds a multi-arch Docker image (amd64 + arm64) and pushes to Docker Hub (`snapotter/snapotter`) and GitHub Container Registry (`ghcr.io/snapotter-hq/snapotter`).
- **deploy-docs.yml** -- Builds this documentation site and deploys it to Cloudflare Pages on push to `main`. - **deploy-docs.yml** - Builds this documentation site and deploys it to Cloudflare Pages on push to `main`.
To create a release, go to **Actions > Release > Run workflow** in the GitHub UI, or run: To create a release, go to **Actions > Release > Run workflow** in the GitHub UI, or run:
+1 -1
View File
@@ -79,7 +79,7 @@ pnpm test:coverage # tests with coverage report
## Database ## Database
PostgreSQL 17 via Drizzle ORM (pg-core). Local dev requires Postgres and Redis running -- start them with: PostgreSQL 17 via Drizzle ORM (pg-core). Local dev requires Postgres and Redis running - start them with:
```bash ```bash
docker compose -f docker-compose.dev.yml up -d docker compose -f docker-compose.dev.yml up -d
+1 -1
View File
@@ -5,7 +5,7 @@ description: Install SnapOtter with Docker in one command. Includes Docker Compo
# Getting Started # Getting Started
::: tip Try before installing ::: tip Try before installing
Explore the full UI at [demo.snapotter.com](https://demo.snapotter.com) -- no signup or install required. Explore the full UI at [demo.snapotter.com](https://demo.snapotter.com) - no signup or install required.
::: :::
## Quick Start ## Quick Start
+1 -1
View File
@@ -32,7 +32,7 @@ The response contains the token. Save it immediately; it cannot be retrieved aga
```json ```json
{ {
"token": "a1b2c3d4e5f6...", "token": "a1b2c3d4e5f6...",
"message": "Save this token -- it cannot be retrieved again" "message": "Save this token - it cannot be retrieved again"
} }
``` ```
+2 -2
View File
@@ -36,7 +36,7 @@ services:
# --- Resource limits --- # --- Resource limits ---
mem_limit: 6g # Prevents runaway memory from crashing the host mem_limit: 6g # Prevents runaway memory from crashing the host
memswap_limit: 6g # No swap -- fail fast instead of degrading the host memswap_limit: 6g # No swap - fail fast instead of degrading the host
cpus: 4 # Cap CPU usage to 4 cores cpus: 4 # Cap CPU usage to 4 cores
pids_limit: 512 # Prevents fork bombs pids_limit: 512 # Prevents fork bombs
@@ -130,7 +130,7 @@ The only exception is **AI model downloads**: when a user installs an AI feature
|---|---| |---|---|
| Air-gapped (no AI) | Block all outbound traffic from the container | | Air-gapped (no AI) | Block all outbound traffic from the container |
| AI bundles needed | Allow HTTPS to `github.com`, `objects.githubusercontent.com`, `pypi.org`, `files.pythonhosted.org` during install, then block | | AI bundles needed | Allow HTTPS to `github.com`, `objects.githubusercontent.com`, `pypi.org`, `files.pythonhosted.org` during install, then block |
| After AI install | Block all outbound traffic -- models are cached locally | | After AI install | Block all outbound traffic - models are cached locally |
For reverse proxy configuration (Nginx, Traefik, Caddy, Cloudflare Tunnels), see the [Deployment guide](/guide/deployment#reverse-proxy). For reverse proxy configuration (Nginx, Traefik, Caddy, Cloudflare Tunnels), see the [Deployment guide](/guide/deployment#reverse-proxy).
+5 -5
View File
@@ -36,10 +36,10 @@ SnapOtter ships with 21 languages out of the box. The i18n system uses a lightwe
SnapOtter uses a three-tier resolution order: SnapOtter uses a three-tier resolution order:
1. **User preference** -- stored in `localStorage("snapotter-locale")` and synced to user settings when authenticated 1. **User preference** - stored in `localStorage("snapotter-locale")` and synced to user settings when authenticated
2. **Browser auto-detect** -- walks the `navigator.languages` array with BCP 47 prefix matching 2. **Browser auto-detect** - walks the `navigator.languages` array with BCP 47 prefix matching
3. **Instance default** -- the admin's `DEFAULT_LOCALE` env var (fetched from `GET /api/v1/config/locale`) 3. **Instance default** - the admin's `DEFAULT_LOCALE` env var (fetched from `GET /api/v1/config/locale`)
4. **English fallback** -- always available 4. **English fallback** - always available
Users can change language from: Users can change language from:
- The **footer Globe selector** (desktop, always visible) - The **footer Globe selector** (desktop, always visible)
@@ -152,7 +152,7 @@ Open a PR against `main` with a title like `feat(i18n): add Swedish translation`
When adding a new feature that needs new UI strings: When adding a new feature that needs new UI strings:
1. Add the new keys to `en.ts` first (the reference file) 1. Add the new keys to `en.ts` first (the reference file)
2. Run `pnpm typecheck` -- every locale file will fail if missing the new key 2. Run `pnpm typecheck` - every locale file will fail if missing the new key
3. Add the new key to all locale files (use English as a temporary fallback) 3. Add the new key to all locale files (use English as a temporary fallback)
## Configuration ## Configuration
+4 -4
View File
@@ -31,10 +31,10 @@ The default admin is required to change their password on first login.
Users can authenticate through several methods: Users can authenticate through several methods:
- **Local** -- username and password stored in the SnapOtter database - **Local** - username and password stored in the SnapOtter database
- **OIDC** -- any OpenID Connect provider (see [OIDC / SSO](/guide/oidc)) - **OIDC** - any OpenID Connect provider (see [OIDC / SSO](/guide/oidc))
- **SAML** -- SAML 2.0 identity providers (see [SAML SSO](/guide/saml)) - **SAML** - SAML 2.0 identity providers (see [SAML SSO](/guide/saml))
- **SCIM** -- automated provisioning from an identity provider (see [SCIM Provisioning](/guide/scim)) - **SCIM** - automated provisioning from an identity provider (see [SCIM Provisioning](/guide/scim))
### Disabling authentication ### Disabling authentication
+2 -2
View File
@@ -18,8 +18,8 @@ Accepts multipart form data with a CSV or JSON file and a JSON `settings` field.
|-----------|------|----------|---------|-------------| |-----------|------|----------|---------|-------------|
| kind | string | No | `"bar"` | Chart type: `bar`, `line`, `pie` | | kind | string | No | `"bar"` | Chart type: `bar`, `line`, `pie` |
| title | string | No | - | Chart title (max 120 characters) | | title | string | No | - | Chart title (max 120 characters) |
| width | integer | No | `960` | Chart width in pixels (320--2048) | | width | integer | No | `960` | Chart width in pixels (320-2048) |
| height | integer | No | `540` | Chart height in pixels (240--1536) | | height | integer | No | `540` | Chart height in pixels (240-1536) |
## Example Request ## Example Request
+1 -1
View File
@@ -14,7 +14,7 @@ Accepts multipart form data with two or more files. No settings field is require
## Parameters ## Parameters
This tool has no configurable parameters. Upload 2--50 files of any type to bundle. This tool has no configurable parameters. Upload 2-50 files of any type to bundle.
## Example Request ## Example Request
+1 -1
View File
@@ -14,7 +14,7 @@ Accepts multipart form data with two or more CSV files. No settings field is req
## Parameters ## Parameters
This tool has no configurable parameters. Upload 2--20 CSV or TSV files with matching column headers. This tool has no configurable parameters. Upload 2-20 CSV or TSV files with matching column headers.
## Example Request ## Example Request
+1 -1
View File
@@ -16,7 +16,7 @@ Accepts multipart form data with a CSV file and a JSON `settings` field.
| Parameter | Type | Required | Default | Description | | Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------| |-----------|------|----------|---------|-------------|
| rowsPerFile | integer | No | `1000` | Number of data rows per output file (1--1,000,000) | | rowsPerFile | integer | No | `1000` | Number of data rows per output file (1-1,000,000) |
| keepHeader | boolean | No | `true` | Repeat the header row in each output file | | keepHeader | boolean | No | `true` | Repeat the header row in each output file |
## Example Request ## Example Request
+1 -1
View File
@@ -8,7 +8,7 @@ Smart subject-aware, face-aware, or trim-based cropping. Uses Sharp's attention/
**Processing:** Asynchronous (returns 202, poll `/api/v1/jobs/{jobId}/progress` for status via SSE) **Processing:** Asynchronous (returns 202, poll `/api/v1/jobs/{jobId}/progress` for status via SSE)
**Model bundle:** `face-detection` (200-300 MB) -- required only for `face` mode **Model bundle:** `face-detection` (200-300 MB) - required only for `face` mode
## Parameters ## Parameters
@@ -1,5 +1,6 @@
--- ---
import { SECTIONS, TOOLS, toolSection } from "@snapotter/shared"; import { CATEGORIES, SECTIONS, TOOLS, toolSection } from "@snapotter/shared";
import * as lucideIcons from "lucide";
import Footer from "@/components/Footer.astro"; import Footer from "@/components/Footer.astro";
import Navbar from "@/components/Navbar.astro"; import Navbar from "@/components/Navbar.astro";
import Base from "@/layouts/Base.astro"; import Base from "@/layouts/Base.astro";
@@ -11,7 +12,22 @@ export function getStaticPaths() {
})); }));
} }
function renderIcon(iconName: string): string {
const iconData = (lucideIcons as Record<string, unknown>)[iconName];
if (!iconData || !Array.isArray(iconData))
return '<rect width="14" height="14" x="5" y="5" rx="2" />';
return iconData
.map(([tag, attrs]: [string, Record<string, string>]) => {
const attrStr = Object.entries(attrs)
.map(([k, v]) => `${k}="${v}"`)
.join(" ");
return `<${tag} ${attrStr}/>`;
})
.join("");
}
const { section, tools } = Astro.props; const { section, tools } = Astro.props;
const categoryMap = new Map(CATEGORIES.map((c) => [c.id, c]));
const canonical = `https://snapotter.com/tools/${section.id}/`; const canonical = `https://snapotter.com/tools/${section.id}/`;
const title = `${section.name} Tools | SnapOtter`; const title = `${section.name} Tools | SnapOtter`;
--- ---
@@ -41,18 +57,45 @@ const title = `${section.name} Tools | SnapOtter`;
<section class="mx-auto max-w-6xl px-6 pt-8 pb-16"> <section class="mx-auto max-w-6xl px-6 pt-8 pb-16">
<div class="reveal"> <div class="reveal">
<h1 class="font-display text-3xl font-bold">{section.name} Tools</h1> <h1 class="font-display text-3xl font-bold">{section.name} Tools</h1>
<ul class="mt-8 grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <ul class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{tools.map((tool) => ( {tools.map((tool) => {
<li> const cat = categoryMap.get(tool.category);
<a const color = cat?.color ?? "#E07832";
class="card-hover block rounded-lg border border-border p-4" return (
href={`/tools/${section.id}/${tool.id}/`} <li>
> <a
<span class="font-medium">{tool.name}</span> class="tool-item group flex items-start gap-4 rounded-xl border border-border bg-surface p-5 transition-all hover:border-primary hover:shadow-md"
<span class="mt-1 block text-sm text-muted">{tool.description}</span> href={`/tools/${section.id}/${tool.id}/`}
</a> >
</li> <div
))} class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
style={`background-color: ${color}14`}
>
<svg
class="h-[18px] w-[18px]"
viewBox="0 0 24 24"
fill="none"
stroke={color}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
set:html={renderIcon(tool.icon)}
/>
</div>
<div class="min-w-0">
<span class="text-sm font-bold">{tool.name}</span>
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">{tool.description}</p>
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-all group-hover:gap-2">
Learn more
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
</svg>
</span>
</div>
</a>
</li>
);
})}
</ul> </ul>
</div> </div>
</section> </section>