| import { X } from "lucide-react"; |
| import { useState, useEffect, useCallback } from "react"; |
| import { useFrontmatter } from "./useFrontmatter"; |
| import type { FrontmatterStore, FrontmatterData } from "./frontmatter-store"; |
| import type * as Y from "yjs"; |
| import { HueSlider } from "./HueSlider"; |
|
|
| interface SettingsDrawerProps { |
| open: boolean; |
| onClose: () => void; |
| store: FrontmatterStore | null; |
| settingsMap?: Y.Map<any> | null; |
| } |
|
|
| export function SettingsDrawer({ open, onClose, store, settingsMap }: SettingsDrawerProps) { |
| const { data, update } = useFrontmatter(store); |
| const [citationStyle, setCitationStyle] = useState("apa"); |
| const [hue, setHue] = useState(47); |
|
|
| useEffect(() => { |
| if (!settingsMap) return; |
| const sync = () => { |
| const cit = settingsMap.get("citationStyle"); |
| if (cit) setCitationStyle(cit as string); |
| const h = settingsMap.get("primaryHue"); |
| if (h !== undefined) setHue(h as number); |
| }; |
| sync(); |
| settingsMap.observe(sync); |
| return () => settingsMap.unobserve(sync); |
| }, [settingsMap]); |
|
|
| const handleEscape = useCallback((e: KeyboardEvent) => { |
| if (e.key === "Escape") onClose(); |
| }, [onClose]); |
|
|
| useEffect(() => { |
| if (open) { |
| document.addEventListener("keydown", handleEscape); |
| return () => document.removeEventListener("keydown", handleEscape); |
| } |
| }, [open, handleEscape]); |
|
|
| if (!data) return null; |
|
|
| return ( |
| <> |
| <div className={`drawer-backdrop ${open ? "open" : ""}`} onClick={onClose} /> |
| <aside className={`drawer-panel ${open ? "open" : ""}`}> |
| <div className="settings-drawer"> |
| <div className="settings-drawer__header"> |
| <span className="settings-drawer__title">Article settings</span> |
| <button className="icon-btn" onClick={onClose} aria-label="Close"> |
| <X size={16} /> |
| </button> |
| </div> |
| |
| <div className="settings-drawer__body"> |
| <FieldGroup label="Template"> |
| <select |
| className="form-select" |
| value={data.template} |
| onChange={(e) => update("template", e.target.value as FrontmatterData["template"])} |
| > |
| <option value="article">Article (full layout)</option> |
| <option value="paper">Paper (single column)</option> |
| </select> |
| </FieldGroup> |
| |
| <FieldGroup label="Description (SEO)"> |
| <textarea |
| className="form-input" |
| rows={3} |
| placeholder="Short description for meta tags..." |
| value={data.description} |
| onChange={(e) => update("description", e.target.value)} |
| /> |
| </FieldGroup> |
| |
| <FieldGroup label="Citation style"> |
| <select |
| className="form-select" |
| value={citationStyle} |
| onChange={(e) => { |
| const val = e.target.value; |
| setCitationStyle(val); |
| settingsMap?.set("citationStyle", val); |
| }} |
| > |
| <option value="apa">APA (7th edition)</option> |
| <option value="ieee">IEEE</option> |
| <option value="vancouver">Vancouver</option> |
| <option value="chicago-author-date">Chicago (author-date)</option> |
| <option value="harvard1">Harvard</option> |
| </select> |
| </FieldGroup> |
| |
| <FieldGroup label="Primary color"> |
| <HueSlider |
| hue={hue} |
| onChange={(h) => { |
| setHue(h); |
| settingsMap?.set("primaryHue", h); |
| }} |
| /> |
| </FieldGroup> |
| |
| <hr className="divider-h" /> |
| |
| <SwitchField |
| label="Show PDF download" |
| checked={data.showPdf} |
| onChange={(v) => update("showPdf", v)} |
| /> |
| |
| <SwitchField |
| label="Auto-collapse TOC" |
| checked={data.tableOfContentsAutoCollapse} |
| onChange={(v) => update("tableOfContentsAutoCollapse", v)} |
| /> |
| |
| <hr className="divider-h" /> |
| |
| <FieldGroup label="DOI"> |
| <input |
| className="form-input" |
| placeholder="10.xxxx/xxxxx" |
| value={data.doi} |
| onChange={(e) => update("doi", e.target.value)} |
| /> |
| </FieldGroup> |
| |
| <FieldGroup label="Licence"> |
| <textarea |
| className="form-input" |
| rows={2} |
| placeholder="e.g. CC BY 4.0 (HTML allowed)" |
| value={data.licence} |
| onChange={(e) => update("licence", e.target.value)} |
| /> |
| </FieldGroup> |
| |
| <FieldGroup label="SEO thumbnail image"> |
| <input |
| className="form-input" |
| placeholder="https://..." |
| value={data.seoThumbImage} |
| onChange={(e) => update("seoThumbImage", e.target.value)} |
| /> |
| </FieldGroup> |
| |
| <SwitchField |
| label="PDF Pro only" |
| checked={data.pdfProOnly} |
| onChange={(v) => update("pdfProOnly", v)} |
| /> |
| </div> |
| </div> |
| </aside> |
| </> |
| ); |
| } |
|
|
| function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) { |
| return ( |
| <div> |
| <label className="field-label">{label}</label> |
| {children} |
| </div> |
| ); |
| } |
|
|
| function SwitchField({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) { |
| return ( |
| <label className="form-switch-label"> |
| <span className="form-switch"> |
| <input |
| type="checkbox" |
| role="switch" |
| checked={checked} |
| onChange={(e) => onChange(e.target.checked)} |
| /> |
| <span className="form-switch__track" /> |
| <span className="form-switch__thumb" /> |
| </span> |
| {label} |
| </label> |
| ); |
| } |
|
|