@hiai-gg/hiai-docs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.all-contributorsrc +18 -0
- package/.claude/settings.local.json +61 -0
- package/.dockerignore +113 -0
- package/.env.example +68 -0
- package/.github/FUNDING.yml +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
- package/.github/dependabot.yml +136 -0
- package/.github/pull_request_template.md +96 -0
- package/.github/workflows/ci.yml +283 -0
- package/AGENTS.md +237 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +77 -0
- package/Caddyfile +50 -0
- package/Dockerfile.backend +60 -0
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/RELEASE_CHECKLIST.md +34 -0
- package/SECURITY.md +60 -0
- package/backend/package.json +43 -0
- package/backend/src/__tests__/auth-helpers.test.ts +51 -0
- package/backend/src/__tests__/chunker.test.ts +65 -0
- package/backend/src/__tests__/config.test.ts +91 -0
- package/backend/src/__tests__/csrf.test.ts +91 -0
- package/backend/src/__tests__/embedding.test.ts +48 -0
- package/backend/src/__tests__/rate-limit.test.ts +46 -0
- package/backend/src/__tests__/routes.test.ts +38 -0
- package/backend/src/__tests__/schema.test.ts +31 -0
- package/backend/src/__tests__/validation.test.ts +556 -0
- package/backend/src/api/middleware/auth.ts +56 -0
- package/backend/src/api/middleware/csrf.ts +91 -0
- package/backend/src/api/middleware/rate-limit.ts +77 -0
- package/backend/src/api/middleware/webhook-verify.ts +22 -0
- package/backend/src/api/routes/attachments.ts +280 -0
- package/backend/src/api/routes/auth.ts +52 -0
- package/backend/src/api/routes/collaboration.ts +121 -0
- package/backend/src/api/routes/documents.ts +664 -0
- package/backend/src/api/routes/folders.ts +226 -0
- package/backend/src/api/routes/search.ts +354 -0
- package/backend/src/api/routes/share.ts +512 -0
- package/backend/src/api/routes/tags.ts +247 -0
- package/backend/src/api/routes/versions.ts +99 -0
- package/backend/src/api/routes/webhooks.ts +43 -0
- package/backend/src/embedding/chunker.ts +74 -0
- package/backend/src/embedding/index.ts +117 -0
- package/backend/src/embedding/providers/ollama.ts +63 -0
- package/backend/src/embedding/providers/openrouter.ts +71 -0
- package/backend/src/embedding/utils.ts +13 -0
- package/backend/src/embedding/worker.ts +89 -0
- package/backend/src/index.ts +147 -0
- package/backend/src/lib/auth-helpers.ts +27 -0
- package/backend/src/lib/auth.ts +35 -0
- package/backend/src/lib/config.ts +73 -0
- package/backend/src/lib/db.ts +7 -0
- package/backend/src/lib/embedding-queue.ts +12 -0
- package/backend/src/lib/logger.ts +18 -0
- package/backend/src/lib/markdown-to-doc.ts +45 -0
- package/backend/src/lib/minio.ts +46 -0
- package/backend/src/lib/redis.ts +19 -0
- package/backend/src/lib/yjs-provider.ts +182 -0
- package/backend/tests/integration/_harness.ts +754 -0
- package/backend/tests/integration/auth.test.ts +296 -0
- package/backend/tests/integration/routes.documents.test.ts +459 -0
- package/backend/tests/integration/routes.folders.test.ts +337 -0
- package/backend/tests/integration/routes.search.test.ts +322 -0
- package/backend/tests/integration/routes.share.test.ts +773 -0
- package/backend/tests/integration/routes.tags.test.ts +425 -0
- package/backend/tests/integration/routes.versions.test.ts +233 -0
- package/backend/tsconfig.json +18 -0
- package/docker-compose.yml +218 -0
- package/docs/API.md +328 -0
- package/docs/ARCHITECTURE.md +75 -0
- package/docs/DEPLOYMENT.md +113 -0
- package/docs/PRODUCTION_STATUS.md +61 -0
- package/docs/openapi.json +385 -0
- package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
- package/frontend/.svelte-kit.old/env.d.ts +1 -0
- package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
- package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
- package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
- package/frontend/.svelte-kit.old/generated/root.js +3 -0
- package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
- package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
- package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
- package/frontend/.svelte-kit.old/tsconfig.json +59 -0
- package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
- package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
- package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
- package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
- package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
- package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
- package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
- package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
- package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
- package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
- package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
- package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
- package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
- package/frontend/Dockerfile +44 -0
- package/frontend/biome.json +40 -0
- package/frontend/components.json +18 -0
- package/frontend/messages/en.json +434 -0
- package/frontend/package.json +70 -0
- package/frontend/project.inlang/settings.json +12 -0
- package/frontend/src/app.css +6 -0
- package/frontend/src/app.d.ts +13 -0
- package/frontend/src/app.html +30 -0
- package/frontend/src/hooks.server.ts +10 -0
- package/frontend/src/hooks.ts +10 -0
- package/frontend/src/lib/api/attachments.ts +45 -0
- package/frontend/src/lib/api/client.test.ts +15 -0
- package/frontend/src/lib/api/client.ts +57 -0
- package/frontend/src/lib/api/documents.ts +83 -0
- package/frontend/src/lib/api/folders.ts +180 -0
- package/frontend/src/lib/api/search.test.ts +52 -0
- package/frontend/src/lib/api/search.ts +128 -0
- package/frontend/src/lib/api/settings.ts +95 -0
- package/frontend/src/lib/api/share.ts +71 -0
- package/frontend/src/lib/api/tags.test.ts +91 -0
- package/frontend/src/lib/api/tags.ts +87 -0
- package/frontend/src/lib/auth-client.ts +10 -0
- package/frontend/src/lib/collaboration.ts +63 -0
- package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
- package/frontend/src/lib/components/DatePicker.svelte +322 -0
- package/frontend/src/lib/components/DocumentCard.svelte +166 -0
- package/frontend/src/lib/components/EmptyState.svelte +49 -0
- package/frontend/src/lib/components/FolderCard.svelte +93 -0
- package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
- package/frontend/src/lib/components/SearchBar.svelte +47 -0
- package/frontend/src/lib/components/SearchResult.svelte +115 -0
- package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
- package/frontend/src/lib/components/ShareDialog.svelte +158 -0
- package/frontend/src/lib/components/ShareLink.svelte +98 -0
- package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
- package/frontend/src/lib/components/VersionDiff.svelte +55 -0
- package/frontend/src/lib/components/VersionHistory.svelte +96 -0
- package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
- package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
- package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
- package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
- package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
- package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
- package/frontend/src/lib/components/editor/markdown.ts +38 -0
- package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
- package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
- package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
- package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
- package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
- package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
- package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
- package/frontend/src/lib/stores/theme.svelte.ts +97 -0
- package/frontend/src/lib/svelte.d.ts +6 -0
- package/frontend/src/lib/types.ts +44 -0
- package/frontend/src/lib/utils/clipboard.ts +17 -0
- package/frontend/src/lib/utils/strip-markdown.ts +59 -0
- package/frontend/src/lib/utils.ts +33 -0
- package/frontend/src/routes/(app)/+layout.svelte +17 -0
- package/frontend/src/routes/(app)/+page.server.ts +10 -0
- package/frontend/src/routes/(app)/+page.svelte +303 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
- package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
- package/frontend/src/routes/(app)/search/+page.svelte +593 -0
- package/frontend/src/routes/(app)/search/+page.ts +25 -0
- package/frontend/src/routes/+error.svelte +12 -0
- package/frontend/src/routes/+layout.svelte +18 -0
- package/frontend/src/routes/+layout.ts +2 -0
- package/frontend/src/routes/api/[...path]/+server.ts +111 -0
- package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
- package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
- package/frontend/src/routes/folders/[id]/+page.ts +14 -0
- package/frontend/src/routes/login/+page.svelte +90 -0
- package/frontend/src/routes/register/+page.svelte +97 -0
- package/frontend/src/routes/s/[token]/+page.svelte +496 -0
- package/frontend/src/routes/s/[token]/+page.ts +5 -0
- package/frontend/src/routes/settings/+page.svelte +175 -0
- package/frontend/static/favicon.png +0 -0
- package/frontend/static/logo.png +0 -0
- package/frontend/svelte.config.js +15 -0
- package/frontend/tsconfig.json +15 -0
- package/frontend/vite.config.ts +25 -0
- package/init.sql +9 -0
- package/logo.png +0 -0
- package/package.json +39 -0
- package/package.public.json +39 -0
- package/packages/db/drizzle.config.ts +10 -0
- package/packages/db/package.json +30 -0
- package/packages/db/src/client.ts +9 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
- package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
- package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
- package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
- package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
- package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
- package/packages/db/src/migrations/meta/_journal.json +27 -0
- package/packages/db/src/schema.ts +378 -0
- package/packages/db/tsconfig.json +17 -0
- package/scripts/export-openapi.ts +37 -0
- package/scripts/health-check.sh +75 -0
- package/scripts/migrate.sh +135 -0
- package/scripts/prework_backup.sh +25 -0
- package/scripts/release.sh +83 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
Clock,
|
|
5
|
+
Copy,
|
|
6
|
+
FileText,
|
|
7
|
+
Loader2,
|
|
8
|
+
Plus,
|
|
9
|
+
Tag,
|
|
10
|
+
Upload,
|
|
11
|
+
} from "lucide-svelte";
|
|
12
|
+
import { onDestroy } from "svelte";
|
|
13
|
+
import { goto } from "$app/navigation";
|
|
14
|
+
import {
|
|
15
|
+
createDocument,
|
|
16
|
+
type Document,
|
|
17
|
+
getDocument,
|
|
18
|
+
importDocument,
|
|
19
|
+
listDocuments,
|
|
20
|
+
} from "$lib/api/documents";
|
|
21
|
+
import SearchBar from "$lib/components/SearchBar.svelte";
|
|
22
|
+
import * as m from "$lib/paraglide/messages.js";
|
|
23
|
+
import {
|
|
24
|
+
getSelectedTag,
|
|
25
|
+
refreshDocs,
|
|
26
|
+
setSelectedTag,
|
|
27
|
+
} from "$lib/stores/tag-store.svelte";
|
|
28
|
+
import { copyToClipboard } from "$lib/utils/clipboard.js";
|
|
29
|
+
import { stripMarkdown } from "$lib/utils/strip-markdown";
|
|
30
|
+
|
|
31
|
+
let recentDocs = $state<Document[]>([]);
|
|
32
|
+
let loading = $state(true);
|
|
33
|
+
let error = $state<string | null>(null);
|
|
34
|
+
let importInput = $state<HTMLInputElement | undefined>(undefined);
|
|
35
|
+
let copiedDocId = $state<string | null>(null);
|
|
36
|
+
let copyLoadingDocId = $state<string | null>(null);
|
|
37
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
|
+
|
|
39
|
+
async function loadDocs(tagId: string | null = getSelectedTag()) {
|
|
40
|
+
loading = true;
|
|
41
|
+
error = null;
|
|
42
|
+
try {
|
|
43
|
+
const res = await listDocuments({
|
|
44
|
+
limit: 6,
|
|
45
|
+
...(tagId ? { tag: tagId } : {}),
|
|
46
|
+
});
|
|
47
|
+
recentDocs = res.items;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
error = err instanceof Error ? err.message : m.doc_load_error();
|
|
50
|
+
} finally {
|
|
51
|
+
loading = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Load on mount and reload whenever the shared selected tag changes (driven
|
|
56
|
+
// by the sidebar TagList), so a tag selection filters the dashboard too.
|
|
57
|
+
$effect(() => {
|
|
58
|
+
const tag = getSelectedTag();
|
|
59
|
+
void loadDocs(tag);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
onDestroy(() => {
|
|
63
|
+
if (copyTimer) {
|
|
64
|
+
clearTimeout(copyTimer);
|
|
65
|
+
copyTimer = null;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
async function handleCopyContent(e: MouseEvent, docId: string) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
if (typeof window === "undefined") return;
|
|
73
|
+
const cached = recentDocs.find((d) => d.id === docId);
|
|
74
|
+
let text = "";
|
|
75
|
+
copyLoadingDocId = docId;
|
|
76
|
+
try {
|
|
77
|
+
const full = await getDocument(docId);
|
|
78
|
+
text = full.content ?? "";
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("Dashboard: failed to fetch full document for copy", err);
|
|
81
|
+
text = cached?.excerpt ?? cached?.content ?? "";
|
|
82
|
+
} finally {
|
|
83
|
+
copyLoadingDocId = null;
|
|
84
|
+
}
|
|
85
|
+
if (!text) return;
|
|
86
|
+
const ok = await copyToClipboard(text);
|
|
87
|
+
if (!ok) return;
|
|
88
|
+
copiedDocId = docId;
|
|
89
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
90
|
+
copyTimer = setTimeout(() => {
|
|
91
|
+
copiedDocId = null;
|
|
92
|
+
copyTimer = null;
|
|
93
|
+
}, 2000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function triggerImport() {
|
|
97
|
+
importInput?.click();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleNewDocument() {
|
|
101
|
+
try {
|
|
102
|
+
const doc = await createDocument({
|
|
103
|
+
title: m.dashboard_untitled_document(),
|
|
104
|
+
content: "",
|
|
105
|
+
});
|
|
106
|
+
refreshDocs();
|
|
107
|
+
goto(`/docs/${doc.id}`);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
error = err instanceof Error ? err.message : m.error_document_save();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function handleImportFile(e: Event) {
|
|
114
|
+
const input = e.target as HTMLInputElement;
|
|
115
|
+
const file = input.files?.[0];
|
|
116
|
+
if (!file) return;
|
|
117
|
+
try {
|
|
118
|
+
await importDocument(file);
|
|
119
|
+
await loadDocs();
|
|
120
|
+
// Nudge sidebar components (RecentDocs, FolderTree) to refetch
|
|
121
|
+
// their document lists. They subscribe to the doc refresh nonce
|
|
122
|
+
// via $effect and re-load on change, so the imported document
|
|
123
|
+
// appears immediately without a page reload.
|
|
124
|
+
refreshDocs();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
error = err instanceof Error ? err.message : m.error_generic();
|
|
127
|
+
}
|
|
128
|
+
input.value = "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function relativeTime(iso: string): string {
|
|
132
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
133
|
+
const mins = Math.floor(diff / 60000);
|
|
134
|
+
if (mins < 60) return m.time_minutes_ago({ count: mins });
|
|
135
|
+
const hrs = Math.floor(mins / 60);
|
|
136
|
+
if (hrs < 24) return m.time_hours_ago({ count: hrs });
|
|
137
|
+
return m.time_days_ago({ count: Math.floor(hrs / 24) });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hasDocs = $derived(recentDocs.length > 0);
|
|
141
|
+
|
|
142
|
+
const availableTags = $derived(
|
|
143
|
+
(() => {
|
|
144
|
+
const seen = new Map<string, { id: string; name: string }>();
|
|
145
|
+
for (const doc of recentDocs) {
|
|
146
|
+
for (const t of doc.tags ?? []) {
|
|
147
|
+
if (!seen.has(t.id)) seen.set(t.id, { id: t.id, name: t.name });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
151
|
+
})(),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
function selectTag(tagId: string | null) {
|
|
155
|
+
// Drives the shared store; the $effect above reloads the doc list.
|
|
156
|
+
setSelectedTag(tagId);
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<svelte:head>
|
|
161
|
+
<title>{m.dashboard_page_title()}</title>
|
|
162
|
+
</svelte:head>
|
|
163
|
+
|
|
164
|
+
<div class="mx-auto max-w-5xl px-6 py-8">
|
|
165
|
+
<!-- Header -->
|
|
166
|
+
<div class="mb-8 flex items-center justify-between">
|
|
167
|
+
<div>
|
|
168
|
+
<h1 class="text-2xl font-semibold tracking-tight">{m.dashboard_title()}</h1>
|
|
169
|
+
<p class="text-sm text-muted-foreground">{m.dashboard_subtitle()}</p>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="flex items-center gap-2">
|
|
172
|
+
<input type="file" accept=".md,.txt,.json,.markdown" class="hidden" bind:this={importInput} onchange={handleImportFile} />
|
|
173
|
+
<button onclick={triggerImport} class="inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground">
|
|
174
|
+
<Upload class="size-4" />
|
|
175
|
+
{m.dashboard_import()}
|
|
176
|
+
</button>
|
|
177
|
+
<button onclick={handleNewDocument} class="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90">
|
|
178
|
+
<Plus class="size-4" />
|
|
179
|
+
{m.dashboard_new_document()}
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Search -->
|
|
185
|
+
<SearchBar class="mb-8" />
|
|
186
|
+
|
|
187
|
+
<!-- Tag Filter Bar -->
|
|
188
|
+
{#if !loading && !error && availableTags.length > 0}
|
|
189
|
+
<div class="mb-6 flex flex-wrap items-center gap-2">
|
|
190
|
+
<span class="text-xs font-medium text-muted-foreground">{m.dashboard_filter_label()}</span>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onclick={() => selectTag(null)}
|
|
194
|
+
class="inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors {getSelectedTag() === null ? 'bg-primary text-primary-foreground' : 'bg-secondary text-secondary-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
195
|
+
aria-pressed={getSelectedTag() === null}
|
|
196
|
+
>
|
|
197
|
+
{m.search_filter_all()}
|
|
198
|
+
</button>
|
|
199
|
+
{#each availableTags as tag (tag.id)}
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
onclick={() => selectTag(tag.id)}
|
|
203
|
+
class="inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors {getSelectedTag() === tag.id ? 'bg-primary text-primary-foreground' : 'bg-secondary text-secondary-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
204
|
+
aria-pressed={getSelectedTag() === tag.id}
|
|
205
|
+
>
|
|
206
|
+
<Tag class="size-3" />
|
|
207
|
+
{tag.name}
|
|
208
|
+
</button>
|
|
209
|
+
{/each}
|
|
210
|
+
</div>
|
|
211
|
+
{/if}
|
|
212
|
+
|
|
213
|
+
{#if loading}
|
|
214
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
215
|
+
{#each Array(3) as _}
|
|
216
|
+
<div class="rounded-lg border border-border bg-card p-4 animate-pulse">
|
|
217
|
+
<div class="mb-2 h-5 w-3/4 rounded bg-muted"></div>
|
|
218
|
+
<div class="mb-1 h-4 w-full rounded bg-muted"></div>
|
|
219
|
+
<div class="mb-3 h-4 w-2/3 rounded bg-muted"></div>
|
|
220
|
+
<div class="h-3 w-1/3 rounded bg-muted"></div>
|
|
221
|
+
</div>
|
|
222
|
+
{/each}
|
|
223
|
+
</div>
|
|
224
|
+
{:else if error}
|
|
225
|
+
<div class="flex flex-col items-center justify-center py-20 text-center">
|
|
226
|
+
<div class="mb-4 flex size-16 items-center justify-center rounded-full bg-destructive/10">
|
|
227
|
+
<FileText class="size-8 text-destructive" />
|
|
228
|
+
</div>
|
|
229
|
+
<h2 class="mb-2 text-lg font-semibold">{m.dashboard_error_title()}</h2>
|
|
230
|
+
<p class="mb-6 max-w-sm text-sm text-muted-foreground">{error}</p>
|
|
231
|
+
<button
|
|
232
|
+
onclick={() => loadDocs()}
|
|
233
|
+
class="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
|
234
|
+
>
|
|
235
|
+
{m.dashboard_error_retry()}
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
{:else if hasDocs}
|
|
239
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
240
|
+
{#each recentDocs as doc (doc.id)}
|
|
241
|
+
<a
|
|
242
|
+
href={`/docs/${doc.id}`}
|
|
243
|
+
class="group relative rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:shadow-md hover:border-primary/30"
|
|
244
|
+
>
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
class="absolute right-2 top-2 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-accent-foreground group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring {copiedDocId === doc.id || copyLoadingDocId === doc.id ? 'opacity-100' : ''}"
|
|
248
|
+
aria-label={m.action_copy_content()}
|
|
249
|
+
title={m.action_copy_content()}
|
|
250
|
+
disabled={copyLoadingDocId === doc.id}
|
|
251
|
+
onclick={(e: MouseEvent) => void handleCopyContent(e, doc.id)}
|
|
252
|
+
>
|
|
253
|
+
{#if copyLoadingDocId === doc.id}
|
|
254
|
+
<Loader2 class="size-3.5 animate-spin" />
|
|
255
|
+
{:else if copiedDocId === doc.id}
|
|
256
|
+
<Check class="size-3.5" />
|
|
257
|
+
{:else}
|
|
258
|
+
<Copy class="size-3.5" />
|
|
259
|
+
{/if}
|
|
260
|
+
</button>
|
|
261
|
+
<div class="mb-2 flex items-start justify-between pr-8">
|
|
262
|
+
<div class="flex items-center gap-2">
|
|
263
|
+
<FileText class="size-4 shrink-0 text-muted-foreground" />
|
|
264
|
+
<h3 class="font-medium leading-tight group-hover:text-primary">{doc.title}</h3>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
<p class="mb-3 text-sm text-muted-foreground line-clamp-2">{stripMarkdown(doc.content || "").slice(0, 120)}</p>
|
|
268
|
+
<div class="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
|
|
269
|
+
<span class="flex items-center gap-1 shrink-0">
|
|
270
|
+
<Clock class="size-3" />
|
|
271
|
+
{relativeTime(doc.updatedAt)}
|
|
272
|
+
</span>
|
|
273
|
+
{#if doc.tags?.length}
|
|
274
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
275
|
+
{#each doc.tags as tag (tag.id)}
|
|
276
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground">
|
|
277
|
+
<span class="inline-block size-2 rounded-full" style="background-color: {tag.color}"></span>
|
|
278
|
+
{tag.name}
|
|
279
|
+
</span>
|
|
280
|
+
{/each}
|
|
281
|
+
</div>
|
|
282
|
+
{/if}
|
|
283
|
+
</div>
|
|
284
|
+
</a>
|
|
285
|
+
{/each}
|
|
286
|
+
</div>
|
|
287
|
+
{:else}
|
|
288
|
+
<!-- Empty State -->
|
|
289
|
+
<div class="flex flex-col items-center justify-center py-20 text-center">
|
|
290
|
+
<div class="mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
|
|
291
|
+
<FileText class="size-8 text-muted-foreground" />
|
|
292
|
+
</div>
|
|
293
|
+
<h2 class="mb-2 text-lg font-semibold">{m.dashboard_empty_title()}</h2>
|
|
294
|
+
<p class="mb-6 max-w-sm text-sm text-muted-foreground">
|
|
295
|
+
{m.dashboard_empty_description()}
|
|
296
|
+
</p>
|
|
297
|
+
<button onclick={handleNewDocument} class="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90">
|
|
298
|
+
<Plus class="size-4" />
|
|
299
|
+
{m.dashboard_new_document()}
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServerLoadEvent } from "@sveltejs/kit";
|
|
2
|
+
import { redirect } from "@sveltejs/kit";
|
|
3
|
+
|
|
4
|
+
export async function load({ params, cookies }: ServerLoadEvent) {
|
|
5
|
+
const sessionCookie = cookies.get("better-auth.session_token");
|
|
6
|
+
if (!sessionCookie) {
|
|
7
|
+
throw redirect(302, "/login");
|
|
8
|
+
}
|
|
9
|
+
return { id: params.id };
|
|
10
|
+
}
|