Skip to content
Draft

v14 #45

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
23 changes: 0 additions & 23 deletions .cursor/rules/00-foundry-v13-basics.mdc

This file was deleted.

25 changes: 25 additions & 0 deletions .cursor/rules/00-foundry-v14-basics.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
description: Global guardrails for Foundry VTT v14 projects using App V2
globs: "**/*"
alwaysApply: true
---

# Foundry V14 / App V2 – Global Rules

You are working in a **Foundry VTT v14** module that **must** use the **ApplicationV2** framework and **DocumentSheetV2** sheets.

## Absolutes
- **Always** target `ApplicationV2` and `DocumentSheetV2` APIs from v14 docs.
- **Never** introduce V1 classes or jQuery-era hooks (`Application`, `FormApplication`, `DocumentSheet`, `$html`, `activateListeners(html)`, `getData()` returning POJOs, etc.).
- Prefer modern DOM and event APIs (`addEventListener`, `EventTarget`) over jQuery.
- Use lifecycle overrides provided by V2: `_prepareContext`, `_renderHTML`, `_onFirstRender`, `_onRender`, `_preClose`, `_onChangeForm`, `submit()`, etc.
- For tabs, use the built-in `ApplicationV2.TABS` and `_getTabsConfig` rather than ad-hoc tab widgets.
- Only use methods documented as `@public` or `@protected` in the v14 API docs. Do not call `@private` or `@internal` methods.

## Canonical references (v14)
- `ApplicationV2` methods and lifecycle: https://foundryvtt.com/api/v14/classes/foundry.applications.api.ApplicationV2.html
- `DocumentSheetV2` hierarchy and lifecycle: https://foundryvtt.com/api/v14/classes/foundry.applications.api.DocumentSheetV2.html
- `AbstractSidebarTab` (extends ApplicationV2): https://foundryvtt.com/api/v14/classes/foundry.applications.sidebar.AbstractSidebarTab.html
- V14 stable release notes: https://foundryvtt.com/releases/14.359

When the user asks for examples, **generate only V2-style code** and call out any legacy snippet as an anti-pattern with a V2 rewrite.
82 changes: 23 additions & 59 deletions .cursor/rules/10-applicationv2-patterns.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Patterns and snippets for ApplicationV2 windows (v13)
globs: "src/**"
description: Patterns and snippets for ApplicationV2 windows (v14)
globs: "scripts/**"
alwaysApply: true
---

Expand All @@ -9,52 +9,51 @@ alwaysApply: true
## Skeleton
Use `DEFAULT_OPTIONS`, implement `_prepareContext` and `_renderHTML`, and wire events with `addEventListener`:

```ts
import { HandlebarsApplicationMixin } from "foundry/applications/api.mjs"; // if using HBS

export class SyncManager extends HandlebarsApplicationMixin(ApplicationV2) {
static override DEFAULT_OPTIONS = {
```js
export class SyncManager extends foundry.applications.api.HandlebarsApplicationMixin(
foundry.applications.api.ApplicationV2
) {
static DEFAULT_OPTIONS = {
id: "archivist-sync-manager",
window: { title: "Archivist: Sync Manager", resizable: true },
classes: ["archivist", "sync"],
position: { width: 640, height: "auto" },
};

override async _prepareContext(_options) {
static PARTS = {
form: { template: "modules/archivist-sync/templates/sync-manager.hbs" },
};

async _prepareContext(_options) {
return {
sessions: await game.modules.get("archivist-sync")?.api?.listSessions?.() ?? [],
};
}

override async _renderHTML(context, _options) {
// Return a string or Node for HandlebarsApplicationMixin, or mount your own DOM.
return await renderTemplate("modules/archivist-sync/templates/sync-manager.hbs", context);
}

override async _onFirstRender(_context, _options) {
_onRender(context, options) {
super._onRender(context, options);
this.element.querySelector("[data-action='refresh']")
?.addEventListener("click", () => this.render({ force: true }));
}
}
```

## Tabs (built-in)
Configure with `TABS` + `_getTabsConfig` instead of custom tab libs:
Configure with `TABS` + `_getTabsConfig`:

```ts
```js
static TABS = {
main: { group: "main", navSelector: ".tabs", contentSelector: ".content", initial: "overview" }
};
protected override _getTabsConfig(group: string) { return (SyncManager as any).TABS[group] ?? null; }
```

See `ApplicationV2` tab APIs. [oai_citation:11‡Foundry Virtual Tabletop](https://foundryvtt.com/api/classes/foundry.applications.api.ApplicationV2.html)
See ApplicationV2 tab APIs: https://foundryvtt.com/api/v14/classes/foundry.applications.api.ApplicationV2.html

## Header controls
Use `_getHeaderControls()` to add window buttons (gear, help, etc.):
Use `_getHeaderControls()` to add window buttons:

```ts
protected override _getHeaderControls() {
```js
_getHeaderControls() {
return [
...super._getHeaderControls(),
{
Expand All @@ -68,42 +67,7 @@ protected override _getHeaderControls() {
```

## Drag & Drop
Instantiate `DragDrop` in the constructor and wire targets via modern listeners—do not use V1 helpers. Keep handlers on `this.element` with `addEventListener` and translate drops into V2 updates.
---
description: Patterns and snippets for ApplicationV2 windows (v13)
globs: "src/**"
alwaysApply: true
---
Wire drag/drop handlers on `this.element` with `addEventListener` inside `_onRender`. Do not use V1 helpers.

# ApplicationV2 – How to structure windows

## Skeleton
Use `DEFAULT_OPTIONS`, implement `_prepareContext` and `_renderHTML`, and wire events with `addEventListener`:

```ts
import { HandlebarsApplicationMixin } from "foundry/applications/api.mjs"; // if using HBS

export class SyncManager extends HandlebarsApplicationMixin(ApplicationV2) {
static override DEFAULT_OPTIONS = {
id: "archivist-sync-manager",
window: { title: "Archivist: Sync Manager", resizable: true },
classes: ["archivist", "sync"],
position: { width: 640, height: "auto" },
};

override async _prepareContext(_options) {
return {
sessions: await game.modules.get("myarchivist")?.api?.listSessions?.() ?? [],
};
}

override async _renderHTML(context, _options) {
// Return a string or Node for HandlebarsApplicationMixin, or mount your own DOM.
return await renderTemplate("modules/myarchivist/templates/sync-manager.hbs", context);
}

override async _onFirstRender(context, options) {
this.element.querySelector("[data-action='refresh']")
?.addEventListener("click", () => this.render({ force: true }));
}
}
## Pop-out windows (v14)
V14 adds native pop-out support. Any `ApplicationV2` can call `attachWindow()` / `detachWindow()` to render in a separate browser window. No special code needed unless you need to customize the pop-out behavior.
65 changes: 28 additions & 37 deletions .cursor/rules/20-documentsheetv2-sheets.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Rules and examples for DocumentSheetV2-based sheets (Actors, Items, Journals, custom)
globs: "src/**"
globs: "scripts/**"
alwaysApply: true
---

Expand All @@ -9,43 +9,36 @@ alwaysApply: true
## Sheet base
Extend `DocumentSheetV2` directly (or a project `BaseSheet` that extends it). Use V2 accessors (`this.document`, `this.form`) and lifecycle:

```ts
import { HandlebarsApplicationMixin } from "foundry/applications/api.mjs";
```js
class ArchivistBasePageSheetV2 extends foundry.applications.api.HandlebarsApplicationMixin(
foundry.applications.api.DocumentSheetV2
) {
static DEFAULT_OPTIONS = {
id: "archivist-page-sheet-{id}",
classes: ["archivist-sync", "archivist-v2-sheet"],
sheetConfig: true,
window: { icon: "fa-solid fa-book", resizable: true },
position: { width: 800, height: 700 },
};

export class FactionSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
static override DEFAULT_OPTIONS = {
id: "archivist-faction-sheet",
window: { title: "Faction" },
classes: ["archivist", "sheet", "faction"],
position: { width: 600, height: "auto" },
form: { handler: FactionSheet.prototype._onSubmit },
static PARTS = {
form: { template: "modules/archivist-sync/templates/sheets/base.hbs" },
};

get title() { return `Faction: ${this.document?.name ?? "Untitled"}`; }
get title() { return this.document?.name ?? "Untitled"; }

override async _prepareContext(_options) {
async _prepareContext(_options) {
return {
faction: this.document,
links: this.document.getFlag("archivist-sync", "links") ?? [],
entry: this.document,
flags: this.document?.getFlag?.("archivist-sync", "archivist") || {},
};
}

override async _renderHTML(context, _options) {
return await renderTemplate("modules/archivist-sync/templates/faction.hbs", context);
}

protected async _onSubmit(ev: SubmitEvent) {
ev.preventDefault();
const data = this._prepareSubmitData(new FormData(this.form!));
await this.document.update(data);
}

protected override _prepareSubmitData(form: FormData) {
// Map HBS form fields to the document schema (v13).
return {
"system.description": form.get("description") ?? this.document.system?.description ?? "",
img: (form.get("img") as string) || this.document.img,
} as any;
_onRender(context, options) {
super._onRender(context, options);
const root = this.element;
root.addEventListener("drop", (ev) => this._onArchivistDrop(ev));
root.addEventListener("dragover", (ev) => ev.preventDefault());
}
}
```
Expand All @@ -54,16 +47,14 @@ export class FactionSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
- Rendering & context: `_prepareContext`, `_renderHTML`, `_onFirstRender`, `_onRender`, `_postRender`.
- Forms: `_onChangeForm`, `submit()`, `_prepareSubmitData`, `_processSubmitData`.
- Accessors: `document`, `form`, `title`, `element`, `classList`.
All are described in `DocumentSheetV2`. [oai_citation:1‡Foundry Virtual Tabletop](https://foundryvtt.com/api/classes/foundry.applications.api.DocumentSheetV2.html)

API docs: https://foundryvtt.com/api/v14/classes/foundry.applications.api.DocumentSheetV2.html

## Images & assets
Bind image fields to `document.img` (or relevant system path) and update via `this.document.update({ img })` in submit handlers—do **not** rely on V1 jQuery form traversal.
Bind image fields to `document.img` and update via `this.document.update({ img })` in submit handlers. Do not rely on V1 jQuery form traversal.

## Tabs and header controls
Reuse `ApplicationV2` mechanisms from the window base: `TABS`, `_getTabsConfig`, `_getHeaderControls`. [oai_citation:11‡Foundry Virtual Tabletop](https://foundryvtt.com/api/classes/foundry.applications.api.ApplicationV2.html)
Reuse `ApplicationV2` mechanisms: `TABS`, `_getTabsConfig`, `_getHeaderControls`.

## Journal-like content
For rich text, prefer v13 editors or ProseMirror-based editors exposed by the system. Avoid direct DOM HTML scraping; persist via `update` on the document.
---
alwaysApply: true
---
For rich text, use ProseMirror-based editors exposed by Foundry. Avoid direct DOM HTML scraping; persist via `update` on the document.
19 changes: 10 additions & 9 deletions .cursor/rules/30-legacy-apis-do-not-use.mdc
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
---
description: Explicit anti-patterns to block V1/legacy code in v13 modules
description: Explicit anti-patterns to block V1/legacy code in v14 modules
globs: "**/*"
alwaysApply: true
---

# Do NOT use these in this project

## Disallowed classes (V1)
- `Application`, `FormApplication`, `DocumentSheet` (and any method like `getData()`, `activateListeners(html)`, `_updateObject(event, formData)` that expects jQuery/HTML args). [oai_citation:1‡Foundry Virtual Tabletop](https://foundryvtt.com/api/classes/foundry.applications.api.DocumentSheetV2.html)
- `Application`, `FormApplication`, `DocumentSheet` (V1 base classes).
- Any method signature from V1: `getData()`, `activateListeners(html)`, `_updateObject(event, formData)`.

## Disallowed idioms
- jQuery-specific patterns: `$(...)`, `html.find(...)`, passing `html` objects into lifecycle hooks.
- V1 render signature assumptions (boolean `force` without V2 options).
- Manual tab widgets when `ApplicationV2` tabs exist. [oai_citation:11‡Foundry Virtual Tabletop](https://foundryvtt.com/api/classes/foundry.applications.api.ApplicationV2.html)
- V1 render signature assumptions (boolean `force` without V2 options object).
- Manual tab widgets when `ApplicationV2.TABS` exists.
- Manual `renderTemplate` + `innerHTML` mounting instead of using `HandlebarsApplicationMixin` with `PARTS`.
- Calling `@private` or `@internal` Foundry methods (see v14 Public API guidance).

## Required alternatives
- Use `ApplicationV2` / `DocumentSheetV2` lifecycle methods.
- Use `addEventListener` on `this.element` or specific elements.
- Use `addEventListener` on `this.element` or specific elements inside `_onRender` / `_onFirstRender`.
- Use `submit()` and `_prepareSubmitData` for forms.
- For context, compute in `_prepareContext`; for markup, use `_renderHTML`.
---
alwaysApply: true
---
- For context, compute in `_prepareContext`; for markup, define `PARTS` templates.
- For sidebar tabs, extend `AbstractSidebarTab` using V2 lifecycle (`_prepareContext`, `_renderHTML`, `_onActivate`, `_onDeactivate`).
71 changes: 71 additions & 0 deletions .cursor/rules/50-v14-sidebar-and-popouts.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
description: How to build sidebar tabs and pop-out windows with V14 App V2 hooks
globs: "scripts/sidebar/**"
alwaysApply: false
---

# V14 Sidebar Tabs & Pop-outs

## Sidebar tab implementation

Custom sidebar tabs must extend `foundry.applications.sidebar.AbstractSidebarTab`, which is an `ApplicationV2` subclass. Use V2 lifecycle methods only:

```js
export class AskChatSidebarTab extends foundry.applications.sidebar.AbstractSidebarTab {
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
foundry.utils.deepClone(super.DEFAULT_OPTIONS),
{
id: "archivist-chat",
classes: ["archivist-chat-sidebar"],
}
);

async _prepareContext(_options) {
return { messages: this._getMessages() };
}

async _renderHTML(context, _options) {
return await foundry.applications.handlebars.renderTemplate(
"modules/archivist-sync/templates/ask-chat-window.hbs",
context
);
}

_onRender(context, options) {
super._onRender(context, options);
const root = this.element;
root.querySelector(".ask-form")
?.addEventListener("submit", (e) => this._onSubmit(e));
}

_onActivate() { /* called when this tab becomes active */ }
_onDeactivate() { /* called when switching away from this tab */ }
}
```

## Registering the tab

Register during `init` by adding to `Sidebar.TABS`:

```js
Hooks.once("init", () => {
const Sidebar = foundry.applications.sidebar?.Sidebar;
if (Sidebar) {
Sidebar.TABS["archivist-chat"] = {
id: "archivist-chat",
title: "Archivist Chat",
icon: "fa-solid fa-sparkles",
tab: AskChatSidebarTab,
app: AskChatSidebarTab,
};
}
});
```

## Do NOT use
- `getData()` or `activateListeners()` on sidebar tabs — these are V1 patterns.
- Manual `innerHTML` replacement inside sidebar panels.

## Pop-out windows (v14 native)

Any `ApplicationV2` can pop out. Sidebar tabs can use `renderPopout()` to create a detached copy. The framework handles window management. Override `_onDetach` / `_onAttach` if you need custom behavior when popping out or reattaching.
Loading