@danieltmn/tamnora 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/harness/packages/core/src/agent/compact.js +6 -1
- package/harness/packages/server/public/app.js +131 -15
- package/harness/packages/server/public/appearance.js +227 -0
- package/harness/packages/server/public/index.html +78 -5
- package/harness/packages/server/public/landing/index.html +316 -5
- package/harness/packages/server/public/landing/landing.js +3 -1
- package/harness/packages/server/public/styles.css +417 -3
- package/harness/packages/server/src/lib/appearance.js +70 -0
- package/harness/packages/server/src/routes/providers.js +9 -1
- package/harness/packages/server/test/appearance-route.test.js +74 -0
- package/harness/packages/server/test/appearance.test.js +106 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
|
|
|
5
5
|
El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
|
|
6
6
|
y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
|
|
7
7
|
|
|
8
|
+
## [0.21.0] - 2026-10-14
|
|
9
|
+
|
|
10
|
+
### Cambios
|
|
11
|
+
|
|
12
|
+
- feat(consola): modulo de apariencia con temas, tipografia y tamano
|
|
13
|
+
|
|
14
|
+
## [0.20.1] - 2026-10-14
|
|
15
|
+
|
|
16
|
+
### Cambiado
|
|
17
|
+
|
|
18
|
+
- **Resumen de compactación con formato**: `/compact` y la auto-compactación ahora
|
|
19
|
+
producen **markdown con secciones fijas** (`Estado`, `Decisiones`, `Archivos y
|
|
20
|
+
cambios`, `Hallazgos`, `Pendientes`) y la tarjeta lo **renderiza** (títulos y
|
|
21
|
+
bullets con estilo, sin `#`/`**` crudos).
|
|
22
|
+
|
|
23
|
+
### Corregido
|
|
24
|
+
|
|
25
|
+
- **Consola**: al ejecutar un comando (`/…`) el input se **limpia de inmediato**,
|
|
26
|
+
sin quedar escrito mientras el comando tarda (p. ej. `compactar`).
|
|
27
|
+
|
|
8
28
|
## [0.20.0] - 2026-10-14
|
|
9
29
|
|
|
10
30
|
### Agregado
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
import { estimateTokens } from './context.js';
|
|
5
5
|
|
|
6
6
|
export const COMPACT_INSTRUCTION =
|
|
7
|
-
'Resumí la conversación
|
|
7
|
+
'Resumí la conversación para poder continuar el trabajo sin el historial completo. ' +
|
|
8
|
+
'Devolvé SOLO markdown, en español, conciso, sin introducción ni cierre, usando estas secciones ' +
|
|
9
|
+
'(omití las que queden vacías): "## Estado" (dónde quedó el trabajo, en 1-3 frases), "## Decisiones", ' +
|
|
10
|
+
'"## Archivos y cambios" (un bullet por archivo, con la forma `path: qué cambió`), "## Hallazgos" ' +
|
|
11
|
+
'(causas, gotchas o descubrimientos relevantes) y "## Pendientes" (lo que falta o el próximo paso). ' +
|
|
12
|
+
'Usá bullets "-" en las listas.';
|
|
8
13
|
|
|
9
14
|
export const SUMMARY_INSTRUCTION =
|
|
10
15
|
'Resumí la conversación para la persona: qué se pidió, qué se hizo, decisiones tomadas, archivos o comandos relevantes y el estado actual. Sé claro y conciso, en español, sin markdown pesado.';
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
import { renderMarkdown, speechText, highlight } from './markdown.js';
|
|
4
4
|
import { streamSse, followRun } from './runStream.js';
|
|
5
5
|
import { diffState, pollDelayMs, localState, summarizeArgs } from './reconcile.js';
|
|
6
|
+
import {
|
|
7
|
+
THEMES,
|
|
8
|
+
FONT_PRESETS,
|
|
9
|
+
DEFAULTS,
|
|
10
|
+
normalizeAppearance,
|
|
11
|
+
nextTheme,
|
|
12
|
+
readLocalAppearance,
|
|
13
|
+
applyAppearance,
|
|
14
|
+
clampChatFont,
|
|
15
|
+
clampScale,
|
|
16
|
+
} from './appearance.js';
|
|
6
17
|
|
|
7
18
|
const $ = (sel) => document.querySelector(sel);
|
|
8
19
|
let csrfToken = '';
|
|
@@ -297,7 +308,8 @@ const PALETTE_COMMANDS = [
|
|
|
297
308
|
{ id: 'ajustes', label: 'MCP y Skills (ajustes)', run: () => activateTab('ajustes') },
|
|
298
309
|
{ id: 'explorer', label: 'Mostrar u ocultar archivos', run: () => setExplorerOpen(!state.explorerOpen) },
|
|
299
310
|
{ id: 'sidebar', label: 'Mostrar u ocultar sesiones', run: () => setSidebarOpen(!state.sidebarOpen) },
|
|
300
|
-
{ id: 'theme', label: 'Cambiar tema', run: () =>
|
|
311
|
+
{ id: 'theme', label: 'Cambiar tema', run: () => cycleTheme() },
|
|
312
|
+
{ id: 'apariencia', label: 'Apariencia (tema, tipografía, tamaño)', run: () => openAppearance() },
|
|
301
313
|
];
|
|
302
314
|
|
|
303
315
|
const palette = {
|
|
@@ -459,6 +471,11 @@ async function loadModes() {
|
|
|
459
471
|
if (legacy) writeDefaultCwd(legacy);
|
|
460
472
|
}
|
|
461
473
|
syncDefaultCwd();
|
|
474
|
+
if (settings?.settings?.appearance) {
|
|
475
|
+
appearance = normalizeAppearance(settings.settings.appearance);
|
|
476
|
+
applyAppearance(appearance, { persist: true });
|
|
477
|
+
renderAppearanceControls();
|
|
478
|
+
}
|
|
462
479
|
updateStatus();
|
|
463
480
|
}
|
|
464
481
|
|
|
@@ -582,16 +599,110 @@ function updateStatus(extra = {}) {
|
|
|
582
599
|
if (extra.running) $('#sb-rounds').textContent = 'pensando…';
|
|
583
600
|
}
|
|
584
601
|
|
|
602
|
+
/* --- apariencia (tema, tipografía, tamaño) -------------------------------- */
|
|
603
|
+
|
|
604
|
+
let appearance = normalizeAppearance(readLocalAppearance());
|
|
585
605
|
const themeToggle = $('#theme-toggle');
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
606
|
+
const themeGrid = $('#apariencia-themes');
|
|
607
|
+
const fontSelect = $('#apariencia-font');
|
|
608
|
+
const chatFontRange = $('#apariencia-chat-font');
|
|
609
|
+
const chatFontOut = $('#apariencia-chat-font-out');
|
|
610
|
+
const scaleRange = $('#apariencia-scale');
|
|
611
|
+
const scaleOut = $('#apariencia-scale-out');
|
|
612
|
+
|
|
613
|
+
function renderAppearanceControls() {
|
|
614
|
+
if (themeGrid) {
|
|
615
|
+
themeGrid
|
|
616
|
+
.querySelectorAll('.theme-swatch')
|
|
617
|
+
.forEach((btn) => btn.classList.toggle('active', btn.dataset.themePick === appearance.theme));
|
|
618
|
+
}
|
|
619
|
+
if (fontSelect) fontSelect.value = appearance.font;
|
|
620
|
+
if (chatFontRange) chatFontRange.value = appearance.chatFont;
|
|
621
|
+
if (chatFontOut) chatFontOut.textContent = `${appearance.chatFont}px`;
|
|
622
|
+
if (scaleRange) scaleRange.value = Math.round(appearance.scale * 100);
|
|
623
|
+
if (scaleOut) scaleOut.textContent = `${Math.round(appearance.scale * 100)}%`;
|
|
624
|
+
if (themeToggle) {
|
|
625
|
+
const label = THEMES.find((t) => t.id === appearance.theme)?.label ?? appearance.theme;
|
|
626
|
+
themeToggle.title = `tema: ${label}`;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function persistAppearance() {
|
|
631
|
+
applyAppearance(appearance, { persist: true });
|
|
632
|
+
renderAppearanceControls();
|
|
633
|
+
api('/api/settings', { method: 'PUT', body: { appearance } }).catch(() => null);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function buildAppearanceControls() {
|
|
637
|
+
if (themeGrid) {
|
|
638
|
+
themeGrid.replaceChildren();
|
|
639
|
+
for (const theme of THEMES) {
|
|
640
|
+
const swatch = document.createElement('button');
|
|
641
|
+
swatch.type = 'button';
|
|
642
|
+
swatch.className = 'theme-swatch';
|
|
643
|
+
swatch.dataset.themePick = theme.id;
|
|
644
|
+
swatch.title = theme.label;
|
|
645
|
+
swatch.setAttribute('aria-label', `tema ${theme.label}`);
|
|
646
|
+
const dot = document.createElement('span');
|
|
647
|
+
dot.className = 'theme-swatch-dot';
|
|
648
|
+
dot.style.background = theme.metaColor;
|
|
649
|
+
const label = document.createElement('span');
|
|
650
|
+
label.className = 'theme-swatch-label';
|
|
651
|
+
label.textContent = theme.label;
|
|
652
|
+
swatch.append(dot, label);
|
|
653
|
+
swatch.addEventListener('click', () => {
|
|
654
|
+
appearance.theme = theme.id;
|
|
655
|
+
persistAppearance();
|
|
656
|
+
});
|
|
657
|
+
themeGrid.appendChild(swatch);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (fontSelect) {
|
|
661
|
+
fontSelect.replaceChildren();
|
|
662
|
+
for (const font of FONT_PRESETS) {
|
|
663
|
+
const option = document.createElement('option');
|
|
664
|
+
option.value = font.id;
|
|
665
|
+
option.textContent = font.label;
|
|
666
|
+
fontSelect.appendChild(option);
|
|
667
|
+
}
|
|
668
|
+
fontSelect.addEventListener('change', () => {
|
|
669
|
+
appearance.font = fontSelect.value;
|
|
670
|
+
persistAppearance();
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
chatFontRange?.addEventListener('input', () => {
|
|
674
|
+
appearance.chatFont = clampChatFont(Number(chatFontRange.value));
|
|
675
|
+
applyAppearance(appearance, { persist: false });
|
|
676
|
+
if (chatFontOut) chatFontOut.textContent = `${appearance.chatFont}px`;
|
|
677
|
+
});
|
|
678
|
+
chatFontRange?.addEventListener('change', persistAppearance);
|
|
679
|
+
scaleRange?.addEventListener('input', () => {
|
|
680
|
+
appearance.scale = clampScale(Number(scaleRange.value) / 100);
|
|
681
|
+
applyAppearance(appearance, { persist: false });
|
|
682
|
+
if (scaleOut) scaleOut.textContent = `${Math.round(appearance.scale * 100)}%`;
|
|
683
|
+
});
|
|
684
|
+
scaleRange?.addEventListener('change', persistAppearance);
|
|
685
|
+
$('#apariencia-reset')?.addEventListener('click', () => {
|
|
686
|
+
appearance = normalizeAppearance(DEFAULTS);
|
|
687
|
+
persistAppearance();
|
|
688
|
+
setStatus('#apariencia-msg', 'restablecido', 'ok');
|
|
689
|
+
});
|
|
690
|
+
renderAppearanceControls();
|
|
590
691
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
);
|
|
594
|
-
|
|
692
|
+
|
|
693
|
+
function cycleTheme() {
|
|
694
|
+
appearance.theme = nextTheme(appearance.theme);
|
|
695
|
+
persistAppearance();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function openAppearance() {
|
|
699
|
+
activateTab('ajustes');
|
|
700
|
+
activateAjustesPanel('apariencia');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
themeToggle?.addEventListener('click', cycleTheme);
|
|
704
|
+
appearance = applyAppearance(appearance, { persist: false });
|
|
705
|
+
buildAppearanceControls();
|
|
595
706
|
|
|
596
707
|
const logout = $('#logout');
|
|
597
708
|
if (logout) {
|
|
@@ -1786,7 +1897,11 @@ function addCompactCard(container, { text, label = 'contexto compactado' } = {})
|
|
|
1786
1897
|
const card = el('div', 'compact-card');
|
|
1787
1898
|
card.append(el('div', 'compact-rule', label));
|
|
1788
1899
|
const clean = String(text ?? '').trim();
|
|
1789
|
-
if (clean)
|
|
1900
|
+
if (clean) {
|
|
1901
|
+
const body = el('div', 'compact-text text');
|
|
1902
|
+
body.innerHTML = renderMarkdown(clean);
|
|
1903
|
+
card.append(body);
|
|
1904
|
+
}
|
|
1790
1905
|
container.appendChild(card);
|
|
1791
1906
|
return card;
|
|
1792
1907
|
}
|
|
@@ -2838,11 +2953,12 @@ async function sendMessage() {
|
|
|
2838
2953
|
if (input.startsWith('/') && !images.length) {
|
|
2839
2954
|
const name = input.slice(1).split(/\s+/)[0].toLowerCase();
|
|
2840
2955
|
const arg = input.slice(1 + name.length).trim();
|
|
2956
|
+
// Limpiamos el input de una: hay comandos lentos (compactar, resumir) y no
|
|
2957
|
+
// queremos dejar el comando escrito mientras corren.
|
|
2958
|
+
chatInput.value = '';
|
|
2959
|
+
onChatInputChanged();
|
|
2841
2960
|
const outcome = await runCommand(name, arg);
|
|
2842
|
-
if (!outcome || outcome.handled)
|
|
2843
|
-
chatInput.value = '';
|
|
2844
|
-
return;
|
|
2845
|
-
}
|
|
2961
|
+
if (!outcome || outcome.handled) return;
|
|
2846
2962
|
input = outcome.input ?? input;
|
|
2847
2963
|
}
|
|
2848
2964
|
if (!input && !images.length) return;
|
|
@@ -3374,7 +3490,7 @@ function renderHandsFree() {
|
|
|
3374
3490
|
}
|
|
3375
3491
|
if (handsFree.active && handsFree.phase === 'listening') renderFooterActivity('listening');
|
|
3376
3492
|
else if (handsFree.active && handsFree.phase === 'thinking') renderFooterActivity('thinking');
|
|
3377
|
-
else
|
|
3493
|
+
else renderFooterActivity(null);
|
|
3378
3494
|
}
|
|
3379
3495
|
|
|
3380
3496
|
function setHandsFreePhase(phase) {
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Apariencia de la consola: temas (paletas), presets de tipografía y escala.
|
|
2
|
+
// Registro puro + helpers de aplicación. Los ids de tema/fuente deben coincidir
|
|
3
|
+
// con src/lib/appearance.js (validación del server).
|
|
4
|
+
|
|
5
|
+
export const THEMES = [
|
|
6
|
+
{ id: 'paper', label: 'Papel', scheme: 'light', metaColor: '#f5efe2' },
|
|
7
|
+
{ id: 'dark', label: 'Oscuro', scheme: 'dark', metaColor: '#101107' },
|
|
8
|
+
{ id: 'midnight', label: 'Medianoche', scheme: 'dark', metaColor: '#0b1220' },
|
|
9
|
+
{ id: 'nord', label: 'Nord', scheme: 'dark', metaColor: '#2e3440' },
|
|
10
|
+
{ id: 'dracula', label: 'Dracula', scheme: 'dark', metaColor: '#282a36' },
|
|
11
|
+
{ id: 'solarized', label: 'Solarized', scheme: 'light', metaColor: '#fdf6e3' },
|
|
12
|
+
{ id: 'solarized-dark', label: 'Solarized oscuro', scheme: 'dark', metaColor: '#002b36' },
|
|
13
|
+
{ id: 'bosque', label: 'Bosque', scheme: 'light', metaColor: '#f2f4ec' },
|
|
14
|
+
{ id: 'vino', label: 'Vino', scheme: 'light', metaColor: '#f7eeee' },
|
|
15
|
+
{ id: 'vercel', label: 'Vercel', scheme: 'dark', metaColor: '#000000' },
|
|
16
|
+
{ id: 'dark-plus', label: 'Dark+', scheme: 'dark', metaColor: '#1e1e1e' },
|
|
17
|
+
{ id: 'github', label: 'GitHub', scheme: 'light', metaColor: '#ffffff' },
|
|
18
|
+
{ id: 'github-dark', label: 'GitHub oscuro', scheme: 'dark', metaColor: '#0d1117' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const FONT_PRESETS = [
|
|
22
|
+
{
|
|
23
|
+
id: 'clasico',
|
|
24
|
+
label: 'Clásico · Manrope / Libre Baskerville / DM Mono',
|
|
25
|
+
sans: 'Manrope, system-ui, "Segoe UI", Arial, sans-serif',
|
|
26
|
+
serif: '"Libre Baskerville", Georgia, "Times New Roman", serif',
|
|
27
|
+
mono: '"DM Mono", ui-monospace, "Cascadia Code", "JetBrains Mono", "SF Mono", Consolas, monospace',
|
|
28
|
+
google: 'family=DM+Mono:wght@400;500&family=Libre+Baskerville:wght@400;700&family=Manrope:wght@400;500;600;700',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'sistema',
|
|
32
|
+
label: 'Sistema · sin fuentes externas',
|
|
33
|
+
sans: 'system-ui, "Segoe UI", Arial, sans-serif',
|
|
34
|
+
serif: 'Georgia, "Times New Roman", serif',
|
|
35
|
+
mono: 'ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace',
|
|
36
|
+
google: null,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'plex',
|
|
40
|
+
label: 'IBM Plex · Sans / Serif / Mono',
|
|
41
|
+
sans: '"IBM Plex Sans", system-ui, "Segoe UI", Arial, sans-serif',
|
|
42
|
+
serif: '"IBM Plex Serif", Georgia, "Times New Roman", serif',
|
|
43
|
+
mono: '"IBM Plex Mono", ui-monospace, Consolas, monospace',
|
|
44
|
+
google: 'family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Serif:wght@400;600',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'inter',
|
|
48
|
+
label: 'Inter · Inter / Newsreader / JetBrains Mono',
|
|
49
|
+
sans: 'Inter, system-ui, "Segoe UI", Arial, sans-serif',
|
|
50
|
+
serif: 'Newsreader, Georgia, "Times New Roman", serif',
|
|
51
|
+
mono: '"JetBrains Mono", ui-monospace, Consolas, monospace',
|
|
52
|
+
google: 'family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Newsreader:wght@400;600',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'jetbrains',
|
|
56
|
+
label: 'JetBrains Mono · código',
|
|
57
|
+
sans: '"JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace',
|
|
58
|
+
serif: 'Georgia, "Times New Roman", serif',
|
|
59
|
+
mono: '"JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace',
|
|
60
|
+
google: 'family=JetBrains+Mono:wght@400;500;700',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'fira',
|
|
64
|
+
label: 'Fira Code · código',
|
|
65
|
+
sans: '"Fira Code", ui-monospace, "Cascadia Code", Consolas, monospace',
|
|
66
|
+
serif: 'Georgia, "Times New Roman", serif',
|
|
67
|
+
mono: '"Fira Code", ui-monospace, "Cascadia Code", Consolas, monospace',
|
|
68
|
+
google: 'family=Fira+Code:wght@400;500;600;700',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'source',
|
|
72
|
+
label: 'Source Code Pro · código',
|
|
73
|
+
sans: '"Source Code Pro", ui-monospace, "Cascadia Code", Consolas, monospace',
|
|
74
|
+
serif: 'Georgia, "Times New Roman", serif',
|
|
75
|
+
mono: '"Source Code Pro", ui-monospace, "Cascadia Code", Consolas, monospace',
|
|
76
|
+
google: 'family=Source+Code+Pro:wght@400;500;600;700',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export const DEFAULTS = { theme: 'paper', font: 'clasico', chatFont: 14.5, scale: 1 };
|
|
81
|
+
|
|
82
|
+
const MIN_CHAT_FONT = 12;
|
|
83
|
+
const MAX_CHAT_FONT = 20;
|
|
84
|
+
const MIN_SCALE = 0.8;
|
|
85
|
+
const MAX_SCALE = 1.3;
|
|
86
|
+
|
|
87
|
+
const LS_KEY = 'tamnora-console-appearance';
|
|
88
|
+
const THEME_KEY = 'tamnora-console-theme';
|
|
89
|
+
|
|
90
|
+
function lsGet(key) {
|
|
91
|
+
try {
|
|
92
|
+
return localStorage.getItem(key);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function lsSet(key, value) {
|
|
99
|
+
try {
|
|
100
|
+
localStorage.setItem(key, value);
|
|
101
|
+
} catch {
|
|
102
|
+
// storage unavailable
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function themeIds() {
|
|
107
|
+
return THEMES.map((t) => t.id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function fontIds() {
|
|
111
|
+
return FONT_PRESETS.map((f) => f.id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveTheme(id) {
|
|
115
|
+
const legacy = id === 'light' ? 'paper' : id;
|
|
116
|
+
return THEMES.some((t) => t.id === legacy) ? legacy : DEFAULTS.theme;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function resolveFont(id) {
|
|
120
|
+
return FONT_PRESETS.some((f) => f.id === id) ? id : DEFAULTS.font;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function clampChatFont(value) {
|
|
124
|
+
const n = Number(value);
|
|
125
|
+
if (!Number.isFinite(n)) return DEFAULTS.chatFont;
|
|
126
|
+
return Math.min(MAX_CHAT_FONT, Math.max(MIN_CHAT_FONT, Math.round(n * 2) / 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function clampScale(value) {
|
|
130
|
+
const n = Number(value);
|
|
131
|
+
if (!Number.isFinite(n)) return DEFAULTS.scale;
|
|
132
|
+
return Math.min(MAX_SCALE, Math.max(MIN_SCALE, Math.round(n * 100) / 100));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function normalizeAppearance(raw) {
|
|
136
|
+
const value = raw && typeof raw === 'object' ? raw : {};
|
|
137
|
+
return {
|
|
138
|
+
theme: resolveTheme(value.theme),
|
|
139
|
+
font: resolveFont(value.font),
|
|
140
|
+
chatFont: clampChatFont(value.chatFont),
|
|
141
|
+
scale: clampScale(value.scale),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function nextTheme(id) {
|
|
146
|
+
const ids = themeIds();
|
|
147
|
+
const index = ids.indexOf(resolveTheme(id));
|
|
148
|
+
return ids[(index + 1) % ids.length];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Lee la apariencia cacheada en localStorage (para aplicar antes del render). */
|
|
152
|
+
export function readLocalAppearance() {
|
|
153
|
+
let data = null;
|
|
154
|
+
try {
|
|
155
|
+
data = JSON.parse(lsGet(LS_KEY) || 'null');
|
|
156
|
+
} catch {
|
|
157
|
+
data = null;
|
|
158
|
+
}
|
|
159
|
+
const stored = data && typeof data === 'object' ? data : {};
|
|
160
|
+
return {
|
|
161
|
+
theme: stored.theme ?? lsGet(THEME_KEY) ?? undefined,
|
|
162
|
+
font: stored.font,
|
|
163
|
+
chatFont: stored.chatFont,
|
|
164
|
+
scale: stored.scale,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Inyecta (o quita) la hoja de Google Fonts del preset elegido. */
|
|
169
|
+
export function loadFontPreset(preset) {
|
|
170
|
+
const existing = document.getElementById('tamnora-fonts');
|
|
171
|
+
if (!preset || !preset.google) {
|
|
172
|
+
existing?.remove();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const href = `https://fonts.googleapis.com/css2?${preset.google}&display=swap`;
|
|
176
|
+
if (!existing) {
|
|
177
|
+
const link = document.createElement('link');
|
|
178
|
+
link.rel = 'stylesheet';
|
|
179
|
+
link.id = 'tamnora-fonts';
|
|
180
|
+
link.href = href;
|
|
181
|
+
document.head.appendChild(link);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (existing.getAttribute('href') !== href) existing.setAttribute('href', href);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Guarda la apariencia resuelta en localStorage (con la tipografía ya resuelta). */
|
|
188
|
+
export function writeLocalAppearance(raw) {
|
|
189
|
+
const a = normalizeAppearance(raw);
|
|
190
|
+
const font = FONT_PRESETS.find((f) => f.id === a.font) ?? FONT_PRESETS[0];
|
|
191
|
+
lsSet(
|
|
192
|
+
LS_KEY,
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
theme: a.theme,
|
|
195
|
+
font: a.font,
|
|
196
|
+
fontCss: { sans: font.sans, serif: font.serif, mono: font.mono },
|
|
197
|
+
google: font.google,
|
|
198
|
+
chatFont: a.chatFont,
|
|
199
|
+
scale: a.scale,
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
lsSet(THEME_KEY, a.theme);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Aplica tema, tipografía, tamaño de chat y escala al documento. */
|
|
206
|
+
export function applyAppearance(raw, { persist = true } = {}) {
|
|
207
|
+
const a = normalizeAppearance(raw);
|
|
208
|
+
const theme = THEMES.find((t) => t.id === a.theme) ?? THEMES[0];
|
|
209
|
+
const font = FONT_PRESETS.find((f) => f.id === a.font) ?? FONT_PRESETS[0];
|
|
210
|
+
const root = document.documentElement;
|
|
211
|
+
|
|
212
|
+
root.dataset.theme = theme.id;
|
|
213
|
+
root.dataset.scheme = theme.scheme;
|
|
214
|
+
loadFontPreset(font);
|
|
215
|
+
root.style.setProperty('--sans', font.sans);
|
|
216
|
+
root.style.setProperty('--serif', font.serif);
|
|
217
|
+
root.style.setProperty('--mono', font.mono);
|
|
218
|
+
root.style.setProperty('--font', 'var(--sans)');
|
|
219
|
+
root.style.setProperty('--chat-font', `${a.chatFont}px`);
|
|
220
|
+
root.style.zoom = String(a.scale);
|
|
221
|
+
|
|
222
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
223
|
+
if (meta) meta.setAttribute('content', theme.metaColor);
|
|
224
|
+
|
|
225
|
+
if (persist) writeLocalAppearance(a);
|
|
226
|
+
return a;
|
|
227
|
+
}
|
|
@@ -10,14 +10,51 @@
|
|
|
10
10
|
<meta name="theme-color" content="#231a17" />
|
|
11
11
|
<script>
|
|
12
12
|
(function () {
|
|
13
|
-
var
|
|
13
|
+
var root = document.documentElement;
|
|
14
|
+
var THEMES = ['paper', 'dark', 'midnight', 'nord', 'dracula', 'solarized', 'solarized-dark', 'bosque', 'vino', 'vercel', 'dark-plus', 'github', 'github-dark'];
|
|
15
|
+
var DARK = ['dark', 'midnight', 'nord', 'dracula', 'solarized-dark', 'vercel', 'dark-plus', 'github-dark'];
|
|
16
|
+
var appearance = null;
|
|
14
17
|
try {
|
|
15
|
-
|
|
18
|
+
appearance = JSON.parse(localStorage.getItem('tamnora-console-appearance') || 'null');
|
|
16
19
|
} catch {
|
|
17
|
-
|
|
20
|
+
appearance = null;
|
|
21
|
+
}
|
|
22
|
+
var theme = appearance && appearance.theme;
|
|
23
|
+
if (!theme) {
|
|
24
|
+
try {
|
|
25
|
+
theme = localStorage.getItem('tamnora-console-theme');
|
|
26
|
+
} catch {
|
|
27
|
+
theme = null;
|
|
28
|
+
}
|
|
18
29
|
}
|
|
19
30
|
if (theme === 'light') theme = 'paper';
|
|
20
|
-
|
|
31
|
+
if (THEMES.indexOf(theme) < 0) {
|
|
32
|
+
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'paper';
|
|
33
|
+
}
|
|
34
|
+
root.dataset.theme = theme;
|
|
35
|
+
root.dataset.scheme = DARK.indexOf(theme) >= 0 ? 'dark' : 'light';
|
|
36
|
+
if (!appearance) return;
|
|
37
|
+
if (appearance.fontCss) {
|
|
38
|
+
root.style.setProperty('--sans', appearance.fontCss.sans);
|
|
39
|
+
root.style.setProperty('--serif', appearance.fontCss.serif);
|
|
40
|
+
root.style.setProperty('--mono', appearance.fontCss.mono);
|
|
41
|
+
root.style.setProperty('--font', 'var(--sans)');
|
|
42
|
+
}
|
|
43
|
+
var href = appearance.google ? 'https://fonts.googleapis.com/css2?' + appearance.google + '&display=swap' : null;
|
|
44
|
+
var flink = document.getElementById('tamnora-fonts');
|
|
45
|
+
if (!href) {
|
|
46
|
+
if (flink) flink.remove();
|
|
47
|
+
} else if (!flink) {
|
|
48
|
+
flink = document.createElement('link');
|
|
49
|
+
flink.rel = 'stylesheet';
|
|
50
|
+
flink.id = 'tamnora-fonts';
|
|
51
|
+
flink.href = href;
|
|
52
|
+
document.head.appendChild(flink);
|
|
53
|
+
} else if (flink.getAttribute('href') !== href) {
|
|
54
|
+
flink.setAttribute('href', href);
|
|
55
|
+
}
|
|
56
|
+
if (appearance.chatFont) root.style.setProperty('--chat-font', appearance.chatFont + 'px');
|
|
57
|
+
if (appearance.scale) root.style.zoom = String(appearance.scale);
|
|
21
58
|
})();
|
|
22
59
|
(function () {
|
|
23
60
|
// La consola resuelve el acceso con /api/auth/me; mientras tanto, oculta.
|
|
@@ -30,6 +67,7 @@
|
|
|
30
67
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
31
68
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
32
69
|
<link
|
|
70
|
+
id="tamnora-fonts"
|
|
33
71
|
rel="stylesheet"
|
|
34
72
|
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Libre+Baskerville:wght@400;700&family=Manrope:wght@400;500;600;700&display=swap"
|
|
35
73
|
/>
|
|
@@ -37,7 +75,7 @@
|
|
|
37
75
|
</head>
|
|
38
76
|
<body>
|
|
39
77
|
<header class="topbar">
|
|
40
|
-
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.
|
|
78
|
+
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.21.0</span></div>
|
|
41
79
|
<nav class="tabs">
|
|
42
80
|
<button class="tab active" data-tab="chat">chat</button>
|
|
43
81
|
<button class="tab" data-tab="providers">proveedores</button>
|
|
@@ -490,6 +528,7 @@
|
|
|
490
528
|
<button type="button" class="ajustes-tab" data-panel="asesor" role="tab">Asesor</button>
|
|
491
529
|
<button type="button" class="ajustes-tab" data-panel="transcripcion" role="tab">Transcripción</button>
|
|
492
530
|
<button type="button" class="ajustes-tab" data-panel="limites" role="tab">Límites</button>
|
|
531
|
+
<button type="button" class="ajustes-tab" data-panel="apariencia" role="tab">Apariencia</button>
|
|
493
532
|
</nav>
|
|
494
533
|
|
|
495
534
|
<div class="panel ajustes-panel active" data-panel="agente">
|
|
@@ -920,6 +959,40 @@
|
|
|
920
959
|
<span id="limits-msg" class="summary"></span>
|
|
921
960
|
</div>
|
|
922
961
|
</div>
|
|
962
|
+
|
|
963
|
+
<div class="panel ajustes-panel" data-panel="apariencia">
|
|
964
|
+
<div class="row between">
|
|
965
|
+
<h2>Apariencia</h2>
|
|
966
|
+
</div>
|
|
967
|
+
<p class="muted verify-line">
|
|
968
|
+
Tema, tipografía y tamaño de la consola. Se guardan en tu casa y te siguen entre
|
|
969
|
+
dispositivos.
|
|
970
|
+
</p>
|
|
971
|
+
<label class="field"><span>tema</span></label>
|
|
972
|
+
<div id="apariencia-themes" class="theme-grid"></div>
|
|
973
|
+
<label class="field"><span>tipografía</span>
|
|
974
|
+
<select id="apariencia-font"></select>
|
|
975
|
+
</label>
|
|
976
|
+
<div id="apariencia-preview" class="apariencia-preview">
|
|
977
|
+
<strong>Tamnora Harness</strong>
|
|
978
|
+
<span>El veloz murciélago hindú comía feliz cardillo y kiwi.</span>
|
|
979
|
+
<code>const saludo = 'hola';</code>
|
|
980
|
+
</div>
|
|
981
|
+
<div class="voz-range">
|
|
982
|
+
<span class="muted">tamaño del chat</span>
|
|
983
|
+
<input id="apariencia-chat-font" type="range" min="12" max="20" step="0.5" />
|
|
984
|
+
<output id="apariencia-chat-font-out">14.5px</output>
|
|
985
|
+
</div>
|
|
986
|
+
<div class="voz-range">
|
|
987
|
+
<span class="muted">escala de la interfaz</span>
|
|
988
|
+
<input id="apariencia-scale" type="range" min="80" max="130" step="5" />
|
|
989
|
+
<output id="apariencia-scale-out">100%</output>
|
|
990
|
+
</div>
|
|
991
|
+
<div class="row">
|
|
992
|
+
<button id="apariencia-reset" class="btn" type="button">restablecer</button>
|
|
993
|
+
<span id="apariencia-msg" class="summary"></span>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
923
996
|
</div>
|
|
924
997
|
</section>
|
|
925
998
|
|