diff --git a/DESPLIEGUE.md b/DESPLIEGUE.md index 7a7fd1f..7f22301 100644 --- a/DESPLIEGUE.md +++ b/DESPLIEGUE.md @@ -597,6 +597,22 @@ sobre todo cuando algo pasó a Hecho sin estar terminado. Desplegado el 28-oct: páginas con rsync después de la comparación con el servidor, backend con `kill -HUP` al maestro de gunicorn. Para deshacer: `git revert` del commit, `./scripts/deploy.sh` y `ssh -t leo@git.helgen.online 'sudo systemctl restart actas-hbox'`. +## A14 · 28-oct: elegir uno de tus comentarios como el porqué + +Lo pidió Leo (28-oct, 18:00): Michelle comenta antes de mover y luego el «¿por qué?» la hacía escribir lo mismo otra vez. + +- `backend/app.py` (PATCH `/api/tarea`): `_nota_de` = el `cuando` de un comentario **de la misma persona** en ese + pendiente (si no existe o es de otra persona: 400). La entrada del movimiento guarda `ref` (sin `nota`, así no se + repite) y su texto para la Actividad lleva «por su comentario del dd/mm hh:mm: «extracto»». Con `_nota` además, guarda + las dos cosas. +- `docs/tableros/kanban.js` + `tablero.css`: el «¿por qué?» lista tus últimos 5 comentarios del pendiente; si comentaste + después del último cambio de columna, ese ya viene elegido (un clic en «Mover»). «Escribir otro motivo» sigue ahí. En el + hilo, el comentario elegido lleva la etiqueta «→ Hecho». El porqué pegado igual que un comentario anterior de la misma + persona también se muestra una sola vez. El orden se toma del historial, no de la hora (hay horas en UTC y en hora local). +- Pruebas: `bash backend/tests-comentarios.sh .` (26) y las demás; en el navegador, el caso de Michelle de punta a punta. + +Desplegado igual que A13 (rsync de páginas tras la comparación; `kill -HUP` al maestro de gunicorn). + ## B · Verificación después de desplegar ```bash diff --git a/backend/app.py b/backend/app.py index 9208a29..5b0a9f5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1535,6 +1535,16 @@ def tarea_modificar(): # comentario (28-oct): el «¿por qué?» al mover y los comentarios de la tarjeta; hasta 1000 caracteres. Va en el # texto del historial (la Actividad lo lee así) y aparte en «nota», para el hilo de comentarios del detalle. nota = _texto(d.get("_nota"), 1000) + # «_nota_de» (28-oct, Leo): el porqué es un comentario que la misma persona ya dejó en el pendiente (su «cuando»). + # No se repite el texto en el hilo: la entrada guarda «ref» y la Actividad lleva un extracto. + ref = _texto(d.get("_nota_de"), 40) + ref_txt = "" + if ref: + previo = next((h for h in reversed(base.get("historial") or []) if h.get("cuando") == ref and h.get("quien") == u), None) + if not previo: + return jsonify(ok=False, error="No encontré ese comentario tuyo en el pendiente; escribe el porqué."), 400 + citado = str(previo.get("nota") or previo.get("cambio") or "").strip() + ref_txt = "por su comentario del " + ref[8:10] + "/" + ref[5:7] + " " + ref[11:16] + ": «" + (citado[:140] + "…" if len(citado) > 140 else citado) + "»" if not cambios and not nota: if t.get("orden") != base.get("orden") or t.get("responsable_ids") != base.get("responsable_ids"): # solo cambió la posición (o se completaron los ids): se guarda sin entrada en el historial @@ -1545,9 +1555,12 @@ def tarea_modificar(): return jsonify(ok=True, tarea=t, aviso=aviso) t["vencida"] = _vencida(t) return jsonify(ok=True, tarea=t, sin_cambios=True, aviso=aviso) - _hist(t, u, ("; ".join(cambios) + (" — " + nota if nota else "")) if cambios else nota) + extra = " — ".join(x for x in (nota, ref_txt if cambios else "") if x) + _hist(t, u, ("; ".join(cambios) + (" — " + extra if extra else "")) if cambios else nota) if nota: t["historial"][-1]["nota"] = nota + if ref_txt and cambios: + t["historial"][-1]["ref"] = ref _guardar_tarea(t) t["vencida"] = _vencida(t) return jsonify(ok=True, tarea=t, aviso=aviso) diff --git a/backend/lotes/2026-10-28_comentarios-elegir.json b/backend/lotes/2026-10-28_comentarios-elegir.json new file mode 100644 index 0000000..394d11a --- /dev/null +++ b/backend/lotes/2026-10-28_comentarios-elegir.json @@ -0,0 +1,11 @@ +{ + "lote": "2026-10-28_comentarios-elegir", + "autor": "claude", + "motivo": "28-oct, 18:00 (Leo): Michelle comenta antes de mover y el «¿por qué?» la hacía escribir lo mismo. Ahora elige uno de sus comentarios.", + "fuente": "/tableros/pendientes/", + "cambios": [ + { "id": "t-pendientes-comentarios", + "si_estado": ["hecho"], + "nota": "28-oct 18:00 (Leo): al mover se puede elegir uno de tus comentarios como el porqué; si comentaste después del último movimiento ya viene elegido y no se repite en el hilo. Pruebas: 26 del servidor y el caso de Michelle en el navegador." } + ] +} diff --git a/backend/tests-comentarios.sh b/backend/tests-comentarios.sh old mode 100755 new mode 100644 index 1e77356..682b96a --- a/backend/tests-comentarios.sh +++ b/backend/tests-comentarios.sh @@ -12,6 +12,7 @@ for u in leo adriana jesus michelle claude; do echo "{\"id\":\"$u\",\"nombre\":\"$u\",\"area\":\"ingenieria\",\"activo\":true}" > $T/equipo/$u.json done echo '{"id":"t-a","titulo":"Cotizar gabinetes","responsable":"adriana, jesus","campeon":"adriana","estado":"en-curso","pista":"compras","sitios":["qro"],"orden":0,"historial":[]}' > $T/tareas/t-a.json +echo '{"id":"t-m","titulo":"v6 bloque E","responsable":"michelle","campeon":"michelle","estado":"en-curso","pista":"ingenieria","sitios":["qro"],"orden":1,"historial":[]}' > $T/tareas/t-m.json python3 $REPO/backend/app.py > $T/app.log 2>&1 & APP=$!; trap "kill $APP 2>/dev/null" EXIT; sleep 1.8 U="http://127.0.0.1:$PORT" @@ -45,6 +46,24 @@ R=$(as adriana -X PATCH -d '{"estado":"bloqueado"}' "$U/api/tarea?id=t-a") check "mover sin nota sigue funcionando (lotes, standup)" '"estado":"bloqueado"' "$R" check "y sin nota no hay «nota»" 'None' "$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1].get('nota'))")" +echo "## elegir uno de sus comentarios como el porqué (28-oct)" +R=$(as michelle -X PATCH -d '{"_nota":"D12 quedó a 3.44 mm de U7.1 (se pedían 2 mm); L2 a 5 mm."}' "$U/api/tarea?id=t-m") +CUANDO=$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1]['cuando'])") +sleep 1.1; as jesus -X PATCH -d '{"_nota":"Comentario de Jesús en el mismo pendiente."}' "$U/api/tarea?id=t-m" > /dev/null +OTRO=$(as leo "$U/api/tareas" | python3 -c "import json,sys; t=[x for x in json.load(sys.stdin)['tareas'] if x['id']=='t-m'][0]; print(t['historial'][-1]['cuando'])") +check "con el comentario de otra persona no" 'comentario tuyo' "$(as michelle -X PATCH -d "{\"estado\":\"hecho\",\"_nota_de\":\"$OTRO\"}" "$U/api/tarea?id=t-m")" +check "con un comentario que no existe no" 'comentario tuyo' "$(as michelle -X PATCH -d '{"estado":"hecho","_nota_de":"2020-01-01T00:00:00"}' "$U/api/tarea?id=t-m")" +check "…y no se movió" '"estado": "en-curso"' "$(as leo "$U/api/tareas" | python3 -c "import json,sys; t=[x for x in json.load(sys.stdin)['tareas'] if x['id']=='t-m'][0]; print(json.dumps({'estado': t['estado']}))")" +R=$(as michelle -X PATCH -d "{\"estado\":\"hecho\",\"_nota_de\":\"$CUANDO\"}" "$U/api/tarea?id=t-m") +check "con su propio comentario se mueve" '"estado":"hecho"' "$R" +check "la entrada guarda «ref» con su «cuando»" "\"ref\": \"$CUANDO\"" "$(echo "$R" | ultimo)" +check "y no repite el texto como comentario (sin «nota»)" 'None' "$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1].get('nota'))")" +check "la Actividad lleva el extracto" 'por su comentario del' "$(echo "$R" | ultimo)" +R=$(as michelle -X PATCH -d "{\"estado\":\"en-curso\",\"_nota_de\":\"$CUANDO\",\"_nota\":\"Leo pidió revisar L2 otra vez.\"}" "$U/api/tarea?id=t-m") +check "citar y además escribir: guarda las dos cosas" '"nota": "Leo pidió revisar L2 otra vez."' "$(echo "$R" | ultimo)" +check "…con su «ref»" "\"ref\": \"$CUANDO\"" "$(echo "$R" | ultimo)" +check "citar sin cambiar nada no deja rastro" '"sin_cambios":true' "$(as michelle -X PATCH -d "{\"_nota_de\":\"$CUANDO\"}" "$U/api/tarea?id=t-m")" + echo "## la Actividad sigue leyendo el historial" check "la Actividad muestra el comentario" 'a dos proveedores' "$(as leo "$U/api/actividad?dias=2")" diff --git a/docs/tableros/kanban.js b/docs/tableros/kanban.js index bb1d4fe..07c3df9 100644 --- a/docs/tableros/kanban.js +++ b/docs/tableros/kanban.js @@ -36,7 +36,10 @@ Comentarios (28-oct, Leo): el detalle tiene «Comentarios» (hilo + caja; PATCH solo con «_nota», cualquiera con sesión) y la tarjeta muestra cuántos tiene. Al mover se pide «¿por qué?» cuando hace falta (reglaMover): a Hecho (la evidencia, D-27-6), al reabrir algo hecho, a Bloqueado y de En curso a Por hacer. «Cancelar» la deja donde estaba. - La nota va en «_nota» del mismo PATCH y queda en el historial («nota»). */ + La nota va en «_nota» del mismo PATCH y queda en el historial («nota»). Si la persona ya había comentado, puede + elegir uno de sus comentarios como el porqué («_nota_de» = su «cuando»; la entrada guarda «ref»): no se repite en el + hilo, el comentario elegido lleva la etiqueta «→ Hecho». Si comentó después del último cambio de columna, ese viene + ya elegido. El orden es el del historial, no la hora (hay horas en UTC y en hora local). */ (function () { const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo', gestion: 'Gestión' }; const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']]; @@ -186,10 +189,16 @@ const NOMBRE_ESTADO = Object.fromEntries(ESTADOS); const NO_ES_COMENTARIO = /^(creado en |borró el pendiente|restaur)/i; function comentarios(t) { - const out = []; - (t.historial || []).forEach(x => { + const out = [], porClave = {}; + (t.historial || []).forEach((x, i) => { const c = String(x.cambio || ''); const mv = c.match(/estado: ([a-z-]+) → ([a-z-]+)/); + // movió citando uno de sus comentarios: la etiqueta va en ese comentario, sin repetir el texto + if (x.ref && !x.nota) { + const o = porClave[x.ref + '|' + x.quien]; + if (o && mv) (o.movs = o.movs || []).push({ a: mv[2], quien: x.quien, cuando: x.cuando }); + return; + } let texto = x.nota || ''; if (!texto) { if (!c || NO_ES_COMENTARIO.test(c)) return; @@ -197,10 +206,20 @@ if (flecha < 0) texto = c; else { const k = c.indexOf(' — ', flecha); if (k < 0) return; texto = c.slice(k + 3); } } - if (texto.trim()) out.push({ quien: x.quien, cuando: x.cuando, texto: texto.trim(), de: mv && mv[1], a: mv && mv[2] }); + // el porqué pegado igual que un comentario suyo de antes (como se hacía antes del 28-oct en la tarde): una sola vez + if (mv && texto.trim()) { + const igual = out.slice().reverse().find(o => o.quien === x.quien && !o.a && o.texto === texto.trim()); + if (igual) { (igual.movs = igual.movs || []).push({ a: mv[2], quien: x.quien, cuando: x.cuando }); return; } + } + if (texto.trim()) { + const o = { quien: x.quien, cuando: x.cuando, texto: texto.trim(), de: mv && mv[1], a: mv && mv[2], i }; + out.push(o); porClave[x.cuando + '|' + x.quien] = o; + } }); return out; } + // posición en el historial del último cambio de columna (-1 si nunca se movió) + const ultimoMovimiento = t => { const h = t.historial || []; for (let i = h.length - 1; i >= 0; i--) if (/estado: [a-z-]+ → /.test(String(h[i].cambio || ''))) return i; return -1; }; // cuándo pide el «¿por qué?» un cambio de columna (null = se mueve sin preguntar) function reglaMover(desde, hacia) { if (desde === hacia) return null; @@ -624,12 +643,15 @@ } const mismaCol = t.estado === estado; // 28-oct: algunos cambios de columna piden «¿por qué?» antes de moverse; «Cancelar» la deja donde estaba - let nota = ''; + let pq = null; const regla = mismaCol ? null : reglaMover(t.estado, estado); if (regla) { - nota = await porQue(t, estado, regla); - if (nota === null) { pintar(); return; } + pq = await porQue(t, estado, regla); + if (pq === null) { pintar(); return; } } + const extra = {}; + if (pq && pq.nota) extra._nota = pq.nota; + if (pq && pq.ref) extra._nota_de = pq.ref; const columna = tareas.filter(x => x.estado === estado && x.id !== id).sort((a, b) => a.orden - b.orden); columna.splice(Math.max(0, Math.min(idx, columna.length)), 0, t); const ids = columna.map(x => x.id); @@ -637,9 +659,9 @@ const prev = { estado: t.estado, orden: t.orden }; t.estado = estado; columna.forEach((x, i) => x.orden = i); pintar(); try { - if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', Object.assign({ estado, orden: ids.indexOf(id) }, nota ? { _nota: nota } : {}))); + if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', Object.assign({ estado, orden: ids.indexOf(id) }, extra))); await api('/api/tareas/orden', json('POST', { estado, ids })); - if (!mismaCol) mensaje('«' + corto(t.titulo, 41) + '» pasó a ' + ESTADOS.find(e => e[0] === estado)[1] + '.' + (nota ? ' El porqué quedó en sus comentarios.' : ''), true); + if (!mismaCol) mensaje('«' + corto(t.titulo, 41) + '» pasó a ' + ESTADOS.find(e => e[0] === estado)[1] + '.' + (pq ? ' El porqué quedó en sus comentarios.' : ''), true); await recargar(); } catch (e) { t.estado = prev.estado; t.orden = prev.orden; pintar(); mensaje('No se pudo mover: ' + e.message, false); } } @@ -661,20 +683,51 @@ const lab = el('label', { text: regla.pregunta }); const txt = el('textarea', { rows: '3', maxlength: '1000', placeholder: regla.ph, 'aria-required': 'true' }); lab.setAttribute('for', 'kb-pq-' + t.id); txt.id = 'kb-pq-' + t.id; - const ayuda = el('div', { class: 'd-nota', text: 'Queda en los comentarios del pendiente, con tu nombre.' }); - cuerpo.append(lab, txt, ayuda); dlgPq.append(cuerpo); + // 28-oct (Leo): si ya comentaste en este pendiente, elige uno de tus comentarios en vez de repetirlo + const yo = norm(user), desde = ultimoMovimiento(t); + const mios = comentarios(t).filter(c => norm(c.quien) === yo).slice(-5).reverse(); + let elegido = null; + const ops = el('div', { class: 'pq-ops', role: 'radiogroup', 'aria-label': 'Tus comentarios' }); + const radios = []; + function elegir(c) { + elegido = c; + radios.forEach(r => { r.inp.checked = r.c === c; r.lab.classList.toggle('on', r.c === c); }); + txt.placeholder = c ? 'Agrega algo más (opcional)' : regla.ph; + txt.setAttribute('aria-required', c ? 'false' : 'true'); + listoPara(); + } + if (mios.length) { + cuerpo.append(lab); + mios.forEach(c => { + const l = el('label', { class: 'pq-op' }); + const inp = el('input', { type: 'radio', name: 'kb-pq-op-' + t.id }); + l.append(inp, el('span', { class: 'pq-op-c' }, el('span', { class: 'pq-op-f', text: 'Tu comentario · ' + fmt(c.cuando) }), el('span', { class: 'pq-op-t', text: c.texto }))); + inp.addEventListener('change', () => elegir(c)); + radios.push({ inp, lab: l, c }); ops.append(l); + }); + const lOtro = el('label', { class: 'pq-op' }); + const inpOtro = el('input', { type: 'radio', name: 'kb-pq-op-' + t.id }); + lOtro.append(inpOtro, el('span', { class: 'pq-op-c' }, el('span', { class: 'pq-op-t', text: 'Escribir otro motivo' }))); + inpOtro.addEventListener('change', () => { elegir(null); txt.focus(); }); + radios.push({ inp: inpOtro, lab: lOtro, c: null }); ops.append(lOtro); + cuerpo.append(ops, txt); + } else cuerpo.append(lab, txt); + const ayuda = el('div', { class: 'd-nota', text: mios.length ? 'El comentario que elijas queda marcado con este cambio; no se repite.' : 'Queda en los comentarios del pendiente, con tu nombre.' }); + cuerpo.append(ayuda); dlgPq.append(cuerpo); const pie = el('div', { class: 'dpie' }); const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); const ok = el('button', { type: 'button', text: 'Mover a ' + (NOMBRE_ESTADO[estado] || estado) }); - const listoPara = () => { ok.disabled = txt.value.trim().length < 5; }; - txt.addEventListener('input', listoPara); listoPara(); + function listoPara() { ok.disabled = !elegido && txt.value.trim().length < 5; } + txt.addEventListener('input', listoPara); + // si comentaste después del último cambio de columna, ese comentario ya viene elegido + if (mios.length && mios[0].i > desde) elegir(mios[0]); else if (mios.length) elegir(null); else listoPara(); txt.addEventListener('keydown', e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !ok.disabled) ok.click(); }); cancelar.addEventListener('click', () => dlgPq.close()); - ok.addEventListener('click', () => { valor = txt.value.trim(); dlgPq.close(); }); + ok.addEventListener('click', () => { valor = { nota: txt.value.trim(), ref: elegido ? elegido.cuando : '' }; dlgPq.close(); }); pie.append(cancelar, ok); dlgPq.append(pie); dlgPq.addEventListener('close', function fin() { dlgPq.removeEventListener('close', fin); listo(valor); }); if (typeof dlgPq.showModal === 'function') dlgPq.showModal(); else dlgPq.setAttribute('open', ''); - setTimeout(() => txt.focus(), 50); + setTimeout(() => (elegido ? ok : txt).focus(), 50); }); } @@ -693,6 +746,8 @@ const cab = el('div', { class: 'cm-cab' }); cab.append(el('b', { text: EQUIPO[id] ? nombreCorto(id) : (c.quien || '') }), el('span', { class: 'cm-f', text: fmt(c.cuando) })); if (c.a) cab.append(el('span', { class: 'cm-mv e-' + c.a, text: '→ ' + (NOMBRE_ESTADO[c.a] || c.a) })); + (c.movs || []).forEach(m => cab.append(el('span', { class: 'cm-mv e-' + m.a, title: 'Movido con este comentario como porqué · ' + fmt(m.cuando), + text: '→ ' + (NOMBRE_ESTADO[m.a] || m.a) + (norm(m.quien) !== norm(c.quien) ? ' · ' + nombreCorto(norm(m.quien)) : '') }))); cuerpo.append(cab, el('div', { class: 'cm-t', text: c.texto })); d.append(cuerpo); return d; @@ -714,10 +769,12 @@ 'aria-label': 'Nuevo comentario' }); const btn = el('button', { type: 'button', text: 'Comentar' }); const msg = el('div', { class: 'msg' }); - const listoPara = () => { btn.disabled = !user || !txt.value.trim(); }; - txt.disabled = !user; txt.addEventListener('input', listoPara); listoPara(); + // el botón no depende de los eventos de teclado (autocompletado, dictado, pegar): revisa el texto al hacer clic + const listoPara = () => { btn.disabled = !user; }; + txt.disabled = !user; listoPara(); btn.addEventListener('click', async () => { - const nota = txt.value.trim(); if (!nota) return; + const nota = txt.value.trim(); + if (!nota) { msg.className = 'msg err'; msg.textContent = 'Escribe el comentario primero.'; txt.focus(); return; } btn.disabled = true; try { const r = await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', { _nota: nota })); diff --git a/docs/tableros/pendientes.md b/docs/tableros/pendientes.md index 0270da4..eb79320 100644 --- a/docs/tableros/pendientes.md +++ b/docs/tableros/pendientes.md @@ -18,7 +18,7 @@ hide:
  • Cada tarjeta del tablero es un pendiente (no confundir con la tarjeta EinarBoard). Léelo antes de empezar el trabajo.
  • Para cambiarlo de columna: arrástralo (arriba aparece una franja con las cuatro columnas, suéltalo ahí y no tienes que bajar), usa las flechas ← → de la tarjeta o los botones de Columna en el detalle; en el teléfono, Mover a…. El tablero anota quién lo movió y cuándo.
  • Campeón: aunque un pendiente tenga varios responsables, uno solo lo mueve entre columnas y responde por él (su avatar va primero, con anillo). Se elige en el detalle; sin elegir, es el primero de la lista. Los demás responsables editan todo lo demás; para moverlo, pídele al campeón que te lo pase. Leo (administrador) puede mover cualquiera.
  • -
  • ¿Por qué? (28-oct): algunos cambios de columna preguntan antes de moverse y lo que escribas queda en los comentarios con tu nombre. A Hecho: la evidencia (el acta, la liga o el archivo). Al reabrir algo que estaba en Hecho: qué faltó. A Bloqueado: qué lo bloquea y quién lo destraba. De En curso a Por hacer: por qué regresa. «Cancelar» lo deja donde estaba.
  • +
  • ¿Por qué? (28-oct): algunos cambios de columna preguntan antes de moverse y lo que escribas queda en los comentarios con tu nombre. A Hecho: la evidencia (el acta, la liga o el archivo). Al reabrir algo que estaba en Hecho: qué faltó. A Bloqueado: qué lo bloquea y quién lo destraba. De En curso a Por hacer: por qué regresa. Si ya lo explicaste en un comentario, elige tu comentario en vez de escribirlo otra vez (si comentaste después del último movimiento, ya viene elegido): queda marcado con el cambio y no se repite. «Cancelar» lo deja donde estaba.
  • Toca el título para ver el detalle: comentarios, descripción, enlaces, de qué depende y su historial. Ahí mismo edita y guarda. Cualquiera puede comentar, aunque no sea el campeón; el número junto a la fecha de la tarjeta dice cuántos comentarios tiene (tócalo para abrirlos).
  • Nuevo pendiente: título corto, pista, responsable y fecha límite. Si depende de otro, márcalo en «Bloqueado por».
  • Columna Bloqueado: solo si no puedes avanzar; al moverlo te pregunta qué falta y quién lo destraba.
  • diff --git a/docs/tableros/tablero.css b/docs/tableros/tablero.css index 9d7cc9b..4674c4a 100644 --- a/docs/tableros/tablero.css +++ b/docs/tableros/tablero.css @@ -777,3 +777,16 @@ .tb dialog .dcuerpo { max-height: none; flex: 1 1 auto; min-height: 0; } .tb dialog .dpie { flex: none; } } +/* «¿por qué?»: elegir uno de tus comentarios (28-oct) */ +.tb dialog.kb-porque .pq-ops { display: grid; gap: 6px; margin: 0 0 8px; max-height: 240px; overflow: auto; } +.tb dialog.kb-porque .pq-op { + display: flex; gap: 9px; align-items: flex-start; margin: 0; padding: 8px 10px; cursor: pointer; + border: 1px solid var(--hg-linea-2); border-radius: 9px; background: var(--hg-plano); color: var(--hg-texto); font-weight: 500; +} +.tb dialog.kb-porque .pq-op input { width: auto; margin-top: 3px; flex: none; accent-color: var(--hg-curso); } +.tb dialog.kb-porque .pq-op.on { border-color: color-mix(in srgb, var(--hg-curso) 55%, transparent); background: var(--hg-curso-bg); } +.tb dialog.kb-porque .pq-op:focus-within { outline: 2px solid var(--hg-foco); outline-offset: 1px; } +.tb dialog.kb-porque .pq-op-c { display: grid; gap: 2px; min-width: 0; } +.tb dialog.kb-porque .pq-op-f { font-size: 12px; color: var(--hg-texto-3); font-weight: 600; } +.tb dialog.kb-porque .pq-op-t { font-size: 13.5px; line-height: 1.4; color: var(--hg-texto); white-space: pre-wrap; overflow-wrap: anywhere; + display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }