Compare commits

..

52 commits
v2.6.2 ... main

Author SHA1 Message Date
86ff3c9b79 Fix the tabActive for the content tab since we assert the selected tab on render 2026-04-14 20:47:44 -06:00
1f213a890a Merge pull request 'Item Support' (#69) from feature/item-support into main
Reviewed-on: #69
2026-04-15 02:42:52 +00:00
3ca1de9645 Remove unused css 2026-04-14 20:34:07 -06:00
1eebde246e Update --divider-color to --divider-colour 2026-04-14 20:29:20 -06:00
0e966f4b6a Make the comment read nicer 2026-04-12 22:42:20 -06:00
4bbca4e786 Remove the tabs partial from the removedParts set 2026-04-12 22:41:42 -06:00
57b902f986 Lint the code 2026-04-12 22:39:12 -06:00
48760343ce Merge remote-tracking branch 'origin/main' into feature/item-support 2026-04-12 22:26:59 -06:00
e128f2a828 Enable the item tab hiding on the Attribute-only actor sheet 2026-04-12 18:07:46 -06:00
76f5a4f27a Add an assertion that makes sure that tabs which should be hidden do not get displayed to the user if the condition becomes falsy due to a document update 2026-04-12 17:57:42 -06:00
687440c0da Make the disabled details collapse/expand more obvious when it's disabled (closes #63) 2026-04-09 21:48:43 -06:00
9955b76956 Finish adding the create button in the item group lists 2026-04-09 21:41:37 -06:00
fc24caf08a Add header control for adding an embedded item into an actor. 2026-04-08 01:08:52 -06:00
380ed3fe15 Make the quantity input update the embedded document of actor 2026-04-06 23:33:32 -06:00
6f3139edf1 Fix typo for showing/hiding the %-used of carry capacity 2026-04-06 23:33:12 -06:00
f666fd2fa9 Add missing localization string for the sheet name 2026-04-06 23:01:43 -06:00
13619b6a09 Change all instances of "color" in my theme variables to be "colour" 2026-04-06 22:55:12 -06:00
f05311d7c8 Hide the "% used" indicator when there is no maximum capacity, and add a placeholder to be more explicit for the input 2026-04-06 22:47:53 -06:00
e4e1f30fcb Add comments for the v13 shims to make it easier to find them 2026-04-05 20:23:11 -06:00
cfa352e5e0 Allow negative item quantities, don't allow negative weights 2026-04-05 20:14:18 -06:00
4c05171e04 Add the context menu for items in the player sheet 2026-04-05 20:14:02 -06:00
441930a5e5 Add _loc compatibility for v13 2026-04-05 20:08:55 -06:00
66d2452d1d Update the item sheet styling to use Forgejo colours in dark mode 2026-04-05 19:18:32 -06:00
c9ad0d8a4e Tweak the way the config/api globals are structured (closes #50) 2026-03-29 01:27:26 -06:00
2d6db98530 Make the localizer config sealed instead of non-extendible 2026-03-29 01:24:39 -06:00
9d49a45f0d Swap the checkbox for a toggle in the item sheet for consistency of UI and help user expectations 2026-03-29 01:04:30 -06:00
b20240699b Update the toggle so that the animation plays fully before the events are emitted 2026-03-28 20:01:16 -06:00
e4b6a5c635 Update the toggle element to have the foreign update listener attached to it 2026-03-28 18:44:13 -06:00
25840a65e9 Set verified to v14 2026-03-28 18:43:50 -06:00
c65e960bd7 Add listener registration for foreign document updates on all sheets 2026-03-28 18:43:39 -06:00
0f1ba90161 Update the TafToggle to actually properly dispatch change/input events as required 2026-03-28 18:43:21 -06:00
1892a02794 Remove the unused cache from the config 2026-03-28 18:42:48 -06:00
9f19072426 Add a utility method for performing a document update on any arbitrary document 2026-03-28 18:42:11 -06:00
f5c7e1c4bc Add the toggle for item equipped status and inventory properties 2026-03-23 00:23:32 -06:00
44372d0a17 Add a customizable weight formatter function 2026-03-23 00:22:54 -06:00
6e2dfa1cf1 Add a toggle component 2026-03-23 00:08:30 -06:00
92553cb1f1 Realize I haven't updated the html-data for my custom elements and correct the descriptions 2026-03-19 23:32:20 -06:00
2b0fbdfa8b Finish the primary design of the item tab, still awaiting an equipped toggle 2026-03-19 23:31:50 -06:00
704ff4672a Get the item list styles finished, and start working on the weight summary / carry capacity for the tab 2026-03-19 00:23:01 -06:00
96eccc62f2 Add partial rerendering to document sheets via a custom mixin 2026-03-16 22:06:30 -06:00
761f0b6563 Fix eslint and lint the codebase 2026-03-16 22:01:06 -06:00
23a402f11a Get items appearing on the sheet, still a lot to do but this is a strong step in the right direction 2026-03-15 22:37:55 -06:00
6b03d62234 Add the item list compatibility for the Attribute Only Sheet 2026-03-15 15:47:28 -06:00
40f0e1ea2c Add support for the tabs in the two contentful Actor sheets 2026-03-15 15:35:57 -06:00
76d8f3675c Make group sorting be case insensitive 2026-03-14 16:02:37 -06:00
f2420a6848 Make the labels not take up so much space so that click events are a bit more predictable on when they happen 2026-03-14 16:01:01 -06:00
ce81212bbe Add a group property on the Item data model 2026-03-14 15:58:06 -06:00
c6ec60b5bf Update the Player data model and move it into an Actor folder since we now have an item data model 2026-03-13 21:45:13 -06:00
d9d66abf27 Add localization for the item sheet fields 2026-03-13 21:35:10 -06:00
94b3ec923b Finish getting the main item sheet completed and fully functional 2026-03-13 21:31:03 -06:00
2518c7cf05 Begin work on the most basic item sheet version 2026-03-13 01:05:52 -06:00
f91c3d2419 Create the Item data model 2026-03-08 13:11:14 -06:00
50 changed files with 1590 additions and 121 deletions

View file

@ -2,24 +2,36 @@
"version": 1.1,
"tags": [
{
"name": "dd-incrementer",
"description": "A number input that allows more flexible increase/decrease buttons",
"name": "taf-icon",
"description": "Loads an icon asynchronously, caching the result for future uses",
"attributes": [
{ "name": "value", "description": "The initial value to put in the input" },
{ "name": "name", "description": "The form name to use when this input is used to submit data" },
{ "name": "min", "description": "The minimum value that this input can contain" },
{ "name": "max", "description": "The maximum value that this input can contain" },
{ "name": "smallStep", "description": "The value that the input is changed by when clicking a delta button or using the up/down arrow key" },
{ "name": "largeStep", "description": "The value that the input is changed by when clicking a delta button with control held or using the page up/ page down arrow key" }
{ "name": "name", "description": "The name of the icon, this is relative to the assets folder of the system" },
{ "name": "path", "description": "The full path of the icon, this will only be used if `name` isn't provided or fails to fetch." }
]
},
{
"name": "dd-icon",
"description": "Loads an icon asynchronously, caching the result for future uses",
"name": "taf-svg",
"description": "Loads an SVG file asynchronously, caching the result for future uses",
"attributes": [
{ "name": "name", "description": "The name of the icon, this is relative to the assets folder of the dotdungeon system" },
{ "name": "name", "description": "The name of the icon, this is relative to the assets folder of the system" },
{ "name": "path", "description": "The full path of the icon, this will only be used if `name` isn't provided or fails to fetch." }
]
},
{
"name": "taf-toggle",
"description": "A conveniency component for a toggle switch",
"attributes": [
{
"name": "type",
"description": "The type of toggle that this should be",
"values": [
{
"name": "round",
"description": "The slider is a full circle"
}
]
}
]
}
],
"globalAttributes": [],

1
assets/icons/chevron.svg Normal file
View file

@ -0,0 +1 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M7.637 25.637a8.997 8.997 0 0 1 12.727 0L50 55.274l29.637-29.637c3.515-3.516 9.21-3.516 12.727 0s3.515 9.21 0 12.727l-36 36a8.997 8.997 0 0 1-12.727 0l-36-36a8.997 8.997 0 0 1 0-12.727" fill-rule="evenodd" fill="var(--colour, currentcolor)"/></svg>

After

Width:  |  Height:  |  Size: 319 B

1
assets/icons/plus.svg Normal file
View file

@ -0,0 +1 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M81.801 41.801H58.199V18.199c0-4.5-3.7-8.2-8.2-8.2s-8.198 3.7-8.198 8.2v23.602H18.199c-4.5 0-8.2 3.7-8.2 8.2s3.7 8.198 8.2 8.198H41.8v23.602c0 4.5 3.699 8.2 8.199 8.2s8.199-3.7 8.199-8.2V58.2H81.8c4.5 0 8.2-3.699 8.2-8.199s-3.7-8.199-8.2-8.199"/></svg>

After

Width:  |  Height:  |  Size: 323 B

View file

@ -22,6 +22,8 @@ export default [
Hooks: `readonly`,
ui: `readonly`,
foundry: `readonly`,
Actor: `readonly`,
Item: `readonly`,
ChatMessage: `readonly`,
ActiveEffect: `readonly`,
fromUuid: `readonly`,
@ -29,7 +31,7 @@ export default [
// v14 Additions:
_loc: `readonly`,
_del: `reaonly`,
_del: `readonly`,
_replace: `readonly`,
},
},
@ -72,7 +74,16 @@ export default [
"@stylistic/space-infix-ops": `warn`,
"@stylistic/eol-last": `warn`,
"@stylistic/operator-linebreak": [`warn`, `before`],
"@stylistic/indent": [`warn`, `tab`],
"@stylistic/indent": [
`warn`,
`tab`,
{
SwitchCase: 1,
ignoredNodes: [
`.superClass CallExpression`,
],
},
],
"@stylistic/brace-style": [`off`],
"@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }],
"@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }],

View file

@ -2,10 +2,18 @@
"TYPES": {
"Actor": {
"player": "Player"
},
"Item": {
"generic": "Generic Item"
}
},
"taf": {
"settings": {
"actorDefaultAttributes": {
"name": "Remove Default Attributes",
"hint": "This removes the default attributes that are applied when a new actor is created, making it so that no attributes get created alongside the actor.",
"label": "Remove Attributes"
},
"canPlayersManageAttributes": {
"name": "Players Can Manage Attributes",
"hint": "This allows players who have edit access to a document to be able to edit what attributes those characters have via the attribute editor"
@ -31,16 +39,16 @@
"true": "Resizable"
}
},
"actorDefaultAttributes": {
"name": "Remove Default Attributes",
"hint": "This removes the default attributes that are applied when a new actor is created, making it so that no attributes get created alongside the actor.",
"label": "Remove Attributes"
"weightUnit": {
"name": "Weight Unit",
"hint": "This unit is used to display the units for the weights of items and carrying capacity of actors. This does NOTHING beyond adding the unit into the displays, it will not automatically convert between any units."
}
},
"sheet-names": {
"PlayerSheet": "Player Sheet",
"SingleModePlayerSheet": "Player Sheet (Always Editing)",
"AttributeOnlyPlayerSheet": "Player Sheet (Attributes Only)"
"AttributeOnlyPlayerSheet": "Player Sheet (Attributes Only)",
"GenericItemSheet": "System Item Sheet"
},
"misc": {
"Key": "Key",
@ -51,8 +59,15 @@
"confirm-and-close": "Confirm and Close",
"save-and-close": "Save and Close",
"delete": "Delete",
"edit": "Edit",
"resizable": "Resizable",
"not-resizable": "Not Resizable"
"not-resizable": "Not Resizable",
"item": {
"weight": "Weight",
"quantity": "Quantity",
"equipped": "Equipped",
"group": "Group"
}
},
"Apps": {
"Ask": {
@ -69,8 +84,21 @@
},
"PlayerSheet": {
"manage-attributes": "Manage Attributes",
"create-item": "Create Embedded Item",
"current-value": "Current value",
"max-value": "Maximum value"
"max-value": "Maximum value",
"carry-capacity-used": "({percent}% Used)",
"carrying-capacity": {
"placeholder": "Unlimited",
"title": "Carrying Capacity:",
"label": "Maximum carrying weight"
},
"total-weight": "Total weight",
"toggle-item-description": "Show/Hide Item Description",
"tab-names": {
"content": "Content",
"items": "Items"
}
},
"QueryStatus": {
"title": "Information Request Status",

View file

@ -13,11 +13,7 @@ import { toID } from "./utils/toID.mjs";
const { deepFreeze } = foundry.utils;
Object.defineProperty(
globalThis,
`taf`,
{
value: deepFreeze({
export const api = deepFreeze({
DialogManager,
QueryManager,
Apps: {
@ -31,6 +27,4 @@ Object.defineProperty(
localizer,
toID,
},
}),
},
);
});

View file

@ -1,6 +1,6 @@
import { __ID__, filePath } from "../consts.mjs";
import { attributeSorter } from "../utils/attributeSort.mjs";
import { ask } from "../utils/DialogManager.mjs";
import { attributeSorter } from "../utils/attributeSort.mjs";
import { localizer } from "../utils/localizer.mjs";
import { toID } from "../utils/toID.mjs";
@ -29,7 +29,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
label: `Save As Defaults`,
visible: () => game.user.isGM,
action: `saveAsDefault`,
}
},
],
},
form: {
@ -137,7 +137,8 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
const attrs = [];
for (const [id, data] of Object.entries(this.#attributes)) {
if (data == null) { continue };
if (game.release.generation >= 14 && data == _del) continue;
// Remove with issue: Foundry/taf#54
if (game.release.generation >= 14 && data == _del) {continue}
attrs.push({
id,
name: data.name,
@ -186,6 +187,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
static async #remove($e, element) {
const attribute = element.closest(`[data-attribute]`)?.dataset.attribute;
if (!attribute) { return };
// Remove with issue: Foundry/taf#54
if (game.release.generation < 14) {
delete this.#attributes[attribute];
this.#attributes[`-=${attribute}`] = null;
@ -227,7 +229,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
},
],
},
{ type: `divider` }
{ type: `divider` },
);
continue;
};
@ -254,6 +256,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
switch (response.state) {
case `errored`:
ui.notifications.error(response.error);
// eslint-disable-next-line no-fallthrough
case `fronted`:
return;
};

View file

@ -2,6 +2,8 @@ import { PlayerSheet } from "./PlayerSheet.mjs";
const { deepClone } = foundry.utils;
const removedParts = new Set([`content`]);
export class AttributeOnlyPlayerSheet extends PlayerSheet {
// #region Options
static DEFAULT_OPTIONS = {
@ -15,14 +17,45 @@ export class AttributeOnlyPlayerSheet extends PlayerSheet {
delete parts.content;
return parts;
};
static get TABS() {
const tabs = deepClone(super.TABS);
tabs.primary.tabs = tabs.primary.tabs
.filter(tab => tab.id !== `content`);
tabs.primary.initial = tabs.primary.tabs.at(0).id;
return tabs;
};
// #endregion Options
// #region Instance Data
/**
* This method is used in order to ensure that when we hide specific
* tabs due to programmatic logic (e.g. having no items), that the tab
* doesn't stay selected in the app if the logic for it being visible
* no longer holds true.
*/
_assertSelectedTabs() {
// Intentional No-Op Function
};
get hasContentTab() {
return false;
};
// #endregion Instance Data
// #region Lifecycle
_configureRenderOptions(options) {
super._configureRenderOptions(options);
// don't attempt to rerender the content
options.parts = options.parts?.filter(partID => partID !== `content`);
// don't attempt to rerender the parts that get removed
options.parts = options.parts?.filter(partID => !removedParts.has(partID));
};
// #endregion Lifecycle
// #region Data Prep
async _prepareItems(ctx) {
await super._prepareItems(ctx);
ctx.tabActive &&= this.hasItemsTab;
};
// #endregion Data Prep
};

View file

@ -0,0 +1,90 @@
import { __ID__, filePath } from "../consts.mjs";
import { TAFDocumentSheetMixin } from "./mixins/TAFDocumentSheetMixin.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ItemSheetV2 } = foundry.applications.sheets;
const { setProperty } = foundry.utils;
export class GenericItemSheet extends
TAFDocumentSheetMixin(
HandlebarsApplicationMixin(
ItemSheetV2,
)) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
__ID__,
`GenericItemSheet`,
],
position: {
width: 400,
height: 450,
},
window: {
resizable: true,
},
form: {
submitOnChange: true,
closeOnSubmit: false,
},
actions: {},
};
static PARTS = {
header: { template: filePath(`templates/GenericItemSheet/header.hbs`) },
content: { template: filePath(`templates/GenericItemSheet/content.hbs`) },
};
/**
* This tells the Application's TAFDocumentSheetMixin how to rerender this app
* when specific properties get changed on the actor, so that it doesn't need
* to full-app rendering if we can do a partial rerender instead.
*/
static PROPERTY_TO_PARTIAL = {
"name": [`header`],
"img": [`header`],
"system": [`content`],
};
// #endregion Options
// #region Instance Data
// #endregion Instance Data
// #region Lifecycle
async _prepareContext() {
return {
meta: {
idp: this.id,
editable: this.isEditable,
limited: this.isLimited,
},
item: this.item,
system: this.item.system,
};
};
async _preparePartContext(partID, ctx) {
switch (partID) {
case `content`: {
await this._prepareContentContext(ctx);
break;
};
};
return ctx;
};
async _prepareContentContext(ctx) {
const TextEditor = foundry.applications.ux.TextEditor.implementation;
setProperty(
ctx,
`enriched.system.description`,
await TextEditor.enrichHTML(this.item.system.description),
);
};
// #endregion Lifecycle
// #region Actions
// #endregion Actions
};

View file

@ -1,22 +1,22 @@
import { __ID__, filePath } from "../consts.mjs";
import { deleteItemFromElement, editItemFromElement } from "./utils.mjs";
import { AttributeManager } from "./AttributeManager.mjs";
import { attributeSorter } from "../utils/attributeSort.mjs";
import { config } from "../config.mjs";
import { Logger } from "../utils/Logger.mjs";
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
import { TAFDocumentSheetMixin } from "./mixins/TAFDocumentSheetMixin.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets;
const { getProperty, hasProperty } = foundry.utils;
const { getProperty } = foundry.utils;
const { ContextMenu, TextEditor } = foundry.applications.ux;
const propertyToParts = {
"name": [`header`],
"img": [`header`],
"system.attr": [`attributes`],
"system.attr.value": [`attributes`, `content`],
"system.attr.max": [`attributes`, `content`],
"system.content": [`content`],
};
export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
export class PlayerSheet extends
TAFDocumentSheetMixin(
HandlebarsApplicationMixin(
ActorSheetV2,
)) {
// #region Options
static DEFAULT_OPTIONS = {
@ -36,18 +36,101 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
closeOnSubmit: false,
},
actions: {
createEmbeddedItem: this.#createEmbeddedItem,
manageAttributes: this.#manageAttributes,
configureSheet: this.#configureSheet,
toggleExpand: this.#toggleExpand,
},
};
static PARTS = {
header: { template: filePath(`templates/PlayerSheet/header.hbs`) },
attributes: { template: filePath(`templates/PlayerSheet/attributes.hbs`) },
tabs: { template: filePath(`templates/generic/tabs.hbs`) },
content: { template: filePath(`templates/PlayerSheet/content.hbs`) },
items: {
template: filePath(`templates/PlayerSheet/item-lists.hbs`),
scrollable: [``],
templates: [
filePath(`templates/PlayerSheet/item.hbs`),
],
},
};
/**
* This tells the Application's TAFDocumentSheetMixin how to rerender this app
* when specific properties get changed on the actor, so that it doesn't need
* to full-app rendering if we can do a partial rerender instead.
*/
static PROPERTY_TO_PARTIAL = {
"name": [`header`],
"img": [`header`],
"system.attr": [`attributes`],
"system.attr.value": [`attributes`, `content`],
"system.attr.max": [`attributes`, `content`],
"system.content": [`content`],
"system.carryCapacity": [`items`],
};
static TABS = {
primary: {
initial: `content`,
labelPrefix: `taf.Apps.PlayerSheet.tab-names`,
tabs: [
{ id: `content` },
{ id: `items` },
],
},
};
// #endregion Options
// #region Instance Data
/**
* This Set is used to keep track of which items have had their full
* details expanded so that it can be persisted across rerenders as
* they occur.
*/
#expandedItems = new Set();
/**
* This method is used in order to ensure that when we hide specific
* tabs due to programmatic logic (e.g. having no items), that the tab
* doesn't stay selected in the app if the logic for it being visible
* no longer holds true.
*/
_assertSelectedTabs() {
const initial = this.constructor.TABS.primary.initial;
if (this.tabGroups.primary === `items` && !this.hasItemsTab) {
Logger.debug(`Asserting app "${this.id}" from tab "items" to "${initial}"`);
this.tabGroups.primary = initial;
};
};
/**
* A helper method that allows a shortcut to determine if a tab is visible
* solely based on it's ID. This usually redirects to the relevant getter in
* the class, but if the tab ID doesn't exist it always returns false.
*
* @param {string} tabID The ID of the relevant tab
* @returns Whether or not the tab is visible
*/
hasTab(tabID) {
switch (tabID) {
case `content`: return this.hasContentTab;
case `items`: return this.hasItemsTab;
};
return false;
};
get hasContentTab() {
return true;
};
get hasItemsTab() {
return this.actor.items.size > 0;
};
// #endregion Instance Data
// #region Lifecycle
_initializeApplicationOptions(options) {
const sizing = getProperty(options.document, `flags.${__ID__}.PlayerSheet.size`) ?? {};
@ -89,7 +172,8 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
_getHeaderControls() {
const controls = super._getHeaderControls();
controls.push({
controls.push(
{
icon: `fa-solid fa-at`,
label: `taf.Apps.PlayerSheet.manage-attributes`,
action: `manageAttributes`,
@ -99,24 +183,53 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
const editable = this.isEditable;
return isGM || (allowPlayerEdits && editable);
},
});
},
{
icon: `fa-solid fa-suitcase`,
label: `taf.Apps.PlayerSheet.create-item`,
action: `createEmbeddedItem`,
visible: () => {
return this.isEditable;
},
},
);
return controls;
};
_configureRenderOptions(options) {
// Only rerender the parts of the app that got changed
if (options.renderContext === `updateActor`) {
const parts = new Set();
for (const property in propertyToParts) {
if (hasProperty(options.renderData, property)) {
propertyToParts[property].forEach(partID => parts.add(partID));
};
};
options.parts = options.parts?.filter(part => !parts.has(part)) ?? Array.from(parts);
async _preRender(ctx, options) {
this._assertSelectedTabs();
return super._preRender(ctx, options);
};
super._configureRenderOptions(options);
async _onRender(ctx, options) {
await super._onRender(ctx, options);
new ContextMenu.implementation(
this.element,
`li.item`,
[
{
label: _loc(`taf.misc.edit`),
condition: (el) => {
const itemUuid = el.dataset.itemUuid;
const itemExists = itemUuid != null && itemUuid !== ``;
return this.isEditable && itemExists;
},
onClick: editItemFromElement,
},
{
label: _loc(`taf.misc.delete`),
condition: (el) => {
const itemUuid = el.dataset.itemUuid;
const itemExists = itemUuid != null && itemUuid !== ``;
return this.isEditable && itemExists;
},
onClick: deleteItemFromElement,
},
],
{ jQuery: false, fixed: true },
);
};
async close() {
@ -127,22 +240,36 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// #endregion Lifecycle
// #region Data Prep
async _preparePartContext(partID) {
let ctx = {
async _prepareContext() {
return {
meta: {
idp: this.id,
editable: this.isEditable,
},
actor: this.actor,
system: this.actor.system,
editable: this.isEditable,
};
};
async _preparePartContext(partID, ctx) {
switch (partID) {
case `attributes`: {
await this._prepareAttributes(ctx);
break;
};
case `tabs`: {
await this._prepareTabList(ctx);
break;
};
case `content`: {
await this._prepareContent(ctx);
break;
};
case `items`: {
await this._prepareItems(ctx);
break;
};
};
return ctx;
@ -162,22 +289,91 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
ctx.attrs = attrs.toSorted(attributeSorter);
};
async _prepareTabList(ctx) {
ctx.tabs = await this._prepareTabs(`primary`);
let amountVisible = 0;
for (const tabID in ctx.tabs) {
const visible = this.hasTab(tabID);
ctx.tabs[tabID].visible = visible;
if (visible) { amountVisible++ };
};
ctx.hideTabs = amountVisible <= 1;
};
async _prepareContent(ctx) {
// Whether or not the prose-mirror is toggled or always-edit
ctx.toggled = true;
ctx.tabActive = this.tabGroups.primary === `content`;
const TextEditor = foundry.applications.ux.TextEditor.implementation;
ctx.enriched = {
system: {
content: await TextEditor.enrichHTML(this.actor.system.content),
content: await TextEditor.implementation.enrichHTML(this.actor.system.content),
},
};
};
async _prepareItems(ctx) {
ctx.tabActive = this.tabGroups.primary === `items`;
let totalWeight = 0;
ctx.itemGroups = [];
for (const [groupName, items] of Object.entries(this.actor.itemTypes)) {
const preparedItems = [];
let summedWeight = 0;
for (const item of items) {
summedWeight += item.system.quantifiedWeight;
preparedItems.push(await this._prepareItem(item));
};
totalWeight += summedWeight;
ctx.itemGroups.push({
name: groupName.titleCase(),
items: preparedItems,
weight: config.weightFormatter(totalWeight),
});
};
ctx.totalWeight = config.weightFormatter(totalWeight);
ctx.hasCarryingCapacity = this.actor.system.carryCapacity != null;
ctx.carryCapacityPercent = Math.round(totalWeight / this.actor.system.carryCapacity * 100);
};
async _prepareItem(item) {
const ctx = {
uuid: item.uuid,
img: item.img,
name: item.name,
equipped: item.system.equipped,
quantity: item.system.quantity,
weight: config.weightFormatter(item.system.quantifiedWeight),
isExpanded: this.#expandedItems.has(item.uuid),
canExpand: item.system.description.length > 0,
};
ctx.description = ``;
if (item.system.description.length > 0) {
ctx.description = await TextEditor.implementation.enrichHTML(item.system.description);
};
return ctx;
};
// #endregion Data Prep
// #region Actions
#attributeManager = null;
/** @this {PlayerSheet} */
/**
* This action opens an instance of the AttributeManager application
* so that the user can edit and update all of the attributes for the
* actor. This persists the application instance for the duration of
* the ActorSheet's lifespan.
*
* @this {PlayerSheet}
*/
static async #manageAttributes() {
this.#attributeManager ??= new AttributeManager({ document: this.actor });
if (this.#attributeManager.rendered) {
@ -190,6 +386,13 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
};
};
/**
* This action overrides the default Foundry action in order to tell
* it to open my custom DocumentSheetConfig application instead of
* opening the non-customized sheet config app.
*
* @this {PlayerSheet}
*/
static async #configureSheet(event) {
event.stopPropagation();
if ( event.detail > 1 ) { return }
@ -205,5 +408,52 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
window: { windowId: this.window.windowId },
});
};
/**
* This action is used by the item lists in order to expand/collapse
* the descriptions while maintaining that state across renders.
*
* @this {PlayerSheet}
*/
static async #toggleExpand(event, target) {
const element = target.closest(`[data-item-uuid]`);
const { itemUuid } = element?.dataset ?? {};
if (!itemUuid) { return };
const expanded = this.#expandedItems.has(itemUuid);
const newExpanded = !expanded;
this.#expandedItems[newExpanded ? `add` : `delete`]?.(itemUuid);
target.dataset.expanded = newExpanded;
const collapses = element.querySelectorAll(`[data-expanded]`);
collapses.forEach(el => {
el.dataset.expanded = newExpanded;
});
};
/**
* Used by the sheet in order to create embedded items without needing to have
* equivalent World Items or Compendiums initially.
*
* @this {PlayerSheet}
*/
static async #createEmbeddedItem(event, target) {
let { itemGroup } = target.dataset ?? {};
if (itemGroup === `Items`) { itemGroup = undefined };
const data = {
name: Item.defaultName({
type: `generic`,
parent: this.actor,
}),
type: `generic`,
system: {
group: itemGroup,
},
};
const item = await Item.create(data, { parent: this.actor });
item?.sheet?.render({ force: true });
};
// #endregion Actions
};

View file

@ -26,10 +26,13 @@ export function StyledShadowElement(Base) {
/** @type {ShadowRoot} */
_shadow;
constructor() {
constructor({focusable = false} = {}) {
super();
this._shadow = this.attachShadow({ mode: `open` });
this._shadow = this.attachShadow({
mode: `open`,
delegatesFocus: focusable,
});
this._style = document.createElement(`style`);
this._shadow.appendChild(this._style);
};

View file

@ -0,0 +1,117 @@
import { StyledShadowElement } from "./StyledShadowElement.mjs";
const { debounce } = foundry.utils;
export class TafToggle extends StyledShadowElement(HTMLElement) {
static elementName = `taf-toggle`;
static formAssociated = true;
static _stylePath = `toggle.css`;
_mounted;
_internals;
constructor() {
super({ focusable: true });
this._internals = this.attachInternals();
this._internals.role = `checkbox`;
};
get type() {
return `checkbox`;
};
get name() {
return this.getAttribute(`name`);
};
set name(newName) {
this.setAttribute(`name`, newName);
};
get value() {
return this._input.value;
};
set value(newValue) {
this._input.value = newValue;
};
get checked() {
return this._input.checked ?? false;
};
set checked(newValue) {
if (typeof newValue !== `boolean`) { return };
this._input.checked = newValue;
this.#emitEvents();
};
get disabled() {
return this.matches(`:disabled`);
};
set disabled(value) {
this.toggleAttribute(`disabled`, value);
};
get editable() {
return true;
};
connectedCallback() {
super.connectedCallback();
if (this._mounted) { return };
this._internals.checked = this.hasAttribute(`checked`);
/*
This converts all of the double-dash prefixed properties on the
element to CSS variables so that they don't all need to be
provided by doing style=""
*/
for (const attrVar of this.attributes) {
if (attrVar.name?.startsWith(`var:`)) {
const prop = attrVar.name.replace(`var:`, ``);
this.style.setProperty(`--` + prop, attrVar.value);
};
};
const container = document.createElement(`div`);
container.classList = `toggle`;
container.dataset.type = `round`;
const input = this._input = document.createElement(`input`);
input.type = `checkbox`;
input.toggleAttribute(`switch`, true);
input.checked = this.hasAttribute(`checked`);
input.addEventListener(`change`, () => {
this.#emitEvents();
});
this.addEventListener(`click`, () => {
input.click();
});
container.appendChild(input);
const slider = document.createElement(`div`);
slider.classList = `slider`;
container.appendChild(slider);
this._shadow.appendChild(container);
this._mounted = true;
};
disconnectedCallback() {
super.disconnectedCallback();
if (!this._mounted) { return };
this._mounted = false;
};
#emitEvents = debounce(
() => {
this.dispatchEvent(new Event(`input`, {bubbles: true, cancelable: false}));
this.dispatchEvent(new Event(`change`, {bubbles: true, cancelable: false}));
},
150,
);
};

View file

@ -1,14 +1,15 @@
import { Logger } from "../../utils/Logger.mjs";
import { TafIcon } from "./Icon.mjs";
import { TafSVGLoader } from "./svgLoader.mjs";
import { TafToggle } from "./Toggle.mjs";
const components = [
TafSVGLoader,
TafIcon,
TafToggle,
];
export function registerCustomComponents() {
(CONFIG.CACHE ??= {}).componentListeners ??= [];
for (const component of components) {
if (!window.customElements.get(component.elementName)) {
Logger.debug(`Registering component "${component.elementName}"`);
@ -16,9 +17,6 @@ export function registerCustomComponents() {
component.elementName,
component,
);
if (component.formAssociated) {
CONFIG.CACHE.componentListeners.push(component.elementName);
}
};
}
};

View file

@ -0,0 +1,53 @@
import { updateForeignDocumentFromEvent } from "../utils.mjs";
const { hasProperty } = foundry.utils;
export function TAFDocumentSheetMixin(HandlebarsApplication) {
class TAFDocumentSheet extends HandlebarsApplication {
/** @type {Record<string, string[]> | null} */
static PROPERTY_TO_PARTIAL = null;
// #region Lifecycle
/**
* This override is used by the mixin in order to allow for partial
* re-rendering of applications based on what properties changed.
* It requires that a static PROPERTY_TO_PARTIAL to be defined as
* an object of path keys to arrays of part IDs in order to work.
* This will not interfere with renders that are not started as
* part of the actor update lifecycle.
*/
_configureRenderOptions(options) {
if (options.renderContext === `updateActor`) {
const propertyToParts = this.constructor.PROPERTY_TO_PARTIAL;
if (propertyToParts) {
const parts = new Set();
for (const property in propertyToParts) {
if (hasProperty(options.renderData, property)) {
propertyToParts[property].forEach(partID => parts.add(partID));
};
};
options.parts = options.parts?.filter(part => !parts.has(part)) ?? Array.from(parts);
}
};
super._configureRenderOptions(options);
};
async _onRender(...args) {
await super._onRender(...args);
this._attachEmbeddedChangeListeners();
};
_attachEmbeddedChangeListeners() {
/** @type {HTMLElement[]} */
const elements = this.element.querySelectorAll(`[data-foreign-name]`);
for (const el of elements) {
el.addEventListener(`change`, updateForeignDocumentFromEvent);
};
};
// #endregion Lifecycle
};
return TAFDocumentSheet;
};

58
module/apps/utils.mjs Normal file
View file

@ -0,0 +1,58 @@
/*
This file contains utility methods used by Applications in order to be
DRYer
*/
/**
* @param {Event} _event The click event
* @param {HTMLElement} target The element to operate on
*/
export async function editItemFromElement(_event, target) {
const itemEl = target.closest(`[data-item-uuid]`);
if (!itemEl) { return };
const uuid = itemEl.dataset.itemUuid;
if (!uuid) { return };
const item = await fromUuid(uuid);
item.sheet.render({ force: true, orBringToFront: true });
};
/**
* @param {Event} _event The click event
* @param {HTMLElement} target The element to operate on
*/
export async function deleteItemFromElement(_event, target) {
const itemEl = target.closest(`[data-item-uuid]`);
if (!itemEl) { return };
const uuid = itemEl.dataset.itemUuid;
if (!uuid) { return };
const item = await fromUuid(uuid);
item.deleteDialog();
};
/**
* Updates a document using the UUID, this is most useful for editing
* documents from a sheet of another document (e.g. an Item embedded
* in an Actor). This requires the dataset of the element to have a
* "data-foreign-name" which is the data path of the property being
* edited. As well as the input, or a parent of it, to have the
* "data-foreign-uuid" attribute, representing the UUID of the document
* to edit.
*/
export async function updateForeignDocumentFromEvent(event) {
const target = event.currentTarget;
const name = target.dataset.foreignName;
let uuid = target.dataset.foreignUuid;
uuid ??= target.closest(`[data-foreign-uuid]`)?.dataset.foreignUuid;
if (!name || !uuid) {
throw `Cannot edit foreign document with the name and UUID`;
};
let value = target.value;
switch (target.type) {
case `checkbox`: value = target.checked; break;
};
let doc = await fromUuid(uuid);
await doc?.update({ [name]: value });
};

7
module/config.mjs Normal file
View file

@ -0,0 +1,7 @@
import { formatWeight } from "./utils/formatWeight.mjs";
const { deepSeal } = foundry.utils;
export const config = deepSeal({
weightFormatter: formatWeight,
});

View file

@ -7,6 +7,11 @@ export class PlayerData extends foundry.abstract.TypeDataModel {
trim: true,
initial: ``,
}),
carryCapacity: new fields.NumberField({
min: 0,
nullable: true,
initial: null,
}),
attr: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ blank: false, trim: true }),

View file

@ -0,0 +1,41 @@
import { toPrecision } from "../../utils/roundToPrecision.mjs";
export class GenericItemData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
group: new fields.StringField({
blank: false,
trim: true,
initial: null,
nullable: true,
}),
weight: new fields.NumberField({
min: 0,
initial: 0,
nullable: false,
}),
quantity: new fields.NumberField({
integer: true,
initial: 1,
}),
equipped: new fields.BooleanField({
initial: true,
}),
description: new fields.HTMLField({
blank: true,
trim: true,
initial: ``,
}),
};
};
/**
* Calculates the total weight of the item based on the quantity of it, this
* rounds the number to the nearest 2 decimal places.
*/
get quantifiedWeight() {
const value = this.weight * this.quantity;
return toPrecision(Math.max(value, 0), 2);
};
};

View file

@ -6,19 +6,36 @@ const { hasProperty } = foundry.utils;
export class TAFActor extends Actor {
// #region Lifecycle
/**
* This makes sure that the actor gets created with the global attributes if
* they exist, while still allowing programmatic creation through the API with
* specific attributes.
*/
async _preCreate(data, options, user) {
// Assign the defaults from the world setting if they exist
const defaults = game.settings.get(__ID__, `actorDefaultAttributes`) ?? {};
if (!hasProperty(data, `system.attr`)) {
// Remove with issue: Foundry/taf#55
const value = game.release.generation > 13 ? _replace(defaults) : defaults;
this.updateSource({ "system.==attr": value });
};
return super._preCreate(data, options, user);
};
/**
* This resets the cache of the item groupings whenever a descedant document
* gets changed (created, updated, deleted) so that we keep the cache as close
* to accurate as can be possible.
*/
_onEmbeddedDocumentChange(...args) {
super._onEmbeddedDocumentChange(...args);
this.#sortedTypes = null;
};
// #endregion Lifecycle
// #region Token Attributes
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
const attr = foundry.utils.getProperty(this.system, attribute);
const current = isBar ? attr.value : attr;
@ -40,9 +57,18 @@ export class TAFActor extends Actor {
return allowed !== false ? this.update(updates) : this;
};
// #endregion Token Attributes
// #region Roll Data
getRollData() {
const data = {};
/*
All properties assigned during this phase of the roll data prep can potentially
be overridden by users creating attributes of the same key, if users shouldn't
be able to override, assign the property before the return of this function.
*/
const data = {
carryCapacity: this.system.carryCapacity ?? null,
};
if (`attr` in this.system) {
for (const attrID in this.system.attr) {
@ -60,4 +86,24 @@ export class TAFActor extends Actor {
return data;
};
// #endregion Roll Data
// #region Getters
#sortedTypes = null;
get itemTypes() {
if (this.#sortedTypes) { return this.#sortedTypes };
const types = {};
for (const item of this.items) {
if (item.type !== `generic`) {
types[item.type] ??= [];
types[item.type].push(item);
} else {
const group = item.system.group?.toLowerCase() ?? `items`;
types[group] ??= [];
types[group].push(item);
};
};
return this.#sortedTypes = types;
};
// #endregion Getters
};

View file

@ -1,7 +0,0 @@
const { Item } = foundry.documents;
export class TAFItem extends Item {
async _preCreate() {
return false;
};
};

View file

@ -1,15 +1,16 @@
// Apps
import { AttributeOnlyPlayerSheet } from "../apps/AttributeOnlyPlayerSheet.mjs";
import { GenericItemSheet } from "../apps/GenericItemSheet.mjs";
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
import { SingleModePlayerSheet } from "../apps/SingleModePlayerSheet.mjs";
// Data Models
import { PlayerData } from "../data/Player.mjs";
import { GenericItemData } from "../data/Item/generic.mjs";
import { PlayerData } from "../data/Actor/player.mjs";
// Documents
import { TAFActor } from "../documents/Actor.mjs";
import { TAFCombatant } from "../documents/Combatant.mjs";
import { TAFItem } from "../documents/Item.mjs";
import { TAFTokenDocument } from "../documents/Token.mjs";
// Settings
@ -25,16 +26,18 @@ import { registerSockets } from "../sockets/_index.mjs";
Hooks.on(`init`, () => {
Logger.debug(`Initializing`);
// #region Documents
CONFIG.Token.documentClass = TAFTokenDocument;
CONFIG.Actor.documentClass = TAFActor;
CONFIG.Combatant.documentClass = TAFCombatant;
// #endregion Documents
// #region Data Models
CONFIG.Actor.dataModels.player = PlayerData;
CONFIG.Item.dataModels.generic = GenericItemData;
// #endregion Data Models
// We disable items in the system for now
CONFIG.Item.documentClass = TAFItem;
delete CONFIG.ui.sidebar.TABS.items;
// #region Sheets
foundry.documents.collections.Actors.registerSheet(
__ID__,
PlayerSheet,
@ -54,6 +57,16 @@ Hooks.on(`init`, () => {
{ label: `taf.sheet-names.AttributeOnlyPlayerSheet` },
);
foundry.documents.collections.Items.registerSheet(
__ID__,
GenericItemSheet,
{
makeDefault: true,
label: `taf.sheet-names.GenericItemSheet`,
},
);
// #endregion Sheets
registerWorldSettings();
registerSockets();

6
module/hooks/ready.mjs Normal file
View file

@ -0,0 +1,6 @@
Hooks.on(`ready`, () => {
// Remove with issue: Foundry/taf#52
if (game.release.generation < 14 && globalThis._loc == null) {
globalThis._loc = game.i18n.format.bind(game.i18n);
};
});

View file

@ -1,6 +1,6 @@
import { __ID__ } from "../consts.mjs";
Hooks.on(`renderSettingsConfig`, (app, html, context, options) => {
Hooks.on(`renderSettingsConfig`, (app, html) => {
/*
This section is used to insert a button into the settings config that unsets
a world setting when it exists but doesn't allow any other form of editing it.

View file

@ -1,4 +1,20 @@
import "./api.mjs";
import "./hooks/init.mjs";
import "./hooks/ready.mjs";
import "./hooks/userConnected.mjs";
import "./hooks/renderSettingsConfig.mjs";
import { api } from "./api.mjs";
import { config } from "./config.mjs";
Object.defineProperty(
globalThis,
`taf`,
{
value: Object.seal({
api,
config,
}),
writable: false,
enumerable: true,
},
);

View file

@ -13,6 +13,15 @@ export function registerWorldSettings() {
scope: `world`,
});
game.settings.register(__ID__, `weightUnit`, {
name: `taf.settings.weightUnit.name`,
hint: `taf.settings.weightUnit.hint`,
config: true,
type: String,
default: ``,
scope: `world`,
});
game.settings.register(__ID__, `canPlayersManageAttributes`, {
name: `taf.settings.canPlayersManageAttributes.name`,
hint: `taf.settings.canPlayersManageAttributes.hint`,

View file

@ -0,0 +1,12 @@
import { __ID__ } from "../consts.mjs";
import { toPrecision } from "./roundToPrecision.mjs";
/**
* Formats a numerical value as a weight.
*
* @param {number} weight The numerical weight to format
*/
export function formatWeight(weight) {
const unit = game.settings.get(__ID__, `weightUnit`);
return toPrecision(weight, 2) + unit;
};

View file

@ -1,4 +1,4 @@
const config = Object.preventExtensions({
const config = Object.seal({
subKeyPattern: /@(?<key>[a-zA-Z.]+)/gm,
maxDepth: 10,
});

View file

@ -0,0 +1,20 @@
/**
* Takes a possibly-decimal value and rounds after a certain precision, keeping
* only the specified amount of decimals.
*
* @param {number} value The value that is to be rounded.
* @param {number} precision The number of decimal places to round to. Must be a
* positive integer.
* @returns The rounded number
*/
export function toPrecision(value, precision = 1) {
if (!Number.isInteger(precision)) {
throw `Precision must be an integer`;
};
if (precision < 0) {
throw `Precision must be greater than or equal to 0`;
};
const m = 10 ** precision;
return Math.round(value * m) / m;
};

View file

@ -0,0 +1,78 @@
.taf.GenericItemSheet {
> .window-content {
padding: 0;
color: var(--item-sheet-colour);
background: var(--item-sheet-background);
}
.sheet-header {
display: grid;
grid-template-columns: auto 1fr min-content 75px;
gap: 4px;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid var(--item-sheet-divider-colour);
img {
border-radius: 4px;
}
}
.content {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: inherit;
padding: 0.5rem;
}
.property {
display: grid;
grid-template-columns: 1fr 100px;
align-items: center;
justify-items: left;
gap: 8px;
taf-toggle {
justify-self: end;
}
}
.description {
flex-grow: 1;
overflow: hidden;
border-radius: 8px;
--table-row-color-odd: var(--table-header-bg-color);
&:not(:has(> prose-mirror)) {
padding: 0.5rem;
}
}
prose-mirror {
height: 100%;
background: var(--item-sheet-description-content-background);
--divider-colour: currentColor;
menu {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: var(--item-sheet-description-menu-background);
button {
color: var(--item-sheet-description-menu-colour);
}
}
}
input {
color: var(--item-sheet-input-colour);
background: var(--item-sheet-input-background);
}
taf-toggle {
--toggle-background: var(--item-sheet-input-background);
--slider-checked-colour: var(--item-sheet-toggle-slider-enabled-colour);
--slider-unchecked-colour: var(--item-sheet-toggle-slider-disabled-colour)
}
}

View file

@ -37,6 +37,141 @@
}
}
.items-tab.active {
display: flex;
flex-direction: column;
gap: 8px;
}
.inventory-summary {
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
background: var(--inventory-summary-background);
color: var(--inventory-summary-colour);
padding: 6px;
border-radius: 4px;
input {
width: 75px;
text-align: center;
background: var(--inventory-input-background);
color: var(--inventory-input-colour);
text-align: center;
&:disabled {
color: var(--inventory-input-disabled-colour);
cursor: not-allowed;
}
}
}
.item-list-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
border-radius: 6px 6px 0 0;
padding: 6px 6px 4px;
margin-bottom: 2px;
background: var(--item-list-header-background);
color: var(--item-list-header-colour);
button {
padding: 2px;
border: none;
aspect-ratio: 1;
height: unset;
min-height: unset;
background: var(--item-list-header-input-background);
color: var(--item-list-header-input-colour);
}
}
.item-list {
display: flex;
flex-direction: column;
gap: 2px;
list-style: none;
margin: 0;
padding: 0;
}
.item {
background: var(--item-card-background);
color: var(--item-card-colour);
overflow: hidden;
margin-bottom: 0;
.summary {
display: grid;
grid-template-columns: min-content auto 1fr 50px auto;
align-items: center;
gap: 8px;
background: var(--item-card-header-background);
color: var(--item-card-header-colour);
padding: 4px;
img {
--size: 35px;
width: var(--size);
height: var(--size);
border-radius: 6px;
}
.title {
display: flex;
flex-direction: column;
gap: 4px;
}
.name {
font-size: 1.1rem;
}
.subtitle {
font-size: 0.7rem;
opacity: 90%;
}
input, button {
background: var(--item-card-header-input-background);
color: var(--item-card-header-input-colour);
text-align: center;
&:disabled {
color: var(--item-card-header-disabled-input-colour);
}
}
}
.expand-button {
border: none;
aspect-ratio: 1;
&:focus-visible {
filter: brightness(150%);
outline: none;
}
&[data-expanded="true"] {
rotate: 180deg;
}
}
.full-details {
padding: 4px;
&[data-expanded="false"] {
display: none;
}
}
&:last-child {
border-radius: 0 0 6px 6px;
}
}
.content {
flex-grow: 1;
overflow: hidden;

View file

@ -6,4 +6,26 @@
gap: 0.5rem;
overflow: auto;
}
> .window-content nav.system-tabs {
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
gap: 8px;
button {
border: none;
text-shadow: none;
box-shadow: none;
&.active {
outline: 1px solid var(--tab-button-active-border);
}
&:hover {
background: var(--tab-button-hover-bg);
}
}
}
}

View file

@ -0,0 +1,55 @@
:host {
display: block;
}
input {
width: 0;
height: 0;
margin: 0;
padding: 0;
}
.slider {
width: var(--size, 16px);
height: var(--size, 16px);
background: var(
--slider-colour,
var(--toggle-slider-unchecked-colour)
);
transition: all 150ms ease-in-out;
border-radius: 9999px;
}
.toggle {
display: flex;
padding: var(--padding, 4px);
height: calc(var(--size, 16px) + (var(--padding, 4px) * 2));
width: calc((var(--size, 16px) * 2) + (var(--padding, 4px) * 2));
border-radius: 9999px;
background: var(
--toggle-background,
var(--toggle-background-colour)
);
box-sizing: border-box;
cursor: pointer;
/* Non-checked, clicking */
&:is(&:active, &.active) .slider {
width: calc(var(--size, 16px) * 1.5);
}
/* checked, non-clicking */
& > :checked + .slider {
transform: translateX(var(--size, 16px));
background: var(
--slider-checked-colour,
var(--toggle-slider-checked-colour)
);
}
/* checked, clicking */
&:is(&:active, &.active) > :checked + .slider {
width: calc(var(--size, 16px) * 1.5);
transform: translateX(calc(var(--size, 16px) * 0.5));
}
}

View file

@ -0,0 +1,5 @@
.taf > .window-content button {
&:disabled {
cursor: not-allowed;
}
}

View file

@ -1,9 +1,9 @@
.taf > .window-content div {
&.chip {
display: inline flex;
color: var(--chip-color);
color: var(--chip-colour);
background: var(--chip-background);
border: 1px solid var(--chip-border-color);
border: 1px solid var(--chip-border-colour);
border-radius: 4px;
.key {
@ -13,7 +13,7 @@
.value {
padding: 2px 4px;
border-radius: 0 4px 4px 0;
color: var(--chip-value-color);
color: var(--chip-value-colour);
background: var(--chip-value-background);
}
}

View file

@ -1,5 +1,14 @@
.taf > .window-content {
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-body);
color: currentColor;
margin: 0;
}
h1 { font-size: 1.25rem; }
h2 { font-size: 1.20rem; }
h3 { font-size: 1.15rem; }
h4 { font-size: 1.1rem; }
h5 { font-size: 1.1rem; }
h6 { font-size: 1.1rem; }
}

View file

@ -1,7 +1,8 @@
.taf > .window-content hr {
color: inherit;
display: block;
height: 1px;
background: rebeccapurple;
background: var(--divider-colour, rebeccapurple);
border-radius: 0;
margin: 0;
padding: 0;

View file

@ -1,4 +1,10 @@
.taf > .window-content input {
border: none;
&::placeholder {
color: color-mix(in srgb, currentColor 40%, transparent 60%);
}
&.large {
--input-height: 2.5rem;
font-size: 1.75rem;

View file

@ -1,16 +1,19 @@
@layer resets, themes, elements, components, partials, apps, exceptions;
/* Resets */
@import url("./resets/button.css") layer(resets);
@import url("./resets/hr.css") layer(resets);
@import url("./resets/inputs.css") layer(resets);
@import url("./resets/button.css") layer(resets);
@import url("./resets/tabs.css") layer(resets);
/* Themes */
@import url("./palettes/forgejo.css") layer(themes);
@import url("./themes/dark.css") layer(themes);
@import url("./themes/light.css") layer(themes);
/* Elements */
@import url("./elements/utils.css") layer(elements);
@import url("./elements/button.css") layer(elements);
@import url("./elements/div.css") layer(elements);
@import url("./elements/headers.css") layer(elements);
@import url("./elements/hr.css") layer(elements);
@ -24,6 +27,7 @@
@import url("./Apps/common.css") layer(apps);
@import url("./Apps/Ask.css") layer(apps);
@import url("./Apps/AttributeManager.css") layer(apps);
@import url("./Apps/GenericItemSheet.css") layer(apps);
@import url("./Apps/PlayerSheet.css") layer(apps);
@import url("./Apps/QueryStatus.css") layer(apps);
@import url("./Apps/TAFDocumentSheetConfig.css") layer(apps);

View file

@ -0,0 +1,49 @@
/*
Most of this file comes from a combination of Forgejo's themes
- https://codeberg.org/forgejo/forgejo/src/commit/b68caa311fe5e3b7118130c2894c5b396b319681/web_src/css/themes/theme-forgejo-dark.css
- https://codeberg.org/forgejo/forgejo/src/commit/b68caa311fe5e3b7118130c2894c5b396b319681/web_src/css/themes/theme-forgejo-light.css
Licensed under the GNU GPL v3
*/
:root {
/* Steel */
--steel-900: #10161d;
--steel-850: #131a21;
--steel-800: #171e26;
--steel-750: #1d262f;
--steel-700: #242d38;
--steel-650: #2b3642;
--steel-600: #374351;
--steel-550: #445161;
--steel-500: #515f70;
--steel-450: #5f6e80;
--steel-400: #6d7d8f;
--steel-350: #7c8c9f;
--steel-300: #8c9caf;
--steel-250: #9dadc0;
--steel-200: #aebed0;
--steel-150: #c0cfe0;
--steel-100: #d2e0f0;
/* Zinc */
--zinc-50: #fafafa;
--zinc-100: #f4f4f5;
--zinc-150: #ececee;
--zinc-200: #e4e4e7;
--zinc-250: #dcdce0;
--zinc-300: #d4d4d8;
--zinc-350: #babac1;
--zinc-400: #a1a1aa;
--zinc-450: #898992;
--zinc-500: #71717a;
--zinc-550: #61616a;
--zinc-600: #52525b;
--zinc-650: #484850;
--zinc-700: #3f3f46;
--zinc-750: #333338;
--zinc-800: #27272a;
--zinc-850: #1f1f23;
--zinc-900: #18181b;
}

View file

@ -1,3 +1,8 @@
.taf > .window-content button {
height: initial;
&:focus {
outline: none;
box-shadow: none;
}
}

10
styles/resets/tabs.css Normal file
View file

@ -0,0 +1,10 @@
.taf > .window-content {
nav.tabs.system-tabs {
all: initial;
}
nav.sheet-tabs.top-tabs {
margin-inline: 0;
margin-top: calc(var(--spacer-8) * -1);
}
}

View file

@ -4,10 +4,50 @@
--spinner-outer-colour: white;
--spinner-inner-colour: #FF3D00;
--toggle-background-colour: #171e26;
--toggle-slider-unchecked-colour: maroon;
--toggle-slider-checked-colour: green;
--tab-button-active-border: rebeccapurple;
--tab-button-hover-bg: var(--color-cool-3);
/* Actor Sheet Variables */
/* Use --steel-850 as the main sheet background */
--inventory-summary-background: var(--steel-800);
--inventory-summary-colour: var(--steel-100);
--inventory-input-background: var(--steel-650);
--inventory-input-colour: var(--steel-100);
--inventory-input-disabled-colour: var(--steel-350);
--item-list-header-background: var(--steel-800);
--item-list-header-colour: var(--steel-100);
--item-list-header-input-background: var(--steel-650);
--item-list-header-input-colour: var(--steel-100);
--item-card-background: #1d262f;
--item-card-colour: var(--steel-100);
--item-card-header-background: var(--steel-700);
--item-card-header-colour: var(--steel-100);
--item-card-header-input-background: var(--steel-650);
--item-card-header-input-colour: var(--steel-100);
--item-card-header-disabled-input-colour: var(--steel-350);
/* Item Sheet Variables */
--item-sheet-colour: var(--steel-100);
--item-sheet-background: var(--steel-800);
--item-sheet-divider-colour: var(--steel-700);
--item-sheet-input-colour: var(--steel-100);
--item-sheet-input-background: var(--steel-650);
--item-sheet-toggle-slider-enabled-colour: green;
--item-sheet-toggle-slider-disabled-colour: maroon;
--item-sheet-description-menu-colour: var(--steel-100);
--item-sheet-description-menu-background: var(--steel-700);
--item-sheet-description-content-background: var(--steel-650);
/* Chip Variables */
--chip-color: #fff7ed;
--chip-colour: #fff7ed;
--chip-background: #2b3642;
--chip-value-color: #fff7ed;
--chip-value-colour: #fff7ed;
--chip-value-background: #10161d;
--chip-border-color: var(--chip-value-background);
--chip-border-colour: var(--chip-value-background);
}

View file

@ -4,10 +4,13 @@
--spinner-outer-colour: black;
--spinner-inner-colour: #FF3D00;
--tab-button-active: rebeccapurple;
--tab-button-hover-bg: var(--color-light-3);
/* Chip Variables */
--chip-color: #18181b;
--chip-colour: #18181b;
--chip-background: #fafafa;
--chip-value-color: #18181b;
--chip-value-colour: #18181b;
--chip-value-background: #d4d4d8aa;
--chip-border-color: var(--chip-value-background);
--chip-border-colour: var(--chip-value-background);
}

View file

@ -39,7 +39,14 @@
"filePathFields": {}
}
},
"Item": {}
"Item": {
"generic": {
"htmlFields": [
"description"
],
"filePathFields": {}
}
}
},
"socket": true,
"flags": {

View file

@ -0,0 +1,50 @@
<div class="content">
<div class="property">
<label for="{{meta.idp}}-weight">
{{localize "taf.misc.item.weight"}}
</label>
<input
type="number"
id="{{meta.idp}}-weight"
name="system.weight"
value="{{system.weight}}"
{{disabled (not meta.editable)}}
>
</div>
<div class="property">
<label for="{{meta.idp}}-group">
{{localize "taf.misc.item.group"}}
</label>
<input
type="text"
id="{{meta.idp}}-group"
name="system.group"
value="{{system.group}}"
{{disabled (not meta.editable)}}
>
</div>
<div class="property">
<label for="{{meta.idp}}-equipped">
{{localize "taf.misc.item.equipped"}}
</label>
<taf-toggle
id="{{meta.idp}}-equipped"
name="system.equipped"
{{checked system.equipped}}
></taf-toggle>
</div>
<div class="bordered description">
{{#if meta.editable}}
<prose-mirror
name="system.description"
value="{{system.description}}"
collaborate="true"
data-document-uuid="{{item.uuid}}"
>
{{{ enriched.system.description }}}
</prose-mirror>
{{else}}
{{{ enriched.system.description }}}
{{/if}}
</div>
</div>

View file

@ -0,0 +1,28 @@
<header class="sheet-header bordered">
<img
src="{{item.img}}"
data-action="editImage"
data-edit="img"
title="{{item.name}}"
height="48"
width="48"
>
<input
type="text"
class="large"
name="name"
value="{{item.name}}"
title="{{item.name}}"
{{disabled (not meta.editable)}}
placeholder="{{localize "Name"}}"
>
<span aria-hidden="true">x</span>
<input
type="number"
name="system.quantity"
value="{{system.quantity}}"
{{disabled (not meta.editable)}}
data-tooltip
aria-label="{{localize "taf.misc.item.quantity"}}"
>
</header>

View file

@ -1,4 +1,8 @@
<div class="content">
<div
class="content tab {{ifThen tabActive "active" ""}}"
data-group="primary"
data-tab="content"
>
{{#if editable}}
<prose-mirror
class="actor-text"

View file

@ -0,0 +1,69 @@
<div
class="tab items-tab {{ifThen tabActive "active" ""}}"
data-group="primary"
data-tab="items"
>
<section class="inventory-summary">
<h3 class="grow">
{{localize "taf.Apps.PlayerSheet.carrying-capacity.title"}}
</h3>
{{#if hasCarryingCapacity}}
<div>
{{localize
"taf.Apps.PlayerSheet.carry-capacity-used"
percent=carryCapacityPercent
}}
</div>
{{/if}}
<input
type="text"
value="{{totalWeight}}"
disabled
aria-label="{{localize "taf.Apps.PlayerSheet.total-weight"}}"
data-tooltip
data-tooltip-direction="UP"
>
<div aria-hidden="true">
/
</div>
<input
type="number"
name="system.carryCapacity"
value="{{system.carryCapacity}}"
placeholder="{{localize "taf.Apps.PlayerSheet.carrying-capacity.placeholder"}}"
aria-label="{{localize "taf.Apps.PlayerSheet.carrying-capacity.label"}}"
data-tooltip
data-tooltip-direction="UP"
>
</section>
{{#each itemGroups as | group |}}
<section>
<div class="item-list-header">
{{#if @root.meta.editable}}
<button
data-action="createEmbeddedItem"
data-item-group="{{ group.name }}"
aria-label="{{localize "taf.Apps.PlayerSheet.create-item"}}"
data-tooltip
>
<taf-icon
name="icons/plus"
var:fill="currentColor"
></taf-icon>
</button>
{{/if}}
<h3 class="grow">
{{ group.name }}
</h3>
<span class="weight">
{{ group.weight }}
</span>
</div>
<ul class="item-list">
{{#each group.items as |item|}}
{{> (systemFilePath "templates/PlayerSheet/item.hbs") item }}
{{/each}}
</ul>
</section>
{{/each}}
</div>

View file

@ -0,0 +1,50 @@
<li
class="item"
data-item-uuid="{{uuid}}"
>
<div class="summary">
<taf-toggle
data-foreign-name="system.equipped"
data-foreign-uuid="{{uuid}}"
var:size="8px"
var:padding="2px"
{{checked equipped}}
></taf-toggle>
<img
src="{{img}}"
alt=""
>
<div class="title">
<span class="name">{{ name }}</span>
<span class="subtitle">{{ weight }}</span>
</div>
<input
id="{{uuid}}-quantity"
type="number"
value="{{quantity}}"
data-foreign-name="system.quantity"
data-foreign-uuid="{{uuid}}"
>
<button
type="button"
class="expand-button"
data-action="toggleExpand"
data-expanded="{{isExpanded}}"
{{disabled (not canExpand)}}
aria-label="{{localize "taf.Apps.PlayerSheet.toggle-item-description"}}"
>
<taf-icon
name="icons/chevron"
var:colour="var(--item-card-header-input-color)"
></taf-icon>
</button>
</div>
{{#if canExpand}}
<div
class="full-details"
data-expanded="{{isExpanded}}"
>
{{{ description }}}
</div>
{{/if}}
</li>

View file

@ -0,0 +1,19 @@
{{#if hideTabs}}
<template></template>
{{else}}
<nav class="tabs system-tabs">
{{#each tabs as |tab|}}
{{#if tab.visible}}
<button
type="button"
class="{{tab.cssClass}}"
data-action="tab"
data-group="{{tab.group}}"
data-tab="{{tab.id}}"
>
{{localize tab.label}}
</button>
{{/if}}
{{/each}}
</nav>
{{/if}}