@danieltmn/tamnora 0.20.1 → 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.
@@ -72,6 +72,421 @@ html[data-theme='paper'] .tok-selector { color: #8a5a00; }
72
72
  html[data-theme='paper'] .tok-punct { color: #7a7669; }
73
73
  html[data-theme='paper'] .btn.primary { color: #fbf6ec; }
74
74
 
75
+ html[data-theme='midnight'] {
76
+ color-scheme: dark;
77
+ --bg: #0b1220;
78
+ --panel: rgba(17, 24, 39, 0.86);
79
+ --rail: rgba(13, 19, 33, 0.82);
80
+ --theirs: #1b2438;
81
+ --mine: #6ea8fe;
82
+ --mine-soft: rgba(110, 168, 254, 0.14);
83
+ --text: #e2e8f0;
84
+ --muted: #94a3b8;
85
+ --border: rgba(226, 232, 240, 0.14);
86
+ --danger: #f87171;
87
+ --accent: #5b8def;
88
+ --accent-dark: #3b6fd4;
89
+ --accent-soft: rgba(91, 141, 239, 0.2);
90
+ --ok: #4ade80;
91
+ --warn: #fbbf24;
92
+ --info: #60a5fa;
93
+ --danger-soft: rgba(248, 113, 113, 0.16);
94
+ --ok-soft: rgba(74, 222, 128, 0.16);
95
+ --warn-soft: rgba(251, 191, 36, 0.16);
96
+ --info-soft: rgba(96, 165, 250, 0.16);
97
+ --muted-soft: rgba(148, 163, 184, 0.14);
98
+ --solid: #0d1524;
99
+ --primary-fg: #f8fafc;
100
+ --page-bg: #0b1220;
101
+ }
102
+ html[data-theme='midnight'] .tok-comment { color: #6b7a99; }
103
+ html[data-theme='midnight'] .tok-string { color: #9ecbff; }
104
+ html[data-theme='midnight'] .tok-number { color: #f0b866; }
105
+ html[data-theme='midnight'] .tok-keyword { color: #7aa2f7; }
106
+ html[data-theme='midnight'] .tok-tag { color: #7aa2f7; }
107
+ html[data-theme='midnight'] .tok-attr { color: #7dd3a0; }
108
+ html[data-theme='midnight'] .tok-selector { color: #e0af68; }
109
+ html[data-theme='midnight'] .tok-punct { color: #8294b3; }
110
+
111
+ html[data-theme='nord'] {
112
+ color-scheme: dark;
113
+ --bg: #2e3440;
114
+ --panel: rgba(46, 52, 64, 0.86);
115
+ --rail: rgba(59, 66, 82, 0.82);
116
+ --theirs: #3b4252;
117
+ --mine: #81a1c1;
118
+ --mine-soft: rgba(129, 161, 193, 0.16);
119
+ --text: #eceff4;
120
+ --muted: #9aa5b5;
121
+ --border: rgba(216, 222, 233, 0.16);
122
+ --danger: #bf616a;
123
+ --accent: #88c0d0;
124
+ --accent-dark: #5e81ac;
125
+ --accent-soft: rgba(136, 192, 208, 0.2);
126
+ --ok: #a3be8c;
127
+ --warn: #ebcb8b;
128
+ --info: #81a1c1;
129
+ --danger-soft: rgba(191, 97, 106, 0.18);
130
+ --ok-soft: rgba(163, 190, 140, 0.16);
131
+ --warn-soft: rgba(235, 203, 139, 0.16);
132
+ --info-soft: rgba(129, 161, 193, 0.16);
133
+ --muted-soft: rgba(154, 165, 181, 0.14);
134
+ --solid: #2b313d;
135
+ --primary-fg: #2e3440;
136
+ --page-bg: #2e3440;
137
+ }
138
+ html[data-theme='nord'] .tok-comment { color: #7b88a1; }
139
+ html[data-theme='nord'] .tok-string { color: #a3be8c; }
140
+ html[data-theme='nord'] .tok-number { color: #b48ead; }
141
+ html[data-theme='nord'] .tok-keyword { color: #81a1c1; }
142
+ html[data-theme='nord'] .tok-tag { color: #81a1c1; }
143
+ html[data-theme='nord'] .tok-attr { color: #8fbcbb; }
144
+ html[data-theme='nord'] .tok-selector { color: #ebcb8b; }
145
+ html[data-theme='nord'] .tok-punct { color: #9aa5b5; }
146
+
147
+ html[data-theme='dracula'] {
148
+ color-scheme: dark;
149
+ --bg: #282a36;
150
+ --panel: rgba(40, 42, 54, 0.88);
151
+ --rail: rgba(33, 34, 44, 0.85);
152
+ --theirs: #343746;
153
+ --mine: #ff79c6;
154
+ --mine-soft: rgba(255, 121, 198, 0.14);
155
+ --text: #f8f8f2;
156
+ --muted: #a7a9be;
157
+ --border: rgba(248, 248, 242, 0.14);
158
+ --danger: #ff5555;
159
+ --accent: #bd93f9;
160
+ --accent-dark: #9b6ef0;
161
+ --accent-soft: rgba(189, 147, 249, 0.2);
162
+ --ok: #50fa7b;
163
+ --warn: #f1fa8c;
164
+ --info: #8be9fd;
165
+ --danger-soft: rgba(255, 85, 85, 0.16);
166
+ --ok-soft: rgba(80, 250, 123, 0.14);
167
+ --warn-soft: rgba(241, 250, 140, 0.14);
168
+ --info-soft: rgba(139, 233, 253, 0.16);
169
+ --muted-soft: rgba(167, 169, 190, 0.14);
170
+ --solid: #21222c;
171
+ --primary-fg: #282a36;
172
+ --page-bg: #282a36;
173
+ }
174
+ html[data-theme='dracula'] .tok-comment { color: #6272a4; }
175
+ html[data-theme='dracula'] .tok-string { color: #f1fa8c; }
176
+ html[data-theme='dracula'] .tok-number { color: #bd93f9; }
177
+ html[data-theme='dracula'] .tok-keyword { color: #ff79c6; }
178
+ html[data-theme='dracula'] .tok-tag { color: #ff79c6; }
179
+ html[data-theme='dracula'] .tok-attr { color: #50fa7b; }
180
+ html[data-theme='dracula'] .tok-selector { color: #8be9fd; }
181
+ html[data-theme='dracula'] .tok-punct { color: #a7a9be; }
182
+
183
+ html[data-theme='solarized'] {
184
+ color-scheme: light;
185
+ --bg: #fdf6e3;
186
+ --panel: rgba(253, 246, 227, 0.82);
187
+ --rail: rgba(238, 232, 213, 0.78);
188
+ --theirs: #eee8d5;
189
+ --mine: #cb4b16;
190
+ --mine-soft: rgba(203, 75, 22, 0.1);
191
+ --text: #073642;
192
+ --muted: #657b83;
193
+ --border: rgba(7, 54, 66, 0.18);
194
+ --danger: #dc322f;
195
+ --accent: #b58900;
196
+ --accent-dark: #cb4b16;
197
+ --accent-soft: rgba(181, 137, 0, 0.14);
198
+ --ok: #859900;
199
+ --warn: #b58900;
200
+ --info: #268bd2;
201
+ --danger-soft: rgba(220, 50, 47, 0.12);
202
+ --ok-soft: rgba(133, 153, 0, 0.14);
203
+ --warn-soft: rgba(181, 137, 0, 0.14);
204
+ --info-soft: rgba(38, 139, 210, 0.12);
205
+ --muted-soft: rgba(101, 123, 131, 0.12);
206
+ --solid: #fdf6e3;
207
+ --primary-fg: #fdf6e3;
208
+ --page-bg: #fdf6e3;
209
+ }
210
+ html[data-theme='solarized'] .tok-comment { color: #93a1a1; }
211
+ html[data-theme='solarized'] .tok-string { color: #2aa198; }
212
+ html[data-theme='solarized'] .tok-number { color: #d33682; }
213
+ html[data-theme='solarized'] .tok-keyword { color: #859900; }
214
+ html[data-theme='solarized'] .tok-tag { color: #268bd2; }
215
+ html[data-theme='solarized'] .tok-attr { color: #b58900; }
216
+ html[data-theme='solarized'] .tok-selector { color: #cb4b16; }
217
+ html[data-theme='solarized'] .tok-punct { color: #657b83; }
218
+
219
+ html[data-theme='solarized-dark'] {
220
+ color-scheme: dark;
221
+ --bg: #002b36;
222
+ --panel: rgba(0, 43, 54, 0.88);
223
+ --rail: rgba(7, 54, 66, 0.82);
224
+ --theirs: #073642;
225
+ --mine: #cb4b16;
226
+ --mine-soft: rgba(203, 75, 22, 0.16);
227
+ --text: #eee8d5;
228
+ --muted: #93a1a1;
229
+ --border: rgba(238, 232, 213, 0.16);
230
+ --danger: #dc322f;
231
+ --accent: #b58900;
232
+ --accent-dark: #cb4b16;
233
+ --accent-soft: rgba(181, 137, 0, 0.2);
234
+ --ok: #859900;
235
+ --warn: #b58900;
236
+ --info: #268bd2;
237
+ --danger-soft: rgba(220, 50, 47, 0.18);
238
+ --ok-soft: rgba(133, 153, 0, 0.16);
239
+ --warn-soft: rgba(181, 137, 0, 0.16);
240
+ --info-soft: rgba(38, 139, 210, 0.16);
241
+ --muted-soft: rgba(147, 161, 161, 0.14);
242
+ --solid: #00252e;
243
+ --primary-fg: #002b36;
244
+ --page-bg: #002b36;
245
+ }
246
+ html[data-theme='solarized-dark'] .tok-comment { color: #586e75; }
247
+ html[data-theme='solarized-dark'] .tok-string { color: #2aa198; }
248
+ html[data-theme='solarized-dark'] .tok-number { color: #d33682; }
249
+ html[data-theme='solarized-dark'] .tok-keyword { color: #859900; }
250
+ html[data-theme='solarized-dark'] .tok-tag { color: #268bd2; }
251
+ html[data-theme='solarized-dark'] .tok-attr { color: #b58900; }
252
+ html[data-theme='solarized-dark'] .tok-selector { color: #cb4b16; }
253
+ html[data-theme='solarized-dark'] .tok-punct { color: #93a1a1; }
254
+
255
+ html[data-theme='bosque'] {
256
+ color-scheme: light;
257
+ --bg: #f2f4ec;
258
+ --panel: rgba(244, 246, 238, 0.8);
259
+ --rail: rgba(231, 235, 223, 0.75);
260
+ --theirs: #e7ebdf;
261
+ --mine: #4f8a4a;
262
+ --mine-soft: rgba(79, 138, 74, 0.1);
263
+ --text: #1e241c;
264
+ --muted: #5f6b59;
265
+ --border: rgba(30, 36, 28, 0.16);
266
+ --danger: #a33a2a;
267
+ --accent: #4f8a4a;
268
+ --accent-dark: #3d6f39;
269
+ --accent-soft: rgba(79, 138, 74, 0.12);
270
+ --ok: #4f8a4a;
271
+ --warn: #8a5a00;
272
+ --info: #3f6ea6;
273
+ --danger-soft: rgba(163, 58, 42, 0.1);
274
+ --ok-soft: rgba(79, 138, 74, 0.12);
275
+ --warn-soft: rgba(138, 90, 0, 0.12);
276
+ --info-soft: rgba(63, 110, 166, 0.12);
277
+ --muted-soft: rgba(95, 107, 89, 0.12);
278
+ --solid: #f4f6ee;
279
+ --primary-fg: #f8faf4;
280
+ --page-bg: #f2f4ec;
281
+ }
282
+ html[data-theme='bosque'] .tok-comment { color: #77836f; }
283
+ html[data-theme='bosque'] .tok-string { color: #3f6b46; }
284
+ html[data-theme='bosque'] .tok-number { color: #8a5a00; }
285
+ html[data-theme='bosque'] .tok-keyword { color: #a3341f; }
286
+ html[data-theme='bosque'] .tok-tag { color: #a3341f; }
287
+ html[data-theme='bosque'] .tok-attr { color: #4f6b46; }
288
+ html[data-theme='bosque'] .tok-selector { color: #8a5a00; }
289
+ html[data-theme='bosque'] .tok-punct { color: #5f6b59; }
290
+
291
+ html[data-theme='vino'] {
292
+ color-scheme: light;
293
+ --bg: #f7eeee;
294
+ --panel: rgba(248, 240, 240, 0.8);
295
+ --rail: rgba(238, 227, 228, 0.75);
296
+ --theirs: #eedfdf;
297
+ --mine: #a3344a;
298
+ --mine-soft: rgba(163, 52, 74, 0.1);
299
+ --text: #26181a;
300
+ --muted: #6d5a5d;
301
+ --border: rgba(38, 24, 26, 0.16);
302
+ --danger: #934022;
303
+ --accent: #a3344a;
304
+ --accent-dark: #82273a;
305
+ --accent-soft: rgba(163, 52, 74, 0.12);
306
+ --ok: #66865d;
307
+ --warn: #8a5a00;
308
+ --info: #3f6ea6;
309
+ --danger-soft: rgba(147, 64, 34, 0.1);
310
+ --ok-soft: rgba(102, 134, 93, 0.14);
311
+ --warn-soft: rgba(138, 90, 0, 0.12);
312
+ --info-soft: rgba(63, 110, 166, 0.12);
313
+ --muted-soft: rgba(109, 90, 93, 0.12);
314
+ --solid: #f8f0f0;
315
+ --primary-fg: #fbf5f5;
316
+ --page-bg: #f7eeee;
317
+ }
318
+ html[data-theme='vino'] .tok-comment { color: #837276; }
319
+ html[data-theme='vino'] .tok-string { color: #7a4a2c; }
320
+ html[data-theme='vino'] .tok-number { color: #8a5a00; }
321
+ html[data-theme='vino'] .tok-keyword { color: #a3341f; }
322
+ html[data-theme='vino'] .tok-tag { color: #a3341f; }
323
+ html[data-theme='vino'] .tok-attr { color: #4f6b46; }
324
+ html[data-theme='vino'] .tok-selector { color: #8a5a00; }
325
+ html[data-theme='vino'] .tok-punct { color: #6d5a5d; }
326
+
327
+ html[data-theme='vercel'] {
328
+ color-scheme: dark;
329
+ --bg: #000000;
330
+ --panel: rgba(10, 10, 10, 0.86);
331
+ --rail: rgba(8, 8, 8, 0.85);
332
+ --theirs: #111111;
333
+ --mine: #0070f3;
334
+ --mine-soft: rgba(0, 112, 243, 0.14);
335
+ --text: #ededed;
336
+ --muted: #8f8f8f;
337
+ --border: rgba(255, 255, 255, 0.14);
338
+ --danger: #ff5c5c;
339
+ --accent: #0070f3;
340
+ --accent-dark: #3291ff;
341
+ --accent-soft: rgba(0, 112, 243, 0.18);
342
+ --ok: #50e3c2;
343
+ --warn: #f5a623;
344
+ --info: #3291ff;
345
+ --danger-soft: rgba(255, 92, 92, 0.16);
346
+ --ok-soft: rgba(80, 227, 194, 0.16);
347
+ --warn-soft: rgba(245, 166, 35, 0.16);
348
+ --info-soft: rgba(50, 145, 255, 0.16);
349
+ --muted-soft: rgba(143, 143, 143, 0.14);
350
+ --solid: #0a0a0a;
351
+ --primary-fg: #ffffff;
352
+ --page-bg: #000000;
353
+ }
354
+ html[data-theme='vercel'] .tok-comment { color: #6f6f6f; }
355
+ html[data-theme='vercel'] .tok-string { color: #50e3c2; }
356
+ html[data-theme='vercel'] .tok-number { color: #f5a623; }
357
+ html[data-theme='vercel'] .tok-keyword { color: #3291ff; }
358
+ html[data-theme='vercel'] .tok-tag { color: #3291ff; }
359
+ html[data-theme='vercel'] .tok-attr { color: #50e3c2; }
360
+ html[data-theme='vercel'] .tok-selector { color: #f5a623; }
361
+ html[data-theme='vercel'] .tok-punct { color: #8f8f8f; }
362
+
363
+ html[data-theme='dark-plus'] {
364
+ color-scheme: dark;
365
+ --bg: #1e1e1e;
366
+ --panel: rgba(37, 37, 38, 0.87);
367
+ --rail: rgba(30, 30, 30, 0.86);
368
+ --theirs: #2d2d2d;
369
+ --mine: #569cd6;
370
+ --mine-soft: rgba(86, 156, 214, 0.14);
371
+ --text: #d4d4d4;
372
+ --muted: #9a9a9a;
373
+ --border: rgba(255, 255, 255, 0.12);
374
+ --danger: #f14c4c;
375
+ --accent: #0e639c;
376
+ --accent-dark: #1177bb;
377
+ --accent-soft: rgba(14, 99, 156, 0.28);
378
+ --ok: #89d185;
379
+ --warn: #cca700;
380
+ --info: #3794ff;
381
+ --danger-soft: rgba(241, 76, 76, 0.16);
382
+ --ok-soft: rgba(137, 209, 133, 0.16);
383
+ --warn-soft: rgba(204, 167, 0, 0.16);
384
+ --info-soft: rgba(55, 148, 255, 0.16);
385
+ --muted-soft: rgba(154, 154, 154, 0.14);
386
+ --solid: #252526;
387
+ --primary-fg: #ffffff;
388
+ --page-bg: #1e1e1e;
389
+ }
390
+ html[data-theme='dark-plus'] .tok-comment { color: #6a9955; }
391
+ html[data-theme='dark-plus'] .tok-string { color: #ce9178; }
392
+ html[data-theme='dark-plus'] .tok-number { color: #b5cea8; }
393
+ html[data-theme='dark-plus'] .tok-keyword { color: #569cd6; }
394
+ html[data-theme='dark-plus'] .tok-tag { color: #569cd6; }
395
+ html[data-theme='dark-plus'] .tok-attr { color: #9cdcfe; }
396
+ html[data-theme='dark-plus'] .tok-selector { color: #d7ba7d; }
397
+ html[data-theme='dark-plus'] .tok-punct { color: #d4d4d4; }
398
+
399
+ html[data-theme='github'] {
400
+ color-scheme: light;
401
+ --bg: #ffffff;
402
+ --panel: rgba(246, 248, 250, 0.85);
403
+ --rail: rgba(246, 248, 250, 0.85);
404
+ --theirs: #f6f8fa;
405
+ --mine: #0969da;
406
+ --mine-soft: rgba(9, 105, 218, 0.08);
407
+ --text: #1f2328;
408
+ --muted: #656d76;
409
+ --border: rgba(31, 35, 40, 0.16);
410
+ --danger: #cf222e;
411
+ --accent: #0969da;
412
+ --accent-dark: #0757ba;
413
+ --accent-soft: rgba(9, 105, 218, 0.1);
414
+ --ok: #1a7f37;
415
+ --warn: #9a6700;
416
+ --info: #0969da;
417
+ --danger-soft: rgba(207, 34, 46, 0.1);
418
+ --ok-soft: rgba(26, 127, 55, 0.12);
419
+ --warn-soft: rgba(154, 103, 0, 0.12);
420
+ --info-soft: rgba(9, 105, 218, 0.1);
421
+ --muted-soft: rgba(101, 109, 118, 0.12);
422
+ --solid: #ffffff;
423
+ --primary-fg: #ffffff;
424
+ --page-bg: #ffffff;
425
+ }
426
+ html[data-theme='github'] .tok-comment { color: #6e7781; }
427
+ html[data-theme='github'] .tok-string { color: #0a3069; }
428
+ html[data-theme='github'] .tok-number { color: #0550ae; }
429
+ html[data-theme='github'] .tok-keyword { color: #cf222e; }
430
+ html[data-theme='github'] .tok-tag { color: #116329; }
431
+ html[data-theme='github'] .tok-attr { color: #0550ae; }
432
+ html[data-theme='github'] .tok-selector { color: #0550ae; }
433
+ html[data-theme='github'] .tok-punct { color: #1f2328; }
434
+
435
+ html[data-theme='github-dark'] {
436
+ color-scheme: dark;
437
+ --bg: #0d1117;
438
+ --panel: rgba(22, 27, 34, 0.9);
439
+ --rail: rgba(13, 17, 23, 0.86);
440
+ --theirs: #161b22;
441
+ --mine: #58a6ff;
442
+ --mine-soft: rgba(88, 166, 255, 0.14);
443
+ --text: #e6edf3;
444
+ --muted: #8b949e;
445
+ --border: rgba(240, 246, 252, 0.14);
446
+ --danger: #f85149;
447
+ --accent: #58a6ff;
448
+ --accent-dark: #79c0ff;
449
+ --accent-soft: rgba(88, 166, 255, 0.18);
450
+ --ok: #3fb950;
451
+ --warn: #d29922;
452
+ --info: #58a6ff;
453
+ --danger-soft: rgba(248, 81, 73, 0.16);
454
+ --ok-soft: rgba(63, 185, 80, 0.16);
455
+ --warn-soft: rgba(210, 153, 34, 0.16);
456
+ --info-soft: rgba(88, 166, 255, 0.16);
457
+ --muted-soft: rgba(139, 148, 158, 0.14);
458
+ --solid: #161b22;
459
+ --primary-fg: #0d1117;
460
+ --page-bg: #0d1117;
461
+ }
462
+ html[data-theme='github-dark'] .tok-comment { color: #8b949e; }
463
+ html[data-theme='github-dark'] .tok-string { color: #a5d6ff; }
464
+ html[data-theme='github-dark'] .tok-number { color: #79c0ff; }
465
+ html[data-theme='github-dark'] .tok-keyword { color: #ff7b72; }
466
+ html[data-theme='github-dark'] .tok-tag { color: #7ee787; }
467
+ html[data-theme='github-dark'] .tok-attr { color: #79c0ff; }
468
+ html[data-theme='github-dark'] .tok-selector { color: #d2a8ff; }
469
+ html[data-theme='github-dark'] .tok-punct { color: #c9d1d9; }
470
+
471
+ /* ajustes: apariencia (tema + tipografía + tamaño) ------------------------ */
472
+ .theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; }
473
+ .theme-swatch {
474
+ display: flex; align-items: center; gap: 8px; padding: 8px 10px; text-align: left;
475
+ border: 1px solid var(--border); border-radius: 10px; background: var(--theirs);
476
+ color: var(--text); cursor: pointer; font: 600 12px var(--sans);
477
+ }
478
+ .theme-swatch:hover { border-color: var(--accent); }
479
+ .theme-swatch.active { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
480
+ .theme-swatch-dot { width: 16px; height: 16px; flex: 0 0 auto; border-radius: 50%; border: 1px solid var(--border); }
481
+ .theme-swatch-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
482
+ .apariencia-preview {
483
+ display: flex; flex-direction: column; gap: 5px; padding: 12px 14px;
484
+ border: 1px solid var(--border); border-radius: 12px; background: var(--panel);
485
+ }
486
+ .apariencia-preview strong { font-family: var(--serif); font-size: 20px; letter-spacing: -0.02em; }
487
+ .apariencia-preview span { color: var(--muted); }
488
+ .apariencia-preview code { font-family: var(--mono); font-size: 12.5px; color: var(--accent); }
489
+
75
490
  * { box-sizing: border-box; }
76
491
  [hidden] { display: none !important; }
77
492
 
@@ -159,8 +574,8 @@ pre, code, kbd, samp { font-family: var(--mono); }
159
574
  /* El icono muestra el tema al que se va a cambiar: claro -> luna, oscuro -> sol. */
160
575
  #theme-toggle .ico-sun { display: none; }
161
576
  #theme-toggle .ico-moon { display: block; }
162
- html[data-theme='dark'] #theme-toggle .ico-sun { display: block; }
163
- html[data-theme='dark'] #theme-toggle .ico-moon { display: none; }
577
+ html[data-scheme='dark'] #theme-toggle .ico-sun { display: block; }
578
+ html[data-scheme='dark'] #theme-toggle .ico-moon { display: none; }
164
579
  .menu-pop {
165
580
  position: absolute;
166
581
  top: calc(100% + 4px);
@@ -0,0 +1,70 @@
1
+ // Apariencia de la consola (app.json -> appearance): tema, tipografía y tamaño.
2
+ // Los ids deben coincidir con public/appearance.js; acá solo se valida y acota.
3
+
4
+ export const THEME_IDS = [
5
+ 'paper',
6
+ 'dark',
7
+ 'midnight',
8
+ 'nord',
9
+ 'dracula',
10
+ 'solarized',
11
+ 'solarized-dark',
12
+ 'bosque',
13
+ 'vino',
14
+ 'vercel',
15
+ 'dark-plus',
16
+ 'github',
17
+ 'github-dark',
18
+ ];
19
+
20
+ export const FONT_IDS = ['clasico', 'sistema', 'plex', 'inter', 'jetbrains', 'fira', 'source'];
21
+
22
+ export const DEFAULT_APPEARANCE = { theme: 'paper', font: 'clasico', chatFont: 14.5, scale: 1 };
23
+
24
+ const MIN_CHAT_FONT = 12;
25
+ const MAX_CHAT_FONT = 20;
26
+ const MIN_SCALE = 0.8;
27
+ const MAX_SCALE = 1.3;
28
+
29
+ export function resolveThemeId(id) {
30
+ const legacy = id === 'light' ? 'paper' : id;
31
+ return THEME_IDS.includes(legacy) ? legacy : DEFAULT_APPEARANCE.theme;
32
+ }
33
+
34
+ export function resolveFontId(id) {
35
+ return FONT_IDS.includes(id) ? id : DEFAULT_APPEARANCE.font;
36
+ }
37
+
38
+ export function clampChatFont(value) {
39
+ const n = Number(value);
40
+ if (!Number.isFinite(n)) return DEFAULT_APPEARANCE.chatFont;
41
+ return Math.min(MAX_CHAT_FONT, Math.max(MIN_CHAT_FONT, Math.round(n * 2) / 2));
42
+ }
43
+
44
+ export function clampScale(value) {
45
+ const n = Number(value);
46
+ if (!Number.isFinite(n)) return DEFAULT_APPEARANCE.scale;
47
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, Math.round(n * 100) / 100));
48
+ }
49
+
50
+ /** Apariencia efectiva: defaults + lo guardado en app.json. */
51
+ export function resolveAppearance(app) {
52
+ const saved = app?.appearance && typeof app.appearance === 'object' ? app.appearance : {};
53
+ return {
54
+ theme: resolveThemeId(saved.theme),
55
+ font: resolveFontId(saved.font),
56
+ chatFont: clampChatFont(saved.chatFont),
57
+ scale: clampScale(saved.scale),
58
+ };
59
+ }
60
+
61
+ /** Aplica un body de PUT sobre la apariencia actual. */
62
+ export function sanitizeAppearance(body, current = DEFAULT_APPEARANCE) {
63
+ const value = body && typeof body === 'object' ? body : {};
64
+ const next = { ...current };
65
+ if ('theme' in value) next.theme = resolveThemeId(value.theme);
66
+ if ('font' in value) next.font = resolveFontId(value.font);
67
+ if ('chatFont' in value) next.chatFont = clampChatFont(value.chatFont);
68
+ if ('scale' in value) next.scale = clampScale(value.scale);
69
+ return next;
70
+ }
@@ -1,4 +1,5 @@
1
- import { MODES, MODE_IDS, resolveMode, PROVIDER_META, testProviderConnection } from '../lib/core.js';
1
+ import { MODES, MODE_IDS, resolveMode, PROVIDER_META, testProviderConnection, readApp, writeApp } from '../lib/core.js';
2
+ import { resolveAppearance, sanitizeAppearance } from '../lib/appearance.js';
2
3
  import { readJson, sendJson, sendError } from '../lib/http.js';
3
4
 
4
5
  const ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
@@ -80,6 +81,7 @@ export function registerProviders(route, ctx) {
80
81
  strictScope: ctx.effectiveStrictScope(),
81
82
  mcpMode: ctx.effectiveMcpMode(),
82
83
  mcpPinned: ctx.effectiveMcpPinned(),
84
+ appearance: resolveAppearance(ctx.app),
83
85
  });
84
86
 
85
87
  route('GET', '/api/settings', ({ res }) => {
@@ -108,6 +110,12 @@ export function registerProviders(route, ctx) {
108
110
  const value = Number(body[key]);
109
111
  ctx.store.setSetting(key, Number.isFinite(value) && value > 0 ? Math.round(value) : 0);
110
112
  }
113
+ if (body.appearance !== undefined) {
114
+ const app = readApp();
115
+ app.appearance = sanitizeAppearance(body.appearance, resolveAppearance(app));
116
+ writeApp(app);
117
+ ctx.app = app;
118
+ }
111
119
  sendJson(res, 200, { settings: settingsView() });
112
120
  });
113
121
 
@@ -0,0 +1,74 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { createTamnoraServer } from '../src/index.js';
7
+ import { setHome, readApp } from '../../core/src/index.js';
8
+
9
+ const fakeProvider = {
10
+ id: 'fake',
11
+ async streamChat(_config, { onEvent }) {
12
+ onEvent?.({ type: 'text', delta: 'hola', full: 'hola' });
13
+ return { content: 'hola', reasoning: '', toolCalls: [] };
14
+ },
15
+ };
16
+
17
+ async function startServer(overrides = {}) {
18
+ const cwd = overrides.cwd ?? (await mkdtemp(path.join(tmpdir(), 'tamnora-app-')));
19
+ setHome(cwd);
20
+ const instance = await createTamnoraServer({
21
+ cwd,
22
+ dbPath: path.join(cwd, 'tamnora.db'),
23
+ loadMcp: false,
24
+ resolveProvider: () => ({ provider: fakeProvider, config: {} }),
25
+ ...overrides,
26
+ });
27
+ const info = await instance.listen({ port: 0 });
28
+ return { instance, base: info.url, cwd };
29
+ }
30
+
31
+ test('la apariencia arranca con los defaults', async () => {
32
+ const { instance, base } = await startServer();
33
+ try {
34
+ const { settings } = await (await fetch(`${base}/api/settings`)).json();
35
+ assert.deepEqual(settings.appearance, { theme: 'paper', font: 'clasico', chatFont: 14.5, scale: 1 });
36
+ } finally {
37
+ await instance.close();
38
+ }
39
+ });
40
+
41
+ test('la apariencia se guarda en app.json y se valida', async () => {
42
+ const { instance, base } = await startServer();
43
+ try {
44
+ const put = await fetch(`${base}/api/settings`, {
45
+ method: 'PUT',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ appearance: { theme: 'nord', font: 'plex', chatFont: 18, scale: 1.1 } }),
48
+ });
49
+ assert.equal(put.status, 200);
50
+ const saved = (await put.json()).settings.appearance;
51
+ assert.deepEqual(saved, { theme: 'nord', font: 'plex', chatFont: 18, scale: 1.1 });
52
+ assert.equal(readApp().appearance.theme, 'nord');
53
+
54
+ const reloaded = (await (await fetch(`${base}/api/settings`)).json()).settings.appearance;
55
+ assert.deepEqual(reloaded, saved);
56
+ } finally {
57
+ await instance.close();
58
+ }
59
+ });
60
+
61
+ test('la apariencia rechaza valores fuera de rango', async () => {
62
+ const { instance, base } = await startServer();
63
+ try {
64
+ const put = await fetch(`${base}/api/settings`, {
65
+ method: 'PUT',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ appearance: { theme: 'inventado', font: 'raro', chatFont: 99, scale: 5 } }),
68
+ });
69
+ const saved = (await put.json()).settings.appearance;
70
+ assert.deepEqual(saved, { theme: 'paper', font: 'clasico', chatFont: 20, scale: 1.3 });
71
+ } finally {
72
+ await instance.close();
73
+ }
74
+ });