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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ 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.1] - 2026-08-20

### Fixed

- **La barra de navegación de móvil no se veía en un teléfono real.** Era el último hijo de una columna de `100vh`, y `100vh` en un navegador móvil **no es la altura visible**: es la que habría con las barras del navegador retraídas. El contenedor era más alto que la pantalla y la barra caía por debajo del corte. En el emulador no pasaba, porque allí el viewport y `innerHeight` coinciden — por eso las pruebas estaban en verde. Ahora la barra va `fixed`, anclada a lo que se ve, y el armazón mide `100dvh`.
- **Al cerrar el teclado, la página se quedaba corrida hacia un lado**, con el texto cortado por la izquierda. Faltaba `interactive-widget=resizes-content` en la etiqueta `viewport`: sin eso el navegador no encoge la maqueta al abrir el teclado, la deja igual y desplaza el viewport visual por encima.
- **El menú del sprint se salía de la pantalla.** Se ancló primero a la derecha del botón y luego a la izquierda, y cada versión fallaba en la mitad de los tableros: en qué columna de la rejilla cae el botón depende de si el espacio tiene etiquetas, porque el filtro «Todas las etiquetas» solo se pinta entonces. Ahora se fija a los dos bordes de la pantalla y deja de depender del botón.
- **El botón que abre el índice de la base de conocimiento pasa a la fila del título.** Flotando chocaba con el contenido las dos veces que se intentó — abajo quedaba fuera del área visible, arriba tapaba el título de la página. Dos colisiones seguidas eran la señal de que el sitio equivocado era «flotando», no «arriba» o «abajo».

## [1.26.0] - 2026-08-19

### Added
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.26.0",
"version": "1.26.1",
"engines": {
"node": ">=22.12.0"
},
Expand Down
23 changes: 23 additions & 0 deletions src/components/layout/BottomNav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,31 @@ const pestanas = enEspacio
atributo del componente, así que suben por encima de las dos y las pestañas
se apilan como tienen que apilarse.
*/
/*
Anclada al viewport, no al final del contenedor.

Era el último hijo de una columna flex de `100vh`, y en un navegador móvil
`100vh` **no es la altura visible**: es la que habría con las barras del
navegador retraídas. Así que el contenedor era más alto que la pantalla y la
barra caía por debajo del corte — en producción, en un teléfono real, no se
veía en absoluto. En el emulador sí, porque allí el viewport y `innerHeight`
coinciden y el fallo no existe.

`fixed` la saca de esa dependencia: se pega al borde de lo que se ve, pase
lo que pase con la altura del documento o con las barras del navegador
apareciendo y desapareciendo.

El `env(safe-area-inset-bottom)` es para el indicador de inicio de los
iPhone sin botón, que si no se come la fila de etiquetas.
*/
.nav-movil {
display: flex;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 40;
padding-bottom: env(safe-area-inset-bottom, 0px);
}

/*
Expand Down
21 changes: 20 additions & 1 deletion src/components/notion/EditorClient.astro
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,27 @@ function formatDate(dateStr: string | undefined) {
</nav>
)}

{/* El índice se abre desde aquí, no desde un botón flotante.

Flotando chocaba con todo: abajo quedaba fuera del área visible por el
`100vh` del armazón, y arriba se comía el título de la página. Dos
intentos, dos colisiones — la señal de que el sitio equivocado era
«flotando», no «arriba» o «abajo».

En la fila del título no se superpone a nada, y queda a la izquierda del
título igual que el árbol está a la izquierda del texto en escritorio. */}
<!-- Title & Delete -->
<div class="flex items-center justify-between mb-4 group relative">
<div class="flex items-center justify-between gap-2 mb-4 group relative">
<button
type="button"
id="btn-open-tree"
aria-controls="page-tree-aside"
aria-expanded="false"
class="md:hidden shrink-0 w-10 h-10 rounded-lg border border-forge-border text-forge-muted hover:text-forge-accent-text flex items-center justify-center"
aria-label={t('kb.open_tree')}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
</button>
<input type="text" id="editor-title" aria-label={t('a11y.page_title')}
value={initialTitle}
readonly={!canEdit}
Expand Down
28 changes: 5 additions & 23 deletions src/components/notion/PageTree.astro
Original file line number Diff line number Diff line change
Expand Up @@ -91,29 +91,11 @@ for (const p of pages) {
el botón para volver al texto. */}
<div id="page-tree-backdrop" class="fixed inset-0 bg-black/60 z-30 hidden md:hidden"></div>

{/* Abre el cajón. Solo en móvil: en escritorio el árbol ya está a la vista.

`z-20` y no `z-50`: por encima del contenido de la página, pero por debajo
del velo del propio cajón (`z-30`) y del desplegable de notificaciones
(`z-50`). Con `z-50` el botón se quedaba flotando encima de los dos, tapando
la cabecera del cajón que él mismo acababa de abrir.

Anclado **arriba**, no abajo. Abajo se montaba encima de la primera pestaña
de la barra de navegación, y subirlo para esquivarla lo dejaba fuera del
área visible: en emulación móvil el viewport de maquetación es más alto que
el que se ve, así que `bottom` no cae donde parece. Arriba no hay ninguna de
las dos cosas — y es donde el índice está en escritorio, así que se busca en
el mismo sitio en las dos pantallas. */}
<button
id="btn-open-tree"
type="button"
aria-controls="page-tree-aside"
aria-expanded="false"
class="md:hidden fixed top-20 left-4 z-20 w-12 h-12 rounded-full bg-forge-primary text-forge-on-accent shadow-lg flex items-center justify-center"
aria-label={t('kb.open_tree')}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
</button>
{/* El botón que abre este cajón vive en la fila del título del editor
(`EditorClient.astro`), no aquí. Estuvo flotando y chocó con el contenido
las dos veces que se intentó — abajo se salía de la pantalla, arriba tapaba
el título. El manejador sigue aquí abajo porque el cajón es de este
componente; lo busca por id. */}

<script>
// Handle toggling of folders
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/en/kb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const en = {
'kb.saved': 'Saved',
'kb.empty_title': 'No pages yet.',
'kb.empty_desc': 'No pages found in this workspace.',
'kb.empty_cta_movil': 'Open the page index with the button at the top left and create the first one.',
'kb.empty_cta_movil': 'Open the page index with the button at the bottom left and create the first one.',
'kb.empty_cta': 'Click "New Page" in the sidebar to create one.',
'kb.open_tree': 'Open page index',
'kb.editor_tips': 'Editor tips',
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/es/kb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const es = {
'kb.saved': 'Guardado',
'kb.empty_title': 'Aún no hay páginas.',
'kb.empty_desc': 'No se encontraron páginas en este workspace.',
'kb.empty_cta_movil': 'Ábrelo con el botón del índice, arriba a la izquierda, y crea la primera.',
'kb.empty_cta_movil': 'Ábrelo con el botón del índice, abajo a la izquierda, y crea la primera.',
'kb.empty_cta': 'Haz clic en "Nueva Página" en la barra lateral para crear una.',
'kb.open_tree': 'Abrir el índice de páginas',
'kb.editor_tips': 'Ayuda del editor',
Expand Down
16 changes: 13 additions & 3 deletions src/layouts/MainLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ Astro.response.headers.set('Pragma', 'no-cache');
<html lang={Astro.locals.lang || 'es'} data-theme={user?.theme_preference || 'light'}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5" />
{/* `interactive-widget=resizes-content`: cuando se abre el teclado, que el
navegador **encoja la maqueta** en vez de dejarla igual y desplazar el
viewport visual por encima. Sin esto, al escribir en el editor y volver
a cerrar el teclado la página se quedaba corrida hacia un lado, con el
texto cortado por la izquierda y la barra inferior desplazada con él.
Va de la mano de `h-dvh` en el armazón: los dos miden lo que se ve. */}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, interactive-widget=resizes-content" />
<meta name="description" content={metaDesc} />
<meta name="keywords" content="workspace, kanban, knowledge base, forge os, team collaboration, productivity" />
<meta name="author" content="Forge OS" />
Expand Down Expand Up @@ -144,12 +150,16 @@ Astro.response.headers.set('Pragma', 'no-cache');
})();
</script>
</head>
<body tabindex="-1" class="bg-forge-bg text-forge-text flex h-screen overflow-hidden selection:bg-forge-primary selection:text-white" data-user-role={user?.is_sysadmin ? 'sysadmin' : 'user'} data-is-sysadmin={user?.is_sysadmin}>
<body tabindex="-1" class="bg-forge-bg text-forge-text flex h-dvh overflow-hidden selection:bg-forge-primary selection:text-white" data-user-role={user?.is_sysadmin ? 'sysadmin' : 'user'} data-is-sysadmin={user?.is_sysadmin}>
<Sidebar workspaceId={workspaceId} workspaceName={workspaceName} />
<div id="sidebar-backdrop" class="fixed inset-0 bg-black/60 z-40 hidden md:hidden transition-opacity opacity-0 backdrop-blur-sm max-md:backdrop-blur-none cursor-pointer"></div>
<div class="flex-1 flex flex-col min-w-0">
<TopBar title={title} workspaceName={workspaceName} />
<main class="flex-1 overflow-auto flex flex-col relative min-h-0">
{/* `pb-16` en móvil: la barra inferior es `fixed`, así que ya no ocupa
sitio en el flujo y taparía los últimos 64 px de lo que se desplaza.
El relleno va dentro del contenedor con scroll, así que el contenido
sube por encima de él al desplazarse. */}
<main class="flex-1 overflow-auto flex flex-col relative min-h-0 pb-16 md:pb-0">
<div class="max-w-[1440px] w-full mx-auto px-4 sm:px-6 py-4 sm:py-6 flex-1 shrink-0">
<GuestBanner />
<slot />
Expand Down
27 changes: 26 additions & 1 deletion src/pages/w/[sys_tag]/board.astro
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ const metaDescription = `${workspace.name} board on Forge OS. Track issues, spri
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</button>

<div id="sprint-menu" role="menu" class="hidden absolute left-0 right-auto sm:left-auto sm:right-0 top-full mt-1 min-w-[13rem] max-w-[calc(100vw-2rem)] bg-forge-panel border border-forge-border rounded-xl shadow-xl overflow-hidden z-50 py-1">
<div id="sprint-menu" role="menu" class="hidden menu-sprint absolute sm:right-0 top-full mt-1 min-w-[13rem] bg-forge-panel border border-forge-border rounded-xl shadow-xl overflow-hidden z-50 py-1">
{/* Crear un sprint también es una acción de sprint, y tampoco es
algo de cada día: sacarlo de la barra es lo que hace que
«+ Issue» —eso sí es de cada día— deje de caer a otra fila. */}
Expand Down Expand Up @@ -957,6 +957,31 @@ const metaDescription = `${workspace.name} board on Forge OS. Track issues, spri
</MainLayout>

<style>
/*
En móvil el menú no se ancla al botón, sino a la pantalla.

El botón vive en una rejilla de dos columnas y en cuál cae depende de
cuántos controles haya delante — el filtro de etiquetas solo aparece si el
espacio tiene alguna. Anclado a la izquierda del botón se salía por la
derecha cuando caía en la columna derecha; anclado a la derecha se salía por
la izquierda cuando caía en la izquierda. Se probaron las dos y cada una
falla en la mitad de los tableros.

Fijado a los dos bordes ya no depende de dónde esté el botón. `top: auto`
es imprescindible: `top-full` significa «100 % de la altura», que en
`absolute` es «justo debajo del botón» y en `fixed` es «el borde de abajo
de la pantalla».
*/
@media (max-width: 40rem) {
.menu-sprint {
position: fixed;
left: 1rem;
right: 1rem;
top: auto;
min-width: 0;
}
}

.sprint-menu-item {
display: block;
width: 100%;
Expand Down
11 changes: 11 additions & 0 deletions src/styles/themes.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@
dialog {
margin: auto;
}

/* `h-dvh` en el armazón sigue a la altura **visible**, que es lo que cambia
cuando el navegador móvil retrae sus barras. `100vh` da la altura con las
barras escondidas y deja el final del documento fuera de la pantalla.
Esta reserva es para navegadores anteriores a 2022, que no conocen `dvh`
y se quedarían sin ninguna altura. */
@supports not (height: 100dvh) {
body {
height: 100vh;
}
}
}


Expand Down
30 changes: 30 additions & 0 deletions tests/e2e/movil/capas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,33 @@ test('el menú del sprint no se sale de la pantalla', async ({ page }) => {
expect(caja!.x + caja!.width, 'el menú acaba fuera de la pantalla')
.toBeLessThanOrEqual(page.viewportSize()!.width + 1);
});

test('el menú del sprint tampoco se sale con el filtro de etiquetas puesto', async ({ page }) => {
await entrar(page);
const wsId = conEspacio();

/*
* Esta es la condición que se escapó en producción.
*
* El filtro «Todas las etiquetas» solo se pinta si el espacio tiene alguna, y
* al aparecer empuja el botón del sprint a la otra columna de la rejilla. La
* prueba anterior usaba un espacio sin etiquetas, así que fijaba el menú en
* la única posición donde el anclaje elegido funcionaba.
*/
const db = getTestDb();
db.prepare('DELETE FROM labels WHERE workspace_id = ?').run(wsId);
db.prepare("INSERT INTO labels (id, workspace_id, name, color) VALUES (?,?,?,?)")
.run(crypto.randomUUID(), wsId, 'Parcial 2', '#FF5D00');

await page.goto(`/w/${ESPACIO}/board`);
await expect(page.locator('#label-filter, [id*="label"]').first()).toBeVisible();

await page.locator('#btn-sprint-menu').click();
const menu = page.locator('#sprint-menu');
await expect(menu).toBeVisible();

const caja = await menu.boundingBox();
const ancho = page.viewportSize()!.width;
expect(caja!.x, 'se sale por la izquierda').toBeGreaterThanOrEqual(-1);
expect(caja!.x + caja!.width, 'se sale por la derecha').toBeLessThanOrEqual(ancho + 1);
});