Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
993a6133cf |
@@ -123,13 +123,7 @@
|
|||||||
.modal {
|
.modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
/* Bottom sheet sized against the overlay (which tracks the visual
|
max-height: 95vh;
|
||||||
viewport), NOT vh: when the keyboard is open the overlay is short, so
|
|
||||||
max-height:100% keeps the whole sheet — input and results — above the
|
|
||||||
keyboard. min-height gives a comfortable default without a tiny stub,
|
|
||||||
but is capped at 100% so it never exceeds the visible area. */
|
|
||||||
min-height: min(55vh, 100%);
|
|
||||||
max-height: 100%;
|
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
animation: slideUp 0.3s ease;
|
animation: slideUp 0.3s ease;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useCallback, useRef } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import styles from './Modal.module.css';
|
import styles from './Modal.module.css';
|
||||||
|
|
||||||
@@ -18,8 +18,6 @@ interface ModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({ isOpen, onClose, children, title, size = 'medium' }: ModalProps) {
|
export function Modal({ isOpen, onClose, children, title, size = 'medium' }: ModalProps) {
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleEscape = useCallback((e: KeyboardEvent) => {
|
const handleEscape = useCallback((e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onClose();
|
onClose();
|
||||||
@@ -41,32 +39,6 @@ export function Modal({ isOpen, onClose, children, title, size = 'medium' }: Mod
|
|||||||
};
|
};
|
||||||
}, [isOpen, handleEscape]);
|
}, [isOpen, handleEscape]);
|
||||||
|
|
||||||
// Pin the overlay to the VISUAL viewport, not the layout viewport. On mobile
|
|
||||||
// the on-screen keyboard shrinks the visual viewport but not the layout one,
|
|
||||||
// so a `position: fixed; inset: 0` overlay keeps full height — leaving the
|
|
||||||
// bottom-anchored sheet (and the dim backdrop's lower half) hidden behind
|
|
||||||
// the keyboard. Tracking visualViewport.height/offsetTop keeps the whole
|
|
||||||
// overlay — backdrop and sheet — inside the visible area, above the keyboard.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
|
|
||||||
const el = overlayRef.current;
|
|
||||||
if (!vv || !el) return;
|
|
||||||
|
|
||||||
const sync = () => {
|
|
||||||
el.style.top = `${vv.offsetTop}px`;
|
|
||||||
el.style.height = `${vv.height}px`;
|
|
||||||
el.style.bottom = 'auto';
|
|
||||||
};
|
|
||||||
sync();
|
|
||||||
vv.addEventListener('resize', sync);
|
|
||||||
vv.addEventListener('scroll', sync);
|
|
||||||
return () => {
|
|
||||||
vv.removeEventListener('resize', sync);
|
|
||||||
vv.removeEventListener('scroll', sync);
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
if (!isOpen || typeof window === 'undefined') return null;
|
if (!isOpen || typeof window === 'undefined') return null;
|
||||||
|
|
||||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||||
@@ -76,7 +48,7 @@ export function Modal({ isOpen, onClose, children, title, size = 'medium' }: Mod
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div ref={overlayRef} className={styles.overlay} onClick={handleOverlayClick}>
|
<div className={styles.overlay} onClick={handleOverlayClick}>
|
||||||
<div className={`${styles.modal} ${styles[size]}`}>
|
<div className={`${styles.modal} ${styles[size]}`}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
{title && <h2 className={styles.title}>{title}</h2>}
|
{title && <h2 className={styles.title}>{title}</h2>}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #1a1612);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||||
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
background: var(--accent-gold-bg);
|
background: var(--accent-gold-bg);
|
||||||
border: 1px solid var(--accent-gold, #c9a227);
|
border: 1px solid var(--accent-gold, #c9a227);
|
||||||
@@ -111,16 +119,12 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #1a1612);
|
color: var(--text-primary, #1a1612);
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultButton {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schoolMeta {
|
.schoolMeta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -155,30 +159,21 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* One scroll container on mobile: the modal content itself scrolls, so the
|
.title {
|
||||||
results list must not add its own inner scroll (double scrollbars, and
|
font-size: 1.25rem;
|
||||||
the input would be trapped above a short 400px window when the keyboard
|
|
||||||
shrinks the sheet). */
|
|
||||||
.results {
|
|
||||||
max-height: none;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact stacked card: name + meta, then a full-width action so the tap
|
|
||||||
target is obvious and the card doesn't waste vertical space. */
|
|
||||||
.resultItem {
|
.resultItem {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 0.625rem;
|
|
||||||
padding: 0.875rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultButton {
|
.addButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schoolMeta {
|
.schoolMeta {
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.25rem 1rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,10 @@ export function SchoolSearchModal({ isOpen, onClose }: SchoolSearchModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={handleClose} title="Add School to Comparison">
|
<Modal isOpen={isOpen} onClose={handleClose}>
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
|
<h2 className={styles.title}>Add School to Comparison</h2>
|
||||||
|
|
||||||
{!canAddMore && (
|
{!canAddMore && (
|
||||||
<div className={styles.warning}>
|
<div className={styles.warning}>
|
||||||
Maximum 5 schools can be compared. Remove a school to add another.
|
Maximum 5 schools can be compared. Remove a school to add another.
|
||||||
@@ -127,9 +129,9 @@ export function SchoolSearchModal({ isOpen, onClose }: SchoolSearchModalProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleAddSchool(school)}
|
onClick={() => handleAddSchool(school)}
|
||||||
disabled={alreadySelected || !canAddMore}
|
disabled={alreadySelected || !canAddMore}
|
||||||
className={`${styles.resultButton} ${
|
className={
|
||||||
alreadySelected ? "btn btn-active" : "btn btn-secondary"
|
alreadySelected ? "btn btn-active" : "btn btn-secondary"
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
{alreadySelected ? "✓ Comparing" : "+ Compare"}
|
{alreadySelected ? "✓ Comparing" : "+ Compare"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user