diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 00000000..8de9d583 --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "svelte": { + "command": "npx", + "args": ["-y", "@sveltejs/mcp"] + } + } +} diff --git a/CLAUDE.md b/AGENTS.md similarity index 99% rename from CLAUDE.md rename to AGENTS.md index 843fdc9b..36d041bd 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -5,11 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands **Development:** + - `npm run dev` - Start development server - `npm run build` - Build for production - `npm run preview` - Preview production build **Implementation Guidelines:** + - Before implementing any feature, read `ARCHITECTURE.md` for design decisions and `IMPLEMENTATION_PLAN.md` for the step-by-step implementation spec - Design decisions go in `ARCHITECTURE.md`, implementation steps go in `IMPLEMENTATION_PLAN.md` - New features must be specified before implementation begins — the spec should be concise but sufficient to derive the implementation from @@ -20,22 +22,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - You can suggest what the next step could be, but don't implement it **Refactoring Guidelines:** + - During refactors, make ONLY the minimal changes needed (e.g., renaming APIs) - Do NOT "improve" or restructure logic while refactoring - If you see something that could be improved, note it separately for a future task - Refactoring and improving are two separate activities - never combine them **Code Style:** + - Use snake_case for all variable names, function names, and identifiers - This applies to JavaScript/TypeScript code, test files, and any new code written **Styling:** + - Use Tailwind CSS classes whenever possible - Minimize custom CSS - only use it for things Tailwind can't handle (e.g., CSS custom properties like `var(--svedit-editing-stroke)`) - Use Tailwind's arbitrary value syntax for custom properties: `text-(--svedit-editing-stroke)`, `border-(--svedit-editing-stroke)` - Do not use rounded corners (keep elements rectangular) **What to NOT change (keep camelCase):** + - `window.getSelection()` - native API - `document.activeElement` - native API - `navigator.clipboard` - native API @@ -49,9 +55,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **Pattern**: If it's a web platform API or Svelte API, keep camelCase. If it's our custom variable/function name, use snake_case. **File Extensions:** + - Files using Svelte runes (`$state`, `$derived`, `$effect`, etc.) must use `.svelte.js` or `.svelte.ts` extension **Documentation Style:** + - Use sentence case for all headings in documentation (README.md, etc.) - Use sentence case for code comments - Sentence case means: capitalize only the first word and proper nouns @@ -72,16 +80,19 @@ Svedit is a rich content editor template built with Svelte 5 that uses a graph-b ### Core Components **Document Model:** + - `Document` - Central document class with state management, transactions, and history - `Tras` - Handles atomic operations on the document - Documents are represented as graphs of nodes with properties and references **Selection:** + - Supports text, node, and property selections - Maps between internal selection model and DOM selection - Handles complex selection scenarios like backwards selections and multi-node selections **Key Components:** + - `Svedit.svelte` - Main editor component with event handling and selection management - `NodeArrayProperty.svelte` - Renders containers that hold sequences of nodes - `AnnotatedTextProperty.svelte` - Handles annotated text rendering and editing @@ -90,6 +101,7 @@ Svedit is a rich content editor template built with Svelte 5 that uses a graph-b ### Schema Content is defined through schemas that specify: + - Node types and their properties - Property types: `string`, `integer`, `boolean`, `string_array`, `annotated_text`, `node`, `node_array` - Reference relationships between nodes @@ -105,6 +117,7 @@ Content is defined through schemas that specify: ## Schema and Inserter When adding new properties to a node type: + 1. Add to schema in `create_session.js` (`document_schema`) 2. Add to inserter in `create_session.js` (`inserters`) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 33be4a1e..92f2a950 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -14,17 +14,17 @@ The app uses Svelte's experimental async features and SvelteKit's remote functio ```js const config = { - kit: { - adapter: adapter(), - experimental: { - remoteFunctions: true - } - }, - compilerOptions: { - experimental: { - async: true - } - } + kit: { + adapter: adapter(), + experimental: { + remoteFunctions: true + } + }, + compilerOptions: { + experimental: { + async: true + } + } }; ``` @@ -40,7 +40,7 @@ const config = { import migrate from '$lib/server/migrate.js'; export async function init() { - migrate(); + migrate(); } ``` @@ -205,16 +205,16 @@ There are two media node types: `image` and `video`. Each has the same visual pr ```json { - "id": "feature_1_image", - "type": "image", - "src": "c4b519da4c0a6512b5d9519aac0d9df7fab9152a6df109515456ada4702fabdb.webp", - "width": 1600, - "height": 900, - "alt": "Feature image", - "scale": 1.0, - "focal_point_x": 0.5, - "focal_point_y": 0.5, - "object_fit": "cover" + "id": "feature_1_image", + "type": "image", + "src": "c4b519da4c0a6512b5d9519aac0d9df7fab9152a6df109515456ada4702fabdb.webp", + "width": 1600, + "height": 900, + "alt": "Feature image", + "scale": 1.0, + "focal_point_x": 0.5, + "focal_point_y": 0.5, + "object_fit": "cover" } ``` @@ -222,16 +222,16 @@ There are two media node types: `image` and `video`. Each has the same visual pr ```json { - "id": "hero_video", - "type": "video", - "src": "e7a3f1bc...abcd.mp4", - "width": 1920, - "height": 1080, - "alt": "Product demo", - "scale": 1.0, - "focal_point_x": 0.5, - "focal_point_y": 0.5, - "object_fit": "cover" + "id": "hero_video", + "type": "video", + "src": "e7a3f1bc...abcd.mp4", + "width": 1920, + "height": 1080, + "alt": "Product demo", + "scale": 1.0, + "focal_point_x": 0.5, + "focal_point_y": 0.5, + "object_fit": "cover" } ``` @@ -297,8 +297,8 @@ The paste handler uses `get_media_type(file)` to map each file's MIME type to a ```js /** @returns {'image' | 'video'} */ function get_media_type(file) { - if (file.type.startsWith('video/')) return 'video'; - return 'image'; + if (file.type.startsWith('video/')) return 'video'; + return 'image'; } ``` @@ -384,13 +384,13 @@ variant URL = /assets/{stem}/w320.webp ```html ``` @@ -402,12 +402,12 @@ Different media types are handled differently: The asset id always includes the file extension. The stem (id without extension) is used to derive the variant directory. -| Type | Node type | Client processing | Asset id example | Variants | -|---|---|---|---|---| -| Static images (JPEG, PNG, WebP, HEIC) | `image` | Resize to `MAX_IMAGE_WIDTH`, convert to WebP via WASM | `c4b519da...fabdb.webp` | Yes (`c4b519da...fabdb/w320.webp`, etc.) | -| Animated GIFs | `image` | Passthrough | `c4b519da...fabdb.gif` | No | -| SVGs | `image` | Passthrough | `c4b519da...fabdb.svg` | No | -| Videos (MP4) | `video` | Passthrough | `c4b519da...fabdb.mp4` | No | +| Type | Node type | Client processing | Asset id example | Variants | +| ------------------------------------- | --------- | ----------------------------------------------------- | ----------------------- | ---------------------------------------- | +| Static images (JPEG, PNG, WebP, HEIC) | `image` | Resize to `MAX_IMAGE_WIDTH`, convert to WebP via WASM | `c4b519da...fabdb.webp` | Yes (`c4b519da...fabdb/w320.webp`, etc.) | +| Animated GIFs | `image` | Passthrough | `c4b519da...fabdb.gif` | No | +| SVGs | `image` | Passthrough | `c4b519da...fabdb.svg` | No | +| Videos (MP4) | `video` | Passthrough | `c4b519da...fabdb.mp4` | No | ### Image size constraints @@ -618,6 +618,41 @@ In `hooks.server.js`, on every request: - `POST /api/login` — authenticate with `{ password }`, sets session cookie - `POST /api/logout` — clears session cookie, deletes session row +## Highlights block + +The highlights block is a compact list for property-style metadata (e.g. "Units: 8"). It renders as a flex list with `justify-between`, and each row has a bottom border underline. + +Schema: + +- `highlights` (block) + - `items` — `node_array` of `highlight_item` +- `highlight_item` (block) + - `label` — `annotated_text`, single line + - `value` — `annotated_text`, single line + +Editing: + +- A visible "add row" handle appears below the list in edit mode. + +## Columns block + +The columns block is a parent layout container that holds multiple column node arrays. Each column can contain any standard page body block (prose, hero, gallery, etc.), enabling multi-column sections without changing the page body structure. + +Schema: + +- `columns` (block) + - `layout` — integer layout selector + - `columns` — `node_array` of `column` +- `column` (block) + - `content` — `node_array` of page body block types + +Layouts: + +- Layout 1: two equal columns (`md:grid-cols-2`) +- Layout 2: three equal columns (`md:grid-cols-3`) + +Mobile behavior: columns stack into a single column (`grid-cols-1`). + ## Future: optional S3 storage Assets are stored on the local filesystem (`ASSET_PATH`) by default. This keeps the app fully self-contained — a single deployment with no external dependencies beyond the server itself. For most sites this is sufficient. diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 8326069e..cd5be3e3 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -70,6 +70,7 @@ export const ASSET_BASE = '/assets'; `src/lib/config.js` (which imports from `node:path`) remains server-only and unchanged. The Web Worker and client code import from `asset-config.js`. `ASSET_BASE` is the URL prefix used to construct asset URLs on the client. All `src` values in saved documents are bare asset ids (e.g. `c4b519da...fabdb.webp`). Components prefix them with `ASSET_BASE` at render time to build the full URL. This is the only valid source for saved images — no absolute URLs, no external URLs, no relative paths. A `src` value is either: + - `blob:...` — unsaved, only valid during the current editing session - A bare asset id — saved, rendered as `{ASSET_BASE}/{asset_id}` @@ -96,10 +97,10 @@ The client computes the SHA-256 hex hash of the **source file** (before any proc ```js async function hash_blob(blob) { - const buffer = await blob.arrayBuffer(); - const hash_buffer = await crypto.subtle.digest('SHA-256', buffer); - const hash_array = Array.from(new Uint8Array(hash_buffer)); - return hash_array.map(b => b.toString(16).padStart(2, '0')).join(''); + const buffer = await blob.arrayBuffer(); + const hash_buffer = await crypto.subtle.digest('SHA-256', buffer); + const hash_array = Array.from(new Uint8Array(hash_buffer)); + return hash_array.map((b) => b.toString(16).padStart(2, '0')).join(''); } ``` @@ -166,6 +167,7 @@ ASSET_PATH/ ``` Functions: + - `asset_path(asset_id)` — full path for an original: `join(ASSET_PATH, asset_id)` - `variant_dir(asset_id)` — directory for variants: strip extension from asset_id, `join(ASSET_PATH, stem)` - `variant_path(asset_id, width)` — full path for a variant: `join(variant_dir(asset_id), 'w' + width + '.webp')` @@ -187,10 +189,10 @@ Svedit already has a `handle_image_paste` callback in `create_session.js` that f 2. Start background processing for the pasted file (runs concurrently, does not block the editor): a. Compute the SHA-256 hash of the source file. b. Determine the asset type: - - **SVG** (`image/svg+xml`): passthrough — no WASM processing. Extract dimensions via `` element. The blob is the original file. Extension: `.svg`. - - **Animated GIF** (`image/gif` with multiple frames): passthrough — no WASM processing. Extract dimensions via `` element. The blob is the original file. Extension: `.gif`. - - **Static raster image** (JPEG, PNG, WebP, HEIC, HEIF, still GIF): process via WASM worker — decode, resize if > `MAX_IMAGE_WIDTH`, encode as WebP, generate all applicable width variants. Extension: `.webp`. - c. Store the processing result (hash, original blob, variants, dimensions) keyed by the blob URL in a `Map`. This map is consulted during the save flow. + - **SVG** (`image/svg+xml`): passthrough — no WASM processing. Extract dimensions via `` element. The blob is the original file. Extension: `.svg`. + - **Animated GIF** (`image/gif` with multiple frames): passthrough — no WASM processing. Extract dimensions via `` element. The blob is the original file. Extension: `.gif`. + - **Static raster image** (JPEG, PNG, WebP, HEIC, HEIF, still GIF): process via WASM worker — decode, resize if > `MAX_IMAGE_WIDTH`, encode as WebP, generate all applicable width variants. Extension: `.webp`. + c. Store the processing result (hash, original blob, variants, dimensions) keyed by the blob URL in a `Map`. This map is consulted during the save flow. 3. The user can continue editing. Processing happens in the background. The processing/upload map and its logic should live in a dedicated module (e.g. `src/lib/client/asset-upload.js`), not inline in `create_session.js` or `+page.svelte`. @@ -224,6 +226,7 @@ Use XHR (not `fetch`) for uploads so we get `upload.onprogress` events for progr Update `save_document` in `api.remote.js` to track asset references as part of the save transaction: 1. Add the `asset_refs` table in a new migration step (`add_asset_refs`): + ```sql CREATE TABLE asset_refs ( asset_id TEXT NOT NULL, @@ -248,9 +251,9 @@ The Svedit image component needs to handle two `src` modes: ```html ``` @@ -259,6 +262,7 @@ Only include variants for widths strictly smaller than the image's `width`. The ### File summary New files: + - `src/lib/client/asset-processor.js` — Web Worker for WASM image processing - `src/lib/client/process-asset.js` — main-thread wrapper for the worker - `src/lib/server/asset-storage.js` — filesystem operations for assets @@ -270,6 +274,7 @@ New files: - `src/routes/assets/[...path]/+server.js` — GET serve assets from disk Modified files: + - `vite.config.js` — add `optimizeDeps.exclude` and `worker.format` - `package.json` — add `@jsquash/webp` and `@jsquash/resize` dependencies - `src/lib/server/migrations.js` — add `add_asset_refs` migration step @@ -363,8 +368,8 @@ CMD ["node", "/app/scripts/start-app.js"] import { server as app } from '/app/build/index.js'; function shutdownServer() { - console.log('Server doing graceful shutdown'); - app.server.close(); + console.log('Server doing graceful shutdown'); + app.server.close(); } process.on('SIGINT', shutdownServer); @@ -477,6 +482,7 @@ ls /data/assets/ # inspect uploaded assets (from SSH) ### File summary New files: + - `Dockerfile` — multi-stage Docker build - `fly.toml` — Fly.io configuration - `.dockerignore` — Docker build context exclusions @@ -484,6 +490,7 @@ New files: - `scripts/start-app.js` — Node.js entry point with graceful shutdown Modified files: + - `svelte.config.js` — switch to `@sveltejs/adapter-node` - `package.json` — add `@sveltejs/adapter-node`, remove `@sveltejs/adapter-auto` @@ -534,6 +541,7 @@ The property names stay as `image` / `logo` for now. `default_node_type` stays ` ### Component changes **`Video.svelte`** — new component, mirrors `Image.svelte` structure: + - Receives `path` prop, reads node from session - Resolves `src` the same way (blob URL → direct, asset id → `ASSET_BASE` prefix) - Renders `