You've already forked obsidian-visualiser
Fixes and responsive character sheet.
This commit is contained in:
24
shared/breakpoint.ts
Normal file
24
shared/breakpoint.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { reactive } from '#shared/reactive';
|
||||||
|
|
||||||
|
export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||||
|
|
||||||
|
const breakpoints = [
|
||||||
|
{ name: 'sm', query: '(min-width: 640px)' },
|
||||||
|
{ name: 'md', query: '(min-width: 768px)' },
|
||||||
|
{ name: 'lg', query: '(min-width: 1024px)' },
|
||||||
|
{ name: 'xl', query: '(min-width: 1280px)' },
|
||||||
|
{ name: '2xl', query: '(min-width: 1536px)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const breakpoint = reactive({ current: 'lg' as Breakpoint });
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const updateBreakpoint = () => {
|
||||||
|
for (let i = breakpoints.length - 1; i >= 0; i--)
|
||||||
|
if (window.matchMedia(breakpoints[i]!.query).matches)
|
||||||
|
{ breakpoint.current = breakpoints[i]!.name as Breakpoint; break; }
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBreakpoint();
|
||||||
|
breakpoints.forEach(b => window.matchMedia(b.query).addEventListener('change', updateBreakpoint));
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import type { User } from "~/types/auth";
|
|||||||
import { MarkdownEditor } from "#shared/editor";
|
import { MarkdownEditor } from "#shared/editor";
|
||||||
import { Socket } from "#shared/websocket";
|
import { Socket } from "#shared/websocket";
|
||||||
import { raw, reactive, reactivity } from '#shared/reactive';
|
import { raw, reactive, reactivity } from '#shared/reactive';
|
||||||
|
import { breakpoint } from '#shared/breakpoint';
|
||||||
import { parseDice, stringifyRoll } from "./dice";
|
import { parseDice, stringifyRoll } from "./dice";
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
@@ -1489,14 +1490,13 @@ export const subnameFactory = (item: ItemConfig, state?: ItemState): string[] =>
|
|||||||
result = ['Arme', ...(item as WeaponConfig).type.filter(e => e !== 'classic').map(e => weaponTypeTexts[e])];
|
result = ['Arme', ...(item as WeaponConfig).type.filter(e => e !== 'classic').map(e => weaponTypeTexts[e])];
|
||||||
break;
|
break;
|
||||||
case 'mundane':
|
case 'mundane':
|
||||||
result = ['Objet'];
|
result = item.consummable ? ['Objet'] : ['Consommable'];
|
||||||
break;
|
break;
|
||||||
case 'wondrous':
|
case 'wondrous':
|
||||||
result = ['Objet magique'];
|
result = item.consummable ? ['Objet magique'] : ['Consommable magique'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(state && state.improvements !== undefined && state.improvements.length > 0) result.push('Amélioré');
|
if(state && state.improvements !== undefined && state.improvements.length > 0) result.push('amélioré');
|
||||||
if(item.consummable) result.push('Consommable');
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -1527,7 +1527,6 @@ export class CharacterSheet
|
|||||||
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
|
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
|
||||||
private tabs?: HTMLElement;
|
private tabs?: HTMLElement;
|
||||||
private tab: string = localStorage.getItem('character-tab') ?? 'actions';
|
private tab: string = localStorage.getItem('character-tab') ?? 'actions';
|
||||||
|
|
||||||
private _variableDebounce: NodeJS.Timeout = setTimeout(() => {});
|
private _variableDebounce: NodeJS.Timeout = setTimeout(() => {});
|
||||||
|
|
||||||
ws?: Socket;
|
ws?: Socket;
|
||||||
@@ -1601,6 +1600,8 @@ export class CharacterSheet
|
|||||||
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
|
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
|
||||||
|
|
||||||
this.tabs = tabgroup([
|
this.tabs = tabgroup([
|
||||||
|
() => (breakpoint.current === 'sm' || breakpoint.current === 'md') ? { id: 'stats', title: [ text('Stats') ], content: () => this.sidebarTab() } : undefined,
|
||||||
|
|
||||||
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab() },
|
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab() },
|
||||||
|
|
||||||
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab() },
|
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab() },
|
||||||
@@ -1625,43 +1626,43 @@ export class CharacterSheet
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
] },
|
] },
|
||||||
], { 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; localStorage.setItem('this.character.compiled-tab', v); } });
|
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 lg:max-w-[960px] h-full', content: 'overflow-auto h-full' }, switch: v => { this.tab = v; localStorage.setItem('this.character.compiled-tab', v); } });
|
||||||
|
|
||||||
this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full w-full min-w-half', [
|
this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full w-full min-w-half', [
|
||||||
div("flex lg:flex-row flex-col gap-6 items-center justify-center", [
|
div("flex lg:flex-row gap-4 lg:gap-6 items-center justify-center", [
|
||||||
div("flex gap-6 items-center", [
|
div("flex gap-4 lg:gap-6 items-center", [
|
||||||
div('inline-flex select-none items-center justify-center overflow-hidden align-middle h-16', [
|
div('inline-flex select-none items-center justify-center overflow-hidden align-middle h-12 lg:h-16', [
|
||||||
div('text-light-100 dark:text-dark-100 leading-1 flex p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium', [
|
div('text-light-100 dark:text-dark-100 leading-1 flex p-3 lg:p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium', [
|
||||||
icon("radix-icons:person", { width: 16, height: 16 }),
|
icon("radix-icons:person", { width: 16, height: 16 }),
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("flex flex-col", [
|
div("flex flex-col", [
|
||||||
span("text-xl font-bold", () => this.character.compiled.name === '' ? "Inconnu" : this.character.compiled.name),
|
span("text-lg lg:text-xl font-bold", () => this.character.compiled.name === '' ? "Inconnu" : this.character.compiled.name),
|
||||||
span("text-sm", () => this.character.compiled.username ? `De ${this.character.compiled.username}` : `De ${this.user.value?.username}`)
|
span("text-xs lg:text-sm", () => this.character.compiled.username ? `De ${this.character.compiled.username}` : `De ${this.user.value?.username}`)
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("flex flex-col", [
|
div("flex flex-col", [
|
||||||
span("font-bold", () =>`Niveau ${this.character.compiled?.level ?? 0}`),
|
span("font-bold text-sm lg:text-base", () =>`Niveau ${this.character.compiled?.level ?? 0}`),
|
||||||
span('', () => this.character.compiled && this.character.compiled.race ? config.peoples[this.character.compiled.race]?.name ?? 'Peuple inconnu' : '')
|
span('text-xs lg:text-sm', () => this.character.compiled && this.character.compiled.race ? config.peoples[this.character.compiled.race]?.name ?? 'Peuple inconnu' : '')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
|
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-2 lg:py-4 ps-2 lg:ps-4 gap-4 lg:gap-8", [
|
||||||
div("flex flex-row items-center gap-2 text-3xl font-light", [
|
div("flex flex-row items-center gap-1 lg:gap-2 text-xl sm:text-2xl lg:text-3xl font-light", [
|
||||||
text("PV: "),
|
text("PV: "),
|
||||||
() => this.character.compiled ? dom("span", {
|
() => this.character.compiled ? dom("span", {
|
||||||
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
class: "font-bold px-1 lg:px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
||||||
text: () => `${this.character.compiled.health - this.character.compiled.variables.health}`,
|
text: () => `${this.character.compiled.health - this.character.compiled.variables.health}`,
|
||||||
listeners: { click: healthPanel.show },
|
listeners: { click: healthPanel.show },
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
() => this.character.compiled ? text('/') : text('-'),
|
() => this.character.compiled ? text('/') : text('-'),
|
||||||
() => this.character.compiled ? text(() => this.character.compiled.health) : undefined,
|
() => this.character.compiled ? text(() => this.character.compiled.health) : undefined,
|
||||||
]),
|
]),
|
||||||
div("flex flex-row items-center gap-2 text-3xl font-light", [
|
div("flex flex-row items-center gap-1 lg:gap-2 text-xl sm:text-2xl lg:text-3xl font-light", [
|
||||||
text("Mana: "),
|
text("Mana: "),
|
||||||
() => this.character.compiled ? dom("span", {
|
() => this.character.compiled ? dom("span", {
|
||||||
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
class: "font-bold px-1 lg:px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
||||||
text: () => `${this.character.compiled.mana - this.character.compiled.variables.mana}`,
|
text: () => `${this.character.compiled.mana - this.character.compiled.variables.mana}`,
|
||||||
listeners: { click: healthPanel.show },
|
listeners: { click: healthPanel.show },
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
@@ -1671,35 +1672,35 @@ export class CharacterSheet
|
|||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("flex flex-row justify-center gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
|
div("hidden lg:flex flex-row flex-wrap justify-center gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
|
||||||
div("flex gap-2 flex-row items-center justify-between", [
|
div("flex gap-2 flex-row items-center justify-between", [
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'strength' }], () => `+${this.character.compiled.modifier.strength}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'strength' }], () => `+${this.character.compiled.modifier.strength}`),
|
||||||
span("text-sm ", "Force")
|
span("text-sm", mainStatShortTexts.strength)
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'dexterity' }], () => `+${this.character.compiled.modifier.dexterity}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'dexterity' }], () => `+${this.character.compiled.modifier.dexterity}`),
|
||||||
span("text-sm ", "Dextérité")
|
span("text-sm", mainStatShortTexts.dexterity)
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'constitution' }], () => `+${this.character.compiled.modifier.constitution}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'constitution' }], () => `+${this.character.compiled.modifier.constitution}`),
|
||||||
span("text-sm ", "Constitution")
|
span("text-sm", mainStatShortTexts.constitution)
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'intelligence' }], () => `+${this.character.compiled.modifier.intelligence}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'intelligence' }], () => `+${this.character.compiled.modifier.intelligence}`),
|
||||||
span("text-sm ", "Intelligence")
|
span("text-sm", mainStatShortTexts.intelligence)
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'curiosity' }], () => `+${this.character.compiled.modifier.curiosity}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'curiosity' }], () => `+${this.character.compiled.modifier.curiosity}`),
|
||||||
span("text-sm ", "Curiosité")
|
span("text-sm", mainStatShortTexts.curiosity)
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'charisma' }], () => `+${this.character.compiled.modifier.charisma}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'charisma' }], () => `+${this.character.compiled.modifier.charisma}`),
|
||||||
span("text-sm ", "Charisme")
|
span("text-sm", mainStatShortTexts.charisma)
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
div("flex flex-col items-center px-2", [
|
||||||
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'psyche' }], () => `+${this.character.compiled.modifier.psyche}`),
|
() => !this.character.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === 'psyche' }], () => `+${this.character.compiled.modifier.psyche}`),
|
||||||
span("text-sm ", "Psyché")
|
span("text-sm ", mainStatShortTexts.psyche)
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@@ -1708,7 +1709,7 @@ export class CharacterSheet
|
|||||||
div("flex gap-2 flex-row items-center justify-between", [
|
div("flex gap-2 flex-row items-center justify-between", [
|
||||||
div("flex flex-col px-2 items-center", [
|
div("flex flex-col px-2 items-center", [
|
||||||
span("text-xl font-bold", () => !this.character.compiled ? '-' : `+${this.character.compiled.initiative}`),
|
span("text-xl font-bold", () => !this.character.compiled ? '-' : `+${this.character.compiled.initiative}`),
|
||||||
span("text-sm ", "Initiative")
|
span("text-sm ", "Init.")
|
||||||
]),
|
]),
|
||||||
div("flex flex-col px-2 items-center", [
|
div("flex flex-col px-2 items-center", [
|
||||||
span("text-xl font-bold", () => !this.character.compiled ? '-' : this.character.compiled.speed === false ? "N/A" : `${this.character.compiled.speed}`),
|
span("text-xl font-bold", () => !this.character.compiled ? '-' : this.character.compiled.speed === false ? "N/A" : `${this.character.compiled.speed}`),
|
||||||
@@ -1719,24 +1720,24 @@ export class CharacterSheet
|
|||||||
div('border-l border-light-35 dark:border-dark-35'),
|
div('border-l border-light-35 dark:border-dark-35'),
|
||||||
|
|
||||||
div("flex gap-2 flex-row items-center justify-between", [
|
div("flex gap-2 flex-row items-center justify-between", [
|
||||||
icon("game-icons:checked-shield", { width: 32, height: 32 }),
|
icon("game-icons:checked-shield", { width: 24, height: 24 }),
|
||||||
div("flex flex-col px-2 items-center", [
|
div("flex flex-col px-2 items-center", [
|
||||||
span(" text-xl font-bold", () => !this.character.compiled ? '-' : clamp(this.character.compiled.defense.static + this.character.compiled.defense.passiveparry + this.character.compiled.defense.passivedodge, 0, this.character.compiled.defense.hardcap)),
|
span("text-xl font-bold", () => !this.character.compiled ? '-' : clamp(this.character.compiled.defense.static + this.character.compiled.defense.passiveparry + this.character.compiled.defense.passivedodge, 0, this.character.compiled.defense.hardcap)),
|
||||||
span("text-sm ", "Passive")
|
span("text-sm ", "Passive")
|
||||||
]),
|
]),
|
||||||
div("flex flex-col px-2 items-center", [
|
div("flex flex-col px-2 items-center", [
|
||||||
span(" text-xl font-bold", () => !this.character.compiled ? '-' : clamp(this.character.compiled.defense.static + this.character.compiled.defense.activeparry + this.character.compiled.defense.passivedodge, 0, this.character.compiled.defense.hardcap)),
|
span("text-xl font-bold", () => !this.character.compiled ? '-' : clamp(this.character.compiled.defense.static + this.character.compiled.defense.activeparry + this.character.compiled.defense.passivedodge, 0, this.character.compiled.defense.hardcap)),
|
||||||
span("text-sm ", "Blocage")
|
span("text-sm ", "Blocage")
|
||||||
]),
|
]),
|
||||||
div("flex flex-col px-2 items-center", [
|
div("flex flex-col px-2 items-center", [
|
||||||
span(" text-xl font-bold", () => !this.character.compiled ? '-' : clamp(this.character.compiled.defense.static + this.character.compiled.defense.passiveparry + this.character.compiled.defense.activedodge, 0, this.character.compiled.defense.hardcap)),
|
span("text-xl font-bold", () => !this.character.compiled ? '-' : clamp(this.character.compiled.defense.static + this.character.compiled.defense.passiveparry + this.character.compiled.defense.activedodge, 0, this.character.compiled.defense.hardcap)),
|
||||||
span("text-sm ", "Esquive")
|
span("text-sm ", "Esquive")
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4 h-0", [
|
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4 h-0", [
|
||||||
div("flex flex-col gap-4 py-1 w-60", [
|
div("hidden lg:flex flex-col gap-4 py-1 w-60", [
|
||||||
div("flex flex-col py-1 gap-4", [
|
div("flex flex-col py-1 gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-4", [
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
dom("div", { class: 'text-xl font-semibold', text: "Compétences" }),
|
dom("div", { class: 'text-xl font-semibold', text: "Compétences" }),
|
||||||
@@ -1766,7 +1767,7 @@ export class CharacterSheet
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div('border-l border-light-35 dark:border-dark-35'),
|
div('hidden lg:block border-l border-light-35 dark:border-dark-35'),
|
||||||
|
|
||||||
this.tabs,
|
this.tabs,
|
||||||
])
|
])
|
||||||
@@ -2006,14 +2007,18 @@ export class CharacterSheet
|
|||||||
const spell = config.spells[e] as SpellConfig | undefined;
|
const spell = config.spells[e] as SpellConfig | undefined;
|
||||||
if(!spell) return;
|
if(!spell) return;
|
||||||
|
|
||||||
return div('flex flex-col gap-2', [
|
return foldable(() => [
|
||||||
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${spell.cost ?? 0} mana` }) ]),
|
|
||||||
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
|
|
||||||
div('flex flex-row gap-2', [ span('flex flex-row', `Sort ${spell.type === 'instinct' ? 'd\'instinct' : spell.type === 'knowledge' ? 'de savoir' : 'de précision'} ${spell.rank === 4 ? 'unique' :`de rang ${spell.rank}`}`), ...(spell.elements ?? []).map(elementDom) ]),
|
|
||||||
div('flex flex-row gap-4 items-center', [ spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof spell.range === 'number' && spell.range > 0 ? `${spell.range} case${spell.range > 1 ? 's' : ''}` : spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof spell.speed === 'number' ? `${spell.speed} minute${spell.speed > 1 ? 's' : ''}` : spell.speed) ])
|
|
||||||
]),
|
|
||||||
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(spell.description) ]),
|
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(spell.description) ]),
|
||||||
])
|
], [
|
||||||
|
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${spell.cost ?? 0} mana` }) ]),
|
||||||
|
div('flex flex-row flex-wrap items-center gap-x-3 gap-y-1 text-light-70 dark:text-dark-70', [
|
||||||
|
span('flex flex-row', `Sort ${spell.type === 'instinct' ? 'd\'instinct' : spell.type === 'knowledge' ? 'de savoir' : 'de précision'} ${spell.rank === 4 ? 'unique' :`de rang ${spell.rank}`}`),
|
||||||
|
div('flex flex-row flex-wrap gap-1', (spell.elements ?? []).map(elementDom)),
|
||||||
|
spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined,
|
||||||
|
span(undefined, typeof spell.range === 'number' && spell.range > 0 ? `${spell.range} case${spell.range > 1 ? 's' : ''}` : spell.range === 0 ? 'toucher' : 'personnel'),
|
||||||
|
span(undefined, typeof spell.speed === 'number' ? `${spell.speed} minute${spell.speed > 1 ? 's' : ''}` : spell.speed)
|
||||||
|
]),
|
||||||
|
], { open: false, class: { container: 'flex flex-col gap-1', icon: 'px-2' } })
|
||||||
}, list: () => sort([...(this.character.compiled.lists.spells ?? []), ...this.character.compiled.variables.spells]) }),
|
}, list: () => sort([...(this.character.compiled.lists.spells ?? []), ...this.character.compiled.variables.spells]) }),
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
@@ -2123,7 +2128,17 @@ export class CharacterSheet
|
|||||||
button(text('Modifier'), () => panel.show(), 'py-1 px-4'),
|
button(text('Modifier'), () => panel.show(), 'py-1 px-4'),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.character.compiled.variables.items, render: (e, _c) => {
|
div('flex flex-col flex-1', [
|
||||||
|
div('flex flex-row items-center text-xs text-light-70 dark:text-dark-70 border-b border-light-35 dark:border-dark-35 pb-1 px-1 gap-2', [
|
||||||
|
div('w-6 shrink-0 pl-5'),
|
||||||
|
div('flex-1 min-w-0', [text('Nom')]),
|
||||||
|
div('w-32 shrink-0', [text('Stats')]),
|
||||||
|
div('w-10 shrink-0 text-center', [text('Qté')]),
|
||||||
|
div('w-16 shrink-0 text-center hidden lg:block', [text('Puis.')]),
|
||||||
|
div('w-12 shrink-0 text-center hidden lg:block', [text('Poids')]),
|
||||||
|
div('w-12 shrink-0 text-center hidden lg:block', [text('Charg.')]),
|
||||||
|
]),
|
||||||
|
div('flex flex-col', { list: this.character.compiled.variables.items, render: (e, _c) => {
|
||||||
if(_c) return _c;
|
if(_c) return _c;
|
||||||
|
|
||||||
const item = config.items[e.id];
|
const item = config.items[e.id];
|
||||||
@@ -2132,11 +2147,16 @@ export class CharacterSheet
|
|||||||
|
|
||||||
const itempower = () => (item.powercost ?? 0) + (e.improvements?.reduce((_p, _v) => (config.improvements[_v]?.power ?? 0) + _p, 0) ?? 0);
|
const itempower = () => (item.powercost ?? 0) + (e.improvements?.reduce((_p, _v) => (config.improvements[_v]?.power ?? 0) + _p, 0) ?? 0);
|
||||||
|
|
||||||
const price = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.price }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.price }), () => item.price ? `${item.price * e.amount}` : '-') ]);
|
|
||||||
const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]);
|
|
||||||
return foldable(() => [
|
return foldable(() => [
|
||||||
markdown(getText(item.description)),
|
markdown(getText(item.description)),
|
||||||
div('flex flex-row gap-1', { list: () => e.improvements!.map(e => config.improvements[e]).filter(e => !!e), render: (e, _c) => _c ?? floater(div(() => ['flex flex-row gap-2 border px-2 rounded-full py-px !bg-opacity-20', { 'border-accent-blue bg-accent-blue': !e.cursed, 'border-light-purple bg-light-purple dark:border-dark-purple dark:bg-dark-purple': e.cursed }], [ span('text-sm font-semibold tracking-tight', e.name), div('flex flex-row gap-1 items-center', [icon('game-icons:bolt-drop', { width: 12, height: 12 }), span('text-sm font-light', e.power)]) ]), () => [markdown(getText(e.description), undefined, { tags: { a: preview } })], { class: 'max-w-96 max-h-48 p-2', position: "bottom-start" }) }),
|
div('flex flex-row gap-1', { list: () => e.improvements!.map(e => config.improvements[e]).filter(e => !!e), render: (e, _c) => _c ?? floater(div(() => ['flex flex-row gap-2 border px-2 rounded-full py-px !bg-opacity-20', { 'border-accent-blue bg-accent-blue': !e.cursed, 'border-light-purple bg-light-purple dark:border-dark-purple dark:bg-dark-purple': e.cursed }], [ span('text-sm font-semibold tracking-tight', e.name), div('flex flex-row gap-1 items-center', [icon('game-icons:bolt-drop', { width: 12, height: 12 }), span('text-sm font-light', e.power)]) ]), () => [markdown(getText(e.description), undefined, { tags: { a: preview } })], { class: 'max-w-96 max-h-48 p-2', position: "bottom-start" }) }),
|
||||||
|
div('flex flex-row flex-wrap gap-x-3 gap-y-1 text-xs text-light-70 dark:text-dark-70 lg:hidden', [
|
||||||
|
item.category === 'armor' ? span('', () => `Armure: ${item.health + ((e.state as ArmorState)?.health ?? 0) - ((e.state as ArmorState)?.loss ?? 0)}/${item.health + ((e.state as ArmorState)?.health ?? 0)} (${[item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0) > 0 ? '-' + (item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0)) : undefined, item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0) > 0 ? '-' + (item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0)) + '%' : undefined].filter(e => !!e).join('/')})`) : undefined,
|
||||||
|
item.category === 'weapon' ? span('', () => `${stringifyRoll(parseDice(`${item.damage.value}${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), this.character.compiled.modifier, true)} ${damageTypeTexts[item.damage.type].toLowerCase()}`) : undefined,
|
||||||
|
item.capacity ? span('', () => `Puissance: ${itempower()}/${item.capacity}`) : undefined,
|
||||||
|
item.weight ? span('', () => `${e.amount > 1 ? `Poids: ${item.weight} (×${e.amount} = ${item.weight ?? 0 * e.amount})` : `Poids: ${item.weight}`}`) : undefined,
|
||||||
|
item.charge ? span('', `Charges: ${item.charge}`) : undefined,
|
||||||
|
]),
|
||||||
div('flex flex-row justify-center gap-1', [
|
div('flex flex-row justify-center gap-1', [
|
||||||
this.character.character.campaign ? button(text('Partager'), () => {
|
this.character.character.campaign ? button(text('Partager'), () => {
|
||||||
|
|
||||||
@@ -2164,30 +2184,37 @@ export class CharacterSheet
|
|||||||
improve.show(e);
|
improve.show(e);
|
||||||
}, 'px-2 text-sm h-5 box-content'),
|
}, 'px-2 text-sm h-5 box-content'),
|
||||||
])
|
])
|
||||||
], [ div('flex flex-row justify-between', [
|
], [
|
||||||
div('flex flex-row items-center gap-y-1 gap-x-4 flex-wrap', [
|
div('flex flex-row items-center gap-2 px-1', [
|
||||||
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
|
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
|
||||||
if(v && config.items[e.id]?.category === 'armor' && this.character.compiled.variables.items.find(e => config.items[e.id]?.category === 'armor' && e.equipped))
|
if(v && config.items[e.id]?.category === 'armor' && this.character.compiled.variables.items.find(e => config.items[e.id]?.category === 'armor' && e.equipped))
|
||||||
return Toaster.add({ content: "Vous ne pouvez equipper qu'une seule armure à la fois.", duration: 5000, timer: true, type: 'info' }), false;
|
return Toaster.add({ content: "Vous ne pouvez equipper qu'une seule armure à la fois.", duration: 5000, timer: true, type: 'info' }), false;
|
||||||
|
|
||||||
e.equipped = v;
|
e.equipped = v;
|
||||||
this.character.improve(e);
|
this.character.improve(e);
|
||||||
}, class: { container: '!w-5 !h-5' } }) : undefined,
|
}, class: { container: '!w-5 !h-5' } }) : div('w-5 h-5'),
|
||||||
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
|
div('flex-1 min-w-0 flex flex-row items-center justify-between gap-1 flex-wrap', [
|
||||||
item.category === 'armor' ? div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:shoulder-armor', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${item.health + ((e.state as ArmorState)?.health ?? 0) - ((e.state as ArmorState)?.loss ?? 0)}/${item.health + ((e.state as ArmorState)?.health ?? 0)} (${[item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0) > 0 ? '-' + (item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0)) : undefined, item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0) > 0 ? '-' + (item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0)) + '%' : undefined].filter(e => !!e).join('/')})`) ]) :
|
span([colorByRarity[item.rarity], 'text-md lg:text-lg'], item.name),
|
||||||
item.category === 'weapon' ? div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:broadsword', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => stringifyRoll(parseDice(`${item.damage.value}${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), this.character.compiled.modifier, true)), proses('a', preview, [ text(damageTypeTexts[item.damage.type].toLowerCase()) ], { href: `regles/le-combat/les-types-de-degats#${damageTypeTexts[item.damage.type]}`, label: damageTypeTexts[item.damage.type], navigate: false }) ]) :
|
span('text-xs text-light-60 dark:text-dark-60 italic', subnameFactory(item).join(' ')),
|
||||||
|
]),
|
||||||
|
div('w-32 shrink-0 flex items-center text-xs', [
|
||||||
|
item.category === 'armor' ? span('italic', () => `${item.health + ((e.state as ArmorState)?.health ?? 0) - ((e.state as ArmorState)?.loss ?? 0)}/${item.health + ((e.state as ArmorState)?.health ?? 0)} (${[item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0) > 0 ? '-' + (item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0)) : undefined, item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0) > 0 ? '-' + (item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0)) + '%' : undefined].filter(e => !!e).join('/')})`) :
|
||||||
|
item.category === 'weapon' ? div('flex flex-row gap-1 items-center', [ span('italic', () => stringifyRoll(parseDice(`${item.damage.value}${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), this.character.compiled.modifier, true)), proses('a', preview, [ text(damageTypeTexts[item.damage.type].toLowerCase()) ], { href: `regles/le-combat/les-types-de-degats#${damageTypeTexts[item.damage.type]}`, label: damageTypeTexts[item.damage.type], navigate: false }) ]) :
|
||||||
undefined
|
undefined
|
||||||
]),
|
]),
|
||||||
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
|
div('w-10 shrink-0 text-center text-sm', [text(() => e.amount.toString())]),
|
||||||
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
|
div('w-16 shrink-0 text-center text-xs hidden lg:block', [
|
||||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => e.amount ?? '-') ]),
|
span(() => ({ 'text-light-red dark:text-dark-red': !!item.capacity && itempower() > item.capacity }), () => item.capacity ? `${itempower()}/${item.capacity}` : '-')
|
||||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'text-red': !!item.capacity && itempower() > item.capacity }), () => item.capacity ? `${itempower()}/${item.capacity ?? 0}` : '-') ]),
|
|
||||||
e.amount > 1 && !!item.weight ? tooltip(weight, `Poids unitaire: ${item.weight}`, 'bottom') : weight,
|
|
||||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.charge ? `${item.charge}` : '-') ]),
|
|
||||||
]),
|
]),
|
||||||
])], { open: false, class: { icon: 'px-2', container: 'p-1 gap-2', content: 'px-4 pb-1 flex flex-col gap-1' } })
|
div('w-12 shrink-0 text-center text-xs hidden lg:block', [
|
||||||
|
e.amount > 1 && !!item.weight ? tooltip(span(() => ({ 'cursor-help underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-'), `Poids unitaire: ${item.weight}`, 'bottom') : span('', () => item.weight ? `${item.weight * e.amount}` : '-')
|
||||||
|
]),
|
||||||
|
div('w-12 shrink-0 text-center text-xs hidden lg:block', [span('', () => item.charge ? `${item.charge}` : '-')]),
|
||||||
|
])
|
||||||
|
], { open: false, class: { container: 'border-b border-dashed border-light-35 dark:border-dark-35 py-1', icon: 'px-1', content: 'px-4 pb-2 flex flex-col gap-1' } })
|
||||||
}})
|
}})
|
||||||
])
|
])
|
||||||
|
])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
itemsPanel()
|
itemsPanel()
|
||||||
@@ -2346,4 +2373,74 @@ export class CharacterSheet
|
|||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
sidebarTab()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
div('flex flex-1 flex-col gap-2 p-2', [
|
||||||
|
div('flex flex-row justify-between gap-1 md:gap-2',
|
||||||
|
MAIN_STATS.map(stat =>
|
||||||
|
div('flex flex-col items-center', [
|
||||||
|
span(() => ['text-xl font-bold', { 'text-accent-blue': this.character.compiled.variables.transformed && config.aspects[this.character.compiled.aspect.id]?.stat === stat }], () => !this.character.compiled ? '-' : `+${this.character.compiled.modifier[stat]}`),
|
||||||
|
span('text-sm', mainStatShortTexts[stat]),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
div('border-t border-dashed border-light-35 dark:border-dark-35'),
|
||||||
|
div('flex flex-row justify-between gap-2', [
|
||||||
|
div('flex flex-col items-center', [
|
||||||
|
span('text-lg font-bold', () => !this.character.compiled ? '-' : `+${this.character.compiled.initiative}`),
|
||||||
|
span('text-xs text-light-70 dark:text-dark-70', 'Init.'),
|
||||||
|
]),
|
||||||
|
div('flex flex-col items-center', [
|
||||||
|
span('text-lg font-bold', () => !this.character.compiled ? '-' : this.character.compiled.speed === false ? 'N/A' : `${this.character.compiled.speed}`),
|
||||||
|
span('text-xs text-light-70 dark:text-dark-70', 'Course'),
|
||||||
|
]),
|
||||||
|
...(['Passive', 'Blocage', 'Esquive'] as const).map((label, i) => {
|
||||||
|
const defs = [
|
||||||
|
() => this.character.compiled.defense.static + this.character.compiled.defense.passiveparry + this.character.compiled.defense.passivedodge,
|
||||||
|
() => this.character.compiled.defense.static + this.character.compiled.defense.activeparry + this.character.compiled.defense.passivedodge,
|
||||||
|
() => this.character.compiled.defense.static + this.character.compiled.defense.passiveparry + this.character.compiled.defense.activedodge,
|
||||||
|
];
|
||||||
|
return div('flex flex-col items-center', [
|
||||||
|
span('text-lg font-bold', () => !this.character.compiled ? '-' : clamp(defs[i](), 0, this.character.compiled.defense.hardcap)),
|
||||||
|
span('text-xs text-light-70 dark:text-dark-70', label),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
div("flex flex-col md:flex-row gap-4 py-1", [
|
||||||
|
div("flex flex-col flex-1 gap-4", [
|
||||||
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
|
dom("div", { class: 'text-xl font-semibold', text: "Compétences" }),
|
||||||
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
||||||
|
]),
|
||||||
|
|
||||||
|
div("grid grid-cols-2 gap-2",
|
||||||
|
ABILITIES.map((ability) =>
|
||||||
|
div("flex flex-row px-1 justify-between items-center", [
|
||||||
|
proses('a', preview, [ span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline", abilityTexts[ability as Ability] || ability) ], { href: `regles/l'entrainement/competences#${abilityTexts[ability as Ability]}`, label: abilityTexts[ability as Ability], navigate: false }),
|
||||||
|
span("font-bold text-base text-light-100 dark:text-dark-100", () => !this.character.compiled ? '-' : `+${this.character.compiled.abilities[ability as Ability] ?? 0}`),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
div('hidden md:block border-l border-light-35 dark:border-dark-35'),
|
||||||
|
|
||||||
|
div('flex flex-col flex-1 gap-4', [
|
||||||
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
|
dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }),
|
||||||
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
||||||
|
]),
|
||||||
|
|
||||||
|
div("grid grid-cols-2 gap-x-3 gap-y-3 text-sm", { list: () => this.character.compiled?.mastery ?? [], render: (e, _c) => proses('a', preview, [ text(masteryTexts[e].text) ], { href: masteryTexts[e].href, label: masteryTexts[e].text, class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline', }) }),
|
||||||
|
div("grid grid-cols-2 gap-x-3 gap-y-2 text-sm", [
|
||||||
|
() => (this.character.compiled?.spellranks?.precision ?? 0) > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Précision') ], { href: 'regles/la-magie/magie#Les sorts de précision', label: 'Précision', class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline' }), span('font-bold', () => this.character.compiled?.spellranks?.precision ?? 0) ]) : undefined,
|
||||||
|
() => (this.character.compiled?.spellranks?.knowledge ?? 0) > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Savoir') ], { href: 'regles/la-magie/magie#Les sorts de savoir', label: 'Savoir', class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline' }), span('font-bold', () => this.character.compiled?.spellranks?.knowledge ?? 0) ]) : undefined,
|
||||||
|
() => (this.character.compiled?.spellranks?.instinct ?? 0) > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Instinct') ], { href: 'regles/la-magie/magie#Les sorts instinctif', label: 'Instinct', class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline' }), span('font-bold', () => this.character.compiled?.spellranks?.instinct ?? 0) ]) : undefined,
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -743,29 +743,54 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HT
|
|||||||
}, [ 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] }), ]);
|
}, [ 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;
|
return element;
|
||||||
}
|
}
|
||||||
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: Reactive<NodeChildren> }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): HTMLElement
|
export function tabgroup(tabs: Array<Reactive<{ id: string, title: NodeChildren, content: Reactive<NodeChildren> } | undefined>>, 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;
|
let focus = settings?.focused ?? '';
|
||||||
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() {
|
|
||||||
if(this.hasAttribute('data-focus'))
|
const resolveTab = (t: typeof tabs[number]) => typeof t === 'function' ? t() : t;
|
||||||
|
|
||||||
|
const tabbar = div(['flex flex-row items-center gap-1', settings?.class?.tabbar]);
|
||||||
|
const content = div(['', settings?.class?.content]);
|
||||||
|
const container = div(['flex flex-col', settings?.class?.container], [tabbar, content]);
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
const resolved = tabs.map(resolveTab).filter((t): t is NonNullable<typeof t> => !!t);
|
||||||
|
|
||||||
|
if (!resolved.find(t => t.id === focus))
|
||||||
|
focus = resolved[0]?.id ?? '';
|
||||||
|
|
||||||
|
const titles = resolved.map(t => 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: focus === t.id ? { 'data-focus': '' } : {},
|
||||||
|
listeners: { click: function() {
|
||||||
|
if (this.hasAttribute('data-focus'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(settings?.switch && settings.switch(e.id) === false)
|
if (settings?.switch && settings.switch(t.id) === false)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
titles.forEach(e => e.toggleAttribute('data-focus', false));
|
tabbar.querySelectorAll('[data-focus]').forEach(e => e.removeAttribute('data-focus'));
|
||||||
this.toggleAttribute('data-focus', true);
|
this.setAttribute('data-focus', '');
|
||||||
focus = e.id;
|
focus = t.id;
|
||||||
const lazyContent = typeof e.content === 'function' ? e.content() : e.content;
|
const lazyContent = typeof t.content === 'function' ? t.content() : t.content;
|
||||||
lazyContent && content.replaceChildren(...lazyContent?.map(e => typeof e === 'function' ? e() : e)?.filter(e => !!e));
|
lazyContent && content.replaceChildren(...lazyContent.map(e => typeof e === 'function' ? e() : e).filter(e => !!e));
|
||||||
}}}, e.title));
|
} }
|
||||||
const _content = tabs.find(e => e.id === focus)?.content;
|
}, t.title));
|
||||||
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
|
|
||||||
|
tabbar.replaceChildren(...titles);
|
||||||
|
|
||||||
|
const active = resolved.find(t => t.id === focus);
|
||||||
|
if (active) {
|
||||||
|
const lazyContent = typeof active.content === 'function' ? active.content() : active.content;
|
||||||
|
lazyContent && content.replaceChildren(...lazyContent.map(e => typeof e === 'function' ? e() : e).filter(e => !!e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reactivity(() => {
|
||||||
|
const resolved = tabs.map(resolveTab);
|
||||||
|
return resolved.map(t => t?.id).join(',');
|
||||||
|
}, render);
|
||||||
|
|
||||||
const container = div(['flex flex-col', settings?.class?.container], [
|
|
||||||
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
|
|
||||||
content
|
|
||||||
]);
|
|
||||||
return container as HTMLElement;
|
return container as HTMLElement;
|
||||||
}
|
}
|
||||||
export function floater(container: HTMLElement, 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 })
|
export function floater(container: HTMLElement, 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 })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
|
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
|
||||||
import { loading } from './components';
|
import { loading } from './components';
|
||||||
import { _defer, raw, reactivity, type Proxy, type Reactive } from './reactive';
|
import { _defer, raw, reactivity, type Reactive } from './reactive';
|
||||||
|
|
||||||
export type Node = HTMLElement | SVGElement | Text | undefined;
|
export type Node = HTMLElement | SVGElement | Text | undefined;
|
||||||
export type NodeChildren = Array<Reactive<Node>> | undefined;
|
export type NodeChildren = Array<Reactive<Node>> | undefined;
|
||||||
@@ -46,6 +46,7 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
|
|||||||
{
|
{
|
||||||
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||||
const _cache = new Map<U, Node | Node[] | undefined>();
|
const _cache = new Map<U, Node | Node[] | undefined>();
|
||||||
|
const seen = new Set<U>(); // Cache pruning utility
|
||||||
|
|
||||||
if(children)
|
if(children)
|
||||||
{
|
{
|
||||||
@@ -83,10 +84,14 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
list.forEach(e => {
|
list.forEach(e => {
|
||||||
|
seen.add(e);
|
||||||
const child = raw(children.render(e, _cache.get(e)));
|
const child = raw(children.render(e, _cache.get(e)));
|
||||||
_cache.set(e, child);
|
_cache.set(e, child);
|
||||||
append(element, child);
|
append(element, child);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const key of _cache.keys()) if (!seen.has(key)) _cache.delete(key);
|
||||||
|
seen.clear();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -226,7 +231,7 @@ const iconLoadingRegistry: Map<string, Promise<Required<IconifyIcon>> | null | u
|
|||||||
export function icon(name: Reactive<string>, properties?: IconProperties)
|
export function icon(name: Reactive<string>, properties?: IconProperties)
|
||||||
{
|
{
|
||||||
const element = dom('div', { class: properties?.class, style: properties?.style });
|
const element = dom('div', { class: properties?.class, style: properties?.style });
|
||||||
let timeout: NodeJS.Timeout = setTimeout(() => {}, 0);
|
let timeout: NodeJS.Timeout = setTimeout(() => {}, 0), target: string;
|
||||||
|
|
||||||
const build = (icon: IconifyIcon | null | undefined) => {
|
const build = (icon: IconifyIcon | null | undefined) => {
|
||||||
if(!icon) return clearTimeout(timeout) ?? element.replaceChildren();
|
if(!icon) return clearTimeout(timeout) ?? element.replaceChildren();
|
||||||
@@ -237,11 +242,12 @@ export function icon(name: Reactive<string>, properties?: IconProperties)
|
|||||||
element.replaceChildren(dom);
|
element.replaceChildren(dom);
|
||||||
}
|
}
|
||||||
reactivity(name, (name) => {
|
reactivity(name, (name) => {
|
||||||
|
target = name;
|
||||||
if(!iconLoaded(name))
|
if(!iconLoaded(name))
|
||||||
{
|
{
|
||||||
timeout = setTimeout(() => { element.replaceChildren(loading('small')); }, 100);
|
timeout = setTimeout(() => { element.replaceChildren(loading('small')); }, 100);
|
||||||
if(!iconLoadingRegistry.has(name)) iconLoadingRegistry.set(name, loadIcon(name));
|
if(!iconLoadingRegistry.has(name)) iconLoadingRegistry.set(name, loadIcon(name));
|
||||||
iconLoadingRegistry.get(name)?.then(build);
|
iconLoadingRegistry.get(name)?.then((icon) => target === name && build(icon));
|
||||||
}
|
}
|
||||||
else build(getIcon(name));
|
else build(getIcon(name));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const _defer = (fn: () => void) => {
|
|||||||
|
|
||||||
_deferSet.add(fn);
|
_deferSet.add(fn);
|
||||||
}
|
}
|
||||||
let activeEffect: (() => void) | null = null, _isTracking = true;
|
let effectStack: Array<(() => void)> = [], _isTracking = true;
|
||||||
const SYMBOLS = {
|
const SYMBOLS = {
|
||||||
PROXY: Symbol('is a proxy'),
|
PROXY: Symbol('is a proxy'),
|
||||||
ITERATE: Symbol('iterating'),
|
ITERATE: Symbol('iterating'),
|
||||||
@@ -26,7 +26,7 @@ const SYMBOLS = {
|
|||||||
|
|
||||||
function reactiveReadArray<T>(array: T[]): T[]
|
function reactiveReadArray<T>(array: T[]): T[]
|
||||||
{
|
{
|
||||||
const _raw = raw(array)
|
const _raw = raw(array);
|
||||||
if (_raw === array) return _raw;
|
if (_raw === array) return _raw;
|
||||||
track(_raw, SYMBOLS.ITERATE);
|
track(_raw, SYMBOLS.ITERATE);
|
||||||
return _raw.map(wrapReactive);
|
return _raw.map(wrapReactive);
|
||||||
@@ -42,7 +42,7 @@ function iterator(self: unknown[], method: keyof Array<unknown>, wrapValue: (val
|
|||||||
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
|
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
|
||||||
_next: IterableIterator<unknown>['next']
|
_next: IterableIterator<unknown>['next']
|
||||||
};
|
};
|
||||||
if (arr !== self && !isShallow(self))
|
if (arr !== self)
|
||||||
{
|
{
|
||||||
iter._next = iter.next;
|
iter._next = iter.next;
|
||||||
iter.next = () => {
|
iter.next = () => {
|
||||||
@@ -82,7 +82,7 @@ function apply(self: unknown[], method: keyof Array<any>, fn: (item: unknown, in
|
|||||||
else if (fn.length > 2)
|
else if (fn.length > 2)
|
||||||
{
|
{
|
||||||
wrappedFn = function (this: unknown, item, index) {
|
wrappedFn = function (this: unknown, item, index) {
|
||||||
return fn.call(this, item, index, self);
|
return fn.call(this, wrapReactive(item), index, self);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,14 +119,19 @@ function searchProxy(self: unknown[], method: keyof Array<any>, args: unknown[])
|
|||||||
}
|
}
|
||||||
function noTracking(self: unknown[], method: keyof Array<any>, args: unknown[] = [])
|
function noTracking(self: unknown[], method: keyof Array<any>, args: unknown[] = [])
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
_isTracking = false;
|
_isTracking = false;
|
||||||
const res = (raw(self) as any)[method].apply(self, args);
|
return (raw(self) as any)[method].apply(self, args);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
_isTracking = true;
|
_isTracking = true;
|
||||||
return res;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ without getting an error
|
const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ without getting an error
|
||||||
__proto__: null, // <-- Required to remove the object prototype removing the object default functions from the substitution
|
__proto__: null, // <-- Required to remove the object prototype, removing the object default functions from the substitution as a result
|
||||||
[Symbol.iterator]() { return iterator(this, Symbol.iterator, item => wrapReactive(item)) },
|
[Symbol.iterator]() { return iterator(this, Symbol.iterator, item => wrapReactive(item)) },
|
||||||
concat(...args: unknown[]) { return reactiveReadArray(this).concat(...args.map(x => (Array.isArray(x) ? reactiveReadArray(x) : x))) },
|
concat(...args: unknown[]) { return reactiveReadArray(this).concat(...args.map(x => (Array.isArray(x) ? reactiveReadArray(x) : x))) },
|
||||||
entries() { return iterator(this, 'entries', (value: [number, unknown]) => { value[1] = wrapReactive(value[1]); return value; }) },
|
entries() { return iterator(this, 'entries', (value: [number, unknown]) => { value[1] = wrapReactive(value[1]); return value; }) },
|
||||||
@@ -192,7 +197,7 @@ function trigger(target: object, key?: string | symbol | null, value?: unknown)
|
|||||||
}
|
}
|
||||||
function track(target: object, key: string | symbol | null)
|
function track(target: object, key: string | symbol | null)
|
||||||
{
|
{
|
||||||
if(!activeEffect || !_isTracking) return;
|
if(effectStack.length === 0 || !_isTracking) return;
|
||||||
|
|
||||||
let dependencies = _tracker.get(target);
|
let dependencies = _tracker.get(target);
|
||||||
if(!dependencies)
|
if(!dependencies)
|
||||||
@@ -208,9 +213,7 @@ function track(target: object, key: string | symbol | null)
|
|||||||
dependencies.set(key, set);
|
dependencies.set(key, set);
|
||||||
}
|
}
|
||||||
|
|
||||||
set.add(activeEffect);
|
set.add(effectStack.slice(-1)[0]!);
|
||||||
|
|
||||||
//if(set) console.log('Tracking %o with key "%s"', target, key, set.size);
|
|
||||||
}
|
}
|
||||||
export type Proxy<T> = T & {
|
export type Proxy<T> = T & {
|
||||||
[SYMBOLS.PROXY]?: boolean;
|
[SYMBOLS.PROXY]?: boolean;
|
||||||
@@ -294,11 +297,11 @@ export function reactivity<T>(reactiveProperty: Reactive<T>, effect: (processed:
|
|||||||
// Also useful to retrigger the tracking system if the reactive property provides new properties (via conditions for example)
|
// Also useful to retrigger the tracking system if the reactive property provides new properties (via conditions for example)
|
||||||
const secureEffect = () => effect(typeof reactiveProperty === 'function' ? (reactiveProperty as () => T)() : reactiveProperty);
|
const secureEffect = () => effect(typeof reactiveProperty === 'function' ? (reactiveProperty as () => T)() : reactiveProperty);
|
||||||
const secureContext = () => {
|
const secureContext = () => {
|
||||||
activeEffect = secureContext;
|
effectStack.push(secureContext);
|
||||||
try {
|
try {
|
||||||
return secureEffect();
|
return secureEffect();
|
||||||
} finally {
|
} finally {
|
||||||
activeEffect = null;
|
effectStack.pop();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user