diff --git a/app/pages/admin/jobs.vue b/app/pages/admin/jobs.vue index dd8b423..0332f3b 100644 --- a/app/pages/admin/jobs.vue +++ b/app/pages/admin/jobs.vue @@ -16,6 +16,7 @@ const schemaList: Record | null> = { import { z } from 'zod/v4'; import { Icon } from '@iconify/vue'; import { Toaster } from '~~/shared/components'; +import { Content } from '~~/shared/content'; definePageMeta({ rights: ['admin'], @@ -51,6 +52,9 @@ async function fetch() error.value = null; success.value = true; + if(job.value === 'pull') + await Content.pull(true); + Toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, }); } catch(e) diff --git a/db.sqlite b/db.sqlite index d3cd7ac..94a82ff 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/nuxt.config.ts b/nuxt.config.ts index 1e8a763..53f8c66 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -178,6 +178,11 @@ export default defineNuxtConfig({ tokensPerInterval: 1 }, } + }, + '/api/file/**': { + security: { + rateLimiter: false, + } } }, security: { diff --git a/server/tasks/pull.ts b/server/tasks/pull.ts index 5e8bb65..c2b9093 100644 --- a/server/tasks/pull.ts +++ b/server/tasks/pull.ts @@ -71,10 +71,10 @@ export default defineTask({ const db = useDatabase(); db.transaction(tx => { - db.delete(projectFilesTable).run(); - db.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run(); - db.delete(projectContentTable).run(); - db.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run(); + tx.delete(projectFilesTable).run(); + tx.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run(); + tx.delete(projectContentTable).run(); + tx.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run(); }); return { result: true }; diff --git a/shared/campaign.ts b/shared/campaign.ts index 0a4c52f..0ff70a6 100644 --- a/shared/campaign.ts +++ b/shared/campaign.ts @@ -2,7 +2,7 @@ import { z } from "zod/v4"; import type { User } from "~/types/auth"; import characterConfig from '#shared/character-config.json'; import type { Campaign } from "~/types/campaign"; -import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom"; +import { div, dom, icon, span, text, type HTMLElement } from "#shared/dom"; import { button, foldable, loading, numberpicker, tabgroup, Toaster } from "#shared/components"; import { CharacterCompiler, colorByRarity, stateFactory, subnameFactory } from "#shared/character"; import { modal, tooltip } from "#shared/floating"; @@ -27,7 +27,7 @@ export const CampaignValidation = z.object({ class CharacterPrinter { compiler?: CharacterCompiler; - container: RedrawableHTML; + container: HTMLElement; name: string; id: number; constructor(character: number, name: string) @@ -64,7 +64,7 @@ export class CampaignSheet private campaign?: Campaign; private characters!: Array; - container: RedrawableHTML = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6'); + container: HTMLElement = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6'); ws?: Socket; constructor(id: string, user: ComputedRef) diff --git a/shared/canvas.ts b/shared/canvas.ts index e32d084..f20d61c 100644 --- a/shared/canvas.ts +++ b/shared/canvas.ts @@ -1,6 +1,6 @@ import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas"; import { clamp, lerp } from "#shared/general"; -import { dom, icon, svg, type RedrawableHTML } from "#shared/dom"; +import { dom, icon, svg, type HTMLElement } from "#shared/dom"; import render from "#shared/markdown"; import { tooltip } from "#shared/floating"; import { History } from "#shared/history"; @@ -200,7 +200,7 @@ export class Node extends EventTarget { properties: CanvasNode; - nodeDom?: RedrawableHTML; + nodeDom?: HTMLElement; constructor(properties: CanvasNode) { @@ -334,7 +334,7 @@ export class Edge extends EventTarget { properties: CanvasEdge; - edgeDom?: RedrawableHTML; + edgeDom?: HTMLElement; protected from: Node; protected to: Node; protected path: Path; @@ -388,7 +388,7 @@ export class EdgeEditable extends Edge private editing: boolean = false; private pathDom?: SVGPathElement; - private inputDom?: RedrawableHTML; + private inputDom?: HTMLElement; constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable) { super(properties, from, to); @@ -441,8 +441,8 @@ export class Canvas protected tweener: Tweener = new Tweener(); private debouncedTimeout: Timer = setTimeout(() => {}, 0); - protected transform!: RedrawableHTML; - container!: RedrawableHTML; + protected transform!: HTMLElement; + container!: HTMLElement; protected firstX = 0; protected firstY = 0; diff --git a/shared/character.ts b/shared/character.ts index d5ff240..50ccddd 100644 --- a/shared/character.ts +++ b/shared/character.ts @@ -3,7 +3,7 @@ import { z } from "zod/v4"; import characterConfig from '#shared/character-config.json'; import proses, { preview } from "#shared/proses"; import { button, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components"; -import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom"; +import { div, dom, icon, span, text, type HTMLElement } from "#shared/dom"; import { followermenu, fullblocker, tooltip } from "#shared/floating"; import { clamp } from "#shared/general"; import markdown from "#shared/markdown"; @@ -701,16 +701,16 @@ function setProperty(root: any, path: string, value: T | ((old: T) => T), for } export class CharacterBuilder extends CharacterCompiler { - private _container: RedrawableHTML; - private _content?: RedrawableHTML; - private _stepsHeader: RedrawableHTML[] = []; + private _container: HTMLElement; + private _content?: HTMLElement; + private _stepsHeader: HTMLElement[] = []; private _steps: Array = []; private _stepContent: Array = []; private _currentStep: number = 0; private _helperText!: Text; private id?: string; - constructor(container: RedrawableHTML, id?: string) + constructor(container: HTMLElement, id?: string) { super(Object.assign({}, defaultCharacter)); this.id = id; @@ -944,7 +944,7 @@ export class CharacterBuilder extends CharacterCompiler this.add(config.training[stat][level][choice]); } } - handleChoice(element: RedrawableHTML, feature: string) + handleChoice(element: HTMLElement, feature: string) { const choices = config.features[feature]!.effect.filter(e => e.category === 'choice'); if(choices.length === 0) @@ -981,8 +981,8 @@ type BuilderTabConstructor = { class PeoplePicker extends BuilderTab { private _nameInput: HTMLInputElement; - private _visibilityInput: RedrawableHTML; - private _options: RedrawableHTML[]; + private _visibilityInput: HTMLElement; + private _options: HTMLElement[]; static override header = 'Peuple'; static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.'; @@ -1043,7 +1043,7 @@ class LevelPicker extends BuilderTab private _levelInput: HTMLInputElement; private _pointsInput: HTMLInputElement; - private _options: RedrawableHTML[][]; + private _options: HTMLElement[][]; static override header = 'Niveaux'; static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.'; @@ -1112,8 +1112,8 @@ class LevelPicker extends BuilderTab e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level); e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level); e[1]?.childNodes.forEach((option, j) => { - 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as RedrawableHTML).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level)); - '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as RedrawableHTML).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j)); + 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level)); + '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLElement).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j)); }); }); } @@ -1125,11 +1125,11 @@ class LevelPicker extends BuilderTab class TrainingPicker extends BuilderTab { private _pointsInput: HTMLInputElement; - private _options: Record; + private _options: Record; private _tab: number = 0; - private _statIndicator: RedrawableHTML; - private _statContainer: RedrawableHTML; + private _statIndicator: HTMLElement; + private _statContainer: HTMLElement; static override header = 'Entrainement'; static override description = 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.'; @@ -1156,7 +1156,7 @@ class TrainingPicker extends BuilderTab this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }}); - this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); + this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); this._statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' }); this._statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e])))); @@ -1207,7 +1207,7 @@ class TrainingPicker extends BuilderTab e[0]?.classList.toggle("opacity-30", (i as TrainingLevel) > max); e[1]?.classList.toggle("opacity-30", (i as TrainingLevel) > max); e[1]?.childNodes.forEach((option, j) => { - '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as RedrawableHTML).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j))); + '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLElement).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j))); }) }) }); @@ -1223,9 +1223,9 @@ class TrainingPicker extends BuilderTab class AbilityPicker extends BuilderTab { private _pointsInput: HTMLInputElement; - private _options: RedrawableHTML[]; + private _options: HTMLElement[]; - private _maxs: RedrawableHTML[] = []; + private _maxs: HTMLElement[] = []; static override header = 'Compétences'; static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.'; @@ -1266,7 +1266,7 @@ class AbilityPicker extends BuilderTab ABILITIES.forEach((e, i) => { const max = (values[`bonus/abilities/${e}`] ?? 0); - const load = this._options[i]?.lastElementChild as RedrawableHTML | undefined; + const load = this._options[i]?.lastElementChild as HTMLElement | undefined; const valid = (compiled.abilities[e] ?? 0) <= max; if(load) { @@ -1293,7 +1293,7 @@ class AspectPicker extends BuilderTab private _filter: boolean = true; - private _options: RedrawableHTML[]; + private _options: HTMLElement[]; static override header = 'Aspect'; static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'; @@ -1370,7 +1370,7 @@ class AspectPicker extends BuilderTab this._mentalInput.value = mental.toString(); this._personalityInput.value = personality.toString(); - (this._content[1] as RedrawableHTML).replaceChildren(...this._options.filter(e => { + (this._content[1] as HTMLElement).replaceChildren(...this._options.filter(e => { const id = e.getAttribute('data-aspect')!; const aspect = config.aspects[id]!; @@ -1498,8 +1498,8 @@ export class CharacterSheet { private user: ComputedRef; private character?: CharacterCompiler; - container: RedrawableHTML = div('flex flex-1 h-full w-full items-start justify-center'); - private tabs?: RedrawableHTML; + container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center'); + private tabs?: HTMLElement; private tab: string = 'actions'; ws?: Socket; @@ -1582,7 +1582,7 @@ export class CharacterSheet publicNotes.content = this.character!.character.notes!.public!; privateNotes.content = this.character!.character.notes!.private!; - const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: RedrawableHTML }) => { + const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: HTMLElement }) => { character.variables[property] = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10); obj.edit.value = (character[property] - character.variables[property]).toString(); obj.edit.replaceWith(obj.readonly); @@ -1624,12 +1624,19 @@ export class CharacterSheet { id: 'effects', title: [ text('Afflictions') ], content: () => this.effectsTab(character) }, { id: 'notes', title: [ text('Notes') ], content: () => [ - div('flex flex-col gap-2', [ - div('flex flex-col gap-2 border-b border-light-35 dark:border-dark-35 pb-4', [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ publicNotes.dom ]) ]), - div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]), + div('flex flex-col h-full divide-y divide-light-30 dark:divide-dark-30', [ + foldable([ div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 flex-1', [ publicNotes.dom ]) ], + [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), ], { + class: { container: 'flex flex-col gap-2 data-[active]:flex-1 py-2', content: 'h-full' }, open: true + }), + foldable([ div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 flex-1', [ privateNotes.dom ]) ], + [ span('text-lg font-bold', 'Notes privés'), ], { + class: { container: 'flex flex-col gap-2 data-[active]:flex-1 py-2', content: 'h-full' }, open: false + }), + //div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]), ]) ] }, - ], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto' }, switch: v => { this.tab = v; } }); + ], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto h-full' }, switch: v => { this.tab = v; } }); this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full', [ div("flex flex-row gap-4 justify-between", [ @@ -1791,7 +1798,7 @@ export class CharacterSheet div('flex flex-col gap-2', [ 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', preview, [ 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', lowers: false }))), div('flex flex-col gap-2', { render: (e, _c) => _c ?? 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]), + div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', 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 } }), ]), list: character.lists.action }), ]), @@ -1806,7 +1813,7 @@ export class CharacterSheet div('flex flex-col gap-2', [ div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => proses('a', preview, [ 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', lowers: false }))), div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [ - div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]), + div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]), markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }), ]), list: character.lists.reaction }), ]), @@ -1820,7 +1827,7 @@ export class CharacterSheet div('flex flex-col gap-2', [ div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => proses('a', preview, [ 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', lowers: false }))), div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [ - div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]), + div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.freeaction[e]?.name }) ]), markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }), ]), list: character.lists.reaction }) ]), @@ -1832,7 +1839,7 @@ export class CharacterSheet { return [ div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [ - div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]), + div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.passive[e]?.name }) ]), markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }), ]), list: character.lists.passive }), ]; diff --git a/shared/components.ts b/shared/components.ts index e653f3b..621329b 100644 --- a/shared/components.ts +++ b/shared/components.ts @@ -1,5 +1,5 @@ import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router"; -import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type RedrawableHTML } from "#shared/dom"; +import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type HTMLElement } from "#shared/dom"; import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "#shared/floating"; import { clamp } from "#shared/general"; import { Tree } from "#shared/tree"; @@ -18,11 +18,11 @@ export function link(children: NodeChildren, properties?: NodeProperties & { act } } : undefined }, children); } -export function loading(size: 'small' | 'normal' | 'large' = 'normal'): RedrawableHTML +export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement { return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] }) } -export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise) +export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise) { let state = { current: loading(size) }; @@ -36,7 +36,7 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise return state; } -export function button(content: Node | NodeChildren, onClick?: (this: RedrawableHTML) => void, cls?: Class) +export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: Class) { const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow] text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40 @@ -73,17 +73,17 @@ export function buttongroup(options: Array<{ text: string, value: }}})) return div(['flex flex-row', settings?.class?.container], elements); } -export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: RedrawableHTML) => void +export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: HTMLElement) => void { let close: () => void; const element = div(['flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100', settings?.class?.container], options.map(e => dom('div', { class: ['flex flex-row px-2 py-1 hover:bg-light-35 dark:hover:bg-dark-35 cursor-pointer', settings?.class?.option], text: e.title, listeners: { click: () => { e.click(); close() } } }))); - return function(this: RedrawableHTML, target?: RedrawableHTML) { + return function(this: HTMLElement, target?: HTMLElement) { close = followermenu(target ?? this, [ element ], { arrow: true, placement: settings?.position, offset: 8 }).close; } } -export type Option = { text: string, render?: () => RedrawableHTML, value: T | Option[] } | undefined; -type StoredOption = { item: Option, dom: RedrawableHTML, container?: RedrawableHTML, children?: Array> }; -export function select>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): RedrawableHTML +export type Option = { text: string, render?: () => HTMLElement, value: T | Option[] } | undefined; +type StoredOption = { item: Option, dom: HTMLElement, container?: HTMLElement, children?: Array> }; +export function select>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement { let context: { close: Function }; let focused: number | undefined; @@ -151,7 +151,7 @@ export function select>(options: Array<{ text: string }) return select; } -export function multiselect>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): RedrawableHTML +export function multiselect>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement { let context: { close: Function }; let focused: number | undefined; @@ -225,7 +225,7 @@ export function multiselect>(options: Array<{ text: s } export function combobox>(options: Option[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean, fill?: 'contain' | 'cover' }) { - let context: { container: RedrawableHTML, content: NodeChildren, close: () => void }; + let context: { container: HTMLElement, content: NodeChildren, close: () => void }; let selected = true, tree: StoredOption[] = []; let focused: number | undefined; let currentOptions: StoredOption[] = []; @@ -470,10 +470,10 @@ export function foldable(content: Reactive, title: NodeChildren, s fold.toggleAttribute('data-active', settings?.open ?? true); return fold; } -type TableRow = Record RedrawableHTML) | RedrawableHTML | string>; +type TableRow = Record HTMLElement) | HTMLElement | string>; export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } }) { - const render = (item: (() => RedrawableHTML) | RedrawableHTML | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item; + const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item; return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]); } export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: boolean, class?: { container?: Class } }) @@ -494,7 +494,7 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo }, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]); return element; } -export function checkbox(settings?: { defaultValue?: boolean, change?: (this: RedrawableHTML, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } }) +export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } }) { let state = settings?.defaultValue ?? false; const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20 @@ -512,7 +512,7 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: Re }, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]); return element; } -export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: Reactive }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): RedrawableHTML +export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: Reactive }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): HTMLElement { let focus = settings?.focused ?? tabs[0]?.id; const titles = tabs.map(e => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() { @@ -535,15 +535,15 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles), content ]); - return container as RedrawableHTML; + return container as HTMLElement; } -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 }) +export function floater(container: HTMLElement, 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; let minimizeRect: DOMRect, minimized = false; - const events: { show: Array, hide: Array, onshow?: (this: RedrawableHTML, state: FloatState) => boolean, onhide?: (this: RedrawableHTML, state: FloatState) => boolean } = Object.assign({ + const events: { show: Array, hide: Array, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({ show: ['mouseenter', 'mousemove', 'focus'], hide: ['mouseleave', 'blur'], } as { show: Array, hide: Array }, settings?.events ?? {}); @@ -634,7 +634,7 @@ export function floater(container: RedrawableHTML, content: NodeChildren | (() = if(!floating.content.hasAttribute('data-pinned')) return; - [...this.parentElement?.children ?? []].forEach(e => (e as any as RedrawableHTML).attributeStyleMap.set('z-index', -1)); + [...this.parentElement?.children ?? []].forEach(e => (e as any as HTMLElement).attributeStyleMap.set('z-index', -1)); this.attributeStyleMap.set('z-index', 0); }, { passive: true }); } @@ -717,13 +717,13 @@ export interface ToastConfig timer?: boolean type?: ToastType } -type ToastDom = ToastConfig & { dom: RedrawableHTML }; +type ToastDom = ToastConfig & { dom: HTMLElement }; export type ToastType = 'info' | 'success' | 'error'; export class Toaster { private static _MAX_DRAG = 150; private static _list: Array = []; - private static _container: RedrawableHTML; + private static _container: HTMLElement; static init() { @@ -793,4 +793,23 @@ export class Toaster ], { easing: 'ease-in', duration: 100 }).onfinish = () => config.dom.remove(); Toaster._list = Toaster._list.filter(e => e !== config); } +} +export class DiceRoller +{ + private static _dom: HTMLElement; + static init() + { + DiceRoller.dispose(); + + DiceRoller._dom = dom('div', { attributes: { id: 'dice-roller' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72 empty:hidden' }); + document.body.appendChild(DiceRoller._dom); + } + static dispose() + { + DiceRoller._dom?.remove(); + } + static get dom() + { + return DiceRoller._dom; + } } \ No newline at end of file diff --git a/shared/content.ts b/shared/content.ts index 47cdb40..b73bf3d 100644 --- a/shared/content.ts +++ b/shared/content.ts @@ -2,7 +2,7 @@ import { safeDestr as parse } from 'destr'; import { Canvas, CanvasEditor } from "#shared/canvas"; import render, { renderMDAsText } from "#shared/markdown"; import { confirm, contextmenu, tooltip } from "#shared/floating"; -import { cancelPropagation, dom, icon, text, type Node, type RedrawableHTML } from "#shared/dom"; +import { cancelPropagation, dom, icon, text, type Node, type HTMLElement } from "#shared/dom"; import { loading } from "#shared/components"; import prose, { h1, h2 } from "#shared/proses"; import { getID, lerp, parsePath } from '~~/shared/general'; @@ -381,7 +381,7 @@ export class Content return handlers[overview.type].fromString(content); } - static async render(parent: RedrawableHTML, path: string): Promise | undefined> + static async render(parent: HTMLElement, path: string): Promise | undefined> { parent.appendChild(dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')])) @@ -489,7 +489,7 @@ const handlers: { [K in FileType]: ContentTypeHandler } = { return c.container; }, renderEditor: (content) => { - let element: RedrawableHTML; + let element: HTMLElement; if(content.hasOwnProperty('content')) { const c = new CanvasEditor(content.content); @@ -527,7 +527,7 @@ const handlers: { [K in FileType]: ContentTypeHandler } = { ]) }, renderEditor: (content) => { - let element: RedrawableHTML; + let element: HTMLElement; if(content.hasOwnProperty('content')) { MarkdownEditor.singleton.content = content.content; @@ -582,11 +582,11 @@ export const iconByType: Record = { export class Editor { tree!: TreeDOM; - container: RedrawableHTML; + container: HTMLElement; - selected?: Recursive; + selected?: Recursive; - private instruction: RedrawableHTML; + private instruction: HTMLElement; private cleanup?: CleanupFn; private history: History; @@ -638,7 +638,7 @@ export class Editor if(!action.element) { const depth = getPath(action.element as LocalContent).split('/').length; - action.element.element = this.tree.render(action.element as LocalContent, depth) as RedrawableHTML; + action.element.element = this.tree.render(action.element as LocalContent, depth) as HTMLElement; this.dragndrop(action.element as LocalContent, depth, (action.element as Recursive).parent); } this.tree.tree.insertAt(action.element as Recursive, action.to as number); @@ -718,7 +718,7 @@ export class Editor ])]); }); - this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive | undefined); + this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive | undefined); this.cleanup = this.setupDnD(); }); @@ -740,14 +740,14 @@ export class Editor private add(type: FileType, nextTo: Recursive) { const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length; - const item: Recursive & { element?: RedrawableHTML }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent }; + const item: Recursive & { element?: HTMLElement }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent }; this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]); } - private remove(item: LocalContent & { element?: RedrawableHTML }) + private remove(item: LocalContent & { element?: HTMLElement }) { this.history.add('overview', 'remove', [{ element: item, from: item.order, to: undefined }], true); } - private rename(item: LocalContent & { element?: RedrawableHTML }) + private rename(item: LocalContent & { element?: HTMLElement }) { let exists = true; const change = () => @@ -769,13 +769,13 @@ export class Editor text?.parentElement?.replaceChild(input, text); input.focus(); } - private toggleNavigable(e: Event, item: LocalContent & { element?: RedrawableHTML }) + private toggleNavigable(e: Event, item: LocalContent & { element?: HTMLElement }) { cancelPropagation(e); this.history.add('overview', 'navigable', [{ element: item, from: item.navigable, to: !item.navigable }], true); } - private togglePrivate(e: Event, item: LocalContent & { element?: RedrawableHTML }) + private togglePrivate(e: Event, item: LocalContent & { element?: HTMLElement }) { cancelPropagation(e); @@ -800,7 +800,7 @@ export class Editor element: this.tree.container, })); } - private dragndrop(item: Omit void }, "content">, depth: number, parent?: Omit): CleanupFn + private dragndrop(item: Omit void }, "content">, depth: number, parent?: Omit): CleanupFn { item.cleanup && item.cleanup(); @@ -896,7 +896,7 @@ export class Editor { return handlers[item.type].renderEditor(item); } - private select(item?: LocalContent & { element?: RedrawableHTML }) + private select(item?: LocalContent & { element?: HTMLElement }) { if(this.selected && item) { @@ -917,7 +917,7 @@ export class Editor useRouter().push({ hash: this.selected ? '#' + this.selected.id : '' }) this.container.firstElementChild!.replaceChildren(); - this.selected && this.container.firstElementChild!.appendChild(this.render(this.selected) as RedrawableHTML); + this.selected && this.container.firstElementChild!.appendChild(this.render(this.selected) as HTMLElement); } unmount() { @@ -937,8 +937,9 @@ export function getPath(item: any): string return parsePath(item.title) ?? item.path; } - -/* export function buildSpellMD() +import characterConfig from '#shared/character-config.json'; +const config = characterConfig as CharacterConfig; +export function buildSpellMD() { const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"]; const SPELL_TYPE_TEXTS = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" }; @@ -981,4 +982,4 @@ export function buildTrainingFile() return Object.entries(config.training).map(e => { return `# ${mainStatTexts[e[0] as MainStat]}\n` + Object.entries(e[1]).map(_e => `## Niveau ${_e[0]}\n` + _e[1].map(feature => renderMDAsText(getText(config.features[feature]!.description))).join('\nou\n')).join('\n'); }).join('\n'); -} */ \ No newline at end of file +} \ No newline at end of file diff --git a/shared/dom.ts b/shared/dom.ts index bc00ae2..cfcc579 100644 --- a/shared/dom.ts +++ b/shared/dom.ts @@ -2,14 +2,14 @@ import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'icon import { loading } from './components'; import { _defer, raw, reactivity, type Proxy, type Reactive } from './reactive'; -export type RedrawableHTML = HTMLElement; +export type HTMLElement = HTMLElement; export type Node = HTMLElement | SVGElement | Text | undefined; export type NodeChildren = Array> | undefined; export type Class = string | Array | Record | undefined; -type Listener = | ((this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any) | { +type Listener = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | { options?: boolean | AddEventListenerOptions; - listener: (this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any; + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any; } | undefined; export interface DOMList extends Array{ diff --git a/shared/editor.ts b/shared/editor.ts index 15f4901..9e8b8d3 100644 --- a/shared/editor.ts +++ b/shared/editor.ts @@ -7,7 +7,7 @@ import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, c import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common'; import { tags } from '@lezer/highlight'; -import { div, dom, icon, span, type RedrawableHTML } from '~~/shared/dom'; +import { div, dom, icon, span, type HTMLElement } from '~~/shared/dom'; import { callout as calloutExtension, calloutKeymap } from '#shared/grammar/callout.extension'; import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } from '#shared/grammar/wikilink.extension'; import renderMarkdown from '~~/shared/markdown'; @@ -56,7 +56,7 @@ class CalloutWidget extends WidgetType foldable?: boolean; content: string; - contentMD: RedrawableHTML; + contentMD: HTMLElement; static create(node: SyntaxNodeRef, state: EditorState): CalloutWidget | undefined { diff --git a/shared/feature.ts b/shared/feature.ts index 4801b3d..75fe0bd 100644 --- a/shared/feature.ts +++ b/shared/feature.ts @@ -1,5 +1,5 @@ import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character"; -import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom"; +import { div, dom, icon, span, text, type NodeChildren, type HTMLElement } from "#shared/dom"; import { MarkdownEditor } from "#shared/editor"; import { preview } from "#shared/proses"; import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components"; @@ -18,10 +18,10 @@ type Rarity = ItemConfig['rarity']; const config = reactive(characterConfig as CharacterConfig); export class HomebrewBuilder { - private _container: RedrawableHTML; - private _tabs: RedrawableHTML; + private _container: HTMLElement; + private _tabs: HTMLElement; - constructor(container: RedrawableHTML) + constructor(container: HTMLElement) { this._container = container; @@ -58,7 +58,7 @@ export class HomebrewBuilder }).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record) }; config.peoples[people.id] = people; - (content[0] as RedrawableHTML).appendChild(peopleRender(people)); + (content[0] as HTMLElement).appendChild(peopleRender(people)); } const render = (people: string, level: Level, feature: string) => { let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { @@ -144,7 +144,7 @@ export class HomebrewBuilder ]) } - const _options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); + const _options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); const _statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' }); const _statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(_options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e])))); @@ -507,7 +507,7 @@ class FeatureEditor private _arr: boolean; private option!: FeatureOption; - container!: RedrawableHTML; + container!: HTMLElement; constructor(list: Record | FeatureOption[], id: string, draft: boolean) { @@ -795,7 +795,7 @@ export class FeaturePanel } static edit(feature: Feature): Promise { - let container: RedrawableHTML, close: Function; + let container: HTMLElement, close: Function; return new Promise((success, failure) => { container = FeaturePanel.render(feature, success, failure); close = fullblocker([container], { @@ -869,7 +869,7 @@ export class ItemPanel } static edit(item: ItemConfig): Promise { - let container: RedrawableHTML, close: Function; + let container: HTMLElement, close: Function; return new Promise((success, failure) => { container = ItemPanel.render(item, success, failure); close = fullblocker([container], { diff --git a/shared/floating.ts b/shared/floating.ts index e565853..a1a8beb 100644 --- a/shared/floating.ts +++ b/shared/floating.ts @@ -1,5 +1,5 @@ import * as FloatingUI from "@floating-ui/dom"; -import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type RedrawableHTML } from "./dom"; +import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type HTMLElement } from "./dom"; import { button } from "./components"; import type { Reactive } from "./reactive"; @@ -10,7 +10,7 @@ export interface FloatingProperties arrow?: boolean; class?: Class; style?: Record | string; - viewport?: RedrawableHTML; + viewport?: HTMLElement; cover?: 'width' | 'height' | 'all' | 'none'; persistant?: boolean; } @@ -41,8 +41,8 @@ export interface ModalProperties onClose?: () => boolean | void; } type ModalInternals = { - container: RedrawableHTML; - content: RedrawableHTML; + container: HTMLElement; + content: HTMLElement; stop: Function; start: Function; show: Function; @@ -50,7 +50,7 @@ type ModalInternals = { persistant: boolean; }; -export let teleport: RedrawableHTML, minimizeBox: RedrawableHTML, cache: ModalInternals[] = [], hook: VoidFunction = () => {}; +export let teleport: HTMLElement, minimizeBox: HTMLElement, cache: ModalInternals[] = [], hook: VoidFunction = () => {}; export function init() { dispose(); @@ -76,7 +76,7 @@ function clear() cache = cache.filter(e => !(!e.persistant && e.content.remove())); } -export function popper(container: RedrawableHTML, properties?: PopperProperties) +export function popper(container: HTMLElement, properties?: PopperProperties) { let state: FloatState = 'hidden', timeout: Timer; const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]); @@ -233,7 +233,7 @@ export function popper(container: RedrawableHTML, properties?: PopperProperties) clearTimeout(timeout); } - function link(element: RedrawableHTML) { + function link(element: HTMLElement) { (properties?.events?.show ?? ['mouseenter', 'mousemove', 'focus']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, show)); (properties?.events?.hide ?? ['mouseleave', 'blur']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, hide)); } @@ -384,7 +384,7 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert }, }, content, properties); } -export function tooltip(container: RedrawableHTML, txt: string | Text, placement: FloatingUI.Placement, delay?: number): RedrawableHTML +export function tooltip(container: HTMLElement, txt: string | Text, placement: FloatingUI.Placement, delay?: number): HTMLElement { return popper(container, { arrow: true, diff --git a/shared/tree.ts b/shared/tree.ts index 7d17998..3c4ec60 100644 --- a/shared/tree.ts +++ b/shared/tree.ts @@ -1,5 +1,5 @@ import { Content, type LocalContent } from "./content"; -import { dom, type RedrawableHTML } from "./dom"; +import { dom, type HTMLElement } from "./dom"; import { clamp } from "./general"; export type Recursive = T & { @@ -138,14 +138,14 @@ export class Tree> } export class TreeDOM { - container: RedrawableHTML; - tree: Tree>; + container: HTMLElement; + tree: Tree>; private filter?: (item: Recursive>, depth: number) => boolean | undefined; - private folder: (item: Recursive>, depth: number) => RedrawableHTML; - private leaf: (item: Recursive>, depth: number) => RedrawableHTML; + private folder: (item: Recursive>, depth: number) => HTMLElement; + private leaf: (item: Recursive>, depth: number) => HTMLElement; - constructor(folder: (item: Recursive>, depth: number) => RedrawableHTML, leaf: (item: Recursive>, depth: number) => RedrawableHTML, filter?: (item: Recursive>, depth: number) => boolean | undefined) + constructor(folder: (item: Recursive>, depth: number) => HTMLElement, leaf: (item: Recursive>, depth: number) => HTMLElement, filter?: (item: Recursive>, depth: number) => boolean | undefined) { this.tree = new Tree(Content.tree); @@ -156,7 +156,7 @@ export class TreeDOM const elements = this.tree.accumulate(this.render.bind(this)); this.container = dom('div', { class: 'list-none select-none text-light-100 dark:text-dark-100 text-sm ps-2' }, elements); } - render(item: Recursive>, depth: number): RedrawableHTML | undefined + render(item: Recursive>, depth: number): HTMLElement | undefined { if(this.filter && !(this.filter(item, depth) ?? true)) return; @@ -187,7 +187,7 @@ export class TreeDOM { this.container.replaceChildren(...this.tree.flatten.map(e => e.element!)); } - toggle(item?: Omit, state?: boolean) + toggle(item?: Omit, state?: boolean) { if(item && item.type === 'folder') { @@ -202,7 +202,7 @@ export class TreeDOM }); } } - opened(item?: Omit): boolean | undefined + opened(item?: Omit): boolean | undefined { return item ? item.element!.getAttribute('data-state') === 'open' : undefined; }