Skip to content

Commit 4ae7350

Browse files
committed
docs: add tabbed terminal component, show npm/bun install
1 parent ab18d2f commit 4ae7350

13 files changed

+448
-131
lines changed

src/components/MDX/MDXComponents.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Diagram from './Diagram';
2525
import DiagramGroup from './DiagramGroup';
2626
import SimpleCallout from './SimpleCallout';
2727
import TerminalBlock from './TerminalBlock';
28+
import TabTerminalBlock from './TabTerminalBlock';
2829
import YouWillLearnCard from './YouWillLearnCard';
2930
import {Challenges, Hint, Solution} from './Challenges';
3031
import {IconNavArrow} from '../Icon/IconNavArrow';
@@ -521,6 +522,7 @@ export const MDXComponents = {
521522
SandpackWithHTMLOutput,
522523
TeamMember,
523524
TerminalBlock,
525+
TabTerminalBlock,
524526
YouWillLearn,
525527
YouWillLearnCard,
526528
Challenges,
+245
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*/
4+
5+
import * as React from 'react';
6+
import {useState, useEffect, useCallback} from 'react';
7+
import TerminalBlock from './TerminalBlock';
8+
import {IconTerminal} from '../Icon/IconTerminal';
9+
10+
type TabOption = {
11+
label: string;
12+
value: string;
13+
content: string;
14+
};
15+
16+
// Define this outside of any conditionals for SSR compatibility
17+
const STORAGE_KEY = 'react-terminal-tabs';
18+
19+
// Map key for active tab preferences - only used on client
20+
let activeTabsByKey: Record<string, string> = {};
21+
let subscribersByKey: Record<string, Set<(tab: string) => void>> = {};
22+
23+
function saveToLocalStorage() {
24+
if (typeof window !== 'undefined') {
25+
try {
26+
localStorage.setItem(STORAGE_KEY, JSON.stringify(activeTabsByKey));
27+
} catch (e) {
28+
// Ignore errors
29+
}
30+
}
31+
}
32+
33+
function getSubscribers(key: string): Set<(tab: string) => void> {
34+
if (!subscribersByKey[key]) {
35+
subscribersByKey[key] = new Set();
36+
}
37+
return subscribersByKey[key];
38+
}
39+
40+
function setActiveTab(key: string, tab: string) {
41+
activeTabsByKey[key] = tab;
42+
saveToLocalStorage();
43+
44+
const subscribers = getSubscribers(key);
45+
subscribers.forEach((callback) => callback(tab));
46+
}
47+
48+
function useTabState(
49+
key: string,
50+
defaultTab: string
51+
): [string, (tab: string) => void] {
52+
// Start with the default tab for SSR
53+
const [activeTab, setLocalActiveTab] = useState(defaultTab);
54+
const [initialized, setInitialized] = useState(false);
55+
56+
// Initialize from localStorage after mount
57+
useEffect(() => {
58+
// Read from localStorage
59+
try {
60+
const savedState = localStorage.getItem(STORAGE_KEY);
61+
if (savedState) {
62+
const parsed = JSON.parse(savedState);
63+
if (parsed && typeof parsed === 'object') {
64+
Object.assign(activeTabsByKey, parsed);
65+
}
66+
}
67+
} catch (e) {
68+
// Ignore errors
69+
}
70+
71+
// Set up storage event listener
72+
const handleStorageChange = (e: StorageEvent) => {
73+
if (e.key === STORAGE_KEY && e.newValue) {
74+
try {
75+
const parsed = JSON.parse(e.newValue);
76+
if (parsed && typeof parsed === 'object') {
77+
Object.assign(activeTabsByKey, parsed);
78+
79+
Object.entries(parsed).forEach(([k, value]) => {
80+
const subscribers = subscribersByKey[k];
81+
if (subscribers) {
82+
subscribers.forEach((callback) => callback(value as string));
83+
}
84+
});
85+
}
86+
} catch (e) {
87+
// Ignore errors
88+
}
89+
}
90+
};
91+
92+
window.addEventListener('storage', handleStorageChange);
93+
94+
// Now get the value from localStorage or keep using default
95+
const storedValue = activeTabsByKey[key] || defaultTab;
96+
setLocalActiveTab(storedValue);
97+
setInitialized(true);
98+
99+
// Make sure this key is in our global store
100+
if (!activeTabsByKey[key]) {
101+
activeTabsByKey[key] = defaultTab;
102+
saveToLocalStorage();
103+
}
104+
105+
return () => {
106+
window.removeEventListener('storage', handleStorageChange);
107+
};
108+
}, [key, defaultTab]);
109+
110+
// Set up subscription effect
111+
useEffect(() => {
112+
// Skip if not yet initialized
113+
if (!initialized) return;
114+
115+
const onTabChange = (newTab: string) => {
116+
setLocalActiveTab(newTab);
117+
};
118+
119+
const subscribers = getSubscribers(key);
120+
subscribers.add(onTabChange);
121+
122+
return () => {
123+
subscribers.delete(onTabChange);
124+
125+
if (subscribers.size === 0) {
126+
delete subscribersByKey[key];
127+
}
128+
};
129+
}, [key, initialized]);
130+
131+
// Create a stable setter function
132+
const setTab = useCallback(
133+
(newTab: string) => {
134+
setActiveTab(key, newTab);
135+
},
136+
[key]
137+
);
138+
139+
return [activeTab, setTab];
140+
}
141+
142+
interface TabTerminalBlockProps {
143+
/** Terminal's message level: info, warning, or error */
144+
level?: 'info' | 'warning' | 'error';
145+
146+
/**
147+
* Tab options, each with a label, value, and content.
148+
* Example: [
149+
* { label: 'npm', value: 'npm', content: 'npm install react' },
150+
* { label: 'Bun', value: 'bun', content: 'bun install react' }
151+
* ]
152+
*/
153+
tabs?: Array<TabOption>;
154+
155+
/** Optional initial active tab value */
156+
defaultTab?: string;
157+
158+
/**
159+
* Optional storage key for tab state.
160+
* All TabTerminalBlocks with the same key will share tab selection.
161+
*/
162+
storageKey?: string;
163+
}
164+
165+
/**
166+
* TabTerminalBlock displays a terminal block with tabs.
167+
* Tabs sync across instances with the same storageKey.
168+
*
169+
* @example
170+
* <TabTerminalBlock
171+
* tabs={[
172+
* { label: 'npm', value: 'npm', content: 'npm install react' },
173+
* { label: 'Bun', value: 'bun', content: 'bun install react' }
174+
* ]}
175+
* />
176+
*/
177+
function TabTerminalBlock({
178+
level = 'info',
179+
tabs = [],
180+
defaultTab,
181+
storageKey = 'package-manager',
182+
}: TabTerminalBlockProps) {
183+
// Create a fallback tab if none provided
184+
const safeTabsList =
185+
tabs && tabs.length > 0
186+
? tabs
187+
: [{label: 'Terminal', value: 'default', content: 'No content provided'}];
188+
189+
// Always use the first tab as initial defaultTab for SSR consistency
190+
// This ensures server and client render the same content initially
191+
const initialDefaultTab = defaultTab || safeTabsList[0].value;
192+
193+
// Set up tab state
194+
const [activeTab, setTabValue] = useTabState(storageKey, initialDefaultTab);
195+
196+
const handleTabClick = useCallback(
197+
(tabValue: string) => {
198+
return () => setTabValue(tabValue);
199+
},
200+
[setTabValue]
201+
);
202+
203+
// Handle the case with no content - after hooks have been called
204+
if (
205+
safeTabsList.length === 0 ||
206+
safeTabsList[0].content === 'No content provided'
207+
) {
208+
return (
209+
<TerminalBlock level="error">
210+
Error: No tab content provided
211+
</TerminalBlock>
212+
);
213+
}
214+
215+
const activeTabOption =
216+
safeTabsList.find((tab) => tab.value === activeTab) || safeTabsList[0];
217+
218+
const customHeader = (
219+
<div className="flex items-center">
220+
<IconTerminal className="mr-3" />
221+
<div className="flex items-center">
222+
{safeTabsList.map((tab) => (
223+
<button
224+
key={tab.value}
225+
className={`text-sm font-medium px-3 py-1 h-7 mx-0.5 inline-flex items-center justify-center rounded-sm transition-colors ${
226+
activeTab === tab.value
227+
? 'bg-gray-50/50 text-primary dark:bg-gray-800/30 dark:text-primary-dark'
228+
: 'text-primary dark:text-primary-dark hover:bg-gray-50/30 dark:hover:bg-gray-800/20'
229+
}`}
230+
onClick={handleTabClick(tab.value)}>
231+
{tab.label}
232+
</button>
233+
))}
234+
</div>
235+
</div>
236+
);
237+
238+
return (
239+
<TerminalBlock level={level} customHeader={customHeader}>
240+
{activeTabOption.content}
241+
</TerminalBlock>
242+
);
243+
}
244+
245+
export default TabTerminalBlock;

src/components/MDX/TerminalBlock.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type LogLevel = 'info' | 'warning' | 'error';
1212
interface TerminalBlockProps {
1313
level?: LogLevel;
1414
children: React.ReactNode;
15+
customHeader?: React.ReactNode;
1516
}
1617

1718
function LevelText({type}: {type: LogLevel}) {
@@ -25,7 +26,11 @@ function LevelText({type}: {type: LogLevel}) {
2526
}
2627
}
2728

28-
function TerminalBlock({level = 'info', children}: TerminalBlockProps) {
29+
function TerminalBlock({
30+
level = 'info',
31+
children,
32+
customHeader,
33+
}: TerminalBlockProps) {
2934
let message: string | undefined;
3035
if (typeof children === 'string') {
3136
message = children;
@@ -53,15 +58,20 @@ function TerminalBlock({level = 'info', children}: TerminalBlockProps) {
5358
}, [copied]);
5459

5560
return (
56-
<div className="rounded-lg bg-secondary dark:bg-gray-50 h-full">
61+
<div className="rounded-lg bg-secondary dark:bg-gray-50 h-full my-4">
5762
<div className="bg-gray-90 dark:bg-gray-60 w-full rounded-t-lg">
5863
<div className="text-primary-dark dark:text-primary-dark flex text-sm px-4 py-0.5 relative justify-between">
5964
<div>
60-
<IconTerminal className="inline-flex me-2 self-center" /> Terminal
65+
{customHeader || (
66+
<>
67+
<IconTerminal className="inline-flex me-2 self-center" />{' '}
68+
Terminal
69+
</>
70+
)}
6171
</div>
6272
<div>
6373
<button
64-
className="w-full text-start text-primary-dark dark:text-primary-dark "
74+
className="w-full text-start text-primary-dark dark:text-primary-dark"
6575
onClick={() => {
6676
window.navigator.clipboard.writeText(message ?? '');
6777
setCopied(true);

src/content/blog/2021/12/17/react-conf-2021-recap.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ In the keynote, we also announced that the React 18 RC is available to try now.
4343

4444
To try the React 18 RC, upgrade your dependencies:
4545

46-
```bash
47-
npm install react@rc react-dom@rc
48-
```
46+
<TabTerminalBlock
47+
tabs={[
48+
{ label: 'npm', value: 'npm', content: 'npm install react@rc react-dom@rc' },
49+
{ label: 'Bun', value: 'bun', content: 'bun add react@rc react-dom@rc' }
50+
]}
51+
/>
4952

5053
and switch to the new `createRoot` API:
5154

src/content/blog/2022/03/08/react-18-upgrade-guide.md

+7-9
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,13 @@ For React Native users, React 18 will ship in a future version of React Native.
2929

3030
To install the latest version of React:
3131

32-
```bash
33-
npm install react react-dom
34-
```
35-
36-
Or if you’re using yarn:
37-
38-
```bash
39-
yarn add react react-dom
40-
```
32+
<TabTerminalBlock
33+
tabs={[
34+
{ label: 'npm', value: 'npm', content: 'npm install react react-dom' },
35+
{ label: 'yarn', value: 'yarn', content: 'yarn add react react-dom' },
36+
{ label: 'Bun', value: 'bun', content: 'bun add react react-dom' }
37+
]}
38+
/>
4139

4240
## Updates to Client Rendering APIs {/*updates-to-client-rendering-apis*/}
4341

0 commit comments

Comments
 (0)