diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7e4f9..5d12940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 49248c3..adef81b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forge-js", - "version": "1.25.0", + "version": "1.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forge-js", - "version": "1.25.0", + "version": "1.26.0", "dependencies": { "@astrojs/check": "^0.9.10", "@astrojs/node": "^11.1.2", diff --git a/package.json b/package.json index 3a76a5a..c0d0cb3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "forge-js", "type": "module", - "version": "1.25.0", + "version": "1.26.0", "engines": { "node": ">=22.12.0" }, diff --git a/src/components/db/CeldaValor.astro b/src/components/db/CeldaValor.astro new file mode 100644 index 0000000..155b4f4 --- /dev/null +++ b/src/components/db/CeldaValor.astro @@ -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; +} +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 ? ( + {t('db.no_file')} + ) : archivo.webViewLink ? ( + {archivo.name} + ) : ( + {archivo.name} + ) +) : tipo === 'select' && !vacio ? ( + {valor} +) : ( + {vacio ? t('db.empty_cell') : valor} +)} diff --git a/src/components/jira/BoardColumn.astro b/src/components/jira/BoardColumn.astro index e636b6d..3fccdf3 100644 --- a/src/components/jira/BoardColumn.astro +++ b/src/components/jira/BoardColumn.astro @@ -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. */ @@ -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 @@ -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; --- -
+{/* + 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. +*/} +
diff --git a/src/components/jira/IssueCard.astro b/src/components/jira/IssueCard.astro index 5886156..1c5d723 100644 --- a/src/components/jira/IssueCard.astro +++ b/src/components/jira/IssueCard.astro @@ -90,6 +90,37 @@ const t = useTranslations(lang);
+ {/* + 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 && ( + + )} + {canEdit && issue.status !== 'done' && (
{issue.active_session_start ? ( diff --git a/src/components/jira/IssueDetailsModal.astro b/src/components/jira/IssueDetailsModal.astro index 23e0c92..05650e1 100644 --- a/src/components/jira/IssueDetailsModal.astro +++ b/src/components/jira/IssueDetailsModal.astro @@ -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. + */} -
-
+
+
- ID-XXX + ID-XXX
-
+
{/* Duplicar. Va aquí y no en la tarjeta del tablero porque duplicar se decide después de mirar el ticket entero, no de pasada. */}
-
+
-
+
-
+
@@ -278,13 +291,13 @@ const t = useTranslations(lang);
- +

{t('modal.log_work')}

-
+
diff --git a/src/components/jira/KanbanBoard.astro b/src/components/jira/KanbanBoard.astro index 894caca..38719f4 100644 --- a/src/components/jira/KanbanBoard.astro +++ b/src/components/jira/KanbanBoard.astro @@ -51,12 +51,28 @@ const ordenManual = sortMode === 'manual'; const sePuedeArrastrar = canEdit; const columns = [ - { id: 'todo', title: t('status.todo') }, - { id: 'in_progress', title: t('status.in_progress') }, - { id: 'review', title: t('status.review') }, - { id: 'done', title: t('status.done') } + { id: 'todo', title: t('status.todo'), corto: t('status.short.todo') }, + { id: 'in_progress', title: t('status.in_progress'), corto: t('status.short.in_progress') }, + { id: 'review', title: t('status.review'), corto: t('status.short.review') }, + { id: 'done', title: t('status.done'), corto: t('status.short.done') } ]; +/** + * El estado que se ve al abrir el tablero en un teléfono. + * + * `todo` y no «el primero que tenga algo»: el tablero tiene que abrirse + * siempre en el mismo sitio, o el conmutador salta solo entre visitas y deja + * de servir como punto de referencia. + */ +const ESTADO_INICIAL = 'todo'; + +// Se cuenta una vez y lo usan las columnas y el conmutador. Antes el `filter` +// se repetía dos veces por columna dentro del propio `map`. +const porEstado = new Map( + columns.map((col) => [col.id, issues.filter((i) => i.status === col.id)]) +); +const totalDe = (id: string) => statusCounts[id] ?? (porEstado.get(id)?.length ?? 0); + // Helper para extraer iniciales function getInitials(username: string) { if (!username) return 'UN'; @@ -64,15 +80,50 @@ function getInitials(username: string) { } ---
+ {/* + El conmutador de estado. + + En escritorio las cuatro columnas se ven de un vistazo y el encabezado de + cada una lleva su cuenta. En un teléfono no cabe ninguna de las dos cosas, + así que la información se traslada aquí: qué estados hay, cuál se está + mirando y cuánto hay en cada uno, en una fila. + + Es `role="tablist"` y no un `` que hay + * dentro lo abría encima y el desplegable no llegaba a desplegarse: en + * móvil, mover un ticket era imposible **incluso con el control puesto**. + */ + if ((e.target as HTMLElement)?.closest?.('.mover-issue')) return; + try { const modal = document.getElementById('issue-details-modal'); const titleEl = document.getElementById('modal-issue-title'); diff --git a/src/components/layout/BottomNav.astro b/src/components/layout/BottomNav.astro new file mode 100644 index 0000000..17b9b69 --- /dev/null +++ b/src/components/layout/BottomNav.astro @@ -0,0 +1,333 @@ +--- +/** + * La navegación de móvil. + * + * Hasta ahora un teléfono navegaba con el mismo menú lateral del escritorio, + * metido en un cajón detrás de una hamburguesa arriba a la izquierda: la + * esquina más lejos del pulgar de toda la pantalla. Cada salto entre el tablero + * y las páginas costaba abrir el cajón, buscar en una lista de once entradas y + * cerrarlo. + * + * Aquí abajo van las tres a las que de verdad se salta —el panel, el tablero y + * las páginas— y una cuarta que abre el resto. El cajón lateral sigue existiendo + * y sigue siendo el único menú a partir de `md`; esto solo lo sustituye por + * debajo, que es donde no servía. + * + * Las mismas consultas que el cajón, y por eso salen de `lib/consultas`: dos + * componentes preguntando lo mismo con dos SQL distintos es cómo se acaba + * enseñando un rol en un sitio y otro en el otro. + */ +interface Props { + workspaceId?: string; + workspaceName?: string; +} +const { workspaceId } = Astro.props; + +import { useTranslations } from '../../i18n/ui'; +import { solicitudesPendientes, rolEnEspacio, sysTagDeEspacio } from '../../lib/consultas'; + +const user = Astro.locals.user!; +const lang = Astro.locals.lang || 'en'; +const t = useTranslations(lang); + +let sysTag = Astro.params.sys_tag; +if (!sysTag && user?.last_workspace_id) { + sysTag = sysTagDeEspacio(user.last_workspace_id) ?? sysTag; +} + +const ruta = Astro.url.pathname; +const pendientes = solicitudesPendientes(user?.id ?? '', !!user?.is_guest); + +let rol = user?.is_sysadmin === 1 ? 'sysadmin' : 'viewer'; +if (workspaceId && user?.is_sysadmin !== 1) { + rol = rolEnEspacio(workspaceId, user?.id ?? '') ?? rol; +} +const esPropietario = rol === 'owner' || rol === 'sysadmin'; + +/** + * Fuera de un espacio la barra no se esconde: cambia de destinos. + * + * Una barra que aparece y desaparece según la página obliga a mirar antes de + * tocar, y con eso ya ha perdido lo que la hace útil. En el hub, la actividad o + * el perfil las tres primeras pestañas son las globales. + */ +const enEspacio = Boolean(sysTag); + +/* + * Etiquetas propias, no las del menú lateral. + * + * Se reusaban `sidebar.dashboard`, `sidebar.kanban` y `sidebar.knowledge`, que + * en español son «Panel Principal», «Tablero Kanban» y «Base de Conocimiento». + * En una barra de cuatro huecos sobre 360 px eso son 90 px por etiqueta: las + * tres se desbordaban y se pisaban entre sí. En el lateral caben porque hay una + * columna entera; aquí no, y una etiqueta que invade la de al lado es peor que + * una corta. + */ +const pestanas = enEspacio + ? [ + { href: `/w/${sysTag}`, etiqueta: t('nav.panel'), icono: 'panel', + activa: ruta === `/w/${sysTag}` || ruta === `/w/${sysTag}/` }, + { href: `/w/${sysTag}/board`, etiqueta: t('nav.board'), icono: 'tablero', + activa: ruta.includes('/board') || ruta.includes('/backlog') }, + { href: `/w/${sysTag}/p`, etiqueta: t('nav.pages'), icono: 'paginas', + activa: ruta.endsWith('/p') || ruta.includes('/p/') }, + ] + : [ + { href: '/', etiqueta: t('nav.hub'), icono: 'panel', activa: ruta === '/' }, + { href: '/activity', etiqueta: t('sidebar.activity'), icono: 'tablero', + activa: ruta.startsWith('/activity') }, + { href: '/people', etiqueta: t('friends.title'), icono: 'paginas', + activa: ruta.startsWith('/people') }, + ]; +--- + + + + +
+
+ + {enEspacio && ( + <> +

{t('nav.this_space')}

+ + + {t('sidebar.databases')} + + + + {t('drive.sidebar')} + + + + {t('sidebar.metrics')} + + {esPropietario && ( + + + {t('sidebar.settings')} + + )} +
+ + )} + + + + {t('sidebar.switch_ws')} + + + {user?.is_guest === 1 ? ( + + + {t('user.menu.register')} + + ) : ( + <> + + + {t('user.menu.profile')} + + + + {t('friends.title')} + {pendientes > 0 && ( + {pendientes > 9 ? '9+' : pendientes} + )} + + + )} + + + + {t('user.menu.account')} + + + + +
+ +
+ + +
+
+
+
+ + + + diff --git a/src/components/layout/Footer.astro b/src/components/layout/Footer.astro index 228c07d..bcb02dc 100644 --- a/src/components/layout/Footer.astro +++ b/src/components/layout/Footer.astro @@ -30,7 +30,10 @@ const version = pkg.version;
-
+ {/* Tres columnas sobre 360 px dejan 90 px por columna: «OPEN SOURCE» se parte + en dos líneas y cada enlace debajo también. Dos columnas en móvil y tres a + partir de `sm`. */} +

{t('footer.opensource')}