Progress on ItemEditor interface and rendering

This commit is contained in:
Clément Pons 2025-10-13 17:56:22 +02:00
parent d187957915
commit 48e767944a
6 changed files with 64 additions and 70 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,21 @@ import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.ut
import { Tree } from "#shared/tree";
import { getText } from "#shared/i18n";
type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity'];
const categoryText: Record<Category, string> = {
'mundane': 'Objet inerte',
'armor': 'Armure',
'weapon': 'Arme',
'wondrous': 'Objet magique'
};
const rarityText: Record<Rarity, string> = {
'common': 'Commun',
'uncommon': 'Peu commun',
'rare': 'Rare',
'legendary': 'Légendaire'
};
const config = characterConfig as CharacterConfig;
export class HomebrewBuilder
{
@ -19,13 +34,12 @@ export class HomebrewBuilder
private _config: CharacterConfig;
private _featureEditor: FeatureEditor;
private _itemEditor: ItemEditor;
constructor(container: HTMLDivElement)
{
this._config = config as CharacterConfig;
this._featureEditor = new FeatureEditor();
this._itemEditor = new ItemEditor();
ItemEditor.config = this._config;
this._container = container;
this._tabs = tabgroup([
@ -317,8 +331,8 @@ export class HomebrewBuilder
editing = { id, type };
const buttons = div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:check'), () => {
this._config.texts[feature.description].default = editor.content;
this._config.texts[feature.description]['fr_FR'] = editor.content;
this._config.texts[feature.description]!.default = editor.content;
this._config.texts[feature.description]!['fr_FR'] = editor.content;
const rerender = render(type, feature);
option!.buttons.replaceWith(rerender.buttons);
@ -357,21 +371,6 @@ export class HomebrewBuilder
}
items()
{
type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity'];
const categoryText: Record<Category, string> = {
'mundane': 'Objet inerte',
'armor': 'Armure',
'weapon': 'Arme',
'wondrous': 'Objet magique'
};
const rarityText: Record<Rarity, string> = {
'common': 'Commun',
'uncommon': 'Peu commun',
'rare': 'Rare',
'legendary': 'Légendaire'
};
const defaultItem = (category: Category): ItemConfig => {
const common: CommonItemConfig = {
id: getID(),
@ -384,19 +383,19 @@ export class HomebrewBuilder
{
case 'armor':
return { ...common, category: category, health: 0, absorb: { percent: 0, static: 0 }, type: 'light' };
case 'mundane':
return { ...common, category: category };
case 'weapon':
return { ...common, category: category, damage: '0', type: ['classic'] };
case 'wondrous':
case 'mundane':
return { ...common, category: category };
}
};
const render = (item: ItemConfig) => {
return {
dom: div('flex flex-col gap-2 border border-light-35 dark:border-dark-35 p-1', [
div('flex flex-row justify-between', [ span('text-xl font-bold px-4', item.name), div('flex flex-row gap-2 items-center', [ div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', categoryText[item.category]), text('-'), span('text-sm text-light-70 dark:text-dark-70', rarityText[item.rarity]), tooltip(button(icon('radix-icons:pencil-1'), () => edit(item), 'p-1'), 'Modifier', 'top'), tooltip(button(icon('radix-icons:trash'), () => remove(item), 'p-1'), 'Supprimer', 'top') ]) ])]),
markdown(getText(item.description), undefined, { tags: { a: preview }, class: 'ms-2 px-2 py-1 border-l-4 border-light-30 dark:border-dark-30' }),
div('flex flex-row justify-between', [ span('text-xl font-bold ps-2', item.name), div('flex flex-row gap-2 items-center', [ div('flex flex-row items-center gap-2', [ tooltip(button(icon('radix-icons:pencil-1'), () => edit(item), 'p-1'), 'Modifier', 'top'), tooltip(button(icon('radix-icons:trash'), () => remove(item), 'p-1'), 'Supprimer', 'top') ]) ])]),
div('flex flex-row gap-2 px-4 items-center', [ span('text-sm text-light-70 dark:text-dark-70', categoryText[item.category]), text('-'), span('text-sm text-light-70 dark:text-dark-70', rarityText[item.rarity]), ]),
markdown(getText(item.description), undefined, { tags: { a: preview }, class: 'px-2 py-1 border-l-4 border-light-30 dark:border-dark-30 h-full' }),
]),
item,
};
@ -423,18 +422,13 @@ export class HomebrewBuilder
});
};
const edit = (item: ItemConfig) => {
const idx = options.findIndex(e => e.item === item);
this._itemEditor.edit(item).then(f => {
ItemEditor.edit(item).then(f => {
const idx = options.findIndex(e => e.item === item);
this._config.items[item.id] = f;
options[idx] = render(f);
}).catch((e) => {}).finally(() => {
setTimeout(popup.close, 150);
this._itemEditor.container.setAttribute('data-state', 'inactive');
});
const popup = fullblocker([this._itemEditor.container], {
priority: true, closeWhenOutside: false,
});
setTimeout(() => this._itemEditor.container.setAttribute('data-state', 'active'), 1);
const element = render(f);
options[idx]?.dom.replaceWith(element.dom);
options[idx] = element;
}).catch((e) => {});
}
const options = Object.values(this._config.items).map(e => render(e));
const optionHolder = div('grid grid-cols-3 gap-2', options.map(e => e.dom));
@ -688,31 +682,24 @@ export class FeatureEditor
}
export class ItemEditor
{
private _container: HTMLDivElement;
private _success?: Function;
private _failure?: Function;
private _item?: ItemConfig;
private _idInput: HTMLInputElement;
private _table: HTMLDivElement;
constructor()
static config: CharacterConfig;
static render(item: ItemConfig, success: (item: ItemConfig) => void, failure: (item: ItemConfig) => void)
{
this._idInput = dom("input", { attributes: { 'disabled': true }, class: `mx-4 text-light-70 dark:text-dark-70 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-25 dark:bg-dark-25 border-light-30 dark:border-dark-30` });
this._table = div('grid grid-cols-2 gap-4 px-2');
this._container = dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
const _item = JSON.parse(JSON.stringify(item)) as ItemConfig;
MarkdownEditor.singleton.content = getText(_item.description);
MarkdownEditor.singleton.onChange = (value) => ItemEditor.config.texts[_item.description]!.default = value;
return dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
div('flex flex-row justify-between items-center', [
tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => {
this._success!(this._item);
success!(_item);
MarkdownEditor.singleton.onChange = undefined;
}, 'p-1'), 'Valider', 'left'),
dom('label', { class: 'flex justify-center items-center my-2' }, [
dom('span', { class: 'pb-1 md:p-0', text: "ID" }),
this._idInput
dom('span', { class: 'pb-1 md:p-0', text: "Nom" }),
input('text', { defaultValue: _item.name, input: (v) => _item.name = v })
]),
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
this._failure!(this._item);
failure!(item);
MarkdownEditor.singleton.onChange = undefined;
}, 'p-1'), 'Annuler', 'left'),
]),
@ -720,6 +707,17 @@ export class ItemEditor
dom('span', { class: 'pb-1 md:p-0 w-full', text: "Description" }),
div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ MarkdownEditor.singleton.dom ]),
]),
dom('div', { class: 'flex flex-col justify-start items-start my-2 gap-4' }, [
dom('span', { class: 'pb-1 md:p-0 w-full', text: "Propriétés" }),
div('flex flex-row gap-4 px-2 py-1 items-center justify-center w-full', [
div('flex flex-col gap-2 items-center px-1', [ span('', 'Rareté'), select(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: _item.rarity, change: (v) => _item.rarity = v }), ]),
div('flex flex-col gap-2 items-center px-1', [ span('', 'Poids'), numberpicker({ defaultValue: _item.weight, input: (v) => _item.weight = v }), ]),
div('flex flex-col gap-2 items-center px-1', [ span('', 'Prix'), numberpicker({ defaultValue: _item.price, input: (v) => _item.price = v }), ]),
div('flex flex-col gap-2 items-center px-1', [ span('', 'Puissance magique'), numberpicker({ defaultValue: _item.power, input: (v) => _item.power = v }), ]),
div('flex flex-col gap-2 items-center px-1', [ span('', 'Charges'), numberpicker({ defaultValue: _item.charge, input: (v) => _item.charge = v }), ]),
div('flex flex-col gap-2 items-center px-1', [ span('', 'Equippable ?'), toggle({ defaultValue: _item.equippable, change: (v) => _item.equippable = v }), ]),
]),
]),
div('flex flex-col gap-2 w-full', [
div('flex flex-row justify-between', [
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
@ -727,22 +725,22 @@ export class ItemEditor
//this._table.appendChild(this._edit({ id: getID() }));
}, 'p-1'), 'Ajouter', 'left'),
]),
this._table,
div('grid grid-cols-2 gap-4 px-2'),
])
]);
])
}
edit(item: ItemConfig): Promise<ItemConfig>
static edit(item: ItemConfig): Promise<ItemConfig>
{
return new Promise((success, failure) => {
this._success = success;
this._failure = failure;
this._item = JSON.parse(JSON.stringify(item)) as ItemConfig;
//this._table.replaceChildren(...this._item.effect.map(this._renderEffect.bind(this)));
this._idInput.value = this._item.id;
MarkdownEditor.singleton.onChange = (e) => this._item!.description = e;
MarkdownEditor.singleton.content = this._item.description;
let container: HTMLElement, close: Function;
return new Promise<ItemConfig>((success, failure) => {
container = ItemEditor.render(item, success, failure);
close = fullblocker([container], {
priority: true, closeWhenOutside: false,
}).close;
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
}).finally(() => {
setTimeout(close, 150);
container.setAttribute('data-state', 'inactive');
});
}
/* private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement
@ -900,10 +898,6 @@ export class ItemEditor
let content = redraw();
return content;
} */
get container()
{
return this._container;
}
}
const featureChoices: Option<Partial<FeatureItem>>[] = [

View File

@ -1,5 +1,5 @@
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES } from "#shared/character.util";
import type { Localized } from "#shared/general";
import type { Localized } from "../types/general";
export type MainStat = typeof MAIN_STATS[number];
export type Ability = typeof ABILITIES[number];