@primer/doctocat-nextjs 0.8.1-rc.bc6f4fb → 0.9.0-rc.f3a7116
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 +8 -0
- package/components/layout/code-block/CodeBlock.tsx +0 -37
- package/components/layout/code-block/ReactCodeBlock.tsx +83 -82
- package/components/layout/nav-drawer/useNavDrawerState.ts +0 -1
- package/components/layout/root-layout/Theme.module.css +1 -2
- package/components/layout/root-layout/Theme.tsx +0 -2
- package/components/layout/sidebar/Sidebar.tsx +1 -2
- package/css/prose.module.css +1 -25
- package/eslint.config.js +18 -0
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @primer/doctocat-nextjs
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#87](https://github.com/primer/doctocat-nextjs/pull/87) [`7a28f1d`](https://github.com/primer/doctocat-nextjs/commit/7a28f1d707bcfd54b95d16d74e3bdea96eae252c) Thanks [@rezrah](https://github.com/rezrah)! - Upgrades Next.js and Nextra.
|
|
8
|
+
- **Next.js version >= v16.x is now required**. v15.x is no longer supported.
|
|
9
|
+
- `nextra` version is now `4.6.x`
|
|
10
|
+
|
|
3
11
|
## 0.8.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import React, {PropsWithChildren} from 'react'
|
|
3
3
|
import dynamic from 'next/dynamic'
|
|
4
|
-
import {renderToStaticMarkup} from 'react-dom/server'
|
|
5
4
|
|
|
6
5
|
import {Pre} from 'nextra/components'
|
|
7
6
|
import {Box, Stack, Text} from '@primer/react-brand'
|
|
@@ -34,42 +33,6 @@ type CodeBlockProps = {
|
|
|
34
33
|
|
|
35
34
|
export function CodeBlock(props: CodeBlockProps) {
|
|
36
35
|
if (['tsx', 'jsx'].includes(props['data-language'])) {
|
|
37
|
-
// Next.js v15.4+ will lazy render components on the server, which prevents us from
|
|
38
|
-
// sending usable React nodes to the ReactCodeBlock component.
|
|
39
|
-
// Workaround is to convert the code snippets to string on the client and pass to react-live.
|
|
40
|
-
|
|
41
|
-
// suppresses compilation warnings
|
|
42
|
-
if (typeof window !== 'undefined') {
|
|
43
|
-
try {
|
|
44
|
-
const childrenAsString = renderToStaticMarkup(<>{props.children}</>)
|
|
45
|
-
|
|
46
|
-
// Extract text content using browser's HTML parser (immune to regex bypass attacks)
|
|
47
|
-
const cleanHtmlTag = (str: string): string => {
|
|
48
|
-
const parser = new DOMParser()
|
|
49
|
-
const doc = parser.parseFromString(str, 'text/html')
|
|
50
|
-
return doc.body.textContent || doc.body.innerText || ''
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const textContent = cleanHtmlTag(childrenAsString)
|
|
54
|
-
|
|
55
|
-
// Restore escaped chars
|
|
56
|
-
const decodedText = textContent
|
|
57
|
-
.replace(/</g, '<')
|
|
58
|
-
.replace(/>/g, '>')
|
|
59
|
-
.replace(/"/g, '"')
|
|
60
|
-
.replace(/'/g, "'")
|
|
61
|
-
.replace(/&/g, '&')
|
|
62
|
-
|
|
63
|
-
return <ReactCodeBlock {...props} code={decodedText} />
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.log('Error extracting code snippet. Forwarding children directly:', error)
|
|
67
|
-
// Fallback to original children-based approach
|
|
68
|
-
return <ReactCodeBlock {...props} />
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// During SSR/build, use children-based approach
|
|
73
36
|
return <ReactCodeBlock {...props} />
|
|
74
37
|
}
|
|
75
38
|
|
|
@@ -18,7 +18,6 @@ const COLLAPSE_HEIGHT = 400 // TODO: Hoist this to config to make user customiza
|
|
|
18
18
|
type ReactCodeBlockProps = {
|
|
19
19
|
'data-language': string
|
|
20
20
|
'data-filename'?: string
|
|
21
|
-
code?: string
|
|
22
21
|
jsxScope: Record<string, unknown>
|
|
23
22
|
} & PropsWithChildren<HTMLElement>
|
|
24
23
|
|
|
@@ -35,8 +34,9 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
35
34
|
const uniqueId = useId()
|
|
36
35
|
const {colorMode, setColorMode} = useColorMode()
|
|
37
36
|
const {basePath} = useConfig()
|
|
38
|
-
const
|
|
39
|
-
const [code, setCode] = useState(
|
|
37
|
+
const codeSourceRef = useRef<HTMLDivElement>(null)
|
|
38
|
+
const [code, setCode] = useState<string>('')
|
|
39
|
+
const [isReady, setIsReady] = useState(false)
|
|
40
40
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
41
41
|
const [isCodePaneCollapsed, setIsCodePaneCollapsed] = useState<boolean | null>(null)
|
|
42
42
|
const [initialPosition, setInitialPosition] = useState<number | null>(null)
|
|
@@ -44,6 +44,16 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
44
44
|
const resetButtonRef = useRef<HTMLButtonElement>(null)
|
|
45
45
|
const shouldShowPreview = ['tsx', 'jsx'].includes(props['data-language'])
|
|
46
46
|
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (codeSourceRef.current && !isReady) {
|
|
49
|
+
const textContent = codeSourceRef.current.textContent || ''
|
|
50
|
+
if (textContent) {
|
|
51
|
+
setCode(textContent)
|
|
52
|
+
setIsReady(true)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, [isReady, props.children])
|
|
56
|
+
|
|
47
57
|
// scroll back to the initial y pos on collapse state change
|
|
48
58
|
useEffect(() => {
|
|
49
59
|
if (rootRef.current && initialPosition === null) {
|
|
@@ -71,8 +81,10 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
71
81
|
)
|
|
72
82
|
|
|
73
83
|
const handleReset = useCallback(() => {
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
if (codeSourceRef.current) {
|
|
85
|
+
setCode(codeSourceRef.current.textContent || '')
|
|
86
|
+
}
|
|
87
|
+
}, [])
|
|
76
88
|
|
|
77
89
|
const handleCopy = useCallback(() => {
|
|
78
90
|
navigator.clipboard.writeText(code)
|
|
@@ -132,88 +144,77 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
132
144
|
|
|
133
145
|
return (
|
|
134
146
|
<>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
{/* Hidden el to render children and extract text content */}
|
|
148
|
+
<div ref={codeSourceRef} style={{display: 'none'}} aria-hidden="true">
|
|
149
|
+
{props.children}
|
|
150
|
+
</div>
|
|
151
|
+
{isReady && (
|
|
152
|
+
<LiveProvider transformCode={transformCodeWithBasePath} code={code} scope={props.jsxScope} noInline={noInline}>
|
|
153
|
+
<div ref={rootRef} className={clsx(styles.CodeBlock, 'custom-component')}>
|
|
154
|
+
{shouldShowPreview && (
|
|
155
|
+
<div>
|
|
156
|
+
<div className={styles.colorModeMenu}>
|
|
157
|
+
<ActionBar aria-label="Toolbar">
|
|
158
|
+
{colorModes.map((mode, index) => {
|
|
159
|
+
const Icon = mode === 'light' ? SunIcon : MoonIcon
|
|
160
|
+
return (
|
|
161
|
+
<ActionBar.IconButton
|
|
162
|
+
className={clsx(styles.colorModeButton, colorMode === mode && styles.colorModeButtonActive)}
|
|
163
|
+
key={`color-mode-${mode}-${index}`}
|
|
164
|
+
icon={Icon}
|
|
165
|
+
aria-label={mode}
|
|
166
|
+
onClick={() => setColorMode(mode)}
|
|
167
|
+
/>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</ActionBar>
|
|
158
171
|
</div>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
ref={editorRef}
|
|
174
|
-
id={`${uniqueId}-code-editor-content`}
|
|
175
|
-
>
|
|
176
|
-
<LiveEditor theme={colorMode === 'light' ? lightTheme : darkTheme} onChange={setCode} />
|
|
172
|
+
<ThemeProvider colorMode={colorMode}>
|
|
173
|
+
<div className="custom-component">
|
|
174
|
+
<LivePreview className={styles.Preview} />
|
|
175
|
+
</div>
|
|
176
|
+
</ThemeProvider>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
<div className={styles.Toolbar}>
|
|
180
|
+
<Button size="small" leadingVisual={CopyIcon} onClick={handleCopy}>
|
|
181
|
+
Copy
|
|
182
|
+
</Button>
|
|
183
|
+
<Button size="small" leadingVisual={UndoIcon} onClick={handleReset} ref={resetButtonRef}>
|
|
184
|
+
Reset
|
|
185
|
+
</Button>
|
|
177
186
|
</div>
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
className={clsx(styles.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
aria-controls={`${uniqueId}-code-editor-content`}
|
|
184
|
-
aria-label={isCodePaneCollapsed ? 'Show full code block' : 'Collapse code block'}
|
|
187
|
+
<div className={styles.EditorWrapper}>
|
|
188
|
+
<div
|
|
189
|
+
className={clsx(styles.Editor, isCodePaneCollapsed && styles['Editor--is-collapsed'])}
|
|
190
|
+
ref={editorRef}
|
|
191
|
+
id={`${uniqueId}-code-editor-content`}
|
|
185
192
|
>
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
193
|
+
<LiveEditor theme={colorMode === 'light' ? lightTheme : darkTheme} onChange={setCode} />
|
|
194
|
+
</div>
|
|
195
|
+
{shouldShowCollapse && (
|
|
196
|
+
<button
|
|
197
|
+
className={clsx(styles.collapseButton, isCodePaneCollapsed && styles['collapseButton--collapsed'])}
|
|
198
|
+
onClick={handleCollapsibleCodePane}
|
|
199
|
+
aria-expanded={!isCodePaneCollapsed}
|
|
200
|
+
aria-controls={`${uniqueId}-code-editor-content`}
|
|
201
|
+
aria-label={isCodePaneCollapsed ? 'Show full code block' : 'Collapse code block'}
|
|
202
|
+
>
|
|
203
|
+
<Text size="100" className={styles.collapseLabel}>
|
|
204
|
+
{isCodePaneCollapsed ? 'Show full code' : 'Collapse code'}
|
|
205
|
+
</Text>
|
|
206
|
+
<Text variant="muted">{isCodePaneCollapsed ? <UnfoldIcon /> : <FoldIcon />}</Text>
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
{shouldShowPreview && (
|
|
211
|
+
<div className={styles.Error}>
|
|
212
|
+
<LiveError />
|
|
213
|
+
</div>
|
|
191
214
|
)}
|
|
192
215
|
</div>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<LiveError />
|
|
196
|
-
</div>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
</LiveProvider>
|
|
216
|
+
</LiveProvider>
|
|
217
|
+
)}
|
|
200
218
|
</>
|
|
201
219
|
)
|
|
202
220
|
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Helper function to turn Nextra <code> children into plain text
|
|
206
|
-
*/
|
|
207
|
-
function getCodeFromChildren(children: React.ReactNode) {
|
|
208
|
-
const extractText = (node: React.ReactNode): string => {
|
|
209
|
-
if (typeof node === 'string') return node
|
|
210
|
-
if (Array.isArray(node)) return node.map(extractText).join('')
|
|
211
|
-
if (React.isValidElement(node)) {
|
|
212
|
-
const element = node as React.ReactElement<{children?: React.ReactNode}>
|
|
213
|
-
return extractText(element.props.children)
|
|
214
|
-
}
|
|
215
|
-
return ''
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return extractText(children)
|
|
219
|
-
}
|
|
@@ -24,7 +24,6 @@ export function useNavDrawerState(breakpoint: string | number): [boolean, (value
|
|
|
24
24
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
if (isOpen) {
|
|
27
|
-
// eslint-disable-next-line github/prefer-observers
|
|
28
27
|
window.addEventListener('resize', debouncedOnResize)
|
|
29
28
|
return () => {
|
|
30
29
|
// cancel any debounced invocation of the resize handler
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
.BreadcrumbRoot,
|
|
14
14
|
.BreadcrumbItem {
|
|
15
|
+
--fgColor-link: var(--brand-InlineLink-color-rest);
|
|
15
16
|
color: var(--brand-InlineLink-color-rest);
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -24,12 +25,10 @@
|
|
|
24
25
|
width: 100%;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
28
|
.ActionsWrapper {
|
|
29
29
|
padding-block-start: var(--base-size-16);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
.RelatedLinks {
|
|
34
33
|
padding-top: var(--base-size-20);
|
|
35
34
|
}
|
|
@@ -43,7 +43,6 @@ const repoSrcPath = process.env.NEXT_PUBLIC_REPO_SRC_PATH || ''
|
|
|
43
43
|
const repoURL = process.env.NEXT_PUBLIC_REPO || ''
|
|
44
44
|
|
|
45
45
|
if (!repoURL) {
|
|
46
|
-
// eslint-disable-next-line no-console
|
|
47
46
|
console.warn(
|
|
48
47
|
'NEXT_PUBLIC_REPO is not set. Edit the .env.local file to set the NEXT_PUBLIC_REPO environment variable.',
|
|
49
48
|
)
|
|
@@ -75,7 +74,6 @@ export function Theme({pageMap, children}: ThemeProps) {
|
|
|
75
74
|
[pageMap, fsPath],
|
|
76
75
|
)
|
|
77
76
|
|
|
78
|
-
// eslint-disable-next-line i18n-text/no-en
|
|
79
77
|
const siteTitle = process.env.NEXT_PUBLIC_SITE_TITLE || 'Example Site'
|
|
80
78
|
const {headerLinks} = useConfig()
|
|
81
79
|
const activeHeaderLink = headerLinks.find(link => link.isActive)
|
|
@@ -56,8 +56,7 @@ export function Sidebar({pageMap}: SidebarProps) {
|
|
|
56
56
|
)}
|
|
57
57
|
<NavList className={styles.NavList} aria-labelledby="nav-list-heading">
|
|
58
58
|
{reorderedPageMap.map(item => {
|
|
59
|
-
if (
|
|
60
|
-
|
|
59
|
+
if (Object.hasOwn(item, 'data')) return null
|
|
61
60
|
if (!hasChildren(item)) return null
|
|
62
61
|
|
|
63
62
|
const indexPage = (item as Folder).children.find(child => (child as MdxFile).name === 'index') as MdxFile
|
package/css/prose.module.css
CHANGED
|
@@ -172,35 +172,11 @@
|
|
|
172
172
|
|
|
173
173
|
.Prose a:not(:global(.custom-component) a),
|
|
174
174
|
.Prose a:-webkit-any-link:not(:global(.custom-component) a:-webkit-any-link) {
|
|
175
|
+
--fgColor-accent: var(--brand-InlineLink-color-rest);
|
|
175
176
|
color: var(--brand-InlineLink-color-rest);
|
|
176
|
-
position: relative;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.Prose a:hover:not(:global(.custom-component) a:hover) {
|
|
180
177
|
text-decoration: underline;
|
|
181
178
|
}
|
|
182
179
|
|
|
183
|
-
.Prose a::after:not(:global(.custom-component) a::after) {
|
|
184
|
-
content: '';
|
|
185
|
-
border-bottom: var(--brand-borderWidth-thin) solid var(--brand-InlineLink-color-rest);
|
|
186
|
-
height: 0;
|
|
187
|
-
transition: var(--brand-InlineLink-transition-timing) transform;
|
|
188
|
-
transform: scaleY(var(--brand-InlineLink-transition-scale-start));
|
|
189
|
-
transform-origin: 50% 100%;
|
|
190
|
-
width: 100%;
|
|
191
|
-
position: absolute;
|
|
192
|
-
bottom: 0;
|
|
193
|
-
left: 0;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.Prose a:hover::after:not(:global(.custom-component) a:hover::after) {
|
|
197
|
-
transform: scaleY(var(--brand-InlineLink-transition-scale-end));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.Prose a:active::after:not(:global(.custom-component) a:active::after) {
|
|
201
|
-
border-color: var(--brand-InlineLink-color-pressed);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
180
|
.Prose code:not(:global(.custom-component) code) {
|
|
205
181
|
font-family: var(--brand-fontStack-monospace);
|
|
206
182
|
font-size: var(--brand-text-size-100);
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import baseConfig from '../../eslint.config.js'
|
|
2
|
+
import reactHooksPlugin from 'eslint-plugin-react-hooks'
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
...baseConfig,
|
|
6
|
+
|
|
7
|
+
// React Hooks rules
|
|
8
|
+
{
|
|
9
|
+
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
10
|
+
plugins: {
|
|
11
|
+
'react-hooks': reactHooksPlugin,
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
15
|
+
'react-hooks/exhaustive-deps': 'warn',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primer/doctocat-nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-rc.f3a7116",
|
|
4
4
|
"description": "A Next.js theme for building Primer documentation sites",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"check": "tsc --noEmit",
|
|
13
|
-
"lint": "eslint '**/*.{js,ts,tsx,md,mdx}' --max-warnings=0
|
|
13
|
+
"lint": "eslint '**/*.{js,ts,tsx,md,mdx}' --max-warnings=0",
|
|
14
14
|
"test": "vitest",
|
|
15
15
|
"test:ui": "vitest --ui",
|
|
16
16
|
"test:no-watch": "vitest run",
|
|
@@ -20,19 +20,20 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@primer/react-brand": ">=0.54.0",
|
|
23
|
-
"next": "^
|
|
23
|
+
"next": "^16.0.0",
|
|
24
24
|
"react": ">=18.0.0 <20.0.0",
|
|
25
25
|
"react-dom": ">=18.0.0 <20.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@next/mdx": "
|
|
28
|
+
"@next/mdx": "16.1.6",
|
|
29
29
|
"@primer/octicons-react": "19.15.1",
|
|
30
30
|
"@primer/react": "^38.3.0",
|
|
31
31
|
"@types/lodash.debounce": "^4.0.9",
|
|
32
32
|
"framer-motion": "12.23.24",
|
|
33
33
|
"lodash.debounce": "^4.0.8",
|
|
34
|
-
"nextra": "4.
|
|
34
|
+
"nextra": "4.6.1",
|
|
35
35
|
"react-focus-on": "^3.10.0",
|
|
36
|
+
"react-is": "^19.2.0",
|
|
36
37
|
"react-live": "^4.1.8"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"@vitest/ui": "^3.2.4",
|
|
50
51
|
"clsx": "2.1.1",
|
|
51
52
|
"jsdom": "^26.0.1",
|
|
52
|
-
"next": "
|
|
53
|
+
"next": "16.1.6",
|
|
53
54
|
"styled-components": "5.3.11",
|
|
54
55
|
"vitest": "^3.2.4"
|
|
55
56
|
}
|