@hiai-gg/docsmint 0.3.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 (57) hide show
  1. package/LICENSE +171 -0
  2. package/README.md +348 -0
  3. package/backend/src/lib/logger.ts +18 -0
  4. package/backend/src/lib/redis-factory.ts +40 -0
  5. package/backend/src/lib/storage-factory.ts +56 -0
  6. package/frontend/src/lib/components/editor/shared-document.ts +237 -0
  7. package/frontend/src/lib/extensions/context.ts +60 -0
  8. package/frontend/src/lib/extensions/doc-tabs.ts +18 -0
  9. package/frontend/src/lib/extensions/resolve.ts +48 -0
  10. package/frontend/src/lib/extensions/types.ts +202 -0
  11. package/frontend/src/lib/hosts/DocsmintSharedDocumentHost.svelte +65 -0
  12. package/frontend/src/lib/hosts/HiaiDocsDashboardHost.svelte +1007 -0
  13. package/frontend/src/lib/hosts/HiaiDocsExtensionProvider.svelte +20 -0
  14. package/frontend/src/lib/hosts/HiaiDocsSearchHost.svelte +996 -0
  15. package/frontend/src/lib/hosts/index.ts +25 -0
  16. package/frontend/src/lib/index.ts +65 -0
  17. package/frontend/src/lib/stores/doc-tab-registry.svelte.ts +68 -0
  18. package/package.json +178 -0
  19. package/packages/cli/src/client.ts +271 -0
  20. package/packages/cli/src/commands/config.ts +47 -0
  21. package/packages/cli/src/commands/create.ts +35 -0
  22. package/packages/cli/src/commands/delete.ts +37 -0
  23. package/packages/cli/src/commands/export.ts +36 -0
  24. package/packages/cli/src/commands/folders.ts +88 -0
  25. package/packages/cli/src/commands/history.ts +55 -0
  26. package/packages/cli/src/commands/list.ts +61 -0
  27. package/packages/cli/src/commands/read.ts +38 -0
  28. package/packages/cli/src/commands/restore.ts +30 -0
  29. package/packages/cli/src/commands/search.ts +56 -0
  30. package/packages/cli/src/commands/snapshot.ts +35 -0
  31. package/packages/cli/src/commands/update.ts +54 -0
  32. package/packages/cli/src/config.ts +83 -0
  33. package/packages/cli/src/format.ts +153 -0
  34. package/packages/cli/src/index.ts +73 -0
  35. package/packages/db/src/client.ts +20 -0
  36. package/packages/db/src/index.ts +5 -0
  37. package/packages/db/src/schema.ts +692 -0
  38. package/packages/db/src/with-tenant.ts +75 -0
  39. package/packages/mcp-server/src/client.ts +172 -0
  40. package/packages/mcp-server/src/index.ts +109 -0
  41. package/packages/mcp-server/src/tools/create-document.ts +32 -0
  42. package/packages/mcp-server/src/tools/create-folder.ts +24 -0
  43. package/packages/mcp-server/src/tools/create-snapshot.ts +30 -0
  44. package/packages/mcp-server/src/tools/export-document.ts +22 -0
  45. package/packages/mcp-server/src/tools/get-document.ts +20 -0
  46. package/packages/mcp-server/src/tools/list-documents.ts +42 -0
  47. package/packages/mcp-server/src/tools/list-folders.ts +25 -0
  48. package/packages/mcp-server/src/tools/search.ts +42 -0
  49. package/packages/mcp-server/src/tools/update-document.ts +30 -0
  50. package/packages/mcp-server/src/tools/version-history.ts +32 -0
  51. package/packages/mcp-server/src/types.ts +126 -0
  52. package/packages/sdk/dist/client.d.ts +187 -0
  53. package/packages/sdk/dist/client.js +568 -0
  54. package/packages/sdk/dist/index.d.ts +3 -0
  55. package/packages/sdk/dist/index.js +1 -0
  56. package/packages/sdk/dist/types.d.ts +391 -0
  57. package/packages/sdk/dist/types.js +8 -0
@@ -0,0 +1,237 @@
1
+ export type ProseMirrorNode = {
2
+ type: string;
3
+ text?: string;
4
+ content?: ProseMirrorNode[];
5
+ attrs?: Record<string, unknown>;
6
+ marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
7
+ };
8
+
9
+ export type ProseMirrorDoc = ProseMirrorNode & {
10
+ content?: ProseMirrorNode[];
11
+ };
12
+
13
+ /** Object URLs created while hydrating protected share attachments. */
14
+ export type SharedAttachmentObjectUrls = string[];
15
+
16
+ const ATTACHMENT_PATH = /^\/api\/attachments\/[0-9a-f-]+\/raw$/i;
17
+ const SAFE_LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
18
+
19
+ function escapeHtml(value: string): string {
20
+ return value
21
+ .replace(/&/g, "&amp;")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;")
25
+ .replace(/'/g, "&#39;");
26
+ }
27
+
28
+ function wrapMark(
29
+ mark: { type: string; attrs?: Record<string, unknown> },
30
+ html: string,
31
+ ): string {
32
+ switch (mark.type) {
33
+ case "bold":
34
+ return `<strong>${html}</strong>`;
35
+ case "italic":
36
+ return `<em>${html}</em>`;
37
+ case "strike":
38
+ case "strikethrough":
39
+ return `<s>${html}</s>`;
40
+ case "underline":
41
+ return `<u>${html}</u>`;
42
+ case "code":
43
+ return `<code>${html}</code>`;
44
+ case "link": {
45
+ const rawHref = (mark.attrs?.href as string) ?? "#";
46
+ const href = safeLinkHref(rawHref);
47
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${html}</a>`;
48
+ }
49
+ case "highlight": {
50
+ const color = (mark.attrs?.color as string) ?? "#fde68a";
51
+ return `<mark style="background-color: ${escapeHtml(color)}">${html}</mark>`;
52
+ }
53
+ default:
54
+ return html;
55
+ }
56
+ }
57
+
58
+ function safeLinkHref(href: string): string {
59
+ if (href.startsWith("#") || href.startsWith("/") || href.startsWith("./")) {
60
+ return href;
61
+ }
62
+ try {
63
+ const url = new URL(href);
64
+ return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : "#";
65
+ } catch {
66
+ return "#";
67
+ }
68
+ }
69
+
70
+ function alignStyle(attrs?: Record<string, unknown>): string {
71
+ const align = attrs?.textAlign;
72
+ if (
73
+ align !== "left" &&
74
+ align !== "center" &&
75
+ align !== "right" &&
76
+ align !== "justify"
77
+ ) {
78
+ return "";
79
+ }
80
+ return ` style="text-align: ${align}"`;
81
+ }
82
+
83
+ /**
84
+ * Render descendants as inline content. Older imported documents can contain
85
+ * a paragraph inside a heading. That is invalid ProseMirror/HTML, so emitting
86
+ * it verbatim makes browsers restructure the document unpredictably. Flatten
87
+ * block wrappers in inline-only parents while preserving their text/marks.
88
+ */
89
+ function renderInline(node: ProseMirrorNode): string {
90
+ if (node.type === "text") {
91
+ let html = escapeHtml(node.text ?? "");
92
+ for (const mark of node.marks ?? []) html = wrapMark(mark, html);
93
+ return html;
94
+ }
95
+ return (node.content ?? []).map(renderInline).join("");
96
+ }
97
+
98
+ export function renderSharedDocument(doc: ProseMirrorDoc): string {
99
+ const renderNode = (node: ProseMirrorNode): string => {
100
+ if (node.type === "text") return renderInline(node);
101
+
102
+ const align = alignStyle(node.attrs);
103
+ const inner = (node.content ?? []).map(renderNode).join("");
104
+ switch (node.type) {
105
+ case "paragraph":
106
+ return `<p${align}>${inner}</p>`;
107
+ case "heading": {
108
+ const level = Math.min(Math.max(Number(node.attrs?.level ?? 1), 1), 6);
109
+ return `<h${level}${align}>${(node.content ?? []).map(renderInline).join("")}</h${level}>`;
110
+ }
111
+ case "bulletList":
112
+ return `<ul${align}>${inner}</ul>`;
113
+ case "orderedList": {
114
+ const rawStart = Number(node.attrs?.start ?? 1);
115
+ const start =
116
+ Number.isSafeInteger(rawStart) && rawStart > 1
117
+ ? ` start="${rawStart}"`
118
+ : "";
119
+ return `<ol${start}${align}>${inner}</ol>`;
120
+ }
121
+ case "listItem":
122
+ return `<li${align}>${inner}</li>`;
123
+ case "taskList":
124
+ return `<ul data-type="taskList">${inner}</ul>`;
125
+ case "taskItem": {
126
+ const checked =
127
+ node.attrs?.checked === true || node.attrs?.checked === "true";
128
+ return `<li data-type="taskItem"${checked ? ' data-checked="true"' : ""}><label><input type="checkbox" disabled${checked ? " checked" : ""} /></label><div>${inner}</div></li>`;
129
+ }
130
+ case "blockquote":
131
+ return `<blockquote${align}>${inner}</blockquote>`;
132
+ case "table":
133
+ return `<table><tbody>${inner}</tbody></table>`;
134
+ case "tableRow":
135
+ return `<tr>${inner}</tr>`;
136
+ case "tableHeader":
137
+ return `<th${align}>${inner}</th>`;
138
+ case "tableCell":
139
+ return `<td${align}>${inner}</td>`;
140
+ case "codeBlock": {
141
+ const lang = (node.attrs?.language as string) ?? "";
142
+ return `<pre><code${lang ? ` class="language-${escapeHtml(lang)}"` : ""}>${inner}</code></pre>`;
143
+ }
144
+ case "horizontalRule":
145
+ return "<hr />";
146
+ case "hardBreak":
147
+ return "<br />";
148
+ case "image": {
149
+ const src = (node.attrs?.src as string) ?? "";
150
+ const alt = (node.attrs?.alt as string) ?? "";
151
+ const width = Number(node.attrs?.width);
152
+ const height = Number(node.attrs?.height);
153
+ const dimensions = `${Number.isFinite(width) && width > 0 ? ` width="${Math.round(width)}"` : ""}${Number.isFinite(height) && height > 0 ? ` height="${Math.round(height)}"` : ""}`;
154
+ if (ATTACHMENT_PATH.test(src)) {
155
+ return `<img data-shared-attachment-src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"${dimensions} />`;
156
+ }
157
+ return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"${dimensions} />`;
158
+ }
159
+ default:
160
+ return inner;
161
+ }
162
+ };
163
+
164
+ return (doc.content ?? []).map(renderNode).join("");
165
+ }
166
+
167
+ /** Mark Markdown task items so print/PDF CSS can suppress the regular bullet. */
168
+ export function markMarkdownTaskItems(html: string): string {
169
+ return html.replace(
170
+ /<li>(\s*<input\b[^>]*type=["']checkbox["'][^>]*>)/gi,
171
+ '<li class="task-list-item">$1',
172
+ );
173
+ }
174
+
175
+ export function sharedAttachmentHeaders(
176
+ token: string,
177
+ password = "",
178
+ ): HeadersInit {
179
+ return {
180
+ "x-share-token": token,
181
+ ...(password ? { "x-share-password": password } : {}),
182
+ };
183
+ }
184
+
185
+ /** Replace protected attachment placeholders with authenticated blob URLs. */
186
+ export async function hydrateSharedAttachmentImages(
187
+ root: ParentNode,
188
+ token: string,
189
+ password = "",
190
+ ): Promise<SharedAttachmentObjectUrls> {
191
+ const objectUrls: string[] = [];
192
+ const images = root.querySelectorAll<HTMLImageElement>(
193
+ "img[data-shared-attachment-src]",
194
+ );
195
+ await Promise.all(
196
+ Array.from(images, async (image) => {
197
+ const src = image.dataset.sharedAttachmentSrc;
198
+ if (!src) return;
199
+ try {
200
+ const response = await fetch(src, {
201
+ headers: sharedAttachmentHeaders(token, password),
202
+ });
203
+ if (!response.ok) {
204
+ image.dataset.sharedAttachmentError = String(response.status);
205
+ return;
206
+ }
207
+ const objectUrl = URL.createObjectURL(await response.blob());
208
+ objectUrls.push(objectUrl);
209
+ image.src = objectUrl;
210
+ image.removeAttribute("data-shared-attachment-src");
211
+ } catch {
212
+ image.dataset.sharedAttachmentError = "network";
213
+ }
214
+ }),
215
+ );
216
+ return objectUrls;
217
+ }
218
+
219
+ /** Wait until every image in a print/export subtree has finished loading. */
220
+ export async function waitForSharedDocumentImages(
221
+ root: ParentNode,
222
+ ): Promise<void> {
223
+ const images = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
224
+ await Promise.all(
225
+ images.map(async (image) => {
226
+ if (image.complete) return;
227
+ if (typeof image.decode === "function") {
228
+ await image.decode().catch(() => undefined);
229
+ return;
230
+ }
231
+ await new Promise<void>((resolve) => {
232
+ image.addEventListener("load", () => resolve(), { once: true });
233
+ image.addEventListener("error", () => resolve(), { once: true });
234
+ });
235
+ }),
236
+ );
237
+ }
@@ -0,0 +1,60 @@
1
+ import { createContext } from "svelte";
2
+ import type { HiaiDocsFrontendExtensions } from "./types";
3
+
4
+ /**
5
+ * Create a fresh manifest for one app/request.
6
+ *
7
+ * A new array is created for every category so an SSR request or HMR update
8
+ * cannot mutate another request's extension set.
9
+ */
10
+ export function createFrontendExtensions(
11
+ initial: Partial<HiaiDocsFrontendExtensions> = {},
12
+ ): HiaiDocsFrontendExtensions {
13
+ return {
14
+ navigation: [...(initial.navigation ?? [])],
15
+ dashboardWidgets: [...(initial.dashboardWidgets ?? [])],
16
+ searchWidgets: [...(initial.searchWidgets ?? [])],
17
+ documentTabs: [...(initial.documentTabs ?? [])],
18
+ editorActions: [...(initial.editorActions ?? [])],
19
+ documentMenuActions: [...(initial.documentMenuActions ?? [])],
20
+ settingsSections: [...(initial.settingsSections ?? [])],
21
+ commandPaletteActions: [...(initial.commandPaletteActions ?? [])],
22
+ sharedDocumentHeaderActions: [
23
+ ...(initial.sharedDocumentHeaderActions ?? []),
24
+ ],
25
+ sharedDocumentTabs: [...(initial.sharedDocumentTabs ?? [])],
26
+ sharedDocumentNotesModes: [...(initial.sharedDocumentNotesModes ?? [])],
27
+ sharedDocumentEditorModes: [...(initial.sharedDocumentEditorModes ?? [])],
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Svelte's context storage is scoped to a component tree, which makes this
33
+ * safe for concurrent SSR requests. Do not replace this with a module-level
34
+ * mutable registry.
35
+ */
36
+ const [getProvidedFrontendExtensions, setFrontendExtensions] =
37
+ createContext<HiaiDocsFrontendExtensions>();
38
+
39
+ export { setFrontendExtensions };
40
+
41
+ /**
42
+ * Read the request-scoped manifest, defaulting to an empty manifest for the
43
+ * standalone open-source application. This keeps every host backward
44
+ * compatible when no extension provider is mounted above it.
45
+ */
46
+ export function getFrontendExtensions(): HiaiDocsFrontendExtensions {
47
+ return getProvidedFrontendExtensions() ?? createFrontendExtensions();
48
+ }
49
+
50
+ /** Stable aliases for hosts that prefer the product name in their imports. */
51
+ export const getHiaiDocsExtensions = getFrontendExtensions;
52
+ export const setHiaiDocsExtensions = setFrontendExtensions;
53
+
54
+ export function provideFrontendExtensions(
55
+ initial: Partial<HiaiDocsFrontendExtensions> = {},
56
+ ): HiaiDocsFrontendExtensions {
57
+ const extensions = createFrontendExtensions(initial);
58
+ setFrontendExtensions(extensions);
59
+ return extensions;
60
+ }
@@ -0,0 +1,18 @@
1
+ import type { Component, ComponentType, SvelteComponent } from "svelte";
2
+
3
+ export type DocTabIcon = ComponentType<SvelteComponent>;
4
+
5
+ export interface DocTabPanelProps {
6
+ documentId: string;
7
+ content: string;
8
+ contentJson: object | undefined;
9
+ }
10
+
11
+ export interface DocTabDefinition {
12
+ id: string;
13
+ label: string;
14
+ component: Component<DocTabPanelProps>;
15
+ order?: number;
16
+ icon?: DocTabIcon;
17
+ disabled?: boolean;
18
+ }
@@ -0,0 +1,48 @@
1
+ import type { ExtensionVisibility, ExtensionVisibilityContext } from "./types";
2
+
3
+ type OrderedExtension = {
4
+ id: string;
5
+ order?: number;
6
+ visible?: ExtensionVisibility;
7
+ };
8
+
9
+ /**
10
+ * Returns extensions in a deterministic order, omitting duplicate ids and
11
+ * extensions that are unavailable in the current host context.
12
+ *
13
+ * Extension manifests are an optional product boundary. A faulty visibility
14
+ * predicate must not prevent the stock HiAi-Docs UI from rendering, so a
15
+ * throwing predicate is treated as not visible.
16
+ */
17
+ export function resolveExtensions<T extends OrderedExtension>(
18
+ extensions: readonly T[],
19
+ context: ExtensionVisibilityContext = {},
20
+ ): T[] {
21
+ const seen = new Set<string>();
22
+ const visible: T[] = [];
23
+
24
+ for (const extension of extensions) {
25
+ if (seen.has(extension.id) || !isVisible(extension.visible, context)) {
26
+ continue;
27
+ }
28
+ seen.add(extension.id);
29
+ visible.push(extension);
30
+ }
31
+
32
+ return visible.sort((a, b) => {
33
+ const orderDifference = (a.order ?? 0) - (b.order ?? 0);
34
+ return orderDifference !== 0 ? orderDifference : a.id.localeCompare(b.id);
35
+ });
36
+ }
37
+
38
+ function isVisible(
39
+ visible: ExtensionVisibility | undefined,
40
+ context: ExtensionVisibilityContext,
41
+ ): boolean {
42
+ if (!visible) return true;
43
+ try {
44
+ return visible(context);
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
@@ -0,0 +1,202 @@
1
+ import type { Component } from "svelte";
2
+ import type {
3
+ DocTabDefinition,
4
+ DocTabIcon,
5
+ DocTabPanelProps,
6
+ } from "./doc-tabs";
7
+
8
+ /**
9
+ * Runtime information supplied to extension visibility predicates.
10
+ *
11
+ * The object is intentionally data-only. Extensions can therefore be
12
+ * evaluated during SSR without reaching for browser globals or a module-level
13
+ * store.
14
+ */
15
+ export interface ExtensionVisibilityContext {
16
+ userId?: string;
17
+ pathname?: string;
18
+ capabilities?: Readonly<Record<string, boolean>>;
19
+ permissions?: Readonly<Record<string, boolean>>;
20
+ }
21
+
22
+ export type ExtensionVisibility = (
23
+ context: ExtensionVisibilityContext,
24
+ ) => boolean;
25
+
26
+ /** Icon shape shared with the existing document-tab contract. */
27
+ export type ExtensionIcon = DocTabIcon;
28
+
29
+ export interface NavigationExtension {
30
+ id: string;
31
+ label: string;
32
+ href?: string;
33
+ icon?: ExtensionIcon;
34
+ order?: number;
35
+ badge?: string | number;
36
+ disabled?: boolean;
37
+ visible?: ExtensionVisibility;
38
+ }
39
+
40
+ export interface DashboardWidgetProps {
41
+ userId?: string;
42
+ }
43
+
44
+ export interface DashboardWidgetExtension {
45
+ id: string;
46
+ title?: string;
47
+ component: Component<DashboardWidgetProps>;
48
+ order?: number;
49
+ colSpan?: 1 | 2 | 3 | 4 | 6 | 12;
50
+ visible?: ExtensionVisibility;
51
+ }
52
+
53
+ /**
54
+ * Read-only state supplied to a search extension.
55
+ *
56
+ * Search extensions deliberately receive result metadata only. They cannot
57
+ * alter retrieval, ranking, filters, or the authenticated API client.
58
+ */
59
+ export interface SearchWidgetProps {
60
+ query: string;
61
+ loading: boolean;
62
+ total?: number;
63
+ }
64
+
65
+ export interface SearchWidgetExtension {
66
+ id: string;
67
+ title?: string;
68
+ component: Component<SearchWidgetProps>;
69
+ order?: number;
70
+ visible?: ExtensionVisibility;
71
+ }
72
+
73
+ export interface EditorActionContext {
74
+ documentId: string;
75
+ content: string;
76
+ contentJson: object | undefined;
77
+ selection?: unknown;
78
+ /** The host supplies an editor command facade; no editor implementation leaks into the contract. */
79
+ commands?: Readonly<Record<string, (...args: unknown[]) => unknown>>;
80
+ }
81
+
82
+ export type ExtensionAction = (
83
+ context: EditorActionContext,
84
+ ) => void | Promise<void>;
85
+
86
+ export interface EditorActionExtension {
87
+ id: string;
88
+ label: string;
89
+ icon?: ExtensionIcon;
90
+ order?: number;
91
+ disabled?: boolean | ((context: EditorActionContext) => boolean);
92
+ visible?: ExtensionVisibility;
93
+ run: ExtensionAction;
94
+ }
95
+
96
+ export interface DocumentMenuActionContext extends EditorActionContext {
97
+ title?: string;
98
+ }
99
+
100
+ export type DocumentMenuAction = (
101
+ context: DocumentMenuActionContext,
102
+ ) => void | Promise<void>;
103
+
104
+ export interface DocumentMenuActionExtension {
105
+ id: string;
106
+ label: string;
107
+ icon?: ExtensionIcon;
108
+ order?: number;
109
+ destructive?: boolean;
110
+ disabled?: boolean | ((context: DocumentMenuActionContext) => boolean);
111
+ visible?: ExtensionVisibility;
112
+ run: DocumentMenuAction;
113
+ }
114
+
115
+ export interface SettingsSectionProps {
116
+ userId?: string;
117
+ }
118
+
119
+ export interface SettingsSectionExtension {
120
+ id: string;
121
+ label: string;
122
+ component: Component<SettingsSectionProps>;
123
+ order?: number;
124
+ description?: string;
125
+ visible?: ExtensionVisibility;
126
+ }
127
+
128
+ export interface CommandPaletteActionContext {
129
+ query?: string;
130
+ }
131
+
132
+ export type CommandPaletteAction = (
133
+ context: CommandPaletteActionContext,
134
+ ) => void | Promise<void>;
135
+
136
+ export interface CommandPaletteActionExtension {
137
+ id: string;
138
+ label: string;
139
+ keywords?: readonly string[];
140
+ group?: string;
141
+ shortcut?: string;
142
+ icon?: ExtensionIcon;
143
+ order?: number;
144
+ disabled?: boolean;
145
+ visible?: ExtensionVisibility;
146
+ run: CommandPaletteAction;
147
+ }
148
+
149
+ /** Safe capability hints for a public shared-document extension. */
150
+ export interface SharedDocumentExtensionContext {
151
+ shareToken: string;
152
+ documentId: string;
153
+ title: string;
154
+ content: string;
155
+ contentJson?: object;
156
+ role: "viewer" | "commenter" | "editor";
157
+ permissions: {
158
+ read: true;
159
+ annotate: boolean;
160
+ edit: boolean;
161
+ export: boolean;
162
+ };
163
+ }
164
+
165
+ export interface SharedDocumentExtension {
166
+ id: string;
167
+ label: string;
168
+ icon?: ExtensionIcon;
169
+ order?: number;
170
+ permission: "annotate" | "edit";
171
+ visible?: (context: SharedDocumentExtensionContext) => boolean;
172
+ component: Component<{ context: SharedDocumentExtensionContext }>;
173
+ }
174
+
175
+ /**
176
+ * Complete frontend extension manifest consumed by hiai-docs app-shell and
177
+ * page components. Arrays are readonly to keep registration request-scoped
178
+ * and to prevent extensions mutating one another during SSR.
179
+ */
180
+ export interface DocsmintFrontendExtensions {
181
+ navigation: readonly NavigationExtension[];
182
+ dashboardWidgets: readonly DashboardWidgetExtension[];
183
+ searchWidgets: readonly SearchWidgetExtension[];
184
+ documentTabs: readonly DocTabDefinition[];
185
+ editorActions: readonly EditorActionExtension[];
186
+ documentMenuActions: readonly DocumentMenuActionExtension[];
187
+ settingsSections: readonly SettingsSectionExtension[];
188
+ commandPaletteActions: readonly CommandPaletteActionExtension[];
189
+ sharedDocumentHeaderActions: readonly SharedDocumentExtension[];
190
+ sharedDocumentTabs: readonly SharedDocumentExtension[];
191
+ sharedDocumentNotesModes: readonly SharedDocumentExtension[];
192
+ sharedDocumentEditorModes: readonly SharedDocumentExtension[];
193
+ }
194
+
195
+ /** @deprecated Use DocsmintFrontendExtensions. */
196
+ export type HiaiDocsFrontendExtensions = DocsmintFrontendExtensions;
197
+
198
+ /** Backwards-compatible, concise name for consumers defining a manifest. */
199
+ export type FrontendExtensions = DocsmintFrontendExtensions;
200
+
201
+ /** Props used by a document tab component in extension manifests. */
202
+ export type { DocTabDefinition, DocTabPanelProps };
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import { getFrontendExtensions } from "../extensions/context";
4
+ import type {
5
+ SharedDocumentExtension,
6
+ SharedDocumentExtensionContext,
7
+ } from "../extensions/types";
8
+
9
+ const {
10
+ context,
11
+ children,
12
+ }: {
13
+ context: SharedDocumentExtensionContext;
14
+ children: Snippet;
15
+ } = $props();
16
+
17
+ const extensions = getFrontendExtensions();
18
+ function permitted(extension: SharedDocumentExtension) {
19
+ return extension.permission === "annotate"
20
+ ? context.permissions.annotate
21
+ : context.permissions.edit;
22
+ }
23
+ function visible(items: readonly SharedDocumentExtension[]) {
24
+ const seen = new Set<string>();
25
+ return items
26
+ .filter((extension) => {
27
+ if (seen.has(extension.id) || !permitted(extension)) return false;
28
+ seen.add(extension.id);
29
+ try {
30
+ return extension.visible?.(context) ?? true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ })
35
+ .sort(
36
+ (a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id),
37
+ );
38
+ }
39
+ </script>
40
+
41
+ <div data-docsmint-shared-document-host>
42
+ <div data-extension-zone="shared-header-actions">
43
+ {#each visible(extensions.sharedDocumentHeaderActions) as extension (extension.id)}
44
+ <extension.component {context} />
45
+ {/each}
46
+ </div>
47
+ {@render children()}
48
+ <div data-extension-zone="shared-document-tabs">
49
+ {#each visible(extensions.sharedDocumentTabs) as extension (extension.id)}
50
+ <extension.component {context} />
51
+ {/each}
52
+ </div>
53
+ <div data-extension-zone="shared-document-notes">
54
+ {#each visible(extensions.sharedDocumentNotesModes) as extension (extension.id)}
55
+ <extension.component {context} />
56
+ {/each}
57
+ </div>
58
+ <div data-extension-zone="shared-document-editor">
59
+ {#if context.permissions.edit}
60
+ {#each visible(extensions.sharedDocumentEditorModes) as extension (extension.id)}
61
+ <extension.component {context} />
62
+ {/each}
63
+ {/if}
64
+ </div>
65
+ </div>