@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.
Files changed (216) hide show
  1. package/.all-contributorsrc +18 -0
  2. package/.claude/settings.local.json +61 -0
  3. package/.dockerignore +113 -0
  4. package/.env.example +68 -0
  5. package/.github/FUNDING.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
  8. package/.github/dependabot.yml +136 -0
  9. package/.github/pull_request_template.md +96 -0
  10. package/.github/workflows/ci.yml +283 -0
  11. package/AGENTS.md +237 -0
  12. package/CODE_OF_CONDUCT.md +134 -0
  13. package/CONTRIBUTING.md +77 -0
  14. package/Caddyfile +50 -0
  15. package/Dockerfile.backend +60 -0
  16. package/LICENSE +21 -0
  17. package/README.md +284 -0
  18. package/RELEASE_CHECKLIST.md +34 -0
  19. package/SECURITY.md +60 -0
  20. package/backend/package.json +43 -0
  21. package/backend/src/__tests__/auth-helpers.test.ts +51 -0
  22. package/backend/src/__tests__/chunker.test.ts +65 -0
  23. package/backend/src/__tests__/config.test.ts +91 -0
  24. package/backend/src/__tests__/csrf.test.ts +91 -0
  25. package/backend/src/__tests__/embedding.test.ts +48 -0
  26. package/backend/src/__tests__/rate-limit.test.ts +46 -0
  27. package/backend/src/__tests__/routes.test.ts +38 -0
  28. package/backend/src/__tests__/schema.test.ts +31 -0
  29. package/backend/src/__tests__/validation.test.ts +556 -0
  30. package/backend/src/api/middleware/auth.ts +56 -0
  31. package/backend/src/api/middleware/csrf.ts +91 -0
  32. package/backend/src/api/middleware/rate-limit.ts +77 -0
  33. package/backend/src/api/middleware/webhook-verify.ts +22 -0
  34. package/backend/src/api/routes/attachments.ts +280 -0
  35. package/backend/src/api/routes/auth.ts +52 -0
  36. package/backend/src/api/routes/collaboration.ts +121 -0
  37. package/backend/src/api/routes/documents.ts +664 -0
  38. package/backend/src/api/routes/folders.ts +226 -0
  39. package/backend/src/api/routes/search.ts +354 -0
  40. package/backend/src/api/routes/share.ts +512 -0
  41. package/backend/src/api/routes/tags.ts +247 -0
  42. package/backend/src/api/routes/versions.ts +99 -0
  43. package/backend/src/api/routes/webhooks.ts +43 -0
  44. package/backend/src/embedding/chunker.ts +74 -0
  45. package/backend/src/embedding/index.ts +117 -0
  46. package/backend/src/embedding/providers/ollama.ts +63 -0
  47. package/backend/src/embedding/providers/openrouter.ts +71 -0
  48. package/backend/src/embedding/utils.ts +13 -0
  49. package/backend/src/embedding/worker.ts +89 -0
  50. package/backend/src/index.ts +147 -0
  51. package/backend/src/lib/auth-helpers.ts +27 -0
  52. package/backend/src/lib/auth.ts +35 -0
  53. package/backend/src/lib/config.ts +73 -0
  54. package/backend/src/lib/db.ts +7 -0
  55. package/backend/src/lib/embedding-queue.ts +12 -0
  56. package/backend/src/lib/logger.ts +18 -0
  57. package/backend/src/lib/markdown-to-doc.ts +45 -0
  58. package/backend/src/lib/minio.ts +46 -0
  59. package/backend/src/lib/redis.ts +19 -0
  60. package/backend/src/lib/yjs-provider.ts +182 -0
  61. package/backend/tests/integration/_harness.ts +754 -0
  62. package/backend/tests/integration/auth.test.ts +296 -0
  63. package/backend/tests/integration/routes.documents.test.ts +459 -0
  64. package/backend/tests/integration/routes.folders.test.ts +337 -0
  65. package/backend/tests/integration/routes.search.test.ts +322 -0
  66. package/backend/tests/integration/routes.share.test.ts +773 -0
  67. package/backend/tests/integration/routes.tags.test.ts +425 -0
  68. package/backend/tests/integration/routes.versions.test.ts +233 -0
  69. package/backend/tsconfig.json +18 -0
  70. package/docker-compose.yml +218 -0
  71. package/docs/API.md +328 -0
  72. package/docs/ARCHITECTURE.md +75 -0
  73. package/docs/DEPLOYMENT.md +113 -0
  74. package/docs/PRODUCTION_STATUS.md +61 -0
  75. package/docs/openapi.json +385 -0
  76. package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
  77. package/frontend/.svelte-kit.old/env.d.ts +1 -0
  78. package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
  79. package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
  80. package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
  81. package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
  82. package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
  83. package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
  84. package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
  85. package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
  86. package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
  87. package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
  88. package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
  89. package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
  90. package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
  91. package/frontend/.svelte-kit.old/generated/root.js +3 -0
  92. package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
  93. package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
  94. package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
  95. package/frontend/.svelte-kit.old/tsconfig.json +59 -0
  96. package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
  97. package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
  98. package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
  99. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
  100. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
  101. package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
  102. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
  103. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
  104. package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
  105. package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
  106. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
  107. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
  108. package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
  109. package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
  110. package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
  111. package/frontend/Dockerfile +44 -0
  112. package/frontend/biome.json +40 -0
  113. package/frontend/components.json +18 -0
  114. package/frontend/messages/en.json +434 -0
  115. package/frontend/package.json +70 -0
  116. package/frontend/project.inlang/settings.json +12 -0
  117. package/frontend/src/app.css +6 -0
  118. package/frontend/src/app.d.ts +13 -0
  119. package/frontend/src/app.html +30 -0
  120. package/frontend/src/hooks.server.ts +10 -0
  121. package/frontend/src/hooks.ts +10 -0
  122. package/frontend/src/lib/api/attachments.ts +45 -0
  123. package/frontend/src/lib/api/client.test.ts +15 -0
  124. package/frontend/src/lib/api/client.ts +57 -0
  125. package/frontend/src/lib/api/documents.ts +83 -0
  126. package/frontend/src/lib/api/folders.ts +180 -0
  127. package/frontend/src/lib/api/search.test.ts +52 -0
  128. package/frontend/src/lib/api/search.ts +128 -0
  129. package/frontend/src/lib/api/settings.ts +95 -0
  130. package/frontend/src/lib/api/share.ts +71 -0
  131. package/frontend/src/lib/api/tags.test.ts +91 -0
  132. package/frontend/src/lib/api/tags.ts +87 -0
  133. package/frontend/src/lib/auth-client.ts +10 -0
  134. package/frontend/src/lib/collaboration.ts +63 -0
  135. package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
  136. package/frontend/src/lib/components/DatePicker.svelte +322 -0
  137. package/frontend/src/lib/components/DocumentCard.svelte +166 -0
  138. package/frontend/src/lib/components/EmptyState.svelte +49 -0
  139. package/frontend/src/lib/components/FolderCard.svelte +93 -0
  140. package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
  141. package/frontend/src/lib/components/SearchBar.svelte +47 -0
  142. package/frontend/src/lib/components/SearchResult.svelte +115 -0
  143. package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
  144. package/frontend/src/lib/components/ShareDialog.svelte +158 -0
  145. package/frontend/src/lib/components/ShareLink.svelte +98 -0
  146. package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
  147. package/frontend/src/lib/components/VersionDiff.svelte +55 -0
  148. package/frontend/src/lib/components/VersionHistory.svelte +96 -0
  149. package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
  150. package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
  151. package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
  152. package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
  153. package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
  154. package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
  155. package/frontend/src/lib/components/editor/markdown.ts +38 -0
  156. package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
  157. package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
  158. package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
  159. package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
  160. package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
  161. package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
  162. package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
  163. package/frontend/src/lib/stores/theme.svelte.ts +97 -0
  164. package/frontend/src/lib/svelte.d.ts +6 -0
  165. package/frontend/src/lib/types.ts +44 -0
  166. package/frontend/src/lib/utils/clipboard.ts +17 -0
  167. package/frontend/src/lib/utils/strip-markdown.ts +59 -0
  168. package/frontend/src/lib/utils.ts +33 -0
  169. package/frontend/src/routes/(app)/+layout.svelte +17 -0
  170. package/frontend/src/routes/(app)/+page.server.ts +10 -0
  171. package/frontend/src/routes/(app)/+page.svelte +303 -0
  172. package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
  173. package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
  174. package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
  175. package/frontend/src/routes/(app)/search/+page.svelte +593 -0
  176. package/frontend/src/routes/(app)/search/+page.ts +25 -0
  177. package/frontend/src/routes/+error.svelte +12 -0
  178. package/frontend/src/routes/+layout.svelte +18 -0
  179. package/frontend/src/routes/+layout.ts +2 -0
  180. package/frontend/src/routes/api/[...path]/+server.ts +111 -0
  181. package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
  182. package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
  183. package/frontend/src/routes/folders/[id]/+page.ts +14 -0
  184. package/frontend/src/routes/login/+page.svelte +90 -0
  185. package/frontend/src/routes/register/+page.svelte +97 -0
  186. package/frontend/src/routes/s/[token]/+page.svelte +496 -0
  187. package/frontend/src/routes/s/[token]/+page.ts +5 -0
  188. package/frontend/src/routes/settings/+page.svelte +175 -0
  189. package/frontend/static/favicon.png +0 -0
  190. package/frontend/static/logo.png +0 -0
  191. package/frontend/svelte.config.js +15 -0
  192. package/frontend/tsconfig.json +15 -0
  193. package/frontend/vite.config.ts +25 -0
  194. package/init.sql +9 -0
  195. package/logo.png +0 -0
  196. package/package.json +39 -0
  197. package/package.public.json +39 -0
  198. package/packages/db/drizzle.config.ts +10 -0
  199. package/packages/db/package.json +30 -0
  200. package/packages/db/src/client.ts +9 -0
  201. package/packages/db/src/index.ts +2 -0
  202. package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
  203. package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
  204. package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
  205. package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
  206. package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
  207. package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
  208. package/packages/db/src/migrations/meta/_journal.json +27 -0
  209. package/packages/db/src/schema.ts +378 -0
  210. package/packages/db/tsconfig.json +17 -0
  211. package/scripts/export-openapi.ts +37 -0
  212. package/scripts/health-check.sh +75 -0
  213. package/scripts/migrate.sh +135 -0
  214. package/scripts/prework_backup.sh +25 -0
  215. package/scripts/release.sh +83 -0
  216. package/tsconfig.json +25 -0
@@ -0,0 +1,166 @@
1
+ <script lang="ts">
2
+ import { Badge } from "@hiai-gg/hiai-ui/components/ui/badge";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardHeader,
7
+ } from "@hiai-gg/hiai-ui/components/ui/card";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from "@hiai-gg/hiai-ui/components/ui/dropdown-menu";
15
+ import {
16
+ ArrowUpRight,
17
+ Check,
18
+ Copy,
19
+ Files,
20
+ FileText,
21
+ FolderInput,
22
+ Loader2,
23
+ MoreVertical,
24
+ Trash2,
25
+ } from "lucide-svelte";
26
+ import { goto } from "$app/navigation";
27
+ import { getDocument } from "$lib/api/documents";
28
+ import * as m from "$lib/paraglide/messages.js";
29
+ import type { Document } from "$lib/types.js";
30
+ import { copyToClipboard } from "$lib/utils/clipboard.js";
31
+ import { stripMarkdown } from "$lib/utils/strip-markdown";
32
+ import { cn, formatRelativeTime } from "$lib/utils.js";
33
+
34
+ const {
35
+ document: doc,
36
+ onDelete,
37
+ onDuplicate,
38
+ }: {
39
+ document: Document;
40
+ onDelete?: (id: string) => void;
41
+ onDuplicate?: (id: string) => void;
42
+ } = $props();
43
+
44
+ function navigateToDoc() {
45
+ goto(`/docs/${doc.id}`);
46
+ }
47
+
48
+ function handleKeydown(e: KeyboardEvent) {
49
+ if (e.key === "Enter" || e.key === " ") {
50
+ e.preventDefault();
51
+ navigateToDoc();
52
+ }
53
+ }
54
+
55
+ let contentCopied = $state(false);
56
+ let contentCopying = $state(false);
57
+ let copyTimer: ReturnType<typeof setTimeout> | null = null;
58
+
59
+ async function handleCopyContent(e: Event) {
60
+ e.stopPropagation();
61
+ if (typeof window === "undefined") return;
62
+ // Copy the document's full markdown source. The list endpoint returns
63
+ // `content` truncated to 200 chars at the SQL level, so we fetch the
64
+ // single-document endpoint first to get the complete text. If the
65
+ // fetch fails we fall back to the card payload (excerpt, then
66
+ // truncated content) so the menu item still does something.
67
+ let text = "";
68
+ contentCopying = true;
69
+ try {
70
+ const full = await getDocument(doc.id);
71
+ text = full.content ?? "";
72
+ } catch (err) {
73
+ console.error("DocumentCard: failed to fetch full document for copy", err);
74
+ text = doc.excerpt || doc.content || "";
75
+ } finally {
76
+ contentCopying = false;
77
+ }
78
+ if (!text) return;
79
+ const ok = await copyToClipboard(text);
80
+ if (!ok) return;
81
+ contentCopied = true;
82
+ if (copyTimer) clearTimeout(copyTimer);
83
+ copyTimer = setTimeout(() => {
84
+ contentCopied = false;
85
+ copyTimer = null;
86
+ }, 2000);
87
+ }
88
+
89
+ const preview = $derived(
90
+ stripMarkdown(doc.excerpt || doc.content || "").slice(0, 100),
91
+ );
92
+ </script>
93
+
94
+ <Card
95
+ class="group cursor-pointer transition-shadow duration-200 hover:shadow-md"
96
+ onclick={navigateToDoc}
97
+ onkeydown={handleKeydown}
98
+ role="button"
99
+ tabindex={0}
100
+ >
101
+ <CardHeader class="flex-row items-start justify-between space-y-0 pb-2">
102
+ <div class="flex items-center gap-2 text-muted-foreground">
103
+ <FileText class="size-4 shrink-0" />
104
+ <span class="text-xs">{m.doc_updated({ time: formatRelativeTime(doc.updatedAt) })}</span>
105
+ </div>
106
+ <DropdownMenu>
107
+ <DropdownMenuTrigger
108
+ class="inline-flex size-8 items-center justify-center rounded-md opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100"
109
+ onclick={(e: MouseEvent) => e.stopPropagation()}
110
+ >
111
+ <MoreVertical class="size-4" />
112
+ <span class="sr-only">{m.doc_open_menu()}</span>
113
+ </DropdownMenuTrigger>
114
+ <DropdownMenuContent align="end">
115
+ <DropdownMenuItem onclick={() => goto(`/docs/${doc.id}`)}>
116
+ <ArrowUpRight class="size-4" />
117
+ {m.doc_open()}
118
+ </DropdownMenuItem>
119
+ <DropdownMenuItem onclick={handleCopyContent} disabled={contentCopying}>
120
+ {#if contentCopying}
121
+ <Loader2 class="size-4 animate-spin" />
122
+ {m.action_copy_content()}
123
+ {:else if contentCopied}
124
+ <Check class="size-4" />
125
+ {m.share_copied()}
126
+ {:else}
127
+ <Copy class="size-4" />
128
+ {m.action_copy_content()}
129
+ {/if}
130
+ </DropdownMenuItem>
131
+ <DropdownMenuItem onclick={(e: Event) => { e.stopPropagation(); onDuplicate?.(doc.id); }}>
132
+ <Files class="size-4" />
133
+ {m.doc_duplicate()}
134
+ </DropdownMenuItem>
135
+ <DropdownMenuItem onclick={(e: Event) => e.stopPropagation()}>
136
+ <FolderInput class="size-4" />
137
+ {m.doc_move_to_folder()}
138
+ </DropdownMenuItem>
139
+ <DropdownMenuSeparator />
140
+ <DropdownMenuItem
141
+ class="text-destructive"
142
+ onclick={(e: Event) => { e.stopPropagation(); onDelete?.(doc.id); }}
143
+ >
144
+ <Trash2 class="size-4" />
145
+ {m.action_delete()}
146
+ </DropdownMenuItem>
147
+ </DropdownMenuContent>
148
+ </DropdownMenu>
149
+ </CardHeader>
150
+ <CardContent>
151
+ <h3 class="mb-1 truncate text-sm font-medium leading-snug">{doc.title}</h3>
152
+ {#if preview}
153
+ <p class="mb-3 line-clamp-2 text-xs text-muted-foreground">{preview}</p>
154
+ {/if}
155
+ {#if doc.tags.length > 0}
156
+ <div class="flex flex-wrap gap-1">
157
+ {#each doc.tags.slice(0, 3) as tag (tag)}
158
+ <Badge variant="secondary" class="text-[10px]">{tag}</Badge>
159
+ {/each}
160
+ {#if doc.tags.length > 3}
161
+ <Badge variant="outline" class="text-[10px]">+{doc.tags.length - 3}</Badge>
162
+ {/if}
163
+ </div>
164
+ {/if}
165
+ </CardContent>
166
+ </Card>
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import { cn } from "$lib/utils.js";
3
+
4
+ interface Props {
5
+ icon?: typeof import("lucide-svelte").FileText;
6
+ title: string;
7
+ description?: string;
8
+ actionLabel?: string;
9
+ onAction?: () => void;
10
+ class?: string;
11
+ }
12
+
13
+ const {
14
+ icon: Icon,
15
+ title,
16
+ description,
17
+ actionLabel,
18
+ onAction,
19
+ class: className,
20
+ }: Props = $props();
21
+ </script>
22
+
23
+ <div
24
+ class={cn(
25
+ "flex flex-col items-center justify-center rounded-lg border border-dashed border-border p-12 text-center",
26
+ className,
27
+ )}
28
+ >
29
+ {#if Icon}
30
+ <div class="mb-4 rounded-full bg-muted p-4">
31
+ <Icon class="h-8 w-8 text-muted-foreground" strokeWidth={1.5} />
32
+ </div>
33
+ {/if}
34
+
35
+ <h3 class="text-lg font-medium text-foreground">{title}</h3>
36
+
37
+ {#if description}
38
+ <p class="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
39
+ {/if}
40
+
41
+ {#if actionLabel && onAction}
42
+ <button
43
+ onclick={onAction}
44
+ class="mt-4 inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
45
+ >
46
+ {actionLabel}
47
+ </button>
48
+ {/if}
49
+ </div>
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import { Card, CardContent } from "@hiai-gg/hiai-ui/components/ui/card";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuTrigger,
9
+ } from "@hiai-gg/hiai-ui/components/ui/dropdown-menu";
10
+ import {
11
+ Folder,
12
+ FolderInput,
13
+ MoreVertical,
14
+ Pencil,
15
+ Trash2,
16
+ } from "lucide-svelte";
17
+ import { goto } from "$app/navigation";
18
+ import * as m from "$lib/paraglide/messages.js";
19
+ import type { Folder as FolderType } from "$lib/types.js";
20
+ import { formatRelativeTime } from "$lib/utils.js";
21
+
22
+ const {
23
+ folder,
24
+ onDelete,
25
+ onRename,
26
+ }: {
27
+ folder: FolderType;
28
+ onDelete?: (id: string) => void;
29
+ onRename?: (id: string) => void;
30
+ } = $props();
31
+
32
+ function navigateToFolder() {
33
+ goto(`/folders/${folder.id}`);
34
+ }
35
+
36
+ function handleKeydown(e: KeyboardEvent) {
37
+ if (e.key === "Enter" || e.key === " ") {
38
+ e.preventDefault();
39
+ navigateToFolder();
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <Card
45
+ class="group cursor-pointer transition-shadow duration-200 hover:shadow-md"
46
+ onclick={navigateToFolder}
47
+ onkeydown={handleKeydown}
48
+ role="button"
49
+ tabindex={0}
50
+ >
51
+ <CardContent class="flex items-center gap-3 p-4">
52
+ <div class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
53
+ <Folder class="size-5 text-primary" />
54
+ </div>
55
+ <div class="min-w-0 flex-1">
56
+ <h3 class="truncate text-sm font-medium">{folder.name}</h3>
57
+ <p class="text-xs text-muted-foreground">
58
+ {folder.documentCount} {folder.documentCount === 1 ? m.folders_document() : m.folders_documents()} &middot; {formatRelativeTime(folder.updatedAt)}
59
+ </p>
60
+ </div>
61
+ <DropdownMenu>
62
+ <DropdownMenuTrigger
63
+ class="inline-flex size-8 shrink-0 items-center justify-center rounded-md opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100"
64
+ onclick={(e: MouseEvent) => e.stopPropagation()}
65
+ >
66
+ <MoreVertical class="size-4" />
67
+ <span class="sr-only">{m.doc_open_menu()}</span>
68
+ </DropdownMenuTrigger>
69
+ <DropdownMenuContent align="end">
70
+ <DropdownMenuItem onclick={() => goto(`/folders/${folder.id}`)}>
71
+ <Folder class="size-4" />
72
+ {m.doc_open()}
73
+ </DropdownMenuItem>
74
+ <DropdownMenuItem onclick={(e: Event) => { e.stopPropagation(); onRename?.(folder.id); }}>
75
+ <Pencil class="size-4" />
76
+ {m.folders_rename()}
77
+ </DropdownMenuItem>
78
+ <DropdownMenuItem onclick={(e: Event) => e.stopPropagation()}>
79
+ <FolderInput class="size-4" />
80
+ {m.folders_move()}
81
+ </DropdownMenuItem>
82
+ <DropdownMenuSeparator />
83
+ <DropdownMenuItem
84
+ class="text-destructive"
85
+ onclick={(e: Event) => { e.stopPropagation(); onDelete?.(folder.id); }}
86
+ >
87
+ <Trash2 class="size-4" />
88
+ {m.action_delete()}
89
+ </DropdownMenuItem>
90
+ </DropdownMenuContent>
91
+ </DropdownMenu>
92
+ </CardContent>
93
+ </Card>
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import { ArrowUp } from "lucide-svelte";
3
+ import { onDestroy } from "svelte";
4
+ import * as m from "$lib/paraglide/messages.js";
5
+
6
+ const SCROLL_THRESHOLD = 300;
7
+
8
+ let { scrollTarget }: { scrollTarget?: HTMLElement | null } = $props();
9
+
10
+ let visible = $state(false);
11
+ let activeTarget: HTMLElement | null = null;
12
+
13
+ function handleScroll() {
14
+ visible =
15
+ (activeTarget ? activeTarget.scrollTop : window.scrollY) > SCROLL_THRESHOLD;
16
+ }
17
+
18
+ function scrollToTop() {
19
+ if (activeTarget) {
20
+ activeTarget.scrollTo({ top: 0, behavior: "smooth" });
21
+ } else {
22
+ window.scrollTo({ top: 0, behavior: "smooth" });
23
+ }
24
+ }
25
+
26
+ function attach(target: HTMLElement | null | undefined) {
27
+ detach();
28
+ if (!target) return;
29
+ activeTarget = target;
30
+ target.addEventListener("scroll", handleScroll, { passive: true });
31
+ handleScroll();
32
+ }
33
+
34
+ function detach() {
35
+ if (activeTarget) {
36
+ activeTarget.removeEventListener("scroll", handleScroll);
37
+ }
38
+ activeTarget = null;
39
+ }
40
+
41
+ // React to changes in scrollTarget so we always listen on the right element.
42
+ // When scrollTarget is undefined, fall back to window-level scroll.
43
+ $effect(() => {
44
+ const target = scrollTarget;
45
+ attach(target);
46
+ if (!target) {
47
+ window.addEventListener("scroll", handleScroll, { passive: true });
48
+ handleScroll();
49
+ return () => {
50
+ window.removeEventListener("scroll", handleScroll);
51
+ };
52
+ }
53
+ return () => {
54
+ detach();
55
+ };
56
+ });
57
+
58
+ onDestroy(() => {
59
+ detach();
60
+ visible = false;
61
+ });
62
+ </script>
63
+
64
+ <button
65
+ type="button"
66
+ class="fixed bottom-6 right-6 z-50 inline-flex size-10 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-md transition-all hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring {visible ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
67
+ aria-label={m.scroll_to_top_aria()}
68
+ title={m.scroll_to_top_aria()}
69
+ onclick={scrollToTop}
70
+ >
71
+ <ArrowUp class="size-5" />
72
+ </button>
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { Search, X } from "lucide-svelte";
3
+ import { goto } from "$app/navigation";
4
+ import * as m from "$lib/paraglide/messages.js";
5
+ import { cn } from "$lib/utils.js";
6
+
7
+ const { class: className }: { class?: string } = $props();
8
+ let query = $state("");
9
+ let inputEl = $state<HTMLInputElement | null>(null);
10
+
11
+ function handleKeydown(e: KeyboardEvent) {
12
+ if (e.key === "Enter" && query.trim()) {
13
+ goto(`/search?q=${encodeURIComponent(query.trim())}`);
14
+ }
15
+ }
16
+
17
+ function clearQuery() {
18
+ query = "";
19
+ inputEl?.focus();
20
+ }
21
+ </script>
22
+
23
+ <div class={cn("relative", className)}>
24
+ <Search class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
25
+ <input
26
+ bind:this={inputEl}
27
+ type="text"
28
+ bind:value={query}
29
+ onkeydown={handleKeydown}
30
+ placeholder={m.search_placeholder()}
31
+ class={cn(
32
+ "flex h-9 w-full rounded-md border border-input bg-transparent pl-9 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
33
+ query ? "pr-9" : "pr-3"
34
+ )}
35
+ />
36
+ {#if query}
37
+ <button
38
+ type="button"
39
+ onclick={clearQuery}
40
+ class="absolute right-2 top-1/2 inline-flex size-5 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
41
+ aria-label={m.search_clear()}
42
+ title={m.search_clear()}
43
+ >
44
+ <X class="size-3.5" />
45
+ </button>
46
+ {/if}
47
+ </div>
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ import { Calendar, Folder, Tag } from "lucide-svelte";
3
+
4
+ interface Props {
5
+ id: string;
6
+ title: string;
7
+ snippet: string; // may contain <mark> tags
8
+ score: number;
9
+ folderName: string;
10
+ tags: Array<{ id: string; name: string; color: string | null }>;
11
+ createdAt: string;
12
+ query?: string;
13
+ }
14
+
15
+ const {
16
+ id,
17
+ title,
18
+ snippet,
19
+ score,
20
+ folderName,
21
+ tags,
22
+ createdAt,
23
+ query = "",
24
+ }: Props = $props();
25
+
26
+ function escapeHtml(text: string): string {
27
+ return text
28
+ .replace(/&/g, "&amp;")
29
+ .replace(/</g, "&lt;")
30
+ .replace(/>/g, "&gt;")
31
+ .replace(/"/g, "&quot;")
32
+ .replace(/'/g, "&#39;");
33
+ }
34
+
35
+ function highlightText(text: string, q: string): string {
36
+ if (!q) return escapeHtml(text);
37
+ const escapedQuery = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38
+ const safe = escapeHtml(text);
39
+ return safe.replace(new RegExp(`(${escapedQuery})`, "gi"), "<mark>$1</mark>");
40
+ }
41
+
42
+ const highlightedSnippet = $derived(highlightText(snippet, query));
43
+
44
+ const scorePercent = $derived(Math.round(score * 100));
45
+
46
+ const scoreColor = $derived(
47
+ scorePercent >= 90
48
+ ? "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300"
49
+ : scorePercent >= 75
50
+ ? "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300"
51
+ : "bg-muted text-muted-foreground",
52
+ );
53
+
54
+ const formattedDate = $derived(
55
+ new Date(createdAt).toLocaleDateString("en-US", {
56
+ year: "numeric",
57
+ month: "short",
58
+ day: "numeric",
59
+ }),
60
+ );
61
+ </script>
62
+
63
+ <a
64
+ href="/docs/{id}"
65
+ class="group block rounded-lg border border-border bg-card p-5 shadow-sm transition-all hover:border-primary/30 hover:shadow-md"
66
+ >
67
+ <!-- Header: title + score -->
68
+ <div class="flex items-start justify-between gap-3">
69
+ <h3
70
+ class="text-lg font-semibold leading-snug text-foreground group-hover:text-primary transition-colors"
71
+ >
72
+ {title}
73
+ </h3>
74
+ <span
75
+ class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {scoreColor}"
76
+ >
77
+ {scorePercent}%
78
+ </span>
79
+ </div>
80
+
81
+ <!-- Snippet with highlighted terms -->
82
+ <p
83
+ class="mt-2 text-sm leading-relaxed text-muted-foreground [&_mark]:rounded-sm [&_mark]:bg-yellow-200 [&_mark]:px-0.5 [&_mark]:text-foreground dark:[&_mark]:bg-yellow-900/60"
84
+ >
85
+ {@html highlightedSnippet}
86
+ </p>
87
+
88
+ <!-- Meta row: folder + tags + date -->
89
+ <div
90
+ class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground"
91
+ >
92
+ {#if folderName}
93
+ <span class="inline-flex items-center gap-1">
94
+ <Folder class="size-3.5" />
95
+ {folderName}
96
+ </span>
97
+ {/if}
98
+
99
+ {#if tags.length > 0}
100
+ {#each tags as tag (tag.id)}
101
+ <span class="inline-flex items-center gap-1 rounded-full bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground">
102
+ {#if tag.color}
103
+ <span class="inline-block size-2 rounded-full" style="background-color: {tag.color}"></span>
104
+ {/if}
105
+ {tag.name}
106
+ </span>
107
+ {/each}
108
+ {/if}
109
+
110
+ <span class="inline-flex items-center gap-1">
111
+ <Calendar class="size-3.5" />
112
+ {formattedDate}
113
+ </span>
114
+ </div>
115
+ </a>