Progress on option rendering

This commit is contained in:
Clément Pons 2025-08-18 17:42:07 +02:00
parent 72982a4ea9
commit 06276b3fbc
5 changed files with 49 additions and 22 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat } from "~/types/character";
import { div, dom, icon, text, type NodeChildren } from "./dom.util";
import { MarkdownEditor } from "./editor.util";
import { button, combobox, fakeA, input, numberpicker, select, type Option } from "./proses";
import { button, combobox, fakeA, foldable, input, numberpicker, select, type Option } from "./proses";
import { fullblocker, tooltip } from "./floating.util";
import { MAIN_STATS, mainStatShortTexts, mainStatTexts } from "./character.util";
import config from "#shared/character-config.json";
@ -338,7 +338,7 @@ export class FeatureEditor
MarkdownEditor.singleton.content = this._feature.description;
});
}
private _renderEffect(effect: FeatureItem): HTMLDivElement
private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement
{
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-center', [
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
@ -351,7 +351,7 @@ export class FeatureEditor
]) ]);
return content;
}
private _edit(effect: FeatureItem): HTMLDivElement
private _edit(effect: Partial<FeatureItem>): HTMLDivElement
{
const match = (effect: FeatureItem): Partial<FeatureItem> | undefined => {
switch(effect.category)
@ -383,14 +383,14 @@ export class FeatureEditor
}
let buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem;
const redraw = () => {
const drawByCategory = (buffer: Partial<FeatureItem>) => {
let top: NodeChildren = [], bottom: NodeChildren = [];
switch(buffer.category)
{
case 'value':
const summaryText = text(textFromEffect(buffer));
top = [
select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property) || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }),
select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }),
typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }),
button(icon('radix-icons:update'), () => {
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
@ -400,7 +400,7 @@ export class FeatureEditor
summaryText.textContent = textFromEffect(buffer);
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'),
];
bottom = [div('px-2 py-1', [summaryText])];
bottom = [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ];
break;
case 'list':
if(buffer.action === 'add')
@ -412,10 +412,10 @@ export class FeatureEditor
else
{
const editor = new MarkdownEditor();
editor.content = buffer.item;
editor.content = buffer.item ?? '';
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1', [ editor.dom ]) ];
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ];
}
}
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ];
@ -424,16 +424,32 @@ export class FeatureEditor
const add = () => {
const option: Extract<FeatureItem, { category: 'choice' }>["options"][number] = { id: getID(ID_SIZE), category: 'value', text: '', operation: 'add', property: '', value: 0 };
(buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option);
render(option);
list.appendChild(render(option));
};
const render = (option: FeatureEffect) => {
const remove = (option: FeatureEffect) => {
};
const render = (option: FeatureEffect): HTMLElement => {
const { top: _top, bottom: _bottom } = drawByCategory(option);
const combo = combobox(featureChoices.filter(e => (e?.value as FeatureItem)?.category !== 'choice'), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => {
option = { id: option.id, ...e } as FeatureEffect;
const element = render(option);
_content?.parentElement?.replaceChild(element, _content);
_content = element;
} });
let _content: HTMLElement = foldable(_bottom, [ div('flex flex-1 justify-between', [ div('flex flex-row',[ combo, ..._top ]), tooltip(button(icon('radix-icons:trash'), () => remove(option), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' } });
return _content;
}
const list = div('flex flex-row');
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }) ];
const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => render(e)) ?? []);
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ];
bottom = [ list ];
break;
default: break;
}
return { top, bottom };
}
const redraw = () => {
const { top, bottom } = drawByCategory(buffer);
return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [
div('flex flex-row flex-1', [
combobox(featureChoices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => {
@ -445,7 +461,7 @@ export class FeatureEditor
...top,
]),
div('flex', [ tooltip(button(icon('radix-icons:check'), approve, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), reject, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-auto items-center', bottom) ]);
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', bottom) ]);
}
let content = redraw();
@ -504,10 +520,13 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Choix', value: { category: 'choice', text: '', options: [] }, },
];
const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial<FeatureItem>[];
function textFromEffect(effect: FeatureItem)
function textFromEffect(effect: Partial<FeatureItem>): string
{
if(effect.category === 'value')
{
if(effect.property === undefined)
return '';
switch(effect.property)
{
case 'health':
@ -595,7 +614,7 @@ function textFromEffect(effect: FeatureItem)
case 'reaction':
case 'freeaction':
case 'passive':
return effect.action === 'add' ? effect.item : 'Suppression d\'effet.';
return effect.action === 'add' ? effect.item ?? '' : 'Suppression d\'effet.';
case 'spells':
return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".`;
case 'sickness':
@ -604,14 +623,12 @@ function textFromEffect(effect: FeatureItem)
}
else if(effect.category === 'choice')
{
return `${effect.text} (${effect.options.length} options)`;
return `${effect.text} (${effect.options?.length ?? 0} options)`;
}
else
{
return `Inconnu`;
}
}
function textFromValue(value: `modifier/${MainStat}` | number | false, settings?: {
function textFromValue(value?: `modifier/${MainStat}` | number | false, settings?: {
prefix?: { text?: string, positive?: string, negative?: string, truely?: string },
suffix?: { text?: string, positive?: string, negative?: string, truely?: string },
falsely?: string
@ -619,7 +636,7 @@ function textFromValue(value: `modifier/${MainStat}` | number | false, settings?
{
if(typeof value === 'string')
return `${settings?.prefix?.truely?.replaceAll('(s)', 's') ?? ''}${settings?.prefix?.text?.replaceAll('(s)', 's') ?? ''}${mainStatShortTexts[value.split('/')[1] as MainStat] ?? 'inconnu'}${settings?.suffix?.text?.replaceAll('(s)', 's') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', 's') ?? ''}`;
else if(value === false)
else if(value === false || value === undefined)
return settings?.falsely ?? '0';
else if(value >= 0)
return `${settings?.prefix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.prefix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}`;

View File

@ -423,3 +423,13 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
return field;
}
// Open by default
export function foldable(content: NodeChildren, title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
{
const fold = div(['group flex flex-1 w-full flex-col', settings?.class?.container], [
div('flex', [ dom('div', { listeners: { click: () => fold.toggleAttribute('data-active') }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div(['flex-1', settings?.class?.title], title) ]),
div(['hidden group-data-[active]:flex', settings?.class?.content], content),
]);
fold.toggleAttribute('data-active', settings?.open ?? true);
return fold;
}