diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 49c92ef..781b983 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -5099,9 +5099,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 18e0f3d..8974299 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -236,6 +236,33 @@
[data-theme="neon"] .class-btn.active {
box-shadow: 0 0 10px rgba(0,245,255,0.5);
}
+ /* Galaxy Theme */
+[data-theme="galaxy"] {
+ --bg: #0a0614;
+ --text: #e8e0f5;
+ --text-muted: #8b7aa8;
+ --panel-bg: #110d1f;
+ --border: #2d1f4a;
+ --border-subtle: #1a1230;
+ --primary: #7c3aed;
+ --primary-hover: #6d28d9;
+ --card-bg: #110d1f;
+ --box-bg: #0d0a1a;
+ --input-bg: #080512;
+ --input-border: #3d2566;
+ --input-text: #e8e0f5;
+ --btn-primary: #7c3aed;
+ --btn-primary-hover: #6d28d9;
+ --btn-download: #5b21b6;
+ --btn-download-hover: #4c1d95;
+ --btn-clear: #db2777;
+ --btn-clear-hover: #be185d;
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.5);
+ --shadow-md: 0 4px 12px rgba(124,58,237,0.25);
+ --shadow-lg: 0 8px 24px rgba(124,58,237,0.3);
+ --shadow-inset: inset 0 1px 3px rgba(0,0,0,0.6);
+}
+
/* ==========================================================================
BASE STYLES
========================================================================== */
@@ -3019,3 +3046,103 @@ three - column responsive
justify-content: center;
}
}
+
+/* ── Animated Compile Button ── */
+.btn-compile {
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ transition: all var(--transition-base);
+}
+
+.btn-compile-icon {
+ display: inline-block;
+ transition: transform 0.3s ease;
+}
+
+.btn-compile:hover:not(:disabled) .btn-compile-icon {
+ transform: scale(1.3) rotate(-10deg);
+}
+
+/* Pulse ring on click */
+.btn-compile::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: var(--radius-md);
+ background: rgba(255, 255, 255, 0.15);
+ opacity: 0;
+ transform: scale(0.8);
+ transition: none;
+}
+
+.btn-compile:active::after {
+ opacity: 1;
+ transform: scale(1);
+ transition: transform 0.15s ease, opacity 0.15s ease;
+}
+
+/* Compiling state — shimmer sweep */
+.btn-compile.is-compiling {
+ cursor: not-allowed;
+ background: linear-gradient(
+ 90deg,
+ var(--btn-primary) 0%,
+ #6ba3f9 40%,
+ #a5c8ff 50%,
+ #6ba3f9 60%,
+ var(--btn-primary) 100%
+ );
+ background-size: 200% 100%;
+ animation: compile-shimmer 1.4s linear infinite;
+}
+
+@keyframes compile-shimmer {
+ 0% { background-position: 200% center; }
+ 100% { background-position: -200% center; }
+}
+
+/* Compiling state — spinning icon */
+.btn-compile.is-compiling .btn-compile-icon {
+ animation: compile-spin 0.8s linear infinite;
+}
+
+@keyframes compile-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* Success flash — add/remove a class via JS after compile */
+.btn-compile.compile-success {
+ background: linear-gradient(180deg, #34d399 0%, var(--btn-download) 100%);
+ animation: compile-success-flash 0.6s ease forwards;
+}
+
+@keyframes compile-success-flash {
+ 0% { transform: scale(1); }
+ 40% { transform: scale(1.04); }
+ 70% { transform: scale(0.97); }
+ 100% { transform: scale(1); }
+}
+
+.save-status {
+ font-size: 12px;
+ opacity: 0.75;
+ margin-left: 10px;
+ transition: opacity 0.2s ease;
+}
+
+.save-status.saving {
+ color: #888;
+}
+
+.save-status.offline {
+ color: #d97706;
+}
+
+.save-status.saved {
+ color: #16a34a;
+}
\ No newline at end of file
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0b249c0..d3e32e2 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -146,7 +146,8 @@ const THEMES = [
{ id: 'miami', label: '🌴 Miami 🐬' },
{ id: 'forest', label: '🌲 Forest' },
{ id: 'coolGrey', label: '❄️ Cool Grey'},
- { id: 'neon', label: '🩵 neon'}
+ { id: 'neon', label: '🩵 neon'},
+ { id: 'galaxy', label: '🌌 Galaxy' },
];
function App() {
diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx
index 1d51c53..5fbd2b6 100644
--- a/frontend/src/components/CreateCheatSheet.jsx
+++ b/frontend/src/components/CreateCheatSheet.jsx
@@ -234,7 +234,7 @@ function FormulaReorderPanel({ groupedFormulas, onReorderClass, onReorderFormula
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
- const [expandedGroups, setExpandedGroups] = React.useState({});
+ const [expandedGroups, setExpandedGroups] = useState({});
const handleDragEnd = (event) => {
const { active, over } = event;
@@ -267,6 +267,7 @@ function FormulaReorderPanel({ groupedFormulas, onReorderClass, onReorderFormula
}
};
+
const toggleGroup = (className) => {
setExpandedGroups(prev => ({ ...prev, [className]: !prev[className] }));
};
@@ -683,7 +684,7 @@ const LatexEditor = ({ content, onChange, isModified, compileError }) => {
value={content}
onChange={(e) => onChange(e.target.value)}
onScroll={handleScroll}
- placeholder='Select classes and categories above, then click "Compile PDF" to see the LaTeX code here.'
+ placeholder='Select classes and categories above, then click "GET CHEAT SHEET" to see the LaTeX code here.'
className={`textarea-field ${isModified ? 'modified' : ''}`}
rows={15}
spellCheck="false"
@@ -1035,6 +1036,8 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
const [rightPanelVisible, setRightPanelVisible] = useState(true);
const [panelLayout, setPanelLayout] = useState(() => loadPanelLayout());
const [videoSearchRequest, setVideoSearchRequest] = useState(null);
+ const [saveStatus, setSaveStatus] = useState('idle');
+ const [lastSavedAt, setLastSavedAt] = useState(null);
const [classesCollapseSignal, setClassesCollapseSignal] = useState(0);
const pendingPanelLayoutRef = useRef(panelLayout);
const hasCollapsedLeftPanelOnceRef = useRef(false);
@@ -1043,6 +1046,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
const modalDialogRef = useRef(null);
const appBodyRef = useRef(null);
const centerPanelRef = useRef(null);
+ const compileBtnRef = useRef(null);
const snapshots = useMemo(() => [...(initialData?.compileHistory || [])].reverse(), [initialData?.compileHistory]);
const selectedClassNames = useMemo(
() => classesData.filter((cls) => selectedClasses[cls.name]).map((cls) => cls.name),
@@ -1094,6 +1098,31 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
}
}, []);
+ const getSaveStatusText = () => {
+ if (saveStatus === 'saving') return 'Saving...';
+ if (saveStatus === 'offline') return 'Offline changes pending'
+ if (saveStatus === 'saved' && lastSavedAt) {
+ const diff = Date.now() - lastSavedAt;
+ const minutes = Math.floor(diff / 60000);
+ if (minutes < 1) return 'Saved just now';
+ if (minutes === 1) return 'Saved 1 min ago';
+ return `Saved ${minutes} min ago`;
+ }
+ return '';
+ };
+
+ useEffect(() => {
+ if(!initialData) return
+ if(initialData.title) setTitle(initialData.title);
+ if (initialData.content){
+ handleContentChange(initialData.content);
+ }
+ if (initialData.columns) setColumns(initialData.columns);
+ if(initialData.fontSize) setFontSize(initialData.fontSize);
+ if (initialData.spacing) setSpacing(initialData.spacing);
+ if (initialData.margins) setMargins(initialData.margins);
+ }, [initialData]);
+
useEffect(() => {
const hasCompiledBefore = Boolean(initialData?.compileHistory?.length || pdfBlob || content.trim());
if (hasCompiledBefore) return;
@@ -1194,7 +1223,8 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
}
lastAutoSavedPdfRef.current = pdfBlob;
-
+ setSaveStatus('saving');
+ setLastSavedAt(Date.now());
onSave({
title,
content,
@@ -1215,11 +1245,18 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
selectedFormulas: getSelectedFormulasList(),
compiledAt: new Date().toISOString(),
},
- }, false).catch((error) => {
+ }, false)
+ .then(() => {
+ setSaveStatus('saved');
+ setLastSavedAt(Date.now());
+ }).catch((error) => {
console.error('Failed to autosave compiled sheet', error);
+ setSaveStatus('offline');
});
}, [columns, compileError, content, contentSource, fontSize, getSelectedFormulasList, margins, onSave, pdfBlob, spacing, title]);
+
+
const startResize = useCallback((panel) => (event) => {
event.preventDefault();
@@ -1300,7 +1337,16 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
const workspaceSplitTemplate = `minmax(${LATEX_PANEL_MIN_WIDTH}px, ${panelLayout.latexWidth}px) 10px minmax(${MIN_PREVIEW_WIDTH}px, 1fr)`;
const previewLayoutSignature = `${appBodyGridTemplate}|${workspaceSplitTemplate}|${leftPanelVisible}|${rightPanelVisible}|${showLatex}`;
-
+ useEffect(() => {
+ if (!pdfBlob || isCompiling) return;
+ const btn = compileBtnRef.current;
+ if (!btn) return;
+ btn.classList.add('compile-success');
+ const timer = setTimeout(() => {
+ btn.classList.remove('compile-success');
+ }, 600);
+ return () => clearTimeout(timer);
+ }, [pdfBlob, isCompiling]);
const handleCompileClick = () => {
if (!hasCollapsedLeftPanelOnceRef.current) {
// First compile: keep controls reachable while reclaiming preview space.
@@ -1328,7 +1374,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
};
const handleSave = async (e) => {
- e.preventDefault();
+ e?.preventDefault?.();
await onSave({
title,
content,
@@ -1405,12 +1451,17 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
{/* Footer buttons */}
@@ -1502,6 +1553,9 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
+
+ {getSaveStatusText()}
+