@lapis-notes/lapis-plugin-docs 0.0.0-stage → 0.2.0

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 (197) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +188 -2
  3. package/dist/docs/DocsBlockMarkdownEditor.svelte +37 -0
  4. package/dist/docs/DocsBlockMarkdownEditor.svelte.d.ts +13 -0
  5. package/dist/docs/DocsBlockRefPicker.svelte +75 -0
  6. package/dist/docs/DocsBlockRefPicker.svelte.d.ts +11 -0
  7. package/dist/docs/DocsBookEditor.svelte +141 -0
  8. package/dist/docs/DocsBookEditor.svelte.d.ts +10 -0
  9. package/dist/docs/DocsBookNavigation.svelte +838 -0
  10. package/dist/docs/DocsBookNavigation.svelte.d.ts +11 -0
  11. package/dist/docs/DocsBooksColumn.svelte +82 -0
  12. package/dist/docs/DocsBooksColumn.svelte.d.ts +10 -0
  13. package/dist/docs/DocsCardBackMatter.svelte +32 -0
  14. package/dist/docs/DocsCardBackMatter.svelte.d.ts +10 -0
  15. package/dist/docs/DocsCardBlockExtra.svelte +43 -0
  16. package/dist/docs/DocsCardBlockExtra.svelte.d.ts +9 -0
  17. package/dist/docs/DocsCardPreviewFlip.svelte +143 -0
  18. package/dist/docs/DocsCardPreviewFlip.svelte.d.ts +17 -0
  19. package/dist/docs/DocsCardPreviewStack.svelte +139 -0
  20. package/dist/docs/DocsCardPreviewStack.svelte.d.ts +13 -0
  21. package/dist/docs/DocsCardPrompt.svelte +33 -0
  22. package/dist/docs/DocsCardPrompt.svelte.d.ts +8 -0
  23. package/dist/docs/DocsCardReviewAnswer.svelte +45 -0
  24. package/dist/docs/DocsCardReviewAnswer.svelte.d.ts +8 -0
  25. package/dist/docs/DocsCardTagChips.svelte +19 -0
  26. package/dist/docs/DocsCardTagChips.svelte.d.ts +7 -0
  27. package/dist/docs/DocsCardsPanel.svelte +232 -0
  28. package/dist/docs/DocsCardsPanel.svelte.d.ts +17 -0
  29. package/dist/docs/DocsCardsReview.css +1050 -0
  30. package/dist/docs/DocsChapterFooter.svelte +248 -0
  31. package/dist/docs/DocsChapterFooter.svelte.d.ts +11 -0
  32. package/dist/docs/DocsChapterScrollRing.svelte +101 -0
  33. package/dist/docs/DocsChapterScrollRing.svelte.d.ts +6 -0
  34. package/dist/docs/DocsDisplayPanel.css +627 -0
  35. package/dist/docs/DocsDisplayPanel.svelte +78 -0
  36. package/dist/docs/DocsDisplayPanel.svelte.d.ts +16 -0
  37. package/dist/docs/DocsEditorPanel.css +325 -0
  38. package/dist/docs/DocsEditorPanel.svelte +570 -0
  39. package/dist/docs/DocsEditorPanel.svelte.d.ts +22 -0
  40. package/dist/docs/DocsFlipCard.svelte +68 -0
  41. package/dist/docs/DocsFlipCard.svelte.d.ts +8 -0
  42. package/dist/docs/DocsFolderDirectoryList.svelte +206 -0
  43. package/dist/docs/DocsFolderDirectoryList.svelte.d.ts +14 -0
  44. package/dist/docs/DocsFolderPanel.css +357 -0
  45. package/dist/docs/DocsFolderPanel.svelte +102 -0
  46. package/dist/docs/DocsFolderPanel.svelte.d.ts +17 -0
  47. package/dist/docs/DocsLinksPanel.css +61 -0
  48. package/dist/docs/DocsLinksPanel.svelte +55 -0
  49. package/dist/docs/DocsLinksPanel.svelte.d.ts +11 -0
  50. package/dist/docs/DocsMarkdownField.svelte +93 -0
  51. package/dist/docs/DocsMarkdownField.svelte.d.ts +11 -0
  52. package/dist/docs/DocsMarkdownPreview.svelte +278 -0
  53. package/dist/docs/DocsMarkdownPreview.svelte.d.ts +17 -0
  54. package/dist/docs/DocsReviewPanel.svelte +63 -0
  55. package/dist/docs/DocsReviewPanel.svelte.d.ts +11 -0
  56. package/dist/docs/DocsRichBlock.svelte +291 -0
  57. package/dist/docs/DocsRichBlock.svelte.d.ts +13 -0
  58. package/dist/docs/DocsRichDisplay.svelte +316 -0
  59. package/dist/docs/DocsRichDisplay.svelte.d.ts +14 -0
  60. package/dist/docs/DocsSlidesPreview.css +243 -0
  61. package/dist/docs/DocsSlidesPreview.svelte +206 -0
  62. package/dist/docs/DocsSlidesPreview.svelte.d.ts +10 -0
  63. package/dist/docs/DocsWorkspace.css +570 -0
  64. package/dist/docs/DocsWorkspace.svelte +831 -0
  65. package/dist/docs/DocsWorkspace.svelte.d.ts +48 -0
  66. package/dist/docs/DocsWorkspaceToolbar.svelte +211 -0
  67. package/dist/docs/DocsWorkspaceToolbar.svelte.d.ts +27 -0
  68. package/dist/docs/candidates/ChoiceMenu.svelte +114 -0
  69. package/dist/docs/candidates/ChoiceMenu.svelte.d.ts +10 -0
  70. package/dist/docs/candidates/FlipCardDeck.svelte +817 -0
  71. package/dist/docs/candidates/FlipCardDeck.svelte.d.ts +37 -0
  72. package/dist/docs/candidates/MarkdownFloatingToc.svelte +313 -0
  73. package/dist/docs/candidates/MarkdownFloatingToc.svelte.d.ts +14 -0
  74. package/dist/docs/candidates/SearchableChoicePicker.css +161 -0
  75. package/dist/docs/candidates/SearchableChoicePicker.svelte +208 -0
  76. package/dist/docs/candidates/SearchableChoicePicker.svelte.d.ts +19 -0
  77. package/dist/docs/candidates/StructuredTable.svelte +98 -0
  78. package/dist/docs/candidates/StructuredTable.svelte.d.ts +8 -0
  79. package/dist/docs/candidates/StructuredTableEditor.svelte +374 -0
  80. package/dist/docs/candidates/StructuredTableEditor.svelte.d.ts +12 -0
  81. package/dist/docs/candidates/TagEditor.svelte +31 -0
  82. package/dist/docs/candidates/TagEditor.svelte.d.ts +14 -0
  83. package/dist/docs/candidates/WorkspaceHistoryControls.svelte +112 -0
  84. package/dist/docs/candidates/WorkspaceHistoryControls.svelte.d.ts +8 -0
  85. package/dist/docs/candidates/markdown-floating-toc.css +151 -0
  86. package/dist/docs/candidates/markdown-floating-toc.d.ts +1 -0
  87. package/dist/docs/candidates/markdown-floating-toc.js +1 -0
  88. package/dist/docs/docs-anki-import.d.ts +34 -0
  89. package/dist/docs/docs-anki-import.js +516 -0
  90. package/dist/docs/docs-book-view-model.d.ts +9 -0
  91. package/dist/docs/docs-book-view-model.js +71 -0
  92. package/dist/docs/docs-card-answer-markdown.d.ts +1 -0
  93. package/dist/docs/docs-card-answer-markdown.js +63 -0
  94. package/dist/docs/docs-card-display.d.ts +10 -0
  95. package/dist/docs/docs-card-display.js +18 -0
  96. package/dist/docs/docs-display-focus.d.ts +7 -0
  97. package/dist/docs/docs-display-focus.js +42 -0
  98. package/dist/docs/docs-document-surface.d.ts +7 -0
  99. package/dist/docs/docs-document-surface.js +25 -0
  100. package/dist/docs/docs-folder-directory.d.ts +21 -0
  101. package/dist/docs/docs-folder-directory.js +56 -0
  102. package/dist/docs/docs-link-mode.d.ts +16 -0
  103. package/dist/docs/docs-link-mode.js +94 -0
  104. package/dist/docs/docs-workspace-helpers.d.ts +25 -0
  105. package/dist/docs/docs-workspace-helpers.js +195 -0
  106. package/dist/docs/docs.tokens.css +13 -0
  107. package/dist/docs/import-anki-package.d.ts +10 -0
  108. package/dist/docs/import-anki-package.js +30 -0
  109. package/dist/docs/lapis-markdown-extensions.d.ts +9 -0
  110. package/dist/docs/lapis-markdown-extensions.js +128 -0
  111. package/dist/docs/pin-reading-chrome.d.ts +4 -0
  112. package/dist/docs/pin-reading-chrome.js +32 -0
  113. package/dist/docs/sidebar/DocsFoldersSidebar.svelte +134 -0
  114. package/dist/docs/sidebar/DocsFoldersSidebar.svelte.d.ts +15 -0
  115. package/dist/docs/sidebar/DocsLinksSidebar.svelte +105 -0
  116. package/dist/docs/sidebar/DocsLinksSidebar.svelte.d.ts +11 -0
  117. package/dist/docs/sidebar/DocsSidebar.css +346 -0
  118. package/dist/docs/sidebar/DocsSidebar.svelte +430 -0
  119. package/dist/docs/sidebar/DocsSidebar.svelte.d.ts +18 -0
  120. package/dist/docs/sidebar/DocsSidebarHost.svelte +139 -0
  121. package/dist/docs/sidebar/DocsSidebarHost.svelte.d.ts +12 -0
  122. package/dist/docs/sidebar/DocsTagsSidebar.svelte +176 -0
  123. package/dist/docs/sidebar/DocsTagsSidebar.svelte.d.ts +12 -0
  124. package/dist/docs/sidebar/docs-book-note-tree.d.ts +23 -0
  125. package/dist/docs/sidebar/docs-book-note-tree.js +88 -0
  126. package/dist/docs/sidebar/docs-note-sort.d.ts +26 -0
  127. package/dist/docs/sidebar/docs-note-sort.js +54 -0
  128. package/dist/docs/sidebar/docs-sidebar-filters.d.ts +17 -0
  129. package/dist/docs/sidebar/docs-sidebar-filters.js +74 -0
  130. package/dist/docs/sidebar/docs-sidebar-ui-storage.d.ts +24 -0
  131. package/dist/docs/sidebar/docs-sidebar-ui-storage.js +87 -0
  132. package/dist/docs/sidebar/docs-source-folder-tree.d.ts +16 -0
  133. package/dist/docs/sidebar/docs-source-folder-tree.js +143 -0
  134. package/dist/docs/sidebar/docs-tag-tree.d.ts +17 -0
  135. package/dist/docs/sidebar/docs-tag-tree.js +84 -0
  136. package/dist/docs-cards-view.d.ts +18 -0
  137. package/dist/docs-cards-view.js +52 -0
  138. package/dist/docs-document-view.d.ts +24 -0
  139. package/dist/docs-document-view.js +127 -0
  140. package/dist/docs-document-workspace.svelte +215 -0
  141. package/dist/docs-document-workspace.svelte.d.ts +16 -0
  142. package/dist/docs-index.d.ts +5 -0
  143. package/dist/docs-index.js +49 -0
  144. package/dist/docs-path.d.ts +6 -0
  145. package/dist/docs-path.js +10 -0
  146. package/dist/docs-plugin.d.ts +21 -0
  147. package/dist/docs-plugin.js +140 -0
  148. package/dist/docs-repository.d.ts +15 -0
  149. package/dist/docs-repository.js +75 -0
  150. package/dist/docs-review-view.d.ts +18 -0
  151. package/dist/docs-review-view.js +52 -0
  152. package/dist/docs-settings-tab.d.ts +8 -0
  153. package/dist/docs-settings-tab.js +25 -0
  154. package/dist/docs-settings.d.ts +17 -0
  155. package/dist/docs-settings.js +38 -0
  156. package/dist/docs-view.d.ts +22 -0
  157. package/dist/docs-view.js +51 -0
  158. package/dist/docs-workspace.svelte +394 -0
  159. package/dist/docs-workspace.svelte.d.ts +15 -0
  160. package/dist/index.d.ts +12 -0
  161. package/dist/index.js +12 -0
  162. package/dist/model/book.d.ts +6 -0
  163. package/dist/model/book.js +101 -0
  164. package/dist/model/coding-runtime.d.ts +4 -0
  165. package/dist/model/coding-runtime.js +9 -0
  166. package/dist/model/docs-card-context.d.ts +32 -0
  167. package/dist/model/docs-card-context.js +89 -0
  168. package/dist/model/docs-card-preview-stack-motion.d.ts +51 -0
  169. package/dist/model/docs-card-preview-stack-motion.js +94 -0
  170. package/dist/model/docs-media-context.d.ts +9 -0
  171. package/dist/model/docs-media-context.js +18 -0
  172. package/dist/model/docs-note-list.d.ts +8 -0
  173. package/dist/model/docs-note-list.js +25 -0
  174. package/dist/model/docs-relative-media.d.ts +30 -0
  175. package/dist/model/docs-relative-media.js +116 -0
  176. package/dist/model/docs-review-constants.d.ts +4 -0
  177. package/dist/model/docs-review-constants.js +13 -0
  178. package/dist/model/fsrs.d.ts +14 -0
  179. package/dist/model/fsrs.js +153 -0
  180. package/dist/model/graph.d.ts +12 -0
  181. package/dist/model/graph.js +169 -0
  182. package/dist/model/index.d.ts +14 -0
  183. package/dist/model/index.js +14 -0
  184. package/dist/model/markdown-normalize.d.ts +11 -0
  185. package/dist/model/markdown-normalize.js +755 -0
  186. package/dist/model/markdown.d.ts +167 -0
  187. package/dist/model/markdown.js +365 -0
  188. package/dist/model/sample.d.ts +7 -0
  189. package/dist/model/sample.js +524 -0
  190. package/dist/model/types.d.ts +328 -0
  191. package/dist/model/types.js +1 -0
  192. package/dist/model/workspace.d.ts +6 -0
  193. package/dist/model/workspace.js +81 -0
  194. package/dist/storybook.css +52 -0
  195. package/dist/styles.css +1 -0
  196. package/manifest.json +11 -0
  197. package/package.json +147 -4
@@ -0,0 +1,430 @@
1
+ <script lang="ts">
2
+ import BrainIcon from "@lucide/svelte/icons/brain";
3
+ import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
4
+ import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
5
+ import ClockIcon from "@lucide/svelte/icons/clock";
6
+ import HashIcon from "@lucide/svelte/icons/hash";
7
+ import PlusIcon from "@lucide/svelte/icons/plus";
8
+ import UploadIcon from "@lucide/svelte/icons/upload";
9
+ import XIcon from "@lucide/svelte/icons/x";
10
+ import { Button } from "@lapismd/design-core/shadcn/button";
11
+ import * as DropdownMenu from "@lapismd/design-core/shadcn/dropdown-menu";
12
+ import * as ScrollArea from "@lapismd/design-core/shadcn/scroll-area";
13
+ import { TagList, type TagListItem } from "@lapismd/design-core/shadcn/tag-list";
14
+ import {
15
+ docsNoteCardCount,
16
+ type DocsNotePanel,
17
+ type DocsNoteSummary,
18
+ type DocsReviewScope,
19
+ type DocsWorkspaceIndex,
20
+ } from "../../model";
21
+ import { isDocsSidebarPanelActive } from "../docs-folder-directory";
22
+ import DocsFoldersSidebar from "./DocsFoldersSidebar.svelte";
23
+ import DocsLinksSidebar from "./DocsLinksSidebar.svelte";
24
+ import DocsTagsSidebar from "./DocsTagsSidebar.svelte";
25
+ import {
26
+ createDocsBookNoteTree,
27
+ type DocsBookNoteTreeEntry,
28
+ } from "./docs-book-note-tree";
29
+ import {
30
+ docsSidebarFiltersActive,
31
+ docsTagSummariesForNotes,
32
+ filterDocsSidebarAmbiguousLinks,
33
+ filterDocsSidebarBacklinks,
34
+ filterDocsSidebarNotes,
35
+ filterDocsSidebarOutgoingLinks,
36
+ } from "./docs-sidebar-filters";
37
+ import {
38
+ DEFAULT_DOCS_NOTE_SORT,
39
+ type DocsNoteSortMode,
40
+ } from "./docs-note-sort";
41
+ import {
42
+ readDocsSidebarUiState,
43
+ writeDocsSidebarUiState,
44
+ type DocsSidebarView,
45
+ type DocsTagsViewMode,
46
+ } from "./docs-sidebar-ui-storage";
47
+ import "./DocsSidebar.css";
48
+
49
+ let {
50
+ workspace = null,
51
+ loading = false,
52
+ error = null,
53
+ activeNoteId = null,
54
+ activePanel = "display",
55
+ reviewScope = "note",
56
+ onSelectNote = () => {},
57
+ onSelectPanel = () => {},
58
+ onCreateNote = () => {},
59
+ onStartWorkspaceReview = () => {},
60
+ onImportAnkiPackage = undefined,
61
+ }: {
62
+ workspace?: DocsWorkspaceIndex | null;
63
+ loading?: boolean;
64
+ error?: string | null;
65
+ activeNoteId?: string | null;
66
+ activePanel?: DocsNotePanel;
67
+ reviewScope?: DocsReviewScope;
68
+ onSelectNote?: (noteId: string) => void;
69
+ onSelectPanel?: (noteId: string, panel: DocsNotePanel, reviewScope?: DocsReviewScope) => void;
70
+ onCreateNote?: () => void;
71
+ onStartWorkspaceReview?: () => void;
72
+ onImportAnkiPackage?: (file: File) => void;
73
+ } = $props();
74
+
75
+ const stored = readDocsSidebarUiState();
76
+ let view = $state<DocsSidebarView>(stored.sidebarView);
77
+ let tagsViewMode = $state<DocsTagsViewMode>(stored.tagsViewMode);
78
+ let sortMode = $state<DocsNoteSortMode>(stored.noteSortMode ?? DEFAULT_DOCS_NOTE_SORT);
79
+ let query = $state("");
80
+ let dueOnly = $state(false);
81
+ let selectedTags = $state<string[]>([]);
82
+ let bookOpenNodes = $state<Record<string, boolean>>(stored.bookOpenNodes);
83
+ let sourceFolderOpenNodes = $state<Record<string, boolean>>(stored.sourceFolderOpenNodes);
84
+ let tagsOpenNodes = $state<Record<string, boolean>>(stored.tagsOpenNodes);
85
+ let expandedNotes = $state<Record<string, boolean>>({});
86
+
87
+ const notes = $derived(workspace?.notes ?? []);
88
+ const dueNoteIds = $derived(
89
+ new Set((workspace?.reviewQueue ?? []).map((item) => item.noteId)),
90
+ );
91
+ const reviewAll = $derived(reviewScope === "workspace");
92
+ const noteFilters = $derived({
93
+ query,
94
+ dueOnly,
95
+ reviewAll,
96
+ selectedTags,
97
+ dueNoteIds,
98
+ });
99
+ const filtersActive = $derived(docsSidebarFiltersActive(noteFilters));
100
+ const filteredNotes = $derived(filterDocsSidebarNotes(notes, noteFilters));
101
+ const visibleNoteIds = $derived(new Set(filteredNotes.map((note) => note.id)));
102
+ const sidebarTags = $derived(
103
+ filtersActive ? docsTagSummariesForNotes(filteredNotes) : workspace?.tags ?? [],
104
+ );
105
+ const tree = $derived(
106
+ createDocsBookNoteTree(filteredNotes, workspace?.books ?? [], sortMode, filtersActive),
107
+ );
108
+ const activeBacklinks = $derived(
109
+ filterDocsSidebarBacklinks(
110
+ activeNoteId ? workspace?.graph.backlinks[activeNoteId] ?? [] : [],
111
+ visibleNoteIds,
112
+ filtersActive,
113
+ ),
114
+ );
115
+ const activeOutgoing = $derived(
116
+ filterDocsSidebarOutgoingLinks(
117
+ workspace?.graph.links.filter((link) => link.sourceNoteId === activeNoteId) ?? [],
118
+ visibleNoteIds,
119
+ filtersActive,
120
+ ),
121
+ );
122
+ const activeAmbiguous = $derived(
123
+ filterDocsSidebarAmbiguousLinks(
124
+ workspace?.graph.ambiguousLinks ?? [],
125
+ visibleNoteIds,
126
+ filtersActive,
127
+ ),
128
+ );
129
+ const sortOptions: Array<{ value: DocsNoteSortMode; label: string }> = [
130
+ { value: "updated-desc", label: "Updated" },
131
+ { value: "title-asc", label: "Title" },
132
+ { value: "created-desc", label: "Created" },
133
+ ];
134
+ const sortLabel = $derived(
135
+ sortOptions.find((option) => option.value === sortMode)?.label ?? "Title",
136
+ );
137
+ const tabs: Array<{ id: DocsSidebarView; label: string }> = [
138
+ { id: "docs", label: "Docs" },
139
+ { id: "folders", label: "Folders" },
140
+ { id: "links", label: "Links" },
141
+ { id: "tags", label: "Tags" },
142
+ ];
143
+
144
+ $effect(() => {
145
+ writeDocsSidebarUiState({
146
+ ...stored,
147
+ sidebarView: view,
148
+ tagsViewMode,
149
+ noteSortMode: sortMode,
150
+ bookOpenNodes,
151
+ sourceFolderOpenNodes,
152
+ tagsOpenNodes,
153
+ });
154
+ });
155
+
156
+ function bookOpen(bookId: string) {
157
+ return bookOpenNodes[bookId] !== false;
158
+ }
159
+
160
+ function noteOpen(noteId: string) {
161
+ return activeNoteId === noteId ? expandedNotes[noteId] !== false : expandedNotes[noteId] === true;
162
+ }
163
+
164
+ const selectedTagItems = $derived<TagListItem[]>(
165
+ selectedTags.map((tag) => ({
166
+ id: tag,
167
+ label: tag,
168
+ icon: HashIcon,
169
+ tone: "accent",
170
+ tooltip: `Remove #${tag}`,
171
+ onClick: () => {
172
+ selectedTags = selectedTags.filter((item) => item !== tag);
173
+ },
174
+ })),
175
+ );
176
+
177
+ function panels(note: DocsNoteSummary) {
178
+ return [
179
+ { id: "note" as const, label: "Note" },
180
+ { id: "cards" as const, label: "Cards", count: docsNoteCardCount(note) },
181
+ { id: "review" as const, label: "Review", count: note.dueCardCount ?? 0 },
182
+ ];
183
+ }
184
+
185
+ function chooseFile(event: Event) {
186
+ const input = event.currentTarget;
187
+ if (!(input instanceof HTMLInputElement)) return;
188
+ const file = input.files?.[0];
189
+ input.value = "";
190
+ if (file) onImportAnkiPackage?.(file);
191
+ }
192
+ </script>
193
+
194
+ <aside class="docs-sidebar" data-ui-component="docs-sidebar" data-docs-sidebar-view={view}>
195
+ <header class="docs-sidebar__header">
196
+ <h2 class="docs-sidebar__title">Docs</h2>
197
+ <div class="docs-sidebar__actions">
198
+ <button type="button" class="docs-sidebar__icon-button" aria-label="Create note" onclick={() => onCreateNote()}>
199
+ <PlusIcon aria-hidden="true" />
200
+ </button>
201
+ {#if onImportAnkiPackage}
202
+ <label class="docs-sidebar__icon-button" aria-label="Import Anki package">
203
+ <UploadIcon aria-hidden="true" />
204
+ <input
205
+ class="docs-sidebar__file"
206
+ type="file"
207
+ accept=".apkg,application/octet-stream"
208
+ aria-label="Anki package file"
209
+ onchange={chooseFile}
210
+ />
211
+ </label>
212
+ {/if}
213
+ </div>
214
+ </header>
215
+
216
+ <div class="docs-sidebar__views" role="group" aria-label="Docs sidebar">
217
+ {#each tabs as tab (tab.id)}
218
+ <Button
219
+ type="button"
220
+ variant="ghost"
221
+ size="sm"
222
+ class="docs-sidebar__view"
223
+ aria-pressed={view === tab.id}
224
+ title={tab.label}
225
+ onclick={() => (view = tab.id)}
226
+ >
227
+ <span class="docs-sidebar__view-label">{tab.label}</span>
228
+ </Button>
229
+ {/each}
230
+ </div>
231
+
232
+ <div class="docs-sidebar__toolbar">
233
+ <input class="docs-sidebar__search" placeholder="Filter notes" bind:value={query} />
234
+ <DropdownMenu.Root>
235
+ <DropdownMenu.Trigger>
236
+ {#snippet child({ props })}
237
+ <Button
238
+ {...props}
239
+ variant="outline"
240
+ size="sm"
241
+ class="docs-sidebar__sort"
242
+ aria-label="Sort notes"
243
+ >
244
+ {sortLabel}
245
+ <ChevronDownIcon aria-hidden="true" />
246
+ </Button>
247
+ {/snippet}
248
+ </DropdownMenu.Trigger>
249
+ <DropdownMenu.Content>
250
+ <DropdownMenu.RadioGroup bind:value={sortMode}>
251
+ {#each sortOptions as option (option.value)}
252
+ <DropdownMenu.RadioItem value={option.value}>{option.label}</DropdownMenu.RadioItem>
253
+ {/each}
254
+ </DropdownMenu.RadioGroup>
255
+ </DropdownMenu.Content>
256
+ </DropdownMenu.Root>
257
+ </div>
258
+ <div class="docs-sidebar__filters">
259
+ <Button
260
+ type="button"
261
+ variant="ghost"
262
+ size="sm"
263
+ class="docs-sidebar__filter"
264
+ aria-pressed={dueOnly}
265
+ title="Due"
266
+ onclick={() => (dueOnly = !dueOnly)}
267
+ >
268
+ <ClockIcon aria-hidden="true" />
269
+ <span class="docs-sidebar__view-label">Due</span>
270
+ </Button>
271
+ <Button
272
+ type="button"
273
+ variant="ghost"
274
+ size="sm"
275
+ class="docs-sidebar__filter"
276
+ aria-current={reviewScope === "workspace" ? "page" : undefined}
277
+ title="Review all"
278
+ onclick={() => onStartWorkspaceReview()}
279
+ >
280
+ <BrainIcon aria-hidden="true" />
281
+ <span class="docs-sidebar__view-label">Review all</span>
282
+ </Button>
283
+ </div>
284
+
285
+ {#if error}
286
+ <p class="docs-sidebar__status docs-sidebar__status--error">{error}</p>
287
+ {:else if loading}
288
+ <p class="docs-sidebar__status">Loading docs</p>
289
+ {/if}
290
+
291
+ <ScrollArea.Root class="docs-sidebar__scroll">
292
+ <div class="docs-sidebar__scroll-content">
293
+ {#if selectedTags.length}
294
+ <div class="docs-sidebar__selected-tags">
295
+ <TagList
296
+ class="docs-sidebar__tag-list"
297
+ label="Selected tag filters"
298
+ size="sm"
299
+ tags={selectedTagItems}
300
+ />
301
+ <Button
302
+ type="button"
303
+ variant="ghost"
304
+ size="icon-sm"
305
+ class="docs-sidebar__clear-tags"
306
+ aria-label="Clear tag filters"
307
+ title="Clear tag filters"
308
+ onclick={() => (selectedTags = [])}
309
+ >
310
+ <XIcon aria-hidden="true" />
311
+ </Button>
312
+ </div>
313
+ {/if}
314
+ {#if view === "docs"}
315
+ {#each tree as entry (entry.kind === "book" ? entry.book.indexNoteId : entry.note.id)}
316
+ {#if entry.kind === "book"}
317
+ {@render book(entry)}
318
+ {:else}
319
+ {@render noteRow(entry.note, 0)}
320
+ {/if}
321
+ {:else}
322
+ <p class="docs-sidebar__empty">No docs match this filter.</p>
323
+ {/each}
324
+ {:else if view === "folders"}
325
+ <DocsFoldersSidebar
326
+ notes={filteredNotes}
327
+ {sortMode}
328
+ {query}
329
+ {activeNoteId}
330
+ {activePanel}
331
+ bind:openNodes={sourceFolderOpenNodes}
332
+ {onSelectNote}
333
+ onSelectPanel={(noteId, panel) => onSelectPanel(noteId, panel)}
334
+ />
335
+ {:else if view === "links"}
336
+ <DocsLinksSidebar
337
+ {notes}
338
+ backlinks={activeBacklinks}
339
+ outgoingLinks={activeOutgoing}
340
+ ambiguousLinks={activeAmbiguous}
341
+ {onSelectNote}
342
+ />
343
+ {:else}
344
+ <DocsTagsSidebar
345
+ tags={sidebarTags}
346
+ {selectedTags}
347
+ bind:viewMode={tagsViewMode}
348
+ bind:openNodes={tagsOpenNodes}
349
+ onSelectTag={(tag) => {
350
+ selectedTags = selectedTags.includes(tag)
351
+ ? selectedTags.filter((item) => item !== tag)
352
+ : [...selectedTags, tag];
353
+ view = "docs";
354
+ }}
355
+ />
356
+ {/if}
357
+ </div>
358
+ </ScrollArea.Root>
359
+ </aside>
360
+
361
+ {#snippet book(entry: DocsBookNoteTreeEntry & { kind: "book" })}
362
+ <div class="docs-sidebar__group">
363
+ <button
364
+ type="button"
365
+ class="docs-sidebar__row"
366
+ onclick={() => {
367
+ bookOpenNodes = { ...bookOpenNodes, [entry.book.indexNoteId]: !bookOpen(entry.book.indexNoteId) };
368
+ if (entry.indexNote) onSelectNote(entry.indexNote.id);
369
+ }}
370
+ >
371
+ <span class="docs-sidebar__chevron" data-open={bookOpen(entry.book.indexNoteId) ? "true" : "false"}>
372
+ <ChevronRightIcon aria-hidden="true" />
373
+ </span>
374
+ <span class="docs-sidebar__label">{entry.book.title}</span>
375
+ <span class="docs-sidebar__count">{entry.chapters.length}</span>
376
+ </button>
377
+ {#if bookOpen(entry.book.indexNoteId)}
378
+ {#if entry.indexNote}
379
+ {@render noteRow(entry.indexNote, 1)}
380
+ {/if}
381
+ {#each entry.chapters as chapter (chapter.id)}
382
+ {@render noteRow(chapter, 1)}
383
+ {/each}
384
+ {/if}
385
+ </div>
386
+ {/snippet}
387
+
388
+ {#snippet noteRow(note: DocsNoteSummary, depth: number)}
389
+ <div class="docs-sidebar__group">
390
+ <button
391
+ type="button"
392
+ class="docs-sidebar__row"
393
+ style={`padding-inline-start: ${0.2 + depth * 0.85}rem`}
394
+ aria-current={activeNoteId === note.id ? "true" : undefined}
395
+ onclick={() => {
396
+ expandedNotes = { ...expandedNotes, [note.id]: true };
397
+ onSelectNote(note.id);
398
+ }}
399
+ >
400
+ <span
401
+ class="docs-sidebar__chevron"
402
+ data-open={noteOpen(note.id) ? "true" : "false"}
403
+ role="presentation"
404
+ onclick={(event) => {
405
+ event.stopPropagation();
406
+ expandedNotes = { ...expandedNotes, [note.id]: !noteOpen(note.id) };
407
+ }}
408
+ >
409
+ <ChevronRightIcon aria-hidden="true" />
410
+ </span>
411
+ <span class="docs-sidebar__label">{note.title}</span>
412
+ </button>
413
+ {#if noteOpen(note.id)}
414
+ {#each panels(note) as panel (panel.id)}
415
+ <button
416
+ type="button"
417
+ class="docs-sidebar__panel"
418
+ style={`padding-inline-start: ${1.3 + depth * 0.85}rem`}
419
+ aria-current={activeNoteId === note.id && isDocsSidebarPanelActive(activePanel, panel.id) ? "true" : undefined}
420
+ onclick={() => onSelectPanel(note.id, panel.id, panel.id === "review" ? "note" : undefined)}
421
+ >
422
+ <span class="docs-sidebar__label">{panel.label}</span>
423
+ {#if panel.count}
424
+ <span class="docs-sidebar__count">{panel.count}</span>
425
+ {/if}
426
+ </button>
427
+ {/each}
428
+ {/if}
429
+ </div>
430
+ {/snippet}
@@ -0,0 +1,18 @@
1
+ import { type DocsNotePanel, type DocsReviewScope, type DocsWorkspaceIndex } from "../../model";
2
+ import "./DocsSidebar.css";
3
+ type $$ComponentProps = {
4
+ workspace?: DocsWorkspaceIndex | null;
5
+ loading?: boolean;
6
+ error?: string | null;
7
+ activeNoteId?: string | null;
8
+ activePanel?: DocsNotePanel;
9
+ reviewScope?: DocsReviewScope;
10
+ onSelectNote?: (noteId: string) => void;
11
+ onSelectPanel?: (noteId: string, panel: DocsNotePanel, reviewScope?: DocsReviewScope) => void;
12
+ onCreateNote?: () => void;
13
+ onStartWorkspaceReview?: () => void;
14
+ onImportAnkiPackage?: (file: File) => void;
15
+ };
16
+ declare const DocsSidebar: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type DocsSidebar = ReturnType<typeof DocsSidebar>;
18
+ export default DocsSidebar;
@@ -0,0 +1,139 @@
1
+ <script lang="ts">
2
+ import type { App, TFile } from "@lapis-notes/api";
3
+ import type { DocsRepository } from "../../docs-repository";
4
+ import {
5
+ requestDocsDocumentSurface,
6
+ subscribeDocsDocumentSurface,
7
+ } from "../docs-document-surface";
8
+ import {
9
+ buildDocsWorkspaceIndex,
10
+ createDocsNote,
11
+ slugifyDocsId,
12
+ type DocsNote,
13
+ type DocsNotePanel,
14
+ type DocsReviewScope,
15
+ type DocsWorkspaceIndex,
16
+ } from "../../model";
17
+ import DocsSidebar from "./DocsSidebar.svelte";
18
+
19
+ let {
20
+ app,
21
+ repository,
22
+ docsRoot = "docs",
23
+ clock = () => new Date(),
24
+ onImportAnkiPackage = undefined,
25
+ }: {
26
+ app: App;
27
+ repository: DocsRepository;
28
+ docsRoot?: string;
29
+ clock?: () => Date;
30
+ onImportAnkiPackage?: (file: File) => void | Promise<void>;
31
+ } = $props();
32
+
33
+ let notes = $state<DocsNote[]>([]);
34
+ let loading = $state(true);
35
+ let error = $state<string | null>(null);
36
+ let activePath = $state<string | null>(null);
37
+ let activePanel = $state<DocsNotePanel>("display");
38
+ let reviewScope = $state<DocsReviewScope>("note");
39
+
40
+ const workspace = $derived<DocsWorkspaceIndex>(
41
+ buildDocsWorkspaceIndex(notes, clock()),
42
+ );
43
+ const activeNote = $derived(
44
+ notes.find((note) => note.sourcePath === activePath) ?? null,
45
+ );
46
+
47
+ async function refresh() {
48
+ loading = true;
49
+ try {
50
+ notes = await repository.listNotes(docsRoot);
51
+ error = null;
52
+ } catch (err) {
53
+ error = err instanceof Error ? err.message : String(err);
54
+ } finally {
55
+ loading = false;
56
+ }
57
+ }
58
+
59
+ $effect(() => {
60
+ void docsRoot;
61
+ void refresh();
62
+ });
63
+
64
+ $effect(() => {
65
+ const workspace = app.workspace;
66
+ activePath = workspace.getActiveFile()?.path ?? null;
67
+ const fileOpen = workspace.on("file-open", (file: TFile | null) => {
68
+ activePath = file?.path ?? null;
69
+ });
70
+ return () => workspace.offref(fileOpen);
71
+ });
72
+
73
+ $effect(() => {
74
+ const path = activePath;
75
+ if (!path) return;
76
+ return subscribeDocsDocumentSurface(path, (surface) => {
77
+ activePanel = surface.panel === "note" ? "display" : surface.panel;
78
+ if (surface.reviewScope) reviewScope = surface.reviewScope;
79
+ });
80
+ });
81
+
82
+ async function openNote(
83
+ noteId: string,
84
+ panel: DocsNotePanel = "note",
85
+ scope?: DocsReviewScope,
86
+ ) {
87
+ const note = notes.find((entry) => entry.id === noteId);
88
+ if (!note?.sourcePath) return;
89
+ if (scope) reviewScope = scope;
90
+ activePanel = panel === "note" ? "display" : panel;
91
+ requestDocsDocumentSurface(note.sourcePath, {
92
+ panel,
93
+ ...(scope ? { reviewScope: scope } : {}),
94
+ });
95
+ activePath = note.sourcePath;
96
+ const file = app.vault.getFileByPath(note.sourcePath);
97
+ if (file) await app.workspace.openLinkText(note.sourcePath, "", false);
98
+ }
99
+
100
+ async function importPackage(file: File) {
101
+ const { importAnkiPackageFile } = await import("../import-anki-package");
102
+ await importAnkiPackageFile({
103
+ app,
104
+ repository,
105
+ docsRoot,
106
+ file,
107
+ now: clock(),
108
+ });
109
+ await refresh();
110
+ }
111
+
112
+ async function createNote() {
113
+ const title = "Untitled note";
114
+ const id = slugifyDocsId(`${title}-${clock().getTime()}`);
115
+ const note = createDocsNote({ id, title }, clock());
116
+ note.sourcePath = `${docsRoot}/${id}.doc.md`;
117
+ await repository.writeNote(note);
118
+ await refresh();
119
+ await openNote(note.id, "note");
120
+ }
121
+ </script>
122
+
123
+ <DocsSidebar
124
+ {workspace}
125
+ {loading}
126
+ {error}
127
+ activeNoteId={activeNote?.id ?? null}
128
+ {activePanel}
129
+ {reviewScope}
130
+ onSelectNote={(noteId) => void openNote(noteId, "note")}
131
+ onSelectPanel={(noteId, panel, scope) => void openNote(noteId, panel, scope)}
132
+ onCreateNote={() => void createNote()}
133
+ onStartWorkspaceReview={() => {
134
+ const due = workspace.reviewQueue[0];
135
+ if (!due) return;
136
+ void openNote(due.noteId, "review", "workspace");
137
+ }}
138
+ onImportAnkiPackage={onImportAnkiPackage ?? importPackage}
139
+ />
@@ -0,0 +1,12 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ import type { DocsRepository } from "../../docs-repository";
3
+ type $$ComponentProps = {
4
+ app: App;
5
+ repository: DocsRepository;
6
+ docsRoot?: string;
7
+ clock?: () => Date;
8
+ onImportAnkiPackage?: (file: File) => void | Promise<void>;
9
+ };
10
+ declare const DocsSidebarHost: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type DocsSidebarHost = ReturnType<typeof DocsSidebarHost>;
12
+ export default DocsSidebarHost;