@primer/doctocat-nextjs 0.5.4-rc.3b1107b → 0.5.4-rc.bcfc99f

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 CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  ### Patch Changes
6
6
 
7
+ - [#51](https://github.com/primer/doctocat-nextjs/pull/51) [`4c76c4f`](https://github.com/primer/doctocat-nextjs/commit/4c76c4f5ccd248ec7d1f448c054808287a3ff51d) Thanks [@rezrah](https://github.com/rezrah)! - Add auto-collapsed React code blocks for large code snippets. This feature only applies to code fences with the `jsx live` language identifiers.
8
+
9
+ E.g.
10
+
11
+ ```jsx live
12
+ <>Your code</>
13
+ ```
14
+
7
15
  - [#48](https://github.com/primer/doctocat-nextjs/pull/48) [`ce73c24`](https://github.com/primer/doctocat-nextjs/commit/ce73c24b2e4e924667bf7446a504bd88d8f2ccf0) Thanks [@rezrah](https://github.com/rezrah)! - - Fix inline code font-size in markdown headings. Now inherits size used in the heading.
8
16
 
9
17
  - Increased spacing below React code blocks, which was previously too small.
@@ -28,6 +28,11 @@
28
28
  font-family: var(--brand-fontStack-monospace);
29
29
  background-color: var(--brand-color-canvas-subtle);
30
30
  overflow: auto;
31
+ position: relative;
32
+ }
33
+
34
+ .EditorWrapper {
35
+ position: relative;
31
36
  }
32
37
 
33
38
  .Editor pre {
@@ -37,6 +42,11 @@
37
42
  background-color: var(--brand-color-canvas-subtle) !important;
38
43
  }
39
44
 
45
+ .Editor--is-collapsed {
46
+ max-height: 400px;
47
+ overflow: hidden;
48
+ }
49
+
40
50
  .Toolbar {
41
51
  display: flex;
42
52
  gap: var(--base-size-8);
@@ -67,3 +77,44 @@
67
77
  .colorModeButtonActive {
68
78
  background-color: var(--brand-color-canvas-subtle);
69
79
  }
80
+
81
+ .collapseButton {
82
+ position: relative;
83
+ display: flex;
84
+ justify-content: center;
85
+ width: 100%;
86
+ background-color: var(--brand-color-canvas-subtle);
87
+ border: none;
88
+ border-top: var(--brand-borderWidth-thin) solid var(--brand-color-border-default);
89
+ border-bottom-left-radius: var(--brand-borderRadius-medium);
90
+ border-bottom-right-radius: var(--brand-borderRadius-medium);
91
+ padding: var(--base-size-8) var(--base-size-12);
92
+ font-size: var(--brand-text-size-100);
93
+ font-family: var(--brand-fontStack-system);
94
+ cursor: pointer;
95
+ display: flex;
96
+ align-items: center;
97
+ gap: var(--base-size-4);
98
+ z-index: 10;
99
+ transition: background-color var(--brand-animation-duration-default) var(--brand-animation-easing-glide);
100
+ }
101
+
102
+ .collapseButton--collapsed::before {
103
+ content: '';
104
+ position: absolute;
105
+ top: -41px;
106
+ left: 0;
107
+ width: 100%;
108
+ height: 40px;
109
+ background: linear-gradient(to bottom, transparent, var(--brand-color-canvas-subtle));
110
+ z-index: -1;
111
+ }
112
+
113
+ .collapseButton:hover,
114
+ .collapseButton:focus {
115
+ background-color: var(--brand-color-canvas-default);
116
+ }
117
+
118
+ .collapseLabel {
119
+ font-size: var(--brand-text-size-100) !important;
120
+ }
@@ -1,17 +1,20 @@
1
1
  'use client'
2
- import React, {type PropsWithChildren, useCallback, useState, useRef, useEffect} from 'react'
2
+ import React, {type PropsWithChildren, useCallback, useState, useRef, useEffect, useId} from 'react'
3
3
  import clsx from 'clsx'
4
4
  import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live'
5
5
  import {useColorMode} from '../../context/color-modes/useColorMode'
6
6
  import {useConfig} from '../../context/useConfig'
7
7
  import styles from './ReactCodeBlock.module.css'
8
8
  import {ActionBar, Button, ThemeProvider} from '@primer/react'
9
- import {CopyIcon, MoonIcon, SunIcon, UndoIcon} from '@primer/octicons-react'
9
+ import {Text} from '@primer/react-brand'
10
+ import {CopyIcon, MoonIcon, SunIcon, UndoIcon, UnfoldIcon, FoldIcon} from '@primer/octicons-react'
10
11
  import {colorModes} from '../../context/color-modes/context'
11
12
 
12
13
  import {lightTheme, darkTheme} from './syntax-highlighting-themes'
13
14
  import {codeTransformer} from './code-transformer'
14
15
 
16
+ const COLLAPSE_HEIGHT = 400 // TODO: Hoist this to config to make user customizable eventually
17
+
15
18
  type ReactCodeBlockProps = {
16
19
  'data-language': string
17
20
  'data-filename'?: string
@@ -28,13 +31,35 @@ const getFocusableElements = () => {
28
31
  }
29
32
 
30
33
  export function ReactCodeBlock(props: ReactCodeBlockProps) {
34
+ const uniqueId = useId()
31
35
  const {colorMode, setColorMode} = useColorMode()
32
36
  const {basePath} = useConfig()
33
37
  const initialCode = getCodeFromChildren(props.children)
34
38
  const [code, setCode] = useState(initialCode)
35
- const shouldShowPreview = ['tsx', 'jsx'].includes(props['data-language'])
39
+ const rootRef = useRef<HTMLDivElement>(null)
40
+ const [isCodePaneCollapsed, setIsCodePaneCollapsed] = useState<boolean | null>(null)
41
+ const [initialPosition, setInitialPosition] = useState<number | null>(null)
36
42
  const editorRef = useRef<HTMLDivElement>(null)
37
43
  const resetButtonRef = useRef<HTMLButtonElement>(null)
44
+ const shouldShowPreview = ['tsx', 'jsx'].includes(props['data-language'])
45
+
46
+ // scroll back to the initial y pos on collapse state change
47
+ useEffect(() => {
48
+ if (rootRef.current && initialPosition === null) {
49
+ const rect = rootRef.current.getBoundingClientRect()
50
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop
51
+ setInitialPosition(rect.top + scrollTop)
52
+ }
53
+ }, [initialPosition])
54
+
55
+ useEffect(() => {
56
+ if (editorRef.current && isCodePaneCollapsed === null) {
57
+ const editorHeight = editorRef.current.scrollHeight
58
+ setIsCodePaneCollapsed(editorHeight > COLLAPSE_HEIGHT)
59
+ }
60
+ }, [code, isCodePaneCollapsed])
61
+
62
+ const shouldShowCollapse = isCodePaneCollapsed !== null && (editorRef.current?.scrollHeight || 0) > COLLAPSE_HEIGHT
38
63
 
39
64
  /**
40
65
  * Transforms code to prepend basePath to img src attributes
@@ -52,6 +77,21 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
52
77
  navigator.clipboard.writeText(code)
53
78
  }, [code])
54
79
 
80
+ const handleCollapsibleCodePane = useCallback(() => {
81
+ const newCollapsedState = !isCodePaneCollapsed
82
+
83
+ if (!isCodePaneCollapsed && newCollapsedState && initialPosition !== null) {
84
+ requestAnimationFrame(() => {
85
+ window.scrollTo({
86
+ top: initialPosition,
87
+ behavior: 'smooth',
88
+ })
89
+ })
90
+ }
91
+
92
+ setIsCodePaneCollapsed(newCollapsedState)
93
+ }, [isCodePaneCollapsed, initialPosition])
94
+
55
95
  const noInline = props['data-filename'] === 'noinline' || false
56
96
 
57
97
  useEffect(() => {
@@ -92,7 +132,7 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
92
132
  return (
93
133
  <>
94
134
  <LiveProvider transformCode={transformCodeWithBasePath} code={code} scope={props.jsxScope} noInline={noInline}>
95
- <div className={clsx(styles.CodeBlock, 'custom-component')}>
135
+ <div ref={rootRef} className={clsx(styles.CodeBlock, 'custom-component')}>
96
136
  {shouldShowPreview && (
97
137
  <div>
98
138
  <div className={styles.colorModeMenu}>
@@ -126,8 +166,28 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
126
166
  Reset
127
167
  </Button>
128
168
  </div>
129
- <div className={styles.Editor} ref={editorRef}>
130
- <LiveEditor theme={colorMode === 'light' ? lightTheme : darkTheme} onChange={setCode} />
169
+ <div className={styles.EditorWrapper}>
170
+ <div
171
+ className={clsx(styles.Editor, isCodePaneCollapsed && styles['Editor--is-collapsed'])}
172
+ ref={editorRef}
173
+ id={`${uniqueId}-code-editor-content`}
174
+ >
175
+ <LiveEditor theme={colorMode === 'light' ? lightTheme : darkTheme} onChange={setCode} />
176
+ </div>
177
+ {shouldShowCollapse && (
178
+ <button
179
+ className={clsx(styles.collapseButton, isCodePaneCollapsed && styles['collapseButton--collapsed'])}
180
+ onClick={handleCollapsibleCodePane}
181
+ aria-expanded={!isCodePaneCollapsed}
182
+ aria-controls={`${uniqueId}-code-editor-content`}
183
+ aria-label={isCodePaneCollapsed ? 'Show full code block' : 'Collapse code block'}
184
+ >
185
+ <Text size="100" className={styles.collapseLabel}>
186
+ {isCodePaneCollapsed ? 'Show full code' : 'Collapse code'}
187
+ </Text>
188
+ <Text variant="muted">{isCodePaneCollapsed ? <UnfoldIcon /> : <FoldIcon />}</Text>
189
+ </button>
190
+ )}
131
191
  </div>
132
192
  {shouldShowPreview && (
133
193
  <div className={styles.Error}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/doctocat-nextjs",
3
- "version": "0.5.4-rc.3b1107b",
3
+ "version": "0.5.4-rc.bcfc99f",
4
4
  "description": "A Next.js theme for building Primer documentation sites",
5
5
  "main": "index.js",
6
6
  "type": "module",