Skip to content

Commit 302f7d7

Browse files
committed
Add code tabs
1 parent 05c39c8 commit 302f7d7

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

components/Book/CodeTabs.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React from "react";
2+
3+
const STORAGE_KEY = "codeTabPreference";
4+
5+
const createTabStore = () => {
6+
let state: string[] = [];
7+
if (typeof window !== "undefined") {
8+
try {
9+
state = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
10+
} catch {
11+
}
12+
}
13+
14+
const listeners = new Set<() => void>();
15+
return {
16+
subscribe(fn: () => void) {
17+
listeners.add(fn);
18+
return () => listeners.delete(fn);
19+
},
20+
21+
getSnapshot() {
22+
return state;
23+
},
24+
25+
bump(label: string) {
26+
state = [label, ...state.filter(x => x !== label)].slice(0, 20);
27+
if (typeof window !== "undefined") {
28+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
29+
}
30+
listeners.forEach(l => l());
31+
},
32+
};
33+
}
34+
35+
let tabStore: ReturnType<typeof createTabStore> | null = null;
36+
37+
function getTabStore() {
38+
if (typeof window === "undefined") {
39+
// SSR safety: return a dummy store
40+
return {
41+
subscribe: () => () => {},
42+
getSnapshot: () => [],
43+
bump: () => {},
44+
};
45+
}
46+
if (!tabStore) {
47+
tabStore = createTabStore();
48+
}
49+
return tabStore;
50+
}
51+
52+
export function useTabPrefs() {
53+
const store = getTabStore();
54+
const order = React.useSyncExternalStore(
55+
store.subscribe,
56+
store.getSnapshot,
57+
store.getSnapshot
58+
);
59+
return {
60+
order,
61+
bump: store.bump,
62+
};
63+
}
64+
65+
type CodeTabElement = React.ReactElement<{
66+
label: string;
67+
id?: string;
68+
children: React.ReactNode;
69+
}>;
70+
71+
export const CodeTabs = ({ children }: { children: React.ReactNode }) => {
72+
const tabs = React.Children
73+
.toArray(children)
74+
.filter(child => child && React.isValidElement(child) && child.type === CodeTab
75+
) as CodeTabElement[];
76+
77+
const { order, bump } = useTabPrefs();
78+
const active = React.useMemo(() => {
79+
const labels = tabs.map(t => t.props.id ?? t.props.label);
80+
for (const pref of order) {
81+
const idx = labels.indexOf(pref);
82+
if (idx !== -1) {
83+
return idx;
84+
}
85+
}
86+
return 0;
87+
}, [order, tabs]);
88+
89+
return (
90+
<div className="my-4 border border-gray-300 rounded-lg overflow-hidden" style={{boxShadow: "0.1rem 0.1rem 0.2rem #00000028"}}>
91+
<div className="flex bg-gray-100 border-b border-gray-300">
92+
{tabs.map((tab, i) => (
93+
<button
94+
key={i}
95+
onClick={() => { bump(tab.props.id ?? tab.props.label); }}
96+
className={`px-3 py-2 text-sm ${
97+
i === active ? "bg-white border-b-2 border-blue-500" : ""
98+
}`}
99+
>
100+
{tab.props.label}
101+
</button>
102+
))}
103+
</div>
104+
<div className="p-0">
105+
{tabs[active]}
106+
</div>
107+
</div>
108+
);
109+
}
110+
111+
112+
export const CodeTab = ({ children }: { children: React.ReactNode }) =>
113+
<div className="code-tab">{children}</div>;

components/MdxContent.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Question, { QuizPropsBase } from "./Quiz/Question";
1010
import { Quiz } from "@/components/Quiz/Quiz";
1111
import Image from "./Image";
1212
import { ExpandingSideImg, Sidenote } from "@/components/Book/Sidenote";
13+
import { CodeTab, CodeTabs } from "@/components/Book/CodeTabs";
1314

1415

1516
export interface QuestionProps extends QuizPropsBase {
@@ -73,6 +74,8 @@ export const MdxContent = ({content, chapterId, bookId, t, env, allAnswers}: {
7374
SideNote: Sidenote,
7475
ExpandingSideImg,
7576

77+
CodeTabs, CodeTab,
78+
7679
FullWidth: ({ children }: { children: React.ReactNode }) =>
7780
<div className="full-width">
7881
{children}

styles/globals.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ $gap: var(--gap);
211211
}
212212
}
213213

214+
.code-tab > :not(.expressive-code) {
215+
margin: 12px;
216+
}
217+
214218
@page {
215219
margin: 0.75in;
216220
}

0 commit comments

Comments
 (0)