Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions components/Book/CodeTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from "react";

const STORAGE_KEY = "codeTabPreference";
Comment thread
janezd marked this conversation as resolved.

const createTabStore = () => {
let state: string[] = [];
if (typeof window !== "undefined") {
try {
state = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
} catch {
}
}
Comment thread
janezd marked this conversation as resolved.

const listeners = new Set<() => void>();
return {
subscribe(fn: () => void) {
listeners.add(fn);
return () => listeners.delete(fn);
},

getSnapshot() {
return state;
},

bump(label: string) {
state = [label, ...state.filter(x => x !== label)].slice(0, 20);
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
Comment thread
janezd marked this conversation as resolved.
}
listeners.forEach(l => l());
},
};
}

let tabStore: ReturnType<typeof createTabStore> | null = null;

function getTabStore() {
if (typeof window === "undefined") {
// SSR safety: return a dummy store
return {
subscribe: () => () => {},
getSnapshot: () => [],
bump: () => {},
};
}
if (!tabStore) {
tabStore = createTabStore();
}
return tabStore;
}

export function useTabPrefs() {
const store = getTabStore();
const order = React.useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getSnapshot
);
return {
order,
bump: store.bump,
};
}

type CodeTabElement = React.ReactElement<{
label: string;
id?: string;
children: React.ReactNode;
}>;
Comment thread
janezd marked this conversation as resolved.

export const CodeTabs = ({ children }: { children: React.ReactNode }) => {
const tabs = React.Children
.toArray(children)
.filter(child => child && React.isValidElement(child) && child.type === CodeTab
) as CodeTabElement[];

const { order, bump } = useTabPrefs();
const active = React.useMemo(() => {
const labels = tabs.map(t => t.props.id ?? t.props.label);
for (const pref of order) {
const idx = labels.indexOf(pref);
if (idx !== -1) {
return idx;
}
}
return 0;
}, [order, tabs]);

return (
<div className="my-4 border border-gray-300 rounded-lg overflow-hidden" style={{boxShadow: "0.1rem 0.1rem 0.2rem #00000028"}}>
<div className="flex bg-gray-100 border-b border-gray-300">
{tabs.map((tab, i) => (
<button
key={i}
onClick={() => { bump(tab.props.id ?? tab.props.label); }}
className={`px-3 py-2 text-sm ${
i === active ? "bg-white border-b-2 border-blue-500" : ""
}`}
>
{tab.props.label}
</button>
Comment thread
janezd marked this conversation as resolved.
))}
</div>
<div className="p-0">
{tabs[active]}
</div>
</div>
);
}


export const CodeTab = ({ children }: { children: React.ReactNode }) =>
<div className="code-tab">{children}</div>;
3 changes: 3 additions & 0 deletions components/MdxContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Question, { QuizPropsBase } from "./Quiz/Question";
import { Quiz } from "@/components/Quiz/Quiz";
import Image from "./Image";
import { ExpandingSideImg, Sidenote } from "@/components/Book/Sidenote";
import { CodeTab, CodeTabs } from "@/components/Book/CodeTabs";


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

CodeTabs, CodeTab,

FullWidth: ({ children }: { children: React.ReactNode }) =>
<div className="full-width">
{children}
Expand Down
4 changes: 4 additions & 0 deletions styles/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ $gap: var(--gap);
}
}

.code-tab > :not(.expressive-code) {
margin: 12px;
}

@page {
margin: 0.75in;
}
Expand Down
Loading