@cerca.red/mapa-estilos 0.2.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.
@@ -0,0 +1,62 @@
1
+ import type { EspecificacionDeCapa } from "./tipos.ts";
2
+ /**
3
+ * El orden de capas, escrito como datos.
4
+ *
5
+ * Está aquí y no repartido en llamadas a `addLayer` a propósito: si el orden
6
+ * fuera el efecto de en qué secuencia alguien añadió las capas, no habría
7
+ * nada que leer ni nada que comprobar, y la primera capa insertada en el
8
+ * sitio equivocado pasaría desapercibida hasta que alguien mirara el mapa.
9
+ * Así el orden se afirma en una prueba contra esta lista.
10
+ */
11
+ /**
12
+ * Las anclas: capas vacías con identificador estable que solo existen para
13
+ * ser un punto de referencia.
14
+ *
15
+ * **Cada ancla marca el techo de su franja**, y la regla de uso es una sola:
16
+ * para poner una capa en la franja X, se inserta *antes* de `ANCLAS.X`
17
+ * (`map.addLayer(capa, ANCLAS.puntos)`). Sin ellas, quien añade una capa
18
+ * tiene que nombrar una capa de Protomaps para insertarse encima o debajo —y
19
+ * esos identificadores cambian con cada versión del upstream, sin aviso y sin
20
+ * que nada se ponga rojo—.
21
+ */
22
+ export declare const ANCLAS: {
23
+ readonly areas: "cerca-ancla-areas";
24
+ readonly lineas: "cerca-ancla-lineas";
25
+ readonly puntos: "cerca-ancla-puntos";
26
+ readonly seleccion: "cerca-ancla-seleccion";
27
+ };
28
+ export type Ancla = (typeof ANCLAS)[keyof typeof ANCLAS];
29
+ /** Qué pone cada franja dentro del estilo. */
30
+ export type ContenidoDeFranja =
31
+ /** Las capas del mapa base sin rótulos, tal como las da el upstream. */
32
+ "mapa-base"
33
+ /** Los rótulos del mapa base, en el idioma pedido. */
34
+ | "rotulos"
35
+ /** Un ancla vacía: el sitio donde `packages/mapa` inserta lo de Cerca. */
36
+ | "cerca";
37
+ export interface Franja {
38
+ readonly nombre: string;
39
+ readonly contenido: ContenidoDeFranja;
40
+ /** Solo en las franjas de Cerca. */
41
+ readonly ancla?: Ancla;
42
+ readonly porQue: string;
43
+ }
44
+ /**
45
+ * De abajo hacia arriba.
46
+ *
47
+ * Los rótulos del mapa base van **en medio**, y esa es la decisión
48
+ * cartográfica del documento: el nombre de la calle se tiene que seguir
49
+ * leyendo por debajo de un polígono de cobertura, y los alfileres tienen que
50
+ * quedar por encima de todo. Un mapa que tapa los nombres de las calles no
51
+ * sirve para lo único que se le pide.
52
+ */
53
+ export declare const ORDEN_DE_CAPAS: readonly Franja[];
54
+ /**
55
+ * Una capa ancla.
56
+ *
57
+ * Es un `background` con la visibilidad apagada: no pinta nada, no necesita
58
+ * fuente y ocupa un sitio en la lista de capas, que es todo lo que se le
59
+ * pide. El `metadata` deja la marca para que un consumidor pueda distinguir
60
+ * un ancla de una capa de verdad sin comparar contra la lista de nombres.
61
+ */
62
+ export declare function capaAncla(ancla: Ancla): EspecificacionDeCapa;
package/dist/capas.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * El orden de capas, escrito como datos.
3
+ *
4
+ * Está aquí y no repartido en llamadas a `addLayer` a propósito: si el orden
5
+ * fuera el efecto de en qué secuencia alguien añadió las capas, no habría
6
+ * nada que leer ni nada que comprobar, y la primera capa insertada en el
7
+ * sitio equivocado pasaría desapercibida hasta que alguien mirara el mapa.
8
+ * Así el orden se afirma en una prueba contra esta lista.
9
+ */
10
+ /**
11
+ * Las anclas: capas vacías con identificador estable que solo existen para
12
+ * ser un punto de referencia.
13
+ *
14
+ * **Cada ancla marca el techo de su franja**, y la regla de uso es una sola:
15
+ * para poner una capa en la franja X, se inserta *antes* de `ANCLAS.X`
16
+ * (`map.addLayer(capa, ANCLAS.puntos)`). Sin ellas, quien añade una capa
17
+ * tiene que nombrar una capa de Protomaps para insertarse encima o debajo —y
18
+ * esos identificadores cambian con cada versión del upstream, sin aviso y sin
19
+ * que nada se ponga rojo—.
20
+ */
21
+ export const ANCLAS = {
22
+ areas: "cerca-ancla-areas",
23
+ lineas: "cerca-ancla-lineas",
24
+ puntos: "cerca-ancla-puntos",
25
+ seleccion: "cerca-ancla-seleccion",
26
+ };
27
+ /**
28
+ * De abajo hacia arriba.
29
+ *
30
+ * Los rótulos del mapa base van **en medio**, y esa es la decisión
31
+ * cartográfica del documento: el nombre de la calle se tiene que seguir
32
+ * leyendo por debajo de un polígono de cobertura, y los alfileres tienen que
33
+ * quedar por encima de todo. Un mapa que tapa los nombres de las calles no
34
+ * sirve para lo único que se le pide.
35
+ */
36
+ export const ORDEN_DE_CAPAS = [
37
+ {
38
+ nombre: "mapa base",
39
+ contenido: "mapa-base",
40
+ porQue: "El contexto: suelo, agua, usos del suelo, edificios y vías. Va abajo " +
41
+ "porque todo lo de Cerca se dibuja sobre él.",
42
+ },
43
+ {
44
+ nombre: "áreas de Cerca",
45
+ contenido: "cerca",
46
+ ancla: ANCLAS.areas,
47
+ porQue: "Zonas de cobertura, isócronas y halos de ubicación aproximada. Son " +
48
+ "rellenos grandes y semitransparentes: encima del mapa base, pero " +
49
+ "debajo de los rótulos, que si no taparían.",
50
+ },
51
+ {
52
+ nombre: "líneas de Cerca",
53
+ contenido: "cerca",
54
+ ancla: ANCLAS.lineas,
55
+ porQue: "Rutas y trazas. Encima de las áreas para que una ruta que cruza una " +
56
+ "zona de cobertura se siga viendo.",
57
+ },
58
+ {
59
+ nombre: "rótulos del mapa base",
60
+ contenido: "rotulos",
61
+ porQue: "En medio, a propósito. El nombre de la calle tiene que leerse por " +
62
+ "debajo de un polígono de cobertura y por encima de él no cabría sin " +
63
+ "tapar los alfileres.",
64
+ },
65
+ {
66
+ nombre: "puntos de Cerca",
67
+ contenido: "cerca",
68
+ ancla: ANCLAS.puntos,
69
+ porQue: "Negocios, sedes, vehículos y la ubicación del usuario. Por encima de " +
70
+ "todo el mapa base: son lo que se toca.",
71
+ },
72
+ {
73
+ nombre: "selección",
74
+ contenido: "cerca",
75
+ ancla: ANCLAS.seleccion,
76
+ porQue: "Lo seleccionado va en su propia franja y no en un estado de entidad " +
77
+ "de la capa de puntos, porque la clave de ordenación de símbolos de " +
78
+ "MapLibre no lee el estado de entidad: un alfiler seleccionado no " +
79
+ "puede subir por encima de sus vecinos sin una capa aparte.",
80
+ },
81
+ ];
82
+ /**
83
+ * Una capa ancla.
84
+ *
85
+ * Es un `background` con la visibilidad apagada: no pinta nada, no necesita
86
+ * fuente y ocupa un sitio en la lista de capas, que es todo lo que se le
87
+ * pide. El `metadata` deja la marca para que un consumidor pueda distinguir
88
+ * un ancla de una capa de verdad sin comparar contra la lista de nombres.
89
+ */
90
+ export function capaAncla(ancla) {
91
+ return {
92
+ id: ancla,
93
+ type: "background",
94
+ layout: { visibility: "none" },
95
+ metadata: { "cerca:ancla": true },
96
+ };
97
+ }
98
+ //# sourceMappingURL=capas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capas.js","sourceRoot":"","sources":["../src/capas.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,mBAAmB;IAC1B,MAAM,EAAE,oBAAoB;IAC5B,MAAM,EAAE,oBAAoB;IAC5B,SAAS,EAAE,uBAAuB;CAC1B,CAAC;AAqBX;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C;QACE,MAAM,EAAE,WAAW;QACnB,SAAS,EAAE,WAAW;QACtB,MAAM,EACJ,uEAAuE;YACvE,6CAA6C;KAChD;IACD;QACE,MAAM,EAAE,gBAAgB;QACxB,SAAS,EAAE,OAAO;QAClB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EACJ,qEAAqE;YACrE,mEAAmE;YACnE,4CAA4C;KAC/C;IACD;QACE,MAAM,EAAE,iBAAiB;QACzB,SAAS,EAAE,OAAO;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,MAAM,EACJ,sEAAsE;YACtE,mCAAmC;KACtC;IACD;QACE,MAAM,EAAE,uBAAuB;QAC/B,SAAS,EAAE,SAAS;QACpB,MAAM,EACJ,oEAAoE;YACpE,sEAAsE;YACtE,sBAAsB;KACzB;IACD;QACE,MAAM,EAAE,iBAAiB;QACzB,SAAS,EAAE,OAAO;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,MAAM,EACJ,uEAAuE;YACvE,wCAAwC;KAC3C;IACD;QACE,MAAM,EAAE,WAAW;QACnB,SAAS,EAAE,OAAO;QAClB,KAAK,EAAE,MAAM,CAAC,SAAS;QACvB,MAAM,EACJ,sEAAsE;YACtE,qEAAqE;YACrE,mEAAmE;YACnE,4DAA4D;KAC/D;CACF,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,OAAO;QACL,EAAE,EAAE,KAAK;QACT,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;QAC9B,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE;KAClC,CAAC;AACJ,CAAC","sourcesContent":["import type { EspecificacionDeCapa } from \"./tipos.ts\";\n\n/**\n * El orden de capas, escrito como datos.\n *\n * Está aquí y no repartido en llamadas a `addLayer` a propósito: si el orden\n * fuera el efecto de en qué secuencia alguien añadió las capas, no habría\n * nada que leer ni nada que comprobar, y la primera capa insertada en el\n * sitio equivocado pasaría desapercibida hasta que alguien mirara el mapa.\n * Así el orden se afirma en una prueba contra esta lista.\n */\n\n/**\n * Las anclas: capas vacías con identificador estable que solo existen para\n * ser un punto de referencia.\n *\n * **Cada ancla marca el techo de su franja**, y la regla de uso es una sola:\n * para poner una capa en la franja X, se inserta *antes* de `ANCLAS.X`\n * (`map.addLayer(capa, ANCLAS.puntos)`). Sin ellas, quien añade una capa\n * tiene que nombrar una capa de Protomaps para insertarse encima o debajo —y\n * esos identificadores cambian con cada versión del upstream, sin aviso y sin\n * que nada se ponga rojo—.\n */\nexport const ANCLAS = {\n areas: \"cerca-ancla-areas\",\n lineas: \"cerca-ancla-lineas\",\n puntos: \"cerca-ancla-puntos\",\n seleccion: \"cerca-ancla-seleccion\",\n} as const;\n\nexport type Ancla = (typeof ANCLAS)[keyof typeof ANCLAS];\n\n/** Qué pone cada franja dentro del estilo. */\nexport type ContenidoDeFranja =\n /** Las capas del mapa base sin rótulos, tal como las da el upstream. */\n | \"mapa-base\"\n /** Los rótulos del mapa base, en el idioma pedido. */\n | \"rotulos\"\n /** Un ancla vacía: el sitio donde `packages/mapa` inserta lo de Cerca. */\n | \"cerca\";\n\nexport interface Franja {\n readonly nombre: string;\n readonly contenido: ContenidoDeFranja;\n /** Solo en las franjas de Cerca. */\n readonly ancla?: Ancla;\n readonly porQue: string;\n}\n\n/**\n * De abajo hacia arriba.\n *\n * Los rótulos del mapa base van **en medio**, y esa es la decisión\n * cartográfica del documento: el nombre de la calle se tiene que seguir\n * leyendo por debajo de un polígono de cobertura, y los alfileres tienen que\n * quedar por encima de todo. Un mapa que tapa los nombres de las calles no\n * sirve para lo único que se le pide.\n */\nexport const ORDEN_DE_CAPAS: readonly Franja[] = [\n {\n nombre: \"mapa base\",\n contenido: \"mapa-base\",\n porQue:\n \"El contexto: suelo, agua, usos del suelo, edificios y vías. Va abajo \" +\n \"porque todo lo de Cerca se dibuja sobre él.\",\n },\n {\n nombre: \"áreas de Cerca\",\n contenido: \"cerca\",\n ancla: ANCLAS.areas,\n porQue:\n \"Zonas de cobertura, isócronas y halos de ubicación aproximada. Son \" +\n \"rellenos grandes y semitransparentes: encima del mapa base, pero \" +\n \"debajo de los rótulos, que si no taparían.\",\n },\n {\n nombre: \"líneas de Cerca\",\n contenido: \"cerca\",\n ancla: ANCLAS.lineas,\n porQue:\n \"Rutas y trazas. Encima de las áreas para que una ruta que cruza una \" +\n \"zona de cobertura se siga viendo.\",\n },\n {\n nombre: \"rótulos del mapa base\",\n contenido: \"rotulos\",\n porQue:\n \"En medio, a propósito. El nombre de la calle tiene que leerse por \" +\n \"debajo de un polígono de cobertura y por encima de él no cabría sin \" +\n \"tapar los alfileres.\",\n },\n {\n nombre: \"puntos de Cerca\",\n contenido: \"cerca\",\n ancla: ANCLAS.puntos,\n porQue:\n \"Negocios, sedes, vehículos y la ubicación del usuario. Por encima de \" +\n \"todo el mapa base: son lo que se toca.\",\n },\n {\n nombre: \"selección\",\n contenido: \"cerca\",\n ancla: ANCLAS.seleccion,\n porQue:\n \"Lo seleccionado va en su propia franja y no en un estado de entidad \" +\n \"de la capa de puntos, porque la clave de ordenación de símbolos de \" +\n \"MapLibre no lee el estado de entidad: un alfiler seleccionado no \" +\n \"puede subir por encima de sus vecinos sin una capa aparte.\",\n },\n];\n\n/**\n * Una capa ancla.\n *\n * Es un `background` con la visibilidad apagada: no pinta nada, no necesita\n * fuente y ocupa un sitio en la lista de capas, que es todo lo que se le\n * pide. El `metadata` deja la marca para que un consumidor pueda distinguir\n * un ancla de una capa de verdad sin comparar contra la lista de nombres.\n */\nexport function capaAncla(ancla: Ancla): EspecificacionDeCapa {\n return {\n id: ancla,\n type: \"background\",\n layout: { visibility: \"none\" },\n metadata: { \"cerca:ancla\": true },\n };\n}\n"]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Contraste WCAG 2.1.
3
+ *
4
+ * **Es una copia de `Cerca-PWA/packages/ui/src/contraste.ts`** (leída el
5
+ * 2026-09-07), y la copia es forzada, no cómoda: `@cerca/ui` es privado y
6
+ * vive en otro repositorio, el CI de este solo descarga este repositorio, y
7
+ * la comprobación de AA tiene que correr aquí —es lo único que impide que el
8
+ * sabor de Cerca vuelva a los grises ilegibles del upstream—.
9
+ *
10
+ * Lo que sí se puede guardar desde aquí, y se guarda en `contraste.test.ts`,
11
+ * es que estas tres funciones sigan implementando **el estándar**: la fórmula
12
+ * de WCAG 2.1 tiene valores de referencia conocidos (blanco contra negro es
13
+ * 21, un color contra sí mismo es 1) y contra eso se comprueba. Si algún día
14
+ * las dos copias divergieran, la que estuviera mal sería la que dejara de
15
+ * cumplir el estándar, no la que se apartara de la otra.
16
+ *
17
+ * Ver `tokens.ts` para por qué no hay una guarda de deriva entre repositorios
18
+ * y cuál es la salida escrita en ADR-027.
19
+ */
20
+ /** Luminancia relativa de un color `#rrggbb` según WCAG 2.1. */
21
+ export declare function luminancia(hex: string): number;
22
+ /** Razón de contraste entre dos colores, de 1 (nulo) a 21 (máximo). */
23
+ export declare function contraste(unColor: string, otroColor: string): number;
24
+ /** AA pide 4.5 para texto normal y 3 para texto grande (≥ 24 px o ≥ 18.66 px en negrita). */
25
+ export declare function cumpleAA(fondo: string, frente: string, textoGrande?: boolean): boolean;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Contraste WCAG 2.1.
3
+ *
4
+ * **Es una copia de `Cerca-PWA/packages/ui/src/contraste.ts`** (leída el
5
+ * 2026-09-07), y la copia es forzada, no cómoda: `@cerca/ui` es privado y
6
+ * vive en otro repositorio, el CI de este solo descarga este repositorio, y
7
+ * la comprobación de AA tiene que correr aquí —es lo único que impide que el
8
+ * sabor de Cerca vuelva a los grises ilegibles del upstream—.
9
+ *
10
+ * Lo que sí se puede guardar desde aquí, y se guarda en `contraste.test.ts`,
11
+ * es que estas tres funciones sigan implementando **el estándar**: la fórmula
12
+ * de WCAG 2.1 tiene valores de referencia conocidos (blanco contra negro es
13
+ * 21, un color contra sí mismo es 1) y contra eso se comprueba. Si algún día
14
+ * las dos copias divergieran, la que estuviera mal sería la que dejara de
15
+ * cumplir el estándar, no la que se apartara de la otra.
16
+ *
17
+ * Ver `tokens.ts` para por qué no hay una guarda de deriva entre repositorios
18
+ * y cuál es la salida escrita en ADR-027.
19
+ */
20
+ /** Luminancia relativa de un color `#rrggbb` según WCAG 2.1. */
21
+ export function luminancia(hex) {
22
+ const canales = descomponer(hex).map((valor) => {
23
+ const s = valor / 255;
24
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
25
+ });
26
+ const [r, g, b] = canales;
27
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
28
+ }
29
+ /** Razón de contraste entre dos colores, de 1 (nulo) a 21 (máximo). */
30
+ export function contraste(unColor, otroColor) {
31
+ const a = luminancia(unColor);
32
+ const b = luminancia(otroColor);
33
+ const claro = Math.max(a, b);
34
+ const oscuro = Math.min(a, b);
35
+ return (claro + 0.05) / (oscuro + 0.05);
36
+ }
37
+ /** AA pide 4.5 para texto normal y 3 para texto grande (≥ 24 px o ≥ 18.66 px en negrita). */
38
+ export function cumpleAA(fondo, frente, textoGrande = false) {
39
+ return contraste(fondo, frente) >= (textoGrande ? 3 : 4.5);
40
+ }
41
+ function descomponer(hex) {
42
+ const limpio = hex.replace("#", "").trim();
43
+ if (!/^[0-9a-fA-F]{6}$/.test(limpio)) {
44
+ throw new Error(`Color hexadecimal inválido: ${hex}`);
45
+ }
46
+ return [
47
+ Number.parseInt(limpio.slice(0, 2), 16),
48
+ Number.parseInt(limpio.slice(2, 4), 16),
49
+ Number.parseInt(limpio.slice(4, 6), 16),
50
+ ];
51
+ }
52
+ //# sourceMappingURL=contraste.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contraste.js","sourceRoot":"","sources":["../src/contraste.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,gEAAgE;AAChE,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC7C,MAAM,CAAC,GAAG,KAAK,GAAG,GAAG,CAAC;QACtB,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,OAAmC,CAAC;IACtD,OAAO,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,SAAiB;IAC1D,MAAM,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,QAAQ,CACtB,KAAa,EACb,MAAc,EACd,WAAW,GAAG,KAAK;IAEnB,OAAO,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,OAAO;QACL,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;KACxC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Contraste WCAG 2.1.\n *\n * **Es una copia de `Cerca-PWA/packages/ui/src/contraste.ts`** (leída el\n * 2026-09-07), y la copia es forzada, no cómoda: `@cerca/ui` es privado y\n * vive en otro repositorio, el CI de este solo descarga este repositorio, y\n * la comprobación de AA tiene que correr aquí —es lo único que impide que el\n * sabor de Cerca vuelva a los grises ilegibles del upstream—.\n *\n * Lo que sí se puede guardar desde aquí, y se guarda en `contraste.test.ts`,\n * es que estas tres funciones sigan implementando **el estándar**: la fórmula\n * de WCAG 2.1 tiene valores de referencia conocidos (blanco contra negro es\n * 21, un color contra sí mismo es 1) y contra eso se comprueba. Si algún día\n * las dos copias divergieran, la que estuviera mal sería la que dejara de\n * cumplir el estándar, no la que se apartara de la otra.\n *\n * Ver `tokens.ts` para por qué no hay una guarda de deriva entre repositorios\n * y cuál es la salida escrita en ADR-027.\n */\n\n/** Luminancia relativa de un color `#rrggbb` según WCAG 2.1. */\nexport function luminancia(hex: string): number {\n const canales = descomponer(hex).map((valor) => {\n const s = valor / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n });\n\n const [r, g, b] = canales as [number, number, number];\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\n/** Razón de contraste entre dos colores, de 1 (nulo) a 21 (máximo). */\nexport function contraste(unColor: string, otroColor: string): number {\n const a = luminancia(unColor);\n const b = luminancia(otroColor);\n const claro = Math.max(a, b);\n const oscuro = Math.min(a, b);\n\n return (claro + 0.05) / (oscuro + 0.05);\n}\n\n/** AA pide 4.5 para texto normal y 3 para texto grande (≥ 24 px o ≥ 18.66 px en negrita). */\nexport function cumpleAA(\n fondo: string,\n frente: string,\n textoGrande = false,\n): boolean {\n return contraste(fondo, frente) >= (textoGrande ? 3 : 4.5);\n}\n\nfunction descomponer(hex: string): [number, number, number] {\n const limpio = hex.replace(\"#\", \"\").trim();\n if (!/^[0-9a-fA-F]{6}$/.test(limpio)) {\n throw new Error(`Color hexadecimal inválido: ${hex}`);\n }\n\n return [\n Number.parseInt(limpio.slice(0, 2), 16),\n Number.parseInt(limpio.slice(2, 4), 16),\n Number.parseInt(limpio.slice(4, 6), 16),\n ];\n}\n"]}
@@ -0,0 +1,64 @@
1
+ import { type Idioma } from "./idiomas.ts";
2
+ import { type Tema } from "./sabor.ts";
3
+ import type { EspecificacionDeEstilo } from "./tipos.ts";
4
+ /**
5
+ * El estilo completo de Cerca, como objeto.
6
+ *
7
+ * Es una función pura de objeto a objeto: se prueba sin navegador y sin GPU,
8
+ * validando el resultado contra el esquema del style spec de MapLibre.
9
+ */
10
+ /** El nombre de la fuente vectorial dentro del estilo. */
11
+ export declare const FUENTE_BASE = "mapa-base";
12
+ export interface OpcionesDeEstilo {
13
+ /**
14
+ * La URL de las teselas: `pmtiles://https://…/colombia/2026-09-07.pmtiles`
15
+ * o el TileJSON equivalente.
16
+ *
17
+ * **Obligatoria y sin valor por defecto, a propósito.** ADR-024 y ADR-027
18
+ * dicen que esta URL sale del manifiesto y de ningún otro sitio: basta con
19
+ * que una superficie la escriba a mano para que promover el puntero a una
20
+ * versión anterior deje de revertir nada ahí. Un valor por defecto sería
21
+ * exactamente esa URL escrita a mano, solo que escondida en un paquete.
22
+ */
23
+ readonly urlDeTeselas: string;
24
+ /**
25
+ * La URL de los glifos, con `{fontstack}` y `{range}`.
26
+ *
27
+ * También obligatoria, y por un motivo distinto: el extremo de glifos es
28
+ * un tercero al que el mapa le pide bytes en cada carga. Quemar aquí el de
29
+ * Protomaps metería una dependencia de red externa dentro de un paquete
30
+ * publicado sin que apareciera en ninguna configuración.
31
+ */
32
+ readonly urlDeGlifos: string;
33
+ /**
34
+ * La URL base del sprite, sin extensión.
35
+ *
36
+ * Opcional, y el coste de no pasarla está medido: sin sprite no se dibujan
37
+ * ni los escudos de carretera (`roads_shields`) ni las flechas de sentido
38
+ * único (`roads_oneway`). MapLibre no falla —avisa por consola
39
+ * (`Image "generic_shield-2char" could not be loaded`) y sigue—.
40
+ *
41
+ * **Y no bloquea la carga del estilo**, que es el diagnóstico fácil y
42
+ * equivocado. Medido el 2026-09-07 en el taller sobre `pereira.pmtiles`,
43
+ * con sprite y sin él: la secuencia es la misma en los dos casos —
44
+ * `map.isStyleLoaded()` devuelve `false` dentro de cada `styledata` y
45
+ * `true` en `idle`—. Quien monte algo sobre `styledata` y solo mire ahí no
46
+ * arranca nunca, y la causa no es la imagen que falta.
47
+ */
48
+ readonly urlDeSprite?: string;
49
+ readonly tema?: Tema;
50
+ readonly idioma?: Idioma;
51
+ }
52
+ /**
53
+ * Construye el estilo.
54
+ *
55
+ * El orden de capas **no** es el efecto de en qué secuencia se llamó a nada:
56
+ * se recorre `ORDEN_DE_CAPAS` y cada franja aporta lo suyo. Cambiar el orden
57
+ * es reordenar esa lista, y hay una prueba que compara las dos cosas.
58
+ *
59
+ * No lleva atribución escrita. La de OpenStreetMap viene dentro del archivo
60
+ * de teselas —`pmtiles show` la lee de sus metadatos— y `new Protocol({
61
+ * metadata: true })` la publica sola. Escribirla aquí sería duplicar un
62
+ * texto legal, que es como una superficie termina sirviendo un mapa sin ella.
63
+ */
64
+ export declare function construirEstilo(opciones: OpcionesDeEstilo): EspecificacionDeEstilo;
package/dist/estilo.js ADDED
@@ -0,0 +1,65 @@
1
+ import { layers } from "@protomaps/basemaps";
2
+ import { capaAncla, ORDEN_DE_CAPAS } from "./capas.js";
3
+ import { IDIOMA_POR_DEFECTO } from "./idiomas.js";
4
+ import { saborDeCerca, TEMA_POR_DEFECTO } from "./sabor.js";
5
+ /**
6
+ * El estilo completo de Cerca, como objeto.
7
+ *
8
+ * Es una función pura de objeto a objeto: se prueba sin navegador y sin GPU,
9
+ * validando el resultado contra el esquema del style spec de MapLibre.
10
+ */
11
+ /** El nombre de la fuente vectorial dentro del estilo. */
12
+ export const FUENTE_BASE = "mapa-base";
13
+ /**
14
+ * Construye el estilo.
15
+ *
16
+ * El orden de capas **no** es el efecto de en qué secuencia se llamó a nada:
17
+ * se recorre `ORDEN_DE_CAPAS` y cada franja aporta lo suyo. Cambiar el orden
18
+ * es reordenar esa lista, y hay una prueba que compara las dos cosas.
19
+ *
20
+ * No lleva atribución escrita. La de OpenStreetMap viene dentro del archivo
21
+ * de teselas —`pmtiles show` la lee de sus metadatos— y `new Protocol({
22
+ * metadata: true })` la publica sola. Escribirla aquí sería duplicar un
23
+ * texto legal, que es como una superficie termina sirviendo un mapa sin ella.
24
+ */
25
+ export function construirEstilo(opciones) {
26
+ const urlDeTeselas = exigirUrl(opciones.urlDeTeselas, "urlDeTeselas");
27
+ const urlDeGlifos = exigirUrl(opciones.urlDeGlifos, "urlDeGlifos");
28
+ const tema = opciones.tema ?? TEMA_POR_DEFECTO;
29
+ const idioma = opciones.idioma ?? IDIOMA_POR_DEFECTO;
30
+ const sabor = saborDeCerca(tema);
31
+ const capas = ORDEN_DE_CAPAS.flatMap((franja) => {
32
+ if (franja.contenido === "mapa-base") {
33
+ return layers(FUENTE_BASE, sabor);
34
+ }
35
+ if (franja.contenido === "rotulos") {
36
+ return layers(FUENTE_BASE, sabor, { labelsOnly: true, lang: idioma });
37
+ }
38
+ return franja.ancla ? [capaAncla(franja.ancla)] : [];
39
+ });
40
+ return {
41
+ version: 8,
42
+ glyphs: urlDeGlifos,
43
+ ...(opciones.urlDeSprite === undefined
44
+ ? {}
45
+ : { sprite: opciones.urlDeSprite }),
46
+ sources: {
47
+ [FUENTE_BASE]: { type: "vector", url: urlDeTeselas },
48
+ },
49
+ layers: capas,
50
+ };
51
+ }
52
+ /**
53
+ * Falla temprano y con el nombre del parámetro.
54
+ *
55
+ * Una cadena vacía llega hasta MapLibre y vuelve como un mapa en blanco sin
56
+ * excepción, que es la peor forma de romperse: no hay nada que buscar.
57
+ */
58
+ function exigirUrl(valor, nombre) {
59
+ if (typeof valor !== "string" || valor.trim() === "") {
60
+ throw new Error(`construirEstilo: falta ${nombre}. No tiene valor por ` +
61
+ `defecto: sale del manifiesto (ADR-024), nunca escrita a mano.`);
62
+ }
63
+ return valor;
64
+ }
65
+ //# sourceMappingURL=estilo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estilo.js","sourceRoot":"","sources":["../src/estilo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAe,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAa,MAAM,YAAY,CAAC;AAGvE;;;;;GAKG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AA8CvC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,QAA0B;IAE1B,MAAM,YAAY,GAAG,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IACtE,MAAM,WAAW,GAAG,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,gBAAgB,CAAC;IAC/C,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,kBAAkB,CAAC;IACrD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEjC,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,EAA0B,EAAE;QACtE,IAAI,MAAM,CAAC,SAAS,KAAK,WAAW,EAAE,CAAC;YACrC,OAAO,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,WAAW;QACnB,GAAG,CAAC,QAAQ,CAAC,WAAW,KAAK,SAAS;YACpC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC;QACrC,OAAO,EAAE;YACP,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE;SACrD;QACD,MAAM,EAAE,KAAK;KACd,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAC,KAAa,EAAE,MAAc;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,0BAA0B,MAAM,uBAAuB;YACrD,+DAA+D,CAClE,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { layers } from \"@protomaps/basemaps\";\n\nimport { capaAncla, ORDEN_DE_CAPAS } from \"./capas.ts\";\nimport { IDIOMA_POR_DEFECTO, type Idioma } from \"./idiomas.ts\";\nimport { saborDeCerca, TEMA_POR_DEFECTO, type Tema } from \"./sabor.ts\";\nimport type { EspecificacionDeCapa, EspecificacionDeEstilo } from \"./tipos.ts\";\n\n/**\n * El estilo completo de Cerca, como objeto.\n *\n * Es una función pura de objeto a objeto: se prueba sin navegador y sin GPU,\n * validando el resultado contra el esquema del style spec de MapLibre.\n */\n\n/** El nombre de la fuente vectorial dentro del estilo. */\nexport const FUENTE_BASE = \"mapa-base\";\n\nexport interface OpcionesDeEstilo {\n /**\n * La URL de las teselas: `pmtiles://https://…/colombia/2026-09-07.pmtiles`\n * o el TileJSON equivalente.\n *\n * **Obligatoria y sin valor por defecto, a propósito.** ADR-024 y ADR-027\n * dicen que esta URL sale del manifiesto y de ningún otro sitio: basta con\n * que una superficie la escriba a mano para que promover el puntero a una\n * versión anterior deje de revertir nada ahí. Un valor por defecto sería\n * exactamente esa URL escrita a mano, solo que escondida en un paquete.\n */\n readonly urlDeTeselas: string;\n\n /**\n * La URL de los glifos, con `{fontstack}` y `{range}`.\n *\n * También obligatoria, y por un motivo distinto: el extremo de glifos es\n * un tercero al que el mapa le pide bytes en cada carga. Quemar aquí el de\n * Protomaps metería una dependencia de red externa dentro de un paquete\n * publicado sin que apareciera en ninguna configuración.\n */\n readonly urlDeGlifos: string;\n\n /**\n * La URL base del sprite, sin extensión.\n *\n * Opcional, y el coste de no pasarla está medido: sin sprite no se dibujan\n * ni los escudos de carretera (`roads_shields`) ni las flechas de sentido\n * único (`roads_oneway`). MapLibre no falla —avisa por consola\n * (`Image \"generic_shield-2char\" could not be loaded`) y sigue—.\n *\n * **Y no bloquea la carga del estilo**, que es el diagnóstico fácil y\n * equivocado. Medido el 2026-09-07 en el taller sobre `pereira.pmtiles`,\n * con sprite y sin él: la secuencia es la misma en los dos casos —\n * `map.isStyleLoaded()` devuelve `false` dentro de cada `styledata` y\n * `true` en `idle`—. Quien monte algo sobre `styledata` y solo mire ahí no\n * arranca nunca, y la causa no es la imagen que falta.\n */\n readonly urlDeSprite?: string;\n\n readonly tema?: Tema;\n readonly idioma?: Idioma;\n}\n\n/**\n * Construye el estilo.\n *\n * El orden de capas **no** es el efecto de en qué secuencia se llamó a nada:\n * se recorre `ORDEN_DE_CAPAS` y cada franja aporta lo suyo. Cambiar el orden\n * es reordenar esa lista, y hay una prueba que compara las dos cosas.\n *\n * No lleva atribución escrita. La de OpenStreetMap viene dentro del archivo\n * de teselas —`pmtiles show` la lee de sus metadatos— y `new Protocol({\n * metadata: true })` la publica sola. Escribirla aquí sería duplicar un\n * texto legal, que es como una superficie termina sirviendo un mapa sin ella.\n */\nexport function construirEstilo(\n opciones: OpcionesDeEstilo,\n): EspecificacionDeEstilo {\n const urlDeTeselas = exigirUrl(opciones.urlDeTeselas, \"urlDeTeselas\");\n const urlDeGlifos = exigirUrl(opciones.urlDeGlifos, \"urlDeGlifos\");\n const tema = opciones.tema ?? TEMA_POR_DEFECTO;\n const idioma = opciones.idioma ?? IDIOMA_POR_DEFECTO;\n const sabor = saborDeCerca(tema);\n\n const capas = ORDEN_DE_CAPAS.flatMap((franja): EspecificacionDeCapa[] => {\n if (franja.contenido === \"mapa-base\") {\n return layers(FUENTE_BASE, sabor);\n }\n if (franja.contenido === \"rotulos\") {\n return layers(FUENTE_BASE, sabor, { labelsOnly: true, lang: idioma });\n }\n return franja.ancla ? [capaAncla(franja.ancla)] : [];\n });\n\n return {\n version: 8,\n glyphs: urlDeGlifos,\n ...(opciones.urlDeSprite === undefined\n ? {}\n : { sprite: opciones.urlDeSprite }),\n sources: {\n [FUENTE_BASE]: { type: \"vector\", url: urlDeTeselas },\n },\n layers: capas,\n };\n}\n\n/**\n * Falla temprano y con el nombre del parámetro.\n *\n * Una cadena vacía llega hasta MapLibre y vuelve como un mapa en blanco sin\n * excepción, que es la peor forma de romperse: no hay nada que buscar.\n */\nfunction exigirUrl(valor: string, nombre: string): string {\n if (typeof valor !== \"string\" || valor.trim() === \"\") {\n throw new Error(\n `construirEstilo: falta ${nombre}. No tiene valor por ` +\n `defecto: sale del manifiesto (ADR-024), nunca escrita a mano.`,\n );\n }\n return valor;\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * El idioma de los rótulos del mapa base.
3
+ *
4
+ * **Es un parámetro y nunca un valor quemado.** No es una precaución teórica:
5
+ * `layers()` de `@protomaps/basemaps` solo devuelve rótulos si se le pasa
6
+ * `lang`, así que el idioma es la diferencia entre un mapa con nombres de
7
+ * calle y uno mudo. Quemarlo dejaría a `cerca-maps` —que es infraestructura
8
+ * compartida por varias superficies— con una decisión de producto adentro.
9
+ *
10
+ * Hoy Cerca es `es-CO` y por eso `es` es el valor por defecto. `en` y `pt`
11
+ * están declarados porque son los dos que el ecosistema tendría a mano si
12
+ * cruzara frontera, y porque un tipo con un solo miembro no obliga a nadie a
13
+ * escribir el código que acepta otro.
14
+ *
15
+ * El código es el de dos letras que espera `layers({ lang })`, no una
16
+ * etiqueta BCP-47: `es`, no `es-CO`. Lo que el archivo de teselas trae son
17
+ * los campos `name:es`, `name:en`, … de OpenStreetMap.
18
+ */
19
+ export type Idioma = "es" | "en" | "pt";
20
+ /**
21
+ * Los idiomas declarados, en el orden en que se ofrecen.
22
+ *
23
+ * `satisfies` en vez de anotación: así el arreglo conserva su tipo literal
24
+ * —que es lo que hace que el laboratorio pueda recorrerlo sin castear— y a
25
+ * la vez el compilador comprueba que no se cuele uno que `Idioma` no admita.
26
+ */
27
+ export declare const IDIOMAS: readonly ["es", "en", "pt"];
28
+ /** `es` porque los copys de Cerca son `es-CO` (CLAUDE.md §1). */
29
+ export declare const IDIOMA_POR_DEFECTO: Idioma;
30
+ /** Verdadero si el valor es uno de los idiomas declarados. */
31
+ export declare function esIdioma(valor: string): valor is Idioma;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * El idioma de los rótulos del mapa base.
3
+ *
4
+ * **Es un parámetro y nunca un valor quemado.** No es una precaución teórica:
5
+ * `layers()` de `@protomaps/basemaps` solo devuelve rótulos si se le pasa
6
+ * `lang`, así que el idioma es la diferencia entre un mapa con nombres de
7
+ * calle y uno mudo. Quemarlo dejaría a `cerca-maps` —que es infraestructura
8
+ * compartida por varias superficies— con una decisión de producto adentro.
9
+ *
10
+ * Hoy Cerca es `es-CO` y por eso `es` es el valor por defecto. `en` y `pt`
11
+ * están declarados porque son los dos que el ecosistema tendría a mano si
12
+ * cruzara frontera, y porque un tipo con un solo miembro no obliga a nadie a
13
+ * escribir el código que acepta otro.
14
+ *
15
+ * El código es el de dos letras que espera `layers({ lang })`, no una
16
+ * etiqueta BCP-47: `es`, no `es-CO`. Lo que el archivo de teselas trae son
17
+ * los campos `name:es`, `name:en`, … de OpenStreetMap.
18
+ */
19
+ /**
20
+ * Los idiomas declarados, en el orden en que se ofrecen.
21
+ *
22
+ * `satisfies` en vez de anotación: así el arreglo conserva su tipo literal
23
+ * —que es lo que hace que el laboratorio pueda recorrerlo sin castear— y a
24
+ * la vez el compilador comprueba que no se cuele uno que `Idioma` no admita.
25
+ */
26
+ export const IDIOMAS = ["es", "en", "pt"];
27
+ /** `es` porque los copys de Cerca son `es-CO` (CLAUDE.md §1). */
28
+ export const IDIOMA_POR_DEFECTO = "es";
29
+ /** Verdadero si el valor es uno de los idiomas declarados. */
30
+ export function esIdioma(valor) {
31
+ return IDIOMAS.includes(valor);
32
+ }
33
+ //# sourceMappingURL=idiomas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idiomas.js","sourceRoot":"","sources":["../src/idiomas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAsC,CAAC;AAE/E,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAW,IAAI,CAAC;AAE/C,8DAA8D;AAC9D,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAQ,OAA6B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC","sourcesContent":["/**\n * El idioma de los rótulos del mapa base.\n *\n * **Es un parámetro y nunca un valor quemado.** No es una precaución teórica:\n * `layers()` de `@protomaps/basemaps` solo devuelve rótulos si se le pasa\n * `lang`, así que el idioma es la diferencia entre un mapa con nombres de\n * calle y uno mudo. Quemarlo dejaría a `cerca-maps` —que es infraestructura\n * compartida por varias superficies— con una decisión de producto adentro.\n *\n * Hoy Cerca es `es-CO` y por eso `es` es el valor por defecto. `en` y `pt`\n * están declarados porque son los dos que el ecosistema tendría a mano si\n * cruzara frontera, y porque un tipo con un solo miembro no obliga a nadie a\n * escribir el código que acepta otro.\n *\n * El código es el de dos letras que espera `layers({ lang })`, no una\n * etiqueta BCP-47: `es`, no `es-CO`. Lo que el archivo de teselas trae son\n * los campos `name:es`, `name:en`, … de OpenStreetMap.\n */\n\nexport type Idioma = \"es\" | \"en\" | \"pt\";\n\n/**\n * Los idiomas declarados, en el orden en que se ofrecen.\n *\n * `satisfies` en vez de anotación: así el arreglo conserva su tipo literal\n * —que es lo que hace que el laboratorio pueda recorrerlo sin castear— y a\n * la vez el compilador comprueba que no se cuele uno que `Idioma` no admita.\n */\nexport const IDIOMAS = [\"es\", \"en\", \"pt\"] as const satisfies readonly Idioma[];\n\n/** `es` porque los copys de Cerca son `es-CO` (CLAUDE.md §1). */\nexport const IDIOMA_POR_DEFECTO: Idioma = \"es\";\n\n/** Verdadero si el valor es uno de los idiomas declarados. */\nexport function esIdioma(valor: string): valor is Idioma {\n return (IDIOMAS as readonly string[]).includes(valor);\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Los estilos de MapLibre como datos: capas, orden, colores.
3
+ *
4
+ * Todo lo de aquí es puro. No importa `maplibre-gl`, no toca el DOM y no lee
5
+ * un archivo, así que corre igual en un navegador, en workerd y en Node —que
6
+ * es lo que permite que un Worker genere el mismo estilo que el navegador—.
7
+ */
8
+ export { color, tap, referenciaMovil, type Color } from "./tokens.ts";
9
+ export { contraste, cumpleAA, luminancia } from "./contraste.ts";
10
+ export { esIdioma, IDIOMA_POR_DEFECTO, IDIOMAS, type Idioma, } from "./idiomas.ts";
11
+ export { DIVERGENCIAS, FUENTES, saborDeCerca, TEMA_POR_DEFECTO, TEMAS, type Divergencia, type Tema, } from "./sabor.ts";
12
+ export { ANCLAS, capaAncla, ORDEN_DE_CAPAS, type Ancla, type ContenidoDeFranja, type Franja, } from "./capas.ts";
13
+ export { construirEstilo, FUENTE_BASE, type OpcionesDeEstilo, } from "./estilo.ts";
14
+ export type { EspecificacionDeCapa, EspecificacionDeEstilo } from "./tipos.ts";
15
+ export { fuentesDelEstilo, rotulosDelEstilo, type OpcionesDeRotulos, type ParDeContraste, type RotulosDelEstilo, } from "./rotulos.ts";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Los estilos de MapLibre como datos: capas, orden, colores.
3
+ *
4
+ * Todo lo de aquí es puro. No importa `maplibre-gl`, no toca el DOM y no lee
5
+ * un archivo, así que corre igual en un navegador, en workerd y en Node —que
6
+ * es lo que permite que un Worker genere el mismo estilo que el navegador—.
7
+ */
8
+ export { color, tap, referenciaMovil } from "./tokens.js";
9
+ export { contraste, cumpleAA, luminancia } from "./contraste.js";
10
+ export { esIdioma, IDIOMA_POR_DEFECTO, IDIOMAS, } from "./idiomas.js";
11
+ export { DIVERGENCIAS, FUENTES, saborDeCerca, TEMA_POR_DEFECTO, TEMAS, } from "./sabor.js";
12
+ export { ANCLAS, capaAncla, ORDEN_DE_CAPAS, } from "./capas.js";
13
+ export { construirEstilo, FUENTE_BASE, } from "./estilo.js";
14
+ export { fuentesDelEstilo, rotulosDelEstilo, } from "./rotulos.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,eAAe,EAAc,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,OAAO,GAER,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,KAAK,GAGN,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,MAAM,EACN,SAAS,EACT,cAAc,GAIf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,eAAe,EACf,WAAW,GAEZ,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,GAIjB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Los estilos de MapLibre como datos: capas, orden, colores.\n *\n * Todo lo de aquí es puro. No importa `maplibre-gl`, no toca el DOM y no lee\n * un archivo, así que corre igual en un navegador, en workerd y en Node —que\n * es lo que permite que un Worker genere el mismo estilo que el navegador—.\n */\n\nexport { color, tap, referenciaMovil, type Color } from \"./tokens.ts\";\nexport { contraste, cumpleAA, luminancia } from \"./contraste.ts\";\nexport {\n esIdioma,\n IDIOMA_POR_DEFECTO,\n IDIOMAS,\n type Idioma,\n} from \"./idiomas.ts\";\nexport {\n DIVERGENCIAS,\n FUENTES,\n saborDeCerca,\n TEMA_POR_DEFECTO,\n TEMAS,\n type Divergencia,\n type Tema,\n} from \"./sabor.ts\";\nexport {\n ANCLAS,\n capaAncla,\n ORDEN_DE_CAPAS,\n type Ancla,\n type ContenidoDeFranja,\n type Franja,\n} from \"./capas.ts\";\nexport {\n construirEstilo,\n FUENTE_BASE,\n type OpcionesDeEstilo,\n} from \"./estilo.ts\";\nexport type { EspecificacionDeCapa, EspecificacionDeEstilo } from \"./tipos.ts\";\nexport {\n fuentesDelEstilo,\n rotulosDelEstilo,\n type OpcionesDeRotulos,\n type ParDeContraste,\n type RotulosDelEstilo,\n} from \"./rotulos.ts\";\n"]}
@@ -0,0 +1,50 @@
1
+ import type { EspecificacionDeEstilo } from "./tipos.ts";
2
+ /**
3
+ * Los pares rótulo/fondo del estilo, leídos del estilo.
4
+ *
5
+ * **Se extraen, no se listan.** Una lista escrita a mano de qué rótulo va
6
+ * sobre qué fondo envejece con cada versión de `@protomaps/basemaps` sin que
7
+ * nada se ponga rojo: el upstream añade una capa de texto, la lista no la
8
+ * tiene, y la comprobación de contraste pasa en verde sin haberla mirado.
9
+ * Aquí el recorrido es sobre las capas que el estilo produjo de verdad.
10
+ */
11
+ export interface ParDeContraste {
12
+ readonly capa: string;
13
+ /** El `text-color` de la capa. */
14
+ readonly frente: string;
15
+ /** El `text-halo-color`, o el suelo si la capa no tiene halo. */
16
+ readonly fondo: string;
17
+ readonly tieneHalo: boolean;
18
+ }
19
+ export interface RotulosDelEstilo {
20
+ readonly pares: readonly ParDeContraste[];
21
+ /**
22
+ * Capas de símbolo con texto cuyo `text-color` no es un color literal sino
23
+ * una expresión.
24
+ *
25
+ * Se devuelven aparte en vez de omitirse en silencio: una capa que no se
26
+ * puede comprobar tiene que doler igual que una que falla, o la
27
+ * comprobación se vacía sola con el tiempo.
28
+ */
29
+ readonly noComprobables: readonly string[];
30
+ }
31
+ export interface OpcionesDeRotulos {
32
+ /**
33
+ * El color del suelo, que es el fondo de reserva cuando una capa de texto
34
+ * no declara halo. Es conservador y no exacto: `roads_shields` dibuja su
35
+ * texto sobre el icono del escudo, que viene del sprite y no del estilo.
36
+ */
37
+ readonly suelo: string;
38
+ }
39
+ export declare function rotulosDelEstilo(estilo: EspecificacionDeEstilo, { suelo }: OpcionesDeRotulos): RotulosDelEstilo;
40
+ /**
41
+ * Todas las familias tipográficas que el estilo pide, sin repetir.
42
+ *
43
+ * Mira dentro de las expresiones y no solo el `text-font` de nivel superior,
44
+ * porque el upstream mete `Noto Sans Devanagari Regular v1` dentro del
45
+ * `text-field`, como opción de una sección de `format`. Una comprobación que
46
+ * solo leyera el valor de arriba diría tres familias, y el extremo de glifos
47
+ * tendría que servir cuatro. Un `fontstack` que no existe no falla de forma
48
+ * visible: el rótulo sencillamente no aparece.
49
+ */
50
+ export declare function fuentesDelEstilo(estilo: EspecificacionDeEstilo): string[];
@@ -0,0 +1,87 @@
1
+ export function rotulosDelEstilo(estilo, { suelo }) {
2
+ const pares = [];
3
+ const noComprobables = [];
4
+ for (const capa of estilo.layers) {
5
+ if (capa.type !== "symbol")
6
+ continue;
7
+ const frente = capa.paint?.["text-color"];
8
+ const tieneTexto = capa.layout?.["text-field"] !== undefined;
9
+ if (!tieneTexto)
10
+ continue;
11
+ if (typeof frente !== "string") {
12
+ if (frente !== undefined)
13
+ noComprobables.push(capa.id);
14
+ continue;
15
+ }
16
+ const halo = capa.paint?.["text-halo-color"];
17
+ const tieneHalo = typeof halo === "string";
18
+ pares.push({
19
+ capa: capa.id,
20
+ frente,
21
+ fondo: tieneHalo ? halo : suelo,
22
+ tieneHalo,
23
+ });
24
+ }
25
+ return { pares, noComprobables };
26
+ }
27
+ /**
28
+ * Todas las familias tipográficas que el estilo pide, sin repetir.
29
+ *
30
+ * Mira dentro de las expresiones y no solo el `text-font` de nivel superior,
31
+ * porque el upstream mete `Noto Sans Devanagari Regular v1` dentro del
32
+ * `text-field`, como opción de una sección de `format`. Una comprobación que
33
+ * solo leyera el valor de arriba diría tres familias, y el extremo de glifos
34
+ * tendría que servir cuatro. Un `fontstack` que no existe no falla de forma
35
+ * visible: el rótulo sencillamente no aparece.
36
+ */
37
+ export function fuentesDelEstilo(estilo) {
38
+ const encontradas = new Set();
39
+ for (const capa of estilo.layers) {
40
+ if (capa.type !== "symbol")
41
+ continue;
42
+ buscarTextFont(capa.layout, encontradas);
43
+ }
44
+ return [...encontradas].sort();
45
+ }
46
+ /**
47
+ * Operadores que pueden tener una lista de fuentes en alguna de sus ramas.
48
+ *
49
+ * La lista es corta a propósito. Bajar por cualquier expresión traería
50
+ * también los nombres de campo de las condiciones —`["get", "script"]`
51
+ * daría `get` y `script` como si fueran tipografías—, y un falso positivo
52
+ * en esta lista es peor que un hueco: manda a servir un `fontstack` que
53
+ * nadie pide.
54
+ */
55
+ const RAMIFICADORES = new Set(["case", "match", "step", "coalesce"]);
56
+ /** Busca claves `text-font` a cualquier profundidad. */
57
+ function buscarTextFont(valor, dentro) {
58
+ if (Array.isArray(valor)) {
59
+ for (const parte of valor)
60
+ buscarTextFont(parte, dentro);
61
+ return;
62
+ }
63
+ if (valor === null || typeof valor !== "object")
64
+ return;
65
+ for (const [clave, hijo] of Object.entries(valor)) {
66
+ if (clave === "text-font")
67
+ extraerFuentes(hijo, dentro);
68
+ else
69
+ buscarTextFont(hijo, dentro);
70
+ }
71
+ }
72
+ /** De `["A", "B"]`, de `["literal", ["A"]]` y de las ramas de un `case`. */
73
+ function extraerFuentes(valor, dentro) {
74
+ if (!Array.isArray(valor) || valor.length === 0)
75
+ return;
76
+ const [primero, ...resto] = valor;
77
+ if (primero === "literal" || RAMIFICADORES.has(String(primero))) {
78
+ for (const parte of resto)
79
+ extraerFuentes(parte, dentro);
80
+ return;
81
+ }
82
+ if (valor.every((parte) => typeof parte === "string")) {
83
+ for (const parte of valor)
84
+ dentro.add(parte);
85
+ }
86
+ }
87
+ //# sourceMappingURL=rotulos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotulos.js","sourceRoot":"","sources":["../src/rotulos.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EspecificacionDeEstilo } from \"./tipos.ts\";\n\n/**\n * Los pares rótulo/fondo del estilo, leídos del estilo.\n *\n * **Se extraen, no se listan.** Una lista escrita a mano de qué rótulo va\n * sobre qué fondo envejece con cada versión de `@protomaps/basemaps` sin que\n * nada se ponga rojo: el upstream añade una capa de texto, la lista no la\n * tiene, y la comprobación de contraste pasa en verde sin haberla mirado.\n * Aquí el recorrido es sobre las capas que el estilo produjo de verdad.\n */\n\nexport interface ParDeContraste {\n readonly capa: string;\n /** El `text-color` de la capa. */\n readonly frente: string;\n /** El `text-halo-color`, o el suelo si la capa no tiene halo. */\n readonly fondo: string;\n readonly tieneHalo: boolean;\n}\n\nexport interface RotulosDelEstilo {\n readonly pares: readonly ParDeContraste[];\n /**\n * Capas de símbolo con texto cuyo `text-color` no es un color literal sino\n * una expresión.\n *\n * Se devuelven aparte en vez de omitirse en silencio: una capa que no se\n * puede comprobar tiene que doler igual que una que falla, o la\n * comprobación se vacía sola con el tiempo.\n */\n readonly noComprobables: readonly string[];\n}\n\nexport interface OpcionesDeRotulos {\n /**\n * El color del suelo, que es el fondo de reserva cuando una capa de texto\n * no declara halo. Es conservador y no exacto: `roads_shields` dibuja su\n * texto sobre el icono del escudo, que viene del sprite y no del estilo.\n */\n readonly suelo: string;\n}\n\nexport function rotulosDelEstilo(\n estilo: EspecificacionDeEstilo,\n { suelo }: OpcionesDeRotulos,\n): RotulosDelEstilo {\n const pares: ParDeContraste[] = [];\n const noComprobables: string[] = [];\n\n for (const capa of estilo.layers) {\n if (capa.type !== \"symbol\") continue;\n\n const frente = capa.paint?.[\"text-color\"];\n const tieneTexto = capa.layout?.[\"text-field\"] !== undefined;\n if (!tieneTexto) continue;\n\n if (typeof frente !== \"string\") {\n if (frente !== undefined) noComprobables.push(capa.id);\n continue;\n }\n\n const halo = capa.paint?.[\"text-halo-color\"];\n const tieneHalo = typeof halo === \"string\";\n\n pares.push({\n capa: capa.id,\n frente,\n fondo: tieneHalo ? halo : suelo,\n tieneHalo,\n });\n }\n\n return { pares, noComprobables };\n}\n\n/**\n * Todas las familias tipográficas que el estilo pide, sin repetir.\n *\n * Mira dentro de las expresiones y no solo el `text-font` de nivel superior,\n * porque el upstream mete `Noto Sans Devanagari Regular v1` dentro del\n * `text-field`, como opción de una sección de `format`. Una comprobación que\n * solo leyera el valor de arriba diría tres familias, y el extremo de glifos\n * tendría que servir cuatro. Un `fontstack` que no existe no falla de forma\n * visible: el rótulo sencillamente no aparece.\n */\nexport function fuentesDelEstilo(estilo: EspecificacionDeEstilo): string[] {\n const encontradas = new Set<string>();\n\n for (const capa of estilo.layers) {\n if (capa.type !== \"symbol\") continue;\n buscarTextFont(capa.layout, encontradas);\n }\n\n return [...encontradas].sort();\n}\n\n/**\n * Operadores que pueden tener una lista de fuentes en alguna de sus ramas.\n *\n * La lista es corta a propósito. Bajar por cualquier expresión traería\n * también los nombres de campo de las condiciones —`[\"get\", \"script\"]`\n * daría `get` y `script` como si fueran tipografías—, y un falso positivo\n * en esta lista es peor que un hueco: manda a servir un `fontstack` que\n * nadie pide.\n */\nconst RAMIFICADORES = new Set([\"case\", \"match\", \"step\", \"coalesce\"]);\n\n/** Busca claves `text-font` a cualquier profundidad. */\nfunction buscarTextFont(valor: unknown, dentro: Set<string>): void {\n if (Array.isArray(valor)) {\n for (const parte of valor) buscarTextFont(parte, dentro);\n return;\n }\n if (valor === null || typeof valor !== \"object\") return;\n\n for (const [clave, hijo] of Object.entries(valor)) {\n if (clave === \"text-font\") extraerFuentes(hijo, dentro);\n else buscarTextFont(hijo, dentro);\n }\n}\n\n/** De `[\"A\", \"B\"]`, de `[\"literal\", [\"A\"]]` y de las ramas de un `case`. */\nfunction extraerFuentes(valor: unknown, dentro: Set<string>): void {\n if (!Array.isArray(valor) || valor.length === 0) return;\n\n const [primero, ...resto] = valor;\n if (primero === \"literal\" || RAMIFICADORES.has(String(primero))) {\n for (const parte of resto) extraerFuentes(parte, dentro);\n return;\n }\n\n if (valor.every((parte) => typeof parte === \"string\")) {\n for (const parte of valor) dentro.add(parte as string);\n }\n}\n"]}
@@ -0,0 +1,117 @@
1
+ import { type Flavor } from "@protomaps/basemaps";
2
+ /**
3
+ * Cerca White: el sabor de color del mapa base.
4
+ *
5
+ * **Nace igual al sabor `WHITE` de `@protomaps/basemaps` y diverge después.**
6
+ * Eso no es una figura retórica: `saborDeCerca` literalmente parte de `WHITE`
7
+ * y aplica encima la lista `DIVERGENCIAS`, así que todo lo que no esté en esa
8
+ * lista es upstream sin tocar, y no hay forma de cambiar un color sin
9
+ * escribir al lado por qué.
10
+ *
11
+ * El sabor `WHITE` se eligió porque su propósito declarado es el de Cerca:
12
+ * un suelo casi en blanco sobre el que se dibujan datos propios. Los alfileres
13
+ * de negocio, las áreas de cobertura y las rutas son la información; el mapa
14
+ * base es el contexto y tiene que quedarse atrás.
15
+ *
16
+ * ## Por qué hay divergencias: los rótulos del upstream no se leen
17
+ *
18
+ * Medido el 2026-09-07 con la fórmula de WCAG 2.1 sobre los doce pares
19
+ * rótulo/halo que produce `@protomaps/basemaps@5.7.2` con el sabor `WHITE`
20
+ * —los pares salen del propio `base_layers.ts`, no de una lista escrita a
21
+ * mano—: **once de los doce fallan AA**. `roads_labels_minor` da 2.24:1 y
22
+ * `places_region` 2.10:1, contra el 4.5:1 que pide el nivel AA para texto
23
+ * normal. El único que pasa es `places_locality`, con 6.69:1.
24
+ *
25
+ * Un mapa que no deja leer el nombre de la calle no sirve para lo único que
26
+ * se le pide, y Cerca se usa con una mano, en la calle y con sol. Así que los
27
+ * ocho colores de rótulo se reemplazan por tokens de tinta de Cerca, y el peor
28
+ * par pasa a 5.41:1. Los números por par están en `sabor.test.ts`, que es
29
+ * donde se comprueban y no se pueden quedar viejos.
30
+ *
31
+ * ## Lo que no se cambia, y por qué
32
+ *
33
+ * `pois` y `landcover` se quedan **sin definir**, igual que en `WHITE`. No es
34
+ * un descuido: `@protomaps/basemaps` omite esas dos capas enteras cuando el
35
+ * sabor no las define, y omitirlas es lo correcto aquí. Los puntos de interés
36
+ * de OpenStreetMap competirían con los alfileres de Cerca por el mismo sitio
37
+ * de la pantalla, y traerían nombres de comercio real a un mapa donde la
38
+ * decisión de qué negocio se muestra es de Cerca, no de la cartografía.
39
+ * De las nueve capas que trae el archivo de teselas —`boundaries`,
40
+ * `buildings`, `earth`, `landcover`, `landuse`, `places`, `pois`, `roads`,
41
+ * `water`, medidas con `pmtiles show --metadata`— quedan siete dibujadas.
42
+ *
43
+ * El agua, los edificios y las vías se quedan como están. Bajar el suelo a
44
+ * `canvas` les come contraste —los edificios pasan de 1.15:1 a 1.08:1 contra
45
+ * el suelo, y las vías menores de 1.19:1 a 1.12:1— y esa erosión está medida
46
+ * y aceptada: son contrastes de textura, muy por debajo de cualquier umbral
47
+ * de legibilidad en los dos casos, y compensar uno de los quince colores de
48
+ * relleno sin tocar los otros catorce sería arbitrario.
49
+ */
50
+ /** El tema del mapa. */
51
+ export type Tema = "claro";
52
+ /**
53
+ * Los temas declarados.
54
+ *
55
+ * Hay uno solo, y el tipo es la costura por la que entrará el segundo. No hay
56
+ * `"oscuro"` porque **Cerca no tiene todavía una paleta oscura**:
57
+ * `Cerca-PWA/packages/ui/src/tokens.ts` declara un único juego de colores
58
+ * (leído el 2026-09-07). Inventar aquí setenta y dos valores oscuros sería
59
+ * fabricar una paleta de marca desde la superficie equivocada, y la primera
60
+ * pantalla que la usara la daría por buena.
61
+ *
62
+ * Lo que hace falta para añadirlo: un juego oscuro en los tokens de la PWA.
63
+ * Con eso, `"oscuro"` entra en esta unión y el compilador señala cada sitio
64
+ * que hay que completar.
65
+ */
66
+ export declare const TEMAS: readonly ["claro"];
67
+ export declare const TEMA_POR_DEFECTO: Tema;
68
+ /**
69
+ * Las cuatro familias tipográficas que el estilo pide.
70
+ *
71
+ * Las tres primeras son las que `@protomaps/basemaps` usa por defecto cuando
72
+ * el sabor no declara `regular`, `bold` ni `italic`; Cerca las escribe
73
+ * explícitas para que la dependencia se vea en el diff y no en un fallo de
74
+ * red. La cuarta la mete el propio upstream dentro de una expresión, para el
75
+ * texto en devanagari, y no se puede quitar desde el sabor.
76
+ *
77
+ * **El extremo de glifos tiene que servir las cuatro.** Un `text-font` cuyo
78
+ * `fontstack` no existe no rompe nada de forma visible: el rótulo
79
+ * sencillamente no aparece.
80
+ */
81
+ export declare const FUENTES: readonly ["Noto Sans Regular", "Noto Sans Medium", "Noto Sans Italic", "Noto Sans Devanagari Regular v1"];
82
+ /**
83
+ * Las claves de `Flavor` cuyo valor es un color o un nombre de fuente.
84
+ *
85
+ * Deja fuera `pois` y `landcover`, que son objetos anidados. Sin esto, una
86
+ * divergencia podría declarar `clave: "pois"` con un texto por valor y el
87
+ * compilador no diría nada hasta que el mapa saliera roto.
88
+ */
89
+ type ClaveDeColor = {
90
+ [K in keyof Flavor]-?: Flavor[K] extends string | undefined ? K : never;
91
+ }[keyof Flavor];
92
+ /**
93
+ * Una divergencia respecto del sabor `WHITE` del upstream.
94
+ *
95
+ * `upstream` guarda el valor que tenía `WHITE` el día que se decidió la
96
+ * divergencia, y `sabor.test.ts` comprueba que siga siendo ese. Es la parte
97
+ * que evita el envejecimiento silencioso: si Protomaps cambia un color de
98
+ * `WHITE`, la prueba se pone roja y alguien vuelve a leer la justificación en
99
+ * vez de heredar una decisión tomada sobre un valor que ya no existe.
100
+ */
101
+ export interface Divergencia {
102
+ readonly clave: ClaveDeColor;
103
+ /** Lo que vale en `WHITE`, o `null` si el sabor del upstream no la define. */
104
+ readonly upstream: string | null;
105
+ readonly valor: string;
106
+ readonly porQue: string;
107
+ }
108
+ export declare const DIVERGENCIAS: readonly Divergencia[];
109
+ /**
110
+ * El sabor de Cerca para un tema.
111
+ *
112
+ * Función pura: mismo tema, mismo objeto. No lee el DOM, no lee la red y no
113
+ * lee un archivo, así que se prueba sin navegador y sin GPU. Devuelve una
114
+ * copia para que nadie modifique el sabor compartido desde lejos.
115
+ */
116
+ export declare function saborDeCerca(tema?: Tema): Flavor;
117
+ export {};
package/dist/sabor.js ADDED
@@ -0,0 +1,150 @@
1
+ import { WHITE } from "@protomaps/basemaps";
2
+ import { color } from "./tokens.js";
3
+ /**
4
+ * Los temas declarados.
5
+ *
6
+ * Hay uno solo, y el tipo es la costura por la que entrará el segundo. No hay
7
+ * `"oscuro"` porque **Cerca no tiene todavía una paleta oscura**:
8
+ * `Cerca-PWA/packages/ui/src/tokens.ts` declara un único juego de colores
9
+ * (leído el 2026-09-07). Inventar aquí setenta y dos valores oscuros sería
10
+ * fabricar una paleta de marca desde la superficie equivocada, y la primera
11
+ * pantalla que la usara la daría por buena.
12
+ *
13
+ * Lo que hace falta para añadirlo: un juego oscuro en los tokens de la PWA.
14
+ * Con eso, `"oscuro"` entra en esta unión y el compilador señala cada sitio
15
+ * que hay que completar.
16
+ */
17
+ export const TEMAS = ["claro"];
18
+ export const TEMA_POR_DEFECTO = "claro";
19
+ /**
20
+ * Las cuatro familias tipográficas que el estilo pide.
21
+ *
22
+ * Las tres primeras son las que `@protomaps/basemaps` usa por defecto cuando
23
+ * el sabor no declara `regular`, `bold` ni `italic`; Cerca las escribe
24
+ * explícitas para que la dependencia se vea en el diff y no en un fallo de
25
+ * red. La cuarta la mete el propio upstream dentro de una expresión, para el
26
+ * texto en devanagari, y no se puede quitar desde el sabor.
27
+ *
28
+ * **El extremo de glifos tiene que servir las cuatro.** Un `text-font` cuyo
29
+ * `fontstack` no existe no rompe nada de forma visible: el rótulo
30
+ * sencillamente no aparece.
31
+ */
32
+ export const FUENTES = [
33
+ "Noto Sans Regular",
34
+ "Noto Sans Medium",
35
+ "Noto Sans Italic",
36
+ "Noto Sans Devanagari Regular v1",
37
+ ];
38
+ const ROTULO = "Contraste: el valor del upstream no llega a AA sobre su halo.";
39
+ export const DIVERGENCIAS = [
40
+ {
41
+ clave: "background",
42
+ upstream: "#ffffff",
43
+ valor: color.canvas,
44
+ porQue: "El mapa es el plano de fondo de Cerca, y el sistema de diseño reserva " +
45
+ "`surface` (#FFFFFF) para lo que flota encima: tarjetas, hojas y " +
46
+ "controles. Con el suelo en blanco, el mapa y la hoja que lo tapa son " +
47
+ "el mismo color.",
48
+ },
49
+ {
50
+ clave: "earth",
51
+ upstream: "#ffffff",
52
+ valor: color.canvas,
53
+ porQue: "El mismo motivo que `background`: la tierra es el suelo de Cerca.",
54
+ },
55
+ {
56
+ clave: "roads_label_minor",
57
+ upstream: "#adadad",
58
+ valor: color.label,
59
+ porQue: `${ROTULO} 2.24:1 sobre su halo blanco.`,
60
+ },
61
+ {
62
+ clave: "roads_label_major",
63
+ upstream: "#999999",
64
+ valor: color.textSecondary,
65
+ porQue: `${ROTULO} 2.85:1 sobre su halo blanco.`,
66
+ },
67
+ {
68
+ clave: "subplace_label",
69
+ upstream: "#8f8f8f",
70
+ valor: color.textSecondary,
71
+ porQue: `${ROTULO} 3.23:1 sobre su halo blanco.`,
72
+ },
73
+ {
74
+ clave: "city_label",
75
+ upstream: "#5c5c5c",
76
+ valor: color.ink,
77
+ porQue: "El único rótulo del upstream que ya cumplía AA (6.69:1). Sube a " +
78
+ "`ink` igual, porque el nombre de la ciudad es la etiqueta más " +
79
+ "importante del mapa y con los demás rótulos oscurecidos dejaría de " +
80
+ "leerse como la primera de la jerarquía.",
81
+ },
82
+ {
83
+ clave: "state_label",
84
+ upstream: "#b3b3b3",
85
+ valor: color.textMuted,
86
+ porQue: `${ROTULO} 2.10:1 sobre su halo blanco.`,
87
+ },
88
+ {
89
+ clave: "country_label",
90
+ upstream: "#b8b8b8",
91
+ valor: color.textMuted,
92
+ porQue: `${ROTULO} 1.98:1, y su halo es la tierra, no blanco.`,
93
+ },
94
+ {
95
+ clave: "ocean_label",
96
+ upstream: "#adadad",
97
+ valor: color.textSecondary,
98
+ porQue: "1.64:1 sobre el agua, que es el peor par del sabor del upstream: el " +
99
+ "halo de los rótulos de agua es el propio color del agua (#dcdcdc), no " +
100
+ "blanco, así que el gris claro casi desaparece.",
101
+ },
102
+ {
103
+ clave: "address_label",
104
+ upstream: "#adadad",
105
+ valor: color.label,
106
+ porQue: `${ROTULO} 2.24:1 sobre su halo blanco.`,
107
+ },
108
+ {
109
+ clave: "regular",
110
+ upstream: null,
111
+ valor: "Noto Sans Regular",
112
+ porQue: "Mismo valor que el que el upstream usa por defecto. Se escribe para " +
113
+ "que la dependencia de glifos esté en el sabor y no escondida en un " +
114
+ "`||` de `base_layers.ts`.",
115
+ },
116
+ {
117
+ clave: "bold",
118
+ upstream: null,
119
+ valor: "Noto Sans Medium",
120
+ porQue: "El mismo motivo que `regular`: la dependencia de glifos se declara, " +
121
+ "no se hereda de un `||`.",
122
+ },
123
+ {
124
+ clave: "italic",
125
+ upstream: null,
126
+ valor: "Noto Sans Italic",
127
+ porQue: "El mismo motivo que `regular`: la dependencia de glifos se declara, " +
128
+ "no se hereda de un `||`.",
129
+ },
130
+ ];
131
+ /** `WHITE` con las divergencias aplicadas encima, en orden. */
132
+ const CERCA_WHITE = DIVERGENCIAS.reduce((sabor, { clave, valor }) => ({ ...sabor, [clave]: valor }), WHITE);
133
+ /**
134
+ * Los sabores por tema.
135
+ *
136
+ * `Record<Tema, Flavor>` y no un `switch`: el día que `Tema` gane `"oscuro"`,
137
+ * esta línea es un error de compilación y no un caso que se olvidó.
138
+ */
139
+ const SABORES = { claro: CERCA_WHITE };
140
+ /**
141
+ * El sabor de Cerca para un tema.
142
+ *
143
+ * Función pura: mismo tema, mismo objeto. No lee el DOM, no lee la red y no
144
+ * lee un archivo, así que se prueba sin navegador y sin GPU. Devuelve una
145
+ * copia para que nadie modifique el sabor compartido desde lejos.
146
+ */
147
+ export function saborDeCerca(tema = TEMA_POR_DEFECTO) {
148
+ return { ...SABORES[tema] };
149
+ }
150
+ //# sourceMappingURL=sabor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sabor.js","sourceRoot":"","sources":["../src/sabor.ts"],"names":[],"mappings":"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","sourcesContent":["import { WHITE, type Flavor } from \"@protomaps/basemaps\";\n\nimport { color } from \"./tokens.ts\";\n\n/**\n * Cerca White: el sabor de color del mapa base.\n *\n * **Nace igual al sabor `WHITE` de `@protomaps/basemaps` y diverge después.**\n * Eso no es una figura retórica: `saborDeCerca` literalmente parte de `WHITE`\n * y aplica encima la lista `DIVERGENCIAS`, así que todo lo que no esté en esa\n * lista es upstream sin tocar, y no hay forma de cambiar un color sin\n * escribir al lado por qué.\n *\n * El sabor `WHITE` se eligió porque su propósito declarado es el de Cerca:\n * un suelo casi en blanco sobre el que se dibujan datos propios. Los alfileres\n * de negocio, las áreas de cobertura y las rutas son la información; el mapa\n * base es el contexto y tiene que quedarse atrás.\n *\n * ## Por qué hay divergencias: los rótulos del upstream no se leen\n *\n * Medido el 2026-09-07 con la fórmula de WCAG 2.1 sobre los doce pares\n * rótulo/halo que produce `@protomaps/basemaps@5.7.2` con el sabor `WHITE`\n * —los pares salen del propio `base_layers.ts`, no de una lista escrita a\n * mano—: **once de los doce fallan AA**. `roads_labels_minor` da 2.24:1 y\n * `places_region` 2.10:1, contra el 4.5:1 que pide el nivel AA para texto\n * normal. El único que pasa es `places_locality`, con 6.69:1.\n *\n * Un mapa que no deja leer el nombre de la calle no sirve para lo único que\n * se le pide, y Cerca se usa con una mano, en la calle y con sol. Así que los\n * ocho colores de rótulo se reemplazan por tokens de tinta de Cerca, y el peor\n * par pasa a 5.41:1. Los números por par están en `sabor.test.ts`, que es\n * donde se comprueban y no se pueden quedar viejos.\n *\n * ## Lo que no se cambia, y por qué\n *\n * `pois` y `landcover` se quedan **sin definir**, igual que en `WHITE`. No es\n * un descuido: `@protomaps/basemaps` omite esas dos capas enteras cuando el\n * sabor no las define, y omitirlas es lo correcto aquí. Los puntos de interés\n * de OpenStreetMap competirían con los alfileres de Cerca por el mismo sitio\n * de la pantalla, y traerían nombres de comercio real a un mapa donde la\n * decisión de qué negocio se muestra es de Cerca, no de la cartografía.\n * De las nueve capas que trae el archivo de teselas —`boundaries`,\n * `buildings`, `earth`, `landcover`, `landuse`, `places`, `pois`, `roads`,\n * `water`, medidas con `pmtiles show --metadata`— quedan siete dibujadas.\n *\n * El agua, los edificios y las vías se quedan como están. Bajar el suelo a\n * `canvas` les come contraste —los edificios pasan de 1.15:1 a 1.08:1 contra\n * el suelo, y las vías menores de 1.19:1 a 1.12:1— y esa erosión está medida\n * y aceptada: son contrastes de textura, muy por debajo de cualquier umbral\n * de legibilidad en los dos casos, y compensar uno de los quince colores de\n * relleno sin tocar los otros catorce sería arbitrario.\n */\n\n/** El tema del mapa. */\nexport type Tema = \"claro\";\n\n/**\n * Los temas declarados.\n *\n * Hay uno solo, y el tipo es la costura por la que entrará el segundo. No hay\n * `\"oscuro\"` porque **Cerca no tiene todavía una paleta oscura**:\n * `Cerca-PWA/packages/ui/src/tokens.ts` declara un único juego de colores\n * (leído el 2026-09-07). Inventar aquí setenta y dos valores oscuros sería\n * fabricar una paleta de marca desde la superficie equivocada, y la primera\n * pantalla que la usara la daría por buena.\n *\n * Lo que hace falta para añadirlo: un juego oscuro en los tokens de la PWA.\n * Con eso, `\"oscuro\"` entra en esta unión y el compilador señala cada sitio\n * que hay que completar.\n */\nexport const TEMAS = [\"claro\"] as const satisfies readonly Tema[];\n\nexport const TEMA_POR_DEFECTO: Tema = \"claro\";\n\n/**\n * Las cuatro familias tipográficas que el estilo pide.\n *\n * Las tres primeras son las que `@protomaps/basemaps` usa por defecto cuando\n * el sabor no declara `regular`, `bold` ni `italic`; Cerca las escribe\n * explícitas para que la dependencia se vea en el diff y no en un fallo de\n * red. La cuarta la mete el propio upstream dentro de una expresión, para el\n * texto en devanagari, y no se puede quitar desde el sabor.\n *\n * **El extremo de glifos tiene que servir las cuatro.** Un `text-font` cuyo\n * `fontstack` no existe no rompe nada de forma visible: el rótulo\n * sencillamente no aparece.\n */\nexport const FUENTES = [\n \"Noto Sans Regular\",\n \"Noto Sans Medium\",\n \"Noto Sans Italic\",\n \"Noto Sans Devanagari Regular v1\",\n] as const;\n\n/**\n * Las claves de `Flavor` cuyo valor es un color o un nombre de fuente.\n *\n * Deja fuera `pois` y `landcover`, que son objetos anidados. Sin esto, una\n * divergencia podría declarar `clave: \"pois\"` con un texto por valor y el\n * compilador no diría nada hasta que el mapa saliera roto.\n */\ntype ClaveDeColor = {\n [K in keyof Flavor]-?: Flavor[K] extends string | undefined ? K : never;\n}[keyof Flavor];\n\n/**\n * Una divergencia respecto del sabor `WHITE` del upstream.\n *\n * `upstream` guarda el valor que tenía `WHITE` el día que se decidió la\n * divergencia, y `sabor.test.ts` comprueba que siga siendo ese. Es la parte\n * que evita el envejecimiento silencioso: si Protomaps cambia un color de\n * `WHITE`, la prueba se pone roja y alguien vuelve a leer la justificación en\n * vez de heredar una decisión tomada sobre un valor que ya no existe.\n */\nexport interface Divergencia {\n readonly clave: ClaveDeColor;\n /** Lo que vale en `WHITE`, o `null` si el sabor del upstream no la define. */\n readonly upstream: string | null;\n readonly valor: string;\n readonly porQue: string;\n}\n\nconst ROTULO = \"Contraste: el valor del upstream no llega a AA sobre su halo.\";\n\nexport const DIVERGENCIAS: readonly Divergencia[] = [\n {\n clave: \"background\",\n upstream: \"#ffffff\",\n valor: color.canvas,\n porQue:\n \"El mapa es el plano de fondo de Cerca, y el sistema de diseño reserva \" +\n \"`surface` (#FFFFFF) para lo que flota encima: tarjetas, hojas y \" +\n \"controles. Con el suelo en blanco, el mapa y la hoja que lo tapa son \" +\n \"el mismo color.\",\n },\n {\n clave: \"earth\",\n upstream: \"#ffffff\",\n valor: color.canvas,\n porQue: \"El mismo motivo que `background`: la tierra es el suelo de Cerca.\",\n },\n\n {\n clave: \"roads_label_minor\",\n upstream: \"#adadad\",\n valor: color.label,\n porQue: `${ROTULO} 2.24:1 sobre su halo blanco.`,\n },\n {\n clave: \"roads_label_major\",\n upstream: \"#999999\",\n valor: color.textSecondary,\n porQue: `${ROTULO} 2.85:1 sobre su halo blanco.`,\n },\n {\n clave: \"subplace_label\",\n upstream: \"#8f8f8f\",\n valor: color.textSecondary,\n porQue: `${ROTULO} 3.23:1 sobre su halo blanco.`,\n },\n {\n clave: \"city_label\",\n upstream: \"#5c5c5c\",\n valor: color.ink,\n porQue:\n \"El único rótulo del upstream que ya cumplía AA (6.69:1). Sube a \" +\n \"`ink` igual, porque el nombre de la ciudad es la etiqueta más \" +\n \"importante del mapa y con los demás rótulos oscurecidos dejaría de \" +\n \"leerse como la primera de la jerarquía.\",\n },\n {\n clave: \"state_label\",\n upstream: \"#b3b3b3\",\n valor: color.textMuted,\n porQue: `${ROTULO} 2.10:1 sobre su halo blanco.`,\n },\n {\n clave: \"country_label\",\n upstream: \"#b8b8b8\",\n valor: color.textMuted,\n porQue: `${ROTULO} 1.98:1, y su halo es la tierra, no blanco.`,\n },\n {\n clave: \"ocean_label\",\n upstream: \"#adadad\",\n valor: color.textSecondary,\n porQue:\n \"1.64:1 sobre el agua, que es el peor par del sabor del upstream: el \" +\n \"halo de los rótulos de agua es el propio color del agua (#dcdcdc), no \" +\n \"blanco, así que el gris claro casi desaparece.\",\n },\n {\n clave: \"address_label\",\n upstream: \"#adadad\",\n valor: color.label,\n porQue: `${ROTULO} 2.24:1 sobre su halo blanco.`,\n },\n\n {\n clave: \"regular\",\n upstream: null,\n valor: \"Noto Sans Regular\",\n porQue:\n \"Mismo valor que el que el upstream usa por defecto. Se escribe para \" +\n \"que la dependencia de glifos esté en el sabor y no escondida en un \" +\n \"`||` de `base_layers.ts`.\",\n },\n {\n clave: \"bold\",\n upstream: null,\n valor: \"Noto Sans Medium\",\n porQue:\n \"El mismo motivo que `regular`: la dependencia de glifos se declara, \" +\n \"no se hereda de un `||`.\",\n },\n {\n clave: \"italic\",\n upstream: null,\n valor: \"Noto Sans Italic\",\n porQue:\n \"El mismo motivo que `regular`: la dependencia de glifos se declara, \" +\n \"no se hereda de un `||`.\",\n },\n];\n\n/** `WHITE` con las divergencias aplicadas encima, en orden. */\nconst CERCA_WHITE: Flavor = DIVERGENCIAS.reduce<Flavor>(\n (sabor, { clave, valor }) => ({ ...sabor, [clave]: valor }),\n WHITE,\n);\n\n/**\n * Los sabores por tema.\n *\n * `Record<Tema, Flavor>` y no un `switch`: el día que `Tema` gane `\"oscuro\"`,\n * esta línea es un error de compilación y no un caso que se olvidó.\n */\nconst SABORES: Record<Tema, Flavor> = { claro: CERCA_WHITE };\n\n/**\n * El sabor de Cerca para un tema.\n *\n * Función pura: mismo tema, mismo objeto. No lee el DOM, no lee la red y no\n * lee un archivo, así que se prueba sin navegador y sin GPU. Devuelve una\n * copia para que nadie modifique el sabor compartido desde lejos.\n */\nexport function saborDeCerca(tema: Tema = TEMA_POR_DEFECTO): Flavor {\n return { ...SABORES[tema] };\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { LayerSpecification, StyleSpecification } from "@maplibre/maplibre-gl-style-spec";
2
+ /**
3
+ * Los tipos del style spec, con un solo dueño.
4
+ *
5
+ * `packages/mapa` y `packages/mapa-react` los necesitan para escribir sus
6
+ * capas, y no pueden declararlos por su cuenta: dos alias del mismo tipo en
7
+ * dos paquetes se separan el día que uno actualice y el otro no, y el
8
+ * compilador no lo dice —los dos siguen siendo `StyleSpecification`, solo que
9
+ * de dos copias distintas del paquete—. Salen de aquí porque este es el
10
+ * paquete que produce estilos.
11
+ *
12
+ * El nombre va en español, como el resto de la API pública (ADR-027 §2).
13
+ * `StyleSpecification` se queda en inglés dentro del import porque es el
14
+ * nombre literal de la herramienta.
15
+ */
16
+ /** Un estilo completo de MapLibre. */
17
+ export type EspecificacionDeEstilo = StyleSpecification;
18
+ /** Una capa de un estilo de MapLibre. */
19
+ export type EspecificacionDeCapa = LayerSpecification;
package/dist/tipos.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tipos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tipos.js","sourceRoot":"","sources":["../src/tipos.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n LayerSpecification,\n StyleSpecification,\n} from \"@maplibre/maplibre-gl-style-spec\";\n\n/**\n * Los tipos del style spec, con un solo dueño.\n *\n * `packages/mapa` y `packages/mapa-react` los necesitan para escribir sus\n * capas, y no pueden declararlos por su cuenta: dos alias del mismo tipo en\n * dos paquetes se separan el día que uno actualice y el otro no, y el\n * compilador no lo dice —los dos siguen siendo `StyleSpecification`, solo que\n * de dos copias distintas del paquete—. Salen de aquí porque este es el\n * paquete que produce estilos.\n *\n * El nombre va en español, como el resto de la API pública (ADR-027 §2).\n * `StyleSpecification` se queda en inglés dentro del import porque es el\n * nombre literal de la herramienta.\n */\n\n/** Un estilo completo de MapLibre. */\nexport type EspecificacionDeEstilo = StyleSpecification;\n\n/** Una capa de un estilo de MapLibre. */\nexport type EspecificacionDeCapa = LayerSpecification;\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Los tokens de marca de Cerca, redeclarados aquí.
3
+ *
4
+ * **Es una copia, y eso está decidido, no olvidado.** ADR-027 lo dice con
5
+ * estas palabras: «`cerca-maps` no comparte `packages/ui` con la PWA, igual
6
+ * que la landing (ADR-021). Los colores del mapa se declaran otra vez y
7
+ * pueden divergir. La salida, si duele, es publicar los tokens como paquete
8
+ * — no fusionar superficies.»
9
+ *
10
+ * Fuente: `Cerca-PWA/packages/ui/src/tokens.ts`, leída el 2026-09-07. Los
11
+ * valores de abajo son los mismos, byte a byte, con el nombre en la misma
12
+ * grafía para que un `grep` de un hexadecimal encuentre los dos sitios.
13
+ *
14
+ * Lo que **no** hay es una guarda automática de deriva, y conviene decirlo
15
+ * con nombre y no dejarlo para que lo descubra otro: no se puede escribir
16
+ * desde aquí. El CI de este repositorio hace `actions/checkout` de este
17
+ * repositorio y nada más, así que `Cerca-PWA` no existe cuando corren las
18
+ * pruebas; y una prueba que se salte sola cuando no encuentra el archivo
19
+ * pasaría en verde con la deriva puesta, que es peor que no tenerla. La
20
+ * salida de verdad es la que ya nombró ADR-027: publicar los tokens como
21
+ * paquete y depender de él.
22
+ */
23
+ export declare const color: {
24
+ readonly canvas: "#F7F8F9";
25
+ readonly surface: "#FFFFFF";
26
+ readonly elevated: "#EFF1F3";
27
+ readonly border: "#D9DDE1";
28
+ readonly divider: "#E8ECEF";
29
+ readonly ink: "#16110F";
30
+ readonly textSecondary: "#3A4046";
31
+ readonly textMuted: "#5F676D";
32
+ readonly label: "#626A70";
33
+ readonly disabled: "#A9B0B6";
34
+ readonly action: "#D42314";
35
+ readonly actionHover: "#A81B0F";
36
+ readonly focus: "#17558F";
37
+ readonly focusSobreOscuro: "#FFFFFF";
38
+ readonly success: "#1F6B4A";
39
+ readonly warning: "#8A5A0B";
40
+ readonly danger: "#B3261E";
41
+ };
42
+ export type Color = keyof typeof color;
43
+ /**
44
+ * Mínimos táctiles: el proyecto exige 44 px (CLAUDE.md §1) y dibuja a 48 px
45
+ * para no quedar en el límite exacto. Sobre un canvas esto no se mide
46
+ * leyendo una caja —no hay elemento DOM— sino tocando: se proyecta el punto
47
+ * y se toca dentro y fuera del radio esperado.
48
+ */
49
+ export declare const tap: {
50
+ readonly minimo: 44;
51
+ readonly boton: 48;
52
+ };
53
+ /** Mobile first: el diseño se hace a 390 × 844 y solo después se sube. */
54
+ export declare const referenciaMovil: {
55
+ readonly ancho: 390;
56
+ readonly alto: 844;
57
+ };
package/dist/tokens.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Los tokens de marca de Cerca, redeclarados aquí.
3
+ *
4
+ * **Es una copia, y eso está decidido, no olvidado.** ADR-027 lo dice con
5
+ * estas palabras: «`cerca-maps` no comparte `packages/ui` con la PWA, igual
6
+ * que la landing (ADR-021). Los colores del mapa se declaran otra vez y
7
+ * pueden divergir. La salida, si duele, es publicar los tokens como paquete
8
+ * — no fusionar superficies.»
9
+ *
10
+ * Fuente: `Cerca-PWA/packages/ui/src/tokens.ts`, leída el 2026-09-07. Los
11
+ * valores de abajo son los mismos, byte a byte, con el nombre en la misma
12
+ * grafía para que un `grep` de un hexadecimal encuentre los dos sitios.
13
+ *
14
+ * Lo que **no** hay es una guarda automática de deriva, y conviene decirlo
15
+ * con nombre y no dejarlo para que lo descubra otro: no se puede escribir
16
+ * desde aquí. El CI de este repositorio hace `actions/checkout` de este
17
+ * repositorio y nada más, así que `Cerca-PWA` no existe cuando corren las
18
+ * pruebas; y una prueba que se salte sola cuando no encuentra el archivo
19
+ * pasaría en verde con la deriva puesta, que es peor que no tenerla. La
20
+ * salida de verdad es la que ya nombró ADR-027: publicar los tokens como
21
+ * paquete y depender de él.
22
+ */
23
+ export const color = {
24
+ canvas: "#F7F8F9",
25
+ surface: "#FFFFFF",
26
+ elevated: "#EFF1F3",
27
+ border: "#D9DDE1",
28
+ divider: "#E8ECEF",
29
+ ink: "#16110F",
30
+ textSecondary: "#3A4046",
31
+ textMuted: "#5F676D",
32
+ label: "#626A70",
33
+ disabled: "#A9B0B6",
34
+ action: "#D42314",
35
+ actionHover: "#A81B0F",
36
+ focus: "#17558F",
37
+ focusSobreOscuro: "#FFFFFF",
38
+ success: "#1F6B4A",
39
+ warning: "#8A5A0B",
40
+ danger: "#B3261E",
41
+ };
42
+ /**
43
+ * Mínimos táctiles: el proyecto exige 44 px (CLAUDE.md §1) y dibuja a 48 px
44
+ * para no quedar en el límite exacto. Sobre un canvas esto no se mide
45
+ * leyendo una caja —no hay elemento DOM— sino tocando: se proyecta el punto
46
+ * y se toca dentro y fuera del radio esperado.
47
+ */
48
+ export const tap = {
49
+ minimo: 44,
50
+ boton: 48,
51
+ };
52
+ /** Mobile first: el diseño se hace a 390 × 844 y solo después se sube. */
53
+ export const referenciaMovil = { ancho: 390, alto: 844 };
54
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,SAAS;IACnB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAElB,GAAG,EAAE,SAAS;IACd,aAAa,EAAE,SAAS;IACxB,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,SAAS;IAChB,QAAQ,EAAE,SAAS;IAEnB,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,SAAS;IACtB,KAAK,EAAE,SAAS;IAChB,gBAAgB,EAAE,SAAS;IAE3B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;CACT,CAAC;AAIX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;CACD,CAAC;AAEX,0EAA0E;AAC1E,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAW,CAAC","sourcesContent":["/**\n * Los tokens de marca de Cerca, redeclarados aquí.\n *\n * **Es una copia, y eso está decidido, no olvidado.** ADR-027 lo dice con\n * estas palabras: «`cerca-maps` no comparte `packages/ui` con la PWA, igual\n * que la landing (ADR-021). Los colores del mapa se declaran otra vez y\n * pueden divergir. La salida, si duele, es publicar los tokens como paquete\n * — no fusionar superficies.»\n *\n * Fuente: `Cerca-PWA/packages/ui/src/tokens.ts`, leída el 2026-09-07. Los\n * valores de abajo son los mismos, byte a byte, con el nombre en la misma\n * grafía para que un `grep` de un hexadecimal encuentre los dos sitios.\n *\n * Lo que **no** hay es una guarda automática de deriva, y conviene decirlo\n * con nombre y no dejarlo para que lo descubra otro: no se puede escribir\n * desde aquí. El CI de este repositorio hace `actions/checkout` de este\n * repositorio y nada más, así que `Cerca-PWA` no existe cuando corren las\n * pruebas; y una prueba que se salte sola cuando no encuentra el archivo\n * pasaría en verde con la deriva puesta, que es peor que no tenerla. La\n * salida de verdad es la que ya nombró ADR-027: publicar los tokens como\n * paquete y depender de él.\n */\n\nexport const color = {\n canvas: \"#F7F8F9\",\n surface: \"#FFFFFF\",\n elevated: \"#EFF1F3\",\n border: \"#D9DDE1\",\n divider: \"#E8ECEF\",\n\n ink: \"#16110F\",\n textSecondary: \"#3A4046\",\n textMuted: \"#5F676D\",\n label: \"#626A70\",\n disabled: \"#A9B0B6\",\n\n action: \"#D42314\",\n actionHover: \"#A81B0F\",\n focus: \"#17558F\",\n focusSobreOscuro: \"#FFFFFF\",\n\n success: \"#1F6B4A\",\n warning: \"#8A5A0B\",\n danger: \"#B3261E\",\n} as const;\n\nexport type Color = keyof typeof color;\n\n/**\n * Mínimos táctiles: el proyecto exige 44 px (CLAUDE.md §1) y dibuja a 48 px\n * para no quedar en el límite exacto. Sobre un canvas esto no se mide\n * leyendo una caja —no hay elemento DOM— sino tocando: se proyecta el punto\n * y se toca dentro y fuera del radio esperado.\n */\nexport const tap = {\n minimo: 44,\n boton: 48,\n} as const;\n\n/** Mobile first: el diseño se hace a 390 × 844 y solo después se sube. */\nexport const referenciaMovil = { ancho: 390, alto: 844 } as const;\n"]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@cerca.red/mapa-estilos",
3
+ "version": "0.2.0",
4
+ "description": "Cerca White sobre el sabor White de Protomaps: los estilos de MapLibre como datos, con el orden de capas, los temas y el idioma de los rótulos.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "dependencies": {
21
+ "@maplibre/maplibre-gl-style-spec": "26.4.1"
22
+ },
23
+ "peerDependencies": {
24
+ "@protomaps/basemaps": "^5.7.2"
25
+ },
26
+ "devDependencies": {
27
+ "@protomaps/basemaps": "5.7.2"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Exus-Agencia-Web/cerca-maps.git",
32
+ "directory": "packages/mapa-estilos"
33
+ },
34
+ "scripts": {
35
+ "construir": "tsc -p tsconfig.build.json"
36
+ }
37
+ }