@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.
- package/CHANGELOG.md +11 -0
- package/README.md +188 -2
- package/dist/docs/DocsBlockMarkdownEditor.svelte +37 -0
- package/dist/docs/DocsBlockMarkdownEditor.svelte.d.ts +13 -0
- package/dist/docs/DocsBlockRefPicker.svelte +75 -0
- package/dist/docs/DocsBlockRefPicker.svelte.d.ts +11 -0
- package/dist/docs/DocsBookEditor.svelte +141 -0
- package/dist/docs/DocsBookEditor.svelte.d.ts +10 -0
- package/dist/docs/DocsBookNavigation.svelte +838 -0
- package/dist/docs/DocsBookNavigation.svelte.d.ts +11 -0
- package/dist/docs/DocsBooksColumn.svelte +82 -0
- package/dist/docs/DocsBooksColumn.svelte.d.ts +10 -0
- package/dist/docs/DocsCardBackMatter.svelte +32 -0
- package/dist/docs/DocsCardBackMatter.svelte.d.ts +10 -0
- package/dist/docs/DocsCardBlockExtra.svelte +43 -0
- package/dist/docs/DocsCardBlockExtra.svelte.d.ts +9 -0
- package/dist/docs/DocsCardPreviewFlip.svelte +143 -0
- package/dist/docs/DocsCardPreviewFlip.svelte.d.ts +17 -0
- package/dist/docs/DocsCardPreviewStack.svelte +139 -0
- package/dist/docs/DocsCardPreviewStack.svelte.d.ts +13 -0
- package/dist/docs/DocsCardPrompt.svelte +33 -0
- package/dist/docs/DocsCardPrompt.svelte.d.ts +8 -0
- package/dist/docs/DocsCardReviewAnswer.svelte +45 -0
- package/dist/docs/DocsCardReviewAnswer.svelte.d.ts +8 -0
- package/dist/docs/DocsCardTagChips.svelte +19 -0
- package/dist/docs/DocsCardTagChips.svelte.d.ts +7 -0
- package/dist/docs/DocsCardsPanel.svelte +232 -0
- package/dist/docs/DocsCardsPanel.svelte.d.ts +17 -0
- package/dist/docs/DocsCardsReview.css +1050 -0
- package/dist/docs/DocsChapterFooter.svelte +248 -0
- package/dist/docs/DocsChapterFooter.svelte.d.ts +11 -0
- package/dist/docs/DocsChapterScrollRing.svelte +101 -0
- package/dist/docs/DocsChapterScrollRing.svelte.d.ts +6 -0
- package/dist/docs/DocsDisplayPanel.css +627 -0
- package/dist/docs/DocsDisplayPanel.svelte +78 -0
- package/dist/docs/DocsDisplayPanel.svelte.d.ts +16 -0
- package/dist/docs/DocsEditorPanel.css +325 -0
- package/dist/docs/DocsEditorPanel.svelte +570 -0
- package/dist/docs/DocsEditorPanel.svelte.d.ts +22 -0
- package/dist/docs/DocsFlipCard.svelte +68 -0
- package/dist/docs/DocsFlipCard.svelte.d.ts +8 -0
- package/dist/docs/DocsFolderDirectoryList.svelte +206 -0
- package/dist/docs/DocsFolderDirectoryList.svelte.d.ts +14 -0
- package/dist/docs/DocsFolderPanel.css +357 -0
- package/dist/docs/DocsFolderPanel.svelte +102 -0
- package/dist/docs/DocsFolderPanel.svelte.d.ts +17 -0
- package/dist/docs/DocsLinksPanel.css +61 -0
- package/dist/docs/DocsLinksPanel.svelte +55 -0
- package/dist/docs/DocsLinksPanel.svelte.d.ts +11 -0
- package/dist/docs/DocsMarkdownField.svelte +93 -0
- package/dist/docs/DocsMarkdownField.svelte.d.ts +11 -0
- package/dist/docs/DocsMarkdownPreview.svelte +278 -0
- package/dist/docs/DocsMarkdownPreview.svelte.d.ts +17 -0
- package/dist/docs/DocsReviewPanel.svelte +63 -0
- package/dist/docs/DocsReviewPanel.svelte.d.ts +11 -0
- package/dist/docs/DocsRichBlock.svelte +291 -0
- package/dist/docs/DocsRichBlock.svelte.d.ts +13 -0
- package/dist/docs/DocsRichDisplay.svelte +316 -0
- package/dist/docs/DocsRichDisplay.svelte.d.ts +14 -0
- package/dist/docs/DocsSlidesPreview.css +243 -0
- package/dist/docs/DocsSlidesPreview.svelte +206 -0
- package/dist/docs/DocsSlidesPreview.svelte.d.ts +10 -0
- package/dist/docs/DocsWorkspace.css +570 -0
- package/dist/docs/DocsWorkspace.svelte +831 -0
- package/dist/docs/DocsWorkspace.svelte.d.ts +48 -0
- package/dist/docs/DocsWorkspaceToolbar.svelte +211 -0
- package/dist/docs/DocsWorkspaceToolbar.svelte.d.ts +27 -0
- package/dist/docs/candidates/ChoiceMenu.svelte +114 -0
- package/dist/docs/candidates/ChoiceMenu.svelte.d.ts +10 -0
- package/dist/docs/candidates/FlipCardDeck.svelte +817 -0
- package/dist/docs/candidates/FlipCardDeck.svelte.d.ts +37 -0
- package/dist/docs/candidates/MarkdownFloatingToc.svelte +313 -0
- package/dist/docs/candidates/MarkdownFloatingToc.svelte.d.ts +14 -0
- package/dist/docs/candidates/SearchableChoicePicker.css +161 -0
- package/dist/docs/candidates/SearchableChoicePicker.svelte +208 -0
- package/dist/docs/candidates/SearchableChoicePicker.svelte.d.ts +19 -0
- package/dist/docs/candidates/StructuredTable.svelte +98 -0
- package/dist/docs/candidates/StructuredTable.svelte.d.ts +8 -0
- package/dist/docs/candidates/StructuredTableEditor.svelte +374 -0
- package/dist/docs/candidates/StructuredTableEditor.svelte.d.ts +12 -0
- package/dist/docs/candidates/TagEditor.svelte +31 -0
- package/dist/docs/candidates/TagEditor.svelte.d.ts +14 -0
- package/dist/docs/candidates/WorkspaceHistoryControls.svelte +112 -0
- package/dist/docs/candidates/WorkspaceHistoryControls.svelte.d.ts +8 -0
- package/dist/docs/candidates/markdown-floating-toc.css +151 -0
- package/dist/docs/candidates/markdown-floating-toc.d.ts +1 -0
- package/dist/docs/candidates/markdown-floating-toc.js +1 -0
- package/dist/docs/docs-anki-import.d.ts +34 -0
- package/dist/docs/docs-anki-import.js +516 -0
- package/dist/docs/docs-book-view-model.d.ts +9 -0
- package/dist/docs/docs-book-view-model.js +71 -0
- package/dist/docs/docs-card-answer-markdown.d.ts +1 -0
- package/dist/docs/docs-card-answer-markdown.js +63 -0
- package/dist/docs/docs-card-display.d.ts +10 -0
- package/dist/docs/docs-card-display.js +18 -0
- package/dist/docs/docs-display-focus.d.ts +7 -0
- package/dist/docs/docs-display-focus.js +42 -0
- package/dist/docs/docs-document-surface.d.ts +7 -0
- package/dist/docs/docs-document-surface.js +25 -0
- package/dist/docs/docs-folder-directory.d.ts +21 -0
- package/dist/docs/docs-folder-directory.js +56 -0
- package/dist/docs/docs-link-mode.d.ts +16 -0
- package/dist/docs/docs-link-mode.js +94 -0
- package/dist/docs/docs-workspace-helpers.d.ts +25 -0
- package/dist/docs/docs-workspace-helpers.js +195 -0
- package/dist/docs/docs.tokens.css +13 -0
- package/dist/docs/import-anki-package.d.ts +10 -0
- package/dist/docs/import-anki-package.js +30 -0
- package/dist/docs/lapis-markdown-extensions.d.ts +9 -0
- package/dist/docs/lapis-markdown-extensions.js +128 -0
- package/dist/docs/pin-reading-chrome.d.ts +4 -0
- package/dist/docs/pin-reading-chrome.js +32 -0
- package/dist/docs/sidebar/DocsFoldersSidebar.svelte +134 -0
- package/dist/docs/sidebar/DocsFoldersSidebar.svelte.d.ts +15 -0
- package/dist/docs/sidebar/DocsLinksSidebar.svelte +105 -0
- package/dist/docs/sidebar/DocsLinksSidebar.svelte.d.ts +11 -0
- package/dist/docs/sidebar/DocsSidebar.css +346 -0
- package/dist/docs/sidebar/DocsSidebar.svelte +430 -0
- package/dist/docs/sidebar/DocsSidebar.svelte.d.ts +18 -0
- package/dist/docs/sidebar/DocsSidebarHost.svelte +139 -0
- package/dist/docs/sidebar/DocsSidebarHost.svelte.d.ts +12 -0
- package/dist/docs/sidebar/DocsTagsSidebar.svelte +176 -0
- package/dist/docs/sidebar/DocsTagsSidebar.svelte.d.ts +12 -0
- package/dist/docs/sidebar/docs-book-note-tree.d.ts +23 -0
- package/dist/docs/sidebar/docs-book-note-tree.js +88 -0
- package/dist/docs/sidebar/docs-note-sort.d.ts +26 -0
- package/dist/docs/sidebar/docs-note-sort.js +54 -0
- package/dist/docs/sidebar/docs-sidebar-filters.d.ts +17 -0
- package/dist/docs/sidebar/docs-sidebar-filters.js +74 -0
- package/dist/docs/sidebar/docs-sidebar-ui-storage.d.ts +24 -0
- package/dist/docs/sidebar/docs-sidebar-ui-storage.js +87 -0
- package/dist/docs/sidebar/docs-source-folder-tree.d.ts +16 -0
- package/dist/docs/sidebar/docs-source-folder-tree.js +143 -0
- package/dist/docs/sidebar/docs-tag-tree.d.ts +17 -0
- package/dist/docs/sidebar/docs-tag-tree.js +84 -0
- package/dist/docs-cards-view.d.ts +18 -0
- package/dist/docs-cards-view.js +52 -0
- package/dist/docs-document-view.d.ts +24 -0
- package/dist/docs-document-view.js +127 -0
- package/dist/docs-document-workspace.svelte +215 -0
- package/dist/docs-document-workspace.svelte.d.ts +16 -0
- package/dist/docs-index.d.ts +5 -0
- package/dist/docs-index.js +49 -0
- package/dist/docs-path.d.ts +6 -0
- package/dist/docs-path.js +10 -0
- package/dist/docs-plugin.d.ts +21 -0
- package/dist/docs-plugin.js +140 -0
- package/dist/docs-repository.d.ts +15 -0
- package/dist/docs-repository.js +75 -0
- package/dist/docs-review-view.d.ts +18 -0
- package/dist/docs-review-view.js +52 -0
- package/dist/docs-settings-tab.d.ts +8 -0
- package/dist/docs-settings-tab.js +25 -0
- package/dist/docs-settings.d.ts +17 -0
- package/dist/docs-settings.js +38 -0
- package/dist/docs-view.d.ts +22 -0
- package/dist/docs-view.js +51 -0
- package/dist/docs-workspace.svelte +394 -0
- package/dist/docs-workspace.svelte.d.ts +15 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/model/book.d.ts +6 -0
- package/dist/model/book.js +101 -0
- package/dist/model/coding-runtime.d.ts +4 -0
- package/dist/model/coding-runtime.js +9 -0
- package/dist/model/docs-card-context.d.ts +32 -0
- package/dist/model/docs-card-context.js +89 -0
- package/dist/model/docs-card-preview-stack-motion.d.ts +51 -0
- package/dist/model/docs-card-preview-stack-motion.js +94 -0
- package/dist/model/docs-media-context.d.ts +9 -0
- package/dist/model/docs-media-context.js +18 -0
- package/dist/model/docs-note-list.d.ts +8 -0
- package/dist/model/docs-note-list.js +25 -0
- package/dist/model/docs-relative-media.d.ts +30 -0
- package/dist/model/docs-relative-media.js +116 -0
- package/dist/model/docs-review-constants.d.ts +4 -0
- package/dist/model/docs-review-constants.js +13 -0
- package/dist/model/fsrs.d.ts +14 -0
- package/dist/model/fsrs.js +153 -0
- package/dist/model/graph.d.ts +12 -0
- package/dist/model/graph.js +169 -0
- package/dist/model/index.d.ts +14 -0
- package/dist/model/index.js +14 -0
- package/dist/model/markdown-normalize.d.ts +11 -0
- package/dist/model/markdown-normalize.js +755 -0
- package/dist/model/markdown.d.ts +167 -0
- package/dist/model/markdown.js +365 -0
- package/dist/model/sample.d.ts +7 -0
- package/dist/model/sample.js +524 -0
- package/dist/model/types.d.ts +328 -0
- package/dist/model/types.js +1 -0
- package/dist/model/workspace.d.ts +6 -0
- package/dist/model/workspace.js +81 -0
- package/dist/storybook.css +52 -0
- package/dist/styles.css +1 -0
- package/manifest.json +11 -0
- 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;
|