@gmickel/gno 0.3.5 → 0.5.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 (71) hide show
  1. package/README.md +74 -7
  2. package/package.json +30 -1
  3. package/src/cli/commands/ask.ts +12 -187
  4. package/src/cli/commands/embed.ts +10 -4
  5. package/src/cli/commands/models/pull.ts +9 -4
  6. package/src/cli/commands/serve.ts +19 -0
  7. package/src/cli/commands/vsearch.ts +5 -2
  8. package/src/cli/program.ts +28 -0
  9. package/src/config/types.ts +11 -6
  10. package/src/llm/registry.ts +3 -1
  11. package/src/mcp/tools/vsearch.ts +5 -2
  12. package/src/pipeline/answer.ts +224 -0
  13. package/src/pipeline/contextual.ts +57 -0
  14. package/src/pipeline/expansion.ts +49 -31
  15. package/src/pipeline/explain.ts +11 -3
  16. package/src/pipeline/fusion.ts +20 -9
  17. package/src/pipeline/hybrid.ts +57 -40
  18. package/src/pipeline/index.ts +7 -0
  19. package/src/pipeline/rerank.ts +55 -27
  20. package/src/pipeline/types.ts +0 -3
  21. package/src/pipeline/vsearch.ts +3 -2
  22. package/src/serve/CLAUDE.md +91 -0
  23. package/src/serve/bunfig.toml +2 -0
  24. package/src/serve/context.ts +181 -0
  25. package/src/serve/index.ts +7 -0
  26. package/src/serve/public/app.tsx +56 -0
  27. package/src/serve/public/components/ai-elements/code-block.tsx +176 -0
  28. package/src/serve/public/components/ai-elements/conversation.tsx +98 -0
  29. package/src/serve/public/components/ai-elements/inline-citation.tsx +285 -0
  30. package/src/serve/public/components/ai-elements/loader.tsx +96 -0
  31. package/src/serve/public/components/ai-elements/message.tsx +443 -0
  32. package/src/serve/public/components/ai-elements/prompt-input.tsx +1421 -0
  33. package/src/serve/public/components/ai-elements/sources.tsx +75 -0
  34. package/src/serve/public/components/ai-elements/suggestion.tsx +51 -0
  35. package/src/serve/public/components/preset-selector.tsx +403 -0
  36. package/src/serve/public/components/ui/badge.tsx +46 -0
  37. package/src/serve/public/components/ui/button-group.tsx +82 -0
  38. package/src/serve/public/components/ui/button.tsx +62 -0
  39. package/src/serve/public/components/ui/card.tsx +92 -0
  40. package/src/serve/public/components/ui/carousel.tsx +244 -0
  41. package/src/serve/public/components/ui/collapsible.tsx +31 -0
  42. package/src/serve/public/components/ui/command.tsx +181 -0
  43. package/src/serve/public/components/ui/dialog.tsx +141 -0
  44. package/src/serve/public/components/ui/dropdown-menu.tsx +255 -0
  45. package/src/serve/public/components/ui/hover-card.tsx +42 -0
  46. package/src/serve/public/components/ui/input-group.tsx +167 -0
  47. package/src/serve/public/components/ui/input.tsx +21 -0
  48. package/src/serve/public/components/ui/progress.tsx +28 -0
  49. package/src/serve/public/components/ui/scroll-area.tsx +56 -0
  50. package/src/serve/public/components/ui/select.tsx +188 -0
  51. package/src/serve/public/components/ui/separator.tsx +26 -0
  52. package/src/serve/public/components/ui/table.tsx +114 -0
  53. package/src/serve/public/components/ui/textarea.tsx +18 -0
  54. package/src/serve/public/components/ui/tooltip.tsx +59 -0
  55. package/src/serve/public/globals.css +226 -0
  56. package/src/serve/public/hooks/use-api.ts +112 -0
  57. package/src/serve/public/index.html +13 -0
  58. package/src/serve/public/pages/Ask.tsx +442 -0
  59. package/src/serve/public/pages/Browse.tsx +270 -0
  60. package/src/serve/public/pages/Dashboard.tsx +202 -0
  61. package/src/serve/public/pages/DocView.tsx +302 -0
  62. package/src/serve/public/pages/Search.tsx +335 -0
  63. package/src/serve/routes/api.ts +763 -0
  64. package/src/serve/server.ts +249 -0
  65. package/src/store/migrations/002-documents-fts.ts +40 -0
  66. package/src/store/migrations/index.ts +2 -1
  67. package/src/store/sqlite/adapter.ts +216 -33
  68. package/src/store/sqlite/fts5-snowball.ts +144 -0
  69. package/src/store/types.ts +33 -3
  70. package/src/store/vector/stats.ts +3 -0
  71. package/src/store/vector/types.ts +1 -0
@@ -0,0 +1,56 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import Ask from './pages/Ask';
4
+ import Browse from './pages/Browse';
5
+ import Dashboard from './pages/Dashboard';
6
+ import DocView from './pages/DocView';
7
+ import Search from './pages/Search';
8
+ import './globals.css';
9
+
10
+ type Route = '/' | '/search' | '/browse' | '/doc' | '/ask';
11
+ type Navigate = (to: string | number) => void;
12
+
13
+ const routes: Record<Route, React.ComponentType<{ navigate: Navigate }>> = {
14
+ '/': Dashboard,
15
+ '/search': Search,
16
+ '/browse': Browse,
17
+ '/doc': DocView,
18
+ '/ask': Ask,
19
+ };
20
+
21
+ function App() {
22
+ // Track full location (pathname + search) for proper query param handling
23
+ const [location, setLocation] = useState<string>(
24
+ window.location.pathname + window.location.search
25
+ );
26
+
27
+ useEffect(() => {
28
+ const handlePopState = () =>
29
+ setLocation(window.location.pathname + window.location.search);
30
+ window.addEventListener('popstate', handlePopState);
31
+ return () => window.removeEventListener('popstate', handlePopState);
32
+ }, []);
33
+
34
+ const navigate = (to: string | number) => {
35
+ if (typeof to === 'number') {
36
+ // Handle history.go(-1) style navigation
37
+ window.history.go(to);
38
+ return;
39
+ }
40
+ window.history.pushState({}, '', to);
41
+ setLocation(to);
42
+ };
43
+
44
+ // Extract base path for routing (ignore query params)
45
+ const basePath = location.split('?')[0] as Route;
46
+ const Page = routes[basePath] || Dashboard;
47
+
48
+ return <Page navigate={navigate} />;
49
+ }
50
+
51
+ const rootElement = document.getElementById('root');
52
+ if (!rootElement) {
53
+ throw new Error('Root element not found');
54
+ }
55
+ const root = createRoot(rootElement);
56
+ root.render(<App />);
@@ -0,0 +1,176 @@
1
+ import { CheckIcon, CopyIcon } from 'lucide-react';
2
+ import {
3
+ type ComponentProps,
4
+ createContext,
5
+ type HTMLAttributes,
6
+ useContext,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import { type BundledLanguage, codeToHtml, type ShikiTransformer } from 'shiki';
12
+ import { cn } from '../../lib/utils';
13
+ import { Button } from '../ui/button';
14
+
15
+ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
16
+ code: string;
17
+ language: BundledLanguage;
18
+ showLineNumbers?: boolean;
19
+ };
20
+
21
+ interface CodeBlockContextType {
22
+ code: string;
23
+ }
24
+
25
+ const CodeBlockContext = createContext<CodeBlockContextType>({
26
+ code: '',
27
+ });
28
+
29
+ const lineNumberTransformer: ShikiTransformer = {
30
+ name: 'line-numbers',
31
+ line(node, line) {
32
+ node.children.unshift({
33
+ type: 'element',
34
+ tagName: 'span',
35
+ properties: {
36
+ className: [
37
+ 'inline-block',
38
+ 'min-w-10',
39
+ 'mr-4',
40
+ 'text-right',
41
+ 'select-none',
42
+ 'text-muted-foreground',
43
+ ],
44
+ },
45
+ children: [{ type: 'text', value: String(line) }],
46
+ });
47
+ },
48
+ };
49
+
50
+ export async function highlightCode(
51
+ code: string,
52
+ language: BundledLanguage,
53
+ showLineNumbers = false
54
+ ) {
55
+ const transformers: ShikiTransformer[] = showLineNumbers
56
+ ? [lineNumberTransformer]
57
+ : [];
58
+
59
+ return await Promise.all([
60
+ codeToHtml(code, {
61
+ lang: language,
62
+ theme: 'one-light',
63
+ transformers,
64
+ }),
65
+ codeToHtml(code, {
66
+ lang: language,
67
+ theme: 'one-dark-pro',
68
+ transformers,
69
+ }),
70
+ ]);
71
+ }
72
+
73
+ export const CodeBlock = ({
74
+ code,
75
+ language,
76
+ showLineNumbers = false,
77
+ className,
78
+ children,
79
+ ...props
80
+ }: CodeBlockProps) => {
81
+ const [html, setHtml] = useState<string>('');
82
+ const [darkHtml, setDarkHtml] = useState<string>('');
83
+ const mounted = useRef(false);
84
+
85
+ useEffect(() => {
86
+ highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
87
+ if (!mounted.current) {
88
+ setHtml(light);
89
+ setDarkHtml(dark);
90
+ mounted.current = true;
91
+ }
92
+ });
93
+
94
+ return () => {
95
+ mounted.current = false;
96
+ };
97
+ }, [code, language, showLineNumbers]);
98
+
99
+ return (
100
+ <CodeBlockContext.Provider value={{ code }}>
101
+ <div
102
+ className={cn(
103
+ 'group relative w-full overflow-hidden rounded-md border bg-background text-foreground',
104
+ className
105
+ )}
106
+ {...props}
107
+ >
108
+ <div className="relative">
109
+ <div
110
+ className="overflow-auto dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
111
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
112
+ dangerouslySetInnerHTML={{ __html: html }}
113
+ />
114
+ <div
115
+ className="hidden overflow-auto dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
116
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
117
+ dangerouslySetInnerHTML={{ __html: darkHtml }}
118
+ />
119
+ {children && (
120
+ <div className="absolute top-2 right-2 flex items-center gap-2">
121
+ {children}
122
+ </div>
123
+ )}
124
+ </div>
125
+ </div>
126
+ </CodeBlockContext.Provider>
127
+ );
128
+ };
129
+
130
+ export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
131
+ onCopy?: () => void;
132
+ onError?: (error: Error) => void;
133
+ timeout?: number;
134
+ };
135
+
136
+ export const CodeBlockCopyButton = ({
137
+ onCopy,
138
+ onError,
139
+ timeout = 2000,
140
+ children,
141
+ className,
142
+ ...props
143
+ }: CodeBlockCopyButtonProps) => {
144
+ const [isCopied, setIsCopied] = useState(false);
145
+ const { code } = useContext(CodeBlockContext);
146
+
147
+ const copyToClipboard = async () => {
148
+ if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
149
+ onError?.(new Error('Clipboard API not available'));
150
+ return;
151
+ }
152
+
153
+ try {
154
+ await navigator.clipboard.writeText(code);
155
+ setIsCopied(true);
156
+ onCopy?.();
157
+ setTimeout(() => setIsCopied(false), timeout);
158
+ } catch (error) {
159
+ onError?.(error as Error);
160
+ }
161
+ };
162
+
163
+ const Icon = isCopied ? CheckIcon : CopyIcon;
164
+
165
+ return (
166
+ <Button
167
+ className={cn('shrink-0', className)}
168
+ onClick={copyToClipboard}
169
+ size="icon"
170
+ variant="ghost"
171
+ {...props}
172
+ >
173
+ {children ?? <Icon size={14} />}
174
+ </Button>
175
+ );
176
+ };
@@ -0,0 +1,98 @@
1
+ import { ArrowDownIcon } from 'lucide-react';
2
+ import type { ComponentProps } from 'react';
3
+ import { useCallback } from 'react';
4
+ import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
5
+ import { cn } from '../../lib/utils';
6
+ import { Button } from '../ui/button';
7
+
8
+ export type ConversationProps = ComponentProps<typeof StickToBottom>;
9
+
10
+ export const Conversation = ({ className, ...props }: ConversationProps) => (
11
+ <StickToBottom
12
+ className={cn('relative flex-1 overflow-y-hidden', className)}
13
+ initial="smooth"
14
+ resize="smooth"
15
+ role="log"
16
+ {...props}
17
+ />
18
+ );
19
+
20
+ export type ConversationContentProps = ComponentProps<
21
+ typeof StickToBottom.Content
22
+ >;
23
+
24
+ export const ConversationContent = ({
25
+ className,
26
+ ...props
27
+ }: ConversationContentProps) => (
28
+ <StickToBottom.Content
29
+ className={cn('flex flex-col gap-8 p-4', className)}
30
+ {...props}
31
+ />
32
+ );
33
+
34
+ export type ConversationEmptyStateProps = ComponentProps<'div'> & {
35
+ title?: string;
36
+ description?: string;
37
+ icon?: React.ReactNode;
38
+ };
39
+
40
+ export const ConversationEmptyState = ({
41
+ className,
42
+ title = 'No messages yet',
43
+ description = 'Start a conversation to see messages here',
44
+ icon,
45
+ children,
46
+ ...props
47
+ }: ConversationEmptyStateProps) => (
48
+ <div
49
+ className={cn(
50
+ 'flex size-full flex-col items-center justify-center gap-3 p-8 text-center',
51
+ className
52
+ )}
53
+ {...props}
54
+ >
55
+ {children ?? (
56
+ <>
57
+ {icon && <div className="text-muted-foreground">{icon}</div>}
58
+ <div className="space-y-1">
59
+ <h3 className="font-medium text-sm">{title}</h3>
60
+ {description && (
61
+ <p className="text-muted-foreground text-sm">{description}</p>
62
+ )}
63
+ </div>
64
+ </>
65
+ )}
66
+ </div>
67
+ );
68
+
69
+ export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
70
+
71
+ export const ConversationScrollButton = ({
72
+ className,
73
+ ...props
74
+ }: ConversationScrollButtonProps) => {
75
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
76
+
77
+ const handleScrollToBottom = useCallback(() => {
78
+ scrollToBottom();
79
+ }, [scrollToBottom]);
80
+
81
+ return (
82
+ !isAtBottom && (
83
+ <Button
84
+ className={cn(
85
+ 'absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full',
86
+ className
87
+ )}
88
+ onClick={handleScrollToBottom}
89
+ size="icon"
90
+ type="button"
91
+ variant="outline"
92
+ {...props}
93
+ >
94
+ <ArrowDownIcon className="size-4" />
95
+ </Button>
96
+ )
97
+ );
98
+ };
@@ -0,0 +1,285 @@
1
+ import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
2
+ import {
3
+ type ComponentProps,
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useState,
9
+ } from 'react';
10
+ import { cn } from '../../lib/utils';
11
+ import { Badge } from '../ui/badge';
12
+ import {
13
+ Carousel,
14
+ type CarouselApi,
15
+ CarouselContent,
16
+ CarouselItem,
17
+ } from '../ui/carousel';
18
+ import {
19
+ HoverCard,
20
+ HoverCardContent,
21
+ HoverCardTrigger,
22
+ } from '../ui/hover-card';
23
+
24
+ export type InlineCitationProps = ComponentProps<'span'>;
25
+
26
+ export const InlineCitation = ({
27
+ className,
28
+ ...props
29
+ }: InlineCitationProps) => (
30
+ <span
31
+ className={cn('group inline items-center gap-1', className)}
32
+ {...props}
33
+ />
34
+ );
35
+
36
+ export type InlineCitationTextProps = ComponentProps<'span'>;
37
+
38
+ export const InlineCitationText = ({
39
+ className,
40
+ ...props
41
+ }: InlineCitationTextProps) => (
42
+ <span
43
+ className={cn('transition-colors group-hover:bg-accent', className)}
44
+ {...props}
45
+ />
46
+ );
47
+
48
+ export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
49
+
50
+ export const InlineCitationCard = (props: InlineCitationCardProps) => (
51
+ <HoverCard closeDelay={0} openDelay={0} {...props} />
52
+ );
53
+
54
+ export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
55
+ sources: string[];
56
+ };
57
+
58
+ export const InlineCitationCardTrigger = ({
59
+ sources,
60
+ className,
61
+ ...props
62
+ }: InlineCitationCardTriggerProps) => (
63
+ <HoverCardTrigger asChild>
64
+ <Badge
65
+ className={cn('ml-1 rounded-full', className)}
66
+ variant="secondary"
67
+ {...props}
68
+ >
69
+ {sources[0] ? (
70
+ <>
71
+ {new URL(sources[0]).hostname}{' '}
72
+ {sources.length > 1 && `+${sources.length - 1}`}
73
+ </>
74
+ ) : (
75
+ 'unknown'
76
+ )}
77
+ </Badge>
78
+ </HoverCardTrigger>
79
+ );
80
+
81
+ export type InlineCitationCardBodyProps = ComponentProps<'div'>;
82
+
83
+ export const InlineCitationCardBody = ({
84
+ className,
85
+ ...props
86
+ }: InlineCitationCardBodyProps) => (
87
+ <HoverCardContent className={cn('relative w-80 p-0', className)} {...props} />
88
+ );
89
+
90
+ const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
91
+
92
+ const useCarouselApi = () => {
93
+ const context = useContext(CarouselApiContext);
94
+ return context;
95
+ };
96
+
97
+ export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
98
+
99
+ export const InlineCitationCarousel = ({
100
+ className,
101
+ children,
102
+ ...props
103
+ }: InlineCitationCarouselProps) => {
104
+ const [api, setApi] = useState<CarouselApi>();
105
+
106
+ return (
107
+ <CarouselApiContext.Provider value={api}>
108
+ <Carousel className={cn('w-full', className)} setApi={setApi} {...props}>
109
+ {children}
110
+ </Carousel>
111
+ </CarouselApiContext.Provider>
112
+ );
113
+ };
114
+
115
+ export type InlineCitationCarouselContentProps = ComponentProps<'div'>;
116
+
117
+ export const InlineCitationCarouselContent = (
118
+ props: InlineCitationCarouselContentProps
119
+ ) => <CarouselContent {...props} />;
120
+
121
+ export type InlineCitationCarouselItemProps = ComponentProps<'div'>;
122
+
123
+ export const InlineCitationCarouselItem = ({
124
+ className,
125
+ ...props
126
+ }: InlineCitationCarouselItemProps) => (
127
+ <CarouselItem
128
+ className={cn('w-full space-y-2 p-4 pl-8', className)}
129
+ {...props}
130
+ />
131
+ );
132
+
133
+ export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>;
134
+
135
+ export const InlineCitationCarouselHeader = ({
136
+ className,
137
+ ...props
138
+ }: InlineCitationCarouselHeaderProps) => (
139
+ <div
140
+ className={cn(
141
+ 'flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2',
142
+ className
143
+ )}
144
+ {...props}
145
+ />
146
+ );
147
+
148
+ export type InlineCitationCarouselIndexProps = ComponentProps<'div'>;
149
+
150
+ export const InlineCitationCarouselIndex = ({
151
+ children,
152
+ className,
153
+ ...props
154
+ }: InlineCitationCarouselIndexProps) => {
155
+ const api = useCarouselApi();
156
+ const [current, setCurrent] = useState(0);
157
+ const [count, setCount] = useState(0);
158
+
159
+ useEffect(() => {
160
+ if (!api) {
161
+ return;
162
+ }
163
+
164
+ setCount(api.scrollSnapList().length);
165
+ setCurrent(api.selectedScrollSnap() + 1);
166
+
167
+ api.on('select', () => {
168
+ setCurrent(api.selectedScrollSnap() + 1);
169
+ });
170
+ }, [api]);
171
+
172
+ return (
173
+ <div
174
+ className={cn(
175
+ 'flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs',
176
+ className
177
+ )}
178
+ {...props}
179
+ >
180
+ {children ?? `${current}/${count}`}
181
+ </div>
182
+ );
183
+ };
184
+
185
+ export type InlineCitationCarouselPrevProps = ComponentProps<'button'>;
186
+
187
+ export const InlineCitationCarouselPrev = ({
188
+ className,
189
+ ...props
190
+ }: InlineCitationCarouselPrevProps) => {
191
+ const api = useCarouselApi();
192
+
193
+ const handleClick = useCallback(() => {
194
+ if (api) {
195
+ api.scrollPrev();
196
+ }
197
+ }, [api]);
198
+
199
+ return (
200
+ <button
201
+ aria-label="Previous"
202
+ className={cn('shrink-0', className)}
203
+ onClick={handleClick}
204
+ type="button"
205
+ {...props}
206
+ >
207
+ <ArrowLeftIcon className="size-4 text-muted-foreground" />
208
+ </button>
209
+ );
210
+ };
211
+
212
+ export type InlineCitationCarouselNextProps = ComponentProps<'button'>;
213
+
214
+ export const InlineCitationCarouselNext = ({
215
+ className,
216
+ ...props
217
+ }: InlineCitationCarouselNextProps) => {
218
+ const api = useCarouselApi();
219
+
220
+ const handleClick = useCallback(() => {
221
+ if (api) {
222
+ api.scrollNext();
223
+ }
224
+ }, [api]);
225
+
226
+ return (
227
+ <button
228
+ aria-label="Next"
229
+ className={cn('shrink-0', className)}
230
+ onClick={handleClick}
231
+ type="button"
232
+ {...props}
233
+ >
234
+ <ArrowRightIcon className="size-4 text-muted-foreground" />
235
+ </button>
236
+ );
237
+ };
238
+
239
+ export type InlineCitationSourceProps = ComponentProps<'div'> & {
240
+ title?: string;
241
+ url?: string;
242
+ description?: string;
243
+ };
244
+
245
+ export const InlineCitationSource = ({
246
+ title,
247
+ url,
248
+ description,
249
+ className,
250
+ children,
251
+ ...props
252
+ }: InlineCitationSourceProps) => (
253
+ <div className={cn('space-y-1', className)} {...props}>
254
+ {title && (
255
+ <h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
256
+ )}
257
+ {url && (
258
+ <p className="truncate break-all text-muted-foreground text-xs">{url}</p>
259
+ )}
260
+ {description && (
261
+ <p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
262
+ {description}
263
+ </p>
264
+ )}
265
+ {children}
266
+ </div>
267
+ );
268
+
269
+ export type InlineCitationQuoteProps = ComponentProps<'blockquote'>;
270
+
271
+ export const InlineCitationQuote = ({
272
+ children,
273
+ className,
274
+ ...props
275
+ }: InlineCitationQuoteProps) => (
276
+ <blockquote
277
+ className={cn(
278
+ 'border-muted border-l-2 pl-3 text-muted-foreground text-sm italic',
279
+ className
280
+ )}
281
+ {...props}
282
+ >
283
+ {children}
284
+ </blockquote>
285
+ );
@@ -0,0 +1,96 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ interface LoaderIconProps {
5
+ size?: number;
6
+ }
7
+
8
+ const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
9
+ <svg
10
+ height={size}
11
+ strokeLinejoin="round"
12
+ style={{ color: 'currentcolor' }}
13
+ viewBox="0 0 16 16"
14
+ width={size}
15
+ >
16
+ <title>Loader</title>
17
+ <g clipPath="url(#clip0_2393_1490)">
18
+ <path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
19
+ <path
20
+ d="M8 16V12"
21
+ opacity="0.5"
22
+ stroke="currentColor"
23
+ strokeWidth="1.5"
24
+ />
25
+ <path
26
+ d="M3.29773 1.52783L5.64887 4.7639"
27
+ opacity="0.9"
28
+ stroke="currentColor"
29
+ strokeWidth="1.5"
30
+ />
31
+ <path
32
+ d="M12.7023 1.52783L10.3511 4.7639"
33
+ opacity="0.1"
34
+ stroke="currentColor"
35
+ strokeWidth="1.5"
36
+ />
37
+ <path
38
+ d="M12.7023 14.472L10.3511 11.236"
39
+ opacity="0.4"
40
+ stroke="currentColor"
41
+ strokeWidth="1.5"
42
+ />
43
+ <path
44
+ d="M3.29773 14.472L5.64887 11.236"
45
+ opacity="0.6"
46
+ stroke="currentColor"
47
+ strokeWidth="1.5"
48
+ />
49
+ <path
50
+ d="M15.6085 5.52783L11.8043 6.7639"
51
+ opacity="0.2"
52
+ stroke="currentColor"
53
+ strokeWidth="1.5"
54
+ />
55
+ <path
56
+ d="M0.391602 10.472L4.19583 9.23598"
57
+ opacity="0.7"
58
+ stroke="currentColor"
59
+ strokeWidth="1.5"
60
+ />
61
+ <path
62
+ d="M15.6085 10.4722L11.8043 9.2361"
63
+ opacity="0.3"
64
+ stroke="currentColor"
65
+ strokeWidth="1.5"
66
+ />
67
+ <path
68
+ d="M0.391602 5.52783L4.19583 6.7639"
69
+ opacity="0.8"
70
+ stroke="currentColor"
71
+ strokeWidth="1.5"
72
+ />
73
+ </g>
74
+ <defs>
75
+ <clipPath id="clip0_2393_1490">
76
+ <rect fill="white" height="16" width="16" />
77
+ </clipPath>
78
+ </defs>
79
+ </svg>
80
+ );
81
+
82
+ export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
83
+ size?: number;
84
+ };
85
+
86
+ export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
87
+ <div
88
+ className={cn(
89
+ 'inline-flex animate-spin items-center justify-center',
90
+ className
91
+ )}
92
+ {...props}
93
+ >
94
+ <LoaderIcon size={size} />
95
+ </div>
96
+ );