Push Preview Gong Notes
This commit is contained in:
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
||||
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
"recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode"]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
- Non modificare mai i file direttamente, proponi la soluzione nella chat.
|
||||
- Puoi modificare direttamente i tag e lo stile di componenti e pagine.
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -21,6 +21,8 @@
|
||||
"better-auth": "~1.4.21",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -4658,6 +4660,17 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-svelte": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
|
||||
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0",
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes"
|
||||
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "~1.4.21",
|
||||
@@ -26,6 +28,8 @@
|
||||
"better-auth": "~1.4.21",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
5
src/app.d.ts
vendored
5
src/app.d.ts
vendored
@@ -4,7 +4,10 @@ import type { User, Session } from 'better-auth/minimal';
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals { user?: User; session?: Session }
|
||||
interface Locals {
|
||||
user?: User;
|
||||
session?: Session;
|
||||
}
|
||||
|
||||
// interface Error {}
|
||||
// interface PageData {}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
@@ -4,8 +4,6 @@ import { auth } from '$lib/server/auth';
|
||||
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
|
||||
|
||||
|
||||
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
||||
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||
|
||||
@@ -29,6 +27,6 @@ const handleRouting: Handle = async ({ event, resolve }) => {
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
}
|
||||
};
|
||||
|
||||
export const handle: Handle = sequence(handleBetterAuth, handleRouting);
|
||||
|
||||
2
src/lib/assets/composer/palette.svg
Normal file
2
src/lib/assets/composer/palette.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.022,23a11.053,11.053,0,0,0,10.921-9.5,5.853,5.853,0,0,0-.577-3.5c-1.655-3.146-4.777-2.671-7.056-2.322-1.18.178-2.4.366-2.865-.035A2.416,2.416,0,0,1,12.02,6c0-2.683,0-5-3-5C3.753,1,1,6.534,1,12A11.023,11.023,0,0,0,12.022,23ZM9.016,3c.909,0,1,0,1,3a3.941,3.941,0,0,0,1.122,3.168c1.163,1,2.844.741,4.469.494,2.483-.379,4.061-.482,4.986,1.276a3.844,3.844,0,0,1,.363,2.293A9.024,9.024,0,0,1,3,12C3,8.382,4.6,3,9.016,3ZM5,7.5A1.5,1.5,0,1,1,6.5,9,1.5,1.5,0,0,1,5,7.5ZM4,12a1.5,1.5,0,1,1,1.5,1.5A1.5,1.5,0,0,1,4,12Zm3.5,3A1.5,1.5,0,1,1,6,16.5,1.5,1.5,0,0,1,7.5,15Zm8,3A3.5,3.5,0,1,0,12,14.5,3.5,3.5,0,0,0,15.5,18Zm0-5A1.5,1.5,0,1,1,14,14.5,1.5,1.5,0,0,1,15.5,13Z"/></svg>
|
||||
|
After Width: | Height: | Size: 899 B |
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { FileAttachment, ImageAttachment, Note } from "$lib/editor";
|
||||
import { nanoid } from "nanoid";
|
||||
import { onMount } from "svelte";
|
||||
import imageSvg from "$lib/assets/composer/image.svg"
|
||||
import fileSvg from "$lib/assets/composer/file.svg"
|
||||
|
||||
type Props = {
|
||||
note?: Note;
|
||||
onSubmit: (note: Note) => void;
|
||||
}
|
||||
|
||||
let { note, onSubmit } : Props = $props();
|
||||
|
||||
const palette = ["#ffffff", "#fef3c7", "#dbeafe", "#dcfce7", "#fee2e2", "#f3e8ff"];
|
||||
|
||||
onMount(() => {
|
||||
if (!note) {
|
||||
note = {
|
||||
id: nanoid(),
|
||||
title: "",
|
||||
content: "",
|
||||
color: palette[0],
|
||||
date: new Date(),
|
||||
images: [],
|
||||
files: []
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Note } from "$lib/editor";
|
||||
import NoteEditor from "./NoteEditor.svelte";
|
||||
|
||||
let expanded = $state(false);
|
||||
|
||||
const onCreate = (note: Note) => {
|
||||
console.log("create note")
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "n" && e.metaKey) {
|
||||
expanded = true;
|
||||
return;
|
||||
}
|
||||
if( e.key === "Escape") {
|
||||
expanded = false;
|
||||
return;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if !expanded}
|
||||
<div>Scrivi una nota...</div>
|
||||
{:else}
|
||||
<div class="note-editor-container">
|
||||
<NoteEditor note={undefined} onSubmit={onCreate} />
|
||||
</div>
|
||||
{/if}
|
||||
61
src/lib/components/board/BoardCanvas.svelte
Normal file
61
src/lib/components/board/BoardCanvas.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import NoteCard from '$lib/components/board/Note.svelte';
|
||||
import type { BoardNote } from '$lib/components/board/script/types';
|
||||
import { BOARD_GRID_SIZE, BOARD_HEIGHT, BOARD_WIDTH } from '$lib/components/board/script/constants';
|
||||
|
||||
type Props = {
|
||||
notes: BoardNote[];
|
||||
onStartPan: (event: PointerEvent) => void;
|
||||
onStartDrag: (event: PointerEvent, id: string) => void;
|
||||
};
|
||||
|
||||
let { notes, onStartPan, onStartDrag }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="board"
|
||||
role="application"
|
||||
aria-label="Bacheca note"
|
||||
onpointerdown={onStartPan}
|
||||
style={`--board-width:${BOARD_WIDTH}px; --board-height:${BOARD_HEIGHT}px; --grid-size:${BOARD_GRID_SIZE}px;`}
|
||||
>
|
||||
{#each notes as note (note.id)}
|
||||
<div
|
||||
class="note-item"
|
||||
style={`left:${note.x}px; top:${note.y}px; z-index:${note.z};`}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onpointerdown={(event) => {
|
||||
event.stopPropagation();
|
||||
onStartDrag(event, note.id);
|
||||
}}
|
||||
>
|
||||
<NoteCard {note} />
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.board {
|
||||
position: relative;
|
||||
width: var(--board-width);
|
||||
height: var(--board-height);
|
||||
padding: 90px 30px 60px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(140, 155, 176, 0.14) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(140, 155, 176, 0.14) 1px, transparent 1px);
|
||||
background-size: var(--grid-size) var(--grid-size);
|
||||
}
|
||||
|
||||
.note-item {
|
||||
position: absolute;
|
||||
width: 280px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.note-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
65
src/lib/components/board/Note.svelte
Normal file
65
src/lib/components/board/Note.svelte
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import type { Note } from '$lib/components/editor/script/editor';
|
||||
|
||||
type Props = {
|
||||
note: Note;
|
||||
};
|
||||
|
||||
let { note }: Props = $props();
|
||||
</script>
|
||||
|
||||
<article class="note-card" style={`background:${note.color};`}>
|
||||
{#if note.title.trim().length > 0}
|
||||
<h3 class="note-title">{note.title}</h3>
|
||||
{/if}
|
||||
|
||||
{#if note.content.trim().length > 0}
|
||||
<p class="note-content">{note.content}</p>
|
||||
{:else}
|
||||
<p class="note-content empty">Nota senza contenuto</p>
|
||||
{/if}
|
||||
|
||||
<div class="note-date">{note.date.toLocaleString('it-IT')}</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.note-card {
|
||||
width: 280px;
|
||||
min-height: 140px;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 2px rgba(60, 64, 67, 0.2), 0 2px 6px rgba(60, 64, 67, 0.18);
|
||||
padding: 14px 14px 12px;
|
||||
color: #202124;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.note-title {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
margin: 0;
|
||||
font-size: 1.42rem;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.note-content.empty {
|
||||
color: #5f6368;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.note-date {
|
||||
margin-top: auto;
|
||||
font-size: 1.2rem;
|
||||
color: #5f6368;
|
||||
}
|
||||
</style>
|
||||
9
src/lib/components/board/script/constants.ts
Normal file
9
src/lib/components/board/script/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const BOARD_WIDTH = 5400;
|
||||
export const BOARD_HEIGHT = 5400;
|
||||
|
||||
export const BOARD_GRID_SIZE = 16;
|
||||
|
||||
export const NOTE_CARD_WIDTH = 280;
|
||||
export const NOTE_CARD_MIN_HEIGHT = 140;
|
||||
|
||||
export const MINIMAP_WIDTH = 220;
|
||||
31
src/lib/components/board/script/types.ts
Normal file
31
src/lib/components/board/script/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Note } from '$lib/components/editor/script/editor';
|
||||
|
||||
export type BoardNote = Note & {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
|
||||
export type DragState =
|
||||
| {
|
||||
id: string;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
| null;
|
||||
|
||||
export type PanState =
|
||||
| {
|
||||
startX: number;
|
||||
startY: number;
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
}
|
||||
| null;
|
||||
|
||||
export type ViewportState = {
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
18
src/lib/components/board/script/utils.ts
Normal file
18
src/lib/components/board/script/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
BOARD_HEIGHT,
|
||||
BOARD_WIDTH,
|
||||
NOTE_CARD_MIN_HEIGHT,
|
||||
NOTE_CARD_WIDTH
|
||||
} from '$lib/components/board/script/constants';
|
||||
|
||||
export const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(max, Math.max(min, value));
|
||||
|
||||
export const getRandomPosition = (idx: number) => ({
|
||||
x: 200 + ((idx * 420) % (BOARD_WIDTH - NOTE_CARD_WIDTH - 260)),
|
||||
y: 140 + Math.floor(idx / 8) * 260
|
||||
});
|
||||
|
||||
export const clampNoteX = (x: number) => clamp(x, 0, BOARD_WIDTH - NOTE_CARD_WIDTH);
|
||||
|
||||
export const clampNoteY = (y: number) => clamp(y, 0, BOARD_HEIGHT - NOTE_CARD_MIN_HEIGHT);
|
||||
0
src/lib/components/editor/ColorSelector.svelte
Normal file
0
src/lib/components/editor/ColorSelector.svelte
Normal file
193
src/lib/components/editor/NoteEditor.svelte
Normal file
193
src/lib/components/editor/NoteEditor.svelte
Normal file
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
import type { FileAttachment, ImageAttachment, Note } from '$lib/components/editor/script/editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { onMount } from 'svelte';
|
||||
import imageSvg from '$lib/assets/composer/image.svg';
|
||||
import fileSvg from '$lib/assets/composer/file.svg';
|
||||
import paletteSvg from '$lib/assets/composer/palette.svg';
|
||||
|
||||
type Props = {
|
||||
note?: Note;
|
||||
onSubmit: (note: Note) => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
let { note, onSubmit, onClose }: Props = $props();
|
||||
|
||||
const palette = ['#ffffff', '#fef3c7', '#dbeafe', '#dcfce7', '#fee2e2', '#f3e8ff'];
|
||||
|
||||
onMount(() => {
|
||||
if (!note) {
|
||||
note = {
|
||||
id: nanoid(),
|
||||
title: '',
|
||||
content: '',
|
||||
color: palette[0],
|
||||
date: new Date(),
|
||||
images: [],
|
||||
files: []
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (note) {
|
||||
const hasContent = note.title.trim().length > 0 || note.content.trim().length > 0;
|
||||
if (hasContent) {
|
||||
onSubmit({ ...note, date: new Date() });
|
||||
}
|
||||
}
|
||||
onClose?.();
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window />
|
||||
|
||||
{#if note}
|
||||
<section class="note-editor" style={`background: ${note.color};`}>
|
||||
<input
|
||||
class="title-input"
|
||||
type="text"
|
||||
bind:value={note.title}
|
||||
placeholder="Titolo"
|
||||
aria-label="Titolo"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
class="content-input"
|
||||
bind:value={note.content}
|
||||
placeholder="Scrivi una nota..."
|
||||
aria-label="Contenuto nota"
|
||||
></textarea>
|
||||
|
||||
<footer class="editor-footer">
|
||||
<div class="left-actions" aria-label="Azioni nota">
|
||||
<button type="button" class="icon-btn" aria-label="Sfondo colore">
|
||||
<img src={paletteSvg} alt="" class="icon-svg" />
|
||||
</button>
|
||||
<button type="button" class="icon-btn" aria-label="Immagine">
|
||||
<img src={imageSvg} alt="" class="icon-svg" />
|
||||
</button>
|
||||
<button type="button" class="icon-btn" aria-label="File">
|
||||
<img src={fileSvg} alt="" class="icon-svg" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<button type="button" class="close-btn" onclick={handleClose}>Chiudi</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.note-editor {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(60, 64, 67, 0.3), 0 4px 8px rgba(60, 64, 67, 0.15);
|
||||
padding: 10px 14px 8px;
|
||||
}
|
||||
|
||||
.title-input,
|
||||
.content-input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #202124;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.title-input:focus,
|
||||
.content-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
min-height: 36px;
|
||||
padding-right: 34px;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title-input::placeholder {
|
||||
color: #3c4043;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content-input {
|
||||
min-height: 110px;
|
||||
resize: none;
|
||||
padding: 2px 0 8px;
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.content-input::placeholder {
|
||||
color: #5f6368;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.left-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #5f6368;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #202124;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 500;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
</style>
|
||||
23
src/lib/components/editor/script/types.ts
Normal file
23
src/lib/components/editor/script/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type Note = {
|
||||
id: string;
|
||||
|
||||
title: string;
|
||||
content: string;
|
||||
color: string;
|
||||
date: Date;
|
||||
|
||||
images: ImageAttachment[];
|
||||
files: FileAttachment[];
|
||||
};
|
||||
|
||||
export type ImageAttachment = NoteAttachment & { kind: 'image' };
|
||||
export type FileAttachment = NoteAttachment & { kind: 'file' };
|
||||
|
||||
interface NoteAttachment {
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
previewUrl?: string;
|
||||
}
|
||||
0
src/lib/components/editor/script/utils.ts
Normal file
0
src/lib/components/editor/script/utils.ts
Normal file
112
src/lib/components/ui/Minimap.svelte
Normal file
112
src/lib/components/ui/Minimap.svelte
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { BOARD_HEIGHT, BOARD_WIDTH, MINIMAP_WIDTH } from '$lib/components/board/script/constants';
|
||||
import type { BoardNote, ViewportState } from '$lib/components/board/script/types';
|
||||
import { clamp } from '$lib/components/board/script/utils';
|
||||
|
||||
type Props = {
|
||||
notes: BoardNote[];
|
||||
viewport: ViewportState;
|
||||
onNavigate: (scrollLeft: number, scrollTop: number) => void;
|
||||
};
|
||||
|
||||
let { notes, viewport, onNavigate }: Props = $props();
|
||||
|
||||
let dragging = $state(false);
|
||||
|
||||
const minimapHeight = (MINIMAP_WIDTH * BOARD_HEIGHT) / BOARD_WIDTH;
|
||||
|
||||
const viewportLeftPercent = $derived((viewport.scrollLeft / BOARD_WIDTH) * 100);
|
||||
const viewportTopPercent = $derived((viewport.scrollTop / BOARD_HEIGHT) * 100);
|
||||
const viewportWidthPercent = $derived((viewport.width / BOARD_WIDTH) * 100);
|
||||
const viewportHeightPercent = $derived((viewport.height / BOARD_HEIGHT) * 100);
|
||||
|
||||
const navigateFromPointer = (event: PointerEvent) => {
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
const relX = clamp(event.clientX - rect.left, 0, rect.width);
|
||||
const relY = clamp(event.clientY - rect.top, 0, rect.height);
|
||||
|
||||
const nextLeft = (relX / rect.width) * BOARD_WIDTH - viewport.width / 2;
|
||||
const nextTop = (relY / rect.height) * BOARD_HEIGHT - viewport.height / 2;
|
||||
|
||||
onNavigate(nextLeft, nextTop);
|
||||
};
|
||||
</script>
|
||||
|
||||
<aside class="minimap-shell" aria-label="Minimappa bacheca">
|
||||
<div
|
||||
class="minimap"
|
||||
role="application"
|
||||
aria-label="Minimappa interattiva"
|
||||
style={`--minimap-width:${MINIMAP_WIDTH}px; --minimap-height:${minimapHeight}px;`}
|
||||
onpointerdown={(event) => {
|
||||
dragging = true;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
navigateFromPointer(event);
|
||||
}}
|
||||
onpointermove={(event) => {
|
||||
if (!dragging) return;
|
||||
navigateFromPointer(event);
|
||||
}}
|
||||
onpointerup={() => (dragging = false)}
|
||||
onpointercancel={() => (dragging = false)}
|
||||
>
|
||||
{#each notes as note (note.id)}
|
||||
<span
|
||||
class="note-dot"
|
||||
style={`left:${(note.x / BOARD_WIDTH) * 100}%; top:${(note.y / BOARD_HEIGHT) * 100}%;`}
|
||||
></span>
|
||||
{/each}
|
||||
|
||||
<div
|
||||
class="viewport-frame"
|
||||
style={`left:${viewportLeftPercent}%; top:${viewportTopPercent}%; width:${viewportWidthPercent}%; height:${viewportHeightPercent}%;`}
|
||||
></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.minimap-shell {
|
||||
position: fixed;
|
||||
left: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 90;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
backdrop-filter: blur(5px);
|
||||
box-shadow: 0 4px 14px rgba(32, 33, 36, 0.16);
|
||||
}
|
||||
|
||||
.minimap {
|
||||
position: relative;
|
||||
width: var(--minimap-width);
|
||||
height: var(--minimap-height);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(140, 155, 176, 0.14) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(140, 155, 176, 0.14) 1px, transparent 1px);
|
||||
background-size: 10px 10px;
|
||||
background-color: rgba(234, 239, 245, 0.92);
|
||||
}
|
||||
|
||||
.note-dot {
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: rgba(50, 84, 122, 0.76);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.viewport-frame {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(17, 80, 145, 0.9);
|
||||
background: rgba(63, 134, 206, 0.12);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.75);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
62
src/lib/components/ui/Navbar.svelte
Normal file
62
src/lib/components/ui/Navbar.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import type { Note } from '$lib/editor';
|
||||
import NoteEditor from '../editor/NoteEditor.svelte';
|
||||
|
||||
type Props = {
|
||||
onCreate: (note: Note) => void;
|
||||
};
|
||||
|
||||
let { onCreate }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
expanded = false;
|
||||
return;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if !expanded}
|
||||
<button class="note-navbar-trigger" type="button" onclick={() => (expanded = true)}>
|
||||
Scrivi una nota...
|
||||
</button>
|
||||
{:else}
|
||||
<div class="note-editor-container">
|
||||
<NoteEditor note={undefined} onSubmit={onCreate} onClose={() => (expanded = false)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.note-navbar-trigger {
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
border: 1px solid #d3d3d3;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
|
||||
padding: 0 18px;
|
||||
text-align: left;
|
||||
font-size: 1.95rem;
|
||||
line-height: 1;
|
||||
color: #5f6368;
|
||||
cursor: text;
|
||||
transition:
|
||||
box-shadow 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.note-navbar-trigger:hover,
|
||||
.note-navbar-trigger:focus-visible {
|
||||
border-color: #c6c6c6;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.note-editor-container {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
export type Note = {
|
||||
id: string;
|
||||
|
||||
title: string;
|
||||
content: string;
|
||||
color: string;
|
||||
date: Date;
|
||||
|
||||
images: ImageAttachment[];
|
||||
files: FileAttachment[];
|
||||
};
|
||||
|
||||
|
||||
export type ImageAttachment = NoteAttachment & { kind: "image" };
|
||||
export type FileAttachment = NoteAttachment & { kind: "file" };
|
||||
|
||||
interface NoteAttachment {
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
previewUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type MediaProvider = 'youtube' | 'youtube-music' | 'spotify';
|
||||
@@ -1,107 +1,105 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||
import { relations, sql } from 'drizzle-orm';
|
||||
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const user = sqliteTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: integer("email_verified", { mode: "boolean" })
|
||||
.default(false)
|
||||
.notNull(),
|
||||
image: text("image"),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
export const user = sqliteTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull().unique(),
|
||||
emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
|
||||
image: text('image'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const session = sqliteTable(
|
||||
"session",
|
||||
'session',
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: 'cascade' })
|
||||
},
|
||||
(table) => [index("session_userId_idx").on(table.userId)],
|
||||
(table) => [index('session_userId_idx').on(table.userId)]
|
||||
);
|
||||
|
||||
export const account = sqliteTable(
|
||||
"account",
|
||||
'account',
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: integer("access_token_expires_at", {
|
||||
mode: "timestamp_ms",
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: integer('access_token_expires_at', {
|
||||
mode: 'timestamp_ms'
|
||||
}),
|
||||
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
||||
mode: "timestamp_ms",
|
||||
refreshTokenExpiresAt: integer('refresh_token_expires_at', {
|
||||
mode: 'timestamp_ms'
|
||||
}),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
.notNull()
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
(table) => [index('account_userId_idx').on(table.userId)]
|
||||
);
|
||||
|
||||
export const verification = sqliteTable(
|
||||
"verification",
|
||||
'verification',
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
id: text('id').primaryKey(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
.notNull()
|
||||
},
|
||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||
(table) => [index('verification_identifier_idx').on(table.identifier)]
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
accounts: many(account)
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
references: [user.id]
|
||||
})
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
references: [user.id]
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -5,30 +5,40 @@ export * from './auth.schema';
|
||||
|
||||
export const notes = sqliteTable('notes', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
|
||||
title: text('title').notNull(),
|
||||
content: text('text').notNull(),
|
||||
|
||||
x: real("x").default(0).notNull(),
|
||||
y: real("y").default(0).notNull(),
|
||||
z: integer("z").default(1).notNull(),
|
||||
x: real('x').default(0).notNull(),
|
||||
y: real('y').default(0).notNull(),
|
||||
z: integer('z').default(1).notNull(),
|
||||
|
||||
color: text("color").default("#ffffff").notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
||||
color: text('color').default('#ffffff').notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
});
|
||||
|
||||
export const noteFiles = sqliteTable('note_files', {
|
||||
id: text('id').primaryKey(),
|
||||
noteId: text('note_id').notNull().references(() => notes.id, { onDelete: 'cascade' }),
|
||||
noteId: text('note_id')
|
||||
.notNull()
|
||||
.references(() => notes.id, { onDelete: 'cascade' }),
|
||||
|
||||
kind: text('kind', { enum: ['image', 'file'] }).notNull().default('file'),
|
||||
kind: text('kind', { enum: ['image', 'file'] })
|
||||
.notNull()
|
||||
.default('file'),
|
||||
position: integer('position').notNull().default(0),
|
||||
|
||||
name: text('name').notNull(),
|
||||
size: integer('size').notNull(),
|
||||
mimeType: text('mime_type').notNull(),
|
||||
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date())
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date())
|
||||
});
|
||||
|
||||
@@ -50,7 +50,9 @@ export const imapAuth = () => ({
|
||||
const adapter = ctx.context.internalAdapter;
|
||||
const existingUser = await adapter.findUserByEmail(email, { includeAccounts: true });
|
||||
|
||||
const credentialAccount = existingUser?.accounts.find((account) => account.providerId === 'credential');
|
||||
const credentialAccount = existingUser?.accounts.find(
|
||||
(account) => account.providerId === 'credential'
|
||||
);
|
||||
const credentialPassword = credentialAccount?.password;
|
||||
const isDbPasswordValid =
|
||||
typeof credentialPassword === 'string' &&
|
||||
|
||||
@@ -9,3 +9,9 @@
|
||||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,183 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Navbar from '$lib/components/ui/Navbar.svelte';
|
||||
import BoardCanvas from '$lib/components/board/BoardCanvas.svelte';
|
||||
import Minimap from '$lib/components/ui/Minimap.svelte';
|
||||
import type { Note } from '$lib/editor';
|
||||
import { BOARD_HEIGHT, BOARD_WIDTH } from '$lib/board/constants';
|
||||
import type { BoardNote, DragState, PanState, ViewportState } from '$lib/board/types';
|
||||
import { clamp, clampNoteX, clampNoteY, getRandomPosition } from '$lib/board/utils';
|
||||
|
||||
let notes = $state<BoardNote[]>([]);
|
||||
let drag = $state<DragState>(null);
|
||||
let pan = $state<PanState>(null);
|
||||
let maxZ = $state(1);
|
||||
|
||||
let viewport = $state<ViewportState>({
|
||||
scrollLeft: 0,
|
||||
scrollTop: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
});
|
||||
|
||||
let viewportEl: HTMLDivElement | null = null;
|
||||
|
||||
const updateViewportMetrics = () => {
|
||||
if (!viewportEl) return;
|
||||
|
||||
viewport = {
|
||||
scrollLeft: viewportEl.scrollLeft,
|
||||
scrollTop: viewportEl.scrollTop,
|
||||
width: viewportEl.clientWidth,
|
||||
height: viewportEl.clientHeight
|
||||
};
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
updateViewportMetrics();
|
||||
});
|
||||
|
||||
const handleCreate = (note: Note) => {
|
||||
const pos = getRandomPosition(notes.length);
|
||||
maxZ += 1;
|
||||
notes = [...notes, { ...note, x: pos.x, y: pos.y, z: maxZ }];
|
||||
};
|
||||
|
||||
const bringToFront = (id: string) => {
|
||||
maxZ += 1;
|
||||
notes = notes.map((note) => (note.id === id ? { ...note, z: maxZ } : note));
|
||||
};
|
||||
|
||||
const startDrag = (event: PointerEvent, id: string) => {
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
drag = {
|
||||
id,
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top
|
||||
};
|
||||
bringToFront(id);
|
||||
element.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
drag = null;
|
||||
};
|
||||
|
||||
const moveDrag = (event: PointerEvent) => {
|
||||
if (!drag || !viewportEl) return;
|
||||
|
||||
const viewportRect = viewportEl.getBoundingClientRect();
|
||||
const pointerBoardX = viewportEl.scrollLeft + (event.clientX - viewportRect.left);
|
||||
const pointerBoardY = viewportEl.scrollTop + (event.clientY - viewportRect.top);
|
||||
|
||||
const nextX = clampNoteX(pointerBoardX - drag.offsetX);
|
||||
const nextY = clampNoteY(pointerBoardY - drag.offsetY);
|
||||
|
||||
notes = notes.map((note) => (note.id === drag?.id ? { ...note, x: nextX, y: nextY } : note));
|
||||
};
|
||||
|
||||
const startPan = (event: PointerEvent) => {
|
||||
if (!viewportEl || drag) return;
|
||||
|
||||
pan = {
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
scrollLeft: viewportEl.scrollLeft,
|
||||
scrollTop: viewportEl.scrollTop
|
||||
};
|
||||
};
|
||||
|
||||
const movePan = (event: PointerEvent) => {
|
||||
if (!pan || !viewportEl || drag) return;
|
||||
|
||||
const dx = event.clientX - pan.startX;
|
||||
const dy = event.clientY - pan.startY;
|
||||
|
||||
viewportEl.scrollLeft = pan.scrollLeft - dx;
|
||||
viewportEl.scrollTop = pan.scrollTop - dy;
|
||||
updateViewportMetrics();
|
||||
};
|
||||
|
||||
const stopPan = () => {
|
||||
pan = null;
|
||||
};
|
||||
|
||||
const navigateTo = (scrollLeft: number, scrollTop: number) => {
|
||||
if (!viewportEl) return;
|
||||
|
||||
viewportEl.scrollLeft = clamp(scrollLeft, 0, BOARD_WIDTH - viewportEl.clientWidth);
|
||||
viewportEl.scrollTop = clamp(scrollTop, 0, BOARD_HEIGHT - viewportEl.clientHeight);
|
||||
updateViewportMetrics();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<svelte:window
|
||||
onresize={updateViewportMetrics}
|
||||
onpointermove={(event) => {
|
||||
moveDrag(event);
|
||||
movePan(event);
|
||||
}}
|
||||
onpointerup={() => {
|
||||
stopDrag();
|
||||
stopPan();
|
||||
}}
|
||||
onpointercancel={() => {
|
||||
stopDrag();
|
||||
stopPan();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="board-viewport" bind:this={viewportEl} onscroll={updateViewportMetrics}>
|
||||
<div class="top-composer-shell">
|
||||
<div class="top-composer">
|
||||
<Navbar onCreate={handleCreate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BoardCanvas notes={notes} onStartPan={startPan} onStartDrag={startDrag} />
|
||||
|
||||
<Minimap notes={notes} {viewport} onNavigate={navigateTo} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.board-viewport {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background:
|
||||
radial-gradient(circle at top right, #f8fbff, #eef2f7 45%, #e6ebf2 100%);
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.board-viewport:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.board-viewport::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-composer-shell {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 80;
|
||||
padding: 16px 14px 0;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.top-composer {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,8 @@ const config = {
|
||||
adapter: adapter()
|
||||
},
|
||||
vitePlugin: {
|
||||
dynamicCompileOptions: ({ filename }) => filename.includes('node_modules') ? undefined : { runes: true }
|
||||
dynamicCompileOptions: ({ filename }) =>
|
||||
filename.includes('node_modules') ? undefined : { runes: true }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user