Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1

> Las entradas entre la 0.6.0 y la 1.4.0 se reconstruyeron a posteriori a partir del historial de git, agrupadas por los saltos de versión que realmente ocurrieron en `package.json`. La 1.1.0 nunca existió: se pasó directamente de la 1.0.0 a la 1.2.0.

## [1.26.0] - 2026-08-19

### Added

- **Rediseño móvil del espacio de trabajo, fase 4 del plan.** Móvil deja de ser el escritorio encogido: barra inferior de cuatro pestañas (Panel, Tablero, Páginas, Más) que sustituye a la hamburguesa, con una hoja para lo que no cabe.
- **Tablero en móvil.** Una columna a ancho completo con un conmutador de estado, en vez de cuatro columnas de 320px en 360 de pantalla. Mover un ticket se hace desde un desplegable en la tarjeta, porque el arrastre HTML5 del tablero no dispara con eventos táctiles.
- **Modal de ticket en móvil.** Los cuatro pares de campos se apilan en vez de partirse en columnas de 150px, y la cabecera se envuelve en vez de abrir un desplazamiento horizontal.
- **Tabla dinámica en móvil.** Cada fila pasa a ficha, con el campo principal de título y el resto plegado tras «N campos más».

### Fixed

- **La regla que garantiza el área táctil resucitaba `.hidden`.** `a, button { display: inline-flex }` en `themes.css` ganaba a las utilidades de Tailwind por especificidad, así que por debajo de 768px `.hidden` dejaba de esconder nada: en el móvil aparecían los controles de información, tema, idioma y registro que el código manda ocultar por no caber. La misma regla forzaba `display` sobre enlaces que ya traían el suyo, y las tarjetas del panel que son enlaces —rótulo arriba, cifra debajo— se volvían filas.
- **Varios `z-50` puestos para ganar a un tooltip también ganaban al panel de notificaciones y al cajón lateral.** El botón «+ Nuevo ticket» se pintaba encima del panel de avisos abierto, y el botón que abre el árbol de páginas quedaba por delante de su propio cajón.
- **El modal «Acerca de» estaba a 650px fijos**, se salía de una pantalla de 360, ofrecía una pestaña de atajos de teclado a quien no tiene teclado, y tres de sus cadenas seguían en inglés dentro de una interfaz en español.
- **Un intermitente real en la paleta de comandos**: una búsqueda que llegaba después de cerrar el diálogo lo repintaba con la consulta anterior. Fallaba una de cada ocho corridas de la suite completa; con la protección puesta, cinco corridas seguidas en verde.
- El menú del sprint se desplegaba fuera de la pantalla al anclarse a un botón que ahora vive en la columna izquierda de una rejilla en móvil.
- El tablero vacío ofrecía «Presiona [C] para crear una nueva tarea» en un teléfono sin teclado.
- «1 campos más», «Ajustes del Workspace» junto a «Espacio» en otra pantalla, y «Knowledge Base» sin traducir dentro del modal «Acerca de».

## [1.25.0] - 2026-08-17

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "forge-js",
"type": "module",
"version": "1.25.0",
"version": "1.26.0",
"engines": {
"node": ">=22.12.0"
},
Expand Down
38 changes: 38 additions & 0 deletions src/components/db/CeldaValor.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
/**
* El valor de una celda, pintado según el tipo de su columna.
*
* Existe porque la misma celda se pinta ahora en dos sitios: la tabla de
* escritorio y la ficha de móvil. Las tres reglas que lleva dentro —un archivo
* que ya no resuelve no se enseña como un id crudo, una selección va en
* insignia, un vacío va en cursiva y no en blanco— se escribieron una vez para
* la tabla, y copiarlas a la ficha era garantizar que la próxima corrección
* llegara solo a una de las dos.
*/
interface Props {
tipo: string;
valor: any;
/** Los archivos del espacio, por id. Resolver aquí evita una consulta por celda. */
archivos: Map<string, { name: string; webViewLink?: string | null }>;
}
const { tipo, valor, archivos } = Astro.props;

import { useTranslations } from '../../i18n/ui';
const t = useTranslations(Astro.locals.lang || 'en');

const archivo = tipo === 'file' && valor ? archivos.get(valor) : null;
const vacio = valor === null || valor === undefined || valor === '';
---
{tipo === 'file' ? (
!archivo ? (
<span class="text-forge-muted italic">{t('db.no_file')}</span>
) : archivo.webViewLink ? (
<a href={archivo.webViewLink} target="_blank" rel="noopener noreferrer" class="text-forge-accent-text hover:underline">{archivo.name}</a>
) : (
<span>{archivo.name}</span>
)
) : tipo === 'select' && !vacio ? (
<span class="inline-block px-2 py-0.5 rounded text-[10px] font-bold bg-forge-secondary/10 text-forge-accent-text border border-forge-secondary/20">{valor}</span>
) : (
<span class={vacio ? 'text-forge-muted italic' : ''}>{vacio ? t('db.empty_cell') : valor}</span>
)}
21 changes: 19 additions & 2 deletions src/components/jira/BoardColumn.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
interface Props {
title: string;
status: string;
/** La que se ve en móvil. Las demás se esconden por debajo de `sm`. */
activaEnMovil?: boolean;
/** Cuántos hay de verdad en esta columna, contados en la base. */
total?: number;
/** Cuántos se han pintado. Si es menor que `total`, se avisa. */
Expand All @@ -10,7 +12,7 @@ interface Props {

import { useTranslations } from '../../i18n/ui';

const { title, status, total, shown } = Astro.props;
const { title, status, total, shown, activaEnMovil = false } = Astro.props;
const t = useTranslations(Astro.locals.lang || 'en');

// El contador del encabezado lo pisa el JavaScript al arrastrar tarjetas, pero
Expand All @@ -19,7 +21,22 @@ const t = useTranslations(Astro.locals.lang || 'en');
const headerCount = typeof total === 'number' ? total : 0;
const truncated = typeof total === 'number' && typeof shown === 'number' && total > shown;
---
<div class="board-column flex-1 flex flex-col bg-forge-bg/60 border border-forge-border/40 rounded-xl overflow-hidden min-w-[320px] shadow-sm relative group/boardcol" data-status={status} data-total={headerCount} data-shown={shown ?? 0}>
{/*
En móvil se ve una columna, no cuatro.

Eran cuatro `min-w-[320px]`: 1328 px de carril en los 393 que hay, sin anclaje
ni indicador. Se desplazaba a ciegas y siempre se acababa entre dos columnas,
viendo media de cada una.

El paso intermedio fue `w-[85vw]` con anclaje, que arregla el desplazamiento
pero no el modelo: seguían siendo cuatro columnas de escritorio puestas en
fila. Ahora se enseña **la columna elegida a ancho completo** y el conmutador
de arriba dice cuáles son las otras y cuánto hay en cada una — que es la
información que el carril escondía.

En `sm` y por encima no cambia nada: las cuatro columnas, como siempre.
*/}
<div class:list={["board-column shrink-0 max-sm:w-full sm:w-auto sm:shrink sm:flex-1 flex flex-col bg-forge-bg/60 border border-forge-border/40 rounded-xl overflow-hidden sm:min-w-[320px] shadow-sm relative group/boardcol", !activaEnMovil && "max-sm:hidden"]} data-status={status} data-total={headerCount} data-shown={shown ?? 0}>
<!-- Subtle top accent line for column -->
<div class="h-1 w-full bg-gradient-to-r from-forge-secondary/10 via-forge-secondary/40 to-forge-secondary/10 opacity-0 group-hover/boardcol:opacity-100 transition-opacity"></div>
<div class="bg-forge-panel/70 backdrop-blur-sm max-md:backdrop-blur-none border-b border-forge-border/40 p-4 font-bold text-[13px] tracking-wide text-forge-text uppercase flex items-center justify-between">
Expand Down
31 changes: 31 additions & 0 deletions src/components/jira/IssueCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,37 @@ const t = useTranslations(lang);
</span>
</div>

{/*
Mover un ticket, en móvil.

El arrastrar y soltar del tablero es HTML5 —`dragstart`, `dragover`,
`drop`— y **no dispara con eventos táctiles**. En un teléfono no había forma
de mover una tarjeta de columna: ni arrastrando, ni desde el modal, que ni
siquiera tiene selector de estado. La función simplemente no existía, y nada
lo decía.

Un desplegable en la tarjeta y no un arrastre táctil imitado: reproducir el
arrastre con `touchmove` sobre un carril que además se desplaza en
horizontal es pelearse con el gesto de desplazamiento del sistema, y se
pierden las dos cosas.
*/}
{canEdit && (
<select
class="mover-issue sm:hidden w-full mt-1 mb-3 bg-forge-bg border border-forge-border rounded-md px-2 py-1.5 text-xs text-forge-text focus:outline-none focus:border-forge-secondary"
data-issue-id={issue.id}
aria-label={t('card.move_to')}
>
{[
{ v: 'todo', t: t('status.todo') },
{ v: 'in_progress', t: t('status.in_progress') },
{ v: 'review', t: t('status.review') },
{ v: 'done', t: t('status.done') },
].map((o) => (
<option value={o.v} selected={issue.status === o.v}>{o.t}</option>
))}
</select>
)}

{canEdit && issue.status !== 'done' && (
<div class="mt-3 flex justify-end">
{issue.active_session_start ? (
Expand Down
39 changes: 26 additions & 13 deletions src/components/jira/IssueDetailsModal.astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,40 @@ const t = useTranslations(lang);
data-t-del-error={t('issue.delete_error')}
class="fixed top-0 right-0 w-[550px] max-w-full h-full bg-forge-bg/95 backdrop-blur-md max-md:backdrop-blur-none border-l border-forge-border/40 z-[110] transform translate-x-full transition-transform duration-300 ease-out shadow-2xl shadow-black/50 overflow-y-auto flex flex-col">

{/*
La cabecera se envuelve en vez de desbordarse.

Son seis cosas en una fila —tipo, id, compartir, duplicar, eliminar y
cerrar— y en 360 px suman bastante más de lo que hay. El resultado no era
que se apretaran: era una **barra de desplazamiento horizontal dentro del
modal**, con «Eliminar» y la equis fuera de la pantalla hasta que se
arrastraba de lado. Cerrar el modal exigía descubrir primero que se podía
desplazar.

`flex-wrap` lo parte en dos filas cuando no cabe y lo deja en una cuando sí,
así que en escritorio no cambia nada.
*/}
<!-- Header -->
<div class="px-6 py-4 border-b border-forge-border/30 flex justify-between items-center bg-gradient-to-r from-forge-panel to-forge-bg sticky top-0 z-10">
<div class="flex items-center gap-3">
<div class="px-4 sm:px-6 py-3 sm:py-4 border-b border-forge-border/30 flex flex-wrap justify-between items-center gap-y-2 bg-gradient-to-r from-forge-panel to-forge-bg sticky top-0 z-10">
<div class="flex items-center gap-3 min-w-0">
<select id="modal-issue-type" aria-label={t('a11y.issue_type')} class="px-2 py-1 rounded-md text-[11px] font-bold tracking-wider bg-forge-bg/50 text-forge-text border border-forge-border/50 focus:border-forge-secondary focus:ring-1 focus:ring-forge-secondary/50 focus:outline-none uppercase transition-all shadow-sm">
{issueTypes.map((ti) => (
<option value={ti.key}>{ti.isBuiltin ? (t(`type.${ti.key}` as any) || ti.name) : ti.name}</option>
))}
</select>
<span id="modal-issue-id" class="font-mono text-[13px] text-forge-muted tracking-wide font-medium">ID-XXX</span>
<span id="modal-issue-id" class="font-mono text-[13px] text-forge-muted tracking-wide font-medium truncate">ID-XXX</span>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-1.5 sm:gap-3 ml-auto">
<button id="btn-share-issue" class="text-forge-muted hover:text-forge-accent-text hover:bg-forge-secondary/10 px-3 py-1.5 rounded-md text-xs font-bold transition-all border border-transparent hover:border-forge-secondary/30 flex items-center gap-1" title={t('modal.copy_link')}>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
{t('modal.share')}
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
<span class="hidden sm:inline">{t('modal.share')}</span>
</button>
{/* Duplicar.
Va aquí y no en la tarjeta del tablero porque duplicar se decide
después de mirar el ticket entero, no de pasada. */}
<button id="btn-duplicate-issue" class="text-forge-muted hover:text-forge-accent-text hover:bg-forge-secondary/10 px-3 py-1.5 rounded-md text-xs font-bold transition-all border border-transparent hover:border-forge-secondary/30 flex items-center gap-1 hidden" title={t('issue.duplicate_hint')}>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
{t('issue.duplicate')}
<span class="hidden sm:inline">{t('issue.duplicate')}</span>
</button>
<button id="btn-delete-issue" class="text-forge-error hover:bg-red-400/10 hover:text-forge-error px-3 py-1.5 rounded-md text-xs font-bold transition-all hidden border border-transparent hover:border-red-400/20">
{t('modal.delete')}
Expand All @@ -82,12 +95,12 @@ const t = useTranslations(lang);
</div>

<!-- Content -->
<div class="p-8 flex-1 flex flex-col gap-8">
<div class="p-5 sm:p-8 flex-1 flex flex-col gap-6 sm:gap-8">
<div class="relative group">
<input type="text" id="modal-issue-title" aria-label={t('a11y.issue_title')} class="text-2xl font-black text-forge-text bg-transparent border-b-2 border-transparent hover:border-forge-border/50 focus:border-forge-secondary focus:outline-none w-full transition-all pb-1 tracking-tight" value="Loading...">
</div>

<div class="grid grid-cols-2 gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div class="space-y-2 relative">
<label class="text-[10px] font-bold text-forge-muted uppercase tracking-widest flex items-center gap-1.5" for="modal-issue-assignee">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
Expand Down Expand Up @@ -139,7 +152,7 @@ const t = useTranslations(lang);
</div>
</div>

<div class="grid grid-cols-2 gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div class="space-y-2">
<label class="text-[10px] font-bold text-forge-muted uppercase tracking-widest" for="modal-issue-sprint">{t('modal.sprint')}</label>
<select id="modal-issue-sprint" class="w-full bg-forge-panel/40 border border-forge-border/50 rounded-lg p-2.5 text-sm text-forge-text focus:outline-none focus:border-forge-secondary focus:ring-1 focus:ring-forge-secondary/30 transition-all shadow-sm">
Expand Down Expand Up @@ -204,7 +217,7 @@ const t = useTranslations(lang);
</div>
</div>

<div class="grid grid-cols-2 gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div class="space-y-1.5">
<label class="text-[10px] font-bold text-forge-muted uppercase tracking-widest" for="modal-issue-estimated">{t('modal.estimated_hours')}</label>
<input type="number" id="modal-issue-estimated" value="0" min="0" step="0.5" class="w-full bg-forge-bg border border-forge-border/50 rounded-lg py-2 px-3 text-sm text-forge-text focus:outline-none focus:border-forge-secondary focus:ring-1 focus:ring-forge-secondary/30 transition-all font-mono" />
Expand Down Expand Up @@ -278,13 +291,13 @@ const t = useTranslations(lang);
</div>

<!-- Log Work Modal -->
<dialog id="log-work-modal" class="bg-forge-panel border border-forge-border rounded-xl p-6 w-full max-w-[400px] text-forge-text shadow-2xl backdrop:bg-black/60">
<dialog id="log-work-modal" class="bg-forge-panel border border-forge-border rounded-xl p-5 sm:p-6 w-[calc(100vw-2rem)] max-w-[400px] text-forge-text shadow-2xl backdrop:bg-black/60">
<h2 class="text-lg font-bold mb-5 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-forge-accent-text"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
{t('modal.log_work')}
</h2>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-[10px] font-bold uppercase tracking-wider text-forge-muted mb-1.5" for="log-work-hours">{t('modal.time_spent')}</label>
<input type="number" id="log-work-hours" step="0.25" min="0.25" class="w-full bg-forge-bg border border-forge-border rounded-lg p-2 text-forge-text focus:border-forge-secondary focus:outline-none font-mono" placeholder="e.g. 2.5">
Expand Down
Loading