Campaign character insertion and deletion. Updating the inventory rendering. Update of the character_config IDs.

This commit is contained in:
Clément Pons 2025-11-24 17:28:31 +01:00
parent 41ae5da98c
commit b1229f81f6
13 changed files with 226 additions and 55 deletions

BIN
db.sqlite

Binary file not shown.

View File

@ -159,7 +159,9 @@ export default defineNuxtConfig({
"base-uri": "localhost:*"
}
},
xssValidator: false,
xssValidator: {
escapeHtml: false,
},
},
sitemap: {
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password', '/character/manage', '/campaign/create'],

View File

@ -0,0 +1,43 @@
import { and, eq, or } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { campaignCharactersTable, campaignMembersTable, campaignTable, characterTable } from '~/db/schema';
export default defineEventHandler(async (e) => {
const _id = getRouterParam(e, "id");
if(!_id)
{
setResponseStatus(e, 400);
return;
}
const id = parseInt(_id, 10);
const _campaign_id = getRouterParam(e, "cid");
if(!_campaign_id)
{
setResponseStatus(e, 400);
return;
}
const campaign_id = parseInt(_campaign_id, 10);
const session = await getUserSession(e);
if(!session.user || session.user.state !== 1)
{
setResponseStatus(e, 401);
return;
}
const db = useDatabase();
const campaign = db.select({ id: campaignTable.id }).from(campaignMembersTable).innerJoin(campaignTable, eq(campaignMembersTable.id, campaignTable.id)).where(and(eq(campaignMembersTable.id, campaign_id), or(eq(campaignMembersTable.user, session.user.id), eq(campaignTable.owner, session.user.id)))).get();
if(!campaign || campaign.id !== campaign_id)
return setResponseStatus(e, 404);
const character = db.select({ id: campaignCharactersTable.character }).from(campaignCharactersTable).where(and(eq(campaignCharactersTable.id, campaign_id), eq(campaignCharactersTable.character, id))).get();
if(!character || character.id !== id)
return setResponseStatus(e, 403);
db.delete(campaignCharactersTable).where(and(eq(campaignCharactersTable.id, campaign_id), eq(campaignCharactersTable.character, id))).run();
setResponseStatus(e, 200);
return;
});

View File

@ -0,0 +1,45 @@
import { and, eq, notExists } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { campaignCharactersTable, campaignMembersTable, campaignTable, characterTable } from '~/db/schema';
import { CharacterVariablesValidation } from '#shared/character.util';
export default defineEventHandler(async (e) => {
const _id = getRouterParam(e, "id");
if(!_id)
{
setResponseStatus(e, 400);
return;
}
const id = parseInt(_id, 10);
const _campaign_id = getRouterParam(e, "cid");
if(!_campaign_id)
{
setResponseStatus(e, 400);
return;
}
const campaign_id = parseInt(_campaign_id, 10);
const session = await getUserSession(e);
if(!session.user || session.user.state !== 1)
{
setResponseStatus(e, 401);
return;
}
const db = useDatabase();
const character = db.select({ id: characterTable.id }).from(characterTable).where(and(eq(characterTable.id, id), eq(characterTable.owner, session.user.id))).get();
if(!character || character.id !== id)
return setResponseStatus(e, 403);
const campaign = db.select({ id: campaignMembersTable.id }).from(campaignMembersTable).where(and(eq(campaignMembersTable.id, campaign_id), eq(campaignMembersTable.user, session.user.id))).get();
if(!campaign || campaign.id !== campaign_id)
return setResponseStatus(e, 404);
db.insert(campaignCharactersTable).values({
id: campaign_id,
character: id,
}).onConflictDoNothing().run();
return setResponseStatus(e, 200);
});

View File

@ -3,6 +3,10 @@ import type { User } from "~/types/auth";
export default defineWebSocketHandler({
message(peer, message) {
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
if(!id) return peer.close();
const topic = `campaigns/${id}`;
const data = message.json<SocketMessage>();
switch(data.type)
{
@ -10,6 +14,10 @@ export default defineWebSocketHandler({
peer.send(JSON.stringify({ type: 'PONG' }));
return;
case 'character':
peer.publish(topic, data);
peer.send(data);
default: return;
}
},
@ -23,14 +31,14 @@ export default defineWebSocketHandler({
const topic = `campaigns/${id}`;
peer.subscribe(topic);
peer.publish(topic, { type: 'user', data: [{ user: (peer.context.user as User).id, status: true }] });
peer.send({ type: 'user', data: peer.peers.values().filter(e => e.topics.has(topic)).map(e => ({ user: (e.context.user as User).id, status: true })).toArray() })
peer.publish(topic, { type: 'status', data: [{ user: (peer.context.user as User).id, status: true }] });
peer.send({ type: 'status', data: peer.peers.values().filter(e => e.topics.has(topic)).map(e => ({ user: (e.context.user as User).id, status: true })).toArray() })
},
close(peer, details) {
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
if(!id) return peer.close();
peer.publish(`campaigns/${id}`, { type: 'user', data: [{ user: (peer.context.user as User).id, status: false }] });
peer.publish(`campaigns/${id}`, { type: 'status', data: [{ user: (peer.context.user as User).id, status: false }] });
peer.unsubscribe(`campaigns/${id}`);
}
});

View File

@ -2,9 +2,9 @@ import { z } from "zod/v4";
import type { User } from "~/types/auth";
import type { Campaign, CampaignLog } from "~/types/campaign";
import { div, dom, icon, span, svg, text } from "#shared/dom.util";
import { button, loading, tabgroup } from "#shared/components.util";
import { button, loading, tabgroup, Toaster } from "#shared/components.util";
import { CharacterCompiler } from "#shared/character.util";
import { tooltip } from "#shared/floating.util";
import { modal, tooltip } from "#shared/floating.util";
import markdown from "#shared/markdown.util";
import { preview } from "#shared/proses";
import { format } from "#shared/general.util";
@ -86,6 +86,9 @@ export class CampaignSheet
private dm!: PlayerState;
private players!: Array<PlayerState>;
private characters!: Array<CharacterPrinter>;
private characterList!: HTMLElement;
private tab: string = 'campaign';
ws?: Socket;
@ -102,7 +105,7 @@ export class CampaignSheet
this.players = campaign.members.map(e => defaultPlayerState(e.member));
this.characters = campaign.characters.map(e => new CharacterPrinter(e.character!.id, e.character!.name));
this.ws = new Socket(`/ws/campaign/${id}`, true);
this.ws.handleMessage<{ user: number, status: boolean }[]>('user', (users) => {
this.ws.handleMessage<{ user: number, status: boolean }[]>('status', (users) => {
users.forEach(user => {
if(this.dm.user.id === user.user)
{
@ -121,6 +124,30 @@ export class CampaignSheet
}
})
});
this.ws.handleMessage<{ id: number, name: string, action: 'ADD' | 'REMOVE' }>('character', (character) => {
if(character.action === 'ADD')
{
const printer = new CharacterPrinter(character.id, character.name);
this.characters.push(printer);
this.characterList.appendChild(printer.container);
}
else if(character.action === 'REMOVE')
{
const idx = this.characters.findIndex(e => e.compiler?.character.id !== character.id);
if(idx !== -1)
{
this.characters[idx]!.container.remove();
this.characters.splice(idx, 1);
}
}
});
this.ws.handleMessage<{ id: number, name: string, action: 'ADD' | 'REMOVE' }>('player', () => {
this.render();
});
this.ws.handleMessage<void>('hardsync', () => {
this.render();
});
document.title = `d[any] - Campagne ${campaign.name}`;
this.render();
@ -151,6 +178,7 @@ export class CampaignSheet
if(!campaign)
return;
this.characterList = div('flex flex-col gap-2', this.characters.map(e => e.container));
this.container.replaceChildren(div('grid grid-cols-3 gap-2', [
div('flex flex-row gap-2 items-center py-2', [
this.dm.dom,
@ -171,7 +199,31 @@ export class CampaignSheet
div('flex flex-row gap-4 flex-1 h-0', [
div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4 w-[320px]', [ span('font-bold text-lg', 'Etat'), div('border-t border-light-40 dark:border-dark-40 border-dashed flex-1') ]),
...this.characters.map(e => e.container),
this.characterList,
div('px-8 py-4 w-full flex', [
button([
icon('radix-icons:plus-circled', { width: 24, height: 24 }),
span('text-sm', 'Ajouter un personnage'),
], () => {
const load = loading('normal');
let characters: HTMLElement[] = [];
const close = modal([
div('flex flex-col gap-4 items-center min-w-[480px] min-h-24', [
span('text-xl font-bold', 'Mes personnages'),
load,
]),
], { closeWhenOutside: true, priority: true, class: { container: 'max-w-[560px]' } }).close;
useRequestFetch()(`/api/character`).then((list) => {
characters = list?.map(e => div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [
span('font-bold', e.name),
span('', `Niveau ${e.level}`),
button(text('Ajouter'), () => useRequestFetch()(`/api/character/${e.id}/campaign/${this.campaign!.id}`, { method: 'POST' }).then(() => this.ws!.send('character', { id: e.id, name: e.name, action: 'ADD', })).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(close)),
])) ?? [];
}).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(() => {
load.replaceWith(div('grid grid-cols-3 gap-2', characters.length > 0 ? characters : [span('text-light-60 dark:text-dark-60 text-sm italic', 'Vous n\'avez pas de personnage disponible')]));
});
}, 'flex flex-col flex-1 gap-2 p-4 items-center justify-center text-light-60 dark:text-dark-60'),
])
]),
div('flex h-full border-l border-light-40 dark:border-dark-40'),
div('flex flex-col', [

File diff suppressed because one or more lines are too long

View File

@ -1303,6 +1303,7 @@ export class CharacterSheet
private character?: CharacterCompiler;
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
private tabs?: HTMLDivElement & { refresh: () => void };
private tab: string = 'abilities';
ws?: Socket;
constructor(id: string, user: ComputedRef<User | null>)
@ -1405,7 +1406,7 @@ export class CharacterSheet
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: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px] h-full', content: 'overflow-auto' } });
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 w-[960px] h-full', content: 'overflow-auto' }, 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", [
div(),
@ -1753,18 +1754,16 @@ export class CharacterSheet
{
let debounceId: NodeJS.Timeout | undefined;
//TODO: Recompile values on "equip" checkbox change
const items = (character.variables.items.map(e => ({ ...e, item: config.items[e.id] })).filter(e => !!e.item) as Array<ItemState & { item: ItemConfig }>).map(e => div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [
div('flex flex-col gap-1', [ e.item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
e.equipped = v;
const items = (character.variables.items.map(e => ({ ...e, item: config.items[e.id], ref: e })).filter(e => !!e.item) as Array<ItemState & { item: ItemConfig, ref: ItemState }>).map(e => {
const price = div(['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 }], [ 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 }, e.item.price ? `${e.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 }], [ 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 }, e.item.weight ? `${e.item.weight * e.amount}` : '-') ]);
return foldable(() => [
markdown(getText(e.item.description)),
div('flex flex-row justify-center', [
this.character?.character.campaign ? button(text('Partager'), () => {
this.character!.variable('items', this.character!.character.variables.items);
debounceId && clearTimeout(debounceId);
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
this.tabs?.refresh();
}, class: { container: '!w-5 !h-5' } }) : checkbox({ disabled: true, class: { container: '!w-5 !h-5' } }), button(icon('radix-icons:trash', { width: 16, height: 17 }), () => {
}, 'p-1') : undefined,
button(icon('radix-icons:trash'), () => {
const idx = this.character!.character.variables.items.findIndex(_e => _e.id === e.id);
if(idx === -1) return;
@ -1777,19 +1776,31 @@ export class CharacterSheet
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
this.tabs?.refresh();
}, 'p-px') ]),
div('flex flex-col gap-1', [ span([colorByRarity[e.item.rarity], 'text-lg'], e.item.name), div('flex flex-row gap-4 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(e.item, e).map(text)) ]),
]),
div('grid grid-cols-2 row-gap-2 col-gap-8', [
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', (e.item.powercost || (e.enchantments && e.enchantments.length > 0)) && e.item.capacity ? `${(e.item?.powercost ?? 0) + (e.enchantments?.reduce((p, v) => (config.enchantments[v]?.power ?? 0) + p, 0) ?? 0)}/${e.item.capacity}` : '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', e.item.weight?.toString() ?? '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', e.charges && e.item.charge ? `${e.charges}/${e.item.charge}` : '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', e.amount?.toString() ?? '-') ])
])
]));
}, 'p-1'),
]) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [
e.item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
e.equipped = e.ref.equipped = v;
const power = character.variables.items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + (config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0), 0);
const weight = character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0), 0);
this.character!.variable('items', this.character!.character.variables.items);
debounceId && clearTimeout(debounceId);
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
}, class: { container: '!w-5 !h-5' } }) : undefined,
div('flex flex-row items-center gap-4', [ span([colorByRarity[e.item.rarity], 'text-lg'], e.item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(e.item).map(e => span('', e))) ]),
]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
e.amount > 1 ? tooltip(price, `Prix unitaire: ${e.item.price}`, 'bottom') : price,
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?.toString() ?? '-') ]),
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('', e.item.powercost || e.item.capacity ? `${e.item.powercost ?? 0}/${e.item.capacity ?? 0}` : '-') ]),
e.amount > 1 ? tooltip(weight, `Poids unitaire: ${e.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('', e.item.charge ? `${e.item.charge}` : '-') ]),
]),
])], { open: false, class: { icon: 'px-2', container: 'p-1 gap-2', content: 'px-4 pb-1 flex flex-col' } })
});
const power = character.variables.items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0);
const weight = character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
return [
div('flex flex-col gap-2', [
@ -1798,7 +1809,7 @@ export class CharacterSheet
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': power > (character.capacity === false ? 0 : character.capacity) }], text: `Puissance magique: ${power}/${character.capacity}` }),
button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'),
]),
div('grid grid-cols-2 flex-1 gap-4', items)
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', items)
])
]
}

View File

@ -35,7 +35,7 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise
return state;
}
export function button(content: Node, onClick?: (this: HTMLElement) => void, cls?: Class)
export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: Class)
{
/*
text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
@ -46,7 +46,7 @@ export function button(content: Node, onClick?: (this: HTMLElement) => void, cls
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, [ content ]);
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, Array.isArray(content) ? content : [content]);
let disabled = false;
Object.defineProperty(btn, 'disabled', {
get: () => disabled,
@ -495,13 +495,16 @@ 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] }), ]);
return element;
}
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }): HTMLDivElement & { refresh: () => void }
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): HTMLDivElement & { refresh: () => void }
{
let focus = settings?.focused ?? tabs[0]?.id;
const titles = tabs.map((e, i) => 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() {
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'))
return;
if(settings?.switch && settings.switch(e.id) === false)
return;
titles.forEach(e => e.toggleAttribute('data-focus', false));
this.toggleAttribute('data-focus', true);
focus = e.id;
@ -716,7 +719,7 @@ export class Toaster
static init()
{
Toaster._container = dom('div', { attributes: { id: 'toaster' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72' });
Toaster._container = dom('div', { attributes: { id: 'toaster' }, 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(Toaster._container);
}
static add(_config: ToastConfig)

View File

@ -9,6 +9,10 @@ type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev:
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
} | undefined;
export type DOMList = Node[] & {
remove(predicate: (item: Node, index: number, array: Node[]) => boolean): Node[];
};
export interface NodeProperties
{
attributes?: Record<string, string | undefined | boolean | number>;
@ -21,7 +25,7 @@ export interface NodeProperties
}
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[K]
export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?: NodeProperties, children?: NodeChildren | DOMList): HTMLElementTagNameMap[K]
{
const element = document.createElement(tag);

View File

@ -27,7 +27,6 @@ export class HomebrewBuilder
{
this._config = config as CharacterConfig;
this._featureEditor = new FeaturePanel();
ItemPanel.config = this._config;
this._container = container;
this._tabs = tabgroup([
@ -665,7 +664,7 @@ export class FeaturePanel
const _feature = JSON.parse(JSON.stringify(feature)) as Feature;
const effectContainer = div('grid grid-cols-2 gap-4 px-2', _feature.effect.map(e => new FeatureEditor(_feature.effect!, e.id, false).container));
MarkdownEditor.singleton.content = getText(_feature.description);
MarkdownEditor.singleton.onChange = (value) => ItemPanel.config.texts[_feature.description]!.default = value;
MarkdownEditor.singleton.onChange = (value) => config.texts[_feature.description]!.default = value;
return dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
div('flex flex-row justify-between items-center', [
tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => {
@ -722,12 +721,11 @@ export class FeaturePanel
}
export class ItemPanel
{
static config: CharacterConfig;
static render(item: ItemConfig, success: (item: ItemConfig) => void, failure: (item: ItemConfig) => void)
{
const _item = JSON.parse(JSON.stringify(item)) as ItemConfig;
MarkdownEditor.singleton.content = getText(_item.description);
MarkdownEditor.singleton.onChange = (value) => ItemPanel.config.texts[_item.description]!.default = value;
MarkdownEditor.singleton.onChange = (value) => config.texts[_item.description]!.default = value;
const effectContainer = div('grid grid-cols-2 gap-4 px-2 flex-1', _item.effects?.map(e => new FeatureEditor(_item.effects!, e.id, false).container));
return dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
div('flex flex-row justify-between items-center', [

View File

@ -34,6 +34,7 @@ export interface PopperProperties extends FloatingProperties
export interface ModalProperties
{
priority?: boolean;
class?: { blocker?: Class, popup?: Class },
closeWhenOutside?: boolean;
onClose?: () => boolean | void;
}
@ -381,16 +382,16 @@ export function fullblocker(content: NodeChildren, properties?: ModalProperties)
return { close: () => {} };
const close = () => (!properties?.onClose || properties.onClose() !== false) && _modal.remove();
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } });
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }, properties?.class?.blocker], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } });
const _modal = dom('div', { class: ['fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40', properties?.class?.blocker] }, [ _modalBlocker, ...content]);
teleport.appendChild(_modal);
return { close };
}
export function modal(content: NodeChildren, properties?: ModalProperties)
export function modal(content: NodeChildren, properties?: ModalProperties & { class?: { container?: Class } })
{
return fullblocker([ dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content) ], properties);
return fullblocker([ dom('div', { class: ['max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative', properties?.class?.container] }, content) ], properties);
}
export function confirm(title: string): Promise<boolean>

View File

@ -61,6 +61,10 @@ export class Socket
{
this._handlers.set(type, callback);
}
public send(type: string, data: any)
{
this._ws.readyState === WebSocket.OPEN && this._ws.send(JSON.stringify({ type, data }));
}
public close()
{
this._ws.close(1000);