Compare commits
2 Commits
0db68da00c
...
2bfbf45eba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bfbf45eba | ||
|
|
40f01a7712 |
@@ -1,15 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type Annotation = {
|
import type { Annotation } from '$lib/types';
|
||||||
id: string;
|
|
||||||
documentId: string;
|
|
||||||
pageNumber: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
color: string;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DrawRect = {
|
type DrawRect = {
|
||||||
x: number;
|
x: number;
|
||||||
|
|||||||
@@ -1,25 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, untrack } from 'svelte';
|
import { onMount, untrack } from 'svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import type { Comment, CommentReply } from '$lib/types';
|
||||||
type CommentReply = {
|
|
||||||
id: string;
|
|
||||||
authorId: string | null;
|
|
||||||
authorName: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Comment = {
|
|
||||||
id: string;
|
|
||||||
authorId: string | null;
|
|
||||||
authorName: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
replies: CommentReply[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
@@ -189,154 +171,95 @@ onMount(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Renders a single comment or reply entry.
|
||||||
|
showReplyButton: whether the "Reply" button appears (only on last item in a thread).
|
||||||
|
-->
|
||||||
|
{#snippet commentEntry(comment: Comment | CommentReply, threadId: string, showReplyButton: boolean)}
|
||||||
|
{#if editingId === comment.id}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<textarea
|
||||||
|
class="w-full resize-none rounded border border-line px-3 py-2 font-serif text-sm text-ink focus:ring-1 focus:ring-accent focus:outline-none"
|
||||||
|
rows={3}
|
||||||
|
bind:value={editText}
|
||||||
|
></textarea>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white hover:bg-primary/80 disabled:opacity-40"
|
||||||
|
disabled={posting}
|
||||||
|
onclick={() => saveEdit(comment.id)}
|
||||||
|
>
|
||||||
|
{m.btn_save()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
||||||
|
onclick={cancelEdit}
|
||||||
|
>
|
||||||
|
{m.btn_cancel()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="font-sans text-xs font-semibold text-ink">{comment.authorName}</span>
|
||||||
|
<span class="font-sans text-xs text-ink-3">{timeAgo(comment.createdAt)}</span>
|
||||||
|
{#if wasEdited(comment)}
|
||||||
|
<span class="font-sans text-xs text-ink-3">
|
||||||
|
{m.comment_edited_label()}
|
||||||
|
{timeAgo(comment.updatedAt)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 font-serif text-sm leading-relaxed text-ink-2">{comment.content}</p>
|
||||||
|
</div>
|
||||||
|
{#if canModify(comment)}
|
||||||
|
<div class="flex shrink-0 items-center gap-2">
|
||||||
|
<button
|
||||||
|
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
||||||
|
onclick={() => startEdit(comment)}
|
||||||
|
>
|
||||||
|
{m.btn_edit()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
||||||
|
onclick={() => deleteComment(comment.id)}
|
||||||
|
>
|
||||||
|
{m.btn_delete()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if showReplyButton && canComment}
|
||||||
|
<div class="mt-1">
|
||||||
|
<button
|
||||||
|
class="font-sans text-xs font-medium text-accent transition-colors hover:text-ink"
|
||||||
|
onclick={() => startReply(threadId)}
|
||||||
|
>
|
||||||
|
{m.comment_btn_reply()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each comments as thread, ti (thread.id)}
|
{#each comments as thread, ti (thread.id)}
|
||||||
<div class={ti > 0 ? 'border-t border-line pt-4' : ''}>
|
<div class={ti > 0 ? 'border-t border-line pt-4' : ''}>
|
||||||
<!-- Root comment -->
|
<!-- Root comment -->
|
||||||
<div>
|
<div>
|
||||||
{#if editingId === thread.id}
|
{@render commentEntry(thread, thread.id, thread.replies.length === 0)}
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<textarea
|
|
||||||
class="w-full resize-none rounded border border-line px-3 py-2 font-serif text-sm text-ink focus:ring-1 focus:ring-accent focus:outline-none"
|
|
||||||
rows={3}
|
|
||||||
bind:value={editText}
|
|
||||||
></textarea>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white hover:bg-primary/80 disabled:opacity-40"
|
|
||||||
disabled={posting}
|
|
||||||
onclick={() => saveEdit(thread.id)}
|
|
||||||
>
|
|
||||||
{m.btn_save()}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
|
||||||
onclick={cancelEdit}
|
|
||||||
>
|
|
||||||
{m.btn_cancel()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-start justify-between gap-2">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="font-sans text-xs font-semibold text-ink">{thread.authorName}</span>
|
|
||||||
<span class="font-sans text-xs text-ink-3">{timeAgo(thread.createdAt)}</span>
|
|
||||||
{#if wasEdited(thread)}
|
|
||||||
<span class="font-sans text-xs text-ink-3">
|
|
||||||
{m.comment_edited_label()}
|
|
||||||
{timeAgo(thread.updatedAt)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 font-serif text-sm leading-relaxed text-ink-2">{thread.content}</p>
|
|
||||||
</div>
|
|
||||||
{#if canModify(thread)}
|
|
||||||
<div class="flex shrink-0 items-center gap-2">
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
|
||||||
onclick={() => startEdit(thread)}
|
|
||||||
>
|
|
||||||
{m.btn_edit()}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
|
||||||
onclick={() => deleteComment(thread.id)}
|
|
||||||
>
|
|
||||||
{m.btn_delete()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<!-- Reply button on root comment only if there are no replies -->
|
|
||||||
{#if thread.replies.length === 0 && canComment}
|
|
||||||
<div class="mt-1">
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs font-medium text-accent transition-colors hover:text-ink"
|
|
||||||
onclick={() => startReply(thread.id)}
|
|
||||||
>
|
|
||||||
{m.comment_btn_reply()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Replies -->
|
<!-- Replies -->
|
||||||
{#each thread.replies as reply, ri (reply.id)}
|
{#each thread.replies as reply, ri (reply.id)}
|
||||||
<div class="mt-3 ml-6 border-l-2 border-line pl-4">
|
<div class="mt-3 ml-6 border-l-2 border-line pl-4">
|
||||||
{#if editingId === reply.id}
|
{@render commentEntry(reply, thread.id, ri === thread.replies.length - 1)}
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<textarea
|
|
||||||
class="w-full resize-none rounded border border-line px-3 py-2 font-serif text-sm text-ink focus:ring-1 focus:ring-accent focus:outline-none"
|
|
||||||
rows={3}
|
|
||||||
bind:value={editText}
|
|
||||||
></textarea>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white hover:bg-primary/80 disabled:opacity-40"
|
|
||||||
disabled={posting}
|
|
||||||
onclick={() => saveEdit(reply.id)}
|
|
||||||
>
|
|
||||||
{m.btn_save()}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
|
||||||
onclick={cancelEdit}
|
|
||||||
>
|
|
||||||
{m.btn_cancel()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-start justify-between gap-2">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="font-sans text-xs font-semibold text-ink">{reply.authorName}</span>
|
|
||||||
<span class="font-sans text-xs text-ink-3">{timeAgo(reply.createdAt)}</span>
|
|
||||||
{#if wasEdited(reply)}
|
|
||||||
<span class="font-sans text-xs text-ink-3">
|
|
||||||
{m.comment_edited_label()}
|
|
||||||
{timeAgo(reply.updatedAt)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 font-serif text-sm leading-relaxed text-ink-2">{reply.content}</p>
|
|
||||||
</div>
|
|
||||||
{#if canModify(reply)}
|
|
||||||
<div class="flex shrink-0 items-center gap-2">
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
|
||||||
onclick={() => startEdit(reply)}
|
|
||||||
>
|
|
||||||
{m.btn_edit()}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs text-ink-3 transition-colors hover:text-ink"
|
|
||||||
onclick={() => deleteComment(reply.id)}
|
|
||||||
>
|
|
||||||
{m.btn_delete()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<!-- Reply button only on the last reply -->
|
|
||||||
{#if ri === thread.replies.length - 1 && canComment}
|
|
||||||
<div class="mt-1">
|
|
||||||
<button
|
|
||||||
class="font-sans text-xs font-medium text-accent transition-colors hover:text-ink"
|
|
||||||
onclick={() => startReply(thread.id)}
|
|
||||||
>
|
|
||||||
{m.comment_btn_reply()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Reply textarea (shown when replyingTo === thread.id) -->
|
<!-- Reply compose box -->
|
||||||
{#if replyingTo === thread.id}
|
{#if replyingTo === thread.id}
|
||||||
<div class="mt-3 ml-6 flex flex-col gap-2">
|
<div class="mt-3 ml-6 flex flex-col gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -365,7 +288,7 @@ onMount(() => {
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- New top-level comment textarea -->
|
<!-- New top-level comment -->
|
||||||
{#if canComment}
|
{#if canComment}
|
||||||
<div class={comments.length > 0 ? 'border-t border-line pt-4' : ''}>
|
<div class={comments.length > 0 ? 'border-t border-line pt-4' : ''}>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -4,27 +4,7 @@ import PanelMetadata from './PanelMetadata.svelte';
|
|||||||
import PanelTranscription from './PanelTranscription.svelte';
|
import PanelTranscription from './PanelTranscription.svelte';
|
||||||
import PanelDiscussion from './PanelDiscussion.svelte';
|
import PanelDiscussion from './PanelDiscussion.svelte';
|
||||||
import PanelHistory from './PanelHistory.svelte';
|
import PanelHistory from './PanelHistory.svelte';
|
||||||
|
import type { Comment, DocumentPanelTab } from '$lib/types';
|
||||||
type Tab = 'metadata' | 'transcription' | 'discussion' | 'history';
|
|
||||||
|
|
||||||
type CommentReply = {
|
|
||||||
id: string;
|
|
||||||
authorId: string | null;
|
|
||||||
authorName: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Comment = {
|
|
||||||
id: string;
|
|
||||||
authorId: string | null;
|
|
||||||
authorName: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
replies: CommentReply[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -47,7 +27,7 @@ type Props = {
|
|||||||
canAdmin: boolean;
|
canAdmin: boolean;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
height: number;
|
height: number;
|
||||||
activeTab: Tab;
|
activeTab: DocumentPanelTab;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -72,7 +52,7 @@ function fullHeight() {
|
|||||||
return window.innerHeight - (topbar?.getBoundingClientRect().bottom ?? 0);
|
return window.innerHeight - (topbar?.getBoundingClientRect().bottom ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTab(tab: Tab) {
|
function openTab(tab: DocumentPanelTab) {
|
||||||
activeTab = tab;
|
activeTab = tab;
|
||||||
if (!open) {
|
if (!open) {
|
||||||
open = true;
|
open = true;
|
||||||
@@ -110,7 +90,7 @@ function onDragEnd() {
|
|||||||
isDragging = false;
|
isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs: { id: Tab; label: () => string }[] = [
|
const tabs: { id: DocumentPanelTab; label: () => string }[] = [
|
||||||
{ id: 'metadata', label: m.doc_panel_tab_metadata },
|
{ id: 'metadata', label: m.doc_panel_tab_metadata },
|
||||||
{ id: 'transcription', label: m.doc_panel_tab_transcription },
|
{ id: 'transcription', label: m.doc_panel_tab_transcription },
|
||||||
{ id: 'discussion', label: m.doc_panel_tab_discussion },
|
{ id: 'discussion', label: m.doc_panel_tab_discussion },
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CommentThread from './CommentThread.svelte';
|
import CommentThread from './CommentThread.svelte';
|
||||||
|
import type { Comment } from '$lib/types';
|
||||||
type CommentReply = {
|
|
||||||
id: string;
|
|
||||||
authorId: string | null;
|
|
||||||
authorName: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Comment = {
|
|
||||||
id: string;
|
|
||||||
authorId: string | null;
|
|
||||||
authorName: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
replies: CommentReply[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { onMount } from 'svelte';
|
|||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist';
|
import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist';
|
||||||
import AnnotationLayer from './AnnotationLayer.svelte';
|
import AnnotationLayer from './AnnotationLayer.svelte';
|
||||||
|
import type { Annotation } from '$lib/types';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -43,19 +44,6 @@ let textLayerInstance: { cancel: () => void } | null = null;
|
|||||||
let pdfjsLib: typeof import('pdfjs-dist') | null = null;
|
let pdfjsLib: typeof import('pdfjs-dist') | null = null;
|
||||||
let pdfjsReady = $state(false);
|
let pdfjsReady = $state(false);
|
||||||
|
|
||||||
type Annotation = {
|
|
||||||
id: string;
|
|
||||||
documentId: string;
|
|
||||||
pageNumber: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
color: string;
|
|
||||||
createdAt: string;
|
|
||||||
fileHash?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let annotations = $state<Annotation[]>([]);
|
let annotations = $state<Annotation[]>([]);
|
||||||
let annotateColor = $state('#ffff00');
|
let annotateColor = $state('#ffff00');
|
||||||
let commentCounts = new SvelteMap<string, number>();
|
let commentCounts = new SvelteMap<string, number>();
|
||||||
|
|||||||
33
frontend/src/lib/types.ts
Normal file
33
frontend/src/lib/types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export type CommentReply = {
|
||||||
|
id: string;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Comment = {
|
||||||
|
id: string;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
replies: CommentReply[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocumentPanelTab = 'metadata' | 'transcription' | 'discussion' | 'history';
|
||||||
|
|
||||||
|
export type Annotation = {
|
||||||
|
id: string;
|
||||||
|
documentId: string;
|
||||||
|
pageNumber: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
color: string;
|
||||||
|
createdAt: string;
|
||||||
|
fileHash?: string | null;
|
||||||
|
};
|
||||||
@@ -4,8 +4,7 @@ import DocumentTopBar from '$lib/components/DocumentTopBar.svelte';
|
|||||||
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
|
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
|
||||||
import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte';
|
import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte';
|
||||||
import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte';
|
import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte';
|
||||||
|
import type { DocumentPanelTab } from '$lib/types';
|
||||||
type Tab = 'metadata' | 'transcription' | 'discussion' | 'history';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ const LS_KEY_TAB = 'doc-panel-tab';
|
|||||||
let panelOpen = $state(false);
|
let panelOpen = $state(false);
|
||||||
let panelHeight = $state(0); // set to full height on mount
|
let panelHeight = $state(0); // set to full height on mount
|
||||||
let navHeight = $state(0);
|
let navHeight = $state(0);
|
||||||
let activeTab = $state<Tab>('metadata');
|
let activeTab = $state<DocumentPanelTab>('metadata');
|
||||||
let localStorageRestored = $state(false);
|
let localStorageRestored = $state(false);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -82,7 +81,7 @@ onMount(() => {
|
|||||||
const savedTab = localStorage.getItem(LS_KEY_TAB);
|
const savedTab = localStorage.getItem(LS_KEY_TAB);
|
||||||
|
|
||||||
if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) {
|
if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) {
|
||||||
activeTab = savedTab as Tab;
|
activeTab = savedTab as DocumentPanelTab;
|
||||||
}
|
}
|
||||||
const topbar = document.querySelector('[data-topbar]');
|
const topbar = document.querySelector('[data-topbar]');
|
||||||
panelHeight = window.innerHeight - navHeight - (topbar?.getBoundingClientRect().height ?? 0);
|
panelHeight = window.innerHeight - navHeight - (topbar?.getBoundingClientRect().height ?? 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user