Rework UI to match typst-desktop's design system
This commit is contained in:
+89
@@ -6,12 +6,101 @@
|
|||||||
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-muted: #eff1f5;
|
||||||
|
--color-surface-sunken: #dce0e8;
|
||||||
|
--color-line: #ccd0da;
|
||||||
|
--color-ink: #11111b;
|
||||||
|
--color-ink-muted: #5c5f77;
|
||||||
|
--color-accent: #1e66f5;
|
||||||
|
--color-accent-soft: #dce8fd;
|
||||||
|
--color-danger: #d5382f;
|
||||||
|
--color-success: #1f8a4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--color-surface: #181825;
|
||||||
|
--color-surface-muted: #1e1e2e;
|
||||||
|
--color-surface-sunken: #45475a;
|
||||||
|
--color-line: #313244;
|
||||||
|
--color-ink: #cdd6f4;
|
||||||
|
--color-ink-muted: #6c7086;
|
||||||
|
--color-accent: #89b4fa;
|
||||||
|
--color-accent-soft: #1e2f4d;
|
||||||
|
--color-danger: #f4736a;
|
||||||
|
--color-success: #4cc47f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-color-theme='Cerberus'] {
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-muted: #ffffff;
|
||||||
|
--color-surface-sunken: #f4f4f5;
|
||||||
|
--color-line: #d4d4d4;
|
||||||
|
--color-ink: #000000;
|
||||||
|
--color-ink-muted: #52525b;
|
||||||
|
--color-accent: #4338ca;
|
||||||
|
--color-accent-soft: #e0e7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-color-theme='Cerberus'][data-theme='dark'] {
|
||||||
|
--color-surface: #121212;
|
||||||
|
--color-surface-muted: #171717;
|
||||||
|
--color-surface-sunken: #1f1f1f;
|
||||||
|
--color-line: #262626;
|
||||||
|
--color-ink: #f5f5f5;
|
||||||
|
--color-ink-muted: #737373;
|
||||||
|
--color-accent: #818cf8;
|
||||||
|
--color-accent-soft: #262244;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-color-theme='Arch Linux'] {
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-muted: #ffffff;
|
||||||
|
--color-surface-sunken: #f6f8fa;
|
||||||
|
--color-line: #d0d7de;
|
||||||
|
--color-ink: #0d1117;
|
||||||
|
--color-ink-muted: #57606a;
|
||||||
|
--color-accent: #0550ae;
|
||||||
|
--color-accent-soft: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-color-theme='Arch Linux'][data-theme='dark'] {
|
||||||
|
--color-surface: #010409;
|
||||||
|
--color-surface-muted: #0d1117;
|
||||||
|
--color-surface-sunken: #161b22;
|
||||||
|
--color-line: #21262d;
|
||||||
|
--color-ink: #c9d1d9;
|
||||||
|
--color-ink-muted: #6e7681;
|
||||||
|
--color-accent: #1793d1;
|
||||||
|
--color-accent-soft: #0d2b3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--theme-bg: var(--color-surface-muted);
|
||||||
|
--theme-text: var(--color-ink);
|
||||||
|
--theme-border: var(--color-line);
|
||||||
|
--theme-cursor: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-surface-muted);
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-thin {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--color-line) transparent;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="text-scale" content="scale" />
|
<meta name="text-scale" content="scale" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
<h2 class="text-sm font-semibold text-[var(--theme-text)]">Comments</h2>
|
<h2 class="text-sm font-semibold text-[var(--theme-text)]">Comments</h2>
|
||||||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{comments.length}</span>
|
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{comments.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={onClose} class="p-1.5 hover:text-gray-600 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-white/10 rounded-md transition-colors" title="Close Comments">
|
<button onclick={onClose} class="p-1.5 hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Close Comments">
|
||||||
<Icon icon="mdi:close" class="text-lg" />
|
<Icon icon="mdi:close" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<Icon icon="mdi:loading" class="animate-spin text-2xl" />
|
<Icon icon="mdi:loading" class="animate-spin text-2xl" />
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="text-red-500 text-sm text-center p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/30">
|
<div class="text-[var(--color-danger)] text-sm text-center p-4 bg-[var(--color-danger)]/10 rounded-md border border-[var(--color-danger)]/20">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{:else if comments.length === 0}
|
{:else if comments.length === 0}
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<div class="group flex flex-col gap-2 p-3 border rounded-xl shadow-sm hover:shadow-md transition-all {comment.resolved ? 'opacity-60' : ''} bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
<div class="group flex flex-col gap-2 p-3 border rounded-xl shadow-sm hover:shadow-md transition-all {comment.resolved ? 'opacity-60' : ''} bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 flex items-center justify-center text-xs font-bold">
|
<div class="w-6 h-6 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] flex items-center justify-center text-xs font-bold">
|
||||||
{(comment.author_name || 'A').substring(0, 1).toUpperCase()}
|
{(comment.author_name || 'A').substring(0, 1).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -137,11 +137,11 @@
|
|||||||
|
|
||||||
<div class="flex opacity-0 group-hover:opacity-100 transition-opacity gap-1">
|
<div class="flex opacity-0 group-hover:opacity-100 transition-opacity gap-1">
|
||||||
{#if $userStore?.id === comment.user_id}
|
{#if $userStore?.id === comment.user_id}
|
||||||
<button onclick={() => deleteComment(comment.id)} class="p-1 hover:text-red-500 rounded hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" title="Delete">
|
<button onclick={() => deleteComment(comment.id)} class="p-1 hover:text-[var(--color-danger)] rounded hover:bg-[var(--color-danger)]/10 transition-colors" title="Delete">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-xs" />
|
<Icon icon="mdi:trash-can-outline" class="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button onclick={() => toggleResolve(comment)} class="p-1 hover:text-emerald-500 rounded hover:bg-emerald-50 dark:hover:bg-emerald-500/10 transition-colors" title={comment.resolved ? "Reopen" : "Resolve"}>
|
<button onclick={() => toggleResolve(comment)} class="p-1 hover:text-[var(--color-success)] rounded hover:bg-[var(--color-success)]/10 transition-colors" title={comment.resolved ? "Reopen" : "Resolve"}>
|
||||||
<Icon icon={comment.resolved ? "mdi:check-circle" : "mdi:check-circle-outline"} class="text-xs" />
|
<Icon icon={comment.resolved ? "mdi:check-circle" : "mdi:check-circle-outline"} class="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
bind:value={newCommentContent}
|
bind:value={newCommentContent}
|
||||||
placeholder="Add a comment..."
|
placeholder="Add a comment..."
|
||||||
class="w-full border text-[var(--theme-text)] text-sm rounded-xl px-3 py-2.5 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500/50 resize-none min-h-[80px] bg-[var(--theme-bg)] border-[var(--theme-border)]"
|
class="w-full border text-[var(--theme-text)] text-sm rounded-md px-3 py-2.5 pr-10 focus:outline-none focus:border-[var(--color-accent)] resize-none min-h-[80px] bg-[var(--theme-bg)] border-[var(--theme-border)]"
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
<button
|
<button
|
||||||
onclick={postComment}
|
onclick={postComment}
|
||||||
disabled={!newCommentContent.trim()}
|
disabled={!newCommentContent.trim()}
|
||||||
class="absolute bottom-2.5 right-2.5 p-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 dark:disabled:bg-zinc-700 disabled:text-gray-500 rounded-lg transition-colors"
|
class="absolute bottom-2.5 right-2.5 p-1.5 bg-[var(--color-accent)] hover:opacity-90 disabled:bg-[var(--color-surface-sunken)] disabled:text-[var(--color-ink-muted)] rounded-md transition-colors"
|
||||||
title="Post (Enter)"
|
title="Post (Enter)"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:send" class="text-sm" />
|
<Icon icon="mdi:send" class="text-sm" />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
<div class="h-8 border-t border-[var(--theme-border)] bg-[var(--theme-bg)] flex items-center justify-between px-4 text-xs text-[var(--theme-text)] select-none z-[60] relative">
|
<div class="h-8 border-t border-[var(--theme-border)] bg-[var(--theme-bg)] flex items-center justify-between px-4 text-xs text-[var(--theme-text)] select-none z-[60] relative">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex items-center gap-1.5 font-medium {
|
<div class="flex items-center gap-1.5 font-medium {
|
||||||
$connectionStatus === 'connected' ? 'text-emerald-600 dark:text-emerald-400' : 'text-amber-600 dark:text-amber-400'
|
$connectionStatus === 'connected' ? 'text-[var(--color-success)]' : 'text-amber-600 dark:text-amber-400'
|
||||||
}">
|
}">
|
||||||
<div class="w-1.5 h-1.5 rounded-full {$connectionStatus === 'connected' ? 'bg-emerald-500 shadow-[0_0_4px_rgba(16,185,129,0.4)]' : 'bg-amber-500 animate-pulse'}"></div>
|
<div class="w-1.5 h-1.5 rounded-full {$connectionStatus === 'connected' ? 'bg-[var(--color-success)] shadow-[0_0_4px_rgba(16,185,129,0.4)]' : 'bg-amber-500 animate-pulse'}"></div>
|
||||||
{$connectionStatus === 'connected' ? 'Document synced' : 'Connecting...'}
|
{$connectionStatus === 'connected' ? 'Document synced' : 'Connecting...'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $documentStatsStore}
|
{#if $documentStatsStore}
|
||||||
<button class="hover:bg-gray-100 dark:hover:bg-white/10 px-2 py-0.5 rounded transition-colors flex items-center gap-1 cursor-pointer" onclick={toggleModal} aria-label="Word count statistics">
|
<button class="hover:bg-[var(--color-surface-sunken)] px-2 py-0.5 rounded transition-colors flex items-center gap-1 cursor-pointer" onclick={toggleModal} aria-label="Word count statistics">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
||||||
{$documentStatsStore.words} words
|
{$documentStatsStore.words} words
|
||||||
</button>
|
</button>
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
{#if showStatsModal}
|
{#if showStatsModal}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="fixed inset-0 bg-black/20 dark:bg-black/40 z-[100] flex items-center justify-center backdrop-blur-sm" onclick={toggleModal}>
|
<div class="fixed inset-0 bg-black/40 z-[100] flex items-center justify-center" onclick={toggleModal}>
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded-xl shadow-xl w-80 overflow-hidden" onclick={e => e.stopPropagation()}>
|
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded-xl shadow-xl w-80 overflow-hidden" onclick={e => e.stopPropagation()}>
|
||||||
<div class="px-4 py-3 border-b border-[var(--theme-border)] flex items-center justify-between">
|
<div class="px-4 py-3 border-b border-[var(--theme-border)] flex items-center justify-between">
|
||||||
<h3 class="font-semibold text-sm">Word count</h3>
|
<h3 class="font-semibold text-sm">Word count</h3>
|
||||||
<button onclick={toggleModal} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
<button onclick={toggleModal} aria-label="Close" class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
let { sticky = true }: { sticky?: boolean } = $props();
|
let { sticky = true }: { sticky?: boolean } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="mt-auto py-6 text-center text-sm text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-white/10 bg-[var(--theme-bg)] {sticky ? 'sticky bottom-0 z-10' : ''} w-full flex-shrink-0 transition-colors duration-200">
|
<footer class="mt-auto py-6 text-center text-sm text-[var(--color-ink-muted)] border-t border-[var(--color-line)] bg-[var(--color-surface)] {sticky ? 'sticky bottom-0 z-10' : ''} w-full flex-shrink-0 transition-colors duration-200">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center items-center gap-4">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center items-center gap-4">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/SirBlobby/TypstDrive"
|
href="https://github.com/SirBlobby/TypstDrive"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="inline-flex items-center gap-2 hover:text-gray-900 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-1.5 rounded-lg border border-transparent dark:border-white/10"
|
class="inline-flex items-center gap-2 hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-1.5 rounded-md"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:github" class="text-xl" />
|
<Icon icon="mdi:github" class="text-xl" />
|
||||||
<span class="font-semibold">GitHub Repository</span>
|
<span class="font-semibold">GitHub Repository</span>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '@iconify/svelte';
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
let props = $props<{
|
let props = $props<{
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -60,43 +60,34 @@
|
|||||||
props.onApply(newPageSettings, newDocSettings);
|
props.onApply(newPageSettings, newDocSettings);
|
||||||
props.onClose();
|
props.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldClass = "w-full rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200" role="presentation" onclick={() => props.onClose()} onkeydown={(e) => { if (e.key === "Enter") { props.onClose(); } }}>
|
<Modal title="Document & page settings" icon="ph:file-text" width="max-w-2xl" onclose={props.onClose}>
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-[var(--theme-border)] w-full max-w-2xl overflow-hidden flex flex-col max-h-[85vh]" role="dialog" tabindex="-1" aria-modal="true" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.key === 'Escape' && props.onClose()}>
|
<div class="flex flex-col gap-8">
|
||||||
<div class="flex justify-between items-center p-5 border-b border-[var(--theme-border)]">
|
|
||||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:file-document-edit-outline" class="text-blue-500 text-xl" />
|
|
||||||
Document & Page Settings
|
|
||||||
</h2>
|
|
||||||
<button onclick={() => props.onClose()} class="opacity-60 hover:opacity-100 rounded-full p-1 transition-opacity">
|
|
||||||
<Icon icon="mdi:close" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6 space-y-8 overflow-y-auto flex-1">
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Document Metadata</h3>
|
<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="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="docTitle" class="text-sm font-medium">PDF Title</label>
|
<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="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" />
|
<input id="docTitle" type="text" bind:value={docTitle} class={fieldClass} placeholder="My Report" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="author" class="text-sm font-medium">Author</label>
|
<label for="author" class="text-xs font-medium text-[var(--color-ink-muted)]">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" />
|
<input id="author" type="text" bind:value={author} class={fieldClass} placeholder="Jane Doe" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Page Layout</h3>
|
<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="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="paper" class="text-sm font-medium">Paper Size</label>
|
<label for="paper" class="text-xs font-medium text-[var(--color-ink-muted)]">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">
|
<select id="paper" bind:value={paper} class={fieldClass}>
|
||||||
<option value="a4">A4</option>
|
<option value="a4">A4</option>
|
||||||
<option value="us-letter">US Letter</option>
|
<option value="us-letter">US Letter</option>
|
||||||
<option value="a5">A5</option>
|
<option value="a5">A5</option>
|
||||||
@@ -104,41 +95,41 @@
|
|||||||
<option value="presentation-4-3">4:3 Presentation</option>
|
<option value="presentation-4-3">4:3 Presentation</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="margin" class="text-sm font-medium">Margin</label>
|
<label for="margin" class="text-xs font-medium text-[var(--color-ink-muted)]">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" />
|
<input id="margin" type="text" bind:value={margin} class={fieldClass} placeholder="auto or 1in" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="width" class="text-sm font-medium">Width</label>
|
<label for="width" class="text-xs font-medium text-[var(--color-ink-muted)]">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" />
|
<input id="width" type="text" bind:value={width} class={fieldClass} placeholder="auto" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="height" class="text-sm font-medium">Height</label>
|
<label for="height" class="text-xs font-medium text-[var(--color-ink-muted)]">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" />
|
<input id="height" type="text" bind:value={height} class={fieldClass} placeholder="auto" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="columns" class="text-sm font-medium">Columns</label>
|
<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="w-full bg-[var(--theme-bg)] border border-[var(--theme-border)] text-[var(--theme-text)] text-sm rounded-lg px-3 py-2" />
|
<input id="columns" type="number" min="1" max="10" bind:value={columns} class={fieldClass} />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="fill" class="text-sm font-medium">Background Fill</label>
|
<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="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)" />
|
<input id="fill" type="text" bind:value={fill} class={fieldClass} placeholder="auto or rgb(200, 200, 200)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 mt-4">
|
<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)]" />
|
<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-sm font-medium">Landscape Orientation (Flipped)</label>
|
<label for="flipped" class="text-xs font-medium text-[var(--color-ink-muted)]">Landscape orientation (flipped)</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 border-b border-[var(--theme-border)] pb-2">Headers & Footers</h3>
|
<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="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="numbering" class="text-sm font-medium">Page Numbering</label>
|
<label for="numbering" class="text-xs font-medium text-[var(--color-ink-muted)]">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">
|
<select id="numbering" bind:value={numbering} class={fieldClass}>
|
||||||
<option value="none">None</option>
|
<option value="none">None</option>
|
||||||
<option value="1">1, 2, 3</option>
|
<option value="1">1, 2, 3</option>
|
||||||
<option value="1/1">1/3, 2/3, 3/3</option>
|
<option value="1/1">1/3, 2/3, 3/3</option>
|
||||||
@@ -147,25 +138,24 @@
|
|||||||
<option value="I">I, II, III</option>
|
<option value="I">I, II, III</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="header" class="text-sm font-medium">Header Content</label>
|
<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="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]" />
|
<input id="header" type="text" bind:value={header} class={fieldClass} placeholder="auto or [Text]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2 sm:col-span-2">
|
<div class="flex flex-col gap-1 sm:col-span-2">
|
||||||
<label for="footer" class="text-sm font-medium">Footer Content</label>
|
<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="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]" />
|
<input id="footer" type="text" bind:value={footer} class={fieldClass} placeholder="auto or [Text]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-5 border-t border-[var(--theme-border)] flex justify-end gap-3" style="background-color: var(--theme-border);">
|
{#snippet footer()}
|
||||||
<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)]">
|
<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
|
Cancel
|
||||||
</button>
|
</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">
|
<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
|
Apply settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-gray-400 flex flex-col items-center justify-center h-full">
|
<div class="text-[var(--color-ink-muted)] flex flex-col items-center justify-center h-full">
|
||||||
<p>Document is empty or compiling...</p>
|
<p>Document is empty or compiling...</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
projectId,
|
projectId,
|
||||||
@@ -37,40 +38,33 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={onClose} role="presentation">
|
<Modal title="Publish as package" icon="ph:package" onclose={onClose}>
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md p-6" onclick={(e) => e.stopPropagation()} role="presentation">
|
<p class="text-sm text-[var(--color-ink-muted)] mb-4">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
|
||||||
<Icon icon="mdi:package-variant-closed" class="text-2xl text-purple-500" />
|
|
||||||
<h2 class="text-lg font-bold">Publish as Package</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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
|
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/<name>:<version></code>.
|
<code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">@typstdrive/<name>:<version></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>.
|
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>
|
||||||
|
|
||||||
<label class="block text-sm font-medium mb-1" for="pkg-version">Version override (optional)</label>
|
<label class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1" for="pkg-version">Version override (optional)</label>
|
||||||
<input
|
<input
|
||||||
id="pkg-version"
|
id="pkg-version"
|
||||||
bind:value={version}
|
bind:value={version}
|
||||||
placeholder="e.g. 0.1.0 (defaults to typst.toml)"
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="text-sm text-red-600 dark:text-red-400 mb-3 break-words">{error}</div>
|
<div class="text-sm text-[var(--color-danger)] mt-2 break-words">{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if success}
|
{#if success}
|
||||||
<div class="text-sm text-green-600 dark:text-green-400 mb-3">{success}</div>
|
<div class="text-sm text-[var(--color-success)] mt-2">{success}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end gap-2">
|
{#snippet footer()}
|
||||||
<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={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="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">
|
<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}
|
{#if publishing}<Icon icon="mdi:loading" class="animate-spin" />{/if}
|
||||||
Publish
|
Publish
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
let { onClose, docId = undefined } = $props<{ onClose: () => void, docId?: string }>();
|
let { onClose, docId = undefined } = $props<{ onClose: () => void, docId?: string }>();
|
||||||
|
|
||||||
@@ -103,20 +104,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
|
<Modal title="Share document" icon="ph:share-network" width="max-w-[500px]" onclose={onClose}>
|
||||||
<div tabindex="-1" class="rounded-xl shadow-2xl border w-full max-w-[500px] overflow-hidden bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]" style="background-color: var(--theme-bg); color: var(--theme-text); border-color: var(--theme-border);" role="dialog" aria-modal="true" aria-labelledby="share-dialog-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
<div class="flex flex-col gap-5">
|
||||||
<div class="flex justify-between items-center p-4 border-b border-[var(--theme-border)]" style="border-color: var(--theme-border);">
|
<div class="flex flex-col gap-2">
|
||||||
<h2 id="share-dialog-title" class="text-lg font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">Share Document</h2>
|
<div class="text-xs font-medium text-[var(--color-ink-muted)]">Invite collaborator</div>
|
||||||
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
|
<form onsubmit={inviteUser} class="flex items-center gap-2 rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] p-1.5 focus-within:border-[var(--color-accent)] transition">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
<div class="pl-2 text-[var(--color-ink-muted)]">
|
||||||
</button>
|
|
||||||
</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" />
|
<Icon icon="mdi:account-plus-outline" class="text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -124,23 +117,23 @@
|
|||||||
placeholder="Add people via email..."
|
placeholder="Add people via email..."
|
||||||
bind:value={inviteEmail}
|
bind:value={inviteEmail}
|
||||||
required
|
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"
|
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-gray-300 dark:bg-zinc-700"></div>
|
<div class="h-6 w-px bg-[var(--color-line)]"></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">
|
<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 class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="editor">Editor</option>
|
<option value="editor">Editor</option>
|
||||||
<option class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="viewer">Viewer</option>
|
<option value="viewer">Viewer</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={inviteStatus === 'loading'}
|
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]"
|
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'}
|
{inviteStatus === 'loading' ? 'Inviting...' : 'Invite'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{#if inviteMessage}
|
{#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'}">
|
<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" />
|
<Icon icon={inviteStatus === 'success' ? 'mdi:check-circle' : 'mdi:alert-circle'} class="text-sm" />
|
||||||
{inviteMessage}
|
{inviteMessage}
|
||||||
</div>
|
</div>
|
||||||
@@ -148,31 +141,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if collabLoading}
|
{#if collabLoading}
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-400 dark:text-gray-500 py-1">
|
<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" />
|
<Icon icon="mdi:loading" class="animate-spin text-base" />
|
||||||
Loading collaborators...
|
Loading collaborators...
|
||||||
</div>
|
</div>
|
||||||
{:else if collaborators.length > 0}
|
{:else if collaborators.length > 0}
|
||||||
<div class="h-px bg-gray-200 dark:bg-zinc-800/50"></div>
|
<div class="h-px bg-[var(--color-line)]"></div>
|
||||||
<div class="space-y-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="text-sm font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">People with access</div>
|
<div class="text-xs font-medium text-[var(--color-ink-muted)]">People with access</div>
|
||||||
{#each collaborators as collab (collab.id)}
|
{#each collaborators as collab (collab.id)}
|
||||||
<div class="flex items-center gap-3 py-1.5">
|
<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">
|
<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()}
|
{collab.username[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<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-sm font-medium text-[var(--color-ink)] truncate">{collab.username}</p>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">{collab.email}</p>
|
<p class="text-xs text-[var(--color-ink-muted)] truncate">{collab.email}</p>
|
||||||
</div>
|
</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'}">
|
<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}
|
{collab.role}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onclick={() => removeCollaborator(collab)}
|
onclick={() => removeCollaborator(collab)}
|
||||||
disabled={removingId === collab.id}
|
disabled={removingId === collab.id}
|
||||||
title="Remove collaborator"
|
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"
|
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}
|
{#if removingId === collab.id}
|
||||||
<Icon icon="mdi:loading" class="text-base animate-spin" />
|
<Icon icon="mdi:loading" class="text-base animate-spin" />
|
||||||
@@ -185,43 +178,42 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="h-px bg-gray-200 dark:bg-zinc-800/50"></div>
|
<div class="h-px bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="text-sm font-semibold text-[var(--theme-text)]" style="color: var(--theme-text);">General Access</div>
|
<div class="text-xs font-medium text-[var(--color-ink-muted)]">General access</div>
|
||||||
<div class="flex items-center gap-4 p-3 bg-gray-50/50 dark:bg-zinc-950/30 rounded-xl border border-gray-200 dark:border-zinc-800/50 hover:bg-gray-50 dark:hover:bg-zinc-900/50 transition-colors">
|
<div class="flex 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="bg-gray-200 dark:bg-zinc-800 p-2.5 rounded-full text-gray-600 dark:text-gray-300">
|
<div class="rounded-full bg-[var(--color-surface-sunken)] p-2.5 text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:earth" class="text-xl" />
|
<Icon icon="mdi:earth" class="text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">Anyone with the link</h4>
|
<h4 class="text-sm font-medium text-[var(--color-ink)]">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>
|
<p class="text-xs text-[var(--color-ink-muted)] mt-0.5">Can view and collaborate based on role</p>
|
||||||
</div>
|
</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">
|
<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 class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="viewer">Viewer</option>
|
<option value="viewer">Viewer</option>
|
||||||
<option class="bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100" value="editor">Editor</option>
|
<option value="editor">Editor</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 bg-gray-50 dark:bg-zinc-950/50 border-t border-[var(--theme-border)] flex items-center justify-between" style="border-color: var(--theme-border);">
|
{#snippet footer()}
|
||||||
<button
|
<button
|
||||||
onclick={copyLink}
|
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"
|
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}
|
{#if copied}
|
||||||
<Icon icon="mdi:check" class="text-lg" />
|
<Icon icon="mdi:check" class="text-base" />
|
||||||
<span>Link copied!</span>
|
<span>Link copied!</span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="mdi:link-variant" class="text-lg" />
|
<Icon icon="mdi:link-variant" class="text-base" />
|
||||||
<span>Copy link</span>
|
<span>Copy link</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</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">
|
<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
|
Done
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -5,32 +5,47 @@
|
|||||||
|
|
||||||
let { class: className = '' } = $props();
|
let { class: className = '' } = $props();
|
||||||
|
|
||||||
const themeOptions = Object.keys(themes).flatMap(themeName => [
|
const themeNames = Object.keys(themes);
|
||||||
{ name: `${themeName} Light`, theme: themeName, isDark: false },
|
|
||||||
{ name: `${themeName} Dark`, theme: themeName, isDark: true }
|
|
||||||
]);
|
|
||||||
|
|
||||||
function handleChange(e: Event) {
|
|
||||||
const val = (e.target as HTMLSelectElement).value;
|
|
||||||
const opt = themeOptions.find(o => o.name === val);
|
|
||||||
if (opt) {
|
|
||||||
$themeStore = opt.theme;
|
|
||||||
$darkModeStore = opt.isDark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedValue = $derived(`${$themeStore} ${$darkModeStore ? 'Dark' : 'Light'}`);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 {className}">
|
<div class="flex items-center gap-2 {className}">
|
||||||
<Icon icon={themes[$themeStore]?.icon || 'mdi:palette'} class="text-xl text-[var(--theme-text)] opacity-70" />
|
<div class="flex rounded-lg bg-[var(--color-surface-sunken)] p-0.5 text-xs font-medium">
|
||||||
<select
|
{#each themeNames as name}
|
||||||
value={selectedValue}
|
<button
|
||||||
onchange={handleChange}
|
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 transition
|
||||||
class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] text-sm font-medium rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 block py-1.5 pl-3 pr-8 appearance-none cursor-pointer transition-colors outline-none"
|
{$themeStore === name
|
||||||
|
? 'bg-[var(--color-surface)] text-[var(--color-accent)] shadow-sm'
|
||||||
|
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
|
||||||
|
onclick={() => ($themeStore = name)}
|
||||||
>
|
>
|
||||||
{#each themeOptions as opt}
|
<Icon icon={themes[name].icon} class="text-sm" />
|
||||||
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value={opt.name}>{opt.name}</option>
|
{name}
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex rounded-lg bg-[var(--color-surface-sunken)] p-0.5 text-xs font-medium">
|
||||||
|
<button
|
||||||
|
class="flex items-center rounded-md px-2.5 py-1.5 transition
|
||||||
|
{!$darkModeStore
|
||||||
|
? 'bg-[var(--color-surface)] text-[var(--color-ink)] shadow-sm'
|
||||||
|
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
|
||||||
|
onclick={() => ($darkModeStore = false)}
|
||||||
|
aria-label="Light mode"
|
||||||
|
title="Light mode"
|
||||||
|
>
|
||||||
|
<Icon icon="ph:sun" class="text-sm" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex items-center rounded-md px-2.5 py-1.5 transition
|
||||||
|
{$darkModeStore
|
||||||
|
? 'bg-[var(--color-surface)] text-[var(--color-ink)] shadow-sm'
|
||||||
|
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
|
||||||
|
onclick={() => ($darkModeStore = true)}
|
||||||
|
aria-label="Dark mode"
|
||||||
|
title="Dark mode"
|
||||||
|
>
|
||||||
|
<Icon icon="ph:moon" class="text-sm" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
import PresentationMode from "./PresentationMode.svelte";
|
import PresentationMode from "./PresentationMode.svelte";
|
||||||
import CommentsSidebar from "./CommentsSidebar.svelte";
|
import CommentsSidebar from "./CommentsSidebar.svelte";
|
||||||
import VersionHistorySidebar from "./VersionHistorySidebar.svelte";
|
import VersionHistorySidebar from "./VersionHistorySidebar.svelte";
|
||||||
|
import Modal from './Modal.svelte';
|
||||||
|
import PromptModal from './PromptModal.svelte';
|
||||||
|
import ConfirmModal from './ConfirmModal.svelte';
|
||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
import { undo, redo } from '@codemirror/commands';
|
import { undo, redo } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -348,13 +351,12 @@
|
|||||||
showRenameModal = true;
|
showRenameModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitRename(e: Event) {
|
function submitRename(newTitle: string) {
|
||||||
e.preventDefault();
|
if (newTitle && newTitle !== title) {
|
||||||
if (renameTitle && renameTitle !== title) {
|
|
||||||
fetch(`/api/docs/${docId}`, {
|
fetch(`/api/docs/${docId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ title: renameTitle })
|
body: JSON.stringify({ title: newTitle })
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -401,7 +403,7 @@
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onclick={() => goto('/dashboard')}
|
onclick={() => goto('/dashboard')}
|
||||||
class="p-1.5 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors"
|
class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors"
|
||||||
aria-label="Back to dashboard"
|
aria-label="Back to dashboard"
|
||||||
title="Dashboard"
|
title="Dashboard"
|
||||||
>
|
>
|
||||||
@@ -410,14 +412,14 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-[16px] font-semibold text-gray-900 dark:text-white tracking-tight truncate max-w-[200px] md:max-w-xs" title={title}>
|
<h1 class="text-[16px] font-semibold text-[var(--color-ink)] tracking-tight truncate max-w-[200px] md:max-w-xs" title={title}>
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex items-center gap-0.5 text-[13px] font-medium text-gray-600 dark:text-gray-300 -ml-1 action-menu-container">
|
<div class="flex items-center gap-0.5 text-[13px] font-medium text-[var(--color-ink-muted)] -ml-1 action-menu-container">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }}
|
onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }}
|
||||||
@@ -453,7 +455,7 @@
|
|||||||
<button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)]">HTML (.html)</button>
|
<button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)]">HTML (.html)</button>
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
||||||
<button onclick={() => { activeMenu = null; deleteDoc(); }} class="w-full text-left px-4 py-1.5 text-sm text-red-500 hover:bg-red-500/10">Delete</button>
|
<button onclick={() => { activeMenu = null; deleteDoc(); }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10">Delete</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -493,7 +495,7 @@
|
|||||||
Version History
|
Version History
|
||||||
</button>
|
</button>
|
||||||
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
||||||
<button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; document.documentElement.classList.toggle('dark', $darkModeStore); }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)] flex items-center justify-between">
|
<button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--theme-text)] hover:bg-[var(--theme-border)] flex items-center justify-between">
|
||||||
Dark Mode
|
Dark Mode
|
||||||
<Icon icon={$darkModeStore ? "mdi:check" : ""} class="text-sm" />
|
<Icon icon={$darkModeStore ? "mdi:check" : ""} class="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -510,7 +512,7 @@
|
|||||||
<div class="flex items-center -space-x-2 mr-2">
|
<div class="flex items-center -space-x-2 mr-2">
|
||||||
{#each $connectedUsers as user}
|
{#each $connectedUsers as user}
|
||||||
<div
|
<div
|
||||||
class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-white dark:border-zinc-950 shadow-sm"
|
class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-[var(--color-surface)] shadow-sm"
|
||||||
style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};"
|
style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};"
|
||||||
title={user.name + (user.isLocal ? ' (You)' : '')}
|
title={user.name + (user.isLocal ? ' (You)' : '')}
|
||||||
>
|
>
|
||||||
@@ -522,21 +524,21 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5 px-2">
|
<div class="flex items-center gap-1.5 px-2">
|
||||||
<a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" title="Typst Docs">
|
<a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors" title="Typst Docs">
|
||||||
<Icon icon="mdi:book-open-page-variant-outline" class="text-[18px]" />
|
<Icon icon="mdi:book-open-page-variant-outline" class="text-[18px]" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-gray-500 hover:text-purple-500 dark:text-gray-400 dark:hover:text-purple-400 rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" title="Typst Universe">
|
<a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors" title="Typst Universe">
|
||||||
<Icon icon="mdi:earth" class="text-[18px]" />
|
<Icon icon="mdi:earth" class="text-[18px]" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-5 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
|
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<button
|
<button
|
||||||
onclick={() => (isPresentationOpen = true)}
|
onclick={() => (isPresentationOpen = true)}
|
||||||
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors"
|
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:presentation-play" class="text-[16px]" />
|
<Icon icon="mdi:presentation-play" class="text-[16px]" />
|
||||||
Present
|
Present
|
||||||
@@ -544,7 +546,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={() => ($commentsSidebarOpen = !$commentsSidebarOpen)}
|
onclick={() => ($commentsSidebarOpen = !$commentsSidebarOpen)}
|
||||||
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors"
|
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:comment-outline" class="text-[16px]" />
|
<Icon icon="mdi:comment-outline" class="text-[16px]" />
|
||||||
Comments
|
Comments
|
||||||
@@ -552,27 +554,27 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={() => (isShareModalOpen = true)}
|
onclick={() => (isShareModalOpen = true)}
|
||||||
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors"
|
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" x2="12" y1="2" y2="15"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" x2="12" y1="2" y2="15"/></svg>
|
||||||
Share
|
Share
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-5 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={() => ($previewOpenStore = !$previewOpenStore)}
|
onclick={() => ($previewOpenStore = !$previewOpenStore)}
|
||||||
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10' : 'text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20 dark:hover:bg-blue-900/40'}"
|
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)]' : 'text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90'}"
|
||||||
title={$previewOpenStore ? 'Hide preview' : 'Show preview'}
|
title={$previewOpenStore ? 'Hide preview' : 'Show preview'}
|
||||||
>
|
>
|
||||||
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" />
|
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" />
|
||||||
Preview
|
Preview
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-5 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" title="Print Document">
|
<button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Print Document">
|
||||||
<Icon icon="mdi:printer" class="text-[16px]" />
|
<Icon icon="mdi:printer" class="text-[16px]" />
|
||||||
Print
|
Print
|
||||||
</button>
|
</button>
|
||||||
@@ -580,7 +582,7 @@
|
|||||||
<div class="relative action-menu-container">
|
<div class="relative action-menu-container">
|
||||||
<button
|
<button
|
||||||
onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }}
|
onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }}
|
||||||
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20 dark:hover:bg-blue-900/40 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-blue-500/30' : ''}"
|
class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-[var(--color-accent)]/30' : ''}"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:export-variant" class="text-[16px]" />
|
<Icon icon="mdi:export-variant" class="text-[16px]" />
|
||||||
Export
|
Export
|
||||||
@@ -607,50 +609,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex items-center px-4 py-1.5 bg-white/50 dark:bg-black/10 border-t border-gray-200/60 dark:border-white/10 gap-4 overflow-x-auto no-scrollbar">
|
<div class="flex items-center px-4 py-1.5 bg-[var(--color-surface-muted)] border-t border-[var(--color-line)] gap-4 overflow-x-auto no-scrollbar">
|
||||||
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bold">
|
<button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bold">
|
||||||
<Icon icon="mdi:format-bold" class="text-lg" />
|
<Icon icon="mdi:format-bold" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Italic">
|
<button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Italic">
|
||||||
<Icon icon="mdi:format-italic" class="text-lg" />
|
<Icon icon="mdi:format-italic" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Code">
|
<button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Code">
|
||||||
<Icon icon="mdi:code-tags" class="text-lg" />
|
<Icon icon="mdi:code-tags" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
|
||||||
<button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Math (Inline)">
|
<button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Math (Inline)">
|
||||||
<Icon icon="mdi:sigma" class="text-lg" />
|
<Icon icon="mdi:sigma" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => applyFormat('$ \n ', '\n$ ', 'x = y')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Math (Block)">
|
<button onclick={() => applyFormat('$ \n ', '\n$ ', 'x = y')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Math (Block)">
|
||||||
<Icon icon="mdi:math-integral" class="text-lg" />
|
<Icon icon="mdi:math-integral" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
|
||||||
<button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bullet List">
|
<button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bullet List">
|
||||||
<Icon icon="mdi:format-list-bulleted" class="text-lg" />
|
<Icon icon="mdi:format-list-bulleted" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Numbered List">
|
<button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Numbered List">
|
||||||
<Icon icon="mdi:format-list-numbered" class="text-lg" />
|
<Icon icon="mdi:format-list-numbered" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
|
||||||
<input type="file" bind:this={fileInput} onchange={handleImageUpload} class="hidden" accept="image/*,.ttf,.otf" />
|
<input type="file" bind:this={fileInput} onchange={handleImageUpload} class="hidden" accept="image/*,.ttf,.otf" />
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<button onclick={() => fileInput?.click()} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Upload Image / Font">
|
<button onclick={() => fileInput?.click()} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Upload Image / Font">
|
||||||
<Icon icon="mdi:image-plus" class="text-lg" />
|
<Icon icon="mdi:image-plus" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
|
<label for="font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
|
||||||
<select
|
<select
|
||||||
id="font-select"
|
id="font-select"
|
||||||
onchange={(e) => insertTypstConfig('text', `font: "${e.currentTarget.value}"`)}
|
onchange={(e) => insertTypstConfig('text', `font: "${e.currentTarget.value}"`)}
|
||||||
class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] text-xs rounded shadow-sm focus:ring-blue-500 focus:border-blue-500 block py-1 pl-2 pr-6 appearance-none cursor-pointer transition-colors"
|
class="bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] text-xs rounded shadow-sm focus:border-[var(--color-accent)] focus:outline-none block py-1 pl-2 pr-6 appearance-none cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="New Computer Modern">Default (New CM)</option>
|
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="New Computer Modern">Default (New CM)</option>
|
||||||
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="Libertinus Serif">Libertinus Serif</option>
|
<option class="bg-[var(--theme-bg)] text-[var(--theme-text)]" value="Libertinus Serif">Libertinus Serif</option>
|
||||||
@@ -666,7 +668,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
@@ -680,29 +682,29 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 bg-white dark:bg-black/20 border border-gray-300 dark:border-white/20 rounded shadow-sm overflow-hidden">
|
<div class="flex items-center gap-1 bg-[var(--color-surface)] border border-[var(--color-line)] rounded shadow-sm overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)}
|
onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)}
|
||||||
class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors"
|
class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors"
|
||||||
title="Zoom Out"
|
title="Zoom Out"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:minus" class="text-sm" />
|
<Icon icon="mdi:minus" class="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
<span role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter") $documentZoomStore = 100; }} class="text-[11px] font-semibold text-gray-700 dark:text-gray-200 min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>
|
<span role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter") $documentZoomStore = 100; }} class="text-[11px] font-semibold text-[var(--color-ink-muted)] min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>
|
||||||
{$documentZoomStore}%
|
{$documentZoomStore}%
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)}
|
onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)}
|
||||||
class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors"
|
class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors"
|
||||||
title="Zoom In"
|
title="Zoom In"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:plus" class="text-sm" />
|
<Icon icon="mdi:plus" class="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ThemePicker />
|
<ThemePicker />
|
||||||
@@ -735,105 +737,52 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showInfoModal}
|
{#if showInfoModal}
|
||||||
|
<Modal title={docInfo?.title || title} icon="ph:file-text" onclose={() => showInfoModal = false}>
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={() => showInfoModal = false} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { showInfoModal = false; } }}>
|
<div class="flex flex-col gap-4 text-xs">
|
||||||
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
|
|
||||||
<div 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>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p>
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p>
|
||||||
<p class="text-sm text-gray-900 dark:text-gray-100 capitalize">Document</p>
|
<p class="text-sm text-[var(--color-ink)] capitalize">Document</p>
|
||||||
</div>
|
</div>
|
||||||
{#if docInfo?.created_at}
|
{#if docInfo?.created_at}
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Created At</p>
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Created</p>
|
||||||
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(docInfo.created_at).toLocaleString()}</p>
|
<p class="text-sm text-[var(--color-ink)]">{new Date(docInfo.created_at).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if docInfo?.updated_at}
|
{#if docInfo?.updated_at}
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Last Modified</p>
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
|
||||||
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(docInfo.updated_at).toLocaleString()}</p>
|
<p class="text-sm text-[var(--color-ink)]">{new Date(docInfo.updated_at).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</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 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">
|
{#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
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showRenameModal}
|
{#if showRenameModal}
|
||||||
|
<PromptModal
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={() => showRenameModal = false} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { showRenameModal = false; } }}>
|
title="Rename"
|
||||||
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
|
label="New name"
|
||||||
<form onsubmit={submitRename} class="p-6">
|
icon="ph:pencil-simple"
|
||||||
<div class="flex items-center gap-3 mb-6">
|
value={renameTitle}
|
||||||
<div class="p-2 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-lg">
|
confirmLabel="Save"
|
||||||
<Icon icon="mdi:pencil-outline" class="text-xl" />
|
onsubmit={submitRename}
|
||||||
</div>
|
onclose={() => showRenameModal = false}
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Rename</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
bind:value={renameTitle}
|
|
||||||
class="w-full bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="Enter new name"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-6 flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={() => showRenameModal = false} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDeleteModal}
|
{#if showDeleteModal}
|
||||||
|
<ConfirmModal
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={() => showDeleteModal = false} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { showDeleteModal = false; } }}>
|
title="Delete document"
|
||||||
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
|
message="Are you sure you want to delete this document? This action cannot be undone."
|
||||||
<div class="p-6">
|
confirmLabel="Delete"
|
||||||
<div class="flex items-center gap-3 mb-6">
|
onconfirm={confirmDelete}
|
||||||
<div class="p-2 bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 rounded-lg">
|
onclose={() => showDeleteModal = false}
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-xl" />
|
/>
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Delete Document</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm mb-6">
|
|
||||||
Are you sure you want to delete this document? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={() => showDeleteModal = false} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="button" onclick={confirmDelete} class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -55,13 +55,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed right-0 top-0 bottom-0 w-80 bg-[var(--theme-bg)] backdrop-blur-xl border-l shadow-2xl flex flex-col z-[70] transform transition-transform duration-300 border-[var(--theme-border)]">
|
<div class="fixed right-0 top-0 bottom-0 w-80 bg-[var(--theme-bg)] backdrop-blur-xl border-l shadow-2xl flex flex-col z-[70] transform transition-transform duration-300 border-[var(--theme-border)]">
|
||||||
<div class="flex items-center justify-between px-4 py-3 border-b bg-gray-50/50 bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
<div class="flex items-center justify-between px-4 py-3 border-b bg-[var(--theme-bg)] text-[var(--theme-text)] border-[var(--theme-border)]">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon="mdi:history" class="text-lg" />
|
<Icon icon="mdi:history" class="text-lg" />
|
||||||
<h2 class="text-sm font-semibold text-[var(--theme-text)]">Version History</h2>
|
<h2 class="text-sm font-semibold text-[var(--theme-text)]">Version History</h2>
|
||||||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{versions.length}</span>
|
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full">{versions.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={onClose} class="p-1.5 hover:text-gray-600 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-white/10 rounded-md transition-colors" title="Close Version History">
|
<button onclick={onClose} class="p-1.5 hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Close Version History">
|
||||||
<Icon icon="mdi:close" class="text-lg" />
|
<Icon icon="mdi:close" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<Icon icon="mdi:loading" class="animate-spin text-2xl" />
|
<Icon icon="mdi:loading" class="animate-spin text-2xl" />
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="text-red-500 text-sm text-center p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/30">
|
<div class="text-[var(--color-danger)] text-sm text-center p-4 bg-[var(--color-danger)]/10 rounded-md border border-[var(--color-danger)]/20">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{:else if versions.length === 0}
|
{:else if versions.length === 0}
|
||||||
@@ -96,11 +96,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 mt-2">
|
<div class="flex gap-2 mt-2">
|
||||||
<button onclick={() => previewVersion = version} class="flex-1 px-3 py-1.5 hover:bg-gray-200 dark:hover:bg-white/20 text-xs font-medium rounded-lg transition-colors flex items-center justify-center gap-1.5">
|
<button onclick={() => previewVersion = version} class="flex-1 px-3 py-1.5 hover:bg-[var(--color-surface-sunken)] text-xs font-medium rounded-md transition-colors flex items-center justify-center gap-1.5">
|
||||||
<Icon icon="mdi:eye" class="text-sm" />
|
<Icon icon="mdi:eye" class="text-sm" />
|
||||||
Preview
|
Preview
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => restoreVersion(version)} class="flex-1 px-3 py-1.5 bg-purple-50 text-purple-700 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/40 text-xs font-medium rounded-lg transition-colors flex items-center justify-center gap-1.5">
|
<button onclick={() => restoreVersion(version)} class="flex-1 px-3 py-1.5 bg-purple-50 text-purple-700 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/40 text-xs font-medium rounded-md transition-colors flex items-center justify-center gap-1.5">
|
||||||
<Icon icon="mdi:restore" class="text-sm" />
|
<Icon icon="mdi:restore" class="text-sm" />
|
||||||
Restore
|
Restore
|
||||||
</button>
|
</button>
|
||||||
@@ -120,17 +120,17 @@
|
|||||||
<h3 class="text-lg font-semibold text-[var(--theme-text)]">Previewing Version</h3>
|
<h3 class="text-lg font-semibold text-[var(--theme-text)]">Previewing Version</h3>
|
||||||
<span class="text-sm">{formatDate(previewVersion.created_at)}</span>
|
<span class="text-sm">{formatDate(previewVersion.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={() => previewVersion = null} class="p-1.5 hover:text-gray-600 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-white/10 rounded-md transition-colors" title="Close Preview">
|
<button onclick={() => previewVersion = null} class="p-1.5 hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Close Preview">
|
||||||
<Icon icon="mdi:close" class="text-xl" />
|
<Icon icon="mdi:close" class="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-auto p-6 bg-gray-50/50 bg-[var(--theme-bg)] text-[var(--theme-text)]">
|
<div class="flex-1 overflow-auto p-6 bg-[var(--theme-bg)] text-[var(--theme-text)]">
|
||||||
<pre class="text-sm font-mono whitespace-pre-wrap word-break-break-word">{previewVersion.content}</pre>
|
<pre class="text-sm font-mono whitespace-pre-wrap word-break-break-word">{previewVersion.content}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 border-t flex justify-end gap-3 bg-white/50 rounded-b-2xl border-[var(--theme-border)]">
|
<div class="p-4 border-t flex justify-end gap-3 bg-[var(--color-surface)] rounded-b-2xl border-[var(--theme-border)]">
|
||||||
<button onclick={() => previewVersion = null} class="px-4 py-2 text-sm font-medium hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
<button onclick={() => previewVersion = null} class="px-4 py-2 text-sm font-medium hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors">
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => restoreVersion(previewVersion!)} class="bg-purple-600 hover:bg-purple-700 px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
|
<button onclick={() => restoreVersion(previewVersion!)} class="bg-purple-600 hover:bg-purple-700 px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '@iconify/svelte';
|
|
||||||
let { createDoc, onClose } = $props<{
|
|
||||||
createDoc: (title: string) => void,
|
|
||||||
onClose: () => void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let newDocTitle = $state('Untitled Document');
|
|
||||||
|
|
||||||
function onSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
createDoc(newDocTitle);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
|
|
||||||
<div tabindex="-1" class="bg-[var(--theme-bg)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md overflow-hidden" role="dialog" aria-modal="true" aria-labelledby="create-doc-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
|
||||||
<div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-white/10">
|
|
||||||
<h2 id="create-doc-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:file-document-plus" class="text-blue-500 text-xl" />
|
|
||||||
Create Document
|
|
||||||
</h2>
|
|
||||||
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
|
|
||||||
<Icon icon="mdi:close" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onsubmit={onSubmit} class="p-5 space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label for="doc-title-input" class="text-sm font-medium text-gray-700 dark:text-gray-300 block">Document Title</label>
|
|
||||||
<input
|
|
||||||
id="doc-title-input"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
bind:value={newDocTitle}
|
|
||||||
class="w-full bg-black/5 dark:bg-white/5 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="Untitled Document"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:plus" class="text-lg" />
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '@iconify/svelte';
|
|
||||||
let { createFolder, onClose } = $props<{
|
|
||||||
createFolder: (name: string) => void,
|
|
||||||
onClose: () => void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let newFolderName = $state('New Folder');
|
|
||||||
|
|
||||||
function onSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
createFolder(newFolderName);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
|
|
||||||
<div tabindex="-1" class="bg-[var(--theme-bg)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md overflow-hidden" role="dialog" aria-modal="true" aria-labelledby="create-folder-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
|
||||||
<div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-white/10">
|
|
||||||
<h2 id="create-folder-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:folder-plus" class="text-yellow-500 text-xl" />
|
|
||||||
Create Folder
|
|
||||||
</h2>
|
|
||||||
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
|
|
||||||
<Icon icon="mdi:close" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onsubmit={onSubmit} class="p-5 space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label for="folder-title-input" class="text-sm font-medium text-gray-700 dark:text-gray-300 block">Folder Name</label>
|
|
||||||
<input
|
|
||||||
id="folder-title-input"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
bind:value={newFolderName}
|
|
||||||
class="w-full bg-black/5 dark:bg-white/5 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="New Folder"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="bg-yellow-500 hover:bg-yellow-600 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:plus" class="text-lg" />
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '@iconify/svelte';
|
|
||||||
let { createProject, onClose } = $props<{
|
|
||||||
createProject: (name: string) => void,
|
|
||||||
onClose: () => void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let newProjectName = $state('Untitled Project');
|
|
||||||
|
|
||||||
function onSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
createProject(newProjectName);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 animate-in fade-in duration-200" role="presentation" onclick={onClose}>
|
|
||||||
<div tabindex="-1" class="bg-[var(--theme-bg)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md overflow-hidden" role="dialog" aria-modal="true" aria-labelledby="create-project-title" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
|
||||||
<div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-white/10">
|
|
||||||
<h2 id="create-project-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:folder-multiple-plus" class="text-blue-500 text-xl" />
|
|
||||||
Create Project
|
|
||||||
</h2>
|
|
||||||
<button onclick={onClose} aria-label="Close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full p-1 transition-colors">
|
|
||||||
<Icon icon="mdi:close" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onsubmit={onSubmit} class="p-5 space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label for="project-name-input" class="text-sm font-medium text-gray-700 dark:text-gray-300 block">Project Name</label>
|
|
||||||
<input
|
|
||||||
id="project-name-input"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
bind:value={newProjectName}
|
|
||||||
class="w-full bg-black/5 dark:bg-white/5 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="Untitled Project"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">A multi-file workspace, seeded with a <code class="font-mono">typst.toml</code> and <code class="font-mono">main.typ</code>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm flex items-center gap-2">
|
|
||||||
<Icon icon="mdi:plus" class="text-lg" />
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '@iconify/svelte';
|
|
||||||
let { deleteTarget, confirmDelete, onClose } = $props<{
|
|
||||||
deleteTarget: {id: string, type: 'document'|'folder'|'file', name: string},
|
|
||||||
confirmDelete: () => void,
|
|
||||||
onClose: () => void
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 transition-opacity" onclick={onClose} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { onClose(e); } }}>
|
|
||||||
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex items-center gap-3 mb-6">
|
|
||||||
<div class="p-2 bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 rounded-lg">
|
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-xl" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Delete {deleteTarget.type}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm mb-6">
|
|
||||||
Are you sure you want to delete <span class="font-semibold text-gray-900 dark:text-white">{deleteTarget.name}</span>?
|
|
||||||
{#if deleteTarget.type === 'folder'}This will also delete all of its contents.{/if}
|
|
||||||
This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="button" onclick={confirmDelete} class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 flex flex-col hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500/50 transition-all duration-200 relative group transform hover:-translate-y-1 cursor-pointer overflow-visible"
|
class="bg-[var(--color-surface)] rounded-lg shadow-sm border border-[var(--color-line)] flex flex-col hover:border-[var(--color-accent)] transition relative group cursor-pointer overflow-visible"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={() => goto(`/doc/${doc.id}`)}
|
onclick={() => goto(`/doc/${doc.id}`)}
|
||||||
@@ -41,13 +41,13 @@
|
|||||||
ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'document', id: doc.id }))}
|
ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'document', id: doc.id }))}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="h-40 w-full bg-gray-50 dark:bg-black/40 rounded-t-xl overflow-hidden flex items-center justify-center border-b border-gray-100 dark:border-white/10 relative pointer-events-none">
|
<div class="h-40 w-full bg-[var(--color-surface-muted)] rounded-t-lg overflow-hidden flex items-center justify-center border-b border-[var(--color-line)] relative pointer-events-none">
|
||||||
{#if doc.thumbnail_svg}
|
{#if doc.thumbnail_svg}
|
||||||
<div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110">
|
<div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110">
|
||||||
<img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(doc.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" />
|
<img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(doc.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="p-4 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full transition-transform duration-300 group-hover:scale-110">
|
<div class="p-4 bg-[var(--color-accent-soft)] text-[var(--color-accent)] rounded-full transition-transform duration-300 group-hover:scale-110">
|
||||||
<Icon icon="mdi:file-document" class="text-4xl" />
|
<Icon icon="mdi:file-document" class="text-4xl" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -56,34 +56,34 @@
|
|||||||
|
|
||||||
<div class="p-4 flex flex-col flex-grow">
|
<div class="p-4 flex flex-col flex-grow">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate pr-2 pointer-events-none" title={doc.title}>{doc.title}</h3>
|
<h3 class="text-lg font-semibold text-[var(--color-ink)] truncate pr-2 pointer-events-none" title={doc.title}>{doc.title}</h3>
|
||||||
|
|
||||||
|
|
||||||
<div class="relative action-menu-container">
|
<div class="relative action-menu-container">
|
||||||
<button
|
<button
|
||||||
aria-label="Document actions"
|
aria-label="Document actions"
|
||||||
onclick={toggleMenu}
|
onclick={toggleMenu}
|
||||||
class="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 pointer-events-auto"
|
class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors p-1 rounded-full hover:bg-[var(--color-surface-sunken)] pointer-events-auto"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:dots-vertical" class="text-xl" />
|
<Icon icon="mdi:dots-vertical" class="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if activeMenu === doc.id}
|
{#if activeMenu === doc.id}
|
||||||
<div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-gray-200 dark:border-white/10 py-1 z-[100]">
|
<div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--color-surface)] rounded-md shadow-xl border border-[var(--color-line)] py-1 z-[100]">
|
||||||
<button onclick={(e) => { e.stopPropagation(); openInfo(doc, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); openInfo(doc, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:information-outline" class="text-lg text-blue-500" />
|
<Icon icon="mdi:information-outline" class="text-lg text-blue-500" />
|
||||||
View Info
|
View Info
|
||||||
</button>
|
</button>
|
||||||
<button onclick={(e) => { e.stopPropagation(); openRename(doc.id, doc.title, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); openRename(doc.id, doc.title, 'document'); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" />
|
<Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" />
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
<button onclick={(e) => { e.stopPropagation(); shareItem(doc); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); shareItem(doc); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:share-variant-outline" class="text-lg text-green-500" />
|
<Icon icon="mdi:share-variant-outline" class="text-lg text-green-500" />
|
||||||
Share
|
Share
|
||||||
</button>
|
</button>
|
||||||
<div class="h-px bg-gray-200 dark:bg-white/10 my-1"></div>
|
<div class="h-px bg-[var(--color-line)] my-1"></div>
|
||||||
<button onclick={(e) => { e.stopPropagation(); deleteDoc(doc.id, doc.title); }} class="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-500/10 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); deleteDoc(doc.id, doc.title); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-2">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-lg" />
|
<Icon icon="mdi:trash-can-outline" class="text-lg" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-2 pointer-events-none">
|
<p class="text-xs text-[var(--color-ink-muted)] flex items-center gap-1 mt-2 pointer-events-none">
|
||||||
<Icon icon="mdi:clock-outline" class="text-sm" />
|
<Icon icon="mdi:clock-outline" class="text-sm" />
|
||||||
Edited {new Date(doc.updated_at.endsWith('Z') ? doc.updated_at : doc.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
Edited {new Date(doc.updated_at.endsWith('Z') ? doc.updated_at : doc.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 flex flex-col transition-all duration-200 relative group transform hover:-translate-y-1 {isFont ? 'cursor-default' : 'hover:shadow-lg hover:border-green-400 dark:hover:border-green-500/50 cursor-pointer'}"
|
class="bg-[var(--color-surface)] rounded-lg shadow-sm border border-[var(--color-line)] p-6 flex flex-col transition relative group {isFont ? 'cursor-default' : 'hover:border-[var(--color-accent)] cursor-pointer'}"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={handleOpen}
|
onclick={handleOpen}
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'file', id: file.id }))}
|
ondragstart={(e) => e.dataTransfer?.setData('text/plain', JSON.stringify({ type: 'file', id: file.id }))}
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between mb-4 pointer-events-none">
|
<div class="flex items-start justify-between mb-4 pointer-events-none">
|
||||||
<div class="p-3 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-lg overflow-hidden flex items-center justify-center w-12 h-12">
|
<div class="p-3 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-md overflow-hidden flex items-center justify-center w-12 h-12">
|
||||||
{#if file.mime_type.startsWith('image/')}
|
{#if file.mime_type.startsWith('image/')}
|
||||||
<img src={`/api/files/${file.id}/data`} alt={file.name} class="w-full h-full object-cover rounded" draggable="false" />
|
<img src={`/api/files/${file.id}/data`} alt={file.name} class="w-full h-full object-cover rounded" draggable="false" />
|
||||||
{:else if isFont}
|
{:else if isFont}
|
||||||
@@ -34,12 +34,12 @@
|
|||||||
<Icon icon="mdi:file-outline" class="text-2xl" />
|
<Icon icon="mdi:file-outline" class="text-2xl" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button aria-label="Delete file" onclick={(e) => { e.stopPropagation(); deleteFile(file.id, file.name); }} class="pointer-events-auto text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity bg-gray-50 hover:bg-red-50 dark:bg-white/5 dark:hover:bg-red-900/20 rounded-full p-2 shadow-sm border border-gray-100 dark:border-white/10">
|
<button aria-label="Delete file" onclick={(e) => { e.stopPropagation(); deleteFile(file.id, file.name); }} class="pointer-events-auto text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] opacity-0 group-hover:opacity-100 transition-opacity bg-[var(--color-surface-muted)] hover:bg-[var(--color-danger)]/10 rounded-full p-2 shadow-sm border border-[var(--color-line)]">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-lg" />
|
<Icon icon="mdi:trash-can-outline" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate mb-1 pointer-events-none" title={file.name}>{file.name}</h3>
|
<h3 class="text-lg font-semibold text-[var(--color-ink)] truncate mb-1 pointer-events-none" title={file.name}>{file.name}</h3>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-auto pt-4 border-t border-gray-100 dark:border-white/10 pointer-events-none">
|
<p class="text-xs text-[var(--color-ink-muted)] flex items-center gap-1 mt-auto pt-4 border-t border-[var(--color-line)] pointer-events-none">
|
||||||
<Icon icon="mdi:clock-outline" class="text-sm" />
|
<Icon icon="mdi:clock-outline" class="text-sm" />
|
||||||
Uploaded {new Date(file.created_at ? (file.created_at.endsWith('Z') ? file.created_at : file.created_at + 'Z') : Date.now()).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
Uploaded {new Date(file.created_at ? (file.created_at.endsWith('Z') ? file.created_at : file.created_at + 'Z') : Date.now()).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center p-3 bg-white/50 dark:bg-black/20 backdrop-blur-sm border border-gray-200 dark:border-white/10 rounded-xl shadow-sm hover:shadow-md cursor-pointer group transition-all duration-200 relative {dragOverFolderId === folder.id ? 'ring-2 ring-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'hover:-translate-y-0.5 hover:border-gray-300 dark:hover:border-white/20'}"
|
class="flex flex-row items-center p-3 bg-[var(--color-surface)] border border-[var(--color-line)] rounded-lg shadow-sm cursor-pointer group transition relative {dragOverFolderId === folder.id ? 'ring-2 ring-[var(--color-accent)] bg-[var(--color-accent-soft)]' : 'hover:border-[var(--color-accent)]'}"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={() => navigateToFolder(folder)}
|
onclick={() => navigateToFolder(folder)}
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
ondragleave={() => setDragOverFolderId(null)}
|
ondragleave={() => setDragOverFolderId(null)}
|
||||||
ondrop={(e) => handleDrop(e, folder.id)}
|
ondrop={(e) => handleDrop(e, folder.id)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center w-10 h-10 bg-yellow-50 dark:bg-yellow-500/10 rounded-lg group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3">
|
<div class="flex items-center justify-center w-10 h-10 bg-yellow-50 dark:bg-yellow-500/10 rounded-md group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3">
|
||||||
<Icon icon="mdi:folder" class="text-2xl text-yellow-500" />
|
<Icon icon="mdi:folder" class="text-2xl text-yellow-500" />
|
||||||
</div>
|
</div>
|
||||||
<span class="font-medium text-gray-900 dark:text-white text-sm truncate w-full pointer-events-none pr-6">{folder.name}</span>
|
<span class="font-medium text-[var(--color-ink)] text-sm truncate w-full pointer-events-none pr-6">{folder.name}</span>
|
||||||
|
|
||||||
<button aria-label="Delete folder" onclick={(e) => { e.stopPropagation(); deleteFolder(folder.id, folder.name); }} class="absolute top-1/2 -translate-y-1/2 right-2 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-red-50 dark:hover:bg-red-900/20 shrink-0">
|
<button aria-label="Delete folder" onclick={(e) => { e.stopPropagation(); deleteFolder(folder.id, folder.name); }} class="absolute top-1/2 -translate-y-1/2 right-2 text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded hover:bg-[var(--color-danger)]/10 shrink-0">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-base" />
|
<Icon icon="mdi:trash-can-outline" class="text-base" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '@iconify/svelte';
|
import Modal from '../Modal.svelte';
|
||||||
let { selectedInfo, onClose } = $props<{
|
let { selectedInfo, onClose } = $props<{
|
||||||
selectedInfo: {type: string, title?: string, name?: string, created_at: string, updated_at?: string},
|
selectedInfo: {type: string, title?: string, name?: string, created_at: string, updated_at?: string},
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const icon = $derived(selectedInfo.type === 'document' ? 'ph:file-text' : selectedInfo.type === 'folder' ? 'ph:folder' : 'ph:file');
|
||||||
|
const title = $derived(selectedInfo.title || selectedInfo.name || 'Details');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 transition-opacity" onclick={onClose} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { onClose(e); } }}>
|
<Modal {title} {icon} onclose={onClose}>
|
||||||
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
|
<div class="flex flex-col gap-4 text-xs">
|
||||||
<div class="p-6 border-b border-gray-100 dark:border-white/10">
|
|
||||||
<div 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>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p>
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p>
|
||||||
<p class="text-sm text-gray-900 dark:text-gray-100 capitalize">{selectedInfo.type}</p>
|
<p class="text-sm capitalize text-[var(--color-ink)]">{selectedInfo.type}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Created At</p>
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Created</p>
|
||||||
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(selectedInfo.created_at.endsWith('Z') ? selectedInfo.created_at : selectedInfo.created_at + 'Z').toLocaleString()}</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>
|
</div>
|
||||||
{#if selectedInfo.updated_at}
|
{#if selectedInfo.updated_at}
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Last Modified</p>
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
|
||||||
<p class="text-sm text-gray-900 dark:text-gray-100">{new Date(selectedInfo.updated_at.endsWith('Z') ? selectedInfo.updated_at : selectedInfo.updated_at + 'Z').toLocaleString()}</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>
|
||||||
{/if}
|
{/if}
|
||||||
</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 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">
|
{#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
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -11,44 +11,44 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="bg-[var(--theme-bg)] shadow-sm border-b border-gray-200 dark:border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200">
|
<nav class="bg-[var(--color-surface)] shadow-sm border-b border-[var(--color-line)] px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
<h1 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-3">
|
||||||
<Icon icon="mdi:script-text" class="text-blue-600 dark:text-blue-400 text-3xl" />
|
<span class="flex h-14 w-14 items-center justify-center rounded-lg bg-[var(--color-accent)]">
|
||||||
|
<img src="/favicon.png" alt="TypstDrive" class="h-10 w-10" />
|
||||||
|
</span>
|
||||||
TypstDrive
|
TypstDrive
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300 font-medium">
|
<div class="flex items-center gap-2 text-[var(--color-ink-muted)] font-medium">
|
||||||
<Icon icon="mdi:account-circle" class="text-xl" />
|
<Icon icon="mdi:account-circle" class="text-xl" />
|
||||||
{$userStore?.username}
|
{$userStore?.username}
|
||||||
</div>
|
</div>
|
||||||
<div class="h-6 w-px bg-gray-300 dark:bg-white/20"></div>
|
<div class="h-6 w-px bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<a href="/projects" class="text-sm font-medium text-gray-600 hover:text-blue-600 dark:text-gray-300 dark:hover:text-blue-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Projects">
|
<a href="/projects" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Projects">
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-xl" />
|
<Icon icon="mdi:folder-multiple-outline" class="text-xl" />
|
||||||
</a>
|
</a>
|
||||||
<a href="/packages" class="text-sm font-medium text-gray-600 hover:text-purple-600 dark:text-gray-300 dark:hover:text-purple-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Packages">
|
<a href="/packages" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Packages">
|
||||||
<Icon icon="mdi:package-variant-closed" class="text-xl" />
|
<Icon icon="mdi:package-variant-closed" class="text-xl" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="/api-docs" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="API Docs">
|
||||||
<a href="/api-docs" class="text-sm font-medium text-gray-600 hover:text-green-600 dark:text-gray-300 dark:hover:text-green-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="API Docs">
|
|
||||||
<Icon icon="mdi:api" class="text-xl" />
|
<Icon icon="mdi:api" class="text-xl" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-gray-600 hover:text-blue-500 dark:text-gray-300 dark:hover:text-blue-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Typst Docs">
|
<a href="https://typst.app/docs/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Typst Docs">
|
||||||
<Icon icon="mdi:book-open-page-variant-outline" class="text-xl" />
|
<Icon icon="mdi:book-open-page-variant-outline" class="text-xl" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-gray-600 hover:text-purple-500 dark:text-gray-300 dark:hover:text-purple-400 transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Typst Universe">
|
<a href="https://typst.app/universe/" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Typst Universe">
|
||||||
<Icon icon="mdi:earth" class="text-xl" />
|
<Icon icon="mdi:earth" class="text-xl" />
|
||||||
</a>
|
</a>
|
||||||
<div class="h-6 w-px bg-gray-300 dark:bg-white/20"></div>
|
<div class="h-6 w-px bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
|
|
||||||
<ThemePicker />
|
<ThemePicker />
|
||||||
|
|
||||||
<button onclick={() => goto('/settings')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-3 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10" title="Settings">
|
<button onclick={() => goto('/settings')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-3 py-2 rounded-md flex items-center gap-2" title="Settings">
|
||||||
<Icon icon="mdi:cog" class="text-2xl" />
|
<Icon icon="mdi:cog" class="text-2xl" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={logout} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-4 py-2 rounded-lg flex items-center gap-2 border border-transparent dark:border-white/10">
|
<button onclick={logout} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-4 py-2 rounded-md flex items-center gap-2">
|
||||||
<Icon icon="mdi:logout" class="text-lg" />
|
<Icon icon="mdi:logout" class="text-lg" />
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -27,19 +27,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 flex flex-col hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500/50 transition-all duration-200 relative group transform hover:-translate-y-1 cursor-pointer overflow-visible"
|
class="bg-[var(--color-surface)] rounded-lg shadow-sm border border-[var(--color-line)] flex flex-col hover:border-[var(--color-accent)] transition relative group cursor-pointer overflow-visible"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={() => goto(`/project/${project.id}`)}
|
onclick={() => goto(`/project/${project.id}`)}
|
||||||
onkeydown={(e) => e.key === 'Enter' && goto(`/project/${project.id}`)}
|
onkeydown={(e) => e.key === 'Enter' && goto(`/project/${project.id}`)}
|
||||||
>
|
>
|
||||||
<div class="h-40 w-full bg-gray-50 dark:bg-black/40 rounded-t-xl overflow-hidden flex items-center justify-center border-b border-gray-100 dark:border-white/10 relative pointer-events-none">
|
<div class="h-40 w-full bg-[var(--color-surface-muted)] rounded-t-lg overflow-hidden flex items-center justify-center border-b border-[var(--color-line)] relative pointer-events-none">
|
||||||
{#if project.thumbnail_svg}
|
{#if project.thumbnail_svg}
|
||||||
<div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110">
|
<div class="w-full h-full flex items-start justify-center bg-white transition-transform duration-300 group-hover:scale-110">
|
||||||
<img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(project.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" />
|
<img src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(project.thumbnail_svg)))}`} class="w-full h-auto shadow-sm border border-gray-200" alt="Thumbnail" draggable="false" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="p-4 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full transition-transform duration-300 group-hover:scale-110">
|
<div class="p-4 bg-[var(--color-accent-soft)] text-[var(--color-accent)] rounded-full transition-transform duration-300 group-hover:scale-110">
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-4xl" />
|
<Icon icon="mdi:folder-multiple-outline" class="text-4xl" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -47,29 +47,29 @@
|
|||||||
|
|
||||||
<div class="p-4 flex flex-col flex-grow">
|
<div class="p-4 flex flex-col flex-grow">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate pr-2 pointer-events-none" title={project.name}>{project.name}</h3>
|
<h3 class="text-lg font-semibold text-[var(--color-ink)] truncate pr-2 pointer-events-none" title={project.name}>{project.name}</h3>
|
||||||
|
|
||||||
<div class="relative action-menu-container">
|
<div class="relative action-menu-container">
|
||||||
<button aria-label="Project actions" onclick={toggleMenu} class="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 pointer-events-auto">
|
<button aria-label="Project actions" onclick={toggleMenu} class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors p-1 rounded-full hover:bg-[var(--color-surface-sunken)] pointer-events-auto">
|
||||||
<Icon icon="mdi:dots-vertical" class="text-xl" />
|
<Icon icon="mdi:dots-vertical" class="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if activeMenu === project.id}
|
{#if activeMenu === project.id}
|
||||||
<div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-gray-200 dark:border-white/10 py-1 z-[100]">
|
<div class="absolute right-0 {dropUp ? 'bottom-full mb-1' : 'top-full mt-1'} w-48 bg-[var(--color-surface)] rounded-md shadow-xl border border-[var(--color-line)] py-1 z-[100]">
|
||||||
<button onclick={(e) => { e.stopPropagation(); openInfo(project); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); openInfo(project); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:information-outline" class="text-lg text-blue-500" />
|
<Icon icon="mdi:information-outline" class="text-lg text-blue-500" />
|
||||||
View Info
|
View Info
|
||||||
</button>
|
</button>
|
||||||
<button onclick={(e) => { e.stopPropagation(); openRename(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); openRename(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" />
|
<Icon icon="mdi:pencil-outline" class="text-lg text-yellow-500" />
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
<button onclick={(e) => { e.stopPropagation(); goto(`/project/${project.id}`); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); goto(`/project/${project.id}`); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:open-in-new" class="text-lg text-green-500" />
|
<Icon icon="mdi:open-in-new" class="text-lg text-green-500" />
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
<div class="h-px bg-gray-200 dark:bg-white/10 my-1"></div>
|
<div class="h-px bg-[var(--color-line)] my-1"></div>
|
||||||
<button onclick={(e) => { e.stopPropagation(); deleteProject(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-500/10 flex items-center gap-2">
|
<button onclick={(e) => { e.stopPropagation(); deleteProject(project.id, project.name); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-2">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-lg" />
|
<Icon icon="mdi:trash-can-outline" class="text-lg" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-2 pointer-events-none">
|
<p class="text-xs text-[var(--color-ink-muted)] flex items-center gap-1 mt-2 pointer-events-none">
|
||||||
<Icon icon="mdi:clock-outline" class="text-sm" />
|
<Icon icon="mdi:clock-outline" class="text-sm" />
|
||||||
Edited {new Date(project.updated_at.endsWith('Z') ? project.updated_at : project.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
Edited {new Date(project.updated_at.endsWith('Z') ? project.updated_at : project.updated_at + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '@iconify/svelte';
|
|
||||||
let { initialTitle, handleRename, onClose } = $props<{
|
|
||||||
initialTitle: string,
|
|
||||||
handleRename: (newTitle: string) => void,
|
|
||||||
onClose: () => void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let renameTitle = $state("");
|
|
||||||
$effect(() => { renameTitle = initialTitle; });
|
|
||||||
|
|
||||||
function onSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleRename(renameTitle);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 transition-opacity" onclick={onClose} role="presentation" onkeydown={(e) => { if (e.key === "Enter") { onClose(e); } }}>
|
|
||||||
<div class="bg-white/90 dark:bg-black/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-white/10 w-full max-w-sm overflow-hidden transform transition-all" onclick={(e) => e.stopPropagation()} role="dialog" tabindex="-1" onkeydown={(e) => e.stopPropagation()}>
|
|
||||||
<form onsubmit={onSubmit} class="p-6">
|
|
||||||
<div class="flex items-center gap-3 mb-6">
|
|
||||||
<div class="p-2 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-lg">
|
|
||||||
<Icon icon="mdi:pencil-outline" class="text-xl" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Rename</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
bind:value={renameTitle}
|
|
||||||
class="w-full bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="Enter new name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-6 flex justify-end gap-3">
|
|
||||||
<button type="button" onclick={onClose} class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium transition-colors shadow-sm">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -54,15 +54,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col bg-[var(--theme-bg)] border-r border-gray-200 dark:border-white/10">
|
<div class="h-full flex flex-col bg-[var(--color-surface)] border-r border-[var(--color-line)]">
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10">
|
<div class="flex items-center justify-between px-3 py-2 border-b border-[var(--color-line)]">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Files</span>
|
<span class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)]">Files</span>
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button onclick={handleCreate} title="New file" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-white/10 text-gray-600 dark:text-gray-300">
|
<button onclick={handleCreate} title="New file" class="p-1 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:file-plus-outline" class="text-lg" />
|
<Icon icon="mdi:file-plus-outline" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => fileInput.click()} title="Upload file" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-white/10 text-gray-600 dark:text-gray-300">
|
<button onclick={() => fileInput.click()} title="Upload file" class="p-1 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:upload" class="text-lg" />
|
<Icon icon="mdi:upload" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<input bind:this={fileInput} type="file" multiple class="hidden" onchange={(e) => { const t = e.target as HTMLInputElement; if (t.files) onUpload(t.files); t.value = ''; }} />
|
<input bind:this={fileInput} type="file" multiple class="hidden" onchange={(e) => { const t = e.target as HTMLInputElement; if (t.files) onUpload(t.files); t.value = ''; }} />
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto py-1">
|
<div class="flex-1 overflow-y-auto py-1">
|
||||||
{#each files as file (file.id)}
|
{#each files as file (file.id)}
|
||||||
<div class="group flex items-center gap-1 px-2 py-1.5 text-sm cursor-pointer {activeFileId === file.id ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5'}">
|
<div class="group flex items-center gap-1 px-2 py-1.5 text-sm cursor-pointer {activeFileId === file.id ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}">
|
||||||
<button class="flex items-center gap-2 flex-1 min-w-0 text-left" onclick={() => onSelect(file)}>
|
<button class="flex items-center gap-2 flex-1 min-w-0 text-left" onclick={() => onSelect(file)}>
|
||||||
<Icon icon={iconFor(file.path)} class="text-base flex-shrink-0" />
|
<Icon icon={iconFor(file.path)} class="text-base flex-shrink-0" />
|
||||||
<span class="truncate">{file.path}</span>
|
<span class="truncate">{file.path}</span>
|
||||||
@@ -85,14 +85,14 @@
|
|||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
{#if file.kind === 'text' && file.path.toLowerCase().endsWith('.typ') && file.path !== entrypoint}
|
{#if file.kind === 'text' && file.path.toLowerCase().endsWith('.typ') && file.path !== entrypoint}
|
||||||
<button onclick={() => onSetEntry(file)} title="Set as entrypoint" class="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-white/10 text-gray-500">
|
<button onclick={() => onSetEntry(file)} title="Set as entrypoint" class="p-0.5 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:star-outline" class="text-sm" />
|
<Icon icon="mdi:star-outline" class="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button onclick={() => handleRename(file)} title="Rename" class="p-0.5 rounded hover:bg-gray-200 dark:hover:bg-white/10 text-gray-500">
|
<button onclick={() => handleRename(file)} title="Rename" class="p-0.5 rounded hover:bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:pencil-outline" class="text-sm" />
|
<Icon icon="mdi:pencil-outline" class="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => onDelete(file)} title="Delete" class="p-0.5 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-500 hover:text-red-600">
|
<button onclick={() => onDelete(file)} title="Delete" class="p-0.5 rounded hover:bg-[var(--color-danger)]/10 text-[var(--color-ink-muted)] hover:text-[var(--color-danger)]">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-sm" />
|
<Icon icon="mdi:trash-can-outline" class="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
import ThemePicker from '../ThemePicker.svelte';
|
import ThemePicker from '../ThemePicker.svelte';
|
||||||
import PageSettingsModal from '../PageSettingsModal.svelte';
|
import PageSettingsModal from '../PageSettingsModal.svelte';
|
||||||
import PresentationMode from '../PresentationMode.svelte';
|
import PresentationMode from '../PresentationMode.svelte';
|
||||||
|
import Modal from '../Modal.svelte';
|
||||||
|
import PromptModal from '../PromptModal.svelte';
|
||||||
|
import ConfirmModal from '../ConfirmModal.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
projectName = 'Project',
|
projectName = 'Project',
|
||||||
@@ -257,13 +260,12 @@
|
|||||||
return name.substring(0, 2).toUpperCase();
|
return name.substring(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitRename(e: Event) {
|
function submitRename(name: string) {
|
||||||
e.preventDefault();
|
if (name && name !== projectName) {
|
||||||
if (renameName && renameName !== projectName) {
|
|
||||||
fetch(`/api/projects/${projectId}`, {
|
fetch(`/api/projects/${projectId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: renameName })
|
body: JSON.stringify({ name })
|
||||||
}).then((res) => { if (res.ok) window.location.reload(); });
|
}).then((res) => { if (res.ok) window.location.reload(); });
|
||||||
}
|
}
|
||||||
showRenameModal = false;
|
showRenameModal = false;
|
||||||
@@ -289,17 +291,17 @@
|
|||||||
<header class="flex flex-col border-b border-[var(--theme-border)] bg-[var(--theme-bg)] text-[var(--theme-text)] select-none w-full relative z-[70]">
|
<header class="flex flex-col border-b border-[var(--theme-border)] bg-[var(--theme-bg)] text-[var(--theme-text)] select-none w-full relative z-[70]">
|
||||||
<div class="flex items-center justify-between px-4 py-2.5">
|
<div class="flex items-center justify-between px-4 py-2.5">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button onclick={() => goto('/projects')} class="p-1.5 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-white/10 transition-colors" title="Projects">
|
<button onclick={() => goto('/projects')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] rounded-md hover:bg-[var(--color-surface-sunken)] transition-colors" title="Projects">
|
||||||
<Icon icon="mdi:arrow-left" class="text-xl" />
|
<Icon icon="mdi:arrow-left" class="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-blue-500 text-base" />
|
<Icon icon="mdi:folder-multiple-outline" class="text-blue-500 text-base" />
|
||||||
<h1 class="text-[16px] font-semibold text-gray-900 dark:text-white tracking-tight truncate max-w-[200px] md:max-w-xs" title={projectName}>{projectName}</h1>
|
<h1 class="text-[16px] font-semibold text-[var(--color-ink)] tracking-tight truncate max-w-[200px] md:max-w-xs" title={projectName}>{projectName}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-0.5 text-[13px] font-medium text-gray-600 dark:text-gray-300 -ml-1 action-menu-container">
|
<div class="flex items-center gap-0.5 text-[13px] font-medium text-[var(--color-ink-muted)] -ml-1 action-menu-container">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }} class="px-2 py-0.5 rounded transition-colors {activeMenu === 'file' ? 'bg-[var(--theme-border)]' : 'hover:bg-[var(--theme-border)]'}">File</button>
|
<button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'file' ? null : 'file'; }} class="px-2 py-0.5 rounded transition-colors {activeMenu === 'file' ? 'bg-[var(--theme-border)]' : 'hover:bg-[var(--theme-border)]'}">File</button>
|
||||||
{#if activeMenu === 'file'}
|
{#if activeMenu === 'file'}
|
||||||
@@ -329,7 +331,7 @@
|
|||||||
<button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm hover:bg-[var(--theme-border)]">HTML (.html)</button>
|
<button onclick={() => { activeMenu = null; handlePandocExport('html'); }} class="w-full text-left px-4 py-1 text-sm hover:bg-[var(--theme-border)]">HTML (.html)</button>
|
||||||
{#if role === 'owner'}
|
{#if role === 'owner'}
|
||||||
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
||||||
<button onclick={() => { activeMenu = null; showDeleteModal = true; }} class="w-full text-left px-4 py-1.5 text-sm text-red-500 hover:bg-red-500/10">Delete Project</button>
|
<button onclick={() => { activeMenu = null; showDeleteModal = true; }} class="w-full text-left px-4 py-1.5 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10">Delete Project</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -352,7 +354,7 @@
|
|||||||
{#if activeMenu === 'view'}
|
{#if activeMenu === 'view'}
|
||||||
<div class="absolute left-0 top-full mt-1 w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-[var(--theme-border)] py-1 z-[100]">
|
<div class="absolute left-0 top-full mt-1 w-48 bg-[var(--theme-bg)] rounded-xl shadow-xl border border-[var(--theme-border)] py-1 z-[100]">
|
||||||
<button onclick={() => { activeMenu = null; $previewOpenStore = !$previewOpenStore; }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Preview<Icon icon={$previewOpenStore ? 'mdi:check' : ''} class="text-sm" /></button>
|
<button onclick={() => { activeMenu = null; $previewOpenStore = !$previewOpenStore; }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Preview<Icon icon={$previewOpenStore ? 'mdi:check' : ''} class="text-sm" /></button>
|
||||||
<button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; document.documentElement.classList.toggle('dark', $darkModeStore); }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Dark Mode<Icon icon={$darkModeStore ? 'mdi:check' : ''} class="text-sm" /></button>
|
<button onclick={() => { activeMenu = null; $darkModeStore = !$darkModeStore; }} class="w-full text-left px-4 py-1.5 text-sm hover:bg-[var(--theme-border)] flex items-center justify-between">Dark Mode<Icon icon={$darkModeStore ? 'mdi:check' : ''} class="text-sm" /></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -364,36 +366,36 @@
|
|||||||
{#if $connectedUsers.length > 0}
|
{#if $connectedUsers.length > 0}
|
||||||
<div class="flex items-center -space-x-2 mr-2">
|
<div class="flex items-center -space-x-2 mr-2">
|
||||||
{#each $connectedUsers as user}
|
{#each $connectedUsers as user}
|
||||||
<div class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-white dark:border-zinc-950 shadow-sm" style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};" title={user.name + (user.isLocal ? ' (You)' : '')}>{getInitials(user.name)}</div>
|
<div class="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold text-white border-2 border-[var(--color-surface)] shadow-sm" style="background-color: {user.color}; z-index: {user.isLocal ? 10 : 1};" title={user.name + (user.isLocal ? ' (You)' : '')}>{getInitials(user.name)}</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-5 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<button onclick={() => (isPresentationOpen = true)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors">
|
<button onclick={() => (isPresentationOpen = true)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors">
|
||||||
<Icon icon="mdi:presentation-play" class="text-[16px]" /> Present
|
<Icon icon="mdi:presentation-play" class="text-[16px]" /> Present
|
||||||
</button>
|
</button>
|
||||||
{#if role === 'owner'}
|
{#if role === 'owner'}
|
||||||
<button onclick={onPublish} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md transition-colors">
|
<button onclick={onPublish} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-white bg-[var(--color-accent)] hover:opacity-90 rounded-md transition-colors">
|
||||||
<Icon icon="mdi:package-variant-closed" class="text-[16px]" /> Publish
|
<Icon icon="mdi:package-variant-closed" class="text-[16px]" /> Publish
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="w-px h-5 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-5 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<button onclick={() => ($previewOpenStore = !$previewOpenStore)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10' : 'text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20'}" title={$previewOpenStore ? 'Hide preview' : 'Show preview'}>
|
<button onclick={() => ($previewOpenStore = !$previewOpenStore)} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium transition-colors rounded-md {$previewOpenStore ? 'text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)]' : 'text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90'}" title={$previewOpenStore ? 'Hide preview' : 'Show preview'}>
|
||||||
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" /> Preview
|
<Icon icon={$previewOpenStore ? 'mdi:eye-off-outline' : 'mdi:eye-outline'} class="text-[16px]" /> Preview
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-black/20 dark:hover:bg-white/10 rounded-md transition-colors" title="Print">
|
<button onclick={handlePrint} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-ink-muted)] bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] rounded-md transition-colors" title="Print">
|
||||||
<Icon icon="mdi:printer" class="text-[16px]" /> Print
|
<Icon icon="mdi:printer" class="text-[16px]" /> Print
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="relative action-menu-container">
|
<div class="relative action-menu-container">
|
||||||
<button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/20 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-blue-500/30' : ''}">
|
<button onclick={(e) => { e.stopPropagation(); activeMenu = activeMenu === 'export' ? null : 'export'; }} class="flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium text-[var(--color-accent)] bg-[var(--color-accent-soft)] hover:opacity-90 rounded-md transition-colors {activeMenu === 'export' ? 'ring-2 ring-[var(--color-accent)]/30' : ''}">
|
||||||
<Icon icon="mdi:export-variant" class="text-[16px]" /> Export <Icon icon="mdi:chevron-down" class="text-sm opacity-70" />
|
<Icon icon="mdi:export-variant" class="text-[16px]" /> Export <Icon icon="mdi:chevron-down" class="text-sm opacity-70" />
|
||||||
</button>
|
</button>
|
||||||
{#if activeMenu === 'export'}
|
{#if activeMenu === 'export'}
|
||||||
@@ -408,23 +410,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center px-4 py-1.5 bg-white/50 dark:bg-black/10 border-t border-gray-200/60 dark:border-white/10 gap-4 overflow-x-auto no-scrollbar">
|
<div class="flex items-center px-4 py-1.5 bg-[var(--color-surface-muted)] border-t border-[var(--color-line)] gap-4 overflow-x-auto no-scrollbar">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bold"><Icon icon="mdi:format-bold" class="text-lg" /></button>
|
<button onclick={() => applyFormat('*', '*', 'bold')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bold"><Icon icon="mdi:format-bold" class="text-lg" /></button>
|
||||||
<button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Italic"><Icon icon="mdi:format-italic" class="text-lg" /></button>
|
<button onclick={() => applyFormat('_', '_', 'italic')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Italic"><Icon icon="mdi:format-italic" class="text-lg" /></button>
|
||||||
<button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Code"><Icon icon="mdi:code-tags" class="text-lg" /></button>
|
<button onclick={() => applyFormat('`', '`', 'code')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Code"><Icon icon="mdi:code-tags" class="text-lg" /></button>
|
||||||
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
|
||||||
<button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Math (Inline)"><Icon icon="mdi:sigma" class="text-lg" /></button>
|
<button onclick={() => applyFormat('$ ', ' $', 'x = y')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Math (Inline)"><Icon icon="mdi:sigma" class="text-lg" /></button>
|
||||||
<button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Bullet List"><Icon icon="mdi:format-list-bulleted" class="text-lg" /></button>
|
<button onclick={() => applyFormat('- ', '', 'List item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Bullet List"><Icon icon="mdi:format-list-bulleted" class="text-lg" /></button>
|
||||||
<button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Numbered List"><Icon icon="mdi:format-list-numbered" class="text-lg" /></button>
|
<button onclick={() => applyFormat('+ ', '', 'Numbered item')} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Numbered List"><Icon icon="mdi:format-list-numbered" class="text-lg" /></button>
|
||||||
<div class="w-px h-4 mx-1 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 mx-1 bg-[var(--color-line)]"></div>
|
||||||
<input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" accept="image/*,.ttf,.otf" />
|
<input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" accept="image/*,.ttf,.otf" />
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<button onclick={() => fileInput?.click()} class="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 rounded transition-colors" title="Upload Image / Font"><Icon icon="mdi:image-plus" class="text-lg" /></button>
|
<button onclick={() => fileInput?.click()} class="p-1.5 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] rounded transition-colors" title="Upload Image / Font"><Icon icon="mdi:image-plus" class="text-lg" /></button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="project-font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
|
<label for="project-font-select" class="text-[11px] font-semibold uppercase tracking-wider opacity-60">Font</label>
|
||||||
@@ -443,22 +445,22 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<button onclick={() => (isPageSettingsOpen = true)} class="flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded shadow-sm transition-colors opacity-90 hover:opacity-100">
|
<button onclick={() => (isPageSettingsOpen = true)} class="flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold bg-[var(--theme-bg)] text-[var(--theme-text)] border border-[var(--theme-border)] rounded shadow-sm transition-colors opacity-90 hover:opacity-100">
|
||||||
<Icon icon="mdi:file-document-edit-outline" class="text-sm" /> Page Settings
|
<Icon icon="mdi:file-document-edit-outline" class="text-sm" /> Page Settings
|
||||||
</button>
|
</button>
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center gap-1 bg-white dark:bg-black/20 border border-gray-300 dark:border-white/20 rounded shadow-sm overflow-hidden">
|
<div class="flex items-center gap-1 bg-[var(--color-surface)] border border-[var(--color-line)] rounded shadow-sm overflow-hidden">
|
||||||
<button onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)} class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors" title="Zoom Out"><Icon icon="mdi:minus" class="text-sm" /></button>
|
<button onclick={() => $documentZoomStore = Math.max(10, $documentZoomStore - 10)} class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors" title="Zoom Out"><Icon icon="mdi:minus" class="text-sm" /></button>
|
||||||
<span role="button" tabindex="0" onkeydown={(e) => { if (e.key === 'Enter') $documentZoomStore = 100; }} class="text-[11px] font-semibold text-gray-700 dark:text-gray-200 min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>{$documentZoomStore}%</span>
|
<span role="button" tabindex="0" onkeydown={(e) => { if (e.key === 'Enter') $documentZoomStore = 100; }} class="text-[11px] font-semibold text-[var(--color-ink-muted)] min-w-[3rem] text-center select-none" ondblclick={() => $documentZoomStore = 100}>{$documentZoomStore}%</span>
|
||||||
<button onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)} class="px-2 py-1 text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-white/10 transition-colors" title="Zoom In"><Icon icon="mdi:plus" class="text-sm" /></button>
|
<button onclick={() => $documentZoomStore = Math.min(500, $documentZoomStore + 10)} class="px-2 py-1 text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)] transition-colors" title="Zoom In"><Icon icon="mdi:plus" class="text-sm" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-4 bg-gray-300 dark:bg-white/10"></div>
|
<div class="w-px h-4 bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<ThemePicker />
|
<ThemePicker />
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
@@ -474,50 +476,37 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showInfoModal}
|
{#if showInfoModal}
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onclick={() => showInfoModal = false} role="presentation">
|
<Modal title={projectName} icon="ph:folder-star" onclose={() => showInfoModal = false}>
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-2xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation">
|
<div class="flex flex-col gap-4 text-xs">
|
||||||
<div class="p-6 border-b border-gray-100 dark:border-white/10 flex items-center gap-3">
|
<div><p class="mb-1 font-medium text-[var(--color-ink-muted)]">Type</p><p class="text-sm text-[var(--color-ink)]">Project (multi-file)</p></div>
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-xl text-blue-500" />
|
<div><p class="mb-1 font-medium text-[var(--color-ink-muted)]">Entrypoint</p><p class="font-mono text-sm text-[var(--color-ink)]">{entrypoint}</p></div>
|
||||||
<h3 class="text-lg font-semibold flex-grow truncate">{projectName}</h3>
|
<div><p class="mb-1 font-medium text-[var(--color-ink-muted)]">Your role</p><p class="text-sm text-[var(--color-ink)] capitalize">{role}</p></div>
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-4 text-sm">
|
|
||||||
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Type</p><p>Project (multi-file)</p></div>
|
|
||||||
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Entrypoint</p><p class="font-mono">{entrypoint}</p></div>
|
|
||||||
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Your role</p><p class="capitalize">{role}</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 bg-gray-50 dark:bg-white/5 border-t border-gray-100 dark:border-white/10 flex justify-end">
|
|
||||||
<button onclick={() => showInfoModal = false} class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<button onclick={() => showInfoModal = false} class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90">Close</button>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showRenameModal}
|
{#if showRenameModal}
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onclick={() => showRenameModal = false} role="presentation">
|
<PromptModal
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-2xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation">
|
title="Rename project"
|
||||||
<form onsubmit={submitRename} class="p-6">
|
label="Project name"
|
||||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2"><Icon icon="mdi:pencil-outline" class="text-yellow-500" /> Rename Project</h3>
|
icon="ph:pencil-simple"
|
||||||
<input type="text" required bind:value={renameName} class="w-full bg-transparent border border-gray-300 dark:border-white/20 text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
value={renameName}
|
||||||
<div class="pt-6 flex justify-end gap-3">
|
confirmLabel="Save"
|
||||||
<button type="button" onclick={() => showRenameModal = false} class="px-4 py-2 text-sm font-medium hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg">Cancel</button>
|
onsubmit={submitRename}
|
||||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium">Save</button>
|
onclose={() => showRenameModal = false}
|
||||||
</div>
|
/>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDeleteModal}
|
{#if showDeleteModal}
|
||||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onclick={() => showDeleteModal = false} role="presentation">
|
<ConfirmModal
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-2xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation">
|
title="Delete project"
|
||||||
<div class="p-6">
|
message="Delete this project and all its files? This cannot be undone."
|
||||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2"><Icon icon="mdi:trash-can-outline" class="text-red-500" /> Delete Project</h3>
|
confirmLabel="Delete"
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm mb-6">Delete this project and all its files? This cannot be undone.</p>
|
onconfirm={confirmDelete}
|
||||||
<div class="flex justify-end gap-3">
|
onclose={() => showDeleteModal = false}
|
||||||
<button type="button" onclick={() => showDeleteModal = false} class="px-4 py-2 text-sm font-medium hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg">Cancel</button>
|
/>
|
||||||
<button type="button" onclick={confirmDelete} class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg text-sm font-medium">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+6
-6
@@ -34,14 +34,14 @@ if (typeof window !== 'undefined') {
|
|||||||
if (savedDark !== null) darkModeStore.set(savedDark === 'true');
|
if (savedDark !== null) darkModeStore.set(savedDark === 'true');
|
||||||
if (savedZoom !== null) documentZoomStore.set(parseInt(savedZoom, 10));
|
if (savedZoom !== null) documentZoomStore.set(parseInt(savedZoom, 10));
|
||||||
|
|
||||||
themeStore.subscribe(value => localStorage.setItem('editor-theme', value));
|
themeStore.subscribe(value => {
|
||||||
|
localStorage.setItem('editor-theme', value);
|
||||||
|
document.documentElement.dataset.colorTheme = value;
|
||||||
|
});
|
||||||
darkModeStore.subscribe(value => {
|
darkModeStore.subscribe(value => {
|
||||||
localStorage.setItem('editor-dark-mode', value.toString());
|
localStorage.setItem('editor-dark-mode', value.toString());
|
||||||
if (value) {
|
document.documentElement.dataset.theme = value ? 'dark' : 'light';
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.toggle('dark', value);
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
documentZoomStore.subscribe(value => localStorage.setItem('editor-document-zoom', value.toString()));
|
documentZoomStore.subscribe(value => localStorage.setItem('editor-document-zoom', value.toString()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,26 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
|
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
|
||||||
<div class="text-center max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-xl border border-gray-200 dark:border-zinc-800 p-8">
|
<div class="text-center max-w-md bg-[var(--color-surface)] rounded-2xl shadow-xl border border-[var(--color-line)] p-8">
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-red-100 dark:bg-red-500/10 mb-6">
|
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--color-danger)]/10 mb-6">
|
||||||
<Icon icon="mdi:alert-circle-outline" class="h-10 w-10 text-red-600 dark:text-red-500" />
|
<Icon icon="mdi:alert-circle-outline" class="h-10 w-10 text-[var(--color-danger)]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-6xl font-bold text-gray-900 dark:text-white mb-2 tracking-tight">
|
<h1 class="text-6xl font-bold text-[var(--color-ink)] mb-2 tracking-tight">
|
||||||
{$page.status}
|
{$page.status}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4">
|
<h2 class="text-xl font-semibold text-[var(--color-ink)] mb-4">
|
||||||
Something went wrong
|
Something went wrong
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="text-base text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
|
<p class="text-base text-[var(--color-ink-muted)] mb-8 leading-relaxed">
|
||||||
{$page.error?.message || 'We experienced an unexpected error processing your request.'}
|
{$page.error?.message || 'We experienced an unexpected error processing your request.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="inline-flex items-center justify-center gap-2 px-6 py-3 border border-transparent text-sm font-semibold rounded-xl shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-colors w-full"
|
class="inline-flex items-center justify-center gap-2 px-6 py-3 text-sm font-semibold rounded-xl shadow-sm text-white bg-[var(--color-accent)] hover:opacity-90 focus:outline-none transition w-full"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:home" class="text-lg" />
|
<Icon icon="mdi:home" class="text-lg" />
|
||||||
Return to Dashboard
|
Return to Dashboard
|
||||||
|
|||||||
@@ -40,21 +40,11 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div
|
<div class="min-h-screen w-full flex flex-col bg-[var(--color-surface-muted)] text-[var(--color-ink)] font-sans transition-colors duration-200">
|
||||||
class="min-h-screen w-full flex flex-col font-sans transition-colors duration-200"
|
|
||||||
style="
|
|
||||||
background-color: {currentColors.background};
|
|
||||||
color: {currentColors.text};
|
|
||||||
--theme-bg: {currentColors.background};
|
|
||||||
--theme-text: {currentColors.text};
|
|
||||||
--theme-border: {currentColors.selection};
|
|
||||||
--theme-cursor: {currentColors.cursor};
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="min-h-screen w-full flex items-center justify-center bg-gray-50 dark:bg-zinc-950">
|
<div class="min-h-screen w-full flex items-center justify-center bg-[var(--color-surface-muted)]">
|
||||||
<div class="text-gray-500 dark:text-gray-400 font-medium animate-pulse">Loading TypstDrive...</div>
|
<div class="text-[var(--color-ink-muted)] font-medium animate-pulse">Loading TypstDrive...</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<meta name="description" content="Collaborative Typst Editor." />
|
<meta name="description" content="Collaborative Typst Editor." />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="h-full flex items-center justify-center text-gray-500">
|
<div class="h-full flex items-center justify-center text-[var(--color-ink-muted)]">
|
||||||
Redirecting...
|
Redirecting...
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -170,12 +170,12 @@ with open("output.png", "wb") as f:
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="min-h-screen flex flex-col">
|
||||||
<nav class="bg-[var(--theme-bg)] shadow-sm border-b border-gray-200 dark:border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0">
|
<nav class="bg-[var(--color-surface)] shadow-sm border-b border-[var(--color-line)] px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
<h1 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-3">
|
||||||
<Icon icon="mdi:api" class="text-blue-600 dark:text-blue-400 text-3xl" />
|
<Icon icon="mdi:api" class="text-[var(--color-accent)] text-3xl" />
|
||||||
API Reference
|
API Reference
|
||||||
</h1>
|
</h1>
|
||||||
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-4 py-2 rounded-lg flex items-center gap-2">
|
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-4 py-2 rounded-md flex items-center gap-2">
|
||||||
<Icon icon="mdi:arrow-left" class="text-lg" />
|
<Icon icon="mdi:arrow-left" class="text-lg" />
|
||||||
Back to Dashboard
|
Back to Dashboard
|
||||||
</button>
|
</button>
|
||||||
@@ -187,16 +187,16 @@ with open("output.png", "wb") as f:
|
|||||||
{#each navSections as section}
|
{#each navSections as section}
|
||||||
<button
|
<button
|
||||||
onclick={() => activeSection = section.id}
|
onclick={() => activeSection = section.id}
|
||||||
class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 {activeSection === section.id
|
class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium transition-colors {activeSection === section.id
|
||||||
? 'bg-blue-600 text-white shadow-sm'
|
? 'bg-[var(--color-accent)] text-white shadow-sm'
|
||||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5'}"
|
: 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||||
>
|
>
|
||||||
<Icon icon={section.icon} class="text-lg flex-shrink-0" />
|
<Icon icon={section.icon} class="text-lg flex-shrink-0" />
|
||||||
{section.label}
|
{section.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="pt-4 mt-4 border-t border-gray-200 dark:border-white/10">
|
<div class="pt-4 mt-4 border-t border-[var(--color-line)]">
|
||||||
<a href="/settings" class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5 transition-all duration-150">
|
<a href="/settings" class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)] transition-colors">
|
||||||
<Icon icon="mdi:key-plus" class="text-lg flex-shrink-0" />
|
<Icon icon="mdi:key-plus" class="text-lg flex-shrink-0" />
|
||||||
Manage API Keys
|
Manage API Keys
|
||||||
</a>
|
</a>
|
||||||
@@ -210,7 +210,7 @@ with open("output.png", "wb") as f:
|
|||||||
{#each navSections as section}
|
{#each navSections as section}
|
||||||
<button
|
<button
|
||||||
onclick={() => activeSection = section.id}
|
onclick={() => activeSection = section.id}
|
||||||
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors {activeSection === section.id ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300'}"
|
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors {activeSection === section.id ? 'bg-[var(--color-accent)] text-white' : 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]'}"
|
||||||
>
|
>
|
||||||
{section.label}
|
{section.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -218,51 +218,51 @@ with open("output.png", "wb") as f:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if activeSection === 'overview'}
|
{#if activeSection === 'overview'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-4 flex items-center gap-2">
|
||||||
<Icon icon="mdi:book-open-outline" class="text-2xl text-blue-500" />
|
<Icon icon="mdi:book-open-outline" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Overview
|
Overview
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
<p class="text-[var(--color-ink-muted)] mb-6">
|
||||||
The TypstDrive Render API lets you compile Typst markup into PNG images or PDF documents programmatically.
|
The TypstDrive Render API lets you compile Typst markup into PNG images or PDF documents programmatically.
|
||||||
Authenticate with an API key and POST Typst code — get back binary output.
|
Authenticate with an API key and POST Typst code — get back binary output.
|
||||||
</p>
|
</p>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
<div class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/10 border border-blue-100 dark:border-blue-800/30">
|
<div class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/10 border border-blue-100 dark:border-blue-800/30">
|
||||||
<Icon icon="mdi:image-outline" class="text-2xl text-blue-500 mb-2" />
|
<Icon icon="mdi:image-outline" class="text-2xl text-[var(--color-accent)] mb-2" />
|
||||||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">PNG output</p>
|
<p class="text-sm font-semibold text-[var(--color-ink)]">PNG output</p>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">First page rendered at 2× scale</p>
|
<p class="text-xs text-[var(--color-ink-muted)] mt-1">First page rendered at 2× scale</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 rounded-xl bg-purple-50 dark:bg-purple-900/10 border border-purple-100 dark:border-purple-800/30">
|
<div class="p-4 rounded-xl bg-purple-50 dark:bg-purple-900/10 border border-purple-100 dark:border-purple-800/30">
|
||||||
<Icon icon="mdi:file-pdf-box" class="text-2xl text-purple-500 mb-2" />
|
<Icon icon="mdi:file-pdf-box" class="text-2xl text-purple-500 mb-2" />
|
||||||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">PDF output</p>
|
<p class="text-sm font-semibold text-[var(--color-ink)]">PDF output</p>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Full multi-page PDF document</p>
|
<p class="text-xs text-[var(--color-ink-muted)] mt-1">Full multi-page PDF document</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 rounded-xl bg-green-50 dark:bg-green-900/10 border border-green-100 dark:border-green-800/30">
|
<div class="p-4 rounded-xl bg-[var(--color-success)]/10 border border-[var(--color-success)]/20">
|
||||||
<Icon icon="mdi:lightning-bolt" class="text-2xl text-green-500 mb-2" />
|
<Icon icon="mdi:lightning-bolt" class="text-2xl text-green-500 mb-2" />
|
||||||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">Cached results</p>
|
<p class="text-sm font-semibold text-[var(--color-ink)]">Cached results</p>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Identical inputs skip recompilation</p>
|
<p class="text-xs text-[var(--color-ink-muted)] mt-1">Identical inputs skip recompilation</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 dark:bg-black/30 rounded-xl p-4 border border-gray-200 dark:border-white/10">
|
<div class="bg-[var(--color-surface-muted)] rounded-xl p-4 border border-[var(--color-line)]">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-1">Base URL</p>
|
<p class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)] mb-1">Base URL</p>
|
||||||
<code class="font-mono text-sm text-blue-600 dark:text-blue-400">{baseUrl}</code>
|
<code class="font-mono text-sm text-[var(--color-accent)]">{baseUrl}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'auth'}
|
{#if activeSection === 'auth'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-4 flex items-center gap-2">
|
||||||
<Icon icon="mdi:key-outline" class="text-2xl text-blue-500" />
|
<Icon icon="mdi:key-outline" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Authentication
|
Authentication
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
<p class="text-[var(--color-ink-muted)] mb-6">
|
||||||
All requests must include an API key in the <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">Authorization</code> header.
|
All requests must include an API key in the <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">Authorization</code> header.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Header format</p>
|
<p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-2">Header format</p>
|
||||||
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-3 text-xs font-mono leading-relaxed rounded-xl">{@html hljs.highlight('Authorization: Bearer td_your_api_key_here', { language: 'bash' }).value}</code></pre>
|
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-3 text-xs font-mono leading-relaxed rounded-xl">{@html hljs.highlight('Authorization: Bearer td_your_api_key_here', { language: 'bash' }).value}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 flex gap-3">
|
<div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 flex gap-3">
|
||||||
@@ -273,10 +273,10 @@ with open("output.png", "wb") as f:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Managing keys</p>
|
<p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-1">Managing keys</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-[var(--color-ink-muted)]">
|
||||||
Create, regenerate, and revoke keys in
|
Create, regenerate, and revoke keys in
|
||||||
<a href="/settings" class="text-blue-600 dark:text-blue-400 hover:underline">Settings → API Keys</a>.
|
<a href="/settings" class="text-[var(--color-accent)] hover:underline">Settings → API Keys</a>.
|
||||||
The full key is shown only once at creation time.
|
The full key is shown only once at creation time.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,43 +285,43 @@ with open("output.png", "wb") as f:
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'endpoint'}
|
{#if activeSection === 'endpoint'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8 space-y-6">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8 space-y-6">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:api" class="text-2xl text-blue-500" />
|
<Icon icon="mdi:api" class="text-2xl text-[var(--color-accent)]" />
|
||||||
POST /v1/render
|
POST /v1/render
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
<span class="px-2 py-0.5 text-xs font-bold bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-md">POST</span>
|
<span class="px-2 py-0.5 text-xs font-bold bg-green-100 dark:bg-green-900/30 text-[var(--color-success)] rounded-md">POST</span>
|
||||||
<code class="font-mono text-sm text-gray-800 dark:text-gray-200">/v1/render</code>
|
<code class="font-mono text-sm text-[var(--color-ink)]">/v1/render</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-[var(--color-ink-muted)]">
|
||||||
Compile Typst markup and return rendered output as PNG, PDF, or HTML.
|
Compile Typst markup and return rendered output as PNG, PDF, or HTML.
|
||||||
Results are cached for 1 hour — identical inputs return the cached result without recompiling.
|
Results are cached for 1 hour — identical inputs return the cached result without recompiling.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-px bg-gray-200 dark:bg-white/10"></div>
|
<div class="h-px bg-[var(--color-line)]"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Request headers</p>
|
<p class="text-sm font-bold text-[var(--color-ink)] mb-3">Request headers</p>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200 dark:border-white/10">
|
<tr class="border-b border-[var(--color-line)]">
|
||||||
<th class="text-left py-2 pr-4 font-semibold text-gray-700 dark:text-gray-300 w-40">Header</th>
|
<th class="text-left py-2 pr-4 font-semibold text-[var(--color-ink-muted)] w-40">Header</th>
|
||||||
<th class="text-left py-2 font-semibold text-gray-700 dark:text-gray-300">Value</th>
|
<th class="text-left py-2 font-semibold text-[var(--color-ink-muted)]">Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-gray-600 dark:text-gray-400">
|
<tbody class="text-[var(--color-ink-muted)]">
|
||||||
<tr class="border-b border-gray-100 dark:border-white/5">
|
<tr class="border-b border-[var(--color-line)]">
|
||||||
<td class="py-2 pr-4 font-mono text-xs">Authorization</td>
|
<td class="py-2 pr-4 font-mono text-xs">Authorization</td>
|
||||||
<td class="py-2"><code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">Bearer <api-key></code> — required</td>
|
<td class="py-2"><code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">Bearer <api-key></code> — required</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 pr-4 font-mono text-xs">Content-Type</td>
|
<td class="py-2 pr-4 font-mono text-xs">Content-Type</td>
|
||||||
<td class="py-2"><code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">application/json</code> — required</td>
|
<td class="py-2"><code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">application/json</code> — required</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -329,23 +329,23 @@ with open("output.png", "wb") as f:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Request body</p>
|
<p class="text-sm font-bold text-[var(--color-ink)] mb-3">Request body</p>
|
||||||
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hSchema}</code></pre>
|
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hSchema}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Response</p>
|
<p class="text-sm font-bold text-[var(--color-ink)] mb-3">Response</p>
|
||||||
<div class="p-3 rounded-lg bg-green-50 dark:bg-green-900/10 border border-green-100 dark:border-green-800/30 text-sm">
|
<div class="p-3 rounded-lg bg-[var(--color-success)]/10 border border-[var(--color-success)]/20 text-sm">
|
||||||
<span class="font-mono text-xs font-bold text-green-700 dark:text-green-400">200 OK</span>
|
<span class="font-mono text-xs font-bold text-[var(--color-success)]">200 OK</span>
|
||||||
<span class="text-gray-600 dark:text-gray-400 ml-2">Response body with <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">Content-Type: image/png</code>, <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">application/pdf</code>, or <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">text/html</code></span>
|
<span class="text-[var(--color-ink-muted)] ml-2">Response body with <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">Content-Type: image/png</code>, <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">application/pdf</code>, or <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">text/html</code></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Compilation errors</p>
|
<p class="text-sm font-bold text-[var(--color-ink)] mb-3">Compilation errors</p>
|
||||||
<div class="p-3 mb-3 rounded-lg bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-800/30 text-sm">
|
<div class="p-3 mb-3 rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 text-sm">
|
||||||
<span class="font-mono text-xs font-bold text-red-700 dark:text-red-400">422 Unprocessable Entity</span>
|
<span class="font-mono text-xs font-bold text-[var(--color-danger)]">422 Unprocessable Entity</span>
|
||||||
<span class="text-gray-600 dark:text-gray-400 ml-2">JSON body describing every Typst error. <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">error</code> is a readable summary; <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">details</code> lists each diagnostic with its message, severity, and source line and column.</span>
|
<span class="text-[var(--color-ink-muted)] ml-2">JSON body describing every Typst error. <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">error</code> is a readable summary; <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-2 rounded">details</code> lists each diagnostic with its message, severity, and source line and column.</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hCompileErr}</code></pre>
|
<pre class="rounded-xl border border-gray-700 overflow-x-auto"><code class="hljs block px-4 py-4 text-xs font-mono leading-relaxed rounded-xl">{@html hCompileErr}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -358,9 +358,9 @@ with open("output.png", "wb") as f:
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'examples'}
|
{#if activeSection === 'examples'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8 space-y-8">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8 space-y-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:code-braces" class="text-2xl text-blue-500" />
|
<Icon icon="mdi:code-braces" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Examples
|
Examples
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -374,11 +374,11 @@ with open("output.png", "wb") as f:
|
|||||||
] as ex}
|
] as ex}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 flex items-center gap-2">
|
<p class="text-sm font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon={ex.icon} class="text-lg {ex.iconColor}" />
|
<Icon icon={ex.icon} class="text-lg {ex.iconColor}" />
|
||||||
{ex.label}
|
{ex.label}
|
||||||
</p>
|
</p>
|
||||||
<button onclick={() => copy(ex.id, ex.raw)} class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors px-2 py-1 rounded-md hover:bg-gray-100 dark:hover:bg-white/10">
|
<button onclick={() => copy(ex.id, ex.raw)} class="flex items-center gap-1 text-xs text-[var(--color-ink-muted)] hover:text-[var(--color-accent)] transition-colors px-2 py-1 rounded-md hover:bg-[var(--color-surface-sunken)]">
|
||||||
<Icon icon={copiedSnippet === ex.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
|
<Icon icon={copiedSnippet === ex.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
|
||||||
{copiedSnippet === ex.id ? 'Copied!' : 'Copy'}
|
{copiedSnippet === ex.id ? 'Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
@@ -390,19 +390,19 @@ with open("output.png", "wb") as f:
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'rate-limits'}
|
{#if activeSection === 'rate-limits'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
|
||||||
<Icon icon="mdi:speedometer" class="text-2xl text-blue-500" />
|
<Icon icon="mdi:speedometer" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Rate Limits
|
Rate Limits
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||||
<div class="p-4 rounded-xl bg-gray-50 dark:bg-black/30 border border-gray-200 dark:border-white/10">
|
<div class="p-4 rounded-xl bg-[var(--color-surface-muted)] border border-[var(--color-line)]">
|
||||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">60</p>
|
<p class="text-2xl font-bold text-[var(--color-ink)]">60</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">requests / minute per key</p>
|
<p class="text-sm text-[var(--color-ink-muted)] mt-1">requests / minute per key</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 rounded-xl bg-gray-50 dark:bg-black/30 border border-gray-200 dark:border-white/10">
|
<div class="p-4 rounded-xl bg-[var(--color-surface-muted)] border border-[var(--color-line)]">
|
||||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">10</p>
|
<p class="text-2xl font-bold text-[var(--color-ink)]">10</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">API keys per account</p>
|
<p class="text-sm text-[var(--color-ink-muted)] mt-1">API keys per account</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 text-sm text-amber-800 dark:text-amber-300 mb-4">
|
<div class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-700/30 text-sm text-amber-800 dark:text-amber-300 mb-4">
|
||||||
@@ -410,19 +410,19 @@ with open("output.png", "wb") as f:
|
|||||||
<p>Identical inputs (same code + files) skip recompilation and are served from cache for up to 1 hour. Cached responses return instantly and do not consume your rate limit.</p>
|
<p>Identical inputs (same code + files) skip recompilation and are served from cache for up to 1 hour. Cached responses return instantly and do not consume your rate limit.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">When exceeded</p>
|
<p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-2">When exceeded</p>
|
||||||
<div class="p-3 rounded-lg bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-800/30 text-sm">
|
<div class="p-3 rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 text-sm">
|
||||||
<code class="font-mono text-xs font-bold text-red-700 dark:text-red-400">429 Too Many Requests</code>
|
<code class="font-mono text-xs font-bold text-[var(--color-danger)]">429 Too Many Requests</code>
|
||||||
<span class="text-gray-600 dark:text-gray-400 ml-2">— wait for the current 60-second window to reset.</span>
|
<span class="text-[var(--color-ink-muted)] ml-2">— wait for the current 60-second window to reset.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'errors'}
|
{#if activeSection === 'errors'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 p-6 sm:p-8">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
|
||||||
<Icon icon="mdi:alert-circle-outline" class="text-2xl text-blue-500" />
|
<Icon icon="mdi:alert-circle-outline" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Error Reference
|
Error Reference
|
||||||
</h2>
|
</h2>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@@ -433,18 +433,18 @@ with open("output.png", "wb") as f:
|
|||||||
{ code: '429', name: 'Too Many Requests', desc: 'Rate limit exceeded. Wait for the current 60-second window to reset.' },
|
{ code: '429', name: 'Too Many Requests', desc: 'Rate limit exceeded. Wait for the current 60-second window to reset.' },
|
||||||
{ code: '500', name: 'Internal Server Error', desc: 'Unexpected server error. Try again after a short delay.' },
|
{ code: '500', name: 'Internal Server Error', desc: 'Unexpected server error. Try again after a short delay.' },
|
||||||
] as err}
|
] as err}
|
||||||
<div class="flex items-start gap-4 p-4 rounded-xl border border-gray-100 dark:border-white/10 bg-gray-50 dark:bg-black/20">
|
<div class="flex items-start gap-4 p-4 rounded-xl border border-[var(--color-line)] bg-[var(--color-surface-muted)]">
|
||||||
<code class="font-mono text-sm font-bold text-gray-800 dark:text-gray-200 flex-shrink-0 w-8">{err.code}</code>
|
<code class="font-mono text-sm font-bold text-[var(--color-ink)] flex-shrink-0 w-8">{err.code}</code>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">{err.name}</p>
|
<p class="text-sm font-semibold text-[var(--color-ink)]">{err.name}</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">{err.desc}</p>
|
<p class="text-sm text-[var(--color-ink-muted)] mt-0.5">{err.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 p-4 rounded-xl bg-gray-50 dark:bg-black/30 border border-gray-200 dark:border-white/10">
|
<div class="mt-6 p-4 rounded-xl bg-[var(--color-surface-muted)] border border-[var(--color-line)]">
|
||||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Error body</p>
|
<p class="text-sm font-semibold text-[var(--color-ink-muted)] mb-1">Error body</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Compilation failures (<code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">422</code>) return a JSON body with an <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">error</code> summary and a <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">details</code> array. All other errors return plain text describing the issue.</p>
|
<p class="text-sm text-[var(--color-ink-muted)]">Compilation failures (<code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">422</code>) return a JSON body with an <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">error</code> summary and a <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">details</code> array. All other errors return plain text describing the issue.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -10,12 +10,9 @@
|
|||||||
import DocCard from '$lib/components/dashboard/DocCard.svelte';
|
import DocCard from '$lib/components/dashboard/DocCard.svelte';
|
||||||
import FileCard from '$lib/components/dashboard/FileCard.svelte';
|
import FileCard from '$lib/components/dashboard/FileCard.svelte';
|
||||||
import ShareModal from '$lib/components/ShareModal.svelte';
|
import ShareModal from '$lib/components/ShareModal.svelte';
|
||||||
import DeleteModal from '$lib/components/dashboard/DeleteModal.svelte';
|
import PromptModal from '$lib/components/PromptModal.svelte';
|
||||||
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||||
import InfoModal from '$lib/components/dashboard/InfoModal.svelte';
|
import InfoModal from '$lib/components/dashboard/InfoModal.svelte';
|
||||||
import RenameModal from '$lib/components/dashboard/RenameModal.svelte';
|
|
||||||
import CreateDocModal from '$lib/components/dashboard/CreateDocModal.svelte';
|
|
||||||
import CreateProjectModal from '$lib/components/dashboard/CreateProjectModal.svelte';
|
|
||||||
import CreateFolderModal from '$lib/components/dashboard/CreateFolderModal.svelte';
|
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
|
||||||
let documents = $state<any[]>([]);
|
let documents = $state<any[]>([]);
|
||||||
@@ -424,33 +421,33 @@
|
|||||||
|
|
||||||
<main class="max-w-7xl w-full mx-auto py-10 px-4 sm:px-6 lg:px-8 grow block">
|
<main class="max-w-7xl w-full mx-auto py-10 px-4 sm:px-6 lg:px-8 grow block">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">My Documents</h2>
|
<h2 class="text-3xl font-bold text-[var(--color-ink)] tracking-tight">My Documents</h2>
|
||||||
|
|
||||||
<div class="relative plus-dropdown-container">
|
<div class="relative plus-dropdown-container">
|
||||||
<button onclick={() => showPlusDropdown = !showPlusDropdown} class="flex items-center justify-center text-[var(--theme-text)] bg-[var(--theme-border)] opacity-90 hover:opacity-100 w-10 h-10 rounded-full shadow-md hover:shadow-lg transition-all duration-200 transform hover:-translate-y-0.5 border border-white/10 dark:border-black/20">
|
<button onclick={() => showPlusDropdown = !showPlusDropdown} class="flex items-center justify-center text-white bg-[var(--color-accent)] hover:opacity-90 w-10 h-10 rounded-full shadow-md transition">
|
||||||
<Icon icon="mdi:plus" class="text-2xl" />
|
<Icon icon="mdi:plus" class="text-2xl" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if showPlusDropdown}
|
{#if showPlusDropdown}
|
||||||
<div class="absolute right-0 mt-2 w-48 bg-[var(--theme-bg)] rounded-lg shadow-xl border border-gray-200 dark:border-white/10 py-1 z-20">
|
<div class="absolute right-0 mt-2 w-48 bg-[var(--color-surface)] rounded-md shadow-xl border border-[var(--color-line)] py-1 z-20">
|
||||||
<button onclick={openCreateModal} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={openCreateModal} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:file-document-plus" class="text-lg text-blue-500" />
|
<Icon icon="mdi:file-document-plus" class="text-lg text-blue-500" />
|
||||||
New Document
|
New Document
|
||||||
</button>
|
</button>
|
||||||
<button onclick={openCreateProjectModal} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={openCreateProjectModal} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:folder-multiple-plus" class="text-lg text-indigo-500" />
|
<Icon icon="mdi:folder-multiple-plus" class="text-lg text-indigo-500" />
|
||||||
New Project
|
New Project
|
||||||
</button>
|
</button>
|
||||||
<button onclick={openCreateFolderModal} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={openCreateFolderModal} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:folder-plus" class="text-lg text-yellow-500" />
|
<Icon icon="mdi:folder-plus" class="text-lg text-yellow-500" />
|
||||||
New Folder
|
New Folder
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => { showPlusDropdown = false; fileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2">
|
<button onclick={() => { showPlusDropdown = false; fileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:upload" class="text-lg text-green-500" />
|
<Icon icon="mdi:upload" class="text-lg text-green-500" />
|
||||||
Upload File
|
Upload File
|
||||||
</button>
|
</button>
|
||||||
<div class="h-px bg-gray-200 dark:bg-white/10 my-1"></div>
|
<div class="h-px bg-[var(--color-line)] my-1"></div>
|
||||||
<button onclick={() => { showPlusDropdown = false; importFileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-black/5 dark:hover:bg-white/5 flex items-center gap-2" disabled={isImporting}>
|
<button onclick={() => { showPlusDropdown = false; importFileInput?.click(); }} class="w-full text-left px-4 py-2 text-sm text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)] flex items-center gap-2" disabled={isImporting}>
|
||||||
{#if isImporting}
|
{#if isImporting}
|
||||||
<Icon icon="mdi:loading" class="text-lg text-purple-500 animate-spin" />
|
<Icon icon="mdi:loading" class="text-lg text-purple-500 animate-spin" />
|
||||||
Importing...
|
Importing...
|
||||||
@@ -467,29 +464,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-6 bg-white/50 dark:bg-black/20 p-3 rounded-lg border border-gray-200 dark:border-white/10">
|
<div class="flex items-center gap-2 text-sm text-[var(--color-ink-muted)] mb-6 bg-[var(--color-surface)] p-3 rounded-md border border-[var(--color-line)]">
|
||||||
<button
|
<button
|
||||||
onclick={() => navigateToBreadcrumb(-1)}
|
onclick={() => navigateToBreadcrumb(-1)}
|
||||||
ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = -1; }}
|
ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = -1; }}
|
||||||
ondragleave={() => dragOverBreadcrumbIndex = null}
|
ondragleave={() => dragOverBreadcrumbIndex = null}
|
||||||
ondrop={(e) => handleDrop(e, null)}
|
ondrop={(e) => handleDrop(e, null)}
|
||||||
class="hover:text-blue-600 dark:hover:text-blue-400 font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === -1 ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' : ''}">
|
class="hover:text-[var(--color-accent)] font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === -1 ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : ''}">
|
||||||
<Icon icon="mdi:home" class="text-lg inline-block pb-0.5" /> Home
|
<Icon icon="mdi:home" class="text-lg inline-block pb-0.5" /> Home
|
||||||
</button>
|
</button>
|
||||||
{#if inSharedDrive}
|
{#if inSharedDrive}
|
||||||
<Icon icon="mdi:chevron-right" class="text-lg text-gray-400" />
|
<Icon icon="mdi:chevron-right" class="text-lg text-[var(--color-ink-muted)]" />
|
||||||
<span class="font-medium text-purple-600 dark:text-purple-400 flex items-center gap-1 px-2 py-1">
|
<span class="font-medium text-[var(--color-accent)] flex items-center gap-1 px-2 py-1">
|
||||||
<Icon icon="mdi:folder-account" class="text-base" /> Shared with me
|
<Icon icon="mdi:folder-account" class="text-base" /> Shared with me
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
{#each folderPath as folder, index}
|
{#each folderPath as folder, index}
|
||||||
<Icon icon="mdi:chevron-right" class="text-lg text-gray-400" />
|
<Icon icon="mdi:chevron-right" class="text-lg text-[var(--color-ink-muted)]" />
|
||||||
<button
|
<button
|
||||||
onclick={() => navigateToBreadcrumb(index)}
|
onclick={() => navigateToBreadcrumb(index)}
|
||||||
ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = index; }}
|
ondragover={(e) => { e.preventDefault(); dragOverBreadcrumbIndex = index; }}
|
||||||
ondragleave={() => dragOverBreadcrumbIndex = null}
|
ondragleave={() => dragOverBreadcrumbIndex = null}
|
||||||
ondrop={(e) => handleDrop(e, folder.id)}
|
ondrop={(e) => handleDrop(e, folder.id)}
|
||||||
class="hover:text-blue-600 dark:hover:text-blue-400 font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === index ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' : ''}">
|
class="hover:text-[var(--color-accent)] font-medium transition-colors px-2 py-1 rounded {dragOverBreadcrumbIndex === index ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : ''}">
|
||||||
{folder.name}
|
{folder.name}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -499,19 +496,19 @@
|
|||||||
{#if inSharedDrive}
|
{#if inSharedDrive}
|
||||||
{#if sharedDocsLoading}
|
{#if sharedDocsLoading}
|
||||||
<div class="min-h-[50vh] flex items-center justify-center">
|
<div class="min-h-[50vh] flex items-center justify-center">
|
||||||
<div class="flex flex-col items-center gap-4 text-gray-500 dark:text-gray-400 animate-pulse">
|
<div class="flex flex-col items-center gap-4 text-[var(--color-ink-muted)] animate-pulse">
|
||||||
<Icon icon="mdi:loading" class="text-4xl animate-spin" />
|
<Icon icon="mdi:loading" class="text-4xl animate-spin" />
|
||||||
<p class="text-lg font-medium">Loading shared documents...</p>
|
<p class="text-lg font-medium">Loading shared documents...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if sharedDocs.length === 0}
|
{:else if sharedDocs.length === 0}
|
||||||
<div class="min-h-[50vh] flex items-center justify-center">
|
<div class="min-h-[50vh] flex items-center justify-center">
|
||||||
<div class="text-center p-12 bg-white/50 dark:bg-black/20 rounded-2xl shadow-sm border border-gray-200 dark:border-white/10 max-w-md w-full">
|
<div class="text-center p-12 bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] max-w-md w-full">
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-purple-100/50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 mb-6">
|
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] mb-6">
|
||||||
<Icon icon="mdi:folder-account-outline" class="text-4xl" />
|
<Icon icon="mdi:folder-account-outline" class="text-4xl" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">No shared documents</h3>
|
<h3 class="text-xl font-bold text-[var(--color-ink)] mb-2">No shared documents</h3>
|
||||||
<p class="text-gray-500 dark:text-gray-400">Documents shared with you by other users will appear here.</p>
|
<p class="text-[var(--color-ink-muted)]">Documents shared with you by other users will appear here.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -532,7 +529,7 @@
|
|||||||
|
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
<div class="min-h-[50vh] flex items-center justify-center">
|
<div class="min-h-[50vh] flex items-center justify-center">
|
||||||
<div class="flex flex-col items-center gap-4 text-gray-500 dark:text-gray-400 animate-pulse">
|
<div class="flex flex-col items-center gap-4 text-[var(--color-ink-muted)] animate-pulse">
|
||||||
<Icon icon="mdi:loading" class="text-4xl animate-spin" />
|
<Icon icon="mdi:loading" class="text-4xl animate-spin" />
|
||||||
<p class="text-lg font-medium">Loading your workspace...</p>
|
<p class="text-lg font-medium">Loading your workspace...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -540,20 +537,20 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#if currentFolderId === null || folders.length > 0}
|
{#if currentFolderId === null || folders.length > 0}
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="px-2 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300">Folders</div>
|
<div class="px-2 py-3 text-sm font-semibold text-[var(--color-ink-muted)]">Folders</div>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
{#if currentFolderId === null}
|
{#if currentFolderId === null}
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center p-3 bg-white/50 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-xl shadow-sm hover:shadow-md cursor-pointer group transition-all duration-200 hover:-translate-y-0.5 hover:border-purple-300 dark:hover:border-purple-500/30"
|
class="flex flex-row items-center p-3 bg-[var(--color-surface)] border border-[var(--color-line)] rounded-lg shadow-sm hover:border-[var(--color-accent)] cursor-pointer group transition"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={enterSharedDrive}
|
onclick={enterSharedDrive}
|
||||||
onkeydown={(e) => e.key === 'Enter' && enterSharedDrive()}
|
onkeydown={(e) => e.key === 'Enter' && enterSharedDrive()}
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center w-10 h-10 bg-purple-50 dark:bg-purple-500/10 rounded-lg group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3">
|
<div class="flex items-center justify-center w-10 h-10 bg-[var(--color-accent-soft)] rounded-md group-hover:scale-105 transition-transform pointer-events-none shrink-0 mr-3">
|
||||||
<Icon icon="mdi:folder-account" class="text-2xl text-purple-500" />
|
<Icon icon="mdi:folder-account" class="text-2xl text-[var(--color-accent)]" />
|
||||||
</div>
|
</div>
|
||||||
<span class="font-medium text-gray-900 dark:text-white text-sm truncate w-full pointer-events-none">Shared with me</span>
|
<span class="font-medium text-[var(--color-ink)] text-sm truncate w-full pointer-events-none">Shared with me</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each folders as folder}
|
{#each folders as folder}
|
||||||
@@ -591,16 +588,16 @@
|
|||||||
|
|
||||||
{#if documents.length === 0 && files.length === 0 && currentFolderId !== null && folders.length === 0}
|
{#if documents.length === 0 && files.length === 0 && currentFolderId !== null && folders.length === 0}
|
||||||
<div class="min-h-[30vh] flex items-center justify-center">
|
<div class="min-h-[30vh] flex items-center justify-center">
|
||||||
<p class="text-gray-500 dark:text-gray-400">This folder is empty.</p>
|
<p class="text-[var(--color-ink-muted)]">This folder is empty.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if documents.length === 0 && files.length === 0 && folders.length === 0 && currentFolderId === null}
|
{:else if documents.length === 0 && files.length === 0 && folders.length === 0 && currentFolderId === null}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100/50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 mb-4">
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] mb-4">
|
||||||
<Icon icon="mdi:file-document-outline" class="text-3xl" />
|
<Icon icon="mdi:file-document-outline" class="text-3xl" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-1">No documents yet</h3>
|
<h3 class="text-lg font-bold text-[var(--color-ink)] mb-1">No documents yet</h3>
|
||||||
<p class="text-gray-500 dark:text-gray-400 mb-6 text-sm">Create your first Typst document to get started.</p>
|
<p class="text-[var(--color-ink-muted)] mb-6 text-sm">Create your first Typst document to get started.</p>
|
||||||
<button onclick={openCreateModal} class="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg shadow-sm text-sm font-medium transition-colors">
|
<button onclick={openCreateModal} class="inline-flex items-center gap-2 bg-[var(--color-accent)] hover:opacity-90 text-white px-5 py-2.5 rounded-md shadow-sm text-sm font-medium transition">
|
||||||
<Icon icon="mdi:plus" class="text-lg" />
|
<Icon icon="mdi:plus" class="text-lg" />
|
||||||
Create Document
|
Create Document
|
||||||
</button>
|
</button>
|
||||||
@@ -614,7 +611,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDeleteModal && deleteTarget}
|
{#if showDeleteModal && deleteTarget}
|
||||||
<DeleteModal {deleteTarget} {confirmDelete} onClose={() => showDeleteModal = false} />
|
<ConfirmModal
|
||||||
|
title={`Delete ${deleteTarget.type}`}
|
||||||
|
message={`Are you sure you want to delete '${deleteTarget.name}'? ${deleteTarget.type === 'folder' ? 'This will also delete all of its contents. ' : ''}This action cannot be undone.`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onconfirm={confirmDelete}
|
||||||
|
onclose={() => showDeleteModal = false}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showInfoModal && selectedInfo}
|
{#if showInfoModal && selectedInfo}
|
||||||
@@ -622,19 +625,51 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showRenameModal}
|
{#if showRenameModal}
|
||||||
<RenameModal initialTitle={renameTitle} {handleRename} onClose={() => showRenameModal = false} />
|
<PromptModal
|
||||||
|
title="Rename"
|
||||||
|
label="New name"
|
||||||
|
icon="ph:pencil-simple"
|
||||||
|
value={renameTitle}
|
||||||
|
confirmLabel="Save"
|
||||||
|
onsubmit={handleRename}
|
||||||
|
onclose={() => showRenameModal = false}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCreateModal}
|
{#if showCreateModal}
|
||||||
<CreateDocModal {createDoc} onClose={() => showCreateModal = false} />
|
<PromptModal
|
||||||
|
title="Create document"
|
||||||
|
label="Document title"
|
||||||
|
icon="ph:file-plus"
|
||||||
|
value="Untitled Document"
|
||||||
|
confirmLabel="Create"
|
||||||
|
onsubmit={createDoc}
|
||||||
|
onclose={() => showCreateModal = false}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCreateProjectModal}
|
{#if showCreateProjectModal}
|
||||||
<CreateProjectModal {createProject} onClose={() => showCreateProjectModal = false} />
|
<PromptModal
|
||||||
|
title="Create project"
|
||||||
|
label="Project name"
|
||||||
|
icon="ph:folder-star"
|
||||||
|
value="Untitled Project"
|
||||||
|
confirmLabel="Create"
|
||||||
|
onsubmit={createProject}
|
||||||
|
onclose={() => showCreateProjectModal = false}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCreateFolderModal}
|
{#if showCreateFolderModal}
|
||||||
<CreateFolderModal {createFolder} onClose={() => showCreateFolderModal = false} />
|
<PromptModal
|
||||||
|
title="Create folder"
|
||||||
|
label="Folder name"
|
||||||
|
icon="ph:folder-plus"
|
||||||
|
value="New Folder"
|
||||||
|
confirmLabel="Create"
|
||||||
|
onsubmit={createFolder}
|
||||||
|
onclose={() => showCreateFolderModal = false}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
<main class="flex-1 flex flex-col md:flex-row overflow-hidden relative" oncontextmenu={handleContextMenu}>
|
<main class="flex-1 flex flex-col md:flex-row overflow-hidden relative" oncontextmenu={handleContextMenu}>
|
||||||
|
|
||||||
{#if !isViewer}
|
{#if !isViewer}
|
||||||
<div class="flex flex-col relative min-h-[50%] md:min-h-0 bg-transparent z-10 shadow-[1px_0_10px_rgba(0,0,0,0.05)] dark:shadow-[1px_0_10px_rgba(0,0,0,0.2)] {$previewOpenStore ? 'w-full md:w-1/2 border-r border-gray-200 dark:border-white/10' : 'w-full'}">
|
<div class="flex flex-col relative min-h-[50%] md:min-h-0 bg-transparent z-10 {$previewOpenStore ? 'w-full md:w-1/2 border-r border-[var(--color-line)]' : 'w-full'}">
|
||||||
{#if initialized}
|
{#if initialized}
|
||||||
<Editor />
|
<Editor />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $previewOpenStore || isViewer}
|
{#if $previewOpenStore || isViewer}
|
||||||
<div class="{isViewer ? 'w-full' : 'w-full md:w-1/2'} relative bg-white/50 dark:bg-black/20 min-h-[50%] md:min-h-0 flex flex-col">
|
<div class="{isViewer ? 'w-full' : 'w-full md:w-1/2'} relative bg-[var(--color-surface)] min-h-[50%] md:min-h-0 flex flex-col">
|
||||||
<Preview {svgs} />
|
<Preview {svgs} />
|
||||||
<ErrorBanner {errors} />
|
<ErrorBanner {errors} />
|
||||||
</div>
|
</div>
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
<div class="h-px bg-[var(--theme-border)] my-1"></div>
|
||||||
<button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2">
|
<button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-500"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--color-ink-muted)]"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
||||||
Copy Text
|
Copy Text
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,65 +51,60 @@
|
|||||||
<meta name="description" content="Sign in to TypstDrive." />
|
<meta name="description" content="Sign in to TypstDrive." />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen flex flex-col relative overflow-hidden">
|
<div class="min-h-screen flex flex-col bg-[var(--color-surface-muted)]">
|
||||||
<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8 rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] p-10 shadow-2xl">
|
||||||
<div class="absolute -top-40 -left-40 w-96 h-96 bg-blue-400/20 dark:bg-blue-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
|
||||||
<div class="absolute top-40 -right-40 w-96 h-96 bg-purple-400/20 dark:bg-purple-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
|
||||||
<div class="absolute -bottom-40 left-20 w-96 h-96 bg-indigo-400/20 dark:bg-indigo-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
|
||||||
|
|
||||||
<div class="max-w-md w-full space-y-8 bg-white/80 dark:bg-black/40 backdrop-blur-xl p-10 rounded-2xl shadow-2xl border border-gray-200/50 dark:border-white/10 relative z-10">
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100/50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-500 mb-6 mx-auto shadow-sm">
|
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-[var(--color-accent)] mb-6 mx-auto shadow-sm">
|
||||||
<Icon icon="mdi:script-text" class="text-3xl" />
|
<img src="/favicon.png" alt="TypstDrive" class="h-14 w-14" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
<h2 class="text-3xl font-extrabold tracking-tight text-[var(--color-ink)]">
|
||||||
Welcome back
|
Welcome back
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 font-medium">
|
<p class="mt-2 text-sm text-[var(--color-ink-muted)] font-medium">
|
||||||
Sign in to your TypstDrive workspace
|
Sign in to your TypstDrive workspace
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="mt-8 space-y-6" onsubmit={login}>
|
<form class="mt-8 space-y-6" onsubmit={login}>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Email Address</label>
|
<label for="email" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Email address</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Icon icon="mdi:email" class="text-gray-400 dark:text-gray-500" />
|
<Icon icon="mdi:email" class="text-[var(--color-ink-muted)]" />
|
||||||
</div>
|
</div>
|
||||||
<input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="[email protected]">
|
<input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="[email protected]">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Password</label>
|
<label for="password" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Icon icon="mdi:lock" class="text-gray-400 dark:text-gray-500" />
|
<Icon icon="mdi:lock" class="text-[var(--color-ink-muted)]" />
|
||||||
</div>
|
</div>
|
||||||
<input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="••••••••">
|
<input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="••••••••">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div class="flex items-center gap-2 text-red-600 bg-red-50 dark:bg-red-500/10 dark:text-red-400 p-3 rounded-lg text-sm border border-red-200 dark:border-red-500/20 shadow-sm animate-in slide-in-from-top-1 fade-in duration-200">
|
<div class="flex items-center gap-2 text-[var(--color-danger)] bg-[var(--color-danger)]/10 p-3 rounded-md text-sm border border-[var(--color-danger)]/20">
|
||||||
<Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" />
|
<Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" />
|
||||||
<span class="font-medium">{errorMsg}</span>
|
<span class="font-medium">{errorMsg}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent text-sm font-bold rounded-xl text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 focus:ring-blue-500 transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
<button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 text-sm font-bold rounded-md text-white bg-[var(--color-accent)] hover:opacity-90 focus:outline-none transition">
|
||||||
Sign In
|
Sign in
|
||||||
<Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" />
|
<Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{#if registrationEnabled}
|
{#if registrationEnabled}
|
||||||
<div class="text-sm text-center mt-6 pt-4 border-t border-gray-200 dark:border-white/10">
|
<div class="text-sm text-center mt-6 pt-4 border-t border-[var(--color-line)]">
|
||||||
<span class="text-gray-500 dark:text-gray-400">New to TypstDrive? </span>
|
<span class="text-[var(--color-ink-muted)]">New to TypstDrive? </span>
|
||||||
<a href="/register" class="font-bold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors hover:underline">
|
<a href="/register" class="font-bold text-[var(--color-accent)] hover:underline transition-colors">
|
||||||
Create an account
|
Create an account
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,55 +46,55 @@
|
|||||||
<title>Packages - TypstDrive</title>
|
<title>Packages - TypstDrive</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-[var(--theme-bg)]">
|
<div class="min-h-screen bg-[var(--color-surface-muted)]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors mb-2 flex items-center gap-1.5">
|
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors mb-2 flex items-center gap-1.5">
|
||||||
<Icon icon="mdi:arrow-left" class="text-lg" />
|
<Icon icon="mdi:arrow-left" class="text-lg" />
|
||||||
Back to Dashboard
|
Back to Dashboard
|
||||||
</button>
|
</button>
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h2 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:package-variant-closed" class="text-purple-500" />
|
<Icon icon="mdi:package-variant-closed" class="text-purple-500" />
|
||||||
Packages
|
Packages
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
<p class="text-sm text-[var(--color-ink-muted)] mt-1">
|
||||||
Instance-local Typst packages, published from Projects and importable as
|
Instance-local Typst packages, published from Projects and importable as
|
||||||
<code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">@typstdrive/<name>:<version></code>.
|
<code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">@typstdrive/<name>:<version></code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="text-gray-500 dark:text-gray-400">Loading…</p>
|
<p class="text-[var(--color-ink-muted)]">Loading…</p>
|
||||||
{:else if packages.length === 0}
|
{:else if packages.length === 0}
|
||||||
<div class="text-center py-16 text-gray-500 dark:text-gray-400">
|
<div class="text-center py-16 text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:package-variant" class="text-5xl mx-auto mb-3 opacity-50" />
|
<Icon icon="mdi:package-variant" class="text-5xl mx-auto mb-3 opacity-50" />
|
||||||
<p>No packages published yet. Open a Project and use “Publish” to create one.</p>
|
<p>No packages published yet. Open a Project and use “Publish” to create one.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each packages as pkg (pkg.id)}
|
{#each packages as pkg (pkg.id)}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl border border-gray-200 dark:border-white/10 p-4 flex items-start justify-between gap-4">
|
<div class="bg-[var(--color-surface)] rounded-xl border border-[var(--color-line)] p-4 flex items-start justify-between gap-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="font-semibold text-gray-900 dark:text-white truncate">@typstdrive/{pkg.name}</p>
|
<p class="font-semibold text-[var(--color-ink)] truncate">@typstdrive/{pkg.name}</p>
|
||||||
{#if pkg.latest_version}
|
{#if pkg.latest_version}
|
||||||
<span class="text-xs font-mono bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-1.5 py-0.5 rounded">v{pkg.latest_version}</span>
|
<span class="text-xs font-mono bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-1.5 py-0.5 rounded">v{pkg.latest_version}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if pkg.description}
|
{#if pkg.description}
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 truncate">{pkg.description}</p>
|
<p class="text-sm text-[var(--color-ink-muted)] mt-1 truncate">{pkg.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-xs text-gray-400 mt-1">by {pkg.owner_name ?? 'unknown'}</p>
|
<p class="text-xs text-[var(--color-ink-muted)] mt-1">by {pkg.owner_name ?? 'unknown'}</p>
|
||||||
<pre class="mt-2 text-xs font-mono bg-gray-50 dark:bg-black/30 border border-gray-100 dark:border-white/5 rounded px-2 py-1 overflow-x-auto">{importSnippet(pkg)}</pre>
|
<pre class="mt-2 text-xs font-mono bg-[var(--color-surface-muted)] border border-[var(--color-line)] rounded px-2 py-1 overflow-x-auto">{importSnippet(pkg)}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end gap-2 flex-shrink-0">
|
<div class="flex flex-col items-end gap-2 flex-shrink-0">
|
||||||
<button onclick={() => copy(pkg)} class="text-xs px-2 py-1 rounded-md bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10 flex items-center gap-1">
|
<button onclick={() => copy(pkg)} class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] flex items-center gap-1">
|
||||||
<Icon icon={copied === pkg.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
|
<Icon icon={copied === pkg.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
|
||||||
{copied === pkg.id ? 'Copied' : 'Copy'}
|
{copied === pkg.id ? 'Copied' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => remove(pkg)} title="Delete" class="text-xs px-2 py-1 rounded-md text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-1">
|
<button onclick={() => remove(pkg)} title="Delete" class="text-xs px-2 py-1 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-1">
|
||||||
<Icon icon="mdi:trash-can-outline" class="text-sm" /> Delete
|
<Icon icon="mdi:trash-can-outline" class="text-sm" /> Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
<div class="flex flex-col min-h-0 {$previewOpenStore ? 'w-full md:w-1/2 border-r border-gray-200 dark:border-white/10' : 'flex-1'}">
|
<div class="flex flex-col min-h-0 {$previewOpenStore ? 'w-full md:w-1/2 border-r border-[var(--color-line)]' : 'flex-1'}">
|
||||||
{#if ready && activeEntry}
|
{#if ready && activeEntry}
|
||||||
{#key activeFileId}
|
{#key activeFileId}
|
||||||
<Editor ytext={activeEntry.text} awarenessProvider={activeEntry.provider} filePath={activePath} enableLsp={false} />
|
<Editor ytext={activeEntry.text} awarenessProvider={activeEntry.provider} filePath={activePath} enableLsp={false} />
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $previewOpenStore || readOnly}
|
{#if $previewOpenStore || readOnly}
|
||||||
<div class="{readOnly ? 'flex-1' : 'w-full md:w-1/2'} relative bg-white/50 dark:bg-black/20 flex flex-col">
|
<div class="{readOnly ? 'flex-1' : 'w-full md:w-1/2'} relative bg-[var(--color-surface)] flex flex-col">
|
||||||
<Preview {svgs} />
|
<Preview {svgs} />
|
||||||
<ErrorBanner {errors} />
|
<ErrorBanner {errors} />
|
||||||
</div>
|
</div>
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
{#if contextMenu.show}
|
{#if contextMenu.show}
|
||||||
<div class="fixed z-[9999] bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-lg shadow-xl border border-[var(--theme-border)] py-1 min-w-[180px] overflow-hidden" style="left: {contextMenu.x}px; top: {contextMenu.y}px;">
|
<div class="fixed z-[9999] bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-lg shadow-xl border border-[var(--theme-border)] py-1 min-w-[180px] overflow-hidden" style="left: {contextMenu.x}px; top: {contextMenu.y}px;">
|
||||||
<button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2">
|
<button onclick={() => { navigator.clipboard.writeText(contextMenu.text); closeContextMenu(); }} class="w-full text-left px-4 py-2 text-sm hover:bg-[var(--theme-border)] flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-500"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--color-ink-muted)]"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
||||||
Copy Text
|
Copy Text
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
import Navbar from '$lib/components/dashboard/Navbar.svelte';
|
import Navbar from '$lib/components/dashboard/Navbar.svelte';
|
||||||
import ProjectCard from '$lib/components/dashboard/ProjectCard.svelte';
|
import ProjectCard from '$lib/components/dashboard/ProjectCard.svelte';
|
||||||
|
import PromptModal from '$lib/components/PromptModal.svelte';
|
||||||
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,7 +21,6 @@
|
|||||||
let shared = $state<Project[]>([]);
|
let shared = $state<Project[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let showCreate = $state(false);
|
let showCreate = $state(false);
|
||||||
let newName = $state('');
|
|
||||||
let creating = $state(false);
|
let creating = $state(false);
|
||||||
|
|
||||||
let activeMenu = $state<string | null>(null);
|
let activeMenu = $state<string | null>(null);
|
||||||
@@ -27,21 +29,20 @@
|
|||||||
let renameName = $state('');
|
let renameName = $state('');
|
||||||
let showInfo = $state(false);
|
let showInfo = $state(false);
|
||||||
let infoProject = $state<Project | null>(null);
|
let infoProject = $state<Project | null>(null);
|
||||||
|
let deleteTarget = $state<{ id: string; name: string } | null>(null);
|
||||||
|
|
||||||
function setActiveMenu(id: string | null) { activeMenu = id; }
|
function setActiveMenu(id: string | null) { activeMenu = id; }
|
||||||
function openInfo(project: Project) { activeMenu = null; infoProject = project; showInfo = true; }
|
function openInfo(project: Project) { activeMenu = null; infoProject = project; showInfo = true; }
|
||||||
function openRename(id: string, name: string) { activeMenu = null; renameId = id; renameName = name; showRename = true; }
|
function openRename(id: string, name: string) { activeMenu = null; renameId = id; renameName = name; showRename = true; }
|
||||||
|
|
||||||
async function submitRename(e: Event) {
|
async function submitRename(name: string) {
|
||||||
e.preventDefault();
|
|
||||||
if (!renameName.trim()) return;
|
|
||||||
const res = await fetch(`/api/projects/${renameId}`, {
|
const res = await fetch(`/api/projects/${renameId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: renameName.trim() })
|
body: JSON.stringify({ name })
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
projects = projects.map((p) => (p.id === renameId ? { ...p, name: renameName.trim() } : p));
|
projects = projects.map((p) => (p.id === renameId ? { ...p, name } : p));
|
||||||
}
|
}
|
||||||
showRename = false;
|
showRename = false;
|
||||||
}
|
}
|
||||||
@@ -62,13 +63,12 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create() {
|
async function create(name: string) {
|
||||||
if (!newName.trim()) return;
|
|
||||||
creating = true;
|
creating = true;
|
||||||
const res = await fetch('/api/projects', {
|
const res = await fetch('/api/projects', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: newName.trim() })
|
body: JSON.stringify({ name })
|
||||||
});
|
});
|
||||||
creating = false;
|
creating = false;
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -77,10 +77,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string, name: string) {
|
function requestDelete(id: string, name: string) {
|
||||||
if (!confirm(`Delete project "${name}"? This cannot be undone.`)) return;
|
activeMenu = null;
|
||||||
const res = await fetch(`/api/projects/${id}`, { method: 'DELETE' });
|
deleteTarget = { id, name };
|
||||||
if (res.ok) projects = projects.filter((p) => p.id !== id);
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteProject() {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
const res = await fetch(`/api/projects/${deleteTarget.id}`, { method: 'DELETE' });
|
||||||
|
if (res.ok) projects = projects.filter((p) => p.id !== deleteTarget!.id);
|
||||||
|
deleteTarget = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
@@ -92,32 +98,32 @@
|
|||||||
|
|
||||||
<svelte:window onclick={handleWindowClick} />
|
<svelte:window onclick={handleWindowClick} />
|
||||||
|
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-[var(--theme-bg)]">
|
<div class="min-h-screen bg-[var(--color-surface-muted)]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors mb-2 flex items-center gap-1.5">
|
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors mb-2 flex items-center gap-1.5">
|
||||||
<Icon icon="mdi:arrow-left" class="text-lg" />
|
<Icon icon="mdi:arrow-left" class="text-lg" />
|
||||||
Back to Dashboard
|
Back to Dashboard
|
||||||
</button>
|
</button>
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h2 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-blue-500" />
|
<Icon icon="mdi:folder-multiple-outline" class="text-[var(--color-accent)]" />
|
||||||
Projects
|
Projects
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Multi-file Typst workspaces with their own <code class="font-mono text-xs">typst.toml</code>.</p>
|
<p class="text-sm text-[var(--color-ink-muted)] mt-1">Multi-file Typst workspaces with their own <code class="font-mono text-xs">typst.toml</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={() => { showCreate = true; newName = ''; }} class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700 flex items-center gap-2">
|
<button onclick={() => (showCreate = true)} class="px-4 py-2 text-sm rounded-md bg-[var(--color-accent)] text-white hover:opacity-90 transition flex items-center gap-2">
|
||||||
<Icon icon="mdi:plus" class="text-lg" /> New Project
|
<Icon icon="mdi:plus" class="text-lg" /> New Project
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="text-gray-500 dark:text-gray-400">Loading…</p>
|
<p class="text-[var(--color-ink-muted)]">Loading…</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#if projects.length === 0}
|
{#if projects.length === 0}
|
||||||
<div class="text-center py-16 text-gray-500 dark:text-gray-400">
|
<div class="text-center py-16 text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-5xl mx-auto mb-3 opacity-50" />
|
<Icon icon="mdi:folder-multiple-outline" class="text-5xl mx-auto mb-3 opacity-50" />
|
||||||
<p>No projects yet. Create one to start a multi-file project.</p>
|
<p>No projects yet. Create one to start a multi-file project.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,29 +136,29 @@
|
|||||||
{setActiveMenu}
|
{setActiveMenu}
|
||||||
{openInfo}
|
{openInfo}
|
||||||
{openRename}
|
{openRename}
|
||||||
deleteProject={remove}
|
deleteProject={requestDelete}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if shared.length > 0}
|
{#if shared.length > 0}
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-10 mb-4 flex items-center gap-2">
|
<h3 class="text-lg font-semibold text-[var(--color-ink)] mt-10 mb-4 flex items-center gap-2">
|
||||||
<Icon icon="mdi:account-group-outline" class="text-blue-500" /> Shared with me
|
<Icon icon="mdi:account-group-outline" class="text-[var(--color-accent)]" /> Shared with me
|
||||||
</h3>
|
</h3>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{#each shared as project (project.id)}
|
{#each shared as project (project.id)}
|
||||||
<button onclick={() => goto(`/project/${project.id}`)} class="text-left bg-white dark:bg-black/20 rounded-xl border border-gray-200 dark:border-white/10 overflow-hidden hover:shadow-md transition-shadow">
|
<button onclick={() => goto(`/project/${project.id}`)} class="text-left bg-[var(--color-surface)] rounded-lg border border-[var(--color-line)] overflow-hidden hover:border-[var(--color-accent)] transition">
|
||||||
<div class="h-32 bg-gray-50 dark:bg-black/30 flex items-center justify-center overflow-hidden border-b border-gray-100 dark:border-white/5">
|
<div class="h-32 bg-[var(--color-surface-muted)] flex items-center justify-center overflow-hidden border-b border-[var(--color-line)]">
|
||||||
{#if project.thumbnail_svg}
|
{#if project.thumbnail_svg}
|
||||||
{@html project.thumbnail_svg}
|
{@html project.thumbnail_svg}
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-4xl text-gray-300 dark:text-gray-600" />
|
<Icon icon="mdi:folder-multiple-outline" class="text-4xl text-[var(--color-ink-muted)]" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<p class="font-medium text-gray-900 dark:text-white truncate">{project.name}</p>
|
<p class="font-medium text-[var(--color-ink)] truncate">{project.name}</p>
|
||||||
<p class="text-xs text-gray-400 mt-0.5">{project.effective_role}</p>
|
<p class="text-xs text-[var(--color-ink-muted)] mt-0.5">{project.effective_role}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -163,45 +169,59 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showCreate}
|
{#if showCreate}
|
||||||
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={() => (showCreate = false)} role="presentation">
|
<PromptModal
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md p-6" onclick={(e) => e.stopPropagation()} role="presentation">
|
title="New project"
|
||||||
<h2 class="text-lg font-bold mb-4 flex items-center gap-2"><Icon icon="mdi:folder-plus-outline" class="text-blue-500" /> New Project</h2>
|
label="Project name"
|
||||||
<input bind:value={newName} placeholder="Project name" onkeydown={(e) => e.key === 'Enter' && create()} class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-white/10 bg-transparent text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500/40" />
|
icon="ph:folder-star"
|
||||||
<div class="flex justify-end gap-2">
|
placeholder="Untitled Project"
|
||||||
<button onclick={() => (showCreate = false)} class="px-4 py-2 text-sm rounded-lg bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10">Cancel</button>
|
confirmLabel="Create"
|
||||||
<button onclick={create} disabled={creating} class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50">Create</button>
|
onsubmit={create}
|
||||||
</div>
|
onclose={() => (showCreate = false)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showRename}
|
{#if showRename}
|
||||||
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={() => (showRename = false)} role="presentation">
|
<PromptModal
|
||||||
<form onsubmit={submitRename} class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-md p-6" onclick={(e) => e.stopPropagation()}>
|
title="Rename project"
|
||||||
<h2 class="text-lg font-bold mb-4 flex items-center gap-2"><Icon icon="mdi:pencil-outline" class="text-yellow-500" /> Rename Project</h2>
|
label="Project name"
|
||||||
<input bind:value={renameName} class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-white/10 bg-transparent text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500/40" />
|
icon="ph:pencil-simple"
|
||||||
<div class="flex justify-end gap-2">
|
value={renameName}
|
||||||
<button type="button" onclick={() => (showRename = false)} class="px-4 py-2 text-sm rounded-lg bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10">Cancel</button>
|
confirmLabel="Save"
|
||||||
<button type="submit" class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700">Save</button>
|
onsubmit={submitRename}
|
||||||
</div>
|
onclose={() => (showRename = false)}
|
||||||
</form>
|
/>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showInfo && infoProject}
|
{#if showInfo && infoProject}
|
||||||
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onclick={() => (showInfo = false)} role="presentation">
|
<Modal title={infoProject.name} icon="ph:folder-star" onclose={() => (showInfo = false)}>
|
||||||
<div class="bg-[var(--theme-bg)] text-[var(--theme-text)] rounded-xl shadow-2xl border border-gray-200 dark:border-white/10 w-full max-w-sm overflow-hidden" onclick={(e) => e.stopPropagation()} role="presentation">
|
<div class="flex flex-col gap-4 text-xs">
|
||||||
<div class="p-6 border-b border-gray-100 dark:border-white/10 flex items-center gap-3">
|
<div>
|
||||||
<Icon icon="mdi:folder-multiple-outline" class="text-xl text-blue-500" />
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Entrypoint</p>
|
||||||
<h3 class="text-lg font-semibold flex-grow truncate">{infoProject.name}</h3>
|
<p class="font-mono text-sm text-[var(--color-ink)]">{infoProject.entrypoint}</p>
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-4 text-sm">
|
|
||||||
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Entrypoint</p><p class="font-mono">{infoProject.entrypoint}</p></div>
|
|
||||||
<div><p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Last Modified</p><p>{new Date(infoProject.updated_at.endsWith('Z') ? infoProject.updated_at : infoProject.updated_at + 'Z').toLocaleString()}</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 bg-gray-50 dark:bg-white/5 border-t border-gray-100 dark:border-white/10 flex justify-end">
|
|
||||||
<button onclick={() => (showInfo = false)} class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg">Close</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 font-medium text-[var(--color-ink-muted)]">Last modified</p>
|
||||||
|
<p class="text-sm text-[var(--color-ink)]">{new Date(infoProject.updated_at.endsWith('Z') ? infoProject.updated_at : infoProject.updated_at + 'Z').toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<button
|
||||||
|
onclick={() => (showInfo = false)}
|
||||||
|
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if deleteTarget}
|
||||||
|
<ConfirmModal
|
||||||
|
title="Delete project"
|
||||||
|
message={`'${deleteTarget.name}' will be permanently deleted. This cannot be undone.`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onconfirm={confirmDeleteProject}
|
||||||
|
onclose={() => (deleteTarget = null)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -62,30 +62,26 @@
|
|||||||
<meta name="description" content="Create a new TypstDrive account." />
|
<meta name="description" content="Create a new TypstDrive account." />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen flex flex-col relative overflow-hidden">
|
<div class="min-h-screen flex flex-col bg-[var(--color-surface-muted)]">
|
||||||
<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="absolute -top-40 right-20 w-96 h-96 bg-emerald-400/20 dark:bg-emerald-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
<div class="max-w-md w-full space-y-8 rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] p-10 shadow-2xl">
|
||||||
<div class="absolute top-40 -left-20 w-96 h-96 bg-teal-400/20 dark:bg-teal-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
|
||||||
<div class="absolute -bottom-40 right-40 w-96 h-96 bg-blue-400/20 dark:bg-blue-600/10 rounded-full blur-3xl mix-blend-multiply z-0"></div>
|
|
||||||
|
|
||||||
<div class="max-w-md w-full space-y-8 bg-white/80 dark:bg-black/40 backdrop-blur-xl p-10 rounded-2xl shadow-2xl border border-gray-200/50 dark:border-white/10 relative z-10">
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100/50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-500 mb-6 mx-auto shadow-sm">
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent)] mb-6 mx-auto shadow-sm">
|
||||||
<Icon icon="mdi:account-plus" class="text-3xl" />
|
<Icon icon="mdi:account-plus" class="text-3xl" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
<h2 class="text-3xl font-extrabold tracking-tight text-[var(--color-ink)]">
|
||||||
Create an account
|
Create an account
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 font-medium">
|
<p class="mt-2 text-sm text-[var(--color-ink-muted)] font-medium">
|
||||||
Join TypstDrive to start collaborating
|
Join TypstDrive to start collaborating
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if registrationDisabled}
|
{#if registrationDisabled}
|
||||||
<div class="flex flex-col items-center gap-4 py-4">
|
<div class="flex flex-col items-center gap-4 py-4">
|
||||||
<div class="flex items-center gap-3 w-full bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 p-4 rounded-xl border border-amber-200 dark:border-amber-500/20">
|
<div class="flex items-center gap-3 w-full bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 p-4 rounded-md border border-amber-200 dark:border-amber-500/20">
|
||||||
<Icon icon="mdi:lock-outline" class="text-2xl flex-shrink-0" />
|
<Icon icon="mdi:lock-outline" class="text-2xl flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-sm">Registration Disabled</p>
|
<p class="font-semibold text-sm">Registration disabled</p>
|
||||||
<p class="text-xs mt-0.5 text-amber-600 dark:text-amber-500">New account creation has been disabled by the administrator. Please contact your administrator to get an account.</p>
|
<p class="text-xs mt-0.5 text-amber-600 dark:text-amber-500">New account creation has been disabled by the administrator. Please contact your administrator to get an account.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,52 +90,52 @@
|
|||||||
<form class="mt-8 space-y-6" onsubmit={register}>
|
<form class="mt-8 space-y-6" onsubmit={register}>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Username</label>
|
<label for="username" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Icon icon="mdi:account" class="text-gray-400 dark:text-gray-500" />
|
<Icon icon="mdi:account" class="text-[var(--color-ink-muted)]" />
|
||||||
</div>
|
</div>
|
||||||
<input id="username" name="username" type="text" required bind:value={username} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="Choose a username">
|
<input id="username" name="username" type="text" required bind:value={username} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="Choose a username">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Email Address</label>
|
<label for="email" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Email address</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Icon icon="mdi:email" class="text-gray-400 dark:text-gray-500" />
|
<Icon icon="mdi:email" class="text-[var(--color-ink-muted)]" />
|
||||||
</div>
|
</div>
|
||||||
<input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="[email protected]">
|
<input id="email" name="email" type="email" required bind:value={email} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="[email protected]">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Password</label>
|
<label for="password" class="block text-sm font-semibold text-[var(--color-ink-muted)] mb-1.5">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Icon icon="mdi:lock" class="text-gray-400 dark:text-gray-500" />
|
<Icon icon="mdi:lock" class="text-[var(--color-ink-muted)]" />
|
||||||
</div>
|
</div>
|
||||||
<input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-xl border border-gray-300 dark:border-white/20 pl-10 pr-3 py-2.5 bg-white/50 dark:bg-black/40 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 sm:text-sm transition-all duration-200" placeholder="••••••••">
|
<input id="password" name="password" type="password" required bind:value={password} class="block w-full rounded-md border border-[var(--color-line)] pl-10 pr-3 py-2.5 bg-[var(--color-surface)] text-[var(--color-ink)] placeholder-[var(--color-ink-muted)] focus:border-[var(--color-accent)] focus:outline-none sm:text-sm transition-colors" placeholder="••••••••">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div class="flex items-center gap-2 text-red-600 bg-red-50 dark:bg-red-500/10 dark:text-red-400 p-3 rounded-lg text-sm border border-red-200 dark:border-red-500/20 shadow-sm animate-in slide-in-from-top-1 fade-in duration-200">
|
<div class="flex items-center gap-2 text-[var(--color-danger)] bg-[var(--color-danger)]/10 p-3 rounded-md text-sm border border-[var(--color-danger)]/20">
|
||||||
<Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" />
|
<Icon icon="mdi:alert-circle" class="text-lg flex-shrink-0" />
|
||||||
<span class="font-medium">{errorMsg}</span>
|
<span class="font-medium">{errorMsg}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent text-sm font-bold rounded-xl text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 focus:ring-blue-500 transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
<button type="submit" class="group w-full flex justify-center items-center gap-2 py-3 px-4 text-sm font-bold rounded-md text-white bg-[var(--color-accent)] hover:opacity-90 focus:outline-none transition">
|
||||||
Register
|
Register
|
||||||
<Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" />
|
<Icon icon="mdi:arrow-right" class="text-lg group-hover:translate-x-1 transition-transform" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm text-center mt-6 pt-4 border-t border-gray-200 dark:border-white/10">
|
<div class="text-sm text-center mt-6 pt-4 border-t border-[var(--color-line)]">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Already have an account? </span>
|
<span class="text-[var(--color-ink-muted)]">Already have an account? </span>
|
||||||
<a href="/login" class="font-bold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors hover:underline">
|
<a href="/login" class="font-bold text-[var(--color-accent)] hover:underline transition-colors">
|
||||||
Sign in
|
Sign in
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+126
-122
@@ -418,12 +418,12 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="min-h-screen flex flex-col">
|
||||||
<nav class="bg-[var(--theme-bg)] shadow-sm border-b border-gray-200 dark:border-white/10 px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0">
|
<nav class="bg-[var(--color-surface)] shadow-sm border-b border-[var(--color-line)] px-6 py-4 flex justify-between items-center sticky top-0 z-10 transition-colors duration-200 flex-shrink-0">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
<h1 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-3">
|
||||||
<Icon icon="mdi:cog" class="text-blue-600 dark:text-blue-400 text-3xl" />
|
<Icon icon="mdi:cog" class="text-[var(--color-accent)] text-3xl" />
|
||||||
Settings
|
Settings
|
||||||
</h1>
|
</h1>
|
||||||
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 px-4 py-2 rounded-lg flex items-center gap-2">
|
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] px-4 py-2 rounded-md flex items-center gap-2">
|
||||||
<Icon icon="mdi:arrow-left" class="text-lg" />
|
<Icon icon="mdi:arrow-left" class="text-lg" />
|
||||||
Back to Dashboard
|
Back to Dashboard
|
||||||
</button>
|
</button>
|
||||||
@@ -435,9 +435,9 @@
|
|||||||
{#each navItems as item}
|
{#each navItems as item}
|
||||||
<button
|
<button
|
||||||
onclick={() => activeSection = item.id}
|
onclick={() => activeSection = item.id}
|
||||||
class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 {activeSection === item.id
|
class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium transition-colors {activeSection === item.id
|
||||||
? 'bg-blue-600 text-white shadow-sm'
|
? 'bg-[var(--color-accent)] text-white shadow-sm'
|
||||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/5'}"
|
: 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||||
>
|
>
|
||||||
<Icon icon={item.icon} class="text-lg flex-shrink-0" />
|
<Icon icon={item.icon} class="text-lg flex-shrink-0" />
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -447,8 +447,8 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div class="pt-4 mt-4 border-t border-gray-200 dark:border-white/10">
|
<div class="pt-4 mt-4 border-t border-[var(--color-line)]">
|
||||||
<button onclick={logout} class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all duration-150">
|
<button onclick={logout} class="w-full flex items-center gap-3 px-4 py-2.5 rounded-md text-sm font-medium text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 transition-colors">
|
||||||
<Icon icon="mdi:logout" class="text-lg flex-shrink-0" />
|
<Icon icon="mdi:logout" class="text-lg flex-shrink-0" />
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
@@ -459,20 +459,20 @@
|
|||||||
<main class="flex-1 min-w-0 space-y-6 pb-16">
|
<main class="flex-1 min-w-0 space-y-6 pb-16">
|
||||||
|
|
||||||
{#if activeSection === 'account'}
|
{#if activeSection === 'account'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
|
||||||
<Icon icon="mdi:account-outline" class="text-2xl text-blue-500 dark:text-blue-400" />
|
<Icon icon="mdi:account-outline" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Account Settings
|
Account Settings
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<div class="h-14 w-14 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 text-2xl font-bold border border-blue-500/20 flex-shrink-0">
|
<div class="h-14 w-14 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] text-2xl font-bold flex-shrink-0">
|
||||||
{$userStore?.username?.[0]?.toUpperCase() || '?'}
|
{$userStore?.username?.[0]?.toUpperCase() || '?'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-base font-bold text-gray-900 dark:text-white">{$userStore?.username}</p>
|
<p class="text-base font-bold text-[var(--color-ink)]">{$userStore?.username}</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{$userStore?.email}</p>
|
<p class="text-sm text-[var(--color-ink-muted)]">{$userStore?.email}</p>
|
||||||
{#if $userStore?.is_admin}
|
{#if $userStore?.is_admin}
|
||||||
<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 mt-1">
|
<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 mt-1">
|
||||||
<Icon icon="mdi:shield-crown-outline" class="text-sm" />
|
<Icon icon="mdi:shield-crown-outline" class="text-sm" />
|
||||||
@@ -482,27 +482,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-px bg-gray-200 dark:bg-white/10 mb-6"></div>
|
<div class="h-px bg-[var(--color-line)] mb-6"></div>
|
||||||
|
|
||||||
<h3 class="text-sm font-bold text-gray-900 dark:text-white mb-4">Profile</h3>
|
<h3 class="text-sm font-bold text-[var(--color-ink)] mb-4">Profile</h3>
|
||||||
|
|
||||||
{#if profileError}
|
{#if profileError}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{profileError}</div>
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{profileError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if profileSuccess}
|
{#if profileSuccess}
|
||||||
<div class="bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 p-3 rounded-lg text-sm mb-4">Profile updated successfully.</div>
|
<div class="bg-[var(--color-success)]/10 text-[var(--color-success)] p-3 rounded-md text-sm mb-4">Profile updated successfully.</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label>
|
<label for="username-input" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Username</label>
|
||||||
<input id="username-input" type="text" bind:value={username} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" />
|
<input id="username-input" type="text" bind:value={username} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="email-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email Address</label>
|
<label for="email-input" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Email address</label>
|
||||||
<input id="email-input" type="email" bind:value={email} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" />
|
<input id="email-input" type="email" bind:value={email} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<button onclick={saveProfile} disabled={isSaving || (username === $userStore?.username && email === $userStore?.email)} class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-semibold transition-colors disabled:opacity-50 flex items-center gap-2 shadow-sm">
|
<button onclick={saveProfile} disabled={isSaving || (username === $userStore?.username && email === $userStore?.email)} class="bg-[var(--color-accent)] hover:opacity-90 text-white px-5 py-2 rounded-md text-sm font-semibold transition disabled:opacity-50 flex items-center gap-2 shadow-sm">
|
||||||
{#if isSaving}
|
{#if isSaving}
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-lg" />
|
<Icon icon="mdi:loading" class="animate-spin text-lg" />
|
||||||
Saving...
|
Saving...
|
||||||
@@ -513,31 +513,31 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-px bg-gray-200 dark:bg-white/10 my-6"></div>
|
<div class="h-px bg-[var(--color-line)] my-6"></div>
|
||||||
|
|
||||||
<h3 class="text-sm font-bold text-gray-900 dark:text-white mb-4">Change Password</h3>
|
<h3 class="text-sm font-bold text-[var(--color-ink)] mb-4">Change Password</h3>
|
||||||
|
|
||||||
{#if passwordError}
|
{#if passwordError}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{passwordError}</div>
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{passwordError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if passwordSuccess}
|
{#if passwordSuccess}
|
||||||
<div class="bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 p-3 rounded-lg text-sm mb-4">Password changed successfully.</div>
|
<div class="bg-[var(--color-success)]/10 text-[var(--color-success)] p-3 rounded-md text-sm mb-4">Password changed successfully.</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="current-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Current Password</label>
|
<label for="current-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Current password</label>
|
||||||
<input id="current-password" type="password" bind:value={currentPassword} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" />
|
<input id="current-password" type="password" bind:value={currentPassword} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="new-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">New Password</label>
|
<label for="new-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">New password</label>
|
||||||
<input id="new-password" type="password" bind:value={newPassword} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" />
|
<input id="new-password" type="password" bind:value={newPassword} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm New Password</label>
|
<label for="confirm-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Confirm new password</label>
|
||||||
<input id="confirm-password" type="password" bind:value={confirmPassword} class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" />
|
<input id="confirm-password" type="password" bind:value={confirmPassword} class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2 focus:border-[var(--color-accent)] focus:outline-none transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<button onclick={changePassword} disabled={isSavingPassword || !currentPassword || !newPassword || !confirmPassword} class="bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-white/10 dark:hover:bg-white/20 dark:text-white px-5 py-2 rounded-lg text-sm font-semibold transition-colors disabled:opacity-50 flex items-center gap-2">
|
<button onclick={changePassword} disabled={isSavingPassword || !currentPassword || !newPassword || !confirmPassword} class="bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink)] px-5 py-2 rounded-md text-sm font-semibold transition disabled:opacity-50 flex items-center gap-2">
|
||||||
{#if isSavingPassword}
|
{#if isSavingPassword}
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-lg" />
|
<Icon icon="mdi:loading" class="animate-spin text-lg" />
|
||||||
Updating...
|
Updating...
|
||||||
@@ -552,46 +552,46 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'theme'}
|
{#if activeSection === 'theme'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-2 flex items-center gap-2">
|
||||||
<Icon icon="mdi:palette-outline" class="text-2xl text-blue-500 dark:text-blue-400" />
|
<Icon icon="mdi:palette-outline" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Theme Settings
|
Theme Settings
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">Customize the appearance of your editor and dashboard. These settings are saved to your browser.</p>
|
<p class="text-sm text-[var(--color-ink-muted)] mb-6">Customize the appearance of your editor and dashboard. These settings are saved to your browser.</p>
|
||||||
<ThemePicker />
|
<ThemePicker />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'storage'}
|
{#if activeSection === 'storage'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] mb-6 flex items-center gap-2">
|
||||||
<Icon icon="mdi:harddisk" class="text-2xl text-blue-500 dark:text-blue-400" />
|
<Icon icon="mdi:harddisk" class="text-2xl text-[var(--color-accent)]" />
|
||||||
Storage
|
Storage
|
||||||
</h2>
|
</h2>
|
||||||
<div class="mb-4 flex justify-between items-end">
|
<div class="mb-4 flex justify-between items-end">
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Total Space Used</span>
|
<span class="text-sm font-medium text-[var(--color-ink-muted)]">Total space used</span>
|
||||||
<span class="text-sm font-bold text-gray-900 dark:text-white">
|
<span class="text-sm font-bold text-[var(--color-ink)]">
|
||||||
{storageStats ? formatBytes(storageStats.total_size_bytes) : 'Loading...'}
|
{storageStats ? formatBytes(storageStats.total_size_bytes) : 'Loading...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div class="bg-gray-50 dark:bg-black/30 p-4 rounded-xl border border-gray-200 dark:border-white/10 flex items-center gap-3">
|
<div class="bg-[var(--color-surface-muted)] p-4 rounded-lg border border-[var(--color-line)] flex items-center gap-3">
|
||||||
<div class="w-3 h-3 rounded-full bg-blue-500 flex-shrink-0"></div>
|
<div class="w-3 h-3 rounded-full bg-[var(--color-accent)] flex-shrink-0"></div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 dark:text-gray-400 text-xs">Documents</p>
|
<p class="text-[var(--color-ink-muted)] text-xs">Documents</p>
|
||||||
<p class="font-semibold text-gray-900 dark:text-white">
|
<p class="font-semibold text-[var(--color-ink)]">
|
||||||
{storageStats ? formatBytes(storageStats.documents_size_bytes) : '...'}
|
{storageStats ? formatBytes(storageStats.documents_size_bytes) : '...'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 dark:bg-black/30 p-4 rounded-xl border border-gray-200 dark:border-white/10 flex items-center gap-3">
|
<div class="bg-[var(--color-surface-muted)] p-4 rounded-lg border border-[var(--color-line)] flex items-center gap-3">
|
||||||
<div class="w-3 h-3 rounded-full bg-purple-500 flex-shrink-0"></div>
|
<div class="w-3 h-3 rounded-full bg-purple-500 flex-shrink-0"></div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 dark:text-gray-400 text-xs">Images & Assets</p>
|
<p class="text-[var(--color-ink-muted)] text-xs">Images & assets</p>
|
||||||
<p class="font-semibold text-gray-900 dark:text-white">
|
<p class="font-semibold text-[var(--color-ink)]">
|
||||||
{storageStats ? formatBytes(storageStats.files_size_bytes) : '...'}
|
{storageStats ? formatBytes(storageStats.files_size_bytes) : '...'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -602,33 +602,33 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'api-keys'}
|
{#if activeSection === 'api-keys'}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:key-outline" class="text-2xl text-blue-500 dark:text-blue-400" />
|
<Icon icon="mdi:key-outline" class="text-2xl text-[var(--color-accent)]" />
|
||||||
API Keys
|
API Keys
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onclick={() => { showCreateKeyForm = !showCreateKeyForm; createKeyError = ''; newlyCreatedKey = null; }}
|
onclick={() => { showCreateKeyForm = !showCreateKeyForm; createKeyError = ''; newlyCreatedKey = null; }}
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-lg transition-colors {showCreateKeyForm ? 'bg-gray-200 dark:bg-white/10 text-gray-700 dark:text-gray-300' : 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm'}"
|
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-md transition-colors {showCreateKeyForm ? 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]' : 'bg-[var(--color-accent)] hover:opacity-90 text-white shadow-sm'}"
|
||||||
>
|
>
|
||||||
<Icon icon={showCreateKeyForm ? 'mdi:close' : 'mdi:plus'} class="text-base" />
|
<Icon icon={showCreateKeyForm ? 'mdi:close' : 'mdi:plus'} class="text-base" />
|
||||||
{showCreateKeyForm ? 'Cancel' : 'New Key'}
|
{showCreateKeyForm ? 'Cancel' : 'New Key'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
<p class="text-sm text-[var(--color-ink-muted)] mb-4">
|
||||||
Use API keys to render Typst documents programmatically via <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">POST /v1/render</code>.
|
Use API keys to render Typst documents programmatically via <code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">POST /v1/render</code>.
|
||||||
Each key allows up to 60 requests/minute.
|
Each key allows up to 60 requests/minute.
|
||||||
<a href="/api-docs" class="text-blue-600 dark:text-blue-400 hover:underline ml-1">View API docs →</a>
|
<a href="/api-docs" class="text-[var(--color-accent)] hover:underline ml-1">View API docs →</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mb-6 p-4 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/30">
|
<div class="mb-6 p-4 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)]">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<p class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)]">
|
||||||
Requests — {usagePeriod === '1hr' ? 'Last 60 Min' : usagePeriod === '1day' ? 'Last 24 Hours' : 'Last 7 Days'}
|
Requests — {usagePeriod === '1hr' ? 'Last 60 Min' : usagePeriod === '1day' ? 'Last 24 Hours' : 'Last 7 Days'}
|
||||||
{#if usageData.length > 0}
|
{#if usageData.length > 0}
|
||||||
<span class="ml-2 normal-case font-normal text-gray-400 dark:text-gray-500">
|
<span class="ml-2 normal-case font-normal text-[var(--color-ink-muted)]">
|
||||||
({usageData.reduce((s, p) => s + p.count, 0)} total)
|
({usageData.reduce((s, p) => s + p.count, 0)} total)
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -637,18 +637,18 @@
|
|||||||
{#each ([['1hr', '1 hr'], ['1day', '1 day'], ['1week', '1 week']] as const) as [val, label]}
|
{#each ([['1hr', '1 hr'], ['1day', '1 day'], ['1week', '1 week']] as const) as [val, label]}
|
||||||
<button
|
<button
|
||||||
onclick={() => usagePeriod = val}
|
onclick={() => usagePeriod = val}
|
||||||
class="px-2 py-0.5 text-xs font-semibold rounded-md transition-colors {usagePeriod === val ? 'bg-blue-600 text-white' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-white/10'}"
|
class="px-2 py-0.5 text-xs font-semibold rounded-md transition-colors {usagePeriod === val ? 'bg-[var(--color-accent)] text-white' : 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]'}"
|
||||||
>{label}</button>
|
>{label}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-32">
|
<div class="h-32">
|
||||||
{#if usageLoading}
|
{#if usageLoading}
|
||||||
<div class="h-full flex items-center justify-center text-gray-400 dark:text-gray-500 text-sm">
|
<div class="h-full flex items-center justify-center text-[var(--color-ink-muted)] text-sm">
|
||||||
<Icon icon="mdi:loading" class="animate-spin mr-2" /> Loading...
|
<Icon icon="mdi:loading" class="animate-spin mr-2" /> Loading...
|
||||||
</div>
|
</div>
|
||||||
{:else if usageData.length === 0}
|
{:else if usageData.length === 0}
|
||||||
<div class="h-full flex items-center justify-center text-gray-400 dark:text-gray-500 text-sm">
|
<div class="h-full flex items-center justify-center text-[var(--color-ink-muted)] text-sm">
|
||||||
No usage yet — make your first API call to see data here.
|
No usage yet — make your first API call to see data here.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -658,24 +658,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if newlyCreatedKey}
|
{#if newlyCreatedKey}
|
||||||
<div class="mb-6 p-4 rounded-xl border border-green-200 dark:border-green-700/50 bg-green-50 dark:bg-green-900/10">
|
<div class="mb-6 p-4 rounded-lg border border-[var(--color-success)]/30 bg-[var(--color-success)]/10">
|
||||||
<div class="flex items-start justify-between gap-4 mb-2">
|
<div class="flex items-start justify-between gap-4 mb-2">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-bold text-green-800 dark:text-green-300 flex items-center gap-2">
|
<p class="text-sm font-bold text-[var(--color-success)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:check-circle" class="text-lg" />
|
<Icon icon="mdi:check-circle" class="text-lg" />
|
||||||
Key created: {newlyCreatedKey.name}
|
Key created: {newlyCreatedKey.name}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-green-700 dark:text-green-400 mt-0.5">Copy this key now — it will not be shown again.</p>
|
<p class="text-xs text-[var(--color-success)] mt-0.5 opacity-90">Copy this key now — it will not be shown again.</p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={() => newlyCreatedKey = null} class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200 flex-shrink-0">
|
<button onclick={() => newlyCreatedKey = null} class="text-[var(--color-success)] hover:opacity-70 flex-shrink-0">
|
||||||
<Icon icon="mdi:close" class="text-lg" />
|
<Icon icon="mdi:close" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 mt-3">
|
<div class="flex items-center gap-2 mt-3">
|
||||||
<code class="flex-1 font-mono text-xs bg-white dark:bg-black/40 border border-green-200 dark:border-green-700/50 text-gray-800 dark:text-gray-200 px-3 py-2 rounded-lg break-all">{newlyCreatedKey.key}</code>
|
<code class="flex-1 font-mono text-xs bg-[var(--color-surface)] border border-[var(--color-success)]/30 text-[var(--color-ink)] px-3 py-2 rounded-md break-all">{newlyCreatedKey.key}</code>
|
||||||
<button
|
<button
|
||||||
onclick={() => copyKey(newlyCreatedKey!.key)}
|
onclick={() => copyKey(newlyCreatedKey!.key)}
|
||||||
class="flex-shrink-0 flex items-center gap-1.5 px-3 py-2 text-sm font-semibold rounded-lg transition-colors {copiedKey ? 'bg-green-600 text-white' : 'bg-gray-200 dark:bg-white/10 hover:bg-gray-300 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300'}"
|
class="flex-shrink-0 flex items-center gap-1.5 px-3 py-2 text-sm font-semibold rounded-md transition-colors {copiedKey ? 'bg-[var(--color-success)] text-white' : 'bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)]'}"
|
||||||
>
|
>
|
||||||
<Icon icon={copiedKey ? 'mdi:check' : 'mdi:content-copy'} class="text-base" />
|
<Icon icon={copiedKey ? 'mdi:check' : 'mdi:content-copy'} class="text-base" />
|
||||||
{copiedKey ? 'Copied!' : 'Copy'}
|
{copiedKey ? 'Copied!' : 'Copy'}
|
||||||
@@ -685,29 +685,30 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCreateKeyForm}
|
{#if showCreateKeyForm}
|
||||||
<form onsubmit={createApiKey} class="mb-6 p-4 rounded-xl border border-blue-200 dark:border-blue-800/50 bg-blue-50/50 dark:bg-blue-900/10 space-y-3">
|
<form onsubmit={createApiKey} class="mb-6 p-4 rounded-lg border border-[var(--color-accent)]/30 bg-[var(--color-accent-soft)] space-y-3">
|
||||||
<h3 class="text-sm font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h3 class="text-sm font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:key-plus" class="text-blue-500" />
|
<Icon icon="mdi:key-plus" class="text-[var(--color-accent)]" />
|
||||||
Create API Key
|
Create API Key
|
||||||
</h3>
|
</h3>
|
||||||
{#if createKeyError}
|
{#if createKeyError}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 px-3 py-2 rounded-lg text-sm">{createKeyError}</div>
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] px-3 py-2 rounded-md text-sm">{createKeyError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex items-end gap-3">
|
<div class="flex items-end gap-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Key Name</label>
|
<label for="create-key-name" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Key name</label>
|
||||||
<input
|
<input
|
||||||
|
id="create-key-name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
bind:value={createKeyName}
|
bind:value={createKeyName}
|
||||||
placeholder="e.g. My App, CI Pipeline"
|
placeholder="e.g. My App, CI Pipeline"
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={createKeyLoading || !createKeyName.trim()}
|
disabled={createKeyLoading || !createKeyName.trim()}
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
class="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent)] hover:opacity-90 disabled:opacity-50 text-white text-sm font-semibold rounded-md transition shadow-sm"
|
||||||
>
|
>
|
||||||
{#if createKeyLoading}
|
{#if createKeyLoading}
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-base" />
|
<Icon icon="mdi:loading" class="animate-spin text-base" />
|
||||||
@@ -722,38 +723,38 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if apiKeysError}
|
{#if apiKeysError}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{apiKeysError}</div>
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{apiKeysError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if apiKeysLoading}
|
{#if apiKeysLoading}
|
||||||
<div class="flex items-center justify-center py-12 text-gray-400 dark:text-gray-500">
|
<div class="flex items-center justify-center py-12 text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" />
|
<Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" />
|
||||||
Loading keys...
|
Loading keys...
|
||||||
</div>
|
</div>
|
||||||
{:else if apiKeys.length === 0}
|
{:else if apiKeys.length === 0}
|
||||||
<div class="text-center py-12 text-gray-400 dark:text-gray-500">
|
<div class="text-center py-12 text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:key-outline" class="text-4xl mb-2 opacity-40" />
|
<Icon icon="mdi:key-outline" class="text-4xl mb-2 opacity-40" />
|
||||||
<p class="text-sm">No API keys yet. Create one to get started.</p>
|
<p class="text-sm">No API keys yet. Create one to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each apiKeys as key (key.id)}
|
{#each apiKeys as key (key.id)}
|
||||||
<div class="flex items-center gap-4 px-4 py-3 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/20">
|
<div class="flex items-center gap-4 px-4 py-3 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)]">
|
||||||
<div class="h-9 w-9 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 flex-shrink-0">
|
<div class="h-9 w-9 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] flex-shrink-0">
|
||||||
<Icon icon="mdi:key" class="text-lg" />
|
<Icon icon="mdi:key" class="text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{key.name}</p>
|
<p class="text-sm font-semibold text-[var(--color-ink)] truncate">{key.name}</p>
|
||||||
<p class="text-xs font-mono text-gray-500 dark:text-gray-400">{key.key_prefix}... · {key.rate_limit}/min</p>
|
<p class="text-xs font-mono text-[var(--color-ink-muted)]">{key.key_prefix}... · {key.rate_limit}/min</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right flex-shrink-0 hidden sm:block">
|
<div class="text-right flex-shrink-0 hidden sm:block">
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500">Created {formatDate(key.created_at)}</p>
|
<p class="text-xs text-[var(--color-ink-muted)]">Created {formatDate(key.created_at)}</p>
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500">{key.last_used_at ? `Last used ${formatDate(key.last_used_at)}` : 'Never used'}</p>
|
<p class="text-xs text-[var(--color-ink-muted)]">{key.last_used_at ? `Last used ${formatDate(key.last_used_at)}` : 'Never used'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 flex-shrink-0">
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
{#if confirmRegenerateId === key.id}
|
{#if confirmRegenerateId === key.id}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">Regenerate?</span>
|
<span class="text-xs text-[var(--color-ink-muted)]">Regenerate?</span>
|
||||||
<button
|
<button
|
||||||
onclick={() => regenerateApiKey(key.id)}
|
onclick={() => regenerateApiKey(key.id)}
|
||||||
disabled={regeneratingKeyId === key.id}
|
disabled={regeneratingKeyId === key.id}
|
||||||
@@ -763,24 +764,24 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => confirmRegenerateId = null}
|
onclick={() => confirmRegenerateId = null}
|
||||||
class="text-xs px-2 py-1 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-white/10 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300 font-semibold transition-colors"
|
class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)] font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if confirmDeleteKeyId === key.id}
|
{:else if confirmDeleteKeyId === key.id}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">Delete?</span>
|
<span class="text-xs text-[var(--color-ink-muted)]">Delete?</span>
|
||||||
<button
|
<button
|
||||||
onclick={() => deleteApiKey(key.id)}
|
onclick={() => deleteApiKey(key.id)}
|
||||||
disabled={deletingKeyId === key.id}
|
disabled={deletingKeyId === key.id}
|
||||||
class="text-xs px-2 py-1 rounded-md bg-red-600 hover:bg-red-700 text-white font-semibold transition-colors disabled:opacity-50"
|
class="text-xs px-2 py-1 rounded-md bg-[var(--color-danger)] hover:opacity-90 text-white font-semibold transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{deletingKeyId === key.id ? '...' : 'Yes'}
|
{deletingKeyId === key.id ? '...' : 'Yes'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => confirmDeleteKeyId = null}
|
onclick={() => confirmDeleteKeyId = null}
|
||||||
class="text-xs px-2 py-1 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-white/10 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300 font-semibold transition-colors"
|
class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)] font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
@@ -789,14 +790,14 @@
|
|||||||
<button
|
<button
|
||||||
onclick={() => { confirmRegenerateId = key.id; confirmDeleteKeyId = null; }}
|
onclick={() => { confirmRegenerateId = key.id; confirmDeleteKeyId = null; }}
|
||||||
title="Regenerate key"
|
title="Regenerate key"
|
||||||
class="p-1.5 rounded-lg text-gray-400 hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 transition-colors"
|
class="p-1.5 rounded-md text-[var(--color-ink-muted)] hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 transition-colors"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:refresh" class="text-lg" />
|
<Icon icon="mdi:refresh" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => { confirmDeleteKeyId = key.id; confirmRegenerateId = null; }}
|
onclick={() => { confirmDeleteKeyId = key.id; confirmRegenerateId = null; }}
|
||||||
title="Revoke key"
|
title="Revoke key"
|
||||||
class="p-1.5 rounded-lg text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors"
|
class="p-1.5 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 transition-colors"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:delete-outline" class="text-lg" />
|
<Icon icon="mdi:delete-outline" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
@@ -811,18 +812,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeSection === 'admin' && $userStore?.is_admin}
|
{#if activeSection === 'admin' && $userStore?.is_admin}
|
||||||
<div class="bg-white dark:bg-black/20 rounded-xl shadow-sm border border-gray-200 dark:border-white/10 overflow-hidden">
|
<div class="bg-[var(--color-surface)] rounded-xl shadow-sm border border-[var(--color-line)] overflow-hidden">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
<h2 class="text-xl font-bold text-[var(--color-ink)] flex items-center gap-2">
|
||||||
<Icon icon="mdi:shield-crown-outline" class="text-2xl text-amber-500 dark:text-amber-400" />
|
<Icon icon="mdi:shield-crown-outline" class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||||
User Management
|
User Management
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">{adminUsers.length} user{adminUsers.length !== 1 ? 's' : ''}</span>
|
<span class="text-sm text-[var(--color-ink-muted)]">{adminUsers.length} user{adminUsers.length !== 1 ? 's' : ''}</span>
|
||||||
<button
|
<button
|
||||||
onclick={() => { showCreateForm = !showCreateForm; createError = ''; }}
|
onclick={() => { showCreateForm = !showCreateForm; createError = ''; }}
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-lg transition-colors {showCreateForm ? 'bg-gray-200 dark:bg-white/10 text-gray-700 dark:text-gray-300' : 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm'}"
|
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-semibold rounded-md transition-colors {showCreateForm ? 'bg-[var(--color-surface-sunken)] text-[var(--color-ink-muted)]' : 'bg-[var(--color-accent)] hover:opacity-90 text-white shadow-sm'}"
|
||||||
>
|
>
|
||||||
<Icon icon={showCreateForm ? 'mdi:close' : 'mdi:account-plus-outline'} class="text-base" />
|
<Icon icon={showCreateForm ? 'mdi:close' : 'mdi:account-plus-outline'} class="text-base" />
|
||||||
{showCreateForm ? 'Cancel' : 'New User'}
|
{showCreateForm ? 'Cancel' : 'New User'}
|
||||||
@@ -831,52 +832,55 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showCreateForm}
|
{#if showCreateForm}
|
||||||
<form onsubmit={createUser} class="mb-6 p-4 rounded-xl border border-blue-200 dark:border-blue-800/50 bg-blue-50/50 dark:bg-blue-900/10 space-y-3">
|
<form onsubmit={createUser} class="mb-6 p-4 rounded-lg border border-[var(--color-accent)]/30 bg-[var(--color-accent-soft)] space-y-3">
|
||||||
<h3 class="text-sm font-bold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
<h3 class="text-sm font-bold text-[var(--color-ink)] mb-3 flex items-center gap-2">
|
||||||
<Icon icon="mdi:account-plus-outline" class="text-blue-500" />
|
<Icon icon="mdi:account-plus-outline" class="text-[var(--color-accent)]" />
|
||||||
Create New User
|
Create New User
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{#if createError}
|
{#if createError}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 px-3 py-2 rounded-lg text-sm">{createError}</div>
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] px-3 py-2 rounded-md text-sm">{createError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label>
|
<label for="create-username" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Username</label>
|
||||||
<input
|
<input
|
||||||
|
id="create-username"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
bind:value={createUsername}
|
bind:value={createUsername}
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
|
<label for="create-email" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Email</label>
|
||||||
<input
|
<input
|
||||||
|
id="create-email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
bind:value={createEmail}
|
bind:value={createEmail}
|
||||||
placeholder="[email protected]"
|
placeholder="[email protected]"
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Temporary Password</label>
|
<label for="create-password" class="block text-xs font-medium text-[var(--color-ink-muted)] mb-1">Temporary password</label>
|
||||||
<input
|
<input
|
||||||
|
id="create-password"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
bind:value={createPassword}
|
bind:value={createPassword}
|
||||||
placeholder="Set a password the user can change later"
|
placeholder="Set a password the user can change later"
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors font-mono"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none transition-colors font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between pt-1">
|
<div class="flex items-center justify-between pt-1">
|
||||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||||
<input type="checkbox" bind:checked={createIsAdmin} class="w-4 h-4 rounded accent-amber-500" />
|
<input type="checkbox" bind:checked={createIsAdmin} class="w-4 h-4 rounded accent-amber-500" />
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300 flex items-center gap-1">
|
<span class="text-sm text-[var(--color-ink-muted)] flex items-center gap-1">
|
||||||
<Icon icon="mdi:shield-crown-outline" class="text-amber-500 text-base" />
|
<Icon icon="mdi:shield-crown-outline" class="text-amber-500 text-base" />
|
||||||
Grant admin privileges
|
Grant admin privileges
|
||||||
</span>
|
</span>
|
||||||
@@ -884,7 +888,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={createLoading}
|
disabled={createLoading}
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
class="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent)] hover:opacity-90 disabled:opacity-50 text-white text-sm font-semibold rounded-md transition shadow-sm"
|
||||||
>
|
>
|
||||||
{#if createLoading}
|
{#if createLoading}
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-base" />
|
<Icon icon="mdi:loading" class="animate-spin text-base" />
|
||||||
@@ -899,56 +903,56 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adminError}
|
{#if adminError}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm mb-4">{adminError}</div>
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] p-3 rounded-md text-sm mb-4">{adminError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adminLoading}
|
{#if adminLoading}
|
||||||
<div class="flex items-center justify-center py-12 text-gray-400 dark:text-gray-500">
|
<div class="flex items-center justify-center py-12 text-[var(--color-ink-muted)]">
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" />
|
<Icon icon="mdi:loading" class="animate-spin text-2xl mr-2" />
|
||||||
Loading users...
|
Loading users...
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each adminUsers as user (user.id)}
|
{#each adminUsers as user (user.id)}
|
||||||
<div class="flex items-center gap-4 px-4 py-3 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-black/20 group">
|
<div class="flex items-center gap-4 px-4 py-3 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-muted)] group">
|
||||||
<div class="h-9 w-9 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-sm flex-shrink-0">
|
<div class="h-9 w-9 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center text-[var(--color-accent)] font-bold text-sm flex-shrink-0">
|
||||||
{user.username[0].toUpperCase()}
|
{user.username[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{user.username}</p>
|
<p class="text-sm font-semibold text-[var(--color-ink)] truncate">{user.username}</p>
|
||||||
{#if user.is_admin}
|
{#if user.is_admin}
|
||||||
<span class="text-xs font-bold px-1.5 py-0.5 rounded-md bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 flex-shrink-0">Admin</span>
|
<span class="text-xs font-bold px-1.5 py-0.5 rounded-md bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 flex-shrink-0">Admin</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if user.id === $userStore?.id}
|
{#if user.id === $userStore?.id}
|
||||||
<span class="text-xs px-1.5 py-0.5 rounded-md bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 flex-shrink-0">You</span>
|
<span class="text-xs px-1.5 py-0.5 rounded-md bg-[var(--color-accent-soft)] text-[var(--color-accent)] flex-shrink-0">You</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">{user.email}</p>
|
<p class="text-xs text-[var(--color-ink-muted)] truncate">{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500 flex-shrink-0 hidden sm:block">{formatDate(user.created_at)}</p>
|
<p class="text-xs text-[var(--color-ink-muted)] flex-shrink-0 hidden sm:block">{formatDate(user.created_at)}</p>
|
||||||
<div class="flex items-center gap-2 flex-shrink-0">
|
<div class="flex items-center gap-2 flex-shrink-0">
|
||||||
{#if user.id !== $userStore?.id}
|
{#if user.id !== $userStore?.id}
|
||||||
<button
|
<button
|
||||||
onclick={() => toggleAdmin(user)}
|
onclick={() => toggleAdmin(user)}
|
||||||
title={user.is_admin ? 'Revoke admin' : 'Grant admin'}
|
title={user.is_admin ? 'Revoke admin' : 'Grant admin'}
|
||||||
class="p-1.5 rounded-lg transition-colors {user.is_admin ? 'text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10' : 'text-gray-400 hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10'}"
|
class="p-1.5 rounded-md transition-colors {user.is_admin ? 'text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10' : 'text-[var(--color-ink-muted)] hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10'}"
|
||||||
>
|
>
|
||||||
<Icon icon={user.is_admin ? 'mdi:shield-crown' : 'mdi:shield-crown-outline'} class="text-lg" />
|
<Icon icon={user.is_admin ? 'mdi:shield-crown' : 'mdi:shield-crown-outline'} class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
{#if confirmDeleteId === user.id}
|
{#if confirmDeleteId === user.id}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">Delete?</span>
|
<span class="text-xs text-[var(--color-ink-muted)]">Delete?</span>
|
||||||
<button
|
<button
|
||||||
onclick={() => deleteUser(user.id)}
|
onclick={() => deleteUser(user.id)}
|
||||||
disabled={deletingUserId === user.id}
|
disabled={deletingUserId === user.id}
|
||||||
class="text-xs px-2 py-1 rounded-md bg-red-600 hover:bg-red-700 text-white font-semibold transition-colors disabled:opacity-50"
|
class="text-xs px-2 py-1 rounded-md bg-[var(--color-danger)] hover:opacity-90 text-white font-semibold transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{deletingUserId === user.id ? '...' : 'Yes'}
|
{deletingUserId === user.id ? '...' : 'Yes'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => confirmDeleteId = null}
|
onclick={() => confirmDeleteId = null}
|
||||||
class="text-xs px-2 py-1 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-white/10 dark:hover:bg-white/20 text-gray-700 dark:text-gray-300 font-semibold transition-colors"
|
class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-sunken)] hover:opacity-90 text-[var(--color-ink-muted)] font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
@@ -957,7 +961,7 @@
|
|||||||
<button
|
<button
|
||||||
onclick={() => confirmDeleteId = user.id}
|
onclick={() => confirmDeleteId = user.id}
|
||||||
title="Delete user"
|
title="Delete user"
|
||||||
class="p-1.5 rounded-lg text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors"
|
class="p-1.5 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 transition-colors"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:delete-outline" class="text-lg" />
|
<Icon icon="mdi:delete-outline" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -51,84 +51,84 @@
|
|||||||
<title>Setup - TypstDrive</title>
|
<title>Setup - TypstDrive</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen flex items-center justify-center px-4 py-16">
|
<div class="min-h-screen flex items-center justify-center px-4 py-16 bg-[var(--color-surface-muted)]">
|
||||||
<div class="w-full max-w-md">
|
<div class="w-full max-w-md">
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-blue-600 text-white mb-4 shadow-lg">
|
<div class="inline-flex items-center justify-center w-24 h-24 rounded-2xl bg-[var(--color-accent)] text-white mb-4 shadow-lg">
|
||||||
<Icon icon="mdi:shield-crown-outline" class="text-3xl" />
|
<img src="/favicon.png" alt="TypstDrive" class="h-14 w-14" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Welcome to TypstDrive</h1>
|
<h1 class="text-3xl font-bold text-[var(--color-ink)]">Welcome to TypstDrive</h1>
|
||||||
<p class="mt-2 text-gray-500 dark:text-gray-400">Create your admin account to get started.</p>
|
<p class="mt-2 text-[var(--color-ink-muted)]">Create your admin account to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-black/20 rounded-2xl shadow-xl border border-gray-200 dark:border-white/10 p-8">
|
<div class="rounded-xl border border-[var(--color-line)] bg-[var(--color-surface)] p-8 shadow-xl">
|
||||||
<div class="flex items-center gap-2 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 text-sm px-4 py-3 rounded-lg mb-6 border border-blue-200 dark:border-blue-800/50">
|
<div class="flex items-center gap-2 bg-[var(--color-accent-soft)] text-[var(--color-accent)] text-sm px-4 py-3 rounded-md mb-6">
|
||||||
<Icon icon="mdi:information-outline" class="text-lg flex-shrink-0" />
|
<Icon icon="mdi:information-outline" class="text-lg flex-shrink-0" />
|
||||||
<span>This is a one-time setup. The account you create here will have full admin privileges.</span>
|
<span>This is a one-time setup. The account you create here will have full admin privileges.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm mb-5 border border-red-200 dark:border-red-800/50">
|
<div class="bg-[var(--color-danger)]/10 text-[var(--color-danger)] px-4 py-3 rounded-md text-sm mb-5 border border-[var(--color-danger)]/20">
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form onsubmit={handleSetup} class="space-y-4">
|
<form onsubmit={handleSetup} class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label>
|
<label for="username" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Username</label>
|
||||||
<input
|
<input
|
||||||
id="username"
|
id="username"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
bind:value={username}
|
bind:value={username}
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
placeholder="admin"
|
placeholder="admin"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
|
<label for="email" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Email</label>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
placeholder="[email protected]"
|
placeholder="[email protected]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
|
<label for="password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Password</label>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
placeholder="Min. 8 characters"
|
placeholder="Min. 8 characters"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
|
<label for="confirm-password" class="block text-sm font-medium text-[var(--color-ink-muted)] mb-1">Confirm password</label>
|
||||||
<input
|
<input
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
bind:value={confirmPassword}
|
bind:value={confirmPassword}
|
||||||
class="w-full bg-white dark:bg-black/40 border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
class="w-full bg-[var(--color-surface)] border border-[var(--color-line)] text-[var(--color-ink)] rounded-md px-4 py-2.5 focus:border-[var(--color-accent)] focus:outline-none transition-colors"
|
||||||
placeholder="Repeat password"
|
placeholder="Repeat password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white font-semibold py-2.5 rounded-lg transition-colors mt-2 shadow-sm"
|
class="w-full flex items-center justify-center gap-2 bg-[var(--color-accent)] hover:opacity-90 disabled:opacity-60 text-white font-semibold py-2.5 rounded-md transition mt-2"
|
||||||
>
|
>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<Icon icon="mdi:loading" class="animate-spin text-lg" />
|
<Icon icon="mdi:loading" class="animate-spin text-lg" />
|
||||||
Creating account...
|
Creating account...
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="mdi:shield-check-outline" class="text-lg" />
|
<Icon icon="mdi:shield-check-outline" class="text-lg" />
|
||||||
Create Admin Account
|
Create admin account
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Reference in New Issue
Block a user