@datatechsolutions/tympan-tokens 0.4.2 → 0.5.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.
Files changed (80) hide show
  1. package/THIRD_PARTY_NOTICES.md +5 -5
  2. package/dist/fonts.css +80 -80
  3. package/dist/index.d.ts +229 -55
  4. package/dist/index.js +359 -16
  5. package/dist/index.js.map +1 -1
  6. package/dist/print-themes/print-aquarela.css +7 -7
  7. package/dist/print-themes/print-art-nouveau.css +2 -2
  8. package/dist/print-themes/print-atlas-oficial.css +7 -7
  9. package/dist/print-themes/print-azulejo-modernista.css +2 -2
  10. package/dist/print-themes/print-bauhaus.css +2 -2
  11. package/dist/print-themes/print-blocos-coloridos.css +5 -5
  12. package/dist/print-themes/print-brutalista.css +8 -8
  13. package/dist/print-themes/print-caderno.css +14 -14
  14. package/dist/print-themes/print-cartao-postal.css +5 -5
  15. package/dist/print-themes/print-cientifico.css +3 -3
  16. package/dist/print-themes/print-concretismo.css +8 -8
  17. package/dist/print-themes/print-construtivismo.css +6 -6
  18. package/dist/print-themes/print-cordel.css +6 -6
  19. package/dist/print-themes/print-dados-br.css +12 -12
  20. package/dist/print-themes/print-dashboard.css +18 -18
  21. package/dist/print-themes/print-diagrama-modernista.css +3 -3
  22. package/dist/print-themes/print-divulgacao.css +3 -3
  23. package/dist/print-themes/print-fluxo-historico.css +3 -3
  24. package/dist/print-themes/print-grade-holandesa.css +2 -2
  25. package/dist/print-themes/print-graficos-1900.css +6 -6
  26. package/dist/print-themes/print-infografico-ilustrado.css +6 -6
  27. package/dist/print-themes/print-isotype.css +2 -2
  28. package/dist/print-themes/print-jornal-1959.css +5 -5
  29. package/dist/print-themes/print-jornal.css +6 -6
  30. package/dist/print-themes/print-mapa-de-metro.css +1 -1
  31. package/dist/print-themes/print-memphis.css +5 -5
  32. package/dist/print-themes/print-minimo-de-tinta.css +4 -4
  33. package/dist/print-themes/print-papel-recortado.css +2 -2
  34. package/dist/print-themes/print-papel-salmao.css +6 -6
  35. package/dist/print-themes/print-pictogramas.css +2 -2
  36. package/dist/print-themes/print-pop-art.css +2 -2
  37. package/dist/print-themes/print-prancheta-clara.css +5 -5
  38. package/dist/print-themes/print-prancheta.css +5 -5
  39. package/dist/print-themes/print-proporcao-modular.css +2 -2
  40. package/dist/print-themes/print-riso.css +4 -4
  41. package/dist/print-themes/print-semanario.css +13 -13
  42. package/dist/print-themes/print-sinalizacao.css +8 -8
  43. package/dist/print-themes/print-suico.css +4 -4
  44. package/dist/print-themes/print-tropicalia.css +3 -3
  45. package/dist/print-themes.css +133 -133
  46. package/fonts/ibmplexsans/ibmplexsans-italic-cyrillic-400_500_600.woff2 +0 -0
  47. package/fonts/ibmplexsans/ibmplexsans-italic-cyrillic-ext-400_500_600.woff2 +0 -0
  48. package/fonts/ibmplexsans/ibmplexsans-italic-greek-400_500_600.woff2 +0 -0
  49. package/fonts/ibmplexsans/ibmplexsans-italic-latin-400_500_600.woff2 +0 -0
  50. package/fonts/ibmplexsans/ibmplexsans-italic-latin-ext-400_500_600.woff2 +0 -0
  51. package/fonts/ibmplexsans/ibmplexsans-italic-vietnamese-400_500_600.woff2 +0 -0
  52. package/fonts/manifest.json +998 -820
  53. package/fonts/nunitosans/nunitosans-normal-cyrillic-400_600_700_800.woff2 +0 -0
  54. package/fonts/nunitosans/nunitosans-normal-cyrillic-ext-400_600_700_800.woff2 +0 -0
  55. package/fonts/nunitosans/nunitosans-normal-latin-400_600_700_800.woff2 +0 -0
  56. package/fonts/nunitosans/nunitosans-normal-latin-ext-400_600_700_800.woff2 +0 -0
  57. package/fonts/nunitosans/nunitosans-normal-vietnamese-400_600_700_800.woff2 +0 -0
  58. package/fonts/sourceserif4/sourceserif4-italic-cyrillic-400_600.woff2 +0 -0
  59. package/fonts/sourceserif4/sourceserif4-italic-cyrillic-ext-400_600.woff2 +0 -0
  60. package/fonts/sourceserif4/sourceserif4-italic-greek-400_600.woff2 +0 -0
  61. package/fonts/sourceserif4/sourceserif4-italic-latin-400_600.woff2 +0 -0
  62. package/fonts/sourceserif4/sourceserif4-italic-latin-ext-400_600.woff2 +0 -0
  63. package/fonts/sourceserif4/sourceserif4-italic-vietnamese-400_600.woff2 +0 -0
  64. package/package.json +1 -1
  65. package/src/fonts.ts +2 -2
  66. package/src/index.ts +1 -0
  67. package/src/print-book-themes.ts +239 -0
  68. package/src/print-presets.ts +220 -10
  69. /package/fonts/ibmplexsans/{ibmplexsans-italic-cyrillic.woff2 → ibmplexsans-italic-cyrillic-400.woff2} +0 -0
  70. /package/fonts/ibmplexsans/{ibmplexsans-italic-cyrillic-ext.woff2 → ibmplexsans-italic-cyrillic-ext-400.woff2} +0 -0
  71. /package/fonts/ibmplexsans/{ibmplexsans-italic-greek.woff2 → ibmplexsans-italic-greek-400.woff2} +0 -0
  72. /package/fonts/ibmplexsans/{ibmplexsans-italic-latin.woff2 → ibmplexsans-italic-latin-400.woff2} +0 -0
  73. /package/fonts/ibmplexsans/{ibmplexsans-italic-latin-ext.woff2 → ibmplexsans-italic-latin-ext-400.woff2} +0 -0
  74. /package/fonts/ibmplexsans/{ibmplexsans-italic-vietnamese.woff2 → ibmplexsans-italic-vietnamese-400.woff2} +0 -0
  75. /package/fonts/sourceserif4/{sourceserif4-italic-cyrillic.woff2 → sourceserif4-italic-cyrillic-400.woff2} +0 -0
  76. /package/fonts/sourceserif4/{sourceserif4-italic-cyrillic-ext.woff2 → sourceserif4-italic-cyrillic-ext-400.woff2} +0 -0
  77. /package/fonts/sourceserif4/{sourceserif4-italic-greek.woff2 → sourceserif4-italic-greek-400.woff2} +0 -0
  78. /package/fonts/sourceserif4/{sourceserif4-italic-latin.woff2 → sourceserif4-italic-latin-400.woff2} +0 -0
  79. /package/fonts/sourceserif4/{sourceserif4-italic-latin-ext.woff2 → sourceserif4-italic-latin-ext-400.woff2} +0 -0
  80. /package/fonts/sourceserif4/{sourceserif4-italic-vietnamese.woff2 → sourceserif4-italic-vietnamese-400.woff2} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datatechsolutions/tympan-tokens",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Tympan design tokens: W3C DTCG source (colour, space, radius, type, elevation and glass, motion, z) and an OKLCH theme generator, with the tympan, fakhir, astrlabe, neutral and high-contrast presets in light and dark (plus opt-in UI themes derived from the print book styles), built to --ty-* CSS custom properties, JSON and a TypeScript export.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "author": "Natalia Mesquita",
package/src/fonts.ts CHANGED
@@ -16,8 +16,8 @@ import { SCRIPT_RULES } from './scripts.ts'
16
16
 
17
17
  /** Base stacks of every preset: Source Serif 4, IBM Plex Sans, IBM Plex Mono. */
18
18
  export const BASE_FONT_SPECS: readonly string[] = [
19
- 'Source Serif 4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400',
20
- 'IBM Plex Sans:ital,wght@0,400;0,500;0,600;0,700;1,400',
19
+ 'Source Serif 4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400;1,8..60,600',
20
+ 'IBM Plex Sans:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600',
21
21
  'IBM Plex Mono:ital,wght@0,400;0,500;0,600;0,700;1,400',
22
22
  ]
23
23
 
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from './dtcg.ts'
6
6
  export * from './stylesheet.ts'
7
7
  export * from './flow.ts'
8
8
  export * from './print-presets.ts'
9
+ export * from './print-book-themes.ts'
9
10
  export * from './print-themes.ts'
10
11
  export * from './print-aliases.ts'
11
12
  export * from './fonts.ts'
@@ -0,0 +1,239 @@
1
+ // Book themes: how a publisher or a collection defines its own look for
2
+ // @datatechsolutions/tympan-print without touching tympan's code.
3
+ //
4
+ // A theme inherits a base (one of the 39 presets or another registered theme)
5
+ // and overrides any token of PrintStyleOverrides, plus what belongs to a
6
+ // publisher rather than to a style: its marks (publisher logo, collection
7
+ // seal), the page format and bleed, the running head and folio, and local
8
+ // font files. Register it by id and pass the id to PrintBook as `estilo`.
9
+ //
10
+ // A theme may come from JSON, either in the theme's own shape
11
+ // (`PrintBookThemeJson`) or as a DTCG tree (`printThemeFromDtcg`), so a brand's
12
+ // identity can live in the brand's repository.
13
+
14
+ import { fromDtcgColor, type DtcgColor } from './dtcg.ts'
15
+ import { toHex } from './color.ts'
16
+ import { SYSTEM_FONT_FAMILIES, fontFaceCss, type FontManifest } from './fonts.ts'
17
+ import {
18
+ mergePrintStyle,
19
+ printPresetById,
20
+ resolvePrintStyleName,
21
+ type PrintCoresParciais,
22
+ type PrintPagina,
23
+ type PrintStyle,
24
+ type PrintStyleOverrides,
25
+ } from './print-presets.ts'
26
+
27
+ /** A book theme: an id, a base and overrides. */
28
+ export interface PrintBookTheme extends PrintStyleOverrides {
29
+ /** Registry id, e.g. 'lakebrasil-editorial'. Use it as PrintBook's `estilo`. */
30
+ id: string
31
+ /** A preset name (or deprecated alias) or the id of another registered theme. */
32
+ base: string
33
+ /** What the theme interprets (shown in galleries). */
34
+ referencia?: string
35
+ }
36
+
37
+ /** The JSON form of a theme: the same fields, colours as hex strings or DTCG colour objects. */
38
+ export type PrintBookThemeJson = Omit<PrintBookTheme, 'cor' | 'pb'> & {
39
+ cor?: Record<string, unknown>
40
+ pb?: Record<string, unknown>
41
+ }
42
+
43
+ const REGISTRO = new Map<string, PrintBookTheme>()
44
+
45
+ /** A style with overrides and theme extras applied (same as mergePrintStyle, kept for readability). */
46
+ export const mergePrintTheme = mergePrintStyle
47
+
48
+ /** Resolves a theme id or a preset id to a complete PrintStyle (`name` = the theme id). */
49
+ export function printThemeById(id: string, visitados: string[] = []): PrintStyle {
50
+ const tema = REGISTRO.get(id)
51
+ if (!tema) return printPresetById(id)
52
+ if (visitados.includes(id)) throw new Error(`tympan: book theme cycle ${[...visitados, id].join(' → ')}`)
53
+ const base = printThemeById(tema.base, [...visitados, id])
54
+ const { id: _id, base: _base, referencia, ...over } = tema
55
+ return { ...mergePrintTheme(base, over), name: tema.id, label: tema.label ?? base.label, referencia: referencia ?? `Tema sobre ${base.name}: ${base.referencia}` }
56
+ }
57
+
58
+ /** True when `id` names a registered theme or a preset. */
59
+ export function isPrintThemeId(id: string): boolean {
60
+ return REGISTRO.has(id) || resolvePrintStyleName(id) !== undefined
61
+ }
62
+
63
+ function normalizarCores(c: Record<string, unknown> | undefined): PrintCoresParciais | undefined {
64
+ if (!c) return undefined
65
+ const cor = (v: unknown): string => (typeof v === 'string' ? v : toHex(fromDtcgColor(v as DtcgColor)))
66
+ const out: Record<string, unknown> = {}
67
+ for (const [k, v] of Object.entries(c)) {
68
+ if ((k === 'prova' || k === 'temas' || k === 'categorias') && v && typeof v === 'object') out[k] = Object.fromEntries(Object.entries(v).map(([e, x]) => [e, cor(x)]))
69
+ else if ((k === 'ornamento' || k === 'categorica' || k === 'divergente') && Array.isArray(v)) out[k] = v.map(cor)
70
+ else out[k] = cor(v)
71
+ }
72
+ return out as PrintCoresParciais
73
+ }
74
+
75
+ /** Validates and normalises a theme read from JSON (colours may be hex or DTCG colour objects). */
76
+ export function printThemeFromJson(json: PrintBookThemeJson | string): PrintBookTheme {
77
+ const j = (typeof json === 'string' ? JSON.parse(json) : json) as PrintBookThemeJson
78
+ if (!j || typeof j.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(j.id)) throw new Error('tympan: a book theme needs an `id` in kebab case')
79
+ if (typeof j.base !== 'string') throw new Error(`tympan: book theme "${j.id}" needs a \`base\` (a preset or a registered theme)`)
80
+ const { cor, pb, ...rest } = j
81
+ const out: PrintBookTheme = { ...rest }
82
+ const c = normalizarCores(cor)
83
+ if (c) out.cor = c
84
+ const p = normalizarCores(pb)
85
+ if (p) out.pb = p
86
+ return out
87
+ }
88
+
89
+ type No = Record<string, unknown>
90
+
91
+ /** Namespaces read for the theme's meta (id/name, base/extends): tympan's own and the short one brand repos use. */
92
+ const EXTS = ['br.com.datatechsolutions.tympan', 'dev.tympan.print'] as const
93
+
94
+ /** Unwraps a DTCG group into plain values: `{ $value }` leaves become their value; `$`-keys are dropped. */
95
+ function desembrulhar(n: unknown): unknown {
96
+ if (!n || typeof n !== 'object' || Array.isArray(n)) return n
97
+ const o = n as No
98
+ if ('$value' in o) {
99
+ const v = o.$value
100
+ if (o.$type === 'fontFamily' && Array.isArray(v)) return pilhaDeFontes(v as string[])
101
+ if (o.$type === 'color' && v && typeof v === 'object') return toHex(fromDtcgColor(v as DtcgColor))
102
+ if (o.$type === 'dimension' && v && typeof v === 'object' && 'unit' in (v as No)) return emMm(v as { value: number; unit: string })
103
+ return v
104
+ }
105
+ const out: No = {}
106
+ for (const [k, v] of Object.entries(o)) if (!k.startsWith('$')) out[k] = desembrulhar(v)
107
+ return out
108
+ }
109
+
110
+ const GENERICAS = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-[a-z-]+)$/
111
+ const pilhaDeFontes = (v: string[]) => v.map((x) => (GENERICAS.test(x) || /^["']/.test(x) ? x : `"${x}"`)).join(', ')
112
+ const emMm = (v: { value: number; unit: string }) => (v.unit === 'px' ? (v.value * 25.4) / 96 : v.unit === 'pt' ? (v.value * 25.4) / 72 : v.unit === 'cm' ? v.value * 10 : v.value)
113
+
114
+ /** Figures from the meta: an object by role, or a phrase such as "tabular-nums em numero e rotulo". */
115
+ function lerAlgarismos(v: unknown): Record<string, 'tabular' | 'proporcional'> | undefined {
116
+ if (v && typeof v === 'object') return v as Record<string, 'tabular' | 'proporcional'>
117
+ if (typeof v !== 'string') return undefined
118
+ const tipo = /proporc|proportional/.test(v) ? 'proporcional' : /tabular/.test(v) ? 'tabular' : undefined
119
+ if (!tipo) return undefined
120
+ const papeis = ['numero', 'rotulo', 'corpo'].filter((p) => v.includes(p))
121
+ return Object.fromEntries((papeis.length ? papeis : ['numero']).map((p) => [p, tipo]))
122
+ }
123
+
124
+ /**
125
+ * A theme from a DTCG (2025.10) tree. The tree mirrors PrintStyleOverrides: each group is a field
126
+ * (`label`, `fontes.<role>`, `googleFonts`, `cor.<role>`, `cor.prova.<state>`, `cor.categorica.<n>`,
127
+ * `cor.divergente.<n>`, `traco.largura`, `pb.*`, `estrutura.*`, `texto.*`, `marcas.*`, `pagina.*`, `folio.*`,
128
+ * `fontesLocais`…) and each `{ "$type", "$value" }` leaf becomes its value (colours as hex or DTCG colour
129
+ * objects, `fontFamily` arrays become CSS stacks, dimensions become mm). The theme's id and base come from
130
+ * the root `$extensions["dev.tympan.print"]` (or `["br.com.datatechsolutions.tympan"]`) as `name`/`id` and
131
+ * `extends`/`base`.
132
+ */
133
+ export function printThemeFromDtcg(tree: No | string): PrintBookTheme {
134
+ const t = (typeof tree === 'string' ? JSON.parse(tree) : tree) as No
135
+ const exts = (t.$extensions ?? {}) as No
136
+ const meta = Object.assign({}, ...EXTS.map((e) => exts[e] as No | undefined).filter(Boolean).map((m) => ({ ...(m!.tema as No | undefined), ...m }))) as No
137
+ const id = (meta.id ?? meta.name) as string | undefined
138
+ const base = (meta.base ?? meta.extends) as string | undefined
139
+ if (!id || !base) throw new Error(`tympan: a DTCG book theme needs $extensions["dev.tympan.print"] with name and extends`)
140
+ const corpo = desembrulhar(t) as No
141
+ // Ordered lists written as groups ("1", "2"… or "a", "b"…) become arrays.
142
+ const cor = corpo.cor as No | undefined
143
+ for (const g of [cor, corpo.pb as No | undefined])
144
+ if (g) for (const k of ['categorica', 'divergente', 'ornamento']) if (g[k] && typeof g[k] === 'object' && !Array.isArray(g[k])) g[k] = Object.entries(g[k] as No).sort(([a], [b]) => a.localeCompare(b, 'en', { numeric: true })).map(([, v]) => v)
145
+ // Brand-repo extras in the meta: per-role weights, figures and the data palette.
146
+ const texto = { ...(corpo.texto as No | undefined) }
147
+ if (meta.pesos && typeof meta.pesos === 'object') texto.pesos = { ...(meta.pesos as No), ...(texto.pesos as No | undefined) }
148
+ const alg = lerAlgarismos(meta.algarismos)
149
+ if (alg) texto.algarismos = { ...alg, ...(texto.algarismos as No | undefined) }
150
+ if (Object.keys(texto).length) corpo.texto = texto
151
+ const dado = meta.dado as Record<string, string> | undefined
152
+ if (dado) {
153
+ const c = { ...(corpo.cor as No | undefined) }
154
+ if (dado.interesse && c.interesse === undefined) c.interesse = dado.interesse
155
+ if (dado.contexto) c.contexto = dado.contexto
156
+ if (dado.perdeu && dado.ganhou && c.divergente === undefined) c.divergente = [dado.perdeu, dado.ganhou]
157
+ corpo.cor = c
158
+ }
159
+ return printThemeFromJson({ ...corpo, id, base, ...(meta.referencia ? { referencia: meta.referencia as string } : {}) } as PrintBookThemeJson)
160
+ }
161
+
162
+ /** Registers (or replaces) a theme; accepts the theme object, its JSON or a DTCG tree. Returns the theme. */
163
+ export function registerPrintTheme(tema: PrintBookTheme | PrintBookThemeJson | No | string): PrintBookTheme {
164
+ const obj = typeof tema === 'string' ? (JSON.parse(tema) as No) : (tema as No)
165
+ const t = obj.$extensions ? printThemeFromDtcg(obj) : printThemeFromJson(obj as PrintBookThemeJson)
166
+ if (resolvePrintStyleName(t.id)) throw new Error(`tympan: book theme id "${t.id}" collides with a preset`)
167
+ REGISTRO.set(t.id, t)
168
+ printThemeById(t.id) // fails now on an unknown base or a cycle
169
+ return t
170
+ }
171
+
172
+ /** Removes a registered theme (tests, hot reload). */
173
+ export function unregisterPrintTheme(id: string): boolean {
174
+ return REGISTRO.delete(id)
175
+ }
176
+
177
+ /** Ids of the registered themes, in registration order. */
178
+ export function registeredPrintThemes(): string[] {
179
+ return [...REGISTRO.keys()]
180
+ }
181
+
182
+ /** `@font-face` rules for a style's local font files ('' when it has none). */
183
+ export function fontesLocaisCss(style: Pick<PrintStyle, 'fontesLocais'>): string {
184
+ return (style.fontesLocais ?? [])
185
+ .map(
186
+ (f) =>
187
+ `@font-face { font-family: "${f.familia.replace(/"/g, '')}"; src: url("${f.src.replace(/"/g, '%22')}") format("woff2"); font-weight: ${f.peso ?? 400}; font-style: ${f.estilo ?? 'normal'}; font-display: block; }`,
188
+ )
189
+ .join('\n')
190
+ }
191
+
192
+ /** Trim and bleed of a style (defaults 170 × 240 mm, 3 mm bleed). */
193
+ export function paginaDoEstilo(style: Pick<PrintStyle, 'pagina'>): Required<PrintPagina> {
194
+ const p = style.pagina ?? {}
195
+ return { largura: p.largura ?? 170, altura: p.altura ?? 240, sangria: p.sangria ?? 3, marcasCorte: p.marcasCorte ?? false }
196
+ }
197
+
198
+ /** A neutral example theme (registered nowhere by default): a small press over `semanario`, in two inks. */
199
+ export const TEMA_EXEMPLO_EDITORA: PrintBookTheme = {
200
+ id: 'editora-exemplo',
201
+ base: 'semanario',
202
+ label: 'Editora Exemplo · coleção ensaios',
203
+ fontes: { titulo: '"Source Serif 4", Georgia, serif' },
204
+ cor: { destaque: '#1f4e79', destaque2: '#b5562b', marcaTexto: '#e8eef5' },
205
+ pb: { destaque: '#333333', destaque2: '#8a8a8a' },
206
+ texto: { nomesProprios: 'preservar' },
207
+ folio: { folio: 'centro', cabeco: 'externo' },
208
+ pagina: { largura: 160, altura: 230, sangria: 3 },
209
+ }
210
+
211
+
212
+ /** Families named in a style's font stacks (quotes removed), in role order, without generic and system faces. */
213
+ export function familiasDoEstilo(style: Pick<PrintStyle, 'fontes'>): string[] {
214
+ const out: string[] = []
215
+ for (const pilha of Object.values(style.fontes) as string[])
216
+ for (const f of pilha.split(',').map((x) => x.trim().replace(/^["']|["']$/g, '')))
217
+ if (f && !SYSTEM_FONT_FAMILIES.includes(f) && !out.includes(f)) out.push(f)
218
+ return out
219
+ }
220
+
221
+ /** Families the style names (first of each stack) that have neither a bundled file nor a theme `fontesLocais`. */
222
+ export function familiasSemArquivo(style: Pick<PrintStyle, 'fontes' | 'fontesLocais'>, manifest: FontManifest): string[] {
223
+ const proprias = new Set((style.fontesLocais ?? []).map((f) => f.familia))
224
+ const primeiras = (Object.values(style.fontes) as string[]).map((p) => p.split(',')[0]!.trim().replace(/^["']|["']$/g, ''))
225
+ return [...new Set(primeiras)].filter((f) => !SYSTEM_FONT_FAMILIES.includes(f) && !proprias.has(f) && !manifest.families[f])
226
+ }
227
+
228
+ /**
229
+ * @font-face rules for a style from the fonts bundled in this package (`fonts/manifest.json`): every face of
230
+ * every family its font stacks name (so a mono or label family outside `googleFonts` is not forgotten), with
231
+ * URLs `<base><file>`, then the theme's own `fontesLocais`, which win for their family.
232
+ */
233
+ export function fontesEmpacotadasCss(style: Pick<PrintStyle, 'fontes' | 'googleFonts' | 'fontesLocais'>, manifest: FontManifest, base: string): string {
234
+ const proprias = new Set((style.fontesLocais ?? []).map((f) => f.familia))
235
+ const familias = new Set(familiasDoEstilo(style).filter((f) => manifest.families[f] && !proprias.has(f)))
236
+ // Every bundled spec of those families (faces are de-duplicated by fontFaceCss).
237
+ const specs = Object.keys(manifest.specs).filter((s) => familias.has(s.split(':')[0]!))
238
+ return [specs.length ? fontFaceCss(manifest, specs, base) : '', fontesLocaisCss(style)].filter(Boolean).join('\n')
239
+ }
@@ -11,7 +11,7 @@
11
11
  // white variant) into `--ty-print-*` custom properties on
12
12
  // `[data-ty-print-style="<name>"]`.
13
13
 
14
- import { luminance, parseColor, toHex, type Rgba } from './color.ts'
14
+ import { contrastRatio, luminance, oklchToRgb, parseColor, rgbToOklch, toHex, type Rgba } from './color.ts'
15
15
  import { PRINT_STYLE_ALIASES, warnDeprecatedPrintId, type PrintStyleAlias } from './print-aliases.ts'
16
16
 
17
17
  export type RenderizadorGrafico = 'limpo' | 'mao' | 'isotype' | 'gravura' | 'prancheta' | 'aquarela' | 'riso' | 'pontos'
@@ -20,7 +20,17 @@ export type EstadoProva = 'sustentada' | 'refutada' | 'nao-da-para-afirmar' | 'p
20
20
 
21
21
  export const ESTADOS_PROVA: readonly EstadoProva[] = ['sustentada', 'refutada', 'nao-da-para-afirmar', 'pendente', 'sem-dado', 'nao-testada']
22
22
 
23
- /** The word printed for each proof state (always visible, whatever the mark's shape). */
23
+ /** The word printed for each proof state in English (tympan-print picks by the book's `lang`). */
24
+ export const ROTULOS_PROVA_EN: Record<EstadoProva, string> = {
25
+ sustentada: 'Supported',
26
+ refutada: 'Refuted',
27
+ 'nao-da-para-afirmar': 'Cannot claim',
28
+ pendente: 'Pending',
29
+ 'sem-dado': 'No data',
30
+ 'nao-testada': 'Not tested',
31
+ }
32
+
33
+ /** The word printed for each proof state (always visible, whatever the mark's shape); pt-BR. */
24
34
  export const ROTULOS_PROVA: Record<EstadoProva, string> = {
25
35
  sustentada: 'Sustentada',
26
36
  refutada: 'Refutada',
@@ -52,6 +62,16 @@ export interface PrintCores {
52
62
  prova: Record<EstadoProva, string>
53
63
  /** Colours of page ornaments only (frames, tiles, waves); never data. Up to four. */
54
64
  ornamento?: string[]
65
+ /** Colours by category key (official party colours…): used by categorical maps, parliament and bars. */
66
+ categorias?: Record<string, string>
67
+ /** Colours of data themes (blocks of a panel page: economia, saude…); --ty-print-tema-<id>. */
68
+ temas?: Record<string, string>
69
+ /** The series of interest in maps and charts (default: `destaque`), when the accent of the page differs from the data ink. */
70
+ interesse?: string
71
+ /** Data palette, diverging: the low and the high side (default destaque2 × destaque). */
72
+ divergente?: [string, string]
73
+ /** Data palette, qualitative (party, category: no implied order), up to eight (default: a colour-blind-safe set). */
74
+ categorica?: string[]
55
75
  }
56
76
 
57
77
  /** Partial colours: any role, and any subset of the proof states. */
@@ -137,6 +157,16 @@ export interface PrintEstrutura {
137
157
  */
138
158
  realce?: 'serie' | 'linha'
139
159
  /** Head of each small multiple: plain title (default), a band in the highlighter colour with an arrow (Schiphol), a dark band (Vignelli), a band in the ornament colour (Aicher), each multiple on its own tinted card (Le Corbusier, Bayer). */
160
+ /**
161
+ * Long runs (one page per unit, thousands of pages): no page ornaments, no tinted panel fills, the 'bolso'
162
+ * panel as a rule instead of a filled box. The style's type, colours and frames stay.
163
+ */
164
+ economiaTinta?: boolean
165
+ /**
166
+ * Pages without a molde fill the type area: panels widen to close their row of the six columns and rows of
167
+ * panels share the free height (dashboard). A page can turn it on or off with `preencher`.
168
+ */
169
+ preencher?: boolean
140
170
  cabecaMultiplo?: 'titulo' | 'faixa' | 'faixa-escura' | 'faixa-cor' | 'caixa-cor'
141
171
  }
142
172
 
@@ -176,7 +206,7 @@ export type Moldura =
176
206
  | 'formas' // triangle, square and circle (Bauhaus)
177
207
  | 'quartos' // quarter circles in the page corners (diagrama-modernista)
178
208
 
179
- export interface PrintStyle {
209
+ export interface PrintStyle extends PrintTemaExtras {
180
210
  /** Stable id, e.g. 'jornal'. */
181
211
  name: string
182
212
  /** Human label, e.g. "Editorial de jornal". */
@@ -207,10 +237,94 @@ export interface PrintStyle {
207
237
  * has approved variations (ink, duotone, per style), see LogoDatatech.
208
238
  */
209
239
  logo?: 'cor' | 'mono'
240
+ /** Typesetting options a book may need against the style's idiom (see PrintTexto). */
241
+ texto?: PrintTexto
242
+ }
243
+
244
+ /**
245
+ * Typesetting switches for books whose content fights the style: a style in lower case (bauhaus,
246
+ * diagrama-modernista) would print "boa vista" for a proper name; a style with old-style figures would
247
+ * misalign table columns.
248
+ */
249
+ export interface PrintTexto {
250
+ /**
251
+ * 'estilo' (default): page titles and eyebrows follow the style's casing. 'preservar': the level-1 title,
252
+ * eyebrows and table cells keep the casing written in the content (proper names, codes), while panel labels
253
+ * and running heads keep the style's.
254
+ */
255
+ nomesProprios?: 'estilo' | 'preservar'
256
+ /** Figures in table cells: 'tabular' (default, lining and equal width, columns align) or 'proporcional'. */
257
+ numerosTabela?: 'tabular' | 'proporcional'
258
+ /** Font weight per role (titles, key numbers, labels, body, notes, mono), e.g. { titulo: 600, numero: 600 }. */
259
+ pesos?: Partial<Record<keyof PrintFontes, number>>
260
+ /** Figures per role: 'tabular' (equal width, lining) or 'proporcional'. Tables follow `numerosTabela`. */
261
+ algarismos?: Partial<Record<'numero' | 'rotulo' | 'corpo', 'tabular' | 'proporcional'>>
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Book-theme extras (see print-book-themes.ts)
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /** A mark the theme prints (publisher logo, collection seal): an image URL (data: URIs work offline) and its text alternative. */
269
+ export interface PrintMarca {
270
+ src: string
271
+ alt: string
272
+ /** Width in mm where the mark is placed (default 18 for the logo, 12 for the seal). */
273
+ largura?: number
274
+ /** Image used in black and white print (default: the same `src`, greyed by CSS). */
275
+ srcPb?: string
276
+ }
277
+
278
+ /** Page format and print finishing. */
279
+ export interface PrintPagina {
280
+ /** Trim width in mm (default 170). */
281
+ largura?: number
282
+ /** Trim height in mm (default 240). */
283
+ altura?: number
284
+ /** Bleed in mm added around every page when the book prints for the press (default 3). */
285
+ sangria?: number
286
+ /** Crop marks in the slug, outside the bleed (default false). */
287
+ marcasCorte?: boolean
288
+ }
289
+
290
+ /** Running head and folio. */
291
+ export interface PrintFolio {
292
+ /** Where the folio sits: outer corner (default) or centred. */
293
+ folio?: 'externo' | 'centro'
294
+ /** Where the running head sits: outer corner (default), centred, or not printed. */
295
+ cabeco?: 'externo' | 'centro' | 'nenhum'
296
+ /** Text before the folio number, e.g. "p. ". */
297
+ prefixoFolio?: string
298
+ }
299
+
300
+ /** A local font file (offline, deterministic builds and EPUB): one @font-face. */
301
+ export interface PrintFonteLocal {
302
+ /** Family name as used in the theme's font stacks. */
303
+ familia: string
304
+ /** URL or path of the .woff2 (relative to the HTML, absolute, or a data: URI). */
305
+ src: string
306
+ peso?: number | string
307
+ estilo?: 'normal' | 'italic'
308
+ }
309
+
310
+ /** What a theme adds to a preset beyond the style tokens. */
311
+ export interface PrintTemaExtras {
312
+ marcas?: { editora?: PrintMarca; selo?: PrintMarca }
313
+ pagina?: PrintPagina
314
+ folio?: PrintFolio
315
+ /** Local font files; with them PrintBook can run without network (`fontes="locais"`). */
316
+ fontesLocais?: PrintFonteLocal[]
317
+ /**
318
+ * Trace of a number (Rastro): sign it with the theme's seal (`marcas.selo`) or the lakebrasil mark, and add
319
+ * the lake version seal when the version carries a date. Default: neutral (neither).
320
+ */
321
+ rastro?: { assinatura?: 'selo' | 'lakebrasil' | 'nenhuma'; seloLake?: boolean }
322
+ /** Texts the print components write by themselves, by key (see tympan-print i18n: TEXTOS_PT / TEXTOS_EN). */
323
+ textos?: Record<string, string>
210
324
  }
211
325
 
212
326
  /** Any token of a style may be overridden by the editor (partial, nested). */
213
- export interface PrintStyleOverrides {
327
+ export interface PrintStyleOverrides extends PrintTemaExtras {
214
328
  label?: string
215
329
  fontes?: Partial<PrintFontes>
216
330
  googleFonts?: string[]
@@ -224,6 +338,7 @@ export interface PrintStyleOverrides {
224
338
  pb?: PrintCoresParciais
225
339
  estrutura?: Partial<PrintEstrutura>
226
340
  logo?: 'cor' | 'mono'
341
+ texto?: PrintTexto
227
342
  }
228
343
 
229
344
  // ---------------------------------------------------------------------------
@@ -265,8 +380,9 @@ const dashboard: PrintStyle = {
265
380
  tinta2: '#3d3933',
266
381
  tinta3: '#5f594f',
267
382
  linha: '#d6cfc2',
268
- destaque: '#1c1a17',
269
- destaque2: '#9a4f2a',
383
+ // The highlight is not the ink (a highlighted bar must stand out): rust, AA on the paper, not a party colour.
384
+ destaque: '#9a4f2a',
385
+ destaque2: '#c4b8a8',
270
386
  marcaTexto: '#f3e6cf',
271
387
  contexto: '#8f877a',
272
388
  prova: {
@@ -285,7 +401,7 @@ const dashboard: PrintStyle = {
285
401
  raio: 1.6,
286
402
  caixaAlta: false,
287
403
  pb: {},
288
- estrutura: { painel: 'nenhum', rotulo: 'letra', rotuloCaixa: 'alta', notas: 'dentro', figura: 'simples', minusculas: false },
404
+ estrutura: { painel: 'nenhum', rotulo: 'letra', rotuloCaixa: 'alta', notas: 'dentro', figura: 'simples', minusculas: false, preencher: true },
289
405
  }
290
406
 
291
407
  const graficos1900: PrintStyle = {
@@ -1476,7 +1592,26 @@ function mergeCores(base: PrintCores, over?: PrintCoresParciais): PrintCores {
1476
1592
  if (!over) return base
1477
1593
  const { prova, ...rest } = over
1478
1594
  const clean = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined)) as Partial<PrintCores>
1479
- return { ...base, ...clean, prova: { ...base.prova, ...(prova ?? {}) } }
1595
+ const temas = base.temas || clean.temas ? { temas: { ...base.temas, ...clean.temas } } : {}
1596
+ const categorias = base.categorias || clean.categorias ? { categorias: { ...base.categorias, ...clean.categorias } } : {}
1597
+ return { ...base, ...clean, ...temas, ...categorias, prova: { ...base.prova, ...(prova ?? {}) } }
1598
+ }
1599
+
1600
+ function mergeExtras(base: PrintTemaExtras, over: PrintTemaExtras): PrintTemaExtras {
1601
+ const out: PrintTemaExtras = {}
1602
+ const marcas = { ...base.marcas, ...over.marcas }
1603
+ if (Object.keys(marcas).length) out.marcas = marcas
1604
+ const pagina = { ...base.pagina, ...over.pagina }
1605
+ if (Object.keys(pagina).length) out.pagina = pagina
1606
+ const folio = { ...base.folio, ...over.folio }
1607
+ if (Object.keys(folio).length) out.folio = folio
1608
+ const fl = over.fontesLocais ?? base.fontesLocais
1609
+ if (fl) out.fontesLocais = fl
1610
+ const rs = { ...base.rastro, ...over.rastro }
1611
+ if (Object.keys(rs).length) out.rastro = rs
1612
+ const tx = { ...base.textos, ...over.textos }
1613
+ if (Object.keys(tx).length) out.textos = tx
1614
+ return out
1480
1615
  }
1481
1616
 
1482
1617
  /** A style with the editor's overrides applied (nested, partial). */
@@ -1494,9 +1629,12 @@ export function mergePrintStyle(style: PrintStyle, overrides?: PrintStyleOverrid
1494
1629
  marcaProva: overrides.marcaProva ?? style.marcaProva,
1495
1630
  raio: overrides.raio ?? style.raio,
1496
1631
  caixaAlta: overrides.caixaAlta ?? style.caixaAlta,
1497
- pb: overrides.pb ? mergeCores({ ...style.cor, ...style.pb, prova: { ...style.cor.prova, ...style.pb.prova } }, overrides.pb) : style.pb,
1632
+ // P&B adjustments stay partial: merging them over the full colour set would put colours back over the greys.
1633
+ pb: overrides.pb ? { ...style.pb, ...overrides.pb, ...(style.pb.prova || overrides.pb.prova ? { prova: { ...style.pb.prova, ...overrides.pb.prova } } : {}) } : style.pb,
1498
1634
  estrutura: { ...style.estrutura, ...overrides.estrutura },
1499
1635
  logo: overrides.logo ?? style.logo,
1636
+ ...(overrides.texto || style.texto ? { texto: { ...style.texto, ...overrides.texto } } : {}),
1637
+ ...mergeExtras(style, overrides),
1500
1638
  }
1501
1639
  }
1502
1640
 
@@ -1525,6 +1663,10 @@ export function printStyleCoresPb(style: PrintStyle): PrintCores {
1525
1663
  contexto: toGrey(g.contexto),
1526
1664
  prova: Object.fromEntries(ESTADOS_PROVA.map((e) => [e, toGrey(g.prova[e])])) as Record<EstadoProva, string>,
1527
1665
  ...(g.ornamento ? { ornamento: g.ornamento.map(toGrey) } : {}),
1666
+ ...(g.interesse ? { interesse: toGrey(g.interesse) } : {}),
1667
+ ...(g.temas ? { temas: Object.fromEntries(Object.entries(g.temas).map(([k, v]) => [k, toGrey(v)])) } : {}),
1668
+ ...(g.divergente ? { divergente: g.divergente.map(toGrey) as [string, string] } : {}),
1669
+ categorica: categoricaDoEstilo(style).map(toGrey),
1528
1670
  }
1529
1671
  return mergeCores(grey, style.pb)
1530
1672
  }
@@ -1550,7 +1692,7 @@ export function resolvePrintStyle(style: PrintStyle | PrintStyleId, options: Omi
1550
1692
  return { ...merged, cor: printStyleCoresPb(merged) }
1551
1693
  }
1552
1694
 
1553
- const kebab: Record<Exclude<keyof PrintCores, 'prova' | 'ornamento'>, string> = {
1695
+ const kebab: Record<Exclude<keyof PrintCores, 'prova' | 'ornamento' | 'divergente' | 'categorica' | 'interesse' | 'temas' | 'categorias'>, string> = {
1554
1696
  papel: 'papel',
1555
1697
  tinta: 'tinta',
1556
1698
  tinta2: 'tinta-2',
@@ -1564,6 +1706,58 @@ const kebab: Record<Exclude<keyof PrintCores, 'prova' | 'ornamento'>, string> =
1564
1706
 
1565
1707
  const num = (n: number) => String(Math.round(n * 1000) / 1000)
1566
1708
 
1709
+ /**
1710
+ * `cor` moved along OKLCH lightness (hue and chroma kept) until it reaches `min`:1 against `papel`: the
1711
+ * context grey of a theme stays visible on cream paper (areas ≥ 1.5:1, strokes ≥ 3:1).
1712
+ */
1713
+ export function contrasteMinimo(cor: string, papel: string, min: number): string {
1714
+ const p = parseColor(papel)
1715
+ if (contrastRatio(parseColor(cor), p) >= min) return toHex(parseColor(cor))
1716
+ const v = rgbToOklch(parseColor(cor))
1717
+ const passo = luminance(p) > 0.2 ? -0.005 : 0.005
1718
+ let out = parseColor(cor)
1719
+ for (let i = 0; i < 200 && contrastRatio(out, p) < min; i++) {
1720
+ v.l = Math.min(0.995, Math.max(0.02, v.l + passo))
1721
+ out = oklchToRgb(v)
1722
+ }
1723
+ return toHex(out)
1724
+ }
1725
+
1726
+ const SELETORES_PAPEL: Record<keyof PrintFontes, string> = {
1727
+ titulo: ':is(h1, h2, h3, h4, .ty-print-title, .ty-print-figure-title, [class*="-title"]:not(.ty-print-panel-title, .ty-print-block-title, .ty-print-margin-title))',
1728
+ numero: ':is(.ty-print-number-value, .ty-print-table td[data-numerica], .ty-print-parliament-total, [class*="-numero"], [class*="-number"])',
1729
+ rotulo: ':is(.ty-print-panel-title, .ty-print-block-title, .ty-print-margin-title, .ty-print-sobretitle, .ty-print-table th, .ty-print-map-legend, .ty-print-running-head, .ty-print-folio, .ty-print-letter)',
1730
+ corpo: '',
1731
+ anotacao: ':is(.ty-print-annotation, .ty-print-margin, .ty-print-callout-text)',
1732
+ mono: ':is(code, pre, .ty-print-mono, .ty-print-table td[data-mono])',
1733
+ }
1734
+
1735
+ /**
1736
+ * Unlayered rules (they win over `@layer tympan-print`) for a theme's per-role font weights and figures,
1737
+ * scoped to `seletor`; '' when the theme sets neither.
1738
+ */
1739
+ export function printTextoRolesCss(style: Pick<PrintStyle, 'texto'>, seletor: string): string {
1740
+ const out: string[] = []
1741
+ for (const [papel, peso] of Object.entries(style.texto?.pesos ?? {}) as Array<[keyof PrintFontes, number]>) {
1742
+ const s = SELETORES_PAPEL[papel]
1743
+ out.push(`${seletor}${s ? ` ${s}` : ''} { font-weight: ${peso}; }`)
1744
+ }
1745
+ for (const [papel, a] of Object.entries(style.texto?.algarismos ?? {}) as Array<[keyof PrintFontes, string]>) {
1746
+ const s = SELETORES_PAPEL[papel]
1747
+ out.push(`${seletor}${s ? ` ${s}` : ''} { font-variant-numeric: ${a === 'tabular' ? 'tabular-nums lining-nums' : 'proportional-nums'}; }`)
1748
+ }
1749
+ return out.join('\n')
1750
+ }
1751
+
1752
+ /** Qualitative palette (Okabe & Ito 2008, colour-blind safe) used when a style defines none. */
1753
+ export const CATEGORICA_PADRAO = ['#0072b2', '#e69f00', '#009e73', '#cc79a7', '#56b4e9', '#d55e00', '#f0e442', '#555555'] as const
1754
+
1755
+ /** The eight qualitative data colours of a style: its own `categorica`, completed by CATEGORICA_PADRAO. */
1756
+ export function categoricaDoEstilo(style: Pick<PrintStyle, 'cor'>): string[] {
1757
+ const own = style.cor.categorica ?? []
1758
+ return Array.from({ length: 8 }, (_, i) => own[i] ?? CATEGORICA_PADRAO[i]!)
1759
+ }
1760
+
1567
1761
  /** Custom properties of a resolved style, in a stable order. */
1568
1762
  export function printStyleVariables(style: PrintStyle): Array<[string, string]> {
1569
1763
  const vars: Array<[string, string]> = []
@@ -1581,6 +1775,22 @@ export function printStyleVariables(style: PrintStyle): Array<[string, string]>
1581
1775
  vars.push(['--ty-print-rotulo-caixa', minusculas ? 'lowercase' : rc === 'alta' ? 'uppercase' : 'none'])
1582
1776
  vars.push(['--ty-print-rotulo-variante', rc === 'versalete' ? 'small-caps' : 'normal'])
1583
1777
  vars.push(['--ty-print-rotulo-espaco', rc === 'normal' ? 'normal' : '0.07em'])
1778
+ vars.push(['--ty-print-pagina-largura', `${num(style.pagina?.largura ?? 170)}mm`])
1779
+ vars.push(['--ty-print-pagina-altura', `${num(style.pagina?.altura ?? 240)}mm`])
1780
+ const preservar = style.texto?.nomesProprios === 'preservar'
1781
+ vars.push(['--ty-print-titulo1-caixa', preservar ? 'none' : style.caixaAlta ? 'uppercase' : minusculas ? 'lowercase' : 'none'])
1782
+ vars.push(['--ty-print-sobretitulo-caixa', preservar ? 'none' : minusculas ? 'lowercase' : rc === 'alta' ? 'uppercase' : 'none'])
1783
+ vars.push(['--ty-print-celula-caixa', preservar ? 'none' : minusculas ? 'lowercase' : 'none'])
1784
+ vars.push(['--ty-print-tabela-numeros', style.texto?.numerosTabela === 'proporcional' ? 'proportional-nums lining-nums' : 'tabular-nums lining-nums'])
1785
+ for (const [k, v] of Object.entries(style.cor.temas ?? {})) vars.push([`--ty-print-tema-${k.replace(/[^a-z0-9-]/gi, '')}`, v])
1786
+ vars.push(['--ty-print-interesse', style.cor.interesse ?? style.cor.destaque])
1787
+ vars.push(['--ty-print-contexto-area', contrasteMinimo(style.cor.contexto, style.cor.papel, 1.5)])
1788
+ vars.push(['--ty-print-contexto-traco', contrasteMinimo(style.cor.contexto, style.cor.papel, 3)])
1789
+ const dv = style.cor.divergente ?? [style.cor.destaque2, style.cor.destaque]
1790
+ vars.push(['--ty-print-divergente-1', dv[0]])
1791
+ vars.push(['--ty-print-divergente-2', dv[1]])
1792
+ const cat = categoricaDoEstilo(style)
1793
+ cat.forEach((c, i) => vars.push([`--ty-print-categorica-${i + 1}`, c]))
1584
1794
  const orn = style.cor.ornamento ?? []
1585
1795
  const reserva = [style.cor.destaque, style.cor.destaque2, style.cor.tinta, style.cor.marcaTexto]
1586
1796
  for (let i = 0; i < 4; i++) vars.push([`--ty-print-ornamento-${i + 1}`, orn[i] ?? reserva[i]!])