@gmickel/gno 0.7.0 → 0.8.2

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 (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -50
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/assets/screenshots/webui-ask-answer.png +0 -0
  5. package/assets/screenshots/webui-collections.png +0 -0
  6. package/assets/screenshots/webui-editor.png +0 -0
  7. package/assets/screenshots/webui-home.png +0 -0
  8. package/assets/skill/SKILL.md +12 -12
  9. package/assets/skill/cli-reference.md +59 -57
  10. package/assets/skill/examples.md +8 -7
  11. package/assets/skill/mcp-reference.md +8 -4
  12. package/package.json +32 -25
  13. package/src/app/constants.ts +43 -42
  14. package/src/cli/colors.ts +1 -1
  15. package/src/cli/commands/ask.ts +44 -43
  16. package/src/cli/commands/cleanup.ts +9 -8
  17. package/src/cli/commands/collection/add.ts +12 -12
  18. package/src/cli/commands/collection/index.ts +4 -4
  19. package/src/cli/commands/collection/list.ts +26 -25
  20. package/src/cli/commands/collection/remove.ts +10 -10
  21. package/src/cli/commands/collection/rename.ts +10 -10
  22. package/src/cli/commands/context/add.ts +1 -1
  23. package/src/cli/commands/context/check.ts +17 -17
  24. package/src/cli/commands/context/index.ts +4 -4
  25. package/src/cli/commands/context/list.ts +11 -11
  26. package/src/cli/commands/context/rm.ts +1 -1
  27. package/src/cli/commands/doctor.ts +86 -84
  28. package/src/cli/commands/embed.ts +30 -28
  29. package/src/cli/commands/get.ts +27 -26
  30. package/src/cli/commands/index-cmd.ts +9 -9
  31. package/src/cli/commands/index.ts +16 -16
  32. package/src/cli/commands/init.ts +13 -12
  33. package/src/cli/commands/ls.ts +20 -19
  34. package/src/cli/commands/mcp/config.ts +30 -28
  35. package/src/cli/commands/mcp/index.ts +4 -4
  36. package/src/cli/commands/mcp/install.ts +17 -17
  37. package/src/cli/commands/mcp/paths.ts +133 -133
  38. package/src/cli/commands/mcp/status.ts +21 -21
  39. package/src/cli/commands/mcp/uninstall.ts +13 -13
  40. package/src/cli/commands/mcp.ts +2 -2
  41. package/src/cli/commands/models/clear.ts +12 -11
  42. package/src/cli/commands/models/index.ts +5 -5
  43. package/src/cli/commands/models/list.ts +31 -30
  44. package/src/cli/commands/models/path.ts +1 -1
  45. package/src/cli/commands/models/pull.ts +19 -18
  46. package/src/cli/commands/models/use.ts +4 -4
  47. package/src/cli/commands/multi-get.ts +38 -36
  48. package/src/cli/commands/query.ts +21 -20
  49. package/src/cli/commands/ref-parser.ts +10 -10
  50. package/src/cli/commands/reset.ts +40 -39
  51. package/src/cli/commands/search.ts +14 -13
  52. package/src/cli/commands/serve.ts +4 -4
  53. package/src/cli/commands/shared.ts +11 -10
  54. package/src/cli/commands/skill/index.ts +5 -5
  55. package/src/cli/commands/skill/install.ts +18 -17
  56. package/src/cli/commands/skill/paths-cmd.ts +11 -10
  57. package/src/cli/commands/skill/paths.ts +23 -23
  58. package/src/cli/commands/skill/show.ts +13 -12
  59. package/src/cli/commands/skill/uninstall.ts +16 -15
  60. package/src/cli/commands/status.ts +25 -24
  61. package/src/cli/commands/update.ts +3 -3
  62. package/src/cli/commands/vsearch.ts +17 -16
  63. package/src/cli/context.ts +5 -5
  64. package/src/cli/errors.ts +3 -3
  65. package/src/cli/format/search-results.ts +37 -37
  66. package/src/cli/options.ts +43 -43
  67. package/src/cli/program.ts +455 -459
  68. package/src/cli/progress.ts +1 -1
  69. package/src/cli/run.ts +24 -23
  70. package/src/collection/add.ts +9 -8
  71. package/src/collection/index.ts +3 -3
  72. package/src/collection/remove.ts +7 -6
  73. package/src/collection/types.ts +6 -6
  74. package/src/config/defaults.ts +1 -1
  75. package/src/config/index.ts +5 -5
  76. package/src/config/loader.ts +19 -18
  77. package/src/config/paths.ts +9 -8
  78. package/src/config/saver.ts +14 -13
  79. package/src/config/types.ts +53 -52
  80. package/src/converters/adapters/markitdownTs/adapter.ts +21 -19
  81. package/src/converters/adapters/officeparser/adapter.ts +18 -16
  82. package/src/converters/canonicalize.ts +12 -12
  83. package/src/converters/errors.ts +26 -22
  84. package/src/converters/index.ts +8 -8
  85. package/src/converters/mime.ts +25 -25
  86. package/src/converters/native/markdown.ts +10 -9
  87. package/src/converters/native/plaintext.ts +8 -7
  88. package/src/converters/path.ts +2 -2
  89. package/src/converters/pipeline.ts +11 -10
  90. package/src/converters/registry.ts +8 -8
  91. package/src/converters/types.ts +14 -14
  92. package/src/converters/versions.ts +4 -4
  93. package/src/index.ts +4 -4
  94. package/src/ingestion/chunker.ts +10 -9
  95. package/src/ingestion/index.ts +6 -6
  96. package/src/ingestion/language.ts +62 -62
  97. package/src/ingestion/sync.ts +50 -49
  98. package/src/ingestion/types.ts +10 -10
  99. package/src/ingestion/walker.ts +14 -13
  100. package/src/llm/cache.ts +51 -49
  101. package/src/llm/errors.ts +40 -36
  102. package/src/llm/index.ts +9 -9
  103. package/src/llm/lockfile.ts +6 -6
  104. package/src/llm/nodeLlamaCpp/adapter.ts +13 -12
  105. package/src/llm/nodeLlamaCpp/embedding.ts +9 -8
  106. package/src/llm/nodeLlamaCpp/generation.ts +7 -6
  107. package/src/llm/nodeLlamaCpp/lifecycle.ts +11 -10
  108. package/src/llm/nodeLlamaCpp/rerank.ts +6 -5
  109. package/src/llm/policy.ts +5 -5
  110. package/src/llm/registry.ts +6 -5
  111. package/src/llm/types.ts +2 -2
  112. package/src/mcp/resources/index.ts +15 -13
  113. package/src/mcp/server.ts +25 -23
  114. package/src/mcp/tools/get.ts +25 -23
  115. package/src/mcp/tools/index.ts +32 -29
  116. package/src/mcp/tools/multi-get.ts +34 -32
  117. package/src/mcp/tools/query.ts +29 -27
  118. package/src/mcp/tools/search.ts +14 -12
  119. package/src/mcp/tools/status.ts +12 -11
  120. package/src/mcp/tools/vsearch.ts +26 -24
  121. package/src/pipeline/answer.ts +9 -9
  122. package/src/pipeline/chunk-lookup.ts +1 -1
  123. package/src/pipeline/contextual.ts +4 -4
  124. package/src/pipeline/expansion.ts +23 -21
  125. package/src/pipeline/explain.ts +21 -21
  126. package/src/pipeline/fusion.ts +9 -9
  127. package/src/pipeline/hybrid.ts +41 -42
  128. package/src/pipeline/index.ts +10 -10
  129. package/src/pipeline/query-language.ts +39 -39
  130. package/src/pipeline/rerank.ts +8 -7
  131. package/src/pipeline/search.ts +22 -22
  132. package/src/pipeline/types.ts +8 -8
  133. package/src/pipeline/vsearch.ts +21 -24
  134. package/src/serve/CLAUDE.md +21 -15
  135. package/src/serve/config-sync.ts +9 -8
  136. package/src/serve/context.ts +19 -18
  137. package/src/serve/index.ts +1 -1
  138. package/src/serve/jobs.ts +7 -7
  139. package/src/serve/public/app.tsx +79 -25
  140. package/src/serve/public/components/AddCollectionDialog.tsx +382 -0
  141. package/src/serve/public/components/CaptureButton.tsx +60 -0
  142. package/src/serve/public/components/CaptureModal.tsx +365 -0
  143. package/src/serve/public/components/IndexingProgress.tsx +333 -0
  144. package/src/serve/public/components/ShortcutHelpModal.tsx +106 -0
  145. package/src/serve/public/components/ai-elements/code-block.tsx +42 -32
  146. package/src/serve/public/components/ai-elements/conversation.tsx +16 -14
  147. package/src/serve/public/components/ai-elements/inline-citation.tsx +33 -32
  148. package/src/serve/public/components/ai-elements/loader.tsx +5 -4
  149. package/src/serve/public/components/ai-elements/message.tsx +39 -37
  150. package/src/serve/public/components/ai-elements/prompt-input.tsx +97 -95
  151. package/src/serve/public/components/ai-elements/sources.tsx +12 -10
  152. package/src/serve/public/components/ai-elements/suggestion.tsx +10 -9
  153. package/src/serve/public/components/editor/CodeMirrorEditor.tsx +142 -0
  154. package/src/serve/public/components/editor/MarkdownPreview.tsx +311 -0
  155. package/src/serve/public/components/editor/index.ts +6 -0
  156. package/src/serve/public/components/preset-selector.tsx +29 -28
  157. package/src/serve/public/components/ui/badge.tsx +13 -12
  158. package/src/serve/public/components/ui/button-group.tsx +13 -12
  159. package/src/serve/public/components/ui/button.tsx +23 -22
  160. package/src/serve/public/components/ui/card.tsx +16 -16
  161. package/src/serve/public/components/ui/carousel.tsx +36 -35
  162. package/src/serve/public/components/ui/collapsible.tsx +1 -1
  163. package/src/serve/public/components/ui/command.tsx +17 -15
  164. package/src/serve/public/components/ui/dialog.tsx +13 -12
  165. package/src/serve/public/components/ui/dropdown-menu.tsx +13 -12
  166. package/src/serve/public/components/ui/hover-card.tsx +6 -5
  167. package/src/serve/public/components/ui/input-group.tsx +45 -43
  168. package/src/serve/public/components/ui/input.tsx +6 -6
  169. package/src/serve/public/components/ui/progress.tsx +5 -4
  170. package/src/serve/public/components/ui/scroll-area.tsx +11 -10
  171. package/src/serve/public/components/ui/select.tsx +19 -18
  172. package/src/serve/public/components/ui/separator.tsx +6 -5
  173. package/src/serve/public/components/ui/table.tsx +18 -18
  174. package/src/serve/public/components/ui/textarea.tsx +4 -4
  175. package/src/serve/public/components/ui/tooltip.tsx +5 -4
  176. package/src/serve/public/globals.css +27 -4
  177. package/src/serve/public/hooks/use-api.ts +8 -8
  178. package/src/serve/public/hooks/useCaptureModal.tsx +83 -0
  179. package/src/serve/public/hooks/useKeyboardShortcuts.ts +85 -0
  180. package/src/serve/public/index.html +4 -4
  181. package/src/serve/public/lib/utils.ts +6 -0
  182. package/src/serve/public/pages/Ask.tsx +27 -26
  183. package/src/serve/public/pages/Browse.tsx +28 -27
  184. package/src/serve/public/pages/Collections.tsx +439 -0
  185. package/src/serve/public/pages/Dashboard.tsx +166 -40
  186. package/src/serve/public/pages/DocView.tsx +258 -73
  187. package/src/serve/public/pages/DocumentEditor.tsx +510 -0
  188. package/src/serve/public/pages/Search.tsx +80 -58
  189. package/src/serve/routes/api.ts +272 -155
  190. package/src/serve/security.ts +4 -4
  191. package/src/serve/server.ts +66 -48
  192. package/src/store/index.ts +5 -5
  193. package/src/store/migrations/001-initial.ts +24 -23
  194. package/src/store/migrations/002-documents-fts.ts +7 -6
  195. package/src/store/migrations/index.ts +4 -4
  196. package/src/store/migrations/runner.ts +17 -15
  197. package/src/store/sqlite/adapter.ts +123 -121
  198. package/src/store/sqlite/fts5-snowball.ts +24 -23
  199. package/src/store/sqlite/index.ts +1 -1
  200. package/src/store/sqlite/setup.ts +12 -12
  201. package/src/store/sqlite/types.ts +4 -4
  202. package/src/store/types.ts +19 -19
  203. package/src/store/vector/index.ts +3 -3
  204. package/src/store/vector/sqlite-vec.ts +23 -20
  205. package/src/store/vector/stats.ts +10 -8
  206. package/src/store/vector/types.ts +2 -2
  207. package/vendor/fts5-snowball/README.md +6 -6
  208. package/assets/screenshots/webui-ask-answer.jpg +0 -0
  209. package/assets/screenshots/webui-home.jpg +0 -0
@@ -1,17 +1,19 @@
1
- import { BookIcon, ChevronDownIcon } from 'lucide-react';
2
- import type { ComponentProps } from 'react';
3
- import { cn } from '../../lib/utils';
1
+ import type { ComponentProps } from "react";
2
+
3
+ import { BookIcon, ChevronDownIcon } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
4
6
  import {
5
7
  Collapsible,
6
8
  CollapsibleContent,
7
9
  CollapsibleTrigger,
8
- } from '../ui/collapsible';
10
+ } from "../ui/collapsible";
9
11
 
10
- export type SourcesProps = ComponentProps<'div'>;
12
+ export type SourcesProps = ComponentProps<typeof Collapsible>;
11
13
 
12
14
  export const Sources = ({ className, ...props }: SourcesProps) => (
13
15
  <Collapsible
14
- className={cn('not-prose mb-4 text-primary text-xs', className)}
16
+ className={cn("not-prose mb-4 text-primary text-xs", className)}
15
17
  {...props}
16
18
  />
17
19
  );
@@ -27,7 +29,7 @@ export const SourcesTrigger = ({
27
29
  ...props
28
30
  }: SourcesTriggerProps) => (
29
31
  <CollapsibleTrigger
30
- className={cn('flex items-center gap-2', className)}
32
+ className={cn("flex items-center gap-2", className)}
31
33
  {...props}
32
34
  >
33
35
  {children ?? (
@@ -47,15 +49,15 @@ export const SourcesContent = ({
47
49
  }: SourcesContentProps) => (
48
50
  <CollapsibleContent
49
51
  className={cn(
50
- 'mt-3 flex w-fit flex-col gap-2',
51
- 'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
52
+ "mt-3 flex w-fit flex-col gap-2",
53
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
52
54
  className
53
55
  )}
54
56
  {...props}
55
57
  />
56
58
  );
57
59
 
58
- export type SourceProps = ComponentProps<'a'>;
60
+ export type SourceProps = ComponentProps<"a">;
59
61
 
60
62
  export const Source = ({ href, title, children, ...props }: SourceProps) => (
61
63
  <a
@@ -1,7 +1,8 @@
1
- import type { ComponentProps } from 'react';
2
- import { cn } from '../../lib/utils';
3
- import { Button } from '../ui/button';
4
- import { ScrollArea, ScrollBar } from '../ui/scroll-area';
1
+ import type { ComponentProps } from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import { Button } from "../ui/button";
5
+ import { ScrollArea, ScrollBar } from "../ui/scroll-area";
5
6
 
6
7
  export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
7
8
 
@@ -11,14 +12,14 @@ export const Suggestions = ({
11
12
  ...props
12
13
  }: SuggestionsProps) => (
13
14
  <ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
14
- <div className={cn('flex w-max flex-nowrap items-center gap-2', className)}>
15
+ <div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>
15
16
  {children}
16
17
  </div>
17
18
  <ScrollBar className="hidden" orientation="horizontal" />
18
19
  </ScrollArea>
19
20
  );
20
21
 
21
- export type SuggestionProps = Omit<ComponentProps<typeof Button>, 'onClick'> & {
22
+ export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
22
23
  suggestion: string;
23
24
  onClick?: (suggestion: string) => void;
24
25
  };
@@ -27,8 +28,8 @@ export const Suggestion = ({
27
28
  suggestion,
28
29
  onClick,
29
30
  className,
30
- variant = 'outline',
31
- size = 'sm',
31
+ variant = "outline",
32
+ size = "sm",
32
33
  children,
33
34
  ...props
34
35
  }: SuggestionProps) => {
@@ -38,7 +39,7 @@ export const Suggestion = ({
38
39
 
39
40
  return (
40
41
  <Button
41
- className={cn('cursor-pointer rounded-full px-4', className)}
42
+ className={cn("cursor-pointer rounded-full px-4", className)}
42
43
  onClick={handleClick}
43
44
  size={size}
44
45
  type="button"
@@ -0,0 +1,142 @@
1
+ /**
2
+ * CodeMirror 6 editor wrapper component.
3
+ *
4
+ * Provides markdown editing with syntax highlighting and dark theme.
5
+ * Exposes imperative methods via ref: getValue, setValue, focus.
6
+ */
7
+
8
+ import { markdown } from "@codemirror/lang-markdown";
9
+ import { oneDark } from "@codemirror/theme-one-dark";
10
+ import { EditorView, basicSetup } from "codemirror";
11
+ import {
12
+ forwardRef,
13
+ useEffect,
14
+ useImperativeHandle,
15
+ useRef,
16
+ type ForwardedRef,
17
+ } from "react";
18
+
19
+ export interface CodeMirrorEditorProps {
20
+ /** Initial content to display */
21
+ initialContent: string;
22
+ /** Called when content changes */
23
+ onChange: (content: string) => void;
24
+ /** Additional CSS classes */
25
+ className?: string;
26
+ }
27
+
28
+ export interface CodeMirrorEditorRef {
29
+ /** Get current editor content */
30
+ getValue: () => string;
31
+ /** Set editor content programmatically */
32
+ setValue: (content: string) => void;
33
+ /** Focus the editor */
34
+ focus: () => void;
35
+ /** Wrap selected text with prefix and suffix */
36
+ wrapSelection: (prefix: string, suffix: string) => void;
37
+ /** Insert text at cursor position */
38
+ insertAtCursor: (text: string) => void;
39
+ }
40
+
41
+ function CodeMirrorEditorInner(
42
+ { initialContent, onChange, className }: CodeMirrorEditorProps,
43
+ ref: ForwardedRef<CodeMirrorEditorRef>
44
+ ) {
45
+ const containerRef = useRef<HTMLDivElement>(null);
46
+ const viewRef = useRef<EditorView | null>(null);
47
+ const onChangeRef = useRef(onChange);
48
+
49
+ // Keep onChange ref current to avoid recreating editor on callback change
50
+ useEffect(() => {
51
+ onChangeRef.current = onChange;
52
+ }, [onChange]);
53
+
54
+ // Initialize CodeMirror
55
+ useEffect(() => {
56
+ if (!containerRef.current) return;
57
+
58
+ const view = new EditorView({
59
+ doc: initialContent,
60
+ extensions: [
61
+ basicSetup,
62
+ markdown(),
63
+ oneDark,
64
+ EditorView.lineWrapping,
65
+ EditorView.updateListener.of((update) => {
66
+ if (update.docChanged) {
67
+ onChangeRef.current(update.state.doc.toString());
68
+ }
69
+ }),
70
+ // Dark theme base styling
71
+ EditorView.theme({
72
+ "&": {
73
+ height: "100%",
74
+ },
75
+ ".cm-scroller": {
76
+ fontFamily: "ui-monospace, monospace",
77
+ fontSize: "14px",
78
+ },
79
+ }),
80
+ ],
81
+ parent: containerRef.current,
82
+ });
83
+
84
+ viewRef.current = view;
85
+ return () => view.destroy();
86
+ // Only run on mount - initialContent should not trigger re-creation
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ }, []);
89
+
90
+ // Expose imperative methods
91
+ useImperativeHandle(ref, () => ({
92
+ getValue: () => {
93
+ return viewRef.current?.state.doc.toString() ?? "";
94
+ },
95
+ setValue: (content: string) => {
96
+ const view = viewRef.current;
97
+ if (view) {
98
+ view.dispatch({
99
+ changes: { from: 0, to: view.state.doc.length, insert: content },
100
+ });
101
+ }
102
+ },
103
+ focus: () => {
104
+ viewRef.current?.focus();
105
+ },
106
+ wrapSelection: (prefix: string, suffix: string) => {
107
+ const view = viewRef.current;
108
+ if (!view) return;
109
+
110
+ const { from, to } = view.state.selection.main;
111
+ const selectedText = view.state.doc.sliceString(from, to);
112
+
113
+ view.dispatch({
114
+ changes: {
115
+ from,
116
+ to,
117
+ insert: `${prefix}${selectedText}${suffix}`,
118
+ },
119
+ selection: {
120
+ anchor: from + prefix.length,
121
+ head: from + prefix.length + selectedText.length,
122
+ },
123
+ });
124
+ view.focus();
125
+ },
126
+ insertAtCursor: (text: string) => {
127
+ const view = viewRef.current;
128
+ if (!view) return;
129
+
130
+ const pos = view.state.selection.main.head;
131
+ view.dispatch({
132
+ changes: { from: pos, to: pos, insert: text },
133
+ selection: { anchor: pos + text.length },
134
+ });
135
+ view.focus();
136
+ },
137
+ }));
138
+
139
+ return <div ref={containerRef} className={className} />;
140
+ }
141
+
142
+ export const CodeMirrorEditor = forwardRef(CodeMirrorEditorInner);
@@ -0,0 +1,311 @@
1
+ /**
2
+ * MarkdownPreview - Renders markdown with syntax highlighting.
3
+ *
4
+ * Uses react-markdown with shiki syntax highlighting via CodeBlock.
5
+ * Matches the "Scholarly Dusk" design system.
6
+ */
7
+
8
+ import type { ComponentProps, FC, ReactNode } from "react";
9
+ import type { BundledLanguage } from "shiki";
10
+
11
+ import { ExternalLinkIcon } from "lucide-react";
12
+ import { memo } from "react";
13
+ import ReactMarkdown from "react-markdown";
14
+ import rehypeSanitize from "rehype-sanitize";
15
+
16
+ import { cn } from "../../lib/utils";
17
+ import { CodeBlock, CodeBlockCopyButton } from "../ai-elements/code-block";
18
+
19
+ export interface MarkdownPreviewProps {
20
+ /** Markdown content to render */
21
+ content: string;
22
+ /** Additional CSS classes */
23
+ className?: string;
24
+ }
25
+
26
+ // Inline code styling
27
+ const InlineCode: FC<ComponentProps<"code">> = ({
28
+ className,
29
+ children,
30
+ ...props
31
+ }) => (
32
+ <code
33
+ className={cn(
34
+ "rounded-md bg-muted px-1.5 py-0.5 font-mono text-[0.9em] text-primary",
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </code>
41
+ );
42
+
43
+ // Link handling - external links open in new tab
44
+ const Link: FC<ComponentProps<"a">> = ({
45
+ href,
46
+ children,
47
+ className,
48
+ ...props
49
+ }) => {
50
+ const isExternal = href?.startsWith("http");
51
+ return (
52
+ <a
53
+ className={cn(
54
+ "text-primary underline decoration-primary/40 underline-offset-2 transition-colors hover:decoration-primary",
55
+ "inline-flex items-center gap-0.5",
56
+ className
57
+ )}
58
+ href={href}
59
+ rel={isExternal ? "noopener noreferrer" : undefined}
60
+ target={isExternal ? "_blank" : undefined}
61
+ {...props}
62
+ >
63
+ {children}
64
+ {isExternal && <ExternalLinkIcon className="inline size-3 opacity-60" />}
65
+ </a>
66
+ );
67
+ };
68
+
69
+ // Heading styles with proper hierarchy
70
+ const createHeading =
71
+ (level: 1 | 2 | 3 | 4 | 5 | 6): FC<{ children?: ReactNode }> =>
72
+ ({ children }) => {
73
+ const Tag = `h${level}` as const;
74
+ const sizes = {
75
+ 1: "text-3xl mt-8 mb-4 pb-2 border-b border-border/50",
76
+ 2: "text-2xl mt-6 mb-3 pb-1.5 border-b border-border/30",
77
+ 3: "text-xl mt-5 mb-2",
78
+ 4: "text-lg mt-4 mb-2",
79
+ 5: "text-base mt-3 mb-1 font-semibold",
80
+ 6: "text-sm mt-3 mb-1 font-semibold text-muted-foreground",
81
+ };
82
+ return (
83
+ <Tag className={cn("font-serif tracking-tight", sizes[level])}>
84
+ {children}
85
+ </Tag>
86
+ );
87
+ };
88
+
89
+ // Code block with syntax highlighting
90
+ const Pre: FC<ComponentProps<"pre">> = ({ children, ...props }) => {
91
+ // Extract code element from children
92
+ const codeElement = children as React.ReactElement<{
93
+ className?: string;
94
+ children?: string;
95
+ }>;
96
+
97
+ if (!codeElement?.props) {
98
+ return <pre {...props}>{children}</pre>;
99
+ }
100
+
101
+ const className = codeElement.props.className ?? "";
102
+ const code = String(codeElement.props.children ?? "").trim();
103
+
104
+ // Extract language from className (e.g., "language-typescript")
105
+ const match = /language-(\w+)/.exec(className);
106
+ const language = (match?.[1] ?? "plaintext") as BundledLanguage;
107
+
108
+ return (
109
+ <div className="group/code my-4">
110
+ <CodeBlock
111
+ className="rounded-lg border border-border/60 bg-muted/30"
112
+ code={code}
113
+ language={language}
114
+ >
115
+ <CodeBlockCopyButton
116
+ className="opacity-0 transition-opacity group-hover/code:opacity-100"
117
+ size="icon-sm"
118
+ variant="ghost"
119
+ />
120
+ </CodeBlock>
121
+ </div>
122
+ );
123
+ };
124
+
125
+ // Blockquote with refined styling
126
+ const Blockquote: FC<ComponentProps<"blockquote">> = ({
127
+ children,
128
+ className,
129
+ ...props
130
+ }) => (
131
+ <blockquote
132
+ className={cn(
133
+ "my-4 border-l-2 border-secondary/60 bg-secondary/5 py-2 pr-4 pl-4 italic text-muted-foreground",
134
+ "[&>p]:mb-0",
135
+ className
136
+ )}
137
+ {...props}
138
+ >
139
+ {children}
140
+ </blockquote>
141
+ );
142
+
143
+ // List styles
144
+ const UnorderedList: FC<ComponentProps<"ul">> = ({
145
+ children,
146
+ className,
147
+ ...props
148
+ }) => (
149
+ <ul
150
+ className={cn("my-3 ml-6 list-disc space-y-1 [&>li]:pl-1", className)}
151
+ {...props}
152
+ >
153
+ {children}
154
+ </ul>
155
+ );
156
+
157
+ const OrderedList: FC<ComponentProps<"ol">> = ({
158
+ children,
159
+ className,
160
+ ...props
161
+ }) => (
162
+ <ol
163
+ className={cn("my-3 ml-6 list-decimal space-y-1 [&>li]:pl-1", className)}
164
+ {...props}
165
+ >
166
+ {children}
167
+ </ol>
168
+ );
169
+
170
+ // Table styles
171
+ const Table: FC<ComponentProps<"table">> = ({
172
+ children,
173
+ className,
174
+ ...props
175
+ }) => (
176
+ <div className="my-4 overflow-x-auto rounded-lg border border-border/60">
177
+ <table
178
+ className={cn("w-full border-collapse text-sm", className)}
179
+ {...props}
180
+ >
181
+ {children}
182
+ </table>
183
+ </div>
184
+ );
185
+
186
+ const TableHead: FC<ComponentProps<"thead">> = ({
187
+ children,
188
+ className,
189
+ ...props
190
+ }) => (
191
+ <thead className={cn("bg-muted/50", className)} {...props}>
192
+ {children}
193
+ </thead>
194
+ );
195
+
196
+ const TableRow: FC<ComponentProps<"tr">> = ({
197
+ children,
198
+ className,
199
+ ...props
200
+ }) => (
201
+ <tr
202
+ className={cn("border-b border-border/40 last:border-0", className)}
203
+ {...props}
204
+ >
205
+ {children}
206
+ </tr>
207
+ );
208
+
209
+ const TableCell: FC<ComponentProps<"td">> = ({
210
+ children,
211
+ className,
212
+ ...props
213
+ }) => (
214
+ <td className={cn("px-4 py-2", className)} {...props}>
215
+ {children}
216
+ </td>
217
+ );
218
+
219
+ const TableHeaderCell: FC<ComponentProps<"th">> = ({
220
+ children,
221
+ className,
222
+ ...props
223
+ }) => (
224
+ <th className={cn("px-4 py-2 text-left font-semibold", className)} {...props}>
225
+ {children}
226
+ </th>
227
+ );
228
+
229
+ // Horizontal rule
230
+ const Hr: FC = () => <hr className="my-6 border-0 border-t border-border/50" />;
231
+
232
+ // Paragraph
233
+ const Paragraph: FC<ComponentProps<"p">> = ({
234
+ children,
235
+ className,
236
+ ...props
237
+ }) => (
238
+ <p className={cn("mb-4 leading-relaxed last:mb-0", className)} {...props}>
239
+ {children}
240
+ </p>
241
+ );
242
+
243
+ // Image with proper styling
244
+ const Image: FC<ComponentProps<"img">> = ({ alt, className, ...props }) => (
245
+ // Alt is already passed via props spread, explicit declaration for required attr
246
+ <img
247
+ alt={alt ?? ""}
248
+ className={cn(
249
+ "my-4 max-w-full rounded-lg border border-border/40",
250
+ className
251
+ )}
252
+ {...props}
253
+ />
254
+ );
255
+
256
+ // Component mapping for react-markdown
257
+ const components = {
258
+ h1: createHeading(1),
259
+ h2: createHeading(2),
260
+ h3: createHeading(3),
261
+ h4: createHeading(4),
262
+ h5: createHeading(5),
263
+ h6: createHeading(6),
264
+ p: Paragraph,
265
+ a: Link,
266
+ code: InlineCode,
267
+ pre: Pre,
268
+ blockquote: Blockquote,
269
+ ul: UnorderedList,
270
+ ol: OrderedList,
271
+ table: Table,
272
+ thead: TableHead,
273
+ tr: TableRow,
274
+ td: TableCell,
275
+ th: TableHeaderCell,
276
+ hr: Hr,
277
+ img: Image,
278
+ };
279
+
280
+ /**
281
+ * Renders markdown content with syntax highlighting and proper styling.
282
+ * Sanitizes HTML to prevent XSS attacks.
283
+ */
284
+ export const MarkdownPreview = memo(
285
+ ({ content, className }: MarkdownPreviewProps) => {
286
+ if (!content) {
287
+ return (
288
+ <div className={cn("text-muted-foreground italic", className)}>
289
+ No content to display
290
+ </div>
291
+ );
292
+ }
293
+
294
+ return (
295
+ <div
296
+ className={cn(
297
+ "prose prose-invert max-w-none",
298
+ "text-foreground/90",
299
+ "[&>*:first-child]:mt-0",
300
+ className
301
+ )}
302
+ >
303
+ <ReactMarkdown components={components} rehypePlugins={[rehypeSanitize]}>
304
+ {content}
305
+ </ReactMarkdown>
306
+ </div>
307
+ );
308
+ }
309
+ );
310
+
311
+ MarkdownPreview.displayName = "MarkdownPreview";
@@ -0,0 +1,6 @@
1
+ export {
2
+ CodeMirrorEditor,
3
+ type CodeMirrorEditorProps,
4
+ type CodeMirrorEditorRef,
5
+ } from "./CodeMirrorEditor";
6
+ export { MarkdownPreview, type MarkdownPreviewProps } from "./MarkdownPreview";