Update Version: 1.4.5
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# TypstDrive
|
# TypstDrive
|
||||||
|
|
||||||
[](https://github.com/your-username/typstdrive)
|
[](https://github.com/your-username/typstdrive)
|
||||||
[](https://typst.app/)
|
[](https://typst.app/)
|
||||||
[](https://www.rust-lang.org/)
|
[](https://www.rust-lang.org/)
|
||||||
[](https://kit.svelte.dev/)
|
[](https://kit.svelte.dev/)
|
||||||
@@ -18,7 +18,8 @@ TypstDrive is a collaborative web editor for Typst. With built-in dark mode, mul
|
|||||||
- **Instant Preview**: Compile Typst to SVG on the fly with sub-second latency, featuring interactive document zoom controls and a collapsible preview pane.
|
- **Instant Preview**: Compile Typst to SVG on the fly with sub-second latency, featuring interactive document zoom controls and a collapsible preview pane.
|
||||||
- **Customizable Themes**: Choose from multiple editor themes (Catppuccin, Arch Linux, Cerberus) and toggle global dark mode.
|
- **Customizable Themes**: Choose from multiple editor themes (Catppuccin, Arch Linux, Cerberus) and toggle global dark mode.
|
||||||
- **Export Options**: Export your compiled documents directly to PDF, PNG, SVG, HTML, Markdown, Word, or LaTeX formats using internal conversion and Pandoc integrations.
|
- **Export Options**: Export your compiled documents directly to PDF, PNG, SVG, HTML, Markdown, Word, or LaTeX formats using internal conversion and Pandoc integrations.
|
||||||
- **User Authentication & Document Access**: Secure accounts, workspaces, and sharing features via email-based collaborator invitations (Editor or Viewer roles) for all your documents.
|
- **Document Sharing**: Invite collaborators by email with Editor or Viewer roles. Collaborators' uploaded fonts and images are available to the compiler. A dedicated "Shared with me" folder on the dashboard surfaces all documents others have shared with you. Manage and remove collaborators directly from the Share modal in the editor.
|
||||||
|
- **Public REST API**: Programmatically render Typst documents to PNG or PDF via `POST /v1/render`. Manage API keys from the Settings panel, with a live usage chart supporting 1-hour, 1-day, and 1-week views. Full API reference available at `/api-docs`.
|
||||||
- **Admin System**: First-run setup wizard creates an admin account. Admins can manage all users, create new accounts with temporary passwords, toggle admin privileges, and delete accounts from the Settings panel.
|
- **Admin System**: First-run setup wizard creates an admin account. Admins can manage all users, create new accounts with temporary passwords, toggle admin privileges, and delete accounts from the Settings panel.
|
||||||
- **Presentation Mode**: Turn your documents into instant slideshows with built-in slide controls and a live drawing/annotation tool overlay.
|
- **Presentation Mode**: Turn your documents into instant slideshows with built-in slide controls and a live drawing/annotation tool overlay.
|
||||||
- **Asset Management**: Upload and seamlessly use custom fonts and images directly within your documents.
|
- **Asset Management**: Upload and seamlessly use custom fonts and images directly within your documents.
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "typstdrive",
|
"name": "typstdrive",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.0",
|
"version": "1.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "1.4.0"
|
version = "1.4.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -96,7 +96,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed right-0 top-0 bottom-0 w-80 bg-[var(--theme-bg)] backdrop-blur-xl border-l shadow-2xl flex flex-col z-[70] transform transition-transform duration-300 border-[var(--theme-border)]">
|
<div class="fixed right-0 top-0 bottom-0 w-80 bg-[var(--theme-bg)] backdrop-blur-xl border-l shadow-2xl flex flex-col z-[70] transform transition-transform duration-300 border-[var(--theme-border)]">
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between px-4 py-3 border-b bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
<div class="flex items-center justify-between px-4 py-3 border-b bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon="mdi:comment-text-multiple-outline" class="text-lg" />
|
<Icon icon="mdi:comment-text-multiple-outline" class="text-lg" />
|
||||||
@@ -108,7 +107,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feed -->
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex justify-center items-center h-full">
|
<div class="flex justify-center items-center h-full">
|
||||||
@@ -137,7 +135,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="flex opacity-0 group-hover:opacity-100 transition-opacity gap-1">
|
<div class="flex opacity-0 group-hover:opacity-100 transition-opacity gap-1">
|
||||||
{#if $userStore?.id === comment.user_id}
|
{#if $userStore?.id === comment.user_id}
|
||||||
<button onclick={() => deleteComment(comment.id)} class="p-1 hover:text-red-500 rounded hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" title="Delete">
|
<button onclick={() => deleteComment(comment.id)} class="p-1 hover:text-red-500 rounded hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" title="Delete">
|
||||||
@@ -155,7 +152,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input Area -->
|
|
||||||
<div class="p-4 border-t bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
<div class="p-4 border-t bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -248,9 +248,6 @@
|
|||||||
const msg = JSON.parse(data);
|
const msg = JSON.parse(data);
|
||||||
if (msg.type === 'init') {
|
if (msg.type === 'init') {
|
||||||
lspInitialized = true;
|
lspInitialized = true;
|
||||||
|
|
||||||
// Recreate the client because the backend started a completely new LSP process
|
|
||||||
// which requires a fresh 'initialize' handshake.
|
|
||||||
client = new LSPClient({
|
client = new LSPClient({
|
||||||
rootUri: msg.rootUri,
|
rootUri: msg.rootUri,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -262,9 +259,7 @@
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {}
|
||||||
// Fallthrough
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let processedData = data;
|
let processedData = data;
|
||||||
@@ -281,9 +276,7 @@
|
|||||||
for (let h of lsHandlers) h(processedData);
|
for (let h of lsHandlers) h(processedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
lsSocket.onopen = () => {
|
lsSocket.onopen = () => {};
|
||||||
// Waiting for init message from server
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectLsp();
|
connectLsp();
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
let isDrawing = false;
|
let isDrawing = false;
|
||||||
let currentPath = $state<{x: number, y: number}[]>([]);
|
let currentPath = $state<{x: number, y: number}[]>([]);
|
||||||
|
|
||||||
// New feature states
|
|
||||||
let tool = $state<'pen' | 'highlighter' | 'eraser' | 'laser'>('laser');
|
let tool = $state<'pen' | 'highlighter' | 'eraser' | 'laser'>('laser');
|
||||||
let selectedColor = $state('#ef4444');
|
let selectedColor = $state('#ef4444');
|
||||||
let showGrid = $state(false);
|
let showGrid = $state(false);
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
|
|
||||||
const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7', '#ffffff', '#000000'];
|
const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7', '#ffffff', '#000000'];
|
||||||
|
|
||||||
// Map from slide index to image data url so we can persist drawings when switching slides
|
|
||||||
let drawings = $state<Record<number, string>>({});
|
let drawings = $state<Record<number, string>>({});
|
||||||
let undoStack = $state<Record<number, string[]>>({});
|
let undoStack = $state<Record<number, string[]>>({});
|
||||||
let redoStack = $state<Record<number, string[]>>({});
|
let redoStack = $state<Record<number, string[]>>({});
|
||||||
@@ -41,7 +39,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Find the preview svgs from the main DOM
|
|
||||||
const previewContainers = document.querySelectorAll('.preview-container svg');
|
const previewContainers = document.querySelectorAll('.preview-container svg');
|
||||||
const svgStrings: string[] = [];
|
const svgStrings: string[] = [];
|
||||||
previewContainers.forEach(container => {
|
previewContainers.forEach(container => {
|
||||||
@@ -49,7 +46,6 @@
|
|||||||
});
|
});
|
||||||
svgs = svgStrings;
|
svgs = svgStrings;
|
||||||
|
|
||||||
// Request fullscreen
|
|
||||||
const el = document.getElementById('presentation-container');
|
const el = document.getElementById('presentation-container');
|
||||||
if (el && el.requestFullscreen) {
|
if (el && el.requestFullscreen) {
|
||||||
el.requestFullscreen().catch(err => console.error(err));
|
el.requestFullscreen().catch(err => console.error(err));
|
||||||
@@ -104,7 +100,6 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (canvas && currentSlide !== undefined && !showGrid) {
|
if (canvas && currentSlide !== undefined && !showGrid) {
|
||||||
// Resize canvas to match the svg
|
|
||||||
const svgEl = document.getElementById('presentation-svg')?.querySelector('svg');
|
const svgEl = document.getElementById('presentation-svg')?.querySelector('svg');
|
||||||
if (svgEl) {
|
if (svgEl) {
|
||||||
const rect = svgEl.getBoundingClientRect();
|
const rect = svgEl.getBoundingClientRect();
|
||||||
@@ -126,7 +121,6 @@
|
|||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
// Load previous drawing if any
|
|
||||||
if (drawings[currentSlide]) {
|
if (drawings[currentSlide]) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
@@ -302,7 +296,6 @@
|
|||||||
|
|
||||||
<div id="presentation-container" class="fixed inset-0 z-[100] flex flex-col items-center justify-center">
|
<div id="presentation-container" class="fixed inset-0 z-[100] flex flex-col items-center justify-center">
|
||||||
|
|
||||||
<!-- Laser Pointer Overlay -->
|
|
||||||
{#if tool === 'laser' && laserPos.visible && !showGrid}
|
{#if tool === 'laser' && laserPos.visible && !showGrid}
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none fixed z-[150] w-3 h-3 bg-red-500 rounded-full shadow-[0_0_15px_5px_rgba(239,68,68,0.8)] -translate-x-1/2 -translate-y-1/2"
|
class="pointer-events-none fixed z-[150] w-3 h-3 bg-red-500 rounded-full shadow-[0_0_15px_5px_rgba(239,68,68,0.8)] -translate-x-1/2 -translate-y-1/2"
|
||||||
@@ -310,7 +303,6 @@
|
|||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Thumbnail Grid UI -->
|
|
||||||
{#if showGrid}
|
{#if showGrid}
|
||||||
<div class="absolute inset-0 z-[50] p-8 overflow-y-auto">
|
<div class="absolute inset-0 z-[50] p-8 overflow-y-auto">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6 max-w-7xl mx-auto pb-24">
|
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6 max-w-7xl mx-auto pb-24">
|
||||||
@@ -331,7 +323,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Top toolbar -->
|
|
||||||
<div class="absolute top-0 inset-x-0 h-16 bg-gradient-to-b from-black/80 to-transparent flex items-center justify-between px-6 transition-opacity duration-300 z-[110] {uiVisible || showGrid ? 'opacity-100' : 'opacity-0'}">
|
<div class="absolute top-0 inset-x-0 h-16 bg-gradient-to-b from-black/80 to-transparent flex items-center justify-between px-6 transition-opacity duration-300 z-[110] {uiVisible || showGrid ? 'opacity-100' : 'opacity-0'}">
|
||||||
<div class="flex items-center gap-4 bg-zinc-900/80 backdrop-blur-md px-4 py-2 rounded-xl border shadow-2xl border-[var(--theme-border)]">
|
<div class="flex items-center gap-4 bg-zinc-900/80 backdrop-blur-md px-4 py-2 rounded-xl border shadow-2xl border-[var(--theme-border)]">
|
||||||
<button class="p-2 rounded-lg transition-colors {tool === 'laser' ? 'bg-red-500/20 text-red-400' : 'text-gray-300 hover:bg-white/10'}" onclick={() => tool = 'laser'} title="Laser Pointer">
|
<button class="p-2 rounded-lg transition-colors {tool === 'laser' ? 'bg-red-500/20 text-red-400' : 'text-gray-300 hover:bg-white/10'}" onclick={() => tool = 'laser'} title="Laser Pointer">
|
||||||
@@ -383,19 +374,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Slide Area -->
|
|
||||||
<div class="relative w-full h-full flex items-center justify-center p-8">
|
<div class="relative w-full h-full flex items-center justify-center p-8">
|
||||||
{#if svgs.length > 0 && !showGrid}
|
{#if svgs.length > 0 && !showGrid}
|
||||||
<div id="presentation-svg" class="relative max-h-full max-w-full shadow-2xl flex items-center justify-center bg-[var(--theme-bg)] text-[var(--theme-text)]">
|
<div id="presentation-svg" class="relative max-h-full max-w-full shadow-2xl flex items-center justify-center bg-[var(--theme-bg)] text-[var(--theme-text)]">
|
||||||
{@html svgs[currentSlide]}
|
{@html svgs[currentSlide]}
|
||||||
|
|
||||||
<!-- Drawing Canvas Overlay -->
|
|
||||||
<canvas
|
<canvas
|
||||||
bind:this={canvas}
|
bind:this={canvas}
|
||||||
class="absolute inset-0 z-10 touch-none pointer-events-none"
|
class="absolute inset-0 z-10 touch-none pointer-events-none"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<!-- Active Stroke Canvas Overlay -->
|
|
||||||
<canvas
|
<canvas
|
||||||
bind:this={activeCanvas}
|
bind:this={activeCanvas}
|
||||||
class="absolute inset-0 z-20 touch-none {tool === 'laser' ? 'cursor-none' : 'cursor-crosshair'}"
|
class="absolute inset-0 z-20 touch-none {tool === 'laser' ? 'cursor-none' : 'cursor-crosshair'}"
|
||||||
@@ -413,7 +401,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Navigation -->
|
|
||||||
{#if svgs.length > 0}
|
{#if svgs.length > 0}
|
||||||
<div class="absolute bottom-6 flex items-center gap-4 bg-zinc-900/80 backdrop-blur-md px-6 py-3 rounded-full border shadow-2xl transition-opacity duration-300 z-[110] {uiVisible || showGrid ? 'opacity-100' : 'opacity-0'} border-[var(--theme-border)]">
|
<div class="absolute bottom-6 flex items-center gap-4 bg-zinc-900/80 backdrop-blur-md px-6 py-3 rounded-full border shadow-2xl transition-opacity duration-300 z-[110] {uiVisible || showGrid ? 'opacity-100' : 'opacity-0'} border-[var(--theme-border)]">
|
||||||
{#if svgs.length > 1}
|
{#if svgs.length > 1}
|
||||||
|
|||||||
@@ -414,7 +414,6 @@
|
|||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Sync status removed from here and moved to Footer -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed right-0 top-0 bottom-0 w-80 bg-[var(--theme-bg)] backdrop-blur-xl border-l shadow-2xl flex flex-col z-[70] transform transition-transform duration-300 border-[var(--theme-border)]">
|
<div class="fixed right-0 top-0 bottom-0 w-80 bg-[var(--theme-bg)] backdrop-blur-xl border-l shadow-2xl flex flex-col z-[70] transform transition-transform duration-300 border-[var(--theme-border)]">
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between px-4 py-3 border-b bg-gray-50/50 bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
<div class="flex items-center justify-between px-4 py-3 border-b bg-gray-50/50 bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon="mdi:history" class="text-lg" />
|
<Icon icon="mdi:history" class="text-lg" />
|
||||||
@@ -67,7 +66,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feed -->
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex justify-center items-center h-full">
|
<div class="flex justify-center items-center h-full">
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
} else {
|
} else {
|
||||||
const button = e.currentTarget as HTMLElement;
|
const button = e.currentTarget as HTMLElement;
|
||||||
const rect = button.getBoundingClientRect();
|
const rect = button.getBoundingClientRect();
|
||||||
// If there's less than 200px below the button, open upwards
|
|
||||||
if (window.innerHeight - rect.bottom < 200) {
|
if (window.innerHeight - rect.bottom < 200) {
|
||||||
dropUp = true;
|
dropUp = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -358,7 +358,6 @@
|
|||||||
try {
|
try {
|
||||||
data = JSON.parse(dataString);
|
data = JSON.parse(dataString);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// For backward compatibility if it's just an id
|
|
||||||
data = { type: 'document', id: dataString };
|
data = { type: 'document', id: dataString };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,13 +511,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Folders section — always visible at root (includes "Shared with me" virtual folder) -->
|
|
||||||
{#if currentFolderId === null || folders.length > 0}
|
{#if currentFolderId === null || folders.length > 0}
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="px-2 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300">Folders</div>
|
<div class="px-2 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300">Folders</div>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
{#if currentFolderId === null}
|
{#if currentFolderId === null}
|
||||||
<!-- Shared with me — permanent, undeletable virtual folder -->
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center p-3 bg-white/50 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-xl shadow-sm hover:shadow-md cursor-pointer group transition-all duration-200 hover:-translate-y-0.5 hover:border-purple-300 dark:hover:border-purple-500/30"
|
class="flex flex-row items-center p-3 bg-white/50 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-xl shadow-sm hover:shadow-md cursor-pointer group transition-all duration-200 hover:-translate-y-0.5 hover:border-purple-300 dark:hover:border-purple-500/30"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -565,7 +562,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Empty states -->
|
|
||||||
{#if documents.length === 0 && files.length === 0 && currentFolderId !== null && folders.length === 0}
|
{#if documents.length === 0 && files.length === 0 && currentFolderId !== null && folders.length === 0}
|
||||||
<div class="min-h-[30vh] flex items-center justify-center">
|
<div class="min-h-[30vh] flex items-center justify-center">
|
||||||
<p class="text-gray-500 dark:text-gray-400">This folder is empty.</p>
|
<p class="text-gray-500 dark:text-gray-400">This folder is empty.</p>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
const view = $editorViewStore;
|
const view = $editorViewStore;
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
|
|
||||||
// Ensure context menu only triggers on editor
|
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (!target.closest('.cm-editor') && !target.closest('.cm-content')) return;
|
if (!target.closest('.cm-editor') && !target.closest('.cm-content')) return;
|
||||||
|
|
||||||
@@ -148,7 +147,6 @@
|
|||||||
<DocFooter />
|
<DocFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom Context Menu for Editor -->
|
|
||||||
{#if contextMenu.show}
|
{#if contextMenu.show}
|
||||||
<div
|
<div
|
||||||
class="fixed z-[9999] bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-lg shadow-xl border border-[var(--theme-border)] py-1 min-w-[200px] overflow-hidden"
|
class="fixed z-[9999] bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-lg shadow-xl border border-[var(--theme-border)] py-1 min-w-[200px] overflow-hidden"
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto login after successful registration using email
|
|
||||||
const loginRes = await fetch('/api/auth/login', {
|
const loginRes = await fetch('/api/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -65,7 +64,6 @@
|
|||||||
|
|
||||||
<div class="min-h-screen flex flex-col relative overflow-hidden">
|
<div class="min-h-screen flex flex-col relative overflow-hidden">
|
||||||
<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
<!-- Background decorative elements -->
|
|
||||||
<div class="absolute -top-40 right-20 w-96 h-96 bg-emerald-400/20 dark:bg-emerald-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
<div class="absolute -top-40 right-20 w-96 h-96 bg-emerald-400/20 dark:bg-emerald-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
||||||
<div class="absolute top-40 -left-20 w-96 h-96 bg-teal-400/20 dark:bg-teal-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
<div class="absolute top-40 -left-20 w-96 h-96 bg-teal-400/20 dark:bg-teal-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
||||||
<div class="absolute -bottom-40 right-40 w-96 h-96 bg-blue-400/20 dark:bg-blue-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
<div class="absolute -bottom-40 right-40 w-96 h-96 bg-blue-400/20 dark:bg-blue-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
let confirmRegenerateId = $state<string | null>(null);
|
let confirmRegenerateId = $state<string | null>(null);
|
||||||
let regeneratingKeyId = $state<string | null>(null);
|
let regeneratingKeyId = $state<string | null>(null);
|
||||||
|
|
||||||
// Usage chart
|
|
||||||
type UsagePoint = { date: string; count: number };
|
type UsagePoint = { date: string; count: number };
|
||||||
type UsagePeriod = '1hr' | '1day' | '1week';
|
type UsagePeriod = '1hr' | '1day' | '1week';
|
||||||
let usageData = $state<UsagePoint[]>([]);
|
let usageData = $state<UsagePoint[]>([]);
|
||||||
@@ -200,7 +199,6 @@
|
|||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
const counts: number[] = [];
|
const counts: number[] = [];
|
||||||
if (period === '1hr') {
|
if (period === '1hr') {
|
||||||
// Floor to current UTC minute, then step back 59 more
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const baseMs = now.getTime() - (now.getUTCSeconds() * 1000 + now.getUTCMilliseconds());
|
const baseMs = now.getTime() - (now.getUTCSeconds() * 1000 + now.getUTCMilliseconds());
|
||||||
for (let i = 59; i >= 0; i--) {
|
for (let i = 59; i >= 0; i--) {
|
||||||
@@ -211,7 +209,6 @@
|
|||||||
counts.push(pt ? pt.count : 0);
|
counts.push(pt ? pt.count : 0);
|
||||||
}
|
}
|
||||||
} else if (period === '1day') {
|
} else if (period === '1day') {
|
||||||
// Floor to current UTC hour, then step back 23 more
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const baseMs = now.getTime() - (now.getUTCMinutes() * 60000 + now.getUTCSeconds() * 1000 + now.getUTCMilliseconds());
|
const baseMs = now.getTime() - (now.getUTCMinutes() * 60000 + now.getUTCSeconds() * 1000 + now.getUTCMilliseconds());
|
||||||
for (let i = 23; i >= 0; i--) {
|
for (let i = 23; i >= 0; i--) {
|
||||||
@@ -626,7 +623,6 @@
|
|||||||
<a href="/api-docs" class="text-blue-600 dark:text-blue-400 hover:underline ml-1">View API docs →</a>
|
<a href="/api-docs" class="text-blue-600 dark:text-blue-400 hover:underline ml-1">View API docs →</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Usage chart -->
|
|
||||||
<div class="mb-6 p-4 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/30">
|
<div class="mb-6 p-4 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/30">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
@@ -814,7 +810,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Admin Section -->
|
|
||||||
{#if activeSection === 'admin' && $userStore?.is_admin}
|
{#if activeSection === 'admin' && $userStore?.is_admin}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
|
|||||||
Reference in New Issue
Block a user