diff --git a/db.sqlite b/db.sqlite index 0dd6d0a..ad07f6c 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/shared/character.util.ts b/shared/character.util.ts index 6ca2156..40ec684 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -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 = []; + private _stepContent: Array = []; 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) @@ -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', [ diff --git a/shared/components.util.ts b/shared/components.util.ts index be9b3e3..f27dc7b 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -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, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array, hide: Array, 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, position?: Placement, pinned?: boolean | { width: number, height: number }, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array, hide: Array, 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, diff --git a/shared/content.util.ts b/shared/content.util.ts index c8d01af..253e636 100644 --- a/shared/content.util.ts +++ b/shared/content.util.ts @@ -178,6 +178,7 @@ export class Content } static async getContent(id: string): Promise { + await Content.ready; const overview = Content._overview[id]; if(!overview) diff --git a/shared/editor.util.ts b/shared/editor.util.ts index c4b5ffe..48d2484 100644 --- a/shared/editor.util.ts +++ b/shared/editor.util.ts @@ -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); diff --git a/shared/floating.util.ts b/shared/floating.util.ts index 789e1af..70bcd9e 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -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); } } diff --git a/shared/markdown.util.ts b/shared/markdown.util.ts index f4dc5ff..fe6de37 100644 --- a/shared/markdown.util.ts +++ b/shared/markdown.util.ts @@ -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) { return dom('div', {}, markdown.children.map(e => renderContent(e, proses))); @@ -43,26 +44,29 @@ export interface MDProperties style?: string | Record; tags?: Record; } +export function filterMarkdown(data: Root, filter: string) +{ + const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1; + + if(start !== -1) + { + let end = start; + const rank = headingRank(data.children[start]!)!; + while(end < data.children.length) + { + end++; + if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank) + break; + } + 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) - { - const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1; - - if(start !== -1) - { - let end = start; - const rank = headingRank(data.children[start]!)!; - while(end < data.children.length) - { - end++; - if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank) - break; - } - data = { ...data, children: data.children.slice(start, end) }; - } - } + 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)))); diff --git a/shared/proses.ts b/shared/proses.ts index 8c52136..3e4a492 100644 --- a/shared/proses.ts +++ b/shared/proses.ts @@ -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; } }