@primer/doctocat-nextjs 0.0.0-20260121104552 → 0.0.0-20260129001930
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 +17 -3
- package/components/layout/code-block/CodeBlock.tsx +0 -37
- package/components/layout/code-block/ReactCodeBlock.tsx +83 -81
- package/components/layout/nav-drawer/useNavDrawerState.ts +0 -1
- package/components/layout/root-layout/Theme.tsx +0 -2
- package/components/layout/sidebar/Sidebar.tsx +1 -2
- package/css/prose.module.css +4 -7
- package/eslint.config.js +18 -0
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
# @primer/doctocat-nextjs
|
|
2
2
|
|
|
3
|
-
## 0.0.0-
|
|
3
|
+
## 0.0.0-20260129001930
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
7
|
- Fake entry to force publishing
|
|
8
8
|
|
|
9
|
-
## 0.0.0-
|
|
9
|
+
## 0.0.0-20260129001928
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#87](https://github.com/primer/doctocat-nextjs/pull/87) [`a0aa353`](https://github.com/primer/doctocat-nextjs/commit/a0aa353c296543f98d5a32d38f3d5631a7081f96) Thanks [@rezrah](https://github.com/rezrah)! - Upgrades Next.js and Nextra.
|
|
14
|
+
- **Next.js v16.x is now required**. v15.x is no longer supported.
|
|
15
|
+
- `nextra` version should be > `4.6.1`
|
|
16
|
+
|
|
17
|
+
| Package | From | To |
|
|
18
|
+
| ----------- | ---- | ------ | --- |
|
|
19
|
+
| `next` | 15.x | 16.1.6 |
|
|
20
|
+
| `@next/mdx` | 15.x | 16.1.6 |
|
|
21
|
+
| `nextra` | 4.x | 4.6.1 | . |
|
|
22
|
+
|
|
23
|
+
## 0.8.1
|
|
10
24
|
|
|
11
25
|
### Patch Changes
|
|
12
26
|
|
|
13
|
-
- [#83](https://github.com/primer/doctocat-nextjs/pull/83) [`
|
|
27
|
+
- [#83](https://github.com/primer/doctocat-nextjs/pull/83) [`3c3b1a9`](https://github.com/primer/doctocat-nextjs/commit/3c3b1a95ff8a301ea81b67f393aad7a0d4ab52f9) Thanks [@danielguillan](https://github.com/danielguillan)! - Remove underlines from header links
|
|
14
28
|
|
|
15
29
|
- [#80](https://github.com/primer/doctocat-nextjs/pull/80) [`772d8de`](https://github.com/primer/doctocat-nextjs/commit/772d8de1b580f7158262f313952227e282035003) Thanks [@danielguillan](https://github.com/danielguillan)! - Improves accessibility of anchor links by adding an underline
|
|
16
30
|
|
|
@@ -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
|
|
|
@@ -35,8 +35,9 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
35
35
|
const uniqueId = useId()
|
|
36
36
|
const {colorMode, setColorMode} = useColorMode()
|
|
37
37
|
const {basePath} = useConfig()
|
|
38
|
-
const
|
|
39
|
-
const [code, setCode] = useState(
|
|
38
|
+
const codeSourceRef = useRef<HTMLDivElement>(null)
|
|
39
|
+
const [code, setCode] = useState<string>('')
|
|
40
|
+
const [isReady, setIsReady] = useState(false)
|
|
40
41
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
41
42
|
const [isCodePaneCollapsed, setIsCodePaneCollapsed] = useState<boolean | null>(null)
|
|
42
43
|
const [initialPosition, setInitialPosition] = useState<number | null>(null)
|
|
@@ -44,6 +45,16 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
44
45
|
const resetButtonRef = useRef<HTMLButtonElement>(null)
|
|
45
46
|
const shouldShowPreview = ['tsx', 'jsx'].includes(props['data-language'])
|
|
46
47
|
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (codeSourceRef.current && !isReady) {
|
|
50
|
+
const textContent = codeSourceRef.current.textContent || ''
|
|
51
|
+
if (textContent) {
|
|
52
|
+
setCode(textContent)
|
|
53
|
+
setIsReady(true)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, [isReady, props.children])
|
|
57
|
+
|
|
47
58
|
// scroll back to the initial y pos on collapse state change
|
|
48
59
|
useEffect(() => {
|
|
49
60
|
if (rootRef.current && initialPosition === null) {
|
|
@@ -71,8 +82,10 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
71
82
|
)
|
|
72
83
|
|
|
73
84
|
const handleReset = useCallback(() => {
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
if (codeSourceRef.current) {
|
|
86
|
+
setCode(codeSourceRef.current.textContent || '')
|
|
87
|
+
}
|
|
88
|
+
}, [])
|
|
76
89
|
|
|
77
90
|
const handleCopy = useCallback(() => {
|
|
78
91
|
navigator.clipboard.writeText(code)
|
|
@@ -132,88 +145,77 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
|
|
|
132
145
|
|
|
133
146
|
return (
|
|
134
147
|
<>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
{/* Hidden el to render children and extract text content */}
|
|
149
|
+
<div ref={codeSourceRef} style={{display: 'none'}} aria-hidden="true">
|
|
150
|
+
{props.children}
|
|
151
|
+
</div>
|
|
152
|
+
{isReady && (
|
|
153
|
+
<LiveProvider transformCode={transformCodeWithBasePath} code={code} scope={props.jsxScope} noInline={noInline}>
|
|
154
|
+
<div ref={rootRef} className={clsx(styles.CodeBlock, 'custom-component')}>
|
|
155
|
+
{shouldShowPreview && (
|
|
156
|
+
<div>
|
|
157
|
+
<div className={styles.colorModeMenu}>
|
|
158
|
+
<ActionBar aria-label="Toolbar">
|
|
159
|
+
{colorModes.map((mode, index) => {
|
|
160
|
+
const Icon = mode === 'light' ? SunIcon : MoonIcon
|
|
161
|
+
return (
|
|
162
|
+
<ActionBar.IconButton
|
|
163
|
+
className={clsx(styles.colorModeButton, colorMode === mode && styles.colorModeButtonActive)}
|
|
164
|
+
key={`color-mode-${mode}-${index}`}
|
|
165
|
+
icon={Icon}
|
|
166
|
+
aria-label={mode}
|
|
167
|
+
onClick={() => setColorMode(mode)}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
})}
|
|
171
|
+
</ActionBar>
|
|
158
172
|
</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} />
|
|
173
|
+
<ThemeProvider colorMode={colorMode}>
|
|
174
|
+
<div className="custom-component">
|
|
175
|
+
<LivePreview className={styles.Preview} />
|
|
176
|
+
</div>
|
|
177
|
+
</ThemeProvider>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
<div className={styles.Toolbar}>
|
|
181
|
+
<Button size="small" leadingVisual={CopyIcon} onClick={handleCopy}>
|
|
182
|
+
Copy
|
|
183
|
+
</Button>
|
|
184
|
+
<Button size="small" leadingVisual={UndoIcon} onClick={handleReset} ref={resetButtonRef}>
|
|
185
|
+
Reset
|
|
186
|
+
</Button>
|
|
177
187
|
</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'}
|
|
188
|
+
<div className={styles.EditorWrapper}>
|
|
189
|
+
<div
|
|
190
|
+
className={clsx(styles.Editor, isCodePaneCollapsed && styles['Editor--is-collapsed'])}
|
|
191
|
+
ref={editorRef}
|
|
192
|
+
id={`${uniqueId}-code-editor-content`}
|
|
185
193
|
>
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
194
|
+
<LiveEditor theme={colorMode === 'light' ? lightTheme : darkTheme} onChange={setCode} />
|
|
195
|
+
</div>
|
|
196
|
+
{shouldShowCollapse && (
|
|
197
|
+
<button
|
|
198
|
+
className={clsx(styles.collapseButton, isCodePaneCollapsed && styles['collapseButton--collapsed'])}
|
|
199
|
+
onClick={handleCollapsibleCodePane}
|
|
200
|
+
aria-expanded={!isCodePaneCollapsed}
|
|
201
|
+
aria-controls={`${uniqueId}-code-editor-content`}
|
|
202
|
+
aria-label={isCodePaneCollapsed ? 'Show full code block' : 'Collapse code block'}
|
|
203
|
+
>
|
|
204
|
+
<Text size="100" className={styles.collapseLabel}>
|
|
205
|
+
{isCodePaneCollapsed ? 'Show full code' : 'Collapse code'}
|
|
206
|
+
</Text>
|
|
207
|
+
<Text variant="muted">{isCodePaneCollapsed ? <UnfoldIcon /> : <FoldIcon />}</Text>
|
|
208
|
+
</button>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
{shouldShowPreview && (
|
|
212
|
+
<div className={styles.Error}>
|
|
213
|
+
<LiveError />
|
|
214
|
+
</div>
|
|
191
215
|
)}
|
|
192
216
|
</div>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<LiveError />
|
|
196
|
-
</div>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
</LiveProvider>
|
|
217
|
+
</LiveProvider>
|
|
218
|
+
)}
|
|
200
219
|
</>
|
|
201
220
|
)
|
|
202
221
|
}
|
|
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
|
|
@@ -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
|
@@ -173,14 +173,11 @@
|
|
|
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
175
|
color: var(--brand-InlineLink-color-rest);
|
|
176
|
-
position: relative;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.Prose a:hover:not(:global(.custom-component) a:hover) {
|
|
180
176
|
text-decoration: underline;
|
|
177
|
+
position: relative;
|
|
181
178
|
}
|
|
182
179
|
|
|
183
|
-
.Prose a
|
|
180
|
+
.Prose a:not(:global(.custom-component) a)::after {
|
|
184
181
|
content: '';
|
|
185
182
|
border-bottom: var(--brand-borderWidth-thin) solid var(--brand-InlineLink-color-rest);
|
|
186
183
|
height: 0;
|
|
@@ -193,11 +190,11 @@
|
|
|
193
190
|
left: 0;
|
|
194
191
|
}
|
|
195
192
|
|
|
196
|
-
.Prose a:
|
|
193
|
+
.Prose a:not(:global(.custom-component) a):hover::after {
|
|
197
194
|
transform: scaleY(var(--brand-InlineLink-transition-scale-end));
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
.Prose a:
|
|
197
|
+
.Prose a:not(:global(.custom-component) a):active::after {
|
|
201
198
|
border-color: var(--brand-InlineLink-color-pressed);
|
|
202
199
|
}
|
|
203
200
|
|
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.0.0-
|
|
3
|
+
"version": "0.0.0-20260129001930",
|
|
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
|
}
|