Several small fixes with rendering and floating components

This commit is contained in:
Clément Pons 2026-01-06 17:40:01 +01:00
parent 7021264c11
commit 0eaffcaa04
8 changed files with 66 additions and 50 deletions

BIN
db.sqlite

Binary file not shown.

View File

@ -1,17 +1,19 @@
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, EnchantementConfig, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses";
import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import proses, { a, preview } from "#shared/proses";
import { async, button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom.util";
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
import { clamp } from "#shared/general.util";
import markdown from "#shared/markdown.util";
import markdown, { defaultProses, filterMarkdown, renderMarkdown } from "#shared/markdown.util";
import { getText } from "#shared/i18n";
import type { User } from "~/types/auth";
import { MarkdownEditor } from "#shared/editor.util";
import { Socket } from "#shared/websocket.util";
import { raw, reactive } from '#shared/reactive';
import { Content, type ContentMap, type LocalContent } from "./content.util";
import type { Root } from "hast";
const config = characterConfig as CharacterConfig;
@ -558,6 +560,7 @@ export class CharacterBuilder extends CharacterCompiler
private _content?: RedrawableHTML;
private _stepsHeader: RedrawableHTML[] = [];
private _steps: Array<BuilderTabConstructor> = [];
private _stepContent: Array<BuilderTab> = [];
private _currentStep: number = 0;
private _helperText!: Text;
private id?: string;
@ -613,9 +616,9 @@ export class CharacterBuilder extends CharacterCompiler
AspectPicker,
];
this._stepsHeader = this._steps.map((e, i) =>
dom("div", { class: "group flex items-center", }, [
i !== 0 ? icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }) : undefined,
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
dom("div", { class: "group/header flex items-center", }, [
i !== 0 ? icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]/header:text-light-50 dark:group-data-[disabled]/header:text-dark-50 group-data-[disabled]/header:hover:border-transparent me-4" }) : undefined,
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]/header:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
])
);
this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
@ -656,7 +659,8 @@ export class CharacterBuilder extends CharacterCompiler
this._currentStep = step;
this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
this._stepContent[step] ??= (new this._steps[step]!(this));
this._content?.replaceChildren(...this._stepContent[step].dom);
this._helperText.textContent = this._steps[step]!.description;
}
@ -1334,7 +1338,7 @@ export class CharacterSheet
private character?: CharacterCompiler;
container: RedrawableHTML = div('flex flex-1 h-full w-full items-start justify-center');
private tabs?: RedrawableHTML;
private tab: string = 'abilities';
private tab: string = 'actions';
ws?: Socket;
constructor(id: string, user: ComputedRef<User | null>)
@ -1591,7 +1595,7 @@ export class CharacterSheet
]),
() => character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère' }) : undefined,
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère', }) : undefined,
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet' }) : undefined,
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle' }) : undefined,
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard' }) : undefined,
@ -1634,11 +1638,11 @@ export class CharacterSheet
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(3).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
div('flex flex-row items-center gap-2', [ ...Array(3).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]),
]),
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
div('flex flex-row flex-wrap gap-2', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => proses('a', a, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60' }))),
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
@ -1649,7 +1653,7 @@ export class CharacterSheet
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(2).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
div('flex flex-row items-center gap-2', [ ...Array(2).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]),
]),
div('flex flex-col gap-2', [

View File

@ -537,7 +537,7 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
]);
return container as RedrawableHTML;
}
export function floater(container: RedrawableHTML, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
export function floater(container: RedrawableHTML, content: NodeChildren | (() => NodeChildren), settings?: { delay?: number, href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean | { width: number, height: number }, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
{
let viewport = document.getElementById('mainContainer') ?? undefined;
let diffX, diffY;
@ -622,9 +622,10 @@ export function floater(container: RedrawableHTML, content: NodeChildren | (() =
Object.assign(floating.content.style, {
left: `${clamp(box.left, viewbox.left, viewbox.right)}px`,
top: `${clamp(box.top, viewbox.top, viewbox.bottom)}px`,
width: `${box.width + 21}px`,
height: `${box.height + 21}px`,
width: `${typeof settings?.pinned === 'object' ? settings.pinned.width : settings?.pinned === false ? box.width + 21 : 400}px`,
height: `${typeof settings?.pinned === 'object' ? settings.pinned.height : settings?.pinned === false ? box.height + 21 : 300}px`,
});
floating.content.attributeStyleMap.delete('bottom');
floating.content.attributeStyleMap.delete('right');
floating.stop();
@ -671,7 +672,7 @@ export function floater(container: RedrawableHTML, content: NodeChildren | (() =
const floating = popper(container, {
arrow: true,
delay: settings?.pinned ? 0 : 150,
delay: settings?.pinned ? 0 : settings?.delay ?? 150,
offset: 12,
cover: settings?.cover,
placement: settings?.position,

View File

@ -178,6 +178,7 @@ export class Content
}
static async getContent(id: string): Promise<LocalContent | undefined>
{
await Content.ready;
const overview = Content._overview[id];
if(!overview)

View File

@ -249,7 +249,7 @@ export class MarkdownEditor
}
constructor()
{
this._dom = div('flex h-full relative', [ div('absolute -top-1 -left-1 -translate-x-px -translate-y-px z-10 group', [ div('group-hover:hidden group-data-[focused]:hidden w-0 h-0 border-8 border-transparent border-l-light-40 dark:border-l-dark-40 border-t-light-40 dark:border-t-dark-40'), button([icon('radix-icons:gear')], MarkdownEditor.settings, 'p-1 hidden group-data-[focused]:block group-hover:block') ]), ]);
this._dom = div('flex h-full relative', [ div('absolute -top-1 -left-1 -translate-x-px -translate-y-px z-10 group/editor', [ div('group-hover/editor:hidden group-data-[focused]/editor:hidden w-0 h-0 border-8 border-transparent border-l-light-40 dark:border-l-dark-40 border-t-light-40 dark:border-t-dark-40'), button([icon('radix-icons:gear')], MarkdownEditor.settings, 'p-1 hidden group-data-[focused]/editor:block group-hover/editor:block') ]), ]);
this._decoratorVisible = ViewPlugin.fromClass(Decorator, {
decorations: undefined,
}).of(undefined);

View File

@ -160,7 +160,7 @@ export function popper(container: RedrawableHTML, properties?: PopperProperties)
clearTimeout(timeout);
state = 'showing';
timeout = setTimeout(() => {
const _show = () => {
if(state !== 'shown')
{
teleport!.appendChild(floater);
@ -179,7 +179,11 @@ export function popper(container: RedrawableHTML, properties?: PopperProperties)
});
}
state = 'shown';
}, properties?.delay ?? 0);
};
if(properties?.delay === 0)
_show();
else
timeout = setTimeout(_show, properties?.delay ?? 0);
}
}
@ -190,7 +194,7 @@ export function popper(container: RedrawableHTML, properties?: PopperProperties)
clearTimeout(timeout);
state = 'hiding';
timeout = setTimeout(() => {
const _hide = () => {
floater.remove();
_stop && _stop();
@ -198,7 +202,11 @@ export function popper(container: RedrawableHTML, properties?: PopperProperties)
floater.classList.toggle('hidden', true);
state = 'hidden';
}, properties?.delay ?? 0);
}
if(properties?.delay === 0)
_hide();
else
timeout = setTimeout(_hide, properties?.delay ?? 0);
}
}

View File

@ -1,4 +1,4 @@
import type { Root, RootContent } from "hast";
import type { Element, Root, RootContent } from "hast";
import { dom, text, type Class, type Node } from "#shared/dom.util";
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout, type Prose } from "#shared/proses";
import { heading } from "hast-util-heading";
@ -6,6 +6,7 @@ import { headingRank } from "hast-util-heading-rank";
import { parseId } from "#shared/general.util";
import { async } from "#shared/components.util";
export const defaultProses = { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th };
export function renderMarkdown(markdown: Root, proses: Record<string, Prose>)
{
return dom('div', {}, markdown.children.map(e => renderContent(e, proses)));
@ -43,11 +44,8 @@ export interface MDProperties
style?: string | Record<string, string>;
tags?: Record<string, Prose>;
}
export function markdownReference(content: string, filter?: string, properties?: MDProperties)
export function filterMarkdown(data: Root, filter: string)
{
const state = async('large', useMarkdown().parse(content).then(data => {
if(filter)
{
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
if(start !== -1)
@ -60,10 +58,16 @@ export function markdownReference(content: string, filter?: string, properties?:
if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank)
break;
}
data = { ...data, children: data.children.slice(start, end) };
}
return { ...data, children: data.children.slice(start, end) };
}
return data;
}
export function markdownReference(content: string, filter?: string, properties?: MDProperties)
{
const state = async('large', useMarkdown().parse(content).then(data => {
if(filter) data = filterMarkdown(data, filter);
const el = dom('div', properties, data.children.map(e => renderContent(e, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags))));
return el;

View File

@ -5,7 +5,6 @@ import { Canvas } from "#shared/canvas.util";
import { Content, iconByType, type LocalContent } from "#shared/content.util";
import { unifySlug } from "#shared/general.util";
import { async, floater } from "./components.util";
import type { FloatState } from "./floating.util";
export type CustomProse = (properties: any, children: NodeChildren) => Node;
@ -20,7 +19,7 @@ export const a: Prose = {
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
const element = dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: {
const element = properties?.navigate ?? true ? dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: {
'click': (e) => {
e.preventDefault();
router.push(link);
@ -30,6 +29,9 @@ export const a: Prose = {
...(children ?? []),
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
])
]) : dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [
...(children ?? []),
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
]);
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
@ -44,11 +46,11 @@ export const a: Prose = {
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
}
return div('');
})).current], { position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element;
})).current], { events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element;
}
}
export const preview: Prose = {
custom(properties: { href: string, class?: Class, label: string }, children) {
custom(properties: { href: string, class?: Class, label?: string, trigger?: 'hover' | 'click', navigate?: boolean }, children) {
const href = properties.href as string;
const { hash, pathname } = parseURL(href);
const router = useRouter();
@ -60,7 +62,6 @@ export const preview: Prose = {
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
]);
const magicKeys = useMagicKeys();
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{
@ -73,13 +74,10 @@ export const preview: Prose = {
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
}
return div();
})).current], { position: 'bottom-start', pinned: false,
})).current], { position: 'bottom-start', pinned: true,
events: {
show: ['mouseenter', 'mousemove'],
hide: ['mouseleave'],
onshow(state: FloatState) {
return state === 'shown' || state === 'hiding' || magicKeys.current.has('control') || magicKeys.current.has('meta');
}
show: ['click'],
hide: ['click'],
}, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
}
}