From 754e9d67ede6c27f9c8627d8b978e6773c104e00 Mon Sep 17 00:00:00 2001 From: Kamisato Date: Tue, 7 Apr 2026 13:49:35 -0700 Subject: [PATCH 1/9] Add layout customization options (columns 1-3, text sizing 8-12pt) - Backend: Dynamic LaTeX header/footer generation with columns/font_size/margins support - Backend: generate_sheet endpoint now accepts layout options from request - Frontend: LayoutOptions component with column and text size dropdowns - Frontend: Compile button regenerates LaTeX with current layout options - All 26 backend tests pass --- backend/api/latex_utils.py | 64 +++++++++++++++++- backend/api/views.py | 14 ++-- frontend/src/App.css | 42 ++++++++++++ frontend/src/components/CreateCheatSheet.jsx | 68 ++++++++++++++++++-- frontend/src/hooks/latex.js | 52 +++++++++++++-- 5 files changed, 219 insertions(+), 21 deletions(-) diff --git a/backend/api/latex_utils.py b/backend/api/latex_utils.py index 81aa7e0..9b84b8c 100644 --- a/backend/api/latex_utils.py +++ b/backend/api/latex_utils.py @@ -24,13 +24,71 @@ \end{document} """ -def build_latex_for_formulas(selected_formulas): +# Font size to LaTeX size command mapping for cheat sheet density +FONT_SIZE_MAP = { + "8pt": "\\tiny", + "9pt": "\\scriptsize", + "10pt": "\\footnotesize", + "11pt": "\\small", + "12pt": "\\normalsize", +} + + +def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in"): + """ + Build a dynamic LaTeX header based on user-selected options. + """ + size_command = FONT_SIZE_MAP.get(font_size, "\\footnotesize") + + header_lines = [ + f"\\documentclass[{font_size},fleqn]{{article}}", + f"\\usepackage[margin={margins}]{{geometry}}", + "\\usepackage{amsmath, amssymb}", + "\\usepackage{enumitem}", + "\\usepackage{multicol}", + "\\usepackage{titlesec}", + "", + "\\setlength{\\mathindent}{0pt}", + "\\setlist[itemize]{noitemsep, topsep=0pt, leftmargin=*}", + "\\pagestyle{empty}", + "", + "\\titlespacing*{\\subsection}{0pt}{2pt}{1pt}", + "\\titlespacing*{\\section}{0pt}{4pt}{2pt}", + "", + "\\begin{document}", + size_command, + ] + + if columns > 1: + header_lines.append(f"\\begin{{multicols}}{{{columns}}}") + + header_lines.append("") + return "\n".join(header_lines) + + +def build_dynamic_footer(columns=2): + """ + Build a dynamic LaTeX footer based on user-selected options. + """ + footer_lines = [] + + if columns > 1: + footer_lines.append("\\end{multicols}") + + footer_lines.append("\\end{document}") + return "\n".join(footer_lines) + + +def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in"): """ Given a list of selected formulas (each with class_name, category, name, latex), build a complete LaTeX document. """ + header = build_dynamic_header(columns, font_size, margins) + footer = build_dynamic_footer(columns) + if not selected_formulas: - return LATEX_HEADER + LATEX_FOOTER + return header + footer body_lines = [] current_class = None @@ -81,7 +139,7 @@ def build_latex_for_formulas(selected_formulas): body_lines.append(r"\end{flushleft}") body = "\n".join(body_lines) - return LATEX_HEADER + body + LATEX_FOOTER + return header + body + footer def compile_latex_to_pdf(content): """ diff --git a/backend/api/views.py b/backend/api/views.py index dc2070d..a7ef67f 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -36,17 +36,21 @@ def get_classes(request): def generate_sheet(request): """ POST /api/generate-sheet/ - Accepts { "formulas": [...] } + Accepts { "formulas": [...], "columns": 2, "font_size": "10pt", "margins": "0.25in" } Each formula: { "class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula" } Or for special classes (like UNIT CIRCLE): { "class": "UNIT CIRCLE", "name": "Unit Circle (Key Angles)" } Returns { "tex_code": "..." } """ selected = request.data.get("formulas", []) + columns = request.data.get("columns", 2) + font_size = request.data.get("font_size", "10pt") + margins = request.data.get("margins", "0.25in") if not selected: return Response({"error": "No formulas selected"}, status=400) - # Get formula details from formula_data + columns = max(1, min(3, int(columns))) + formula_data = get_formula_data() selected_formulas = [] @@ -55,14 +59,12 @@ def generate_sheet(request): category = sel.get("category") name = sel.get("name") - # Check if this is a special class (no categories) if is_special_class(class_name): - # Get the formula directly for special classes formula = get_special_class_formula(class_name) if formula: selected_formulas.append({ "class_name": class_name, - "category": class_name, # Use class name as category for special + "category": class_name, "name": formula["name"], "latex": formula["latex"] }) @@ -82,7 +84,7 @@ def generate_sheet(request): if not selected_formulas: return Response({"error": "No valid formulas found"}, status=400) - tex_code = build_latex_for_formulas(selected_formulas) + tex_code = build_latex_for_formulas(selected_formulas, columns, font_size, margins) return Response({"tex_code": tex_code}) diff --git a/frontend/src/App.css b/frontend/src/App.css index 0dba667..d2a0625 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -396,6 +396,48 @@ body { border-radius: 8px; } +/* ---- Layout Options ---- */ +.layout-options { + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 8px; +} + +.layout-controls { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.layout-control { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.layout-control label { + font-weight: 500; + color: var(--text); +} + +.layout-select { + padding: 0.4rem 0.8rem; + border-radius: 6px; + border: 1px solid var(--input-border); + background: var(--input-bg); + color: var(--input-text); + font-size: 0.875rem; + cursor: pointer; +} + +.layout-select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + /* ---- Class + Dropdown Selection ---- */ .category-dropdowns { margin-top: 1rem; diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 705c767..1ca222e 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -313,7 +313,7 @@ const FormulaSelection = ({ ); -const LatexEditor = ({ content, setContent, handlePreview, isCompiling }) => ( +const LatexEditor = ({ content, setContent, handleCompileClick, isCompiling }) => ( <>
@@ -330,7 +330,7 @@ const LatexEditor = ({ content, setContent, handlePreview, isCompiling }) => (
); +const LayoutOptions = ({ columns, setColumns, fontSize, setFontSize }) => ( +
+ +
+
+ + +
+
+ + +
+
+
+); + const CreateCheatSheet = ({ onSave, initialData }) => { const { classesData, @@ -446,6 +484,10 @@ const CreateCheatSheet = ({ onSave, initialData }) => { setTitle, content, setContent, + columns, + setColumns, + fontSize, + setFontSize, pdfBlob, isGenerating, isCompiling, @@ -458,6 +500,15 @@ const CreateCheatSheet = ({ onSave, initialData }) => { clearLatex } = useLatex(initialData); + const handleCompileClick = () => { + const formulasList = getSelectedFormulasList(); + if (formulasList.length > 0) { + handlePreview(null, { formulas: formulasList, columns, fontSize }); + } else { + handlePreview(); + } + }; + const handleGenerate = () => { const formulasList = getSelectedFormulasList(); handleGenerateSheet(formulasList); @@ -465,14 +516,14 @@ const CreateCheatSheet = ({ onSave, initialData }) => { const handleSave = (e) => { e.preventDefault(); - onSave({ title, content }); + onSave({ title, content, columns, fontSize }); }; const handleClear = () => { if (window.confirm('Are you sure you want to clear everything? This cannot be undone.')) { clearLatex(); clearSelections(); - onSave({ title: '', content: '' }, false); + onSave({ title: '', content: '', columns: 2, fontSize: '10pt' }, false); } }; @@ -510,11 +561,18 @@ const CreateCheatSheet = ({ onSave, initialData }) => { onRemoveFormula={removeSingleFormula} /> + +
diff --git a/frontend/src/hooks/latex.js b/frontend/src/hooks/latex.js index 06c23ad..c7623c4 100644 --- a/frontend/src/hooks/latex.js +++ b/frontend/src/hooks/latex.js @@ -3,6 +3,8 @@ import { useState, useRef, useEffect, useCallback } from 'react'; export function useLatex(initialData) { const [title, setTitle] = useState(initialData?.title ?? ''); const [content, setContent] = useState(initialData?.content ?? ''); + const [columns, setColumns] = useState(initialData?.columns ?? 2); + const [fontSize, setFontSize] = useState(initialData?.fontSize ?? '10pt'); const [pdfBlob, setPdfBlob] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isCompiling, setIsCompiling] = useState(false); @@ -16,13 +18,38 @@ export function useLatex(initialData) { if (initialData) { setTitle(initialData.title ?? ''); setContent(initialData.content ?? ''); + setColumns(initialData.columns ?? 2); + setFontSize(initialData.fontSize ?? '10pt'); } }, [initialData]); - const handlePreview = useCallback(async (latexContent = null) => { + const handlePreview = useCallback(async (latexContent = null, regenerateOptions = null) => { if (isCompilingRef.current) return; - const contentToCompile = latexContent || content; + let contentToCompile = latexContent || content; + + if (regenerateOptions) { + try { + const response = await fetch('/api/generate-sheet/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + formulas: regenerateOptions.formulas, + columns: regenerateOptions.columns, + font_size: regenerateOptions.fontSize, + margins: '0.25in' + }), + }); + if (response.ok) { + const data = await response.json(); + contentToCompile = data.tex_code; + setContent(data.tex_code); + } + } catch (e) { + console.error('Failed to regenerate:', e); + } + } + if (!contentToCompile) return; isCompilingRef.current = true; @@ -71,13 +98,18 @@ export function useLatex(initialData) { const response = await fetch('/api/generate-sheet/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ formulas: selectedList }), + body: JSON.stringify({ + formulas: selectedList, + columns: columns, + font_size: fontSize, + margins: '0.25in' + }), }); if (!response.ok) throw new Error('Failed to generate sheet'); - const data = await response.json(); - setContent(data.tex_code); - setPdfBlob(null); - handlePreview(data.tex_code); + const data = await response.json(); + setContent(data.tex_code); + setPdfBlob(null); + handlePreview(data.tex_code, null); } catch (error) { console.error('Error generating sheet:', error); alert('Failed to generate LaTeX. Is the backend running?'); @@ -132,6 +164,8 @@ export function useLatex(initialData) { const clearLatex = () => { setTitle(''); setContent(''); + setColumns(2); + setFontSize('10pt'); setPdfBlob(null); setCompileError(null); }; @@ -141,6 +175,10 @@ export function useLatex(initialData) { setTitle, content, setContent, + columns, + setColumns, + fontSize, + setFontSize, pdfBlob, isGenerating, isCompiling, From 7d54d3b5e62456e17c570c0e83c4292acdd46af0 Mon Sep 17 00:00:00 2001 From: Kamisato Date: Tue, 7 Apr 2026 13:50:00 -0700 Subject: [PATCH 2/9] Fix 3-column content overflow with raggedcolumns Add \raggedcolumns after \begin{multicols} to prevent content bleeding between columns when using 3-column layout. --- backend/api/latex_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/api/latex_utils.py b/backend/api/latex_utils.py index 9b84b8c..4b1bb5f 100644 --- a/backend/api/latex_utils.py +++ b/backend/api/latex_utils.py @@ -61,6 +61,7 @@ def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in"): if columns > 1: header_lines.append(f"\\begin{{multicols}}{{{columns}}}") + header_lines.append("\\raggedcolumns") header_lines.append("") return "\n".join(header_lines) From aa50292eff8c3bd945e7d10dde8ee2b5c3ac5e5a Mon Sep 17 00:00:00 2001 From: Kamisato Date: Tue, 7 Apr 2026 14:12:59 -0700 Subject: [PATCH 3/9] Add spacing option and improve layout proportions - Add spacing dropdown (tiny/small/medium/large) controlling vertical spacing - Reduce section/subsection header sizes via titleformat - Center editor layout with 42.5%/42.5% pane widths and 2.5% margins --- backend/api/latex_utils.py | 24 +++++++++++++----- backend/api/views.py | 5 ++-- frontend/src/App.css | 17 +++++++------ frontend/src/components/CreateCheatSheet.jsx | 26 +++++++++++++++++--- frontend/src/hooks/latex.js | 7 ++++++ 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/backend/api/latex_utils.py b/backend/api/latex_utils.py index 4b1bb5f..9a33e2a 100644 --- a/backend/api/latex_utils.py +++ b/backend/api/latex_utils.py @@ -33,12 +33,21 @@ "12pt": "\\normalsize", } +# Spacing presets: (section_before, section_after, subsection_before, subsection_after, formula_spacing) +SPACING_MAP = { + "tiny": ("1pt", "0.5pt", "0.5pt", "0.25pt", "1pt"), + "small": ("2pt", "1pt", "1pt", "0.5pt", "2pt"), + "medium": ("3pt", "1.5pt", "1.5pt", "0.75pt", "3pt"), + "large": ("4pt", "2pt", "2pt", "1pt", "4pt"), +} + -def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in"): +def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing="large"): """ Build a dynamic LaTeX header based on user-selected options. """ size_command = FONT_SIZE_MAP.get(font_size, "\\footnotesize") + sec_before, sec_after, subsec_before, subsec_after, _ = SPACING_MAP.get(spacing, SPACING_MAP["large"]) header_lines = [ f"\\documentclass[{font_size},fleqn]{{article}}", @@ -52,8 +61,10 @@ def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in"): "\\setlist[itemize]{noitemsep, topsep=0pt, leftmargin=*}", "\\pagestyle{empty}", "", - "\\titlespacing*{\\subsection}{0pt}{2pt}{1pt}", - "\\titlespacing*{\\section}{0pt}{4pt}{2pt}", + f"\\titleformat{{\\section}}{{\\normalfont\\footnotesize\\bfseries}}{{}}{{0pt}}{{}}", + f"\\titleformat{{\\subsection}}{{\\normalfont\\scriptsize\\bfseries}}{{}}{{0pt}}{{}}", + f"\\titlespacing*{{\\section}}{{0pt}}{{{sec_before}}}{{{sec_after}}}", + f"\\titlespacing*{{\\subsection}}{{0pt}}{{{subsec_before}}}{{{subsec_after}}}", "", "\\begin{document}", size_command, @@ -80,13 +91,14 @@ def build_dynamic_footer(columns=2): return "\n".join(footer_lines) -def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in"): +def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in", spacing="large"): """ Given a list of selected formulas (each with class_name, category, name, latex), build a complete LaTeX document. """ - header = build_dynamic_header(columns, font_size, margins) + header = build_dynamic_header(columns, font_size, margins, spacing) footer = build_dynamic_footer(columns) + _, _, _, _, formula_gap = SPACING_MAP.get(spacing, SPACING_MAP["large"]) if not selected_formulas: return header + footer @@ -134,7 +146,7 @@ def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", mar escaped_name = name.replace("\\", "\\textbackslash ").replace("&", "\\&").replace("%", "\\%").replace("#", "\\#").replace("_", "\\_").replace("^", "\\textasciicircum ").replace("{", "\\{").replace("}", "\\}") body_lines.append("\\textbf{" + escaped_name + "}") body_lines.append("\\[ " + latex + " \\]") - body_lines.append("\\\\[4pt]") + body_lines.append(f"\\\\[{formula_gap}]") if in_flushleft: body_lines.append(r"\end{flushleft}") diff --git a/backend/api/views.py b/backend/api/views.py index a7ef67f..942cbba 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -36,7 +36,7 @@ def get_classes(request): def generate_sheet(request): """ POST /api/generate-sheet/ - Accepts { "formulas": [...], "columns": 2, "font_size": "10pt", "margins": "0.25in" } + Accepts { "formulas": [...], "columns": 2, "font_size": "10pt", "margins": "0.25in", "spacing": "large" } Each formula: { "class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula" } Or for special classes (like UNIT CIRCLE): { "class": "UNIT CIRCLE", "name": "Unit Circle (Key Angles)" } Returns { "tex_code": "..." } @@ -45,6 +45,7 @@ def generate_sheet(request): columns = request.data.get("columns", 2) font_size = request.data.get("font_size", "10pt") margins = request.data.get("margins", "0.25in") + spacing = request.data.get("spacing", "large") if not selected: return Response({"error": "No formulas selected"}, status=400) @@ -84,7 +85,7 @@ def generate_sheet(request): if not selected_formulas: return Response({"error": "No valid formulas found"}, status=400) - tex_code = build_latex_for_formulas(selected_formulas, columns, font_size, margins) + tex_code = build_latex_for_formulas(selected_formulas, columns, font_size, margins, spacing) return Response({"tex_code": tex_code}) diff --git a/frontend/src/App.css b/frontend/src/App.css index d2a0625..384771b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -129,7 +129,7 @@ body { font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; max-width: 100%; margin: 0; - padding: 0.5rem; + padding: 0.5rem 1.25%; } .create-cheat-sheet { @@ -149,20 +149,21 @@ body { .editor-container { display: flex !important; flex-direction: row !important; - gap: 1rem; + justify-content: center; + gap: 12.5%; margin: 0.5rem 0; width: 100%; } .input-section { - flex: 1 1 50%; + flex: 0 0 42.5%; min-width: 0; display: flex; flex-direction: column; } .preview-section { - flex: 1 1 50%; + flex: 0 0 42.5%; min-width: 0; background: transparent; padding: 0; @@ -172,9 +173,9 @@ body { .textarea-field { width: 100%; - height: 800px; + height: 850px; flex-grow: 1; - max-height: 80vh; + max-height: 85vh; padding: 1rem; font-family: 'Consolas', monospace; font-size: 14px; @@ -251,9 +252,9 @@ body { background: var(--box-bg); border: 1px solid var(--border); border-radius: 4px; - height: 800px; + height: 850px; flex-grow: 1; - max-height: 80vh; + max-height: 85vh; overflow-y: auto; overflow-x: hidden; text-align: left; diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 1ca222e..6e5e80e 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -423,7 +423,7 @@ const ActionToolbar = ({ handleDownloadTex, handleDownloadPDF, isLoading, conten
); -const LayoutOptions = ({ columns, setColumns, fontSize, setFontSize }) => ( +const LayoutOptions = ({ columns, setColumns, fontSize, setFontSize, spacing, setSpacing }) => (
+
+ + +
); @@ -488,6 +502,8 @@ const CreateCheatSheet = ({ onSave, initialData }) => { setColumns, fontSize, setFontSize, + spacing, + setSpacing, pdfBlob, isGenerating, isCompiling, @@ -503,7 +519,7 @@ const CreateCheatSheet = ({ onSave, initialData }) => { const handleCompileClick = () => { const formulasList = getSelectedFormulasList(); if (formulasList.length > 0) { - handlePreview(null, { formulas: formulasList, columns, fontSize }); + handlePreview(null, { formulas: formulasList, columns, fontSize, spacing }); } else { handlePreview(); } @@ -516,14 +532,14 @@ const CreateCheatSheet = ({ onSave, initialData }) => { const handleSave = (e) => { e.preventDefault(); - onSave({ title, content, columns, fontSize }); + onSave({ title, content, columns, fontSize, spacing }); }; const handleClear = () => { if (window.confirm('Are you sure you want to clear everything? This cannot be undone.')) { clearLatex(); clearSelections(); - onSave({ title: '', content: '', columns: 2, fontSize: '10pt' }, false); + onSave({ title: '', content: '', columns: 2, fontSize: '10pt', spacing: 'large' }, false); } }; @@ -566,6 +582,8 @@ const CreateCheatSheet = ({ onSave, initialData }) => { setColumns={setColumns} fontSize={fontSize} setFontSize={setFontSize} + spacing={spacing} + setSpacing={setSpacing} />
diff --git a/frontend/src/hooks/latex.js b/frontend/src/hooks/latex.js index c7623c4..9015fda 100644 --- a/frontend/src/hooks/latex.js +++ b/frontend/src/hooks/latex.js @@ -5,6 +5,7 @@ export function useLatex(initialData) { const [content, setContent] = useState(initialData?.content ?? ''); const [columns, setColumns] = useState(initialData?.columns ?? 2); const [fontSize, setFontSize] = useState(initialData?.fontSize ?? '10pt'); + const [spacing, setSpacing] = useState(initialData?.spacing ?? 'large'); const [pdfBlob, setPdfBlob] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isCompiling, setIsCompiling] = useState(false); @@ -20,6 +21,7 @@ export function useLatex(initialData) { setContent(initialData.content ?? ''); setColumns(initialData.columns ?? 2); setFontSize(initialData.fontSize ?? '10pt'); + setSpacing(initialData.spacing ?? 'large'); } }, [initialData]); @@ -37,6 +39,7 @@ export function useLatex(initialData) { formulas: regenerateOptions.formulas, columns: regenerateOptions.columns, font_size: regenerateOptions.fontSize, + spacing: regenerateOptions.spacing, margins: '0.25in' }), }); @@ -102,6 +105,7 @@ export function useLatex(initialData) { formulas: selectedList, columns: columns, font_size: fontSize, + spacing: spacing, margins: '0.25in' }), }); @@ -166,6 +170,7 @@ export function useLatex(initialData) { setContent(''); setColumns(2); setFontSize('10pt'); + setSpacing('large'); setPdfBlob(null); setCompileError(null); }; @@ -179,6 +184,8 @@ export function useLatex(initialData) { setColumns, fontSize, setFontSize, + spacing, + setSpacing, pdfBlob, isGenerating, isCompiling, From ffe6318baf43dcb2630e082fb36904f84d2baf5f Mon Sep 17 00:00:00 2001 From: Kamisato Date: Tue, 7 Apr 2026 17:31:07 -0700 Subject: [PATCH 4/9] Add auto-scaling formulas, line-numbered editor, and split-pane UI --- README.md | 5 +- backend/api/latex_utils.py | 14 +- frontend/src/App.css | 1039 ++++++++++-------- frontend/src/App.jsx | 2 +- frontend/src/components/CreateCheatSheet.jsx | 89 +- frontend/src/index.css | 8 +- 6 files changed, 684 insertions(+), 473 deletions(-) diff --git a/README.md b/README.md index c4960b5..c6cc94d 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ A full-stack web application for generating LaTeX-based cheat sheets. Users sele - **Category Selection**: Select categories with checkboxes for each class (no Ctrl/Cmd needed) - **Drag-and-Drop Reordering**: Intuitively organize your cheat sheet by dragging individual formulas or entire collapsible class groups to reorder them before generating. - **Formula Generation**: Automatically generates formatted LaTeX for selected formulas -- **Live Preview**: Split-view interface with LaTeX code editor and PDF preview +- **Live Preview**: Split-pane interface with line-numbered LaTeX editor (45% width) and PDF preview (45% width) - **PDF Compilation**: Compile to PDF using Tectonic LaTeX engine on the backend - **Download Options**: Download as `.tex` source or `.pdf` ### Formatting Options -- **Column Layout**: Single, two, or three column layouts +- **Column Layout**: Single, two, or three column layouts with auto-scaling formulas that fit within column boundaries - **Margins**: Adjustable page margins - **Font Size**: Configurable font scaling (8pt - 14pt) @@ -43,6 +43,7 @@ A full-stack web application for generating LaTeX-based cheat sheets. Users sele │ ├── api/ # Main API app │ │ ├── views.py # API endpoints │ │ ├── models.py # Database models +│ │ ├── latex_utils.py # LaTeX generation utilities │ │ ├── urls.py # URL routing │ │ ├── serializers.py # DRF serializers │ │ ├── tests.py # Test suite diff --git a/backend/api/latex_utils.py b/backend/api/latex_utils.py index 9a33e2a..c12047e 100644 --- a/backend/api/latex_utils.py +++ b/backend/api/latex_utils.py @@ -56,6 +56,7 @@ def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing= "\\usepackage{enumitem}", "\\usepackage{multicol}", "\\usepackage{titlesec}", + "\\usepackage{graphicx}", # For \resizebox to constrain equation width "", "\\setlength{\\mathindent}{0pt}", "\\setlist[itemize]{noitemsep, topsep=0pt, leftmargin=*}", @@ -66,6 +67,17 @@ def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing= f"\\titlespacing*{{\\section}}{{0pt}}{{{sec_before}}}{{{sec_after}}}", f"\\titlespacing*{{\\subsection}}{{0pt}}{{{subsec_before}}}{{{subsec_after}}}", "", + # Command to auto-scale equations that exceed column width + # This macro is designed to be used INSIDE math mode: \[ \fitmath{...} \] + "\\newcommand{\\fitmath}[1]{%", + " \\sbox0{$\\displaystyle#1$}%", + " \\ifdim\\wd0>\\linewidth", + " \\resizebox{\\linewidth}{!}{\\ensuremath{\\displaystyle#1}}%", + " \\else", + " #1%", + " \\fi", + "}", + "", "\\begin{document}", size_command, ] @@ -145,7 +157,7 @@ def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", mar # Escape special LaTeX characters in the formula name escaped_name = name.replace("\\", "\\textbackslash ").replace("&", "\\&").replace("%", "\\%").replace("#", "\\#").replace("_", "\\_").replace("^", "\\textasciicircum ").replace("{", "\\{").replace("}", "\\}") body_lines.append("\\textbf{" + escaped_name + "}") - body_lines.append("\\[ " + latex + " \\]") + body_lines.append("\\[ \\fitmath{" + latex + "} \\]") body_lines.append(f"\\\\[{formula_gap}]") if in_flushleft: diff --git a/frontend/src/App.css b/frontend/src/App.css index 384771b..1518655 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,683 +1,776 @@ +/* ========================================================================== + DESIGN TOKENS + ========================================================================== */ :root { - --bg: #18181c; - --text: #f2f2f2; - --panel-bg: #23232b; - --border: #333; - --primary: #3498db; - --card-bg: #23232b; - --box-bg: #222127; - --input-bg: #18181c; - --input-border: #444550; - --input-text: #dbeaff; - --btn-primary: #3498db; - --btn-primary-hover: #2980b9; - --btn-download: #10b981; - --btn-download-hover: #059669; - --btn-clear: #f43f5e; - --btn-clear-hover: #e11d48; - --btn-preview: #3498db; - --btn-preview-hover: #2980b9; - --btn-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); - --btn-shadow-hover: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); -} - -[data-theme="light"] { - --bg: #f9f9fa; - --text: #18181c; - --panel-bg: #ffffff; - --border: #e2e8f0; + /* Colors - Dark theme */ + --bg: #121215; + --text: #f4f4f5; + --text-muted: #a1a1aa; + --panel-bg: #1c1c21; + --border: #2d2d35; + --border-subtle: #252530; --primary: #3b82f6; - --card-bg: #f3f4f6; - --box-bg: #f8fafc; - --input-bg: #ffffff; - --input-border: #cbd5e1; - --input-text: #1e293b; + --primary-hover: #2563eb; + --card-bg: #1c1c21; + --box-bg: #18181b; + --input-bg: #0f0f12; + --input-border: #3f3f46; + --input-text: #fafafa; + + /* Button colors */ --btn-primary: #3b82f6; --btn-primary-hover: #2563eb; --btn-download: #10b981; --btn-download-hover: #059669; --btn-clear: #ef4444; --btn-clear-hover: #dc2626; - --btn-preview: #3b82f6; - --btn-preview-hover: #2563eb; - --btn-shadow: 0 1px 2px rgba(0,0,0,0.05); - --btn-shadow-hover: 0 4px 6px rgba(0,0,0,0.1); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.3); + --shadow-inset: inset 0 1px 3px rgba(0, 0, 0, 0.4); + + /* Spacing */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + + /* Border radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-base: 200ms ease; + --transition-slow: 300ms ease; } +[data-theme="light"] { + --bg: #f8fafc; + --text: #18181b; + --text-muted: #71717a; + --panel-bg: #ffffff; + --border: #e4e4e7; + --border-subtle: #f4f4f5; + --primary: #3b82f6; + --primary-hover: #2563eb; + --card-bg: #f4f4f5; + --box-bg: #fafafa; + --input-bg: #ffffff; + --input-border: #d4d4d8; + --input-text: #18181b; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + --shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.06); +} + +/* ========================================================================== + BASE STYLES + ========================================================================== */ * { - font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; + box-sizing: border-box; } body { - transition: background-color 0.3s, color 0.3s; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: var(--bg); + color: var(--text); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition: background-color var(--transition-slow), color var(--transition-slow); } .App { - max-width: 100%; + width: 100%; + max-width: none; margin: 0; - padding: 0.5rem; + padding: var(--space-md) 0; } -.create-cheat-sheet { - background: var(--panel-bg); - padding: 0.5rem; - border-radius: 8px; +/* ========================================================================== + TYPOGRAPHY + ========================================================================== */ +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + letter-spacing: -0.02em; + color: var(--text); + margin: 0; } -.btn { - padding: 0.55rem 1.1rem; - border: 1px solid transparent; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - margin-right: 0.6rem; +label { font-size: 0.875rem; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: var(--btn-shadow); + font-weight: 500; + color: var(--text); letter-spacing: 0.01em; } -.btn.primary { - background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); - color: white; - border-color: #2980b9; -} - -.btn.primary:hover { - background: linear-gradient(135deg, #5dade2 0%, #3498db 100%); - box-shadow: var(--btn-shadow-hover); - transform: translateY(-1px); -} - -.btn.download { - background: linear-gradient(135deg, #10b981 0%, #059669 100%); - color: white; - border-color: #059669; -} - -.btn.download:hover { - background: linear-gradient(135deg, #34d399 0%, #10b981 100%); - box-shadow: var(--btn-shadow-hover); - transform: translateY(-1px); -} - -.btn.clear { - background: transparent; - color: #f43f5e; - border: 1px solid #f43f5e; -} - -.btn.clear:hover { - background: #f43f5e; - color: white; - box-shadow: var(--btn-shadow-hover); - transform: translateY(-1px); -} - -* { - font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -body { - font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; - background-color: var(--bg); - color: var(--text); - transition: background-color 0.3s ease, color 0.3s ease; +/* ========================================================================== + PANELS & CONTAINERS + ========================================================================== */ +.create-cheat-sheet { + background: var(--panel-bg); + width: 100%; + max-width: none; + padding: var(--space-lg) var(--space-md); + border-radius: var(--radius-lg); + border: 1px solid var(--border-subtle); + box-shadow: var(--shadow-md); + margin-bottom: var(--space-md); } -.App { - font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; - max-width: 100%; - margin: 0; - padding: 0.5rem 1.25%; +.selection-panel { + max-width: 900px; + margin-left: auto; + margin-right: auto; + padding: var(--space-lg) var(--space-xl); } -.create-cheat-sheet { - background: var(--panel-bg); - padding: 0.5rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - margin-bottom: 2rem; +.app-header { + margin-bottom: var(--space-md); } .create-cheat-sheet h2 { margin-top: 0; - margin-bottom: 1.5rem; + margin-bottom: var(--space-lg); + font-size: 1.25rem; color: var(--text); } +/* ========================================================================== + EDITOR LAYOUT + ========================================================================== */ .editor-container { - display: flex !important; - flex-direction: row !important; + display: flex; + flex-direction: row; justify-content: center; - gap: 12.5%; - margin: 0.5rem 0; + align-items: stretch; + gap: 0.5vw; + margin: var(--space-md) 0; width: 100%; } -.input-section { - flex: 0 0 42.5%; +.input-section, +.preview-section { + flex: 0 0 45vw; + width: 45vw; + max-width: 45vw; min-width: 0; display: flex; flex-direction: column; } .preview-section { - flex: 0 0 42.5%; - min-width: 0; background: transparent; padding: 0; +} + +.input-section label, +.preview-section label { + display: block; + font-weight: 600; + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin-bottom: var(--space-sm); +} + +.compile-button-column { display: flex; - flex-direction: column; + align-items: center; + justify-content: center; + flex: 0 0 3.6vw; + min-width: 44px; } -.textarea-field { +/* ========================================================================== + FORM ELEMENTS + ========================================================================== */ +.form-group label { + display: block; + margin-bottom: var(--space-sm); + font-weight: 600; + font-size: 0.875rem; +} + +.input-field { width: 100%; - height: 850px; - flex-grow: 1; - max-height: 85vh; - padding: 1rem; - font-family: 'Consolas', monospace; - font-size: 14px; + padding: 0.75rem var(--space-md); + margin-bottom: var(--space-lg); background-color: var(--input-bg); color: var(--input-text); + border: 1px solid var(--input-border); + border-radius: var(--radius-md); + font-size: 1rem; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.input-field:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +.input-field::placeholder { + color: var(--text-muted); +} + +.editor-wrapper { + display: flex; + height: 850px; + max-height: 85vh; + min-height: 480px; border: 1px solid var(--border); - border-radius: 4px; - resize: vertical; - box-sizing: border-box; + border-radius: var(--radius-md); + overflow: hidden; + background-color: var(--input-bg); + box-shadow: var(--shadow-inset); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } -.input-field { +.editor-wrapper:focus-within { + border-color: var(--primary); + box-shadow: var(--shadow-inset), 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.line-numbers { + flex-shrink: 0; + width: 48px; + padding: var(--space-md) var(--space-sm); + background: var(--box-bg); + border-right: 1px solid var(--border); + overflow: hidden; + user-select: none; + text-align: right; +} + +.line-number { + font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + font-size: 13px; + line-height: 1.6; + color: var(--text-muted); + height: calc(13px * 1.6); +} + +.textarea-field { + flex: 1; width: 100%; - padding: 0.8rem; - margin-bottom: 1.5rem; + height: 100%; + padding: var(--space-md); + font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + font-size: 13px; + line-height: 1.6; background-color: var(--input-bg); color: var(--input-text); - border: 1px solid var(--border); - border-radius: 4px; - font-size: 1.1rem; + border: none; + border-radius: 0; + resize: none; + outline: none; } -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: bold; +.textarea-field:focus { + outline: none; + border-color: var(--primary); + box-shadow: var(--shadow-inset), 0 0 0 3px rgba(59, 130, 246, 0.1); } -.actions { - display: flex; - gap: 0.5rem; - margin-top: 2rem; +.layout-select { + padding: 0.5rem 0.75rem; + border-radius: var(--radius-md); + border: 1px solid var(--input-border); + background: var(--input-bg); + color: var(--input-text); + font-size: 0.875rem; + cursor: pointer; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } +.layout-select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +/* ========================================================================== + BUTTONS + ========================================================================== */ .btn { - padding: 0.6rem 1.2rem; - border: none; - border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + padding: 0.625rem 1.25rem; + border: 1px solid transparent; + border-radius: var(--radius-md); cursor: pointer; - font-weight: 600; - margin-right: 0.8rem; - font-size: 0.9rem; - transition: background-color 0.2s; + font-weight: 500; + font-size: 0.875rem; + letter-spacing: 0.01em; + transition: all var(--transition-fast); + box-shadow: var(--shadow-sm); +} + +.btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); } .btn.primary { - background-color: var(--btn-primary); + background: linear-gradient(180deg, #4f8ff7 0%, var(--btn-primary) 100%); color: white; + border-color: var(--btn-primary-hover); } .btn.primary:hover { - background-color: var(--btn-primary-hover); + background: linear-gradient(180deg, #6ba3f9 0%, #4f8ff7 100%); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn.primary:active { + transform: translateY(0); + box-shadow: var(--shadow-sm); } .btn.download { - background-color: var(--btn-download); + background: linear-gradient(180deg, #34d399 0%, var(--btn-download) 100%); color: white; + border-color: var(--btn-download-hover); } .btn.download:hover { - background-color: var(--btn-download-hover); + background: linear-gradient(180deg, #4ade80 0%, #34d399 100%); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn.download:active { + transform: translateY(0); } .btn.clear { - background-color: var(--btn-clear); - color: white; + background: transparent; + color: var(--btn-clear); + border: 1px solid var(--btn-clear); } .btn.clear:hover { - background-color: var(--btn-clear-hover); + background: var(--btn-clear); + color: white; + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn.clear:active { + transform: translateY(0); +} + +.btn.preview { + background: linear-gradient(180deg, #4f8ff7 0%, var(--btn-primary) 100%); + color: white; + border-color: var(--btn-primary-hover); +} + +.btn.preview:hover { + background: linear-gradient(180deg, #6ba3f9 0%, #4f8ff7 100%); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn.preview:disabled, +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.generate-btn { + margin-top: var(--space-sm); + font-size: 0.9375rem; + padding: 0.75rem 1.5rem; +} + +.generate-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Circular compile button */ +.compile-circle { + width: 48px; + height: 48px; + border-radius: var(--radius-full); + background: linear-gradient(180deg, #4f8ff7 0%, var(--btn-primary) 100%); + color: white; + font-size: 1.375rem; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + cursor: pointer; + box-shadow: var(--shadow-md); + transition: all var(--transition-base); +} + +.compile-button-column .compile-circle { + width: 48px; + height: 48px; + font-size: 1.25rem; + margin: 0 auto; } +.compile-circle:hover:not(:disabled) { + background: linear-gradient(180deg, #6ba3f9 0%, #4f8ff7 100%); + transform: rotate(90deg); + box-shadow: var(--shadow-lg); +} + +.compile-circle:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.actions { + display: flex; + gap: var(--space-sm); + margin-top: var(--space-xl); +} + +/* ========================================================================== + PREVIEW BOX + ========================================================================== */ .preview-box { background: var(--box-bg); border: 1px solid var(--border); - border-radius: 4px; + border-radius: var(--radius-md); height: 850px; flex-grow: 1; max-height: 85vh; + min-height: 480px; overflow-y: auto; overflow-x: hidden; text-align: left; width: 100%; - box-sizing: border-box; + box-shadow: var(--shadow-inset); } -/* ---- Class selection checkboxes ---- */ +/* ========================================================================== + CLASS SELECTION CHECKBOXES + ========================================================================== */ .class-checkboxes { display: flex; flex-wrap: wrap; - gap: 0.75rem; - margin-bottom: 1rem; - margin-top: 0.5rem; + gap: var(--space-sm); + margin-bottom: var(--space-md); + margin-top: var(--space-sm); } .class-checkbox-label { display: flex; align-items: center; - gap: 0.4rem; - padding: 0.5rem 1rem; - background: var(--panel-bg); + gap: var(--space-xs); + padding: 0.5rem 0.875rem; + background: var(--box-bg); border: 1px solid var(--border); - border-radius: 6px; + border-radius: var(--radius-md); cursor: pointer; font-weight: 500; - font-size: 0.95rem; - transition: all 0.15s ease; + font-size: 0.875rem; + transition: all var(--transition-fast); user-select: none; } .class-checkbox-label:hover { - border-color: #3498db; - background: var(--card-bg); + border-color: var(--primary); + background: var(--panel-bg); } .class-checkbox-label.checked { - border-color: #3498db; - background: #222a34; - color: #2980b9; - font-weight: 600; + border-color: var(--primary); + background: rgba(59, 130, 246, 0.1); + color: var(--primary); } .category-checkbox-label { display: flex; align-items: center; - gap: 0.4rem; - padding: 0.4rem 0.8rem; - background: var(--panel-bg); + gap: var(--space-xs); + padding: 0.375rem 0.75rem; + background: var(--box-bg); border: 1px solid var(--border); - border-radius: 4px; + border-radius: var(--radius-sm); cursor: pointer; - font-size: 0.9rem; - transition: all 0.15s ease; + font-size: 0.8125rem; + transition: all var(--transition-fast); user-select: none; } .category-checkbox-label:hover { - border-color: #3498db; - background: var(--card-bg); + border-color: var(--primary); + background: var(--panel-bg); } .category-checkbox-label.checked { - border-color: #3498db; - background: #222a34; - color: #2980b9; + border-color: var(--primary); + background: rgba(59, 130, 246, 0.1); + color: var(--primary); } .category-checkbox-label input[type="checkbox"] { - accent-color: #3498db; + accent-color: var(--primary); width: 14px; height: 14px; } -.generate-btn { - margin-top: 0.5rem; - font-size: 1rem; - padding: 0.7rem 1.5rem; -} - -.generate-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn.preview { - background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); - color: white; - border-color: #2980b9; -} - -.btn.preview:hover { - background: linear-gradient(135deg, #5dade2 0%, #3498db 100%); - box-shadow: var(--btn-shadow-hover); - transform: translateY(-1px); -} - -.btn.preview:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -/* ---- Circular compile button ---- */ -.compile-circle { - width: 50px; - height: 50px; - border-radius: 50%; - background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); - color: white; - font-size: 1.5rem; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - align-self: center; - padding: 0; - border: none; - cursor: pointer; - transition: background-color 0.2s, transform 0.2s; -} - -.compile-circle:hover:not(:disabled) { - background-color: var(--btn-preview-hover); - transform: rotate(90deg); -} - -.compile-circle:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* ---- Formula selection ---- */ +/* ========================================================================== + FORMULA SELECTION + ========================================================================== */ .formula-selection { - margin-bottom: 1.5rem; - padding: 1rem; - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 8px; + margin-bottom: var(--space-lg); + padding: var(--space-md) 0; } -/* ---- Layout Options ---- */ +/* ========================================================================== + LAYOUT OPTIONS + ========================================================================== */ .layout-options { - margin-bottom: 1.5rem; - padding: 1rem; - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 8px; + margin-bottom: var(--space-sm); + padding: var(--space-md) 0 0 0; + border-top: 1px solid var(--border); } .layout-controls { display: flex; - gap: 2rem; + gap: var(--space-xl); flex-wrap: wrap; } .layout-control { display: flex; align-items: center; - gap: 0.5rem; + gap: var(--space-sm); } .layout-control label { font-weight: 500; - color: var(--text); -} - -.layout-select { - padding: 0.4rem 0.8rem; - border-radius: 6px; - border: 1px solid var(--input-border); - background: var(--input-bg); - color: var(--input-text); font-size: 0.875rem; - cursor: pointer; -} - -.layout-select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); + color: var(--text); } -/* ---- Class + Dropdown Selection ---- */ +/* ========================================================================== + CATEGORY DROPDOWNS + ========================================================================== */ .category-dropdowns { - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid #e2e8f0; + margin-top: var(--space-md); + padding-top: var(--space-md); + border-top: 1px solid var(--border); } .class-category-section { - margin-bottom: 1rem; + margin-bottom: var(--space-md); } .class-category-label { display: block; font-weight: 600; - margin-bottom: 0.5rem; - color: var(--text); + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin-bottom: var(--space-sm); } .category-checkboxes { display: flex; flex-wrap: wrap; - gap: 0.5rem; + gap: var(--space-sm); } .select-all-label { display: block; - font-size: 0.85rem; - color: #6366f1; - margin-bottom: 0.5rem; + font-size: 0.8125rem; + color: var(--primary); + margin-bottom: var(--space-sm); cursor: pointer; + font-weight: 500; } .select-all-label input { - margin-right: 0.3rem; -} - -.input-section label, -.preview-section label { - display: block; - font-weight: 600; - margin-bottom: 0.5rem; - color: var(--text); + margin-right: var(--space-xs); } -/* ---- Footer ---- */ -.app-footer { - text-align: center; - padding: 1rem; - margin-top: 1rem; -} - -.app-footer a { - color: #999; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.5rem; - border-radius: 50%; - transition: background-color 0.2s, color 0.2s; +/* ========================================================================== + DRAG & DROP / SORTABLE + ========================================================================== */ +.formula-class-group { + margin-bottom: var(--space-sm); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + background: var(--box-bg); + transition: all var(--transition-base); } -.app-footer a:hover { - color: var(--text); - background-color: var(--panel-bg); +.formula-class-group.collapsed { + margin-bottom: var(--space-xs); } -.sortable-formula-item { +.class-group-header { display: flex; align-items: center; - padding: 0.5rem; - margin: 0.25rem 0; - background: var(--box-bg); - border: 1px solid var(--border); - border-radius: 4px; - cursor: grab; - touch-action: none; - user-select: none; + justify-content: space-between; + padding: 0.625rem 0.875rem; + background: linear-gradient(180deg, #4f8ff7 0%, var(--btn-primary) 100%); + color: white; + cursor: pointer; + transition: all var(--transition-fast); } -.sortable-formula-item:hover { - border-color: var(--primary); - background: var(--panel-bg); +.class-group-header:hover { + filter: brightness(1.08); } -.sortable-formula-item:active { +.class-group-header:active { cursor: grabbing; } -.drag-handle { - padding: 0 0.5rem; - color: #888; - font-size: 1.2rem; - cursor: grab; -} - -.formula-name { - flex: 1; - font-weight: 500; +.class-group-title { + font-weight: 600; + font-size: 0.875rem; + user-select: none; + letter-spacing: 0.02em; } -.formula-class { - font-size: 0.8rem; - color: var(--text); - padding: 0.2rem 0.5rem; - background: var(--panel-bg); - border-radius: 3px; - margin-right: 0.5rem; +.class-group-actions { + display: flex; + gap: var(--space-xs); } -.remove-formula { - background: none; +.class-group-btn { + background: rgba(255, 255, 255, 0.2); border: none; - color: #dc3545; - font-size: 1.2rem; + border-radius: var(--radius-sm); + padding: 0.25rem 0.5rem; + font-size: 0.75rem; cursor: pointer; - padding: 0 0.3rem; -} - -.remove-formula:hover { - color: #bd2130; -} - -.reorder-instructions { - font-size: 0.8rem; - color: #888; - margin-bottom: 0.75rem; - font-style: italic; + color: white; + transition: all var(--transition-fast); } -.formula-class-group { - margin-bottom: 0.5rem; - border: 1px solid var(--border); - border-radius: 6px; - overflow: hidden; - background: var(--box-bg); - transition: all 0.2s ease; +.class-group-btn:hover { + background: rgba(255, 255, 255, 0.35); } -.formula-class-group.collapsed { - margin-bottom: 0.25rem; +.class-group-btn.remove { + background: rgba(239, 68, 68, 0.3); } -.sortable-formula-item.nested { - padding-left: 1.5rem; - background: var(--card-bg); - border-left: 3px solid #3498db; +.class-group-btn.remove:hover { + background: var(--btn-clear); } .sortable-formula-item { display: flex; align-items: center; - padding: 0.5rem; - margin: 0.25rem 0; + padding: var(--space-sm); + margin: var(--space-xs) 0; background: var(--box-bg); border: 1px solid var(--border); - border-radius: 4px; + border-radius: var(--radius-sm); cursor: grab; touch-action: none; user-select: none; - transition: all 0.2s ease; -} - -.formula-name.italic { - font-style: italic; + transition: all var(--transition-fast); } -.formula-class.italic { - font-style: italic; +.sortable-formula-item:hover { + border-color: var(--primary); + background: var(--panel-bg); } -.class-group-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.6rem 0.8rem; - background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); - color: white; - cursor: pointer; - transition: all 0.2s ease; +.sortable-formula-item:active { + cursor: grabbing; } -.class-group-header:hover { - filter: brightness(1.1); +.sortable-formula-item.nested { + padding-left: var(--space-lg); + background: var(--card-bg); + border-left: 3px solid var(--primary); } -.class-group-header:active { - cursor: grabbing; +.drag-handle { + padding: 0 var(--space-sm); + color: var(--text-muted); + font-size: 1.125rem; + cursor: grab; } -.class-group-title { - font-weight: 800; - font-size: 0.95rem; - user-select: none; - letter-spacing: 0.02em; +.sortable-formula-item .drag-handle { + color: var(--text-muted); } -.drag-handle { - padding: 0 0.5rem; +.class-group-header .drag-handle { color: rgba(255, 255, 255, 0.7); - font-size: 1.2rem; - cursor: grab; } -.sortable-formula-item .drag-handle { - color: #888; +.formula-name { + flex: 1; + font-weight: 500; + font-size: 0.875rem; } -.class-group-actions { - display: flex; - gap: 0.25rem; +.formula-name.italic { + font-style: italic; } -.class-group-btn { - background: rgba(255,255,255,0.2); - border: none; - border-radius: 3px; - padding: 0.2rem 0.5rem; +.formula-class { font-size: 0.75rem; - cursor: pointer; - color: white; - transition: all 0.15s ease; + color: var(--text-muted); + padding: 0.125rem 0.5rem; + background: var(--panel-bg); + border-radius: var(--radius-sm); + margin-right: var(--space-sm); } -.class-group-btn:hover { - background: rgba(255,255,255,0.35); +.formula-class.italic { + font-style: italic; } -.class-group-btn.remove { - background: rgba(244,63,94,0.3); +.remove-formula { + background: none; + border: none; + color: var(--btn-clear); + font-size: 1.125rem; + cursor: pointer; + padding: 0 var(--space-xs); + transition: color var(--transition-fast); } -.class-group-btn.remove:hover { - background: #f43f5e; +.remove-formula:hover { + color: var(--btn-clear-hover); +} + +.reorder-instructions { + font-size: 0.8125rem; + color: var(--text-muted); + margin-bottom: 0.75rem; + font-style: italic; } .formula-reorder-panel { max-height: 250px; overflow-y: auto; - padding-right: 0.5rem; + padding-right: var(--space-sm); } .formula-reorder-panel::-webkit-scrollbar { @@ -685,26 +778,33 @@ body { } .formula-reorder-panel::-webkit-scrollbar-track { - background: #f1f1f1; + background: var(--box-bg); border-radius: 3px; } .formula-reorder-panel::-webkit-scrollbar-thumb { - background: #c1c1c1; + background: var(--border); border-radius: 3px; } +.formula-reorder-panel::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* ========================================================================== + PDF VIEWER + ========================================================================== */ .pdf-viewer-box { overflow-y: auto; align-items: center; - padding: 1rem 0; + padding: var(--space-md) 0; display: flex; flex-direction: column; } .pdf-page { - margin-bottom: 1.5rem; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + margin-bottom: var(--space-lg); + box-shadow: var(--shadow-lg); background-color: white; border-radius: 2px; } @@ -712,3 +812,80 @@ body { .pdf-page:last-child { margin-bottom: 0; } + +/* ========================================================================== + FOOTER + ========================================================================== */ +.app-footer { + text-align: center; + padding: var(--space-md); + margin-top: var(--space-md); +} + +.app-footer a { + color: var(--text-muted); + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-sm); + border-radius: var(--radius-full); + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.app-footer a:hover { + color: var(--text); + background-color: var(--panel-bg); +} + +/* ========================================================================== + RESPONSIVE + ========================================================================== */ +@media (max-width: 1200px) { + .input-section, + .preview-section { + flex: 1 1 0; + width: auto; + max-width: none; + } + + .editor-container { + gap: var(--space-sm); + } + + .compile-button-column { + flex: 0 0 44px; + } +} + +@media (max-width: 980px) { + .editor-container { + flex-direction: column; + align-items: center; + gap: var(--space-md); + width: 100%; + } + + .input-section, + .preview-section { + width: 100%; + max-width: 100%; + flex: 0 0 auto; + } + + .compile-button-column { + flex: 0 0 auto; + width: 100%; + min-height: 52px; + order: -1; + } + + .editor-wrapper, + .preview-box { + height: auto; + min-height: 350px; + } + + .selection-panel { + padding: var(--space-md); + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2a2632d..f149e48 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -44,7 +44,7 @@ function App() { return (
-
+

Cheat Sheet Generator

Write cheat sheets with LaTeX support

diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 6e5e80e..0177a49 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -313,33 +313,42 @@ const FormulaSelection = ({
); -const LatexEditor = ({ content, setContent, handleCompileClick, isCompiling }) => ( - <> +const LatexEditor = ({ content, setContent }) => { + const textareaRef = useRef(null); + const lineNumbersRef = useRef(null); + + const lineCount = content ? content.split('\n').length : 1; + + const handleScroll = () => { + if (lineNumbersRef.current && textareaRef.current) { + lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop; + } + }; + + return (
-