boltdocs 2.2.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/bin/boltdocs.js +2 -2
- package/dist/base-ui/index.d.mts +4 -4
- package/dist/base-ui/index.d.ts +4 -4
- package/dist/base-ui/index.js +1 -1
- package/dist/base-ui/index.mjs +1 -1
- package/dist/{cache-CRAZ55X7.mjs → cache-P6WK424C.mjs} +1 -1
- package/dist/chunk-2DI3OGHV.mjs +1 -0
- package/dist/chunk-2Z5T6EAU.mjs +1 -0
- package/dist/chunk-64AJ5QLT.mjs +1 -0
- package/dist/chunk-DDX52BX4.mjs +1 -0
- package/dist/chunk-HRZDSFR5.mjs +1 -0
- package/dist/chunk-PPVDMDEL.mjs +1 -0
- package/dist/chunk-UBE4CKOA.mjs +1 -0
- package/dist/chunk-UWT4AJTH.mjs +73 -0
- package/dist/chunk-WWJ7WKDI.mjs +1 -0
- package/dist/chunk-Y4RRHPXC.mjs +1 -0
- package/dist/client/index.d.mts +15 -21
- package/dist/client/index.d.ts +15 -21
- package/dist/client/index.js +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/client/ssr.js +1 -1
- package/dist/client/ssr.mjs +1 -1
- package/dist/client/types.d.mts +1 -1
- package/dist/client/types.d.ts +1 -1
- package/dist/client/types.js +1 -1
- package/dist/{copy-markdown-CbS8X-qe.d.mts → copy-markdown--9yjpbyy.d.mts} +1 -1
- package/dist/{copy-markdown-C-90ixSe.d.ts → copy-markdown-l2MYkcG7.d.ts} +1 -1
- package/dist/hooks/index.d.mts +8 -16
- package/dist/hooks/index.d.ts +8 -16
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/index.d.ts +1 -1
- package/dist/{loading-chS3pm9W.d.ts → loading-BwUos0wZ.d.mts} +5 -16
- package/dist/{loading-BqGrFWO5.d.mts → loading-nlnUD01v.d.ts} +5 -16
- package/dist/mdx/index.d.mts +4 -2
- package/dist/mdx/index.d.ts +4 -2
- package/dist/mdx/index.js +1 -1
- package/dist/mdx/index.mjs +1 -1
- package/dist/node/cli-entry.js +25 -22
- package/dist/node/cli-entry.mjs +5 -1
- package/dist/node/index.d.mts +0 -9
- package/dist/node/index.d.ts +0 -9
- package/dist/node/index.js +14 -15
- package/dist/node/index.mjs +1 -1
- package/dist/primitives/index.d.mts +13 -22
- package/dist/primitives/index.d.ts +13 -22
- package/dist/primitives/index.js +1 -1
- package/dist/primitives/index.mjs +1 -1
- package/dist/search-dialog-OONKKC5H.mjs +1 -0
- package/dist/{types-j7jvWsJj.d.ts → types-opDA2E9-.d.mts} +4 -11
- package/dist/{types-j7jvWsJj.d.mts → types-opDA2E9-.d.ts} +4 -11
- package/dist/{use-routes-Cd806kGw.d.ts → use-routes-DNwgTRpU.d.ts} +1 -1
- package/dist/{use-routes-DDL0_jkQ.d.mts → use-routes-DrT80Eom.d.mts} +1 -1
- package/package.json +2 -1
- package/src/client/app/index.tsx +20 -9
- package/src/client/app/mdx-components-context.tsx +2 -2
- package/src/client/app/mdx-page.tsx +0 -1
- package/src/client/app/scroll-handler.tsx +21 -10
- package/src/client/app/theme-context.tsx +14 -7
- package/src/client/components/default-layout.tsx +6 -4
- package/src/client/components/docs-layout.tsx +34 -4
- package/src/client/components/icons-dev.tsx +154 -0
- package/src/client/components/mdx/code-block.tsx +57 -5
- package/src/client/components/mdx/component-preview.tsx +1 -0
- package/src/client/components/mdx/file-tree.tsx +35 -0
- package/src/client/components/primitives/helpers/observer.ts +30 -39
- package/src/client/components/primitives/index.ts +1 -0
- package/src/client/components/primitives/menu.tsx +18 -12
- package/src/client/components/primitives/navbar.tsx +34 -93
- package/src/client/components/primitives/on-this-page.tsx +7 -161
- package/src/client/components/primitives/popover.tsx +1 -2
- package/src/client/components/primitives/search-dialog.tsx +4 -4
- package/src/client/components/primitives/sidebar.tsx +3 -2
- package/src/client/components/primitives/skeleton.tsx +26 -0
- package/src/client/components/ui-base/copy-markdown.tsx +4 -10
- package/src/client/components/ui-base/index.ts +0 -1
- package/src/client/components/ui-base/loading.tsx +43 -73
- package/src/client/components/ui-base/navbar.tsx +18 -15
- package/src/client/components/ui-base/page-nav.tsx +2 -1
- package/src/client/components/ui-base/powered-by.tsx +4 -1
- package/src/client/components/ui-base/search-dialog.tsx +16 -5
- package/src/client/components/ui-base/sidebar.tsx +4 -2
- package/src/client/hooks/use-i18n.ts +3 -2
- package/src/client/hooks/use-localized-to.ts +6 -5
- package/src/client/hooks/use-navbar.ts +37 -6
- package/src/client/hooks/use-page-nav.ts +27 -6
- package/src/client/hooks/use-routes.ts +2 -1
- package/src/client/hooks/use-search.ts +81 -59
- package/src/client/hooks/use-sidebar.ts +2 -1
- package/src/client/index.ts +0 -1
- package/src/client/store/use-boltdocs-store.ts +6 -5
- package/src/client/theme/neutral.css +31 -3
- package/src/client/types.ts +2 -2
- package/src/node/{cli.ts → cli/build.ts} +17 -23
- package/src/node/cli/dev.ts +22 -0
- package/src/node/cli/doctor.ts +243 -0
- package/src/node/cli/index.ts +9 -0
- package/src/node/cli/ui.ts +54 -0
- package/src/node/cli-entry.ts +16 -16
- package/src/node/config.ts +1 -15
- package/src/node/mdx/cache.ts +1 -1
- package/src/node/mdx/index.ts +2 -0
- package/src/node/mdx/rehype-shiki.ts +9 -0
- package/src/node/mdx/remark-code-meta.ts +35 -0
- package/src/node/mdx/remark-shiki.ts +1 -1
- package/src/node/plugin/entry.ts +22 -15
- package/src/node/plugin/index.ts +46 -14
- package/src/node/routes/parser.ts +12 -9
- package/src/node/search/index.ts +55 -0
- package/src/node/ssg/index.ts +83 -15
- package/src/node/ssg/robots.ts +7 -4
- package/dist/chunk-5D6XPYQ3.mjs +0 -74
- package/dist/chunk-6QXCKZAT.mjs +0 -1
- package/dist/chunk-H4M6P3DM.mjs +0 -1
- package/dist/chunk-JXHNX2WN.mjs +0 -1
- package/dist/chunk-MZBG4N4W.mjs +0 -1
- package/dist/chunk-Q3MLYTIQ.mjs +0 -1
- package/dist/chunk-RSII2UPE.mjs +0 -1
- package/dist/chunk-ZK2266IZ.mjs +0 -1
- package/dist/chunk-ZRJ55GGF.mjs +0 -1
- package/dist/search-dialog-MA5AISC7.mjs +0 -1
- package/src/client/components/ui-base/progress-bar.tsx +0 -67
|
@@ -11,29 +11,40 @@ export function ScrollHandler() {
|
|
|
11
11
|
|
|
12
12
|
// biome-ignore lint/correctness/useExhaustiveDependencies: pathname is used as a trigger for scroll-to-top on navigation
|
|
13
13
|
useLayoutEffect(() => {
|
|
14
|
-
const container = document.querySelector('.boltdocs-content')
|
|
15
|
-
|
|
14
|
+
const container = document.querySelector('.boltdocs-content') || window
|
|
15
|
+
|
|
16
|
+
// Helper to get scroll top
|
|
17
|
+
const getScrollTop = () => {
|
|
18
|
+
if (container === window) return window.scrollY
|
|
19
|
+
return (container as HTMLElement).scrollTop
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Helper to scroll
|
|
23
|
+
const scrollTo = (top: number, behavior: ScrollBehavior = 'auto') => {
|
|
24
|
+
if (container === window) {
|
|
25
|
+
window.scrollTo({ top, behavior })
|
|
26
|
+
} else {
|
|
27
|
+
(container as HTMLElement).scrollTo({ top, behavior })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
if (hash) {
|
|
18
32
|
const id = hash.replace('#', '')
|
|
19
33
|
const element = document.getElementById(id)
|
|
20
34
|
if (element) {
|
|
21
35
|
const offset = 80
|
|
22
|
-
const
|
|
36
|
+
const containerTop = container === window ? 0 : (container as HTMLElement).getBoundingClientRect().top
|
|
23
37
|
const elementRect = element.getBoundingClientRect().top
|
|
24
|
-
const elementPosition = elementRect -
|
|
25
|
-
const offsetPosition = elementPosition - offset +
|
|
38
|
+
const elementPosition = elementRect - containerTop
|
|
39
|
+
const offsetPosition = elementPosition - offset + getScrollTop()
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
top: offsetPosition,
|
|
29
|
-
behavior: 'smooth',
|
|
30
|
-
})
|
|
41
|
+
scrollTo(offsetPosition, 'smooth')
|
|
31
42
|
return
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
// Scroll to top on navigation when no hash is specified
|
|
36
|
-
|
|
47
|
+
scrollTo(0)
|
|
37
48
|
}, [pathname, hash])
|
|
38
49
|
|
|
39
50
|
return null
|
|
@@ -19,12 +19,15 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
setMounted(true)
|
|
21
21
|
const stored = localStorage.getItem('boltdocs-theme') as Theme | null
|
|
22
|
-
const initialTheme =
|
|
23
|
-
|
|
22
|
+
const initialTheme =
|
|
23
|
+
stored === 'light' || stored === 'dark' || stored === 'system'
|
|
24
|
+
? stored
|
|
25
|
+
: 'system'
|
|
26
|
+
|
|
24
27
|
setThemeState(initialTheme)
|
|
25
28
|
|
|
26
29
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
27
|
-
|
|
30
|
+
|
|
28
31
|
const updateResolved = (currentTheme: Theme, isDark: boolean) => {
|
|
29
32
|
if (currentTheme === 'system') {
|
|
30
33
|
setResolvedTheme(isDark ? 'dark' : 'light')
|
|
@@ -37,10 +40,11 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
37
40
|
|
|
38
41
|
const handleChange = (e: MediaQueryListEvent) => {
|
|
39
42
|
// Re-read current theme state from some stable ref would be better, but we can capture it
|
|
40
|
-
// actually, the second useEffect will handle the source of truth,
|
|
43
|
+
// actually, the second useEffect will handle the source of truth,
|
|
41
44
|
// but this listener ensures 'system' updates instantly.
|
|
42
45
|
setResolvedTheme((prevResolved) => {
|
|
43
|
-
const currentTheme =
|
|
46
|
+
const currentTheme =
|
|
47
|
+
(localStorage.getItem('boltdocs-theme') as Theme) || 'system'
|
|
44
48
|
if (currentTheme === 'system') {
|
|
45
49
|
return e.matches ? 'dark' : 'light'
|
|
46
50
|
}
|
|
@@ -56,8 +60,11 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
56
60
|
useEffect(() => {
|
|
57
61
|
if (!mounted) return
|
|
58
62
|
|
|
59
|
-
const isSystemDark = window.matchMedia(
|
|
60
|
-
|
|
63
|
+
const isSystemDark = window.matchMedia(
|
|
64
|
+
'(prefers-color-scheme: dark)',
|
|
65
|
+
).matches
|
|
66
|
+
const nextResolved =
|
|
67
|
+
theme === 'system' ? (isSystemDark ? 'dark' : 'light') : theme
|
|
61
68
|
|
|
62
69
|
setResolvedTheme(nextResolved as 'light' | 'dark')
|
|
63
70
|
|
|
@@ -5,7 +5,6 @@ import { OnThisPage } from '@components/ui-base/on-this-page'
|
|
|
5
5
|
import { Head } from '@components/ui-base/head'
|
|
6
6
|
import { Breadcrumbs } from '@components/ui-base/breadcrumbs'
|
|
7
7
|
import { PageNav } from '@components/ui-base/page-nav'
|
|
8
|
-
import { ProgressBar } from '@components/ui-base/progress-bar'
|
|
9
8
|
import { ErrorBoundary } from '@components/ui-base/error-boundary'
|
|
10
9
|
import { CopyMarkdown } from '@components/ui-base/copy-markdown'
|
|
11
10
|
import { useRoutes } from '@client/hooks/use-routes'
|
|
@@ -40,10 +39,13 @@ export function DefaultLayout({ children }: LayoutProps) {
|
|
|
40
39
|
|
|
41
40
|
return (
|
|
42
41
|
<DocsLayout>
|
|
43
|
-
<ProgressBar />
|
|
44
42
|
<Head
|
|
45
|
-
siteTitle={
|
|
46
|
-
|
|
43
|
+
siteTitle={
|
|
44
|
+
getTranslated(config.theme?.title, currentLocale) || 'Boltdocs'
|
|
45
|
+
}
|
|
46
|
+
siteDescription={
|
|
47
|
+
getTranslated(config.theme?.description, currentLocale) || ''
|
|
48
|
+
}
|
|
47
49
|
routes={allRoutes}
|
|
48
50
|
/>
|
|
49
51
|
<Navbar />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type React from 'react'
|
|
2
2
|
import { cn } from '@client/utils/cn'
|
|
3
|
+
import { useLocation } from '../hooks'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Props shared by all layout slot components.
|
|
@@ -60,17 +61,37 @@ function Content({ children, className, style }: SlotProps) {
|
|
|
60
61
|
<main
|
|
61
62
|
className={cn(
|
|
62
63
|
'boltdocs-content flex-1 min-w-0 overflow-y-auto',
|
|
64
|
+
'contain-layout', // Optimization: isolate main content layout
|
|
63
65
|
className,
|
|
64
66
|
)}
|
|
65
67
|
style={style}
|
|
66
68
|
>
|
|
67
|
-
|
|
68
|
-
{children}
|
|
69
|
-
</div>
|
|
69
|
+
{children}
|
|
70
70
|
</main>
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* MDX Content wrapper with standard page padding and max-width logic.
|
|
76
|
+
*/
|
|
77
|
+
function ContentMdx({ children, className, style }: SlotProps) {
|
|
78
|
+
const { pathname } = useLocation()
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
'boltdocs-page mx-auto pt-4 pb-20 px-4 sm:px-8',
|
|
83
|
+
{
|
|
84
|
+
'max-w-content-max': pathname.includes('/docs/'),
|
|
85
|
+
},
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
style={style}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
/**
|
|
75
96
|
* Content header row (breadcrumbs + copy markdown).
|
|
76
97
|
*/
|
|
@@ -96,10 +117,19 @@ function ContentFooter({ children, className, style }: SlotProps) {
|
|
|
96
117
|
)
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
interface DocsLayoutComponent extends React.FC<SlotProps> {
|
|
121
|
+
Body: typeof Body
|
|
122
|
+
Content: typeof Content
|
|
123
|
+
ContentMdx: typeof ContentMdx
|
|
124
|
+
ContentHeader: typeof ContentHeader
|
|
125
|
+
ContentFooter: typeof ContentFooter
|
|
126
|
+
}
|
|
127
|
+
|
|
99
128
|
// Attach sub-components to the root
|
|
100
129
|
export const DocsLayout = Object.assign(DocsLayoutRoot, {
|
|
101
130
|
Body,
|
|
102
131
|
Content,
|
|
132
|
+
ContentMdx,
|
|
103
133
|
ContentHeader,
|
|
104
134
|
ContentFooter,
|
|
105
|
-
})
|
|
135
|
+
}) as DocsLayoutComponent
|
|
@@ -72,3 +72,157 @@ export const Bluesky = (props: WrapperProps) => (
|
|
|
72
72
|
<path d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026" />
|
|
73
73
|
</svg>
|
|
74
74
|
)
|
|
75
|
+
|
|
76
|
+
// Icons file
|
|
77
|
+
|
|
78
|
+
export const TypeScript = (props: WrapperProps) => (
|
|
79
|
+
<svg
|
|
80
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
81
|
+
fill="none"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
{...wrapperProps(props)}
|
|
84
|
+
>
|
|
85
|
+
<title>{'TypeScript'}</title>
|
|
86
|
+
<path
|
|
87
|
+
fill="#2563EB"
|
|
88
|
+
d="M3.234 9.093V7.318h8.363v1.775H8.479V17.5H6.352V9.093H3.234zm15.263 1.153c-.04-.4-.21-.712-.512-.934-.301-.222-.71-.333-1.228-.333-.351 0-.648.05-.89.149-.242.096-.427.23-.557.403a.969.969 0 0 0-.189.586.838.838 0 0 0 .115.477c.086.136.204.254.353.353.149.097.321.181.517.254.195.07.404.13.626.179l.915.219c.444.1.852.232 1.223.397.371.166.693.37.965.612.271.242.482.527.631.855.152.328.23.704.234 1.129-.004.623-.163 1.163-.478 1.62-.311.454-.762.807-1.352 1.06-.587.248-1.294.372-2.123.372-.822 0-1.538-.126-2.147-.378-.607-.252-1.081-.624-1.422-1.118-.338-.497-.516-1.112-.532-1.845h2.083c.023.342.12.627.293.855.176.226.41.397.701.513a2.8 2.8 0 0 0 1 .168c.364 0 .68-.053.949-.159a1.45 1.45 0 0 0 .631-.442c.15-.189.224-.406.224-.651a.846.846 0 0 0-.204-.577c-.132-.156-.328-.288-.586-.398a5.964 5.964 0 0 0-.94-.298l-1.109-.278c-.858-.21-1.536-.536-2.033-.98-.497-.444-.744-1.042-.74-1.795-.004-.616.16-1.155.491-1.615.335-.461.794-.82 1.377-1.08.584-.258 1.247-.387 1.99-.387.755 0 1.414.13 1.978.388.567.258 1.007.618 1.322 1.079.315.46.477.994.488 1.6h-2.064z"
|
|
89
|
+
/>
|
|
90
|
+
</svg>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
export const JavaScript = (props: WrapperProps) => (
|
|
94
|
+
<svg
|
|
95
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
+
fill="none"
|
|
97
|
+
viewBox="0 0 24 24"
|
|
98
|
+
{...wrapperProps(props)}
|
|
99
|
+
>
|
|
100
|
+
<title>{'JavaScript'}</title>
|
|
101
|
+
<path
|
|
102
|
+
fill="#F59E0B"
|
|
103
|
+
d="M8.383 7.318h2.127v7.1c0 .656-.147 1.226-.442 1.71a2.924 2.924 0 01-1.218 1.118c-.52.262-1.125.393-1.815.393-.613 0-1.17-.107-1.67-.323a2.67 2.67 0 01-1.183-.994c-.292-.448-.436-1.01-.433-1.686h2.143c.006.269.061.5.164.691.106.19.25.335.432.438.186.1.405.15.657.15.265 0 .488-.057.67-.17.186-.116.327-.285.423-.507.096-.222.145-.496.145-.82v-7.1zm9.43 2.928c-.04-.4-.21-.712-.511-.934-.302-.222-.711-.333-1.228-.333-.352 0-.648.05-.89.149-.242.096-.428.23-.557.403a.969.969 0 00-.19.586.838.838 0 00.115.477c.087.136.204.254.353.353.15.097.322.181.517.254.196.07.405.13.627.179l.915.219c.444.1.851.232 1.223.397.37.166.692.37.964.612s.482.527.631.855a2.7 2.7 0 01.234 1.129c-.003.623-.162 1.163-.477 1.62-.312.454-.763.807-1.353 1.06-.586.248-1.294.372-2.122.372-.822 0-1.538-.126-2.148-.378-.607-.252-1.08-.624-1.422-1.118-.338-.497-.515-1.112-.532-1.845h2.083c.023.342.121.627.293.855.176.226.41.397.702.513.295.112.628.168.999.168.364 0 .68-.053.95-.159.271-.106.482-.253.63-.442.15-.189.224-.406.224-.651a.846.846 0 00-.203-.577c-.133-.156-.329-.288-.587-.398a5.964 5.964 0 00-.94-.298l-1.108-.278c-.859-.21-1.537-.536-2.034-.98-.497-.444-.744-1.042-.74-1.795-.004-.616.16-1.155.492-1.615.334-.461.793-.82 1.377-1.08.583-.258 1.246-.387 1.989-.387.755 0 1.415.13 1.978.388.567.258 1.008.618 1.323 1.079.314.46.477.994.487 1.6h-2.063z"
|
|
104
|
+
></path>
|
|
105
|
+
</svg>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const Json = (props: WrapperProps) => (
|
|
109
|
+
<svg
|
|
110
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
111
|
+
fill="none"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
{...wrapperProps(props)}
|
|
114
|
+
>
|
|
115
|
+
<title>{'JSON'}</title>
|
|
116
|
+
<path
|
|
117
|
+
fill="#F59E0B"
|
|
118
|
+
d="M4.778 6.667A2.667 2.667 0 017.444 4a.889.889 0 010 1.778.889.889 0 00-.888.889v3.5c0 .701-.273 1.35-.73 1.833.457.483.73 1.132.73 1.832v3.501c0 .491.398.89.888.89a.889.889 0 010 1.777 2.667 2.667 0 01-2.666-2.667v-3.5a.889.889 0 00-.674-.863l-.43-.108a.889.889 0 010-1.724l.43-.108a.889.889 0 00.674-.862V6.667zm14.222 0A2.667 2.667 0 0016.333 4a.889.889 0 000 1.778c.491 0 .89.398.89.889v3.5c0 .701.272 1.35.729 1.833a2.664 2.664 0 00-.73 1.832v3.501a.889.889 0 01-.889.89.889.889 0 000 1.777A2.667 2.667 0 0019 17.333v-3.5c0-.408.278-.764.673-.863l.431-.108a.889.889 0 000-1.724l-.43-.108a.889.889 0 01-.674-.862V6.667z"
|
|
119
|
+
></path>
|
|
120
|
+
</svg>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
export const Css = (props: WrapperProps) => (
|
|
124
|
+
<svg
|
|
125
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
126
|
+
fill="none"
|
|
127
|
+
viewBox="0 0 24 24"
|
|
128
|
+
{...wrapperProps(props)}
|
|
129
|
+
>
|
|
130
|
+
<title>{'CSS'}</title>
|
|
131
|
+
<path
|
|
132
|
+
fill="#0EA5E9"
|
|
133
|
+
d="M4.778 6.667A2.667 2.667 0 017.444 4a.889.889 0 010 1.778.889.889 0 00-.888.889v3.5c0 .701-.273 1.35-.73 1.833.457.483.73 1.132.73 1.832v3.501c0 .491.398.89.888.89a.889.889 0 010 1.777 2.667 2.667 0 01-2.666-2.667v-3.5a.889.889 0 00-.674-.863l-.43-.108a.889.889 0 010-1.724l.43-.108a.889.889 0 00.674-.862V6.667zm14.222 0A2.667 2.667 0 0016.333 4a.889.889 0 000 1.778c.491 0 .89.398.89.889v3.5c0 .701.272 1.35.729 1.833a2.664 2.664 0 00-.73 1.832v3.501a.889.889 0 01-.889.89.889.889 0 000 1.777A2.667 2.667 0 0019 17.333v-3.5c0-.408.278-.764.673-.863l.431-.108a.889.889 0 000-1.724l-.43-.108a.889.889 0 01-.674-.862V6.667z"
|
|
134
|
+
></path>
|
|
135
|
+
</svg>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
export const BracketsOrange = (props: WrapperProps) => (
|
|
139
|
+
<svg
|
|
140
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
141
|
+
fill="none"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
{...wrapperProps(props)}
|
|
144
|
+
>
|
|
145
|
+
<title>{'HTML'}</title>
|
|
146
|
+
<path
|
|
147
|
+
fill="#EA580C"
|
|
148
|
+
d="M4.778 6.667A2.667 2.667 0 017.444 4a.889.889 0 010 1.778.889.889 0 00-.888.889v3.5c0 .701-.273 1.35-.73 1.833.457.483.73 1.132.73 1.832v3.501c0 .491.398.89.888.89a.889.889 0 010 1.777 2.667 2.667 0 01-2.666-2.667v-3.5a.889.889 0 00-.674-.863l-.43-.108a.889.889 0 010-1.724l.43-.108a.889.889 0 00.674-.862V6.667zm14.222 0A2.667 2.667 0 0016.333 4a.889.889 0 000 1.778c.491 0 .89.398.89.889v3.5c0 .701.272 1.35.729 1.833a2.664 2.664 0 00-.73 1.832v3.501a.889.889 0 01-.889.89.889.889 0 000 1.777A2.667 2.667 0 0019 17.333v-3.5c0-.408.278-.764.673-.863l.431-.108a.889.889 0 000-1.724l-.43-.108a.889.889 0 01-.674-.862V6.667z"
|
|
149
|
+
></path>
|
|
150
|
+
</svg>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
export default BracketsOrange
|
|
154
|
+
|
|
155
|
+
export const React = (props: WrapperProps) => (
|
|
156
|
+
<svg
|
|
157
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
158
|
+
fill="none"
|
|
159
|
+
viewBox="0 0 24 24"
|
|
160
|
+
{...wrapperProps(props)}
|
|
161
|
+
>
|
|
162
|
+
<title>{'React'}</title>
|
|
163
|
+
<path
|
|
164
|
+
fill="#0E8ADC"
|
|
165
|
+
d="M12 13.677a1.677 1.677 0 100-3.354 1.677 1.677 0 000 3.354z"
|
|
166
|
+
></path>
|
|
167
|
+
<path
|
|
168
|
+
stroke="#0E8ADC"
|
|
169
|
+
d="M12 15.436c4.97 0 9-1.538 9-3.436s-4.03-3.436-9-3.436S3 10.102 3 12s4.03 3.436 9 3.436z"
|
|
170
|
+
></path>
|
|
171
|
+
<path
|
|
172
|
+
stroke="#0E8ADC"
|
|
173
|
+
d="M9.024 13.718c2.485 4.305 5.832 7.025 7.476 6.076 1.644-.949.961-5.208-1.524-9.512C12.491 5.977 9.144 3.257 7.5 4.206c-1.644.949-.961 5.208 1.524 9.512z"
|
|
174
|
+
></path>
|
|
175
|
+
<path
|
|
176
|
+
stroke="#0E8ADC"
|
|
177
|
+
d="M9.024 10.282c-2.485 4.304-3.168 8.563-1.524 9.512 1.644.95 4.99-1.771 7.476-6.076 2.485-4.304 3.168-8.563 1.524-9.512-1.644-.95-4.99 1.771-7.476 6.076z"
|
|
178
|
+
></path>
|
|
179
|
+
</svg>
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
export const Markdown = (props: WrapperProps) => (
|
|
183
|
+
<svg
|
|
184
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
185
|
+
fill="none"
|
|
186
|
+
viewBox="0 0 24 24"
|
|
187
|
+
{...wrapperProps(props)}
|
|
188
|
+
>
|
|
189
|
+
<title>{'Markdown'}</title>
|
|
190
|
+
<path
|
|
191
|
+
fill="#60A5FA"
|
|
192
|
+
d="M3 15.714V8h2.323l2.322 2.836L9.968 8h2.322v7.714H9.968V11.29l-2.323 2.836-2.322-2.836v4.424H3zm14.516 0l-3.484-3.743h2.323V8h2.322v3.97H21l-3.484 3.744z"
|
|
193
|
+
></path>
|
|
194
|
+
</svg>
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
export const Shell = (props: WrapperProps) => (
|
|
198
|
+
<svg
|
|
199
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
200
|
+
fill="none"
|
|
201
|
+
viewBox="0 0 25 24"
|
|
202
|
+
{...wrapperProps(props)}
|
|
203
|
+
>
|
|
204
|
+
<title>{'Shell'}</title>
|
|
205
|
+
<path
|
|
206
|
+
stroke="#14B8A6"
|
|
207
|
+
strokeLinecap="round"
|
|
208
|
+
strokeLinejoin="round"
|
|
209
|
+
strokeWidth="2"
|
|
210
|
+
d="M4.336 17l6-6-6-6M12.336 19h8"
|
|
211
|
+
></path>
|
|
212
|
+
</svg>
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
export const Yaml = (props: WrapperProps) => (
|
|
216
|
+
<svg
|
|
217
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
218
|
+
fill="none"
|
|
219
|
+
viewBox="0 0 24 24"
|
|
220
|
+
{...wrapperProps(props)}
|
|
221
|
+
>
|
|
222
|
+
<title>{'YAML'}</title>
|
|
223
|
+
<path
|
|
224
|
+
fill="#A78BFA"
|
|
225
|
+
d="M6.533 5.864h2.755l2.654 5.011h.113l2.654-5.011h2.756l-4.245 7.522V17.5h-2.443v-4.114L6.533 5.864z"
|
|
226
|
+
></path>
|
|
227
|
+
</svg>
|
|
228
|
+
)
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import * as RAC from 'react-aria-components'
|
|
2
|
-
import { Copy, Check } from 'lucide-react'
|
|
2
|
+
import { Copy, Check, File } from 'lucide-react'
|
|
3
3
|
import { cn } from '@client/utils/cn'
|
|
4
4
|
import { useCodeBlock } from './hooks/use-code-block'
|
|
5
5
|
import { useConfig } from '@client/app/config-context'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CodeSandbox,
|
|
8
|
+
TypeScript,
|
|
9
|
+
JavaScript,
|
|
10
|
+
React as ReactIcon,
|
|
11
|
+
Json,
|
|
12
|
+
Css,
|
|
13
|
+
BracketsOrange,
|
|
14
|
+
Markdown,
|
|
15
|
+
Shell,
|
|
16
|
+
Yaml,
|
|
17
|
+
} from '@components/icons-dev'
|
|
7
18
|
import { Tooltip } from '@components/primitives/tooltip'
|
|
8
19
|
|
|
20
|
+
const langIconMap: Record<string, React.ComponentType<{ size?: number }>> = {
|
|
21
|
+
ts: TypeScript,
|
|
22
|
+
tsx: ReactIcon,
|
|
23
|
+
js: JavaScript,
|
|
24
|
+
jsx: ReactIcon,
|
|
25
|
+
json: Json,
|
|
26
|
+
css: Css,
|
|
27
|
+
html: BracketsOrange,
|
|
28
|
+
md: Markdown,
|
|
29
|
+
mdx: Markdown,
|
|
30
|
+
bash: Shell,
|
|
31
|
+
sh: Shell,
|
|
32
|
+
yaml: Yaml,
|
|
33
|
+
yml: Yaml,
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
export interface CodeBlockProps {
|
|
10
37
|
children?: React.ReactNode
|
|
11
38
|
className?: string
|
|
@@ -15,6 +42,8 @@ export interface CodeBlockProps {
|
|
|
15
42
|
title?: string
|
|
16
43
|
lang?: string
|
|
17
44
|
highlightedHtml?: string
|
|
45
|
+
'data-lang'?: string
|
|
46
|
+
plain?: boolean
|
|
18
47
|
[key: string]: any
|
|
19
48
|
}
|
|
20
49
|
|
|
@@ -25,11 +54,15 @@ export function CodeBlock(props: CodeBlockProps) {
|
|
|
25
54
|
hideSandbox = true,
|
|
26
55
|
hideCopy = false,
|
|
27
56
|
highlightedHtml,
|
|
57
|
+
title,
|
|
58
|
+
'data-lang': dataLang,
|
|
59
|
+
plain = false,
|
|
28
60
|
...rest
|
|
29
61
|
} = props
|
|
30
62
|
const config = useConfig()
|
|
31
63
|
const globalSandbox = config?.integrations?.sandbox
|
|
32
64
|
const isSandboxEnabled = !!globalSandbox?.enable && !hideSandbox
|
|
65
|
+
const lang = props.lang || dataLang || ''
|
|
33
66
|
const {
|
|
34
67
|
copied,
|
|
35
68
|
isExpanded,
|
|
@@ -41,13 +74,32 @@ export function CodeBlock(props: CodeBlockProps) {
|
|
|
41
74
|
shouldTruncate,
|
|
42
75
|
} = useCodeBlock(props)
|
|
43
76
|
|
|
77
|
+
const LangIcon = langIconMap[lang]
|
|
78
|
+
|
|
44
79
|
return (
|
|
45
80
|
<div
|
|
46
81
|
className={cn(
|
|
47
|
-
'group relative
|
|
48
|
-
|
|
82
|
+
'group relative overflow-hidden bg-(--color-code-bg)',
|
|
83
|
+
'contain-layout contain-paint', // Optimization: isolate code block rendering
|
|
84
|
+
{
|
|
85
|
+
'my-6 rounded-lg border border-border-subtle': !plain,
|
|
86
|
+
'[&>pre]:max-h-62.5 [&>pre]:overflow-hidden': shouldTruncate,
|
|
87
|
+
},
|
|
88
|
+
props.className,
|
|
49
89
|
)}
|
|
50
90
|
>
|
|
91
|
+
{/* Title Header */}
|
|
92
|
+
{title && (
|
|
93
|
+
<div className="flex items-center gap-2 border-b border-border-subtle bg-bg-surface/50 px-4 py-2 text-[13px] font-medium text-text-muted">
|
|
94
|
+
{LangIcon ? (
|
|
95
|
+
<LangIcon size={14} />
|
|
96
|
+
) : (
|
|
97
|
+
<File size={14} className="opacity-60" />
|
|
98
|
+
)}
|
|
99
|
+
<span>{title}</span>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
51
103
|
{/* Toolbar */}
|
|
52
104
|
<div className="absolute top-3 right-4 z-50 flex items-center gap-2 transition-all duration-300 opacity-0 group-hover:opacity-100">
|
|
53
105
|
{isSandboxEnabled && (
|
|
@@ -66,7 +118,7 @@ export function CodeBlock(props: CodeBlockProps) {
|
|
|
66
118
|
<RAC.Button
|
|
67
119
|
onPress={handleCopy}
|
|
68
120
|
className={cn(
|
|
69
|
-
'grid place-items-center
|
|
121
|
+
'grid place-items-center size-8 bg-transparent outline-none cursor-pointer transition-all duration-200 hover:scale-110 active:scale-95 [&>svg]:size-4 [&>svg]:stroke-2',
|
|
70
122
|
copied
|
|
71
123
|
? 'text-emerald-400'
|
|
72
124
|
: 'text-text-muted hover:text-text-main',
|
|
@@ -10,11 +10,39 @@ import {
|
|
|
10
10
|
} from 'lucide-react'
|
|
11
11
|
import { cn } from '@client/utils/cn'
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
TypeScript,
|
|
15
|
+
JavaScript,
|
|
16
|
+
React as ReactIcon,
|
|
17
|
+
Json,
|
|
18
|
+
Css,
|
|
19
|
+
BracketsOrange,
|
|
20
|
+
Markdown,
|
|
21
|
+
Shell,
|
|
22
|
+
Yaml,
|
|
23
|
+
} from '@components/icons-dev'
|
|
24
|
+
|
|
13
25
|
// --- Constants & Types ---
|
|
14
26
|
|
|
15
27
|
const ICON_SIZE = 16
|
|
16
28
|
const STROKE_WIDTH = 2
|
|
17
29
|
|
|
30
|
+
const FILE_EXTENSION_MAP: Record<string, React.ComponentType<{ size?: number }>> = {
|
|
31
|
+
ts: TypeScript,
|
|
32
|
+
tsx: ReactIcon,
|
|
33
|
+
js: JavaScript,
|
|
34
|
+
jsx: ReactIcon,
|
|
35
|
+
json: Json,
|
|
36
|
+
css: Css,
|
|
37
|
+
html: BracketsOrange,
|
|
38
|
+
md: Markdown,
|
|
39
|
+
mdx: Markdown,
|
|
40
|
+
bash: Shell,
|
|
41
|
+
sh: Shell,
|
|
42
|
+
yaml: Yaml,
|
|
43
|
+
yml: Yaml,
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
const FILE_REGEXES = {
|
|
19
47
|
CODE: /\.(ts|tsx|js|jsx|json|mjs|cjs|astro|vue|svelte)$/i,
|
|
20
48
|
TEXT: /\.(md|mdx|txt)$/i,
|
|
@@ -68,6 +96,13 @@ function getFileIcon(filename: string, isFolder: boolean) {
|
|
|
68
96
|
)
|
|
69
97
|
}
|
|
70
98
|
|
|
99
|
+
// Check for specialized language icons
|
|
100
|
+
const extension = name.split('.').pop() || ''
|
|
101
|
+
const LangIcon = FILE_EXTENSION_MAP[extension]
|
|
102
|
+
if (LangIcon) {
|
|
103
|
+
return <LangIcon size={ICON_SIZE} />
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
const fileIconClass = cn(
|
|
72
107
|
iconClass,
|
|
73
108
|
'text-text-dim group-hover:text-text-main',
|
|
@@ -14,51 +14,42 @@ export class Observer {
|
|
|
14
14
|
private callback(entries: IntersectionObserverEntry[]) {
|
|
15
15
|
if (entries.length === 0) return
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
item
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
17
|
+
// 1. Update internal state based on current intersection and position
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const item = this.items.find((i) => i.id === entry.target.id)
|
|
20
|
+
if (item) {
|
|
21
|
+
// item.active will track if the heading is currently "on or below" the trigger line
|
|
22
|
+
item.active = entry.isIntersecting
|
|
23
|
+
|
|
24
|
+
// item.fallback will track if the heading has scrolled "above" the trigger line
|
|
25
|
+
// RootMargin top is -100px, so trigger line is at 100px.
|
|
26
|
+
const activationLine = 100
|
|
27
|
+
item.fallback =
|
|
28
|
+
!entry.isIntersecting && entry.boundingClientRect.top < activationLine
|
|
30
29
|
}
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let min = Number.MAX_VALUE
|
|
39
|
-
let fallbackIdx = -1
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < this.items.length; i++) {
|
|
42
|
-
const element = document.getElementById(this.items[i].id)
|
|
43
|
-
if (!element) continue
|
|
44
|
-
|
|
45
|
-
const d = Math.abs(viewTop - element.getBoundingClientRect().top)
|
|
46
|
-
if (d < min) {
|
|
47
|
-
fallbackIdx = i
|
|
48
|
-
min = d
|
|
49
|
-
}
|
|
32
|
+
// 2. The active heading is the LAST one in document order that has scrolled past the line.
|
|
33
|
+
let highlightIdx = -1
|
|
34
|
+
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
35
|
+
if (this.items[i].fallback) {
|
|
36
|
+
highlightIdx = i
|
|
37
|
+
break
|
|
50
38
|
}
|
|
39
|
+
}
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
active: true,
|
|
56
|
-
fallback: true,
|
|
57
|
-
t: Date.now(),
|
|
58
|
-
}
|
|
59
|
-
}
|
|
41
|
+
// 3. Initial state: If no headings have passed the line yet, default to the first heading.
|
|
42
|
+
if (highlightIdx === -1 && this.items.length > 0) {
|
|
43
|
+
highlightIdx = 0
|
|
60
44
|
}
|
|
61
45
|
|
|
46
|
+
// 4. Map back to UI state
|
|
47
|
+
this.items = this.items.map((item, idx) => ({
|
|
48
|
+
...item,
|
|
49
|
+
active: idx === highlightIdx,
|
|
50
|
+
t: idx === highlightIdx ? Date.now() : item.t,
|
|
51
|
+
}))
|
|
52
|
+
|
|
62
53
|
this.onChange?.()
|
|
63
54
|
}
|
|
64
55
|
|