'use client'; import { ReactNode, createContext, useContext, useEffect, useId, useState, useCallback, useMemo, useRef } from 'react'; import { useTranslations } from 'next-intl'; import { Lock } from '@/components/icons'; import { cn } from '@/lib/utils'; import { useSettingsStore } from '@/stores/settings-store'; /** * The Advanced switch. Rows and sections marked `advanced` render nothing * while it is off, but report themselves so the page can say how many it * hid. The provider wraps one settings page; without one (tests, plugin * previews) advanced rows simply show. */ interface AdvancedSettingsContextValue { show: boolean; register: (id: string, hidden: boolean) => void; } const AdvancedSettingsContext = createContext(null); export function AdvancedSettingsProvider({ children }: { children: ReactNode }) { const show = useSettingsStore((s) => s.showAdvancedSettings); const [hidden, setHidden] = useState>(() => new Set()); const register = useCallback((id: string, isHidden: boolean) => { setHidden((prev) => { if (prev.has(id) === isHidden) return prev; const next = new Set(prev); if (isHidden) next.add(id); else next.delete(id); return next; }); }, []); const value = useMemo(() => ({ show, register }), [show, register]); return ( {children} ); } /** True when this row or section should render; registers it as hidden otherwise. */ function useAdvancedVisible(advanced: boolean | undefined): boolean { const ctx = useContext(AdvancedSettingsContext); const id = useId(); const hidden = Boolean(advanced) && ctx !== null && !ctx.show; const register = ctx?.register; useEffect(() => { if (!register) return; register(id, hidden); return () => register(id, false); }, [register, id, hidden]); return !hidden; } function HiddenAdvancedNotice({ count }: { count: number }) { const t = useTranslations('settings.advanced_switch'); const updateSetting = useSettingsStore((s) => s.updateSetting); if (count === 0) return null; return (
{t('hidden_count', { count })}
); } /** The sidebar switch that reveals advanced rows and sections everywhere. */ export function AdvancedSettingsSwitch() { const t = useTranslations('settings.advanced_switch'); const show = useSettingsStore((s) => s.showAdvancedSettings); const updateSetting = useSettingsStore((s) => s.updateSetting); const labelId = useId(); return (
{t('label')} updateSetting('showAdvancedSettings', checked)} testId="settings-advanced-switch" />
); } /** Wraps a block that is not a SettingItem but should still hide with Advanced off. */ export function AdvancedOnly({ children }: { children: ReactNode }) { const visible = useAdvancedVisible(true); return visible ? <>{children} : null; } /** Scrolls the section with this id into view, if the open page has it. */ export function scrollToSettingsSection(id: string, root: ParentNode = document): boolean { const el = root.querySelector(`[data-settings-section="${id.replace(/"/g, '\\"')}"]`); if (!el) return false; el.scrollIntoView({ behavior: 'smooth', block: 'start' }); return true; } /** * The jump row at the top of a page: one button per section on the page, * read from the rendered sections so a page needs nothing beyond giving its * SettingsSections an id. Hidden while the page has fewer than two. */ export function SectionNav() { const ref = useRef(null); const [sections, setSections] = useState<{ id: string; title: string }[]>([]); useEffect(() => { const host = ref.current?.parentElement; if (!host) return; const collect = () => { const list = Array.from(host.querySelectorAll('[data-settings-section]')) .map((el) => ({ id: el.dataset.settingsSection ?? '', title: el.querySelector('h3')?.textContent?.trim() ?? '' })) .filter((entry) => entry.id && entry.title); setSections((prev) => ( prev.length === list.length && prev.every((p, i) => p.id === list[i].id && p.title === list[i].title) ? prev : list )); }; collect(); const observer = new MutationObserver(collect); observer.observe(host, { childList: true, subtree: true, characterData: true }); return () => observer.disconnect(); }, []); return ( ); } /** Small marker next to a label of a row that only shows with Advanced on. */ function AdvancedBadge() { const t = useTranslations('settings.advanced_switch'); return ( {t('badge')} ); } /** * Lets the controls inside a SettingItem borrow the row's visible label as * their accessible name. Without it a bare toggle announces as an unnamed * switch, since the row label is a