diff --git a/e2e/taller.e2e.ts b/e2e/taller.e2e.ts index 9442c8e..c2b7faa 100644 --- a/e2e/taller.e2e.ts +++ b/e2e/taller.e2e.ts @@ -271,6 +271,87 @@ test.describe('Taller de letras', () => { await expect(aviso).toContainText('versos'); }); + /* --- El glosario ------------------------------------------------------- */ + + test('los términos del glosario salen marcados en el texto', async ({ page }) => { + // Lo que se pidió: «cada vez que aparezca un término del glosario en el + // texto, estará marcado para visualizarse con un popup». + await page.goto('/taller/son-cubano'); + + const marcados = page.locator('.texto .termino'); + await expect(marcados.first()).toBeVisible(); + + // Cada término una sola vez por apartado, no en cada aparición: un texto + // con nueve «compás» subrayados no hay quien lo lea. + const slugs = await marcados.evaluateAll((bs) => + bs.map((b) => (b as HTMLElement).dataset.termino) + ); + expect(new Set(slugs).size).toBeGreaterThan(1); + }); + + test('al pulsar un término se abre su definición', async ({ page }) => { + await page.goto('/taller/son-cubano'); + + const montuno = page.locator('.texto .termino[data-termino="montuno"]').first(); + await montuno.click(); + + const globo = page.getByRole('dialog', { name: /Definición de montuno/ }); + await expect(globo).toBeVisible(); + await expect(globo).toContainText('llamada y respuesta'); + + // Y se cierra con Escape, devolviendo el foco a donde estaba. + await page.keyboard.press('Escape'); + await expect(globo).toBeHidden(); + await expect(montuno).toBeFocused(); + }); + + test('el globo no tapa el término que se acaba de pulsar', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + await page.goto('/taller/son-cubano'); + + const termino = page.locator('.texto .termino').first(); + await termino.click(); + + const suyo = (await termino.boundingBox())!; + const globo = (await page.locator('.globo').boundingBox())!; + + const seSolapan = globo.y < suyo.y + suyo.height && suyo.y < globo.y + globo.height; + expect(seSolapan).toBe(false); + }); + + test('no se marca dentro de un encabezado', async ({ page }) => { + // Ensuciaría el índice del capítulo, que sale de los encabezados. + await page.goto('/taller/son-cubano'); + await expect(page.locator('.texto h2 .termino')).toHaveCount(0); + }); + + test('el glosario tiene su propia página, con las acepciones numeradas', async ({ page }) => { + await page.goto('/taller/glosario'); + + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Glosario'); + await expect(page.getByRole('heading', { level: 2, name: 'sinalefa' })).toBeVisible(); + + /* + * «Clave» tiene dos: patrón rítmico e instrumento. Con una sola + * definición por palabra, una de las dos sería falsa, y por eso las + * acepciones van numeradas. + */ + const clave = page.locator('#clave'); + await expect(clave.locator('.acepciones > li')).toHaveCount(2); + await expect(clave).toContainText('patrón rítmico'); + await expect(clave).toContainText('percusión'); + }); + + test('se llega al glosario desde el índice del taller', async ({ page }) => { + await page.goto('/taller/bolero'); + await page + .getByRole('navigation', { name: 'Contenido del taller' }) + .getByRole('link', { name: 'Glosario' }) + .click(); + + await expect(page).toHaveURL('/taller/glosario'); + }); + test('el taller está en la navegación principal', async ({ page }) => { await page.goto('/'); await page diff --git a/package.json b/package.json index e1f6cf2..c564d60 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "db:medios": "node --env-file=.env scripts/db/volcar-medios.mjs", "db:generos": "node --env-file=.env scripts/db/sembrar-generos.mjs", "db:versos": "node --env-file=.env scripts/db/sembrar-versos.mjs", + "db:glosario": "node --env-file=.env scripts/db/sembrar-glosario.mjs", "db:completar": "node --env-file=.env scripts/db/completar-catalogo.mjs", "db:comparar": "node --experimental-strip-types --import ./scripts/db/resolver-lib.mjs --env-file=.env scripts/db/comparar-catalogo.mjs" }, diff --git a/scripts/db/sembrar-glosario.mjs b/scripts/db/sembrar-glosario.mjs new file mode 100644 index 0000000..79dc49d --- /dev/null +++ b/scripts/db/sembrar-glosario.mjs @@ -0,0 +1,102 @@ +/** + * Carga el glosario del taller. + * + * npm run db:glosario + * + * Veintiocho términos con sus acepciones numeradas y las formas con las que + * aparecen de verdad en el texto. Es lo que hace posible el marcado: sin las + * variantes, «sinalefas» en plural no se reconocería y nadie echaría en falta + * el globo que no salió. + * + * Idempotente por slug: reimportar una definición corregida actualiza. Las + * acepciones se rehacen enteras —se borran y se vuelven a poner— porque su + * número es su identidad y renumerar a mano dejaría huecos. + */ + +import fs from 'node:fs'; +import pg from 'pg'; + +const SEMILLA = JSON.parse(fs.readFileSync('scripts/db/semilla-glosario.json', 'utf8')); + +const slugificar = (t) => + t + .toLowerCase() + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + +const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 60_000 }); +await c.connect(); +await c.query('BEGIN'); + +const contado = { termino: 0, acepcion: 0, variante: 0 }; +const avisos = []; + +try { + const vistos = new Set(); + + for (const t of SEMILLA.terminos) { + const slug = slugificar(t.termino); + if (vistos.has(slug)) { + avisos.push(`«${t.termino}» sale dos veces en la semilla`); + continue; + } + vistos.add(slug); + + const { rows } = await c.query( + `INSERT INTO termino (id, slug, termino) VALUES ($1,$2,$3) + ON CONFLICT (slug) DO UPDATE SET termino = EXCLUDED.termino + RETURNING id`, + [crypto.randomUUID(), slug, t.termino] + ); + const terminoId = rows[0].id; + contado.termino++; + + await c.query('DELETE FROM acepcion WHERE termino_id = $1', [terminoId]); + for (const [i, a] of t.acepciones.entries()) { + await c.query( + `INSERT INTO acepcion (id, termino_id, numero, categoria, definicion, ampliacion, ejemplo) + VALUES ($1,$2,$3,$4,$5,$6,$7)`, + [ + crypto.randomUUID(), + terminoId, + i + 1, + a.categoria, + a.definicion, + a.ampliacion ?? null, + a.ejemplo ?? null + ] + ); + contado.acepcion++; + } + + /* + * El propio término también es una forma buscable, así no hay que + * repetirlo en la lista de variantes de los veintiocho. + */ + await c.query('DELETE FROM termino_variante WHERE termino_id = $1', [terminoId]); + for (const forma of new Set([t.termino, ...(t.variantes ?? [])])) { + await c.query( + 'INSERT INTO termino_variante (termino_id, forma) VALUES ($1,$2) ON CONFLICT DO NOTHING', + [terminoId, forma.toLowerCase()] + ); + contado.variante++; + } + } + + await c.query('COMMIT'); +} catch (fallo) { + await c.query('ROLLBACK').catch(() => {}); + console.error('Falló, no se ha escrito nada:'); + console.error(' ' + fallo.message); + await c.end(); + process.exit(1); +} + +await c.end(); + +for (const [que, n] of Object.entries(contado)) console.log(` ${que.padEnd(10)} ${n}`); +for (const aviso of avisos) console.log(` aviso: ${aviso}`); +console.log(''); +console.log('Glosario cargado.'); diff --git a/scripts/db/semilla-glosario.json b/scripts/db/semilla-glosario.json new file mode 100644 index 0000000..d889e86 --- /dev/null +++ b/scripts/db/semilla-glosario.json @@ -0,0 +1,376 @@ +{ + "_": [ + "El glosario del taller.", + "", + "Las 'variantes' son las formas con las que la palabra aparece de verdad en", + "el texto: plurales, femeninos, la forma con tilde y sin ella. El marcado", + "busca por ellas, así que una variante que falte es un término que no se", + "marca y nadie echa de menos.", + "", + "Las acepciones van numeradas porque la misma palabra significa cosas", + "distintas: 'clave' es el instrumento de percusión, el patrón rítmico y la", + "armadura de tonalidad. Con una sola definición, dos de las tres serían", + "falsas.", + "", + "'definicion' es lo que cabe en un globo sin tapar el texto. 'ampliacion' es", + "lo que se lee en la página del glosario, y 'ejemplo' un verso donde se vea:", + "un término de métrica sin ejemplo no se entiende." + ], + + "terminos": [ + { + "termino": "sinalefa", + "variantes": ["sinalefas"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Dos vocales que se juntan al cantar y cuentan como una sola sílaba: la última de una palabra y la primera de la siguiente.", + "ampliacion": "Es la regla que más cuesta interiorizar y la que más versos arregla. No es opcional: al cantar se produce sola, y contar sin ella da un verso más largo de lo que suena. Cuando molesta —porque junta dos vocales que quieren oírse aparte— se rompe metiendo una consonante o cambiando la palabra.", + "ejemplo": "«Dejaste la taza a medias» son ocho sílabas, no nueve: «za·a» va en una." + } + ] + }, + { + "termino": "hiato", + "variantes": ["hiatos"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Lo contrario de la sinalefa: dos vocales seguidas que se pronuncian en sílabas separadas.", + "ampliacion": "Se usa a propósito cuando hace falta una sílaba más o cuando la sinalefa atropella una palabra que importa. Forzarlo demasiado suena a verso mal medido, no a licencia.", + "ejemplo": "«que reías con el otro»: «re·í·as», tres sílabas, porque la tilde rompe el diptongo." + } + ] + }, + { + "termino": "octosílabo", + "variantes": ["octosílabos", "octosilabo", "octosilabos"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Verso de ocho sílabas. Es la medida de la copla popular en español y la más usada en la canción.", + "ampliacion": "Suena a lengua hablada porque el castellano cae solo en grupos de ocho. Por eso lo usan la ranchera, el son, la cumbia y el bolero clásico: no hay que forzar nada para que quepa una frase entera.", + "ejemplo": "«Si me llaman por el nombre»" + } + ] + }, + { + "termino": "endecasílabo", + "variantes": ["endecasílabos", "endecasilabo", "endecasilabos"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Verso de once sílabas, con acento obligatorio en la décima. Es el verso culto del español.", + "ampliacion": "Da sitio a subordinadas, que es lo que permite tener sintaxis de prosa sin dejar de cantarse. El tango vive de él; el bolero lo alterna con el octosílabo.", + "ejemplo": "«Volví a la esquina donde me esperabas»" + } + ] + }, + { + "termino": "heptasílabo", + "variantes": ["heptasílabos", "heptasilabo", "heptasilabos"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Verso de siete sílabas. Suele alternar con el endecasílabo para dar respiro.", + "ampliacion": "Es medida corta y por eso corta la frase: obliga a decir una cosa por verso. Alternado con versos largos, marca el pulso de una estrofa sin que se note.", + "ejemplo": "«y el humo por la casa»" + } + ] + }, + { + "termino": "métrica", + "variantes": ["metrica"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "La medida de los versos: cuántas sílabas tiene cada uno y cómo se cuentan.", + "ampliacion": "No es un adorno académico: la métrica es lo que hace que una letra se pueda cantar sobre una melodía sin atropellar palabras. Un verso de más sobra, y se nota antes en el oído que en el papel.", + "ejemplo": null + } + ] + }, + { + "termino": "escansión", + "variantes": ["escansion", "escandir", "escandida", "escandido"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Contar las sílabas de un verso aplicando las reglas: sinalefas, hiatos y la ley del acento final.", + "ampliacion": "Escandir es lo que separa medir de contar letras. Se hace con la voz, no con los ojos: si al decirlo en alto la cuenta no sale, la escansión está mal.", + "ejemplo": null + } + ] + }, + { + "termino": "verso agudo", + "variantes": ["versos agudos", "aguda", "agudas"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Verso que acaba en palabra aguda. Suma una sílaba a la cuenta.", + "ampliacion": "Es la mitad de la ley del acento final, y la que más despista: «yo respondo con el son» tiene siete casillas y es un octosílabo.", + "ejemplo": "«yo respondo con el son» — siete sílabas, mide ocho." + } + ] + }, + { + "termino": "rima consonante", + "variantes": ["consonante", "rima perfecta"], + "acepciones": [ + { + "categoria": "rima", + "definicion": "Riman todos los sonidos desde la última sílaba tónica: vocales y consonantes.", + "ampliacion": "Es la rima cerrada, la que suena a rima. Pide mucho —hay pocas palabras que rimen así— y por eso empuja a decir lo que rima en vez de lo que se quería decir. Úsala cuando el estilo la exija.", + "ejemplo": "techo / lecho, encendido / ruido" + } + ] + }, + { + "termino": "rima asonante", + "variantes": ["asonante", "asonantes"], + "acepciones": [ + { + "categoria": "rima", + "definicion": "Riman solo las vocales desde la última tónica; las consonantes dan igual.", + "ampliacion": "Es la rima de la copla popular. Pide mucho menos que la consonante y por eso deja decir lo que hay que decir: en una letra cantada casi nadie nota la diferencia, y el margen que da se agradece en cada verso.", + "ejemplo": "Jalisco / dicho — las dos en i-o." + } + ] + }, + { + "termino": "estrofa", + "variantes": ["estrofas"], + "acepciones": [ + { + "categoria": "estructura", + "definicion": "Grupo de versos que funciona como unidad: la parte que cuenta algo antes de volver al estribillo.", + "ampliacion": "En una canción la estrofa hace avanzar; el estribillo se queda. Una letra en la que las estrofas no avanzan es una letra que repite el estribillo tres veces con otras palabras.", + "ejemplo": null + } + ] + }, + { + "termino": "estribillo", + "variantes": ["estribillos"], + "acepciones": [ + { + "categoria": "estructura", + "definicion": "La parte que vuelve, igual, cada vez. Es lo que se recuerda y lo que da nombre a la canción.", + "ampliacion": "Tiene que poder cantarse sin haber entendido la estrofa. Si necesita contexto para significar algo, no es estribillo: es otra estrofa con la misma melodía.", + "ejemplo": null + } + ] + }, + { + "termino": "puente", + "variantes": ["puentes"], + "acepciones": [ + { + "categoria": "estructura", + "definicion": "La parte que rompe: distinta de la estrofa y del estribillo, y suele aparecer una sola vez.", + "ampliacion": "Sirve para que el último estribillo signifique algo distinto del primero. Sin puente, una canción de tres minutos dice lo mismo tres veces.", + "ejemplo": null + } + ] + }, + { + "termino": "colchón", + "variantes": ["colchon", "colchones"], + "acepciones": [ + { + "categoria": "musica", + "definicion": "El fondo sostenido —cuerdas, teclado, coros— sobre el que se apoya la voz sin competir con ella.", + "ampliacion": "Al escribir importa porque un colchón admite versos largos y frases alargadas: no hay que dejar hueco. Sobre un acompañamiento marcado, en cambio, cada sílaba cae en un sitio.", + "ejemplo": null + } + ] + }, + { + "termino": "compás", + "variantes": ["compas", "compases"], + "acepciones": [ + { + "categoria": "musica", + "definicion": "Cómo se agrupan los pulsos: de dos en dos, de tres en tres, de cuatro en cuatro.", + "ampliacion": "Es lo primero que hay que saber para escribir en un estilo. Un 3/4 pide frases que respiren de tres en tres; un 4/4 marcado admite versos más apretados. Escribir contra el compás se oye enseguida.", + "ejemplo": "El vals criollo va en 3/4; el son cubano, en 4/4." + } + ] + }, + { + "termino": "clave", + "variantes": ["claves"], + "acepciones": [ + { + "categoria": "musica", + "definicion": "El patrón rítmico de cinco golpes sobre el que se organiza la música cubana. Va en 3-2 o en 2-3.", + "ampliacion": "No es un adorno de la percusión: es la rejilla a la que se ajusta todo lo demás, la letra incluida. Una frase que cae «al revés de la clave» suena mal aunque las sílabas cuadren.", + "ejemplo": null + }, + { + "categoria": "instrumento", + "definicion": "El instrumento de percusión: dos palos de madera que se golpean uno contra otro.", + "ampliacion": "Es lo que marca el patrón que lleva su nombre. Suena por encima de todo sin necesitar volumen.", + "ejemplo": null + } + ] + }, + { + "termino": "cinquillo", + "variantes": ["cinquillos"], + "acepciones": [ + { + "categoria": "musica", + "definicion": "Célula rítmica cubana de cinco golpes en un compás. Es el pulso del bolero y del danzón.", + "ampliacion": "Condiciona dónde caen los acentos de la letra: los versos se apoyan en él, y por eso un bolero escrito sin tenerlo en cuenta se canta a trompicones.", + "ejemplo": null + } + ] + }, + { + "termino": "montuno", + "variantes": ["montunos"], + "acepciones": [ + { + "categoria": "estructura", + "definicion": "La segunda parte del son, con llamada y respuesta entre el solista y el coro.", + "ampliacion": "Es donde la letra deja de contar y empieza a insistir. Las frases se acortan, se repiten y admiten improvisación encima: escribir un montuno es escribir algo que aguante veinte vueltas.", + "ejemplo": null + } + ] + }, + { + "termino": "lunfardo", + "variantes": [], + "acepciones": [ + { + "categoria": "interpretacion", + "definicion": "El habla popular de Buenos Aires y Montevideo. En el tango no es color local: identifica de dónde viene quien habla.", + "ampliacion": "Usado de fuera suena a disfraz. Si no es tu lengua, escribe el tango en la tuya: la estructura y la mirada son lo que hacen tango a un tango, no el diccionario.", + "ejemplo": "«Dejá el escabio, piba, y escuchá»" + } + ] + }, + { + "termino": "melisma", + "variantes": ["melismas", "melismático", "melismatico"], + "acepciones": [ + { + "categoria": "interpretacion", + "definicion": "Cantar varias notas sobre una sola sílaba, alargándola.", + "ampliacion": "Por eso en un bolero cabe menos texto del que parece: cada sílaba se estira. Un verso apretado obliga a atropellar, y el melisma deja de poder hacerse.", + "ejemplo": null + } + ] + }, + { + "termino": "requinto", + "variantes": ["requintos"], + "acepciones": [ + { + "categoria": "instrumento", + "definicion": "Guitarra pequeña afinada más aguda, que hace la introducción y los adornos entre versos del bolero.", + "ampliacion": "Es lo que deja huecos entre frase y frase, y por eso una letra de bolero no debe llenarlos: los versos van con aire porque el requinto va a hablar en medio.", + "ejemplo": null + } + ] + }, + { + "termino": "bandoneón", + "variantes": ["bandoneon", "bandoneones"], + "acepciones": [ + { + "categoria": "instrumento", + "definicion": "El fuelle del tango. Su respiración marca el rubato, esa forma de adelantar y retrasar el tiempo.", + "ampliacion": "La letra del tango se canta con esa elasticidad, así que admite versos largos sin sonar apretados: el bandoneón espera.", + "ejemplo": null + } + ] + }, + { + "termino": "rubato", + "variantes": [], + "acepciones": [ + { + "categoria": "interpretacion", + "definicion": "Estirar y encoger el tiempo al cantar, sin ceñirse al pulso exacto.", + "ampliacion": "Es lo que permite decir un verso como quien habla. También es lo que hace que la métrica importe menos de lo que parece en algunos estilos, y muchísimo en los que no lo admiten.", + "ejemplo": null + } + ] + }, + { + "termino": "copla", + "variantes": ["coplas"], + "acepciones": [ + { + "categoria": "estructura", + "definicion": "Estrofa de cuatro versos octosílabos con rima en los pares. Es la forma popular por excelencia en español.", + "ampliacion": "Los impares van libres, y ese es su truco: dejan sitio para decir algo concreto sin pelearse con la rima. Media canción hispanoamericana está escrita en coplas.", + "ejemplo": null + } + ] + }, + { + "termino": "tónica", + "variantes": ["tonica", "tónicas", "tonicas", "sílaba tónica", "silaba tonica"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "La sílaba en la que cae el acento de una palabra o de un verso.", + "ampliacion": "El octosílabo lleva siempre acento en la séptima; el endecasílabo, en la décima. Eso es lo que hace que un verso suene a lo que es, aunque cambien todas las palabras.", + "ejemplo": null + } + ] + }, + { + "termino": "ley del acento final", + "variantes": ["acento final"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Un verso se mide hasta su última sílaba tónica más una: el agudo suma una sílaba, el esdrújulo resta una.", + "ampliacion": "Es la primera regla que hay que tener automatizada. Sin ella, media escansión sale mal y no se entiende por qué.", + "ejemplo": "«yo respondo con el son»: siete casillas, ocho sílabas." + } + ] + }, + { + "termino": "encabalgamiento", + "variantes": ["encabalgamientos", "encabalgado", "encabalgada"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Cuando la frase no acaba con el verso y continúa en el siguiente.", + "ampliacion": "En una letra cantada es arriesgado: la melodía suele cerrar donde cierra el verso, y una frase que sigue queda partida. Usado a propósito, es lo que da sensación de habla.", + "ejemplo": null + } + ] + }, + { + "termino": "hemistiquio", + "variantes": ["hemistiquios"], + "acepciones": [ + { + "categoria": "metrica", + "definicion": "Cada una de las dos mitades en que una pausa parte un verso largo.", + "ampliacion": "En el alejandrino la pausa cae en el centro y las dos mitades se miden aparte, como si fueran dos versos.", + "ejemplo": null + } + ] + }, + { + "termino": "voseo", + "variantes": [], + "acepciones": [ + { + "categoria": "interpretacion", + "definicion": "Tratar de «vos» en vez de «tú», con su acento al final del verbo: dejá, escuchá, mirá.", + "ampliacion": "Cambia la cuenta del verso, porque convierte en agudas palabras que en el tuteo son llanas. En el tango no es un detalle regional: es la voz.", + "ejemplo": "«y escuchá» hace agudo el verso y le suma una sílaba." + } + ] + } + ] +} diff --git a/src/app.css b/src/app.css index a102913..85f750c 100644 --- a/src/app.css +++ b/src/app.css @@ -777,3 +777,31 @@ a.chip:focus-visible { transform: none; } } + +/* --- términos del glosario ------------------------------------------------ + * + * El subrayado punteado es la convención de «esto tiene definición» desde + * mucho antes que la web, y se distingue de un enlace de verdad: un enlace + * lleva a otra página, esto abre un globo y te deja donde estabas. + * + * Va aquí y no en un componente porque el botón lo escribe el servidor dentro + * del HTML del capítulo, donde el ámbito de Svelte no llega. + */ +.termino { + border: 0; + padding: 0; + background: none; + font: inherit; + color: inherit; + cursor: help; + text-decoration: underline dotted; + text-decoration-color: color-mix(in oklab, var(--c-accent) 60%, transparent); + text-underline-offset: 0.2em; + text-decoration-thickness: 1px; +} + +.termino:hover, +.termino:focus-visible { + color: var(--c-accent); + text-decoration-color: var(--c-accent); +} diff --git a/src/lib/components/GlobosDeGlosario.svelte b/src/lib/components/GlobosDeGlosario.svelte new file mode 100644 index 0000000..7ac368f --- /dev/null +++ b/src/lib/components/GlobosDeGlosario.svelte @@ -0,0 +1,234 @@ + + + e.key === 'Escape' && abierto && cerrar()} + onresize={() => abierto && colocar()} + onscroll={() => abierto && colocar()} +/> + +{#if abierto} + + + + +{/if} + + diff --git a/src/lib/components/IndiceTaller.svelte b/src/lib/components/IndiceTaller.svelte index ea67abc..077266b 100644 --- a/src/lib/components/IndiceTaller.svelte +++ b/src/lib/components/IndiceTaller.svelte @@ -66,6 +66,24 @@ {/each} + + +
+

Consulta

+ +