xonecode 0.31.0 → 0.34.2

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.
Files changed (133) hide show
  1. package/apps/web/dist/assets/{EditorDeFichero-ChyJtiiz.js → EditorDeFichero-Cj0knNmP.js} +1 -1
  2. package/apps/web/dist/assets/index-Ga8fTlWp.js +499 -0
  3. package/apps/web/dist/assets/{index-IptObD2z.css → index-R3kXj69E.css} +1 -1
  4. package/apps/web/dist/assets/{php-B91ZcS-9.js → php-MVUx_K96.js} +1 -1
  5. package/apps/web/dist/assets/{ruby-DrxrTSz0.js → ruby-n-r-AXdi.js} +1 -1
  6. package/apps/web/dist/index.html +2 -2
  7. package/dist/agent/config/fuentesEnRed.js +80 -0
  8. package/dist/agent/config/fuentesEnRed.js.map +1 -0
  9. package/dist/agent/config/modelosDeMotor.js +4 -2
  10. package/dist/agent/config/modelosDeMotor.js.map +1 -1
  11. package/dist/agent/config/settingsEnDisco.js +26 -0
  12. package/dist/agent/config/settingsEnDisco.js.map +1 -1
  13. package/dist/agent/dispositivos/cajasDeMaqueta.js +218 -0
  14. package/dist/agent/dispositivos/cajasDeMaqueta.js.map +1 -0
  15. package/dist/agent/dispositivos/geometriaEnDisco.js +107 -0
  16. package/dist/agent/dispositivos/geometriaEnDisco.js.map +1 -0
  17. package/dist/agent/dispositivos/juezVisual.js +84 -3
  18. package/dist/agent/dispositivos/juezVisual.js.map +1 -1
  19. package/dist/agent/grafo/criticaVisual.js +246 -11
  20. package/dist/agent/grafo/criticaVisual.js.map +1 -1
  21. package/dist/agent/grafo/fuentesDeGoogle.js +133 -0
  22. package/dist/agent/grafo/fuentesDeGoogle.js.map +1 -0
  23. package/dist/agent/grafo/incorporarAdjunto.js +52 -42
  24. package/dist/agent/grafo/incorporarAdjunto.js.map +1 -1
  25. package/dist/agent/grafo/lectorDeReferencias.js +21 -0
  26. package/dist/agent/grafo/lectorDeReferencias.js.map +1 -1
  27. package/dist/agent/grafo/perfiles.js +2 -0
  28. package/dist/agent/grafo/perfiles.js.map +1 -1
  29. package/dist/agent/motores/motoresLocales.js +429 -0
  30. package/dist/agent/motores/motoresLocales.js.map +1 -0
  31. package/dist/agent/motores/trueforge/capacidades.js +9 -6
  32. package/dist/agent/motores/trueforge/capacidades.js.map +1 -1
  33. package/dist/agent/motores/trueforge/informesDeHijos.js +39 -7
  34. package/dist/agent/motores/trueforge/informesDeHijos.js.map +1 -1
  35. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js +75 -37
  36. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js.map +1 -1
  37. package/dist/agent/motores/trueforge/memoriaTrueforge.js +24 -4
  38. package/dist/agent/motores/trueforge/memoriaTrueforge.js.map +1 -1
  39. package/dist/agent/motores/trueforge/modeloLangchain.js +28 -4
  40. package/dist/agent/motores/trueforge/modeloLangchain.js.map +1 -1
  41. package/dist/agent/motores/trueforge/proponerTareas.js +67 -0
  42. package/dist/agent/motores/trueforge/proponerTareas.js.map +1 -0
  43. package/dist/agent/motores/trueforge/sesionTrueforge.js +133 -13
  44. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  45. package/dist/agent/motores/trueforge/toolsDeFichero.js +24 -6
  46. package/dist/agent/motores/trueforge/toolsDeFichero.js.map +1 -1
  47. package/dist/agent/sesiones/adjuntosDeSesion.js +192 -5
  48. package/dist/agent/sesiones/adjuntosDeSesion.js.map +1 -1
  49. package/dist/agent/subagentes/agentesEnDisco.js +12 -4
  50. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  51. package/dist/agent/subagentes/ejecutableEnWindows.js +139 -0
  52. package/dist/agent/subagentes/ejecutableEnWindows.js.map +1 -0
  53. package/dist/agent/subagentes/subagenteCodex.js +7 -2
  54. package/dist/agent/subagentes/subagenteCodex.js.map +1 -1
  55. package/dist/agent/subagentes/subagenteOpencode.js +12 -5
  56. package/dist/agent/subagentes/subagenteOpencode.js.map +1 -1
  57. package/dist/agent/tareas/aumentador.js +4 -3
  58. package/dist/agent/tareas/aumentador.js.map +1 -1
  59. package/dist/agent/turno/informeDeTraza.js +2 -1
  60. package/dist/agent/turno/informeDeTraza.js.map +1 -1
  61. package/dist/agent/turno/interrupts.js +12 -0
  62. package/dist/agent/turno/interrupts.js.map +1 -1
  63. package/dist/agent/turno/resumenDeTool.js +4 -0
  64. package/dist/agent/turno/resumenDeTool.js.map +1 -1
  65. package/dist/agent/turno/turnoReal.js +3 -0
  66. package/dist/agent/turno/turnoReal.js.map +1 -1
  67. package/dist/cli/consola.js.map +1 -1
  68. package/dist/cli/main.js +2 -0
  69. package/dist/cli/main.js.map +1 -1
  70. package/dist/cli/tui/transcript.js +5 -0
  71. package/dist/cli/tui/transcript.js.map +1 -1
  72. package/dist/core/actos.js.map +1 -1
  73. package/dist/core/adjuntos.js +10 -2
  74. package/dist/core/adjuntos.js.map +1 -1
  75. package/dist/core/agentes.js +10 -0
  76. package/dist/core/agentes.js.map +1 -1
  77. package/dist/core/analisisDeSesion.js +1 -1
  78. package/dist/core/analisisDeSesion.js.map +1 -1
  79. package/dist/core/descripcionDePantalla.js +367 -0
  80. package/dist/core/descripcionDePantalla.js.map +1 -0
  81. package/dist/core/estiloMedido.js +352 -0
  82. package/dist/core/estiloMedido.js.map +1 -0
  83. package/dist/core/fuentes.js +174 -0
  84. package/dist/core/fuentes.js.map +1 -0
  85. package/dist/core/geometriaDePantalla.js +537 -0
  86. package/dist/core/geometriaDePantalla.js.map +1 -0
  87. package/dist/core/motoresLocales.js +128 -0
  88. package/dist/core/motoresLocales.js.map +1 -0
  89. package/dist/core/notify.js +4 -0
  90. package/dist/core/notify.js.map +1 -1
  91. package/dist/core/nucleoXone.js +6 -6
  92. package/dist/core/nucleoXone.js.map +1 -1
  93. package/dist/core/ports.js +35 -0
  94. package/dist/core/ports.js.map +1 -1
  95. package/dist/core/repartoDeEncargo.js +87 -0
  96. package/dist/core/repartoDeEncargo.js.map +1 -0
  97. package/dist/core/settings.js +5 -0
  98. package/dist/core/settings.js.map +1 -1
  99. package/dist/core/shellDeAgente.js +36 -0
  100. package/dist/core/shellDeAgente.js.map +1 -1
  101. package/dist/core/tareas.js +7 -0
  102. package/dist/core/tareas.js.map +1 -1
  103. package/dist/core/turno.js +15 -0
  104. package/dist/core/turno.js.map +1 -1
  105. package/dist/core/zipDeAdjunto.js +79 -0
  106. package/dist/core/zipDeAdjunto.js.map +1 -0
  107. package/dist/web/servidor/arranque.js +201 -13
  108. package/dist/web/servidor/arranque.js.map +1 -1
  109. package/dist/web/servidor/consolaWeb.js +5 -0
  110. package/dist/web/servidor/consolaWeb.js.map +1 -1
  111. package/dist/web/servidor/interrupcion.js +52 -0
  112. package/dist/web/servidor/interrupcion.js.map +1 -0
  113. package/dist/web/servidor/pielWeb.js +36 -0
  114. package/dist/web/servidor/pielWeb.js.map +1 -1
  115. package/dist/web/servidor/transporte.js +1 -0
  116. package/dist/web/servidor/transporte.js.map +1 -1
  117. package/dist/web/servidor/vestibulo.js +30 -2
  118. package/dist/web/servidor/vestibulo.js.map +1 -1
  119. package/package.json +1 -1
  120. package/skills/xone-development/SKILL.md +2 -1
  121. package/skills/xone-development/references/css/buenas-practicas-y-parser.md +2 -1
  122. package/skills/xone-development/references/css/de-una-maqueta-a-xone.md +83 -0
  123. package/skills/xone-development/references/css/patrones-material-y-temas.md +15 -13
  124. package/skills/xone-development/references/css/propiedades-y-herencia.md +2 -2
  125. package/skills/xone-development/references/css/selectores-unidades-colores.md +8 -3
  126. package/skills/xone-development/references/indice-completo.md +1 -0
  127. package/skills/xone-development/references/xml-ui/atributos-coll-group-frame.md +4 -4
  128. package/skills/xone-development/references/xml-ui/nodos-group-y-frame.md +4 -4
  129. package/skills/xone-hotswap/lib/geometria.mjs +147 -0
  130. package/skills/xone-hotswap/scripts/xone-captura-android +6 -0
  131. package/skills/xone-hotswap/scripts/xone-hotswap +8 -0
  132. package/skills/xone-hotswap/scripts/xone-recargar-android +15 -0
  133. package/apps/web/dist/assets/index-CFIrUOmW.js +0 -499
@@ -0,0 +1,83 @@
1
+ # De una maqueta HTML (Stitch, Tailwind) a XOne
2
+
3
+ Contenido: §1 cómo leer esta tabla · §2 medidas y unidades · §3 forma, bordes y fondos · §4 color · §5 texto y
4
+ tipografías · §6 composición · §7 lo que no existe
5
+
6
+ Una maqueta de Stitch trae un `code.html` con clases de Tailwind y una configuración propia (`tailwind.config`:
7
+ colores con nombre, `borderRadius`, `fontFamily`, `fontSize`). Esta tabla dice cómo se escribe cada cosa en XOne.
8
+ **XOne ignora en silencio lo que no conoce**: una clase traducida «a ojo» no da error, da una pantalla distinta.
9
+
10
+ ## 1. Cómo leer esta tabla
11
+
12
+ Cada fila dice de dónde sale:
13
+
14
+ - **medido**: comprobado en un emulador Android con una pantalla de prueba. Fíate.
15
+ - **documentado**: lo dice otra referencia de esta skill, sin medida propia.
16
+ - **sin medir**: no se ha comprobado. Pruébalo en el aparato antes de dar nada por bueno.
17
+
18
+ Lo primero, siempre: lee la configuración de Tailwind del `code.html` (`tailwind.config = {…}`). Los nombres de las
19
+ clases (`rounded-lg`, `text-key-numeric`, `bg-surface-container-high`) son de ESA maqueta y valen lo que diga ella.
20
+
21
+ ## 2. Medidas y unidades
22
+
23
+ | En la maqueta | En XOne | De dónde |
24
+ |---|---|---|
25
+ | un ancho en px | `p`: `px_maqueta × resolution-width / ancho_de_la_maqueta`; o en `%` del contenedor | medido |
26
+ | un alto en px | `p` con la misma cuenta. **Ojo**: un alto en `p` escala con el ALTO útil de la pantalla y un ancho con el ANCHO, así que en un teléfono alargado sale más alto (medido: ×1,72 en alto y ×1,5 en ancho, 15 % más alto de proporción) | medido |
27
+ | `w-full` | `width="100%"` | documentado |
28
+ | `p-N`, `px-N`, `py-N` | `lpadding`, `rpadding`, `tpadding`, `bpadding` (no hay abreviado) | documentado |
29
+ | `m-N`, `mt-N`… | `lmargin`, `tmargin`… (no hay abreviado; `rmargin` en `%` se ignora) | documentado / medido |
30
+
31
+ `p` no es el píxel y escala **por eje**: un ancho por `ancho_real / resolution-width`, un alto por
32
+ `alto_útil / resolution-height` (el alto útil es la pantalla sin las barras de estado y de navegación). Lee los
33
+ dos valores en el `app.xml` del proyecto.
34
+
35
+ ## 3. Forma, bordes y fondos
36
+
37
+ | En la maqueta | En XOne | De dónde |
38
+ |---|---|---|
39
+ | `rounded-full` (píldora o círculo) | `border-corner-radius: 999` | medido |
40
+ | `rounded-lg`, `rounded-xl`, `rounded-[Npx]` | `border-corner-radius` en **píxeles del aparato**, no en `p`, calculados como `px_maqueta × ancho_real / ancho_de_la_maqueta` (32 px en una maqueta de 390 son unos 89 en un aparato de 1080). **En un BOTÓN ese número es el radio; en un `<frame>` el radio sale al DOBLE**, así que en un frame se escribe la MITAD (esos 89 px son `border-corner-radius="44"` en un botón y `"22"` en un frame). Ni uno ni otro dependen de la densidad del aparato ni de la `resolution-width` de la app. El radio se recorta a la mitad del alto: en un frame, un valor de la mitad del alto o más ya es una píldora (o un círculo, si es cuadrado) | medido |
41
+ | un botón sin borde | `border-width: 0`. Un `type="B"` pinta un contorno fino por omisión; `border: false`, `border: 0`, `framebox: false` y `labelbox: false` NO lo quitan | medido |
42
+ | `border`, `border-COLOR` | **en un botón**: `border-width: 1; border-color: #RRGGBB`. **En un `<frame>` eso no pinta nada** (ni con `border="15"` ni con `border="true"`): el borde de un frame es `framebox="true"` y su color `forecolor="#RRGGBB"` (sale de unos 2 px) | medido |
43
+ | `bg-gradient-to-br from-A to-B` | un SVG con `generar_fondo_svg` (tipo degradado, ángulo 45 para `to-br`): en un BOTÓN va en `img`, en un frame en `imgbk` (en un botón `imgbk` se ignora) | medido |
44
+ | `shadow-[0_6px_14px_…]` (sombra exterior) | un SVG con `generar_fondo_svg` (tipo sombra). **No** `elevation` en algo redondeado: recorta el fondo por abajo | medido |
45
+ | `shadow-[inset_…]` (brillo interior) | no hay equivalente; si importa, dibújalo en el mismo SVG del fondo | sin medir |
46
+ | `border-corner-radius` sobre una imagen de fondo | no la recorta: la esquina se dibuja DENTRO del SVG | medido |
47
+ | un `<frame>` sin nada dentro (una barra de color, un punto) | **no se pinta**: no llega ni al árbol de controles. Dentro tiene que haber al menos un control (una etiqueta `TL` con `title=" "` vale) | medido |
48
+
49
+ ## 4. Color
50
+
51
+ | En la maqueta | En XOne | De dónde |
52
+ |---|---|---|
53
+ | `bg-<nombre>`, `text-<nombre>` | el `#RRGGBB` que diga `tailwind.config` para ese nombre, en `bgcolor` y `forecolor` / `text-forecolor` | documentado |
54
+ | `bg-<nombre>/80` (transparencia) | `#AARRGGBB` con el alfa **primero**: 80 % es `CC`, 60 % `99`, 5 % `0D` | documentado |
55
+ | un color en un SVG | `#RRGGBB` más `opacity` (HTML y SVG ponen el alfa al final; XOne al principio) | medido |
56
+
57
+ ## 5. Texto y tipografías
58
+
59
+ | En la maqueta | En XOne | De dónde |
60
+ |---|---|---|
61
+ | una fuente de Google Fonts (`fontFamily` en la configuración, `fonts.googleapis.com` en el `<head>`) | su `.ttf` en `fonts/` y `fontname: Nombre.ttf` (`buscar_fuente` → `traer_fuente` lo trae: un `.ttf` estático por peso, `Inter-Bold.ttf`). El `.ttf` variable que da Google Fonts vale tal cual (sale con su peso por omisión). **Se sube desplegando entero**: la recarga en caliente no lleva `fonts/`. Un `fontname` que no está en el aparato saca un diálogo de error y el control no se pinta | medido |
62
+ | `text-[Npx]`, `fontSize` de la configuración | `fontsize: N−8`. En Android la letra sale de **`fontsize` + 8 dp**: `fontsize: 20` pinta una letra de 28 dp, y `fontsize: 40` una de 48. Un píxel de la maqueta (viewport de unos 390 de ancho) es casi un dp del teléfono, así que `text-[48px]` → `fontsize: 40` y `text-[16px]` → `fontsize: 8`. Ese +8 es fijo: no escala con el tamaño ni depende de la `resolution-width` de `app.xml`. Medido con la fuente del sistema; con otra `fontname` el tamaño es el mismo, pero el alto de las mayúsculas cambia con la fuente. `textfont-size` y iOS, sin medir | medido (Android) |
63
+ | `font-medium`, `font-semibold`, `font-bold` | la variante de la fuente con ese peso (`Roboto-Bold.ttf`) o `fontbold` | documentado |
64
+ | `uppercase` | escribe el texto ya en mayúsculas | sin medir |
65
+ | `tracking-*` (espaciado entre letras), `leading-*` (interlineado) | sin equivalente conocido | sin medir |
66
+ | un texto que no cabe | `auto-fontsize="true"`, o una caja más alta: un `height` menor que la letra la corta por abajo | medido |
67
+ | cambiar la fuente de un control (`fontname`) | **revisa la altura de su caja**: con el mismo `fontsize`, otra fuente coloca la letra a otra altura dentro de ella. Space Grotesk queda más abajo que Roboto, así que en una tecla con la altura justa se corta más por abajo (el `-` y el `+` casi desaparecen). Sube el `height` o baja el `fontsize`, y compruébalo con una captura | medido |
68
+ | iconos `material-symbols-outlined` | `buscar_icono` → `icons/ic_<nombre>.svg`, en un `type="IMG"` (en el `img` de un botón se estira) | medido |
69
+
70
+ ## 6. Composición
71
+
72
+ | En la maqueta | En XOne | De dónde |
73
+ |---|---|---|
74
+ | `grid grid-cols-4 gap-N` | una fila de controles con `width` en `%` que, con sus márgenes, sume como mucho el 100 %: el que se pasa desborda por la derecha | medido |
75
+ | `flex justify-between` | márgenes en `%` entre los elementos de la fila | documentado |
76
+ | `flex-col` | controles uno bajo otro (`newline`) dentro de un frame | documentado |
77
+ | una cabecera fija (`fixed top-0`) | un `group` con `fixed="true"` y `orientation="top"` | documentado |
78
+
79
+ ## 7. Lo que no existe en XOne (no lo traduzcas, ni a medias)
80
+
81
+ `hover:`, `active:`, `focus:`, `transition-*`, `animate-*`, `translate-*`, `scale-*`, `blur-*`, `backdrop-blur-*`,
82
+ `opacity` animada, `pointer-events-*`, `select-none`, `overflow-x-auto` con barra oculta, `env(safe-area-inset-*)`.
83
+ Son estados o animaciones del navegador; en XOne no hacen nada, y escribirlos con otro nombre no los hace existir.
@@ -126,13 +126,15 @@ El footer es el frame inferior, normalmente con botones de acción.
126
126
  Los botones siguen el patron de Material Design con esquinas redondeadas y colores por estado:
127
127
 
128
128
  ```css
129
+ /* Píldora: border-corner-radius: 999 (el radio va en PÍXELES del aparato y se recorta a la mitad del alto;
130
+ «28 = la mitad de 56p» solo vale en el aparato de referencia — medido en un emulador Android) */
129
131
  /* Boton primario - Accion principal */
130
132
  .btnPrimario {
131
133
  width: 90%;
132
134
  height: 56p;
133
135
  bgcolor: #1565C0;
134
136
  forecolor: #FFFFFF;
135
- border-corner-radius: 28;
137
+ border-corner-radius: 999;
136
138
  text-align: center;
137
139
  fontsize: 16;
138
140
  fontname: Roboto-Bold.ttf;
@@ -146,7 +148,7 @@ Los botones siguen el patron de Material Design con esquinas redondeadas y color
146
148
  forecolor: #1565C0;
147
149
  border: true;
148
150
  border-color: #1565C0;
149
- border-corner-radius: 28;
151
+ border-corner-radius: 999;
150
152
  text-align: center;
151
153
  fontsize: 16;
152
154
  }
@@ -157,7 +159,7 @@ Los botones siguen el patron de Material Design con esquinas redondeadas y color
157
159
  height: 56p;
158
160
  bgcolor: #F44336;
159
161
  forecolor: #FFFFFF;
160
- border-corner-radius: 28;
162
+ border-corner-radius: 999;
161
163
  text-align: center;
162
164
  fontsize: 16;
163
165
  }
@@ -168,7 +170,7 @@ Los botones siguen el patron de Material Design con esquinas redondeadas y color
168
170
  height: 56p;
169
171
  bgcolor: #4CAF50;
170
172
  forecolor: #FFFFFF;
171
- border-corner-radius: 28;
173
+ border-corner-radius: 999;
172
174
  text-align: center;
173
175
  fontsize: 16;
174
176
  fontname: Roboto-Bold.ttf;
@@ -180,7 +182,7 @@ Los botones siguen el patron de Material Design con esquinas redondeadas y color
180
182
  height: 56p;
181
183
  bgcolor: #00BCD4;
182
184
  forecolor: #FFFFFF;
183
- border-corner-radius: 28;
185
+ border-corner-radius: 999;
184
186
  text-align: center;
185
187
  fontsize: 16;
186
188
  fontname: Roboto-Bold.ttf;
@@ -226,7 +228,7 @@ Los botones siguen el patron de Material Design con esquinas redondeadas y color
226
228
  width: 95%;
227
229
  height: 56p;
228
230
  bgcolor: #FFFFFF;
229
- border-corner-radius: 28;
231
+ border-corner-radius: 999;
230
232
  border: true;
231
233
  border-color: #E0E0E0;
232
234
  lmargin: 15p;
@@ -306,7 +308,7 @@ El FAB es un botón circular flotante que representa la acción principal de la
306
308
  width: 56p;
307
309
  height: 56p;
308
310
  bgcolor: #1565C0;
309
- border-corner-radius: 28;
311
+ border-corner-radius: 999;
310
312
  }
311
313
 
312
314
  /* FAB grande (64p) */
@@ -699,7 +701,7 @@ prop {
699
701
  height: 56p;
700
702
  bgcolor: #1565C0;
701
703
  forecolor: #FFFFFF;
702
- border-corner-radius: 28;
704
+ border-corner-radius: 999;
703
705
  text-align: center;
704
706
  fontsize: 16;
705
707
  fontname: Roboto-Bold.ttf;
@@ -713,7 +715,7 @@ prop {
713
715
  forecolor: #1565C0;
714
716
  border: true;
715
717
  border-color: #1565C0;
716
- border-corner-radius: 28;
718
+ border-corner-radius: 999;
717
719
  text-align: center;
718
720
  fontsize: 16;
719
721
  }
@@ -724,7 +726,7 @@ prop {
724
726
  height: 56p;
725
727
  bgcolor: #F44336;
726
728
  forecolor: #FFFFFF;
727
- border-corner-radius: 28;
729
+ border-corner-radius: 999;
728
730
  text-align: center;
729
731
  fontsize: 16;
730
732
  }
@@ -735,7 +737,7 @@ prop {
735
737
  height: 56p;
736
738
  bgcolor: #4CAF50;
737
739
  forecolor: #FFFFFF;
738
- border-corner-radius: 28;
740
+ border-corner-radius: 999;
739
741
  text-align: center;
740
742
  fontsize: 16;
741
743
  fontname: Roboto-Bold.ttf;
@@ -746,7 +748,7 @@ prop {
746
748
  width: 56p;
747
749
  height: 56p;
748
750
  bgcolor: #FFFFFF;
749
- border-corner-radius: 28;
751
+ border-corner-radius: 999;
750
752
  }
751
753
 
752
754
  /* FAB */
@@ -818,7 +820,7 @@ prop {
818
820
  width: 95%;
819
821
  height: 56p;
820
822
  bgcolor: #FFFFFF;
821
- border-corner-radius: 28;
823
+ border-corner-radius: 999;
822
824
  border: true;
823
825
  border-color: #E0E0E0;
824
826
  lmargin: 15p;
@@ -499,7 +499,7 @@ El atributo `visible` usa un sistema de mascara de bits que controla en que modo
499
499
  height: 56p;
500
500
  bgcolor: #1565C0;
501
501
  forecolor: #FFFFFF;
502
- border-corner-radius: 28;
502
+ border-corner-radius: 999; /* píldora: el radio va en píxeles del aparato */
503
503
  ripple-effect: true;
504
504
  elevation: 4;
505
505
  }
@@ -573,7 +573,7 @@ El caso más común: una clase hereda de otra y sobreescribe algunos atributos.
573
573
  height: 56p;
574
574
  bgcolor: #1565C0;
575
575
  forecolor: #FFFFFF;
576
- border-corner-radius: 28;
576
+ border-corner-radius: 999; /* píldora: el radio va en píxeles del aparato */
577
577
  text-align: center;
578
578
  fontsize: 16;
579
579
  fontname: Roboto-Bold.ttf;
@@ -492,7 +492,12 @@ frame {
492
492
 
493
493
  ### 3.1 `p` (puntos) - Unidad absoluta
494
494
 
495
- La unidad `p` representa **pixels en el dispositivo de referencia** definido por `resolution-width` y `resolution-height` en `app.xml`. En el dispositivo de referencia, `1p = 1px` real. En cualquier otro dispositivo, XOne escala automáticamente con la fórmula `tamaño_real_px = valor_p × (resolucion_real / resolution-width)`.
495
+ La unidad `p` representa **pixels en el dispositivo de referencia** definido por `resolution-width` y `resolution-height` en `app.xml`. En el dispositivo de referencia, `1p = 1px` real. En cualquier otro dispositivo, XOne escala **cada eje por separado**:
496
+
497
+ - un **ancho** en `p`: `px_reales = valor_p × (ancho_real / resolution-width)`;
498
+ - un **alto** en `p`: `px_reales = valor_p × (alto_útil / resolution-height)`, donde el alto útil es la pantalla sin las barras de estado y de navegación.
499
+
500
+ > **Medido** en un emulador Android de 1080×2400 con una app de `resolution-width="720"` y `resolution-height="1280"`: `width: 421p` → 631 px (×1,5) y `height: 120p` → 206 px (×1,72, porque el alto útil son 2205 px). Así que **la proporción de un control declarado en `p` cambia de un aparato a otro**: un cuadrado de `100p × 100p` sale más alto que ancho en un teléfono alargado.
496
501
 
497
502
  > **CRÍTICO: `p` ≠ Material `dp`.** Es un error común asumir que `p` equivale a `dp` (density-independent pixels) de Android. **NO lo es.** Para el dispositivo de referencia por defecto de XOne (1080×1920, xxhdpi, density 3×), Material `56dp` ≈ `168p`, NO `56p`. Aplicar valores Material directamente como `p` produce barras/botones ~3× más pequeños de lo necesario.
498
503
 
@@ -517,7 +522,7 @@ La unidad `p` representa **pixels en el dispositivo de referencia** definido por
517
522
  .btnPrimario {
518
523
  width: 90%;
519
524
  height: 124p; /* Workflow "pill" CTA */
520
- border-corner-radius: 62;
525
+ border-corner-radius: 999; /* píldora en CUALQUIER aparato: el radio va en píxeles y se recorta a la mitad del alto */
521
526
  }
522
527
  ```
523
528
 
@@ -526,7 +531,7 @@ La unidad `p` representa **pixels en el dispositivo de referencia** definido por
526
531
  - Alturas fijas de headers, footers, botones
527
532
  - Tamaños de iconos y avatares
528
533
  - Margenes y paddings fijos
529
- - Radios de bordes (aunque estos van sin unidad)
534
+ - **Los radios NO**: `border-corner-radius` va en **píxeles reales del aparato** y no escala con `p` (medido: un radio `50` en un botón de `100p` sale de ~48 px, un tercio de su alto real de 172 px). Para una píldora usa `border-corner-radius: 999`: el radio se recorta a la mitad del alto, sea cual sea el aparato
530
535
  - Dimensiones de componentes que no deben cambiar con el tamaño de pantalla
531
536
 
532
537
  ### 3.2 `%` (porcentaje) - Unidad relativa
@@ -60,6 +60,7 @@ Lee el fichero que corresponda antes de responder sobre atributos concretos, val
60
60
  - [css/dinamicos-cascada-y-componentes.md](references/css/dinamicos-cascada-y-componentes.md) — `##FLD_CAMPO##`, cascada de dispositivo, `strict-mode`, animaciones y tokens, gráficos, calendario y mapa
61
61
  - [css/patrones-material-y-temas.md](references/css/patrones-material-y-temas.md) — Patrones Material (header/body/footer, botones, inputs, tarjetas, FAB, toolbar, item de lista), temas light/dark y un `default.css` + `colors.css` completos y comentados
62
62
  - [css/buenas-practicas-y-parser.md](references/css/buenas-practicas-y-parser.md) — Buenas prácticas, anti-patrones, checklist de validación y detalle de las funciones del parser (`@import`, variables, `calc()`, `!important`, `!default`, `@extend`, modo estricto)
63
+ - [css/de-una-maqueta-a-xone.md](references/css/de-una-maqueta-a-xone.md) — De una maqueta HTML de Stitch (Tailwind) a XOne, clase a clase: forma, bordes, fondos, color, tipografías y composición, con lo MEDIDO en el aparato separado de lo documentado
63
64
 
64
65
  ### Datos e integración
65
66
  - [datos/appdata.md](references/datos/appdata.md) — `appData` completo: colecciones, login/logout, paso de datos entre pantallas, macros globales, SQL directo, detección de dispositivo, `loadIncludeFile` y `loadCssFile`
@@ -211,10 +211,10 @@ Contenedor visual dentro de un grupo. Puede anidarse.
211
211
  |---|---|---|---|
212
212
  | `bgcolor` | color | heredado | Color de fondo del frame. |
213
213
  | `forecolor` | color | heredado | Color de texto del frame. |
214
- | `border` | int (mask) | `0` | Bordes activos: top=1, right=2, bottom=4, left=8. Sumar para combinar. |
215
- | `border-color` | color | — | Color del borde. |
216
- | `border-width` | medida | `0` | Grosor del borde. |
217
- | `border-corner-radius` | medida | `0` | Radio de esquinas redondeadas. |
214
+ | `border` | int (mask) | `0` | Bordes activos: top=1, right=2, bottom=4, left=8. **Medido en Android: en un `<frame>` no pinta borde** (ni `border="15"` ni con `border-width`/`border-color`); el borde de un frame es `framebox="true"` + `forecolor`. |
215
+ | `border-color` | color | — | Color del borde. En un frame no pinta (ver `border`). |
216
+ | `border-width` | medida | `0` | Grosor del borde. En un frame no pinta (ver `border`). |
217
+ | `border-corner-radius` | medida | `0` | Radio de esquinas redondeadas, en píxeles del aparato. **En un frame sale al DOBLE** que en un botón (`"20"` → unos 40 px; en un botón, unos 20): medido en Android. |
218
218
  | `tmargin` / `bmargin` / `lmargin` / `rmargin` | medida | `0` | Margenes externos. |
219
219
  | `tpadding` / `bpadding` / `lpadding` / `rpadding` | medida | `0` | Padding interno. |
220
220
  | `class` | string | `""` | Clase CSS. |
@@ -422,10 +422,10 @@ XOne posiciona los elementos en **flujo lineal de arriba a abajo**: por defecto
422
422
  | `bgcolor` | color | Color de fondo | `bgcolor="#FFFFFF"` |
423
423
  | `forecolor` | color | Color del borde (en combinacion con `framebox`) | `forecolor="#666666"` |
424
424
  | `elevation` | integer | Elevacion / sombra (estilo Material Design) | `elevation="5"` |
425
- | `border-corner-radius` | string | Radio de esquinas redondeadas | `border-corner-radius="10"` |
426
- | `framebox` | boolean | Muestra un borde visible alrededor del frame | `framebox="true"` |
427
- | `border` | boolean | Muestra el borde del frame | `border="true"` |
428
- | `border-width` | integer | Ancho del borde en puntos | `border-width="2"` |
425
+ | `border-corner-radius` | string | Radio de esquinas redondeadas. **Medido en Android: en un frame sale al DOBLE en píxeles del aparato** (`"20"` → unos 40 px), sin depender de la densidad ni de la resolución de la app | `border-corner-radius="10"` |
426
+ | `framebox` | boolean | Muestra un borde visible alrededor del frame. **Es lo que pinta el borde de un frame** (medido), con `forecolor` de color | `framebox="true"` |
427
+ | `border` | boolean | Muestra el borde del frame. **Medido en Android: en un frame NO pinta** (tampoco como máscara `border="15"`): usa `framebox` | `border="true"` |
428
+ | `border-width` | integer | Ancho del borde en puntos. En un frame no pinta (ver `border`) | `border-width="2"` |
429
429
  | `border-left` | boolean | Muestra solo el borde izquierdo | `border-left="false"` |
430
430
  | `border-top` | boolean | Muestra solo el borde superior | `border-top="false"` |
431
431
  | `border-right` | boolean | Muestra solo el borde derecho | `border-right="false"` |
@@ -0,0 +1,147 @@
1
+ /**
2
+ * La GEOMETRÍA de una captura: el árbol de controles y las barras del sistema EN EL MISMO MOMENTO,
3
+ * guardados al lado de la imagen con su nombre (`calc_08.jpg` → `calc_08.geometria.json`).
4
+ *
5
+ * POR QUÉ, y por qué AQUÍ. El crítico visual del host (`xone_critica_visual`) compara tecla a tecla
6
+ * —qué falta, en qué fila, qué tamaño, qué queda tapado— leyendo cajas, no píxeles. Para eso necesita
7
+ * el árbol de la MISMA pantalla que la captura: emparejarlo después por la fecha de los ficheros ya
8
+ * cruzó una vez el árbol de una ronda con la captura de otra, y una cifra «exacta» de la ronda
9
+ * equivocada es peor que ninguna. Sacarlo en el mismo comando que la captura lo hace imposible.
10
+ *
11
+ * Las barras salen de `dumpsys window` (`InsetsSource … type=navigationBars frame=[0,2337][1080,2400]`):
12
+ * con Android dibujando de borde a borde, un control puede quedar DEBAJO de la barra de navegación, y
13
+ * su alto cambia según el aparato y el modo de navegación (medido en un pixel8: 63 px con gestos), así
14
+ * que no se adivina.
15
+ *
16
+ * NUNCA hace fallar la captura: si no se puede, se dice por stderr y la captura queda sin geometría.
17
+ * Va a `$XONECODE_HOTSWAP` (andamio, como los volcados del canal), no a artefactos: no es algo que mirar.
18
+ */
19
+ import { execFileSync } from "node:child_process";
20
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
21
+ import { tmpdir } from "node:os";
22
+ import { join } from "node:path";
23
+ import { NODE, rutaDeAdb } from "./dispositivo.mjs";
24
+
25
+ /** El nombre del fichero de geometría de una captura: su nombre sin extensión + `.geometria.json`. */
26
+ export function nombreDeGeometria(nombreDeCaptura) {
27
+ return `${nombreDeCaptura.replace(/\.[A-Za-z0-9]{2,4}$/, "")}.geometria.json`;
28
+ }
29
+
30
+ /** Las barras del sistema de la salida de `dumpsys window`, o `undefined` si no salen. Pura. */
31
+ export function barrasDeDumpsys(salida) {
32
+ const frame = (tipo) => {
33
+ const m = new RegExp(`type=${tipo} frame=\\[(-?\\d+),(-?\\d+)\\]\\[(-?\\d+),(-?\\d+)\\] visible=true`).exec(String(salida));
34
+ return m === null ? undefined : { izquierda: +m[1], arriba: +m[2], derecha: +m[3], abajo: +m[4] };
35
+ };
36
+ const estado = frame("statusBars");
37
+ const navegacion = frame("navigationBars");
38
+ if (estado === undefined && navegacion === undefined) return undefined;
39
+ return { ...(estado === undefined ? {} : { estado }), ...(navegacion === undefined ? {} : { navegacion }) };
40
+ }
41
+
42
+ /** Hasta dónde llegan los controles del árbol (el borde derecho y el inferior más lejanos). Pura. */
43
+ export function extensionDelArbol(arbol) {
44
+ let ancho = 0;
45
+ let alto = 0;
46
+ const visitar = (n) => {
47
+ if (Array.isArray(n)) return n.forEach(visitar);
48
+ if (n === null || typeof n !== "object") return;
49
+ const b = n.bounds;
50
+ if (b && typeof b.left === "number" && typeof b.width === "number" && typeof b.top === "number" && typeof b.height === "number") {
51
+ ancho = Math.max(ancho, b.left + b.width);
52
+ alto = Math.max(alto, b.top + b.height);
53
+ }
54
+ for (const v of Object.values(n)) if (v !== null && typeof v === "object") visitar(v);
55
+ };
56
+ visitar(arbol);
57
+ return { ancho, alto };
58
+ }
59
+
60
+ /**
61
+ * La imagen es de UN CONTROL y no de la pantalla entera (`xone-hotswap shot name=…`): es claramente más pequeña que
62
+ * lo que ocupa el árbol. Su geometría no le corresponde —las cajas son de la pantalla— y el crítico mediría píxeles
63
+ * que no son (medido: una foto del visor salía con siete textos «recortados»). Pura.
64
+ */
65
+ export function esCapturaDeUnControl(pantalla, arbol, barras) {
66
+ // La PANTALLA la dice la barra de navegación (llega al borde inferior y ocupa el ancho entero). Sin ella, lo que
67
+ // ocupa el árbol, que puede pasarse: medido, un pie flotante de la app llegaba a y=2542 en una pantalla de 2400, y
68
+ // la captura completa parecía la de un control.
69
+ const n = barras?.navegacion;
70
+ const e = n !== undefined ? { ancho: n.derecha, alto: n.abajo } : extensionDelArbol(arbol);
71
+ return pantalla !== undefined && e.alto > 0 && (pantalla.alto < e.alto * 0.95 || pantalla.ancho < e.ancho * 0.95);
72
+ }
73
+
74
+ /** El ancho y el alto de un PNG o un JPEG, leídos de su cabecera. `undefined` si no se sabe. Pura. */
75
+ export function medidasDeImagen(bytes) {
76
+ if (bytes.length >= 24 && bytes[0] === 0x89 && bytes[1] === 0x50) return { ancho: bytes.readUInt32BE(16), alto: bytes.readUInt32BE(20) };
77
+ if (bytes[0] === 0xff && bytes[1] === 0xd8) {
78
+ let i = 2;
79
+ while (i + 9 < bytes.length) {
80
+ if (bytes[i] !== 0xff) return undefined;
81
+ const marca = bytes[i + 1];
82
+ const largo = bytes.readUInt16BE(i + 2);
83
+ if (marca >= 0xc0 && marca <= 0xc3) return { alto: bytes.readUInt16BE(i + 5), ancho: bytes.readUInt16BE(i + 7) };
84
+ i += 2 + largo;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ /**
91
+ * Guarda `<captura>.geometria.json` en `$XONECODE_HOTSWAP`. Devuelve el nombre, o `undefined` y lo dice.
92
+ * `bytes` son los de la captura (para su tamaño); `serie`, el aparato.
93
+ */
94
+ export function guardarGeometria({ nombreDeCaptura, bytes, serie, entorno = process.env }) {
95
+ const destino = entorno.XONECODE_HOTSWAP;
96
+ if (!destino) {
97
+ process.stderr.write("(sin XONECODE_HOTSWAP: la captura queda sin geometría para el crítico)\n");
98
+ return undefined;
99
+ }
100
+ let arbol;
101
+ const temporal = mkdtempSync(join(tmpdir(), "xonecode-geometria-"));
102
+ try {
103
+ // El cliente guarda un árbol grande en `$XONECODE_HOTSWAP`: se le da uno PROPIO para saber cuál es.
104
+ const cliente = join(import.meta.dirname, "..", "scripts", "xone-hotswap");
105
+ execFileSync(NODE, [cliente, "getAllElements", "format=xone", ...(serie ? ["--serie", serie] : [])], {
106
+ encoding: "utf8",
107
+ env: { ...entorno, XONECODE_HOTSWAP: temporal },
108
+ stdio: ["ignore", "pipe", "ignore"],
109
+ timeout: 30_000,
110
+ });
111
+ const fichero = readdirSync(temporal).find((n) => n.endsWith(".json"));
112
+ if (fichero !== undefined) arbol = JSON.parse(readFileSync(join(temporal, fichero), "utf8"));
113
+ } catch {
114
+ // Sin canal o sin app: se queda sin árbol, y se dice abajo.
115
+ } finally {
116
+ rmSync(temporal, { recursive: true, force: true });
117
+ }
118
+ if (arbol === undefined) {
119
+ process.stderr.write("(no pude leer el árbol de controles: la captura queda sin geometría para el crítico)\n");
120
+ return undefined;
121
+ }
122
+ let barras;
123
+ try {
124
+ barras = barrasDeDumpsys(
125
+ execFileSync(rutaDeAdb(entorno), [...(serie ? ["-s", serie] : []), "shell", "dumpsys", "window"], {
126
+ encoding: "utf8",
127
+ maxBuffer: 64 * 1024 * 1024,
128
+ stdio: ["ignore", "pipe", "ignore"],
129
+ timeout: 15_000,
130
+ })
131
+ );
132
+ } catch {
133
+ // Sin barras, el crítico no puede decir qué queda tapado: lo dirá él.
134
+ }
135
+ const nombre = nombreDeGeometria(nombreDeCaptura);
136
+ const pantalla = medidasDeImagen(bytes);
137
+ if (esCapturaDeUnControl(pantalla, arbol, barras)) {
138
+ process.stderr.write("(captura de un control, no de la pantalla entera: sin geometría para el crítico)\n");
139
+ return undefined;
140
+ }
141
+ mkdirSync(destino, { recursive: true });
142
+ writeFileSync(
143
+ join(destino, nombre),
144
+ JSON.stringify({ v: 1, captura: nombreDeCaptura, en: new Date().toISOString(), ...(pantalla ? { pantalla } : {}), ...(barras ? { barras } : {}), arbol })
145
+ );
146
+ return nombre;
147
+ }
@@ -26,6 +26,7 @@ import { execFileSync } from "node:child_process";
26
26
  import { writeFileSync } from "node:fs";
27
27
  import { join, basename } from "node:path";
28
28
  import { serieAndroid, rutaDeAdb, NODE } from "../lib/dispositivo.mjs";
29
+ import { guardarGeometria } from "../lib/geometria.mjs";
29
30
 
30
31
  const args = process.argv.slice(2);
31
32
  if (args.includes("--help") || args.includes("-h")) {
@@ -183,3 +184,8 @@ try {
183
184
  }
184
185
 
185
186
  console.log(`captura guardada como ${nombre} (${bytes.length} bytes). Es de la vía nativa: enseña lo que el SISTEMA pinta, no lo que la app host dice.`);
187
+
188
+ // Y su GEOMETRÍA, en el mismo momento: el árbol de controles y las barras del sistema, para que el
189
+ // crítico visual compare tecla a tecla con la pantalla que ESTA captura enseña (`../lib/geometria.mjs`).
190
+ const geometria = guardarGeometria({ nombreDeCaptura: nombre, bytes, serie });
191
+ if (geometria !== undefined) console.log(`(con su geometría para el crítico: /hotswap/${geometria})`);
@@ -55,6 +55,7 @@ process.on("warning", (aviso) => {
55
55
 
56
56
  import { mkdirSync, writeFileSync } from "node:fs";
57
57
  import { join } from "node:path";
58
+ import { guardarGeometria } from "../lib/geometria.mjs";
58
59
  import { dispositivoDeLaSesion, puertoConMotivo, retirarOpciones, serieAndroid, PUERTO_DEL_HOTSWAP } from "../lib/dispositivo.mjs";
59
60
 
60
61
  /**
@@ -320,6 +321,13 @@ function sinBinarios(respuesta) {
320
321
  const bytes = imagen === undefined ? Buffer.from(valor, "utf8") : Buffer.from(valor, "base64");
321
322
  mkdirSync(destino, { recursive: true });
322
323
  writeFileSync(join(destino, nombre), bytes);
324
+ // Una captura se guarda con su GEOMETRÍA (árbol de controles y barras del sistema) del mismo
325
+ // momento, para el crítico visual (`../lib/geometria.mjs`). La llamada de dentro pide un árbol, no
326
+ // una imagen, así que no vuelve a entrar aquí.
327
+ if (imagen !== undefined) {
328
+ const geometria = guardarGeometria({ nombreDeCaptura: nombre, bytes, serie: seriePedida });
329
+ if (geometria !== undefined) process.stderr.write(`(con su geometría para el crítico: /hotswap/${geometria})\n`);
330
+ }
323
331
  // Con la ruta VIRTUAL delante, que es la que se puede volver a abrir. Una captura sigue
324
332
  // diciendo `/artefactos/`, que es donde está y donde la tarjeta del hilo la enseña.
325
333
  const virtual = imagen === undefined ? RUTA_VIRTUAL : "/artefactos/";
@@ -198,6 +198,21 @@ if (cambiados.length === 0) {
198
198
  console.log("nada que recargar: los ficheros del proyecto ya son los del aparato.");
199
199
  if (otros.length > 0 && posicionales.length === 0) {
200
200
  console.log(`(no se comparan ${otros.length} fichero(s) que no son xne/js/css/ini/xml: iconos, bd, files…)`);
201
+ // Una FUENTE no se queda en «no se compara»: un `fontname` que no está en el aparato saca un diálogo de error y el
202
+ // control no se pinta (medido). Se dice con el remedio.
203
+ // Solo las NUEVAS o cambiadas (git: cada turno commitea lo que dejó, así que una recién añadida está sin
204
+ // commitear); sin git no se dice nada, mejor que avisar de todas en cada recarga.
205
+ let fuentes = [];
206
+ try {
207
+ fuentes = execFileSync("git", ["status", "--porcelain", "--", "fonts"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
208
+ .split("\n")
209
+ .filter((l) => l.trim() !== "");
210
+ } catch {
211
+ fuentes = [];
212
+ }
213
+ if (fuentes.length > 0) {
214
+ console.log(`(hay ${fuentes.length} fuente(s) nueva(s) o cambiada(s) en fonts/ y la recarga NO las sube: despliega entero con xone-desplegar-android)`);
215
+ }
201
216
  }
202
217
  process.exit(0);
203
218
  }