You've already forked obsidian-visualiser
Compare commits
19 Commits
1642cd513f
...
dev_fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25bd165f1d | ||
|
|
5c1f41b0b7 | ||
| feb2fb56c6 | |||
|
|
df9ae95890 | ||
|
|
72843f2425 | ||
|
|
443612cc58 | ||
|
|
a577e3ccfc | ||
|
|
48e767944a | ||
| d187957915 | |||
|
|
16cc3ee438 | ||
|
|
26aa0847d9 | ||
| b19d2d1b41 | |||
|
|
89c4476ffb | ||
|
|
3113d8b0f3 | ||
| 2b39f26722 | |||
| d2a807694b | |||
|
|
eb0c33deae | ||
|
|
61d2d144b7 | ||
| 81f191d5f6 |
12
app.vue
12
app.vue
@@ -1,13 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||||
<NuxtRouteAnnouncer/>
|
<NuxtRouteAnnouncer/>
|
||||||
<TooltipProvider>
|
<NuxtLayout>
|
||||||
<NuxtLayout>
|
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
|
||||||
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
|
<NuxtPage />
|
||||||
<NuxtPage />
|
</div>
|
||||||
</div>
|
</NuxtLayout>
|
||||||
</NuxtLayout>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span ref="container"></span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseURL } from 'ufo';
|
|
||||||
import proses, { preview } from '#shared/proses';
|
|
||||||
import { text } from '#shared/dom.util';
|
|
||||||
|
|
||||||
const { href, label } = defineProps<{
|
|
||||||
href: string,
|
|
||||||
label: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const container = useTemplateRef('container');
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
queueMicrotask(() => {
|
|
||||||
container.value && container.value.appendChild(proses('a', preview, [ text(label) ], { href }) as HTMLElement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -2,7 +2,9 @@ import { Database } from "bun:sqlite";
|
|||||||
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
import * as schema from '../db/schema';
|
import * as schema from '../db/schema';
|
||||||
|
|
||||||
let instance: BunSQLiteDatabase<typeof schema>;
|
let instance: BunSQLiteDatabase<typeof schema> & {
|
||||||
|
$client: Database;
|
||||||
|
};
|
||||||
export default function useDatabase()
|
export default function useDatabase()
|
||||||
{
|
{
|
||||||
if(!instance)
|
if(!instance)
|
||||||
@@ -13,6 +15,7 @@ export default function useDatabase()
|
|||||||
|
|
||||||
instance.run("PRAGMA journal_mode = WAL;");
|
instance.run("PRAGMA journal_mode = WAL;");
|
||||||
instance.run("PRAGMA foreign_keys = true;");
|
instance.run("PRAGMA foreign_keys = true;");
|
||||||
|
instance.run("PRAGMA optimize=0x10002;");
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
@@ -1,6 +1,5 @@
|
|||||||
import { relations } from 'drizzle-orm';
|
import { relations } from 'drizzle-orm';
|
||||||
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||||
import { ABILITIES, MAIN_STATS } from '~/shared/character.util';
|
|
||||||
|
|
||||||
export const usersTable = table("users", {
|
export const usersTable = table("users", {
|
||||||
id: int().primaryKey({ autoIncrement: true }),
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
@@ -55,9 +54,10 @@ export const characterTable = table("character", {
|
|||||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
people: text().notNull(),
|
people: text().notNull(),
|
||||||
level: int().notNull().default(1),
|
level: int().notNull().default(1),
|
||||||
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": []}'),
|
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}'),
|
||||||
aspect: int(),
|
aspect: int(),
|
||||||
notes: text(),
|
public_notes: text(),
|
||||||
|
private_notes: text(),
|
||||||
|
|
||||||
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
||||||
thumbnail: blob(),
|
thumbnail: blob(),
|
||||||
|
|||||||
20
drizzle/0017_workable_scrambler.sql
Normal file
20
drizzle/0017_workable_scrambler.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_character` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`owner` integer NOT NULL,
|
||||||
|
`people` text NOT NULL,
|
||||||
|
`level` integer DEFAULT 1 NOT NULL,
|
||||||
|
`variables` text DEFAULT '{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}' NOT NULL,
|
||||||
|
`aspect` integer,
|
||||||
|
`public_notes` text,
|
||||||
|
`private_notes` text,
|
||||||
|
`visibility` text DEFAULT 'private' NOT NULL,
|
||||||
|
`thumbnail` blob,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_character`("id", "name", "owner", "people", "level", "variables", "aspect", "public_notes", "private_notes", "visibility", "thumbnail") SELECT "id", "name", "owner", "people", "level", "variables", "aspect", "public_notes", "private_notes", "visibility", "thumbnail" FROM `character`;--> statement-breakpoint
|
||||||
|
DROP TABLE `character`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
711
drizzle/meta/0017_snapshot.json
Normal file
711
drizzle/meta/0017_snapshot.json
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "153969ef-bcdb-4bbd-bd57-01fbd8004fc6",
|
||||||
|
"prevId": "05b549e7-5b3f-40f4-9461-05db59391e20",
|
||||||
|
"tables": {
|
||||||
|
"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": {
|
||||||
|
"\"character\".\"notes\"": "\"character\".\"public_notes\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,6 +120,13 @@
|
|||||||
"when": 1756221197092,
|
"when": 1756221197092,
|
||||||
"tag": "0016_wild_the_anarchist",
|
"tag": "0016_wild_the_anarchist",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1760531331328,
|
||||||
|
"tag": "0017_workable_scrambler",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,87 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
<div class="flex flex-row w-full max-w-full h-full max-h-full" style="--sidebar-width: 300px">
|
||||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
<div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
|
||||||
<div class="flex items-center px-2 gap-4">
|
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
|
||||||
<CollapsibleTrigger asChild>
|
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||||
<Button icon class="!bg-transparent group md:hidden">
|
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||||
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
<span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
|
||||||
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
</NuxtLink>
|
||||||
</Button>
|
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="treeParent"></div>
|
||||||
</CollapsibleTrigger>
|
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
|
||||||
<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 }">
|
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
|
||||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
Copyright Peaceultime - 2025
|
||||||
<span class="text-xl max-md:hidden">d[any]</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
<NavigationMenuRoot class="relative">
|
</div>
|
||||||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
|
||||||
<NavigationMenuItem>
|
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
|
||||||
<NavigationMenuTrigger>
|
<div class="flex flex-row gap-16 items-center">
|
||||||
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70 border-b-[2px] border-transparent hover:border-accent-blue py-2 hover:!text-opacity-70 flex items-center" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
<NavigationMenuRoot class="relative">
|
||||||
</NavigationMenuTrigger>
|
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||||
<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">
|
<NavigationMenuItem>
|
||||||
<NuxtLink :href="{ name: 'character-list' }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
<NavigationMenuTrigger>
|
||||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
<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>
|
||||||
</NavigationMenuContent>
|
</NavigationMenuTrigger>
|
||||||
</NavigationMenuItem>
|
<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">
|
||||||
</NavigationMenuList>
|
<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>
|
||||||
<div class="absolute top-full left-0 flex w-full justify-center">
|
<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>
|
||||||
<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)]" />
|
</NavigationMenuContent>
|
||||||
</div>
|
</NavigationMenuItem>
|
||||||
</NavigationMenuRoot>
|
</NavigationMenuList>
|
||||||
<div class="flex items-center px-2 gap-4">
|
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||||
<template v-if="!loggedIn">
|
<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)]" />
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
|
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70 max-md:hidden" :to="{ name: 'user-register' }">Créer un compte</NuxtLink>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 flex-row relative h-screen w-screen overflow-hidden">
|
|
||||||
<!-- <CollapsibleContent asChild forceMount> -->
|
|
||||||
<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="treeParent">
|
|
||||||
<div v-if="user" class="flex flex-1 py-4 px-2 flex-row flex-1 justify-between items-center">
|
|
||||||
<NuxtLink v-if="hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NavigationMenuRoot>
|
||||||
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
<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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- </CollapsibleContent> -->
|
<div class="flex flex-row gap-16 items-center">
|
||||||
|
<template v-if="!loggedIn">
|
||||||
|
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
|
||||||
|
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70 max-md:hidden" :to="{ name: 'user-register' }">Créer un compte</NuxtLink>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleRoot>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
|
||||||
import { hasPermissions } from '#shared/auth.util';
|
|
||||||
import { TreeDOM } from '#shared/tree';
|
import { TreeDOM } from '#shared/tree';
|
||||||
import { Content, iconByType } from '#shared/content.util';
|
import { Content, iconByType } from '#shared/content.util';
|
||||||
import { dom, icon, text } from '#shared/dom.util';
|
import { dom, icon } from '#shared/dom.util';
|
||||||
import { unifySlug } from '#shared/general.util';
|
import { unifySlug } from '#shared/general.util';
|
||||||
import { popper, tooltip } from '#shared/floating.util';
|
import { tooltip } from '#shared/floating.util';
|
||||||
import { link } from '#shared/components.util';
|
import { link } from '#shared/components.util';
|
||||||
|
|
||||||
const options = ref<DropdownOption[]>([{
|
|
||||||
type: 'item',
|
|
||||||
label: 'Mon profil',
|
|
||||||
select: () => useRouter().push({ name: 'user-profile' }),
|
|
||||||
}, {
|
|
||||||
type: 'item',
|
|
||||||
label: 'Deconnexion',
|
|
||||||
select: () => clear(),
|
|
||||||
}]);
|
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const { loggedIn, user, clear } = useUserSession();
|
const { loggedIn, user } = useUserSession();
|
||||||
const { fetch } = useContent();
|
const { fetch } = useContent();
|
||||||
|
|
||||||
await fetch(false);
|
await fetch(false);
|
||||||
@@ -97,11 +75,11 @@ const tree = new TreeDOM((item, depth) => {
|
|||||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||||
])]);
|
])]);
|
||||||
}, (item, depth) => {
|
}, (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({ 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, [
|
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 }),
|
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 } }),
|
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.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);
|
}, (item) => item.navigable);
|
||||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
||||||
const treeParent = useTemplateRef('treeParent');
|
const treeParent = useTemplateRef('treeParent');
|
||||||
@@ -119,9 +97,7 @@ watch(route, () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if(treeParent.value)
|
if(treeParent.value)
|
||||||
{
|
|
||||||
treeParent.value.appendChild(tree.container);
|
treeParent.value.appendChild(tree.container);
|
||||||
}
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unmount();
|
unmount();
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ async function logout(user: User)
|
|||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-1 flex-col p-4">
|
<div class="flex flex-1 flex-col p-4">
|
||||||
<div class="flex flex-row justify-between items-center">
|
<div class="flex flex-row justify-between items-center">
|
||||||
<ProseH2 class="text-center flex-1">Administration</ProseH2>
|
<h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
|
||||||
<Button><NuxtLink :to="{ name: 'admin-jobs' }">Jobs</NuxtLink></Button>
|
<NuxtLink :to="{ name: 'admin-jobs' }"><Button>Jobs</Button></NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 w-full justify-center items-stretch flex-row gap-4">
|
<div class="flex flex-1 w-full justify-center items-stretch flex-row gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async function fetch()
|
|||||||
<div class="flex flex-col justify-start items-center p-4">
|
<div class="flex flex-col justify-start items-center p-4">
|
||||||
<div class="flex flex-row justify-between items-center gap-8">
|
<div class="flex flex-row justify-between items-center gap-8">
|
||||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
<ProseH2 class="text-center flex-1">Administration</ProseH2>
|
<h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row w-full gap-8">
|
<div class="flex flex-row w-full gap-8">
|
||||||
<Select label="Job" v-model="job">
|
<Select label="Job" v-model="job">
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import characterConfig from '#shared/character-config.json';
|
import characterConfig from '#shared/character-config.json';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { unifySlug } from '#shared/general.util';
|
||||||
import PreviewA from '~/components/prose/PreviewA.vue';
|
|
||||||
import { clamp, unifySlug } from '#shared/general.util';
|
|
||||||
import type { CompiledCharacter, SpellConfig } from '~/types/character';
|
|
||||||
import type { CharacterConfig } from '~/types/character';
|
import type { CharacterConfig } from '~/types/character';
|
||||||
import { abilityTexts, CharacterCompiler, CharacterSheet, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util';
|
import { CharacterSheet } from '#shared/character.util';
|
||||||
import { getText } from '#shared/i18n';
|
|
||||||
import { preview } from '#shared/proses';
|
|
||||||
import { div, dom, icon, text } from '#shared/dom.util';
|
|
||||||
import markdown from '#shared/markdown.util';
|
|
||||||
import { button, foldable } from '#shared/components.util';
|
|
||||||
import { fullblocker, tooltip } from '~/shared/floating.util';
|
|
||||||
/*
|
/*
|
||||||
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
||||||
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
|
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
|
||||||
@@ -35,180 +26,12 @@ onMounted(() => {
|
|||||||
if(container.value && id)
|
if(container.value && id)
|
||||||
{
|
{
|
||||||
const character = new CharacterSheet(id, user);
|
const character = new CharacterSheet(id, user);
|
||||||
container.value.appendChild(character.container);
|
container.value.replaceWith(character.container);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="container"></div>
|
<div class="flex flex-1 w-full h-full items-start justify-center" ref="container"></div>
|
||||||
<!-- <div v-if="status === 'pending'">
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Chargement ...</Title>
|
|
||||||
</Head>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="status === 'success' && character && !error">
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - {{ character.name }}</Title>
|
|
||||||
</Head>
|
|
||||||
<div class="flex flex-row gap-4 justify-between">
|
|
||||||
<div></div>
|
|
||||||
<div class="flex lg:flex-row flex-col gap-6 items-center justify-center">
|
|
||||||
<div class="flex gap-6 items-center">
|
|
||||||
<Avatar src="" icon="radix-icons:person" size="large" />
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-xl font-bold">{{ character.name }}</span>
|
|
||||||
<span class="text-sm">De {{ character.username }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="font-bold">Niveau {{ character.level }}</span>
|
|
||||||
<span>{{ config.peoples[character.race]?.name ?? 'Peuple inconnu' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4 gap-8">
|
|
||||||
<span class="flex flex-row items-center gap-2 text-3xl font-light">PV: <span class="font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35">{{ character.health - character.variables.health }}</span>/ {{ character.health }}</span>
|
|
||||||
<span class="flex flex-row items-center gap-2 text-3xl font-light">Mana: <span class="font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35">{{ character.mana - character.variables.mana }}</span>/ {{ character.mana }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="self-center">
|
|
||||||
<Tooltip side="right" message="Modifier" v-if="user && user.id === character.owner"><NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink></Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
|
|
||||||
<div class="flex flex-row gap-4 items-center border-b border-light-30 dark:border-dark-30 me-4 pe-4 divide-x divide-light-30 dark:divide-dark-30">
|
|
||||||
<div class="flex relative justify-between ps-4 gap-2">
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.strength }}</span><span class="text-sm 2xl:text-base">Force</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.dexterity }}</span><span class="text-sm 2xl:text-base">Dextérité</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.constitution }}</span><span class="text-sm 2xl:text-base">Constitution</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.intelligence }}</span><span class="text-sm 2xl:text-base">Intelligence</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.curiosity }}</span><span class="text-sm 2xl:text-base">Curiosité</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.charisma }}</span><span class="text-sm 2xl:text-base">Charisme</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.psyche }}</span><span class="text-sm 2xl:text-base">Psyché</span></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 relative ps-4 flex-row items-center justify-between">
|
|
||||||
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.initiative }}</span><span>Initiative</span></div>
|
|
||||||
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">{{ character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }}</span><span>Course</span></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 relative ps-4 flex-row items-center justify-between">
|
|
||||||
<Icon icon="ph:shield-checkered" class="w-8 h-8" />
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Passive</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Blocage</span></div>
|
|
||||||
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Esquive</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 px-8">
|
|
||||||
<div class="flex flex-col pe-8 gap-4 py-2 w-80 border-r border-light-30 dark:border-dark-30">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30 mb-2">Compétences</span>
|
|
||||||
<div class="grid grid-cols-3 gap-1">
|
|
||||||
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ abilityTexts[ability] }}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrises</span>
|
|
||||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
|
||||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes légères" label="Arme légère" />
|
|
||||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes de jet" label="Arme de jet" />
|
|
||||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes naturelles" label="Arme naturelle" />
|
|
||||||
<PreviewA v-if="character.mastery.strength > 1" href="regles/annexes/equipement#Les armes" label="Arme standard" />
|
|
||||||
<PreviewA v-if="character.mastery.strength > 1" href="regles/annexes/equipement#Les armes improvisées" label="Arme improvisée" />
|
|
||||||
<PreviewA v-if="character.mastery.strength > 2" href="regles/annexes/equipement#Les armes lourdes" label="Arme lourde" />
|
|
||||||
<PreviewA v-if="character.mastery.strength > 3" href="regles/annexes/equipement#Les armes à deux mains" label="Arme à deux mains" />
|
|
||||||
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="regles/annexes/equipement#Les armes maniables" label="Arme maniable" />
|
|
||||||
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="regles/annexes/equipement#Les armes à projectiles" label="Arme à projectiles" />
|
|
||||||
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="regles/annexes/equipement#Les armes longues" label="Arme longue" />
|
|
||||||
<PreviewA v-if="character.mastery.shield > 0" href="regles/annexes/equipement#Les boucliers" label="Bouclier" />
|
|
||||||
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="regles/annexes/equipement#Les boucliers à deux mains" label="Bouclier à deux mains" />
|
|
||||||
</div>
|
|
||||||
<div v-if="character.mastery.armor > 0" class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
|
||||||
<PreviewA v-if="character.mastery.armor > 0" href="regles/annexes/equipement#Les armures légères" label="Armure légère" />
|
|
||||||
<PreviewA v-if="character.mastery.armor > 1" href="regles/annexes/equipement#Les armures" label="Armure standard" />
|
|
||||||
<PreviewA v-if="character.mastery.armor > 2" href="regles/annexes/equipement#Les armures lourdes" label="Armure lourde" />
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
|
||||||
<span>Précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
|
|
||||||
<span>Savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
|
|
||||||
<span>Instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
|
|
||||||
<span>Oeuvres: <span class="font-bold">{{ character.spellranks.arts }}</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TabsRoot default-value="features" class="w-[60rem] max-h-full">
|
|
||||||
<TabsList class="flex flex-row relative px-4 gap-4">
|
|
||||||
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
|
|
||||||
<TabsTrigger value="features" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Aptitudes</TabsTrigger>
|
|
||||||
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue" v-if="character.spellslots > 0">Sorts</TabsTrigger>
|
|
||||||
<TabsTrigger value="inventory" class="px-2 py-1 border-b border-transparent hover:border-accent-blue" v-if="character.capacity !== false">Inventaire</TabsTrigger>
|
|
||||||
<TabsTrigger value="notes" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Notes</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="features" class="overflow-y-auto max-h-full">
|
|
||||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-4">
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<div class="flex flex-col col-span-2">
|
|
||||||
<span class="text-lg font-semibold">Actions</span>
|
|
||||||
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
|
|
||||||
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-lg font-semibold">Réactions</span>
|
|
||||||
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
|
|
||||||
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-lg font-semibold">Actions libre</span>
|
|
||||||
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
|
|
||||||
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-lg font-semibold">Aptitudes</span>
|
|
||||||
<MarkdownRenderer :content="character.lists.passive?.map(e => getText(e))?.map(e => `> ${e}`).join('\n\n')" :properties="{ tags: { a: preview } }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent v-if="character.spellslots > 0" value="spells" class="overflow-y-auto max-h-full">
|
|
||||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-2">
|
|
||||||
<div class="flex flex-1 justify-between items-baseline px-2"><div></div><div class="flex gap-4 items-baseline"><span class="italic text-light-70 dark:text-dark-70 text-sm">{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés</span><Button class="!font-normal" @click="openSpellPanel">Modifier</Button></div></div>
|
|
||||||
<div class="flex flex-col" v-if="[...(character.lists.spells ?? []), ...character.variables.spells].length > 0">
|
|
||||||
<div class="pb-4 px-2 mt-4 border-b last:border-none border-light-30 dark:border-dark-30 flex flex-col" v-for="spell of [...(character.lists.spells ?? []), ...character.variables.spells].map(e => config.spells.find((f: SpellConfig) => f.id === e)).filter(e => !!e)">
|
|
||||||
<div class="flex flex-row justify-between">
|
|
||||||
<span class="text-lg font-bold">{{ spell.name }}</span>
|
|
||||||
<div class="flex flex-row items-center gap-6">
|
|
||||||
<div class="flex flex-row text-sm gap-2">
|
|
||||||
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row text-sm gap-1">
|
|
||||||
<span class="" v-if="spell.rank !== 4">Rang {{ spell.rank }}</span><span v-if="spell.rank !== 4">/</span>
|
|
||||||
<span class="" v-if="spell.rank !== 4">{{ spellTypeTexts[spell.type] }}</span><span v-if="spell.rank !== 4">/</span>
|
|
||||||
<span class="">{{ spell.cost }} mana</span><span>/</span>
|
|
||||||
<span class="capitalize">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MarkdownRenderer :content="spell.effect" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="inventory" v-if="character.capacity !== false" class="overflow-y-auto max-h-full">
|
|
||||||
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="notes" class="overflow-y-auto max-h-full">
|
|
||||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
|
|
||||||
<MarkdownRenderer :content="character.notes" />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</TabsRoot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Erreur</Title>
|
|
||||||
</Head>
|
|
||||||
<div>Erreur de chargement</div>
|
|
||||||
</div> -->
|
|
||||||
</template>
|
</template>
|
||||||
@@ -35,46 +35,59 @@ async function duplicateCharacter(id: number)
|
|||||||
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
||||||
<Loading size="large" />
|
<Loading size="large" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
<template v-else-if="status === 'success'">
|
||||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
|
<div v-if="characters && characters.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||||
<NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
|
||||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
<NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
||||||
<div class="flex flex-1 flex-col gap-2 justify-center">
|
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||||
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
|
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||||
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
|
||||||
<div class="flex flex-row flex-1 items-stretch gap-4">
|
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||||
<span class="text-sm">Niveau {{ character.level }}</span>
|
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
<span class="text-sm">Niveau {{ character.level }}</span>
|
||||||
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
|
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||||
</div>
|
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
|
||||||
</div>
|
|
||||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
|
||||||
</div>
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="flex justify-around items-center py-2 px-4 gap-4">
|
|
||||||
<NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Editer</NuxtLink>
|
|
||||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
|
||||||
<NuxtLink @click="duplicateCharacter(character.id)" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Dupliquer</NuxtLink>
|
|
||||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
|
||||||
<AlertDialogRoot>
|
|
||||||
<AlertDialogTrigger>
|
|
||||||
<span class="text-sm font-bold text-light-red dark:text-dark-red">Supprimer</span>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
|
||||||
<AlertDialogContent
|
|
||||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
|
||||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
|
||||||
<div class="flex flex-1 justify-end gap-4">
|
|
||||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
|
||||||
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogContent>
|
</div>
|
||||||
</AlertDialogPortal>
|
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||||
</AlertDialogRoot>
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
<div class="flex justify-around items-center py-2 px-4 gap-4">
|
||||||
|
<NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Editer</NuxtLink>
|
||||||
|
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||||
|
<NuxtLink @click="duplicateCharacter(character.id)" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Dupliquer</NuxtLink>
|
||||||
|
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||||
|
<AlertDialogRoot>
|
||||||
|
<AlertDialogTrigger>
|
||||||
|
<span class="text-sm font-bold text-light-red dark:text-dark-red">Supprimer</span>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||||
|
<AlertDialogContent
|
||||||
|
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||||
|
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
||||||
|
<div class="flex flex-1 justify-end gap-4">
|
||||||
|
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else class="flex flex-col gap-2 items-center flex-1">
|
||||||
|
<span class="text-lg font-bold">Vous n'avez pas encore de personnage</span>
|
||||||
|
<NuxtLink 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 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
|
||||||
|
<NuxtLink 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 py-2 px-4" :to="{ name: 'character-list' }">Qu'ont fait les autres ?</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span>Erreur de chargement</span>
|
<span>Erreur de chargement</span>
|
||||||
<span>{{ error?.message }}</span>
|
<span>{{ error?.message }}</span>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import characterConfig from '#shared/character-config.json';
|
||||||
|
import type { CharacterConfig } from '~/types/character';
|
||||||
const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } });
|
const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } });
|
||||||
|
const config = characterConfig as CharacterConfig;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -10,15 +13,34 @@ const { data: characters, error, status } = await useFetch(`/api/character`, { p
|
|||||||
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
||||||
<Loading size="large" />
|
<Loading size="large" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
<template v-else-if="status === 'success'">
|
||||||
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters">
|
<div v-if="characters && characters.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||||
<Avatar size="large" icon="radix-icons:person" src="" />
|
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
|
||||||
<div class="flex flex-1 flex-shrink flex-col truncate">
|
<NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 p-2 flex flex-col gap-2">
|
||||||
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
|
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||||
<span class="text-sm truncate">Niveau {{ character.level }}</span>
|
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||||
|
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
|
||||||
|
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||||
|
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||||
|
<span class="text-sm">Niveau {{ character.level }}</span>
|
||||||
|
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||||
|
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else class="flex flex-col gap-2 items-center flex-1">
|
||||||
|
<span class="text-lg font-bold">Il n'existe pas encore de personnage public</span>
|
||||||
|
Soyez le premier à partager vos créations !
|
||||||
|
<NuxtLink 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 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span>Erreur de chargement</span>
|
<span>Erreur de chargement</span>
|
||||||
<span>{{ error?.message }}</span>
|
<span>{{ error?.message }}</span>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Content, Editor } from '#shared/content.util';
|
import { Content, Editor } from '#shared/content.util';
|
||||||
import { button, loading } from '#shared/components.util';
|
import { button, loading } from '#shared/components.util';
|
||||||
import { dom, icon, text } from '#shared/dom.util';
|
import { dom, icon } from '#shared/dom.util';
|
||||||
import { modal, popper, tooltip } from '#shared/floating.util';
|
import { modal, tooltip } from '#shared/floating.util';
|
||||||
import { Toaster } from '#shared/components.util';
|
import { Toaster } from '#shared/components.util';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<Title>d[any] - Mentions légales</Title>
|
<Title>d[any] - Mentions légales</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-col max-w-[1200px] p-16">
|
<div class="flex flex-col max-w-[1200px] p-16">
|
||||||
<ProseH3>Mentions Légales</ProseH3>
|
<h3 class="text-xl font-bold">Mentions Légales</h3>
|
||||||
<ProseH4>Collecte et Traitement des Données Personnelles</ProseH4>
|
<h4 class="text-lg font-semibold">Collecte et Traitement des Données Personnelles</h4>
|
||||||
Ce site collecte des données personnelles durant l'inscription et des données anonymes durant la navigation sur
|
Ce site collecte des données personnelles durant l'inscription et des données anonymes durant la navigation sur
|
||||||
le site dans un but de collecte statistiques.<br />
|
le site dans un but de collecte statistiques.<br />
|
||||||
|
|
||||||
@@ -12,21 +12,21 @@
|
|||||||
suppression de vos données personnelles. <br />
|
suppression de vos données personnelles. <br />
|
||||||
|
|
||||||
Pour exercer ces droits, vous pouvez vous rendre dans votre profil et selectionner l'option "Supprimer mon
|
Pour exercer ces droits, vous pouvez vous rendre dans votre profil et selectionner l'option "Supprimer mon
|
||||||
compte" qui garanti une suppression de l'intégralité de vos données personnelles.
|
compte" qui garanti une suppression de l'intégralité de vos données personnelles.<br /><br />
|
||||||
|
|
||||||
<ProseH4>Utilisation des Cookies</ProseH4>
|
<h4 class="text-lg font-semibold">Utilisation des Cookies</h4>
|
||||||
Ce site utilise des cookies uniquement pour maintenir la connexion des utilisateurs et faciliter leur navigation
|
Ce site utilise des cookies uniquement pour maintenir la connexion des utilisateurs et faciliter leur navigation
|
||||||
lors de chaque visite. Aucune information de suivi ou de profilage n'est réalisée. Ces cookies sont essentiels
|
lors de chaque visite. Aucune information de suivi ou de profilage n'est réalisée. Ces cookies sont essentiels
|
||||||
au fonctionnement du site et ne nécessitent pas de consentement préalable. <br />
|
au fonctionnement du site et ne nécessitent pas de consentement préalable. <br />
|
||||||
|
|
||||||
Vous pouvez gérer les cookies en configurant les paramètres de votre navigateur, mais la désactivation de ces
|
Vous pouvez gérer les cookies en configurant les paramètres de votre navigateur, mais la désactivation de ces
|
||||||
cookies pourrait affecter votre expérience de navigation.
|
cookies pourrait affecter votre expérience de navigation.<br /><br />
|
||||||
|
|
||||||
<ProseH4>Limitation de Responsabilité</ProseH4>
|
<h4 class="text-lg font-semibold">Limitation de Responsabilité</h4>
|
||||||
Les informations publiées sur ce site sont fournies à titre indicatif et peuvent contenir des erreurs. <br />
|
Les informations publiées sur ce site sont fournies à titre indicatif et peuvent contenir des erreurs. <br />
|
||||||
L'éditeur décline toute responsabilité quant à l'usage qui pourrait être fait de ces informations.
|
L'éditeur décline toute responsabilité quant à l'usage qui pourrait être fait de ces informations.<br /><br />
|
||||||
|
|
||||||
<ProseH4>Propriété Intellectuelle</ProseH4>
|
<h4 class="text-lg font-semibold">Propriété Intellectuelle</h4>
|
||||||
Tous les contenus présents sur ce site (textes, images, logos, etc.) sont protégés par les lois en vigueur
|
Tous les contenus présents sur ce site (textes, images, logos, etc.) sont protégés par les lois en vigueur
|
||||||
sur la propriété intellectuelle. Toute reproduction ou utilisation de ces contenus sans autorisation préalable
|
sur la propriété intellectuelle. Toute reproduction ou utilisation de ces contenus sans autorisation préalable
|
||||||
est interdite. <br /><br />
|
est interdite. <br /><br />
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Roadmap</Title>
|
|
||||||
</Head>
|
|
||||||
<div class="flex flex-col justify-start p-6">
|
|
||||||
<ProseH2>Roadmap</ProseH2>
|
|
||||||
<div class="grid grid-cols-4 gap-x-2 gap-y-4">
|
|
||||||
<div v-if="loggedIn && user && hasPermissions(user.permissions, ['admin'])" class="flex flex-col gap-2 justify-start">
|
|
||||||
<ProseH3>Administration</ProseH3>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Dashboard de statistiques</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Editeur de permissions</span><ProseTag>prioritaire</ProseTag></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Synchro project <-> GIT</span><ProseTag>prioritaire</ProseTag></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Versionning automatisé, releases et newsletter</span></Label>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
|
||||||
<ProseH3>Editeur</ProseH3>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition de page</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition riche de page</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Edition live de page</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Raccourcis d'edition</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Affichage alternatif par page</span></Label>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
|
||||||
<ProseH3>Projet</ProseH3>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition du projet</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Déplacement des fichiers</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Configuration de droit du projet</span><ProseTag>prioritaire</ProseTag></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Theme par projet</span></Label>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
|
||||||
<ProseH3>Nouvelles features</ProseH3>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Historique des modifs</span><ProseTag>prioritaire</ProseTag></Label><!-- Objet release: key hash, timestamp, version, name, description?. Objet edit: key hash, key property, value, timestamp -->
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Commentaire par page</span></Label><!-- Object comment: key path, key comment_id, position, content, owner, following? -->
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Type de fichier: Timeline</span></Label><!-- Propriétés: array of (from, (to || ponctual), ((title, content) || dedicated page)) -->
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Type de fichier: Whiteboard</span></Label><!-- Tableau de données SVG -->
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
|
||||||
<ProseH3>Utilisateur</ProseH3>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
|
|
||||||
<!-- <Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label> -->
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
|
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Préférence d'email</span></Label><!-- New features, newsletter et surveys -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
|
||||||
|
|
||||||
const { loggedIn, user } = useUserSession();
|
|
||||||
</script>
|
|
||||||
45
pages/usage.vue
Normal file
45
pages/usage.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Mentions légales</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-col max-w-[1200px] p-16">
|
||||||
|
<h3 class="text-xl font-bold">Conditions Générales d'Utilisation du site d-any.com</h3>
|
||||||
|
<h4 class="text-lg font-semibold py-2">1. Objet</h4>
|
||||||
|
Le site d-any.com offre un service en ligne dédié au jeu de rôle comprenant une section de règles officielles maintenues par l'administrateur, une section permettant la création de personnages
|
||||||
|
publics ou privés et une section de campagnes visant à rassembler plusieurs joueurs pour faire interagir leurs personnages. L'utilisation du site implique l'acceptation pleine et entière des présentes conditions. <br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">2. Accès et fonctionnement</h4>
|
||||||
|
L'accès au site est gratuit. L'interaction entre utilisateurs est strictement limitée aux personnages et joueurs participant à une même campagne partagée. Aucun contact direct ni interaction n'est possible en dehors de cette structure.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">3. Création et gestion des personnages</h4>
|
||||||
|
Les utilisateurs peuvent créer des personnages publics, visibles par tous les membres des campagnes partagées, ou privés, visibles uniquement par leur créateur.
|
||||||
|
Les utilisateurs sont responsables du contenu des personnages qu'ils créent. Ils s'engagent à ne pas créer ou publier des personnages portant atteinte à la dignité, contenant des propos discriminatoires, diffamatoires, obscènes ou illicites.
|
||||||
|
L'administrateur du site se réserve le droit de supprimer ou masquer tout personnage en infraction avec ces règles.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">4. Règles du jeu</h4>
|
||||||
|
Les règles officielles du jeu, rédigées et entretenues par l'administrateur, doivent être respectées par tous les utilisateurs dans la création et le déroulement des campagnes.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">5. Interaction en campagne</h4>
|
||||||
|
Les communications et interactions entre joueurs et personnages sont strictement limitées aux campagnes partagées.
|
||||||
|
Toute interaction dans ces cadres doit respecter les règles de respect, de courtoisie et de fair-play.
|
||||||
|
Tout comportement abusif, harcèlement, propos haineux ou toute forme de contenu illicite est prohibé et pourra entraîner des sanctions, incluant la suppression de comptes ou personnages.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">6. Propriété intellectuelle</h4>
|
||||||
|
Les règles, outils, et contenus hébergés sur le site sont la propriété de l'administrateur ou des auteurs respectifs.
|
||||||
|
Les personnages créés appartiennent à leurs auteurs, sous réserve du respect des droits d'auteur liés au jeu original et de la charte du site.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">7. Données personnelles</h4>
|
||||||
|
Les données collectées se limitent à celles nécessaires au fonctionnement du site. Toute donnée personnelle est traitée conformément à la réglementation en vigueur et peut être modifiée ou supprimée sur demande.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">8. Responsabilité</h4>
|
||||||
|
L'administrateur ne pourra être tenu responsable des usages faits par les utilisateurs des personnages publics ou des interactions au sein des campagnes. L'éditeur décline toute responsabilité en cas d'abus
|
||||||
|
entre joueurs ou de contenu illégal diffusé par un utilisateur.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">9. Modification des conditions</h4>
|
||||||
|
Ces conditions peuvent être modifiées à tout moment par l'administrateur. Les utilisateurs seront informés des modifications via le site et l'usage continu vaudra acceptation des nouvelles conditions.<br/><br/>
|
||||||
|
|
||||||
|
<h4 class="text-lg font-semibold py-2">10. Droit applicable</h4>
|
||||||
|
Les présentes conditions sont soumises au droit français. Tout litige sera porté devant les tribunaux compétents.<br/><br/>
|
||||||
|
<div class="py-32"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<Title>d[any] - Validation de votre adresse mail</Title>
|
<Title>d[any] - Validation de votre adresse mail</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-col justify-center items-center">
|
<div class="flex flex-col justify-center items-center">
|
||||||
<ProseH2>Votre compte a été validé ! 🎉</ProseH2>
|
<h2 class="text-2xl font-bold">Votre compte a été validé ! 🎉</h2>
|
||||||
<div class="flex flex-row gap-8">
|
<div class="flex flex-row gap-8">
|
||||||
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'user-login', replace: true }">Se connecter</NuxtLink></Button>
|
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'user-login', replace: true }">Se connecter</NuxtLink></Button>
|
||||||
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'index', replace: true }">Retourner à l'accueil</NuxtLink></Button>
|
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'index', replace: true }">Retourner à l'accueil</NuxtLink></Button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
|
<h4 class="text-xl font-bold">Reinitialisation de mon mot de passe</h4>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="email"/>
|
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="email"/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
|
<h4 class="text-center flex-1 text-xl font-bold">Reinitialisation de mon mot de passe</h4>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ '!border-light-red !dark:border-dark-red': error }"/>
|
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ '!border-light-red !dark:border-dark-red': error }"/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
<ProseH4>Modification de mon mot de passe</ProseH4>
|
<h4 class="text-center flex-1 text-xl font-bold">Modification de mon mot de passe</h4>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
<TextInput type="password" label="Ancien mot de passe" name="old-password" autocomplete="current-password" v-model="oldPasswd"/>
|
<TextInput type="password" label="Ancien mot de passe" name="old-password" autocomplete="current-password" v-model="oldPasswd"/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
<ProseH4>Connexion</ProseH4>
|
<h4 class="text-xl font-bold">Connexion</h4>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
<TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>
|
<TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ async function deleteUser()
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
|
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
|
||||||
<div class="flex flex-col items-start">
|
<div class="flex flex-col items-start">
|
||||||
<ProseH5>{{ user.username }}</ProseH5>
|
<h4 class="text-xl font-bold">{{ user.username }}</h4>
|
||||||
<ProseH5>{{ user.email }}</ProseH5>
|
<h4 class="text-xl font-bold">{{ user.email }}</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-light-red dark:border-dark-red bg-light-redBack dark:bg-dark-redBack text-light-red dark:text-dark-red py-1 px-3 flex items-center justify-between flex-col md:flex-row"
|
<div class="border-light-red dark:border-dark-red bg-light-redBack dark:bg-dark-redBack text-light-red dark:text-dark-red py-1 px-3 flex items-center justify-between flex-col md:flex-row"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
<ProseH4>Inscription</ProseH4>
|
<h4 class="text-xl font-bold">Inscription</h4>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0">
|
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0">
|
||||||
<TextInput type="text" label="Nom d'utilisateur" name="username" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
|
<TextInput type="text" label="Nom d'utilisateur" name="username" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||||
</div>
|
</div>
|
||||||
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
||||||
|
<Label class="pb-2 col-span-2 md:col-span-1 flex flex-row gap-2 items-center"><CheckboxRoot v-model:checked="agreeOnRules" class="border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 w-5 h-5" ><CheckboxIndicator ><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span>J'ai lu et j'accepte les <NuxtLink class="text-accent-blue cursor-pointer" :to="{ name: 'usage' }" target="_blank">conditions d'utilisation</NuxtLink></span></Label>
|
||||||
<Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
|
<Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
|
||||||
<span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span>
|
<span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span>
|
||||||
</form>
|
</form>
|
||||||
@@ -50,6 +51,7 @@ const checkedLower = computed(() => state.password.toUpperCase() !== state.passw
|
|||||||
const checkedUpper = computed(() => state.password.toLowerCase() !== state.password);
|
const checkedUpper = computed(() => state.password.toLowerCase() !== state.password);
|
||||||
const checkedDigit = computed(() => /[0-9]/.test(state.password));
|
const checkedDigit = computed(() => /[0-9]/.test(state.password));
|
||||||
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
|
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
|
||||||
|
const agreeOnRules = ref<boolean>(false);
|
||||||
|
|
||||||
const { data: result, status, error, refresh } = await useFetch('/api/auth/register', {
|
const { data: result, status, error, refresh } = await useFetch('/api/auth/register', {
|
||||||
body: state,
|
body: state,
|
||||||
@@ -57,7 +59,7 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/regis
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
watch: false,
|
watch: false,
|
||||||
ignoreResponseError: true,
|
ignoreResponseError: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
async function submit()
|
async function submit()
|
||||||
{
|
{
|
||||||
@@ -69,6 +71,8 @@ async function submit()
|
|||||||
return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||||
if(state.password !== confirmPassword.value)
|
if(state.password !== confirmPassword.value)
|
||||||
return Toaster.add({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 });
|
return Toaster.add({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 });
|
||||||
|
if(agreeOnRules.value !== true)
|
||||||
|
return Toaster.add({ content: 'Veuillez accepter des conditions d\'utilisations pour vous inscrire', timer: true, duration: 10000 });
|
||||||
|
|
||||||
const data = schema.safeParse(state);
|
const data = schema.safeParse(state);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ declare module 'nitropack'
|
|||||||
{
|
{
|
||||||
interface TaskPayload
|
interface TaskPayload
|
||||||
{
|
{
|
||||||
type: string
|
type: string;
|
||||||
|
}
|
||||||
|
interface TaskResult<RT = unknown>
|
||||||
|
{
|
||||||
|
error?: Error | string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +21,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = getRouterParam(e, 'id');
|
const id = getRouterParam(e, 'id');
|
||||||
const payload: Record<string, any> = await readBody(e);
|
const body: Record<string, any> = await readBody(e);
|
||||||
|
|
||||||
if(!id)
|
if(!id)
|
||||||
{
|
{
|
||||||
@@ -25,8 +29,11 @@ export default defineEventHandler(async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.type = id;
|
body.data = JSON.parse(body.data);
|
||||||
payload.data = JSON.parse(payload.data);
|
const payload = {
|
||||||
|
type: id,
|
||||||
|
data: body,
|
||||||
|
}
|
||||||
|
|
||||||
const result = await runTask(id, {
|
const result = await runTask(id, {
|
||||||
payload: payload
|
payload: payload
|
||||||
@@ -36,7 +43,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
{
|
{
|
||||||
setResponseStatus(e, 500);
|
setResponseStatus(e, 500);
|
||||||
|
|
||||||
if(result.error && (result.error as Error).message)
|
if(result.error && result.error.message)
|
||||||
throw result.error;
|
throw result.error;
|
||||||
else if(result.error)
|
else if(result.error)
|
||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { usersDataTable, usersTable } from '~/db/schema';
|
|||||||
import { schema } from '~/schemas/registration';
|
import { schema } from '~/schemas/registration';
|
||||||
import { checkSession, logSession } from '~/server/utils/user';
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
import sendMail from '~/server/tasks/mail';
|
|
||||||
import type { $ZodIssue } from 'zod/v4/core';
|
import type { $ZodIssue } from 'zod/v4/core';
|
||||||
|
|
||||||
interface SuccessHandler
|
interface SuccessHandler
|
||||||
@@ -84,7 +83,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||||||
id: emailId, timestamp,
|
id: emailId, timestamp,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await sendMail({
|
await runTask('mail', {
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [body.data.email],
|
to: [body.data.email],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { and, eq, SQL, sql, type Operators } from 'drizzle-orm';
|
import { eq, SQL, type Operators } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable, userPermissionsTable } from '~/db/schema';
|
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
import { group } from '~/shared/general.util';
|
import { group } from '~/shared/general.util';
|
||||||
import type { Character, Level, MainStat, TrainingLevel } from '~/types/character';
|
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
|
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
|
||||||
@@ -16,16 +16,16 @@ export default defineEventHandler(async (e) => {
|
|||||||
let where: ((character: typeof characterTable._.config.columns, sql: Operators) => SQL | undefined) | undefined = undefined;
|
let where: ((character: typeof characterTable._.config.columns, sql: Operators) => SQL | undefined) | undefined = undefined;
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
if(visibility === "own")
|
if(visibility === "own")
|
||||||
{
|
{
|
||||||
const session = await getUserSession(e);
|
|
||||||
if(!session.user)
|
if(!session.user)
|
||||||
{
|
{
|
||||||
setResponseStatus(e, 401);
|
setResponseStatus(e, 401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id), eq(character.visibility, "private"));
|
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id));
|
||||||
}
|
}
|
||||||
else if(visibility === 'public')
|
else if(visibility === 'public')
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,6 @@ export default defineEventHandler(async (e) => {
|
|||||||
}
|
}
|
||||||
else if(visibility === 'admin')
|
else if(visibility === 'admin')
|
||||||
{
|
{
|
||||||
const session = await getUserSession(e);
|
|
||||||
if(!session.user)
|
if(!session.user)
|
||||||
{
|
{
|
||||||
setResponseStatus(e, 401);
|
setResponseStatus(e, 401);
|
||||||
@@ -73,7 +72,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
people: character.people,
|
people: character.people,
|
||||||
level: character.level,
|
level: character.level,
|
||||||
aspect: character.aspect,
|
aspect: character.aspect,
|
||||||
notes: character.notes,
|
notes: { public: character.public_notes, private: session.user?.id === character.owner ? character.private_notes : undefined },
|
||||||
variables: character.variables,
|
variables: character.variables,
|
||||||
|
|
||||||
training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
||||||
|
|||||||
@@ -31,18 +31,19 @@ export default defineEventHandler(async (e) => {
|
|||||||
people: body.data.people!,
|
people: body.data.people!,
|
||||||
level: body.data.level,
|
level: body.data.level,
|
||||||
aspect: body.data.aspect,
|
aspect: body.data.aspect,
|
||||||
notes: body.data.notes,
|
public_notes: body.data.notes.public,
|
||||||
|
private_notes: body.data.notes.private,
|
||||||
variables: body.data.variables,
|
variables: body.data.variables,
|
||||||
visibility: body.data.visibility,
|
visibility: body.data.visibility,
|
||||||
thumbnail: body.data.thumbnail,
|
thumbnail: body.data.thumbnail,
|
||||||
}).returning({ id: characterTable.id }).get().id;
|
}).returning({ id: characterTable.id }).get().id;
|
||||||
|
|
||||||
if(Object.keys(body.data.leveling).length > 0) tx.insert(characterLevelingTable).values(Object.entries(body.data.leveling).map(e => ({ character: id, level: parseInt(e[0], 10), choice: e[1]! }))).run();
|
if(Object.keys(body.data.leveling).length > 0) tx.insert(characterLevelingTable).values(Object.entries(body.data.leveling).filter(e => e[1] !== undefined).map(e => ({ character: id, level: parseInt(e[0], 10), choice: e[1]! }))).run();
|
||||||
|
|
||||||
const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0], 10), choice: _e[1]! })));
|
const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).filter(e => e[1] !== undefined).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0], 10), choice: _e[1]! })));
|
||||||
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
|
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
|
||||||
|
|
||||||
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
|
const abilities = Object.entries(body.data.abilities).filter(e => e[1] !== undefined).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
|
||||||
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
people: character.people,
|
people: character.people,
|
||||||
level: character.level,
|
level: character.level,
|
||||||
aspect: character.aspect,
|
aspect: character.aspect,
|
||||||
notes: character.notes,
|
notes: { public: character.public_notes, private: session.user?.id === character.owner ? character.private_notes : undefined },
|
||||||
variables: character.variables,
|
variables: character.variables,
|
||||||
|
|
||||||
training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export default defineEventHandler(async (e) => {
|
|||||||
people: body.data.people!,
|
people: body.data.people!,
|
||||||
level: body.data.level,
|
level: body.data.level,
|
||||||
aspect: body.data.aspect,
|
aspect: body.data.aspect,
|
||||||
notes: body.data.notes,
|
public_notes: body.data.notes.public,
|
||||||
|
private_notes: body.data.notes.private,
|
||||||
variables: body.data.variables,
|
variables: body.data.variables,
|
||||||
visibility: body.data.visibility,
|
visibility: body.data.visibility,
|
||||||
thumbnail: body.data.thumbnail,
|
thumbnail: body.data.thumbnail,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable } from '~/db/schema';
|
import { characterTable } from '~/db/schema';
|
||||||
import type { CharacterValues } from '~/types/character';
|
import { CharacterNotesValidation } from '#shared/character.util';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const id = getRouterParam(e, "id");
|
const id = getRouterParam(e, "id");
|
||||||
@@ -11,11 +11,12 @@ export default defineEventHandler(async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await readBody(e) as CharacterValues;
|
const body = await readValidatedBody(e, CharacterNotesValidation.safeParse);
|
||||||
if(!body)
|
if(!body.success)
|
||||||
{
|
{
|
||||||
|
console.error(body.error);
|
||||||
setResponseStatus(e, 400);
|
setResponseStatus(e, 400);
|
||||||
return;
|
throw body.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
@@ -35,8 +36,8 @@ export default defineEventHandler(async (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db.update(characterTable).set({
|
db.update(characterTable).set({
|
||||||
health: body.health,
|
public_notes: body.data.public,
|
||||||
mana: body.mana,
|
private_notes: body.data.private,
|
||||||
}).where(eq(characterTable.id, parseInt(id, 10))).run();
|
}).where(eq(characterTable.id, parseInt(id, 10))).run();
|
||||||
|
|
||||||
setResponseStatus(e, 200);
|
setResponseStatus(e, 200);
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { and, eq, sql } from 'drizzle-orm';
|
|
||||||
import useDatabase from '~/composables/useDatabase';
|
|
||||||
import { characterTable } from '~/db/schema';
|
|
||||||
import type { CharacterVariables } from '~/types/character';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
|
||||||
const id = getRouterParam(e, "id");
|
|
||||||
|
|
||||||
if(!id)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await getUserSession(e);
|
|
||||||
if(!session.user)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 401);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = useDatabase();
|
|
||||||
const character = db.select({
|
|
||||||
health: characterTable.health,
|
|
||||||
mana: characterTable.mana,
|
|
||||||
}).from(characterTable).where(and(eq(characterTable.id, parseInt(id, 10)), eq(characterTable.owner, session.user.id))).get();
|
|
||||||
|
|
||||||
if(character !== undefined)
|
|
||||||
{
|
|
||||||
return character as CharacterVariables;
|
|
||||||
}
|
|
||||||
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
45
server/api/character/[id]/variables.post.ts
Normal file
45
server/api/character/[id]/variables.post.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterTable } from '~/db/schema';
|
||||||
|
import { CharacterVariablesValidation } from '~/shared/character.util';
|
||||||
|
import type { CharacterVariables } from '~/types/character';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, CharacterVariablesValidation.safeParse);
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
console.error(body.error);
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
throw body.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, parseInt(id, 10))).get();
|
||||||
|
|
||||||
|
if(!old)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user || old.owner !== session.user.id || session.user.state !== 1)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(characterTable).set({
|
||||||
|
variables: body.data
|
||||||
|
}).where(eq(characterTable.id, parseInt(id, 10))).run();
|
||||||
|
|
||||||
|
setResponseStatus(e, 200);
|
||||||
|
return;
|
||||||
|
});
|
||||||
@@ -35,7 +35,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
return data.content;
|
return data.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
catch(_e)
|
catch(_e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
|
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
|
||||||
<div style="margin-left: auto; margin-right: auto; text-align: center;">
|
<div style="margin-left: auto; margin-right: auto; text-align: center;">
|
||||||
<a href="https://d-any.com">
|
<a style="display: inline-block;" href="https://d-any.com">
|
||||||
<img src="https://d-any.com/logo.light.png" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
|
<img src="https://d-any.com/logo.light.png" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
|
||||||
<span style="margin-inline-end: 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
|
<span style="margin-inline-end: 1rem; font-size: 1.5rem; color: black; text-decoration: none; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 1rem;">
|
<div style="padding: 1rem;">
|
||||||
@@ -11,6 +11,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="background-color: #171717;">
|
<div style="background-color: #171717;">
|
||||||
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime - d[any] - 2024</p>
|
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime / d[any] - 2024 / 2025</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
25
server/middleware/compress.ts
Normal file
25
server/middleware/compress.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineEventHandler, setResponseHeader } from 'h3';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const acceptEncoding = event.headers.get('accept-encoding') || '';
|
||||||
|
if (!acceptEncoding.includes('zstd')) return;
|
||||||
|
|
||||||
|
const _end = event.node.res.end;
|
||||||
|
//@ts-expect-error
|
||||||
|
event.node.res.end = async (body: any, ...args: any[]) => {
|
||||||
|
const buffer = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
||||||
|
|
||||||
|
if(buffer)
|
||||||
|
{
|
||||||
|
setResponseHeader(event, "Content-Encoding", "zstd");
|
||||||
|
setResponseHeader(event, "Vary", "Accept-Encoding");
|
||||||
|
//@ts-expect-error
|
||||||
|
_end.call(event.node.res, await Bun.zstdCompress(buffer), ...args);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//@ts-expect-error
|
||||||
|
_end.call(event.node.res, body, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -17,10 +17,9 @@ export const templates: Record<string, { component: any, subject: string }> = {
|
|||||||
import type Mail from 'nodemailer/lib/mailer';
|
import type Mail from 'nodemailer/lib/mailer';
|
||||||
interface MailPayload
|
interface MailPayload
|
||||||
{
|
{
|
||||||
type: 'mail'
|
to: string[];
|
||||||
to: string[]
|
template: string;
|
||||||
template: string
|
data: Record<string, any>;
|
||||||
data: Record<string, any>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = nodemailer.createTransport({
|
const transport = nodemailer.createTransport({
|
||||||
@@ -55,51 +54,59 @@ if(process.env.NODE_ENV === 'production')
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function(e: TaskEvent) {
|
export default defineTask({
|
||||||
try {
|
meta: {
|
||||||
if(e.payload.type !== 'mail')
|
name: 'mail',
|
||||||
{
|
description: ''
|
||||||
throw new Error(`Données inconnues`);
|
},
|
||||||
|
run: async ({ payload, context }) => {
|
||||||
|
try {
|
||||||
|
if(payload.type !== 'mail')
|
||||||
|
{
|
||||||
|
throw new Error(`Données inconnues`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailPayload = payload.data as MailPayload;
|
||||||
|
const template = templates[mailPayload.template];
|
||||||
|
|
||||||
|
console.log(mailPayload);
|
||||||
|
|
||||||
|
if(!template)
|
||||||
|
{
|
||||||
|
throw new Error(`Modèle de mail ${mailPayload.template} inconnu`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.time('Generating HTML');
|
||||||
|
const mail: Mail.Options = {
|
||||||
|
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||||
|
to: mailPayload.to,
|
||||||
|
html: await render(template.component, mailPayload.data),
|
||||||
|
subject: template.subject,
|
||||||
|
textEncoding: 'quoted-printable',
|
||||||
|
};
|
||||||
|
console.timeEnd('Generating HTML');
|
||||||
|
|
||||||
|
if(mail.html === '')
|
||||||
|
return { result: false, error: new Error("Invalid content") };
|
||||||
|
|
||||||
|
console.time('Sending Mail');
|
||||||
|
const status = await transport.sendMail(mail);
|
||||||
|
console.timeEnd('Sending Mail');
|
||||||
|
|
||||||
|
if(status.rejected.length > 0)
|
||||||
|
{
|
||||||
|
return { result: false, error: status.response, details: status.rejectedErrors };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: true };
|
||||||
}
|
}
|
||||||
|
catch(e)
|
||||||
const payload = e.payload as MailPayload;
|
|
||||||
const template = templates[payload.template];
|
|
||||||
|
|
||||||
if(!template)
|
|
||||||
{
|
{
|
||||||
throw new Error(`Modèle de mail ${payload.template} inconnu`);
|
console.error(e);
|
||||||
|
return { result: false, error: e };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.time('Generating HTML');
|
|
||||||
const mail: Mail.Options = {
|
|
||||||
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
|
||||||
to: payload.to,
|
|
||||||
html: await render(template.component, payload.data),
|
|
||||||
subject: template.subject,
|
|
||||||
textEncoding: 'quoted-printable',
|
|
||||||
};
|
|
||||||
console.timeEnd('Generating HTML');
|
|
||||||
|
|
||||||
if(mail.html === '')
|
|
||||||
return { result: false, error: new Error("Invalid content") };
|
|
||||||
|
|
||||||
console.time('Sending Mail');
|
|
||||||
const status = await transport.sendMail(mail);
|
|
||||||
console.timeEnd('Sending Mail');
|
|
||||||
|
|
||||||
if(status.rejected.length > 0)
|
|
||||||
{
|
|
||||||
return { result: false, error: status.response, details: status.rejectedErrors };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { result: true };
|
|
||||||
}
|
}
|
||||||
catch(e)
|
})
|
||||||
{
|
|
||||||
console.error(e);
|
|
||||||
return { result: false, error: e };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function render(component: any, data: Record<string, any>): Promise<string>
|
async function render(component: any, data: Record<string, any>): Promise<string>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ function reshapeContent(content: string, type: FileType): string | null
|
|||||||
return content;
|
return content;
|
||||||
case "canvas":
|
case "canvas":
|
||||||
const data = JSON.parse(content) as CanvasContent;
|
const data = JSON.parse(content) as CanvasContent;
|
||||||
data.edges?.forEach(e => { console.log(e.color); e.color = typeof e.color === 'string' ? getColor(e.color) : undefined; console.log(e.color); });
|
data.edges?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||||
data.nodes?.forEach(e => { console.log(e.color); e.color = typeof e.color === 'string' ? getColor(e.color) : undefined; console.log(e.color); });
|
data.nodes?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
default:
|
default:
|
||||||
case 'folder':
|
case 'folder':
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export function hasPermissions(userPermissions: string[], neededPermissions: str
|
|||||||
{
|
{
|
||||||
for(let i = 0; i < neededPermissions.length; i++)
|
for(let i = 0; i < neededPermissions.length; i++)
|
||||||
{
|
{
|
||||||
const list = neededPermissions[i].split(' ');
|
const list = neededPermissions[i]!.split(' ');
|
||||||
|
|
||||||
if(list.every(e => userPermissions.includes(e)))
|
if(list.every(e => userPermissions.includes(e)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,78 @@
|
|||||||
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
|
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
|
||||||
import { clamp, lerp } from "#shared/general.util";
|
import { clamp, lerp } from "#shared/general.util";
|
||||||
import { dom, icon, svg, text } from "#shared/dom.util";
|
import { dom, icon, svg } from "#shared/dom.util";
|
||||||
import render from "#shared/markdown.util";
|
import render from "#shared/markdown.util";
|
||||||
import { popper, tooltip } from "#shared/floating.util";
|
import { tooltip } from "#shared/floating.util";
|
||||||
import { History } from "#shared/history.util";
|
import { History } from "#shared/history.util";
|
||||||
import { preview } from "#shared/proses";
|
import { preview } from "#shared/proses";
|
||||||
import { SpatialGrid } from "#shared/physics.util";
|
import { SpatialGrid } from "#shared/physics.util";
|
||||||
import type { CanvasPreferences } from "~/types/general";
|
import type { CanvasPreferences } from "~/types/general";
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
stroke-light-red
|
||||||
|
stroke-light-orange
|
||||||
|
stroke-light-yellow
|
||||||
|
stroke-light-green
|
||||||
|
stroke-light-cyan
|
||||||
|
stroke-light-purple
|
||||||
|
dark:stroke-dark-red
|
||||||
|
dark:stroke-dark-orange
|
||||||
|
dark:stroke-dark-yellow
|
||||||
|
dark:stroke-dark-green
|
||||||
|
dark:stroke-dark-cyan
|
||||||
|
dark:stroke-dark-purple
|
||||||
|
fill-light-red
|
||||||
|
fill-light-orange
|
||||||
|
fill-light-yellow
|
||||||
|
fill-light-green
|
||||||
|
fill-light-cyan
|
||||||
|
fill-light-purple
|
||||||
|
dark:fill-dark-red
|
||||||
|
dark:fill-dark-orange
|
||||||
|
dark:fill-dark-yellow
|
||||||
|
dark:fill-dark-green
|
||||||
|
dark:fill-dark-cyan
|
||||||
|
dark:fill-dark-purple
|
||||||
|
bg-light-red
|
||||||
|
bg-light-orange
|
||||||
|
bg-light-yellow
|
||||||
|
bg-light-green
|
||||||
|
bg-light-cyan
|
||||||
|
bg-light-purple
|
||||||
|
dark:bg-dark-red
|
||||||
|
dark:bg-dark-orange
|
||||||
|
dark:bg-dark-yellow
|
||||||
|
dark:bg-dark-green
|
||||||
|
dark:bg-dark-cyan
|
||||||
|
dark:bg-dark-purple
|
||||||
|
border-light-red
|
||||||
|
border-light-orange
|
||||||
|
border-light-yellow
|
||||||
|
border-light-green
|
||||||
|
border-light-cyan
|
||||||
|
border-light-purple
|
||||||
|
dark:border-dark-red
|
||||||
|
dark:border-dark-orange
|
||||||
|
dark:border-dark-yellow
|
||||||
|
dark:border-dark-green
|
||||||
|
dark:border-dark-cyan
|
||||||
|
dark:border-dark-purple
|
||||||
|
outline-light-red
|
||||||
|
outline-light-orange
|
||||||
|
outline-light-yellow
|
||||||
|
outline-light-green
|
||||||
|
outline-light-cyan
|
||||||
|
outline-light-purple
|
||||||
|
dark:outline-dark-red
|
||||||
|
dark:outline-dark-orange
|
||||||
|
dark:outline-dark-yellow
|
||||||
|
dark:outline-dark-green
|
||||||
|
dark:outline-dark-cyan
|
||||||
|
dark:outline-dark-purple
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
export type Direction = 'bottom' | 'top' | 'left' | 'right';
|
export type Direction = 'bottom' | 'top' | 'left' | 'right';
|
||||||
export type Position = { x: number, y: number };
|
export type Position = { x: number, y: number };
|
||||||
export type Box = Position & { width: number, height: number };
|
export type Box = Position & { width: number, height: number };
|
||||||
@@ -173,7 +238,6 @@ export class Node extends EventTarget
|
|||||||
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
|
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NodeEditable extends Node
|
export class NodeEditable extends Node
|
||||||
{
|
{
|
||||||
edges: Set<EdgeEditable> = new Set();
|
edges: Set<EdgeEditable> = new Set();
|
||||||
@@ -418,6 +482,8 @@ export class Canvas
|
|||||||
]),
|
]),
|
||||||
]), this.transform,
|
]), this.transform,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
console.log(this.nodes.length, this.edges.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected computeLimits()
|
protected computeLimits()
|
||||||
@@ -440,6 +506,7 @@ export class Canvas
|
|||||||
mount()
|
mount()
|
||||||
{
|
{
|
||||||
const dragMove = (e: MouseEvent) => {
|
const dragMove = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
this.dragMove(e);
|
this.dragMove(e);
|
||||||
};
|
};
|
||||||
const dragEnd = (e: MouseEvent) => {
|
const dragEnd = (e: MouseEvent) => {
|
||||||
@@ -467,11 +534,11 @@ export class Canvas
|
|||||||
this.firstX = pos.x;
|
this.firstX = pos.x;
|
||||||
this.firstY = pos.y;
|
this.firstY = pos.y;
|
||||||
|
|
||||||
window.addEventListener('mouseup', dragEnd, { passive: true });
|
window.addEventListener('mouseup', dragEnd);
|
||||||
window.addEventListener('mousemove', dragMove, { passive: true });
|
window.addEventListener('mousemove', dragMove);
|
||||||
|
|
||||||
this.dragStart(e);
|
this.dragStart(e);
|
||||||
}, { passive: true });
|
});
|
||||||
this.container.addEventListener('wheel', (e) => {
|
this.container.addEventListener('wheel', (e) => {
|
||||||
if((this._zoom >= Canvas.maxZoom && e.deltaY < 0) || (this._zoom <= this.containZoom && e.deltaY > 0))
|
if((this._zoom >= Canvas.maxZoom && e.deltaY < 0) || (this._zoom <= this.containZoom && e.deltaY > 0))
|
||||||
return;
|
return;
|
||||||
@@ -490,10 +557,10 @@ export class Canvas
|
|||||||
this.lastDistance = distance(e.touches);
|
this.lastDistance = distance(e.touches);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.addEventListener('touchend', touchend, { passive: true });
|
this.container.addEventListener('touchend', touchend);
|
||||||
this.container.addEventListener('touchcancel', touchcancel, { passive: true });
|
this.container.addEventListener('touchcancel', touchcancel);
|
||||||
this.container.addEventListener('touchmove', touchmove, { passive: true });
|
this.container.addEventListener('touchmove', touchmove);
|
||||||
}, { passive: true });
|
});
|
||||||
const touchend = (e: TouchEvent) => {
|
const touchend = (e: TouchEvent) => {
|
||||||
if(e.touches.length > 1)
|
if(e.touches.length > 1)
|
||||||
{
|
{
|
||||||
@@ -515,6 +582,7 @@ export class Canvas
|
|||||||
this.container.removeEventListener('touchmove', touchmove);
|
this.container.removeEventListener('touchmove', touchmove);
|
||||||
};
|
};
|
||||||
const touchmove = (e: TouchEvent) => {
|
const touchmove = (e: TouchEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
const pos = center(e.touches);
|
const pos = center(e.touches);
|
||||||
this._x = this.visualX = this._x - (this.lastX - pos.x) / this._zoom;
|
this._x = this.visualX = this._x - (this.lastX - pos.x) / this._zoom;
|
||||||
this._y = this.visualY = this._y - (this.lastY - pos.y) / this._zoom;
|
this._y = this.visualY = this._y - (this.lastY - pos.y) / this._zoom;
|
||||||
@@ -529,19 +597,22 @@ export class Canvas
|
|||||||
this._zoom = clamp(this._zoom * diff, this.containZoom, Canvas.maxZoom);
|
this._zoom = clamp(this._zoom * diff, this.containZoom, Canvas.maxZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateTransform();
|
this.updateTransform(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.computeLimits();
|
this.computeLimits();
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateTransform()
|
protected updateTransform(debounce: boolean)
|
||||||
{
|
{
|
||||||
this.transform.style.transform = `scale3d(${this.visualZoom}, ${this.visualZoom}, 1) translate3d(${this.visualX}px, ${this.visualY}px, 0)`;
|
this.transform.style.transform = `scale3d(${this.visualZoom}, ${this.visualZoom}, 1) translate3d(${this.visualX}px, ${this.visualY}px, 0)`;
|
||||||
|
|
||||||
clearTimeout(this.debouncedTimeout);
|
if(debounce)
|
||||||
this.debouncedTimeout = setTimeout(this.updateScale.bind(this), 150);
|
{
|
||||||
|
clearTimeout(this.debouncedTimeout);
|
||||||
|
this.debouncedTimeout = setTimeout(this.updateScale.bind(this), 50);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateScale()
|
private updateScale()
|
||||||
@@ -562,7 +633,7 @@ export class Canvas
|
|||||||
this.visualY = lerp(e, oldY, y);
|
this.visualY = lerp(e, oldY, y);
|
||||||
this.visualZoom = lerp(e, oldZoom, zoom);
|
this.visualZoom = lerp(e, oldZoom, zoom);
|
||||||
|
|
||||||
this.updateTransform();
|
this.updateTransform(true);
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +651,7 @@ export class Canvas
|
|||||||
this.lastX = e.clientX;
|
this.lastX = e.clientX;
|
||||||
this.lastY = e.clientY;
|
this.lastY = e.clientY;
|
||||||
|
|
||||||
this.updateTransform();
|
this.updateTransform(false);
|
||||||
}
|
}
|
||||||
protected dragEnd(e: MouseEvent) {}
|
protected dragEnd(e: MouseEvent) {}
|
||||||
|
|
||||||
@@ -846,9 +917,9 @@ export class CanvasEditor extends Canvas
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
override updateTransform()
|
override updateTransform(debounce: boolean = true)
|
||||||
{
|
{
|
||||||
super.updateTransform();
|
super.updateTransform(debounce);
|
||||||
|
|
||||||
this.pattern.parentElement?.classList.toggle('hidden', !this.preferences.value.gridSnap);
|
this.pattern.parentElement?.classList.toggle('hidden', !this.preferences.value.gridSnap);
|
||||||
if(this.preferences.value.gridSnap)
|
if(this.preferences.value.gridSnap)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
|||||||
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponType } from "~/types/character";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import characterConfig from '#shared/character-config.json';
|
import characterConfig from '#shared/character-config.json';
|
||||||
import proses, { preview } from "#shared/proses";
|
import proses, { preview } from "#shared/proses";
|
||||||
import { button, buttongroup, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
import { button, buttongroup, floater, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
||||||
import { div, dom, icon, text } from "#shared/dom.util";
|
import { div, dom, icon, span, text } from "#shared/dom.util";
|
||||||
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||||
import { clamp } from "#shared/general.util";
|
import { clamp } from "#shared/general.util";
|
||||||
import markdown from "#shared/markdown.util";
|
import markdown from "#shared/markdown.util";
|
||||||
import { getText } from "./i18n";
|
import { getText } from "./i18n";
|
||||||
import type { User } from "~/types/auth";
|
import type { User } from "~/types/auth";
|
||||||
|
import { MarkdownEditor } from "./editor.util";
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
|
|||||||
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
|
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
|
||||||
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
|
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
|
||||||
export const RESISTANCES = ['stun','bleed','poison','fear','influence','charm','possesion','precision','knowledge','instinct'] as const;
|
export const RESISTANCES = ['stun','bleed','poison','fear','influence','charm','possesion','precision','knowledge','instinct'] as const;
|
||||||
|
export const DAMAGE_TYPES = ['slashing', 'piercing', 'bludgening', 'magic', 'fire', 'thunder', 'cold'] as const;
|
||||||
|
export const WEAPON_TYPES = ["light", "shield", "heavy", "classic", "throw", "natural", "twohanded", "finesse", "reach", "projectile"] as const;
|
||||||
|
|
||||||
export const defaultCharacter: Character = {
|
export const defaultCharacter: Character = {
|
||||||
id: -1,
|
id: -1,
|
||||||
@@ -29,7 +32,7 @@ export const defaultCharacter: Character = {
|
|||||||
people: undefined,
|
people: undefined,
|
||||||
level: 1,
|
level: 1,
|
||||||
|
|
||||||
training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
||||||
leveling: { 1: 0 },
|
leveling: { 1: 0 },
|
||||||
abilities: {},
|
abilities: {},
|
||||||
choices: {},
|
choices: {},
|
||||||
@@ -40,6 +43,7 @@ export const defaultCharacter: Character = {
|
|||||||
items: [],
|
items: [],
|
||||||
exhaustion: 0,
|
exhaustion: 0,
|
||||||
sickness: [],
|
sickness: [],
|
||||||
|
poisons: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
owner: -1,
|
owner: -1,
|
||||||
@@ -113,7 +117,10 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|||||||
magicelement: 0,
|
magicelement: 0,
|
||||||
magicinstinct: 0,
|
magicinstinct: 0,
|
||||||
},
|
},
|
||||||
bonus: {},
|
bonus: {
|
||||||
|
abilities: {},
|
||||||
|
defense: {},
|
||||||
|
},
|
||||||
resistance: {},
|
resistance: {},
|
||||||
initiative: 0,
|
initiative: 0,
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
@@ -125,7 +132,7 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|||||||
spells: [],
|
spells: [],
|
||||||
},
|
},
|
||||||
aspect: "",
|
aspect: "",
|
||||||
notes: character.notes ?? "",
|
notes: Object.assign({ public: '', private: '' }, character.notes),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mainStatTexts: Record<MainStat, string> = {
|
export const mainStatTexts: Record<MainStat, string> = {
|
||||||
@@ -158,6 +165,10 @@ export const elementTexts: Record<SpellElement, { class: string, text: string }>
|
|||||||
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
|
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
|
||||||
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
|
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
|
||||||
};
|
};
|
||||||
|
export const 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> = {
|
export const alignmentTexts: Record<Alignment, string> = {
|
||||||
'loyal_good': 'Loyal bon',
|
'loyal_good': 'Loyal bon',
|
||||||
@@ -204,30 +215,60 @@ export const resistanceTexts: Record<Resistance, string> = {
|
|||||||
'knowledge': 'Sorts de savoir',
|
'knowledge': 'Sorts de savoir',
|
||||||
'instinct': 'Sorts d\'instinct',
|
'instinct': 'Sorts d\'instinct',
|
||||||
};
|
};
|
||||||
|
export const damageTypeTexts: Record<DamageType, string> = {
|
||||||
|
'bludgening': 'Contondant',
|
||||||
|
'cold': 'Froid',
|
||||||
|
'fire': 'Feu',
|
||||||
|
'magic': 'Magique',
|
||||||
|
'piercing': 'Perçant',
|
||||||
|
'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 CharacterVariablesValidation = z.object({
|
||||||
|
health: z.number(),
|
||||||
|
mana: z.number(),
|
||||||
|
exhaustion: z.number(),
|
||||||
|
|
||||||
|
sickness: z.array(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
state: z.number().min(1).max(7).or(z.literal(true)),
|
||||||
|
})),
|
||||||
|
poisons: z.array(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
state: z.number().min(1).max(7).or(z.literal(true)),
|
||||||
|
})),
|
||||||
|
spells: z.array(z.string()),
|
||||||
|
items: z.array(z.string()),
|
||||||
|
});
|
||||||
export const CharacterValidation = z.object({
|
export const CharacterValidation = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
people: z.string().nullable(),
|
people: z.string().nullable(),
|
||||||
level: z.number().min(1).max(20),
|
level: z.number().min(1).max(20),
|
||||||
aspect: z.number().nullable().optional(),
|
aspect: z.number().nullable().optional(),
|
||||||
notes: z.string().nullable().optional(),
|
notes: CharacterNotesValidation,
|
||||||
training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number().optional())),
|
training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number().optional())),
|
||||||
leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()),
|
leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()),
|
||||||
abilities: z.record(z.enum(ABILITIES), z.number().optional()),
|
abilities: z.record(z.enum(ABILITIES), z.number().optional()),
|
||||||
choices: z.record(z.string(), z.array(z.number())),
|
choices: z.record(z.string(), z.array(z.number())),
|
||||||
variables: z.object({
|
variables: CharacterVariablesValidation,
|
||||||
health: z.number(),
|
|
||||||
mana: z.number(),
|
|
||||||
exhaustion: z.number(),
|
|
||||||
|
|
||||||
sickness: z.array(z.object({
|
|
||||||
id: z.string(),
|
|
||||||
state: z.number().min(1).max(7).or(z.literal(true)),
|
|
||||||
})),
|
|
||||||
spells: z.array(z.string()),
|
|
||||||
equipment: z.array(z.string()),
|
|
||||||
}),
|
|
||||||
owner: z.number(),
|
owner: z.number(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
visibility: z.enum(["public", "private"]),
|
visibility: z.enum(["public", "private"]),
|
||||||
@@ -240,7 +281,16 @@ export class CharacterCompiler
|
|||||||
{
|
{
|
||||||
protected _character!: Character;
|
protected _character!: Character;
|
||||||
protected _result!: CompiledCharacter;
|
protected _result!: CompiledCharacter;
|
||||||
protected _buffer: Record<string, PropertySum> = {};
|
protected _buffer: Record<string, PropertySum> = {
|
||||||
|
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/intelligence': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/curiosity': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
};
|
||||||
|
private _variableDirty: boolean = false;
|
||||||
|
|
||||||
constructor(character: Character)
|
constructor(character: Character)
|
||||||
{
|
{
|
||||||
@@ -251,7 +301,15 @@ export class CharacterCompiler
|
|||||||
{
|
{
|
||||||
this._character = value;
|
this._character = value;
|
||||||
this._result = defaultCompiledCharacter(value);
|
this._result = defaultCompiledCharacter(value);
|
||||||
this._buffer = {};
|
this._buffer = {
|
||||||
|
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/intelligence': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/curiosity': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||||
|
};
|
||||||
|
|
||||||
if(value.people !== undefined)
|
if(value.people !== undefined)
|
||||||
{
|
{
|
||||||
@@ -299,6 +357,29 @@ export class CharacterCompiler
|
|||||||
{
|
{
|
||||||
this._character.variables[prop] = value;
|
this._character.variables[prop] = value;
|
||||||
this._result.variables[prop] = value;
|
this._result.variables[prop] = value;
|
||||||
|
this._variableDirty = true;
|
||||||
|
}
|
||||||
|
saveVariables()
|
||||||
|
{
|
||||||
|
if(this._variableDirty)
|
||||||
|
{
|
||||||
|
this._variableDirty = false;
|
||||||
|
useRequestFetch()(`/api/character/${this.character.id}/variables`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: this._character.variables,
|
||||||
|
}).then(() => {}).catch(() => {
|
||||||
|
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveNotes()
|
||||||
|
{
|
||||||
|
return useRequestFetch()(`/api/character/${this.character.id}/notes`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: this._character.notes,
|
||||||
|
}).then(() => {}).catch(() => {
|
||||||
|
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
protected add(feature?: string)
|
protected add(feature?: string)
|
||||||
{
|
{
|
||||||
@@ -324,8 +405,8 @@ export class CharacterCompiler
|
|||||||
case "list":
|
case "list":
|
||||||
if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
|
if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
|
||||||
this._result.lists[feature.list]!.push(feature.item);
|
this._result.lists[feature.list]!.push(feature.item);
|
||||||
else
|
else if(feature.action === 'remove')
|
||||||
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter((e: string) => e !== feature.item);
|
this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e === feature.item), 1);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case "value":
|
case "value":
|
||||||
@@ -336,6 +417,9 @@ export class CharacterCompiler
|
|||||||
this._buffer[feature.property]!.min = -Infinity;
|
this._buffer[feature.property]!.min = -Infinity;
|
||||||
this._buffer[feature.property]!._dirty = true;
|
this._buffer[feature.property]!._dirty = true;
|
||||||
|
|
||||||
|
if(feature.property.startsWith('modifier/'))
|
||||||
|
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case "choice":
|
case "choice":
|
||||||
const choice = this._character.choices[feature.id];
|
const choice = this._character.choices[feature.id];
|
||||||
@@ -358,8 +442,8 @@ export class CharacterCompiler
|
|||||||
case "list":
|
case "list":
|
||||||
if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
|
if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
|
||||||
this._result.lists[feature.list]!.push(feature.item);
|
this._result.lists[feature.list]!.push(feature.item);
|
||||||
else
|
else if(feature.action === 'add')
|
||||||
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter(e => e !== feature.item);
|
this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e === feature.item), 1)
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case "value":
|
case "value":
|
||||||
@@ -370,6 +454,9 @@ export class CharacterCompiler
|
|||||||
this._buffer[feature.property]!.min = -Infinity;
|
this._buffer[feature.property]!.min = -Infinity;
|
||||||
this._buffer[feature.property]!._dirty = true;
|
this._buffer[feature.property]!._dirty = true;
|
||||||
|
|
||||||
|
if(feature.property.startsWith('modifier/'))
|
||||||
|
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case "choice":
|
case "choice":
|
||||||
const choice = this._character.choices[feature.id];
|
const choice = this._character.choices[feature.id];
|
||||||
@@ -382,57 +469,53 @@ export class CharacterCompiler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected compile(properties: string[])
|
protected compile(queue: string[])
|
||||||
{
|
{
|
||||||
const queue = properties;
|
|
||||||
for(let i = 0; i < queue.length; i++)
|
for(let i = 0; i < queue.length; i++)
|
||||||
{
|
{
|
||||||
|
if(queue[i] === undefined || queue[i] === "") continue;
|
||||||
|
|
||||||
const property = queue[i]!;
|
const property = queue[i]!;
|
||||||
const buffer = this._buffer[property];
|
const buffer = this._buffer[property];
|
||||||
|
|
||||||
if(property === "")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if(buffer && buffer._dirty === true)
|
if(buffer && buffer._dirty === true)
|
||||||
{
|
{
|
||||||
let sum = 0, shortcut = false;
|
let sum = 0, shortcut = false;
|
||||||
for(let i = 0; i < buffer.list.length; i++)
|
for(let j = 0; j < buffer.list.length; j++)
|
||||||
{
|
{
|
||||||
if(typeof buffer.list[i]!.value === 'string') // Add or set a modifier
|
const item = buffer.list[j];
|
||||||
|
if(!item)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(typeof item.value === 'string') // Add or set a modifier
|
||||||
{
|
{
|
||||||
const modifier = this._buffer[buffer.list[i]!.value as string];
|
const modifier = this._buffer[item.value as string]!;
|
||||||
if(!modifier)
|
if(modifier._dirty)
|
||||||
{
|
|
||||||
queue.push(property);
|
|
||||||
shortcut = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if(modifier._dirty)
|
|
||||||
{
|
{
|
||||||
//Put it back in queue since its dependencies haven't been resolved yet
|
//Put it back in queue since its dependencies haven't been resolved yet
|
||||||
queue.push(buffer.list[i]!.value as string);
|
queue.push(item.value as string);
|
||||||
queue.push(property);
|
queue.push(property);
|
||||||
shortcut = true;
|
shortcut = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if(buffer.list[i]?.operation === 'add')
|
if(item.operation === 'add')
|
||||||
sum += modifier.value;
|
sum += modifier.value;
|
||||||
else if(buffer.list[i]?.operation === 'set')
|
else if(item.operation === 'set')
|
||||||
sum = modifier.value;
|
sum = modifier.value;
|
||||||
else if(buffer.list[i]?.operation === 'min')
|
else if(item.operation === 'min')
|
||||||
this._buffer[property]!.min = modifier.value;
|
buffer.min = modifier.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if(buffer.list[i]?.operation === 'add')
|
if(item.operation === 'add')
|
||||||
sum += buffer.list[i]!.value as number;
|
sum += item.value as number;
|
||||||
else if(buffer.list[i]?.operation === 'set')
|
else if(item.operation === 'set')
|
||||||
sum = buffer.list[i]!.value as number;
|
sum = item.value as number;
|
||||||
else if(buffer.list[i]?.operation === 'min')
|
else if(item.operation === 'min')
|
||||||
this._buffer[property]!.min = buffer.list[i]!.value as number;
|
buffer.min = item.value as number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +576,14 @@ export class CharacterBuilder extends CharacterCompiler
|
|||||||
}
|
}
|
||||||
private render()
|
private render()
|
||||||
{
|
{
|
||||||
|
const publicNotes = new MarkdownEditor(), privateNotes = new MarkdownEditor();
|
||||||
|
this._character.notes ??= { public: '', private: '' };
|
||||||
|
|
||||||
|
publicNotes.onChange = (v) => this._character.notes!.public = this._result.notes.public = v;
|
||||||
|
privateNotes.onChange = (v) => this._character.notes!.private = this._result.notes.private = v;
|
||||||
|
publicNotes.content = this._character.notes.public!;
|
||||||
|
privateNotes.content = this._character.notes.private!;
|
||||||
|
|
||||||
this._steps = [
|
this._steps = [
|
||||||
PeoplePicker,
|
PeoplePicker,
|
||||||
LevelPicker,
|
LevelPicker,
|
||||||
@@ -510,7 +601,7 @@ export class CharacterBuilder extends CharacterCompiler
|
|||||||
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
|
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
|
||||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||||
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
|
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
|
||||||
div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [ tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
|
div('flex flex-row gap-2', [ floater(tooltip(button(icon('radix-icons:pencil-2', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes publics', 'left'), [ publicNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes publics', position: 'bottom-start' }), floater(tooltip(button(icon('radix-icons:eye-none', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes privés', 'right'), [ privateNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes privés', position: 'bottom-start' }) ]), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [ tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
|
||||||
]),
|
]),
|
||||||
this._content,
|
this._content,
|
||||||
]));
|
]));
|
||||||
@@ -542,7 +633,7 @@ export class CharacterBuilder extends CharacterCompiler
|
|||||||
}
|
}
|
||||||
async save(leave: boolean = true)
|
async save(leave: boolean = true)
|
||||||
{
|
{
|
||||||
if(this.id === 'new')
|
if(this.id === 'new' || this.id === '-1')
|
||||||
{
|
{
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.id = this._character.id = this._result.id = await useRequestFetch()(`/api/character`, {
|
this.id = this._character.id = this._result.id = await useRequestFetch()(`/api/character`, {
|
||||||
@@ -1021,8 +1112,6 @@ class AbilityPicker extends BuilderTab
|
|||||||
const values = builder.values, compiled = builder.compiled;
|
const values = builder.values, compiled = builder.compiled;
|
||||||
const abilities = Object.values(builder.character.abilities).reduce((p, v) => p + v, 0);
|
const abilities = Object.values(builder.character.abilities).reduce((p, v) => p + v, 0);
|
||||||
|
|
||||||
console.log(ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)));
|
|
||||||
|
|
||||||
return ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)).every(e => e) && (values.ability ?? 0) - abilities >= 0;
|
return ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)).every(e => e) && (values.ability ?? 0) - abilities >= 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1142,10 +1231,13 @@ class AspectPicker extends BuilderTab
|
|||||||
|
|
||||||
export class CharacterSheet
|
export class CharacterSheet
|
||||||
{
|
{
|
||||||
|
user: ComputedRef<User | null>;
|
||||||
character?: CharacterCompiler;
|
character?: CharacterCompiler;
|
||||||
container: HTMLElement = div();
|
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
|
||||||
|
tabs?: HTMLDivElement & { refresh: () => void };
|
||||||
constructor(id: string, user: ComputedRef<User | null>)
|
constructor(id: string, user: ComputedRef<User | null>)
|
||||||
{
|
{
|
||||||
|
this.user = user;
|
||||||
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
|
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
|
||||||
this.container.replaceChildren(load);
|
this.container.replaceChildren(load);
|
||||||
useRequestFetch()(`/api/character/${id}`).then(character => {
|
useRequestFetch()(`/api/character/${id}`).then(character => {
|
||||||
@@ -1159,9 +1251,17 @@ export class CharacterSheet
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
throw new Error();
|
||||||
//ERROR
|
}).catch((e) => {
|
||||||
}
|
console.error(e);
|
||||||
|
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
|
||||||
|
span('text-2xl font-bold tracking-wider', 'Personnage introuvable'),
|
||||||
|
span(undefined, 'Ce personnage n\'existe pas ou est privé.'),
|
||||||
|
div('flex flex-row gap-4 justify-center items-center', [
|
||||||
|
button(text('Personnages publics'), () => useRouter().push({ name: 'character-list' }), 'px-2 py-1'),
|
||||||
|
button(text('Créer un personange'), () => useRouter().push({ name: 'character-id-edit', params: { id: 'new' } }), 'px-2 py-1')
|
||||||
|
])
|
||||||
|
]))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
render()
|
render()
|
||||||
@@ -1170,8 +1270,38 @@ export class CharacterSheet
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const character = this.character.compiled;
|
const character = this.character.compiled;
|
||||||
console.log(character);
|
|
||||||
this.container.replaceChildren(div('flex flex-col justify-center gap-1', [
|
const publicNotes = new MarkdownEditor();
|
||||||
|
const privateNotes = new MarkdownEditor();
|
||||||
|
|
||||||
|
const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
|
||||||
|
const saveLoading = loading('small');
|
||||||
|
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.character?.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
|
||||||
|
|
||||||
|
publicNotes.onChange = (v) => this.character!.character.notes!.public = v;
|
||||||
|
privateNotes.onChange = (v) => this.character!.character.notes!.private = v;
|
||||||
|
publicNotes.content = this.character!.character.notes!.public!;
|
||||||
|
privateNotes.content = this.character!.character.notes!.private!;
|
||||||
|
|
||||||
|
this.tabs = tabgroup([
|
||||||
|
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
|
||||||
|
|
||||||
|
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },
|
||||||
|
|
||||||
|
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
|
||||||
|
|
||||||
|
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
||||||
|
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
||||||
|
div('flex flex-col gap-2', [
|
||||||
|
div('flex flex-col gap-2 border-b border-light-35 dark:border-dark-35 pb-4', [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ publicNotes.dom ]) ]),
|
||||||
|
div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
|
||||||
|
])
|
||||||
|
] },
|
||||||
|
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px] h-full', content: 'overflow-auto' } });
|
||||||
|
this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full', [
|
||||||
div("flex flex-row gap-4 justify-between", [
|
div("flex flex-row gap-4 justify-between", [
|
||||||
div(),
|
div(),
|
||||||
|
|
||||||
@@ -1215,10 +1345,9 @@ export class CharacterSheet
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
div("self-center", [
|
div("self-center", [
|
||||||
/* user && user.id === character.owner ?
|
this.user.value && this.user.value.id === character.owner ?
|
||||||
button(icon("radix-icons:pencil-2"), () => {
|
button(icon("radix-icons:pencil-2"), () => useRouter().push({ name: 'character-id-edit', params: { id: this.character?.character.id } }), "p-1")
|
||||||
}, "icon")
|
: div()
|
||||||
: div() */
|
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@@ -1295,156 +1424,167 @@ export class CharacterSheet
|
|||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4", [
|
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4 h-0", [
|
||||||
div("flex flex-col gap-4 py-1 w-80", [
|
div("flex flex-col gap-4 py-1 w-60", [
|
||||||
div("flex flex-col py-1 gap-4", [
|
div("flex flex-col py-1 gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-4", [
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]),
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]),
|
||||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div("grid grid-cols-3 gap-2",
|
div("grid grid-cols-2 gap-2",
|
||||||
Object.entries(character.abilities).map(([ability, value]) =>
|
Object.entries(character.abilities).map(([ability, value]) =>
|
||||||
div("flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70", [
|
div("flex flex-row px-1 justify-between items-center", [
|
||||||
dom("span", { class: "font-bold text-base text-light-100 dark:text-dark-100", text: `+${value}` }),
|
span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability),
|
||||||
dom("span", { text: abilityTexts[ability as Ability] || ability })
|
span("font-bold text-base text-light-100 dark:text-dark-100", `+${value}`),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
div("flex flex-row items-center justify-center gap-4", [
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]),
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]),
|
||||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
||||||
]),
|
]),
|
||||||
|
|
||||||
character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
||||||
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères' }) : undefined,
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère' }) : undefined,
|
||||||
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet' }) : undefined,
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet' }) : undefined,
|
||||||
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles' }) : undefined,
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle' }) : undefined,
|
||||||
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes' }) : undefined,
|
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard' }) : undefined,
|
||||||
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées' }) : undefined,
|
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée' }) : undefined,
|
||||||
character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes' }) : undefined,
|
character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde' }) : undefined,
|
||||||
character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains' }) : undefined,
|
character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains' }) : undefined,
|
||||||
character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables' }) : undefined,
|
character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable' }) : undefined,
|
||||||
character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles' }) : undefined,
|
character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles' }) : undefined,
|
||||||
character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues' }) : undefined,
|
character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue' }) : undefined,
|
||||||
character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers' }) : undefined,
|
character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier' }) : undefined,
|
||||||
character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains' }) : undefined,
|
character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains' }) : undefined,
|
||||||
]) : undefined,
|
]) : undefined,
|
||||||
|
|
||||||
character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
||||||
character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères' }) : undefined,
|
character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', label: 'Armure légère' }) : undefined,
|
||||||
character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures' }) : undefined,
|
character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard' }) : undefined,
|
||||||
character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes' }) : undefined,
|
character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde' }) : undefined,
|
||||||
]) : undefined,
|
]) : undefined,
|
||||||
|
|
||||||
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
||||||
div('flex flex-row items-center gap-2', [ text('Précision'), dom('span', { text: character.spellranks.precision.toString(), class: 'font-bold' }) ]),
|
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', character.spellranks.precision.toString()) ]),
|
||||||
div('flex flex-row items-center gap-2', [ text('Savoir'), dom('span', { text: character.spellranks.knowledge.toString(), class: 'font-bold' }) ]),
|
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', character.spellranks.knowledge.toString()) ]),
|
||||||
div('flex flex-row items-center gap-2', [ text('Instinct'), dom('span', { text: character.spellranks.instinct.toString(), class: 'font-bold' }) ]),
|
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', character.spellranks.instinct.toString()) ]),
|
||||||
div('flex flex-row items-center gap-2', [ text('Oeuvres'), dom('span', { text: character.spellranks.arts.toString(), class: 'font-bold' }) ]),
|
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', character.spellranks.arts.toString()) ]),
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
div('border-l border-light-35 dark:border-dark-35'),
|
div('border-l border-light-35 dark:border-dark-35'),
|
||||||
|
|
||||||
tabgroup([
|
this.tabs,
|
||||||
{ id: 'actions', title: [ text('Actions') ], content: () => [
|
|
||||||
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-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
||||||
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
|
||||||
]),
|
|
||||||
|
|
||||||
div('flex flex-col gap-2', [
|
|
||||||
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
||||||
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
|
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
||||||
])) ?? [])
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
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-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
||||||
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
|
||||||
]),
|
|
||||||
|
|
||||||
div('flex flex-col gap-2', [
|
|
||||||
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
||||||
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
|
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
||||||
])) ?? [])
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
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-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
||||||
]),
|
|
||||||
|
|
||||||
div('flex flex-col gap-2', [
|
|
||||||
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
||||||
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
|
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
|
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
||||||
])) ?? [])
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
] },
|
|
||||||
|
|
||||||
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },
|
|
||||||
|
|
||||||
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
|
|
||||||
|
|
||||||
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
|
||||||
|
|
||||||
] },
|
|
||||||
|
|
||||||
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
|
||||||
|
|
||||||
] },
|
|
||||||
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } }),
|
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
actionsTab(character: CompiledCharacter)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
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-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||||
|
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
div('flex flex-col gap-2', [
|
||||||
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
||||||
|
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
|
||||||
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
||||||
|
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
|
])) ?? [])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
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-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||||
|
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
div('flex flex-col gap-2', [
|
||||||
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
||||||
|
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
|
||||||
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
||||||
|
markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
|
])) ?? [])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
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-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||||
|
]),
|
||||||
|
|
||||||
|
div('flex flex-col gap-2', [
|
||||||
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
||||||
|
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
|
||||||
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
|
||||||
|
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
|
])) ?? [])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
abilitiesTab(character: CompiledCharacter)
|
abilitiesTab(character: CompiledCharacter)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
|
...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
])) ?? []),
|
])) ?? []),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
spellTab(character: CompiledCharacter)
|
spellTab(character: CompiledCharacter)
|
||||||
{
|
{
|
||||||
|
let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';
|
||||||
|
|
||||||
|
const sort = (spells: Array<{ id: string, spell?: SpellConfig, source: string }>) => {
|
||||||
|
spells = spells.filter(e => !!e.spell);
|
||||||
|
switch(sortPreference)
|
||||||
|
{
|
||||||
|
case 'rank': return spells.sort((a, b) => a.spell!.rank - b.spell!.rank || SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!));
|
||||||
|
case 'type': return spells.sort((a, b) => a.spell!.type.localeCompare(b.spell!.type) || a.spell!.rank - b.spell!.rank);
|
||||||
|
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!) || a.spell!.rank - b.spell!.rank);
|
||||||
|
default: return spells;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const spells = sort([...(character.lists.spells ?? []).map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'feature' })), ...character.variables.spells.map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'player' }))]).map(e => ({...e, dom:
|
||||||
|
e.spell ? div('flex flex-col gap-2', [
|
||||||
|
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 ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]),
|
||||||
|
]) : undefined }));
|
||||||
return [
|
return [
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div('flex flex-row justify-between items-center', [
|
div('flex flex-row justify-between items-center', [
|
||||||
div('flex flex-row gap-2 items-center', [
|
div('flex flex-row gap-2 items-center', [
|
||||||
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
|
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
|
||||||
buttongroup([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: 'rank', class: { option: 'px-2 py-1 text-sm' } }),
|
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; this.tabs?.refresh(); } }),
|
||||||
|
]),
|
||||||
|
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'),
|
||||||
])
|
])
|
||||||
])
|
]),
|
||||||
|
div('flex flex-col gap-2', spells.map(e => e.dom))
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
spellPanel()
|
spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>)
|
||||||
{
|
{
|
||||||
if(!this.character)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const character = this.character.compiled;
|
|
||||||
const availableSpells = Object.values(config.spells).filter(spell => {
|
const availableSpells = Object.values(config.spells).filter(spell => {
|
||||||
if (spell.rank === 4) return false;
|
if (spell.rank === 4) return false;
|
||||||
if (character.spellranks[spell.type] < spell.rank) return false;
|
if (character.spellranks[spell.type] < spell.rank) return false;
|
||||||
@@ -1465,21 +1605,21 @@ export class CharacterSheet
|
|||||||
const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
|
const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
|
||||||
if(state === 'choosen')
|
if(state === 'choosen')
|
||||||
{
|
{
|
||||||
//this.character.variable('spells', character.variables.spells.filter(e => e !== spell.id)); //TO REWORK
|
this.character!.variable('spells', character.variables.spells.filter(e => e !== spell.id));
|
||||||
state = 'empty';
|
state = 'empty';
|
||||||
}
|
}
|
||||||
else if(state === 'empty')
|
else if(state === 'empty')
|
||||||
{
|
{
|
||||||
//this.character.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
|
this.character!.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
|
||||||
state = 'choosen';
|
state = 'choosen';
|
||||||
}
|
}
|
||||||
//character = compiler.compiled; //TO REWORK
|
|
||||||
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
|
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
|
||||||
textAmount.textContent = character.variables.spells.length.toString();
|
textAmount.textContent = character.variables.spells.length.toString();
|
||||||
|
this.tabs?.refresh();
|
||||||
}, "px-2 py-1 text-sm font-normal");
|
}, "px-2 py-1 text-sm font-normal");
|
||||||
toggleButton.disabled = state === 'given';
|
toggleButton.disabled = state === 'given';
|
||||||
return foldable(() => [
|
return foldable(() => [
|
||||||
markdown(spell.effect),
|
markdown(spell.description),
|
||||||
], [ div("flex flex-row justify-between gap-2", [
|
], [ div("flex flex-row justify-between gap-2", [
|
||||||
dom("span", { class: "text-lg font-bold", text: spell.name }),
|
dom("span", { class: "text-lg font-bold", text: spell.name }),
|
||||||
div("flex flex-row items-center gap-6", [
|
div("flex flex-row items-center gap-6", [
|
||||||
@@ -1507,7 +1647,7 @@ export class CharacterSheet
|
|||||||
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
|
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
|
||||||
}))
|
}))
|
||||||
]);
|
]);
|
||||||
const blocker = fullblocker([ container ], { closeWhenOutside: true });
|
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
||||||
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
|
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
||||||
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
|
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
|
||||||
import { contextmenu, followermenu } from "./floating.util";
|
import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util";
|
||||||
import { clamp } from "./general.util";
|
import { clamp } from "./general.util";
|
||||||
import { Tree } from "./tree";
|
import { Tree } from "./tree";
|
||||||
|
import type { Placement } from "@floating-ui/dom";
|
||||||
|
|
||||||
export function link(properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>, children?: NodeChildren)
|
export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
|
||||||
{
|
{
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const nav = link ? router.resolve(link) : undefined;
|
const nav = link ? router.resolve(link) : undefined;
|
||||||
@@ -20,24 +21,32 @@ export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElem
|
|||||||
{
|
{
|
||||||
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
|
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
|
||||||
}
|
}
|
||||||
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>): HTMLElement
|
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>)
|
||||||
{
|
{
|
||||||
const load = loading(size);
|
let state = { current: loading(size) };
|
||||||
|
|
||||||
fn.then((element) => {
|
fn.then((element) => {
|
||||||
load.replaceWith(element);
|
state.current.replaceWith(element);
|
||||||
|
state.current = element;
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
load.remove();
|
state.current.remove();
|
||||||
})
|
})
|
||||||
|
|
||||||
return load;
|
return state;
|
||||||
}
|
}
|
||||||
export function button(content: Node, onClick?: () => void, cls?: Class)
|
export function button(content: Node, onClick?: (this: HTMLElement) => void, cls?: Class)
|
||||||
{
|
{
|
||||||
const btn = dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
/*
|
||||||
|
text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: () => disabled || (onClick && onClick()) } }, [ content ]);
|
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50
|
||||||
|
*/
|
||||||
|
const btn = dom('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
|
||||||
|
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, [ content ]);
|
||||||
let disabled = false;
|
let disabled = false;
|
||||||
Object.defineProperty(btn, 'disabled', {
|
Object.defineProperty(btn, 'disabled', {
|
||||||
get: () => disabled,
|
get: () => disabled,
|
||||||
@@ -48,7 +57,7 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
|
|||||||
})
|
})
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean })
|
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void })
|
||||||
{
|
{
|
||||||
let currentValue = settings?.value;
|
let currentValue = settings?.value;
|
||||||
const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none
|
const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none
|
||||||
@@ -60,7 +69,7 @@ export function buttongroup<T extends any>(options: Array<{ text: string, value:
|
|||||||
elements.forEach(e => e.toggleAttribute('data-selected', false));
|
elements.forEach(e => e.toggleAttribute('data-selected', false));
|
||||||
this.toggleAttribute('data-selected', true);
|
this.toggleAttribute('data-selected', true);
|
||||||
|
|
||||||
if(!settings?.onChange || settings?.onChange(e.value))
|
if(!settings?.onChange || settings?.onChange(e.value) !== false)
|
||||||
{
|
{
|
||||||
currentValue = e.value;
|
currentValue = e.value;
|
||||||
}
|
}
|
||||||
@@ -68,6 +77,14 @@ export function buttongroup<T extends any>(options: Array<{ text: string, value:
|
|||||||
}}}))
|
}}}))
|
||||||
return div(['flex flex-row', settings?.class?.container], elements);
|
return div(['flex flex-row', settings?.class?.container], elements);
|
||||||
}
|
}
|
||||||
|
export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: HTMLElement) => void
|
||||||
|
{
|
||||||
|
let close: () => void;
|
||||||
|
const element = div(['flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100', settings?.class?.container], options.map(e => dom('div', { class: ['flex flex-row px-2 py-1 hover:bg-light-35 dark:hover:bg-dark-35 cursor-pointer', settings?.class?.option], text: e.title, listeners: { click: () => { e.click(); close() } } })));
|
||||||
|
return function(this: HTMLElement, target?: HTMLElement) {
|
||||||
|
close = followermenu(target ?? this, [ element ], { arrow: true, placement: settings?.position, offset: 8 }).close;
|
||||||
|
}
|
||||||
|
}
|
||||||
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
|
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
|
||||||
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
|
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
|
||||||
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||||
@@ -383,9 +400,12 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 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: {
|
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 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 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-30 dark:disabled:border-dark-30`, settings?.class], listeners: {
|
||||||
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
||||||
keydown: (e: KeyboardEvent) => {
|
keydown: (e: KeyboardEvent) => {
|
||||||
|
if(field.disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
switch(e.key)
|
switch(e.key)
|
||||||
{
|
{
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
@@ -420,12 +440,11 @@ export function foldable(content: NodeChildren | (() => NodeChildren), title: No
|
|||||||
if(state && !_content)
|
if(state && !_content)
|
||||||
{
|
{
|
||||||
_content = typeof content === 'function' ? content() : content;
|
_content = typeof content === 'function' ? content() : content;
|
||||||
//@ts-ignore
|
_content && contentContainer.replaceChildren(..._content.filter(e => !!e));
|
||||||
contentContainer.replaceChildren(..._content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
|
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
|
||||||
const fold = div(['group flex flex-1 w-full flex-col', settings?.class?.container], [
|
const fold = div(['group flex w-full flex-col', settings?.class?.container], [
|
||||||
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]),
|
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]),
|
||||||
contentContainer
|
contentContainer
|
||||||
]);
|
]);
|
||||||
@@ -444,8 +463,11 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
|
|||||||
let state = settings?.defaultValue ?? false;
|
let state = settings?.defaultValue ?? false;
|
||||||
const element = dom("div", { class: [`group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
const element = dom("div", { class: [`group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked" }, listeners: {
|
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
|
||||||
click: (e: Event) => {
|
click: function(e: Event) {
|
||||||
|
if(this.hasAttribute('data-disabled'))
|
||||||
|
return;
|
||||||
|
|
||||||
state = !state;
|
state = !state;
|
||||||
element.setAttribute('data-state', state ? "checked" : "unchecked");
|
element.setAttribute('data-state', state ? "checked" : "unchecked");
|
||||||
settings?.change && settings.change(state);
|
settings?.change && settings.change(state);
|
||||||
@@ -454,25 +476,216 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
|
|||||||
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
|
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } })
|
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } })
|
||||||
{
|
{
|
||||||
const focus = settings?.focused ?? tabs[0]?.id;
|
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: {
|
||||||
|
click: function(e: Event) {
|
||||||
|
if(this.hasAttribute('data-disabled'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = !state;
|
||||||
|
element.setAttribute('data-state', state ? "checked" : "unchecked");
|
||||||
|
settings?.change && settings.change.bind(this)(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }): HTMLDivElement & { refresh: () => void }
|
||||||
|
{
|
||||||
|
let focus = settings?.focused ?? tabs[0]?.id;
|
||||||
const titles = tabs.map((e, i) => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
|
const titles = tabs.map((e, i) => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
|
||||||
if(this.hasAttribute('data-focus'))
|
if(this.hasAttribute('data-focus'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
titles.forEach(e => e.toggleAttribute('data-focus', false));
|
titles.forEach(e => e.toggleAttribute('data-focus', false));
|
||||||
this.toggleAttribute('data-focus', true);
|
this.toggleAttribute('data-focus', true);
|
||||||
|
focus = e.id;
|
||||||
const _content = typeof e.content === 'function' ? e.content() : e.content;
|
const _content = typeof e.content === 'function' ? e.content() : e.content;
|
||||||
//@ts-expect-error
|
_content && content.replaceChildren(..._content?.filter(e => !!e));
|
||||||
content.replaceChildren(..._content);
|
|
||||||
}}}, e.title));
|
}}}, e.title));
|
||||||
const _content = tabs.find(e => e.id === focus)?.content;
|
const _content = tabs.find(e => e.id === focus)?.content;
|
||||||
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
|
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
|
||||||
return div(['flex flex-col', settings?.class?.container], [
|
|
||||||
|
const container = div(['flex flex-col', settings?.class?.container], [
|
||||||
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
|
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
|
||||||
content
|
content
|
||||||
]);
|
]);
|
||||||
|
Object.defineProperty(container, 'refresh', {
|
||||||
|
writable: false,
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
value: () => {
|
||||||
|
let _content = tabs.find(e => e.id === focus)?.content;
|
||||||
|
_content = (typeof _content === 'function' ? _content() : _content);
|
||||||
|
_content && content.replaceChildren(..._content.filter(e => !!e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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 })
|
||||||
|
{
|
||||||
|
let viewport = document.getElementById('mainContainer') ?? undefined;
|
||||||
|
let diffX, diffY;
|
||||||
|
let minimizeBox: 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 ?? {});
|
||||||
|
|
||||||
|
if(settings?.pinned)
|
||||||
|
{
|
||||||
|
events.onshow = (state) => {
|
||||||
|
if(!settings?.events?.onshow || settings?.events?.onshow(state))
|
||||||
|
{
|
||||||
|
floating.show();
|
||||||
|
pin();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragstart = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if(minimized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', dragmove);
|
||||||
|
window.addEventListener('mouseup', dragend);
|
||||||
|
|
||||||
|
const box = floating.content.getBoundingClientRect();
|
||||||
|
diffX = e.clientX - box.x;
|
||||||
|
diffY = e.clientY - box.y;
|
||||||
|
};
|
||||||
|
const resizestart = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.addEventListener('mousemove', resizemove);
|
||||||
|
window.addEventListener('mouseup', resizeend);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragmove = (e: MouseEvent) => {
|
||||||
|
const box = floating.content.getBoundingClientRect();
|
||||||
|
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
|
||||||
|
box.x = clamp(e.clientX - diffX!, viewbox?.left ?? 0, viewbox.right - box.width);
|
||||||
|
box.y = clamp(e.clientY - diffY!, viewbox?.top ?? 0, viewbox.bottom - box.height);
|
||||||
|
|
||||||
|
Object.assign(floating.content.style, {
|
||||||
|
left: `${box.x}px`,
|
||||||
|
top: `${box.y}px`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const resizemove = (e: MouseEvent) => {
|
||||||
|
const box = floating.content.getBoundingClientRect();
|
||||||
|
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
|
||||||
|
box.width = clamp(e.clientX - box.x, 200, Math.min(750, viewbox.right - box.x));
|
||||||
|
box.height = clamp(e.clientY - box.y, 150, Math.min(750, viewbox.bottom - box.y));
|
||||||
|
|
||||||
|
Object.assign(floating.content.style, {
|
||||||
|
width: `${box.width}px`,
|
||||||
|
height: `${box.height}px`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragend = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.removeEventListener('mousemove', dragmove);
|
||||||
|
window.removeEventListener('mouseup', dragend);
|
||||||
|
};
|
||||||
|
const resizeend = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.removeEventListener('mousemove', resizemove);
|
||||||
|
window.removeEventListener('mouseup', resizeend);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pin = () => {
|
||||||
|
if(floating.content.hasAttribute('data-pinned'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const box = floating.content.children.item(0)!.getBoundingClientRect();
|
||||||
|
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
|
||||||
|
Object.assign(floating.content.style, {
|
||||||
|
left: `${clamp(box.left, viewbox.left, viewbox.right)}px`,
|
||||||
|
top: `${clamp(box.top, viewbox.top, viewbox.bottom)}px`,
|
||||||
|
width: `${box.width + 21}px`,
|
||||||
|
height: `${box.height + 21}px`,
|
||||||
|
});
|
||||||
|
floating.content.attributeStyleMap.delete('bottom');
|
||||||
|
floating.content.attributeStyleMap.delete('right');
|
||||||
|
floating.stop();
|
||||||
|
|
||||||
|
floating.content.addEventListener('mousedown', function() {
|
||||||
|
if(!floating.content.hasAttribute('data-pinned'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
[...this.parentElement?.children ?? []].forEach(e => (e as any as HTMLElement).attributeStyleMap.set('z-index', -1));
|
||||||
|
this.attributeStyleMap.set('z-index', 0);
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
const minimize = () => {
|
||||||
|
minimized = !minimized;
|
||||||
|
floating.content.toggleAttribute('data-minimized', minimized);
|
||||||
|
if(minimized)
|
||||||
|
{
|
||||||
|
minimizeBox = floating.content.getBoundingClientRect();
|
||||||
|
Object.assign(floating.content.style, {
|
||||||
|
left: `0px`,
|
||||||
|
top: `initial`,
|
||||||
|
bottom: `0px`,
|
||||||
|
width: `150px`,
|
||||||
|
height: `21px`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Object.assign(floating.content.style, {
|
||||||
|
left: `${minimizeBox.left}px`,
|
||||||
|
top: `${minimizeBox.top}px`,
|
||||||
|
width: `${minimizeBox.width}px`,
|
||||||
|
height: `${minimizeBox.height}px`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const floating = popper(container, {
|
||||||
|
arrow: true,
|
||||||
|
delay: settings?.pinned ? 0 : 150,
|
||||||
|
offset: 12,
|
||||||
|
cover: settings?.cover,
|
||||||
|
placement: settings?.position,
|
||||||
|
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() ?? '') }),
|
||||||
|
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) => {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
floating.hide();
|
||||||
|
|
||||||
|
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`,
|
||||||
|
});
|
||||||
|
minimized = false;
|
||||||
|
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
||||||
|
div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
|
||||||
|
],
|
||||||
|
viewport,
|
||||||
|
events: events
|
||||||
|
});
|
||||||
|
|
||||||
|
if(settings?.pinned === false)
|
||||||
|
floating.content.addEventListener('dblclick', pin);
|
||||||
|
|
||||||
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToastConfig
|
export interface ToastConfig
|
||||||
@@ -488,13 +701,13 @@ type ToastDom = ToastConfig & { dom: HTMLElement };
|
|||||||
export type ToastType = 'info' | 'success' | 'error';
|
export type ToastType = 'info' | 'success' | 'error';
|
||||||
export class Toaster
|
export class Toaster
|
||||||
{
|
{
|
||||||
private static _MAX_DRAG = 130;
|
private static _MAX_DRAG = 150;
|
||||||
private static _list: Array<ToastDom> = [];
|
private static _list: Array<ToastDom> = [];
|
||||||
private static _container: HTMLDivElement;
|
private static _container: HTMLDivElement;
|
||||||
|
|
||||||
static init()
|
static init()
|
||||||
{
|
{
|
||||||
Toaster._container = div('fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72');
|
Toaster._container = dom('div', { attributes: { id: 'toaster' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72' });
|
||||||
document.body.appendChild(Toaster._container);
|
document.body.appendChild(Toaster._container);
|
||||||
}
|
}
|
||||||
static add(_config: ToastConfig)
|
static add(_config: ToastConfig)
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ export class Content
|
|||||||
if(Content._ready)
|
if(Content._ready)
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
|
|
||||||
|
if(Content.initPromise)
|
||||||
|
return Content.initPromise;
|
||||||
|
|
||||||
Content.initPromise = new Promise(async (res) => {
|
Content.initPromise = new Promise(async (res) => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -764,11 +767,11 @@ export class Editor
|
|||||||
if (location.current.dropTargets.length === 0)
|
if (location.current.dropTargets.length === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const target = location.current.dropTargets[0];
|
const target = location.current.dropTargets[0]!;
|
||||||
const instruction = extractInstruction(target.data);
|
const instruction = extractInstruction(target.data);
|
||||||
|
|
||||||
if (instruction !== null)
|
if (instruction !== null)
|
||||||
this.updateTree(instruction, location.initial.dropTargets[0].data.id as string, target.data.id as string);
|
this.updateTree(instruction, location.initial.dropTargets[0]!.data.id as string, target.data.id as string);
|
||||||
},
|
},
|
||||||
}), autoScrollForElements({
|
}), autoScrollForElements({
|
||||||
element: this.tree.container,
|
element: this.tree.container,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { iconExists, loadIcon } from 'iconify-icon';
|
import { iconExists, loadIcon } from 'iconify-icon';
|
||||||
|
|
||||||
export type Node = HTMLElement | SVGElement | Text | undefined;
|
export type Node = HTMLElement | SVGElement | Text | undefined;
|
||||||
export type NodeChildren = Array<Node>;
|
export type NodeChildren = Array<Node> | undefined;
|
||||||
|
|
||||||
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
||||||
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
|
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
|
||||||
@@ -56,11 +56,11 @@ export function div(cls?: Class, children?: NodeChildren): HTMLDivElement
|
|||||||
{
|
{
|
||||||
return dom("div", { class: cls }, children);
|
return dom("div", { class: cls }, children);
|
||||||
}
|
}
|
||||||
export function span(cls?: Class, children?: NodeChildren): HTMLSpanElement
|
export function span(cls?: Class, text?: string): HTMLSpanElement
|
||||||
{
|
{
|
||||||
return dom("span", { class: cls }, children);
|
return dom("span", { class: cls, text: text });
|
||||||
}
|
}
|
||||||
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Omit<NodeChildren, 'HTMLElement' | 'Text'>): SVGElementTagNameMap[K]
|
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: SVGElement[]): SVGElementTagNameMap[K]
|
||||||
{
|
{
|
||||||
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||||
|
|
||||||
@@ -120,45 +120,45 @@ export interface IconProperties
|
|||||||
const iconCache: Map<string, HTMLElement> = new Map();
|
const iconCache: Map<string, HTMLElement> = new Map();
|
||||||
export function icon(name: string, properties?: IconProperties): HTMLElement
|
export function icon(name: string, properties?: IconProperties): HTMLElement
|
||||||
{
|
{
|
||||||
let el;
|
let element;
|
||||||
|
|
||||||
if(iconCache.has(name))
|
if(iconCache.has(name))
|
||||||
el = iconCache.get(name)!.cloneNode() as HTMLElement;
|
element = iconCache.get(name)!.cloneNode() as HTMLElement;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
el = document.createElement('iconify-icon');
|
element = document.createElement('iconify-icon');
|
||||||
|
|
||||||
if(!iconExists(name))
|
if(!iconExists(name))
|
||||||
loadIcon(name);
|
loadIcon(name);
|
||||||
|
|
||||||
el.setAttribute('icon', name);
|
element.setAttribute('icon', name);
|
||||||
|
|
||||||
iconCache.set(name, el.cloneNode() as HTMLElement);
|
iconCache.set(name, element.cloneNode() as HTMLElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
properties?.mode && el.setAttribute('mode', properties?.mode.toString());
|
properties?.mode && element.setAttribute('mode', properties?.mode.toString());
|
||||||
properties?.inline && el.toggleAttribute('inline', properties?.inline);
|
properties?.inline && element.toggleAttribute('inline', properties?.inline);
|
||||||
properties?.noobserver && el.toggleAttribute('noobserver', properties?.noobserver);
|
element.toggleAttribute('noobserver', properties?.noobserver ?? true);
|
||||||
properties?.width && el.setAttribute('width', properties?.width.toString());
|
properties?.width && element.setAttribute('width', properties?.width.toString());
|
||||||
properties?.height && el.setAttribute('height', properties?.height.toString());
|
properties?.height && element.setAttribute('height', properties?.height.toString());
|
||||||
properties?.flip && el.setAttribute('flip', properties?.flip.toString());
|
properties?.flip && element.setAttribute('flip', properties?.flip.toString());
|
||||||
properties?.rotate && el.setAttribute('rotate', properties?.rotate.toString());
|
properties?.rotate && element.setAttribute('rotate', properties?.rotate.toString());
|
||||||
|
|
||||||
if(properties?.class)
|
if(properties?.class)
|
||||||
{
|
{
|
||||||
el.setAttribute('class', mergeClasses(properties.class));
|
element.setAttribute('class', mergeClasses(properties.class));
|
||||||
}
|
}
|
||||||
if(properties?.style)
|
if(properties?.style)
|
||||||
{
|
{
|
||||||
if(typeof properties.style === 'string')
|
if(typeof properties.style === 'string')
|
||||||
{
|
{
|
||||||
el.setAttribute('style', properties.style);
|
element.setAttribute('style', properties.style);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) el.attributeStyleMap.set(k, v);
|
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) element.attributeStyleMap.set(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
return el;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeClasses(classes: Class): string
|
export function mergeClasses(classes: Class): string
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,38 +17,44 @@ export interface FollowerProperties extends FloatingProperties
|
|||||||
blur?: () => void;
|
blur?: () => void;
|
||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
}
|
}
|
||||||
|
export type FloatState = 'shown' | 'showing' | 'hidden' | 'hiding' | 'pinned';
|
||||||
export interface PopperProperties extends FloatingProperties
|
export interface PopperProperties extends FloatingProperties
|
||||||
{
|
{
|
||||||
content?: NodeChildren | (() => NodeChildren);
|
content?: NodeChildren | (() => NodeChildren);
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
events?: {
|
||||||
onShow?: () => boolean | void;
|
show: Array<keyof HTMLElementEventMap>;
|
||||||
onHide?: () => boolean | void;
|
hide: Array<keyof HTMLElementEventMap>;
|
||||||
|
onshow?: (state: FloatState) => boolean;
|
||||||
|
onhide?: (state: FloatState) => boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalProperties
|
export interface ModalProperties
|
||||||
{
|
{
|
||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
closeWhenOutside?: boolean;
|
closeWhenOutside?: boolean;
|
||||||
|
onClose?: () => boolean | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let teleport: HTMLDivElement;
|
let teleport: HTMLDivElement;
|
||||||
export function init()
|
export function init()
|
||||||
{
|
{
|
||||||
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0' });
|
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
|
||||||
document.body.appendChild(teleport);
|
document.body.appendChild(teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function popper(container: HTMLElement, properties?: PopperProperties): HTMLElement
|
export function popper(container: HTMLElement, properties?: PopperProperties)
|
||||||
{
|
{
|
||||||
let shown = false, timeout: Timer;
|
let state: FloatState = 'hidden', manualStop = false, timeout: Timer;
|
||||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
||||||
const content = dom('div', { class: ['fixed hidden', properties?.class], style: properties?.style, attributes: { 'data-state': 'closed' } });
|
const content = dom('div', { class: properties?.class, style: properties?.style });
|
||||||
|
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
|
||||||
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
||||||
|
|
||||||
function update()
|
function update()
|
||||||
{
|
{
|
||||||
FloatingUI.computePosition(container, content, {
|
FloatingUI.computePosition(container, floater, {
|
||||||
placement: properties?.placement,
|
placement: properties?.placement,
|
||||||
strategy: 'fixed',
|
strategy: 'fixed',
|
||||||
middleware: [
|
middleware: [
|
||||||
@@ -58,14 +64,14 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||||||
FloatingUI.flip({ rootBoundary: rect }),
|
FloatingUI.flip({ rootBoundary: rect }),
|
||||||
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
|
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
|
||||||
if(properties?.cover === 'width' || properties?.cover === 'all')
|
if(properties?.cover === 'width' || properties?.cover === 'all')
|
||||||
content.style.width = `${availableWidth}px`;
|
floater.style.maxWidth = `${availableWidth}px`;
|
||||||
if(properties?.cover === 'height' || properties?.cover === 'all')
|
if(properties?.cover === 'height' || properties?.cover === 'all')
|
||||||
content.style.height = `${availableHeight}px`;
|
floater.style.maxHeight = `${availableHeight}px`;
|
||||||
} }),
|
} }),
|
||||||
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
||||||
]
|
]
|
||||||
}).then(({ x, y, placement, middlewareData }) => {
|
}).then(({ x, y, placement, middlewareData }) => {
|
||||||
Object.assign(content.style, {
|
Object.assign(floater.style, {
|
||||||
left: `${x}px`,
|
left: `${x}px`,
|
||||||
top: `${y}px`,
|
top: `${y}px`,
|
||||||
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
|
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
|
||||||
@@ -73,7 +79,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||||||
|
|
||||||
const side = placement.split('-')[0] as FloatingUI.Side;
|
const side = placement.split('-')[0] as FloatingUI.Side;
|
||||||
|
|
||||||
content.setAttribute('data-side', side);
|
floater.setAttribute('data-side', side);
|
||||||
|
|
||||||
if(middlewareData.arrow)
|
if(middlewareData.arrow)
|
||||||
{
|
{
|
||||||
@@ -84,53 +90,55 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||||||
right: 'left',
|
right: 'left',
|
||||||
bottom: 'top',
|
bottom: 'top',
|
||||||
left: 'right',
|
left: 'right',
|
||||||
}[side]!;
|
}[side]!;
|
||||||
|
|
||||||
const rotation = {
|
const rotation = {
|
||||||
top: "0",
|
top: "0",
|
||||||
bottom: "180",
|
bottom: "180",
|
||||||
left: "270",
|
left: "270",
|
||||||
right: "90"
|
right: "90"
|
||||||
}[side]!;
|
}[side]!;
|
||||||
|
|
||||||
Object.assign(arrow.style, {
|
Object.assign(arrow.style, {
|
||||||
left: arrowX != null ? `${arrowX}px` : '',
|
left: arrowX != null ? `${arrowX}px` : '',
|
||||||
top: arrowY != null ? `${arrowY}px` : '',
|
top: arrowY != null ? `${arrowY}px` : '',
|
||||||
right: '',
|
right: '',
|
||||||
bottom: '',
|
bottom: '',
|
||||||
[staticSide]: `-8px`,
|
[staticSide]: `-7px`,
|
||||||
transform: `rotate(${rotation}deg)`,
|
transform: `rotate(${rotation}deg)`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let stop: () => void | undefined;
|
let _stop: () => void | undefined, empty = true;
|
||||||
function show()
|
function show()
|
||||||
{
|
{
|
||||||
if(shown || !properties?.onShow || properties?.onShow() !== false)
|
if(state !== 'shown' && state !== 'showing' && state !== 'pinned' && (!properties?.events?.onshow || properties?.events?.onshow(state) !== false))
|
||||||
{
|
{
|
||||||
if(typeof properties?.content === 'function')
|
if(typeof properties?.content === 'function')
|
||||||
{
|
|
||||||
properties.content = properties.content();
|
properties.content = properties.content();
|
||||||
}
|
|
||||||
if(content.children.length === 0 && (properties?.content && properties.content.length > 0 || properties?.arrow))
|
if(properties?.content && empty)
|
||||||
{
|
{
|
||||||
content.replaceChildren(...(properties!.content as Node[]), arrow);
|
content.replaceChildren(...properties!.content.filter(e => !!e));
|
||||||
|
empty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
state = 'showing';
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
if(!shown)
|
if(state !== 'shown')
|
||||||
{
|
{
|
||||||
teleport!.appendChild(content);
|
teleport!.appendChild(floater);
|
||||||
|
|
||||||
content.setAttribute('data-state', 'open');
|
floater.setAttribute('data-state', 'open');
|
||||||
content.classList.toggle('hidden', false);
|
floater.classList.toggle('hidden', false);
|
||||||
|
|
||||||
update();
|
update();
|
||||||
stop = FloatingUI.autoUpdate(container, content, update, {
|
_stop && _stop();
|
||||||
|
_stop = FloatingUI.autoUpdate(container, floater, update, {
|
||||||
animationFrame: true,
|
animationFrame: true,
|
||||||
layoutShift: false,
|
layoutShift: false,
|
||||||
elementResize: false,
|
elementResize: false,
|
||||||
@@ -138,41 +146,93 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||||||
ancestorResize: false,
|
ancestorResize: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
shown = true;
|
state = 'shown';
|
||||||
}, properties?.delay ?? 0);
|
}, properties?.delay ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide()
|
function hide()
|
||||||
{
|
{
|
||||||
if(!properties?.onHide || properties?.onHide() !== false)
|
if(state !== 'hiding' && state !== 'pinned' && (!properties?.events?.onhide || properties?.events?.onhide(state) !== false))
|
||||||
{
|
{
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
state = 'hiding';
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
content.remove();
|
floater.remove();
|
||||||
stop && stop();
|
_stop && _stop();
|
||||||
|
|
||||||
shown = false;
|
floater.setAttribute('data-state', 'closed');
|
||||||
}, shown ? properties?.delay ?? 0 : 0);
|
floater.classList.toggle('hidden', true);
|
||||||
|
|
||||||
|
state = 'hidden';
|
||||||
|
}, properties?.delay ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function link(element: HTMLElement) {
|
function start()
|
||||||
Object.entries({
|
{
|
||||||
'mouseenter': show,
|
state = 'hidden';
|
||||||
'mouseleave': hide,
|
floater.toggleAttribute('data-pinned', false);
|
||||||
'focus': show,
|
update();
|
||||||
'blur': hide,
|
_stop && _stop();
|
||||||
} as Record<keyof HTMLElementEventMap, () => void>).forEach(([event, listener]) => {
|
_stop = FloatingUI.autoUpdate(container, floater, update, {
|
||||||
element.addEventListener(event, listener);
|
animationFrame: true,
|
||||||
|
layoutShift: false,
|
||||||
|
elementResize: false,
|
||||||
|
ancestorScroll: false,
|
||||||
|
ancestorResize: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
link(container);
|
function stop()
|
||||||
link(content);
|
{
|
||||||
|
state = 'pinned';
|
||||||
|
floater.toggleAttribute('data-pinned', true);
|
||||||
|
_stop && _stop();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
return container;
|
function link(element: HTMLElement) {
|
||||||
|
(properties?.events?.show ?? ['mouseenter', 'mousemove', 'focus']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, show));
|
||||||
|
(properties?.events?.hide ?? ['mouseleave', 'blur']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, hide));
|
||||||
|
}
|
||||||
|
|
||||||
|
link(container);
|
||||||
|
link(floater);
|
||||||
|
|
||||||
|
return { container, content: floater, stop, start, show: () => {
|
||||||
|
if(typeof properties?.content === 'function')
|
||||||
|
properties.content = properties.content();
|
||||||
|
|
||||||
|
if(properties?.content && empty)
|
||||||
|
{
|
||||||
|
content.replaceChildren(...properties!.content.filter(e => !!e));
|
||||||
|
empty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(state !== 'shown')
|
||||||
|
{
|
||||||
|
teleport!.appendChild(floater);
|
||||||
|
|
||||||
|
floater.setAttribute('data-state', 'open');
|
||||||
|
floater.classList.toggle('hidden', false);
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
state = 'shown';
|
||||||
|
}, hide: () => {
|
||||||
|
floater.remove();
|
||||||
|
_stop && _stop();
|
||||||
|
|
||||||
|
floater.setAttribute('data-state', 'closed');
|
||||||
|
floater.classList.toggle('hidden', true);
|
||||||
|
|
||||||
|
manualStop = false;
|
||||||
|
floater.toggleAttribute('data-pinned', false);
|
||||||
|
|
||||||
|
state = 'hidden';
|
||||||
|
} };
|
||||||
}
|
}
|
||||||
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
||||||
{
|
{
|
||||||
@@ -288,22 +348,24 @@ export function tooltip(container: HTMLElement, txt: string | Text, placement: F
|
|||||||
arrow: true,
|
arrow: true,
|
||||||
offset: 8,
|
offset: 8,
|
||||||
delay: delay,
|
delay: delay,
|
||||||
content: [ typeof txt === 'string' ? text(txt) : txt ],
|
content: () => [ typeof txt === 'string' ? text(txt) : txt ],
|
||||||
placement: placement,
|
placement: placement,
|
||||||
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50 max-w-96"
|
class: "border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50 max-w-96"
|
||||||
});
|
}).container;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fullblocker(content: NodeChildren, properties?: ModalProperties)
|
export function fullblocker(content: NodeChildren, properties?: ModalProperties)
|
||||||
{
|
{
|
||||||
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
|
if(!content)
|
||||||
|
return { close: () => {} };
|
||||||
|
|
||||||
|
const close = () => (!properties?.onClose || properties.onClose() !== false) && _modal.remove();
|
||||||
|
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } });
|
||||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
|
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
|
||||||
|
|
||||||
teleport.appendChild(_modal);
|
teleport.appendChild(_modal);
|
||||||
|
|
||||||
return {
|
return { close };
|
||||||
close: () => _modal.remove(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td
|
|||||||
import { heading } from "hast-util-heading";
|
import { heading } from "hast-util-heading";
|
||||||
import { headingRank } from "hast-util-heading-rank";
|
import { headingRank } from "hast-util-heading-rank";
|
||||||
import { parseId } from "#shared/general.util";
|
import { parseId } from "#shared/general.util";
|
||||||
import { async, loading } from "#shared/components.util";
|
import { async } from "#shared/components.util";
|
||||||
|
|
||||||
export function renderMarkdown(markdown: Root, proses: Record<string, Prose>): HTMLDivElement
|
export function renderMarkdown(markdown: Root, proses: Record<string, Prose>): HTMLDivElement
|
||||||
{
|
{
|
||||||
return dom('div', {}, markdown.children.map(e => renderContent(e, proses)));
|
return dom('div', {}, markdown.children.map(e => renderContent(e, proses)));
|
||||||
}
|
}
|
||||||
export function renderText(markdown: string): string
|
export function renderMDAsText(markdown: string): string
|
||||||
{
|
{
|
||||||
return useMarkdown().text(markdown);
|
return useMarkdown().text(markdown);
|
||||||
}
|
}
|
||||||
@@ -43,9 +43,9 @@ export interface MDProperties
|
|||||||
style?: string | Record<string, string>;
|
style?: string | Record<string, string>;
|
||||||
tags?: Record<string, Prose>;
|
tags?: Record<string, Prose>;
|
||||||
}
|
}
|
||||||
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
|
export function markdownReference(content: string, filter?: string, properties?: MDProperties)
|
||||||
{
|
{
|
||||||
return async('large', useMarkdown().parse(content).then(data => {
|
const state = async('large', useMarkdown().parse(content).then(data => {
|
||||||
if(filter)
|
if(filter)
|
||||||
{
|
{
|
||||||
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||||
@@ -53,11 +53,11 @@ export default function(content: string, filter?: string, properties?: MDPropert
|
|||||||
if(start !== -1)
|
if(start !== -1)
|
||||||
{
|
{
|
||||||
let end = start;
|
let end = start;
|
||||||
const rank = headingRank(data.children[start])!;
|
const rank = headingRank(data.children[start]!)!;
|
||||||
while(end < data.children.length)
|
while(end < data.children.length)
|
||||||
{
|
{
|
||||||
end++;
|
end++;
|
||||||
if(heading(data.children[end]) && headingRank(data.children[end])! <= rank)
|
if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
data = { ...data, children: data.children.slice(start, end) };
|
data = { ...data, children: data.children.slice(start, end) };
|
||||||
@@ -70,4 +70,9 @@ export default function(content: string, filter?: string, properties?: MDPropert
|
|||||||
|
|
||||||
return el;
|
return el;
|
||||||
}));
|
}));
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
|
||||||
|
{
|
||||||
|
return markdownReference(content, filter, properties).current;
|
||||||
}
|
}
|
||||||
111
shared/proses.ts
111
shared/proses.ts
@@ -1,11 +1,11 @@
|
|||||||
import { dom, icon, type NodeChildren, type Node, div } from "#shared/dom.util";
|
import { dom, icon, type NodeChildren, type Node, div, type Class } from "#shared/dom.util";
|
||||||
import { parseURL } from 'ufo';
|
import { parseURL } from 'ufo';
|
||||||
import render from "#shared/markdown.util";
|
import render from "#shared/markdown.util";
|
||||||
import { popper } from "#shared/floating.util";
|
|
||||||
import { Canvas } from "#shared/canvas.util";
|
import { Canvas } from "#shared/canvas.util";
|
||||||
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
||||||
import { parsePath, unifySlug } from "#shared/general.util";
|
import { unifySlug } from "#shared/general.util";
|
||||||
import { async, loading } from "./components.util";
|
import { async, floater } from "./components.util";
|
||||||
|
import type { FloatState } from "./floating.util";
|
||||||
|
|
||||||
|
|
||||||
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
||||||
@@ -20,7 +20,7 @@ export const a: Prose = {
|
|||||||
|
|
||||||
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
|
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
|
||||||
|
|
||||||
const el = dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: {
|
const element = dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: {
|
||||||
'click': (e) => {
|
'click': (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(link);
|
router.push(link);
|
||||||
@@ -32,84 +32,55 @@ export const a: Prose = {
|
|||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(!!overview)
|
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
|
||||||
{
|
if(_content?.type === 'markdown')
|
||||||
popper(el, {
|
{
|
||||||
arrow: true,
|
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
|
||||||
delay: 150,
|
}
|
||||||
offset: 12,
|
if(_content?.type === 'canvas')
|
||||||
cover: "height",
|
{
|
||||||
placement: 'bottom-start',
|
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
||||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]',
|
queueMicrotask(() => canvas.mount());
|
||||||
content: () => {
|
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
|
||||||
return [async('large', Content.getContent(overview.id).then((_content) => {
|
}
|
||||||
if(_content?.type === 'markdown')
|
return div('');
|
||||||
{
|
})).current], { position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element;
|
||||||
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
|
|
||||||
}
|
|
||||||
if(_content?.type === 'canvas')
|
|
||||||
{
|
|
||||||
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
|
||||||
queueMicrotask(() => canvas.mount());
|
|
||||||
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
|
|
||||||
}
|
|
||||||
return div('');
|
|
||||||
}))];
|
|
||||||
},
|
|
||||||
viewport: document.getElementById('mainContainer') ?? undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const preview: Prose = {
|
export const preview: Prose = {
|
||||||
custom(properties, children) {
|
custom(properties: { href: string, class?: Class, label: string }, children) {
|
||||||
const href = properties.href as string;
|
const href = properties.href as string;
|
||||||
const { hash, pathname } = parseURL(href);
|
const { hash, pathname } = parseURL(href);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
|
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
|
||||||
|
|
||||||
const el = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [
|
const element = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [
|
||||||
...(children ?? []),
|
...(children ?? []),
|
||||||
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
|
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const magicKeys = useMagicKeys();
|
||||||
if(!!overview)
|
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
|
||||||
{
|
if(_content?.type === 'markdown')
|
||||||
const magicKeys = useMagicKeys();
|
{
|
||||||
popper(el, {
|
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
|
||||||
arrow: true,
|
}
|
||||||
delay: 150,
|
if(_content?.type === 'canvas')
|
||||||
offset: 12,
|
{
|
||||||
cover: "height",
|
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
||||||
placement: 'bottom-start',
|
queueMicrotask(() => canvas.mount());
|
||||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]',
|
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
|
||||||
content: () => {
|
}
|
||||||
return [async('large', Content.getContent(overview.id).then((_content) => {
|
return div();
|
||||||
if(_content?.type === 'markdown')
|
})).current], { position: 'bottom-start', pinned: false,
|
||||||
{
|
events: {
|
||||||
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
|
show: ['mouseenter', 'mousemove'],
|
||||||
}
|
hide: ['mouseleave'],
|
||||||
if(_content?.type === 'canvas')
|
onshow(state: FloatState) {
|
||||||
{
|
return state === 'shown' || state === 'hiding' || magicKeys.current.has('control') || magicKeys.current.has('meta');
|
||||||
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
|
}
|
||||||
queueMicrotask(() => canvas.mount());
|
}, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
|
||||||
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
|
|
||||||
}
|
|
||||||
return div('');
|
|
||||||
}))];
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const callout: Prose = {
|
export const callout: Prose = {
|
||||||
|
|||||||
46
types/character.d.ts
vendored
46
types/character.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES } from "#shared/character.util";
|
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES } from "#shared/character.util";
|
||||||
import type { Localized } from "#shared/general";
|
import type { Localized } from "../types/general";
|
||||||
|
|
||||||
export type MainStat = typeof MAIN_STATS[number];
|
export type MainStat = typeof MAIN_STATS[number];
|
||||||
export type Ability = typeof ABILITIES[number];
|
export type Ability = typeof ABILITIES[number];
|
||||||
@@ -10,6 +10,8 @@ export type Category = typeof CATEGORIES[number];
|
|||||||
export type SpellElement = typeof SPELL_ELEMENTS[number];
|
export type SpellElement = typeof SPELL_ELEMENTS[number];
|
||||||
export type Alignment = typeof ALIGNMENTS[number];
|
export type Alignment = typeof ALIGNMENTS[number];
|
||||||
export type Resistance = typeof RESISTANCES[number];
|
export type Resistance = typeof RESISTANCES[number];
|
||||||
|
export type DamageType = typeof DAMAGE_TYPES[number];
|
||||||
|
export type WeaponType = typeof WEAPON_TYPES[number];
|
||||||
|
|
||||||
export type FeatureID = string;
|
export type FeatureID = string;
|
||||||
export type i18nID = string;
|
export type i18nID = string;
|
||||||
@@ -67,7 +69,11 @@ export type CharacterConfig = {
|
|||||||
features: Record<FeatureID, Feature>;
|
features: Record<FeatureID, Feature>;
|
||||||
enchantments: Record<string, EnchantementConfig>; //TODO
|
enchantments: Record<string, EnchantementConfig>; //TODO
|
||||||
items: Record<string, ItemConfig>;
|
items: Record<string, ItemConfig>;
|
||||||
lists: Record<string, { id: string, config: ListConfig, values: Record<string, any> }>;
|
sickness: Record<string, { id: string, name: string, description: string, effect: FeatureID[] }>;
|
||||||
|
action: Record<string, { id: string, name: string, description: string, cost: number }>;
|
||||||
|
reaction: Record<string, { id: string, name: string, description: string, cost: number }>;
|
||||||
|
freeaction: Record<string, { id: string, name: string, description: string }>;
|
||||||
|
passive: Record<string, { id: string, name: string, description: string }>;
|
||||||
texts: Record<i18nID, Localized>;
|
texts: Record<i18nID, Localized>;
|
||||||
};
|
};
|
||||||
export type EnchantementConfig = {
|
export type EnchantementConfig = {
|
||||||
@@ -78,36 +84,38 @@ export type EnchantementConfig = {
|
|||||||
export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
|
export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
|
||||||
type CommonItemConfig = {
|
type CommonItemConfig = {
|
||||||
id: string;
|
id: string;
|
||||||
|
name: string; //TODO -> TextID
|
||||||
|
description: i18nID;
|
||||||
rarity: 'common' | 'uncommon' | 'rare' | 'legendary';
|
rarity: 'common' | 'uncommon' | 'rare' | 'legendary';
|
||||||
weight?: number; //Optionnal but highly recommended
|
weight?: number; //Optionnal but highly recommended
|
||||||
price?: number; //Optionnal but highly recommended
|
price?: number; //Optionnal but highly recommended
|
||||||
power?: number; //Optionnal as most mundane items should not receive enchantments (potions, herbal heals, etc...)
|
capacity?: number; //Optionnal as most mundane items should not receive enchantments (potions, herbal heals, etc...)
|
||||||
|
powercost?: number; //Optionnal
|
||||||
charge?: number //Max amount of charges
|
charge?: number //Max amount of charges
|
||||||
|
enchantments?: string[]; //Enchantment ID
|
||||||
|
effects?: Array<FeatureValue | FeatureEquipment | FeatureList>;
|
||||||
equippable: boolean;
|
equippable: boolean;
|
||||||
|
consummable: boolean;
|
||||||
}
|
}
|
||||||
type ArmorConfig = {
|
type ArmorConfig = {
|
||||||
category: 'armor';
|
category: 'armor';
|
||||||
name: string; //TODO -> TextID
|
|
||||||
description: i18nID;
|
|
||||||
health: number;
|
health: number;
|
||||||
|
type: 'light' | 'medium' | 'heavy';
|
||||||
absorb: { static: number, percent: number };
|
absorb: { static: number, percent: number };
|
||||||
};
|
};
|
||||||
type WeaponConfig = {
|
type WeaponConfig = {
|
||||||
category: 'weapon';
|
category: 'weapon';
|
||||||
name: string; //TODO -> TextID
|
type: Array<WeaponType>;
|
||||||
description: i18nID;
|
damage: {
|
||||||
damage: string; //Dice formula
|
value: string; //Dice formula
|
||||||
|
type: DamageType;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
type WondrousConfig = {
|
type WondrousConfig = {
|
||||||
category: 'wondrous';
|
category: 'wondrous';
|
||||||
name: string; //TODO -> TextID
|
|
||||||
description: i18nID;
|
|
||||||
effect: FeatureItem[];
|
|
||||||
};
|
};
|
||||||
type MundaneConfig = {
|
type MundaneConfig = {
|
||||||
category: 'mundane';
|
category: 'mundane';
|
||||||
name: string; //TODO -> TextID
|
|
||||||
description: i18nID;
|
|
||||||
};
|
};
|
||||||
export type SpellConfig = {
|
export type SpellConfig = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -117,8 +125,9 @@ export type SpellConfig = {
|
|||||||
cost: number;
|
cost: number;
|
||||||
speed: "action" | "reaction" | number;
|
speed: "action" | "reaction" | number;
|
||||||
elements: Array<SpellElement>;
|
elements: Array<SpellElement>;
|
||||||
effect: string; //TODO -> TextID
|
description: string; //TODO -> TextID
|
||||||
concentration: boolean;
|
concentration: boolean;
|
||||||
|
range: 'personnal' | number;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
export type RaceConfig = {
|
export type RaceConfig = {
|
||||||
@@ -151,7 +160,7 @@ export type FeatureEquipment = {
|
|||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
category: "value";
|
category: "value";
|
||||||
operation: "add" | "set" | "min";
|
operation: "add" | "set" | "min";
|
||||||
property: 'weapon/damage' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent';
|
property: 'weapon/damage/value' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent';
|
||||||
value: number | `modifier/${MainStat}` | false;
|
value: number | `modifier/${MainStat}` | false;
|
||||||
}
|
}
|
||||||
export type FeatureList = {
|
export type FeatureList = {
|
||||||
@@ -159,8 +168,7 @@ export type FeatureList = {
|
|||||||
category: "list";
|
category: "list";
|
||||||
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
||||||
action: "add" | "remove";
|
action: "add" | "remove";
|
||||||
item: string | i18nID;
|
item: string;
|
||||||
extra?: any;
|
|
||||||
};
|
};
|
||||||
export type FeatureChoice = {
|
export type FeatureChoice = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
@@ -175,7 +183,7 @@ export type FeatureChoice = {
|
|||||||
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
|
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
|
||||||
export type Feature = {
|
export type Feature = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
description: i18nID;
|
description: string; //TODO -> TextID
|
||||||
effect: FeatureItem[];
|
effect: FeatureItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user