Skip to content

Commit ced39b4

Browse files
committed
v14
1 parent 58a34be commit ced39b4

35 files changed

Lines changed: 3297 additions & 2136 deletions

.cursor/rules/00-foundry-v13-basics.mdc

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
description: Global guardrails for Foundry VTT v14 projects using App V2
3+
globs: "**/*"
4+
alwaysApply: true
5+
---
6+
7+
# Foundry V14 / App V2 – Global Rules
8+
9+
You are working in a **Foundry VTT v14** module that **must** use the **ApplicationV2** framework and **DocumentSheetV2** sheets.
10+
11+
## Absolutes
12+
- **Always** target `ApplicationV2` and `DocumentSheetV2` APIs from v14 docs.
13+
- **Never** introduce V1 classes or jQuery-era hooks (`Application`, `FormApplication`, `DocumentSheet`, `$html`, `activateListeners(html)`, `getData()` returning POJOs, etc.).
14+
- Prefer modern DOM and event APIs (`addEventListener`, `EventTarget`) over jQuery.
15+
- Use lifecycle overrides provided by V2: `_prepareContext`, `_renderHTML`, `_onFirstRender`, `_onRender`, `_preClose`, `_onChangeForm`, `submit()`, etc.
16+
- For tabs, use the built-in `ApplicationV2.TABS` and `_getTabsConfig` rather than ad-hoc tab widgets.
17+
- Only use methods documented as `@public` or `@protected` in the v14 API docs. Do not call `@private` or `@internal` methods.
18+
19+
## Canonical references (v14)
20+
- `ApplicationV2` methods and lifecycle: https://foundryvtt.com/api/v14/classes/foundry.applications.api.ApplicationV2.html
21+
- `DocumentSheetV2` hierarchy and lifecycle: https://foundryvtt.com/api/v14/classes/foundry.applications.api.DocumentSheetV2.html
22+
- `AbstractSidebarTab` (extends ApplicationV2): https://foundryvtt.com/api/v14/classes/foundry.applications.sidebar.AbstractSidebarTab.html
23+
- V14 stable release notes: https://foundryvtt.com/releases/14.359
24+
25+
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.
Lines changed: 23 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
description: Patterns and snippets for ApplicationV2 windows (v13)
3-
globs: "src/**"
2+
description: Patterns and snippets for ApplicationV2 windows (v14)
3+
globs: "scripts/**"
44
alwaysApply: true
55
---
66

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

12-
```ts
13-
import { HandlebarsApplicationMixin } from "foundry/applications/api.mjs"; // if using HBS
14-
15-
export class SyncManager extends HandlebarsApplicationMixin(ApplicationV2) {
16-
static override DEFAULT_OPTIONS = {
12+
```js
13+
export class SyncManager extends foundry.applications.api.HandlebarsApplicationMixin(
14+
foundry.applications.api.ApplicationV2
15+
) {
16+
static DEFAULT_OPTIONS = {
1717
id: "archivist-sync-manager",
1818
window: { title: "Archivist: Sync Manager", resizable: true },
1919
classes: ["archivist", "sync"],
2020
position: { width: 640, height: "auto" },
2121
};
2222

23-
override async _prepareContext(_options) {
23+
static PARTS = {
24+
form: { template: "modules/archivist-sync/templates/sync-manager.hbs" },
25+
};
26+
27+
async _prepareContext(_options) {
2428
return {
2529
sessions: await game.modules.get("archivist-sync")?.api?.listSessions?.() ?? [],
2630
};
2731
}
2832

29-
override async _renderHTML(context, _options) {
30-
// Return a string or Node for HandlebarsApplicationMixin, or mount your own DOM.
31-
return await renderTemplate("modules/archivist-sync/templates/sync-manager.hbs", context);
32-
}
33-
34-
override async _onFirstRender(_context, _options) {
33+
_onRender(context, options) {
34+
super._onRender(context, options);
3535
this.element.querySelector("[data-action='refresh']")
3636
?.addEventListener("click", () => this.render({ force: true }));
3737
}
3838
}
3939
```
4040

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

44-
```ts
44+
```js
4545
static TABS = {
4646
main: { group: "main", navSelector: ".tabs", contentSelector: ".content", initial: "overview" }
4747
};
48-
protected override _getTabsConfig(group: string) { return (SyncManager as any).TABS[group] ?? null; }
4948
```
5049

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

5352
## Header controls
54-
Use `_getHeaderControls()` to add window buttons (gear, help, etc.):
53+
Use `_getHeaderControls()` to add window buttons:
5554

56-
```ts
57-
protected override _getHeaderControls() {
55+
```js
56+
_getHeaderControls() {
5857
return [
5958
...super._getHeaderControls(),
6059
{
@@ -68,42 +67,7 @@ protected override _getHeaderControls() {
6867
```
6968

7069
## Drag & Drop
71-
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.
72-
---
73-
description: Patterns and snippets for ApplicationV2 windows (v13)
74-
globs: "src/**"
75-
alwaysApply: true
76-
---
70+
Wire drag/drop handlers on `this.element` with `addEventListener` inside `_onRender`. Do not use V1 helpers.
7771

78-
# ApplicationV2 – How to structure windows
79-
80-
## Skeleton
81-
Use `DEFAULT_OPTIONS`, implement `_prepareContext` and `_renderHTML`, and wire events with `addEventListener`:
82-
83-
```ts
84-
import { HandlebarsApplicationMixin } from "foundry/applications/api.mjs"; // if using HBS
85-
86-
export class SyncManager extends HandlebarsApplicationMixin(ApplicationV2) {
87-
static override DEFAULT_OPTIONS = {
88-
id: "archivist-sync-manager",
89-
window: { title: "Archivist: Sync Manager", resizable: true },
90-
classes: ["archivist", "sync"],
91-
position: { width: 640, height: "auto" },
92-
};
93-
94-
override async _prepareContext(_options) {
95-
return {
96-
sessions: await game.modules.get("myarchivist")?.api?.listSessions?.() ?? [],
97-
};
98-
}
99-
100-
override async _renderHTML(context, _options) {
101-
// Return a string or Node for HandlebarsApplicationMixin, or mount your own DOM.
102-
return await renderTemplate("modules/myarchivist/templates/sync-manager.hbs", context);
103-
}
104-
105-
override async _onFirstRender(context, options) {
106-
this.element.querySelector("[data-action='refresh']")
107-
?.addEventListener("click", () => this.render({ force: true }));
108-
}
109-
}
72+
## Pop-out windows (v14)
73+
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.
Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Rules and examples for DocumentSheetV2-based sheets (Actors, Items, Journals, custom)
3-
globs: "src/**"
3+
globs: "scripts/**"
44
alwaysApply: true
55
---
66

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

12-
```ts
13-
import { HandlebarsApplicationMixin } from "foundry/applications/api.mjs";
12+
```js
13+
class ArchivistBasePageSheetV2 extends foundry.applications.api.HandlebarsApplicationMixin(
14+
foundry.applications.api.DocumentSheetV2
15+
) {
16+
static DEFAULT_OPTIONS = {
17+
id: "archivist-page-sheet-{id}",
18+
classes: ["archivist-sync", "archivist-v2-sheet"],
19+
sheetConfig: true,
20+
window: { icon: "fa-solid fa-book", resizable: true },
21+
position: { width: 800, height: 700 },
22+
};
1423

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

24-
get title() { return `Faction: ${this.document?.name ?? "Untitled"}`; }
28+
get title() { return this.document?.name ?? "Untitled"; }
2529

26-
override async _prepareContext(_options) {
30+
async _prepareContext(_options) {
2731
return {
28-
faction: this.document,
29-
links: this.document.getFlag("archivist-sync", "links") ?? [],
32+
entry: this.document,
33+
flags: this.document?.getFlag?.("archivist-sync", "archivist") || {},
3034
};
3135
}
3236

33-
override async _renderHTML(context, _options) {
34-
return await renderTemplate("modules/archivist-sync/templates/faction.hbs", context);
35-
}
36-
37-
protected async _onSubmit(ev: SubmitEvent) {
38-
ev.preventDefault();
39-
const data = this._prepareSubmitData(new FormData(this.form!));
40-
await this.document.update(data);
41-
}
42-
43-
protected override _prepareSubmitData(form: FormData) {
44-
// Map HBS form fields to the document schema (v13).
45-
return {
46-
"system.description": form.get("description") ?? this.document.system?.description ?? "",
47-
img: (form.get("img") as string) || this.document.img,
48-
} as any;
37+
_onRender(context, options) {
38+
super._onRender(context, options);
39+
const root = this.element;
40+
root.addEventListener("drop", (ev) => this._onArchivistDrop(ev));
41+
root.addEventListener("dragover", (ev) => ev.preventDefault());
4942
}
5043
}
5144
```
@@ -54,16 +47,14 @@ export class FactionSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
5447
- Rendering & context: `_prepareContext`, `_renderHTML`, `_onFirstRender`, `_onRender`, `_postRender`.
5548
- Forms: `_onChangeForm`, `submit()`, `_prepareSubmitData`, `_processSubmitData`.
5649
- Accessors: `document`, `form`, `title`, `element`, `classList`.
57-
All are described in `DocumentSheetV2`. [oai_citation:1‡Foundry Virtual Tabletop](https://foundryvtt.com/api/classes/foundry.applications.api.DocumentSheetV2.html)
50+
51+
API docs: https://foundryvtt.com/api/v14/classes/foundry.applications.api.DocumentSheetV2.html
5852

5953
## Images & assets
60-
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.
54+
Bind image fields to `document.img` and update via `this.document.update({ img })` in submit handlers. Do not rely on V1 jQuery form traversal.
6155

6256
## Tabs and header controls
63-
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)
57+
Reuse `ApplicationV2` mechanisms: `TABS`, `_getTabsConfig`, `_getHeaderControls`.
6458

6559
## Journal-like content
66-
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.
67-
---
68-
alwaysApply: true
69-
---
60+
For rich text, use ProseMirror-based editors exposed by Foundry. Avoid direct DOM HTML scraping; persist via `update` on the document.
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
---
2-
description: Explicit anti-patterns to block V1/legacy code in v13 modules
2+
description: Explicit anti-patterns to block V1/legacy code in v14 modules
33
globs: "**/*"
44
alwaysApply: true
55
---
66

77
# Do NOT use these in this project
88

99
## Disallowed classes (V1)
10-
- `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)
10+
- `Application`, `FormApplication`, `DocumentSheet` (V1 base classes).
11+
- Any method signature from V1: `getData()`, `activateListeners(html)`, `_updateObject(event, formData)`.
1112

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

1720
## Required alternatives
1821
- Use `ApplicationV2` / `DocumentSheetV2` lifecycle methods.
19-
- Use `addEventListener` on `this.element` or specific elements.
22+
- Use `addEventListener` on `this.element` or specific elements inside `_onRender` / `_onFirstRender`.
2023
- Use `submit()` and `_prepareSubmitData` for forms.
21-
- For context, compute in `_prepareContext`; for markup, use `_renderHTML`.
22-
---
23-
alwaysApply: true
24-
---
24+
- For context, compute in `_prepareContext`; for markup, define `PARTS` templates.
25+
- For sidebar tabs, extend `AbstractSidebarTab` using V2 lifecycle (`_prepareContext`, `_renderHTML`, `_onActivate`, `_onDeactivate`).
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
description: How to build sidebar tabs and pop-out windows with V14 App V2 hooks
3+
globs: "scripts/sidebar/**"
4+
alwaysApply: false
5+
---
6+
7+
# V14 Sidebar Tabs & Pop-outs
8+
9+
## Sidebar tab implementation
10+
11+
Custom sidebar tabs must extend `foundry.applications.sidebar.AbstractSidebarTab`, which is an `ApplicationV2` subclass. Use V2 lifecycle methods only:
12+
13+
```js
14+
export class AskChatSidebarTab extends foundry.applications.sidebar.AbstractSidebarTab {
15+
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
16+
foundry.utils.deepClone(super.DEFAULT_OPTIONS),
17+
{
18+
id: "archivist-chat",
19+
classes: ["archivist-chat-sidebar"],
20+
}
21+
);
22+
23+
async _prepareContext(_options) {
24+
return { messages: this._getMessages() };
25+
}
26+
27+
async _renderHTML(context, _options) {
28+
return await foundry.applications.handlebars.renderTemplate(
29+
"modules/archivist-sync/templates/ask-chat-window.hbs",
30+
context
31+
);
32+
}
33+
34+
_onRender(context, options) {
35+
super._onRender(context, options);
36+
const root = this.element;
37+
root.querySelector(".ask-form")
38+
?.addEventListener("submit", (e) => this._onSubmit(e));
39+
}
40+
41+
_onActivate() { /* called when this tab becomes active */ }
42+
_onDeactivate() { /* called when switching away from this tab */ }
43+
}
44+
```
45+
46+
## Registering the tab
47+
48+
Register during `init` by adding to `Sidebar.TABS`:
49+
50+
```js
51+
Hooks.once("init", () => {
52+
const Sidebar = foundry.applications.sidebar?.Sidebar;
53+
if (Sidebar) {
54+
Sidebar.TABS["archivist-chat"] = {
55+
id: "archivist-chat",
56+
title: "Archivist Chat",
57+
icon: "fa-solid fa-sparkles",
58+
tab: AskChatSidebarTab,
59+
app: AskChatSidebarTab,
60+
};
61+
}
62+
});
63+
```
64+
65+
## Do NOT use
66+
- `getData()` or `activateListeners()` on sidebar tabs — these are V1 patterns.
67+
- Manual `innerHTML` replacement inside sidebar panels.
68+
69+
## Pop-out windows (v14 native)
70+
71+
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.

0 commit comments

Comments
 (0)