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 { Tree } from "#shared/tree";
import { getText } from "#shared/i18n"; 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; const config = characterConfig as CharacterConfig;
export class HomebrewBuilder export class HomebrewBuilder
{ {
@ -19,13 +34,12 @@ export class HomebrewBuilder
private _config: CharacterConfig; private _config: CharacterConfig;
private _featureEditor: FeatureEditor; private _featureEditor: FeatureEditor;
private _itemEditor: ItemEditor;
constructor(container: HTMLDivElement) constructor(container: HTMLDivElement)
{ {
this._config = config as CharacterConfig; this._config = config as CharacterConfig;
this._featureEditor = new FeatureEditor(); this._featureEditor = new FeatureEditor();
this._itemEditor = new ItemEditor(); ItemEditor.config = this._config;
this._container = container; this._container = container;
this._tabs = tabgroup([ this._tabs = tabgroup([
@ -317,8 +331,8 @@ export class HomebrewBuilder
editing = { id, type }; 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'), () => { 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]!.default = editor.content;
this._config.texts[feature.description]['fr_FR'] = editor.content; this._config.texts[feature.description]!['fr_FR'] = editor.content;
const rerender = render(type, feature); const rerender = render(type, feature);
option!.buttons.replaceWith(rerender.buttons); option!.buttons.replaceWith(rerender.buttons);
@ -357,21 +371,6 @@ export class HomebrewBuilder
} }
items() 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 defaultItem = (category: Category): ItemConfig => {
const common: CommonItemConfig = { const common: CommonItemConfig = {
id: getID(), id: getID(),
@ -384,19 +383,19 @@ export class HomebrewBuilder
{ {
case 'armor': case 'armor':
return { ...common, category: category, health: 0, absorb: { percent: 0, static: 0 }, type: 'light' }; return { ...common, category: category, health: 0, absorb: { percent: 0, static: 0 }, type: 'light' };
case 'mundane':
return { ...common, category: category };
case 'weapon': case 'weapon':
return { ...common, category: category, damage: '0', type: ['classic'] }; return { ...common, category: category, damage: '0', type: ['classic'] };
case 'wondrous': case 'wondrous':
case 'mundane':
return { ...common, category: category }; return { ...common, category: category };
} }
}; };
const render = (item: ItemConfig) => { const render = (item: ItemConfig) => {
return { return {
dom: div('flex flex-col gap-2 border border-light-35 dark:border-dark-35 p-1', [ 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') ]) ])]), 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') ]) ])]),
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 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, item,
}; };
@ -423,18 +422,13 @@ export class HomebrewBuilder
}); });
}; };
const edit = (item: ItemConfig) => { const edit = (item: ItemConfig) => {
const idx = options.findIndex(e => e.item === item); ItemEditor.edit(item).then(f => {
this._itemEditor.edit(item).then(f => { const idx = options.findIndex(e => e.item === item);
this._config.items[item.id] = f; this._config.items[item.id] = f;
options[idx] = render(f); const element = render(f);
}).catch((e) => {}).finally(() => { options[idx]?.dom.replaceWith(element.dom);
setTimeout(popup.close, 150); options[idx] = element;
this._itemEditor.container.setAttribute('data-state', 'inactive'); }).catch((e) => {});
});
const popup = fullblocker([this._itemEditor.container], {
priority: true, closeWhenOutside: false,
});
setTimeout(() => this._itemEditor.container.setAttribute('data-state', 'active'), 1);
} }
const options = Object.values(this._config.items).map(e => render(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)); const optionHolder = div('grid grid-cols-3 gap-2', options.map(e => e.dom));
@ -688,31 +682,24 @@ export class FeatureEditor
} }
export class ItemEditor export class ItemEditor
{ {
private _container: HTMLDivElement; static config: CharacterConfig;
static render(item: ItemConfig, success: (item: ItemConfig) => void, failure: (item: ItemConfig) => void)
private _success?: Function;
private _failure?: Function;
private _item?: ItemConfig;
private _idInput: HTMLInputElement;
private _table: HTMLDivElement;
constructor()
{ {
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` }); const _item = JSON.parse(JSON.stringify(item)) as ItemConfig;
this._table = div('grid grid-cols-2 gap-4 px-2'); MarkdownEditor.singleton.content = getText(_item.description);
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]' }, [ 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', [ div('flex flex-row justify-between items-center', [
tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => { tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => {
this._success!(this._item); success!(_item);
MarkdownEditor.singleton.onChange = undefined; MarkdownEditor.singleton.onChange = undefined;
}, 'p-1'), 'Valider', 'left'), }, 'p-1'), 'Valider', 'left'),
dom('label', { class: 'flex justify-center items-center my-2' }, [ dom('label', { class: 'flex justify-center items-center my-2' }, [
dom('span', { class: 'pb-1 md:p-0', text: "ID" }), dom('span', { class: 'pb-1 md:p-0', text: "Nom" }),
this._idInput input('text', { defaultValue: _item.name, input: (v) => _item.name = v })
]), ]),
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => { tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
this._failure!(this._item); failure!(item);
MarkdownEditor.singleton.onChange = undefined; MarkdownEditor.singleton.onChange = undefined;
}, 'p-1'), 'Annuler', 'left'), }, 'p-1'), 'Annuler', 'left'),
]), ]),
@ -720,6 +707,17 @@ export class ItemEditor
dom('span', { class: 'pb-1 md:p-0 w-full', text: "Description" }), 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 ]), 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-col gap-2 w-full', [
div('flex flex-row justify-between', [ div('flex flex-row justify-between', [
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }), dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
@ -727,22 +725,22 @@ export class ItemEditor
//this._table.appendChild(this._edit({ id: getID() })); //this._table.appendChild(this._edit({ id: getID() }));
}, 'p-1'), 'Ajouter', 'left'), }, '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) => { let container: HTMLElement, close: Function;
this._success = success; return new Promise<ItemConfig>((success, failure) => {
this._failure = failure; container = ItemEditor.render(item, success, failure);
close = fullblocker([container], {
this._item = JSON.parse(JSON.stringify(item)) as ItemConfig; priority: true, closeWhenOutside: false,
}).close;
//this._table.replaceChildren(...this._item.effect.map(this._renderEffect.bind(this))); setTimeout(() => container.setAttribute('data-state', 'active'), 1);
this._idInput.value = this._item.id; }).finally(() => {
MarkdownEditor.singleton.onChange = (e) => this._item!.description = e; setTimeout(close, 150);
MarkdownEditor.singleton.content = this._item.description; container.setAttribute('data-state', 'inactive');
}); });
} }
/* private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement /* private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement
@ -900,10 +898,6 @@ export class ItemEditor
let content = redraw(); let content = redraw();
return content; return content;
} */ } */
get container()
{
return this._container;
}
} }
const featureChoices: Option<Partial<FeatureItem>>[] = [ 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 { 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 MainStat = typeof MAIN_STATS[number];
export type Ability = typeof ABILITIES[number]; export type Ability = typeof ABILITIES[number];