You've already forked obsidian-visualiser
Merge branch 'character' into dev
This commit is contained in:
5140
shared/character-config.json
Normal file
5140
shared/character-config.json
Normal file
File diff suppressed because it is too large
Load Diff
498
shared/character.util.ts
Normal file
498
shared/character.util.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
import type { Ability, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from './character-config.json';
|
||||
import { button, loading } from "./proses";
|
||||
import { div, dom, icon, text } from "./dom.util";
|
||||
import { popper } from "./floating.util";
|
||||
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
export const MAIN_STATS = ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] as const;
|
||||
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const;
|
||||
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const;
|
||||
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const;
|
||||
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const;
|
||||
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
|
||||
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
|
||||
|
||||
export const defaultCharacter: Character = {
|
||||
id: -1,
|
||||
|
||||
name: "",
|
||||
people: undefined,
|
||||
level: 1,
|
||||
health: 0,
|
||||
mana: 0,
|
||||
|
||||
training: MAIN_STATS.reduce((p, v) => { p[v] = [[0, 0]]; return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
|
||||
leveling: [[1, 0]],
|
||||
abilities: {},
|
||||
spells: [],
|
||||
modifiers: {},
|
||||
choices: {},
|
||||
|
||||
owner: -1,
|
||||
visibility: "private",
|
||||
};
|
||||
|
||||
export const mainStatTexts: Record<MainStat, string> = {
|
||||
"strength": "Force",
|
||||
"dexterity": "Dextérité",
|
||||
"constitution": "Constitution",
|
||||
"intelligence": "Intelligence",
|
||||
"curiosity": "Curiosité",
|
||||
"charisma": "Charisme",
|
||||
"psyche": "Psyché",
|
||||
};
|
||||
|
||||
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
|
||||
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
|
||||
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
|
||||
thunder: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Foudre' },
|
||||
earth: { class: 'text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange', text: 'Terre' },
|
||||
arcana: { class: 'text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo', text: 'Arcane' },
|
||||
air: { class: 'text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime', text: 'Air' },
|
||||
nature: { class: 'text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green', text: 'Nature' },
|
||||
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
|
||||
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
|
||||
};
|
||||
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
|
||||
export const CharacterValidation = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
people: z.number().nullable(),
|
||||
level: z.number().min(1).max(20),
|
||||
aspect: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
health: z.number().default(0),
|
||||
mana: z.number().default(0),
|
||||
training: z.object(MAIN_STATS.reduce((p, v) => {
|
||||
p[v] = z.array(z.tuple([z.number().min(0).max(15), z.number()]));
|
||||
return p;
|
||||
}, {} as Record<MainStat, z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>)),
|
||||
leveling: z.array(z.tuple([z.number().min(1).max(20), z.number()])),
|
||||
abilities: z.object(ABILITIES.reduce((p, v) => {
|
||||
p[v] = z.tuple([z.number(), z.number()]);
|
||||
return p;
|
||||
}, {} as Record<Ability, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>)).partial(),
|
||||
spells: z.string().array(),
|
||||
modifiers: z.object(MAIN_STATS.reduce((p, v) => {
|
||||
p[v] = z.number();
|
||||
return p;
|
||||
}, {} as Record<MainStat, z.ZodNumber>)).partial(),
|
||||
owner: z.number(),
|
||||
username: z.string().optional(),
|
||||
visibility: z.enum(["public", "private"]),
|
||||
thumbnail: z.any(),
|
||||
});
|
||||
|
||||
const stepTexts: Record<number, string> = {
|
||||
0: 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.',
|
||||
1: 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.',
|
||||
2: 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.',
|
||||
3: 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.',
|
||||
4: 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'
|
||||
};
|
||||
|
||||
type PropertySum = { list: Array<string | number>, value: number, _dirty: boolean };
|
||||
export class CharacterBuilder
|
||||
{
|
||||
private _container: HTMLDivElement;
|
||||
private _content?: HTMLDivElement;
|
||||
private _stepsHeader: HTMLDivElement[] = [];
|
||||
private _stepsContent: BuilderTab[] = [];
|
||||
private id?: string;
|
||||
|
||||
private _character!: Character;
|
||||
private _result!: CompiledCharacter;
|
||||
private _buffer: Record<string, PropertySum> = {};
|
||||
|
||||
constructor(container: HTMLDivElement, id?: string)
|
||||
{
|
||||
this.id = id;
|
||||
this._container = container;
|
||||
|
||||
if(id)
|
||||
{
|
||||
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
|
||||
container.replaceChildren(load);
|
||||
useRequestFetch()(`/api/character/${id}`).then(character => {
|
||||
if(character)
|
||||
{
|
||||
this._character = character;
|
||||
|
||||
document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`;
|
||||
|
||||
if(character.people)
|
||||
{
|
||||
const people = config.peoples[character.people]!;
|
||||
|
||||
character.leveling.forEach(e => {
|
||||
const feature = people.options[e[0]][e[1]]!;
|
||||
feature.effect.map(e => this.apply(e));
|
||||
});
|
||||
|
||||
MAIN_STATS.forEach(stat => {
|
||||
character.training[stat].forEach(option => {
|
||||
config.training[stat][option[0]][option[1]]!.features?.forEach(this.apply.bind(this));
|
||||
})
|
||||
});
|
||||
}
|
||||
load.remove();
|
||||
|
||||
this.render();
|
||||
this.display(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this._character = Object.assign({}, defaultCharacter);
|
||||
|
||||
document.title = `d[any] - Edition de nouveau personnage`;
|
||||
|
||||
this.render();
|
||||
this.display(0);
|
||||
}
|
||||
}
|
||||
private render()
|
||||
{
|
||||
this._stepsHeader = [
|
||||
dom("div", { class: "group flex items-center", }, [
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(0) } }, [text("Peuples")]),
|
||||
]),
|
||||
dom("div", { class: "group flex items-center", }, [
|
||||
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(1) } }, [text("Niveaux")]),
|
||||
]),
|
||||
dom("div", { class: "group flex items-center", }, [
|
||||
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(2) } }, [text("Entrainement")]),
|
||||
]),
|
||||
dom("div", { class: "group flex items-center", }, [
|
||||
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(3) } }, [text("Compétences")]),
|
||||
]),
|
||||
dom("div", { class: "group flex items-center", }, [
|
||||
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(4) } }, [text("Aspect")])
|
||||
]),
|
||||
];
|
||||
this._stepsContent = [
|
||||
new PeoplePicker(this),
|
||||
];
|
||||
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
|
||||
div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [popper(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), {
|
||||
arrow: true,
|
||||
offset: 8,
|
||||
content: [ text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.") ],
|
||||
placement: "bottom-end",
|
||||
class: "max-w-96 fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50"
|
||||
})]),
|
||||
]),
|
||||
this._content,
|
||||
]));
|
||||
}
|
||||
display(step: number)
|
||||
{
|
||||
if(step < 0 || step >= this._stepsHeader.length)
|
||||
return;
|
||||
|
||||
if(this._stepsContent.slice(0, step).some(e => !e.validate()))
|
||||
return;
|
||||
|
||||
this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
|
||||
this._stepsHeader[step]!.setAttribute('data-state', 'active');
|
||||
|
||||
this._stepsContent[step]!.update();
|
||||
this._content?.replaceChildren(...this._stepsContent[step]!.dom);
|
||||
}
|
||||
async save(leave: boolean = true)
|
||||
{
|
||||
if(this.id === 'new')
|
||||
{
|
||||
//@ts-ignore
|
||||
this.id = this._character.id = await useRequestFetch()(`/api/character`, {
|
||||
method: 'post',
|
||||
body: this._character,
|
||||
onResponseError: (e) => {
|
||||
//add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||
}
|
||||
});
|
||||
//add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
||||
useRouter().replace({ name: 'character-id-edit', params: { id: this.id } })
|
||||
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
|
||||
}
|
||||
else
|
||||
{
|
||||
//@ts-ignore
|
||||
await useRequestFetch()(`/api/character/${id}`, {
|
||||
method: 'post',
|
||||
body: this._character,
|
||||
onResponseError: (e) => {
|
||||
//add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||
}
|
||||
});
|
||||
//add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
||||
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
|
||||
}
|
||||
}
|
||||
|
||||
get character(): Character
|
||||
{
|
||||
return this._character;
|
||||
}
|
||||
get compiled(): CompiledCharacter
|
||||
{
|
||||
this.compile(Object.keys(this._buffer));
|
||||
|
||||
return this._result;
|
||||
}
|
||||
get values(): Record<string, number>
|
||||
{
|
||||
const keys = Object.keys(this._buffer);
|
||||
this.compile(keys);
|
||||
|
||||
return keys.reduce((p, v) => {
|
||||
p[v] = this._buffer[v]!.value;
|
||||
return p;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
private compile(properties: string[])
|
||||
{
|
||||
const queue = properties;
|
||||
queue.forEach(property => {
|
||||
const buffer = this._buffer[property];
|
||||
|
||||
if(property === "")
|
||||
return
|
||||
|
||||
if(buffer && buffer._dirty === true)
|
||||
{
|
||||
let sum = 0;
|
||||
for(let i = 0; i < buffer.list.length; i++)
|
||||
{
|
||||
if(typeof buffer.list[i] === 'string')
|
||||
{
|
||||
if(this._buffer[buffer.list[i]!]!._dirty)
|
||||
{
|
||||
//Put it back in queue since its dependencies haven't been resolved yet
|
||||
queue.push(property);
|
||||
return;
|
||||
}
|
||||
else
|
||||
sum += this._buffer[buffer.list[i]!]!.value;
|
||||
}
|
||||
else
|
||||
sum += buffer.list[i] as number;
|
||||
}
|
||||
|
||||
const path = property.split("/");
|
||||
const object = path.slice(0, -1).reduce((p, v) => p[v], this._result as any);
|
||||
|
||||
object[path.slice(-1)[0]!] = sum;
|
||||
this._buffer[property]!.value = sum;
|
||||
this._buffer[property]!._dirty = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
private updateLevel(level: Level)
|
||||
{
|
||||
this._character.level = level;
|
||||
|
||||
if(this._character.leveling) //Invalidate higher levels
|
||||
{
|
||||
for(let level = 20; level > this._character.level; level--)
|
||||
{
|
||||
const index = this._character.leveling.findIndex(e => e[0] == level);
|
||||
if(index !== -1)
|
||||
{
|
||||
const option = this._character.leveling[level]!;
|
||||
this._character.leveling.splice(index, 1);
|
||||
|
||||
this.remove(config.peoples[this._character.people!]!.options[option[0]][option[1]]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private toggleLevelOption(level: Level, choice: number)
|
||||
{
|
||||
if(level > this._character.level) //Cannot add more level options than the current level
|
||||
return;
|
||||
|
||||
if(this._character.leveling === undefined) //Add level 1 if missing
|
||||
{
|
||||
this._character.leveling = [[1, 0]];
|
||||
this.add(config.peoples[this._character.people!]!.options[1][0]!);
|
||||
}
|
||||
|
||||
if(level == 1) //Cannot remove level 1
|
||||
return;
|
||||
|
||||
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
||||
{
|
||||
if(!this._character.leveling.some(e => e[0] == i))
|
||||
return;
|
||||
}
|
||||
|
||||
const option = this._character.leveling.find(e => e[0] == level);
|
||||
if(option && option[1] !== choice) //If the given level is already selected, switch to the new choice
|
||||
{
|
||||
this._character.leveling.splice(this._character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
|
||||
|
||||
this.remove(config.peoples[this._character.people!]!.options[option[0]][option[1]]!);
|
||||
this.add(config.peoples[this._character.people!]!.options[level][choice]!);
|
||||
}
|
||||
else if(!option)
|
||||
{
|
||||
this._character.leveling.push([level, choice]);
|
||||
|
||||
this.add(config.peoples[this._character.people!]!.options[level][choice]!);
|
||||
}
|
||||
}
|
||||
private toggleTrainingOption(stat: MainStat, level: TrainingLevel, option: number)
|
||||
{
|
||||
|
||||
}
|
||||
private add(feature: Feature)
|
||||
{
|
||||
feature.effect.forEach(this.apply.bind(this));
|
||||
}
|
||||
private remove(feature: Feature)
|
||||
{
|
||||
|
||||
}
|
||||
private apply(feature: FeatureItem)
|
||||
{
|
||||
switch(feature.category)
|
||||
{
|
||||
case "feature":
|
||||
this._result.features[feature.kind].push(feature.text);
|
||||
|
||||
return;
|
||||
case "list":
|
||||
if(feature.action === 'add' && !this._result[feature.list].includes(feature.item))
|
||||
this._result[feature.list].push(feature.item);
|
||||
else
|
||||
this._result[feature.list] = this._result[feature.list].filter((e: string) => e !== feature.item);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
|
||||
|
||||
if(feature.operation === 'add')
|
||||
this._buffer[feature.property]!.list.push(feature.value);
|
||||
else if(feature.operation === 'set')
|
||||
this._buffer[feature.property]!.list = [feature.value];
|
||||
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id]!;
|
||||
choice.forEach(e => this.apply(feature.options[e]!));
|
||||
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface BuilderTab {
|
||||
dom: Array<Node | string>;
|
||||
update: () => void;
|
||||
validate: () => boolean;
|
||||
};
|
||||
class PeoplePicker implements BuilderTab
|
||||
{
|
||||
private _builder: CharacterBuilder;
|
||||
private _content: Array<Node | string>;
|
||||
|
||||
private _nameInput: HTMLInputElement;
|
||||
private _visibilityInput: HTMLDivElement;
|
||||
private _options: HTMLDivElement[];
|
||||
|
||||
private _activeOption?: HTMLDivElement;
|
||||
|
||||
constructor(builder: CharacterBuilder)
|
||||
{
|
||||
/*
|
||||
<div class="flex flex-1 gap-4 p-2 overflow-x-auto justify-center">
|
||||
<div v-for="(people, i) of config.peoples" @click="model.character.people = i" class="flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35
|
||||
cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]" :class="{ '!border-accent-blue outline-2 outline outline-accent-blue': model.character.people === i }">
|
||||
<Avatar :src="people.name" :text="`Image placeholder`" class="h-[320px]" />
|
||||
<span class="text-xl font-bold text-center">{{ people.name }}</span>
|
||||
<span class="w-full border-b border-light-50 dark:border-dark-50"></span>
|
||||
<span class="text-wrap word-break">{{ people.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
*/
|
||||
this._builder = builder;
|
||||
|
||||
this._nameInput = dom("input", { class: `mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, listeners: {
|
||||
input: (e: Event) => {
|
||||
this._builder.character.name = this._nameInput.value ?? '';
|
||||
document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
|
||||
}
|
||||
}});
|
||||
this._visibilityInput = dom("div", { class: `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": "unckecked" }, listeners: {
|
||||
click: (e: Event) => {
|
||||
this._builder.character.visibility = this._builder.character.visibility === "private" ? "public" : "private";
|
||||
console.log(this._builder.character.visibility);
|
||||
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
|
||||
}
|
||||
}}, [ 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') ]);
|
||||
|
||||
this._options = config.peoples.map(
|
||||
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
|
||||
this._builder.character.people = i;
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
|
||||
this._activeOption = this._options[i]!;
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
|
||||
}
|
||||
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
|
||||
);
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
|
||||
dom("label", { class: "flex justify-center items-center my-2" }, [
|
||||
dom("span", { class: "pb-1 md:p-0", text: "Nom" }),
|
||||
this._nameInput,
|
||||
]),
|
||||
dom("label", { class: "flex justify-center items-center my-2" }, [
|
||||
dom("span", { class: "md:text-base text-sm", text: "Privé ?" }),
|
||||
this._visibilityInput,
|
||||
]),
|
||||
button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'),
|
||||
]), div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options)];
|
||||
}
|
||||
update()
|
||||
{
|
||||
this._nameInput.value = this._builder.character.name;
|
||||
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
|
||||
|
||||
if(this._builder.character.people !== undefined)
|
||||
{
|
||||
this._activeOption = this._options[this._builder.character.people]!;
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
|
||||
}
|
||||
}
|
||||
validate(): boolean
|
||||
{
|
||||
return this._builder.character.people !== undefined;
|
||||
}
|
||||
get dom()
|
||||
{
|
||||
return this._content;
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,7 @@ export class Content
|
||||
private static root: FileSystemDirectoryHandle;
|
||||
|
||||
private static _overview: Record<string, Omit<LocalContent, 'content'>>;
|
||||
private static _reverseMapping: Record<string, string>;
|
||||
private static queue = new AsyncQueue();
|
||||
|
||||
static init(): Promise<boolean>
|
||||
@@ -129,7 +130,6 @@ export class Content
|
||||
return false;
|
||||
|
||||
Content.root = await navigator.storage.getDirectory();
|
||||
Content.root.getDirectoryHandle('storage', { create: true });
|
||||
|
||||
const overview = await Content.read('overview', { create: true });
|
||||
try
|
||||
@@ -141,6 +141,10 @@ export class Content
|
||||
Content._overview = {};
|
||||
await Content.pull();
|
||||
}
|
||||
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => {
|
||||
p[v.path] = v.id;
|
||||
return p;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
Content._ready = true;
|
||||
}
|
||||
@@ -156,7 +160,12 @@ export class Content
|
||||
}
|
||||
static getFromPath(path: string)
|
||||
{
|
||||
return Object.values(Content._overview).find(e => e.path === path);
|
||||
const id = Content.idFromPath(path);
|
||||
return id ? Content._overview[id] : undefined;
|
||||
}
|
||||
static idFromPath(path: string)
|
||||
{
|
||||
return Content._reverseMapping[path];
|
||||
}
|
||||
static get(id: string)
|
||||
{
|
||||
@@ -246,13 +255,13 @@ export class Content
|
||||
if(file.type !== 'folder')
|
||||
{
|
||||
Content.queue.queue(() => Content.write(file.id, content, { create: true }));
|
||||
Content.queue.queue(() => Content.write('storage/' + file.path + (file.type === 'canvas' ? '.canvas' : '.md'), Content.toString({ ...file, content: Content.fromString(file, content) }), { create: true }));
|
||||
//Content.queue.queue(() => Content.write('storage/' + file.path + (file.type === 'canvas' ? '.canvas' : '.md'), Content.toString({ ...file, content: Content.fromString(file, content) }), { create: true }));
|
||||
}
|
||||
}
|
||||
else
|
||||
Content._overview[file.id].error = true;
|
||||
Content._overview[file.id]!.error = true;
|
||||
}).catch(e => {
|
||||
Content._overview[file.id].error = true;
|
||||
Content._overview[file.id]!.error = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -722,15 +731,15 @@ export class Editor
|
||||
{
|
||||
exists = false;
|
||||
|
||||
input.parentElement?.replaceChild(text, input);
|
||||
input.parentElement?.replaceChild(text!, input);
|
||||
input.remove();
|
||||
if(value !== item.title) this.history.add('overview', 'rename', [{ element: item, from: item.title, to: value }], true);
|
||||
}
|
||||
}
|
||||
const text = item.element!.children[0].children[1];
|
||||
const text = item.element!.children[0]?.children[1];
|
||||
const input = dom('input', { attributes: { type: 'text', value: item.title }, class: 'bg-light-20 dark:bg-dark-20 outline outline-light-35 dark:outline-dark-35 outline-offset-0 pl-1.5 py-1.5 flex-1', listeners: { mousedown: cancelPropagation, click: cancelPropagation, blur: change, change: change } });
|
||||
|
||||
text.parentElement?.replaceChild(input, text);
|
||||
text?.parentElement?.replaceChild(input, text);
|
||||
input.focus();
|
||||
}
|
||||
private toggleNavigable(e: Event, item: LocalContent & { element?: HTMLElement })
|
||||
|
||||
@@ -52,6 +52,10 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
|
||||
|
||||
return element;
|
||||
}
|
||||
export function div(cls?: Class, children?: NodeChildren): HTMLDivElement
|
||||
{
|
||||
return dom("div", { class: cls }, children);
|
||||
}
|
||||
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Omit<NodeChildren, 'HTMLElement' | 'Text'>): SVGElementTagNameMap[K]
|
||||
{
|
||||
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||
|
||||
@@ -9,14 +9,11 @@ export interface ContextProperties
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
}
|
||||
export interface PopperProperties
|
||||
export interface PopperProperties extends ContextProperties
|
||||
{
|
||||
placement?: FloatingUI.Placement;
|
||||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
content?: NodeChildren;
|
||||
delay?: number;
|
||||
viewport?: HTMLElement;
|
||||
|
||||
onShow?: (element: HTMLDivElement) => boolean | void;
|
||||
onHide?: (element: HTMLDivElement) => boolean | void;
|
||||
@@ -40,6 +37,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
||||
let shown = false, timeout: Timer;
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const content = dom('div', { class: ['fixed hidden', properties?.class], attributes: { 'data-state': 'closed' } }, [...(properties?.content ?? []), arrow]);
|
||||
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
||||
|
||||
function update()
|
||||
{
|
||||
@@ -48,16 +46,17 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
||||
strategy: 'fixed',
|
||||
middleware: [
|
||||
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
|
||||
FloatingUI.flip(),
|
||||
properties?.offset ? FloatingUI.shift({ padding: properties?.offset }) : undefined,
|
||||
FloatingUI.autoPlacement({ rootBoundary: rect }),
|
||||
properties?.offset ? FloatingUI.shift({ padding: properties?.offset, rootBoundary: rect }) : undefined,
|
||||
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
||||
FloatingUI.hide({ rootBoundary: rect }),
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(content.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
|
||||
|
||||
const side = placement.split('-')[0] as FloatingUI.Side;
|
||||
|
||||
content.setAttribute('data-side', side);
|
||||
|
||||
@@ -10,13 +10,29 @@ export function getID(length: number)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
return id.join("");
|
||||
}
|
||||
export function group<
|
||||
T,
|
||||
K extends keyof T,
|
||||
V extends keyof T,
|
||||
KeyType extends string | number | symbol = Extract<T[K], string | number | symbol>
|
||||
>(
|
||||
table: T[],
|
||||
key: K & (T[K] extends string | number | symbol ? K : never),
|
||||
value: V
|
||||
): Record<KeyType, T[V]> {
|
||||
return table.reduce((p, v) => {
|
||||
p[v[key] as KeyType] = v[value];
|
||||
return p;
|
||||
}, {} as Record<KeyType, T[V]>);
|
||||
}
|
||||
export function parsePath(path: string): string
|
||||
{
|
||||
return path.replace(/(\d+?)\. ?/, '').toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '').replace(/\-+/g, '-');
|
||||
return path.replace(/(\d+?)\. ?/g, '').toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '').replace(/\-+/g, '-');
|
||||
}
|
||||
export function parseId(id: string | undefined): string |undefined
|
||||
export function parseId(id: string | undefined): string | undefined
|
||||
{
|
||||
return id?.normalize('NFD')?.replace(/[\u0300-\u036f]/g, '')?.replace(/^\d\. */g, '')?.replace(/\s/g, "-")?.replace(/%/g, "-percent")?.replace(/\?/g, "-q")?.toLowerCase();
|
||||
return id;
|
||||
//return id?.normalize('NFD')?.replace(/[\u0300-\u036f]/g, '')?.replace(/^\d\. */g, '')?.replace(/\s/g, "-")?.replace(/%/g, "-percent")?.replace(/\?/g, "-q")?.toLowerCase();
|
||||
}
|
||||
export function padLeft(text: string, pad: string, length: number): string
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Root, RootContent } from "hast";
|
||||
import { dom, styling, text, type Class, type Node } from "./dom.util";
|
||||
import prose, { tag, a, blockquote, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout, type Prose } from "./proses";
|
||||
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout, type Prose } from "./proses";
|
||||
import { heading } from "hast-util-heading";
|
||||
import { headingRank } from "hast-util-heading-rank";
|
||||
import { parseId } from "./general.util";
|
||||
@@ -63,7 +63,7 @@ export default function(content: string, filter?: string, properties?: MDPropert
|
||||
}
|
||||
}
|
||||
|
||||
const el = renderMarkdown(data, { tag, a, blockquote, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
|
||||
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
|
||||
|
||||
if(properties) styling(el, properties);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CanvasContent, CanvasNode } from "~/types/canvas";
|
||||
import type { CanvasNode } from "~/types/canvas";
|
||||
import type { CanvasPreferences } from "~/types/general";
|
||||
import type { Position, Box, Direction, CanvasEditor } from "./canvas.util";
|
||||
|
||||
|
||||
@@ -9,45 +9,13 @@ import { unifySlug } from "#shared/general.util";
|
||||
|
||||
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
||||
export type Prose = { class: string } | { custom: CustomProse };
|
||||
export const tag: Prose = {
|
||||
custom(properties, children) {
|
||||
const tag = properties.tag as string;
|
||||
const el = dom('span', { class: "before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30" }, children);
|
||||
const overview = Content.getFromPath('tags');
|
||||
|
||||
let rendered = false;
|
||||
|
||||
if(!!overview)
|
||||
{
|
||||
popper(el, {
|
||||
arrow: true,
|
||||
delay: 150,
|
||||
offset: 10,
|
||||
placement: 'bottom-start',
|
||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 max-w-[600px] max-h-[600px] w-full z-[45]',
|
||||
content: [loading("large")],
|
||||
onShow(content: HTMLDivElement) {
|
||||
if(!rendered)
|
||||
{
|
||||
Content.getContent(overview.id).then((_content) => {
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', tag, { class: 'max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]);
|
||||
});
|
||||
rendered = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
||||
export const a: Prose = {
|
||||
custom(properties, children) {
|
||||
const href = properties.href as string;
|
||||
const href = decodeURIComponent(properties.href) as string;
|
||||
const { hash, pathname } = parseURL(href);
|
||||
const router = useRouter();
|
||||
|
||||
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path) : pathname);
|
||||
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
|
||||
|
||||
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
|
||||
|
||||
@@ -74,18 +42,19 @@ export const a: Prose = {
|
||||
placement: 'bottom-start',
|
||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]',
|
||||
content: [loading("large")],
|
||||
viewport: document.getElementById('mainContainer') ?? undefined,
|
||||
onShow(content: HTMLDivElement) {
|
||||
if(!rendered)
|
||||
{
|
||||
Content.getContent(overview.id).then((_content) => {
|
||||
if(_content?.type === 'markdown')
|
||||
{
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]);
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]!);
|
||||
}
|
||||
if(_content?.type === 'canvas')
|
||||
{
|
||||
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
||||
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]);
|
||||
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!);
|
||||
canvas.mount();
|
||||
}
|
||||
});
|
||||
@@ -104,7 +73,7 @@ export const fakeA: Prose = {
|
||||
const { hash, pathname } = parseURL(href);
|
||||
const router = useRouter();
|
||||
|
||||
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path) : pathname);
|
||||
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
|
||||
|
||||
const el = dom('span', { class: 'cursor-pointer text-accent-blue inline-flex items-center' }, [
|
||||
dom('span', {}, [
|
||||
@@ -127,16 +96,16 @@ export const fakeA: Prose = {
|
||||
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
|
||||
return false;
|
||||
|
||||
content.replaceChild(loading("large"), content.children[0]);
|
||||
content.replaceChild(loading("large"), content.children[0]!);
|
||||
Content.getContent(overview.id).then((_content) => {
|
||||
if(_content?.type === 'markdown')
|
||||
{
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]);
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]!);
|
||||
}
|
||||
if(_content?.type === 'canvas')
|
||||
{
|
||||
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
||||
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]);
|
||||
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!);
|
||||
canvas.mount();
|
||||
}
|
||||
});
|
||||
@@ -173,22 +142,24 @@ export const callout: Prose = {
|
||||
} = properties;
|
||||
|
||||
let open = fold;
|
||||
const trigger = dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'] }, [icon(calloutIconByType[type] ?? defaultCalloutIcon, { inline: true, width: 24, height: 24, class: 'w-6 h-6 stroke-2 float-start me-2 flex-shrink-0' }), !!title ? dom('span', { class: 'block font-bold text-start', text: title }) : undefined, fold !== undefined ? icon('radix-icons:caret-right', { height: 24, width: 24, class: 'transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6' }) : undefined]);
|
||||
const container = dom('div', { class: 'callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
|
||||
trigger,
|
||||
dom('div', { class: 'overflow-hidden group-data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] group-data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out] group-data-[state=closed]:h-0' }, [
|
||||
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => {
|
||||
container.setAttribute('data-state', open ? 'open' : 'closed');
|
||||
open = !open;
|
||||
}}},
|
||||
[icon(calloutIconByType[type] ?? defaultCalloutIcon, { inline: true, width: 24, height: 24, class: 'w-6 h-6 stroke-2 float-start me-2 flex-shrink-0' }), !!title ? dom('span', { class: 'block font-bold text-start', text: title }) : undefined, fold !== undefined ? icon('radix-icons:caret-right', { height: 24, width: 24, class: 'transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6' }) : undefined
|
||||
]),
|
||||
dom('div', { class: {'overflow-hidden': true, 'group-data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] group-data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out] group-data-[state=closed]:h-0': fold !== undefined } }, [
|
||||
dom('div', { class: 'px-2' }, children),
|
||||
])
|
||||
]);
|
||||
|
||||
trigger.addEventListener('click', e => {
|
||||
container.setAttribute('data-state', open ? 'open' : 'closed');
|
||||
open = !open;
|
||||
})
|
||||
|
||||
return container;
|
||||
},
|
||||
}
|
||||
export const tag: Prose = {
|
||||
class: "before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30",
|
||||
}
|
||||
export const blockquote: Prose = {
|
||||
class: 'empty:before:hidden ps-4 my-4 relative before:absolute before:-top-1 before:-bottom-1 before:left-0 before:w-1 before:bg-light-30 dark:before:bg-dark-30',
|
||||
}
|
||||
|
||||
2723
shared/test.ts
Normal file
2723
shared/test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user