Improve responsive UX/UI consistency across all frontend screens.
This polish pass unifies mobile navigation, spacing, typography hierarchy, and CTA behavior so all core exam workflows remain clear and fully usable on both mobile and desktop.
This commit is contained in:
@@ -10,26 +10,41 @@ export default function Navbar() {
|
|||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [confirmOut, setConfirmOut] = useState(false);
|
const [confirmOut, setConfirmOut] = useState(false);
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
const doLogout = () => {
|
const doLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeMobileMenu = () => setMobileOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="navbar">
|
<header className="navbar">
|
||||||
<div className="navbar-inner">
|
<div className="navbar-inner">
|
||||||
<Link to="/" className="brand">
|
<Link to="/" className="brand" onClick={closeMobileMenu}>
|
||||||
<span className="brand-logo">
|
<span className="brand-logo">
|
||||||
<Icon name="document" size={18} />
|
<Icon name="document" size={18} />
|
||||||
</span>
|
</span>
|
||||||
GenExámenes IA
|
<span className="brand-text">GenExámenes IA</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="nav-mobile-toggle"
|
||||||
|
aria-label={mobileOpen ? "Cerrar menú" : "Abrir menú"}
|
||||||
|
onClick={() => setMobileOpen((open) => !open)}
|
||||||
|
>
|
||||||
|
<Icon name={mobileOpen ? "close" : "listChecks"} size={18} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className={`nav-collapse ${mobileOpen ? "open" : ""}`}>
|
||||||
<nav className="nav-links">
|
<nav className="nav-links">
|
||||||
<NavLink to="/" end className="nav-link">
|
<NavLink to="/" end className="nav-link" onClick={closeMobileMenu}>
|
||||||
Mis exámenes
|
Mis exámenes
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink to="/plantillas/nueva" className="nav-link">
|
<NavLink to="/plantillas/nueva" className="nav-link" onClick={closeMobileMenu}>
|
||||||
Crear examen
|
Crear examen
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -38,11 +53,19 @@ export default function Navbar() {
|
|||||||
<div className="avatar" title={user?.email}>
|
<div className="avatar" title={user?.email}>
|
||||||
{initials(user?.full_name || user?.email)}
|
{initials(user?.full_name || user?.email)}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setConfirmOut(true)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
closeMobileMenu();
|
||||||
|
setConfirmOut(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Salir
|
Salir
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={confirmOut}
|
open={confirmOut}
|
||||||
|
|||||||
+342
-26
@@ -31,6 +31,10 @@
|
|||||||
|
|
||||||
--font: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
--font: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
--maxw: 1180px;
|
--maxw: 1180px;
|
||||||
|
--h1: 30px;
|
||||||
|
--h2: 24px;
|
||||||
|
--h3: 19px;
|
||||||
|
--h4: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -48,7 +52,7 @@ body {
|
|||||||
background: var(--c-bg);
|
background: var(--c-bg);
|
||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.55;
|
line-height: 1.6;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +68,22 @@ h1,
|
|||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0 0 0.4em;
|
margin: 0 0 0.45em;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.012em;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: var(--h1);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: var(--h2);
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: var(--h3);
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: var(--h4);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -82,6 +98,12 @@ button {
|
|||||||
.icon-inline {
|
.icon-inline {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
.card-head .icon,
|
||||||
|
.tab .icon,
|
||||||
|
.btn .icon,
|
||||||
|
.badge .icon {
|
||||||
|
stroke-width: 1.9;
|
||||||
|
}
|
||||||
.icon-lg {
|
.icon-lg {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -181,7 +203,7 @@ button {
|
|||||||
.nav-link {
|
.nav-link {
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--c-text-soft);
|
color: #4e566c;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -228,16 +250,16 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 26px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.page-header h1 {
|
.page-header h1 {
|
||||||
font-size: 26px;
|
font-size: clamp(24px, 3vw, 30px);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.page-header p {
|
.page-header p {
|
||||||
margin: 4px 0 0;
|
margin: 7px 0 0;
|
||||||
color: var(--c-text-soft);
|
color: #5a6277;
|
||||||
}
|
}
|
||||||
.page-header-actions {
|
.page-header-actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -245,6 +267,23 @@ button {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-lead {
|
||||||
|
max-width: 68ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--c-text);
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--c-text-faint);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Cards ---------- */
|
/* ---------- Cards ---------- */
|
||||||
.card {
|
.card {
|
||||||
background: var(--c-surface);
|
background: var(--c-surface);
|
||||||
@@ -253,10 +292,10 @@ button {
|
|||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
.card-pad {
|
.card-pad {
|
||||||
padding: 22px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.card-head {
|
.card-head {
|
||||||
padding: 18px 22px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid var(--c-border);
|
border-bottom: 1px solid var(--c-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -265,9 +304,10 @@ button {
|
|||||||
.card-head h3 {
|
.card-head h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
.card-body {
|
.card-body {
|
||||||
padding: 22px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
@@ -365,12 +405,12 @@ button {
|
|||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--c-text);
|
color: #2a3143;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
.field-hint {
|
.field-hint {
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
color: var(--c-text-faint);
|
color: #78809a;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
.field-error {
|
.field-error {
|
||||||
@@ -440,12 +480,12 @@ button {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 3px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--c-surface-2);
|
background: var(--c-surface-2);
|
||||||
color: var(--c-text-soft);
|
color: #505a74;
|
||||||
}
|
}
|
||||||
.badge-primary {
|
.badge-primary {
|
||||||
background: var(--c-primary-soft);
|
background: var(--c-primary-soft);
|
||||||
@@ -477,10 +517,10 @@ button {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
.tab {
|
.tab {
|
||||||
padding: 11px 16px;
|
padding: 11px 15px;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--c-text-soft);
|
color: #556079;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -492,7 +532,7 @@ button {
|
|||||||
gap: 7px;
|
gap: 7px;
|
||||||
}
|
}
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
color: var(--c-text);
|
color: #252c3f;
|
||||||
}
|
}
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: var(--c-primary);
|
color: var(--c-primary);
|
||||||
@@ -627,7 +667,7 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
color: var(--c-text-soft);
|
color: #586178;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.meta-row span {
|
.meta-row span {
|
||||||
@@ -668,13 +708,17 @@ button {
|
|||||||
}
|
}
|
||||||
.list-item-sub {
|
.list-item-sub {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--c-text-faint);
|
color: #7a839c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 56px 24px;
|
padding: 56px 24px;
|
||||||
color: var(--c-text-soft);
|
color: #5c657a;
|
||||||
|
}
|
||||||
|
.empty-state p {
|
||||||
|
margin: 6px auto 0;
|
||||||
|
max-width: 56ch;
|
||||||
}
|
}
|
||||||
.empty-state-icon {
|
.empty-state-icon {
|
||||||
margin: 0 auto 12px;
|
margin: 0 auto 12px;
|
||||||
@@ -813,15 +857,15 @@ button {
|
|||||||
}
|
}
|
||||||
.toast-content {
|
.toast-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
font-size: 13.5px;
|
||||||
}
|
}
|
||||||
.toast-title {
|
.toast-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.toast-msg {
|
.toast-msg {
|
||||||
color: var(--c-text-soft);
|
color: #5b6378;
|
||||||
font-size: 13px;
|
font-size: 12.5px;
|
||||||
}
|
}
|
||||||
.toast-close {
|
.toast-close {
|
||||||
background: none;
|
background: none;
|
||||||
@@ -863,14 +907,17 @@ button {
|
|||||||
.mt-lg {
|
.mt-lg {
|
||||||
margin-top: 26px;
|
margin-top: 26px;
|
||||||
}
|
}
|
||||||
|
.mb-sm {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
.mb {
|
.mb {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.text-soft {
|
.text-soft {
|
||||||
color: var(--c-text-soft);
|
color: #5a6379;
|
||||||
}
|
}
|
||||||
.text-faint {
|
.text-faint {
|
||||||
color: var(--c-text-faint);
|
color: #7b849c;
|
||||||
}
|
}
|
||||||
.text-sm {
|
.text-sm {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -933,3 +980,272 @@ button {
|
|||||||
border: 1px solid var(--c-border);
|
border: 1px solid var(--c-border);
|
||||||
background: var(--c-surface-2);
|
background: var(--c-surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-split {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-split-wide {
|
||||||
|
grid-template-columns: minmax(0, 1.4fr) minmax(260px, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-select-wrap {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Responsive ---------- */
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.navbar-inner {
|
||||||
|
padding: 0 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding: 24px 16px 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.list-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-split,
|
||||||
|
.layout-split-wide {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
:root {
|
||||||
|
--radius: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
--h1: 26px;
|
||||||
|
--h2: 22px;
|
||||||
|
--h3: 18px;
|
||||||
|
--h4: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-inner {
|
||||||
|
height: auto;
|
||||||
|
min-height: 64px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-mobile-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-collapse {
|
||||||
|
width: 100%;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 0 2px;
|
||||||
|
border-top: 1px solid var(--c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-collapse.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-user {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid var(--c-border);
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p,
|
||||||
|
.page-lead {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header-actions {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head,
|
||||||
|
.card-body,
|
||||||
|
.card-pad {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head > .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: -4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-select-wrap {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 10px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-sm {
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-lg {
|
||||||
|
min-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stack {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stack > .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
padding: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
max-height: 94vh;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-head,
|
||||||
|
.modal-body,
|
||||||
|
.modal-foot {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-foot {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-foot .btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-stack {
|
||||||
|
top: auto;
|
||||||
|
bottom: 12px;
|
||||||
|
right: 12px;
|
||||||
|
left: 12px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-center {
|
||||||
|
padding: 36px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 34px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
max-height: 320px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-sub {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 761px) {
|
||||||
|
.nav-mobile-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-collapse {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function CreateTemplatePage() {
|
|||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Nuevo examen</h1>
|
<h1>Nuevo examen</h1>
|
||||||
<p>Define la estructura. Después podrás subir material y generar preguntas.</p>
|
<p className="page-lead">Define la estructura. Después podrás subir material y generar preguntas.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ export default function CreateTemplatePage() {
|
|||||||
className="card"
|
className="card"
|
||||||
style={{ padding: 16, marginBottom: 14, background: "var(--c-surface-2)" }}
|
style={{ padding: 16, marginBottom: 14, background: "var(--c-surface-2)" }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb">
|
<div className="flex justify-between items-center mb wrap gap-sm">
|
||||||
<strong>Bloque {idx + 1}</strong>
|
<strong>Bloque {idx + 1}</strong>
|
||||||
{types.length > 1 && (
|
{types.length > 1 && (
|
||||||
<Button
|
<Button
|
||||||
@@ -263,7 +263,7 @@ export default function CreateTemplatePage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<div style={{ paddingTop: 26 }}>
|
<div style={{ paddingTop: 8 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Permitir varias respuestas correctas"
|
label="Permitir varias respuestas correctas"
|
||||||
checked={t.multiple_correct}
|
checked={t.multiple_correct}
|
||||||
@@ -343,7 +343,7 @@ export default function CreateTemplatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap justify-between">
|
<div className="flex gap justify-between wrap mobile-stack mt">
|
||||||
<Button type="button" variant="ghost" onClick={() => navigate("/")}>
|
<Button type="button" variant="ghost" onClick={() => navigate("/")}>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ export default function DashboardPage() {
|
|||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Mis exámenes</h1>
|
<h1>Mis exámenes</h1>
|
||||||
<p>Gestiona tus plantillas de examen y genera preguntas con IA.</p>
|
<p className="page-lead">Gestiona tus plantillas de examen y genera preguntas con IA.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-header-actions">
|
<div className="page-header-actions">
|
||||||
<Button onClick={() => navigate("/plantillas/nueva")}>
|
<Button onClick={() => navigate("/plantillas/nueva")} size="lg">
|
||||||
<Icon name="plus" size={16} className="icon-inline" />
|
<Icon name="plus" size={16} className="icon-inline" />
|
||||||
Nuevo examen
|
Nuevo examen
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -154,6 +154,24 @@ export default function TemplateDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="tabs-select-wrap">
|
||||||
|
<label className="field-label" htmlFor="template-tab-select">
|
||||||
|
Sección actual
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="template-tab-select"
|
||||||
|
className="select"
|
||||||
|
value={tab}
|
||||||
|
onChange={(e) => setTab(e.target.value)}
|
||||||
|
>
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<option key={t.id} value={t.id}>
|
||||||
|
{t.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
{TABS.map((t) => (
|
{TABS.map((t) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default function ExportTab({ templateId, template, questions }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-soft mb">
|
<p className="text-soft page-lead mb">
|
||||||
Tu examen tiene <strong>{questions.length} preguntas</strong>. Elige un
|
Tu examen tiene <strong>{questions.length} preguntas</strong>. Elige un
|
||||||
formato para descargarlo o previsualizarlo.
|
formato para descargarlo o previsualizarlo.
|
||||||
</p>
|
</p>
|
||||||
@@ -87,7 +87,7 @@ export default function ExportTab({ templateId, template, questions }) {
|
|||||||
<p className="text-sm text-soft" style={{ minHeight: 60 }}>
|
<p className="text-sm text-soft" style={{ minHeight: 60 }}>
|
||||||
{fmt.desc}
|
{fmt.desc}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-sm">
|
<div className="flex gap-sm mobile-stack">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => run(fmt, { download: true })}
|
onClick={() => run(fmt, { download: true })}
|
||||||
loading={loadingFormat === fmt.id}
|
loading={loadingFormat === fmt.id}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function GenerateTab({
|
|||||||
const topicTooShort = topic.trim().length < 5;
|
const topicTooShort = topic.trim().length < 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid" style={{ gridTemplateColumns: "1fr 340px" }}>
|
<div className="grid layout-split">
|
||||||
<div>
|
<div>
|
||||||
<div className="tabs" style={{ marginBottom: 18 }}>
|
<div className="tabs" style={{ marginBottom: 18 }}>
|
||||||
{MODES.map((m) => (
|
{MODES.map((m) => (
|
||||||
@@ -122,6 +122,9 @@ export default function GenerateTab({
|
|||||||
{mode !== "parse" && (
|
{mode !== "parse" && (
|
||||||
<div className="card mb">
|
<div className="card mb">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
<h3 className="section-title mb-sm">
|
||||||
|
{mode === "auto" ? "Generación automática" : "Construcción de prompt"}
|
||||||
|
</h3>
|
||||||
<Field
|
<Field
|
||||||
label="Tema / instrucciones para la IA"
|
label="Tema / instrucciones para la IA"
|
||||||
hint="Describe el contenido o enfoque del examen (mínimo 5 caracteres)."
|
hint="Describe el contenido o enfoque del examen (mínimo 5 caracteres)."
|
||||||
@@ -157,7 +160,7 @@ export default function GenerateTab({
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap mt">
|
<div className="flex gap mt mobile-stack">
|
||||||
{mode === "auto" ? (
|
{mode === "auto" ? (
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -244,7 +247,7 @@ export default function GenerateTab({
|
|||||||
|
|
||||||
{generated.length > 0 && (
|
{generated.length > 0 && (
|
||||||
<div className="mt-lg">
|
<div className="mt-lg">
|
||||||
<div className="flex justify-between items-center mb">
|
<div className="flex justify-between items-center mb wrap gap-sm">
|
||||||
<h3 style={{ margin: 0 }}>
|
<h3 style={{ margin: 0 }}>
|
||||||
Resultado ({generated.length} preguntas)
|
Resultado ({generated.length} preguntas)
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ export default function ImagesTab({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid" style={{ gridTemplateColumns: "1fr 320px" }}>
|
<div className="grid layout-split">
|
||||||
<div>
|
<div>
|
||||||
<div className="card mb">
|
<div className="card mb">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
<h3 className="section-title">Subir imágenes del examen</h3>
|
||||||
|
<p className="section-subtle mb">Añade recursos visuales para preguntas con soporte gráfico.</p>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
accept={IMAGE_ACCEPT}
|
accept={IMAGE_ACCEPT}
|
||||||
icon="image"
|
icon="image"
|
||||||
|
|||||||
@@ -67,10 +67,12 @@ export default function MaterialsTab({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid" style={{ gridTemplateColumns: "1fr 320px" }}>
|
<div className="grid layout-split">
|
||||||
<div>
|
<div>
|
||||||
<div className="card mb">
|
<div className="card mb">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
<h3 className="section-title">Subir material para IA</h3>
|
||||||
|
<p className="section-subtle mb">Soporta PDF, DOCX, TXT, MD e imágenes con OCR.</p>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<div className="text-center" style={{ padding: 24 }}>
|
<div className="text-center" style={{ padding: 24 }}>
|
||||||
<Spinner large />
|
<Spinner large />
|
||||||
@@ -127,7 +129,7 @@ export default function MaterialsTab({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-sm items-center" style={{ flex: "none" }}>
|
<div className="flex gap-sm items-center mobile-stack list-item-actions" style={{ flex: "none" }}>
|
||||||
<Badge variant={badge.variant}>{badge.label}</Badge>
|
<Badge variant={badge.variant}>{badge.label}</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="danger-ghost"
|
variant="danger-ghost"
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ export default function OverviewTab({ template, storage, goToTab }) {
|
|||||||
const profile = template.difficulty_profile || {};
|
const profile = template.difficulty_profile || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid" style={{ gridTemplateColumns: "1.4fr 1fr" }}>
|
<div className="grid layout-split-wide">
|
||||||
<div>
|
<div>
|
||||||
<div className="card mb">
|
<div className="card mb">
|
||||||
<div className="card-head">
|
<div className="card-head">
|
||||||
<h3>Estructura del examen</h3>
|
<h3>Estructura del examen</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h4 className="text-soft text-sm">Tipos de pregunta</h4>
|
<h4 className="section-title">Tipos de pregunta</h4>
|
||||||
{qTypes.map((qt, i) => (
|
{qTypes.map((qt, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex justify-between items-center"
|
className="flex justify-between items-center wrap gap-sm"
|
||||||
style={{
|
style={{
|
||||||
padding: "10px 0",
|
padding: "10px 0",
|
||||||
borderBottom:
|
borderBottom:
|
||||||
@@ -50,7 +50,7 @@ export default function OverviewTab({ template, storage, goToTab }) {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="divider-line" />
|
<div className="divider-line" />
|
||||||
<h4 className="text-soft text-sm">Reparto por dificultad</h4>
|
<h4 className="section-title">Reparto por dificultad</h4>
|
||||||
<div className="flex gap-sm wrap">
|
<div className="flex gap-sm wrap">
|
||||||
{Object.entries(profile).map(([key, val]) =>
|
{Object.entries(profile).map(([key, val]) =>
|
||||||
val > 0 ? (
|
val > 0 ? (
|
||||||
@@ -62,6 +62,7 @@ export default function OverviewTab({ template, storage, goToTab }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider-line" />
|
<div className="divider-line" />
|
||||||
|
<h4 className="section-title">Opciones activas</h4>
|
||||||
<div className="flex gap-sm wrap text-sm">
|
<div className="flex gap-sm wrap text-sm">
|
||||||
<Badge variant={template.settings?.shuffle_questions ? "success" : undefined}>
|
<Badge variant={template.settings?.shuffle_questions ? "success" : undefined}>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export default function QuestionsTab({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb">
|
<div className="flex justify-between items-center mb wrap gap-sm">
|
||||||
<p className="text-soft" style={{ margin: 0 }}>
|
<p className="text-soft page-lead" style={{ margin: 0 }}>
|
||||||
{questions.length} preguntas guardadas. Vincula imágenes a las
|
{questions.length} preguntas guardadas. Vincula imágenes a las
|
||||||
preguntas que las necesiten.
|
preguntas que las necesiten.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user