|
| 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>; |
0 commit comments