Rafael Lopez

diario y descargas

...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
13 "d3": "^7.9.0", 13 "d3": "^7.9.0",
14 "fuse.js": "^7.1.0", 14 "fuse.js": "^7.1.0",
15 "hyparquet": "^1.25.1", 15 "hyparquet": "^1.25.1",
16 + "hyparquet-compressors": "^1.1.1",
16 "marked": "^17.0.4" 17 "marked": "^17.0.4"
17 }, 18 },
18 "devDependencies": { 19 "devDependencies": {
...@@ -1984,6 +1985,12 @@ ...@@ -1984,6 +1985,12 @@
1984 "node": ">=10" 1985 "node": ">=10"
1985 } 1986 }
1986 }, 1987 },
1988 + "node_modules/fzstd": {
1989 + "version": "0.1.1",
1990 + "resolved": "https://registry.npmjs.org/fzstd/-/fzstd-0.1.1.tgz",
1991 + "integrity": "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==",
1992 + "license": "MIT"
1993 + },
1987 "node_modules/graceful-fs": { 1994 "node_modules/graceful-fs": {
1988 "version": "4.2.11", 1995 "version": "4.2.11",
1989 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 1996 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
...@@ -2009,6 +2016,22 @@ ...@@ -2009,6 +2016,22 @@
2009 "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", 2016 "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==",
2010 "license": "MIT" 2017 "license": "MIT"
2011 }, 2018 },
2019 + "node_modules/hyparquet-compressors": {
2020 + "version": "1.1.1",
2021 + "resolved": "https://registry.npmjs.org/hyparquet-compressors/-/hyparquet-compressors-1.1.1.tgz",
2022 + "integrity": "sha512-yx7aA3Rhj0YycbdV71+XznQSLAefa4cT0urpgNXy4aM6eSeCknaVDNne8y45Uz74Fb15yyXUzOStlceOJBan7A==",
2023 + "license": "MIT",
2024 + "dependencies": {
2025 + "fzstd": "0.1.1",
2026 + "hysnappy": "1.0.0"
2027 + }
2028 + },
2029 + "node_modules/hysnappy": {
2030 + "version": "1.0.0",
2031 + "resolved": "https://registry.npmjs.org/hysnappy/-/hysnappy-1.0.0.tgz",
2032 + "integrity": "sha512-MNrC4NfwDGPb889O6gIfEtbvEZCSWUsSEhsz4Oq2FRcpGtXHfeVz3KciSPp5Pnnz1NjFMgDQNfxdJozymJEDDA==",
2033 + "license": "MIT"
2034 + },
2012 "node_modules/iconv-lite": { 2035 "node_modules/iconv-lite": {
2013 "version": "0.6.3", 2036 "version": "0.6.3",
2014 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 2037 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
24 "d3": "^7.9.0", 24 "d3": "^7.9.0",
25 "fuse.js": "^7.1.0", 25 "fuse.js": "^7.1.0",
26 "hyparquet": "^1.25.1", 26 "hyparquet": "^1.25.1",
27 + "hyparquet-compressors": "^1.1.1",
27 "marked": "^17.0.4" 28 "marked": "^17.0.4"
28 } 29 }
29 } 30 }
......
1 -import { API_HOST } from '$lib/api.js'; 1 +const API_BASE = 'https://abierto.economiayfinanzas.gob.bo/presupuesto/_api/diario';
2 -const API_BASE = API_HOST + '/api/diario';
3 2
4 export async function GET({ url }) { 3 export async function GET({ url }) {
5 const tipo = url.searchParams.get('tipo') || 'status'; 4 const tipo = url.searchParams.get('tipo') || 'status';
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
5 5
6 let mounted = $state(false); 6 let mounted = $state(false);
7 let dataset = $state('gasto'); 7 let dataset = $state('gasto');
8 + let datasetMasivo = $state('gasto');
9 + let formatoGestion = $state('csv');
10 + let formatoEntidad = $state('csv');
8 let entities = $state([]); 11 let entities = $state([]);
9 let entitySearch = $state(''); 12 let entitySearch = $state('');
10 let entityFocused = $state(false); 13 let entityFocused = $state(false);
...@@ -12,22 +15,50 @@ ...@@ -12,22 +15,50 @@
12 let selectedIdx = $state(-1); 15 let selectedIdx = $state(-1);
13 let loadingEntities = $state(true); 16 let loadingEntities = $state(true);
14 let entityError = $state(false); 17 let entityError = $state(false);
18 + let copiedUrl = $state(null);
15 19
16 function normalizeText(val) { 20 function normalizeText(val) {
17 return val.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); 21 return val.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
18 } 22 }
19 23
20 - function buildUrl(partition, value) { 24 + function buildUrl(partition, value, formato = 'csv') {
21 if (partition === 'completo') { 25 if (partition === 'completo') {
26 + if (formato === 'parquet') {
27 + return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.parquet`;
28 + }
22 return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.zip`; 29 return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.zip`;
23 } else if (partition === 'gestion') { 30 } else if (partition === 'gestion') {
31 + if (formato === 'xlsx') {
32 + return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}_xlsx.zip`;
33 + }
24 return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}.zip`; 34 return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}.zip`;
25 } else if (partition === 'entidad') { 35 } else if (partition === 'entidad') {
36 + if (formato === 'xlsx') {
37 + return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}_xlsx.zip`;
38 + }
26 return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}.zip`; 39 return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}.zip`;
27 } 40 }
28 return `${DOWNLOAD_BASE}${dataset}/${partition}/${value}`; 41 return `${DOWNLOAD_BASE}${dataset}/${partition}/${value}`;
29 } 42 }
30 43
44 + function buildMasivoUrl(formato) {
45 + if (formato === 'parquet') {
46 + return `${DOWNLOAD_BASE}${datasetMasivo}/ultimo/${datasetMasivo}.parquet`;
47 + }
48 + return `${DOWNLOAD_BASE}${datasetMasivo}/ultimo/${datasetMasivo}.zip`;
49 + }
50 +
51 + const parquetUrls = {
52 + gasto: `${DOWNLOAD_BASE}gasto/ultimo/gasto.parquet`,
53 + ingreso: `${DOWNLOAD_BASE}ingreso/ultimo/ingreso.parquet`
54 + };
55 +
56 + async function copyUrl(url) {
57 + await navigator.clipboard.writeText(url);
58 + copiedUrl = url;
59 + setTimeout(() => copiedUrl = null, 2000);
60 + }
61 +
31 let filteredEntities = $derived.by(() => { 62 let filteredEntities = $derived.by(() => {
32 if (!entitySearch || entitySearch.length < 2) return []; 63 if (!entitySearch || entitySearch.length < 2) return [];
33 const q = normalizeText(entitySearch); 64 const q = normalizeText(entitySearch);
...@@ -69,105 +100,213 @@ ...@@ -69,105 +100,213 @@
69 } 100 }
70 } catch { entityError = true; } 101 } catch { entityError = true; }
71 loadingEntities = false; 102 loadingEntities = false;
72 - console.log('[DESCARGAS] entities loaded:', entities.length);
73 }); 103 });
74 </script> 104 </script>
75 105
76 <svelte:head> 106 <svelte:head>
77 <title>Descargas | Presupuesto Público</title> 107 <title>Descargas | Presupuesto Público</title>
78 - <link rel="preconnect" href="https://fonts.googleapis.com" />
79 - <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
80 </svelte:head> 108 </svelte:head>
81 109
82 <div class="page" class:mounted> 110 <div class="page" class:mounted>
83 <div class="layout"> 111 <div class="layout">
84 <h1 class="title">Descargas</h1> 112 <h1 class="title">Descargas</h1>
85 - <p class="subtitle">Descarga los datos del presupuesto público en formato CSV</p> 113 + <p class="subtitle">Descarga los datos del presupuesto público boliviano en distintos formatos</p>
86 - 114 +
87 - <!-- Dataset toggle --> 115 + <!-- ═══════════════════════════════════════════ -->
88 - <div class="section"> 116 + <!-- SECCIÓN 1: Por gestión o entidad -->
89 - <span class="section-label">Base</span> 117 + <!-- ═══════════════════════════════════════════ -->
90 - <div class="toggle-group"> 118 + <section class="section">
91 - <button class="toggle-btn" class:active={dataset === 'gasto'} onclick={() => { dataset = 'gasto'; selectedEntity = null; }}>Gastos</button> 119 + <h2 class="section-title">Por gestión o entidad</h2>
92 - <button class="toggle-btn" class:active={dataset === 'ingreso'} onclick={() => { dataset = 'ingreso'; selectedEntity = null; }}>Ingresos</button> 120 + <p class="section-desc">Descarga los datos segmentados por año fiscal o por entidad pública</p>
121 +
122 + <!-- Toggle Gastos/Ingresos -->
123 + <div class="control-row">
124 + <span class="control-label">Base</span>
125 + <div class="toggle-group">
126 + <button class="toggle-btn" class:active={dataset === 'gasto'} onclick={() => { dataset = 'gasto'; selectedEntity = null; }}>Gastos</button>
127 + <button class="toggle-btn" class:active={dataset === 'ingreso'} onclick={() => { dataset = 'ingreso'; selectedEntity = null; }}>Ingresos</button>
128 + </div>
93 </div> 129 </div>
94 - </div> 130 +
95 - 131 + <!-- Por gestión -->
96 - <!-- Completo --> 132 + <div class="subsection">
97 - <div class="section"> 133 + <div class="subsection-header">
98 - <h2 class="section-title">Completo</h2> 134 + <h3 class="subsection-title">Por gestión</h3>
99 - <a href={buildUrl('completo', 'ultimo')} download rel="external" class="download-link"> 135 + <div class="formato-toggle">
100 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> 136 + <span class="formato-label">Formato:</span>
101 - Descargar todo ({dataset === 'gasto' ? 'gastos' : 'ingresos'}) 137 + <button class="fmt-btn" class:active={formatoGestion === 'csv'} onclick={() => formatoGestion = 'csv'}>CSV</button>
102 - </a> 138 + <button class="fmt-btn" class:active={formatoGestion === 'xlsx'} onclick={() => formatoGestion = 'xlsx'}>Excel</button>
103 - </div> 139 + </div>
104 - 140 + </div>
105 - <!-- Por gestión --> 141 + <div class="year-grid">
106 - <div class="section"> 142 + {#each YEARS as year}
107 - <h2 class="section-title">Por gestión</h2> 143 + <a href={buildUrl('gestion', year, formatoGestion)} download rel="external" class="year-link">
108 - <div class="year-grid"> 144 + <span class="year-num">{year}</span>
109 - {#each YEARS as year} 145 + <span class="year-fmt">.{formatoGestion === 'xlsx' ? 'xlsx' : 'csv'}</span>
110 - <a href={buildUrl('gestion', year)} download rel="external" class="year-link">{year}</a> 146 + </a>
111 - {/each} 147 + {/each}
148 + </div>
112 </div> 149 </div>
113 - </div>
114 -
115 - <!-- Por entidad -->
116 - <div class="section">
117 - <h2 class="section-title">Por entidad</h2>
118 - <div class="entity-search">
119 - <input
120 - type="text"
121 - bind:value={entitySearch}
122 - onfocus={() => { entityFocused = true; selectedEntity = null; }}
123 - onblur={() => setTimeout(() => entityFocused = false, 150)}
124 - onkeydown={handleKeydown}
125 - placeholder="Buscar entidad..."
126 - class="search-input"
127 - />
128 - {#if loadingEntities}
129 - <span class="search-status">Cargando entidades...</span>
130 - {:else if entityError}
131 - <span class="search-status error">No se pudieron cargar las entidades</span>
132 - {/if}
133 150
134 - {#if entityFocused && filteredEntities.length > 0} 151 + <!-- Por entidad -->
135 - <div class="search-dropdown"> 152 + <div class="subsection">
136 - {#each filteredEntities as entity, i} 153 + <div class="subsection-header">
137 - <button 154 + <h3 class="subsection-title">Por entidad</h3>
138 - class="search-item" 155 + <div class="formato-toggle">
139 - class:active={selectedIdx === i} 156 + <span class="formato-label">Formato:</span>
140 - onmouseenter={() => selectedIdx = i} 157 + <button class="fmt-btn" class:active={formatoEntidad === 'csv'} onclick={() => formatoEntidad = 'csv'}>CSV</button>
141 - onclick={() => selectEntity(entity)} 158 + <button class="fmt-btn" class:active={formatoEntidad === 'xlsx'} onclick={() => formatoEntidad = 'xlsx'}>Excel</button>
142 - >
143 - <span class="item-name">{entity.desc_entidad}</span>
144 - <span class="item-code">Código {entity.entidad}</span>
145 - </button>
146 - {/each}
147 </div> 159 </div>
148 - {/if} 160 + </div>
161 + <div class="entity-search">
162 + <input
163 + type="text"
164 + bind:value={entitySearch}
165 + onfocus={() => { entityFocused = true; selectedEntity = null; }}
166 + onblur={() => setTimeout(() => entityFocused = false, 150)}
167 + onkeydown={handleKeydown}
168 + placeholder="Buscar entidad..."
169 + class="search-input"
170 + />
171 + {#if loadingEntities}
172 + <span class="search-status">Cargando entidades...</span>
173 + {:else if entityError}
174 + <span class="search-status error">No se pudieron cargar las entidades</span>
175 + {/if}
176 +
177 + {#if entityFocused && filteredEntities.length > 0}
178 + <div class="search-dropdown">
179 + {#each filteredEntities as entity, i}
180 + <button
181 + class="search-item"
182 + class:active={selectedIdx === i}
183 + onmouseenter={() => selectedIdx = i}
184 + onclick={() => selectEntity(entity)}
185 + >
186 + <span class="item-name">{entity.desc_entidad}</span>
187 + <span class="item-code">Código {entity.entidad}</span>
188 + </button>
189 + {/each}
190 + </div>
191 + {/if}
192 +
193 + {#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities}
194 + <span class="search-status error">Sin resultados</span>
195 + {/if}
196 + </div>
149 197
150 - {#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities} 198 + {#if selectedEntity}
151 - <span class="search-status error">Sin resultados</span> 199 + <div class="entity-result">
200 + <span class="entity-name">{selectedEntity.desc_entidad}</span>
201 + <a href={buildUrl('entidad', selectedEntity.entidad, formatoEntidad)} download rel="external" class="download-link">
202 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
203 + Descargar .{formatoEntidad === 'xlsx' ? 'xlsx' : 'csv'}
204 + </a>
205 + </div>
152 {/if} 206 {/if}
153 </div> 207 </div>
208 + </section>
209 +
210 + <!-- ═══════════════════════════════════════════ -->
211 + <!-- SECCIÓN 2: Base completa -->
212 + <!-- ═══════════════════════════════════════════ -->
213 + <section class="section">
214 + <h2 class="section-title">Base completa</h2>
215 + <p class="section-desc">Toda la base histórica: todas las entidades y todos los años en un solo archivo</p>
216 +
217 + <div class="control-row">
218 + <span class="control-label">Base</span>
219 + <div class="toggle-group">
220 + <button class="toggle-btn" class:active={datasetMasivo === 'gasto'} onclick={() => datasetMasivo = 'gasto'}>Gastos</button>
221 + <button class="toggle-btn" class:active={datasetMasivo === 'ingreso'} onclick={() => datasetMasivo = 'ingreso'}>Ingresos</button>
222 + </div>
223 + </div>
154 224
155 - {#if selectedEntity} 225 + <div class="masivo-downloads">
156 - <div class="entity-result"> 226 + <a href={buildMasivoUrl('csv')} download rel="external" class="download-card">
157 - <span class="entity-name">{selectedEntity.desc_entidad}</span> 227 + <div class="download-card-icon">
158 - <a href={buildUrl('entidad', selectedEntity.entidad)} download rel="external" class="download-link"> 228 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
159 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> 229 + </div>
160 - Descargar 230 + <div class="download-card-info">
161 - </a> 231 + <span class="download-card-format">CSV</span>
232 + <span class="download-card-desc">{datasetMasivo === 'gasto' ? 'Gastos' : 'Ingresos'} — archivo .zip</span>
233 + </div>
234 + </a>
235 + <a href={buildMasivoUrl('parquet')} download rel="external" class="download-card">
236 + <div class="download-card-icon">
237 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
238 + </div>
239 + <div class="download-card-info">
240 + <span class="download-card-format">Parquet</span>
241 + <span class="download-card-desc">{datasetMasivo === 'gasto' ? 'Gastos' : 'Ingresos'} — archivo .parquet</span>
242 + </div>
243 + </a>
244 + </div>
245 + </section>
246 +
247 + <!-- ═══════════════════════════════════════════ -->
248 + <!-- SECCIÓN 3: Ejecución semanal -->
249 + <!-- ═══════════════════════════════════════════ -->
250 + <section class="section">
251 + <h2 class="section-title">Ejecución semanal</h2>
252 + <p class="section-desc">Reporte semanal de ejecución presupuestaria con datos agregados</p>
253 +
254 + <div class="semanal-downloads disabled">
255 + <span class="semanal-badge">Próximamente</span>
256 + <div class="masivo-downloads">
257 + <span class="download-card download-card-disabled">
258 + <div class="download-card-icon">
259 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
260 + </div>
261 + <div class="download-card-info">
262 + <span class="download-card-format">CSV</span>
263 + <span class="download-card-desc">Ejecución semanal — archivo .zip</span>
264 + </div>
265 + </span>
266 + <span class="download-card download-card-disabled">
267 + <div class="download-card-icon">
268 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
269 + </div>
270 + <div class="download-card-info">
271 + <span class="download-card-format">Excel</span>
272 + <span class="download-card-desc">Ejecución semanal — archivo .xlsx</span>
273 + </div>
274 + </span>
162 </div> 275 </div>
163 - {/if} 276 + </div>
164 - </div> 277 + </section>
278 +
279 + <!-- ═══════════════════════════════════════════ -->
280 + <!-- SECCIÓN 4: Para desarrolladores -->
281 + <!-- ═══════════════════════════════════════════ -->
282 + <section class="section section-last">
283 + <h2 class="section-title">Para desarrolladores</h2>
284 + <p class="section-desc">URLs directas a los archivos Parquet para consumo programático</p>
285 +
286 + <div class="dev-urls">
287 + {#each Object.entries(parquetUrls) as [key, url]}
288 + <div class="dev-url-row">
289 + <span class="dev-url-label">{key === 'gasto' ? 'Gastos' : 'Ingresos'}</span>
290 + <div class="dev-url-block">
291 + <code class="dev-url-code">{url}</code>
292 + <button class="dev-url-copy" onclick={() => copyUrl(url)}>
293 + {#if copiedUrl === url}
294 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
295 + {:else}
296 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
297 + {/if}
298 + </button>
299 + </div>
300 + </div>
301 + {/each}
302 + </div>
303 + </section>
165 </div> 304 </div>
166 </div> 305 </div>
167 306
168 <style> 307 <style>
169 .page { 308 .page {
170 - height: 100dvh; 309 + min-height: 100dvh;
171 background: var(--theme-body); 310 background: var(--theme-body);
172 color: var(--theme-titulo); 311 color: var(--theme-titulo);
173 font-family: 'Qanelas', var(--font-sans), -apple-system, sans-serif; 312 font-family: 'Qanelas', var(--font-sans), -apple-system, sans-serif;
...@@ -181,9 +320,9 @@ ...@@ -181,9 +320,9 @@
181 :global(html:not(.dark)) .page { background: #f2f2f4; } 320 :global(html:not(.dark)) .page { background: #f2f2f4; }
182 321
183 .layout { 322 .layout {
184 - max-width: 700px; 323 + max-width: 760px;
185 margin: 0 auto; 324 margin: 0 auto;
186 - padding: 1.5rem 2rem 2rem; 325 + padding: 1.5rem 2rem 3rem;
187 } 326 }
188 327
189 .title { 328 .title {
...@@ -195,38 +334,50 @@ ...@@ -195,38 +334,50 @@
195 } 334 }
196 335
197 .subtitle { 336 .subtitle {
198 - font-family: 'Qanelas', var(--font-sans);
199 font-size: 0.875rem; 337 font-size: 0.875rem;
200 color: var(--theme-texto); 338 color: var(--theme-texto);
201 opacity: 0.7; 339 opacity: 0.7;
202 - margin: 0 0 1.5rem; 340 + margin: 0 0 2rem;
203 line-height: 1.5; 341 line-height: 1.5;
204 } 342 }
205 343
344 + /* Sections */
206 .section { 345 .section {
207 - margin-bottom: 1.25rem; 346 + margin-bottom: 2rem;
208 - padding-bottom: 1.25rem; 347 + padding-bottom: 2rem;
209 border-bottom: 1px solid var(--theme-borde); 348 border-bottom: 1px solid var(--theme-borde);
210 } 349 }
211 - .section:last-child { border-bottom: none; } 350 + .section-last { border-bottom: none; }
212 351
213 - .section-label { 352 + .section-title {
214 - font-family: 'JetBrains Mono', 'DM Mono', monospace; 353 + font-size: 1.125rem;
354 + font-weight: 700;
355 + margin: 0 0 0.25rem;
356 + letter-spacing: -0.01em;
357 + }
358 +
359 + .section-desc {
360 + font-size: 0.8125rem;
361 + color: var(--theme-texto);
362 + opacity: 0.6;
363 + margin: 0 0 1.25rem;
364 + line-height: 1.5;
365 + }
366 +
367 + /* Control row */
368 + .control-row {
369 + display: flex;
370 + align-items: center;
371 + gap: 0.75rem;
372 + margin-bottom: 1.25rem;
373 + }
374 +
375 + .control-label {
215 font-size: 0.6875rem; 376 font-size: 0.6875rem;
216 letter-spacing: 0.12em; 377 letter-spacing: 0.12em;
217 text-transform: uppercase; 378 text-transform: uppercase;
218 color: var(--theme-texto); 379 color: var(--theme-texto);
219 opacity: 0.5; 380 opacity: 0.5;
220 - display: block;
221 - margin-bottom: 0.75rem;
222 - }
223 -
224 - .section-title {
225 - font-family: 'Qanelas', var(--font-sans);
226 - font-size: 1rem;
227 - font-weight: 800;
228 - margin: 0 0 0.625rem;
229 - letter-spacing: -0.01em;
230 } 381 }
231 382
232 /* Toggle */ 383 /* Toggle */
...@@ -239,12 +390,13 @@ ...@@ -239,12 +390,13 @@
239 padding: 3px; 390 padding: 3px;
240 width: fit-content; 391 width: fit-content;
241 } 392 }
393 + :global(html:not(.dark)) .toggle-group { background: #ffffff; border-color: rgba(0,0,0,0.08); }
242 394
243 .toggle-btn { 395 .toggle-btn {
244 font-family: 'Qanelas', var(--font-sans); 396 font-family: 'Qanelas', var(--font-sans);
245 - font-size: 0.875rem; 397 + font-size: 0.8125rem;
246 font-weight: 600; 398 font-weight: 600;
247 - padding: 0.5rem 1.5rem; 399 + padding: 0.4rem 1.25rem;
248 border: none; 400 border: none;
249 border-radius: 8px; 401 border-radius: 8px;
250 background: transparent; 402 background: transparent;
...@@ -263,46 +415,78 @@ ...@@ -263,46 +415,78 @@
263 color: var(--theme-titulo); 415 color: var(--theme-titulo);
264 } 416 }
265 417
266 - /* Download link */ 418 + /* Subsections */
267 - .download-link { 419 + .subsection {
268 - display: inline-flex; 420 + margin-bottom: 1.5rem;
421 + }
422 + .subsection:last-child { margin-bottom: 0; }
423 +
424 + .subsection-header {
425 + display: flex;
269 align-items: center; 426 align-items: center;
427 + justify-content: space-between;
428 + margin-bottom: 0.75rem;
429 + flex-wrap: wrap;
270 gap: 0.5rem; 430 gap: 0.5rem;
271 - font-family: 'Qanelas', var(--font-sans); 431 + }
432 +
433 + .subsection-title {
272 font-size: 0.9375rem; 434 font-size: 0.9375rem;
273 font-weight: 600; 435 font-weight: 600;
274 - color: var(--theme-accent); 436 + margin: 0;
275 - text-decoration: none;
276 - padding: 0.625rem 1.25rem;
277 - border: 1px solid var(--theme-accent);
278 - border-radius: 10px;
279 - transition: all 0.2s;
280 } 437 }
281 - .download-link:hover { 438 +
282 - background: var(--theme-accent); 439 + /* Formato toggle */
283 - color: #1C1C1A; 440 + .formato-toggle {
441 + display: flex;
442 + align-items: center;
443 + gap: 0.375rem;
284 } 444 }
285 - :global(html:not(.dark)) .download-link { 445 +
286 - border-color: rgba(0,0,0,0.12); 446 + .formato-label {
287 - color: var(--theme-titulo); 447 + font-size: 0.6875rem;
448 + color: var(--theme-texto);
449 + opacity: 0.5;
450 + margin-right: 0.25rem;
288 } 451 }
289 - :global(html:not(.dark)) .download-link:hover { 452 +
290 - background: rgba(0,0,0,0.03); 453 + .fmt-btn {
291 - color: var(--theme-titulo); 454 + font-family: 'Qanelas', var(--font-sans);
455 + font-size: 0.6875rem;
456 + font-weight: 600;
457 + padding: 0.3rem 0.75rem;
458 + border: 1px solid var(--theme-borde);
459 + border-radius: 6px;
460 + background: transparent;
461 + color: var(--theme-texto);
462 + cursor: pointer;
463 + transition: all 0.15s;
464 + }
465 + .fmt-btn:hover { border-color: var(--theme-accent); color: var(--theme-titulo); }
466 + .fmt-btn.active {
467 + background: var(--theme-titulo);
468 + border-color: var(--theme-titulo);
469 + color: var(--theme-body);
470 + }
471 + :global(.dark) .fmt-btn.active {
472 + background: var(--theme-accent);
473 + border-color: var(--theme-accent);
474 + color: var(--theme-body);
292 } 475 }
293 476
294 /* Years */ 477 /* Years */
295 .year-grid { 478 .year-grid {
296 display: grid; 479 display: grid;
297 - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); 480 + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
298 - gap: 0.625rem; 481 + gap: 0.5rem;
299 } 482 }
300 483
301 .year-link { 484 .year-link {
302 - font-family: 'Qanelas', var(--font-sans); 485 + display: flex;
303 - font-size: 0.8125rem; 486 + flex-direction: column;
304 - font-weight: 500; 487 + align-items: center;
305 - padding: 0.625rem; 488 + gap: 0.1rem;
489 + padding: 0.625rem 0.5rem;
306 background: #2A2A27; 490 background: #2A2A27;
307 border: 1px solid rgba(255,255,255,0.08); 491 border: 1px solid rgba(255,255,255,0.08);
308 border-radius: 10px; 492 border-radius: 10px;
...@@ -317,6 +501,7 @@ ...@@ -317,6 +501,7 @@
317 background: rgba(201, 167, 81, 0.06); 501 background: rgba(201, 167, 81, 0.06);
318 transform: translateY(-1px); 502 transform: translateY(-1px);
319 } 503 }
504 + :global(html:not(.dark)) .year-link { background: #ffffff; border-color: rgba(0,0,0,0.08); }
320 :global(html:not(.dark)) .year-link:hover { 505 :global(html:not(.dark)) .year-link:hover {
321 border-color: rgba(0,0,0,0.15); 506 border-color: rgba(0,0,0,0.15);
322 color: var(--theme-titulo); 507 color: var(--theme-titulo);
...@@ -324,11 +509,49 @@ ...@@ -324,11 +509,49 @@
324 transform: none; 509 transform: none;
325 } 510 }
326 511
327 - /* Entity search */ 512 + .year-num {
328 - .entity-search { 513 + font-size: 0.8125rem;
329 - position: relative; 514 + font-weight: 600;
330 } 515 }
331 516
517 + .year-fmt {
518 + font-size: 0.6rem;
519 + color: var(--theme-texto);
520 + opacity: 0.4;
521 + text-transform: uppercase;
522 + letter-spacing: 0.05em;
523 + }
524 +
525 + /* Download link */
526 + .download-link {
527 + display: inline-flex;
528 + align-items: center;
529 + gap: 0.5rem;
530 + font-size: 0.875rem;
531 + font-weight: 600;
532 + color: var(--theme-accent);
533 + text-decoration: none;
534 + padding: 0.5rem 1rem;
535 + border: 1px solid var(--theme-accent);
536 + border-radius: 10px;
537 + transition: all 0.2s;
538 + flex-shrink: 0;
539 + }
540 + .download-link:hover {
541 + background: var(--theme-accent);
542 + color: #1C1C1A;
543 + }
544 + :global(html:not(.dark)) .download-link {
545 + border-color: rgba(0,0,0,0.12);
546 + color: var(--theme-titulo);
547 + }
548 + :global(html:not(.dark)) .download-link:hover {
549 + background: rgba(0,0,0,0.03);
550 + }
551 +
552 + /* Entity search */
553 + .entity-search { position: relative; }
554 +
332 .search-input { 555 .search-input {
333 width: 100%; 556 width: 100%;
334 max-width: 480px; 557 max-width: 480px;
...@@ -338,7 +561,7 @@ ...@@ -338,7 +561,7 @@
338 background: #2A2A27; 561 background: #2A2A27;
339 color: var(--theme-titulo); 562 color: var(--theme-titulo);
340 font-family: 'Qanelas', var(--font-sans); 563 font-family: 'Qanelas', var(--font-sans);
341 - font-size: 0.9375rem; 564 + font-size: 0.875rem;
342 outline: none; 565 outline: none;
343 transition: border-color 0.2s, box-shadow 0.2s; 566 transition: border-color 0.2s, box-shadow 0.2s;
344 } 567 }
...@@ -347,6 +570,7 @@ ...@@ -347,6 +570,7 @@
347 box-shadow: 0 0 0 3px rgba(201, 167, 81, 0.12); 570 box-shadow: 0 0 0 3px rgba(201, 167, 81, 0.12);
348 } 571 }
349 .search-input::placeholder { color: var(--theme-texto); opacity: 0.4; } 572 .search-input::placeholder { color: var(--theme-texto); opacity: 0.4; }
573 + :global(html:not(.dark)) .search-input { background: #ffffff; border-color: rgba(0,0,0,0.08); }
350 574
351 .search-status { 575 .search-status {
352 display: block; 576 display: block;
...@@ -371,6 +595,7 @@ ...@@ -371,6 +595,7 @@
371 overflow-y: auto; 595 overflow-y: auto;
372 z-index: 10; 596 z-index: 10;
373 } 597 }
598 + :global(html:not(.dark)) .search-dropdown { background: #ffffff; }
374 599
375 .search-item { 600 .search-item {
376 display: flex; 601 display: flex;
...@@ -388,13 +613,12 @@ ...@@ -388,13 +613,12 @@
388 .search-item:hover, .search-item.active { background: var(--theme-surface-hover, rgba(128,128,128,0.06)); } 613 .search-item:hover, .search-item.active { background: var(--theme-surface-hover, rgba(128,128,128,0.06)); }
389 614
390 .item-name { 615 .item-name {
391 - font-family: 'Qanelas', var(--font-sans);
392 font-size: 0.875rem; 616 font-size: 0.875rem;
393 font-weight: 500; 617 font-weight: 500;
394 color: var(--theme-titulo); 618 color: var(--theme-titulo);
395 } 619 }
396 .item-code { 620 .item-code {
397 - font-family: 'JetBrains Mono', 'DM Mono', monospace; 621 + font-family: 'JetBrains Mono', monospace;
398 font-size: 0.6875rem; 622 font-size: 0.6875rem;
399 color: var(--theme-texto); 623 color: var(--theme-texto);
400 opacity: 0.4; 624 opacity: 0.4;
...@@ -406,32 +630,157 @@ ...@@ -406,32 +630,157 @@
406 align-items: center; 630 align-items: center;
407 justify-content: space-between; 631 justify-content: space-between;
408 gap: 1rem; 632 gap: 1rem;
409 - margin-top: 1.25rem; 633 + margin-top: 1rem;
410 padding: 1rem 1.25rem; 634 padding: 1rem 1.25rem;
411 background: #2A2A27; 635 background: #2A2A27;
412 border: 1px solid rgba(255,255,255,0.08); 636 border: 1px solid rgba(255,255,255,0.08);
413 border-radius: 12px; 637 border-radius: 12px;
414 max-width: 480px; 638 max-width: 480px;
415 } 639 }
640 + :global(html:not(.dark)) .entity-result { background: #ffffff; border-color: rgba(0,0,0,0.08); }
416 641
417 .entity-name { 642 .entity-name {
418 - font-family: 'Qanelas', var(--font-sans); 643 + font-size: 0.875rem;
644 + font-weight: 600;
645 + color: var(--theme-titulo);
646 + }
647 +
648 + /* Masivo downloads */
649 + .masivo-downloads {
650 + display: grid;
651 + grid-template-columns: 1fr 1fr;
652 + gap: 0.75rem;
653 + max-width: 480px;
654 + }
655 +
656 + .download-card {
657 + display: flex;
658 + align-items: center;
659 + gap: 0.75rem;
660 + padding: 1rem 1.25rem;
661 + background: #2A2A27;
662 + border: 1px solid rgba(255,255,255,0.08);
663 + border-radius: 12px;
664 + text-decoration: none;
665 + color: var(--theme-titulo);
666 + transition: all 0.2s;
667 + }
668 + .download-card:hover {
669 + border-color: var(--theme-accent);
670 + transform: translateY(-1px);
671 + }
672 + :global(html:not(.dark)) .download-card { background: #ffffff; border-color: rgba(0,0,0,0.08); }
673 + :global(html:not(.dark)) .download-card:hover { border-color: rgba(0,0,0,0.15); transform: none; }
674 +
675 + .download-card-icon {
676 + color: var(--theme-accent);
677 + flex-shrink: 0;
678 + }
679 +
680 + .download-card-info {
681 + display: flex;
682 + flex-direction: column;
683 + gap: 0.1rem;
684 + }
685 +
686 + .download-card-format {
419 font-size: 0.9375rem; 687 font-size: 0.9375rem;
688 + font-weight: 700;
689 + }
690 +
691 + .download-card-desc {
692 + font-size: 0.6875rem;
693 + color: var(--theme-texto);
694 + opacity: 0.5;
695 + }
696 +
697 + /* Semanal */
698 + .semanal-downloads {
699 + position: relative;
700 + }
701 + .semanal-downloads.disabled { opacity: 0.5; pointer-events: none; }
702 +
703 + .semanal-badge {
704 + font-size: 0.6rem;
705 + font-weight: 600;
706 + text-transform: uppercase;
707 + letter-spacing: 0.08em;
708 + padding: 0.2rem 0.5rem;
709 + border-radius: 4px;
710 + background: var(--theme-borde);
711 + color: var(--theme-texto);
712 + margin-bottom: 0.75rem;
713 + display: inline-block;
714 + }
715 +
716 + .download-card-disabled {
717 + cursor: default;
718 + pointer-events: none;
719 + }
720 +
721 + /* Dev URLs */
722 + .dev-urls {
723 + display: flex;
724 + flex-direction: column;
725 + gap: 0.75rem;
726 + }
727 +
728 + .dev-url-row {
729 + display: flex;
730 + flex-direction: column;
731 + gap: 0.35rem;
732 + }
733 +
734 + .dev-url-label {
735 + font-size: 0.75rem;
420 font-weight: 600; 736 font-weight: 600;
421 color: var(--theme-titulo); 737 color: var(--theme-titulo);
422 } 738 }
423 739
424 - :global(html:not(.dark)) .toggle-group { background: #ffffff; border-color: rgba(0,0,0,0.08); } 740 + .dev-url-block {
425 - :global(html:not(.dark)) .year-link { background: #ffffff; border-color: rgba(0,0,0,0.08); } 741 + display: flex;
426 - :global(html:not(.dark)) .search-input { background: #ffffff; border-color: rgba(0,0,0,0.08); } 742 + align-items: center;
427 - :global(html:not(.dark)) .entity-result { background: #ffffff; border-color: rgba(0,0,0,0.08); } 743 + gap: 0;
428 - :global(html:not(.dark)) .search-dropdown { background: #ffffff; } 744 + background: #1a1a18;
745 + border: 1px solid rgba(255,255,255,0.08);
746 + border-radius: 8px;
747 + overflow: hidden;
748 + }
749 + :global(html:not(.dark)) .dev-url-block { background: #f5f5f5; border-color: rgba(0,0,0,0.08); }
750 +
751 + .dev-url-code {
752 + flex: 1;
753 + padding: 0.75rem 1rem;
754 + font-family: 'JetBrains Mono', monospace;
755 + font-size: 0.75rem;
756 + color: var(--theme-accent);
757 + white-space: nowrap;
758 + overflow-x: auto;
759 + scrollbar-width: none;
760 + }
761 + .dev-url-code::-webkit-scrollbar { display: none; }
762 + :global(html:not(.dark)) .dev-url-code { color: var(--theme-titulo); }
763 +
764 + .dev-url-copy {
765 + padding: 0.75rem 1rem;
766 + background: transparent;
767 + border: none;
768 + border-left: 1px solid rgba(255,255,255,0.08);
769 + color: var(--theme-texto);
770 + cursor: pointer;
771 + transition: all 0.15s;
772 + flex-shrink: 0;
773 + }
774 + .dev-url-copy:hover { color: var(--theme-accent); }
775 + :global(html:not(.dark)) .dev-url-copy { border-left-color: rgba(0,0,0,0.08); }
429 776
430 @media (max-width: 640px) { 777 @media (max-width: 640px) {
431 .layout { padding: 1rem 1rem 2rem; } 778 .layout { padding: 1rem 1rem 2rem; }
432 - .title { font-size: 2rem; } 779 + .title { font-size: 1.75rem; }
433 - .year-grid { grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); } 780 + .year-grid { grid-template-columns: repeat(auto-fill, minmax(75px, 1fr)); }
781 + .masivo-downloads { grid-template-columns: 1fr; }
434 .search-input, .search-dropdown, .entity-result { max-width: 100%; } 782 .search-input, .search-dropdown, .entity-result { max-width: 100%; }
435 - .toggle-btn { padding: 0.5rem 1rem; font-size: 0.8125rem; } 783 + .subsection-header { flex-direction: column; align-items: flex-start; }
784 + .entity-result { flex-direction: column; align-items: flex-start; }
436 } 785 }
437 </style> 786 </style>
......
...@@ -278,7 +278,7 @@ ...@@ -278,7 +278,7 @@
278 }; 278 };
279 }); 279 });
280 280
281 - // Filtrar datos por transferencias y días 281 + // Filtrar datos por transferencias
282 let timeserieFiltered = $derived( 282 let timeserieFiltered = $derived(
283 timeserie.filter(d => d.transferencias === undefined || d.transferencias === incluirTransferencias) 283 timeserie.filter(d => d.transferencias === undefined || d.transferencias === incluirTransferencias)
284 ); 284 );
......
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type