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
+7 -7
View File
@@ -102,7 +102,7 @@
<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>
</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" />
</button>
</div>
@@ -113,7 +113,7 @@
<Icon icon="mdi:loading" class="animate-spin text-2xl" />
</div>
{: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}
</div>
{: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="flex justify-between items-start">
<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()}
</div>
<div>
@@ -137,11 +137,11 @@
<div class="flex opacity-0 group-hover:opacity-100 transition-opacity gap-1">
{#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" />
</button>
{/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" />
</button>
</div>
@@ -157,7 +157,7 @@
<textarea
bind:value={newCommentContent}
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) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -168,7 +168,7 @@
<button
onclick={postComment}
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)"
>
<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="flex items-center gap-4">
<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...'}
</div>
{#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>
{$documentStatsStore.words} words
</button>
@@ -29,11 +29,11 @@
{#if showStatsModal}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- 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="px-4 py-3 border-b border-[var(--theme-border)] flex items-center justify-between">
<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>
</button>
</div>
+6 -6
View File
@@ -3,13 +3,13 @@
let { sticky = true }: { sticky?: boolean } = $props();
</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">
<a
href="https://github.com/SirBlobby/TypstDrive"
target="_blank"
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"
<a
href="https://github.com/SirBlobby/TypstDrive"
target="_blank"
rel="noopener noreferrer"
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" />
<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>
+97 -107
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import Modal from './Modal.svelte';
let props = $props<{
onClose: () => void;
@@ -9,7 +9,7 @@
let settings = $derived(props.currentSettings || {});
let paper = $state("");
let margin = $state("");
let width = $state("");
@@ -21,7 +21,7 @@
let header = $state("");
let footer = $state("");
let docTitle = $state("");
let author = $state("");
@@ -52,120 +52,110 @@
if (numbering !== 'none') newPageSettings.numbering = `"${numbering}"`;
if (header !== 'auto') newPageSettings.header = header;
if (footer !== 'auto') newPageSettings.footer = footer;
const newDocSettings: Record<string, string> = {};
if (docTitle) newDocSettings.title = `"${docTitle}"`;
if (author) newDocSettings.author = `"${author}"`;
if (author) newDocSettings.author = `"${author}"`;
props.onApply(newPageSettings, newDocSettings);
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>
<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(); } }}>
<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 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-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Document Metadata</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<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>
</section>
<Modal title="Document & page settings" icon="ph:file-text" width="max-w-2xl" onclose={props.onClose}>
<div class="flex flex-col gap-8">
<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>
<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>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<label for="docTitle" class="text-xs font-medium text-[var(--color-ink-muted)]">PDF title</label>
<input id="docTitle" type="text" bind:value={docTitle} class={fieldClass} placeholder="My Report" />
</div>
<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" />
</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)]" />
<label for="flipped" class="text-sm font-medium">Landscape Orientation (Flipped)</label>
</div>
</section>
<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>
<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">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>
</section>
</div>
<div class="p-5 border-t border-[var(--theme-border)] flex justify-end gap-3" style="background-color: var(--theme-border);">
<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)]">
Cancel
</button>
<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">
Apply Settings
</button>
</div>
<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" />
</div>
<div class="flex flex-col gap-1">
<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="flex items-center gap-2 mt-4">
<input id="flipped" type="checkbox" bind:checked={flipped} class="rounded border-[var(--color-line)] text-[var(--color-accent)] focus:ring-[var(--color-accent)]" />
<label for="flipped" class="text-xs font-medium text-[var(--color-ink-muted)]">Landscape orientation (flipped)</label>
</div>
</section>
<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>
{#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>
{/each}
{: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>
</div>
{/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">
import Icon from '@iconify/svelte';
import Modal from './Modal.svelte';
let {
projectId,
@@ -37,40 +38,33 @@
}
</script>
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={onClose} role="presentation">
<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">
<div class="flex items-center gap-2 mb-4">
<Icon icon="mdi:package-variant-closed" class="text-2xl text-purple-500" />
<h2 class="text-lg font-bold">Publish as Package</h2>
</div>
<Modal title="Publish as package" icon="ph:package" onclose={onClose}>
<p class="text-sm text-[var(--color-ink-muted)] mb-4">
Snapshots this project's files into an immutable package version, importable instance-wide as
<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>.
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>.
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
Snapshots this project's files into an immutable package version, importable instance-wide 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>.
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>.
</p>
<label class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1" for="pkg-version">Version override (optional)</label>
<input
id="pkg-version"
bind:value={version}
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>
<input
id="pkg-version"
bind:value={version}
placeholder="e.g. 0.1.0 (defaults to typst.toml)"
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 error}
<div class="text-sm text-[var(--color-danger)] mt-2 break-words">{error}</div>
{/if}
{#if success}
<div class="text-sm text-[var(--color-success)] mt-2">{success}</div>
{/if}
{#if error}
<div class="text-sm text-red-600 dark:text-red-400 mb-3 break-words">{error}</div>
{/if}
{#if success}
<div class="text-sm text-green-600 dark:text-green-400 mb-3">{success}</div>
{/if}
<div class="flex justify-end gap-2">
<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>
{#snippet footer()}
<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>
<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 publishing}<Icon icon="mdi:loading" class="animate-spin" />{/if}
Publish
</button>
{/snippet}
</Modal>
+110 -118
View File
@@ -2,7 +2,8 @@
import { onMount } from 'svelte';
import { page } from '$app/stores';
import Icon from '@iconify/svelte';
import Modal from './Modal.svelte';
let { onClose, docId = undefined } = $props<{ onClose: () => void, docId?: string }>();
type CollaboratorView = { id: string; user_id: string; username: string; email: string; role: string; created_at: string };
@@ -19,7 +20,7 @@
let inviteRole = $state('editor');
let inviteStatus = $state<'idle' | 'loading' | 'success' | 'error'>('idle');
let inviteMessage = $state('');
async function loadCollaborators() {
if (!docId) return;
collabLoading = true;
@@ -76,14 +77,14 @@
link = `${docUrl}?role=${role}`;
loadCollaborators();
});
$effect(() => {
const baseUrl = window.location.origin;
let docUrl = docId ? `${baseUrl}/doc/${docId}` : window.location.href.split('?')[0];
link = `${docUrl}?role=${role}`;
});
function copyLink() {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(link).catch(console.error);
@@ -103,125 +104,116 @@
}
</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}>
<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 justify-between items-center p-4 border-b border-[var(--theme-border)]" style="border-color: var(--theme-border);">
<h2 id="share-dialog-title" class="text-lg font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">Share 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">
<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>
</button>
</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...
<Modal title="Share document" icon="ph:share-network" width="max-w-[500px]" onclose={onClose}>
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-2">
<div class="text-xs font-medium text-[var(--color-ink-muted)]">Invite collaborator</div>
<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">
<div class="pl-2 text-[var(--color-ink-muted)]">
<Icon icon="mdi:account-plus-outline" class="text-xl" />
</div>
{:else if collaborators.length > 0}
<div class="h-px bg-gray-200 dark:bg-zinc-800/50"></div>
<div class="space-y-2">
<div class="text-sm font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">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-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>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{collab.username}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 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-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'}">
{collab.role}
</span>
<button
onclick={() => removeCollaborator(collab)}
disabled={removingId === collab.id}
title="Remove collaborator"
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 removingId === collab.id}
<Icon icon="mdi:loading" class="text-base animate-spin" />
{:else}
<Icon icon="mdi:close" class="text-base" />
{/if}
</button>
</div>
{/each}
<input
type="email"
placeholder="Add people via email..."
bind:value={inviteEmail}
required
class="flex-1 bg-transparent border-none text-sm px-2 py-2 focus:ring-0 focus:outline-none w-full"
/>
<div class="h-6 w-px bg-[var(--color-line)]"></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">
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
<button
type="submit"
disabled={inviteStatus === 'loading'}
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]"
>
{inviteStatus === 'loading' ? 'Inviting...' : 'Invite'}
</button>
</form>
{#if inviteMessage}
<div class="flex items-center gap-1.5 text-xs font-medium {inviteStatus === 'success' ? 'text-[var(--color-success)]' : 'text-[var(--color-danger)]'}">
<Icon icon={inviteStatus === 'success' ? 'mdi:check-circle' : 'mdi:alert-circle'} class="text-sm" />
{inviteMessage}
</div>
{/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="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="bg-gray-200 dark:bg-zinc-800 p-2.5 rounded-full text-gray-600 dark:text-gray-300">
<Icon icon="mdi:earth" class="text-xl" />
</div>
<div class="flex-1">
<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 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)]">General access</div>
<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 class="rounded-full bg-[var(--color-surface-sunken)] p-2.5 text-[var(--color-ink-muted)]">
<Icon icon="mdi:earth" class="text-xl" />
</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 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>
{#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();
const themeOptions = Object.keys(themes).flatMap(themeName => [
{ 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'}`);
const themeNames = Object.keys(themes);
</script>
<div class="flex items-center gap-2 {className}">
<Icon icon={themes[$themeStore]?.icon || 'mdi:palette'} class="text-xl text-[var(--theme-text)] opacity-70" />
<select
value={selectedValue}
onchange={handleChange}
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"
>
{#each themeOptions as opt}
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value={opt.name}>{opt.name}</option>
<div class="flex rounded-lg bg-[var(--color-surface-sunken)] p-0.5 text-xs font-medium">
{#each themeNames as name}
<button
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 transition
{$themeStore === name
? 'bg-[var(--color-surface)] text-[var(--color-accent)] shadow-sm'
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
onclick={() => ($themeStore = name)}
>
<Icon icon={themes[name].icon} class="text-sm" />
{name}
</button>
{/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>
+86 -137
View File
@@ -10,6 +10,9 @@
import PresentationMode from "./PresentationMode.svelte";
import CommentsSidebar from "./CommentsSidebar.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 { undo, redo } from '@codemirror/commands';
@@ -348,13 +351,12 @@
showRenameModal = true;
}
function submitRename(e: Event) {
e.preventDefault();
if (renameTitle && renameTitle !== title) {
function submitRename(newTitle: string) {
if (newTitle && newTitle !== title) {
fetch(`/api/docs/${docId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: renameTitle })
body: JSON.stringify({ title: newTitle })
}).then(res => {
if (res.ok) {
window.location.reload();
@@ -401,7 +403,7 @@
<div class="flex items-center gap-3">
<button
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"
title="Dashboard"
>
@@ -410,14 +412,14 @@
<div class="flex flex-col gap-0.5">
<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}
</h1>
</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">
<button
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>
{#if !isViewer}
<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}
</div>
{/if}
@@ -493,7 +495,7 @@
Version History
</button>
<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
<Icon icon={$darkModeStore ? "mdi:check" : ""} class="text-sm" />
</button>
@@ -510,7 +512,7 @@
<div class="flex items-center -space-x-2 mr-2">
{#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"
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)' : '')}
>
@@ -522,21 +524,21 @@
<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]" />
</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]" />
</a>
</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}
<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"
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
@@ -544,7 +546,7 @@
<button
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]" />
Comments
@@ -552,27 +554,27 @@
<button
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>
Share
</button>
{/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 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'}
>
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" />
Preview
</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]" />
Print
</button>
@@ -580,7 +582,7 @@
<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 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]" />
Export
@@ -607,50 +609,50 @@
</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">
<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" />
</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" />
</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" />
</button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></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)">
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<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('$ \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" />
</button>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></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">
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
<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">
<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={handleImageUpload} class="hidden" accept="image/*,.ttf,.otf" />
{#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" />
</button>
{/if}
</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">
<label for="font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
<select
id="font-select"
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="Libertinus Serif">Libertinus Serif</option>
@@ -666,7 +668,7 @@
</select>
</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">
{#if !isViewer}
@@ -680,29 +682,29 @@
{/if}
</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">
<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"
<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-[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}>
<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"
<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 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">
<ThemePicker />
@@ -735,105 +737,52 @@
{/if}
{#if showInfoModal}
<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="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 border-b border-gray-100 dark:border-white/10">
<div class="flex items-center gap-3">
<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>
<Modal title={docInfo?.title || title} icon="ph:file-text" onclose={() => showInfoModal = false}>
<div class="flex flex-col gap-4 text-xs">
<div>
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p>
<p class="text-sm text-[var(--color-ink)] capitalize">Document</p>
</div>
<div class="p-6 space-y-4">
{#if docInfo?.created_at}
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p>
<p class="text-sm text-gray-900 dark:text-gray-100 capitalize">Document</p>
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Created</p>
<p class="text-sm text-[var(--color-ink)]">{new Date(docInfo.created_at).toLocaleString()}</p>
</div>
{#if docInfo?.created_at}
<div>
<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(docInfo.created_at).toLocaleString()}</p>
</div>
{/if}
{#if docInfo?.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(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>
{/if}
{#if docInfo?.updated_at}
<div>
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
<p class="text-sm text-[var(--color-ink)]">{new Date(docInfo.updated_at).toLocaleString()}</p>
</div>
{/if}
</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 showRenameModal}
<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; } }}>
<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={submitRename} 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={() => 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>
<PromptModal
title="Rename"
label="New name"
icon="ph:pencil-simple"
value={renameTitle}
confirmLabel="Save"
onsubmit={submitRename}
onclose={() => showRenameModal = false}
/>
{/if}
{#if showDeleteModal}
<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; } }}>
<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 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>
<ConfirmModal
title="Delete document"
message="Are you sure you want to delete this document? This action cannot be undone."
confirmLabel="Delete"
onconfirm={confirmDelete}
onclose={() => showDeleteModal = false}
/>
{/if}
+11 -11
View File
@@ -55,13 +55,13 @@
</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="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">
<Icon icon="mdi:history" class="text-lg" />
<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>
</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" />
</button>
</div>
@@ -72,7 +72,7 @@
<Icon icon="mdi:loading" class="animate-spin text-2xl" />
</div>
{: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}
</div>
{:else if versions.length === 0}
@@ -96,11 +96,11 @@
</div>
<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" />
Preview
</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" />
Restore
</button>
@@ -120,17 +120,17 @@
<h3 class="text-lg font-semibold text-[var(--theme-text)]">Previewing Version</h3>
<span class="text-sm">{formatDate(previewVersion.created_at)}</span>
</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" />
</button>
</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>
</div>
<div class="p-4 border-t flex justify-end gap-3 bg-white/50 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">
<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-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors">
Close
</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">
@@ -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>
+25 -25
View File
@@ -31,59 +31,59 @@
}
</script>
<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"
role="button"
tabindex="0"
onclick={() => goto(`/doc/${doc.id}`)}
<div
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"
tabindex="0"
onclick={() => goto(`/doc/${doc.id}`)}
onkeydown={(e) => e.key === 'Enter' && goto(`/doc/${doc.id}`)}
draggable="true"
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}
<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" />
</div>
{: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" />
</div>
{/if}
</div>
<div class="p-4 flex flex-col flex-grow">
<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">
<button
aria-label="Document 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="Document 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" />
</button>
{#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]">
<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">
<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-[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" />
View Info
</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" />
Rename
</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" />
Share
</button>
<div class="h-px bg-gray-200 dark:bg-white/10 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">
<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-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-2">
<Icon icon="mdi:trash-can-outline" class="text-lg" />
Delete
</button>
@@ -91,7 +91,7 @@
{/if}
</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" />
Edited {new Date(doc.updated_at.endsWith('Z') ? doc.updated_at : doc.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</p>
+9 -9
View File
@@ -15,17 +15,17 @@
}
</script>
<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'}"
role="button"
tabindex="0"
onclick={handleOpen}
<div
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"
tabindex="0"
onclick={handleOpen}
onkeydown={(e) => e.key === 'Enter' && handleOpen()}
draggable="true"
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="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/')}
<img src={`/api/files/${file.id}/data`} alt={file.name} class="w-full h-full object-cover rounded" draggable="false" />
{:else if isFont}
@@ -34,12 +34,12 @@
<Icon icon="mdi:file-outline" class="text-2xl" />
{/if}
</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" />
</button>
</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>
<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">
<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-[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" />
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>
@@ -11,22 +11,22 @@
}>();
</script>
<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'}"
role="button"
tabindex="0"
onclick={() => navigateToFolder(folder)}
<div
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"
tabindex="0"
onclick={() => navigateToFolder(folder)}
onkeydown={(e) => e.key === 'Enter' && navigateToFolder(folder)}
ondragover={(e) => { e.preventDefault(); setDragOverFolderId(folder.id); }}
ondragleave={() => setDragOverFolderId(null)}
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" />
</div>
<span class="font-medium text-gray-900 dark:text-white 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">
<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-[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" />
</button>
</div>
+31 -33
View File
@@ -1,41 +1,39 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let { selectedInfo, onClose } = $props<{
selectedInfo: {type: string, title?: string, name?: string, created_at: string, updated_at?: string},
onClose: () => void
import Modal from '../Modal.svelte';
let { selectedInfo, onClose } = $props<{
selectedInfo: {type: string, title?: string, name?: string, created_at: string, updated_at?: string},
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>
<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()}>
<div class="p-6 border-b border-gray-100 dark:border-white/10">
<div class="flex items-center gap-3">
<div class="p-2 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-lg">
<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>
<Modal {title} {icon} onclose={onClose}>
<div class="flex flex-col gap-4 text-xs">
<div>
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p>
<p class="text-sm capitalize text-[var(--color-ink)]">{selectedInfo.type}</p>
</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>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p>
<p class="text-sm text-gray-900 dark:text-gray-100 capitalize">{selectedInfo.type}</p>
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</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>
<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>
{/if}
</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>
+16 -16
View File
@@ -11,44 +11,44 @@
}
</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">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
<Icon icon="mdi:script-text" class="text-blue-600 dark:text-blue-400 text-3xl" />
<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-[var(--color-ink)] flex items-center gap-3">
<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
</h1>
<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" />
{$userStore?.username}
</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" />
</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" />
</a>
<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">
<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">
<Icon icon="mdi:api" class="text-xl" />
</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" />
</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" />
</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 />
<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" />
</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" />
Logout
</button>
+12 -12
View File
@@ -27,19 +27,19 @@
</script>
<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"
tabindex="0"
onclick={() => 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}
<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" />
</div>
{: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" />
</div>
{/if}
@@ -47,29 +47,29 @@
<div class="p-4 flex flex-col flex-grow">
<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">
<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" />
</button>
{#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]">
<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">
<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-[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" />
View Info
</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" />
Rename
</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" />
Open
</button>
<div class="h-px bg-gray-200 dark:bg-white/10 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">
<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-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-2">
<Icon icon="mdi:trash-can-outline" class="text-lg" />
Delete
</button>
@@ -77,7 +77,7 @@
{/if}
</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" />
Edited {new Date(project.updated_at.endsWith('Z') ? project.updated_at : project.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</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>
<div class="h-full flex flex-col bg-[var(--theme-bg)] border-r border-gray-200 dark:border-white/10">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Files</span>
<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-[var(--color-line)]">
<span class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)]">Files</span>
{#if !readOnly}
<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" />
</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" />
</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 = ''; }} />
@@ -72,7 +72,7 @@
<div class="flex-1 overflow-y-auto py-1">
{#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)}>
<Icon icon={iconFor(file.path)} class="text-base flex-shrink-0" />
<span class="truncate">{file.path}</span>
@@ -85,14 +85,14 @@
{#if !readOnly}
<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}
<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" />
</button>
{/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" />
</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" />
</button>
</div>
@@ -13,6 +13,9 @@
import ThemePicker from '../ThemePicker.svelte';
import PageSettingsModal from '../PageSettingsModal.svelte';
import PresentationMode from '../PresentationMode.svelte';
import Modal from '../Modal.svelte';
import PromptModal from '../PromptModal.svelte';
import ConfirmModal from '../ConfirmModal.svelte';
let {
projectName = 'Project',
@@ -257,13 +260,12 @@
return name.substring(0, 2).toUpperCase();
}
function submitRename(e: Event) {
e.preventDefault();
if (renameName && renameName !== projectName) {
function submitRename(name: string) {
if (name && name !== projectName) {
fetch(`/api/projects/${projectId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: renameName })
body: JSON.stringify({ name })
}).then((res) => { if (res.ok) window.location.reload(); });
}
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]">
<div class="flex items-center justify-between px-4 py-2.5">
<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" />
</button>
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2">
<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 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">
<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'}
@@ -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>
{#if role === 'owner'}
<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}
</div>
{/if}
@@ -352,7 +354,7 @@
{#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]">
<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>
{/if}
</div>
@@ -364,36 +366,36 @@
{#if $connectedUsers.length > 0}
<div class="flex items-center -space-x-2 mr-2">
{#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}
</div>
{/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}
<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
</button>
{#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
</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 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
</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
</button>
<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" />
</button>
{#if activeMenu === 'export'}
@@ -408,23 +410,23 @@
</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">
<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('_', '_', '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('`', '`', '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>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></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('- ', '', '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('+ ', '', '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>
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div>
<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-[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-[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-[var(--color-line)]"></div>
<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-[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-[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-[var(--color-line)]"></div>
<input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" accept="image/*,.ttf,.otf" />
{#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}
</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">
<label for="project-font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
@@ -443,22 +445,22 @@
</select>
</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}
<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
</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}
<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">
<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>
<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>
<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>
<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-[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-[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-[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 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 />
<div class="flex-grow"></div>
@@ -474,50 +476,37 @@
{/if}
{#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">
<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="p-6 border-b border-gray-100 dark:border-white/10 flex items-center gap-3">
<Icon icon="mdi:folder-multiple-outline" class="text-xl text-blue-500" />
<h3 class="text-lg font-semibold flex-grow truncate">{projectName}</h3>
</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>
<Modal title={projectName} icon="ph:folder-star" onclose={() => showInfoModal = false}>
<div class="flex flex-col gap-4 text-xs">
<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>
<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>
<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>
{#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 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">
<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">
<form onsubmit={submitRename} class="p-6">
<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>
<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" />
<div class="pt-6 flex justify-end gap-3">
<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>
<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>
</div>
</form>
</div>
</div>
<PromptModal
title="Rename project"
label="Project name"
icon="ph:pencil-simple"
value={renameName}
confirmLabel="Save"
onsubmit={submitRename}
onclose={() => showRenameModal = false}
/>
{/if}
{#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">
<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="p-6">
<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>
<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>
<div class="flex justify-end gap-3">
<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>
<ConfirmModal
title="Delete project"
message="Delete this project and all its files? This cannot be undone."
confirmLabel="Delete"
onconfirm={confirmDelete}
onclose={() => showDeleteModal = false}
/>
{/if}