You've already forked obsidian-visualiser
Compare commits
9 Commits
dev_fix
...
93eaa1e3e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93eaa1e3e4 | ||
|
|
62c1ccf0b4 | ||
| d208049989 | |||
|
|
6db6a4b19d | ||
|
|
fde752b6ed | ||
|
|
1c3211d28e | ||
|
|
ab36eec4de | ||
|
|
b9970ccdf8 | ||
|
|
73b0fdf3f5 |
59
app.vue
59
app.vue
@@ -33,6 +33,13 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
iconify-icon
|
||||
{
|
||||
display: inline-block;
|
||||
width: attr(width px, 1rem);
|
||||
height: attr(height px, 1rem);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.ToastRoot[data-type='error'] {
|
||||
@apply border-light-red;
|
||||
@apply dark:border-dark-red;
|
||||
@@ -184,6 +191,58 @@ onBeforeMount(() => {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
@apply max-w-[400px];
|
||||
@apply !bg-light-20;
|
||||
@apply dark:!bg-dark-20;
|
||||
@apply !border-light-40;
|
||||
@apply dark:!border-dark-40;
|
||||
}
|
||||
|
||||
/* .cm-tooltip-autocomplete > ul {
|
||||
@apply p-1;
|
||||
} */
|
||||
|
||||
.cm-tooltip-autocomplete > ul > li {
|
||||
@apply flex;
|
||||
@apply flex-col;
|
||||
@apply !py-1;
|
||||
@apply hover:bg-light-30;
|
||||
@apply dark:hover:bg-dark-30;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete > ul > li[aria-selected] {
|
||||
@apply !bg-light-35;
|
||||
@apply dark:!bg-dark-35;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply !hidden;
|
||||
}
|
||||
|
||||
.cm-completionLabel {
|
||||
@apply px-4;
|
||||
@apply font-sans;
|
||||
@apply font-normal;
|
||||
@apply text-base;
|
||||
@apply text-light-100;
|
||||
@apply dark:text-dark-100;
|
||||
}
|
||||
|
||||
.cm-completionMatchedText {
|
||||
@apply font-bold;
|
||||
@apply !no-underline;
|
||||
}
|
||||
|
||||
.cm-completionDetail {
|
||||
@apply font-sans;
|
||||
@apply font-normal;
|
||||
@apply text-sm;
|
||||
@apply text-light-60;
|
||||
@apply dark:text-dark-60;
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
@@ -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-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
44
db/schema.ts
44
db/schema.ts
@@ -8,19 +8,16 @@ export const usersTable = table("users", {
|
||||
hash: text().notNull().unique(),
|
||||
state: int().notNull().default(0),
|
||||
});
|
||||
|
||||
export const usersDataTable = table("users_data", {
|
||||
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const userSessionsTable = table("user_sessions", {
|
||||
id: int().notNull(),
|
||||
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
|
||||
|
||||
export const userPermissionsTable = table("user_permissions", {
|
||||
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
permission: text().notNull(),
|
||||
@@ -37,7 +34,6 @@ export const projectFilesTable = table("project_files", {
|
||||
order: int().notNull(),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const projectContentTable = table("project_content", {
|
||||
id: text().primaryKey(),
|
||||
content: blob({ mode: 'buffer' }),
|
||||
@@ -62,38 +58,51 @@ export const characterTable = table("character", {
|
||||
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
||||
thumbnail: blob(),
|
||||
});
|
||||
|
||||
export const characterTrainingTable = table("character_training", {
|
||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(),
|
||||
level: int().notNull(),
|
||||
choice: int().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
|
||||
|
||||
export const characterLevelingTable = table("character_leveling", {
|
||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
level: int().notNull(),
|
||||
choice: int().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
|
||||
|
||||
export const characterAbilitiesTable = table("character_abilities", {
|
||||
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(),
|
||||
value: int().notNull().default(0),
|
||||
max: int().notNull().default(0),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
|
||||
|
||||
export const characterChoicesTable = table("character_choices", {
|
||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
id: text().notNull(),
|
||||
choice: int().notNull(),
|
||||
}, (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 }) => ({
|
||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||
session: many(userSessionsTable),
|
||||
permission: many(userPermissionsTable),
|
||||
files: many(projectFilesTable),
|
||||
characters: many(characterTable),
|
||||
}));
|
||||
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||
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 }) => ({
|
||||
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
||||
}));
|
||||
|
||||
export const characterRelation = relations(characterTable, ({ one, many }) => ({
|
||||
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
|
||||
training: many(characterTrainingTable),
|
||||
levels: many(characterLevelingTable),
|
||||
abilities: many(characterAbilitiesTable),
|
||||
choices: many(characterChoicesTable)
|
||||
choices: many(characterChoicesTable),
|
||||
campaign: one(campaignCharactersTable, { fields: [characterTable.id], references: [campaignCharactersTable.character], }),
|
||||
}));
|
||||
|
||||
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
|
||||
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
|
||||
}));
|
||||
@@ -127,3 +137,17 @@ export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({
|
||||
export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
|
||||
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], })
|
||||
}))
|
||||
24
drizzle/0018_friendly_deadpool.sql
Normal file
24
drizzle/0018_friendly_deadpool.sql
Normal 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
|
||||
);
|
||||
886
drizzle/meta/0018_snapshot.json
Normal file
886
drizzle/meta/0018_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,13 @@
|
||||
"when": 1760531331328,
|
||||
"tag": "0017_workable_scrambler",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1761829250157,
|
||||
"tag": "0018_friendly_deadpool",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,10 @@
|
||||
<div class="text-3xl">Une erreur est survenue.</div>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -56,48 +56,51 @@ import { Content, iconByType } from '#shared/content.util';
|
||||
import { dom, icon } from '#shared/dom.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { tooltip } from '#shared/floating.util';
|
||||
import { link } from '#shared/components.util';
|
||||
import { link, loading } from '#shared/components.util';
|
||||
|
||||
const open = ref(false);
|
||||
let tree: TreeDOM | undefined;
|
||||
const { loggedIn, user } = useUserSession();
|
||||
const { fetch } = useContent();
|
||||
|
||||
await fetch(false);
|
||||
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
||||
|
||||
await Content.init();
|
||||
const 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 } }, [
|
||||
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 } }),
|
||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||
])]);
|
||||
}, (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` } }, [link([
|
||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
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 )]);
|
||||
}, (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));
|
||||
const treeParent = useTemplateRef('treeParent');
|
||||
|
||||
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));
|
||||
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.appendChild(tree.container);
|
||||
{
|
||||
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 } }, [
|
||||
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 } }),
|
||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||
])]);
|
||||
}, (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` } }, [link([
|
||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
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 )]);
|
||||
}, (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));
|
||||
|
||||
treeParent.value!.replaceChildren(tree.container);
|
||||
})
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
unmount();
|
||||
|
||||
@@ -2,13 +2,9 @@ import { hasPermissions } from "#shared/auth.util";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const { loggedIn, fetch, user } = useUserSession();
|
||||
const { fetch: fetchContent } = useContent();
|
||||
const meta = to.meta;
|
||||
|
||||
if(await fetch())
|
||||
{
|
||||
fetchContent(true);
|
||||
}
|
||||
await fetch()
|
||||
|
||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { CharacterBuilder } from '#shared/character.util';
|
||||
import { unifySlug } from '~/shared/general.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
|
||||
@@ -3,6 +3,7 @@ import characterConfig from '#shared/character-config.json';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
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-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)
|
||||
{
|
||||
const character = new CharacterSheet(id, user);
|
||||
container.value.replaceWith(character.container);
|
||||
container.value.appendChild(character.container);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { HomebrewBuilder } from '~/shared/feature.util';
|
||||
import { HomebrewBuilder } from '#shared/feature.util';
|
||||
|
||||
|
||||
definePageMeta({
|
||||
|
||||
@@ -15,9 +15,9 @@ const route = useRouter().currentRoute;
|
||||
const path = computed(() => unifySlug(route.value.params.path ?? ''));
|
||||
|
||||
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>
|
||||
@@ -2,31 +2,43 @@
|
||||
<Head>
|
||||
<Title>d[any] - Modification</Title>
|
||||
</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="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<!-- <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 }">
|
||||
<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="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">
|
||||
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block 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>
|
||||
</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="xl:px-12 px-6 pt-4 pb-2 text-center 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>
|
||||
<p>Copyright Peaceultime - 2025</p>
|
||||
<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" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<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 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 { modal, tooltip } from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
@@ -73,7 +86,7 @@ function push()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if(tree.value && container.value && await Content.ready)
|
||||
if(tree.value && container.value)
|
||||
{
|
||||
const load = loading('normal');
|
||||
tree.value.appendChild(load);
|
||||
@@ -87,7 +100,7 @@ onMounted(async () => {
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,8 +41,6 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/login
|
||||
ignoreResponseError: true,
|
||||
})
|
||||
|
||||
const toastMessage = ref('');
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(state.usernameOrEmail === "")
|
||||
@@ -65,7 +63,7 @@ async function submit()
|
||||
{
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
await navigateTo('/user/profile');
|
||||
useRouter().push({ name: 'user-profile' });
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { eq, SQL, type Operators } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
import { group } from '~/shared/general.util';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { group } from '#shared/general.util';
|
||||
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
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';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable } from '~/db/schema';
|
||||
import { CharacterVariablesValidation } from '~/shared/character.util';
|
||||
import { CharacterVariablesValidation } from '#shared/character.util';
|
||||
import type { CharacterVariables } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
||||
@@ -15,8 +15,6 @@ export default defineEventHandler(async (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(id);
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
return {};
|
||||
});
|
||||
@@ -15,9 +15,6 @@ export default defineEventHandler(async (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(id);
|
||||
console.log(await readBody(e));
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
return;
|
||||
});
|
||||
@@ -17,7 +17,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Bun from 'bun';
|
||||
import { format } from '~/shared/general.util';
|
||||
import { format } from '#shared/general.util';
|
||||
|
||||
const { id, userId, username, timestamp } = defineProps<{
|
||||
id: number
|
||||
|
||||
@@ -91,7 +91,7 @@ export default defineTask({
|
||||
function reshapeLinks(content: string | null, all: ProjectContent[])
|
||||
{
|
||||
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 ?? ''}]]`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -482,8 +482,6 @@ export class Canvas
|
||||
]),
|
||||
]), this.transform,
|
||||
]);
|
||||
|
||||
console.log(this.nodes.length, this.edges.length);
|
||||
}
|
||||
|
||||
protected computeLimits()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 characterConfig from '#shared/character-config.json';
|
||||
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 { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||
import { clamp } from "#shared/general.util";
|
||||
import markdown from "#shared/markdown.util";
|
||||
import { getText } from "./i18n";
|
||||
import { getText } from "#shared/i18n";
|
||||
import type { User } from "~/types/auth";
|
||||
import { MarkdownEditor } from "./editor.util";
|
||||
import { MarkdownEditor } from "#shared/editor.util";
|
||||
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
@@ -134,7 +134,6 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
||||
aspect: "",
|
||||
notes: Object.assign({ public: '', private: '' }, character.notes),
|
||||
});
|
||||
|
||||
export const mainStatTexts: Record<MainStat, string> = {
|
||||
"strength": "Force",
|
||||
"dexterity": "Dextérité",
|
||||
@@ -153,7 +152,6 @@ export const mainStatShortTexts: Record<MainStat, string> = {
|
||||
"charisma": "CHA",
|
||||
"psyche": "PSY",
|
||||
};
|
||||
|
||||
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
|
||||
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
|
||||
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
|
||||
@@ -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],
|
||||
text: elementTexts[element].text
|
||||
});
|
||||
|
||||
export const alignmentTexts: Record<Alignment, string> = {
|
||||
'loyal_good': 'Loyal bon',
|
||||
'neutral_good': 'Neutre bon',
|
||||
@@ -182,7 +179,6 @@ export const alignmentTexts: Record<Alignment, string> = {
|
||||
'chaotic_evil': 'Chaotique mauvais',
|
||||
};
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
|
||||
export const abilityTexts: Record<Ability, string> = {
|
||||
"athletics": "Athlétisme",
|
||||
"acrobatics": "Acrobatique",
|
||||
@@ -202,7 +198,6 @@ export const abilityTexts: Record<Ability, string> = {
|
||||
"animalhandling": "Dressage",
|
||||
"deception": "Mensonge"
|
||||
};
|
||||
|
||||
export const resistanceTexts: Record<Resistance, string> = {
|
||||
'stun': 'Hébètement',
|
||||
'bleed': 'Saignement',
|
||||
@@ -224,23 +219,19 @@ export const damageTypeTexts: Record<DamageType, string> = {
|
||||
'slashing': 'Tranchant',
|
||||
'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({
|
||||
public: 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({
|
||||
health: 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)),
|
||||
})),
|
||||
spells: z.array(z.string()),
|
||||
items: z.array(z.string()),
|
||||
items: z.array(ItemStateValidation),
|
||||
|
||||
money: z.number(),
|
||||
});
|
||||
export const CharacterValidation = z.object({
|
||||
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
|
||||
{
|
||||
user: ComputedRef<User | null>;
|
||||
@@ -1283,6 +1335,38 @@ export class CharacterSheet
|
||||
publicNotes.content = this.character!.character.notes!.public!;
|
||||
privateNotes.content = this.character!.character.notes!.private!;
|
||||
|
||||
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: 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([
|
||||
{ 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: 'inventory', title: [ text('Inventaire') ], content: () => [
|
||||
|
||||
] },
|
||||
{ id: 'inventory', title: [ text('Inventaire') ], content: () => this.itemsTab(character) },
|
||||
|
||||
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
||||
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", [
|
||||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||
text("PV: "),
|
||||
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}`
|
||||
}),
|
||||
health.readonly,
|
||||
text(`/ ${character.health}`)
|
||||
]),
|
||||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||
text("Mana: "),
|
||||
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}`
|
||||
}),
|
||||
mana.readonly,
|
||||
text(`/ ${character.mana}`)
|
||||
])
|
||||
])
|
||||
@@ -1488,7 +1564,7 @@ export class CharacterSheet
|
||||
div('flex flex-col gap-8', [
|
||||
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-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-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-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-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-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"),
|
||||
]),
|
||||
|
||||
@@ -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 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', [ 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) ]),
|
||||
]) : undefined }));
|
||||
@@ -1576,14 +1652,14 @@ export class CharacterSheet
|
||||
]),
|
||||
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' : ''}` }),
|
||||
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))
|
||||
])
|
||||
]
|
||||
}
|
||||
spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>)
|
||||
spellPanel(character: CompiledCharacter)
|
||||
{
|
||||
const availableSpells = Object.values(config.spells).filter(spell => {
|
||||
if (spell.rank === 4) return false;
|
||||
@@ -1650,4 +1726,113 @@ export class CharacterSheet
|
||||
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { 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 { Tree } from "./tree";
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
@@ -368,17 +368,18 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||
})
|
||||
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
|
||||
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: {
|
||||
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),
|
||||
focus: () => settings?.focus,
|
||||
blur: () => settings?.blur,
|
||||
focus: settings?.focus,
|
||||
blur: settings?.blur,
|
||||
}})
|
||||
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
||||
let value = input.value;
|
||||
|
||||
return input;
|
||||
}
|
||||
@@ -481,7 +482,7 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HT
|
||||
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
|
||||
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) {
|
||||
if(this.hasAttribute('data-disabled'))
|
||||
return;
|
||||
@@ -526,16 +527,16 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
|
||||
})
|
||||
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 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({
|
||||
show: ['mouseenter', 'mousemove', 'focus'],
|
||||
hide: ['mouseleave', 'blur'],
|
||||
}, settings?.events ?? {});
|
||||
} as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
|
||||
|
||||
if(settings?.pinned)
|
||||
{
|
||||
@@ -631,23 +632,30 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
||||
floating.content.toggleAttribute('data-minimized', minimized);
|
||||
if(minimized)
|
||||
{
|
||||
minimizeBox = floating.content.getBoundingClientRect();
|
||||
minimizeRect = floating.content.getBoundingClientRect();
|
||||
Object.assign(floating.content.style, {
|
||||
left: `0px`,
|
||||
top: `initial`,
|
||||
bottom: `0px`,
|
||||
width: `150px`,
|
||||
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
|
||||
{
|
||||
Object.assign(floating.content.style, {
|
||||
left: `${minimizeBox.left}px`,
|
||||
top: `${minimizeBox.top}px`,
|
||||
width: `${minimizeBox.width}px`,
|
||||
height: `${minimizeBox.height}px`,
|
||||
left: `${minimizeRect.left}px`,
|
||||
top: `${minimizeRect.top}px`,
|
||||
width: `${minimizeRect.width}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,
|
||||
cover: settings?.cover,
|
||||
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',
|
||||
content: () => [
|
||||
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?.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) => {
|
||||
@@ -669,10 +678,10 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
||||
|
||||
floating.content.toggleAttribute('data-minimized', false);
|
||||
minimized && Object.assign(floating.content.style, {
|
||||
left: `${minimizeBox.left}px`,
|
||||
top: `${minimizeBox.top}px`,
|
||||
width: `${minimizeBox.width}px`,
|
||||
height: `${minimizeBox.height}px`,
|
||||
left: `${minimizeRect.left}px`,
|
||||
top: `${minimizeRect.top}px`,
|
||||
width: `${minimizeRect.width}px`,
|
||||
height: `${minimizeRect.height}px`,
|
||||
});
|
||||
minimized = false;
|
||||
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Canvas, CanvasEditor } from "#shared/canvas.util";
|
||||
import render from "#shared/markdown.util";
|
||||
import { confirm, contextmenu, tooltip } from "#shared/floating.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 { getID, parsePath } from '#shared/general.util';
|
||||
import { TreeDOM, type Recursive } from '#shared/tree';
|
||||
@@ -116,7 +116,7 @@ export class Content
|
||||
private static root: FileSystemDirectoryHandle;
|
||||
|
||||
private static _overview: Record<string, Omit<LocalContent, 'content'>>;
|
||||
private static _reverseMapping: Record<string, string>;
|
||||
private static _reverseMapping: Record<string, string> = {};
|
||||
private static queue = new AsyncQueue();
|
||||
|
||||
static init(): Promise<boolean>
|
||||
@@ -139,16 +139,13 @@ export class Content
|
||||
try
|
||||
{
|
||||
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
|
||||
await Content.pull();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Content._overview = {};
|
||||
await Content.pull();
|
||||
await Content.pull(true);
|
||||
}
|
||||
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => {
|
||||
p[v.path] = v.id;
|
||||
return p;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
Content._ready = true;
|
||||
}
|
||||
@@ -230,7 +227,7 @@ export class Content
|
||||
|
||||
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;
|
||||
|
||||
@@ -241,19 +238,16 @@ export class Content
|
||||
return;
|
||||
}
|
||||
|
||||
const deletable = Object.keys(Content._overview);
|
||||
for(const file of overview)
|
||||
{
|
||||
const _overview = Content._overview[file.id];
|
||||
if(_overview && _overview.localEdit)
|
||||
{
|
||||
//TODO: Ask what to do about this file.
|
||||
}
|
||||
else
|
||||
if(force || !_overview || new Date(_overview.timestamp) < new Date(file.timestamp))
|
||||
{
|
||||
Content._overview[file.id] = file;
|
||||
|
||||
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(file.type !== 'folder')
|
||||
@@ -269,6 +263,16 @@ 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._reverseMapping[Content._overview[id]!.path];
|
||||
delete Content._overview[id];
|
||||
}));
|
||||
}
|
||||
|
||||
return Content.queue.queue(() => {
|
||||
@@ -296,18 +300,16 @@ export class Content
|
||||
{
|
||||
try
|
||||
{
|
||||
console.time(`Reading '${path}'`);
|
||||
const handle = await Content.root.getFileHandle(path, options);
|
||||
const file = await handle.getFile();
|
||||
|
||||
const text = await file.text();
|
||||
console.timeEnd(`Reading '${path}'`);
|
||||
return text;
|
||||
//@ts-ignore
|
||||
const response = await new Response(file.stream().pipeThrough(new DecompressionStream('gzip')));
|
||||
return await response.text();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error(path, e);
|
||||
console.timeEnd(`Reading '${path}'`);
|
||||
}
|
||||
}
|
||||
private static async goto(path: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle | undefined>
|
||||
@@ -330,22 +332,35 @@ export class Content
|
||||
//Easy to use, but not very performant.
|
||||
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
|
||||
{
|
||||
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 file = await handle.createWritable({ keepExistingData: false });
|
||||
|
||||
await file.write(content);
|
||||
await file.close();
|
||||
await new ReadableStream({
|
||||
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)
|
||||
{
|
||||
console.error(path, e);
|
||||
}
|
||||
console.timeEnd(`Writing ${size} bytes to '${path}'`);
|
||||
}
|
||||
|
||||
static get estimate(): Promise<StorageEstimate>
|
||||
@@ -363,26 +378,27 @@ export class 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);
|
||||
|
||||
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
|
||||
{
|
||||
const el = handlers[content.type].render(content);
|
||||
el && parent.replaceChild(el, load);
|
||||
el && parent.replaceChildren(el);
|
||||
}
|
||||
|
||||
Content.getContent(overview.id).then(content => _render(content!));
|
||||
}
|
||||
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;
|
||||
@@ -456,7 +472,9 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
||||
'cyan': '5',
|
||||
'purple': '6',
|
||||
};
|
||||
//@ts-ignore
|
||||
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);
|
||||
|
||||
return JSON.stringify(content);
|
||||
@@ -502,7 +520,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
||||
//TODO: Edition link
|
||||
]),
|
||||
]),
|
||||
render(content.content),
|
||||
render(content.content, undefined, { class: 'pb-64' }),
|
||||
])
|
||||
},
|
||||
renderEditor: (content) => {
|
||||
@@ -560,7 +578,7 @@ export const iconByType: Record<FileType, string> = {
|
||||
|
||||
export class Editor
|
||||
{
|
||||
tree: TreeDOM;
|
||||
tree!: TreeDOM;
|
||||
container: HTMLDivElement;
|
||||
|
||||
selected?: Recursive<LocalContent & { element?: HTMLElement }>;
|
||||
@@ -680,6 +698,7 @@ export class Editor
|
||||
},
|
||||
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
|
||||
|
||||
Content.ready.then(() => {
|
||||
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)} }, [
|
||||
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 +715,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.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.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
|
||||
}
|
||||
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
|
||||
{
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
|
||||
import { Annotation, EditorState, SelectionRange, type Range } from '@codemirror/state';
|
||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
||||
import { Annotation, EditorState, SelectionRange, StateField, type Range } from '@codemirror/state';
|
||||
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 { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||
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 { dom } from './dom.util';
|
||||
import { dom } from '#shared/dom.util';
|
||||
import { callout as calloutExtension } from '#shared/grammar/callout.extension';
|
||||
import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } 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 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.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.meta, color: "#404740" },
|
||||
{ tag: tags.link, textDecoration: "underline" },
|
||||
{ tag: tags.meta, class: 'text-light-60 dark:text-dark-60' },
|
||||
{ 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.emphasis, fontStyle: "italic" },
|
||||
{ tag: tags.strong, fontWeight: "bold" },
|
||||
{ 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
|
||||
{
|
||||
static hiddenNodes: string[] = [
|
||||
@@ -46,11 +123,15 @@ class Decorator
|
||||
'CodeMark',
|
||||
'CodeInfo',
|
||||
'URL',
|
||||
'CalloutMark',
|
||||
'WikilinkMeta',
|
||||
'WikilinkHref',
|
||||
'TagMeta'
|
||||
]
|
||||
decorations: DecorationSet;
|
||||
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)
|
||||
{
|
||||
@@ -59,14 +140,14 @@ class Decorator
|
||||
|
||||
this.decorations = this.decorations.update({
|
||||
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,
|
||||
});
|
||||
}
|
||||
iterate(tree: Tree, visible: readonly {
|
||||
from: number;
|
||||
to: number;
|
||||
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
|
||||
}[], selection: readonly SelectionRange[], state: EditorState): Range<Decoration>[]
|
||||
{
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
|
||||
@@ -74,7 +155,7 @@ class Decorator
|
||||
tree.iterate({
|
||||
from, to, mode: IterMode.IgnoreMounts,
|
||||
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;
|
||||
|
||||
else if(node.name === 'HeaderMark')
|
||||
@@ -97,20 +178,56 @@ class Decorator
|
||||
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
|
||||
{
|
||||
private static _singleton: MarkdownEditor;
|
||||
|
||||
private view: EditorView;
|
||||
private viewer: 'read' | 'live' | 'edit' = 'live';
|
||||
onChange?: (content: string) => void;
|
||||
constructor()
|
||||
{
|
||||
this.view = new EditorView({
|
||||
extensions: [
|
||||
markdown({
|
||||
base: markdownLanguage
|
||||
base: markdownLanguage,
|
||||
extensions: [ calloutExtension, wikilinkExtension, tagExtension ]
|
||||
}),
|
||||
BlockDecorator,
|
||||
history(),
|
||||
search(),
|
||||
dropCursor(),
|
||||
@@ -119,16 +236,21 @@ export class MarkdownEditor
|
||||
syntaxHighlighting(highlight),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion({
|
||||
icons: false,
|
||||
defaultKeymap: true,
|
||||
maxRenderedOptions: 10,
|
||||
activateOnTyping: true,
|
||||
override: [ wikilinkAutocompletion ]
|
||||
}),
|
||||
crosshairCursor(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
]),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MarkdownEditor } from "#shared/editor.util";
|
||||
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 { 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 { getID } from "#shared/general.util";
|
||||
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
|
||||
@@ -13,18 +13,6 @@ import { getText } from "#shared/i18n";
|
||||
|
||||
type Category = ItemConfig['category'];
|
||||
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;
|
||||
export class HomebrewBuilder
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface FloatingProperties
|
||||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
viewport?: HTMLElement;
|
||||
cover?: 'width' | 'height' | 'all' | 'none';
|
||||
persistant?: boolean;
|
||||
}
|
||||
export interface FollowerProperties extends FloatingProperties
|
||||
{
|
||||
@@ -36,17 +37,35 @@ export interface ModalProperties
|
||||
closeWhenOutside?: boolean;
|
||||
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()
|
||||
{
|
||||
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(minimizeBox);
|
||||
|
||||
useRouter().afterEach(clear);
|
||||
}
|
||||
function clear()
|
||||
{
|
||||
cache = cache.filter(e => !(!e.persistant && e.content.remove()));
|
||||
}
|
||||
|
||||
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 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 ]);
|
||||
@@ -201,7 +220,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
|
||||
link(container);
|
||||
link(floater);
|
||||
|
||||
return { container, content: floater, stop, start, show: () => {
|
||||
const result = { container, content: floater, stop, start, show: () => {
|
||||
if(typeof properties?.content === 'function')
|
||||
properties.content = properties.content();
|
||||
|
||||
@@ -228,11 +247,13 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
|
||||
floater.setAttribute('data-state', 'closed');
|
||||
floater.classList.toggle('hidden', true);
|
||||
|
||||
manualStop = false;
|
||||
floater.toggleAttribute('data-pinned', false);
|
||||
|
||||
state = 'hidden';
|
||||
} };
|
||||
|
||||
cache.push({ ...result, persistant: properties?.persistant ?? false });
|
||||
return result;
|
||||
}
|
||||
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ export function format(date: Date, template: string): string
|
||||
|
||||
for(const key of keys)
|
||||
{
|
||||
template = template.replaceAll(key, () => transforms[key](date));
|
||||
template = template.replaceAll(key, () => transforms[key]!(date));
|
||||
}
|
||||
|
||||
return template;
|
||||
|
||||
57
shared/grammar/callout.extension.ts
Normal file
57
shared/grammar/callout.extension.ts
Normal 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,
|
||||
})
|
||||
]
|
||||
};
|
||||
27
shared/grammar/tag.extension.ts
Normal file
27
shared/grammar/tag.extension.ts
Normal 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,
|
||||
})
|
||||
]
|
||||
};
|
||||
120
shared/grammar/wikilink.extension.ts
Normal file
120
shared/grammar/wikilink.extension.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import type { Element, MarkdownConfig } from '@lezer/markdown';
|
||||
import { styleTags, tags } from '@lezer/highlight';
|
||||
import { Content } from '../content.util';
|
||||
|
||||
function fuzzyMatch(text: string, search: string): number {
|
||||
const textLower = text.toLowerCase().normalize('NFC');
|
||||
const searchLower = search.toLowerCase().normalize('NFC');
|
||||
|
||||
let searchIndex = 0;
|
||||
let score = 0;
|
||||
|
||||
for (let i = 0; i < textLower.length && searchIndex < searchLower.length; i++) {
|
||||
if (textLower[i] === searchLower[searchIndex]) {
|
||||
score += 1;
|
||||
if (i === searchIndex) score += 2; // Bonus for sequential match
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return searchIndex === searchLower.length ? score : 0;
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
]
|
||||
};
|
||||
export const autocompletion = (context: CompletionContext): CompletionResult | null => {
|
||||
const word = context.matchBefore(/\[\[[\w\s-]*/);
|
||||
if (!word || (word.from === word.to && !context.explicit))
|
||||
return null;
|
||||
|
||||
const searchTerm = word.text.slice(2).toLowerCase();
|
||||
|
||||
const options = Object.values(Content.files).filter(e => e.type !== 'folder').map(e => ({ ...e, score: fuzzyMatch(e.title, searchTerm) })).filter(e => e.score > 0).sort((a, b) => b.score - a.score).slice(0, 50);
|
||||
|
||||
return {
|
||||
from: word.from + 2,
|
||||
options: options.map(e => ({
|
||||
label: e.title,
|
||||
detail: e.path,
|
||||
apply: (view, completion, from, to) => {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: word.from,
|
||||
to: word.to,
|
||||
insert: `[[${e.path}]]`
|
||||
},
|
||||
selection: { anchor: word.from + e.path.length + 2 }
|
||||
});
|
||||
},
|
||||
type: 'text'
|
||||
})),
|
||||
validFor: /^[\[\w\s-]*$/,
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export const callout: Prose = {
|
||||
} = properties;
|
||||
|
||||
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 => {
|
||||
container.setAttribute('data-state', open ? 'open' : 'closed');
|
||||
open = !open;
|
||||
|
||||
4
types/character.d.ts
vendored
4
types/character.d.ts
vendored
@@ -52,11 +52,13 @@ export type CharacterVariables = {
|
||||
poisons: Array<{ id: string, state: number | true }>;
|
||||
spells: string[]; //Spell ID
|
||||
items: ItemState[];
|
||||
|
||||
money: number;
|
||||
};
|
||||
type ItemState = {
|
||||
id: string;
|
||||
amount: number;
|
||||
enchantments?: [];
|
||||
enchantments?: string[];
|
||||
charges?: number;
|
||||
equipped?: boolean;
|
||||
state?: any;
|
||||
|
||||
Reference in New Issue
Block a user