@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,76 @@
1
+ <!-- ConfirmDialog.svelte — Generic confirmation dialog used in place of
2
+ window.confirm() so destructive actions get styled, accessible UI. -->
3
+ <script lang="ts">
4
+ import { Button } from "@hiai-gg/hiai-ui/components/ui/button";
5
+ import {
6
+ Dialog,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@hiai-gg/hiai-ui/components/ui/dialog";
12
+ import { Loader2 } from "lucide-svelte";
13
+ import * as m from "$lib/paraglide/messages.js";
14
+
15
+ let {
16
+ open = $bindable(false),
17
+ title,
18
+ description,
19
+ confirmLabel,
20
+ cancelLabel,
21
+ variant = "default",
22
+ busy = false,
23
+ onConfirm,
24
+ onCancel,
25
+ }: {
26
+ open?: boolean;
27
+ title: string;
28
+ description?: string;
29
+ confirmLabel?: string;
30
+ cancelLabel?: string;
31
+ variant?: "default" | "destructive";
32
+ busy?: boolean;
33
+ onConfirm?: () => void;
34
+ onCancel?: () => void;
35
+ } = $props();
36
+
37
+ function close() {
38
+ if (busy) return;
39
+ open = false;
40
+ onCancel?.();
41
+ }
42
+
43
+ function handleConfirm() {
44
+ onConfirm?.();
45
+ }
46
+ </script>
47
+
48
+ <Dialog
49
+ bind:open
50
+ onOpenChange={(next) => {
51
+ if (!next) onCancel?.();
52
+ }}
53
+ >
54
+ <DialogHeader>
55
+ <DialogTitle>{title}</DialogTitle>
56
+ {#if description}
57
+ <DialogDescription>{description}</DialogDescription>
58
+ {/if}
59
+ </DialogHeader>
60
+ <DialogFooter>
61
+ <Button variant="outline" type="button" onclick={close} disabled={busy}>
62
+ {cancelLabel ?? m.action_cancel()}
63
+ </Button>
64
+ <Button
65
+ type="button"
66
+ variant={variant === "destructive" ? "destructive" : "default"}
67
+ onclick={handleConfirm}
68
+ disabled={busy}
69
+ >
70
+ {#if busy}
71
+ <Loader2 class="mr-1 size-4 animate-spin" />
72
+ {/if}
73
+ {confirmLabel ?? m.action_confirm()}
74
+ </Button>
75
+ </DialogFooter>
76
+ </Dialog>
@@ -0,0 +1 @@
1
+ export { default as ConfirmDialog } from "./ConfirmDialog.svelte";
@@ -0,0 +1,56 @@
1
+ // tag-store.svelte.ts — Module-level reactive signal for cross-component
2
+ // tag list refresh. The sidebar TagList loads tags only on mount, so any
3
+ // tag mutation elsewhere (e.g. the document editor) needs to nudge it to
4
+ // reload. We expose a simple monotonically-increasing nonce that callers
5
+ // can read inside a $effect to trigger refreshes.
6
+
7
+ let tagRefreshNonce = $state(0);
8
+
9
+ export function refreshTags(): void {
10
+ tagRefreshNonce++;
11
+ }
12
+
13
+ export function getTagRefreshNonce(): number {
14
+ return tagRefreshNonce;
15
+ }
16
+
17
+ // Module-level reactive signal for cross-component document list refresh.
18
+ // The sidebar components (RecentDocs, FolderTree) and any other doc
19
+ // consumer load documents only on mount, so any document mutation
20
+ // elsewhere (e.g. the dashboard Import button) needs to nudge them to
21
+ // reload. Same nonce pattern as tagRefreshNonce: callers read it inside
22
+ // a $effect to trigger refreshes.
23
+ let docRefreshNonce = $state(0);
24
+
25
+ export function refreshDocs(): void {
26
+ docRefreshNonce++;
27
+ }
28
+
29
+ export function getDocRefreshNonce(): number {
30
+ return docRefreshNonce;
31
+ }
32
+
33
+ // Module-level reactive signal for the currently selected tag filter. Set by
34
+ // the sidebar TagList; read by the dashboard and the sidebar RecentDocs so a
35
+ // tag selection filters every document list in one place. `null` means no
36
+ // filter (show all).
37
+ let selectedTagId = $state<string | null>(null);
38
+ // Tag NAME for the same selection. List endpoints filter by tag id, but the
39
+ // search endpoint filters by tag name, so we keep both in sync here.
40
+ let selectedTagName = $state<string | null>(null);
41
+
42
+ export function setSelectedTag(
43
+ id: string | null,
44
+ name: string | null = null,
45
+ ): void {
46
+ selectedTagId = id;
47
+ selectedTagName = id ? name : null;
48
+ }
49
+
50
+ export function getSelectedTag(): string | null {
51
+ return selectedTagId;
52
+ }
53
+
54
+ export function getSelectedTagName(): string | null {
55
+ return selectedTagName;
56
+ }
@@ -0,0 +1,97 @@
1
+ import { browser } from "$app/environment";
2
+
3
+ export type Theme = "light" | "dark" | "system";
4
+
5
+ const STORAGE_KEY = "hiai-docs-theme";
6
+
7
+ type Listener = () => void;
8
+ const listeners = new Set<Listener>();
9
+
10
+ function readStored(): Theme {
11
+ if (!browser) return "system";
12
+ const value = localStorage.getItem(STORAGE_KEY);
13
+ if (value === "light" || value === "dark" || value === "system") return value;
14
+ return "system";
15
+ }
16
+
17
+ function systemPrefersDark(): boolean {
18
+ if (!browser) return false;
19
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
20
+ }
21
+
22
+ function resolveIsDark(theme: Theme): boolean {
23
+ if (theme === "dark") return true;
24
+ if (theme === "light") return false;
25
+ return systemPrefersDark();
26
+ }
27
+
28
+ function applyTheme(theme: Theme) {
29
+ if (!browser) return;
30
+ const isDark = resolveIsDark(theme);
31
+ document.documentElement.classList.toggle("dark", isDark);
32
+ }
33
+
34
+ function notify() {
35
+ for (const listener of listeners) listener();
36
+ }
37
+
38
+ // Runes-backed reactive state so `themeStore.value` / `.isDark` update the UI
39
+ // (e.g. the Settings theme picker highlight) when the theme changes.
40
+ let theme = $state<Theme>("system");
41
+ let resolvedIsDark = $state(false);
42
+ let initialized = false;
43
+
44
+ function initTheme() {
45
+ if (!browser || initialized) return;
46
+ initialized = true;
47
+ theme = readStored();
48
+ resolvedIsDark = resolveIsDark(theme);
49
+ applyTheme(theme);
50
+
51
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
52
+ const onSystemChange = () => {
53
+ if (theme !== "system") return;
54
+ resolvedIsDark = mql.matches;
55
+ applyTheme("system");
56
+ notify();
57
+ };
58
+ mql.addEventListener("change", onSystemChange);
59
+ }
60
+
61
+ function setTheme(value: Theme) {
62
+ theme = value;
63
+ if (browser) {
64
+ localStorage.setItem(STORAGE_KEY, value);
65
+ }
66
+ resolvedIsDark = resolveIsDark(value);
67
+ applyTheme(value);
68
+ notify();
69
+ }
70
+
71
+ function getTheme(): Theme {
72
+ return theme;
73
+ }
74
+
75
+ function getIsDark(): boolean {
76
+ return resolvedIsDark;
77
+ }
78
+
79
+ function subscribeTheme(listener: Listener): () => void {
80
+ listeners.add(listener);
81
+ return () => {
82
+ listeners.delete(listener);
83
+ };
84
+ }
85
+
86
+ export const themeStore = {
87
+ get value(): Theme {
88
+ return theme;
89
+ },
90
+ get isDark(): boolean {
91
+ return resolvedIsDark;
92
+ },
93
+ init: initTheme,
94
+ set: setTheme,
95
+ };
96
+
97
+ export { getIsDark, getTheme, initTheme, setTheme, subscribeTheme };
@@ -0,0 +1,6 @@
1
+ declare module "*.svelte" {
2
+ import type { Component } from "svelte";
3
+
4
+ const component: Component;
5
+ export default component;
6
+ }
@@ -0,0 +1,44 @@
1
+ /** Shared types for hiai-docs frontend */
2
+
3
+ export interface Document {
4
+ id: string;
5
+ title: string;
6
+ content?: string;
7
+ folderId: string | null;
8
+ folderName: string;
9
+ tags: string[];
10
+ createdAt: string; // ISO 8601
11
+ updatedAt: string; // ISO 8601
12
+ excerpt: string;
13
+ }
14
+
15
+ export interface Folder {
16
+ id: string;
17
+ name: string;
18
+ parentId: string | null;
19
+ documentCount: number;
20
+ children: Folder[];
21
+ documents: Document[];
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+
26
+ export interface Tag {
27
+ id: string;
28
+ name: string;
29
+ color: string;
30
+ }
31
+
32
+ export type ViewMode = "grid" | "list";
33
+ export type SortOption = "name" | "updated" | "created";
34
+ export type SortDirection = "asc" | "desc";
35
+
36
+ export interface CreateFolderData {
37
+ name: string;
38
+ parentId?: string | null;
39
+ }
40
+
41
+ export interface UpdateFolderData {
42
+ name?: string;
43
+ parentId?: string | null;
44
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copy `text` to the system clipboard via the async Clipboard API.
3
+ *
4
+ * Returns `true` if the copy succeeded and `false` if it was rejected or
5
+ * the API is unavailable (e.g. older browsers, insecure contexts, missing
6
+ * permissions). Callers should treat `false` as a soft failure — show
7
+ * feedback or fall back to a manual selection prompt rather than
8
+ * throwing.
9
+ */
10
+ export async function copyToClipboard(text: string): Promise<boolean> {
11
+ try {
12
+ await navigator.clipboard.writeText(text);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Strip common markdown syntax from a string, returning plain text.
3
+ * Intended for preview snippets — not a full markdown-to-text converter.
4
+ */
5
+ export function stripMarkdown(markdown: string): string {
6
+ let text = markdown;
7
+
8
+ // Fenced code blocks → content (strip ``` fences)
9
+ text = text.replace(/^```[\s\S]*?\n([\s\S]*?)^```/gm, "$1");
10
+
11
+ // Inline code
12
+ text = text.replace(/`([^`]+)`/g, "$1");
13
+
14
+ // Images → drop entirely (alt text included)
15
+ text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "");
16
+
17
+ // Truncated image markdown (no closing paren)
18
+ text = text.replace(/!\[[^\]]*\]\([^)]*$/g, "");
19
+
20
+ // Links → link text
21
+ text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
22
+
23
+ // Truncated link markdown (no closing paren)
24
+ text = text.replace(/\[[^\]]*\]\([^)]*$/g, "");
25
+
26
+ // Bold (both flavors)
27
+ text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
28
+ text = text.replace(/__([^_]+)__/g, "$1");
29
+
30
+ // Italic (both flavors)
31
+ text = text.replace(/\*([^*]+)\*/g, "$1");
32
+ text = text.replace(/_([^_]+)_/g, "$1");
33
+
34
+ // Strikethrough
35
+ text = text.replace(/~~([^~]+)~~/g, "$1");
36
+
37
+ // Headings: strip leading # + space
38
+ text = text.replace(/^#{1,6}\s+/gm, "");
39
+
40
+ // Blockquotes: strip leading > + space
41
+ text = text.replace(/^>\s?/gm, "");
42
+
43
+ // Horizontal rules
44
+ text = text.replace(/^[-*_]{3,}\s*$/gm, "");
45
+
46
+ // Unordered list markers at line start
47
+ text = text.replace(/^[\s]*[-*+]\s+/gm, "");
48
+
49
+ // Ordered list markers at line start
50
+ text = text.replace(/^[\s]*\d+\.\s+/gm, "");
51
+
52
+ // Collapse multiple blank lines
53
+ text = text.replace(/\n{3,}/g, "\n\n");
54
+
55
+ // Collapse internal whitespace (but preserve newlines)
56
+ text = text.replace(/[^\S\n]+/g, " ");
57
+
58
+ return text.trim();
59
+ }
@@ -0,0 +1,33 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export type WithElementRef<T> = T & { ref?: Element | null };
9
+
10
+ /** Format an ISO timestamp as a human-readable relative time string. */
11
+ export function formatRelativeTime(isoDate: string): string {
12
+ const now = Date.now();
13
+ const then = new Date(isoDate).getTime();
14
+ const diffMs = now - then;
15
+
16
+ const seconds = Math.floor(diffMs / 1000);
17
+ if (seconds < 60) return "just now";
18
+
19
+ const minutes = Math.floor(seconds / 60);
20
+ if (minutes < 60) return `${minutes}m ago`;
21
+
22
+ const hours = Math.floor(minutes / 60);
23
+ if (hours < 24) return `${hours}h ago`;
24
+
25
+ const days = Math.floor(hours / 24);
26
+ if (days < 30) return `${days}d ago`;
27
+
28
+ const months = Math.floor(days / 30);
29
+ if (months < 12) return `${months}mo ago`;
30
+
31
+ const years = Math.floor(months / 12);
32
+ return `${years}y ago`;
33
+ }
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import ScrollToTop from "$lib/components/ScrollToTop.svelte";
3
+ import Sidebar from "$lib/components/sidebar/Sidebar.svelte";
4
+
5
+ const { children } = $props();
6
+
7
+ let mainElement = $state<HTMLElement | null>(null);
8
+ </script>
9
+
10
+ <div class="flex h-screen">
11
+ <Sidebar />
12
+ <main id="main-content" bind:this={mainElement} class="relative z-0 flex-1 overflow-auto">
13
+ {@render children()}
14
+ </main>
15
+ </div>
16
+
17
+ <ScrollToTop scrollTarget={mainElement} />
@@ -0,0 +1,10 @@
1
+ import type { ServerLoadEvent } from "@sveltejs/kit";
2
+ import { redirect } from "@sveltejs/kit";
3
+
4
+ export async function load({ cookies }: ServerLoadEvent) {
5
+ const sessionCookie = cookies.get("better-auth.session_token");
6
+ if (!sessionCookie) {
7
+ throw redirect(302, "/login");
8
+ }
9
+ return {};
10
+ }