serve-my-md 1.1.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 (49) hide show
  1. package/README.md +52 -0
  2. package/bin/index.js +487 -0
  3. package/index.html +70 -0
  4. package/package.json +111 -0
  5. package/shared/constants.json +3 -0
  6. package/shared/index.d.ts +34 -0
  7. package/web/.cta.json +12 -0
  8. package/web/.cursorrules +7 -0
  9. package/web/.prettierignore +3 -0
  10. package/web/.vscode/settings.json +11 -0
  11. package/web/README.md +489 -0
  12. package/web/components.json +21 -0
  13. package/web/eslint.config.js +5 -0
  14. package/web/index.html +66 -0
  15. package/web/prettier.config.js +10 -0
  16. package/web/public/og-image.png +0 -0
  17. package/web/src/.generated/output.json +1 -0
  18. package/web/src/.generated/paths.json +1 -0
  19. package/web/src/App.tsx +15 -0
  20. package/web/src/article.css +199 -0
  21. package/web/src/components/Bettercrumb.tsx +86 -0
  22. package/web/src/components/Fonts.tsx +13 -0
  23. package/web/src/components/Header.tsx +10 -0
  24. package/web/src/components/IntentLink.tsx +20 -0
  25. package/web/src/components/Rendrer.tsx +140 -0
  26. package/web/src/components/Search.tsx +275 -0
  27. package/web/src/components/Sidebar.tsx +89 -0
  28. package/web/src/components/ThemeSwitcher.tsx +46 -0
  29. package/web/src/components/ui/breadcrumb.tsx +122 -0
  30. package/web/src/components/ui/button.tsx +60 -0
  31. package/web/src/components/ui/collapsible.tsx +33 -0
  32. package/web/src/components/ui/dropdown-menu.tsx +255 -0
  33. package/web/src/components/ui/input.tsx +21 -0
  34. package/web/src/components/ui/kbd.tsx +28 -0
  35. package/web/src/components/ui/separator.tsx +26 -0
  36. package/web/src/components/ui/sheet.tsx +139 -0
  37. package/web/src/components/ui/sidebar.tsx +727 -0
  38. package/web/src/components/ui/skeleton.tsx +13 -0
  39. package/web/src/components/ui/tooltip.tsx +59 -0
  40. package/web/src/contexts.ts +10 -0
  41. package/web/src/hooks/useMobile.ts +19 -0
  42. package/web/src/lib/utils.tsx +89 -0
  43. package/web/src/main.tsx +100 -0
  44. package/web/src/reportWebVitals.ts +13 -0
  45. package/web/src/styles.css +196 -0
  46. package/web/src/types/index.ts +3 -0
  47. package/web/tsconfig.json +35 -0
  48. package/web/vite.config.ts +31 -0
  49. package/web/vitest.config.ts +16 -0
@@ -0,0 +1,140 @@
1
+ import { useNavigate } from '@tanstack/react-router';
2
+ import { useEffect, useRef } from 'react';
3
+ import { useHotkeys } from 'react-hotkeys-hook';
4
+
5
+ import Bettercrumb from './Bettercrumb';
6
+ import { Button } from './ui/button';
7
+ import IntentLink from './IntentLink';
8
+ import { Kbd } from './ui/kbd';
9
+ import { SidebarTrigger } from './ui/sidebar';
10
+ import { useIsMobile } from '@/hooks/useMobile';
11
+ import { markElements, slugify } from '@/lib/utils';
12
+ import Search from './Search';
13
+ import ThemeSwitch from './ThemeSwitcher';
14
+ import pathBrowser from 'path-browserify';
15
+ import out from '@/.generated/output.json' with { type: 'json' };
16
+
17
+ export default function Rendrer({
18
+ path,
19
+ content,
20
+ next,
21
+ prev,
22
+ title
23
+ }: {
24
+ path: string;
25
+ content: string;
26
+ next?: string;
27
+ prev?: string;
28
+ title: string;
29
+ }) {
30
+ const articleRef = useRef<HTMLDivElement>(null);
31
+ const navigate = useNavigate();
32
+ const isMobile = useIsMobile();
33
+
34
+ const prevRef = useRef<HTMLButtonElement>(null);
35
+ const nextRef = useRef<HTMLButtonElement>(null);
36
+
37
+ useHotkeys(
38
+ 'alt+shift+enter',
39
+ (e) => {
40
+ e.preventDefault();
41
+ if (prev && prevRef.current) prevRef.current.click();
42
+ },
43
+ [prev]
44
+ );
45
+
46
+ useHotkeys(
47
+ 'alt+enter',
48
+ (e) => {
49
+ e.preventDefault();
50
+ if (next && nextRef.current) nextRef.current.click();
51
+ },
52
+ [next]
53
+ );
54
+
55
+ useEffect(() => {
56
+ if (!articleRef.current) return;
57
+
58
+ const article = articleRef.current;
59
+ article.querySelectorAll('a').forEach((elem: HTMLAnchorElement) => {
60
+ elem.setAttribute(
61
+ 'href',
62
+ pathBrowser.join(out.baseRoute || '/', elem.getAttribute('href') || '')
63
+ );
64
+
65
+ elem.addEventListener('click', (e) => {
66
+ if (
67
+ elem.getAttribute('href')?.startsWith('http://') ||
68
+ elem.getAttribute('href')?.startsWith('https://')
69
+ )
70
+ return;
71
+ e.preventDefault();
72
+ navigate({ to: elem.getAttribute('href') || '/' });
73
+ });
74
+ });
75
+
76
+ markElements(article);
77
+ article.querySelectorAll('h1,h2,h3,h4').forEach((element) => {
78
+ element.id = slugify(element.textContent);
79
+
80
+ const a = document.createElement('a');
81
+ a.href = `#${element.id}`;
82
+ a.classList.add('heading-anchor');
83
+ a.innerHTML = element.innerHTML;
84
+ element.innerHTML = '';
85
+ element.appendChild(a);
86
+ });
87
+
88
+ // Prism.highlightAllUnder(article, true);
89
+ }, [articleRef.current]);
90
+
91
+ return (
92
+ <>
93
+ <main className="py-10 w-full">
94
+ <div className="flex items-center gap-2 justify-between">
95
+ <div className="flex items-center gap-2 justify-start">
96
+ {isMobile && <SidebarTrigger variant="outline" />}
97
+ <Bettercrumb path={path} />
98
+ </div>
99
+ <div className="flex items-center gap-2 justify-end">
100
+ <ThemeSwitch />
101
+ <Search />
102
+ </div>
103
+ </div>
104
+
105
+ <h1 className="text-3xl font-bold mt-4">{title}</h1>
106
+
107
+ <article
108
+ ref={articleRef}
109
+ className="main-article w-full mt-4"
110
+ dangerouslySetInnerHTML={{ __html: content }}
111
+ />
112
+
113
+ <div className="flex justify-between mt-10 w-full">
114
+ {prev ? (
115
+ <>
116
+ <Button variant="outline" asChild ref={prevRef}>
117
+ <IntentLink to={prev}>
118
+ Previous <Kbd>Alt + Shift + ⏎</Kbd>
119
+ </IntentLink>
120
+ </Button>
121
+ </>
122
+ ) : (
123
+ <span></span>
124
+ )}
125
+ {next ? (
126
+ <>
127
+ <Button variant="outline" asChild ref={nextRef}>
128
+ <IntentLink to={next}>
129
+ Next <Kbd>Alt + ⏎</Kbd>
130
+ </IntentLink>
131
+ </Button>
132
+ </>
133
+ ) : (
134
+ <span></span>
135
+ )}
136
+ </div>
137
+ </main>
138
+ </>
139
+ );
140
+ }
@@ -0,0 +1,275 @@
1
+ import ReactDOM from 'react-dom';
2
+ import { SearchIcon } from 'lucide-react';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { useHotkeys } from 'react-hotkeys-hook';
5
+
6
+ import { Button } from './ui/button';
7
+ import { Input } from './ui/input';
8
+ import output from '@/.generated/output.json' with { type: 'json' };
9
+
10
+ import type { Dispatch, SetStateAction } from 'react';
11
+ import {
12
+ cn,
13
+ extractText,
14
+ getTitleFromExtraction,
15
+ highlightSubstring
16
+ } from '@/lib/utils';
17
+ import { useRouterState } from '@tanstack/react-router';
18
+ import IntentLink from './IntentLink';
19
+
20
+ type SearchResult = {
21
+ path: string;
22
+ title: string;
23
+ matches: number;
24
+ text: string;
25
+ targetElement: HTMLElement;
26
+ };
27
+
28
+ type SearchResultSimple = {
29
+ text: string;
30
+ targetElement: HTMLElement;
31
+ };
32
+
33
+ type SearchResults = {
34
+ internal: SearchResultSimple[];
35
+ external: SearchResult[];
36
+ };
37
+
38
+ export default function Search() {
39
+ const [trig, trigger] = useState(false);
40
+ const [results, setResults] = useState<{
41
+ internal: SearchResultSimple[];
42
+ external: SearchResult[];
43
+ }>({
44
+ internal: [],
45
+ external: []
46
+ });
47
+ const pathname = useRouterState({ select: (s) => s.location.pathname });
48
+
49
+ const onChange = (val: string) => {
50
+ if (!val) {
51
+ setResults({ internal: [], external: [] });
52
+
53
+ return;
54
+ }
55
+
56
+ setResults(
57
+ output.routes.reduce(
58
+ (acc, route) => {
59
+ const extraction = extractText(route.content);
60
+
61
+ if (route.path === pathname) {
62
+ extraction.forEach(({ targetElement, text }) => {
63
+ if (text.toLowerCase().includes(val.toLowerCase())) {
64
+ acc.internal.push({
65
+ text,
66
+ targetElement
67
+ });
68
+ }
69
+ });
70
+
71
+ return acc;
72
+ }
73
+
74
+ extraction.every(({ targetElement, text }) => {
75
+ if (text.toLowerCase().includes(val.toLowerCase())) {
76
+ acc.external.push({
77
+ path: route.path,
78
+ matches: (text.match(new RegExp(val, 'gi')) || []).length,
79
+ text,
80
+ targetElement,
81
+ title: getTitleFromExtraction(extraction)
82
+ });
83
+
84
+ return false;
85
+ }
86
+
87
+ return true;
88
+ });
89
+
90
+ return acc;
91
+ },
92
+ { internal: [], external: [] } as typeof results
93
+ )
94
+ );
95
+ };
96
+
97
+ useEffect(() => {
98
+ setResults({ internal: [], external: [] });
99
+ }, [trig]);
100
+
101
+ useHotkeys('ctrl+shift+f', () => trigger((t) => !t));
102
+ useHotkeys('esc', () => trigger(false));
103
+
104
+ return (
105
+ <>
106
+ <Button
107
+ variant="outline"
108
+ size="icon"
109
+ title="Search"
110
+ onClick={() => trigger((t) => !t)}
111
+ aria-keyshortcuts="Ctrl+Shift+F"
112
+ >
113
+ <SearchIcon />
114
+ </Button>
115
+ <SearchInput
116
+ displayState={trig}
117
+ trigger={trigger}
118
+ onChange={onChange}
119
+ searchResults={results}
120
+ />
121
+ </>
122
+ );
123
+ }
124
+
125
+ interface SearchInputProps {
126
+ trigger: Dispatch<SetStateAction<boolean>>;
127
+ displayState: boolean;
128
+ query?: string;
129
+ onChange: (val: string) => void;
130
+ searchResults: SearchResults;
131
+ }
132
+
133
+ function SearchInput({
134
+ displayState,
135
+ trigger,
136
+ query,
137
+ onChange,
138
+ searchResults
139
+ }: SearchInputProps) {
140
+ const inputRef = useRef<HTMLInputElement>(null);
141
+ const [changed, setChanged] = useState(0);
142
+ const [value, setValue] = useState(query || '');
143
+
144
+ useEffect(() => {
145
+ setChanged((c) => c + 1);
146
+ }, [searchResults]);
147
+
148
+ useEffect(() => {
149
+ function remInput(e: MouseEvent) {
150
+ if (!inputRef.current) return;
151
+
152
+ const input = inputRef.current;
153
+ const rect = input.getBoundingClientRect();
154
+ if (
155
+ e.clientX < rect.x ||
156
+ e.clientY < rect.y ||
157
+ e.clientX > rect.x + rect.width ||
158
+ e.clientY > rect.y + rect.height
159
+ ) {
160
+ trigger(false);
161
+ }
162
+ }
163
+
164
+ if (displayState)
165
+ setTimeout(() => document.addEventListener('click', remInput), 100);
166
+
167
+ return () => {
168
+ if (displayState) document.removeEventListener('click', remInput);
169
+ };
170
+ }, [displayState]);
171
+
172
+ useEffect(() => {
173
+ setChanged(0);
174
+ setValue(query || '');
175
+ if (displayState && inputRef.current) {
176
+ inputRef.current.focus();
177
+ }
178
+ }, [displayState]);
179
+
180
+ if (!displayState) return null;
181
+
182
+ return ReactDOM.createPortal(
183
+ <div
184
+ className={cn(
185
+ 'fixed top-0 left-0 w-full h-screen z-50 transition-all',
186
+ searchResults.internal.length + searchResults.external.length ||
187
+ changed > 2
188
+ ? 'bg-background/50 backdrop-blur-sm'
189
+ : ''
190
+ )}
191
+ onClick={(e) => {
192
+ if (e.target === e.currentTarget) {
193
+ trigger(false);
194
+ }
195
+ }}
196
+ >
197
+ <div className="absolute top-4 left-1/2 -translate-x-1/2 w-1/2">
198
+ <Input
199
+ className="shadow-[0px_-25px_50px_-10px_black] backdrop-blur-md bg-background/80!"
200
+ placeholder="Search for a candy.."
201
+ ref={inputRef}
202
+ value={value}
203
+ onChange={(e) => {
204
+ setValue(e.currentTarget.value);
205
+ onChange(e.currentTarget.value);
206
+ }}
207
+ />
208
+
209
+ <SearchResults searchResults={searchResults} query={value} />
210
+ </div>
211
+ </div>,
212
+ document.body
213
+ );
214
+ }
215
+
216
+ interface SearchResultsProps {
217
+ searchResults: SearchResults;
218
+ query: string;
219
+ }
220
+
221
+ function SearchResults({ searchResults, query }: SearchResultsProps) {
222
+ if (!searchResults.internal.length && !searchResults.external.length) {
223
+ return <></>;
224
+ }
225
+
226
+ return (
227
+ <>
228
+ {searchResults.internal.length > 0 && (
229
+ <p className="px-2 text-sm text-muted-foreground">On this page</p>
230
+ )}
231
+ {searchResults.internal.map(
232
+ (result, i) =>
233
+ !result.targetElement.classList.contains('template') && (
234
+ <div
235
+ key={i}
236
+ onClick={() => {
237
+ if (result.targetElement.getAttribute('data-label')) {
238
+ const el = document.querySelector(
239
+ `[data-label="${result.targetElement.getAttribute('data-label')}"]`
240
+ ) as HTMLElement;
241
+ if (el) el.scrollIntoView({ behavior: 'smooth' });
242
+ } else if (result.targetElement.id) {
243
+ const el = document.getElementById(result.targetElement.id);
244
+ if (el) el.scrollIntoView({ behavior: 'smooth' });
245
+ }
246
+ }}
247
+ className="cursor-pointer p-2 rounded hover:bg-secondary"
248
+ >
249
+ <p>{highlightSubstring(result.text, query)}</p>
250
+ </div>
251
+ )
252
+ )}
253
+
254
+ <hr className="my-4" />
255
+
256
+ {searchResults.external.map((result, i) => (
257
+ <IntentLink
258
+ key={i}
259
+ to={result.path}
260
+ className="block p-3 rounded hover:bg-secondary border border-outline my-3"
261
+ >
262
+ <p className="text-sm text-muted-foreground">In {result.title}</p>
263
+ <div className="flex items-center gap-2">
264
+ <p className="block text-ellipsis w-full overflow-hidden text-nowrap">
265
+ {highlightSubstring(result.text, query)}
266
+ </p>
267
+ <span className="py-1 px-1.5 rounded bg-secondary text-xs float-right text-nowrap">
268
+ matches: {result.matches}
269
+ </span>
270
+ </div>
271
+ </IntentLink>
272
+ ))}
273
+ </>
274
+ );
275
+ }
@@ -0,0 +1,89 @@
1
+ import { useContext, useMemo } from 'react';
2
+ import { ChevronsUpDown } from 'lucide-react';
3
+ import type { JSX } from 'react';
4
+
5
+ import type { Out } from '@shared/index';
6
+ import { pathsContext } from '@/contexts';
7
+ import {
8
+ Sidebar as Sb,
9
+ SidebarContent,
10
+ SidebarGroup,
11
+ SidebarGroupContent,
12
+ SidebarHeader
13
+ } from '@/components/ui/sidebar';
14
+ import outJ from '@/.generated/output.json' with { type: 'json' };
15
+ import IntentLink from '@/components/IntentLink';
16
+ import {
17
+ Collapsible,
18
+ CollapsibleContent,
19
+ CollapsibleTrigger
20
+ } from '@/components/ui/collapsible';
21
+
22
+ const out = outJ as Out;
23
+
24
+ export default function Sidebar() {
25
+ const paths = useContext(pathsContext);
26
+
27
+ const nestedLinks = useMemo(() => {
28
+ const buildLinks = (
29
+ pairs: typeof paths,
30
+ prefix: string
31
+ ): Array<JSX.Element | null> => {
32
+ return pairs.map(({label, children, isGrouper, pathSegment}, i) => {
33
+ if (prefix !== '/' && !label) return null;
34
+
35
+ return children ? isGrouper ? (
36
+ <div key={i} title={label} className="pl-2 mt-2 mb-0.5">
37
+ <span className="text-sm text-muted-foreground pr-2">{label}</span>
38
+ <div className="flex flex-col gap-0 pl-1">
39
+ {buildLinks(children, prefix)}
40
+ </div>
41
+ </div>
42
+ ) : (
43
+ <Collapsible key={i} className="my-0.5">
44
+ <CollapsibleTrigger className="flex justify-between w-full font-body">
45
+ <IntentLink to={prefix + '/' + pathSegment} className="px-2 py-0.5">
46
+ {label || 'Home'}
47
+ </IntentLink>
48
+ <ChevronsUpDown className="w-4 text-muted-foreground" />
49
+ </CollapsibleTrigger>
50
+
51
+ <CollapsibleContent style={{ paddingLeft: '0.75em' }}>
52
+ {buildLinks(children, prefix + '/' + pathSegment)}
53
+ </CollapsibleContent>
54
+ </Collapsible>
55
+ ) : (
56
+ <IntentLink
57
+ key={i}
58
+ to={prefix + '/' + pathSegment}
59
+ className="block px-2 py-0.5 my-0.5 font-body"
60
+ >
61
+ {label || 'Home'}
62
+ </IntentLink>
63
+ );
64
+ });
65
+ };
66
+ return buildLinks(paths, out.baseRoute || "/");
67
+ }, []);
68
+
69
+ return (
70
+ <>
71
+ <Sb variant="inset">
72
+ <SidebarHeader>
73
+ {out.logo && (
74
+ <img src={out.logo} height="24px" alt={`${out.name} logo`} />
75
+ )}
76
+ {(out.showNameWithLogo || !out.logo) && out.name}
77
+ </SidebarHeader>
78
+
79
+ <SidebarContent>
80
+ <SidebarGroup>
81
+ <SidebarGroupContent>
82
+ {nestedLinks}
83
+ </SidebarGroupContent>
84
+ </SidebarGroup>
85
+ </SidebarContent>
86
+ </Sb>
87
+ </>
88
+ );
89
+ }
@@ -0,0 +1,46 @@
1
+ import { Moon, Sun } from "lucide-react";
2
+ import { useEffect } from "react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import { cn } from "@/lib/utils";
6
+ import output from "@/.generated/output.json" with { type: "json" };
7
+
8
+ export default function ThemeSwitch({ className }: { className?: string }) {
9
+ const switchTheme = () => {
10
+ if (document.body.classList.contains("dark")) {
11
+ document.body.classList.remove("dark");
12
+ localStorage.setItem("theme", "light");
13
+ } else {
14
+ document.body.classList.add("dark");
15
+ localStorage.setItem("theme", "dark");
16
+ }
17
+ };
18
+
19
+ useEffect(() => {
20
+ const initTheme =
21
+ localStorage.getItem("theme") ||
22
+ output.defaultTheme ||
23
+ (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
24
+
25
+ if (initTheme === "dark") {
26
+ document.body.classList.add("dark");
27
+ } else {
28
+ document.body.classList.remove("dark");
29
+ }
30
+ }, []);
31
+
32
+ return (
33
+ <>
34
+ <Button
35
+ variant="outline"
36
+ size="icon"
37
+ className={cn("relative", className)}
38
+ onClick={switchTheme}
39
+ title={"Switch Theme"}
40
+ >
41
+ <Sun className="scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0 dark:hover:scale-75 dark:hover:rotate-90" />
42
+ <Moon className="absolute scale-100 rotate-0 transition-all hover:scale-100 hover:rotate-0 dark:scale-0 dark:rotate-90" />
43
+ </Button>
44
+ </>
45
+ );
46
+ }
@@ -0,0 +1,122 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { ChevronRight, MoreHorizontal } from 'lucide-react';
4
+
5
+ import IntentLink from '../IntentLink';
6
+ import type { IntentLinkProps } from '../IntentLink';
7
+
8
+ import { cn } from '@/lib/utils';
9
+
10
+ function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
11
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
12
+ }
13
+
14
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
15
+ return (
16
+ <ol
17
+ data-slot="breadcrumb-list"
18
+ className={cn(
19
+ 'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-word sm:gap-2.5',
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
28
+ return (
29
+ <li
30
+ data-slot="breadcrumb-item"
31
+ className={cn('inline-flex items-center gap-1.5', className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function BreadcrumbLink({
38
+ asChild,
39
+ className,
40
+ ...props
41
+ }: IntentLinkProps & {
42
+ asChild?: boolean;
43
+ className?: string;
44
+ }) {
45
+ if (asChild) {
46
+ const slotProps = props as React.ComponentProps<typeof Slot>;
47
+ return (
48
+ <Slot
49
+ data-slot="breadcrumb-link"
50
+ className={cn('hover:text-foreground transition-colors', className)}
51
+ {...slotProps}
52
+ />
53
+ );
54
+ }
55
+
56
+ return (
57
+ <IntentLink
58
+ data-slot="breadcrumb-link"
59
+ className={cn('hover:text-foreground transition-colors', className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
66
+ return (
67
+ <span
68
+ data-slot="breadcrumb-page"
69
+ role="link"
70
+ aria-disabled="true"
71
+ aria-current="page"
72
+ className={cn('text-foreground font-normal', className)}
73
+ {...props}
74
+ />
75
+ );
76
+ }
77
+
78
+ function BreadcrumbSeparator({
79
+ children,
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<'li'>) {
83
+ return (
84
+ <li
85
+ data-slot="breadcrumb-separator"
86
+ role="presentation"
87
+ aria-hidden="true"
88
+ className={cn('[&>svg]:size-3.5', className)}
89
+ {...props}
90
+ >
91
+ {children ?? <ChevronRight />}
92
+ </li>
93
+ );
94
+ }
95
+
96
+ function BreadcrumbEllipsis({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<'span'>) {
100
+ return (
101
+ <span
102
+ data-slot="breadcrumb-ellipsis"
103
+ role="presentation"
104
+ aria-hidden="true"
105
+ className={cn('flex size-9 items-center justify-center', className)}
106
+ {...props}
107
+ >
108
+ <MoreHorizontal className="size-4" />
109
+ <span className="sr-only">More</span>
110
+ </span>
111
+ );
112
+ }
113
+
114
+ export {
115
+ Breadcrumb,
116
+ BreadcrumbList,
117
+ BreadcrumbItem,
118
+ BreadcrumbLink,
119
+ BreadcrumbPage,
120
+ BreadcrumbSeparator,
121
+ BreadcrumbEllipsis
122
+ };