7 Commits

41 changed files with 1688 additions and 11292 deletions

View File

@@ -33,6 +33,13 @@ onBeforeMount(() => {
</script> </script>
<style> <style>
iconify-icon
{
display: inline-block;
width: attr(width px, 1rem);
height: attr(height px, 1rem);
box-sizing: content-box;
}
.ToastRoot[data-type='error'] { .ToastRoot[data-type='error'] {
@apply border-light-red; @apply border-light-red;
@apply dark:border-dark-red; @apply dark:border-dark-red;

View File

@@ -1,64 +0,0 @@
import { Content } from '~/shared/content.util';
import type { ExploreContent, ContentComposable, TreeItem } from '~/types/content';
const useContentState = () => useState<ExploreContent[]>('content', () => []);
export function useContent(): ContentComposable {
const contentState = useContentState();
return {
content: contentState,
tree: computed(() => {
const arr: TreeItem[] = [];
for(const element of contentState.value)
{
addChild(arr, element);
}
return arr;
}),
fetch,
get,
}
}
async function fetch(force: boolean = false) {
const content = useContentState();
if(content.value.length === 0 || force)
content.value = await useRequestFetch()('/api/file/overview');
}
async function get(path: string, force: boolean = false): Promise<ExploreContent | undefined> {
const content = useContentState()
const value = content.value;
const item = value.find(e => e.path === path);
if(item && !item.content)
{
item.content = await useRequestFetch()(`/api/file/content/${encodeURIComponent(path)}`);
}
content.value = value;
return item;
}
function addChild(arr: TreeItem[], e: ExploreContent): void {
const parent = arr.find(f => e.path.startsWith(f.path));
if(parent)
{
if(!parent.children)
parent.children = [];
addChild(parent.children, e);
}
else
{
arr.push({ ...e });
arr.sort((a, b) => {
if(a.order !== b.order)
return a.order - b.order;
return a.title.localeCompare(b.title);
});
}
}

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -8,19 +8,16 @@ export const usersTable = table("users", {
hash: text().notNull().unique(), hash: text().notNull().unique(),
state: int().notNull().default(0), state: int().notNull().default(0),
}); });
export const usersDataTable = table("users_data", { export const usersDataTable = table("users_data", {
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}); });
export const userSessionsTable = table("user_sessions", { export const userSessionsTable = table("user_sessions", {
id: int().notNull(), id: int().notNull(),
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]); }, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
export const userPermissionsTable = table("user_permissions", { export const userPermissionsTable = table("user_permissions", {
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
permission: text().notNull(), permission: text().notNull(),
@@ -37,7 +34,6 @@ export const projectFilesTable = table("project_files", {
order: int().notNull(), order: int().notNull(),
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}); });
export const projectContentTable = table("project_content", { export const projectContentTable = table("project_content", {
id: text().primaryKey(), id: text().primaryKey(),
content: blob({ mode: 'buffer' }), content: blob({ mode: 'buffer' }),
@@ -62,38 +58,51 @@ export const characterTable = table("character", {
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'), visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
thumbnail: blob(), thumbnail: blob(),
}); });
export const characterTrainingTable = table("character_training", { export const characterTrainingTable = table("character_training", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(), stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(),
level: int().notNull(), level: int().notNull(),
choice: int().notNull(), choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]); }, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
export const characterLevelingTable = table("character_leveling", { export const characterLevelingTable = table("character_leveling", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
level: int().notNull(), level: int().notNull(),
choice: int().notNull(), choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.level] })]); }, (table) => [primaryKey({ columns: [table.character, table.level] })]);
export const characterAbilitiesTable = table("character_abilities", { export const characterAbilitiesTable = table("character_abilities", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(), ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(),
value: int().notNull().default(0), value: int().notNull().default(0),
max: int().notNull().default(0), max: int().notNull().default(0),
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]); }, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
export const characterChoicesTable = table("character_choices", { export const characterChoicesTable = table("character_choices", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
id: text().notNull(), id: text().notNull(),
choice: int().notNull(), choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]); }, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]);
export const campaignTable = table("campaign", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
description: text(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
});
export const campaignMembersTable = table("campaign_members", {
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
user: int().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
rights: text({ enum: [ 'player', 'dm' ] }),
}, (table) => [primaryKey({ columns: [table.id, table.user] })]);
export const campaignCharactersTable = table("campaign_characters", {
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
character: int().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
}, (table) => [primaryKey({ columns: [table.id, table.character] })]);
export const usersRelation = relations(usersTable, ({ one, many }) => ({ export const usersRelation = relations(usersTable, ({ one, many }) => ({
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
session: many(userSessionsTable), session: many(userSessionsTable),
permission: many(userPermissionsTable), permission: many(userPermissionsTable),
files: many(projectFilesTable), files: many(projectFilesTable),
characters: many(characterTable),
})); }));
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({ export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }), users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
@@ -107,14 +116,15 @@ export const userPermissionsRelation = relations(userPermissionsTable, ({ one })
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({ export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }), users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
})); }));
export const characterRelation = relations(characterTable, ({ one, many }) => ({ export const characterRelation = relations(characterTable, ({ one, many }) => ({
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }), user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
training: many(characterTrainingTable), training: many(characterTrainingTable),
levels: many(characterLevelingTable), levels: many(characterLevelingTable),
abilities: many(characterAbilitiesTable), abilities: many(characterAbilitiesTable),
choices: many(characterChoicesTable) choices: many(characterChoicesTable),
campaign: one(campaignCharactersTable, { fields: [characterTable.id], references: [campaignCharactersTable.character], }),
})); }));
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({ export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] }) character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
})); }));
@@ -127,3 +137,17 @@ export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({
export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({ export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] }) character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] })
})); }));
export const campaignRelation = relations(campaignTable, ({ one, many }) => ({
members: many(campaignMembersTable),
characters: many(campaignCharactersTable),
owner: one(usersTable),
}));
export const campaignMembersRelation = relations(campaignMembersTable, ({ one }) => ({
campaign: one(campaignTable, { fields: [campaignMembersTable.id], references: [campaignTable.id], }),
member: one(usersTable, { fields: [campaignMembersTable.user], references: [usersTable.id], })
}))
export const campaignCharacterRelation = relations(campaignCharactersTable, ({ one }) => ({
campaign: one(campaignTable, { fields: [campaignCharactersTable.id], references: [campaignTable.id], }),
character: one(characterTable, { fields: [campaignCharactersTable.character], references: [characterTable.id], })
}))

View File

@@ -0,0 +1,24 @@
CREATE TABLE `campaign_characters` (
`id` integer,
`character` integer,
PRIMARY KEY(`id`, `character`),
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `campaign_members` (
`id` integer,
`user` integer,
`rights` text,
PRIMARY KEY(`id`, `user`),
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`user`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `campaign` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`description` text,
`owner` integer NOT NULL,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);

View File

@@ -0,0 +1,886 @@
{
"version": "6",
"dialect": "sqlite",
"id": "42b1cd62-a77f-4fd3-b271-c44a66a56316",
"prevId": "153969ef-bcdb-4bbd-bd57-01fbd8004fc6",
"tables": {
"campaign_characters": {
"name": "campaign_characters",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"campaign_characters_id_campaign_id_fk": {
"name": "campaign_characters_id_campaign_id_fk",
"tableFrom": "campaign_characters",
"tableTo": "campaign",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
},
"campaign_characters_character_character_id_fk": {
"name": "campaign_characters_character_character_id_fk",
"tableFrom": "campaign_characters",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"campaign_characters_id_character_pk": {
"columns": [
"id",
"character"
],
"name": "campaign_characters_id_character_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"campaign_members": {
"name": "campaign_members",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user": {
"name": "user",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"rights": {
"name": "rights",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"campaign_members_id_campaign_id_fk": {
"name": "campaign_members_id_campaign_id_fk",
"tableFrom": "campaign_members",
"tableTo": "campaign",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
},
"campaign_members_user_users_id_fk": {
"name": "campaign_members_user_users_id_fk",
"tableFrom": "campaign_members",
"tableTo": "users",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"campaign_members_id_user_pk": {
"columns": [
"id",
"user"
],
"name": "campaign_members_id_user_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"campaign": {
"name": "campaign",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"campaign_owner_users_id_fk": {
"name": "campaign_owner_users_id_fk",
"tableFrom": "campaign",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_abilities": {
"name": "character_abilities",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ability": {
"name": "ability",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"max": {
"name": "max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_abilities_character_character_id_fk": {
"name": "character_abilities_character_character_id_fk",
"tableFrom": "character_abilities",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_abilities_character_ability_pk": {
"columns": [
"character",
"ability"
],
"name": "character_abilities_character_ability_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_choices": {
"name": "character_choices",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_choices_character_character_id_fk": {
"name": "character_choices_character_character_id_fk",
"tableFrom": "character_choices",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_choices_character_id_choice_pk": {
"columns": [
"character",
"id",
"choice"
],
"name": "character_choices_character_id_choice_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_leveling": {
"name": "character_leveling",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_leveling_character_character_id_fk": {
"name": "character_leveling_character_character_id_fk",
"tableFrom": "character_leveling",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_leveling_character_level_pk": {
"columns": [
"character",
"level"
],
"name": "character_leveling_character_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"people": {
"name": "people",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"variables": {
"name": "variables",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
},
"aspect": {
"name": "aspect",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"public_notes": {
"name": "public_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"private_notes": {
"name": "private_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'private'"
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_training": {
"name": "character_training",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stat": {
"name": "stat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_training_character_character_id_fk": {
"name": "character_training_character_character_id_fk",
"tableFrom": "character_training",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_training_character_stat_level_pk": {
"columns": [
"character",
"stat",
"level"
],
"name": "character_training_character_stat_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_content": {
"name": "project_content",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_files": {
"name": "project_files",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"project_files_path_unique": {
"name": "project_files_path_unique",
"columns": [
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"project_files_owner_users_id_fk": {
"name": "project_files_owner_users_id_fk",
"tableFrom": "project_files",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -127,6 +127,13 @@
"when": 1760531331328, "when": 1760531331328,
"tag": "0017_workable_scrambler", "tag": "0017_workable_scrambler",
"breakpoints": true "breakpoints": true
},
{
"idx": 18,
"version": "6",
"when": 1761829250157,
"tag": "0018_friendly_deadpool",
"breakpoints": true
} }
] ]
} }

View File

@@ -9,7 +9,10 @@
<div class="text-3xl">Une erreur est survenue.</div> <div class="text-3xl">Une erreur est survenue.</div>
</div> </div>
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre> <pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
<Button @click="handleError">Revenir en lieu sûr</Button> <button class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
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 p-2" @click="handleError">Revenir en lieu sûr</button>
</div> </div>
</template> </template>

View File

@@ -56,19 +56,34 @@ import { Content, iconByType } from '#shared/content.util';
import { dom, icon } from '#shared/dom.util'; import { dom, icon } from '#shared/dom.util';
import { unifySlug } from '#shared/general.util'; import { unifySlug } from '#shared/general.util';
import { tooltip } from '#shared/floating.util'; import { tooltip } from '#shared/floating.util';
import { link } from '#shared/components.util'; import { link, loading } from '#shared/components.util';
const open = ref(false); const open = ref(false);
let tree: TreeDOM | undefined;
const { loggedIn, user } = useUserSession(); const { loggedIn, user } = useUserSession();
const { fetch } = useContent();
await fetch(false);
const route = useRouter().currentRoute; const route = useRouter().currentRoute;
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined); const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
await Content.init();
const tree = new TreeDOM((item, depth) => { const unmount = useRouter().afterEach((to, from, failure) => {
if(failure)
return;
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree?.toggle(tree.tree.search('path', e)[0], true));
});
watch(route, () => {
open.value = false;
});
const treeParent = useTemplateRef('treeParent');
onMounted(() => {
if(treeParent.value)
{
treeParent.value.replaceChildren(loading('normal'));
Content.ready.then(() => {
tree = new TreeDOM((item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [ return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }), icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }), dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
@@ -81,23 +96,11 @@ const tree = new TreeDOM((item, depth) => {
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined, item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined )]); ], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined )]);
}, (item) => item.navigable); }, (item) => item.navigable);
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true)); (path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree?.toggle(tree.tree.search('path', e)[0], true));
const treeParent = useTemplateRef('treeParent');
const unmount = useRouter().afterEach((to, from, failure) => { treeParent.value!.replaceChildren(tree.container);
if(failure) })
return; }
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
});
watch(route, () => {
open.value = false;
});
onMounted(() => {
if(treeParent.value)
treeParent.value.appendChild(tree.container);
}) })
onUnmounted(() => { onUnmounted(() => {
unmount(); unmount();

View File

@@ -2,13 +2,9 @@ import { hasPermissions } from "#shared/auth.util";
export default defineNuxtRouteMiddleware(async (to, from) => { export default defineNuxtRouteMiddleware(async (to, from) => {
const { loggedIn, fetch, user } = useUserSession(); const { loggedIn, fetch, user } = useUserSession();
const { fetch: fetchContent } = useContent();
const meta = to.meta; const meta = to.meta;
if(await fetch()) await fetch()
{
fetchContent(true);
}
if(!!meta.guestsGoesTo && !loggedIn.value) if(!!meta.guestsGoesTo && !loggedIn.value)
{ {

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { CharacterBuilder } from '#shared/character.util'; import { CharacterBuilder } from '#shared/character.util';
import { unifySlug } from '~/shared/general.util'; import { unifySlug } from '#shared/general.util';
definePageMeta({ definePageMeta({
guestsGoesTo: '/user/login', guestsGoesTo: '/user/login',

View File

@@ -3,6 +3,7 @@ import characterConfig from '#shared/character-config.json';
import { unifySlug } from '#shared/general.util'; import { unifySlug } from '#shared/general.util';
import type { CharacterConfig } from '~/types/character'; import type { CharacterConfig } from '~/types/character';
import { CharacterSheet } from '#shared/character.util'; import { CharacterSheet } from '#shared/character.util';
/* /*
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
@@ -26,7 +27,7 @@ onMounted(() => {
if(container.value && id) if(container.value && id)
{ {
const character = new CharacterSheet(id, user); const character = new CharacterSheet(id, user);
container.value.replaceWith(character.container); container.value.appendChild(character.container);
} }
}); });
}); });

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { HomebrewBuilder } from '~/shared/feature.util'; import { HomebrewBuilder } from '#shared/feature.util';
definePageMeta({ definePageMeta({

View File

@@ -15,9 +15,9 @@ const route = useRouter().currentRoute;
const path = computed(() => unifySlug(route.value.params.path ?? '')); const path = computed(() => unifySlug(route.value.params.path ?? ''));
onMounted(async () => { onMounted(async () => {
if(element.value && path.value && await Content.ready) if(element.value && path.value)
{ {
overview.value = Content.render(element.value, path.value); overview.value = await Content.render(element.value, path.value);
} }
}); });
</script> </script>

View File

@@ -2,31 +2,43 @@
<Head> <Head>
<Title>d[any] - Modification</Title> <Title>d[any] - Modification</Title>
</Head> </Head>
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden"> <div class="flex flex-row w-full max-w-full h-full max-h-full xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3" style="--sidebar-width: 300px">
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2"> <div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
<div class="flex items-center px-2 gap-4"> <NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
<!-- <CollapsibleTrigger asChild>
<Button icon class="!bg-transparent group md:hidden">
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
</Button>
</CollapsibleTrigger> -->
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" /> <Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" /> <Avatar src="/logo.light.svg" class="block dark:hidden" />
<span class="text-xl max-md:hidden">d[any]</span> <span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
</NuxtLink> </NuxtLink>
</div>
<div class="flex items-center px-2 gap-4">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
</div>
</div>
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div> <div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60"> <div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink> <NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<p>Copyright Peaceultime - 2025</p> <NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
Copyright Peaceultime - 2025
</div>
</div>
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
<div class="flex flex-row gap-16 items-center">
<NavigationMenuRoot class="relative">
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
<NavigationMenuItem>
<NavigationMenuTrigger>
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
</NavigationMenuTrigger>
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<div class="absolute top-full left-0 flex w-full justify-center">
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
</div>
</NavigationMenuRoot>
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
</div>
<div class="flex flex-row gap-16 items-center">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
</div> </div>
</div> </div>
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div> <div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
@@ -40,6 +52,7 @@ import { button, loading } from '#shared/components.util';
import { dom, icon } from '#shared/dom.util'; import { dom, icon } from '#shared/dom.util';
import { modal, tooltip } from '#shared/floating.util'; import { modal, tooltip } from '#shared/floating.util';
import { Toaster } from '#shared/components.util'; import { Toaster } from '#shared/components.util';
import { Icon } from '@iconify/vue/dist/iconify.js';
definePageMeta({ definePageMeta({
rights: ['admin', 'editor'], rights: ['admin', 'editor'],
@@ -73,7 +86,7 @@ function push()
} }
onMounted(async () => { onMounted(async () => {
if(tree.value && container.value && await Content.ready) if(tree.value && container.value)
{ {
const load = loading('normal'); const load = loading('normal');
tree.value.appendChild(load); tree.value.appendChild(load);
@@ -87,7 +100,7 @@ onMounted(async () => {
editor = new Editor(); editor = new Editor();
tree.value.replaceChild(editor.tree.container, load); Content.ready.then(() => tree.value!.replaceChild(editor.tree.container, load));
container.value.appendChild(editor.container); container.value.appendChild(editor.container);
} }
}); });

View File

@@ -41,8 +41,6 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/login
ignoreResponseError: true, ignoreResponseError: true,
}) })
const toastMessage = ref('');
async function submit() async function submit()
{ {
if(state.usernameOrEmail === "") if(state.usernameOrEmail === "")
@@ -65,7 +63,7 @@ async function submit()
{ {
Toaster.clear(); Toaster.clear();
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' }); Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
await navigateTo('/user/profile'); useRouter().push({ name: 'user-profile' });
} }
} }
else else

View File

@@ -1,5 +1,5 @@
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { hasPermissions } from '~/shared/auth.util'; import { hasPermissions } from '#shared/auth.util';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const session = await getUserSession(e); const session = await getUserSession(e);

View File

@@ -1,8 +1,8 @@
import { eq, SQL, type Operators } from 'drizzle-orm'; import { eq, SQL, type Operators } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterTable, userPermissionsTable } from '~/db/schema'; import { characterTable, userPermissionsTable } from '~/db/schema';
import { hasPermissions } from '~/shared/auth.util'; import { hasPermissions } from '#shared/auth.util';
import { group } from '~/shared/general.util'; import { group } from '#shared/general.util';
import type { Character, MainStat, TrainingLevel } from '~/types/character'; import type { Character, MainStat, TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {

View File

@@ -1,6 +1,6 @@
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema'; import { characterTable } from '~/db/schema';
import { group } from '~/shared/general.util'; import { group } from '#shared/general.util';
import type { Character, CharacterVariables, Level, MainStat, TrainingLevel } from '~/types/character'; import type { Character, CharacterVariables, Level, MainStat, TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {

View File

@@ -1,7 +1,7 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema'; import { characterTable } from '~/db/schema';
import { CharacterVariablesValidation } from '~/shared/character.util'; import { CharacterVariablesValidation } from '#shared/character.util';
import type { CharacterVariables } from '~/types/character'; import type { CharacterVariables } from '~/types/character';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {

View File

@@ -15,8 +15,6 @@ export default defineEventHandler(async (e) => {
return; return;
} }
console.log(id);
setResponseStatus(e, 200); setResponseStatus(e, 200);
return {}; return {};
}); });

View File

@@ -15,9 +15,6 @@ export default defineEventHandler(async (e) => {
return; return;
} }
console.log(id);
console.log(await readBody(e));
setResponseStatus(e, 200); setResponseStatus(e, 200);
return; return;
}); });

View File

@@ -17,7 +17,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import Bun from 'bun'; import Bun from 'bun';
import { format } from '~/shared/general.util'; import { format } from '#shared/general.util';
const { id, userId, username, timestamp } = defineProps<{ const { id, userId, username, timestamp } = defineProps<{
id: number id: number

View File

@@ -91,7 +91,7 @@ export default defineTask({
function reshapeLinks(content: string | null, all: ProjectContent[]) function reshapeLinks(content: string | null, all: ProjectContent[])
{ {
return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => { return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => {
return `[[${link ? parsePath(all.find(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`; return `[[${link ? parsePath(all.findLast(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`;
}); });
} }

View File

@@ -482,8 +482,6 @@ export class Canvas
]), ]),
]), this.transform, ]), this.transform,
]); ]);
console.log(this.nodes.length, this.edges.length);
} }
protected computeLimits() protected computeLimits()

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,15 @@
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponType } from "~/types/character"; import type { Ability, Alignment, ArmorConfig, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, ItemConfig, ItemState, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponType } from "~/types/character";
import { z } from "zod/v4"; import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json'; import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses"; import proses, { preview } from "#shared/proses";
import { button, buttongroup, floater, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util"; import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, span, text } from "#shared/dom.util"; import { div, dom, icon, span, text } from "#shared/dom.util";
import { followermenu, fullblocker, tooltip } from "#shared/floating.util"; import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
import { clamp } from "#shared/general.util"; import { clamp } from "#shared/general.util";
import markdown from "#shared/markdown.util"; import markdown from "#shared/markdown.util";
import { getText } from "./i18n"; import { getText } from "#shared/i18n";
import type { User } from "~/types/auth"; import type { User } from "~/types/auth";
import { MarkdownEditor } from "./editor.util"; import { MarkdownEditor } from "#shared/editor.util";
const config = characterConfig as CharacterConfig; const config = characterConfig as CharacterConfig;
@@ -134,7 +134,6 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
aspect: "", aspect: "",
notes: Object.assign({ public: '', private: '' }, character.notes), notes: Object.assign({ public: '', private: '' }, character.notes),
}); });
export const mainStatTexts: Record<MainStat, string> = { export const mainStatTexts: Record<MainStat, string> = {
"strength": "Force", "strength": "Force",
"dexterity": "Dextérité", "dexterity": "Dextérité",
@@ -153,7 +152,6 @@ export const mainStatShortTexts: Record<MainStat, string> = {
"charisma": "CHA", "charisma": "CHA",
"psyche": "PSY", "psyche": "PSY",
}; };
export const elementTexts: Record<SpellElement, { class: string, text: string }> = { 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' }, 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' }, 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' },
@@ -169,7 +167,6 @@ export const elementDom = (element: SpellElement) => dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class], class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class],
text: elementTexts[element].text text: elementTexts[element].text
}); });
export const alignmentTexts: Record<Alignment, string> = { export const alignmentTexts: Record<Alignment, string> = {
'loyal_good': 'Loyal bon', 'loyal_good': 'Loyal bon',
'neutral_good': 'Neutre bon', 'neutral_good': 'Neutre bon',
@@ -182,7 +179,6 @@ export const alignmentTexts: Record<Alignment, string> = {
'chaotic_evil': 'Chaotique mauvais', 'chaotic_evil': 'Chaotique mauvais',
}; };
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" }; export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
export const abilityTexts: Record<Ability, string> = { export const abilityTexts: Record<Ability, string> = {
"athletics": "Athlétisme", "athletics": "Athlétisme",
"acrobatics": "Acrobatique", "acrobatics": "Acrobatique",
@@ -202,7 +198,6 @@ export const abilityTexts: Record<Ability, string> = {
"animalhandling": "Dressage", "animalhandling": "Dressage",
"deception": "Mensonge" "deception": "Mensonge"
}; };
export const resistanceTexts: Record<Resistance, string> = { export const resistanceTexts: Record<Resistance, string> = {
'stun': 'Hébètement', 'stun': 'Hébètement',
'bleed': 'Saignement', 'bleed': 'Saignement',
@@ -224,23 +219,19 @@ export const damageTypeTexts: Record<DamageType, string> = {
'slashing': 'Tranchant', 'slashing': 'Tranchant',
'thunder': 'Foudre', 'thunder': 'Foudre',
}; };
export const weaponTypeTexts: Record<WeaponType, string> = {
"light": "Arme légère",
"shield": "Bouclier",
"heavy": "Arme lourde",
"classic": "Arme",
"throw": "Arme de jet",
"natural": "Arme naturelle",
"twohanded": "Deux mains",
"finesse": "Arme maniable",
"reach": "Arme longue",
"projectile": "Arme à projectile",
};
export const CharacterNotesValidation = z.object({ export const CharacterNotesValidation = z.object({
public: z.string().optional(), public: z.string().optional(),
private: z.string().optional(), private: z.string().optional(),
}); });
export const ItemStateValidation = z.object({
id: z.string(),
amount: z.number().min(1),
enchantments: z.array(z.string()).optional(),
charges: z.number().optional(),
equipped: z.boolean().optional(),
state: z.any().optional(),
})
export const CharacterVariablesValidation = z.object({ export const CharacterVariablesValidation = z.object({
health: z.number(), health: z.number(),
mana: z.number(), mana: z.number(),
@@ -255,7 +246,9 @@ export const CharacterVariablesValidation = z.object({
state: z.number().min(1).max(7).or(z.literal(true)), state: z.number().min(1).max(7).or(z.literal(true)),
})), })),
spells: z.array(z.string()), spells: z.array(z.string()),
items: z.array(z.string()), items: z.array(ItemStateValidation),
money: z.number(),
}); });
export const CharacterValidation = z.object({ export const CharacterValidation = z.object({
id: z.number(), id: z.number(),
@@ -1229,6 +1222,65 @@ class AspectPicker extends BuilderTab
} }
} }
type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity'];
export const colorByRarity: Record<Rarity, string> = {
'common': 'text-light-100 dark:text-dark-100',
'uncommon': 'text-light-cyan dark:text-dark-cyan',
'rare': 'text-light-purple dark:text-dark-purple',
'legendary': 'text-light-orange dark:text-dark-orange'
}
export const weaponTypeTexts: Record<WeaponType, string> = {
"light": 'légère',
"shield": 'bouclier',
"heavy": 'lourde',
"classic": 'arme',
"throw": 'de jet',
"natural": 'naturelle',
"twohanded": 'à deux mains',
"finesse": 'maniable',
"reach": 'longue',
"projectile": 'à projectile',
}
export const armorTypeTexts: Record<ArmorConfig["type"], string> = {
'heavy': 'Armure lourde',
'light': 'Armure légère',
'medium': 'Armure',
}
export const categoryText: Record<Category, string> = {
'mundane': 'Objet',
'armor': 'Armure',
'weapon': 'Arme',
'wondrous': 'Objet magique'
};
export const rarityText: Record<Rarity, string> = {
'common': 'Commun',
'uncommon': 'Atypique',
'rare': 'Rare',
'legendary': 'Légendaire'
};
const subnameFactory = (item: ItemConfig, state?: ItemState): string[] => {
let result = [];
switch(item.category)
{
case 'armor':
result = [armorTypeTexts[(item as ArmorConfig).type]];
break;
case 'weapon':
result = ['Arme', ...(item as WeaponConfig).type.filter(e => e !== 'classic').map(e => weaponTypeTexts[e])];
break;
case 'mundane':
result = ['Objet'];
break;
case 'wondrous':
result = ['Objet magique'];
break;
}
if(state && state.enchantments !== undefined && state.enchantments.length > 0) result.push('Enchanté');
if(item.consummable) result.push('Consommable');
return result;
}
export class CharacterSheet export class CharacterSheet
{ {
user: ComputedRef<User | null>; user: ComputedRef<User | null>;
@@ -1283,6 +1335,38 @@ export class CharacterSheet
publicNotes.content = this.character!.character.notes!.public!; publicNotes.content = this.character!.character.notes!.public!;
privateNotes.content = this.character!.character.notes!.private!; privateNotes.content = this.character!.character.notes!.private!;
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: HTMLElement }) => {
const value = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10);
this.character?.variable(property, clamp(isNaN(value) ? character.variables[property] : value, 0, Infinity));
this.character?.saveVariables();
obj.edit.value = (character[property] - this.character!.character.variables[property]).toString();
obj.readonly.textContent = (character[property] - character.variables[property]).toString();
obj.edit.replaceWith(obj.readonly);
};
const health = {
readonly: dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: `${character.health - character.variables.health}`,
listeners: { click: () => health.readonly.replaceWith(health.edit) },
}),
edit: input('text', { defaultValue: (character.health - character.variables.health).toString(), input: (v) => {
return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10));
}, change: (v) => validateProperty(v, 'health', health), blur: () => validateProperty(health.edit.value, 'health', health), class: 'font-bold px-2 w-20 text-center' }),
};
const mana = {
readonly: dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: `${character.mana - character.variables.mana}`,
listeners: { click: () => mana.readonly.replaceWith(mana.edit) },
}),
edit: input('text', { defaultValue: (character.mana - character.variables.mana).toString(), input: (v) => {
return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10));
}, change: (v) => validateProperty(v, 'mana', mana), blur: () => validateProperty(mana.edit.value, 'mana', mana), class: 'font-bold px-2 w-20 text-center' }),
};
this.tabs = tabgroup([ this.tabs = tabgroup([
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) }, { id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
@@ -1290,9 +1374,7 @@ export class CharacterSheet
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) }, { id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [ { id: 'inventory', title: [ text('Inventaire') ], content: () => this.itemsTab(character) },
] },
{ id: 'notes', title: [ text('Notes') ], content: () => [ { id: 'notes', title: [ text('Notes') ], content: () => [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
@@ -1327,18 +1409,12 @@ export class CharacterSheet
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-4 ps-4 gap-8", [
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [ dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("PV: "), text("PV: "),
dom("span", { health.readonly,
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: `${character.health - character.variables.health}`
}),
text(`/ ${character.health}`) text(`/ ${character.health}`)
]), ]),
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [ dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("Mana: "), text("Mana: "),
dom("span", { mana.readonly,
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: `${character.mana - character.variables.mana}`
}),
text(`/ ${character.mana}`) text(`/ ${character.mana}`)
]) ])
]) ])
@@ -1488,7 +1564,7 @@ export class CharacterSheet
div('flex flex-col gap-8', [ div('flex flex-col gap-8', [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
]), ]),
@@ -1503,7 +1579,7 @@ export class CharacterSheet
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
]), ]),
@@ -1518,7 +1594,7 @@ export class CharacterSheet
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
]), ]),
@@ -1563,7 +1639,7 @@ export class CharacterSheet
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.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: `${e.spell.cost ?? 0} mana` }) ]), div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.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: `${e.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 justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]), div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]),
div('flex flex-row gap-2', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ]) div('flex flex-row gap-4 items-center', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.range === 'number' && e.spell.range > 0 ? `${e.spell.range} case${e.spell.range > 1 ? 's' : ''}` : e.spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
]), ]),
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]), div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]),
]) : undefined })); ]) : undefined }));
@@ -1576,14 +1652,14 @@ export class CharacterSheet
]), ]),
div('flex flex-row gap-2 items-center', [ div('flex flex-row gap-2 items-center', [
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }), dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
button(text('Modifier'), () => this.spellPanel(character, spells), 'py-1 px-4'), button(text('Modifier'), () => this.spellPanel(character), 'py-1 px-4'),
]) ])
]), ]),
div('flex flex-col gap-2', spells.map(e => e.dom)) div('flex flex-col gap-2', spells.map(e => e.dom))
]) ])
] ]
} }
spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>) spellPanel(character: CompiledCharacter)
{ {
const availableSpells = Object.values(config.spells).filter(spell => { const availableSpells = Object.values(config.spells).filter(spell => {
if (spell.rank === 4) return false; if (spell.rank === 4) return false;
@@ -1650,4 +1726,113 @@ export class CharacterSheet
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() }); const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
setTimeout(() => container.setAttribute('data-state', 'active'), 1); setTimeout(() => container.setAttribute('data-state', 'active'), 1);
} }
itemsTab(character: CompiledCharacter)
{
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;
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 }), () => {
const idx = this.character!.character.variables.items.findIndex(_e => _e.id === e.id);
if(idx === -1) return;
this.character!.character.variables.items[idx]!.amount--;
if(this.character!.character.variables.items[idx]!.amount >= 0) this.character!.character.variables.items.splice(idx, 1);
this.character!.variable('items', this.character!.character.variables.items);
debounceId && clearTimeout(debounceId);
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() ?? '-') ])
])
]));
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);
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-end items-center gap-8', [
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': weight > character.itempower }], text: `Poids total: ${weight}/${character.itempower}` }),
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)
])
]
}
itemsPanel(character: CompiledCharacter)
{
const items = Object.values(config.items).map(item => ({ item, dom: foldable(() => [ markdown(getText(item.description)) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [
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 flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
div('flex flex-row 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('', item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.weight?.toString() ?? '-') ]),
div('flex flex-row 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}` : '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]),
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
const list = [...this.character!.character.variables.items];
if(item.equippable) list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false });
else if(list.find(e => e.id === item.id)) this.character!.character.variables.items.find(e => e.id === item.id)!.amount++;
else list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] });
this.character!.variable('items', list); //TO REWORK
this.tabs?.refresh();
}, 'p-1 !border-solid !border-r'),
]),
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } }) }));
const filters: { category: Category[], rarity: Rarity[], name: string, power: { min: number, max: number } } = {
category: [],
rarity: [],
name: '',
power: { min: 0, max: Infinity },
};
const applyFilters = () => {
content.replaceChildren(...items.filter(e =>
(filters.category.length === 0 || filters.category.includes(e.item.category)) &&
(filters.rarity.length === 0 || filters.rarity.includes(e.item.rarity)) &&
(filters.name === '' || e.item.name.toLowerCase().includes(filters.name.toLowerCase()))
).map(e => e.dom));
}
const content = div('grid grid-cols-1 -my-2 overflow-y-auto gap-1');
const container = div("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-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
div("flex flex-row justify-between items-center mb-4", [
dom("h2", { class: "text-xl font-bold", text: "Gestion de l'inventaire" }),
div('flex flex-row gap-4 items-center', [ tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left") ])
]),
div('flex flex-row items-center gap-4', [
div('flex flex-row gap-2 items-center', [ text('Catégorie'), multiselect(Object.keys(categoryText).map(e => ({ text: categoryText[e as Category], value: e as Category })), { defaultValue: filters.category, change: v => { filters.category = v; applyFilters(); }, class: { container: 'w-40' } }) ]),
div('flex flex-row gap-2 items-center', [ text('Rareté'), multiselect(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: filters.rarity, change: v => { filters.rarity = v; applyFilters(); }, class: { container: 'w-40' } }) ]),
div('flex flex-row gap-2 items-center', [ text('Nom'), input('text', { defaultValue: filters.name, input: v => { filters.name = v; applyFilters(); }, class: 'w-64' }) ]),
]),
content,
]);
applyFilters();
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
}
} }

View File

@@ -1,6 +1,6 @@
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router"; import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util"; import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util"; import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "./floating.util";
import { clamp } from "./general.util"; import { clamp } from "./general.util";
import { Tree } from "./tree"; import { Tree } from "./tree";
import type { Placement } from "@floating-ui/dom"; import type { Placement } from "@floating-ui/dom";
@@ -368,17 +368,18 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
}) })
return container; return container;
} }
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void | boolean, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
{ {
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, 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 const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, 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 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`, settings?.class], listeners: { 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`, settings?.class], listeners: {
input: () => settings?.input && settings.input(input.value), input: (e) => { if(settings?.input && settings.input(input.value) === false) input.value = value; else value = input.value; },
change: () => settings?.change && settings.change(input.value), change: () => settings?.change && settings.change(input.value),
focus: () => settings?.focus, focus: settings?.focus,
blur: () => settings?.blur, blur: settings?.blur,
}}) }})
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue; if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
let value = input.value;
return input; return input;
} }
@@ -481,7 +482,7 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HT
let state = settings?.defaultValue ?? false; let state = settings?.defaultValue ?? false;
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20 const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
cursor-pointer hover:bg-light-30 dark:hover:bg-dark-30 hover:border-light-60 dark:hover:border-dark-60 cursor-pointer hover:bg-light-30 dark:hover:bg-dark-30 hover:border-light-60 dark:hover:border-dark-60
data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: { data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0 hover:data-[disabled]:bg-0 dark:hover:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
click: function(e: Event) { click: function(e: Event) {
if(this.hasAttribute('data-disabled')) if(this.hasAttribute('data-disabled'))
return; return;
@@ -526,16 +527,16 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
}) })
return container as HTMLDivElement & { refresh: () => void }; return container as HTMLDivElement & { refresh: () => void };
} }
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, position?: Placement, pinned?: boolean, 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?: { href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean, 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 })
{ {
let viewport = document.getElementById('mainContainer') ?? undefined; let viewport = document.getElementById('mainContainer') ?? undefined;
let diffX, diffY; let diffX, diffY;
let minimizeBox: DOMRect, minimized = false; let minimizeRect: DOMRect, minimized = false;
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({ const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
show: ['mouseenter', 'mousemove', 'focus'], show: ['mouseenter', 'mousemove', 'focus'],
hide: ['mouseleave', 'blur'], hide: ['mouseleave', 'blur'],
}, settings?.events ?? {}); } as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
if(settings?.pinned) if(settings?.pinned)
{ {
@@ -631,23 +632,30 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
floating.content.toggleAttribute('data-minimized', minimized); floating.content.toggleAttribute('data-minimized', minimized);
if(minimized) if(minimized)
{ {
minimizeBox = floating.content.getBoundingClientRect(); minimizeRect = floating.content.getBoundingClientRect();
Object.assign(floating.content.style, { Object.assign(floating.content.style, {
left: `0px`,
top: `initial`,
bottom: `0px`,
width: `150px`, width: `150px`,
height: `21px`, height: `21px`,
position: 'initial',
}); });
floating.content.style.setProperty('top', null);
floating.content.style.setProperty('left', null);
floating.content.style.setProperty('bottom', null);
floating.content.style.setProperty('right', null);
minimizeBox.appendChild(floating.content);
} }
else else
{ {
Object.assign(floating.content.style, { Object.assign(floating.content.style, {
left: `${minimizeBox.left}px`, left: `${minimizeRect.left}px`,
top: `${minimizeBox.top}px`, top: `${minimizeRect.top}px`,
width: `${minimizeBox.width}px`, width: `${minimizeRect.width}px`,
height: `${minimizeBox.height}px`, height: `${minimizeRect.height}px`,
}); });
floating.content.style.setProperty('position', null);
teleport.appendChild(floating.content);
} }
}; };
@@ -657,10 +665,11 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
offset: 12, offset: 12,
cover: settings?.cover, cover: settings?.cover,
placement: settings?.position, placement: settings?.position,
style: settings?.style,
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full', class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
content: () => [ content: () => [
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [ settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [
dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }), dom('span', { class: 'flex-1 w-full h-5 cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined, settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined,
settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined, settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined,
tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
@@ -669,10 +678,10 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
floating.content.toggleAttribute('data-minimized', false); floating.content.toggleAttribute('data-minimized', false);
minimized && Object.assign(floating.content.style, { minimized && Object.assign(floating.content.style, {
left: `${minimizeBox.left}px`, left: `${minimizeRect.left}px`,
top: `${minimizeBox.top}px`, top: `${minimizeRect.top}px`,
width: `${minimizeBox.width}px`, width: `${minimizeRect.width}px`,
height: `${minimizeBox.height}px`, height: `${minimizeRect.height}px`,
}); });
minimized = false; minimized = false;
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined, } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,

View File

@@ -3,7 +3,7 @@ import { Canvas, CanvasEditor } from "#shared/canvas.util";
import render from "#shared/markdown.util"; import render from "#shared/markdown.util";
import { confirm, contextmenu, tooltip } from "#shared/floating.util"; import { confirm, contextmenu, tooltip } from "#shared/floating.util";
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util"; import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
import { loading } from "#shared/components.util"; import { async, loading } from "#shared/components.util";
import prose, { h1, h2 } from "#shared/proses"; import prose, { h1, h2 } from "#shared/proses";
import { getID, parsePath } from '#shared/general.util'; import { getID, parsePath } from '#shared/general.util';
import { TreeDOM, type Recursive } from '#shared/tree'; import { TreeDOM, type Recursive } from '#shared/tree';
@@ -139,11 +139,12 @@ export class Content
try try
{ {
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview); Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
await Content.pull();
} }
catch(e) catch(e)
{ {
Content._overview = {}; Content._overview = {};
await Content.pull(); await Content.pull(true);
} }
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => { Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => {
p[v.path] = v.id; p[v.path] = v.id;
@@ -230,7 +231,7 @@ export class Content
return Content.queue.promise; return Content.queue.promise;
} }
static async pull() static async pull(force: boolean = false)
{ {
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined; const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined;
@@ -241,19 +242,16 @@ export class Content
return; return;
} }
const deletable = Object.keys(Content._overview);
for(const file of overview) for(const file of overview)
{ {
const _overview = Content._overview[file.id]; const _overview = Content._overview[file.id];
if(_overview && _overview.localEdit) if(force || !_overview || new Date(_overview.timestamp) < new Date(file.timestamp))
{
//TODO: Ask what to do about this file.
}
else
{ {
Content._overview[file.id] = file; Content._overview[file.id] = file;
Content.queue.queue(() => { Content.queue.queue(() => {
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined) => { return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined | null) => {
if(content) if(content)
{ {
if(file.type !== 'folder') if(file.type !== 'folder')
@@ -269,8 +267,13 @@ export class Content
}); });
}); });
} }
deletable.splice(deletable.findIndex(e => e === file.id), 1);
} }
for(const id of deletable)
Content.queue.queue(() => Content.remove(id).then(e => delete Content._overview[id]));
return Content.queue.queue(() => { return Content.queue.queue(() => {
return Content.write('overview', JSON.stringify(Content._overview), { create: true }); return Content.write('overview', JSON.stringify(Content._overview), { create: true });
}); });
@@ -296,18 +299,16 @@ export class Content
{ {
try try
{ {
console.time(`Reading '${path}'`);
const handle = await Content.root.getFileHandle(path, options); const handle = await Content.root.getFileHandle(path, options);
const file = await handle.getFile(); const file = await handle.getFile();
const text = await file.text(); //@ts-ignore
console.timeEnd(`Reading '${path}'`); const response = await new Response(file.stream().pipeThrough(new DecompressionStream('gzip')));
return text; return await response.text();
} }
catch(e) catch(e)
{ {
console.error(path, e); console.error(path, e);
console.timeEnd(`Reading '${path}'`);
} }
} }
private static async goto(path: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle | undefined> private static async goto(path: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle | undefined>
@@ -330,22 +331,35 @@ export class Content
//Easy to use, but not very performant. //Easy to use, but not very performant.
private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void> private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void>
{ {
const size = new TextEncoder().encode(content).byteLength;
console.time(`Writing ${size} bytes to '${path}'`);
try try
{ {
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/'); const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
const handle = await (await Content.goto(parent, { create: true }) ?? Content.root).getFileHandle(basename, options); const handle = await (await Content.goto(parent, { create: true }) ?? Content.root).getFileHandle(basename, options);
const file = await handle.createWritable({ keepExistingData: false }); const file = await handle.createWritable({ keepExistingData: false });
await file.write(content); await new ReadableStream({
await file.close(); start(controller) {
controller.enqueue(new TextEncoder().encode(content));
controller.close();
}
}).pipeThrough(new CompressionStream("gzip")).pipeTo(file);
}
catch(e)
{
console.error(path, e);
}
}
private static async remove(path: string): Promise<void>
{
try
{
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
return (await Content.goto(parent, { create: true }) ?? Content.root).removeEntry(basename);
} }
catch(e) catch(e)
{ {
console.error(path, e); console.error(path, e);
} }
console.timeEnd(`Writing ${size} bytes to '${path}'`);
} }
static get estimate(): Promise<StorageEstimate> static get estimate(): Promise<StorageEstimate>
@@ -363,26 +377,27 @@ export class Content
return handlers[overview.type].fromString(content); return handlers[overview.type].fromString(content);
} }
static render(parent: HTMLElement, path: string): Omit<LocalContent, 'content'> | undefined static async render(parent: HTMLElement, path: string): Promise<Omit<LocalContent, 'content'> | undefined>
{ {
parent.appendChild(dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]))
await Content.ready;
const overview = Content.getFromPath(path); const overview = Content.getFromPath(path);
if(!!overview) if(!!overview)
{ {
const load = dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]);
parent.appendChild(load);
function _render<T extends FileType>(content: LocalContent<T>): void function _render<T extends FileType>(content: LocalContent<T>): void
{ {
const el = handlers[content.type].render(content); const el = handlers[content.type].render(content);
el && parent.replaceChild(el, load); el && parent.replaceChildren(el);
} }
Content.getContent(overview.id).then(content => _render(content!)); Content.getContent(overview.id).then(content => _render(content!));
} }
else else
{ {
parent.appendChild(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" })); parent.replaceChildren(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" }));
} }
return overview; return overview;
@@ -456,7 +471,9 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
'cyan': '5', 'cyan': '5',
'purple': '6', 'purple': '6',
}; };
//@ts-ignore
content.edges?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined); content.edges?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
//@ts-ignore
content.nodes?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined); content.nodes?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
return JSON.stringify(content); return JSON.stringify(content);
@@ -502,7 +519,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
//TODO: Edition link //TODO: Edition link
]), ]),
]), ]),
render(content.content), render(content.content, undefined, { class: 'pb-64' }),
]) ])
}, },
renderEditor: (content) => { renderEditor: (content) => {
@@ -560,7 +577,7 @@ export const iconByType: Record<FileType, string> = {
export class Editor export class Editor
{ {
tree: TreeDOM; tree!: TreeDOM;
container: HTMLDivElement; container: HTMLDivElement;
selected?: Recursive<LocalContent & { element?: HTMLElement }>; selected?: Recursive<LocalContent & { element?: HTMLElement }>;
@@ -680,6 +697,7 @@ export class Editor
}, },
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); }); }, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
Content.ready.then(() => {
this.tree = new TreeDOM((item, depth) => { this.tree = new TreeDOM((item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [ return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }), icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
@@ -696,13 +714,14 @@ export class Editor
])]); ])]);
}); });
this.instruction = dom('div', { class: 'absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50' }); this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
this.cleanup = this.setupDnD(); this.cleanup = this.setupDnD();
});
this.instruction = dom('div', { class: 'absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50' });
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]); this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
} }
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>) private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
{ {

View File

@@ -1,14 +1,18 @@
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view'; import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
import { Annotation, EditorState, SelectionRange, type Range } from '@codemirror/state'; import { Annotation, EditorState, SelectionRange, StateField, type Range } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { bracketMatching, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language'; import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { search, searchKeymap } from '@codemirror/search'; import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'; import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { IterMode, Tree } from '@lezer/common'; import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
import { tags } from '@lezer/highlight'; import { tags } from '@lezer/highlight';
import { dom } from './dom.util'; import { dom } from '#shared/dom.util';
import { callout as calloutExtension } from '#shared/grammar/callout.extension';
import { wikilink as wikilinkExtension } from '#shared/grammar/wikilink.extension';
import { renderMarkdown } from '#shared/markdown.util';
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses";
import { tagTag, tag as tagExtension } from './grammar/tag.extension';
const External = Annotation.define<boolean>(); const External = Annotation.define<boolean>();
const Hidden = Decoration.mark({ class: 'hidden' }); const Hidden = Decoration.mark({ class: 'hidden' });
@@ -28,15 +32,88 @@ const highlight = HighlightStyle.define([
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' }, { tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' }, { tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' }, { tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
{ tag: tags.meta, color: "#404740" }, { tag: tags.meta, class: 'text-light-60 dark:text-dark-60' },
{ tag: tags.link, textDecoration: "underline" }, { tag: tags.link, class: 'text-accent-blue hover:underline' },
{ tag: tags.special(tags.link), class: 'text-accent-blue font-semibold' },
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" }, { tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
{ tag: tags.emphasis, fontStyle: "italic" }, { tag: tags.emphasis, fontStyle: "italic" },
{ tag: tags.strong, fontWeight: "bold" }, { tag: tags.strong, fontWeight: "bold" },
{ tag: tags.strikethrough, textDecoration: "line-through" }, { tag: tags.strikethrough, textDecoration: "line-through" },
{ tag: tags.keyword, color: "#708" }, { tag: tags.keyword, class: "text-accent-blue" },
{ tag: tags.monospace, class: "border border-light-35 dark:border-dark-35 px-2 py-px rounded-sm bg-light-20 dark:bg-dark-20" },
{ tag: tagTag, class: "cursor-default bg-accent-blue bg-opacity-10 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30" },
]); ]);
class CalloutWidget extends WidgetType
{
from: number;
to: number;
title: string;
type: string;
foldable?: boolean;
content: string;
contentMD: HTMLElement;
static create(node: SyntaxNodeRef, state: EditorState): CalloutWidget | undefined
{
let type = '';
let title = '';
const content: string[] = [];
let cursor = node.node.cursor();
if (!cursor.firstChild()) return undefined;
do
{
if (cursor.name === 'CalloutMarker')
{
const _cursor = cursor.node.cursor();
_cursor.lastChild();
type = state.doc.sliceString(_cursor.from, _cursor.to).toLowerCase();
}
else if (cursor.name === 'CalloutTitle')
{
title = state.doc.sliceString(cursor.from, cursor.to).trim();
}
else if (cursor.name === 'CalloutLine')
{
const _cursor = cursor.node.cursor();
_cursor.lastChild();
content.push(state.doc.sliceString(_cursor.from, _cursor.to));
}
}
while (cursor.nextSibling());
return new CalloutWidget(node.from, node.to, title || (type.substring(0, 1).toUpperCase() + type.substring(1).toLowerCase()), type, content.join('\n'));
}
constructor(from: number, to: number, title: string, type: string, content: string, foldable?: boolean)
{
super();
this.from = from;
this.to = to;
this.title = title;
this.type = type;
this.content = content;
this.foldable = foldable;
this.contentMD = renderMarkdown(useMarkdown().parseSync(content), { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th });
}
override eq(other: CalloutWidget)
{
return this.from === other.from && this.to === other.to;
}
toDOM(view: EditorView)
{
return dom('div', { class: 'flex cm-line', listeners: { click: e => view.dispatch({ selection: { anchor: this.from, head: this.to } }) } }, [prose('blockquote', callout, [ this.contentMD ], { title: this.title, type: this.type, fold: this.foldable, class: '!m-px ' }) as HTMLElement | undefined]);
}
override ignoreEvent(event: Event)
{
return false;
}
}
class Decorator class Decorator
{ {
static hiddenNodes: string[] = [ static hiddenNodes: string[] = [
@@ -46,11 +123,15 @@ class Decorator
'CodeMark', 'CodeMark',
'CodeInfo', 'CodeInfo',
'URL', 'URL',
'CalloutMark',
'WikilinkMeta',
'WikilinkHref',
'TagMeta'
] ]
decorations: DecorationSet; decorations: DecorationSet;
constructor(view: EditorView) constructor(view: EditorView)
{ {
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true); this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, [], view.state), true);
} }
update(update: ViewUpdate) update(update: ViewUpdate)
{ {
@@ -59,14 +140,14 @@ class Decorator
this.decorations = this.decorations.update({ this.decorations = this.decorations.update({
filter: (f, t, v) => false, filter: (f, t, v) => false,
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges), add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges, update.state),
sort: true, sort: true,
}); });
} }
iterate(tree: Tree, visible: readonly { iterate(tree: Tree, visible: readonly {
from: number; from: number;
to: number; to: number;
}[], selection: readonly SelectionRange[]): Range<Decoration>[] }[], selection: readonly SelectionRange[], state: EditorState): Range<Decoration>[]
{ {
const decorations: Range<Decoration>[] = []; const decorations: Range<Decoration>[] = [];
@@ -74,7 +155,7 @@ class Decorator
tree.iterate({ tree.iterate({
from, to, mode: IterMode.IgnoreMounts, from, to, mode: IterMode.IgnoreMounts,
enter: node => { enter: node => {
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!))) if(node.node.parent && node.node.parent.name !== 'Document' && selection.some(e => intersects(e, node.node.parent!)))
return true; return true;
else if(node.name === 'HeaderMark') else if(node.name === 'HeaderMark')
@@ -97,20 +178,56 @@ class Decorator
return decorations; return decorations;
} }
} }
function blockIterate(tree: Tree, state: EditorState): Range<Decoration>[]
{
const decorations: Range<Decoration>[] = [];
const selection = state.selection.ranges;
tree.iterate({
mode: IterMode.IgnoreMounts,
enter: node => {
if(selection.some(e => intersects(e, node)))
return true;
else if(node.name === 'CalloutBlock')
return decorations.push(Decoration.replace({ widget: CalloutWidget.create(node, state), block: true, }).range(node.from, node.to)), false;
return true;
},
});
return decorations;
}
const BlockDecorator = StateField.define<DecorationSet>({
create(state)
{
return Decoration.set(blockIterate(syntaxTree(state), state), true);
},
update(decorations, transaction)
{
if(transaction.docChanged || transaction.selection)
return Decoration.set(blockIterate(syntaxTree(transaction.state), transaction.state), true);
return decorations.map(transaction.changes);
},
provide: f => EditorView.decorations.from(f),
})
export class MarkdownEditor export class MarkdownEditor
{ {
private static _singleton: MarkdownEditor; private static _singleton: MarkdownEditor;
private view: EditorView; private view: EditorView;
private viewer: 'read' | 'live' | 'edit' = 'live';
onChange?: (content: string) => void; onChange?: (content: string) => void;
constructor() constructor()
{ {
this.view = new EditorView({ this.view = new EditorView({
extensions: [ extensions: [
markdown({ markdown({
base: markdownLanguage base: markdownLanguage,
extensions: [ calloutExtension, wikilinkExtension, tagExtension ]
}), }),
BlockDecorator,
history(), history(),
search(), search(),
dropCursor(), dropCursor(),
@@ -126,9 +243,7 @@ export class MarkdownEditor
...defaultKeymap, ...defaultKeymap,
...searchKeymap, ...searchKeymap,
...historyKeymap, ...historyKeymap,
...foldKeymap, ...completionKeymap
...completionKeymap,
...lintKeymap
]), ]),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => { EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External))) if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))

View File

@@ -4,7 +4,7 @@ import { MarkdownEditor } from "#shared/editor.util";
import { preview } from "#shared/proses"; import { preview } from "#shared/proses";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util"; import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util"; import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, weaponTypeTexts } from "#shared/character.util"; import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, categoryText, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, weaponTypeTexts } from "#shared/character.util";
import characterConfig from "#shared/character-config.json"; import characterConfig from "#shared/character-config.json";
import { getID } from "#shared/general.util"; import { getID } from "#shared/general.util";
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util"; import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
@@ -13,18 +13,6 @@ import { getText } from "#shared/i18n";
type Category = ItemConfig['category']; type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity']; type Rarity = ItemConfig['rarity'];
const categoryText: Record<Category, string> = {
'mundane': 'Objet inerte',
'armor': 'Armure',
'weapon': 'Arme',
'wondrous': 'Objet magique'
};
const rarityText: Record<Rarity, string> = {
'common': 'Commun',
'uncommon': 'Peu commun',
'rare': 'Rare',
'legendary': 'Légendaire'
};
const config = characterConfig as CharacterConfig; const config = characterConfig as CharacterConfig;
export class HomebrewBuilder export class HomebrewBuilder

View File

@@ -11,6 +11,7 @@ export interface FloatingProperties
style?: Record<string, string | undefined | boolean | number> | string; style?: Record<string, string | undefined | boolean | number> | string;
viewport?: HTMLElement; viewport?: HTMLElement;
cover?: 'width' | 'height' | 'all' | 'none'; cover?: 'width' | 'height' | 'all' | 'none';
persistant?: boolean;
} }
export interface FollowerProperties extends FloatingProperties export interface FollowerProperties extends FloatingProperties
{ {
@@ -36,17 +37,35 @@ export interface ModalProperties
closeWhenOutside?: boolean; closeWhenOutside?: boolean;
onClose?: () => boolean | void; onClose?: () => boolean | void;
} }
type ModalInternals = {
container: HTMLElement;
content: HTMLElement;
stop: Function;
start: Function;
show: Function;
hide: Function;
persistant: boolean;
};
let teleport: HTMLDivElement; export let teleport: HTMLDivElement, minimizeBox: HTMLDivElement, cache: ModalInternals[] = [];
export function init() export function init()
{ {
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' }); teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
minimizeBox = dom('div', { attributes: { id: 'minimize-container' }, class: 'absolute bottom-0 left-0 flex flex-row px-4 gap-4 z-40 h-[21px]' });
cache = [];
document.body.appendChild(teleport); document.body.appendChild(teleport);
document.body.appendChild(minimizeBox);
useRouter().afterEach(clear);
}
function clear()
{
cache = cache.filter(e => !(!e.persistant && e.content.remove()));
} }
export function popper(container: HTMLElement, properties?: PopperProperties) export function popper(container: HTMLElement, properties?: PopperProperties)
{ {
let state: FloatState = 'hidden', manualStop = false, timeout: Timer; let state: FloatState = 'hidden', timeout: Timer;
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]); const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
const content = dom('div', { class: properties?.class, style: properties?.style }); const content = dom('div', { class: properties?.class, style: properties?.style });
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]); const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
@@ -201,7 +220,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
link(container); link(container);
link(floater); link(floater);
return { container, content: floater, stop, start, show: () => { const result = { container, content: floater, stop, start, show: () => {
if(typeof properties?.content === 'function') if(typeof properties?.content === 'function')
properties.content = properties.content(); properties.content = properties.content();
@@ -228,11 +247,13 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
floater.setAttribute('data-state', 'closed'); floater.setAttribute('data-state', 'closed');
floater.classList.toggle('hidden', true); floater.classList.toggle('hidden', true);
manualStop = false;
floater.toggleAttribute('data-pinned', false); floater.toggleAttribute('data-pinned', false);
state = 'hidden'; state = 'hidden';
} }; } };
cache.push({ ...result, persistant: properties?.persistant ?? false });
return result;
} }
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties) export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
{ {

View File

@@ -56,7 +56,7 @@ export function format(date: Date, template: string): string
for(const key of keys) for(const key of keys)
{ {
template = template.replaceAll(key, () => transforms[key](date)); template = template.replaceAll(key, () => transforms[key]!(date));
} }
return template; return template;

View File

@@ -0,0 +1,57 @@
import type { MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
export const callout: MarkdownConfig = {
defineNodes: [
'CalloutBlock',
'CalloutMarker',
'CalloutMark',
'CalloutType',
'CalloutTitle',
'CalloutLine',
'CalloutContent',
],
parseBlock: [{
name: 'Callout',
before: 'Blockquote',
parse(cx, line) {
const match = /^>\s*\[!(\w+)\](?:\s+(.*))?/.exec(line.text);
if (!match || !match[1]) return false; //No match
const start = cx.lineStart, children = [];
const quoteEnd = start + line.text.indexOf('[!');
const typeStart = quoteEnd + 2;
const typeEnd = typeStart + match[1].length;
const bracketEnd = typeEnd + 1;
children.push(cx.elt('CalloutMarker', start, bracketEnd, [ cx.elt('CalloutMark', start, quoteEnd), cx.elt('CalloutType', typeStart, typeEnd) ]));
if(match[2]) children.push(cx.elt('CalloutTitle', bracketEnd + 1, start + line.text.length));
while (cx.nextLine() && line.text.startsWith('>'))
{
const pos = line.text.substring(1).search(/\S/) + 1;
children.push(cx.elt('CalloutLine', cx.lineStart, cx.lineStart + line.text.length, [
cx.elt('CalloutMark', cx.lineStart, cx.lineStart + pos),
cx.elt('CalloutContent', cx.lineStart + pos, cx.lineStart + line.text.length),
]))
}
cx.addElement(cx.elt('CalloutBlock', start, cx.lineStart - 1, children));
return true;
}
}],
props: [
styleTags({
'CalloutBlock': tags.special(tags.quote),
'CalloutMarker': tags.meta,
'CalloutMark': tags.meta,
'CalloutType': tags.keyword,
'CalloutTitle': tags.heading,
'CalloutLine': tags.content,
'CalloutContent': tags.content,
})
]
};

View File

@@ -0,0 +1,27 @@
import type { MarkdownConfig } from '@lezer/markdown';
import { styleTags, Tag, tags } from '@lezer/highlight';
export const tagTag = Tag.define('tag');
export const tag: MarkdownConfig = {
defineNodes: [
'Tag',
'TagMeta',
],
parseInline: [{
name: 'Tag',
parse(cx, next, pos)
{
//35 == '#'
if (cx.slice(pos, pos + 1).charCodeAt(0) !== 35 || String.fromCharCode(next).trim() === '') return -1;
const end = cx.slice(pos, cx.end).search(/\s/);
return cx.addElement(cx.elt('Tag', pos, end === -1 ? cx.end : end, [ cx.elt('TagMeta', pos, pos + 1) ]));
},
}],
props: [
styleTags({
'Tag': tagTag,
'TagMeta': tags.meta,
})
]
};

View File

@@ -0,0 +1,71 @@
import type { Element, MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
export const wikilink: MarkdownConfig = {
defineNodes: [
'Wikilink',
'WikilinkMeta',
'WikilinkHref',
'WikilinkTitle',
],
parseInline: [{
name: 'Wikilink',
before: 'Link',
parse(cx, next, pos)
{
// 91 == '['
if (next !== 91 || cx.slice(pos, pos + 1).charCodeAt(0) !== 91) return -1;
const match = /!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/.exec(cx.slice(pos, cx.end));
if(!match) return -1;
const start = pos, children: Element[] = [], end = start + match[0].length;
children.push(cx.elt('WikilinkMeta', start, start + 2));
if(match[1] && !match[2] && !match[3]) //Link only
{
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
}
else if(!match[1] && match[2] && match[3]) //Hash and title
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[2].length));
children.push(cx.elt('WikilinkMeta', start + 2 + match[2].length, start + 2 + match[2].length + 1));
children.push(cx.elt('WikilinkTitle', start + 2 + match[2].length + 1, start + 2 + match[2].length + match[3].length));
}
else if(!match[1] && !match[2] && match[3]) //Hash only
{
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
}
else if(match[1] && match[2] && !match[3]) //Link and hash
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length, start + 2 + match[1].length + match[2].length));
}
else if(match[1] && !match[2] && match[3]) //Link and title
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length, start + 2 + match[1].length + 1));
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + 1, start + 2 + match[1].length + match[3].length));
}
else if(match[1] && match[2] && match[3]) //Link, hash and title
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length + match[2].length));
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length + match[2].length, start + 2 + match[1].length + match[2].length + 1));
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + match[2].length + 1, start + 2 + match[1].length + match[2].length + match[3].length));
}
children.push(cx.elt('WikilinkMeta', end - 2, end));
return cx.addElement(cx.elt('Wikilink', start, end, children));
},
}],
props: [
styleTags({
'Wikilink': tags.special(tags.content),
'WikilinkMeta': tags.meta,
'WikilinkHref': tags.link,
'WikilinkTitle': tags.special(tags.link),
})
]
};

View File

@@ -64,7 +64,7 @@ export function markdownReference(content: string, filter?: string, properties?:
} }
} }
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags }); const el = renderMarkdown(data, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags));
if(properties) styling(el, properties); if(properties) styling(el, properties);

View File

@@ -109,7 +109,7 @@ export const callout: Prose = {
} = properties; } = properties;
let open = fold; let open = fold;
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 } }, [ 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', properties?.class], attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => { 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'); container.setAttribute('data-state', open ? 'open' : 'closed');
open = !open; open = !open;

View File

@@ -52,11 +52,13 @@ export type CharacterVariables = {
poisons: Array<{ id: string, state: number | true }>; poisons: Array<{ id: string, state: number | true }>;
spells: string[]; //Spell ID spells: string[]; //Spell ID
items: ItemState[]; items: ItemState[];
money: number;
}; };
type ItemState = { type ItemState = {
id: string; id: string;
amount: number; amount: number;
enchantments?: []; enchantments?: string[];
charges?: number; charges?: number;
equipped?: boolean; equipped?: boolean;
state?: any; state?: any;