Skip to content

Commit 1b436b7

Browse files
authored
Merge pull request #224 from ayoub3bidi/develop
release: develop to main — landing page & sign-in polish
2 parents 4753eaa + 7b8affa commit 1b436b7

20 files changed

Lines changed: 339 additions & 96 deletions

.impeccable/design.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
"rules": [
121121
{
122122
"name": "The Semantic Color Rule",
123-
"body": "Visualization state colors are algorithm vocabulary — never reuse for marketing chrome.",
123+
"body": "Visualization state colors are algorithm vocabulary. Never reuse for marketing chrome.",
124124
"section": "colors"
125125
},
126126
{

public/.well-known/mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "Bayan Flow",
44
"version": "0.5.0"
55
},
6-
"description": "Interactive algorithm visualizer 45 algorithms across sorting, pathfinding, searching, tree traversal, and graph categories with step-by-step animation, Python code editing, and complexity analysis.",
6+
"description": "Interactive algorithm visualizer | 45 algorithms across sorting, pathfinding, searching, tree traversal, and graph categories with step-by-step animation, Python code editing, and complexity analysis.",
77
"endpoint": "/mcp",
88
"homepageUrl": "https://bayanflow.com",
99
"capabilities": {

public/.well-known/mcp/server-card.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "Bayan Flow",
44
"version": "0.5.0"
55
},
6-
"description": "Interactive algorithm visualizer 45 algorithms across sorting, pathfinding, searching, tree traversal, and graph categories with step-by-step animation, Python code editing, and complexity analysis.",
6+
"description": "Interactive algorithm visualizer | 45 algorithms across sorting, pathfinding, searching, tree traversal, and graph categories with step-by-step animation, Python code editing, and complexity analysis.",
77
"endpoint": "/mcp",
88
"homepageUrl": "https://bayanflow.com",
99
"capabilities": {

src/components/AlgorithmNotesTab.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ describe('AlgorithmNotesTab', () => {
183183
/>
184184
);
185185

186-
expect(screen.getByText(/retrying/)).toBeInTheDocument();
186+
expect(screen.getByText(/Trying again/)).toBeInTheDocument();
187187
});
188188

189189
it('shows unsaved status text when dirty', () => {

src/components/ControlPanel.jsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -335,30 +335,32 @@ function ControlPanel({
335335
}
336336
}}
337337
>
338-
<div className="w-full bg-gray-200 rounded-full h-2.5 overflow-hidden">
339-
<motion.div
340-
className="bg-gradient-to-r from-blue-500 to-blue-600 h-full rounded-full shadow-inner"
341-
initial={{ width: '0%' }}
342-
animate={{ width: `${progressPct}%` }}
343-
transition={{ duration: 0.3, ease: 'easeOut' }}
344-
/>
338+
<div className="relative h-2.5 w-full">
339+
<div className="w-full bg-gray-200 rounded-full h-2.5 overflow-hidden">
340+
<motion.div
341+
className="bg-gradient-to-r from-blue-500 to-blue-600 h-full rounded-full shadow-inner"
342+
initial={{ width: '0%' }}
343+
animate={{ width: `${progressPct}%` }}
344+
transition={{ duration: 0.3, ease: 'easeOut' }}
345+
/>
346+
</div>
347+
{totalSteps > 0 && (
348+
<motion.div
349+
data-testid="seek-thumb"
350+
aria-hidden="true"
351+
initial={false}
352+
animate={
353+
isRTL
354+
? { right: `${progressPct}%` }
355+
: { left: `${progressPct}%` }
356+
}
357+
transition={{ duration: 0.3, ease: 'easeOut' }}
358+
className={`pointer-events-none absolute top-1/2 h-4 w-4 -translate-y-1/2 rounded-full border-2 border-blue-500 bg-white shadow-md ${
359+
isRTL ? 'translate-x-1/2' : '-translate-x-1/2'
360+
}`}
361+
/>
362+
)}
345363
</div>
346-
{totalSteps > 0 && (
347-
<motion.div
348-
data-testid="seek-thumb"
349-
aria-hidden="true"
350-
initial={false}
351-
animate={
352-
isRTL
353-
? { right: `${progressPct}%` }
354-
: { left: `${progressPct}%` }
355-
}
356-
transition={{ duration: 0.3, ease: 'easeOut' }}
357-
className={`pointer-events-none absolute top-1/2 h-4 w-4 -translate-y-1/2 rounded-full border-2 border-blue-500 bg-white shadow-md ${
358-
isRTL ? 'translate-x-1/2' : '-translate-x-1/2'
359-
}`}
360-
/>
361-
)}
362364
{!isGated && onSeek && (
363365
<input
364366
type="range"

src/components/Header.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ function Header({ hideLanguageSwitcher = false }) {
2525
import.meta.env.VITE_DEV_SITE_URL || 'https://dev.bayanflow.com';
2626

2727
const isAppPage = location.pathname.startsWith('/app');
28+
const showThemeToggle =
29+
isAppPage ||
30+
location.pathname.startsWith('/settings') ||
31+
location.pathname === '/roadmap' ||
32+
location.pathname === '/pro';
2833

2934
const handleLogoClick = () => {
3035
navigate(isAppPage ? '/' : '/app');
@@ -132,7 +137,9 @@ function Header({ hideLanguageSwitcher = false }) {
132137
</div>
133138
)}
134139
{!hideLanguageSwitcher && <LanguageSwitcher />}
135-
{isAppPage && <ThemeToggle theme={theme} onToggle={toggleTheme} />}
140+
{showThemeToggle && (
141+
<ThemeToggle theme={theme} onToggle={toggleTheme} />
142+
)}
136143
<UserMenu
137144
variant={isAppPage ? 'compact' : 'landing'}
138145
hideAvatar={!isAppPage}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Copyright (c) 2025 Bayan Flow
3+
* Licensed under Elastic License 2.0 OR Commercial
4+
* See LICENSE for details.
5+
*/
6+
7+
import { useEffect, useRef } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
10+
import { Warning } from '@phosphor-icons/react';
11+
import {
12+
fadeOverlayTransition,
13+
modalPanelInitial,
14+
modalPanelAnimate,
15+
modalPanelExit,
16+
modalPanelTransition,
17+
} from '../motion/chromeMotion';
18+
import { getFocusableElements } from '../utils/focusableElements';
19+
20+
/**
21+
* Modal shown when Google sign-in fails, so the header stays clean.
22+
* Sized like SignInPromptModal (max-w-md).
23+
*
24+
* @param {boolean} isOpen
25+
* @param {Function} onClose
26+
*/
27+
function SignInErrorModal({ isOpen, onClose }) {
28+
const { t } = useTranslation();
29+
const dialogRef = useRef(null);
30+
const closeButtonRef = useRef(null);
31+
const reduceMotion = useReducedMotion();
32+
33+
useEffect(() => {
34+
if (!isOpen) {
35+
return undefined;
36+
}
37+
38+
const prevFocus = document.activeElement;
39+
closeButtonRef.current?.focus();
40+
41+
const handleKeyDown = event => {
42+
if (event.key === 'Escape') {
43+
onClose();
44+
} else if (event.key === 'Tab' && dialogRef.current) {
45+
const focusableElements = getFocusableElements(dialogRef.current);
46+
const firstElement = focusableElements[0];
47+
const lastElement = focusableElements[focusableElements.length - 1];
48+
49+
if (event.shiftKey) {
50+
if (document.activeElement === firstElement) {
51+
event.preventDefault();
52+
lastElement.focus();
53+
}
54+
} else if (document.activeElement === lastElement) {
55+
event.preventDefault();
56+
firstElement.focus();
57+
}
58+
}
59+
};
60+
61+
document.addEventListener('keydown', handleKeyDown);
62+
63+
return () => {
64+
document.removeEventListener('keydown', handleKeyDown);
65+
if (prevFocus && typeof prevFocus.focus === 'function') {
66+
prevFocus.focus();
67+
}
68+
};
69+
}, [isOpen, onClose]);
70+
71+
return (
72+
<AnimatePresence>
73+
{isOpen && (
74+
<motion.div
75+
ref={dialogRef}
76+
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60"
77+
initial={{ opacity: 0 }}
78+
animate={{ opacity: 1 }}
79+
exit={{ opacity: 0 }}
80+
transition={fadeOverlayTransition(reduceMotion)}
81+
onClick={onClose}
82+
role="dialog"
83+
aria-modal="true"
84+
aria-label={t('accessBan.signInUnavailableTitle')}
85+
tabIndex={-1}
86+
>
87+
<motion.div
88+
className="bg-surface rounded-2xl shadow-2xl max-w-md w-full p-8"
89+
initial={modalPanelInitial(reduceMotion)}
90+
animate={modalPanelAnimate()}
91+
exit={modalPanelExit(reduceMotion)}
92+
transition={modalPanelTransition(reduceMotion)}
93+
onClick={e => e.stopPropagation()}
94+
>
95+
<div className="flex items-center gap-3 mb-3">
96+
<Warning
97+
size={24}
98+
weight="bold"
99+
className="shrink-0 text-text-secondary"
100+
aria-hidden="true"
101+
/>
102+
<h2 className="text-2xl font-bold text-text-primary">
103+
{t('accessBan.signInUnavailableTitle')}
104+
</h2>
105+
</div>
106+
<p className="text-text-secondary mb-8 leading-relaxed">
107+
{t('accessBan.signInUnavailable')}
108+
</p>
109+
110+
<button
111+
ref={closeButtonRef}
112+
type="button"
113+
onClick={onClose}
114+
className="w-full px-6 py-3 bg-interactive-bg text-text-primary rounded-xl border border-interactive-border font-medium transition-all duration-200 hover:bg-surface-elevated active:scale-[0.98]"
115+
>
116+
{t('common.close')}
117+
</button>
118+
</motion.div>
119+
</motion.div>
120+
)}
121+
</AnimatePresence>
122+
);
123+
}
124+
125+
export default SignInErrorModal;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Copyright (c) 2025 Bayan Flow
3+
* Licensed under Elastic License 2.0 OR Commercial
4+
* See LICENSE for details.
5+
*/
6+
7+
import { describe, it, expect, vi } from 'vitest';
8+
import { renderWithI18n, screen, fireEvent } from '../test/testUtils';
9+
import SignInErrorModal from './SignInErrorModal';
10+
11+
describe('SignInErrorModal', () => {
12+
it('does not render when isOpen is false', () => {
13+
renderWithI18n(<SignInErrorModal isOpen={false} onClose={vi.fn()} />);
14+
15+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
16+
});
17+
18+
it('renders title, message, and close button when open', () => {
19+
renderWithI18n(<SignInErrorModal isOpen onClose={vi.fn()} />);
20+
21+
const dialog = screen.getByRole('dialog');
22+
expect(dialog).toBeInTheDocument();
23+
expect(dialog).toHaveAccessibleName('Sign-in unavailable');
24+
expect(
25+
screen.getByText(/sign-in is temporarily unavailable/i)
26+
).toBeInTheDocument();
27+
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
28+
});
29+
30+
it('calls onClose when the close button is clicked', () => {
31+
const onClose = vi.fn();
32+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
33+
34+
fireEvent.click(screen.getByRole('button', { name: /close/i }));
35+
expect(onClose).toHaveBeenCalledTimes(1);
36+
});
37+
38+
it('closes when clicking the overlay backdrop', () => {
39+
const onClose = vi.fn();
40+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
41+
42+
const overlay = screen.getByRole('dialog');
43+
fireEvent.click(overlay);
44+
expect(onClose).toHaveBeenCalledTimes(1);
45+
});
46+
47+
it('does not close when clicking the panel content', () => {
48+
const onClose = vi.fn();
49+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
50+
51+
fireEvent.click(screen.getByText(/sign-in is temporarily unavailable/i));
52+
expect(onClose).not.toHaveBeenCalled();
53+
});
54+
55+
it('closes on Escape key', () => {
56+
const onClose = vi.fn();
57+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
58+
59+
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
60+
expect(onClose).toHaveBeenCalledTimes(1);
61+
});
62+
63+
it('does not close on other keys', () => {
64+
const onClose = vi.fn();
65+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
66+
67+
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Enter' });
68+
expect(onClose).not.toHaveBeenCalled();
69+
});
70+
71+
it('keeps focus on the close button when Tab is pressed', () => {
72+
const onClose = vi.fn();
73+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
74+
75+
const closeButton = screen.getByRole('button', { name: /close/i });
76+
closeButton.focus();
77+
78+
fireEvent.keyDown(closeButton, { key: 'Tab' });
79+
expect(closeButton).toHaveFocus();
80+
});
81+
82+
it('keeps focus on the close button when Shift+Tab is pressed', () => {
83+
const onClose = vi.fn();
84+
renderWithI18n(<SignInErrorModal isOpen onClose={onClose} />);
85+
86+
const closeButton = screen.getByRole('button', { name: /close/i });
87+
closeButton.focus();
88+
89+
fireEvent.keyDown(closeButton, { key: 'Tab', shiftKey: true });
90+
expect(closeButton).toHaveFocus();
91+
});
92+
});

0 commit comments

Comments
 (0)