Añadir carpeta: la clase de trabajo se elige entre cuatro tarjetas, y el ejemplo es un árbol

Munka vio el desplegable con las cuatro frases largas y preguntó si se podía presentar mejor.
Ahora las cuatro clases están a la vista, cada una con su título, cómo se ordena y los clientes
que ya van así («como David Reyes, Danney Gómez, Andrea Galeano y 3 más»): se elige por parecido.
Debajo, el ejemplo deja de ser una lista de rutas completas y pasa a ser un árbol: el año y el
mes salen una sola vez, como en el Explorador, y cada carpeta lleva al lado qué guarda.

Los textos de reglas.py quedan como «título · cómo se ordena», sin el «Le hago», para que sirvan
tanto en las tarjetas como en el selector del Mapa y en Ajustes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-10 09:40:19 -05:00
padre 0819f2da89
commit 50da026373
Se han modificado 2 ficheros con 55 adiciones y 20 borrados

Ver fichero

@@ -166,6 +166,19 @@
.ejtipo div{display:flex;gap:4px 10px;align-items:baseline;flex-wrap:wrap}
.ejtipo b{font-family:ui-monospace,Consolas,monospace;font-size:11.5px;font-weight:600;word-break:break-all}
.ejtipo span{font-size:12px;color:var(--mut)}
.tipos{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:6px}
.tipo{display:block;position:relative;padding:9px 12px 9px 34px;border:1px solid var(--line);border-radius:8px;cursor:pointer;background:var(--panel)}
.tipo:hover{border-color:var(--acc)}
.tipo:has(input:checked){border-color:var(--acc);background:var(--acc2)}
.tipo:has(input:focus-visible){outline:2px solid var(--acc);outline-offset:1px}
.tipo input{position:absolute;left:12px;top:12px;margin:0}
.tipo b{display:block;font-size:13px}
.tipo span{display:block;font-size:12px;color:var(--mut);margin-top:1px}
.tipo em{display:block;font-size:11.5px;color:var(--mut);font-style:normal;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.arbol{margin:6px 0 0;padding:10px 12px;background:var(--panel2);border-radius:8px;font-family:ui-monospace,Consolas,monospace;font-size:11.5px;line-height:1.6}
.arbol div{display:flex;gap:10px;align-items:baseline;white-space:pre}
.arbol b{font-weight:600}
.arbol span{font:12px/1.4 Manrope,system-ui,sans-serif;color:var(--mut);white-space:normal}
.arbol{font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.55}
.arbol .nd{display:flex;gap:10px;align-items:baseline;padding:3px 7px;border-radius:5px;cursor:pointer}
.arbol .nd:hover{background:var(--panel2)}
@@ -559,7 +572,7 @@
<h3>Para empezar</h3>
<dl>
<dt>Instalarlo</dt><dd>La primera vez que se abre pregunta si lo instala en el equipo: queda en el menú Inicio, en el Escritorio y en Configuración › Aplicaciones. <i>Cómo: se copia con sus datos a tu carpeta de programas y se da de alta desde ahí; no pide permisos de administrador y de la carpeta original no borra nada.</i></dd>
<dt>Añadir la carpeta de un cliente</dt><dd>Suéltala en la ventana, pulsa «Elegir carpeta…» o pega su ruta. Pregunta el nombre del cliente y qué clase de trabajo le haces, y te enseña cómo va a quedar ordenada. <i>Cómo: al confirmar solo la lee; no mueve nada hasta que tú lo apliques.</i></dd>
<dt>Añadir la carpeta de un cliente</dt><dd>Suéltala en la ventana, pulsa «Elegir carpeta…» o pega su ruta. Pregunta el nombre del cliente y qué clase de trabajo le haces: cuatro tarjetas, cada una con los clientes que ya van así, y debajo el árbol de carpetas tal como va a quedar. <i>Cómo: al confirmar solo la lee; no mueve nada hasta que tú lo apliques.</i></dd>
<dt>Clase de trabajo</dt><dd>Piezas mes a mes, anuncios y campañas, un trabajo puntual o material de Labstream. <i>Cómo: cada clase tiene su plantilla de carpetas, y es lo único que decide dónde se guarda cada cosa.</i></dd>
<dt>Tus carpetas</dt><dd>Al abrir no se entra en ninguna: la portada lista las que ya conoce, cada una con su último escaneo, para entrar con un clic.</dd>
</dl>
@@ -1810,20 +1823,40 @@ const EJEMPLO_CUBETAS = [['GRABACION', 'lo que sale de la cámara'], ['PIEZA', '
['EXPORTADOS', 'lo que ya se entregó'], ['MUSICA', 'música y sonidos de siempre'],
['BASURA', 'lo que sobra, para que lo borres tú']];
function ejemploTipo(ruta, tipo){
// se arma con la plantilla de verdad, así que el ejemplo es exactamente lo que la app va a hacer
// se arma con la plantilla de verdad, así que el ejemplo es exactamente lo que la app va a hacer. Va como
// árbol: las carpetas que comparten camino (el año, el mes) salen una sola vez, como en el Explorador.
const pl = (ESTADO.plantillas || {})[tipo] || {}, h = new Date();
const val = {anio: h.getFullYear(), mes: `${String(h.getMonth() + 1).padStart(2, '0')}- ${MESES_UI[h.getMonth()]}`,
nombre: 'Nombre de la pieza', sub: '', fuente: '', rel: ''};
const base = (ruta || '').replace(/[\\/]+$/, ''), visto = new Set(), filas = [];
nombre: tipo === 'pauta' ? 'Nombre de la campaña' : 'Nombre de la pieza', sub: '', fuente: '', rel: ''};
const raiz = {hijos: new Map()};
for (const [cub, que] of EJEMPLO_CUBETAS){
const dest = (pl[cub] || '').replace(/\{(\w+)\}/g, (_, k) => val[k] !== undefined ? String(val[k]) : '')
.replace(/\/+$/, '').replace(/\/+/g, '\\');
if (!dest || visto.has(dest)) continue;
visto.add(dest);
filas.push(`<div><b>${esc(base)}\\${esc(dest)}\\</b><span>${que}</span></div>`);
if (filas.length === 4) break;
if (!dest) continue;
let n = raiz;
for (const seg of dest.split('\\')) { if (!n.hijos.has(seg)) n.hijos.set(seg, {hijos: new Map(), que: ''}); n = n.hijos.get(seg); }
n.que = n.que || que;
}
return filas.join('');
const lineas = [`<div><b>${esc((ruta || '').replace(/[\\/]+$/, ''))}\\</b></div>`];
const pinta = (nodo, sangria) => {
const hs = [...nodo.hijos.entries()];
hs.forEach(([nombre, h], i) => {
const ultimo = i === hs.length - 1;
lineas.push(`<div><b>${esc(sangria)}${ultimo ? '└─ ' : '├─ '}${esc(nombre)}\\</b>${h.que ? `<span>${esc(h.que)}</span>` : ''}</div>`);
pinta(h, sangria + (ultimo ? ' ' : '│ '));
});
};
pinta(raiz, '');
return lineas.join('');
}
function tarjetasTipo(elegido){
// las cuatro clases a la vista, cada una con los clientes que ya van así: se elige por parecido
const quien = t => Object.entries(ESTADO.config.clientes || {}).filter(([, c]) => (c.tipo || 'contenido') === t).map(([n]) => n);
return Object.entries(ESTADO.tipos).map(([k, v]) => {
const [titulo, orden] = v.split(' · '), q = quien(k);
return `<label class="tipo"><input type="radio" name="nuevoTipo" value="${k}" ${k === elegido ? 'checked' : ''}><b>${esc(titulo)}</b>` +
`<span>${esc(orden || '')}</span>${q.length ? `<em title="${esc(q.join(', '))}">como ${esc(q.slice(0, 3).join(', '))}${q.length > 3 ? ` y ${q.length - 3} más` : ''}</em>` : ''}</label>`;
}).join('');
}
function proponerRaiz(ruta){
if (!ruta) return;
@@ -1836,13 +1869,13 @@ function proponerRaiz(ruta){
<input type="text" id="nuevoCliente" value="${esc(sugerido)}" list="lcli2" style="width:100%">
<datalist id="lcli2">${clientes.map(c => `<option value="${esc(c)}">`).join('')}</datalist>
<span>Con ese nombre reúne todas sus carpetas, aunque en cada disco estén escritas distinto.</span></div>
<div class="preg"><label for="nuevoTipo">¿Qué clase de trabajo le haces?</label>
<select id="nuevoTipo" style="width:100%">${Object.entries(ESTADO.tipos).map(([k,v]) => `<option value="${k}">${esc(v)}</option>`).join('')}</select>
<div class="preg"><label>¿Qué clase de trabajo le haces?</label>
<div class="tipos" id="nuevoTipo">${tarjetasTipo(tipoDe(sugerido))}</div>
<span>Es lo único que decide en qué carpetas va a guardar su material. Se cambia luego en Ajustes.</span></div>
<p style="margin:14px 0 0"><b>Así va a quedar ordenada:</b></p>
<div class="ejtipo" id="ejemploTipo"></div>
<div class="arbol" id="ejemploTipo"></div>
<p class="mut" style="margin-top:12px">Al confirmar solo la lee y te arma una lista de lo que haría. <b>No mueve ni un archivo</b> hasta que tú lo apliques.</p>`, seguro(async () => {
const cliente = $('#nuevoCliente').value.trim() || nombre, tipo = $('#nuevoTipo').value;
const cliente = $('#nuevoCliente').value.trim() || nombre, tipo = tipoElegido();
const raices = ESTADO.config.raices.filter(r => r.ruta.toLowerCase() !== ruta.toLowerCase()); raices.push({ruta, cliente, tipo});
await api('/api/raices', {raices});
if (!ESTADO.config.clientes[cliente]) { ESTADO.config.clientes[cliente] = {tipo, alias: [nombre]}; await api('/api/clientes', {clientes: ESTADO.config.clientes}); }
@@ -1850,12 +1883,13 @@ function proponerRaiz(ruta){
await api('/api/escanear', {raiz: raizActual()}); await cargarEstado(); irA('carpeta');
aviso('Carpeta añadida. Escaneando…', 'ok');
}));
const sel = $('#nuevoTipo'), inp = $('#nuevoCliente');
const pinta = () => $('#ejemploTipo').innerHTML = ejemploTipo(ruta, sel.value);
sel.value = tipoDe(sugerido); sel.onchange = pinta;
const inp = $('#nuevoCliente');
const tipoElegido = () => { const r = document.querySelector('#nuevoTipo input:checked'); return r ? r.value : 'contenido'; };
const pinta = () => $('#ejemploTipo').innerHTML = ejemploTipo(ruta, tipoElegido());
$$('#nuevoTipo input').forEach(r => r.onchange = pinta);
// si escribe el nombre de un cliente que ya existe, manda el tipo que ese cliente ya tiene: dos
// carpetas del mismo cliente ordenadas con plantillas distintas serían un lío
inp.oninput = () => { const c = ESTADO.config.clientes[inp.value.trim()]; if (c && c.tipo && c.tipo !== sel.value) { sel.value = c.tipo; pinta(); } };
inp.oninput = () => { const c = ESTADO.config.clientes[inp.value.trim()]; const r = c && c.tipo && document.querySelector(`#nuevoTipo input[value="${c.tipo}"]`); if (r && !r.checked) { r.checked = true; pinta(); } };
pinta();
}