Merge pull request #52 from Eldritch-Oliver/feature/armour-sheet-improvements

Armour/Shield Sheet Improvements
This commit is contained in:
Oliver 2025-10-04 21:20:02 -06:00 committed by GitHub
commit 1228b32823
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 614 additions and 104 deletions

View file

@ -17,6 +17,7 @@
"RipCrypt": { "RipCrypt": {
"sheet-names": { "sheet-names": {
"AllItemsSheetV1": "RipCrypt Item Sheet", "AllItemsSheetV1": "RipCrypt Item Sheet",
"ArmourSheet": "Armour Sheet",
"CombinedHeroSheet": "Hero Sheet", "CombinedHeroSheet": "Hero Sheet",
"StatsCardV1": "Hero Stat Card", "StatsCardV1": "Hero Stat Card",
"CraftCardV1": "Hero Craft Card", "CraftCardV1": "Hero Craft Card",
@ -157,6 +158,7 @@
} }
}, },
"Apps": { "Apps": {
"damage-reduction": "@RipCrypt.common.damage reduction",
"traits-range": "@RipCrypt.common.traits & @RipCrypt.common.range", "traits-range": "@RipCrypt.common.traits & @RipCrypt.common.range",
"grit-skills": "@RipCrypt.common.abilities.grit Skills", "grit-skills": "@RipCrypt.common.abilities.grit Skills",
"gait-skills": "@RipCrypt.common.abilities.gait Skills", "gait-skills": "@RipCrypt.common.abilities.gait Skills",
@ -184,7 +186,8 @@
"star-button-tooltip": "Add Star", "star-button-tooltip": "Add Star",
"unstar-button": "Remove {name} as a starred ammo", "unstar-button": "Remove {name} as a starred ammo",
"unstar-button-tooltip": "Remove Star" "unstar-button-tooltip": "Remove Star"
} },
"protects-the-location": "Protects your {part}"
}, },
"notifs": { "notifs": {
"error": { "error": {

View file

@ -106,7 +106,6 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi
}; };
}; };
Logger.debug(`Context keys:`, Object.keys(ctx));
return ctx; return ctx;
}; };
// #endregion // #endregion

View file

@ -4,7 +4,8 @@ import { RichEditor } from "./RichEditor.mjs";
import { toBoolean } from "../consts.mjs"; import { toBoolean } from "../consts.mjs";
/** /**
* A mixin that takes the class from HandlebarsApplicationMixin and * A mixin that takes the class from HandlebarsApplicationMixin and combines it
* with utility functions / data that is used across all RipCrypt applications
*/ */
export function GenericAppMixin(HandlebarsApp) { export function GenericAppMixin(HandlebarsApp) {
class GenericRipCryptApp extends HandlebarsApp { class GenericRipCryptApp extends HandlebarsApp {
@ -91,8 +92,9 @@ export function GenericAppMixin(HandlebarsApp) {
ctx.meta.idp = this.document?.uuid ?? this.id; ctx.meta.idp = this.document?.uuid ?? this.id;
if (this.document) { if (this.document) {
ctx.meta.limited = this.document.limited; ctx.meta.limited = this.document.limited;
ctx.meta.editable = ctx.editable; ctx.meta.editable = this.isEditable || game.user.isGM;
} ctx.meta.embedded = this.document.isEmbedded;
};
delete ctx.editable; delete ctx.editable;
return ctx; return ctx;

View file

@ -0,0 +1,135 @@
import { filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ItemSheetV2 } = foundry.applications.sheets;
const { getProperty, hasProperty, setProperty } = foundry.utils;
export class ArmourSheet extends GenericAppMixin(HandlebarsApplicationMixin(ItemSheetV2)) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
`ripcrypt--item`,
`ArmourSheet`,
],
position: {
width: `auto`,
height: `auto`,
},
window: {
resizable: false,
},
form: {
submitOnChange: true,
closeOnSubmit: false,
},
};
static PARTS = {
header: {
template: filePath(`templates/Apps/partials/item-header.hbs`),
},
content: {
template: filePath(`templates/Apps/ArmourSheet/content.hbs`),
},
};
// #endregion
// #region Lifecycle
async _onRender() {
// remove the flag if it exists when we render the sheet
delete this.document?.system?.forceRerender;
};
/**
* Used to make it so that items that don't get updated because of the
* _preUpdate hook removing/changing the data submitted, can still get
* re-rendered when the diff is empty. If the document does get updated,
* this rerendering does not happen.
*
* @override
*/
async _processSubmitData(...args) {
await super._processSubmitData(...args);
if (this.document.system.forceRerender) {
await this.render(false);
};
};
/**
* Customize how form data is extracted into an expanded object.
* @param {SubmitEvent|null} event The originating form submission event
* @param {HTMLFormElement} form The form element that was submitted
* @param {FormDataExtended} formData Processed data for the submitted form
* @returns {object} An expanded object of processed form data
* @throws {Error} Subclasses may throw validation errors here to prevent form submission
* @protected
*/
_processFormData(event, form, formData) {
const data = super._processFormData(event, form, formData);
if (hasProperty(data, `system.location`)) {
let locations = getProperty(data, `system.location`);
locations = locations.filter(value => value != null);
setProperty(data, `system.location`, locations);
};
return data;
};
// #endregion
// #region Data Prep
async _preparePartContext(partId, _, opts) {
const ctx = await super._preparePartContext(partId, {}, opts);
ctx.item = this.document;
ctx.system = this.document.system;
switch (partId) {
case `content`: {
this._prepareContentContext(ctx, opts);
break;
};
};
return ctx;
};
async _prepareContentContext(ctx) {
ctx.weights = [
{
label: `RipCrypt.common.empty`,
value: null,
},
...Object.values(gameTerms.WeightRatings).map(opt => ({
label: `RipCrypt.common.weightRatings.${opt}`,
value: opt,
})),
];
ctx.accesses = [
{
label: `RipCrypt.common.empty`,
value: ``,
},
...gameTerms.Access.map(opt => ({
label: `RipCrypt.common.accessLevels.${opt}`,
value: opt,
})),
];
ctx.protects = {
head: this.document.system.location.has(gameTerms.Anatomy.HEAD),
body: this.document.system.location.has(gameTerms.Anatomy.BODY),
arms: this.document.system.location.has(gameTerms.Anatomy.ARMS),
legs: this.document.system.location.has(gameTerms.Anatomy.LEGS),
};
};
// #endregion
// #region Actions
// #endregion
};

View file

@ -0,0 +1,56 @@
import { filePath } from "../../consts.mjs";
import { StyledShadowElement } from "./mixins/StyledShadowElement.mjs";
const { renderTemplate } = foundry.applications.handlebars;
export class ArmourSummary extends StyledShadowElement(HTMLElement) {
static elementName = `armour-summary`;
static formAssociated = false;
/* Stuff for the mixin to use */
static _stylePath = `css/components/armour-summary.css`;
#container;
get type() {
return this.getAttribute(`type`) ?? `hero`;
};
set type(newValue) {
this.setAttribute(`type`, newValue);
};
_mounted = false;
async connectedCallback() {
super.connectedCallback();
if (this._mounted) { return };
/*
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);
};
};
this.#container = document.createElement(`div`);
this.#container.classList = `person`;
this.#container.innerHTML = await renderTemplate(
filePath(`templates/components/armour-summary.hbs`),
{ type: this.type },
);
this._shadow.appendChild(this.#container);
this._mounted = true;
};
disconnectedCallback() {
super.disconnectedCallback();
if (!this._mounted) { return };
this._mounted = false;
};
};

View file

@ -1,9 +1,11 @@
import { ArmourSummary } from "./ArmourSummary.mjs";
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";
import { RipCryptBorder } from "./RipCryptBorder.mjs"; import { RipCryptBorder } from "./RipCryptBorder.mjs";
import { RipCryptIcon } from "./Icon.mjs"; import { RipCryptIcon } from "./Icon.mjs";
import { RipCryptSVGLoader } from "./svgLoader.mjs"; import { RipCryptSVGLoader } from "./svgLoader.mjs";
const components = [ const components = [
ArmourSummary,
RipCryptIcon, RipCryptIcon,
RipCryptSVGLoader, RipCryptSVGLoader,
RipCryptBorder, RipCryptBorder,

View file

@ -5,7 +5,7 @@ import { Logger } from "../../utils/Logger.mjs";
import { requiredInteger } from "../helpers.mjs"; import { requiredInteger } from "../helpers.mjs";
const { fields } = foundry.data; const { fields } = foundry.data;
const { hasProperty, diffObject, mergeObject } = foundry.utils; const { getProperty, diffObject, mergeObject } = foundry.utils;
/** Used for Armour and Shields */ /** Used for Armour and Shields */
export class ArmourData extends CommonItemData { export class ArmourData extends CommonItemData {
@ -19,16 +19,16 @@ export class ArmourData extends CommonItemData {
blank: false, blank: false,
trim: true, trim: true,
nullable: false, nullable: false,
required: true,
options: Object.values(gameTerms.Anatomy), options: Object.values(gameTerms.Anatomy),
}), }),
{ {
nullable: false, nullable: false,
required: true, initial: [],
}, },
), ),
equipped: new fields.BooleanField({ equipped: new fields.BooleanField({
initial: false, initial: false,
required: true,
nullable: false, nullable: false,
}), }),
weight: new fields.StringField({ weight: new fields.StringField({
@ -59,7 +59,7 @@ export class ArmourData extends CommonItemData {
const diff = diffObject(this.parent._source, changes); const diff = diffObject(this.parent._source, changes);
let valid = await super._preUpdate(changes, options, user); let valid = await super._preUpdate(changes, options, user);
if (hasProperty(diff, `system.equipped`) && !this._canEquip()) { if (getProperty(diff, `system.equipped`) && !this._canEquip()) {
ui.notifications.error( ui.notifications.error(
localizer( localizer(
`RipCrypt.notifs.error.cannot-equip`, `RipCrypt.notifs.error.cannot-equip`,
@ -80,7 +80,10 @@ export class ArmourData extends CommonItemData {
return valid; return valid;
}; };
/** Used to tell the preUpdate logic whether or not to prevent the */ /**
* Used to tell the preUpdate logic whether or not to prevent the item from
* being equipped or not.
*/
_canEquip() { _canEquip() {
const parent = this.parent; const parent = this.parent;
if (!parent.isEmbedded || !(parent.parent instanceof Actor)) { if (!parent.isEmbedded || !(parent.parent instanceof Actor)) {
@ -94,7 +97,7 @@ export class ArmourData extends CommonItemData {
}; };
const slots = parent.parent.system.equippedArmour ?? {}; const slots = parent.parent.system.equippedArmour ?? {};
Logger.debug(`slots`, slots);
for (const locationTag of this.location) { for (const locationTag of this.location) {
if (slots[locationTag.toLowerCase()] != null) { if (slots[locationTag.toLowerCase()] != null) {
Logger.error(`Unable to equip multiple items in the same slot`); Logger.error(`Unable to equip multiple items in the same slot`);
@ -110,90 +113,4 @@ export class ArmourData extends CommonItemData {
return [...this.location].join(`, `); return [...this.location].join(`, `);
}; };
// #endregion // #endregion
// #region Sheet Data
getFormFields(_ctx) {
const fields = [
{
id: `quantity`,
type: `integer`,
label: `RipCrypt.common.quantity`,
path: `system.quantity`,
value: this.quantity,
min: 0,
},
{
id: `access`,
type: `dropdown`,
label: `RipCrypt.common.access`,
path: `system.access`,
value: this.access,
limited: false,
options: [
{
label: `RipCrypt.common.empty`,
value: ``,
},
...gameTerms.Access.map(opt => ({
label: `RipCrypt.common.accessLevels.${opt}`,
value: opt,
})),
],
},
{
id: `cost`,
type: `cost`,
label: `RipCrypt.common.cost`,
gold: this.cost.gold,
silver: this.cost.silver,
copper: this.cost.copper,
},
{
id: `weight`,
type: `dropdown`,
label: `RipCrypt.common.weightRating`,
path: `system.weight`,
value: this.weight,
options: [
{
label: `RipCrypt.common.empty`,
value: null,
},
...Object.values(gameTerms.WeightRatings).map(opt => ({
label: `RipCrypt.common.weightRatings.${opt}`,
value: opt,
})),
],
},
{
id: `location`,
type: `string-set`,
label: `RipCrypt.common.location`,
placeholder: `RipCrypt.Apps.location-placeholder`,
path: `system.location`,
value: this.locationString,
},
{
id: `protection`,
type: `integer`,
label: `RipCrypt.common.protection`,
value: this.protection,
path: `system.protection`,
min: 0,
},
];
if (this.parent.isEmbedded) {
fields.push({
id: `equipped`,
type: `boolean`,
label: `RipCrypt.common.equipped`,
value: this.equipped,
path: `system.equipped`,
});
};
return fields;
};
// #endregion
}; };

View file

@ -1,5 +1,6 @@
// Applications // Applications
import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs";
import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs";
import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs";
import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs"; import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs";
import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs"; import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs";
@ -37,7 +38,6 @@ import { registerUserSettings } from "../settings/userSettings.mjs";
import { registerWorldSettings } from "../settings/worldSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs";
const { Items, Actors } = foundry.documents.collections; const { Items, Actors } = foundry.documents.collections;
const { ItemSheet, ActorSheet } = foundry.appv1.sheets;
Hooks.once(`init`, () => { Hooks.once(`init`, () => {
Logger.log(`Initializing`); Logger.log(`Initializing`);
@ -74,10 +74,6 @@ Hooks.once(`init`, () => {
// #endregion // #endregion
// #region Sheets // #region Sheets
// Unregister core sheets
Items.unregisterSheet(`core`, ItemSheet);
Actors.unregisterSheet(`core`, ActorSheet);
// #region Actors // #region Actors
Actors.registerSheet(game.system.id, CombinedHeroSheet, { Actors.registerSheet(game.system.id, CombinedHeroSheet, {
makeDefault: true, makeDefault: true,
@ -114,6 +110,16 @@ Hooks.once(`init`, () => {
label: `RipCrypt.sheet-names.AllItemsSheetV1`, label: `RipCrypt.sheet-names.AllItemsSheetV1`,
themes: AllItemSheetV1.themes, themes: AllItemSheetV1.themes,
}); });
Items.registerSheet(game.system.id, ArmourSheet, {
makeDefault: true,
types: [`armour`, `shield`],
label: `RipCrypt.sheet-names.ArmourSheet`,
themes: ArmourSheet.themes,
});
Items.unregisterSheet(game.system.id, AllItemSheetV1, {
types: [`armour`, `shield`],
});
// #endregion // #endregion
// #endregion // #endregion

View file

@ -1,6 +1,6 @@
<div class="AllItemSheetV1"> <div class="AllItemSheetV1">
{{#if meta.editable}} {{#if meta.editable}}
<label for="{{meta.idp}}-name">Name</label> <label for="{{meta.idp}}-name">{{ rc-i18n "Name" }}</label>
<input <input
type="text" type="text"
id="{{meta.idp}}-name" id="{{meta.idp}}-name"

View file

@ -0,0 +1,140 @@
<div class="contents">
<div class="contents__left">
<label for="{{ meta.idp }}-access">
{{ rc-i18n "RipCrypt.common.access" }}
</label>
<select name="system.access" id="{{ meta.idp }}-access">
{{ rc-options system.access accesses localize=true }}
</select>
<label for="{{ meta.idp }}-weight">
{{ rc-i18n "RipCrypt.common.weightRating" }}
</label>
<select name="system.weight" id="{{ meta.idp }}-weight">
{{ rc-options system.weight weights localize=true }}
</select>
{{#if meta.embedded}}
<label for="{{meta.idp}}-equipped">
{{ rc-i18n "RipCrypt.common.equipped" }}
</label>
<input
type="checkbox"
id="{{meta.idp}}-equipped"
name="system.equipped"
{{ checked system.equipped }}
>
{{/if}}
<rc-border
var:border-color="var(--accent-1)"
>
<span slot="title">
{{ rc-i18n "RipCrypt.common.cost" }}
</span>
<div slot="content" class="content">
<label
for="{{ meta.idp }}-gold"
>
{{ rc-i18n "RipCrypt.common.currency.gold" }}
</label>
<input
type="number"
id="{{ meta.idp }}-gold"
name="system.cost.gold"
value="{{ system.cost.gold }}"
>
<label
for="{{ meta.idp }}-silver"
>
{{ rc-i18n "RipCrypt.common.currency.silver" }}
</label>
<input
type="number"
id="{{ meta.idp }}-silver"
name="system.cost.silver"
value="{{ system.cost.silver }}"
>
<label
for="{{ meta.idp }}-copper"
>
{{ rc-i18n "RipCrypt.common.currency.copper" }}
</label>
<input
type="number"
id="{{ meta.idp }}-copper"
name="system.cost.copper"
value="{{ system.cost.copper }}"
>
</div>
</rc-border>
</div>
<hr class="vertical">
<div class="contents__right">
<span class="section-pill">
{{ rc-i18n "RipCrypt.common.location" }}
</span>
<armour-summary
var:row-gap="8px"
>
<div slot="head">
<input
type="checkbox"
aria-label="{{
rc-i18n
"RipCrypt.Apps.protects-the-location"
part="@RipCrypt.common.anatomy.head"
}}"
value="head"
{{ checked protects.head }}
name="system.location"
>
</div>
<div slot="body">
<input
type="checkbox"
aria-label="{{
rc-i18n
"RipCrypt.Apps.protects-the-location"
part="@RipCrypt.common.anatomy.body"
}}"
value="body"
{{ checked protects.body }}
name="system.location"
>
</div>
<div slot="arms">
<input
type="checkbox"
aria-label="{{
rc-i18n
"RipCrypt.Apps.protects-the-location"
part="@RipCrypt.common.anatomy.arms"
}}"
value="arms"
{{ checked protects.arms }}
name="system.location"
>
</div>
<div slot="legs">
<input
type="checkbox"
aria-label="{{
rc-i18n
"RipCrypt.Apps.protects-the-location"
part="@RipCrypt.common.anatomy.legs"
}}"
value="legs"
{{ checked protects.legs }}
name="system.location"
>
</div>
</armour-summary>
<input
type="number"
class="center"
aria-label="{{ rc-i18n "RipCrypt.Apps.damage-reduction" }}"
data-tooltip="{{ rc-i18n "RipCrypt.Apps.damage-reduction" }}"
min="0"
name="system.protection"
value="{{ system.protection }}"
>
</div>
</div>

View file

@ -0,0 +1,135 @@
.ripcrypt.ArmourSheet > .window-content {
--input-height: 1rem;
--input-underline: none;
--col-gap: 8px;
--row-gap: 8px;
--string-tags-tag-text: var(--header-text);
--string-tags-tag-background: var(--header-background);
--string-tags-add-text: white;
--string-tags-add-background: var(--accent-1);
--string-tags-input-text: white;
--string-tags-input-background: var(--accent-2);
--input-text: white;
--input-background: var(--accent-2);
--button-text: white;
--button-background: var(--accent-2);
--pill-width: 100%;
--pill-border-radius: 4px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: var(--base-background);
color: var(--base-text);
hr {
background: var(--accent-1);
grid-column: 1 / -1;
height: 1px;
width: 90%;
margin: 0 auto;
&.vertical {
grid-column: unset;
height: 100%;
}
}
label, .label {
display: flex;
align-items: center;
box-sizing: border-box;
padding: 2px 4px;
text-transform: uppercase;
font-size: var(--font-size-14);
overflow: hidden;
text-overflow: ellipsis;
font-weight: bold;
}
button, input, select, .value {
border-radius: 4px;
padding: 2px 4px;
}
input[type="checkbox"] {
justify-self: end;
padding: 0;
}
.value {
border: 2px solid var(--accent-2);
}
.contents {
display: grid;
grid-template-columns: 300px 1px 118px;
gap: 8px;
> .contents__left {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
column-gap: var(--col-gap);
row-gap: var(--row-gap);
}
> .contents__right {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.section-pill {
background: var(--section-header-background);
color: var(--section-header-text);
padding: 0 4px;
border-radius: 999px;
text-align: right;
}
rc-border {
grid-column: 1 / -1;
> .content {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
column-gap: var(--col-gap);
row-gap: var(--row-gap);
}
.label {
background: purple;
}
}
.center {
text-align: center;
}
.compass {
--size: 35px;
width: var(--size);
height: var(--size);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2px solid var(--accent-1);
border-radius: 50%;
font-size: 1.1rem;
position: relative;
background: var(--base-background);
> .value {
background: none;
width: 70%;
text-align: center;
padding: 0;
}
}
}

View file

@ -6,6 +6,7 @@
@import url("./StatsCardV1/style.css"); @import url("./StatsCardV1/style.css");
@import url("./SkillsCardV1/style.css"); @import url("./SkillsCardV1/style.css");
@import url("./RichEditor/style.css"); @import url("./RichEditor/style.css");
@import url("./ArmourSheet/style.css");
@import url("./popover.css"); @import url("./popover.css");
@import url("./popovers/AmmoTracker/style.css"); @import url("./popovers/AmmoTracker/style.css");

View file

@ -0,0 +1,28 @@
{{!--
Required parameters:
"name" : the name of the item
"system.quantity" : the quantity of the item
"meta.idp" : the ID Prefix for the application
--}}
<header class="item-header">
<div class="name-row">
<input
type="text"
class="name"
aria-label="{{ rc-i18n "Name" }}"
name="name"
value="{{item.name}}"
{{disabled meta.limited}}
autocomplete="off"
>
<span aria-hidden="true">x</span>
<input
type="number"
class="quantity"
aria-label="{{ rc-i18n "RipCrypt.common.quantity" }}"
data-tooltip="{{ rc-i18n "RipCrypt.common.quantity" }}"
name="system.quantity"
value="{{system.quantity}}"
>
</div>
</header>

View file

@ -0,0 +1,35 @@
{{#if (eq type "hero")}}
<rc-svg
var:fill="var(--section-header-background)"
var:stroke="var(--base-background)"
var:stroke-width="2"
var:stroke-linejoin="rounded"
class="silhouette"
name="hero-silhouette"
></rc-svg>
{{else}}
<rc-svg
var:fill="var(--section-header-background)"
var:stroke="var(--base-background)"
var:stroke-width="2"
var:stroke-linejoin="rounded"
class="silhouette"
name="geist-silhouette.v2"
></rc-svg>
{{/if}}
<div class="head">
<slot name="head"></slot>
<span class="label" aria-hidden="true">{{ rc-i18n "RipCrypt.common.anatomy.head" }}</span>
</div>
<div class="body">
<slot name="body"></slot>
<span class="label" aria-hidden="true">{{ rc-i18n "RipCrypt.common.anatomy.body" }}</span>
</div>
<div class="arms">
<slot name="arms"></slot>
<span class="label" aria-hidden="true">{{ rc-i18n "RipCrypt.common.anatomy.arms" }}</span>
</div>
<div class="legs">
<slot name="legs"></slot>
<span class="label" aria-hidden="true">{{ rc-i18n "RipCrypt.common.anatomy.legs" }}</span>
</div>

View file

@ -0,0 +1,34 @@
:host {
display: inline-block;
}
.person {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr);
justify-items: center;
align-items: center;
position: relative;
row-gap: var(--row-gap);
> div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
}
> rc-svg {
position: absolute;
bottom: 0;
left: 0;
width: 58%;
}
.head, .body, .legs { grid-column: 1; }
.arms { grid-column: 2; }
.head { grid-row: 1; }
.body, .arms { grid-row: 2; }
.legs { grid-row: 3; }
}

View file

@ -11,6 +11,7 @@
background: var(--input-background); background: var(--input-background);
color: var(--input-text); color: var(--input-text);
padding: 0px 4px; padding: 0px 4px;
cursor: pointer;
&[type="text"], &[type="text"],
&[type="number"] { &[type="number"] {
@ -19,8 +20,10 @@
&[type="checkbox"] { &[type="checkbox"] {
all: revert-layer; all: revert-layer;
cursor: pointer;
--checkbox-checked-color: var(--accent-3); --checkbox-checked-color: var(--accent-3);
--checkbox-background-color: var(--accent-2); --checkbox-background-color: var(--accent-2);
--checkbox-checkmark-color: black;
} }
&::placeholder { &::placeholder {

View file

@ -1,4 +1,4 @@
@layer resets, themes, elements, components, apps, exceptions; @layer resets, themes, elements, partials, apps, exceptions;
/* Resets */ /* Resets */
@import url("./resets/inputs.css") layer(resets); @import url("./resets/inputs.css") layer(resets);
@ -19,6 +19,9 @@
@import url("./elements/string-tags.css") layer(elements); @import url("./elements/string-tags.css") layer(elements);
@import url("./elements/table.css") layer(elements); @import url("./elements/table.css") layer(elements);
/* Partials */
@import url("./partials/item-header.css") layer(partials);
/* Applications */ /* Applications */
@import url("../Apps/apps.css") layer(apps); @import url("../Apps/apps.css") layer(apps);

View file

@ -0,0 +1,11 @@
.item-header {
.name-row {
display: grid;
grid-template-columns: minmax(0, 1fr) min-content 50px;
gap: 4px;
.quantity {
text-align: center;
}
}
}