@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,176 @@
1
+ <script lang="ts">
2
+ import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
3
+ import FolderTreeIcon from "@lucide/svelte/icons/folder-tree";
4
+ import HashIcon from "@lucide/svelte/icons/hash";
5
+ import ListIcon from "@lucide/svelte/icons/list";
6
+ import { Button } from "@lapismd/design-core/shadcn/button";
7
+ import type { DocsTagSummary } from "../../model";
8
+ import type { DocsTagsViewMode } from "./docs-sidebar-ui-storage";
9
+ import {
10
+ createDocsTagTree,
11
+ docsTagCount,
12
+ filterDocsTagsByPathMatch,
13
+ normalizeDocsTag,
14
+ sortDocsTagSummaries,
15
+ type DocsTagSortMode,
16
+ type DocsTagTreeNode,
17
+ } from "./docs-tag-tree";
18
+
19
+ let {
20
+ tags = [],
21
+ selectedTags = [],
22
+ viewMode = $bindable<DocsTagsViewMode>("tree"),
23
+ openNodes = $bindable<Record<string, boolean>>({}),
24
+ onSelectTag = () => {},
25
+ }: {
26
+ tags?: DocsTagSummary[];
27
+ selectedTags?: string[];
28
+ viewMode?: DocsTagsViewMode;
29
+ openNodes?: Record<string, boolean>;
30
+ onSelectTag?: (tag: string) => void;
31
+ } = $props();
32
+
33
+ let query = $state("");
34
+ let sortMode = $state<DocsTagSortMode>("count");
35
+
36
+ const filteredTags = $derived(sortDocsTagSummaries(filterDocsTagsByPathMatch(tags, query), sortMode));
37
+ const tree = $derived(createDocsTagTree(filteredTags, sortMode));
38
+ const selectedTagSet = $derived(
39
+ new Set(selectedTags.map((tag) => normalizeDocsTag(tag).toLowerCase()).filter(Boolean)),
40
+ );
41
+ const hierarchyLabel = $derived(
42
+ viewMode === "tree"
43
+ ? "Showing tag hierarchy. Switch to flat tags."
44
+ : "Showing flat tags. Switch to tag hierarchy.",
45
+ );
46
+
47
+ function isOpen(tag: string) {
48
+ return openNodes[tag] !== false;
49
+ }
50
+
51
+ function toggle(tag: string) {
52
+ openNodes = { ...openNodes, [tag]: !isOpen(tag) };
53
+ }
54
+
55
+ function selectTag(tag: string) {
56
+ const normalized = normalizeDocsTag(tag);
57
+ if (normalized) onSelectTag(normalized);
58
+ }
59
+
60
+ function isSelected(tag: string) {
61
+ return selectedTagSet.has(normalizeDocsTag(tag).toLowerCase());
62
+ }
63
+ </script>
64
+
65
+ <div class="docs-tags">
66
+ <div class="docs-tags__tools">
67
+ <input class="docs-sidebar__search" placeholder="Search tags" bind:value={query} />
68
+ <Button
69
+ type="button"
70
+ variant="ghost"
71
+ size="icon-sm"
72
+ class="docs-tags__mode"
73
+ aria-pressed={viewMode === "tree"}
74
+ aria-label={hierarchyLabel}
75
+ title={hierarchyLabel}
76
+ onclick={() => (viewMode = viewMode === "tree" ? "flat" : "tree")}
77
+ >
78
+ {#if viewMode === "tree"}
79
+ <FolderTreeIcon aria-hidden="true" />
80
+ {:else}
81
+ <ListIcon aria-hidden="true" />
82
+ {/if}
83
+ </Button>
84
+ </div>
85
+
86
+ <div class="docs-sidebar__segment" role="group" aria-label="Sort tags">
87
+ <Button
88
+ type="button"
89
+ variant="ghost"
90
+ size="sm"
91
+ class="docs-sidebar__view"
92
+ aria-pressed={sortMode === "count"}
93
+ onclick={() => (sortMode = "count")}
94
+ >
95
+ <span class="docs-sidebar__view-label">Frequency</span>
96
+ </Button>
97
+ <Button
98
+ type="button"
99
+ variant="ghost"
100
+ size="sm"
101
+ class="docs-sidebar__view"
102
+ aria-pressed={sortMode === "name"}
103
+ onclick={() => (sortMode = "name")}
104
+ >
105
+ <span class="docs-sidebar__view-label">Name</span>
106
+ </Button>
107
+ </div>
108
+
109
+ {#if viewMode === "tree"}
110
+ <div class="docs-sidebar__group">
111
+ {#each tree as node (node.tag)}
112
+ {@render tagNode(node, 0)}
113
+ {:else}
114
+ <p class="docs-sidebar__empty">No tags match the current filter.</p>
115
+ {/each}
116
+ </div>
117
+ {:else}
118
+ <div class="docs-sidebar__group">
119
+ {#each filteredTags as tag (tag.tag)}
120
+ <button
121
+ type="button"
122
+ class="docs-sidebar__row"
123
+ aria-pressed={isSelected(tag.tag)}
124
+ title={`#${tag.tag}`}
125
+ onclick={() => selectTag(tag.tag)}
126
+ >
127
+ <HashIcon class="docs-sidebar__hash" aria-hidden="true" />
128
+ <span class="docs-sidebar__label">{tag.tag}</span>
129
+ <span class="docs-sidebar__count">{docsTagCount(tag)}</span>
130
+ </button>
131
+ {:else}
132
+ <p class="docs-sidebar__empty">No tags match the current filter.</p>
133
+ {/each}
134
+ </div>
135
+ {/if}
136
+ </div>
137
+
138
+ {#snippet tagNode(node: DocsTagTreeNode, depth: number)}
139
+ {@const expanded = node.children.length > 0 && isOpen(node.tag)}
140
+ <div class="docs-sidebar__group">
141
+ <div class="docs-tags__row" style={`padding-inline-start: ${depth * 0.85}rem`}>
142
+ {#if node.children.length}
143
+ <button
144
+ type="button"
145
+ class="docs-sidebar__chevron"
146
+ aria-label={expanded ? `Collapse ${node.name}` : `Expand ${node.name}`}
147
+ aria-expanded={expanded}
148
+ data-open={expanded ? "true" : "false"}
149
+ onclick={() => toggle(node.tag)}
150
+ >
151
+ <ChevronRightIcon aria-hidden="true" />
152
+ </button>
153
+ {:else}
154
+ <span class="docs-tags__spacer" aria-hidden="true"></span>
155
+ {/if}
156
+ <button
157
+ type="button"
158
+ class="docs-sidebar__row"
159
+ aria-pressed={isSelected(node.tag)}
160
+ title={`#${node.tag}`}
161
+ onclick={() => selectTag(node.tag)}
162
+ >
163
+ <HashIcon class="docs-sidebar__hash" aria-hidden="true" />
164
+ <span class="docs-sidebar__label">{node.name}</span>
165
+ <span class="docs-sidebar__count">{node.count}</span>
166
+ </button>
167
+ </div>
168
+ {#if expanded}
169
+ <div class="docs-sidebar__group">
170
+ {#each node.children as child (child.tag)}
171
+ {@render tagNode(child, depth + 1)}
172
+ {/each}
173
+ </div>
174
+ {/if}
175
+ </div>
176
+ {/snippet}
@@ -0,0 +1,12 @@
1
+ import type { DocsTagSummary } from "../../model";
2
+ import type { DocsTagsViewMode } from "./docs-sidebar-ui-storage";
3
+ type $$ComponentProps = {
4
+ tags?: DocsTagSummary[];
5
+ selectedTags?: string[];
6
+ viewMode?: DocsTagsViewMode;
7
+ openNodes?: Record<string, boolean>;
8
+ onSelectTag?: (tag: string) => void;
9
+ };
10
+ declare const DocsTagsSidebar: import("svelte").Component<$$ComponentProps, {}, "openNodes" | "viewMode">;
11
+ type DocsTagsSidebar = ReturnType<typeof DocsTagsSidebar>;
12
+ export default DocsTagsSidebar;
@@ -0,0 +1,23 @@
1
+ import type { DocsBookNavigation, DocsNoteSummary } from '../../model';
2
+ import { type DocsNoteSortMode } from './docs-note-sort';
3
+ export type DocsBookNoteTreeStandalone = {
4
+ kind: 'standalone';
5
+ note: DocsNoteSummary;
6
+ };
7
+ export type DocsBookNoteTreeBook = {
8
+ kind: 'book';
9
+ book: DocsBookNavigation;
10
+ /** Present when the index note is in the filtered set; otherwise null. */
11
+ indexNote: DocsNoteSummary | null;
12
+ chapters: DocsNoteSummary[];
13
+ };
14
+ export type DocsBookNoteTreeEntry = DocsBookNoteTreeStandalone | DocsBookNoteTreeBook;
15
+ /**
16
+ * Nest filtered notes under books using workspace.books TOC membership.
17
+ * Chapter notes listed in any book are omitted from the top level.
18
+ *
19
+ * Without `strict`, a visible book keeps its full chapter TOC, including
20
+ * unresolved entries. With `strict`, only notes that passed the sidebar
21
+ * filter are listed.
22
+ */
23
+ export declare function createDocsBookNoteTree(filteredNotes: DocsNoteSummary[], books: DocsBookNavigation[], sortMode?: DocsNoteSortMode, strict?: boolean): DocsBookNoteTreeEntry[];
@@ -0,0 +1,88 @@
1
+ import { DEFAULT_DOCS_NOTE_SORT, compareDocsNotes } from './docs-note-sort';
2
+ function compareText(left, right) {
3
+ return left.localeCompare(right, undefined, { sensitivity: 'base' });
4
+ }
5
+ function entrySortNote(entry) {
6
+ if (entry.kind === 'standalone')
7
+ return entry.note;
8
+ return entry.indexNote;
9
+ }
10
+ function entrySortTitle(entry) {
11
+ if (entry.kind === 'standalone')
12
+ return entry.note.title;
13
+ return entry.indexNote?.title ?? entry.book.title;
14
+ }
15
+ function entrySortId(entry) {
16
+ if (entry.kind === 'standalone')
17
+ return entry.note.id;
18
+ return entry.book.indexNoteId;
19
+ }
20
+ function compareEntries(left, right, mode) {
21
+ const leftNote = entrySortNote(left);
22
+ const rightNote = entrySortNote(right);
23
+ if (leftNote && rightNote)
24
+ return compareDocsNotes(leftNote, rightNote, mode);
25
+ return compareText(entrySortTitle(left), entrySortTitle(right)) || compareText(entrySortId(left), entrySortId(right));
26
+ }
27
+ function summaryFromBookChapter(chapter) {
28
+ return {
29
+ id: chapter.noteId,
30
+ title: chapter.title || chapter.noteId,
31
+ docType: 'freeform',
32
+ summary: chapter.summary ?? '',
33
+ aliases: [],
34
+ tags: [],
35
+ sourcePath: chapter.sourcePath || `docs/${chapter.noteId}.doc.md`,
36
+ createdAt: chapter.completedAt ?? '',
37
+ updatedAt: chapter.completedAt ?? '',
38
+ cardCount: 0,
39
+ blockCount: 0,
40
+ ...(chapter.completedAt ? { completedAt: chapter.completedAt } : {})
41
+ };
42
+ }
43
+ /**
44
+ * Nest filtered notes under books using workspace.books TOC membership.
45
+ * Chapter notes listed in any book are omitted from the top level.
46
+ *
47
+ * Without `strict`, a visible book keeps its full chapter TOC, including
48
+ * unresolved entries. With `strict`, only notes that passed the sidebar
49
+ * filter are listed.
50
+ */
51
+ export function createDocsBookNoteTree(filteredNotes, books, sortMode = DEFAULT_DOCS_NOTE_SORT, strict = false) {
52
+ const filteredById = new Map(filteredNotes.map((note) => [note.id, note]));
53
+ const chapterNoteIds = new Set();
54
+ const indexNoteIds = new Set();
55
+ for (const book of books) {
56
+ indexNoteIds.add(book.indexNoteId);
57
+ for (const chapter of book.chapters) {
58
+ chapterNoteIds.add(chapter.noteId);
59
+ }
60
+ }
61
+ const entries = [];
62
+ for (const book of books) {
63
+ const indexNote = filteredById.get(book.indexNoteId) ?? null;
64
+ const chapters = strict
65
+ ? book.chapters.flatMap((chapter) => {
66
+ const note = filteredById.get(chapter.noteId);
67
+ return note ? [note] : [];
68
+ })
69
+ : book.chapters.map((chapter) => filteredById.get(chapter.noteId) ?? summaryFromBookChapter(chapter));
70
+ if (!indexNote && chapters.length === 0)
71
+ continue;
72
+ if (!strict && !indexNote && !book.chapters.some((chapter) => filteredById.has(chapter.noteId))) {
73
+ continue;
74
+ }
75
+ entries.push({
76
+ kind: 'book',
77
+ book,
78
+ indexNote,
79
+ chapters
80
+ });
81
+ }
82
+ for (const note of filteredNotes) {
83
+ if (indexNoteIds.has(note.id) || chapterNoteIds.has(note.id))
84
+ continue;
85
+ entries.push({ kind: 'standalone', note });
86
+ }
87
+ return entries.sort((left, right) => compareEntries(left, right, sortMode));
88
+ }
@@ -0,0 +1,26 @@
1
+ import type { DocsNoteSummary } from '../../model';
2
+ export type DocsNoteSortMode = 'title-asc' | 'title-desc' | 'updated-desc' | 'updated-asc' | 'created-desc' | 'created-asc';
3
+ export declare const DOCS_NOTE_SORT_OPTIONS: readonly [{
4
+ readonly value: "title-asc";
5
+ readonly label: "File Name (A to Z)";
6
+ }, {
7
+ readonly value: "title-desc";
8
+ readonly label: "File Name (Z to A)";
9
+ }, {
10
+ readonly value: "updated-desc";
11
+ readonly label: "Modified time (new to old)";
12
+ }, {
13
+ readonly value: "updated-asc";
14
+ readonly label: "Modified time (old to new)";
15
+ }, {
16
+ readonly value: "created-desc";
17
+ readonly label: "Created time (new to old)";
18
+ }, {
19
+ readonly value: "created-asc";
20
+ readonly label: "Created time (old to new)";
21
+ }];
22
+ export declare const DEFAULT_DOCS_NOTE_SORT: DocsNoteSortMode;
23
+ export declare function isDocsNoteSortMode(value: unknown): value is DocsNoteSortMode;
24
+ export declare function docsNoteSortLabel(mode: DocsNoteSortMode): "File Name (A to Z)" | "File Name (Z to A)" | "Modified time (new to old)" | "Modified time (old to new)" | "Created time (new to old)" | "Created time (old to new)";
25
+ export declare function compareDocsNotes(left: DocsNoteSummary, right: DocsNoteSummary, mode: DocsNoteSortMode): number;
26
+ export declare function sortDocsNotes(notes: DocsNoteSummary[], mode?: DocsNoteSortMode): DocsNoteSummary[];
@@ -0,0 +1,54 @@
1
+ export const DOCS_NOTE_SORT_OPTIONS = [
2
+ { value: 'title-asc', label: 'File Name (A to Z)' },
3
+ { value: 'title-desc', label: 'File Name (Z to A)' },
4
+ { value: 'updated-desc', label: 'Modified time (new to old)' },
5
+ { value: 'updated-asc', label: 'Modified time (old to new)' },
6
+ { value: 'created-desc', label: 'Created time (new to old)' },
7
+ { value: 'created-asc', label: 'Created time (old to new)' }
8
+ ];
9
+ export const DEFAULT_DOCS_NOTE_SORT = 'title-asc';
10
+ export function isDocsNoteSortMode(value) {
11
+ return DOCS_NOTE_SORT_OPTIONS.some((option) => option.value === value);
12
+ }
13
+ export function docsNoteSortLabel(mode) {
14
+ return (DOCS_NOTE_SORT_OPTIONS.find((option) => option.value === mode)?.label ??
15
+ DOCS_NOTE_SORT_OPTIONS[0].label);
16
+ }
17
+ function compareText(left, right) {
18
+ return left.localeCompare(right, undefined, { sensitivity: 'base' });
19
+ }
20
+ function compareTime(left, right) {
21
+ const leftTime = Date.parse(left);
22
+ const rightTime = Date.parse(right);
23
+ const safeLeft = Number.isFinite(leftTime) ? leftTime : 0;
24
+ const safeRight = Number.isFinite(rightTime) ? rightTime : 0;
25
+ return safeLeft - safeRight;
26
+ }
27
+ export function compareDocsNotes(left, right, mode) {
28
+ switch (mode) {
29
+ case 'title-desc':
30
+ return compareText(right.title, left.title) || compareText(left.id, right.id);
31
+ case 'updated-desc':
32
+ return (compareTime(right.updatedAt, left.updatedAt) ||
33
+ compareText(left.title, right.title) ||
34
+ compareText(left.id, right.id));
35
+ case 'updated-asc':
36
+ return (compareTime(left.updatedAt, right.updatedAt) ||
37
+ compareText(left.title, right.title) ||
38
+ compareText(left.id, right.id));
39
+ case 'created-desc':
40
+ return (compareTime(right.createdAt, left.createdAt) ||
41
+ compareText(left.title, right.title) ||
42
+ compareText(left.id, right.id));
43
+ case 'created-asc':
44
+ return (compareTime(left.createdAt, right.createdAt) ||
45
+ compareText(left.title, right.title) ||
46
+ compareText(left.id, right.id));
47
+ case 'title-asc':
48
+ default:
49
+ return compareText(left.title, right.title) || compareText(left.id, right.id);
50
+ }
51
+ }
52
+ export function sortDocsNotes(notes, mode = DEFAULT_DOCS_NOTE_SORT) {
53
+ return [...notes].sort((left, right) => compareDocsNotes(left, right, mode));
54
+ }
@@ -0,0 +1,17 @@
1
+ import { type DocsBacklink, type DocsNoteSummary, type DocsTagSummary, type DocsWikiLink } from "../../model";
2
+ export type DocsSidebarNoteFilters = {
3
+ query?: string;
4
+ dueOnly?: boolean;
5
+ reviewAll?: boolean;
6
+ selectedTags?: readonly string[];
7
+ dueNoteIds?: ReadonlySet<string>;
8
+ };
9
+ /** A note has a review when it is in the due review queue. */
10
+ export declare function docsNoteHasReview(note: Pick<DocsNoteSummary, "id" | "dueCardCount">, dueNoteIds: ReadonlySet<string>): boolean;
11
+ export declare function docsSidebarFiltersActive(filters: DocsSidebarNoteFilters): boolean;
12
+ export declare function filterDocsSidebarNotes(notes: readonly DocsNoteSummary[], filters: DocsSidebarNoteFilters): DocsNoteSummary[];
13
+ /** Tags that appear on the supplied notes, including hierarchy prefixes. */
14
+ export declare function docsTagSummariesForNotes(notes: readonly DocsNoteSummary[]): DocsTagSummary[];
15
+ export declare function filterDocsSidebarBacklinks(links: readonly DocsBacklink[], visibleNoteIds: ReadonlySet<string>, filtering: boolean): DocsBacklink[];
16
+ export declare function filterDocsSidebarOutgoingLinks(links: readonly DocsWikiLink[], visibleNoteIds: ReadonlySet<string>, filtering: boolean): DocsWikiLink[];
17
+ export declare function filterDocsSidebarAmbiguousLinks(links: readonly DocsWikiLink[], visibleNoteIds: ReadonlySet<string>, filtering: boolean): DocsWikiLink[];
@@ -0,0 +1,74 @@
1
+ import { docsNoteMatchesTagFolder, docsNoteTagValues, normalizeDocsTag, } from "../../model";
2
+ /** A note has a review when it is in the due review queue. */
3
+ export function docsNoteHasReview(note, dueNoteIds) {
4
+ return dueNoteIds.has(note.id) || (note.dueCardCount ?? 0) > 0;
5
+ }
6
+ export function docsSidebarFiltersActive(filters) {
7
+ return Boolean(filters.query?.trim() ||
8
+ filters.dueOnly ||
9
+ filters.reviewAll ||
10
+ filters.selectedTags?.length);
11
+ }
12
+ export function filterDocsSidebarNotes(notes, filters) {
13
+ const dueNoteIds = filters.dueNoteIds ?? new Set();
14
+ const selectedTags = filters.selectedTags ?? [];
15
+ const needle = filters.query?.trim().toLowerCase() ?? "";
16
+ return notes.filter((note) => {
17
+ if (filters.dueOnly && !docsNoteHasReview(note, dueNoteIds))
18
+ return false;
19
+ if (filters.reviewAll && !docsNoteHasReview(note, dueNoteIds))
20
+ return false;
21
+ if (selectedTags.length &&
22
+ !selectedTags.every((tag) => docsNoteMatchesTagFolder(note, tag))) {
23
+ return false;
24
+ }
25
+ if (!needle)
26
+ return true;
27
+ return [note.title, note.id, note.summary, ...note.tags, ...note.aliases]
28
+ .join(" ")
29
+ .toLowerCase()
30
+ .includes(needle);
31
+ });
32
+ }
33
+ /** Tags that appear on the supplied notes, including hierarchy prefixes. */
34
+ export function docsTagSummariesForNotes(notes) {
35
+ const byTag = new Map();
36
+ for (const note of notes) {
37
+ for (const tag of docsNoteTagValues(note)) {
38
+ for (const prefix of tagPrefixes(tag)) {
39
+ const ids = byTag.get(prefix) ?? new Set();
40
+ ids.add(note.id);
41
+ byTag.set(prefix, ids);
42
+ }
43
+ }
44
+ }
45
+ return [...byTag.entries()]
46
+ .map(([tag, ids]) => ({
47
+ tag,
48
+ noteCount: ids.size,
49
+ cardCount: 0,
50
+ }))
51
+ .sort((left, right) => left.tag.localeCompare(right.tag));
52
+ }
53
+ export function filterDocsSidebarBacklinks(links, visibleNoteIds, filtering) {
54
+ if (!filtering)
55
+ return [...links];
56
+ return links.filter((link) => visibleNoteIds.has(link.sourceNoteId));
57
+ }
58
+ export function filterDocsSidebarOutgoingLinks(links, visibleNoteIds, filtering) {
59
+ if (!filtering)
60
+ return [...links];
61
+ return links.filter((link) => Boolean(link.resolvedNoteId) && visibleNoteIds.has(link.resolvedNoteId ?? ""));
62
+ }
63
+ export function filterDocsSidebarAmbiguousLinks(links, visibleNoteIds, filtering) {
64
+ if (!filtering)
65
+ return [...links];
66
+ return links.filter((link) => visibleNoteIds.has(link.sourceNoteId));
67
+ }
68
+ function tagPrefixes(tag) {
69
+ const parts = normalizeDocsTag(tag)
70
+ .split("/")
71
+ .map((part) => part.trim())
72
+ .filter(Boolean);
73
+ return parts.map((_, index) => parts.slice(0, index + 1).join("/"));
74
+ }
@@ -0,0 +1,24 @@
1
+ import { type DocsNoteSortMode } from './docs-note-sort';
2
+ export declare const DOCS_SIDEBAR_UI_STORAGE_KEY = "lapis:docs:sidebar-ui:v1";
3
+ export type DocsSidebarView = 'docs' | 'folders' | 'links' | 'tags';
4
+ export type DocsTagsViewMode = 'tree' | 'flat';
5
+ export type DocsFolderDisplayMode = 'panel' | 'inline';
6
+ export type { DocsNoteSortMode };
7
+ export type DocsSidebarUiState = {
8
+ sidebarView: DocsSidebarView;
9
+ folderMode: boolean;
10
+ folderFollowActive: boolean;
11
+ folderDisplayMode: DocsFolderDisplayMode;
12
+ sourceFolderFollowActive: boolean;
13
+ sourceFolderOpenNodes: Record<string, boolean>;
14
+ tagsViewMode: DocsTagsViewMode;
15
+ folderOpenNodes: Record<string, boolean>;
16
+ tagsOpenNodes: Record<string, boolean>;
17
+ bookOpenNodes: Record<string, boolean>;
18
+ notesFollowActive: boolean;
19
+ notesFiltersOpen: boolean;
20
+ noteSortMode: DocsNoteSortMode;
21
+ };
22
+ export declare const DEFAULT_DOCS_SIDEBAR_UI_STATE: DocsSidebarUiState;
23
+ export declare function readDocsSidebarUiState(): DocsSidebarUiState;
24
+ export declare function writeDocsSidebarUiState(state: DocsSidebarUiState): void;
@@ -0,0 +1,87 @@
1
+ import { DEFAULT_DOCS_NOTE_SORT, isDocsNoteSortMode } from './docs-note-sort';
2
+ export const DOCS_SIDEBAR_UI_STORAGE_KEY = 'lapis:docs:sidebar-ui:v1';
3
+ export const DEFAULT_DOCS_SIDEBAR_UI_STATE = {
4
+ sidebarView: 'docs',
5
+ folderMode: false,
6
+ folderFollowActive: true,
7
+ folderDisplayMode: 'panel',
8
+ sourceFolderFollowActive: true,
9
+ sourceFolderOpenNodes: {},
10
+ tagsViewMode: 'tree',
11
+ folderOpenNodes: {},
12
+ tagsOpenNodes: {},
13
+ bookOpenNodes: {},
14
+ notesFollowActive: true,
15
+ notesFiltersOpen: false,
16
+ noteSortMode: DEFAULT_DOCS_NOTE_SORT
17
+ };
18
+ function isSidebarView(value) {
19
+ return value === 'docs' || value === 'folders' || value === 'links' || value === 'tags';
20
+ }
21
+ function isTagsViewMode(value) {
22
+ return value === 'tree' || value === 'flat';
23
+ }
24
+ function isFolderDisplayMode(value) {
25
+ return value === 'panel' || value === 'inline';
26
+ }
27
+ function isBooleanRecord(value) {
28
+ if (!value || typeof value !== 'object' || Array.isArray(value))
29
+ return false;
30
+ return Object.values(value).every((item) => typeof item === 'boolean');
31
+ }
32
+ export function readDocsSidebarUiState() {
33
+ if (typeof localStorage === 'undefined') {
34
+ return {
35
+ ...DEFAULT_DOCS_SIDEBAR_UI_STATE,
36
+ folderOpenNodes: {},
37
+ tagsOpenNodes: {},
38
+ bookOpenNodes: {}
39
+ };
40
+ }
41
+ try {
42
+ const rawValue = localStorage.getItem(DOCS_SIDEBAR_UI_STORAGE_KEY);
43
+ if (!rawValue) {
44
+ return {
45
+ ...DEFAULT_DOCS_SIDEBAR_UI_STATE,
46
+ folderOpenNodes: {},
47
+ tagsOpenNodes: {},
48
+ bookOpenNodes: {}
49
+ };
50
+ }
51
+ const parsed = JSON.parse(rawValue);
52
+ return {
53
+ sidebarView: isSidebarView(parsed.sidebarView) ? parsed.sidebarView : 'docs',
54
+ folderMode: parsed.folderMode === true,
55
+ folderFollowActive: parsed.folderFollowActive !== false,
56
+ folderDisplayMode: isFolderDisplayMode(parsed.folderDisplayMode)
57
+ ? parsed.folderDisplayMode
58
+ : 'panel',
59
+ sourceFolderFollowActive: parsed.sourceFolderFollowActive !== false,
60
+ sourceFolderOpenNodes: isBooleanRecord(parsed.sourceFolderOpenNodes)
61
+ ? { ...parsed.sourceFolderOpenNodes }
62
+ : {},
63
+ tagsViewMode: isTagsViewMode(parsed.tagsViewMode) ? parsed.tagsViewMode : 'tree',
64
+ folderOpenNodes: isBooleanRecord(parsed.folderOpenNodes) ? { ...parsed.folderOpenNodes } : {},
65
+ tagsOpenNodes: isBooleanRecord(parsed.tagsOpenNodes) ? { ...parsed.tagsOpenNodes } : {},
66
+ bookOpenNodes: isBooleanRecord(parsed.bookOpenNodes) ? { ...parsed.bookOpenNodes } : {},
67
+ notesFollowActive: parsed.notesFollowActive !== false,
68
+ notesFiltersOpen: parsed.notesFiltersOpen === true,
69
+ noteSortMode: isDocsNoteSortMode(parsed.noteSortMode)
70
+ ? parsed.noteSortMode
71
+ : DEFAULT_DOCS_NOTE_SORT
72
+ };
73
+ }
74
+ catch {
75
+ return {
76
+ ...DEFAULT_DOCS_SIDEBAR_UI_STATE,
77
+ folderOpenNodes: {},
78
+ tagsOpenNodes: {},
79
+ bookOpenNodes: {}
80
+ };
81
+ }
82
+ }
83
+ export function writeDocsSidebarUiState(state) {
84
+ if (typeof localStorage === 'undefined')
85
+ return;
86
+ localStorage.setItem(DOCS_SIDEBAR_UI_STORAGE_KEY, JSON.stringify(state));
87
+ }
@@ -0,0 +1,16 @@
1
+ import type { DocsNoteSummary } from '../../model';
2
+ import { type DocsNoteSortMode } from './docs-note-sort';
3
+ export declare const DOCS_SOURCE_ROOT_ID = "__docs-root__";
4
+ export type DocsSourceFolderTreeNode = {
5
+ id: string;
6
+ path: string;
7
+ label: string;
8
+ count: number;
9
+ isBook: boolean;
10
+ notes: DocsNoteSummary[];
11
+ children: DocsSourceFolderTreeNode[];
12
+ };
13
+ export declare function createDocsSourceFolderTree(notes: DocsNoteSummary[], sortMode?: DocsNoteSortMode): DocsSourceFolderTreeNode;
14
+ export declare function filterDocsSourceFolderTree(tree: DocsSourceFolderTreeNode, query: string, sortMode?: DocsNoteSortMode): DocsSourceFolderTreeNode | null;
15
+ export declare function collectExpandableSourceFolderIds(node: DocsSourceFolderTreeNode): string[];
16
+ export declare function sourceFolderAncestorIds(note: Pick<DocsNoteSummary, 'id' | 'sourcePath'>): string[];