Rework UI to match typst-desktop's design system

This commit is contained in:
2026-07-20 23:59:12 -04:00
parent de2c2aaccc
commit 2110d408bd
48 changed files with 1319 additions and 1315 deletions
+89
View File
@@ -6,12 +6,101 @@
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
} }
:root {
color-scheme: light;
--color-surface: #ffffff;
--color-surface-muted: #eff1f5;
--color-surface-sunken: #dce0e8;
--color-line: #ccd0da;
--color-ink: #11111b;
--color-ink-muted: #5c5f77;
--color-accent: #1e66f5;
--color-accent-soft: #dce8fd;
--color-danger: #d5382f;
--color-success: #1f8a4c;
}
:root[data-theme='dark'] {
color-scheme: dark;
--color-surface: #181825;
--color-surface-muted: #1e1e2e;
--color-surface-sunken: #45475a;
--color-line: #313244;
--color-ink: #cdd6f4;
--color-ink-muted: #6c7086;
--color-accent: #89b4fa;
--color-accent-soft: #1e2f4d;
--color-danger: #f4736a;
--color-success: #4cc47f;
}
:root[data-color-theme='Cerberus'] {
--color-surface: #ffffff;
--color-surface-muted: #ffffff;
--color-surface-sunken: #f4f4f5;
--color-line: #d4d4d4;
--color-ink: #000000;
--color-ink-muted: #52525b;
--color-accent: #4338ca;
--color-accent-soft: #e0e7ff;
}
:root[data-color-theme='Cerberus'][data-theme='dark'] {
--color-surface: #121212;
--color-surface-muted: #171717;
--color-surface-sunken: #1f1f1f;
--color-line: #262626;
--color-ink: #f5f5f5;
--color-ink-muted: #737373;
--color-accent: #818cf8;
--color-accent-soft: #262244;
}
:root[data-color-theme='Arch Linux'] {
--color-surface: #ffffff;
--color-surface-muted: #ffffff;
--color-surface-sunken: #f6f8fa;
--color-line: #d0d7de;
--color-ink: #0d1117;
--color-ink-muted: #57606a;
--color-accent: #0550ae;
--color-accent-soft: #dbeafe;
}
:root[data-color-theme='Arch Linux'][data-theme='dark'] {
--color-surface: #010409;
--color-surface-muted: #0d1117;
--color-surface-sunken: #161b22;
--color-line: #21262d;
--color-ink: #c9d1d9;
--color-ink-muted: #6e7681;
--color-accent: #1793d1;
--color-accent-soft: #0d2b3d;
}
:root {
--theme-bg: var(--color-surface-muted);
--theme-text: var(--color-ink);
--theme-border: var(--color-line);
--theme-cursor: var(--color-accent);
}
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
min-height: 100vh; min-height: 100vh;
} }
body {
background-color: var(--color-surface-muted);
color: var(--color-ink);
}
html.dark { html.dark {
color-scheme: dark; color-scheme: dark;
} }
.scroll-thin {
scrollbar-width: thin;
scrollbar-color: var(--color-line) transparent;
}
+1
View File
@@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" /> <meta name="text-scale" content="scale" />
%sveltekit.head% %sveltekit.head%
+7 -7
View File
@@ -102,7 +102,7 @@
<h2 class="text-sm font-semibold text-[var(--theme-text)]">Comments</h2> <h2 class="text-sm font-semibold text-[var(--theme-text)]">Comments</h2>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{comments.length}</span> <span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{comments.length}</span>
</div> </div>
<button onclick={onClose} class="p-1.5 hover:text-gray-600 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-white/10 rounded-md transition-colors" title="Close Comments"> <button onclick={onClose} class="p-1.5 hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Close Comments">
<Icon icon="mdi:close" class="text-lg" /> <Icon icon="mdi:close" class="text-lg" />
</button> </button>
</div> </div>
@@ -113,7 +113,7 @@
<Icon icon="mdi:loading" class="animate-spin text-2xl" /> <Icon icon="mdi:loading" class="animate-spin text-2xl" />
</div> </div>
{:else if error} {:else if error}
<div class="text-red-500 text-sm text-center p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/30"> <div class="text-[var(--color-danger)] text-sm text-center p-4 bg-[var(--color-danger)]/10 rounded-md border border-[var(--color-danger)]/20">
{error} {error}
</div> </div>
{:else if comments.length === 0} {:else if comments.length === 0}
@@ -126,7 +126,7 @@
<div class="group flex flex-col gap-2 p-3 border rounded-xl shadow-sm hover:shadow-md transition-all {comment.resolved ? 'opacity-60' : ''} bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]"> <div class="group flex flex-col gap-2 p-3 border rounded-xl shadow-sm hover:shadow-md transition-all {comment.resolved ? 'opacity-60' : ''} bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 flex items-center justify-center text-xs font-bold"> <div class="w-6 h-6 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] flex items-center justify-center text-xs font-bold">
{(comment.author_name || 'A').substring(0, 1).toUpperCase()} {(comment.author_name || 'A').substring(0, 1).toUpperCase()}
</div> </div>
<div> <div>
@@ -137,11 +137,11 @@
<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-[var(--color-danger)] rounded hover:bg-[var(--color-danger)]/10 transition-colors" title="Delete">
<Icon icon="mdi:trash-can-outline" class="text-xs" /> <Icon icon="mdi:trash-can-outline" class="text-xs" />
</button> </button>
{/if} {/if}
<button onclick={() => toggleResolve(comment)} class="p-1 hover:text-emerald-500 rounded hover:bg-emerald-50 dark:hover:bg-emerald-500/10 transition-colors" title={comment.resolved ? "Reopen" : "Resolve"}> <button onclick={() => toggleResolve(comment)} class="p-1 hover:text-[var(--color-success)] rounded hover:bg-[var(--color-success)]/10 transition-colors" title={comment.resolved ? "Reopen" : "Resolve"}>
<Icon icon={comment.resolved ? "mdi:check-circle" : "mdi:check-circle-outline"} class="text-xs" /> <Icon icon={comment.resolved ? "mdi:check-circle" : "mdi:check-circle-outline"} class="text-xs" />
</button> </button>
</div> </div>
@@ -157,7 +157,7 @@
<textarea <textarea
bind:value={newCommentContent} bind:value={newCommentContent}
placeholder="Add a comment..." placeholder="Add a comment..."
class="w-full border text-[var(--theme-text)] text-sm rounded-xl px-3 py-2.5 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500/50 resize-none min-h-[80px] bg-[var(--theme-bg)] border-[var(--theme-border)]" class="w-full border text-[var(--theme-text)] text-sm rounded-md px-3 py-2.5 pr-10 focus:outline-none focus:border-[var(--color-accent)] resize-none min-h-[80px] bg-[var(--theme-bg)] border-[var(--theme-border)]"
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
@@ -168,7 +168,7 @@
<button <button
onclick={postComment} onclick={postComment}
disabled={!newCommentContent.trim()} disabled={!newCommentContent.trim()}
class="absolute bottom-2.5 right-2.5 p-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 dark:disabled:bg-zinc-700 disabled:text-gray-500 rounded-lg transition-colors" class="absolute bottom-2.5 right-2.5 p-1.5 bg-[var(--color-accent)] hover:opacity-90 disabled:bg-[var(--color-surface-sunken)] disabled:text-[var(--color-ink-muted)] rounded-md transition-colors"
title="Post (Enter)" title="Post (Enter)"
> >
<Icon icon="mdi:send" class="text-sm" /> <Icon icon="mdi:send" class="text-sm" />
+38
View File
@@ -0,0 +1,38 @@
<script lang="ts">
import Modal from "./Modal.svelte";
interface Props {
title: string;
message: string;
confirmLabel?: string;
onconfirm: () => void;
onclose: () => void;
}
let {
title,
message,
confirmLabel = "Delete",
onconfirm,
onclose,
}: Props = $props();
</script>
<Modal {title} icon="ph:warning-circle" {onclose}>
<p class="text-sm text-[var(--color-ink-muted)]">{message}</p>
{#snippet footer()}
<button
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
onclick={onclose}
>
Cancel
</button>
<button
class="rounded-md bg-[var(--color-danger)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
onclick={onconfirm}
>
{confirmLabel}
</button>
{/snippet}
</Modal>
+5 -5
View File
@@ -11,14 +11,14 @@
<div class="h-8 border-t border-[var(--theme-border)] bg-[var(--theme-bg)] flex items-center justify-between px-4 text-xs text-[var(--theme-text)] select-none z-[60] relative"> <div class="h-8 border-t border-[var(--theme-border)] bg-[var(--theme-bg)] flex items-center justify-between px-4 text-xs text-[var(--theme-text)] select-none z-[60] relative">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="flex items-center gap-1.5 font-medium { <div class="flex items-center gap-1.5 font-medium {
$connectionStatus === 'connected' ? 'text-emerald-600 dark:text-emerald-400' : 'text-amber-600 dark:text-amber-400' $connectionStatus === 'connected' ? 'text-[var(--color-success)]' : 'text-amber-600 dark:text-amber-400'
}"> }">
<div class="w-1.5 h-1.5 rounded-full {$connectionStatus === 'connected' ? 'bg-emerald-500 shadow-[0_0_4px_rgba(16,185,129,0.4)]' : 'bg-amber-500 animate-pulse'}"></div> <div class="w-1.5 h-1.5 rounded-full {$connectionStatus === 'connected' ? 'bg-[var(--color-success)] shadow-[0_0_4px_rgba(16,185,129,0.4)]' : 'bg-amber-500 animate-pulse'}"></div>
{$connectionStatus === 'connected' ? 'Document synced' : 'Connecting...'} {$connectionStatus === 'connected' ? 'Document synced' : 'Connecting...'}
</div> </div>
{#if $documentStatsStore} {#if $documentStatsStore}
<button class="hover:bg-gray-100 dark:hover:bg-white/10 px-2 py-0.5 rounded transition-colors flex items-center gap-1 cursor-pointer" onclick={toggleModal} aria-label="Word count statistics"> <button class="hover:bg-[var(--color-surface-sunken)] px-2 py-0.5 rounded transition-colors flex items-center gap-1 cursor-pointer" onclick={toggleModal} aria-label="Word count statistics">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
{$documentStatsStore.words} words {$documentStatsStore.words} words
</button> </button>
@@ -29,11 +29,11 @@
{#if showStatsModal} {#if showStatsModal}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="fixed inset-0 bg-black/20 dark:bg-black/40 z-[100] flex items-center justify-center backdrop-blur-sm" onclick={toggleModal}> <div class="fixed inset-0 bg-black/40 z-[100] flex items-center justify-center" onclick={toggleModal}>
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded-xl shadow-xl w-80 overflow-hidden" onclick={e => e.stopPropagation()}> <div class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded-xl shadow-xl w-80 overflow-hidden" onclick={e => e.stopPropagation()}>
<div class="px-4 py-3 border-b border-[var(--theme-border)] flex items-center justify-between"> <div class="px-4 py-3 border-b border-[var(--theme-border)] flex items-center justify-between">
<h3 class="font-semibold text-sm">Word count</h3> <h3 class="font-semibold text-sm">Word count</h3>
<button onclick={toggleModal} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"> <button onclick={toggleModal} aria-label="Close" class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button> </button>
</div> </div>
+2 -2
View File
@@ -3,13 +3,13 @@
let { sticky = true }: { sticky?: boolean } = $props(); let { sticky = true }: { sticky?: boolean } = $props();
</script> </script>
<footer class="mt-auto py-6 text-center text-sm text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-white/10 bg-[var(--theme-bg)] {sticky ? 'sticky bottom-0 z-10' : ''} w-full flex-shrink-0 transition-colors duration-200"> <footer class="mt-auto py-6 text-center text-sm text-[var(--color-ink-muted)] border-t border-[var(--color-line)] bg-[var(--color-surface)] {sticky ? 'sticky bottom-0 z-10' : ''} w-full flex-shrink-0 transition-colors duration-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center items-center gap-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center items-center gap-4">
<a <a
href="https://github.com/SirBlobby/TypstDrive" href="https://github.com/SirBlobby/TypstDrive"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-flex items-center gap-2 hover:text-gray-900 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-1.5 rounded-lg border border-transparent dark:border-white/10" class="inline-flex items-center gap-2 hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-1.5 rounded-md"
> >
<Icon icon="mdi:github" class="text-xl" /> <Icon icon="mdi:github" class="text-xl" />
<span class="font-semibold">GitHub Repository</span> <span class="font-semibold">GitHub Repository</span>
+69
View File
@@ -0,0 +1,69 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import type { Snippet } from "svelte";
interface Props {
title: string;
icon?: string;
width?: string;
onclose: () => void;
children: Snippet;
footer?: Snippet;
}
let {
title,
icon = "ph:squares-four",
width = "max-w-md",
onclose,
children,
footer,
}: Props = $props();
function handleKey(event: KeyboardEvent) {
if (event.key === "Escape") onclose();
}
</script>
<svelte:window on:keydown={handleKey} />
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-6"
role="presentation"
onclick={(event) => {
if (event.target === event.currentTarget) onclose();
}}
>
<div
class="w-full {width} overflow-hidden rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] shadow-2xl"
role="dialog"
aria-modal="true"
aria-label={title}
>
<header
class="flex items-center gap-2 border-b border-[var(--color-line)] px-5 py-3.5"
>
<Icon {icon} class="text-lg text-[var(--color-accent)]" />
<h2 class="flex-1 text-sm font-semibold">{title}</h2>
<button
class="rounded p-1 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
onclick={onclose}
aria-label="Close"
>
<Icon icon="ph:x" class="text-base" />
</button>
</header>
<div class="scroll-thin max-h-[70vh] overflow-y-auto px-5 py-4">
{@render children()}
</div>
{#if footer}
<footer
class="flex items-center justify-end gap-2 border-t border-[var(--color-line)] bg-[var(--color-surface-muted)] px-5 py-3"
>
{@render footer()}
</footer>
{/if}
</div>
</div>
+90 -100
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Modal from './Modal.svelte';
let props = $props<{ let props = $props<{
onClose: () => void; onClose: () => void;
@@ -60,112 +60,102 @@
props.onApply(newPageSettings, newDocSettings); props.onApply(newPageSettings, newDocSettings);
props.onClose(); props.onClose();
} }
const fieldClass = "w-full rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none";
</script> </script>
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200" role="presentation" onclick={() => props.onClose()} onkeydown={(e) => { if (e.key === "Enter") { props.onClose(); } }}> <Modal title="Document & page settings" icon="ph:file-text" width="max-w-2xl" onclose={props.onClose}>
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-[var(--theme-border)] w-full max-w-2xl overflow-hidden flex flex-col max-h-[85vh]" role="dialog" tabindex="-1" aria-modal="true" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.key === 'Escape' && props.onClose()}> <div class="flex flex-col gap-8">
<div class="flex justify-between items-center p-5 border-b border-[var(--theme-border)]">
<h2 class="text-lg font-semibold flex items-center gap-2">
<Icon icon="mdi:file-document-edit-outline" class="text-blue-500 text-xl" />
Document & Page Settings
</h2>
<button onclick={() => props.onClose()} class="opacity-60 hover:opacity-100 rounded-full p-1 transition-opacity">
<Icon icon="mdi:close" class="text-xl" />
</button>
</div>
<div class="p-6 space-y-8 overflow-y-auto flex-1"> <section>
<h3 class="text-xs font-semibold uppercase tracking-wider text-[var(--color-ink-muted)] mb-4 border-b border-[var(--color-line)] pb-2">Document metadata</h3>
<section> <div class="grid grid-cols-2 gap-4">
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Document Metadata</h3> <div class="flex flex-col gap-1">
<div class="grid grid-cols-2 gap-4"> <label for="docTitle" class="text-xs font-medium text-[var(--color-ink-muted)]">PDF title</label>
<div class="space-y-2"> <input id="docTitle" type="text" bind:value={docTitle} class={fieldClass} placeholder="My Report" />
<label for="docTitle" class="text-sm font-medium">PDF Title</label>
<input id="docTitle" type="text" bind:value={docTitle} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="My Report" />
</div>
<div class="space-y-2">
<label for="author" class="text-sm font-medium">Author</label>
<input id="author" type="text" bind:value={author} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="Jane Doe" />
</div>
</div> </div>
</section> <div class="flex flex-col gap-1">
<label for="author" class="text-xs font-medium text-[var(--color-ink-muted)]">Author</label>
<input id="author" type="text" bind:value={author} class={fieldClass} placeholder="Jane Doe" />
<section>
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Page Layout</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<label for="paper" class="text-sm font-medium">Paper Size</label>
<select id="paper" bind:value={paper} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2">
<option value="a4">A4</option>
<option value="us-letter">US Letter</option>
<option value="a5">A5</option>
<option value="presentation-16-9">16:9 Presentation</option>
<option value="presentation-4-3">4:3 Presentation</option>
</select>
</div>
<div class="space-y-2">
<label for="margin" class="text-sm font-medium">Margin</label>
<input id="margin" type="text" bind:value={margin} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="auto or 1in" />
</div>
<div class="space-y-2">
<label for="width" class="text-sm font-medium">Width</label>
<input id="width" type="text" bind:value={width} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="auto" />
</div>
<div class="space-y-2">
<label for="height" class="text-sm font-medium">Height</label>
<input id="height" type="text" bind:value={height} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="auto" />
</div>
<div class="space-y-2">
<label for="columns" class="text-sm font-medium">Columns</label>
<input id="columns" type="number" min="1" max="10" bind:value={columns} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" />
</div>
<div class="space-y-2">
<label for="fill" class="text-sm font-medium">Background Fill</label>
<input id="fill" type="text" bind:value={fill} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="auto or rgb(200, 200, 200)" />
</div>
</div> </div>
</div>
</section>
<div class="flex items-center gap-2 mt-4">
<input id="flipped" type="checkbox" bind:checked={flipped} class="rounded text-blue-600 focus:ring-blue-500 bg-[var(--theme-bg)] border-[var(--theme-border)]" /> <section>
<label for="flipped" class="text-sm font-medium">Landscape Orientation (Flipped)</label> <h3 class="text-xs font-semibold uppercase tracking-wider text-[var(--color-ink-muted)] mb-4 border-b border-[var(--color-line)] pb-2">Page layout</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<label for="paper" class="text-xs font-medium text-[var(--color-ink-muted)]">Paper size</label>
<select id="paper" bind:value={paper} class={fieldClass}>
<option value="a4">A4</option>
<option value="us-letter">US Letter</option>
<option value="a5">A5</option>
<option value="presentation-16-9">16:9 Presentation</option>
<option value="presentation-4-3">4:3 Presentation</option>
</select>
</div> </div>
</section> <div class="flex flex-col gap-1">
<label for="margin" class="text-xs font-medium text-[var(--color-ink-muted)]">Margin</label>
<input id="margin" type="text" bind:value={margin} class={fieldClass} placeholder="auto or 1in" />
<section>
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Headers & Footers</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<label for="numbering" class="text-sm font-medium">Page Numbering</label>
<select id="numbering" bind:value={numbering} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2">
<option value="none">None</option>
<option value="1">1, 2, 3</option>
<option value="1/1">1/3, 2/3, 3/3</option>
<option value="a">a, b, c</option>
<option value="i">i, ii, iii</option>
<option value="I">I, II, III</option>
</select>
</div>
<div class="space-y-2">
<label for="header" class="text-sm font-medium">Header Content</label>
<input id="header" type="text" bind:value={header} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="auto or [Text]" />
</div>
<div class="space-y-2 sm:col-span-2">
<label for="footer" class="text-sm font-medium">Footer Content</label>
<input id="footer" type="text" bind:value={footer} class="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" placeholder="auto or [Text]" />
</div>
</div> </div>
</section> <div class="flex flex-col gap-1">
</div> <label for="width" class="text-xs font-medium text-[var(--color-ink-muted)]">Width</label>
<input id="width" type="text" bind:value={width} class={fieldClass} placeholder="auto" />
</div>
<div class="flex flex-col gap-1">
<label for="height" class="text-xs font-medium text-[var(--color-ink-muted)]">Height</label>
<input id="height" type="text" bind:value={height} class={fieldClass} placeholder="auto" />
</div>
<div class="flex flex-col gap-1">
<label for="columns" class="text-xs font-medium text-[var(--color-ink-muted)]">Columns</label>
<input id="columns" type="number" min="1" max="10" bind:value={columns} class={fieldClass} />
</div>
<div class="flex flex-col gap-1">
<label for="fill" class="text-xs font-medium text-[var(--color-ink-muted)]">Background fill</label>
<input id="fill" type="text" bind:value={fill} class={fieldClass} placeholder="auto or rgb(200, 200, 200)" />
</div>
</div>
<div class="p-5 border-t border-[var(--theme-border)] flex justify-end gap-3" style="background-color: var(--theme-border);"> <div class="flex items-center gap-2 mt-4">
<button onclick={() => props.onClose()} class="px-4 py-2 text-sm font-medium bg-[var(--theme-bg)] opacity-80 hover:opacity-100 rounded-lg transition-opacity border border-[var(--theme-border)]"> <input id="flipped" type="checkbox" bind:checked={flipped} class="rounded border-[var(--color-line)] text-[var(--color-accent)] focus:ring-[var(--color-accent)]" />
Cancel <label for="flipped" class="text-xs font-medium text-[var(--color-ink-muted)]">Landscape orientation (flipped)</label>
</button> </div>
<button onclick={apply} class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm"> </section>
Apply Settings
</button>
</div> <section>
<h3 class="text-xs font-semibold uppercase tracking-wider text-[var(--color-ink-muted)] mb-4 border-b border-[var(--color-line)] pb-2">Headers & footers</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<label for="numbering" class="text-xs font-medium text-[var(--color-ink-muted)]">Page numbering</label>
<select id="numbering" bind:value={numbering} class={fieldClass}>
<option value="none">None</option>
<option value="1">1, 2, 3</option>
<option value="1/1">1/3, 2/3, 3/3</option>
<option value="a">a, b, c</option>
<option value="i">i, ii, iii</option>
<option value="I">I, II, III</option>
</select>
</div>
<div class="flex flex-col gap-1">
<label for="header" class="text-xs font-medium text-[var(--color-ink-muted)]">Header content</label>
<input id="header" type="text" bind:value={header} class={fieldClass} placeholder="auto or [Text]" />
</div>
<div class="flex flex-col gap-1 sm:col-span-2">
<label for="footer" class="text-xs font-medium text-[var(--color-ink-muted)]">Footer content</label>
<input id="footer" type="text" bind:value={footer} class={fieldClass} placeholder="auto or [Text]" />
</div>
</div>
</section>
</div> </div>
</div>
{#snippet footer()}
<button onclick={() => props.onClose()} class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]">
Cancel
</button>
<button onclick={apply} class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90">
Apply settings
</button>
{/snippet}
</Modal>
+1 -1
View File
@@ -14,7 +14,7 @@
</div> </div>
{/each} {/each}
{:else} {:else}
<div class="text-gray-400 flex flex-col items-center justify-center h-full"> <div class="text-[var(--color-ink-muted)] flex flex-col items-center justify-center h-full">
<p>Document is empty or compiling...</p> <p>Document is empty or compiling...</p>
</div> </div>
{/if} {/if}
+81
View File
@@ -0,0 +1,81 @@
<script lang="ts">
import { untrack } from "svelte";
import Modal from "./Modal.svelte";
interface Props {
title: string;
label: string;
icon?: string;
value?: string;
placeholder?: string;
confirmLabel?: string;
danger?: boolean;
suffix?: string;
onsubmit: (value: string) => void;
onclose: () => void;
}
let {
title,
label,
icon = "ph:pencil-simple",
value = "",
placeholder = "",
confirmLabel = "Create",
danger = false,
suffix = "",
onsubmit,
onclose,
}: Props = $props();
let text = $state(
untrack(() =>
suffix && value.endsWith(suffix) ? value.slice(0, -suffix.length) : value,
),
);
function submit(event: SubmitEvent) {
event.preventDefault();
if (!text.trim()) return;
onsubmit(text.trim());
}
</script>
<Modal {title} {icon} {onclose}>
<form id="prompt-form" onsubmit={submit}>
<label class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">{label}</span>
<div
class="flex items-center rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] focus-within:border-[var(--color-accent)]"
>
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus
class="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm focus:outline-none"
bind:value={text}
{placeholder}
/>
{#if suffix}
<span class="pr-3 text-sm text-[var(--color-ink-muted)]">{suffix}</span>
{/if}
</div>
</label>
</form>
{#snippet footer()}
<button
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
onclick={onclose}
>
Cancel
</button>
<button
type="submit"
form="prompt-form"
class="rounded-md px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90
{danger ? 'bg-[var(--color-danger)]' : 'bg-[var(--color-accent)]'}"
>
{confirmLabel}
</button>
{/snippet}
</Modal>
+28 -34
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import Modal from './Modal.svelte';
let { let {
projectId, projectId,
@@ -37,40 +38,33 @@
} }
</script> </script>
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={onClose} role="presentation"> <Modal title="Publish as package" icon="ph:package" onclose={onClose}>
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md p-6" onclick={(e) => e.stopPropagation()} role="presentation"> <p class="text-sm text-[var(--color-ink-muted)] mb-4">
<div class="flex items-center gap-2 mb-4"> Snapshots this project's files into an immutable package version, importable instance-wide as
<Icon icon="mdi:package-variant-closed" class="text-2xl text-purple-500" /> <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">@typstdrive/&lt;name&gt;:&lt;version&gt;</code>.
<h2 class="text-lg font-bold">Publish as Package</h2> The name, version and entrypoint come from your <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">typst.toml</code>.
</div> </p>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4"> <label class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1" for="pkg-version">Version override (optional)</label>
Snapshots this project's files into an immutable package version, importable instance-wide as <input
<code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">@typstdrive/&lt;name&gt;:&lt;version&gt;</code>. id="pkg-version"
The name, version and entrypoint come from your <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">typst.toml</code>. bind:value={version}
</p> placeholder="e.g. 0.1.0 (defaults to typst.toml)"
class="w-full rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm mb-2 focus:border-[var(--color-accent)] focus:outline-none"
/>
<label class="block text-sm font-medium mb-1" for="pkg-version">Version override (optional)</label> {#if error}
<input <div class="text-sm text-[var(--color-danger)] mt-2 break-words">{error}</div>
id="pkg-version" {/if}
bind:value={version} {#if success}
placeholder="e.g. 0.1.0 (defaults to typst.toml)" <div class="text-sm text-[var(--color-success)] mt-2">{success}</div>
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-white/10 bg-transparent text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-purple-500/40" {/if}
/>
{#if error} {#snippet footer()}
<div class="text-sm text-red-600 dark:text-red-400 mb-3 break-words">{error}</div> <button onclick={onClose} class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]">Close</button>
{/if} <button onclick={publish} disabled={publishing} class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50 flex items-center gap-2">
{#if success} {#if publishing}<Icon icon="mdi:loading" class="animate-spin" />{/if}
<div class="text-sm text-green-600 dark:text-green-400 mb-3">{success}</div> Publish
{/if} </button>
{/snippet}
<div class="flex justify-end gap-2"> </Modal>
<button onclick={onClose} class="px-4 py-2 text-sm rounded-lg bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10">Close</button>
<button onclick={publish} disabled={publishing} class="px-4 py-2 text-sm rounded-lg bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50 flex items-center gap-2">
{#if publishing}<Icon icon="mdi:loading" class="animate-spin" />{/if}
Publish
</button>
</div>
</div>
</div>
+105 -113
View File
@@ -2,6 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import Modal from './Modal.svelte';
let { onClose, docId = undefined } = $props<{ onClose: () => void, docId?: string }>(); let { onClose, docId = undefined } = $props<{ onClose: () => void, docId?: string }>();
@@ -103,125 +104,116 @@
} }
</script> </script>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}> <Modal title="Share document" icon="ph:share-network" width="max-w-[500px]" onclose={onClose}>
<div tabindex="-1" class="rounded-xl shadow-2xl border w-full max-w-[500px] overflow-hidden bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]" style="background-color: var(--theme-bg); color: var(--theme-text); border-color: var(--theme-border);" role="dialog" aria-modal="true" aria-labelledby="share-dialog-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}> <div class="flex flex-col gap-5">
<div class="flex justify-between items-center p-4 border-b border-[var(--theme-border)]" style="border-color: var(--theme-border);"> <div class="flex flex-col gap-2">
<h2 id="share-dialog-title" class="text-lg font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">Share Document</h2> <div class="text-xs font-medium text-[var(--color-ink-muted)]">Invite collaborator</div>
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors"> <form onsubmit={inviteUser} class="flex items-center gap-2 rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] p-1.5 focus-within:border-[var(--color-accent)] transition">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <div class="pl-2 text-[var(--color-ink-muted)]">
</button> <Icon icon="mdi:account-plus-outline" class="text-xl" />
</div>
<div class="p-6 space-y-6">
<div class="space-y-3">
<div class="text-sm font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">Invite Collaborator</div>
<form onsubmit={inviteUser} class="flex items-center gap-2 bg-gray-50 dark:bg-zinc-900/50 p-1.5 rounded-lg border border-gray-300 dark:border-zinc-700 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 transition-all">
<div class="pl-2 text-gray-400">
<Icon icon="mdi:account-plus-outline" class="text-xl" />
</div>
<input
type="email"
placeholder="Add people via email..."
bind:value={inviteEmail}
required
class="flex-1 bg-transparent border-none text-gray-800 dark:text-gray-200 text-sm px-2 py-2 focus:ring-0 focus:outline-none w-full"
/>
<div class="h-6 w-px bg-gray-300 dark:bg-zinc-700"></div>
<select bind:value={inviteRole} class="bg-transparent border-none text-sm text-gray-700 dark:text-gray-300 px-2 py-2 focus:ring-0 focus:outline-none cursor-pointer font-medium">
<option class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="editor">Editor</option>
<option class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="viewer">Viewer</option>
</select>
<button
type="submit"
disabled={inviteStatus === 'loading'}
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors shadow-sm disabled:opacity-70 min-w-[80px]"
>
{inviteStatus === 'loading' ? 'Inviting...' : 'Invite'}
</button>
</form>
{#if inviteMessage}
<div class="flex items-center gap-1.5 text-xs font-medium {inviteStatus === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'}">
<Icon icon={inviteStatus === 'success' ? 'mdi:check-circle' : 'mdi:alert-circle'} class="text-sm" />
{inviteMessage}
</div>
{/if}
</div>
{#if collabLoading}
<div class="flex items-center gap-2 text-sm text-gray-400 dark:text-gray-500 py-1">
<Icon icon="mdi:loading" class="animate-spin text-base" />
Loading collaborators...
</div> </div>
{:else if collaborators.length > 0} <input
<div class="h-px bg-gray-200 dark:bg-zinc-800/50"></div> type="email"
<div class="space-y-2"> placeholder="Add people via email..."
<div class="text-sm font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">People with access</div> bind:value={inviteEmail}
{#each collaborators as collab (collab.id)} required
<div class="flex items-center gap-3 py-1.5"> class="flex-1 bg-transparent border-none text-sm px-2 py-2 focus:ring-0 focus:outline-none w-full"
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 text-sm font-bold flex-shrink-0"> />
{collab.username[0].toUpperCase()} <div class="h-6 w-px bg-[var(--color-line)]"></div>
</div> <select bind:value={inviteRole} class="bg-transparent border-none text-sm text-[var(--color-ink-muted)] px-2 py-2 focus:ring-0 focus:outline-none cursor-pointer font-medium">
<div class="flex-1 min-w-0"> <option value="editor">Editor</option>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{collab.username}</p> <option value="viewer">Viewer</option>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">{collab.email}</p> </select>
</div> <button
<span class="text-xs font-semibold px-2 py-0.5 rounded-full flex-shrink-0 {collab.role === 'editor' ? 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300' : 'bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-400'}"> type="submit"
{collab.role} disabled={inviteStatus === 'loading'}
</span> class="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-white transition hover:opacity-90 disabled:opacity-70 min-w-[80px]"
<button >
onclick={() => removeCollaborator(collab)} {inviteStatus === 'loading' ? 'Inviting...' : 'Invite'}
disabled={removingId === collab.id} </button>
title="Remove collaborator" </form>
class="flex-shrink-0 p-1 rounded text-gray-400 hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors disabled:opacity-40" {#if inviteMessage}
> <div class="flex items-center gap-1.5 text-xs font-medium {inviteStatus === 'success' ? 'text-[var(--color-success)]' : 'text-[var(--color-danger)]'}">
{#if removingId === collab.id} <Icon icon={inviteStatus === 'success' ? 'mdi:check-circle' : 'mdi:alert-circle'} class="text-sm" />
<Icon icon="mdi:loading" class="text-base animate-spin" /> {inviteMessage}
{:else}
<Icon icon="mdi:close" class="text-base" />
{/if}
</button>
</div>
{/each}
</div> </div>
{/if} {/if}
</div>
<div class="h-px bg-gray-200 dark:bg-zinc-800/50"></div> {#if collabLoading}
<div class="flex items-center gap-2 text-sm text-[var(--color-ink-muted)] py-1">
<Icon icon="mdi:loading" class="animate-spin text-base" />
Loading collaborators...
</div>
{:else if collaborators.length > 0}
<div class="h-px bg-[var(--color-line)]"></div>
<div class="flex flex-col gap-2">
<div class="text-xs font-medium text-[var(--color-ink-muted)]">People with access</div>
{#each collaborators as collab (collab.id)}
<div class="flex items-center gap-3 py-1.5">
<div class="w-8 h-8 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] text-sm font-bold flex-shrink-0">
{collab.username[0].toUpperCase()}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-[var(--color-ink)] truncate">{collab.username}</p>
<p class="text-xs text-[var(--color-ink-muted)] truncate">{collab.email}</p>
</div>
<span class="text-xs font-semibold px-2 py-0.5 rounded-full flex-shrink-0 {collab.role === 'editor' ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]'}">
{collab.role}
</span>
<button
onclick={() => removeCollaborator(collab)}
disabled={removingId === collab.id}
title="Remove collaborator"
class="flex-shrink-0 rounded p-1 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-danger)]/10 hover:text-[var(--color-danger)] disabled:opacity-40"
>
{#if removingId === collab.id}
<Icon icon="mdi:loading" class="text-base animate-spin" />
{:else}
<Icon icon="mdi:close" class="text-base" />
{/if}
</button>
</div>
{/each}
</div>
{/if}
<div class="space-y-3"> <div class="h-px bg-[var(--color-line)]"></div>
<div class="text-sm font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">General Access</div>
<div class="flex items-center gap-4 p-3 bg-gray-50/50 dark:bg-zinc-950/30 rounded-xl border border-gray-200 dark:border-zinc-800/50 hover:bg-gray-50 dark:hover:bg-zinc-900/50 transition-colors"> <div class="flex flex-col gap-2">
<div class="bg-gray-200 dark:bg-zinc-800 p-2.5 rounded-full text-gray-600 dark:text-gray-300"> <div class="text-xs font-medium text-[var(--color-ink-muted)]">General access</div>
<Icon icon="mdi:earth" class="text-xl" /> <div class="flex items-center gap-4 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3 transition hover:bg-[var(--color-surface-sunken)]">
</div> <div class="rounded-full bg-[var(--color-surface-sunken)] p-2.5 text-[var(--color-ink-muted)]">
<div class="flex-1"> <Icon icon="mdi:earth" class="text-xl" />
<h4 class="text-sm font-medium text-gray-900 dark:text-white">Anyone with the link</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Can view and collaborate based on role</p>
</div>
<select bind:value={role} class="bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 text-sm font-medium text-gray-700 dark:text-gray-300 rounded-md px-3 py-1.5 focus:outline-none cursor-pointer focus:ring-2 focus:ring-blue-500/20 hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors">
<option class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="viewer">Viewer</option>
<option class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="editor">Editor</option>
</select>
</div> </div>
<div class="flex-1">
<h4 class="text-sm font-medium text-[var(--color-ink)]">Anyone with the link</h4>
<p class="text-xs text-[var(--color-ink-muted)] mt-0.5">Can view and collaborate based on role</p>
</div>
<select bind:value={role} class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-sunken)] focus:outline-none cursor-pointer">
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
</select>
</div> </div>
</div> </div>
<div class="p-4 bg-gray-50 dark:bg-zinc-950/50 border-t border-[var(--theme-border)] flex items-center justify-between" style="border-color: var(--theme-border);">
<button
onclick={copyLink}
class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-500/10 transition-colors"
>
{#if copied}
<Icon icon="mdi:check" class="text-lg" />
<span>Link copied!</span>
{:else}
<Icon icon="mdi:link-variant" class="text-lg" />
<span>Copy link</span>
{/if}
</button>
<button onclick={onClose} class="px-6 py-2 text-sm font-semibold text-white bg-gray-800 hover:bg-gray-900 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white rounded-lg shadow-sm transition-colors">
Done
</button>
</div>
</div> </div>
</div>
{#snippet footer()}
<button
onclick={copyLink}
class="mr-auto flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium text-[var(--color-accent)] transition hover:bg-[var(--color-accent-soft)]"
>
{#if copied}
<Icon icon="mdi:check" class="text-base" />
<span>Link copied!</span>
{:else}
<Icon icon="mdi:link-variant" class="text-base" />
<span>Copy link</span>
{/if}
</button>
<button onclick={onClose} class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90">
Done
</button>
{/snippet}
</Modal>
+39 -24
View File
@@ -5,32 +5,47 @@
let { class: className = '' } = $props(); let { class: className = '' } = $props();
const themeOptions = Object.keys(themes).flatMap(themeName => [ const themeNames = Object.keys(themes);
{ name: `${themeName} Light`, theme: themeName, isDark: false },
{ name: `${themeName} Dark`, theme: themeName, isDark: true }
]);
function handleChange(e: Event) {
const val = (e.target as HTMLSelectElement).value;
const opt = themeOptions.find(o => o.name === val);
if (opt) {
$themeStore = opt.theme;
$darkModeStore = opt.isDark;
}
}
let selectedValue = $derived(`${$themeStore} ${$darkModeStore ? 'Dark' : 'Light'}`);
</script> </script>
<div class="flex items-center gap-2 {className}"> <div class="flex items-center gap-2 {className}">
<Icon icon={themes[$themeStore]?.icon || 'mdi:palette'} class="text-xl text-[var(--theme-text)] opacity-70" /> <div class="flex rounded-lg bg-[var(--color-surface-sunken)] p-0.5 text-xs font-medium">
<select {#each themeNames as name}
value={selectedValue} <button
onchange={handleChange} class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 transition
class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] text-sm font-medium rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 block py-1.5 pl-3 pr-8 appearance-none cursor-pointer transition-colors outline-none" {$themeStore === name
> ? 'bg-[var(--color-surface)] text-[var(--color-accent)] shadow-sm'
{#each themeOptions as opt} : 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value={opt.name}>{opt.name}</option> onclick={() => ($themeStore = name)}
>
<Icon icon={themes[name].icon} class="text-sm" />
{name}
</button>
{/each} {/each}
</select> </div>
<div class="flex rounded-lg bg-[var(--color-surface-sunken)] p-0.5 text-xs font-medium">
<button
class="flex items-center rounded-md px-2.5 py-1.5 transition
{!$darkModeStore
? 'bg-[var(--color-surface)] text-[var(--color-ink)] shadow-sm'
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
onclick={() => ($darkModeStore = false)}
aria-label="Light mode"
title="Light mode"
>
<Icon icon="ph:sun" class="text-sm" />
</button>
<button
class="flex items-center rounded-md px-2.5 py-1.5 transition
{$darkModeStore
? 'bg-[var(--color-surface)] text-[var(--color-ink)] shadow-sm'
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
onclick={() => ($darkModeStore = true)}
aria-label="Dark mode"
title="Dark mode"
>
<Icon icon="ph:moon" class="text-sm" />
</button>
</div>
</div> </div>
+82 -133
View File
@@ -10,6 +10,9 @@
import PresentationMode from "./PresentationMode.svelte"; import PresentationMode from "./PresentationMode.svelte";
import CommentsSidebar from "./CommentsSidebar.svelte"; import CommentsSidebar from "./CommentsSidebar.svelte";
import VersionHistorySidebar from "./VersionHistorySidebar.svelte"; import VersionHistorySidebar from "./VersionHistorySidebar.svelte";
import Modal from './Modal.svelte';
import PromptModal from './PromptModal.svelte';
import ConfirmModal from './ConfirmModal.svelte';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { undo, redo } from '@codemirror/commands'; import { undo, redo } from '@codemirror/commands';
@@ -348,13 +351,12 @@
showRenameModal = true; showRenameModal = true;
} }
function submitRename(e: Event) { function submitRename(newTitle: string) {
e.preventDefault(); if (newTitle && newTitle !== title) {
if (renameTitle && renameTitle !== title) {
fetch(`/api/docs/${docId}`, { fetch(`/api/docs/${docId}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: renameTitle }) body: JSON.stringify({ title: newTitle })
}).then(res => { }).then(res => {
if (res.ok) { if (res.ok) {
window.location.reload(); window.location.reload();
@@ -401,7 +403,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
onclick={() => goto('/dashboard')} onclick={() => goto('/dashboard')}
class="p-1.5 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors"
aria-label="Back to dashboard" aria-label="Back to dashboard"
title="Dashboard" title="Dashboard"
> >
@@ -410,14 +412,14 @@
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1 class="text-[16px] font-semibold text-gray-900 dark:text-white tracking-tight truncate max-w-[200px] md:max-w-xs" title={title}> <h1 class="text-[16px] font-semibold text-[var(--color-ink)] tracking-tight truncate max-w-[200px] md:max-w-xs" title={title}>
{title} {title}
</h1> </h1>
</div> </div>
<div class="flex items-center gap-0.5 text-[13px] font-medium text-gray-600 dark:text-gray-300 -ml-1 action-menu-container"> <div class="flex items-center gap-0.5 text-[13px] font-medium text-[var(--color-ink-muted)] -ml-1 action-menu-container">
<div class="relative"> <div class="relative">
<button <button
onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }} onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }}
@@ -453,7 +455,7 @@
<button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)]">HTML (.html)</button> <button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)]">HTML (.html)</button>
{#if !isViewer} {#if !isViewer}
<div class="h-px bg-[var(--theme-border)] my-1"></div> <div class="h-px bg-[var(--theme-border)] my-1"></div>
<button onclick={() => { activeMenu = null; deleteDoc(); }} class="w-full text-left px-4 py-1.5 text-sm text-red-500 hover:bg-red-500/10">Delete</button> <button onclick={() => { activeMenu = null; deleteDoc(); }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10">Delete</button>
{/if} {/if}
</div> </div>
{/if} {/if}
@@ -493,7 +495,7 @@
Version History Version History
</button> </button>
<div class="h-px bg-[var(--theme-border)] my-1"></div> <div class="h-px bg-[var(--theme-border)] my-1"></div>
<button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; document.documentElement.classList.toggle('dark', $darkModeStore); }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)] flex items-center justify-between"> <button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)] flex items-center justify-between">
Dark Mode Dark Mode
<Icon icon={$darkModeStore ? "mdi:check" : ""} class="text-sm" /> <Icon icon={$darkModeStore ? "mdi:check" : ""} class="text-sm" />
</button> </button>
@@ -510,7 +512,7 @@
<div class="flex items-center -space-x-2 mr-2"> <div class="flex items-center -space-x-2 mr-2">
{#each $connectedUsers as user} {#each $connectedUsers as user}
<div <div
class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-white dark:border-zinc-950 shadow-sm" class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-[var(--color-surface)] shadow-sm"
style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};" style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};"
title={user.name + (user.isLocal ? ' (You)' : '')} title={user.name + (user.isLocal ? ' (You)' : '')}
> >
@@ -522,21 +524,21 @@
<div class="flex items-center gap-1.5 px-2"> <div class="flex items-center gap-1.5 px-2">
<a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" title="Typst Docs"> <a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors" title="Typst Docs">
<Icon icon="mdi:book-open-page-variant-outline" class="text-[18px]" /> <Icon icon="mdi:book-open-page-variant-outline" class="text-[18px]" />
</a> </a>
<a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-gray-500 hover:text-purple-500 dark:text-gray-400 dark:hover:text-purple-400 rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" title="Typst Universe"> <a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors" title="Typst Universe">
<Icon icon="mdi:earth" class="text-[18px]" /> <Icon icon="mdi:earth" class="text-[18px]" />
</a> </a>
</div> </div>
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-5 bg-[var(--color-line)]"></div>
{#if !isViewer} {#if !isViewer}
<button <button
onclick={() => (isPresentationOpen = true)} onclick={() => (isPresentationOpen = true)}
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors"
> >
<Icon icon="mdi:presentation-play" class="text-[16px]" /> <Icon icon="mdi:presentation-play" class="text-[16px]" />
Present Present
@@ -544,7 +546,7 @@
<button <button
onclick={() => ($commentsSidebarOpen = !$commentsSidebarOpen)} onclick={() => ($commentsSidebarOpen = !$commentsSidebarOpen)}
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors"
> >
<Icon icon="mdi:comment-outline" class="text-[16px]" /> <Icon icon="mdi:comment-outline" class="text-[16px]" />
Comments Comments
@@ -552,27 +554,27 @@
<button <button
onclick={() => (isShareModalOpen = true)} onclick={() => (isShareModalOpen = true)}
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" x2="12" y1="2" y2="15"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" x2="12" y1="2" y2="15"/></svg>
Share Share
</button> </button>
{/if} {/if}
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-5 bg-[var(--color-line)]"></div>
<button <button
onclick={() => ($previewOpenStore = !$previewOpenStore)} onclick={() => ($previewOpenStore = !$previewOpenStore)}
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10' : 'text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20 dark:hover:bg-blue-900/40'}" class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)]' : 'text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90'}"
title={$previewOpenStore ? 'Hide preview' : 'Show preview'} title={$previewOpenStore ? 'Hide preview' : 'Show preview'}
> >
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" /> <Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" />
Preview Preview
</button> </button>
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-5 bg-[var(--color-line)]"></div>
<button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" title="Print Document"> <button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Print Document">
<Icon icon="mdi:printer" class="text-[16px]" /> <Icon icon="mdi:printer" class="text-[16px]" />
Print Print
</button> </button>
@@ -580,7 +582,7 @@
<div class="relative action-menu-container"> <div class="relative action-menu-container">
<button <button
onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }} onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }}
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20 dark:hover:bg-blue-900/40 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-blue-500/30' : ''}" class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-[var(--color-accent)]/30' : ''}"
> >
<Icon icon="mdi:export-variant" class="text-[16px]" /> <Icon icon="mdi:export-variant" class="text-[16px]" />
Export Export
@@ -607,50 +609,50 @@
</div> </div>
<div class="flex items-center px-4 py-1.5 bg-white/50 dark:bg-black/10 border-t border-gray-200/60 dark:border-white/10 gap-4 overflow-x-auto no-scrollbar"> <div class="flex items-center px-4 py-1.5 bg-[var(--color-surface-muted)] border-t border-[var(--color-line)] gap-4 overflow-x-auto no-scrollbar">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bold"> <button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bold">
<Icon icon="mdi:format-bold" class="text-lg" /> <Icon icon="mdi:format-bold" class="text-lg" />
</button> </button>
<button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Italic"> <button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Italic">
<Icon icon="mdi:format-italic" class="text-lg" /> <Icon icon="mdi:format-italic" class="text-lg" />
</button> </button>
<button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Code"> <button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Code">
<Icon icon="mdi:code-tags" class="text-lg" /> <Icon icon="mdi:code-tags" class="text-lg" />
</button> </button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Math (Inline)"> <button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Math (Inline)">
<Icon icon="mdi:sigma" class="text-lg" /> <Icon icon="mdi:sigma" class="text-lg" />
</button> </button>
<button onclick={() => applyFormat('$ \n ', '\n$ ', 'x = y')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Math (Block)"> <button onclick={() => applyFormat('$ \n ', '\n$ ', 'x = y')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Math (Block)">
<Icon icon="mdi:math-integral" class="text-lg" /> <Icon icon="mdi:math-integral" class="text-lg" />
</button> </button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bullet List"> <button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bullet List">
<Icon icon="mdi:format-list-bulleted" class="text-lg" /> <Icon icon="mdi:format-list-bulleted" class="text-lg" />
</button> </button>
<button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Numbered List"> <button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Numbered List">
<Icon icon="mdi:format-list-numbered" class="text-lg" /> <Icon icon="mdi:format-list-numbered" class="text-lg" />
</button> </button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<input type="file" bind:this={fileInput} onchange={handleImageUpload} class="hidden" accept="image/*,.ttf,.otf" /> <input type="file" bind:this={fileInput} onchange={handleImageUpload} class="hidden" accept="image/*,.ttf,.otf" />
{#if !isViewer} {#if !isViewer}
<button onclick={() => fileInput?.click()} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Upload Image / Font"> <button onclick={() => fileInput?.click()} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Upload Image / Font">
<Icon icon="mdi:image-plus" class="text-lg" /> <Icon icon="mdi:image-plus" class="text-lg" />
</button> </button>
{/if} {/if}
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label> <label for="font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
<select <select
id="font-select" id="font-select"
onchange={(e) => insertTypstConfig('text', `font: "${e.currentTarget.value}"`)} onchange={(e) => insertTypstConfig('text', `font: "${e.currentTarget.value}"`)}
class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] text-xs rounded shadow-sm focus:ring-blue-500 focus:border-blue-500 block py-1 pl-2 pr-6 appearance-none cursor-pointer transition-colors" class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] text-xs rounded shadow-sm focus:border-[var(--color-accent)] focus:outline-none block py-1 pl-2 pr-6 appearance-none cursor-pointer transition-colors"
> >
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="New Computer Modern">Default (New CM)</option> <option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="New Computer Modern">Default (New CM)</option>
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="Libertinus Serif">Libertinus Serif</option> <option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="Libertinus Serif">Libertinus Serif</option>
@@ -666,7 +668,7 @@
</select> </select>
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if !isViewer} {#if !isViewer}
@@ -680,29 +682,29 @@
{/if} {/if}
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
<div class="flex items-center gap-1 bg-white dark:bg-black/20 border border-gray-300 dark:border-white/20 rounded shadow-sm overflow-hidden"> <div class="flex items-center gap-1 bg-[var(--color-surface)] border border-[var(--color-line)] rounded shadow-sm overflow-hidden">
<button <button
onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)} onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)}
class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors" class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors"
title="Zoom Out" title="Zoom Out"
> >
<Icon icon="mdi:minus" class="text-sm" /> <Icon icon="mdi:minus" class="text-sm" />
</button> </button>
<span role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter") $documentZoomStore = 100; }} class="text-[11px] font-semibold text-gray-700 dark:text-gray-200 min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}> <span role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter") $documentZoomStore = 100; }} class="text-[11px] font-semibold text-[var(--color-ink-muted)] min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>
{$documentZoomStore}% {$documentZoomStore}%
</span> </span>
<button <button
onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)} onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)}
class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors" class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors"
title="Zoom In" title="Zoom In"
> >
<Icon icon="mdi:plus" class="text-sm" /> <Icon icon="mdi:plus" class="text-sm" />
</button> </button>
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<ThemePicker /> <ThemePicker />
@@ -735,105 +737,52 @@
{/if} {/if}
{#if showInfoModal} {#if showInfoModal}
<Modal title={docInfo?.title || title} icon="ph:file-text" onclose={() => showInfoModal = false}>
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={() => showInfoModal = false} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { showInfoModal = false; } }}> <div class="flex flex-col gap-4 text-xs">
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}> <div>
<div class="p-6 border-b border-gray-100 dark:border-white/10"> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p>
<div class="flex items-center gap-3"> <p class="text-sm text-[var(--color-ink)] capitalize">Document</p>
<div class="p-2 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-lg">
<Icon icon="mdi:file-document" class="text-xl" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex-grow truncate">{docInfo?.title || title}</h3>
</div>
</div> </div>
<div class="p-6 space-y-4"> {#if docInfo?.created_at}
<div> <div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Created</p>
<p class="text-sm text-gray-900 dark:text-gray-100 capitalize">Document</p> <p class="text-sm text-[var(--color-ink)]">{new Date(docInfo.created_at).toLocaleString()}</p>
</div> </div>
{#if docInfo?.created_at} {/if}
<div> {#if docInfo?.updated_at}
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Created At</p> <div>
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(docInfo.created_at).toLocaleString()}</p> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
</div> <p class="text-sm text-[var(--color-ink)]">{new Date(docInfo.updated_at).toLocaleString()}</p>
{/if} </div>
{#if docInfo?.updated_at} {/if}
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Last Modified</p>
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(docInfo.updated_at).toLocaleString()}</p>
</div>
{/if}
</div>
<div class="p-4 bg-gray-50 dark:bg-white/5 border-t border-gray-100 dark:border-white/10 flex justify-end">
<button type="button" onclick={() => showInfoModal = false} class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors">
Close
</button>
</div>
</div> </div>
</div>
{#snippet footer()}
<button type="button" onclick={() => showInfoModal = false} class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90">
Close
</button>
{/snippet}
</Modal>
{/if} {/if}
{#if showRenameModal} {#if showRenameModal}
<PromptModal
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={() => showRenameModal = false} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { showRenameModal = false; } }}> title="Rename"
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}> label="New name"
<form onsubmit={submitRename} class="p-6"> icon="ph:pencil-simple"
<div class="flex items-center gap-3 mb-6"> value={renameTitle}
<div class="p-2 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-lg"> confirmLabel="Save"
<Icon icon="mdi:pencil-outline" class="text-xl" /> onsubmit={submitRename}
</div> onclose={() => showRenameModal = false}
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Rename</h3> />
</div>
<div class="space-y-4">
<input
type="text"
required
bind:value={renameTitle}
class="w-full bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter new name"
/>
</div>
<div class="pt-6 flex justify-end gap-3">
<button type="button" onclick={() => showRenameModal = false} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
Save
</button>
</div>
</form>
</div>
</div>
{/if} {/if}
{#if showDeleteModal} {#if showDeleteModal}
<ConfirmModal
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={() => showDeleteModal = false} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { showDeleteModal = false; } }}> title="Delete document"
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}> message="Are you sure you want to delete this document? This action cannot be undone."
<div class="p-6"> confirmLabel="Delete"
<div class="flex items-center gap-3 mb-6"> onconfirm={confirmDelete}
<div class="p-2 bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 rounded-lg"> onclose={() => showDeleteModal = false}
<Icon icon="mdi:trash-can-outline" class="text-xl" /> />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Delete Document</h3>
</div>
<p class="text-gray-600 dark:text-gray-300 text-sm mb-6">
Are you sure you want to delete this document? This action cannot be undone.
</p>
<div class="flex justify-end gap-3">
<button type="button" onclick={() => showDeleteModal = false} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="button" onclick={confirmDelete} class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
Delete
</button>
</div>
</div>
</div>
</div>
{/if} {/if}
@@ -55,13 +55,13 @@
</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)]">
<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-[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" />
<h2 class="text-sm font-semibold text-[var(--theme-text)]">Version History</h2> <h2 class="text-sm font-semibold text-[var(--theme-text)]">Version History</h2>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{versions.length}</span> <span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{versions.length}</span>
</div> </div>
<button onclick={onClose} class="p-1.5 hover:text-gray-600 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-white/10 rounded-md transition-colors" title="Close Version History"> <button onclick={onClose} class="p-1.5 hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Close Version History">
<Icon icon="mdi:close" class="text-lg" /> <Icon icon="mdi:close" class="text-lg" />
</button> </button>
</div> </div>
@@ -72,7 +72,7 @@
<Icon icon="mdi:loading" class="animate-spin text-2xl" /> <Icon icon="mdi:loading" class="animate-spin text-2xl" />
</div> </div>
{:else if error} {:else if error}
<div class="text-red-500 text-sm text-center p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/30"> <div class="text-[var(--color-danger)] text-sm text-center p-4 bg-[var(--color-danger)]/10 rounded-md border border-[var(--color-danger)]/20">
{error} {error}
</div> </div>
{:else if versions.length === 0} {:else if versions.length === 0}
@@ -96,11 +96,11 @@
</div> </div>
<div class="flex gap-2 mt-2"> <div class="flex gap-2 mt-2">
<button onclick={() => previewVersion = version} class="flex-1 px-3 py-1.5 hover:bg-gray-200 dark:hover:bg-white/20 text-xs font-medium rounded-lg transition-colors flex items-center justify-center gap-1.5"> <button onclick={() => previewVersion = version} class="flex-1 px-3 py-1.5 hover:bg-[var(--color-surface-sunken)] text-xs font-medium rounded-md transition-colors flex items-center justify-center gap-1.5">
<Icon icon="mdi:eye" class="text-sm" /> <Icon icon="mdi:eye" class="text-sm" />
Preview Preview
</button> </button>
<button onclick={() => restoreVersion(version)} class="flex-1 px-3 py-1.5 bg-purple-50 text-purple-700 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/40 text-xs font-medium rounded-lg transition-colors flex items-center justify-center gap-1.5"> <button onclick={() => restoreVersion(version)} class="flex-1 px-3 py-1.5 bg-purple-50 text-purple-700 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/40 text-xs font-medium rounded-md transition-colors flex items-center justify-center gap-1.5">
<Icon icon="mdi:restore" class="text-sm" /> <Icon icon="mdi:restore" class="text-sm" />
Restore Restore
</button> </button>
@@ -120,17 +120,17 @@
<h3 class="text-lg font-semibold text-[var(--theme-text)]">Previewing Version</h3> <h3 class="text-lg font-semibold text-[var(--theme-text)]">Previewing Version</h3>
<span class="text-sm">{formatDate(previewVersion.created_at)}</span> <span class="text-sm">{formatDate(previewVersion.created_at)}</span>
</div> </div>
<button onclick={() => previewVersion = null} class="p-1.5 hover:text-gray-600 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-white/10 rounded-md transition-colors" title="Close Preview"> <button onclick={() => previewVersion = null} class="p-1.5 hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Close Preview">
<Icon icon="mdi:close" class="text-xl" /> <Icon icon="mdi:close" class="text-xl" />
</button> </button>
</div> </div>
<div class="flex-1 overflow-auto p-6 bg-gray-50/50 bg-[var(--theme-bg)] text-[var(--theme-text)]"> <div class="flex-1 overflow-auto p-6 bg-[var(--theme-bg)] text-[var(--theme-text)]">
<pre class="text-sm font-mono whitespace-pre-wrap word-break-break-word">{previewVersion.content}</pre> <pre class="text-sm font-mono whitespace-pre-wrap word-break-break-word">{previewVersion.content}</pre>
</div> </div>
<div class="p-4 border-t flex justify-end gap-3 bg-white/50 rounded-b-2xl border-[var(--theme-border)]"> <div class="p-4 border-t flex justify-end gap-3 bg-[var(--color-surface)] rounded-b-2xl border-[var(--theme-border)]">
<button onclick={() => previewVersion = null} class="px-4 py-2 text-sm font-medium hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors"> <button onclick={() => previewVersion = null} class="px-4 py-2 text-sm font-medium hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors">
Close Close
</button> </button>
<button onclick={() => restoreVersion(previewVersion!)} class="bg-purple-600 hover:bg-purple-700 px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2"> <button onclick={() => restoreVersion(previewVersion!)} class="bg-purple-600 hover:bg-purple-700 px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
@@ -1,52 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let { createDoc, onClose } = $props<{
createDoc: (title: string) => void,
onClose: () => void
}>();
let newDocTitle = $state('Untitled Document');
function onSubmit(e: Event) {
e.preventDefault();
createDoc(newDocTitle);
}
</script>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
<div tabindex="-1" class="bg-[var(--theme-bg)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md overflow-hidden" role="dialog" aria-modal="true" aria-labelledby="create-doc-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-white/10">
<h2 id="create-doc-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Icon icon="mdi:file-document-plus" class="text-blue-500 text-xl" />
Create Document
</h2>
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
<Icon icon="mdi:close" class="text-xl" />
</button>
</div>
<form onsubmit={onSubmit} class="p-5 space-y-4">
<div class="space-y-2">
<label for="doc-title-input" class="text-sm font-medium text-gray-700 dark:text-gray-300 block">Document Title</label>
<input
id="doc-title-input"
type="text"
required
bind:value={newDocTitle}
class="w-full bg-black/5 dark:bg-white/5 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Untitled Document"
/>
</div>
<div class="pt-4 flex justify-end gap-3">
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
<Icon icon="mdi:plus" class="text-lg" />
Create
</button>
</div>
</form>
</div>
</div>
@@ -1,52 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let { createFolder, onClose } = $props<{
createFolder: (name: string) => void,
onClose: () => void
}>();
let newFolderName = $state('New Folder');
function onSubmit(e: Event) {
e.preventDefault();
createFolder(newFolderName);
}
</script>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
<div tabindex="-1" class="bg-[var(--theme-bg)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md overflow-hidden" role="dialog" aria-modal="true" aria-labelledby="create-folder-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-white/10">
<h2 id="create-folder-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Icon icon="mdi:folder-plus" class="text-yellow-500 text-xl" />
Create Folder
</h2>
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
<Icon icon="mdi:close" class="text-xl" />
</button>
</div>
<form onsubmit={onSubmit} class="p-5 space-y-4">
<div class="space-y-2">
<label for="folder-title-input" class="text-sm font-medium text-gray-700 dark:text-gray-300 block">Folder Name</label>
<input
id="folder-title-input"
type="text"
required
bind:value={newFolderName}
class="w-full bg-black/5 dark:bg-white/5 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="New Folder"
/>
</div>
<div class="pt-4 flex justify-end gap-3">
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="submit" class="bg-yellow-500 hover:bg-yellow-600 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
<Icon icon="mdi:plus" class="text-lg" />
Create
</button>
</div>
</form>
</div>
</div>
@@ -1,53 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let { createProject, onClose } = $props<{
createProject: (name: string) => void,
onClose: () => void
}>();
let newProjectName = $state('Untitled Project');
function onSubmit(e: Event) {
e.preventDefault();
createProject(newProjectName);
}
</script>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
<div tabindex="-1" class="bg-[var(--theme-bg)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md overflow-hidden" role="dialog" aria-modal="true" aria-labelledby="create-project-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-white/10">
<h2 id="create-project-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Icon icon="mdi:folder-multiple-plus" class="text-blue-500 text-xl" />
Create Project
</h2>
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
<Icon icon="mdi:close" class="text-xl" />
</button>
</div>
<form onsubmit={onSubmit} class="p-5 space-y-4">
<div class="space-y-2">
<label for="project-name-input" class="text-sm font-medium text-gray-700 dark:text-gray-300 block">Project Name</label>
<input
id="project-name-input"
type="text"
required
bind:value={newProjectName}
class="w-full bg-black/5 dark:bg-white/5 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Untitled Project"
/>
<p class="text-xs text-gray-500 dark:text-gray-400">A multi-file workspace, seeded with a <code class="font-mono">typst.toml</code> and <code class="font-mono">main.typ</code>.</p>
</div>
<div class="pt-4 flex justify-end gap-3">
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
<Icon icon="mdi:plus" class="text-lg" />
Create
</button>
</div>
</form>
</div>
</div>
@@ -1,36 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let { deleteTarget, confirmDelete, onClose } = $props<{
deleteTarget: {id: string, type: 'document'|'folder'|'file', name: string},
confirmDelete: () => void,
onClose: () => void
}>();
</script>
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={onClose} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { onClose(e); } }}>
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
<div class="p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 rounded-lg">
<Icon icon="mdi:trash-can-outline" class="text-xl" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Delete {deleteTarget.type}</h3>
</div>
<p class="text-gray-600 dark:text-gray-300 text-sm mb-6">
Are you sure you want to delete <span class="font-semibold text-gray-900 dark:text-white">{deleteTarget.name}</span>?
{#if deleteTarget.type === 'folder'}This will also delete all of its contents.{/if}
This action cannot be undone.
</p>
<div class="flex justify-end gap-3">
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="button" onclick={confirmDelete} class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
Delete
</button>
</div>
</div>
</div>
</div>
+12 -12
View File
@@ -32,7 +32,7 @@
</script> </script>
<div <div
class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 flex flex-col hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500/50 transition-all duration-200 relative group transform hover:-translate-y-1 cursor-pointer overflow-visible" class="bg-[var(--color-surface)] rounded-lg shadow-sm border border-[var(--color-line)] flex flex-col hover:border-[var(--color-accent)] transition relative group cursor-pointer overflow-visible"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={() => goto(`/doc/${doc.id}`)} onclick={() => goto(`/doc/${doc.id}`)}
@@ -41,13 +41,13 @@
ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'document', id: doc.id }))} ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'document', id: doc.id }))}
> >
<div class="h-40 w-full bg-gray-50 dark:bg-black/40 rounded-t-xl overflow-hidden flex items-center justify-center border-b border-gray-100 dark:border-white/10 relative pointer-events-none"> <div class="h-40 w-full bg-[var(--color-surface-muted)] rounded-t-lg overflow-hidden flex items-center justify-center border-b border-[var(--color-line)] relative pointer-events-none">
{#if doc.thumbnail_svg} {#if doc.thumbnail_svg}
<div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110"> <div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110">
<img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(doc.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" /> <img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(doc.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" />
</div> </div>
{:else} {:else}
<div class="p-4 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full transition-transform duration-300 group-hover:scale-110"> <div class="p-4 bg-[var(--color-accent-soft)] text-[var(--color-accent)] rounded-full transition-transform duration-300 group-hover:scale-110">
<Icon icon="mdi:file-document" class="text-4xl" /> <Icon icon="mdi:file-document" class="text-4xl" />
</div> </div>
{/if} {/if}
@@ -56,34 +56,34 @@
<div class="p-4 flex flex-col flex-grow"> <div class="p-4 flex flex-col flex-grow">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate pr-2 pointer-events-none" title={doc.title}>{doc.title}</h3> <h3 class="text-lg font-semibold text-[var(--color-ink)] truncate pr-2 pointer-events-none" title={doc.title}>{doc.title}</h3>
<div class="relative action-menu-container"> <div class="relative action-menu-container">
<button <button
aria-label="Document actions" aria-label="Document actions"
onclick={toggleMenu} onclick={toggleMenu}
class="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 pointer-events-auto" class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors p-1 rounded-full hover:bg-[var(--color-surface-sunken)] pointer-events-auto"
> >
<Icon icon="mdi:dots-vertical" class="text-xl" /> <Icon icon="mdi:dots-vertical" class="text-xl" />
</button> </button>
{#if activeMenu === doc.id} {#if activeMenu === doc.id}
<div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-gray-200 dark:border-white/10 py-1 z-[100]"> <div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--color-surface)] rounded-md shadow-xl border border-[var(--color-line)] py-1 z-[100]">
<button onclick={(e) => { e.stopPropagation(); openInfo(doc, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); openInfo(doc, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:information-outline" class="text-lg text-blue-500" /> <Icon icon="mdi:information-outline" class="text-lg text-blue-500" />
View Info View Info
</button> </button>
<button onclick={(e) => { e.stopPropagation(); openRename(doc.id, doc.title, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); openRename(doc.id, doc.title, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" /> <Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" />
Rename Rename
</button> </button>
<button onclick={(e) => { e.stopPropagation(); shareItem(doc); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); shareItem(doc); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:share-variant-outline" class="text-lg text-green-500" /> <Icon icon="mdi:share-variant-outline" class="text-lg text-green-500" />
Share Share
</button> </button>
<div class="h-px bg-gray-200 dark:bg-white/10 my-1"></div> <div class="h-px bg-[var(--color-line)] my-1"></div>
<button onclick={(e) => { e.stopPropagation(); deleteDoc(doc.id, doc.title); }} class="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-500/10 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); deleteDoc(doc.id, doc.title); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-2">
<Icon icon="mdi:trash-can-outline" class="text-lg" /> <Icon icon="mdi:trash-can-outline" class="text-lg" />
Delete Delete
</button> </button>
@@ -91,7 +91,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<p class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-2 pointer-events-none"> <p class="text-xs text-[var(--color-ink-muted)] flex items-center gap-1 mt-2 pointer-events-none">
<Icon icon="mdi:clock-outline" class="text-sm" /> <Icon icon="mdi:clock-outline" class="text-sm" />
Edited {new Date(doc.updated_at.endsWith('Z') ? doc.updated_at : doc.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} Edited {new Date(doc.updated_at.endsWith('Z') ? doc.updated_at : doc.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</p> </p>
+5 -5
View File
@@ -16,7 +16,7 @@
</script> </script>
<div <div
class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 flex flex-col transition-all duration-200 relative group transform hover:-translate-y-1 {isFont ? 'cursor-default' : 'hover:shadow-lg hover:border-green-400 dark:hover:border-green-500/50 cursor-pointer'}" class="bg-[var(--color-surface)] rounded-lg shadow-sm border border-[var(--color-line)] p-6 flex flex-col transition relative group {isFont ? 'cursor-default' : 'hover:border-[var(--color-accent)] cursor-pointer'}"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={handleOpen} onclick={handleOpen}
@@ -25,7 +25,7 @@
ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'file', id: file.id }))} ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'file', id: file.id }))}
> >
<div class="flex items-start justify-between mb-4 pointer-events-none"> <div class="flex items-start justify-between mb-4 pointer-events-none">
<div class="p-3 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-lg overflow-hidden flex items-center justify-center w-12 h-12"> <div class="p-3 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-md overflow-hidden flex items-center justify-center w-12 h-12">
{#if file.mime_type.startsWith('image/')} {#if file.mime_type.startsWith('image/')}
<img src={`/api/files/${file.id}/data`} alt={file.name} class="w-full h-full object-cover rounded" draggable="false" /> <img src={`/api/files/${file.id}/data`} alt={file.name} class="w-full h-full object-cover rounded" draggable="false" />
{:else if isFont} {:else if isFont}
@@ -34,12 +34,12 @@
<Icon icon="mdi:file-outline" class="text-2xl" /> <Icon icon="mdi:file-outline" class="text-2xl" />
{/if} {/if}
</div> </div>
<button aria-label="Delete file" onclick={(e) => { e.stopPropagation(); deleteFile(file.id, file.name); }} class="pointer-events-auto text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity bg-gray-50 hover:bg-red-50 dark:bg-white/5 dark:hover:bg-red-900/20 rounded-full p-2 shadow-sm border border-gray-100 dark:border-white/10"> <button aria-label="Delete file" onclick={(e) => { e.stopPropagation(); deleteFile(file.id, file.name); }} class="pointer-events-auto text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] opacity-0 group-hover:opacity-100 transition-opacity bg-[var(--color-surface-muted)] hover:bg-[var(--color-danger)]/10 rounded-full p-2 shadow-sm border border-[var(--color-line)]">
<Icon icon="mdi:trash-can-outline" class="text-lg" /> <Icon icon="mdi:trash-can-outline" class="text-lg" />
</button> </button>
</div> </div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate mb-1 pointer-events-none" title={file.name}>{file.name}</h3> <h3 class="text-lg font-semibold text-[var(--color-ink)] truncate mb-1 pointer-events-none" title={file.name}>{file.name}</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-auto pt-4 border-t border-gray-100 dark:border-white/10 pointer-events-none"> <p class="text-xs text-[var(--color-ink-muted)] flex items-center gap-1 mt-auto pt-4 border-t border-[var(--color-line)] pointer-events-none">
<Icon icon="mdi:clock-outline" class="text-sm" /> <Icon icon="mdi:clock-outline" class="text-sm" />
Uploaded {new Date(file.created_at ? (file.created_at.endsWith('Z') ? file.created_at : file.created_at + 'Z') : Date.now()).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} Uploaded {new Date(file.created_at ? (file.created_at.endsWith('Z') ? file.created_at : file.created_at + 'Z') : Date.now()).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</p> </p>
@@ -12,7 +12,7 @@
</script> </script>
<div <div
class="flex flex-row items-center p-3 bg-white/50 dark:bg-black/20 backdrop-blur-sm border border-gray-200 dark:border-white/10 rounded-xl shadow-sm hover:shadow-md cursor-pointer group transition-all duration-200 relative {dragOverFolderId === folder.id ? 'ring-2 ring-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'hover:-translate-y-0.5 hover:border-gray-300 dark:hover:border-white/20'}" class="flex flex-row items-center p-3 bg-[var(--color-surface)] border border-[var(--color-line)] rounded-lg shadow-sm cursor-pointer group transition relative {dragOverFolderId === folder.id ? 'ring-2 ring-[var(--color-accent)] bg-[var(--color-accent-soft)]' : 'hover:border-[var(--color-accent)]'}"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={() => navigateToFolder(folder)} onclick={() => navigateToFolder(folder)}
@@ -21,12 +21,12 @@
ondragleave={() => setDragOverFolderId(null)} ondragleave={() => setDragOverFolderId(null)}
ondrop={(e) => handleDrop(e, folder.id)} ondrop={(e) => handleDrop(e, folder.id)}
> >
<div class="flex items-center justify-center w-10 h-10 bg-yellow-50 dark:bg-yellow-500/10 rounded-lg group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3"> <div class="flex items-center justify-center w-10 h-10 bg-yellow-50 dark:bg-yellow-500/10 rounded-md group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3">
<Icon icon="mdi:folder" class="text-2xl text-yellow-500" /> <Icon icon="mdi:folder" class="text-2xl text-yellow-500" />
</div> </div>
<span class="font-medium text-gray-900 dark:text-white text-sm truncate w-full pointer-events-none pr-6">{folder.name}</span> <span class="font-medium text-[var(--color-ink)] text-sm truncate w-full pointer-events-none pr-6">{folder.name}</span>
<button aria-label="Delete folder" onclick={(e) => { e.stopPropagation(); deleteFolder(folder.id, folder.name); }} class="absolute top-1/2 -translate-y-1/2 right-2 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-red-50 dark:hover:bg-red-900/20 shrink-0"> <button aria-label="Delete folder" onclick={(e) => { e.stopPropagation(); deleteFolder(folder.id, folder.name); }} class="absolute top-1/2 -translate-y-1/2 right-2 text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-[var(--color-danger)]/10 shrink-0">
<Icon icon="mdi:trash-can-outline" class="text-base" /> <Icon icon="mdi:trash-can-outline" class="text-base" />
</button> </button>
</div> </div>
+28 -30
View File
@@ -1,41 +1,39 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Modal from '../Modal.svelte';
let { selectedInfo, onClose } = $props<{ let { selectedInfo, onClose } = $props<{
selectedInfo: {type: string, title?: string, name?: string, created_at: string, updated_at?: string}, selectedInfo: {type: string, title?: string, name?: string, created_at: string, updated_at?: string},
onClose: () => void onClose: () => void
}>(); }>();
const icon = $derived(selectedInfo.type === 'document' ? 'ph:file-text' : selectedInfo.type === 'folder' ? 'ph:folder' : 'ph:file');
const title = $derived(selectedInfo.title || selectedInfo.name || 'Details');
</script> </script>
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 transition-opacity" onclick={onClose} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { onClose(e); } }}> <Modal {title} {icon} onclose={onClose}>
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}> <div class="flex flex-col gap-4 text-xs">
<div class="p-6 border-b border-gray-100 dark:border-white/10"> <div>
<div class="flex items-center gap-3"> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p>
<div class="p-2 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-lg"> <p class="text-sm capitalize text-[var(--color-ink)]">{selectedInfo.type}</p>
<Icon icon={selectedInfo.type === 'document' ? 'mdi:file-document' : selectedInfo.type === 'folder' ? 'mdi:folder' : 'mdi:file'} class="text-xl" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex-grow truncate">{selectedInfo.title || selectedInfo.name}</h3>
</div>
</div> </div>
<div class="p-6 space-y-4"> <div>
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Created</p>
<p class="text-sm text-[var(--color-ink)]">{new Date(selectedInfo.created_at.endsWith('Z') ? selectedInfo.created_at : selectedInfo.created_at + 'Z').toLocaleString()}</p>
</div>
{#if selectedInfo.updated_at}
<div> <div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
<p class="text-sm text-gray-900 dark:text-gray-100 capitalize">{selectedInfo.type}</p> <p class="text-sm text-[var(--color-ink)]">{new Date(selectedInfo.updated_at.endsWith('Z') ? selectedInfo.updated_at : selectedInfo.updated_at + 'Z').toLocaleString()}</p>
</div> </div>
<div> {/if}
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Created At</p>
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(selectedInfo.created_at.endsWith('Z') ? selectedInfo.created_at : selectedInfo.created_at + 'Z').toLocaleString()}</p>
</div>
{#if selectedInfo.updated_at}
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Last Modified</p>
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(selectedInfo.updated_at.endsWith('Z') ? selectedInfo.updated_at : selectedInfo.updated_at + 'Z').toLocaleString()}</p>
</div>
{/if}
</div>
<div class="p-4 bg-gray-50 dark:bg-white/5 border-t border-gray-100 dark:border-white/10 flex justify-end">
<button type="button" onclick={onClose} class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors">
Close
</button>
</div>
</div> </div>
</div>
{#snippet footer()}
<button
type="button"
onclick={onClose}
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
>
Close
</button>
{/snippet}
</Modal>
+15 -15
View File
@@ -11,44 +11,44 @@
} }
</script> </script>
<nav class="bg-[var(--theme-bg)] shadow-sm border-b border-gray-200 dark:border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200"> <nav class="bg-[var(--color-surface)] shadow-sm border-b border-[var(--color-line)] px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3"> <h1 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-3">
<Icon icon="mdi:script-text" class="text-blue-600 dark:text-blue-400 text-3xl" /> <span class="flex h-14 w-14 items-center justify-center rounded-lg bg-[var(--color-accent)]">
<img src="/favicon.png" alt="TypstDrive" class="h-10 w-10" />
</span>
TypstDrive TypstDrive
</h1> </h1>
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300 font-medium"> <div class="flex items-center gap-2 text-[var(--color-ink-muted)] font-medium">
<Icon icon="mdi:account-circle" class="text-xl" /> <Icon icon="mdi:account-circle" class="text-xl" />
{$userStore?.username} {$userStore?.username}
</div> </div>
<div class="h-6 w-px bg-gray-300 dark:bg-white/20"></div> <div class="h-6 w-px bg-[var(--color-line)]"></div>
<a href="/projects" class="text-sm font-medium text-gray-600 hover:text-blue-600 dark:text-gray-300 dark:hover:text-blue-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Projects"> <a href="/projects" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Projects">
<Icon icon="mdi:folder-multiple-outline" class="text-xl" /> <Icon icon="mdi:folder-multiple-outline" class="text-xl" />
</a> </a>
<a href="/packages" class="text-sm font-medium text-gray-600 hover:text-purple-600 dark:text-gray-300 dark:hover:text-purple-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Packages"> <a href="/packages" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Packages">
<Icon icon="mdi:package-variant-closed" class="text-xl" /> <Icon icon="mdi:package-variant-closed" class="text-xl" />
</a> </a>
<a href="/api-docs" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="API Docs">
<a href="/api-docs" class="text-sm font-medium text-gray-600 hover:text-green-600 dark:text-gray-300 dark:hover:text-green-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="API Docs">
<Icon icon="mdi:api" class="text-xl" /> <Icon icon="mdi:api" class="text-xl" />
</a> </a>
<a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-gray-600 hover:text-blue-500 dark:text-gray-300 dark:hover:text-blue-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Typst Docs"> <a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Typst Docs">
<Icon icon="mdi:book-open-page-variant-outline" class="text-xl" /> <Icon icon="mdi:book-open-page-variant-outline" class="text-xl" />
</a> </a>
<a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-gray-600 hover:text-purple-500 dark:text-gray-300 dark:hover:text-purple-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Typst Universe"> <a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Typst Universe">
<Icon icon="mdi:earth" class="text-xl" /> <Icon icon="mdi:earth" class="text-xl" />
</a> </a>
<div class="h-6 w-px bg-gray-300 dark:bg-white/20"></div> <div class="h-6 w-px bg-[var(--color-line)]"></div>
<ThemePicker /> <ThemePicker />
<button onclick={() => goto('/settings')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Settings"> <button onclick={() => goto('/settings')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Settings">
<Icon icon="mdi:cog" class="text-2xl" /> <Icon icon="mdi:cog" class="text-2xl" />
</button> </button>
<button onclick={logout} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-4 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10"> <button onclick={logout} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-4 py-2 rounded-md flex items-center gap-2">
<Icon icon="mdi:logout" class="text-lg" /> <Icon icon="mdi:logout" class="text-lg" />
Logout Logout
</button> </button>
+12 -12
View File
@@ -27,19 +27,19 @@
</script> </script>
<div <div
class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 flex flex-col hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500/50 transition-all duration-200 relative group transform hover:-translate-y-1 cursor-pointer overflow-visible" class="bg-[var(--color-surface)] rounded-lg shadow-sm border border-[var(--color-line)] flex flex-col hover:border-[var(--color-accent)] transition relative group cursor-pointer overflow-visible"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={() => goto(`/project/${project.id}`)} onclick={() => goto(`/project/${project.id}`)}
onkeydown={(e) => e.key === 'Enter' && goto(`/project/${project.id}`)} onkeydown={(e) => e.key === 'Enter' && goto(`/project/${project.id}`)}
> >
<div class="h-40 w-full bg-gray-50 dark:bg-black/40 rounded-t-xl overflow-hidden flex items-center justify-center border-b border-gray-100 dark:border-white/10 relative pointer-events-none"> <div class="h-40 w-full bg-[var(--color-surface-muted)] rounded-t-lg overflow-hidden flex items-center justify-center border-b border-[var(--color-line)] relative pointer-events-none">
{#if project.thumbnail_svg} {#if project.thumbnail_svg}
<div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110"> <div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110">
<img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(project.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" /> <img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(project.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" />
</div> </div>
{:else} {:else}
<div class="p-4 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full transition-transform duration-300 group-hover:scale-110"> <div class="p-4 bg-[var(--color-accent-soft)] text-[var(--color-accent)] rounded-full transition-transform duration-300 group-hover:scale-110">
<Icon icon="mdi:folder-multiple-outline" class="text-4xl" /> <Icon icon="mdi:folder-multiple-outline" class="text-4xl" />
</div> </div>
{/if} {/if}
@@ -47,29 +47,29 @@
<div class="p-4 flex flex-col flex-grow"> <div class="p-4 flex flex-col flex-grow">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate pr-2 pointer-events-none" title={project.name}>{project.name}</h3> <h3 class="text-lg font-semibold text-[var(--color-ink)] truncate pr-2 pointer-events-none" title={project.name}>{project.name}</h3>
<div class="relative action-menu-container"> <div class="relative action-menu-container">
<button aria-label="Project actions" onclick={toggleMenu} class="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 pointer-events-auto"> <button aria-label="Project actions" onclick={toggleMenu} class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors p-1 rounded-full hover:bg-[var(--color-surface-sunken)] pointer-events-auto">
<Icon icon="mdi:dots-vertical" class="text-xl" /> <Icon icon="mdi:dots-vertical" class="text-xl" />
</button> </button>
{#if activeMenu === project.id} {#if activeMenu === project.id}
<div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-gray-200 dark:border-white/10 py-1 z-[100]"> <div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--color-surface)] rounded-md shadow-xl border border-[var(--color-line)] py-1 z-[100]">
<button onclick={(e) => { e.stopPropagation(); openInfo(project); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); openInfo(project); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:information-outline" class="text-lg text-blue-500" /> <Icon icon="mdi:information-outline" class="text-lg text-blue-500" />
View Info View Info
</button> </button>
<button onclick={(e) => { e.stopPropagation(); openRename(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); openRename(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" /> <Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" />
Rename Rename
</button> </button>
<button onclick={(e) => { e.stopPropagation(); goto(`/project/${project.id}`); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); goto(`/project/${project.id}`); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:open-in-new" class="text-lg text-green-500" /> <Icon icon="mdi:open-in-new" class="text-lg text-green-500" />
Open Open
</button> </button>
<div class="h-px bg-gray-200 dark:bg-white/10 my-1"></div> <div class="h-px bg-[var(--color-line)] my-1"></div>
<button onclick={(e) => { e.stopPropagation(); deleteProject(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-500/10 flex items-center gap-2"> <button onclick={(e) => { e.stopPropagation(); deleteProject(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-2">
<Icon icon="mdi:trash-can-outline" class="text-lg" /> <Icon icon="mdi:trash-can-outline" class="text-lg" />
Delete Delete
</button> </button>
@@ -77,7 +77,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<p class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-2 pointer-events-none"> <p class="text-xs text-[var(--color-ink-muted)] flex items-center gap-1 mt-2 pointer-events-none">
<Icon icon="mdi:clock-outline" class="text-sm" /> <Icon icon="mdi:clock-outline" class="text-sm" />
Edited {new Date(project.updated_at.endsWith('Z') ? project.updated_at : project.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} Edited {new Date(project.updated_at.endsWith('Z') ? project.updated_at : project.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</p> </p>
@@ -1,48 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let { initialTitle, handleRename, onClose } = $props<{
initialTitle: string,
handleRename: (newTitle: string) => void,
onClose: () => void
}>();
let renameTitle = $state("");
$effect(() => { renameTitle = initialTitle; });
function onSubmit(e: Event) {
e.preventDefault();
handleRename(renameTitle);
}
</script>
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 transition-opacity" onclick={onClose} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { onClose(e); } }}>
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
<form onsubmit={onSubmit} class="p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-lg">
<Icon icon="mdi:pencil-outline" class="text-xl" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Rename</h3>
</div>
<div class="space-y-4">
<input
type="text"
required
bind:value={renameTitle}
class="w-full bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter new name"
/>
</div>
<div class="pt-6 flex justify-end gap-3">
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
Cancel
</button>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
Save
</button>
</div>
</form>
</div>
</div>
+9 -9
View File
@@ -54,15 +54,15 @@
} }
</script> </script>
<div class="h-full flex flex-col bg-[var(--theme-bg)] border-r border-gray-200 dark:border-white/10"> <div class="h-full flex flex-col bg-[var(--color-surface)] border-r border-[var(--color-line)]">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10"> <div class="flex items-center justify-between px-3 py-2 border-b border-[var(--color-line)]">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Files</span> <span class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)]">Files</span>
{#if !readOnly} {#if !readOnly}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button onclick={handleCreate} title="New file" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-white/10 text-gray-600 dark:text-gray-300"> <button onclick={handleCreate} title="New file" class="p-1 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
<Icon icon="mdi:file-plus-outline" class="text-lg" /> <Icon icon="mdi:file-plus-outline" class="text-lg" />
</button> </button>
<button onclick={() => fileInput.click()} title="Upload file" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-white/10 text-gray-600 dark:text-gray-300"> <button onclick={() => fileInput.click()} title="Upload file" class="p-1 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
<Icon icon="mdi:upload" class="text-lg" /> <Icon icon="mdi:upload" class="text-lg" />
</button> </button>
<input bind:this={fileInput} type="file" multiple class="hidden" onchange={(e) => { const t = e.target as HTMLInputElement; if (t.files) onUpload(t.files); t.value = ''; }} /> <input bind:this={fileInput} type="file" multiple class="hidden" onchange={(e) => { const t = e.target as HTMLInputElement; if (t.files) onUpload(t.files); t.value = ''; }} />
@@ -72,7 +72,7 @@
<div class="flex-1 overflow-y-auto py-1"> <div class="flex-1 overflow-y-auto py-1">
{#each files as file (file.id)} {#each files as file (file.id)}
<div class="group flex items-center gap-1 px-2 py-1.5 text-sm cursor-pointer {activeFileId === file.id ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5'}"> <div class="group flex items-center gap-1 px-2 py-1.5 text-sm cursor-pointer {activeFileId === file.id ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}">
<button class="flex items-center gap-2 flex-1 min-w-0 text-left" onclick={() => onSelect(file)}> <button class="flex items-center gap-2 flex-1 min-w-0 text-left" onclick={() => onSelect(file)}>
<Icon icon={iconFor(file.path)} class="text-base flex-shrink-0" /> <Icon icon={iconFor(file.path)} class="text-base flex-shrink-0" />
<span class="truncate">{file.path}</span> <span class="truncate">{file.path}</span>
@@ -85,14 +85,14 @@
{#if !readOnly} {#if !readOnly}
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity"> <div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
{#if file.kind === 'text' && file.path.toLowerCase().endsWith('.typ') && file.path !== entrypoint} {#if file.kind === 'text' && file.path.toLowerCase().endsWith('.typ') && file.path !== entrypoint}
<button onclick={() => onSetEntry(file)} title="Set as entrypoint" class="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-white/10 text-gray-500"> <button onclick={() => onSetEntry(file)} title="Set as entrypoint" class="p-0.5 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
<Icon icon="mdi:star-outline" class="text-sm" /> <Icon icon="mdi:star-outline" class="text-sm" />
</button> </button>
{/if} {/if}
<button onclick={() => handleRename(file)} title="Rename" class="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-white/10 text-gray-500"> <button onclick={() => handleRename(file)} title="Rename" class="p-0.5 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
<Icon icon="mdi:pencil-outline" class="text-sm" /> <Icon icon="mdi:pencil-outline" class="text-sm" />
</button> </button>
<button onclick={() => onDelete(file)} title="Delete" class="p-0.5 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-500 hover:text-red-600"> <button onclick={() => onDelete(file)} title="Delete" class="p-0.5 rounded hover:bg-[var(--color-danger)]/10 text-[var(--color-ink-muted)] hover:text-[var(--color-danger)]">
<Icon icon="mdi:trash-can-outline" class="text-sm" /> <Icon icon="mdi:trash-can-outline" class="text-sm" />
</button> </button>
</div> </div>
@@ -13,6 +13,9 @@
import ThemePicker from '../ThemePicker.svelte'; import ThemePicker from '../ThemePicker.svelte';
import PageSettingsModal from '../PageSettingsModal.svelte'; import PageSettingsModal from '../PageSettingsModal.svelte';
import PresentationMode from '../PresentationMode.svelte'; import PresentationMode from '../PresentationMode.svelte';
import Modal from '../Modal.svelte';
import PromptModal from '../PromptModal.svelte';
import ConfirmModal from '../ConfirmModal.svelte';
let { let {
projectName = 'Project', projectName = 'Project',
@@ -257,13 +260,12 @@
return name.substring(0, 2).toUpperCase(); return name.substring(0, 2).toUpperCase();
} }
function submitRename(e: Event) { function submitRename(name: string) {
e.preventDefault(); if (name && name !== projectName) {
if (renameName && renameName !== projectName) {
fetch(`/api/projects/${projectId}`, { fetch(`/api/projects/${projectId}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: renameName }) body: JSON.stringify({ name })
}).then((res) => { if (res.ok) window.location.reload(); }); }).then((res) => { if (res.ok) window.location.reload(); });
} }
showRenameModal = false; showRenameModal = false;
@@ -289,17 +291,17 @@
<header class="flex flex-col border-b border-[var(--theme-border)] bg-[var(--theme-bg)] text-[var(--theme-text)] select-none w-full relative z-[70]"> <header class="flex flex-col border-b border-[var(--theme-border)] bg-[var(--theme-bg)] text-[var(--theme-text)] select-none w-full relative z-[70]">
<div class="flex items-center justify-between px-4 py-2.5"> <div class="flex items-center justify-between px-4 py-2.5">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button onclick={() => goto('/projects')} class="p-1.5 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" title="Projects"> <button onclick={() => goto('/projects')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors" title="Projects">
<Icon icon="mdi:arrow-left" class="text-xl" /> <Icon icon="mdi:arrow-left" class="text-xl" />
</button> </button>
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon="mdi:folder-multiple-outline" class="text-blue-500 text-base" /> <Icon icon="mdi:folder-multiple-outline" class="text-blue-500 text-base" />
<h1 class="text-[16px] font-semibold text-gray-900 dark:text-white tracking-tight truncate max-w-[200px] md:max-w-xs" title={projectName}>{projectName}</h1> <h1 class="text-[16px] font-semibold text-[var(--color-ink)] tracking-tight truncate max-w-[200px] md:max-w-xs" title={projectName}>{projectName}</h1>
</div> </div>
<div class="flex items-center gap-0.5 text-[13px] font-medium text-gray-600 dark:text-gray-300 -ml-1 action-menu-container"> <div class="flex items-center gap-0.5 text-[13px] font-medium text-[var(--color-ink-muted)] -ml-1 action-menu-container">
<div class="relative"> <div class="relative">
<button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }} class="px-2 py-0.5 rounded transition-colors {activeMenu === 'file' ? 'bg-[var(--theme-border)]' : 'hover:bg-[var(--theme-border)]'}">File</button> <button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }} class="px-2 py-0.5 rounded transition-colors {activeMenu === 'file' ? 'bg-[var(--theme-border)]' : 'hover:bg-[var(--theme-border)]'}">File</button>
{#if activeMenu === 'file'} {#if activeMenu === 'file'}
@@ -329,7 +331,7 @@
<button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm hover:bg-[var(--theme-border)]">HTML (.html)</button> <button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm hover:bg-[var(--theme-border)]">HTML (.html)</button>
{#if role === 'owner'} {#if role === 'owner'}
<div class="h-px bg-[var(--theme-border)] my-1"></div> <div class="h-px bg-[var(--theme-border)] my-1"></div>
<button onclick={() => { activeMenu = null; showDeleteModal = true; }} class="w-full text-left px-4 py-1.5 text-sm text-red-500 hover:bg-red-500/10">Delete Project</button> <button onclick={() => { activeMenu = null; showDeleteModal = true; }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10">Delete Project</button>
{/if} {/if}
</div> </div>
{/if} {/if}
@@ -352,7 +354,7 @@
{#if activeMenu === 'view'} {#if activeMenu === 'view'}
<div class="absolute left-0 top-full mt-1 w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-[var(--theme-border)] py-1 z-[100]"> <div class="absolute left-0 top-full mt-1 w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-[var(--theme-border)] py-1 z-[100]">
<button onclick={() => { activeMenu = null; $previewOpenStore = !$previewOpenStore; }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Preview<Icon icon={$previewOpenStore ? 'mdi:check' : ''} class="text-sm" /></button> <button onclick={() => { activeMenu = null; $previewOpenStore = !$previewOpenStore; }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Preview<Icon icon={$previewOpenStore ? 'mdi:check' : ''} class="text-sm" /></button>
<button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; document.documentElement.classList.toggle('dark', $darkModeStore); }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Dark Mode<Icon icon={$darkModeStore ? 'mdi:check' : ''} class="text-sm" /></button> <button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Dark Mode<Icon icon={$darkModeStore ? 'mdi:check' : ''} class="text-sm" /></button>
</div> </div>
{/if} {/if}
</div> </div>
@@ -364,36 +366,36 @@
{#if $connectedUsers.length > 0} {#if $connectedUsers.length > 0}
<div class="flex items-center -space-x-2 mr-2"> <div class="flex items-center -space-x-2 mr-2">
{#each $connectedUsers as user} {#each $connectedUsers as user}
<div class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-white dark:border-zinc-950 shadow-sm" style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};" title={user.name + (user.isLocal ? ' (You)' : '')}>{getInitials(user.name)}</div> <div class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-[var(--color-surface)] shadow-sm" style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};" title={user.name + (user.isLocal ? ' (You)' : '')}>{getInitials(user.name)}</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-5 bg-[var(--color-line)]"></div>
{#if !isViewer} {#if !isViewer}
<button onclick={() => (isPresentationOpen = true)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors"> <button onclick={() => (isPresentationOpen = true)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors">
<Icon icon="mdi:presentation-play" class="text-[16px]" /> Present <Icon icon="mdi:presentation-play" class="text-[16px]" /> Present
</button> </button>
{#if role === 'owner'} {#if role === 'owner'}
<button onclick={onPublish} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md transition-colors"> <button onclick={onPublish} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-white bg-[var(--color-accent)] hover:opacity-90 rounded-md transition-colors">
<Icon icon="mdi:package-variant-closed" class="text-[16px]" /> Publish <Icon icon="mdi:package-variant-closed" class="text-[16px]" /> Publish
</button> </button>
{/if} {/if}
{/if} {/if}
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-5 bg-[var(--color-line)]"></div>
<button onclick={() => ($previewOpenStore = !$previewOpenStore)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10' : 'text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20'}" title={$previewOpenStore ? 'Hide preview' : 'Show preview'}> <button onclick={() => ($previewOpenStore = !$previewOpenStore)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)]' : 'text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90'}" title={$previewOpenStore ? 'Hide preview' : 'Show preview'}>
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" /> Preview <Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" /> Preview
</button> </button>
<button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" title="Print"> <button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Print">
<Icon icon="mdi:printer" class="text-[16px]" /> Print <Icon icon="mdi:printer" class="text-[16px]" /> Print
</button> </button>
<div class="relative action-menu-container"> <div class="relative action-menu-container">
<button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-blue-500/30' : ''}"> <button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-[var(--color-accent)]/30' : ''}">
<Icon icon="mdi:export-variant" class="text-[16px]" /> Export <Icon icon="mdi:chevron-down" class="text-sm opacity-70" /> <Icon icon="mdi:export-variant" class="text-[16px]" /> Export <Icon icon="mdi:chevron-down" class="text-sm opacity-70" />
</button> </button>
{#if activeMenu === 'export'} {#if activeMenu === 'export'}
@@ -408,23 +410,23 @@
</div> </div>
</div> </div>
<div class="flex items-center px-4 py-1.5 bg-white/50 dark:bg-black/10 border-t border-gray-200/60 dark:border-white/10 gap-4 overflow-x-auto no-scrollbar"> <div class="flex items-center px-4 py-1.5 bg-[var(--color-surface-muted)] border-t border-[var(--color-line)] gap-4 overflow-x-auto no-scrollbar">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bold"><Icon icon="mdi:format-bold" class="text-lg" /></button> <button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bold"><Icon icon="mdi:format-bold" class="text-lg" /></button>
<button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Italic"><Icon icon="mdi:format-italic" class="text-lg" /></button> <button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Italic"><Icon icon="mdi:format-italic" class="text-lg" /></button>
<button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Code"><Icon icon="mdi:code-tags" class="text-lg" /></button> <button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Code"><Icon icon="mdi:code-tags" class="text-lg" /></button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Math (Inline)"><Icon icon="mdi:sigma" class="text-lg" /></button> <button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Math (Inline)"><Icon icon="mdi:sigma" class="text-lg" /></button>
<button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bullet List"><Icon icon="mdi:format-list-bulleted" class="text-lg" /></button> <button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bullet List"><Icon icon="mdi:format-list-bulleted" class="text-lg" /></button>
<button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Numbered List"><Icon icon="mdi:format-list-numbered" class="text-lg" /></button> <button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Numbered List"><Icon icon="mdi:format-list-numbered" class="text-lg" /></button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" accept="image/*,.ttf,.otf" /> <input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" accept="image/*,.ttf,.otf" />
{#if !isViewer} {#if !isViewer}
<button onclick={() => fileInput?.click()} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Upload Image / Font"><Icon icon="mdi:image-plus" class="text-lg" /></button> <button onclick={() => fileInput?.click()} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Upload Image / Font"><Icon icon="mdi:image-plus" class="text-lg" /></button>
{/if} {/if}
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="project-font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label> <label for="project-font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
@@ -443,22 +445,22 @@
</select> </select>
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
{#if !isViewer} {#if !isViewer}
<button onclick={() => (isPageSettingsOpen = true)} class="flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded shadow-sm transition-colors opacity-90 hover:opacity-100"> <button onclick={() => (isPageSettingsOpen = true)} class="flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded shadow-sm transition-colors opacity-90 hover:opacity-100">
<Icon icon="mdi:file-document-edit-outline" class="text-sm" /> Page Settings <Icon icon="mdi:file-document-edit-outline" class="text-sm" /> Page Settings
</button> </button>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
{/if} {/if}
<div class="flex items-center gap-1 bg-white dark:bg-black/20 border border-gray-300 dark:border-white/20 rounded shadow-sm overflow-hidden"> <div class="flex items-center gap-1 bg-[var(--color-surface)] border border-[var(--color-line)] rounded shadow-sm overflow-hidden">
<button onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)} class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors" title="Zoom Out"><Icon icon="mdi:minus" class="text-sm" /></button> <button onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)} class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors" title="Zoom Out"><Icon icon="mdi:minus" class="text-sm" /></button>
<span role="button" tabindex="0" onkeydown={(e) => { if (e.key === 'Enter') $documentZoomStore = 100; }} class="text-[11px] font-semibold text-gray-700 dark:text-gray-200 min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>{$documentZoomStore}%</span> <span role="button" tabindex="0" onkeydown={(e) => { if (e.key === 'Enter') $documentZoomStore = 100; }} class="text-[11px] font-semibold text-[var(--color-ink-muted)] min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>{$documentZoomStore}%</span>
<button onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)} class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors" title="Zoom In"><Icon icon="mdi:plus" class="text-sm" /></button> <button onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)} class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors" title="Zoom In"><Icon icon="mdi:plus" class="text-sm" /></button>
</div> </div>
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div> <div class="w-px h-4 bg-[var(--color-line)]"></div>
<ThemePicker /> <ThemePicker />
<div class="flex-grow"></div> <div class="flex-grow"></div>
@@ -474,50 +476,37 @@
{/if} {/if}
{#if showInfoModal} {#if showInfoModal}
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onclick={() => showInfoModal = false} role="presentation"> <Modal title={projectName} icon="ph:folder-star" onclose={() => showInfoModal = false}>
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-2xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation"> <div class="flex flex-col gap-4 text-xs">
<div class="p-6 border-b border-gray-100 dark:border-white/10 flex items-center gap-3"> <div><p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p><p class="text-sm text-[var(--color-ink)]">Project (multi-file)</p></div>
<Icon icon="mdi:folder-multiple-outline" class="text-xl text-blue-500" /> <div><p class="mb-1 font-medium text-[var(--color-ink-muted)]">Entrypoint</p><p class="font-mono text-sm text-[var(--color-ink)]">{entrypoint}</p></div>
<h3 class="text-lg font-semibold flex-grow truncate">{projectName}</h3> <div><p class="mb-1 font-medium text-[var(--color-ink-muted)]">Your role</p><p class="text-sm text-[var(--color-ink)] capitalize">{role}</p></div>
</div>
<div class="p-6 space-y-4 text-sm">
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p><p>Project (multi-file)</p></div>
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Entrypoint</p><p class="font-mono">{entrypoint}</p></div>
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Your role</p><p class="capitalize">{role}</p></div>
</div>
<div class="p-4 bg-gray-50 dark:bg-white/5 border-t border-gray-100 dark:border-white/10 flex justify-end">
<button onclick={() => showInfoModal = false} class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg">Close</button>
</div>
</div> </div>
</div>
{#snippet footer()}
<button onclick={() => showInfoModal = false} class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90">Close</button>
{/snippet}
</Modal>
{/if} {/if}
{#if showRenameModal} {#if showRenameModal}
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onclick={() => showRenameModal = false} role="presentation"> <PromptModal
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-2xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation"> title="Rename project"
<form onsubmit={submitRename} class="p-6"> label="Project name"
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2"><Icon icon="mdi:pencil-outline" class="text-yellow-500" /> Rename Project</h3> icon="ph:pencil-simple"
<input type="text" required bind:value={renameName} class="w-full bg-transparent border border-gray-300 dark:border-white/20 text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500" /> value={renameName}
<div class="pt-6 flex justify-end gap-3"> confirmLabel="Save"
<button type="button" onclick={() => showRenameModal = false} class="px-4 py-2 text-sm font-medium hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg">Cancel</button> onsubmit={submitRename}
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium">Save</button> onclose={() => showRenameModal = false}
</div> />
</form>
</div>
</div>
{/if} {/if}
{#if showDeleteModal} {#if showDeleteModal}
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onclick={() => showDeleteModal = false} role="presentation"> <ConfirmModal
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-2xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation"> title="Delete project"
<div class="p-6"> message="Delete this project and all its files? This cannot be undone."
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2"><Icon icon="mdi:trash-can-outline" class="text-red-500" /> Delete Project</h3> confirmLabel="Delete"
<p class="text-gray-600 dark:text-gray-300 text-sm mb-6">Delete this project and all its files? This cannot be undone.</p> onconfirm={confirmDelete}
<div class="flex justify-end gap-3"> onclose={() => showDeleteModal = false}
<button type="button" onclick={() => showDeleteModal = false} class="px-4 py-2 text-sm font-medium hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg">Cancel</button> />
<button type="button" onclick={confirmDelete} class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg text-sm font-medium">Delete</button>
</div>
</div>
</div>
</div>
{/if} {/if}
+6 -6
View File
@@ -34,14 +34,14 @@ if (typeof window !== 'undefined') {
if (savedDark !== null) darkModeStore.set(savedDark === 'true'); if (savedDark !== null) darkModeStore.set(savedDark === 'true');
if (savedZoom !== null) documentZoomStore.set(parseInt(savedZoom, 10)); if (savedZoom !== null) documentZoomStore.set(parseInt(savedZoom, 10));
themeStore.subscribe(value => localStorage.setItem('editor-theme', value)); themeStore.subscribe(value => {
localStorage.setItem('editor-theme', value);
document.documentElement.dataset.colorTheme = value;
});
darkModeStore.subscribe(value => { darkModeStore.subscribe(value => {
localStorage.setItem('editor-dark-mode', value.toString()); localStorage.setItem('editor-dark-mode', value.toString());
if (value) { document.documentElement.dataset.theme = value ? 'dark' : 'light';
document.documentElement.classList.add('dark'); document.documentElement.classList.toggle('dark', value);
} else {
document.documentElement.classList.remove('dark');
}
}); });
documentZoomStore.subscribe(value => localStorage.setItem('editor-document-zoom', value.toString())); documentZoomStore.subscribe(value => localStorage.setItem('editor-document-zoom', value.toString()));
} }
+7 -7
View File
@@ -4,26 +4,26 @@
</script> </script>
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4"> <div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
<div class="text-center max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-xl border border-gray-200 dark:border-zinc-800 p-8"> <div class="text-center max-w-md bg-[var(--color-surface)] rounded-2xl shadow-xl border border-[var(--color-line)] p-8">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-red-100 dark:bg-red-500/10 mb-6"> <div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--color-danger)]/10 mb-6">
<Icon icon="mdi:alert-circle-outline" class="h-10 w-10 text-red-600 dark:text-red-500" /> <Icon icon="mdi:alert-circle-outline" class="h-10 w-10 text-[var(--color-danger)]" />
</div> </div>
<h1 class="text-6xl font-bold text-gray-900 dark:text-white mb-2 tracking-tight"> <h1 class="text-6xl font-bold text-[var(--color-ink)] mb-2 tracking-tight">
{$page.status} {$page.status}
</h1> </h1>
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> <h2 class="text-xl font-semibold text-[var(--color-ink)] mb-4">
Something went wrong Something went wrong
</h2> </h2>
<p class="text-base text-gray-600 dark:text-gray-400 mb-8 leading-relaxed"> <p class="text-base text-[var(--color-ink-muted)] mb-8 leading-relaxed">
{$page.error?.message || 'We experienced an unexpected error processing your request.'} {$page.error?.message || 'We experienced an unexpected error processing your request.'}
</p> </p>
<a <a
href="/" href="/"
class="inline-flex items-center justify-center gap-2 px-6 py-3 border border-transparent text-sm font-semibold rounded-xl shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-colors w-full" class="inline-flex items-center justify-center gap-2 px-6 py-3 text-sm font-semibold rounded-xl shadow-sm text-white bg-[var(--color-accent)] hover:opacity-90 focus:outline-none transition w-full"
> >
<Icon icon="mdi:home" class="text-lg" /> <Icon icon="mdi:home" class="text-lg" />
Return to Dashboard Return to Dashboard
+3 -13
View File
@@ -40,21 +40,11 @@
</svelte:head> </svelte:head>
{#if loaded} {#if loaded}
<div <div class="min-h-screen w-full flex flex-col bg-[var(--color-surface-muted)] text-[var(--color-ink)] font-sans transition-colors duration-200">
class="min-h-screen w-full flex flex-col font-sans transition-colors duration-200"
style="
background-color: {currentColors.background};
color: {currentColors.text};
--theme-bg: {currentColors.background};
--theme-text: {currentColors.text};
--theme-border: {currentColors.selection};
--theme-cursor: {currentColors.cursor};
"
>
{@render children()} {@render children()}
</div> </div>
{:else} {:else}
<div class="min-h-screen w-full flex items-center justify-center bg-gray-50 dark:bg-zinc-950"> <div class="min-h-screen w-full flex items-center justify-center bg-[var(--color-surface-muted)]">
<div class="text-gray-500 dark:text-gray-400 font-medium animate-pulse">Loading TypstDrive...</div> <div class="text-[var(--color-ink-muted)] font-medium animate-pulse">Loading TypstDrive...</div>
</div> </div>
{/if} {/if}
+1 -1
View File
@@ -17,6 +17,6 @@
<meta name="description" content="Collaborative Typst Editor." /> <meta name="description" content="Collaborative Typst Editor." />
</svelte:head> </svelte:head>
<div class="h-full flex items-center justify-center text-gray-500"> <div class="h-full flex items-center justify-center text-[var(--color-ink-muted)]">
Redirecting... Redirecting...
</div> </div>
+86 -86
View File
@@ -170,12 +170,12 @@ with open("output.png", "wb") as f:
</style> </style>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<nav class="bg-[var(--theme-bg)] shadow-sm border-b border-gray-200 dark:border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0"> <nav class="bg-[var(--color-surface)] shadow-sm border-b border-[var(--color-line)] px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3"> <h1 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-3">
<Icon icon="mdi:api" class="text-blue-600 dark:text-blue-400 text-3xl" /> <Icon icon="mdi:api" class="text-[var(--color-accent)] text-3xl" />
API Reference API Reference
</h1> </h1>
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-4 py-2 rounded-lg flex items-center gap-2"> <button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-4 py-2 rounded-md flex items-center gap-2">
<Icon icon="mdi:arrow-left" class="text-lg" /> <Icon icon="mdi:arrow-left" class="text-lg" />
Back to Dashboard Back to Dashboard
</button> </button>
@@ -187,16 +187,16 @@ with open("output.png", "wb") as f:
{#each navSections as section} {#each navSections as section}
<button <button
onclick={() => activeSection = section.id} onclick={() => activeSection = section.id}
class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 {activeSection === section.id class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium transition-colors {activeSection === section.id
? 'bg-blue-600 text-white shadow-sm' ? 'bg-[var(--color-accent)] text-white shadow-sm'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5'}" : 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
> >
<Icon icon={section.icon} class="text-lg flex-shrink-0" /> <Icon icon={section.icon} class="text-lg flex-shrink-0" />
{section.label} {section.label}
</button> </button>
{/each} {/each}
<div class="pt-4 mt-4 border-t border-gray-200 dark:border-white/10"> <div class="pt-4 mt-4 border-t border-[var(--color-line)]">
<a href="/settings" class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5 transition-all duration-150"> <a href="/settings" class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)] transition-colors">
<Icon icon="mdi:key-plus" class="text-lg flex-shrink-0" /> <Icon icon="mdi:key-plus" class="text-lg flex-shrink-0" />
Manage API Keys Manage API Keys
</a> </a>
@@ -210,7 +210,7 @@ with open("output.png", "wb") as f:
{#each navSections as section} {#each navSections as section}
<button <button
onclick={() => activeSection = section.id} onclick={() => activeSection = section.id}
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors {activeSection === section.id ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300'}" class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors {activeSection === section.id ? 'bg-[var(--color-accent)] text-white' : 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]'}"
> >
{section.label} {section.label}
</button> </button>
@@ -218,51 +218,51 @@ with open("output.png", "wb") as f:
</div> </div>
{#if activeSection === 'overview'} {#if activeSection === 'overview'}
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8"> <div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-4 flex items-center gap-2">
<Icon icon="mdi:book-open-outline" class="text-2xl text-blue-500" /> <Icon icon="mdi:book-open-outline" class="text-2xl text-[var(--color-accent)]" />
Overview Overview
</h2> </h2>
<p class="text-gray-600 dark:text-gray-300 mb-6"> <p class="text-[var(--color-ink-muted)] mb-6">
The TypstDrive Render API lets you compile Typst markup into PNG images or PDF documents programmatically. The TypstDrive Render API lets you compile Typst markup into PNG images or PDF documents programmatically.
Authenticate with an API key and POST Typst code — get back binary output. Authenticate with an API key and POST Typst code — get back binary output.
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<div class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/10 border border-blue-100 dark:border-blue-800/30"> <div class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/10 border border-blue-100 dark:border-blue-800/30">
<Icon icon="mdi:image-outline" class="text-2xl text-blue-500 mb-2" /> <Icon icon="mdi:image-outline" class="text-2xl text-[var(--color-accent)] mb-2" />
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">PNG output</p> <p class="text-sm font-semibold text-[var(--color-ink)]">PNG output</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">First page rendered at 2× scale</p> <p class="text-xs text-[var(--color-ink-muted)] mt-1">First page rendered at 2× scale</p>
</div> </div>
<div class="p-4 rounded-xl bg-purple-50 dark:bg-purple-900/10 border border-purple-100 dark:border-purple-800/30"> <div class="p-4 rounded-xl bg-purple-50 dark:bg-purple-900/10 border border-purple-100 dark:border-purple-800/30">
<Icon icon="mdi:file-pdf-box" class="text-2xl text-purple-500 mb-2" /> <Icon icon="mdi:file-pdf-box" class="text-2xl text-purple-500 mb-2" />
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">PDF output</p> <p class="text-sm font-semibold text-[var(--color-ink)]">PDF output</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Full multi-page PDF document</p> <p class="text-xs text-[var(--color-ink-muted)] mt-1">Full multi-page PDF document</p>
</div> </div>
<div class="p-4 rounded-xl bg-green-50 dark:bg-green-900/10 border border-green-100 dark:border-green-800/30"> <div class="p-4 rounded-xl bg-[var(--color-success)]/10 border border-[var(--color-success)]/20">
<Icon icon="mdi:lightning-bolt" class="text-2xl text-green-500 mb-2" /> <Icon icon="mdi:lightning-bolt" class="text-2xl text-green-500 mb-2" />
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">Cached results</p> <p class="text-sm font-semibold text-[var(--color-ink)]">Cached results</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Identical inputs skip recompilation</p> <p class="text-xs text-[var(--color-ink-muted)] mt-1">Identical inputs skip recompilation</p>
</div> </div>
</div> </div>
<div class="bg-gray-50 dark:bg-black/30 rounded-xl p-4 border border-gray-200 dark:border-white/10"> <div class="bg-[var(--color-surface-muted)] rounded-xl p-4 border border-[var(--color-line)]">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-1">Base URL</p> <p class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)] mb-1">Base URL</p>
<code class="font-mono text-sm text-blue-600 dark:text-blue-400">{baseUrl}</code> <code class="font-mono text-sm text-[var(--color-accent)]">{baseUrl}</code>
</div> </div>
</div> </div>
{/if} {/if}
{#if activeSection === 'auth'} {#if activeSection === 'auth'}
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8"> <div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-4 flex items-center gap-2">
<Icon icon="mdi:key-outline" class="text-2xl text-blue-500" /> <Icon icon="mdi:key-outline" class="text-2xl text-[var(--color-accent)]" />
Authentication Authentication
</h2> </h2>
<p class="text-gray-600 dark:text-gray-300 mb-6"> <p class="text-[var(--color-ink-muted)] mb-6">
All requests must include an API key in the <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">Authorization</code> header. All requests must include an API key in the <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">Authorization</code> header.
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Header format</p> <p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-2">Header format</p>
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-3 text-xs font-mono leading-relaxed rounded-xl">{@html hljs.highlight('Authorization: Bearer td_your_api_key_here', { language: 'bash' }).value}</code></pre> <pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-3 text-xs font-mono leading-relaxed rounded-xl">{@html hljs.highlight('Authorization: Bearer td_your_api_key_here', { language: 'bash' }).value}</code></pre>
</div> </div>
<div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 flex gap-3"> <div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 flex gap-3">
@@ -273,10 +273,10 @@ with open("output.png", "wb") as f:
</div> </div>
</div> </div>
<div> <div>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Managing keys</p> <p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-1">Managing keys</p>
<p class="text-sm text-gray-500 dark:text-gray-400"> <p class="text-sm text-[var(--color-ink-muted)]">
Create, regenerate, and revoke keys in Create, regenerate, and revoke keys in
<a href="/settings" class="text-blue-600 dark:text-blue-400 hover:underline">Settings → API Keys</a>. <a href="/settings" class="text-[var(--color-accent)] hover:underline">Settings → API Keys</a>.
The full key is shown only once at creation time. The full key is shown only once at creation time.
</p> </p>
</div> </div>
@@ -285,43 +285,43 @@ with open("output.png", "wb") as f:
{/if} {/if}
{#if activeSection === 'endpoint'} {#if activeSection === 'endpoint'}
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8 space-y-6"> <div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8 space-y-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:api" class="text-2xl text-blue-500" /> <Icon icon="mdi:api" class="text-2xl text-[var(--color-accent)]" />
POST /v1/render POST /v1/render
</h2> </h2>
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<span class="px-2 py-0.5 text-xs font-bold bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-md">POST</span> <span class="px-2 py-0.5 text-xs font-bold bg-green-100 dark:bg-green-900/30 text-[var(--color-success)] rounded-md">POST</span>
<code class="font-mono text-sm text-gray-800 dark:text-gray-200">/v1/render</code> <code class="font-mono text-sm text-[var(--color-ink)]">/v1/render</code>
</div> </div>
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-[var(--color-ink-muted)]">
Compile Typst markup and return rendered output as PNG, PDF, or HTML. Compile Typst markup and return rendered output as PNG, PDF, or HTML.
Results are cached for 1 hour — identical inputs return the cached result without recompiling. Results are cached for 1 hour — identical inputs return the cached result without recompiling.
</p> </p>
</div> </div>
<div class="h-px bg-gray-200 dark:bg-white/10"></div> <div class="h-px bg-[var(--color-line)]"></div>
<div> <div>
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Request headers</p> <p class="text-sm font-bold text-[var(--color-ink)] mb-3">Request headers</p>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead> <thead>
<tr class="border-b border-gray-200 dark:border-white/10"> <tr class="border-b border-[var(--color-line)]">
<th class="text-left py-2 pr-4 font-semibold text-gray-700 dark:text-gray-300 w-40">Header</th> <th class="text-left py-2 pr-4 font-semibold text-[var(--color-ink-muted)] w-40">Header</th>
<th class="text-left py-2 font-semibold text-gray-700 dark:text-gray-300">Value</th> <th class="text-left py-2 font-semibold text-[var(--color-ink-muted)]">Value</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-gray-600 dark:text-gray-400"> <tbody class="text-[var(--color-ink-muted)]">
<tr class="border-b border-gray-100 dark:border-white/5"> <tr class="border-b border-[var(--color-line)]">
<td class="py-2 pr-4 font-mono text-xs">Authorization</td> <td class="py-2 pr-4 font-mono text-xs">Authorization</td>
<td class="py-2"><code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">Bearer &lt;api-key&gt;</code> — required</td> <td class="py-2"><code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">Bearer &lt;api-key&gt;</code> — required</td>
</tr> </tr>
<tr> <tr>
<td class="py-2 pr-4 font-mono text-xs">Content-Type</td> <td class="py-2 pr-4 font-mono text-xs">Content-Type</td>
<td class="py-2"><code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">application/json</code> — required</td> <td class="py-2"><code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">application/json</code> — required</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -329,23 +329,23 @@ with open("output.png", "wb") as f:
</div> </div>
<div> <div>
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Request body</p> <p class="text-sm font-bold text-[var(--color-ink)] mb-3">Request body</p>
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hSchema}</code></pre> <pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hSchema}</code></pre>
</div> </div>
<div> <div>
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Response</p> <p class="text-sm font-bold text-[var(--color-ink)] mb-3">Response</p>
<div class="p-3 rounded-lg bg-green-50 dark:bg-green-900/10 border border-green-100 dark:border-green-800/30 text-sm"> <div class="p-3 rounded-lg bg-[var(--color-success)]/10 border border-[var(--color-success)]/20 text-sm">
<span class="font-mono text-xs font-bold text-green-700 dark:text-green-400">200 OK</span> <span class="font-mono text-xs font-bold text-[var(--color-success)]">200 OK</span>
<span class="text-gray-600 dark:text-gray-400 ml-2">Response body with <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">Content-Type: image/png</code>, <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">application/pdf</code>, or <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">text/html</code></span> <span class="text-[var(--color-ink-muted)] ml-2">Response body with <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">Content-Type: image/png</code>, <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">application/pdf</code>, or <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">text/html</code></span>
</div> </div>
</div> </div>
<div> <div>
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Compilation errors</p> <p class="text-sm font-bold text-[var(--color-ink)] mb-3">Compilation errors</p>
<div class="p-3 mb-3 rounded-lg bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-800/30 text-sm"> <div class="p-3 mb-3 rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 text-sm">
<span class="font-mono text-xs font-bold text-red-700 dark:text-red-400">422 Unprocessable Entity</span> <span class="font-mono text-xs font-bold text-[var(--color-danger)]">422 Unprocessable Entity</span>
<span class="text-gray-600 dark:text-gray-400 ml-2">JSON body describing every Typst error. <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">error</code> is a readable summary; <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">details</code> lists each diagnostic with its message, severity, and source line and column.</span> <span class="text-[var(--color-ink-muted)] ml-2">JSON body describing every Typst error. <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">error</code> is a readable summary; <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">details</code> lists each diagnostic with its message, severity, and source line and column.</span>
</div> </div>
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hCompileErr}</code></pre> <pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hCompileErr}</code></pre>
</div> </div>
@@ -358,9 +358,9 @@ with open("output.png", "wb") as f:
{/if} {/if}
{#if activeSection === 'examples'} {#if activeSection === 'examples'}
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8 space-y-8"> <div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8 space-y-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:code-braces" class="text-2xl text-blue-500" /> <Icon icon="mdi:code-braces" class="text-2xl text-[var(--color-accent)]" />
Examples Examples
</h2> </h2>
@@ -374,11 +374,11 @@ with open("output.png", "wb") as f:
] as ex} ] as ex}
<div> <div>
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 flex items-center gap-2"> <p class="text-sm font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon={ex.icon} class="text-lg {ex.iconColor}" /> <Icon icon={ex.icon} class="text-lg {ex.iconColor}" />
{ex.label} {ex.label}
</p> </p>
<button onclick={() => copy(ex.id, ex.raw)} class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors px-2 py-1 rounded-md hover:bg-gray-100 dark:hover:bg-white/10"> <button onclick={() => copy(ex.id, ex.raw)} class="flex items-center gap-1 text-xs text-[var(--color-ink-muted)] hover:text-[var(--color-accent)] transition-colors px-2 py-1 rounded-md hover:bg-[var(--color-surface-sunken)]">
<Icon icon={copiedSnippet === ex.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" /> <Icon icon={copiedSnippet === ex.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
{copiedSnippet === ex.id ? 'Copied!' : 'Copy'} {copiedSnippet === ex.id ? 'Copied!' : 'Copy'}
</button> </button>
@@ -390,19 +390,19 @@ with open("output.png", "wb") as f:
{/if} {/if}
{#if activeSection === 'rate-limits'} {#if activeSection === 'rate-limits'}
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8"> <div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
<Icon icon="mdi:speedometer" class="text-2xl text-blue-500" /> <Icon icon="mdi:speedometer" class="text-2xl text-[var(--color-accent)]" />
Rate Limits Rate Limits
</h2> </h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
<div class="p-4 rounded-xl bg-gray-50 dark:bg-black/30 border border-gray-200 dark:border-white/10"> <div class="p-4 rounded-xl bg-[var(--color-surface-muted)] border border-[var(--color-line)]">
<p class="text-2xl font-bold text-gray-900 dark:text-white">60</p> <p class="text-2xl font-bold text-[var(--color-ink)]">60</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">requests / minute per key</p> <p class="text-sm text-[var(--color-ink-muted)] mt-1">requests / minute per key</p>
</div> </div>
<div class="p-4 rounded-xl bg-gray-50 dark:bg-black/30 border border-gray-200 dark:border-white/10"> <div class="p-4 rounded-xl bg-[var(--color-surface-muted)] border border-[var(--color-line)]">
<p class="text-2xl font-bold text-gray-900 dark:text-white">10</p> <p class="text-2xl font-bold text-[var(--color-ink)]">10</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">API keys per account</p> <p class="text-sm text-[var(--color-ink-muted)] mt-1">API keys per account</p>
</div> </div>
</div> </div>
<div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 text-sm text-amber-800 dark:text-amber-300 mb-4"> <div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 text-sm text-amber-800 dark:text-amber-300 mb-4">
@@ -410,19 +410,19 @@ with open("output.png", "wb") as f:
<p>Identical inputs (same code + files) skip recompilation and are served from cache for up to 1 hour. Cached responses return instantly and do not consume your rate limit.</p> <p>Identical inputs (same code + files) skip recompilation and are served from cache for up to 1 hour. Cached responses return instantly and do not consume your rate limit.</p>
</div> </div>
<div> <div>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">When exceeded</p> <p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-2">When exceeded</p>
<div class="p-3 rounded-lg bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-800/30 text-sm"> <div class="p-3 rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 text-sm">
<code class="font-mono text-xs font-bold text-red-700 dark:text-red-400">429 Too Many Requests</code> <code class="font-mono text-xs font-bold text-[var(--color-danger)]">429 Too Many Requests</code>
<span class="text-gray-600 dark:text-gray-400 ml-2">— wait for the current 60-second window to reset.</span> <span class="text-[var(--color-ink-muted)] ml-2">— wait for the current 60-second window to reset.</span>
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}
{#if activeSection === 'errors'} {#if activeSection === 'errors'}
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8"> <div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
<Icon icon="mdi:alert-circle-outline" class="text-2xl text-blue-500" /> <Icon icon="mdi:alert-circle-outline" class="text-2xl text-[var(--color-accent)]" />
Error Reference Error Reference
</h2> </h2>
<div class="space-y-3"> <div class="space-y-3">
@@ -433,18 +433,18 @@ with open("output.png", "wb") as f:
{ code: '429', name: 'Too Many Requests', desc: 'Rate limit exceeded. Wait for the current 60-second window to reset.' }, { code: '429', name: 'Too Many Requests', desc: 'Rate limit exceeded. Wait for the current 60-second window to reset.' },
{ code: '500', name: 'Internal Server Error', desc: 'Unexpected server error. Try again after a short delay.' }, { code: '500', name: 'Internal Server Error', desc: 'Unexpected server error. Try again after a short delay.' },
] as err} ] as err}
<div class="flex items-start gap-4 p-4 rounded-xl border border-gray-100 dark:border-white/10 bg-gray-50 dark:bg-black/20"> <div class="flex items-start gap-4 p-4 rounded-xl border border-[var(--color-line)] bg-[var(--color-surface-muted)]">
<code class="font-mono text-sm font-bold text-gray-800 dark:text-gray-200 flex-shrink-0 w-8">{err.code}</code> <code class="font-mono text-sm font-bold text-[var(--color-ink)] flex-shrink-0 w-8">{err.code}</code>
<div> <div>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">{err.name}</p> <p class="text-sm font-semibold text-[var(--color-ink)]">{err.name}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">{err.desc}</p> <p class="text-sm text-[var(--color-ink-muted)] mt-0.5">{err.desc}</p>
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
<div class="mt-6 p-4 rounded-xl bg-gray-50 dark:bg-black/30 border border-gray-200 dark:border-white/10"> <div class="mt-6 p-4 rounded-xl bg-[var(--color-surface-muted)] border border-[var(--color-line)]">
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Error body</p> <p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-1">Error body</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Compilation failures (<code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">422</code>) return a JSON body with an <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">error</code> summary and a <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">details</code> array. All other errors return plain text describing the issue.</p> <p class="text-sm text-[var(--color-ink-muted)]">Compilation failures (<code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">422</code>) return a JSON body with an <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">error</code> summary and a <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">details</code> array. All other errors return plain text describing the issue.</p>
</div> </div>
</div> </div>
{/if} {/if}
+76 -41
View File
@@ -10,12 +10,9 @@
import DocCard from '$lib/components/dashboard/DocCard.svelte'; import DocCard from '$lib/components/dashboard/DocCard.svelte';
import FileCard from '$lib/components/dashboard/FileCard.svelte'; import FileCard from '$lib/components/dashboard/FileCard.svelte';
import ShareModal from '$lib/components/ShareModal.svelte'; import ShareModal from '$lib/components/ShareModal.svelte';
import DeleteModal from '$lib/components/dashboard/DeleteModal.svelte'; import PromptModal from '$lib/components/PromptModal.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import InfoModal from '$lib/components/dashboard/InfoModal.svelte'; import InfoModal from '$lib/components/dashboard/InfoModal.svelte';
import RenameModal from '$lib/components/dashboard/RenameModal.svelte';
import CreateDocModal from '$lib/components/dashboard/CreateDocModal.svelte';
import CreateProjectModal from '$lib/components/dashboard/CreateProjectModal.svelte';
import CreateFolderModal from '$lib/components/dashboard/CreateFolderModal.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
let documents = $state<any[]>([]); let documents = $state<any[]>([]);
@@ -424,33 +421,33 @@
<main class="max-w-7xl w-full mx-auto py-10 px-4 sm:px-6 lg:px-8 grow block"> <main class="max-w-7xl w-full mx-auto py-10 px-4 sm:px-6 lg:px-8 grow block">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">My Documents</h2> <h2 class="text-3xl font-bold text-[var(--color-ink)] tracking-tight">My Documents</h2>
<div class="relative plus-dropdown-container"> <div class="relative plus-dropdown-container">
<button onclick={() => showPlusDropdown = !showPlusDropdown} class="flex items-center justify-center text-[var(--theme-text)] bg-[var(--theme-border)] opacity-90 hover:opacity-100 w-10 h-10 rounded-full shadow-md hover:shadow-lg transition-all duration-200 transform hover:-translate-y-0.5 border border-white/10 dark:border-black/20"> <button onclick={() => showPlusDropdown = !showPlusDropdown} class="flex items-center justify-center text-white bg-[var(--color-accent)] hover:opacity-90 w-10 h-10 rounded-full shadow-md transition">
<Icon icon="mdi:plus" class="text-2xl" /> <Icon icon="mdi:plus" class="text-2xl" />
</button> </button>
{#if showPlusDropdown} {#if showPlusDropdown}
<div class="absolute right-0 mt-2 w-48 bg-[var(--theme-bg)] rounded-lg shadow-xl border border-gray-200 dark:border-white/10 py-1 z-20"> <div class="absolute right-0 mt-2 w-48 bg-[var(--color-surface)] rounded-md shadow-xl border border-[var(--color-line)] py-1 z-20">
<button onclick={openCreateModal} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={openCreateModal} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:file-document-plus" class="text-lg text-blue-500" /> <Icon icon="mdi:file-document-plus" class="text-lg text-blue-500" />
New Document New Document
</button> </button>
<button onclick={openCreateProjectModal} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={openCreateProjectModal} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:folder-multiple-plus" class="text-lg text-indigo-500" /> <Icon icon="mdi:folder-multiple-plus" class="text-lg text-indigo-500" />
New Project New Project
</button> </button>
<button onclick={openCreateFolderModal} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={openCreateFolderModal} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:folder-plus" class="text-lg text-yellow-500" /> <Icon icon="mdi:folder-plus" class="text-lg text-yellow-500" />
New Folder New Folder
</button> </button>
<button onclick={() => { showPlusDropdown = false; fileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2"> <button onclick={() => { showPlusDropdown = false; fileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:upload" class="text-lg text-green-500" /> <Icon icon="mdi:upload" class="text-lg text-green-500" />
Upload File Upload File
</button> </button>
<div class="h-px bg-gray-200 dark:bg-white/10 my-1"></div> <div class="h-px bg-[var(--color-line)] my-1"></div>
<button onclick={() => { showPlusDropdown = false; importFileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2" disabled={isImporting}> <button onclick={() => { showPlusDropdown = false; importFileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2" disabled={isImporting}>
{#if isImporting} {#if isImporting}
<Icon icon="mdi:loading" class="text-lg text-purple-500 animate-spin" /> <Icon icon="mdi:loading" class="text-lg text-purple-500 animate-spin" />
Importing... Importing...
@@ -467,29 +464,29 @@
</div> </div>
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-6 bg-white/50 dark:bg-black/20 p-3 rounded-lg border border-gray-200 dark:border-white/10"> <div class="flex items-center gap-2 text-sm text-[var(--color-ink-muted)] mb-6 bg-[var(--color-surface)] p-3 rounded-md border border-[var(--color-line)]">
<button <button
onclick={() => navigateToBreadcrumb(-1)} onclick={() => navigateToBreadcrumb(-1)}
ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = -1; }} ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = -1; }}
ondragleave={() => dragOverBreadcrumbIndex = null} ondragleave={() => dragOverBreadcrumbIndex = null}
ondrop={(e) => handleDrop(e, null)} ondrop={(e) => handleDrop(e, null)}
class="hover:text-blue-600 dark:hover:text-blue-400 font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === -1 ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' : ''}"> class="hover:text-[var(--color-accent)] font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === -1 ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : ''}">
<Icon icon="mdi:home" class="text-lg inline-block pb-0.5" /> Home <Icon icon="mdi:home" class="text-lg inline-block pb-0.5" /> Home
</button> </button>
{#if inSharedDrive} {#if inSharedDrive}
<Icon icon="mdi:chevron-right" class="text-lg text-gray-400" /> <Icon icon="mdi:chevron-right" class="text-lg text-[var(--color-ink-muted)]" />
<span class="font-medium text-purple-600 dark:text-purple-400 flex items-center gap-1 px-2 py-1"> <span class="font-medium text-[var(--color-accent)] flex items-center gap-1 px-2 py-1">
<Icon icon="mdi:folder-account" class="text-base" /> Shared with me <Icon icon="mdi:folder-account" class="text-base" /> Shared with me
</span> </span>
{:else} {:else}
{#each folderPath as folder, index} {#each folderPath as folder, index}
<Icon icon="mdi:chevron-right" class="text-lg text-gray-400" /> <Icon icon="mdi:chevron-right" class="text-lg text-[var(--color-ink-muted)]" />
<button <button
onclick={() => navigateToBreadcrumb(index)} onclick={() => navigateToBreadcrumb(index)}
ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = index; }} ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = index; }}
ondragleave={() => dragOverBreadcrumbIndex = null} ondragleave={() => dragOverBreadcrumbIndex = null}
ondrop={(e) => handleDrop(e, folder.id)} ondrop={(e) => handleDrop(e, folder.id)}
class="hover:text-blue-600 dark:hover:text-blue-400 font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === index ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' : ''}"> class="hover:text-[var(--color-accent)] font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === index ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : ''}">
{folder.name} {folder.name}
</button> </button>
{/each} {/each}
@@ -499,19 +496,19 @@
{#if inSharedDrive} {#if inSharedDrive}
{#if sharedDocsLoading} {#if sharedDocsLoading}
<div class="min-h-[50vh] flex items-center justify-center"> <div class="min-h-[50vh] flex items-center justify-center">
<div class="flex flex-col items-center gap-4 text-gray-500 dark:text-gray-400 animate-pulse"> <div class="flex flex-col items-center gap-4 text-[var(--color-ink-muted)] animate-pulse">
<Icon icon="mdi:loading" class="text-4xl animate-spin" /> <Icon icon="mdi:loading" class="text-4xl animate-spin" />
<p class="text-lg font-medium">Loading shared documents...</p> <p class="text-lg font-medium">Loading shared documents...</p>
</div> </div>
</div> </div>
{:else if sharedDocs.length === 0} {:else if sharedDocs.length === 0}
<div class="min-h-[50vh] flex items-center justify-center"> <div class="min-h-[50vh] flex items-center justify-center">
<div class="text-center p-12 bg-white/50 dark:bg-black/20 rounded-2xl shadow-sm border border-gray-200 dark:border-white/10 max-w-md w-full"> <div class="text-center p-12 bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] max-w-md w-full">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-purple-100/50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 mb-6"> <div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] mb-6">
<Icon icon="mdi:folder-account-outline" class="text-4xl" /> <Icon icon="mdi:folder-account-outline" class="text-4xl" />
</div> </div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">No shared documents</h3> <h3 class="text-xl font-bold text-[var(--color-ink)] mb-2">No shared documents</h3>
<p class="text-gray-500 dark:text-gray-400">Documents shared with you by other users will appear here.</p> <p class="text-[var(--color-ink-muted)]">Documents shared with you by other users will appear here.</p>
</div> </div>
</div> </div>
{:else} {:else}
@@ -532,7 +529,7 @@
{:else if loading} {:else if loading}
<div class="min-h-[50vh] flex items-center justify-center"> <div class="min-h-[50vh] flex items-center justify-center">
<div class="flex flex-col items-center gap-4 text-gray-500 dark:text-gray-400 animate-pulse"> <div class="flex flex-col items-center gap-4 text-[var(--color-ink-muted)] animate-pulse">
<Icon icon="mdi:loading" class="text-4xl animate-spin" /> <Icon icon="mdi:loading" class="text-4xl animate-spin" />
<p class="text-lg font-medium">Loading your workspace...</p> <p class="text-lg font-medium">Loading your workspace...</p>
</div> </div>
@@ -540,20 +537,20 @@
{:else} {:else}
{#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-[var(--color-ink-muted)]">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}
<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-[var(--color-surface)] border border-[var(--color-line)] rounded-lg shadow-sm hover:border-[var(--color-accent)] cursor-pointer group transition"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={enterSharedDrive} onclick={enterSharedDrive}
onkeydown={(e) => e.key === 'Enter' && enterSharedDrive()} onkeydown={(e) => e.key === 'Enter' && enterSharedDrive()}
> >
<div class="flex items-center justify-center w-10 h-10 bg-purple-50 dark:bg-purple-500/10 rounded-lg group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3"> <div class="flex items-center justify-center w-10 h-10 bg-[var(--color-accent-soft)] rounded-md group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3">
<Icon icon="mdi:folder-account" class="text-2xl text-purple-500" /> <Icon icon="mdi:folder-account" class="text-2xl text-[var(--color-accent)]" />
</div> </div>
<span class="font-medium text-gray-900 dark:text-white text-sm truncate w-full pointer-events-none">Shared with me</span> <span class="font-medium text-[var(--color-ink)] text-sm truncate w-full pointer-events-none">Shared with me</span>
</div> </div>
{/if} {/if}
{#each folders as folder} {#each folders as folder}
@@ -591,16 +588,16 @@
{#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-[var(--color-ink-muted)]">This folder is empty.</p>
</div> </div>
{:else if documents.length === 0 && files.length === 0 && folders.length === 0 && currentFolderId === null} {:else if documents.length === 0 && files.length === 0 && folders.length === 0 && currentFolderId === null}
<div class="text-center py-12"> <div class="text-center py-12">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100/50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 mb-4"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] mb-4">
<Icon icon="mdi:file-document-outline" class="text-3xl" /> <Icon icon="mdi:file-document-outline" class="text-3xl" />
</div> </div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-1">No documents yet</h3> <h3 class="text-lg font-bold text-[var(--color-ink)] mb-1">No documents yet</h3>
<p class="text-gray-500 dark:text-gray-400 mb-6 text-sm">Create your first Typst document to get started.</p> <p class="text-[var(--color-ink-muted)] mb-6 text-sm">Create your first Typst document to get started.</p>
<button onclick={openCreateModal} class="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg shadow-sm text-sm font-medium transition-colors"> <button onclick={openCreateModal} class="inline-flex items-center gap-2 bg-[var(--color-accent)] hover:opacity-90 text-white px-5 py-2.5 rounded-md shadow-sm text-sm font-medium transition">
<Icon icon="mdi:plus" class="text-lg" /> <Icon icon="mdi:plus" class="text-lg" />
Create Document Create Document
</button> </button>
@@ -614,7 +611,13 @@
{/if} {/if}
{#if showDeleteModal && deleteTarget} {#if showDeleteModal && deleteTarget}
<DeleteModal {deleteTarget} {confirmDelete} onClose={() => showDeleteModal = false} /> <ConfirmModal
title={`Delete ${deleteTarget.type}`}
message={`Are you sure you want to delete '${deleteTarget.name}'? ${deleteTarget.type === 'folder' ? 'This will also delete all of its contents. ' : ''}This action cannot be undone.`}
confirmLabel="Delete"
onconfirm={confirmDelete}
onclose={() => showDeleteModal = false}
/>
{/if} {/if}
{#if showInfoModal && selectedInfo} {#if showInfoModal && selectedInfo}
@@ -622,19 +625,51 @@
{/if} {/if}
{#if showRenameModal} {#if showRenameModal}
<RenameModal initialTitle={renameTitle} {handleRename} onClose={() => showRenameModal = false} /> <PromptModal
title="Rename"
label="New name"
icon="ph:pencil-simple"
value={renameTitle}
confirmLabel="Save"
onsubmit={handleRename}
onclose={() => showRenameModal = false}
/>
{/if} {/if}
{#if showCreateModal} {#if showCreateModal}
<CreateDocModal {createDoc} onClose={() => showCreateModal = false} /> <PromptModal
title="Create document"
label="Document title"
icon="ph:file-plus"
value="Untitled Document"
confirmLabel="Create"
onsubmit={createDoc}
onclose={() => showCreateModal = false}
/>
{/if} {/if}
{#if showCreateProjectModal} {#if showCreateProjectModal}
<CreateProjectModal {createProject} onClose={() => showCreateProjectModal = false} /> <PromptModal
title="Create project"
label="Project name"
icon="ph:folder-star"
value="Untitled Project"
confirmLabel="Create"
onsubmit={createProject}
onclose={() => showCreateProjectModal = false}
/>
{/if} {/if}
{#if showCreateFolderModal} {#if showCreateFolderModal}
<CreateFolderModal {createFolder} onClose={() => showCreateFolderModal = false} /> <PromptModal
title="Create folder"
label="Folder name"
icon="ph:folder-plus"
value="New Folder"
confirmLabel="Create"
onsubmit={createFolder}
onclose={() => showCreateFolderModal = false}
/>
{/if} {/if}
</main> </main>
+3 -3
View File
@@ -129,7 +129,7 @@
<main class="flex-1 flex flex-col md:flex-row overflow-hidden relative" oncontextmenu={handleContextMenu}> <main class="flex-1 flex flex-col md:flex-row overflow-hidden relative" oncontextmenu={handleContextMenu}>
{#if !isViewer} {#if !isViewer}
<div class="flex flex-col relative min-h-[50%] md:min-h-0 bg-transparent z-10 shadow-[1px_0_10px_rgba(0,0,0,0.05)] dark:shadow-[1px_0_10px_rgba(0,0,0,0.2)] {$previewOpenStore ? 'w-full md:w-1/2 border-r border-gray-200 dark:border-white/10' : 'w-full'}"> <div class="flex flex-col relative min-h-[50%] md:min-h-0 bg-transparent z-10 {$previewOpenStore ? 'w-full md:w-1/2 border-r border-[var(--color-line)]' : 'w-full'}">
{#if initialized} {#if initialized}
<Editor /> <Editor />
{/if} {/if}
@@ -137,7 +137,7 @@
{/if} {/if}
{#if $previewOpenStore || isViewer} {#if $previewOpenStore || isViewer}
<div class="{isViewer ? 'w-full' : 'w-full md:w-1/2'} relative bg-white/50 dark:bg-black/20 min-h-[50%] md:min-h-0 flex flex-col"> <div class="{isViewer ? 'w-full' : 'w-full md:w-1/2'} relative bg-[var(--color-surface)] min-h-[50%] md:min-h-0 flex flex-col">
<Preview {svgs} /> <Preview {svgs} />
<ErrorBanner {errors} /> <ErrorBanner {errors} />
</div> </div>
@@ -158,7 +158,7 @@
</button> </button>
<div class="h-px bg-[var(--theme-border)] my-1"></div> <div class="h-px bg-[var(--theme-border)] my-1"></div>
<button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2"> <button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-500"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--color-ink-muted)]"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
Copy Text Copy Text
</button> </button>
</div> </div>
+19 -24
View File
@@ -51,65 +51,60 @@
<meta name="description" content="Sign in to TypstDrive." /> <meta name="description" content="Sign in to TypstDrive." />
</svelte:head> </svelte:head>
<div class="min-h-screen flex flex-col relative overflow-hidden"> <div class="min-h-screen flex flex-col bg-[var(--color-surface-muted)]">
<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">
<div class="max-w-md w-full space-y-8 rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] p-10 shadow-2xl">
<div class="absolute -top-40 -left-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 top-40 -right-40 w-96 h-96 bg-purple-400/20 dark:bg-purple-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
<div class="absolute -bottom-40 left-20 w-96 h-96 bg-indigo-400/20 dark:bg-indigo-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
<div class="max-w-md w-full space-y-8 bg-white/80 dark:bg-black/40 backdrop-blur-xl p-10 rounded-2xl shadow-2xl border border-gray-200/50 dark:border-white/10 relative z-10">
<div class="text-center"> <div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100/50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-500 mb-6 mx-auto shadow-sm"> <div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-[var(--color-accent)] mb-6 mx-auto shadow-sm">
<Icon icon="mdi:script-text" class="text-3xl" /> <img src="/favicon.png" alt="TypstDrive" class="h-14 w-14" />
</div> </div>
<h2 class="text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white"> <h2 class="text-3xl font-extrabold tracking-tight text-[var(--color-ink)]">
Welcome back Welcome back
</h2> </h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 font-medium"> <p class="mt-2 text-sm text-[var(--color-ink-muted)] font-medium">
Sign in to your TypstDrive workspace Sign in to your TypstDrive workspace
</p> </p>
</div> </div>
<form class="mt-8 space-y-6" onsubmit={login}> <form class="mt-8 space-y-6" onsubmit={login}>
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="email" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Email Address</label> <label for="email" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Email address</label>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon icon="mdi:email" class="text-gray-400 dark:text-gray-500" /> <Icon icon="mdi:email" class="text-[var(--color-ink-muted)]" />
</div> </div>
<input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="[email protected]"> <input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="[email protected]">
</div> </div>
</div> </div>
<div> <div>
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Password</label> <label for="password" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Password</label>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon icon="mdi:lock" class="text-gray-400 dark:text-gray-500" /> <Icon icon="mdi:lock" class="text-[var(--color-ink-muted)]" />
</div> </div>
<input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="••••••••"> <input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="••••••••">
</div> </div>
</div> </div>
</div> </div>
{#if errorMsg} {#if errorMsg}
<div class="flex items-center gap-2 text-red-600 bg-red-50 dark:bg-red-500/10 dark:text-red-400 p-3 rounded-lg text-sm border border-red-200 dark:border-red-500/20 shadow-sm animate-in slide-in-from-top-1 fade-in duration-200"> <div class="flex items-center gap-2 text-[var(--color-danger)] bg-[var(--color-danger)]/10 p-3 rounded-md text-sm border border-[var(--color-danger)]/20">
<Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" /> <Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" />
<span class="font-medium">{errorMsg}</span> <span class="font-medium">{errorMsg}</span>
</div> </div>
{/if} {/if}
<div class="pt-2"> <div class="pt-2">
<button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent text-sm font-bold rounded-xl text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 focus:ring-blue-500 transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5"> <button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 text-sm font-bold rounded-md text-white bg-[var(--color-accent)] hover:opacity-90 focus:outline-none transition">
Sign In Sign in
<Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" /> <Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" />
</button> </button>
</div> </div>
</form> </form>
{#if registrationEnabled} {#if registrationEnabled}
<div class="text-sm text-center mt-6 pt-4 border-t border-gray-200 dark:border-white/10"> <div class="text-sm text-center mt-6 pt-4 border-t border-[var(--color-line)]">
<span class="text-gray-500 dark:text-gray-400">New to TypstDrive? </span> <span class="text-[var(--color-ink-muted)]">New to TypstDrive? </span>
<a href="/register" class="font-bold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors hover:underline"> <a href="/register" class="font-bold text-[var(--color-accent)] hover:underline transition-colors">
Create an account Create an account
</a> </a>
</div> </div>
+14 -14
View File
@@ -46,55 +46,55 @@
<title>Packages - TypstDrive</title> <title>Packages - TypstDrive</title>
</svelte:head> </svelte:head>
<div class="min-h-screen bg-gray-50 dark:bg-[var(--theme-bg)]"> <div class="min-h-screen bg-[var(--color-surface-muted)]">
<Navbar /> <Navbar />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6"> <div class="mb-6">
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors mb-2 flex items-center gap-1.5"> <button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors mb-2 flex items-center gap-1.5">
<Icon icon="mdi:arrow-left" class="text-lg" /> <Icon icon="mdi:arrow-left" class="text-lg" />
Back to Dashboard Back to Dashboard
</button> </button>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h2 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:package-variant-closed" class="text-purple-500" /> <Icon icon="mdi:package-variant-closed" class="text-purple-500" />
Packages Packages
</h2> </h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1"> <p class="text-sm text-[var(--color-ink-muted)] mt-1">
Instance-local Typst packages, published from Projects and importable as Instance-local Typst packages, published from Projects and importable as
<code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">@typstdrive/&lt;name&gt;:&lt;version&gt;</code>. <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">@typstdrive/&lt;name&gt;:&lt;version&gt;</code>.
</p> </p>
</div> </div>
{#if loading} {#if loading}
<p class="text-gray-500 dark:text-gray-400">Loading…</p> <p class="text-[var(--color-ink-muted)]">Loading…</p>
{:else if packages.length === 0} {:else if packages.length === 0}
<div class="text-center py-16 text-gray-500 dark:text-gray-400"> <div class="text-center py-16 text-[var(--color-ink-muted)]">
<Icon icon="mdi:package-variant" class="text-5xl mx-auto mb-3 opacity-50" /> <Icon icon="mdi:package-variant" class="text-5xl mx-auto mb-3 opacity-50" />
<p>No packages published yet. Open a Project and use “Publish” to create one.</p> <p>No packages published yet. Open a Project and use “Publish” to create one.</p>
</div> </div>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each packages as pkg (pkg.id)} {#each packages as pkg (pkg.id)}
<div class="bg-white dark:bg-black/20 rounded-xl border border-gray-200 dark:border-white/10 p-4 flex items-start justify-between gap-4"> <div class="bg-[var(--color-surface)] rounded-xl border border-[var(--color-line)] p-4 flex items-start justify-between gap-4">
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="font-semibold text-gray-900 dark:text-white truncate">@typstdrive/{pkg.name}</p> <p class="font-semibold text-[var(--color-ink)] truncate">@typstdrive/{pkg.name}</p>
{#if pkg.latest_version} {#if pkg.latest_version}
<span class="text-xs font-mono bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-1.5 py-0.5 rounded">v{pkg.latest_version}</span> <span class="text-xs font-mono bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-1.5 py-0.5 rounded">v{pkg.latest_version}</span>
{/if} {/if}
</div> </div>
{#if pkg.description} {#if pkg.description}
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 truncate">{pkg.description}</p> <p class="text-sm text-[var(--color-ink-muted)] mt-1 truncate">{pkg.description}</p>
{/if} {/if}
<p class="text-xs text-gray-400 mt-1">by {pkg.owner_name ?? 'unknown'}</p> <p class="text-xs text-[var(--color-ink-muted)] mt-1">by {pkg.owner_name ?? 'unknown'}</p>
<pre class="mt-2 text-xs font-mono bg-gray-50 dark:bg-black/30 border border-gray-100 dark:border-white/5 rounded px-2 py-1 overflow-x-auto">{importSnippet(pkg)}</pre> <pre class="mt-2 text-xs font-mono bg-[var(--color-surface-muted)] border border-[var(--color-line)] rounded px-2 py-1 overflow-x-auto">{importSnippet(pkg)}</pre>
</div> </div>
<div class="flex flex-col items-end gap-2 flex-shrink-0"> <div class="flex flex-col items-end gap-2 flex-shrink-0">
<button onclick={() => copy(pkg)} class="text-xs px-2 py-1 rounded-md bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10 flex items-center gap-1"> <button onclick={() => copy(pkg)} class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] flex items-center gap-1">
<Icon icon={copied === pkg.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" /> <Icon icon={copied === pkg.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
{copied === pkg.id ? 'Copied' : 'Copy'} {copied === pkg.id ? 'Copied' : 'Copy'}
</button> </button>
<button onclick={() => remove(pkg)} title="Delete" class="text-xs px-2 py-1 rounded-md text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-1"> <button onclick={() => remove(pkg)} title="Delete" class="text-xs px-2 py-1 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-1">
<Icon icon="mdi:trash-can-outline" class="text-sm" /> Delete <Icon icon="mdi:trash-can-outline" class="text-sm" /> Delete
</button> </button>
</div> </div>
+3 -3
View File
@@ -224,7 +224,7 @@
</aside> </aside>
{#if !readOnly} {#if !readOnly}
<div class="flex flex-col min-h-0 {$previewOpenStore ? 'w-full md:w-1/2 border-r border-gray-200 dark:border-white/10' : 'flex-1'}"> <div class="flex flex-col min-h-0 {$previewOpenStore ? 'w-full md:w-1/2 border-r border-[var(--color-line)]' : 'flex-1'}">
{#if ready && activeEntry} {#if ready && activeEntry}
{#key activeFileId} {#key activeFileId}
<Editor ytext={activeEntry.text} awarenessProvider={activeEntry.provider} filePath={activePath} enableLsp={false} /> <Editor ytext={activeEntry.text} awarenessProvider={activeEntry.provider} filePath={activePath} enableLsp={false} />
@@ -234,7 +234,7 @@
{/if} {/if}
{#if $previewOpenStore || readOnly} {#if $previewOpenStore || readOnly}
<div class="{readOnly ? 'flex-1' : 'w-full md:w-1/2'} relative bg-white/50 dark:bg-black/20 flex flex-col"> <div class="{readOnly ? 'flex-1' : 'w-full md:w-1/2'} relative bg-[var(--color-surface)] flex flex-col">
<Preview {svgs} /> <Preview {svgs} />
<ErrorBanner {errors} /> <ErrorBanner {errors} />
</div> </div>
@@ -247,7 +247,7 @@
{#if contextMenu.show} {#if contextMenu.show}
<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-[180px] overflow-hidden" style="left: {contextMenu.x}px; top: {contextMenu.y}px;"> <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-[180px] overflow-hidden" style="left: {contextMenu.x}px; top: {contextMenu.y}px;">
<button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2"> <button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-500"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--color-ink-muted)]"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
Copy Text Copy Text
</button> </button>
</div> </div>
+81 -61
View File
@@ -4,6 +4,9 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import Navbar from '$lib/components/dashboard/Navbar.svelte'; import Navbar from '$lib/components/dashboard/Navbar.svelte';
import ProjectCard from '$lib/components/dashboard/ProjectCard.svelte'; import ProjectCard from '$lib/components/dashboard/ProjectCard.svelte';
import PromptModal from '$lib/components/PromptModal.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import Modal from '$lib/components/Modal.svelte';
interface Project { interface Project {
id: string; id: string;
@@ -18,7 +21,6 @@
let shared = $state<Project[]>([]); let shared = $state<Project[]>([]);
let loading = $state(true); let loading = $state(true);
let showCreate = $state(false); let showCreate = $state(false);
let newName = $state('');
let creating = $state(false); let creating = $state(false);
let activeMenu = $state<string | null>(null); let activeMenu = $state<string | null>(null);
@@ -27,21 +29,20 @@
let renameName = $state(''); let renameName = $state('');
let showInfo = $state(false); let showInfo = $state(false);
let infoProject = $state<Project | null>(null); let infoProject = $state<Project | null>(null);
let deleteTarget = $state<{ id: string; name: string } | null>(null);
function setActiveMenu(id: string | null) { activeMenu = id; } function setActiveMenu(id: string | null) { activeMenu = id; }
function openInfo(project: Project) { activeMenu = null; infoProject = project; showInfo = true; } function openInfo(project: Project) { activeMenu = null; infoProject = project; showInfo = true; }
function openRename(id: string, name: string) { activeMenu = null; renameId = id; renameName = name; showRename = true; } function openRename(id: string, name: string) { activeMenu = null; renameId = id; renameName = name; showRename = true; }
async function submitRename(e: Event) { async function submitRename(name: string) {
e.preventDefault();
if (!renameName.trim()) return;
const res = await fetch(`/api/projects/${renameId}`, { const res = await fetch(`/api/projects/${renameId}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: renameName.trim() }) body: JSON.stringify({ name })
}); });
if (res.ok) { if (res.ok) {
projects = projects.map((p) => (p.id === renameId ? { ...p, name: renameName.trim() } : p)); projects = projects.map((p) => (p.id === renameId ? { ...p, name } : p));
} }
showRename = false; showRename = false;
} }
@@ -62,13 +63,12 @@
loading = false; loading = false;
} }
async function create() { async function create(name: string) {
if (!newName.trim()) return;
creating = true; creating = true;
const res = await fetch('/api/projects', { const res = await fetch('/api/projects', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName.trim() }) body: JSON.stringify({ name })
}); });
creating = false; creating = false;
if (res.ok) { if (res.ok) {
@@ -77,10 +77,16 @@
} }
} }
async function remove(id: string, name: string) { function requestDelete(id: string, name: string) {
if (!confirm(`Delete project "${name}"? This cannot be undone.`)) return; activeMenu = null;
const res = await fetch(`/api/projects/${id}`, { method: 'DELETE' }); deleteTarget = { id, name };
if (res.ok) projects = projects.filter((p) => p.id !== id); }
async function confirmDeleteProject() {
if (!deleteTarget) return;
const res = await fetch(`/api/projects/${deleteTarget.id}`, { method: 'DELETE' });
if (res.ok) projects = projects.filter((p) => p.id !== deleteTarget!.id);
deleteTarget = null;
} }
onMount(load); onMount(load);
@@ -92,32 +98,32 @@
<svelte:window onclick={handleWindowClick} /> <svelte:window onclick={handleWindowClick} />
<div class="min-h-screen bg-gray-50 dark:bg-[var(--theme-bg)]"> <div class="min-h-screen bg-[var(--color-surface-muted)]">
<Navbar /> <Navbar />
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<div> <div>
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors mb-2 flex items-center gap-1.5"> <button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors mb-2 flex items-center gap-1.5">
<Icon icon="mdi:arrow-left" class="text-lg" /> <Icon icon="mdi:arrow-left" class="text-lg" />
Back to Dashboard Back to Dashboard
</button> </button>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h2 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:folder-multiple-outline" class="text-blue-500" /> <Icon icon="mdi:folder-multiple-outline" class="text-[var(--color-accent)]" />
Projects Projects
</h2> </h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Multi-file Typst workspaces with their own <code class="font-mono text-xs">typst.toml</code>.</p> <p class="text-sm text-[var(--color-ink-muted)] mt-1">Multi-file Typst workspaces with their own <code class="font-mono text-xs">typst.toml</code>.</p>
</div> </div>
<button onclick={() => { showCreate = true; newName = ''; }} class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700 flex items-center gap-2"> <button onclick={() => (showCreate = true)} class="px-4 py-2 text-sm rounded-md bg-[var(--color-accent)] text-white hover:opacity-90 transition flex items-center gap-2">
<Icon icon="mdi:plus" class="text-lg" /> New Project <Icon icon="mdi:plus" class="text-lg" /> New Project
</button> </button>
</div> </div>
{#if loading} {#if loading}
<p class="text-gray-500 dark:text-gray-400">Loading…</p> <p class="text-[var(--color-ink-muted)]">Loading…</p>
{:else} {:else}
{#if projects.length === 0} {#if projects.length === 0}
<div class="text-center py-16 text-gray-500 dark:text-gray-400"> <div class="text-center py-16 text-[var(--color-ink-muted)]">
<Icon icon="mdi:folder-multiple-outline" class="text-5xl mx-auto mb-3 opacity-50" /> <Icon icon="mdi:folder-multiple-outline" class="text-5xl mx-auto mb-3 opacity-50" />
<p>No projects yet. Create one to start a multi-file project.</p> <p>No projects yet. Create one to start a multi-file project.</p>
</div> </div>
@@ -130,29 +136,29 @@
{setActiveMenu} {setActiveMenu}
{openInfo} {openInfo}
{openRename} {openRename}
deleteProject={remove} deleteProject={requestDelete}
/> />
{/each} {/each}
</div> </div>
{/if} {/if}
{#if shared.length > 0} {#if shared.length > 0}
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-10 mb-4 flex items-center gap-2"> <h3 class="text-lg font-semibold text-[var(--color-ink)] mt-10 mb-4 flex items-center gap-2">
<Icon icon="mdi:account-group-outline" class="text-blue-500" /> Shared with me <Icon icon="mdi:account-group-outline" class="text-[var(--color-accent)]" /> Shared with me
</h3> </h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{#each shared as project (project.id)} {#each shared as project (project.id)}
<button onclick={() => goto(`/project/${project.id}`)} class="text-left bg-white dark:bg-black/20 rounded-xl border border-gray-200 dark:border-white/10 overflow-hidden hover:shadow-md transition-shadow"> <button onclick={() => goto(`/project/${project.id}`)} class="text-left bg-[var(--color-surface)] rounded-lg border border-[var(--color-line)] overflow-hidden hover:border-[var(--color-accent)] transition">
<div class="h-32 bg-gray-50 dark:bg-black/30 flex items-center justify-center overflow-hidden border-b border-gray-100 dark:border-white/5"> <div class="h-32 bg-[var(--color-surface-muted)] flex items-center justify-center overflow-hidden border-b border-[var(--color-line)]">
{#if project.thumbnail_svg} {#if project.thumbnail_svg}
{@html project.thumbnail_svg} {@html project.thumbnail_svg}
{:else} {:else}
<Icon icon="mdi:folder-multiple-outline" class="text-4xl text-gray-300 dark:text-gray-600" /> <Icon icon="mdi:folder-multiple-outline" class="text-4xl text-[var(--color-ink-muted)]" />
{/if} {/if}
</div> </div>
<div class="p-3"> <div class="p-3">
<p class="font-medium text-gray-900 dark:text-white truncate">{project.name}</p> <p class="font-medium text-[var(--color-ink)] truncate">{project.name}</p>
<p class="text-xs text-gray-400 mt-0.5">{project.effective_role}</p> <p class="text-xs text-[var(--color-ink-muted)] mt-0.5">{project.effective_role}</p>
</div> </div>
</button> </button>
{/each} {/each}
@@ -163,45 +169,59 @@
</div> </div>
{#if showCreate} {#if showCreate}
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={() => (showCreate = false)} role="presentation"> <PromptModal
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md p-6" onclick={(e) => e.stopPropagation()} role="presentation"> title="New project"
<h2 class="text-lg font-bold mb-4 flex items-center gap-2"><Icon icon="mdi:folder-plus-outline" class="text-blue-500" /> New Project</h2> label="Project name"
<input bind:value={newName} placeholder="Project name" onkeydown={(e) => e.key === 'Enter' && create()} class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-white/10 bg-transparent text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500/40" /> icon="ph:folder-star"
<div class="flex justify-end gap-2"> placeholder="Untitled Project"
<button onclick={() => (showCreate = false)} class="px-4 py-2 text-sm rounded-lg bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10">Cancel</button> confirmLabel="Create"
<button onclick={create} disabled={creating} class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50">Create</button> onsubmit={create}
</div> onclose={() => (showCreate = false)}
</div> />
</div>
{/if} {/if}
{#if showRename} {#if showRename}
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={() => (showRename = false)} role="presentation"> <PromptModal
<form onsubmit={submitRename} class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md p-6" onclick={(e) => e.stopPropagation()}> title="Rename project"
<h2 class="text-lg font-bold mb-4 flex items-center gap-2"><Icon icon="mdi:pencil-outline" class="text-yellow-500" /> Rename Project</h2> label="Project name"
<input bind:value={renameName} class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-white/10 bg-transparent text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500/40" /> icon="ph:pencil-simple"
<div class="flex justify-end gap-2"> value={renameName}
<button type="button" onclick={() => (showRename = false)} class="px-4 py-2 text-sm rounded-lg bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10">Cancel</button> confirmLabel="Save"
<button type="submit" class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700">Save</button> onsubmit={submitRename}
</div> onclose={() => (showRename = false)}
</form> />
</div>
{/if} {/if}
{#if showInfo && infoProject} {#if showInfo && infoProject}
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={() => (showInfo = false)} role="presentation"> <Modal title={infoProject.name} icon="ph:folder-star" onclose={() => (showInfo = false)}>
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation"> <div class="flex flex-col gap-4 text-xs">
<div class="p-6 border-b border-gray-100 dark:border-white/10 flex items-center gap-3"> <div>
<Icon icon="mdi:folder-multiple-outline" class="text-xl text-blue-500" /> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Entrypoint</p>
<h3 class="text-lg font-semibold flex-grow truncate">{infoProject.name}</h3> <p class="font-mono text-sm text-[var(--color-ink)]">{infoProject.entrypoint}</p>
</div> </div>
<div class="p-6 space-y-4 text-sm"> <div>
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Entrypoint</p><p class="font-mono">{infoProject.entrypoint}</p></div> <p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Last Modified</p><p>{new Date(infoProject.updated_at.endsWith('Z') ? infoProject.updated_at : infoProject.updated_at + 'Z').toLocaleString()}</p></div> <p class="text-sm text-[var(--color-ink)]">{new Date(infoProject.updated_at.endsWith('Z') ? infoProject.updated_at : infoProject.updated_at + 'Z').toLocaleString()}</p>
</div>
<div class="p-4 bg-gray-50 dark:bg-white/5 border-t border-gray-100 dark:border-white/10 flex justify-end">
<button onclick={() => (showInfo = false)} class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg">Close</button>
</div> </div>
</div> </div>
</div>
{#snippet footer()}
<button
onclick={() => (showInfo = false)}
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
>
Close
</button>
{/snippet}
</Modal>
{/if}
{#if deleteTarget}
<ConfirmModal
title="Delete project"
message={`'${deleteTarget.name}' will be permanently deleted. This cannot be undone.`}
confirmLabel="Delete"
onconfirm={confirmDeleteProject}
onclose={() => (deleteTarget = null)}
/>
{/if} {/if}
+22 -26
View File
@@ -62,30 +62,26 @@
<meta name="description" content="Create a new TypstDrive account." /> <meta name="description" content="Create a new TypstDrive account." />
</svelte:head> </svelte:head>
<div class="min-h-screen flex flex-col relative overflow-hidden"> <div class="min-h-screen flex flex-col bg-[var(--color-surface-muted)]">
<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">
<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="max-w-md w-full space-y-8 rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] p-10 shadow-2xl">
<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="max-w-md w-full space-y-8 bg-white/80 dark:bg-black/40 backdrop-blur-xl p-10 rounded-2xl shadow-2xl border border-gray-200/50 dark:border-white/10 relative z-10">
<div class="text-center"> <div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100/50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-500 mb-6 mx-auto shadow-sm"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] mb-6 mx-auto shadow-sm">
<Icon icon="mdi:account-plus" class="text-3xl" /> <Icon icon="mdi:account-plus" class="text-3xl" />
</div> </div>
<h2 class="text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white"> <h2 class="text-3xl font-extrabold tracking-tight text-[var(--color-ink)]">
Create an account Create an account
</h2> </h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 font-medium"> <p class="mt-2 text-sm text-[var(--color-ink-muted)] font-medium">
Join TypstDrive to start collaborating Join TypstDrive to start collaborating
</p> </p>
</div> </div>
{#if registrationDisabled} {#if registrationDisabled}
<div class="flex flex-col items-center gap-4 py-4"> <div class="flex flex-col items-center gap-4 py-4">
<div class="flex items-center gap-3 w-full bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 p-4 rounded-xl border border-amber-200 dark:border-amber-500/20"> <div class="flex items-center gap-3 w-full bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 p-4 rounded-md border border-amber-200 dark:border-amber-500/20">
<Icon icon="mdi:lock-outline" class="text-2xl flex-shrink-0" /> <Icon icon="mdi:lock-outline" class="text-2xl flex-shrink-0" />
<div> <div>
<p class="font-semibold text-sm">Registration Disabled</p> <p class="font-semibold text-sm">Registration disabled</p>
<p class="text-xs mt-0.5 text-amber-600 dark:text-amber-500">New account creation has been disabled by the administrator. Please contact your administrator to get an account.</p> <p class="text-xs mt-0.5 text-amber-600 dark:text-amber-500">New account creation has been disabled by the administrator. Please contact your administrator to get an account.</p>
</div> </div>
</div> </div>
@@ -94,52 +90,52 @@
<form class="mt-8 space-y-6" onsubmit={register}> <form class="mt-8 space-y-6" onsubmit={register}>
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="username" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Username</label> <label for="username" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Username</label>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon icon="mdi:account" class="text-gray-400 dark:text-gray-500" /> <Icon icon="mdi:account" class="text-[var(--color-ink-muted)]" />
</div> </div>
<input id="username" name="username" type="text" required bind:value={username} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="Choose a username"> <input id="username" name="username" type="text" required bind:value={username} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="Choose a username">
</div> </div>
</div> </div>
<div> <div>
<label for="email" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Email Address</label> <label for="email" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Email address</label>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon icon="mdi:email" class="text-gray-400 dark:text-gray-500" /> <Icon icon="mdi:email" class="text-[var(--color-ink-muted)]" />
</div> </div>
<input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="[email protected]"> <input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="[email protected]">
</div> </div>
</div> </div>
<div> <div>
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Password</label> <label for="password" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Password</label>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon icon="mdi:lock" class="text-gray-400 dark:text-gray-500" /> <Icon icon="mdi:lock" class="text-[var(--color-ink-muted)]" />
</div> </div>
<input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="••••••••"> <input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="••••••••">
</div> </div>
</div> </div>
</div> </div>
{#if errorMsg} {#if errorMsg}
<div class="flex items-center gap-2 text-red-600 bg-red-50 dark:bg-red-500/10 dark:text-red-400 p-3 rounded-lg text-sm border border-red-200 dark:border-red-500/20 shadow-sm animate-in slide-in-from-top-1 fade-in duration-200"> <div class="flex items-center gap-2 text-[var(--color-danger)] bg-[var(--color-danger)]/10 p-3 rounded-md text-sm border border-[var(--color-danger)]/20">
<Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" /> <Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" />
<span class="font-medium">{errorMsg}</span> <span class="font-medium">{errorMsg}</span>
</div> </div>
{/if} {/if}
<div class="pt-2"> <div class="pt-2">
<button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent text-sm font-bold rounded-xl text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 focus:ring-blue-500 transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5"> <button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 text-sm font-bold rounded-md text-white bg-[var(--color-accent)] hover:opacity-90 focus:outline-none transition">
Register Register
<Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" /> <Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" />
</button> </button>
</div> </div>
</form> </form>
{/if} {/if}
<div class="text-sm text-center mt-6 pt-4 border-t border-gray-200 dark:border-white/10"> <div class="text-sm text-center mt-6 pt-4 border-t border-[var(--color-line)]">
<span class="text-gray-500 dark:text-gray-400">Already have an account? </span> <span class="text-[var(--color-ink-muted)]">Already have an account? </span>
<a href="/login" class="font-bold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors hover:underline"> <a href="/login" class="font-bold text-[var(--color-accent)] hover:underline transition-colors">
Sign in Sign in
</a> </a>
</div> </div>
+126 -122
View File
@@ -418,12 +418,12 @@
</svelte:head> </svelte:head>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<nav class="bg-[var(--theme-bg)] shadow-sm border-b border-gray-200 dark:border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0"> <nav class="bg-[var(--color-surface)] shadow-sm border-b border-[var(--color-line)] px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3"> <h1 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-3">
<Icon icon="mdi:cog" class="text-blue-600 dark:text-blue-400 text-3xl" /> <Icon icon="mdi:cog" class="text-[var(--color-accent)] text-3xl" />
Settings Settings
</h1> </h1>
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-4 py-2 rounded-lg flex items-center gap-2"> <button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-4 py-2 rounded-md flex items-center gap-2">
<Icon icon="mdi:arrow-left" class="text-lg" /> <Icon icon="mdi:arrow-left" class="text-lg" />
Back to Dashboard Back to Dashboard
</button> </button>
@@ -435,9 +435,9 @@
{#each navItems as item} {#each navItems as item}
<button <button
onclick={() => activeSection = item.id} onclick={() => activeSection = item.id}
class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 {activeSection === item.id class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium transition-colors {activeSection === item.id
? 'bg-blue-600 text-white shadow-sm' ? 'bg-[var(--color-accent)] text-white shadow-sm'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5'}" : 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
> >
<Icon icon={item.icon} class="text-lg flex-shrink-0" /> <Icon icon={item.icon} class="text-lg flex-shrink-0" />
{item.label} {item.label}
@@ -447,8 +447,8 @@
</button> </button>
{/each} {/each}
<div class="pt-4 mt-4 border-t border-gray-200 dark:border-white/10"> <div class="pt-4 mt-4 border-t border-[var(--color-line)]">
<button onclick={logout} class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all duration-150"> <button onclick={logout} class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 transition-colors">
<Icon icon="mdi:logout" class="text-lg flex-shrink-0" /> <Icon icon="mdi:logout" class="text-lg flex-shrink-0" />
Sign Out Sign Out
</button> </button>
@@ -459,20 +459,20 @@
<main class="flex-1 min-w-0 space-y-6 pb-16"> <main class="flex-1 min-w-0 space-y-6 pb-16">
{#if activeSection === 'account'} {#if activeSection === 'account'}
<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-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
<div class="p-6 sm:p-8"> <div class="p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
<Icon icon="mdi:account-outline" class="text-2xl text-blue-500 dark:text-blue-400" /> <Icon icon="mdi:account-outline" class="text-2xl text-[var(--color-accent)]" />
Account Settings Account Settings
</h2> </h2>
<div class="flex items-center gap-4 mb-6"> <div class="flex items-center gap-4 mb-6">
<div class="h-14 w-14 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 text-2xl font-bold border border-blue-500/20 flex-shrink-0"> <div class="h-14 w-14 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] text-2xl font-bold flex-shrink-0">
{$userStore?.username?.[0]?.toUpperCase() || '?'} {$userStore?.username?.[0]?.toUpperCase() || '?'}
</div> </div>
<div> <div>
<p class="text-base font-bold text-gray-900 dark:text-white">{$userStore?.username}</p> <p class="text-base font-bold text-[var(--color-ink)]">{$userStore?.username}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">{$userStore?.email}</p> <p class="text-sm text-[var(--color-ink-muted)]">{$userStore?.email}</p>
{#if $userStore?.is_admin} {#if $userStore?.is_admin}
<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 mt-1"> <span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 mt-1">
<Icon icon="mdi:shield-crown-outline" class="text-sm" /> <Icon icon="mdi:shield-crown-outline" class="text-sm" />
@@ -482,27 +482,27 @@
</div> </div>
</div> </div>
<div class="h-px bg-gray-200 dark:bg-white/10 mb-6"></div> <div class="h-px bg-[var(--color-line)] mb-6"></div>
<h3 class="text-sm font-bold text-gray-900 dark:text-white mb-4">Profile</h3> <h3 class="text-sm font-bold text-[var(--color-ink)] mb-4">Profile</h3>
{#if profileError} {#if profileError}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{profileError}</div> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{profileError}</div>
{/if} {/if}
{#if profileSuccess} {#if profileSuccess}
<div class="bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 p-3 rounded-lg text-sm mb-4">Profile updated successfully.</div> <div class="bg-[var(--color-success)]/10 text-[var(--color-success)] p-3 rounded-md text-sm mb-4">Profile updated successfully.</div>
{/if} {/if}
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label for="username-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label> <label for="username-input" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Username</label>
<input id="username-input" type="text" bind:value={username} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" /> <input id="username-input" type="text" bind:value={username} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
</div> </div>
<div> <div>
<label for="email-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email Address</label> <label for="email-input" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Email address</label>
<input id="email-input" type="email" bind:value={email} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" /> <input id="email-input" type="email" bind:value={email} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
</div> </div>
<button onclick={saveProfile} disabled={isSaving || (username === $userStore?.username && email === $userStore?.email)} class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-semibold transition-colors disabled:opacity-50 flex items-center gap-2 shadow-sm"> <button onclick={saveProfile} disabled={isSaving || (username === $userStore?.username && email === $userStore?.email)} class="bg-[var(--color-accent)] hover:opacity-90 text-white px-5 py-2 rounded-md text-sm font-semibold transition disabled:opacity-50 flex items-center gap-2 shadow-sm">
{#if isSaving} {#if isSaving}
<Icon icon="mdi:loading" class="animate-spin text-lg" /> <Icon icon="mdi:loading" class="animate-spin text-lg" />
Saving... Saving...
@@ -513,31 +513,31 @@
</button> </button>
</div> </div>
<div class="h-px bg-gray-200 dark:bg-white/10 my-6"></div> <div class="h-px bg-[var(--color-line)] my-6"></div>
<h3 class="text-sm font-bold text-gray-900 dark:text-white mb-4">Change Password</h3> <h3 class="text-sm font-bold text-[var(--color-ink)] mb-4">Change Password</h3>
{#if passwordError} {#if passwordError}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{passwordError}</div> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{passwordError}</div>
{/if} {/if}
{#if passwordSuccess} {#if passwordSuccess}
<div class="bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 p-3 rounded-lg text-sm mb-4">Password changed successfully.</div> <div class="bg-[var(--color-success)]/10 text-[var(--color-success)] p-3 rounded-md text-sm mb-4">Password changed successfully.</div>
{/if} {/if}
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label for="current-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Current Password</label> <label for="current-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Current password</label>
<input id="current-password" type="password" bind:value={currentPassword} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" /> <input id="current-password" type="password" bind:value={currentPassword} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
</div> </div>
<div> <div>
<label for="new-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">New Password</label> <label for="new-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">New password</label>
<input id="new-password" type="password" bind:value={newPassword} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" /> <input id="new-password" type="password" bind:value={newPassword} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
</div> </div>
<div> <div>
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm New Password</label> <label for="confirm-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Confirm new password</label>
<input id="confirm-password" type="password" bind:value={confirmPassword} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" /> <input id="confirm-password" type="password" bind:value={confirmPassword} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
</div> </div>
<button onclick={changePassword} disabled={isSavingPassword || !currentPassword || !newPassword || !confirmPassword} class="bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-white/10 dark:hover:bg-white/20 dark:text-white px-5 py-2 rounded-lg text-sm font-semibold transition-colors disabled:opacity-50 flex items-center gap-2"> <button onclick={changePassword} disabled={isSavingPassword || !currentPassword || !newPassword || !confirmPassword} class="bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink)] px-5 py-2 rounded-md text-sm font-semibold transition disabled:opacity-50 flex items-center gap-2">
{#if isSavingPassword} {#if isSavingPassword}
<Icon icon="mdi:loading" class="animate-spin text-lg" /> <Icon icon="mdi:loading" class="animate-spin text-lg" />
Updating... Updating...
@@ -552,46 +552,46 @@
{/if} {/if}
{#if activeSection === 'theme'} {#if activeSection === 'theme'}
<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-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
<div class="p-6 sm:p-8"> <div class="p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-2 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-2 flex items-center gap-2">
<Icon icon="mdi:palette-outline" class="text-2xl text-blue-500 dark:text-blue-400" /> <Icon icon="mdi:palette-outline" class="text-2xl text-[var(--color-accent)]" />
Theme Settings Theme Settings
</h2> </h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">Customize the appearance of your editor and dashboard. These settings are saved to your browser.</p> <p class="text-sm text-[var(--color-ink-muted)] mb-6">Customize the appearance of your editor and dashboard. These settings are saved to your browser.</p>
<ThemePicker /> <ThemePicker />
</div> </div>
</div> </div>
{/if} {/if}
{#if activeSection === 'storage'} {#if activeSection === 'storage'}
<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-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
<div class="p-6 sm:p-8"> <div class="p-6 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
<Icon icon="mdi:harddisk" class="text-2xl text-blue-500 dark:text-blue-400" /> <Icon icon="mdi:harddisk" class="text-2xl text-[var(--color-accent)]" />
Storage Storage
</h2> </h2>
<div class="mb-4 flex justify-between items-end"> <div class="mb-4 flex justify-between items-end">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Total Space Used</span> <span class="text-sm font-medium text-[var(--color-ink-muted)]">Total space used</span>
<span class="text-sm font-bold text-gray-900 dark:text-white"> <span class="text-sm font-bold text-[var(--color-ink)]">
{storageStats ? formatBytes(storageStats.total_size_bytes) : 'Loading...'} {storageStats ? formatBytes(storageStats.total_size_bytes) : 'Loading...'}
</span> </span>
</div> </div>
<div class="grid grid-cols-2 gap-4 text-sm"> <div class="grid grid-cols-2 gap-4 text-sm">
<div class="bg-gray-50 dark:bg-black/30 p-4 rounded-xl border border-gray-200 dark:border-white/10 flex items-center gap-3"> <div class="bg-[var(--color-surface-muted)] p-4 rounded-lg border border-[var(--color-line)] flex items-center gap-3">
<div class="w-3 h-3 rounded-full bg-blue-500 flex-shrink-0"></div> <div class="w-3 h-3 rounded-full bg-[var(--color-accent)] flex-shrink-0"></div>
<div> <div>
<p class="text-gray-500 dark:text-gray-400 text-xs">Documents</p> <p class="text-[var(--color-ink-muted)] text-xs">Documents</p>
<p class="font-semibold text-gray-900 dark:text-white"> <p class="font-semibold text-[var(--color-ink)]">
{storageStats ? formatBytes(storageStats.documents_size_bytes) : '...'} {storageStats ? formatBytes(storageStats.documents_size_bytes) : '...'}
</p> </p>
</div> </div>
</div> </div>
<div class="bg-gray-50 dark:bg-black/30 p-4 rounded-xl border border-gray-200 dark:border-white/10 flex items-center gap-3"> <div class="bg-[var(--color-surface-muted)] p-4 rounded-lg border border-[var(--color-line)] flex items-center gap-3">
<div class="w-3 h-3 rounded-full bg-purple-500 flex-shrink-0"></div> <div class="w-3 h-3 rounded-full bg-purple-500 flex-shrink-0"></div>
<div> <div>
<p class="text-gray-500 dark:text-gray-400 text-xs">Images & Assets</p> <p class="text-[var(--color-ink-muted)] text-xs">Images & assets</p>
<p class="font-semibold text-gray-900 dark:text-white"> <p class="font-semibold text-[var(--color-ink)]">
{storageStats ? formatBytes(storageStats.files_size_bytes) : '...'} {storageStats ? formatBytes(storageStats.files_size_bytes) : '...'}
</p> </p>
</div> </div>
@@ -602,33 +602,33 @@
{/if} {/if}
{#if activeSection === 'api-keys'} {#if activeSection === 'api-keys'}
<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-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
<div class="p-6 sm:p-8"> <div class="p-6 sm:p-8">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:key-outline" class="text-2xl text-blue-500 dark:text-blue-400" /> <Icon icon="mdi:key-outline" class="text-2xl text-[var(--color-accent)]" />
API Keys API Keys
</h2> </h2>
<button <button
onclick={() => { showCreateKeyForm = !showCreateKeyForm; createKeyError = ''; newlyCreatedKey = null; }} onclick={() => { showCreateKeyForm = !showCreateKeyForm; createKeyError = ''; newlyCreatedKey = null; }}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-lg transition-colors {showCreateKeyForm ? 'bg-gray-200 dark:bg-white/10 text-gray-700 dark:text-gray-300' : 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm'}" class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-md transition-colors {showCreateKeyForm ? 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]' : 'bg-[var(--color-accent)] hover:opacity-90 text-white shadow-sm'}"
> >
<Icon icon={showCreateKeyForm ? 'mdi:close' : 'mdi:plus'} class="text-base" /> <Icon icon={showCreateKeyForm ? 'mdi:close' : 'mdi:plus'} class="text-base" />
{showCreateKeyForm ? 'Cancel' : 'New Key'} {showCreateKeyForm ? 'Cancel' : 'New Key'}
</button> </button>
</div> </div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4"> <p class="text-sm text-[var(--color-ink-muted)] mb-4">
Use API keys to render Typst documents programmatically via <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">POST /v1/render</code>. Use API keys to render Typst documents programmatically via <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">POST /v1/render</code>.
Each key allows up to 60 requests/minute. Each key allows up to 60 requests/minute.
<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-[var(--color-accent)] hover:underline ml-1">View API docs →</a>
</p> </p>
<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-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)]">
<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-[var(--color-ink-muted)]">
Requests — {usagePeriod === '1hr' ? 'Last 60 Min' : usagePeriod === '1day' ? 'Last 24 Hours' : 'Last 7 Days'} Requests — {usagePeriod === '1hr' ? 'Last 60 Min' : usagePeriod === '1day' ? 'Last 24 Hours' : 'Last 7 Days'}
{#if usageData.length > 0} {#if usageData.length > 0}
<span class="ml-2 normal-case font-normal text-gray-400 dark:text-gray-500"> <span class="ml-2 normal-case font-normal text-[var(--color-ink-muted)]">
({usageData.reduce((s, p) => s + p.count, 0)} total) ({usageData.reduce((s, p) => s + p.count, 0)} total)
</span> </span>
{/if} {/if}
@@ -637,18 +637,18 @@
{#each ([['1hr', '1 hr'], ['1day', '1 day'], ['1week', '1 week']] as const) as [val, label]} {#each ([['1hr', '1 hr'], ['1day', '1 day'], ['1week', '1 week']] as const) as [val, label]}
<button <button
onclick={() => usagePeriod = val} onclick={() => usagePeriod = val}
class="px-2 py-0.5 text-xs font-semibold rounded-md transition-colors {usagePeriod === val ? 'bg-blue-600 text-white' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-white/10'}" class="px-2 py-0.5 text-xs font-semibold rounded-md transition-colors {usagePeriod === val ? 'bg-[var(--color-accent)] text-white' : 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]'}"
>{label}</button> >{label}</button>
{/each} {/each}
</div> </div>
</div> </div>
<div class="h-32"> <div class="h-32">
{#if usageLoading} {#if usageLoading}
<div class="h-full flex items-center justify-center text-gray-400 dark:text-gray-500 text-sm"> <div class="h-full flex items-center justify-center text-[var(--color-ink-muted)] text-sm">
<Icon icon="mdi:loading" class="animate-spin mr-2" /> Loading... <Icon icon="mdi:loading" class="animate-spin mr-2" /> Loading...
</div> </div>
{:else if usageData.length === 0} {:else if usageData.length === 0}
<div class="h-full flex items-center justify-center text-gray-400 dark:text-gray-500 text-sm"> <div class="h-full flex items-center justify-center text-[var(--color-ink-muted)] text-sm">
No usage yet — make your first API call to see data here. No usage yet — make your first API call to see data here.
</div> </div>
{:else} {:else}
@@ -658,24 +658,24 @@
</div> </div>
{#if newlyCreatedKey} {#if newlyCreatedKey}
<div class="mb-6 p-4 rounded-xl border border-green-200 dark:border-green-700/50 bg-green-50 dark:bg-green-900/10"> <div class="mb-6 p-4 rounded-lg border border-[var(--color-success)]/30 bg-[var(--color-success)]/10">
<div class="flex items-start justify-between gap-4 mb-2"> <div class="flex items-start justify-between gap-4 mb-2">
<div> <div>
<p class="text-sm font-bold text-green-800 dark:text-green-300 flex items-center gap-2"> <p class="text-sm font-bold text-[var(--color-success)] flex items-center gap-2">
<Icon icon="mdi:check-circle" class="text-lg" /> <Icon icon="mdi:check-circle" class="text-lg" />
Key created: {newlyCreatedKey.name} Key created: {newlyCreatedKey.name}
</p> </p>
<p class="text-xs text-green-700 dark:text-green-400 mt-0.5">Copy this key now — it will not be shown again.</p> <p class="text-xs text-[var(--color-success)] mt-0.5 opacity-90">Copy this key now — it will not be shown again.</p>
</div> </div>
<button onclick={() => newlyCreatedKey = null} class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200 flex-shrink-0"> <button onclick={() => newlyCreatedKey = null} class="text-[var(--color-success)] hover:opacity-70 flex-shrink-0">
<Icon icon="mdi:close" class="text-lg" /> <Icon icon="mdi:close" class="text-lg" />
</button> </button>
</div> </div>
<div class="flex items-center gap-2 mt-3"> <div class="flex items-center gap-2 mt-3">
<code class="flex-1 font-mono text-xs bg-white dark:bg-black/40 border border-green-200 dark:border-green-700/50 text-gray-800 dark:text-gray-200 px-3 py-2 rounded-lg break-all">{newlyCreatedKey.key}</code> <code class="flex-1 font-mono text-xs bg-[var(--color-surface)] border border-[var(--color-success)]/30 text-[var(--color-ink)] px-3 py-2 rounded-md break-all">{newlyCreatedKey.key}</code>
<button <button
onclick={() => copyKey(newlyCreatedKey!.key)} onclick={() => copyKey(newlyCreatedKey!.key)}
class="flex-shrink-0 flex items-center gap-1.5 px-3 py-2 text-sm font-semibold rounded-lg transition-colors {copiedKey ? 'bg-green-600 text-white' : 'bg-gray-200 dark:bg-white/10 hover:bg-gray-300 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300'}" class="flex-shrink-0 flex items-center gap-1.5 px-3 py-2 text-sm font-semibold rounded-md transition-colors {copiedKey ? 'bg-[var(--color-success)] text-white' : 'bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)]'}"
> >
<Icon icon={copiedKey ? 'mdi:check' : 'mdi:content-copy'} class="text-base" /> <Icon icon={copiedKey ? 'mdi:check' : 'mdi:content-copy'} class="text-base" />
{copiedKey ? 'Copied!' : 'Copy'} {copiedKey ? 'Copied!' : 'Copy'}
@@ -685,29 +685,30 @@
{/if} {/if}
{#if showCreateKeyForm} {#if showCreateKeyForm}
<form onsubmit={createApiKey} class="mb-6 p-4 rounded-xl border border-blue-200 dark:border-blue-800/50 bg-blue-50/50 dark:bg-blue-900/10 space-y-3"> <form onsubmit={createApiKey} class="mb-6 p-4 rounded-lg border border-[var(--color-accent)]/30 bg-[var(--color-accent-soft)] space-y-3">
<h3 class="text-sm font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h3 class="text-sm font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:key-plus" class="text-blue-500" /> <Icon icon="mdi:key-plus" class="text-[var(--color-accent)]" />
Create API Key Create API Key
</h3> </h3>
{#if createKeyError} {#if createKeyError}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 px-3 py-2 rounded-lg text-sm">{createKeyError}</div> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] px-3 py-2 rounded-md text-sm">{createKeyError}</div>
{/if} {/if}
<div class="flex items-end gap-3"> <div class="flex items-end gap-3">
<div class="flex-1"> <div class="flex-1">
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Key Name</label> <label for="create-key-name" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Key name</label>
<input <input
id="create-key-name"
type="text" type="text"
required required
bind:value={createKeyName} bind:value={createKeyName}
placeholder="e.g. My App, CI Pipeline" placeholder="e.g. My App, CI Pipeline"
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={createKeyLoading || !createKeyName.trim()} disabled={createKeyLoading || !createKeyName.trim()}
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm" class="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent)] hover:opacity-90 disabled:opacity-50 text-white text-sm font-semibold rounded-md transition shadow-sm"
> >
{#if createKeyLoading} {#if createKeyLoading}
<Icon icon="mdi:loading" class="animate-spin text-base" /> <Icon icon="mdi:loading" class="animate-spin text-base" />
@@ -722,38 +723,38 @@
{/if} {/if}
{#if apiKeysError} {#if apiKeysError}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{apiKeysError}</div> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{apiKeysError}</div>
{/if} {/if}
{#if apiKeysLoading} {#if apiKeysLoading}
<div class="flex items-center justify-center py-12 text-gray-400 dark:text-gray-500"> <div class="flex items-center justify-center py-12 text-[var(--color-ink-muted)]">
<Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" /> <Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" />
Loading keys... Loading keys...
</div> </div>
{:else if apiKeys.length === 0} {:else if apiKeys.length === 0}
<div class="text-center py-12 text-gray-400 dark:text-gray-500"> <div class="text-center py-12 text-[var(--color-ink-muted)]">
<Icon icon="mdi:key-outline" class="text-4xl mb-2 opacity-40" /> <Icon icon="mdi:key-outline" class="text-4xl mb-2 opacity-40" />
<p class="text-sm">No API keys yet. Create one to get started.</p> <p class="text-sm">No API keys yet. Create one to get started.</p>
</div> </div>
{:else} {:else}
<div class="space-y-2"> <div class="space-y-2">
{#each apiKeys as key (key.id)} {#each apiKeys as key (key.id)}
<div class="flex items-center gap-4 px-4 py-3 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/20"> <div class="flex items-center gap-4 px-4 py-3 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)]">
<div class="h-9 w-9 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 flex-shrink-0"> <div class="h-9 w-9 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] flex-shrink-0">
<Icon icon="mdi:key" class="text-lg" /> <Icon icon="mdi:key" class="text-lg" />
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{key.name}</p> <p class="text-sm font-semibold text-[var(--color-ink)] truncate">{key.name}</p>
<p class="text-xs font-mono text-gray-500 dark:text-gray-400">{key.key_prefix}... · {key.rate_limit}/min</p> <p class="text-xs font-mono text-[var(--color-ink-muted)]">{key.key_prefix}... · {key.rate_limit}/min</p>
</div> </div>
<div class="text-right flex-shrink-0 hidden sm:block"> <div class="text-right flex-shrink-0 hidden sm:block">
<p class="text-xs text-gray-400 dark:text-gray-500">Created {formatDate(key.created_at)}</p> <p class="text-xs text-[var(--color-ink-muted)]">Created {formatDate(key.created_at)}</p>
<p class="text-xs text-gray-400 dark:text-gray-500">{key.last_used_at ? `Last used ${formatDate(key.last_used_at)}` : 'Never used'}</p> <p class="text-xs text-[var(--color-ink-muted)]">{key.last_used_at ? `Last used ${formatDate(key.last_used_at)}` : 'Never used'}</p>
</div> </div>
<div class="flex items-center gap-1 flex-shrink-0"> <div class="flex items-center gap-1 flex-shrink-0">
{#if confirmRegenerateId === key.id} {#if confirmRegenerateId === key.id}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="text-xs text-gray-500 dark:text-gray-400">Regenerate?</span> <span class="text-xs text-[var(--color-ink-muted)]">Regenerate?</span>
<button <button
onclick={() => regenerateApiKey(key.id)} onclick={() => regenerateApiKey(key.id)}
disabled={regeneratingKeyId === key.id} disabled={regeneratingKeyId === key.id}
@@ -763,24 +764,24 @@
</button> </button>
<button <button
onclick={() => confirmRegenerateId = null} onclick={() => confirmRegenerateId = null}
class="text-xs px-2 py-1 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-white/10 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300 font-semibold transition-colors" class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)] font-semibold transition-colors"
> >
No No
</button> </button>
</div> </div>
{:else if confirmDeleteKeyId === key.id} {:else if confirmDeleteKeyId === key.id}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="text-xs text-gray-500 dark:text-gray-400">Delete?</span> <span class="text-xs text-[var(--color-ink-muted)]">Delete?</span>
<button <button
onclick={() => deleteApiKey(key.id)} onclick={() => deleteApiKey(key.id)}
disabled={deletingKeyId === key.id} disabled={deletingKeyId === key.id}
class="text-xs px-2 py-1 rounded-md bg-red-600 hover:bg-red-700 text-white font-semibold transition-colors disabled:opacity-50" class="text-xs px-2 py-1 rounded-md bg-[var(--color-danger)] hover:opacity-90 text-white font-semibold transition-colors disabled:opacity-50"
> >
{deletingKeyId === key.id ? '...' : 'Yes'} {deletingKeyId === key.id ? '...' : 'Yes'}
</button> </button>
<button <button
onclick={() => confirmDeleteKeyId = null} onclick={() => confirmDeleteKeyId = null}
class="text-xs px-2 py-1 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-white/10 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300 font-semibold transition-colors" class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)] font-semibold transition-colors"
> >
No No
</button> </button>
@@ -789,14 +790,14 @@
<button <button
onclick={() => { confirmRegenerateId = key.id; confirmDeleteKeyId = null; }} onclick={() => { confirmRegenerateId = key.id; confirmDeleteKeyId = null; }}
title="Regenerate key" title="Regenerate key"
class="p-1.5 rounded-lg text-gray-400 hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 transition-colors" class="p-1.5 rounded-md text-[var(--color-ink-muted)] hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 transition-colors"
> >
<Icon icon="mdi:refresh" class="text-lg" /> <Icon icon="mdi:refresh" class="text-lg" />
</button> </button>
<button <button
onclick={() => { confirmDeleteKeyId = key.id; confirmRegenerateId = null; }} onclick={() => { confirmDeleteKeyId = key.id; confirmRegenerateId = null; }}
title="Revoke key" title="Revoke key"
class="p-1.5 rounded-lg text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" class="p-1.5 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 transition-colors"
> >
<Icon icon="mdi:delete-outline" class="text-lg" /> <Icon icon="mdi:delete-outline" class="text-lg" />
</button> </button>
@@ -811,18 +812,18 @@
{/if} {/if}
{#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-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
<div class="p-6 sm:p-8"> <div class="p-6 sm:p-8">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2"> <h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:shield-crown-outline" class="text-2xl text-amber-500 dark:text-amber-400" /> <Icon icon="mdi:shield-crown-outline" class="text-2xl text-amber-500 dark:text-amber-400" />
User Management User Management
</h2> </h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-sm text-gray-500 dark:text-gray-400">{adminUsers.length} user{adminUsers.length !== 1 ? 's' : ''}</span> <span class="text-sm text-[var(--color-ink-muted)]">{adminUsers.length} user{adminUsers.length !== 1 ? 's' : ''}</span>
<button <button
onclick={() => { showCreateForm = !showCreateForm; createError = ''; }} onclick={() => { showCreateForm = !showCreateForm; createError = ''; }}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-lg transition-colors {showCreateForm ? 'bg-gray-200 dark:bg-white/10 text-gray-700 dark:text-gray-300' : 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm'}" class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-md transition-colors {showCreateForm ? 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]' : 'bg-[var(--color-accent)] hover:opacity-90 text-white shadow-sm'}"
> >
<Icon icon={showCreateForm ? 'mdi:close' : 'mdi:account-plus-outline'} class="text-base" /> <Icon icon={showCreateForm ? 'mdi:close' : 'mdi:account-plus-outline'} class="text-base" />
{showCreateForm ? 'Cancel' : 'New User'} {showCreateForm ? 'Cancel' : 'New User'}
@@ -831,52 +832,55 @@
</div> </div>
{#if showCreateForm} {#if showCreateForm}
<form onsubmit={createUser} class="mb-6 p-4 rounded-xl border border-blue-200 dark:border-blue-800/50 bg-blue-50/50 dark:bg-blue-900/10 space-y-3"> <form onsubmit={createUser} class="mb-6 p-4 rounded-lg border border-[var(--color-accent)]/30 bg-[var(--color-accent-soft)] space-y-3">
<h3 class="text-sm font-bold text-gray-900 dark:text-white mb-3 flex items-center gap-2"> <h3 class="text-sm font-bold text-[var(--color-ink)] mb-3 flex items-center gap-2">
<Icon icon="mdi:account-plus-outline" class="text-blue-500" /> <Icon icon="mdi:account-plus-outline" class="text-[var(--color-accent)]" />
Create New User Create New User
</h3> </h3>
{#if createError} {#if createError}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 px-3 py-2 rounded-lg text-sm">{createError}</div> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] px-3 py-2 rounded-md text-sm">{createError}</div>
{/if} {/if}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label> <label for="create-username" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Username</label>
<input <input
id="create-username"
type="text" type="text"
required required
bind:value={createUsername} bind:value={createUsername}
placeholder="username" placeholder="username"
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors"
/> />
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label> <label for="create-email" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Email</label>
<input <input
id="create-email"
type="email" type="email"
required required
bind:value={createEmail} bind:value={createEmail}
placeholder="[email protected]" placeholder="[email protected]"
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors"
/> />
</div> </div>
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Temporary Password</label> <label for="create-password" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Temporary password</label>
<input <input
id="create-password"
type="text" type="text"
required required
bind:value={createPassword} bind:value={createPassword}
placeholder="Set a password the user can change later" placeholder="Set a password the user can change later"
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors font-mono" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors font-mono"
/> />
</div> </div>
<div class="flex items-center justify-between pt-1"> <div class="flex items-center justify-between pt-1">
<label class="flex items-center gap-2 cursor-pointer select-none"> <label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" bind:checked={createIsAdmin} class="w-4 h-4 rounded accent-amber-500" /> <input type="checkbox" bind:checked={createIsAdmin} class="w-4 h-4 rounded accent-amber-500" />
<span class="text-sm text-gray-700 dark:text-gray-300 flex items-center gap-1"> <span class="text-sm text-[var(--color-ink-muted)] flex items-center gap-1">
<Icon icon="mdi:shield-crown-outline" class="text-amber-500 text-base" /> <Icon icon="mdi:shield-crown-outline" class="text-amber-500 text-base" />
Grant admin privileges Grant admin privileges
</span> </span>
@@ -884,7 +888,7 @@
<button <button
type="submit" type="submit"
disabled={createLoading} disabled={createLoading}
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm" class="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent)] hover:opacity-90 disabled:opacity-50 text-white text-sm font-semibold rounded-md transition shadow-sm"
> >
{#if createLoading} {#if createLoading}
<Icon icon="mdi:loading" class="animate-spin text-base" /> <Icon icon="mdi:loading" class="animate-spin text-base" />
@@ -899,56 +903,56 @@
{/if} {/if}
{#if adminError} {#if adminError}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{adminError}</div> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{adminError}</div>
{/if} {/if}
{#if adminLoading} {#if adminLoading}
<div class="flex items-center justify-center py-12 text-gray-400 dark:text-gray-500"> <div class="flex items-center justify-center py-12 text-[var(--color-ink-muted)]">
<Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" /> <Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" />
Loading users... Loading users...
</div> </div>
{:else} {:else}
<div class="space-y-2"> <div class="space-y-2">
{#each adminUsers as user (user.id)} {#each adminUsers as user (user.id)}
<div class="flex items-center gap-4 px-4 py-3 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/20 group"> <div class="flex items-center gap-4 px-4 py-3 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)] group">
<div class="h-9 w-9 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-sm flex-shrink-0"> <div class="h-9 w-9 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] font-bold text-sm flex-shrink-0">
{user.username[0].toUpperCase()} {user.username[0].toUpperCase()}
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{user.username}</p> <p class="text-sm font-semibold text-[var(--color-ink)] truncate">{user.username}</p>
{#if user.is_admin} {#if user.is_admin}
<span class="text-xs font-bold px-1.5 py-0.5 rounded-md bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 flex-shrink-0">Admin</span> <span class="text-xs font-bold px-1.5 py-0.5 rounded-md bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 flex-shrink-0">Admin</span>
{/if} {/if}
{#if user.id === $userStore?.id} {#if user.id === $userStore?.id}
<span class="text-xs px-1.5 py-0.5 rounded-md bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 flex-shrink-0">You</span> <span class="text-xs px-1.5 py-0.5 rounded-md bg-[var(--color-accent-soft)] text-[var(--color-accent)] flex-shrink-0">You</span>
{/if} {/if}
</div> </div>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">{user.email}</p> <p class="text-xs text-[var(--color-ink-muted)] truncate">{user.email}</p>
</div> </div>
<p class="text-xs text-gray-400 dark:text-gray-500 flex-shrink-0 hidden sm:block">{formatDate(user.created_at)}</p> <p class="text-xs text-[var(--color-ink-muted)] flex-shrink-0 hidden sm:block">{formatDate(user.created_at)}</p>
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-2 flex-shrink-0">
{#if user.id !== $userStore?.id} {#if user.id !== $userStore?.id}
<button <button
onclick={() => toggleAdmin(user)} onclick={() => toggleAdmin(user)}
title={user.is_admin ? 'Revoke admin' : 'Grant admin'} title={user.is_admin ? 'Revoke admin' : 'Grant admin'}
class="p-1.5 rounded-lg transition-colors {user.is_admin ? 'text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10' : 'text-gray-400 hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10'}" class="p-1.5 rounded-md transition-colors {user.is_admin ? 'text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10' : 'text-[var(--color-ink-muted)] hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10'}"
> >
<Icon icon={user.is_admin ? 'mdi:shield-crown' : 'mdi:shield-crown-outline'} class="text-lg" /> <Icon icon={user.is_admin ? 'mdi:shield-crown' : 'mdi:shield-crown-outline'} class="text-lg" />
</button> </button>
{#if confirmDeleteId === user.id} {#if confirmDeleteId === user.id}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="text-xs text-gray-500 dark:text-gray-400">Delete?</span> <span class="text-xs text-[var(--color-ink-muted)]">Delete?</span>
<button <button
onclick={() => deleteUser(user.id)} onclick={() => deleteUser(user.id)}
disabled={deletingUserId === user.id} disabled={deletingUserId === user.id}
class="text-xs px-2 py-1 rounded-md bg-red-600 hover:bg-red-700 text-white font-semibold transition-colors disabled:opacity-50" class="text-xs px-2 py-1 rounded-md bg-[var(--color-danger)] hover:opacity-90 text-white font-semibold transition-colors disabled:opacity-50"
> >
{deletingUserId === user.id ? '...' : 'Yes'} {deletingUserId === user.id ? '...' : 'Yes'}
</button> </button>
<button <button
onclick={() => confirmDeleteId = null} onclick={() => confirmDeleteId = null}
class="text-xs px-2 py-1 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-white/10 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300 font-semibold transition-colors" class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)] font-semibold transition-colors"
> >
No No
</button> </button>
@@ -957,7 +961,7 @@
<button <button
onclick={() => confirmDeleteId = user.id} onclick={() => confirmDeleteId = user.id}
title="Delete user" title="Delete user"
class="p-1.5 rounded-lg text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" class="p-1.5 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 transition-colors"
> >
<Icon icon="mdi:delete-outline" class="text-lg" /> <Icon icon="mdi:delete-outline" class="text-lg" />
</button> </button>
+18 -18
View File
@@ -51,84 +51,84 @@
<title>Setup - TypstDrive</title> <title>Setup - TypstDrive</title>
</svelte:head> </svelte:head>
<div class="min-h-screen flex items-center justify-center px-4 py-16"> <div class="min-h-screen flex items-center justify-center px-4 py-16 bg-[var(--color-surface-muted)]">
<div class="w-full max-w-md"> <div class="w-full max-w-md">
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-blue-600 text-white mb-4 shadow-lg"> <div class="inline-flex items-center justify-center w-24 h-24 rounded-2xl bg-[var(--color-accent)] text-white mb-4 shadow-lg">
<Icon icon="mdi:shield-crown-outline" class="text-3xl" /> <img src="/favicon.png" alt="TypstDrive" class="h-14 w-14" />
</div> </div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Welcome to TypstDrive</h1> <h1 class="text-3xl font-bold text-[var(--color-ink)]">Welcome to TypstDrive</h1>
<p class="mt-2 text-gray-500 dark:text-gray-400">Create your admin account to get started.</p> <p class="mt-2 text-[var(--color-ink-muted)]">Create your admin account to get started.</p>
</div> </div>
<div class="bg-white dark:bg-black/20 rounded-2xl shadow-xl border border-gray-200 dark:border-white/10 p-8"> <div class="rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] p-8 shadow-xl">
<div class="flex items-center gap-2 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 text-sm px-4 py-3 rounded-lg mb-6 border border-blue-200 dark:border-blue-800/50"> <div class="flex items-center gap-2 bg-[var(--color-accent-soft)] text-[var(--color-accent)] text-sm px-4 py-3 rounded-md mb-6">
<Icon icon="mdi:information-outline" class="text-lg flex-shrink-0" /> <Icon icon="mdi:information-outline" class="text-lg flex-shrink-0" />
<span>This is a one-time setup. The account you create here will have full admin privileges.</span> <span>This is a one-time setup. The account you create here will have full admin privileges.</span>
</div> </div>
{#if errorMsg} {#if errorMsg}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm mb-5 border border-red-200 dark:border-red-800/50"> <div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] px-4 py-3 rounded-md text-sm mb-5 border border-[var(--color-danger)]/20">
{errorMsg} {errorMsg}
</div> </div>
{/if} {/if}
<form onsubmit={handleSetup} class="space-y-4"> <form onsubmit={handleSetup} class="space-y-4">
<div> <div>
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label> <label for="username" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Username</label>
<input <input
id="username" id="username"
type="text" type="text"
required required
bind:value={username} bind:value={username}
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
placeholder="admin" placeholder="admin"
/> />
</div> </div>
<div> <div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label> <label for="email" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Email</label>
<input <input
id="email" id="email"
type="email" type="email"
required required
bind:value={email} bind:value={email}
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
placeholder="[email protected]" placeholder="[email protected]"
/> />
</div> </div>
<div> <div>
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label> <label for="password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Password</label>
<input <input
id="password" id="password"
type="password" type="password"
required required
bind:value={password} bind:value={password}
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
placeholder="Min. 8 characters" placeholder="Min. 8 characters"
/> />
</div> </div>
<div> <div>
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label> <label for="confirm-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Confirm password</label>
<input <input
id="confirm-password" id="confirm-password"
type="password" type="password"
required required
bind:value={confirmPassword} bind:value={confirmPassword}
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
placeholder="Repeat password" placeholder="Repeat password"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
class="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white font-semibold py-2.5 rounded-lg transition-colors mt-2 shadow-sm" class="w-full flex items-center justify-center gap-2 bg-[var(--color-accent)] hover:opacity-90 disabled:opacity-60 text-white font-semibold py-2.5 rounded-md transition mt-2"
> >
{#if loading} {#if loading}
<Icon icon="mdi:loading" class="animate-spin text-lg" /> <Icon icon="mdi:loading" class="animate-spin text-lg" />
Creating account... Creating account...
{:else} {:else}
<Icon icon="mdi:shield-check-outline" class="text-lg" /> <Icon icon="mdi:shield-check-outline" class="text-lg" />
Create Admin Account Create admin account
{/if} {/if}
</button> </button>
</form> </form>
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB