Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,33 +63,43 @@ In full runtime mode, Editable also supports a simple owner-only admin authentic
2. **Edit-for-fun mode** — unauthenticated, can temporarily edit the currently open page in the browser UI, but cannot save changes or access private site-management features
3. **Public browsing mode** — normal site visitor mode with no editing UI active

## Language: TypeScript

The codebase is written in TypeScript throughout — modules use `.ts` (or `.svelte.ts` when they use runes), and Svelte components use `<script lang="ts">`. This follows the decision in [michael/editable#144](https://github.com/michael/editable/issues/144): Svelte developers are the primary technical extenders, and a schema-driven system gains unusually much from types.

Guidelines:

- **Non-strict for now.** `tsconfig.json` keeps `strict: false`; implicit `any` is allowed. The goal of the conversion is consistency and schema-derived autocomplete, not exhaustive type coverage. Strictness can be ratcheted up later.
- **Readable surface, ceremonial internals.** Customizable node components (e.g. `Paragraph.svelte`, `Figure.svelte`) must stay light: typed props plus a typed `node`, nothing more. Internal modules (server code, `api.remote.ts`, `PagesDrawer`, overlays, commands) may carry heavier type ceremony where it stabilizes things.
- **Schema-derived node types.** `src/lib/document_schema.ts` exports `type Nodes = NodeMap<typeof document_schema>` (from svedit). Node components access the session through the typed `get_svedit_context()` helper in `src/routes/svedit_context.ts` and annotate their node as `let node: Nodes['paragraph'] = $derived(svedit.session.get(path));`, which gives property autocomplete and catches misspelled properties in `npm run check`.
- **Import specifiers keep the `.js` extension** even when the target file is `.ts` (TypeScript's `bundler` resolution and Vite both resolve `./foo.js` → `./foo.ts`). This matches svedit's own convention.
- **`npm run check` must stay at 0 errors** and is the required validation step after schema or component changes.

## SvelteKit configuration

The app uses Svelte's experimental async features and SvelteKit's remote functions. Both are enabled in `svelte.config.js`:
The app uses Svelte's experimental async features and SvelteKit's remote functions. All SvelteKit configuration is passed to the `sveltekit()` plugin in `vite.config.ts` (the newer configuration style, expected to become the default in SvelteKit 3); `svelte.config.js` stays an empty stub so tools that require its presence keep working. The "svelte.config.js is ignored" warning printed by dev/build/check commands is expected with this setup.

```js
const config = {
kit: {
adapter: adapter(),
experimental: {
remoteFunctions: true
}
```ts
sveltekit({
adapter: adapter(),
experimental: {
remoteFunctions: true
},
compilerOptions: {
experimental: {
async: true
}
}
};
});
```

**Experimental async** allows `await` in Svelte markup and top-level `await` in `<script>` tags, enabling components to load data inline without separate `+page.server.js` load functions.

**Remote functions** (`$app/server`) allow server-side functions to be called directly from components via `query()` and `action()`. This replaces traditional REST endpoints for document loading and saving:

- **`src/lib/api.remote.js`** — server-side functions for document and asset operations, called directly from components. Uses `query()` for reads and `action()` for writes. Access to `locals` (e.g. for auth checks) via `getRequestEvent()`.
- **`src/lib/api.remote.ts`** — server-side functions for document and asset operations, called directly from components. Uses `query()` for reads and `action()` for writes. Access to `locals` (e.g. for auth checks) via `getRequestEvent()`.

**Server initialization** — `src/hooks.server.js` exports an `init()` function (SvelteKit's `ServerInit` hook) that runs once on server startup. This is where database migration runs:
**Server initialization** — `src/hooks.server.ts` exports an `init()` function (SvelteKit's `ServerInit` hook) that runs once on server startup. This is where database migration runs:

```js
import migrate from '$lib/server/migrate.js';
Expand Down Expand Up @@ -200,7 +210,7 @@ CREATE TABLE sessions (

**`documents`**

- `document_id` — a persistent identifier (nanoid with a custom alphabet — letters only, no numbers, no `_` or `-` — so ids are safe to use as HTML ids; see `src/routes/nanoid.js`)
- `document_id` — a persistent identifier (nanoid with a custom alphabet — letters only, no numbers, no `_` or `-` — so ids are safe to use as HTML ids; see `src/routes/nanoid.ts`)
- `type` — categorizes the document, e.g. `page`, `nav`, or `footer`
- `data` — the full Svedit document serialized as JSON (`{ document_id, nodes }`)

Expand Down
17 changes: 15 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,22 +24,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- In home-route server files, import backend-only code lazily inside the `has_backend` guard so static/Vercel deployments do not evaluate database code at module load time

**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
Expand All @@ -51,9 +57,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
Expand All @@ -74,16 +82,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
Expand All @@ -92,6 +103,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
Expand All @@ -107,8 +119,9 @@ 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`)

1. Add to schema in `src/lib/document_schema.ts` (`document_schema`)
2. Add to inserter in `src/routes/create_session.ts` (`inserters`)

## Available MCP Tools

Expand Down
11 changes: 11 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Implementation plan

## TypeScript conversion

Convert the whole codebase from JS+JSDoc to TypeScript per the "Language: TypeScript" section in [ARCHITECTURE.md](ARCHITECTURE.md). Non-strict; svelte-check must stay at 0 errors.

1. Replace `jsconfig.json` with `tsconfig.json` (same options, `allowJs`/`checkJs` kept during transition) and point the `check` script at it.
2. Convert `src/lib/document_schema.js` → `.ts`, export `type Nodes = NodeMap<typeof document_schema>`, and add a typed `get_svedit_context()` in `src/routes/svedit_context.ts` (mirrors the svedit demo app).
3. Convert `src/lib` modules (root, `client/`, `server/`, `server/markdown/`, `server/migrations/`), translating existing JSDoc annotations to TS syntax.
4. Convert `src/routes` modules (`hooks.server`, load functions, API endpoints, `app_utils`, `commands.svelte`, `create_session`, helpers).
5. Convert all Svelte components to `<script lang="ts">`. Node components use the typed-node pattern; internal components get explicit prop types.
6. Convert tests to `.ts`, update the vitest include glob and `vite.config` filename, then verify `npm run check`, `npm test`, `npm run build`, and `npm run lint` all pass.

## Node navigator and variant switcher

- While editing at every viewport size, show the closest node with variants inside the main toolbar.
Expand Down
42 changes: 25 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,20 @@ path // the hero node
Pass those paths to primitives and use the same paths to read values when layout depends on content:

```svelte
<script>
const svedit = getContext('svedit');
let { path } = $props();
let hero = $derived(svedit.session.get(path));
<script lang="ts">
import { get_svedit_context } from '../svedit_context.js';
import type { DocumentPath } from 'svedit';
import type { Nodes } from '$lib/document_schema.js';

const svedit = get_svedit_context();
let { path }: { path: DocumentPath } = $props();
let hero: Nodes['hero'] = $derived(svedit.session.get(path));
let media = $derived(svedit.session.get([...path, 'media']));
</script>
```

The `Nodes['hero']` annotation types the node against the schema, so `hero.` autocompletes its properties and a misspelled property fails `npm run check`.

`session.get` follows node references for you, so the last expression returns the media node rather than its stored id. Components do not need to know whether they are on a page, inside another block, or nested several arrays deep.

### Node: establish the editing boundary
Expand Down Expand Up @@ -298,7 +304,7 @@ The first four come from `svedit`; the last two live in `src/routes/components`.

A small, typed vocabulary for Editable's pages and shared site content.

Editable's content model defines the nodes and properties available to pages and shared site content. Its schema lives in `src/lib/document_schema.js`; this section is the reference.
Editable's content model defines the nodes and properties available to pages and shared site content. Its schema lives in `src/lib/document_schema.ts`; this section is the reference.

Documents are graphs of nodes stored by id. Each node has an `id`, a `type`, and type-specific properties. A few naming conventions hold throughout: `content` is the string payload of text properties, `body` holds authored nested content, `items` holds repeated structured children, and `label`/`title`/`description`/`meta` are text properties with semantic meaning.

Expand Down Expand Up @@ -500,13 +506,13 @@ Each dropped video goes through this decision tree:

1. **Filename escape hatch** — a file named `*_optimized.mp4` (or `*.optimized.mp4`) is uploaded byte-identical, bypassing all processing and all caps. Use this when you've deliberately prepared a file — say a high-bitrate 4K export — and want it kept exactly as exported.
2. **Already good** — if the video is already H.264, within the resolution cap and within the size goal (with 25% tolerance, since re-encoding a marginally-over file costs quality and saves little), nothing is re-encoded: an MP4 is uploaded untouched, and other containers (e.g. an H.264 `.mov`) are losslessly repackaged into an MP4 container.
3. **Everything else** is transcoded to fit the size goal: the bitrate is derived from the video's duration, and the resolution is chosen as the largest that still looks good at that bitrate — starting from the resolution cap (1080 means landscape 1920×1080 *and* portrait 1080×1920; videos are never upscaled) and stepping down (720, 540, …) for long videos where the size budget would otherwise spread too thin. Rotation is preserved.
3. **Everything else** is transcoded to fit the size goal: the bitrate is derived from the video's duration, and the resolution is chosen as the largest that still looks good at that bitrate — starting from the resolution cap (1080 means landscape 1920×1080 _and_ portrait 1080×1920; videos are never upscaled) and stepping down (720, 540, …) for long videos where the size budget would otherwise spread too thin. Rotation is preserved.

Two knobs in `src/lib/config.js`:
Two knobs in `src/lib/config.ts`:

```js
export const MAX_VIDEO_RESOLUTION = 1080; // cap on the short side: 720, 1080, …
export const MAX_VIDEO_FILESIZE = 50 * 1024 * 1024; // size goal for transcoded videos
export const MAX_VIDEO_RESOLUTION = 1080; // cap on the short side: 720, 1080, …
export const MAX_VIDEO_FILESIZE = 50 * 1024 * 1024; // size goal for transcoded videos
```

Things worth knowing:
Expand All @@ -526,7 +532,7 @@ Editable's built-in types are just a starting set. This walkthrough adds a `hero

### 1. Define the type in the schema

In `src/lib/document_schema.js`, add the node type definition. A hero is a `block` with a `layout` variant, two text properties, and a media reference:
In `src/lib/document_schema.ts`, add the node type definition. A hero is a `block` with a `layout` variant, two text properties, and a media reference:

```js
hero: {
Expand Down Expand Up @@ -578,15 +584,17 @@ That's the whole data model. Documents containing heroes now validate, and every
Create `src/routes/components/Hero.svelte`. It reads the node at `path`, renders each property through an editable primitive (`TextProperty` for text, `MediaProperty` for the image), and picks a snippet per layout:

```svelte
<script>
import { getContext } from 'svelte';
<script lang="ts">
import { Node, TextProperty } from 'svedit';
import type { DocumentPath } from 'svedit';
import type { Nodes } from '$lib/document_schema.js';
import { get_svedit_context } from '../svedit_context.js';
import MediaProperty from './MediaProperty.svelte';
import { TW_LIMITER, TW_PAGE_PADDING_X } from '../tailwind_theme.js';

const svedit = getContext('svedit');
let { path } = $props();
let node = $derived(svedit.session.get(path));
const svedit = get_svedit_context();
let { path }: { path: DocumentPath } = $props();
let node: Nodes['hero'] = $derived(svedit.session.get(path));
let layout = $derived(node.layout || 'side-by-side');
</script>

Expand Down Expand Up @@ -630,7 +638,7 @@ There is no read-only twin to keep in sync: this one component is the live page

### 3. Register it in the session

In `src/routes/create_session.js`, import the component and add it to `node_components`, so Svedit knows what to render:
In `src/routes/create_session.ts`, import the component and add it to `node_components`, so Svedit knows what to render:

```js
import Hero from './components/Hero.svelte';
Expand Down Expand Up @@ -912,7 +920,7 @@ A deployment can expose selected repository markdown files as read-only pages re

### Configuration

Any markdown file in the repository can be mapped to a URL in `src/lib/content_config.js` (server/build-only — never import it from client code). Reference the file with a `?raw` import, so Vite inlines exactly the mapped files and a missing file fails the build:
Any markdown file in the repository can be mapped to a URL in `src/lib/content_config.ts` (server/build-only — never import it from client code). Reference the file with a `?raw` import, so Vite inlines exactly the mapped files and a missing file fails the build:

```js
import manual_md from '../../README.md?raw';
Expand Down
52 changes: 45 additions & 7 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,52 @@ export default [
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
}
},
{
files: ['**/*.svelte', '**/*.svelte.js', '**/*.svelte.ts'],
languageOptions: { parserOptions: { svelteConfig, parser: typescriptParser } },
plugins: {
'@typescript-eslint': typescript
},
rules: {
'svelte/no-navigation-without-resolve': 'off',
// The base rule reports parameters in type annotations; use the
// TypeScript-aware variant.
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
]
}
},
// TypeScript configuration for regular .ts modules (.svelte.ts files are
// handled by the svelte plugin configs above so runes stay defined)
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: { parserOptions: { svelteConfig } },
files: ['**/*.ts'],
ignores: ['**/*.d.ts', '**/*.svelte.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
'svelte/no-navigation-without-resolve': 'off'
// TypeScript itself checks undefined names and unused variables of
// type-only constructs; use the TS-aware variants.
'no-undef': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
]
}
},
// TypeScript configuration for .d.ts files
Expand All @@ -35,7 +74,7 @@ export default [
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: './jsconfig.json',
project: './tsconfig.json',
ecmaVersion: 2022,
sourceType: 'module'
}
Expand All @@ -45,15 +84,14 @@ export default [
},
rules: {
// Enable TypeScript-specific rules for .d.ts files
'no-undef': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/triple-slash-reference': 'error',
'@typescript-eslint/no-var-requires': 'off', // Often needed in .d.ts files
'@typescript-eslint/ban-types': 'error',
'@typescript-eslint/no-duplicate-enum-values': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
Expand Down
Loading