@sparkelf/dsh-knowledge-base 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/SKILL.md +147 -0
  3. package/cordis.patch.yml +5 -0
  4. package/i18n/zh.json +10 -0
  5. package/lib/client.js +18308 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +6005 -0
  8. package/lib/types/api.d.ts +620 -0
  9. package/lib/types/client/KnowledgeAudit.d.ts +39 -0
  10. package/lib/types/client/KnowledgeContextMenu.d.ts +121 -0
  11. package/lib/types/client/KnowledgeCreateDialog.d.ts +134 -0
  12. package/lib/types/client/KnowledgeFileTree.d.ts +117 -0
  13. package/lib/types/client/KnowledgeHome.d.ts +102 -0
  14. package/lib/types/client/KnowledgeIndexing.d.ts +60 -0
  15. package/lib/types/client/KnowledgeListing.d.ts +174 -0
  16. package/lib/types/client/KnowledgeLoading.d.ts +41 -0
  17. package/lib/types/client/KnowledgeMenuIcon.d.ts +27 -0
  18. package/lib/types/client/KnowledgePanel.d.ts +58 -0
  19. package/lib/types/client/KnowledgePermissionOverview.d.ts +147 -0
  20. package/lib/types/client/KnowledgePermissions.d.ts +47 -0
  21. package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
  22. package/lib/types/client/KnowledgeReferencePicker.d.ts +81 -0
  23. package/lib/types/client/KnowledgeSearch.d.ts +83 -0
  24. package/lib/types/client/KnowledgeSectionHeader.d.ts +71 -0
  25. package/lib/types/client/KnowledgeSelectionBar.d.ts +45 -0
  26. package/lib/types/client/KnowledgeSettings.d.ts +96 -0
  27. package/lib/types/client/KnowledgeSettingsSection.d.ts +21 -0
  28. package/lib/types/client/KnowledgeShareDialog.d.ts +70 -0
  29. package/lib/types/client/KnowledgeTemplates.d.ts +43 -0
  30. package/lib/types/client/KnowledgeTrash.d.ts +62 -0
  31. package/lib/types/client/MenuGlyphs.d.ts +37 -0
  32. package/lib/types/client/auditPaging.d.ts +40 -0
  33. package/lib/types/client/departmentFolders.d.ts +57 -0
  34. package/lib/types/client/deployTarget.d.ts +34 -0
  35. package/lib/types/client/index.d.ts +40 -0
  36. package/lib/types/client/insertReadiness.d.ts +60 -0
  37. package/lib/types/client/knowledgeMenuCopy.d.ts +23 -0
  38. package/lib/types/client/locales.d.ts +505 -0
  39. package/lib/types/client/openInSidebar.d.ts +62 -0
  40. package/lib/types/client/ownerLabel.d.ts +39 -0
  41. package/lib/types/client/paging.d.ts +69 -0
  42. package/lib/types/client/permissionRows.d.ts +265 -0
  43. package/lib/types/client/progressRules.d.ts +54 -0
  44. package/lib/types/client/referenceTargets.d.ts +60 -0
  45. package/lib/types/client/roleSummary.d.ts +47 -0
  46. package/lib/types/client/saveAudit.d.ts +87 -0
  47. package/lib/types/client/shareRelations.d.ts +81 -0
  48. package/lib/types/client/useFavorites.d.ts +49 -0
  49. package/lib/types/client/useFileDrop.d.ts +171 -0
  50. package/lib/types/client/useFileDropHook.d.ts +64 -0
  51. package/lib/types/client/usePinnedPaths.d.ts +39 -0
  52. package/lib/types/client/useTreeState.d.ts +46 -0
  53. package/lib/types/client/useViewMode.d.ts +39 -0
  54. package/lib/types/deploy.d.ts +209 -0
  55. package/lib/types/extract.d.ts +144 -0
  56. package/lib/types/extractionQueue.d.ts +154 -0
  57. package/lib/types/index.d.ts +899 -0
  58. package/lib/types/permissions.d.ts +237 -0
  59. package/lib/types/protocol.d.ts +229 -0
  60. package/lib/types/remote.d.ts +105 -0
  61. package/lib/types/trash.d.ts +142 -0
  62. package/lib/types/viewMetadata.d.ts +20 -0
  63. package/lib/types/views.d.ts +923 -0
  64. package/package.json +109 -4
  65. package/skills/kb-organize/SKILL.md +365 -0
  66. package/skills/kb-organize/i18n/zh.json +10 -0
  67. package/README.md +0 -3
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Wire the knowledge base into the sidebar editor's save events.
3
+ *
4
+ * The knowledge base does not own the editor. Documents are shown in the host's own
5
+ * file window, and that window saves through its own route -- so a save never passes
6
+ * through a knowledge-base code path, and an audit trail written only from this
7
+ * plugin's routes would miss every edit made in the editor.
8
+ *
9
+ * The sidebar therefore accepts two registrations, both optional to it:
10
+ *
11
+ * - A **save actor** answers "who is acting right now". The knowledge base registers
12
+ * one that reports the DataOps identity the Host resolved.
13
+ * - A **save listener** receives every successful save after the bytes landed.
14
+ *
15
+ * The knowledge base registers both. What it does with an event is deliberately
16
+ * duller than it could be: it posts the save to its own append-only audit route and
17
+ * forgets about it. Delivery is fire-and-forget, so a knowledge base that is down,
18
+ * slow, or misconfigured cannot turn a completed save into a failed one -- which is
19
+ * the whole reason the sidebar calls listeners without awaiting them.
20
+ *
21
+ * @module @sparkelf/dsh-knowledge-base/client
22
+ */
23
+ /**
24
+ * Turn an absolute path back into a knowledge-base relative path.
25
+ *
26
+ * The sidebar reports absolute paths because that is what it edits. The audit route
27
+ * speaks the relative paths every other knowledge-base surface uses, so the mount
28
+ * point is stripped here rather than sending an absolute path the route would have
29
+ * to guess a root for.
30
+ * @param absolutePath - the path the sidebar saved.
31
+ * @param mountPoint - the knowledge root, absolute.
32
+ * @returns The relative path, or undefined when the save was outside the root.
33
+ */
34
+ export declare function toKnowledgePath(absolutePath: string, mountPoint: string): string | undefined;
35
+ /**
36
+ * Decide whether a save belongs to the knowledge base, and under which path.
37
+ *
38
+ * Two shapes show up in practice and both mean the same document:
39
+ *
40
+ * - The editor reports an absolute path when it edits outside a session workspace.
41
+ * - It reports a path relative to the session workspace (`kb/test-toc.md`) when the
42
+ * document was reached through the workspace, which is the ordinary case here: the
43
+ * knowledge view is linked into `/workspace/kb`.
44
+ *
45
+ * The knowledge root is reported by the page as its configured mount point, and the
46
+ * view is what the editor actually opens, so the mount point's last segment (`kb`)
47
+ * is the prefix that identifies knowledge-base saves in the relative shape. Matching
48
+ * on that segment rather than on the full absolute path is what makes the workspace
49
+ * route work at all.
50
+ * @param savedPath - the path the editor reported.
51
+ * @param mountPoint - the knowledge root as the page configured it.
52
+ * @returns The knowledge-base relative path, or undefined when the save is elsewhere.
53
+ */
54
+ export declare function matchKnowledgeSave(savedPath: string, mountPoint: string): string | undefined;
55
+ /**
56
+ * Register the knowledge base as a save actor and a save listener.
57
+ *
58
+ * Safe to call on a host without the extension: every access is optional and an
59
+ * absent service simply means nothing is registered. The returned disposer removes
60
+ * both registrations, so a plugin unload cannot leave a listener pointing at a
61
+ * closed page.
62
+ *
63
+ * @param ctx - the client context the page runs in.
64
+ * @param mountPoint - the knowledge root, used to keep saves outside it out of the trail.
65
+ * @returns A disposer, or a no-op when the sidebar service is absent.
66
+ */
67
+ export declare function registerSaveAudit(ctx: unknown): () => void;
68
+ /**
69
+ * Install the resolver that reports the knowledge root.
70
+ *
71
+ * The page is what knows the mount point (it comes from the Host's config), and the
72
+ * save listener needs it to tell a knowledge-base save from any other file's save.
73
+ * Passing a getter rather than a string keeps the registration alive across a mount
74
+ * point that arrives after the listener is installed.
75
+ * @param resolver - returns the current knowledge root, absolute.
76
+ */
77
+ export declare function setMountPointResolver(resolver: () => string): void;
78
+ /** Install the resolver the plugin's apply step owns. */
79
+ export declare function setActorResolver(resolver: () => string | undefined): void;
80
+ /**
81
+ * Publish the DataOps identity to the save-actor registration.
82
+ *
83
+ * The page resolves it through its own status call; the registration lives at plugin
84
+ * scope, so the value travels through here rather than being fetched twice.
85
+ * @param id - the caller id DataOps reported, or undefined when none is known.
86
+ */
87
+ export declare function publishActor(id: string | undefined): void;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Turning grants into the two "share" groups the Home tab shows.
3
+ *
4
+ * The split the reader cares about is DIRECTION: what has been shared with me, and what
5
+ * I have shared out. The grant data has no grantor field (the backend's `toSummary`
6
+ * omits it and the table has no `created_by`), so "who gave me this" cannot be answered
7
+ * from it. What CAN be answered is the audience side: which principal a grant names.
8
+ *
9
+ * Everything here is a pure function of the grant list plus the caller, which is what
10
+ * makes the two-way split testable without a browser.
11
+ *
12
+ * @module @sparkelf/dsh-knowledge-base/client
13
+ */
14
+ /** One grant, in the shape the Host relays. */
15
+ export interface ShareGrant {
16
+ /** Stable row id. */
17
+ id: string;
18
+ /** Path relative to the knowledge mount point. */
19
+ relPath: string;
20
+ /** Whether the path is a file or a directory. */
21
+ kind: 'file' | 'directory';
22
+ /** Principal kind the grant names. */
23
+ principalType: 'user' | 'role' | 'department' | 'group';
24
+ /** Principal id within its kind. */
25
+ principalId: string;
26
+ /** Permission the grant confers. */
27
+ action: string;
28
+ }
29
+ /** A principal name lookup: id per kind, falling back to the id itself. */
30
+ export type PrincipalNameOf = (kind: ShareGrant['principalType'], id: string) => string | undefined;
31
+ /** Which group one grant belongs to. */
32
+ export type ShareDirection = 'received' | 'given';
33
+ /** One row of either group. */
34
+ export interface ShareRow {
35
+ /** Stable row id, taken from the grant. */
36
+ id: string;
37
+ /** Path relative to the knowledge mount point. */
38
+ relPath: string;
39
+ /** File name alone, for the row's title. */
40
+ name: string;
41
+ /** Directory the path sits in, for the location column. */
42
+ location: string;
43
+ /** Principal kind the grant names. */
44
+ principalType: ShareGrant['principalType'];
45
+ /** The audience this grant names, resolved to a display name when possible. */
46
+ audience: string;
47
+ /** The permission the grant confers. */
48
+ action: string;
49
+ }
50
+ /**
51
+ * Whether a grant names the caller directly.
52
+ *
53
+ * Only a `user` grant can: a role, department or group is an audience the caller
54
+ * belongs to rather than the caller themselves, and treating it as "me" would let a
55
+ * department-wide grant read as a personal share.
56
+ * @param grant - the grant to test.
57
+ * @param callerId - the caller's own user id, when known.
58
+ * @returns True when the grant names this exact user.
59
+ */
60
+ export declare function isOwnGrant(grant: ShareGrant, callerId: string | undefined): boolean;
61
+ /**
62
+ * Split grants into the two directions the Home tab shows.
63
+ *
64
+ * The rule is deliberately simple and says only what the data supports: a grant that
65
+ * names the caller is "shared with me"; every other grant is "shared out". It does NOT
66
+ * claim the caller created the latter -- the grantor is not in this data at all -- so
67
+ * callers must word that group as an audience ("I shared to X"), never as a fact about
68
+ * who granted it.
69
+ *
70
+ * A grant naming the caller is excluded from the outgoing group even when it also
71
+ * appears there, so one document cannot look like it was shared to the reader and by
72
+ * the reader at the same time.
73
+ * @param grants - every grant the Host relayed.
74
+ * @param callerId - the caller's own user id, when known.
75
+ * @param nameOf - the principal directory lookup.
76
+ * @returns Both groups, each in the order the grants arrived.
77
+ */
78
+ export declare function splitShareRelations(grants: readonly ShareGrant[], callerId: string | undefined, nameOf: PrincipalNameOf): {
79
+ received: ShareRow[];
80
+ given: ShareRow[];
81
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The favourites store the tree's star column renders.
3
+ *
4
+ * A star is a document-level preference, so it lives beside `.pinned.json` and
5
+ * `.kb-tree-state.json` in the caller's own view directory rather than in browser
6
+ * storage: a reader who stars a report on one machine expects to find it starred on
7
+ * the next, and a browser profile is not the unit a knowledge base follows.
8
+ *
9
+ * The write path is deliberately the same shape as `useTreeState`'s. A reader who
10
+ * stars a few rows in a row makes one gesture per row, and writing through
11
+ * immediately would put a round trip behind each click; saves are therefore
12
+ * coalesced behind one timer, only one is ever on the wire, and the latest state
13
+ * always wins. The optimistic update exists because a star is a one-click
14
+ * affordance on the row the reader is looking at -- waiting for a round trip
15
+ * before it fills in makes the control feel broken when it is merely slow.
16
+ *
17
+ * Keeping this a separate hook rather than extending `useTreeState` is a
18
+ * consequence of the two having different payload shapes and different failure
19
+ * costs: a lost expansion collapses a folder, while a lost star loses the mark the
20
+ * reader was just told had been saved.
21
+ *
22
+ * @module @sparkelf/dsh-knowledge-base/client
23
+ */
24
+ /** What the hook exposes to the tree. */
25
+ export interface FavoritesStore {
26
+ /** Relative paths of the starred rows, as the Host last answered. */
27
+ relPaths: readonly string[];
28
+ /** Whether one path is starred. */
29
+ has: (relPath: string) => boolean;
30
+ /**
31
+ * Star or unstar one path.
32
+ *
33
+ * Idempotent in the direction asked for, like `setPinned`: asking for the state
34
+ * that already holds writes nothing, so a double click cannot toggle a star back
35
+ * off by accident.
36
+ */
37
+ setFavorite: (relPath: string, favorite: boolean) => void;
38
+ /** Re-read from the Host. */
39
+ refresh: () => void;
40
+ /** Set when the last write was refused, so the surface can say why. */
41
+ error: string | undefined;
42
+ }
43
+ /**
44
+ * Read and write the caller's favourites.
45
+ * @param enabled - false while the mount point is unknown, which is the only state
46
+ * where a favourites request would name a view that does not exist yet.
47
+ * @returns The store; `relPaths` is empty until the first read answers.
48
+ */
49
+ export declare function useFavorites(enabled: boolean): FavoritesStore;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Drag-and-drop upload: the part that can be decided without a browser.
3
+ *
4
+ * A drop needs three things settled before anything is uploaded: which directory the
5
+ * pointer is over, whether that directory may receive a drop, and the list of files
6
+ * (with their paths inside a dropped folder) that the drop carried. All three are
7
+ * answers about *data*, not about React state, so they live here and are tested
8
+ * directly instead of through a rendered tree.
9
+ *
10
+ * The module deliberately does not touch `DataTransfer`, `FileList` or `document`.
11
+ * The test runner has no DOM (measured: `DataTransfer`, `FileList` and `document` are
12
+ * all undefined, only `File` exists), and a module that reached for them could only be
13
+ * tested by faking the entire browser. It describes the two shapes it needs instead,
14
+ * and the component adapts a real event to them at the boundary. That is also what
15
+ * keeps the traversal order and the collision rules honest: they are pure functions of
16
+ * their inputs, so a test can assert them without a browser.
17
+ *
18
+ * @module @sparkelf/dsh-knowledge-base/client
19
+ */
20
+ /** A file plus the path it should occupy under the drop target. */
21
+ export interface DroppedFile {
22
+ /** The browser's `File`, or anything carrying a name and a byte source. */
23
+ file: unknown;
24
+ /**
25
+ * Path relative to the directory the reader dropped onto.
26
+ *
27
+ * A single dropped file carries only its name. A file from a dropped folder carries
28
+ * the folder's segments too (`specs/2026/api.md`), which is what preserves the shape
29
+ * the reader saw in the file manager instead of flattening it.
30
+ */
31
+ relativePath: string;
32
+ }
33
+ /**
34
+ * What a directory row knows about itself, as far as a drop is concerned.
35
+ *
36
+ * The two flags are separate on purpose. `isDirectory` says what the row IS; `drops`
37
+ * says whether this surface accepts drops AT ALL right now (an upload already running,
38
+ * or a read-only caller). A file row is a directory the drop must ignore, while a
39
+ * disabled directory row is one that must also ignore it -- the reason differs but the
40
+ * answer does not, so `acceptsDrop()` is the single place that combines them.
41
+ */
42
+ export interface DropTargetLike {
43
+ /** The directory an accepted drop uploads into, relative to the knowledge root. */
44
+ relativePath: string;
45
+ /** Whether the row names a folder rather than a document. */
46
+ isDirectory: boolean;
47
+ /** Whether this surface accepts drops at the moment (false while busy/read-only). */
48
+ drops: boolean;
49
+ }
50
+ /** The minimum a drop event must expose for this module to read it. */
51
+ export interface DropEventLike {
52
+ /** Whether the event carries file payload; a text drag must not upload anything. */
53
+ hasFiles: boolean;
54
+ /** The pending file list, or undefined when the browser exposes none. */
55
+ files: readonly DroppedFile[] | undefined;
56
+ }
57
+ /** Why a drop produced no upload. Each case is a different thing to tell the reader. */
58
+ export type DropRefusal =
59
+ /** The pointer was over a document, or outside any drop target. */
60
+ 'not-a-directory'
61
+ /** A drop target was hit but it is not accepting drops right now. */
62
+ | 'disabled'
63
+ /** The target accepted, but the drop carried no files (a text or link drag). */
64
+ | 'no-files';
65
+ /** The verdict for one drop. */
66
+ export type DropPlan = {
67
+ kind: 'upload';
68
+ directory: string;
69
+ files: DroppedFile[];
70
+ } | {
71
+ kind: 'refuse';
72
+ reason: DropRefusal;
73
+ };
74
+ /**
75
+ * Whether this row may receive the drop that is being dragged over it.
76
+ *
77
+ * Used by `dragover` to decide `preventDefault` (which is what makes the browser show
78
+ * "copy" and allows the drop) and whether to paint the highlight. It answers `false`
79
+ * for a document row so a file dragged onto a file is refused rather than silently
80
+ * doing nothing, and `false` while the surface is busy so a second drop cannot start
81
+ * a competing upload.
82
+ * @param target - the row under the pointer, or undefined outside any row.
83
+ * @returns Whether the row would accept a drop.
84
+ */
85
+ export declare function acceptsDrop(target: DropTargetLike | undefined): boolean;
86
+ /**
87
+ * Decide what one drop should do.
88
+ *
89
+ * Kept separate from the event so the refusal cases stay distinguishable: "you dropped
90
+ * it on a document" and "you dragged text, not files" need different sentences, and a
91
+ * single `undefined` return would force the caller to guess which happened.
92
+ * @param target - the row under the pointer, or undefined outside any row.
93
+ * @param event - the drop's payload.
94
+ * @returns The upload to perform, or why it was refused.
95
+ */
96
+ export declare function planDrop(target: DropTargetLike | undefined, event: DropEventLike): DropPlan;
97
+ /**
98
+ * Normalise a path relative to the drop target.
99
+ *
100
+ * Browsers report a dropped folder's contents with either separator (the `File` API on
101
+ * Windows reports `\`, `webkitRelativePath` uses `/`), and a leading separator would
102
+ * make the joined path absolute and fail the server's containment check. Both are
103
+ * collapsed here so the server sees one form.
104
+ * @param raw - the path as the browser reported it.
105
+ * @returns The path with forward slashes and no leading separator.
106
+ */
107
+ export declare function normalizeDropPath(raw: string): string;
108
+ /**
109
+ * Join a drop target with one file's own path.
110
+ * @param directory - the target directory relative to the knowledge root, `''` for the root.
111
+ * @param relativePath - the file's path inside the drop, already normalised or not.
112
+ * @returns The target path relative to the knowledge root.
113
+ */
114
+ export declare function dropTargetPath(directory: string, relativePath: string): string;
115
+ /**
116
+ * Collapse a dropped list to one entry per destination.
117
+ *
118
+ * Dropping the same folder twice, or a folder plus one of its files, would otherwise
119
+ * send two writes to the same path: the first succeeds and the second is refused as
120
+ * "already exists", so the reader would be shown a failure for a file that is actually
121
+ * there. The FIRST occurrence wins because that is the one the browser enumerated
122
+ * first, and its bytes are the ones the reader saw at the top of the drag.
123
+ * @param files - the dropped files.
124
+ * @returns One entry per distinct destination path, in first-seen order.
125
+ */
126
+ export declare function dedupeDropped(files: readonly DroppedFile[]): DroppedFile[];
127
+ /**
128
+ * One line describing the outcome of a whole drop, or undefined when there is nothing
129
+ * worth saying (all files landed).
130
+ *
131
+ * The reader dropped N files and gets one message: naming each failure is unreadable,
132
+ * while "some failed" without a count is not actionable. Partial success therefore
133
+ * states both numbers and names the first few failures, because the next step (retry
134
+ * that one, rename it, ask for access) depends on WHICH file was refused.
135
+ * @param results - one outcome per attempted file, in order.
136
+ * @param maxNames - how many failing names to spell out before summarising the rest.
137
+ * @returns The sentence to show, or undefined when every file landed.
138
+ */
139
+ export declare function summarizeDrop(results: readonly {
140
+ relativePath: string;
141
+ ok: boolean;
142
+ }[], maxNames?: number): {
143
+ ok: number;
144
+ failed: string[];
145
+ } | undefined;
146
+ /**
147
+ * Whether a drag event carries files this surface could upload.
148
+ *
149
+ * `types` is the only part of a `DataTransfer` readable during `dragover`: the payload
150
+ * itself is protected until the drop, so this is what `dragover` must ask to decide
151
+ * whether to `preventDefault`. An internal row drag sets `text/plain` and the knowledge
152
+ * base's own MIME type but NOT `Files`, which is how an in-page move stays a move.
153
+ * @param types - `event.dataTransfer.types`.
154
+ * @returns Whether the drag is an external file payload.
155
+ */
156
+ export declare function isFileDrag(types: readonly string[] | undefined): boolean;
157
+ /**
158
+ * Read the files of a drop as the shape {@link planDrop} takes.
159
+ *
160
+ * `webkitGetAsEntry` is preferred because it is the only way a DROPPED FOLDER arrives:
161
+ * folders never appear in `dataTransfer.files`, so a plain file list silently drops them
162
+ * and the reader sees "nothing happened". The entry API must be read synchronously from
163
+ * the drop handler (the payload enters protected mode once the event dispatch ends), so
164
+ * entries are captured here first and traversed afterwards.
165
+ *
166
+ * Falls back to the flat file list when the entry API is missing or yields nothing
167
+ * usable, which is also the correct behaviour for a browser that only reports files.
168
+ * @param transfer - the drop's `dataTransfer`.
169
+ * @returns One entry per file, with paths relative to the drop target.
170
+ */
171
+ export declare function readDroppedFiles(transfer: DataTransfer | undefined | null): Promise<DroppedFile[]>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The React shell around the drag-and-drop decisions.
3
+ *
4
+ * Everything that can be decided without a browser lives in `useFileDrop.ts` next to
5
+ * this file -- the target rules, the path joining, the de-duplication, the summary.
6
+ * What is left is the one thing that genuinely needs a component: which row is
7
+ * currently painted as the drop target.
8
+ *
9
+ * Kept in its own module for two reasons. The pure module must stay importable from a
10
+ * test that has no DOM (measured: the runner has `File`, but no `DataTransfer`,
11
+ * `FileList` or `document`), and importing React into it would make that impossible.
12
+ * And the component wiring is the part that has to wait for the in-flight edits to
13
+ * `KnowledgeFileTree.tsx` / `KnowledgeHome.tsx` to land, so it is kept separate rather
14
+ * than half-merged.
15
+ *
16
+ * @module @sparkelf/dsh-knowledge-base/client
17
+ */
18
+ import type { DropEventLike, DropPlan, DropTargetLike } from './useFileDrop.ts';
19
+ /** What a component gets back from {@link useFileDrop}. */
20
+ export interface FileDropHandle {
21
+ /** The path of the row that should paint its drop highlight, or undefined. */
22
+ highlighted: string | undefined;
23
+ /**
24
+ * Call from a row's `onDragOver`.
25
+ *
26
+ * Returns whether the row accepts this drag, which the caller uses to decide
27
+ * `event.preventDefault()`: without it the browser refuses the drop outright.
28
+ * @param target - the row under the pointer.
29
+ * @returns Whether the row would accept the drop.
30
+ */
31
+ onDragOver: (target: DropTargetLike | undefined) => boolean;
32
+ /**
33
+ * Call from a row's `onDragEnter`.
34
+ *
35
+ * @param target - the row the pointer entered.
36
+ * @returns Whether the row would accept the drop.
37
+ */
38
+ onDragEnter: (target: DropTargetLike | undefined) => boolean;
39
+ /**
40
+ * Call from a row's `onDragLeave`, and from the surface's own `onDragLeave` for the
41
+ * case where the pointer left the panel entirely.
42
+ * @param target - the row the pointer left, if any.
43
+ */
44
+ onDragLeave: (target?: DropTargetLike | undefined) => void;
45
+ /**
46
+ * Call from a row's `onDrop`. Clears the highlight and returns what to upload.
47
+ * @param target - the row the pointer was over.
48
+ * @param event - the drop payload.
49
+ * @returns The upload to perform, or why nothing happens.
50
+ */
51
+ onDrop: (target: DropTargetLike | undefined, event: DropEventLike) => DropPlan;
52
+ /** Clear the highlight without a drop (Escape, or the drag leaving the window). */
53
+ reset: () => void;
54
+ }
55
+ /**
56
+ * Track the drag state for one drop surface.
57
+ *
58
+ * Note the enter/leave counter. A drag crossing from a row onto its own child fires
59
+ * `dragleave` on the parent BEFORE `dragenter` on the child, so clearing the highlight
60
+ * on every `dragleave` makes it flicker off and back on. Counting enter/leave pairs is
61
+ * what keeps it stable; the counter reaching zero is the real exit.
62
+ * @returns The handle a component wires into its rows.
63
+ */
64
+ export declare function useFileDrop(): FileDropHandle;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The pinned-path store the rail renders.
3
+ *
4
+ * Pins are a document-level preference, so they live in the caller's own view
5
+ * directory on the share rather than in browser storage; the Host owns that file
6
+ * and this hook only reads and writes it through the two routes. The optimistic
7
+ * update exists because toggling a pin is a one-click gesture on a menu the reader
8
+ * is looking at: waiting for a round trip before the row moves makes the control
9
+ * feel broken when it is merely slow. A failed write reloads from the Host rather
10
+ * than leaving the guess on screen.
11
+ *
12
+ * @module @sparkelf/dsh-knowledge-base/client
13
+ */
14
+ /** What the hook exposes to the rail. */
15
+ export interface PinnedStore {
16
+ /** Pinned paths, in the order the Host returned them. */
17
+ relPaths: readonly string[];
18
+ /** Whether one path is pinned. */
19
+ has: (relPath: string) => boolean;
20
+ /**
21
+ * Add or remove one path.
22
+ *
23
+ * Idempotent in the direction asked for: pinning an already-pinned path leaves
24
+ * the list alone rather than toggling it off, so a double click cannot silently
25
+ * undo the first one.
26
+ */
27
+ setPinned: (relPath: string, pinned: boolean) => void;
28
+ /** Re-read from the Host. */
29
+ refresh: () => void;
30
+ /** Set when the last write was refused, so the surface can say why. */
31
+ error: string | undefined;
32
+ }
33
+ /**
34
+ * Read and write the caller's pins.
35
+ * @param enabled - false while the mount point is unknown, which is the only state
36
+ * where a pin request would name a tree that does not exist yet.
37
+ * @returns The store.
38
+ */
39
+ export declare function usePinnedPaths(enabled: boolean): PinnedStore;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The set of folders the reader has open in the explorer, remembered across visits.
3
+ *
4
+ * Expansion is a document-level preference, so it lives beside `.pinned.json` and
5
+ * `.view-mode.json` in the caller's view directory rather than in browser storage: a
6
+ * reader who opens `department/platform` on one machine expects to find it open on the
7
+ * next, and a browser profile is not the unit a knowledge base follows.
8
+ *
9
+ * Two costs shape the implementation.
10
+ *
11
+ * The read is a Host round trip, so the tree renders collapsed first and expands once
12
+ * the answer lands. Rendering it collapsed is the honest initial state: it is exactly
13
+ * what a reader who never opened a folder sees, and the alternative -- holding the whole
14
+ * tree back until a preference resolves -- would trade a flicker for a blank panel.
15
+ *
16
+ * The write happens on every toggle, and a reader opening a deep path toggles once per
17
+ * level, so writing through immediately would put four round trips behind one gesture.
18
+ * Saves are therefore coalesced: the first toggle arms a timer, later toggles inside the
19
+ * window replace the pending payload, and the timer sends whatever the latest state is.
20
+ * A save still in flight when the reader keeps clicking leaves a dirty flag rather than
21
+ * queueing a second request, so the wire never carries more than one save at a time and
22
+ * the last state always wins.
23
+ *
24
+ * A failed write is not surfaced as a page error. This is a view preference whose worst
25
+ * outcome is a folder that comes back closed, and shouting at a reader for it would be
26
+ * worse than the outcome; the next successful save repairs it.
27
+ *
28
+ * @module @sparkelf/dsh-knowledge-base/client
29
+ */
30
+ /** What the hook exposes to the rail. */
31
+ export interface TreeStateStore {
32
+ /** Absolute paths of the expanded folders, as the Host last answered. */
33
+ expanded: readonly string[];
34
+ /** Toggle one directory open or closed. */
35
+ toggle: (path: string) => void;
36
+ /** Set the whole set at once (a reveal, or a Host round trip). */
37
+ setExpanded: (paths: readonly string[]) => void;
38
+ }
39
+ /**
40
+ * Read and write the caller's expanded folders.
41
+ * @param enabled - false while the mount point is unknown, which is the only state
42
+ * where there is no view directory holding the preference.
43
+ * @param root - the knowledge root, used to bound what is stored; '' while unknown.
44
+ * @returns The store; `expanded` is empty until the first read answers.
45
+ */
46
+ export declare function useTreeState(enabled: boolean, root?: string): TreeStateStore;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The list/grid preference the rail renders.
3
+ *
4
+ * A document-level preference stored beside the pins, for the same reason they are:
5
+ * a reader who switches to the grid on one machine expects it on the next. The
6
+ * optimistic update exists because the toggle is a one-click gesture on a control
7
+ * the reader is looking at, and waiting for a round trip before the layout changes
8
+ * makes it feel broken when it is merely slow. A failed write reloads from the Host
9
+ * rather than leaving the guess on screen.
10
+ *
11
+ * @module @sparkelf/dsh-knowledge-base/client
12
+ */
13
+ /**
14
+ * Which layout the rail is showing.
15
+ *
16
+ * `explorer` renders the product's own file explorer (see `KnowledgeFileTree`), which is
17
+ * the tree a reader already knows from the workspace sidebar. It is a third option rather
18
+ * than a replacement because the two layouts answer different questions: the listing shows
19
+ * knowledge-base state the explorer has no concept of (whether a row is readable, its
20
+ * pinned mark, multi-select for a batch grant), while the explorer brings the affordances a
21
+ * file tree is expected to have (sort, inline rename, drag-and-drop, upload strip).
22
+ */
23
+ export type ViewMode = 'list' | 'grid' | 'explorer';
24
+ /** What the hook exposes to the rail. */
25
+ export interface ViewModeStore {
26
+ /** The stored layout. */
27
+ mode: ViewMode;
28
+ /** Switch layouts, writing through to the Host. */
29
+ setMode: (mode: ViewMode) => void;
30
+ /** Set when the last write was refused, so the surface can say why. */
31
+ error: string | undefined;
32
+ }
33
+ /**
34
+ * Read and write the caller's layout preference.
35
+ * @param enabled - false while the mount point is unknown, which is the only state
36
+ * where there is no view directory to hold the preference.
37
+ * @returns The store.
38
+ */
39
+ export declare function useViewMode(enabled: boolean): ViewModeStore;