@tekkare/romulus 0.1.178 → 0.1.179

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.177",
7
+ "version": "0.1.178",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -90,7 +90,7 @@ const emit = defineEmits<{
90
90
  const WORDS = {
91
91
  fr: {
92
92
  source: 'Source :',
93
- verified: 'Vérifié le',
93
+ verified: 'Dernière mise à jour le',
94
94
  emptyTitle: 'Aucun résultat',
95
95
  emptyText:
96
96
  'Aucune ligne ne correspond aux filtres en cours. Élargissez votre période ou retirez une dimension.',
@@ -98,7 +98,7 @@ const WORDS = {
98
98
  },
99
99
  en: {
100
100
  source: 'Source:',
101
- verified: 'Verified on',
101
+ verified: 'Last updated',
102
102
  emptyTitle: 'No results',
103
103
  emptyText:
104
104
  'No row matches the current filters. Widen your period or drop a dimension.',
@@ -31,8 +31,8 @@ const props = withDefaults(
31
31
 
32
32
  /** Memes mots que le pied de RmChartCard. */
33
33
  const WORDS = {
34
- fr: { source: 'Source :', verified: 'Vérifié le' },
35
- en: { source: 'Source:', verified: 'Verified on' },
34
+ fr: { source: 'Source :', verified: 'Dernière mise à jour le' },
35
+ en: { source: 'Source:', verified: 'Last updated' },
36
36
  } as const
37
37
  const t = computed(() => WORDS[props.lang])
38
38
 
@@ -82,6 +82,7 @@ const WORDS = {
82
82
  favorites: 'Favoris',
83
83
  actions: 'Actions',
84
84
  unsaved: 'Modifications non enregistrées',
85
+ modified: 'modifiée',
85
86
  applied: 'Vue appliquée',
86
87
  save: 'Sauvegarder',
87
88
  aria: 'Vue affichée',
@@ -92,6 +93,7 @@ const WORDS = {
92
93
  favorites: 'Favorites',
93
94
  actions: 'Actions',
94
95
  unsaved: 'Unsaved changes',
96
+ modified: 'edited',
95
97
  applied: 'View applied',
96
98
  save: 'Save',
97
99
  aria: 'Current view',
@@ -99,7 +101,15 @@ const WORDS = {
99
101
  } as const
100
102
 
101
103
  const t = computed(() => WORDS[props.lang])
102
- const label = computed(() => props.viewName || t.value.none)
104
+ /**
105
+ * Le nom de la vue, suivi de son etat quand l'ecran ne lui correspond plus.
106
+ * La pastille coloree ne suffisait pas : on lisait « Vue : GLP-1 » sur un
107
+ * ecran qui ne montrait plus les produits de GLP-1.
108
+ */
109
+ const label = computed(() => {
110
+ if (!props.viewName) return t.value.none
111
+ return props.dirty ? `${props.viewName} (${t.value.modified})` : props.viewName
112
+ })
103
113
 
104
114
  const open = ref(false)
105
115
  const root = ref<HTMLElement | null>(null)
@@ -88,6 +88,12 @@ function download(blob, fileName) {
88
88
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
89
89
  return blob.size;
90
90
  }
91
+ function titleGap(title) {
92
+ return title ? 22 : 0;
93
+ }
94
+ function drawableCharts(payload) {
95
+ return (payload.charts ?? []).map((chart) => ({ title: chart.title, svg: svgOf(chart.el) })).filter((chart) => !!chart.svg);
96
+ }
91
97
  function svgOf(el) {
92
98
  return el instanceof SVGSVGElement ? el : el.querySelector("svg");
93
99
  }
@@ -106,6 +112,21 @@ function serialize(svg) {
106
112
  if (!clone.getAttribute("viewBox")) clone.setAttribute("viewBox", `0 0 ${width} ${height}`);
107
113
  return { markup: new XMLSerializer().serializeToString(clone), width, height };
108
114
  }
115
+ async function capture(el) {
116
+ try {
117
+ const { default: html2canvas } = await import("html2canvas-pro");
118
+ const canvas = await html2canvas(el, {
119
+ scale: 2,
120
+ backgroundColor: null,
121
+ logging: false,
122
+ useCORS: true
123
+ });
124
+ if (!canvas.width || !canvas.height) return null;
125
+ return { data: canvas.toDataURL("image/png"), width: canvas.width, height: canvas.height };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
109
130
  async function raster(markup, width, height, scale = 2) {
110
131
  const image = new Image();
111
132
  image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
@@ -292,15 +313,15 @@ export function useRmExport() {
292
313
  return { fileName, rows: rowCount, bytes: download(blob, fileName) };
293
314
  }
294
315
  async function toPng(config, payload, lang) {
295
- const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
296
- if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
316
+ const drawn = drawableCharts(payload);
317
+ if (!drawn.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
297
318
  const scale = 2;
298
319
  const pad = 24;
299
320
  const sources = sourcesOf(config, payload);
300
321
  const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
301
- const parts = charts.map((svg) => serialize(svg));
322
+ const parts = drawn.map((chart) => ({ ...serialize(chart.svg), title: chart.title }));
302
323
  const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
303
- const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
324
+ const height = parts.reduce((total, part) => total + part.height + pad + titleGap(part.title), pad) + head;
304
325
  const canvas = document.createElement("canvas");
305
326
  canvas.width = width * scale;
306
327
  canvas.height = height * scale;
@@ -321,6 +342,12 @@ export function useRmExport() {
321
342
  }
322
343
  let y = head + pad;
323
344
  for (const part of parts) {
345
+ if (part.title) {
346
+ context.fillStyle = "#1A1A2E";
347
+ context.font = "bold 14px Figtree, Helvetica, sans-serif";
348
+ context.fillText(part.title, pad, y + 12);
349
+ y += titleGap(part.title);
350
+ }
324
351
  const raw = await raster(part.markup, part.width, part.height, scale);
325
352
  context.drawImage(raw, pad, y, part.width, part.height);
326
353
  y += part.height + pad;
@@ -332,19 +359,21 @@ export function useRmExport() {
332
359
  return { fileName, rows: 0, bytes: download(blob, fileName) };
333
360
  }
334
361
  async function toSvg(config, payload, lang) {
335
- const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
336
- if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
362
+ const drawn = drawableCharts(payload);
363
+ if (!drawn.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
337
364
  const pad = 24;
338
365
  const sources = sourcesOf(config, payload);
339
366
  const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
340
- const parts = charts.map((svg) => serialize(svg));
367
+ const parts = drawn.map((chart) => ({ ...serialize(chart.svg), title: chart.title }));
341
368
  const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
342
- const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
369
+ const height = parts.reduce((total, part) => total + part.height + pad + titleGap(part.title), pad) + head;
343
370
  let y = head + pad;
344
371
  const nested = parts.map((part) => {
372
+ const label = part.title ? `<text x="${pad}" y="${y + 12}" font-family="Figtree, Helvetica, sans-serif" font-size="14" font-weight="bold" fill="#1A1A2E">${escapeXml(part.title)}</text>` : "";
373
+ y += titleGap(part.title);
345
374
  const markup2 = part.markup.replace(/^<svg /, `<svg x="${pad}" y="${y}" `);
346
375
  y += part.height + pad;
347
- return markup2;
376
+ return label + markup2;
348
377
  }).join("\n");
349
378
  const header = head ? `<text x="${pad}" y="32" font-family="Figtree, Helvetica, sans-serif" font-size="20" font-weight="bold" fill="#1A1A2E">${BRAND_TITLE}</text><text x="${pad}" y="52" font-family="Figtree, Helvetica, sans-serif" font-size="12" fill="#6C6C8A">${escapeXml(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`)}</text>` + (sources.length ? `<text x="${pad}" y="68" font-family="Figtree, Helvetica, sans-serif" font-size="12" fill="#6C6C8A">${escapeXml(`Source${sources.length > 1 ? "s" : ""} : ${sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`)}</text>` : "") : "";
350
379
  const markup = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" fill="#FFFFFF"/>${header}${nested}</svg>`;
@@ -541,8 +570,47 @@ export function useRmExport() {
541
570
  );
542
571
  y += cardHeight + 6;
543
572
  }
573
+ const snapshots = (payload.nodes ?? []).filter((node) => {
574
+ if (node.kind === "chart") return false;
575
+ if (node.kind === "kpi") return config.sections.includes("kpi");
576
+ if (node.kind === "table") return config.sections.includes("table") || config.sections.includes("cards");
577
+ return config.sections.includes("cards");
578
+ });
579
+ const shots = [];
580
+ for (const node of snapshots) {
581
+ const shot = await capture(node.el);
582
+ if (shot) shots.push(shot);
583
+ }
584
+ const faithful = shots.length > 0;
585
+ if (faithful) {
586
+ sectionTitle(lang === "en" ? "Page" : "La page");
587
+ for (const shot of shots) {
588
+ let drawWidth = inner;
589
+ let drawHeight = shot.height / shot.width * drawWidth;
590
+ const maxHeight = bottom - margin;
591
+ if (drawHeight > maxHeight) {
592
+ drawHeight = maxHeight;
593
+ drawWidth = shot.width / shot.height * drawHeight;
594
+ }
595
+ if (y + drawHeight > bottom) {
596
+ doc.addPage();
597
+ y = margin;
598
+ }
599
+ doc.addImage(
600
+ shot.data,
601
+ "PNG",
602
+ margin + (inner - drawWidth) / 2,
603
+ y,
604
+ drawWidth,
605
+ drawHeight,
606
+ void 0,
607
+ "FAST"
608
+ );
609
+ y += drawHeight + 6;
610
+ }
611
+ }
544
612
  let rowCount = 0;
545
- if (config.sections.includes("cards")) {
613
+ if (config.sections.includes("cards") && !faithful) {
546
614
  const cards = cardsOf(payload, config, lang);
547
615
  if (cards.length) {
548
616
  if (y + 12 > bottom) {
@@ -754,7 +822,7 @@ export function useRmExport() {
754
822
  y += 6;
755
823
  }
756
824
  }
757
- if (config.sections.includes("table")) {
825
+ if (config.sections.includes("table") && !faithful) {
758
826
  const MAX_COLUMNS = landscape ? 16 : 10;
759
827
  payload.sheets.forEach((sheet) => {
760
828
  const all = columnsOf(sheet, config.columns);
@@ -838,6 +906,10 @@ export function useRmExport() {
838
906
  });
839
907
  }
840
908
  const pages = doc.getNumberOfPages();
909
+ const footList = sourcesOf(config, payload);
910
+ const footSources = footList.length ? printable(
911
+ `Source${footList.length > 1 ? "s" : ""} : ${footList.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`
912
+ ) : "";
841
913
  for (let page = 1; page <= pages; page += 1) {
842
914
  doc.setPage(page);
843
915
  const width = doc.internal.pageSize.getWidth();
@@ -854,6 +926,13 @@ export function useRmExport() {
854
926
  )[0] ?? "";
855
927
  doc.text(trail, margin, foot);
856
928
  doc.text(`${page} / ${pages}`, width - margin, foot, { align: "right" });
929
+ if (footSources) {
930
+ doc.text(
931
+ doc.splitTextToSize(footSources, width - margin * 2)[0] ?? "",
932
+ margin,
933
+ foot - 8.5
934
+ );
935
+ }
857
936
  }
858
937
  const fileName = `${config.fileName}.pdf`;
859
938
  const blob = doc.output("blob");
@@ -105,10 +105,14 @@ export declare function rmBaseOption(theme: RmChartTheme, decal?: boolean): {
105
105
  };
106
106
  borderColor: string;
107
107
  borderWidth: number;
108
+ appendTo: string;
109
+ confine: boolean;
110
+ enterable: boolean;
111
+ extraCssText: string;
108
112
  };
109
113
  };
110
114
  export declare const rmTooltipTrigger: (count: number) => "axis" | "item";
111
- export declare function rmLegend(theme: RmChartTheme, count: number, position?: 'top' | 'bottom'): {
115
+ export declare function rmLegend(theme: RmChartTheme, count: number, position?: 'top' | 'bottom', names?: string[]): {
112
116
  pageIconSize?: number | undefined;
113
117
  pageIconColor?: string | undefined;
114
118
  pageIconInactiveColor?: string | undefined;
@@ -207,6 +211,10 @@ export declare function rmBarVerticalOption(categories: (string | number)[], ser
207
211
  };
208
212
  borderColor: string;
209
213
  borderWidth: number;
214
+ appendTo: string;
215
+ confine: boolean;
216
+ enterable: boolean;
217
+ extraCssText: string;
210
218
  };
211
219
  legend: {
212
220
  pageIconSize?: number | undefined;
@@ -411,6 +419,10 @@ export declare function rmBarHorizontalOption(labels: string[], values: (number
411
419
  };
412
420
  borderColor: string;
413
421
  borderWidth: number;
422
+ appendTo: string;
423
+ confine: boolean;
424
+ enterable: boolean;
425
+ extraCssText: string;
414
426
  };
415
427
  };
416
428
  /** Line ou area chart. `step` produit la marche d'escalier des series de prix. */
@@ -472,6 +484,10 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
472
484
  };
473
485
  borderColor: string;
474
486
  borderWidth: number;
487
+ appendTo: string;
488
+ confine: boolean;
489
+ enterable: boolean;
490
+ extraCssText: string;
475
491
  };
476
492
  legend: {
477
493
  pageIconSize?: number | undefined;
@@ -729,6 +745,10 @@ export declare function rmDonutOption(data: {
729
745
  };
730
746
  borderColor: string;
731
747
  borderWidth: number;
748
+ appendTo: string;
749
+ confine: boolean;
750
+ enterable: boolean;
751
+ extraCssText: string;
732
752
  };
733
753
  legend: {
734
754
  pageIconSize?: number | undefined;
@@ -86,7 +86,17 @@ export function rmBaseOption(theme, decal = false) {
86
86
  backgroundColor: theme.tooltipBg,
87
87
  textStyle: { color: theme.tooltipText, fontFamily: FONT, fontSize: 12 },
88
88
  borderColor: theme.tooltipBorder,
89
- borderWidth: 1
89
+ borderWidth: 1,
90
+ // Posee par defaut DANS le conteneur du graphe, l'infobulle est rognee
91
+ // par la premiere carte a `overflow: hidden` des qu'elle en deborde :
92
+ // sur une colonne de quarante series il n'en reste qu'un morceau. Dans
93
+ // le body elle passe par-dessus la carte ; `confine` la garde ancree au
94
+ // cadre du graphe, et la hauteur plafonnee l'empeche de sortir de
95
+ // l'ecran (elle defile alors, d'ou `enterable`).
96
+ appendTo: "body",
97
+ confine: true,
98
+ enterable: true,
99
+ extraCssText: "max-height:70vh;overflow:auto;"
90
100
  }
91
101
  };
92
102
  }
@@ -96,8 +106,12 @@ const LEGEND_MAX_INLINE = 6;
96
106
  const LEGEND_NAME_MAX = 38;
97
107
  const LEGEND_NAME_TAIL = 12;
98
108
  const rmLegendName = (name) => name.length > LEGEND_NAME_MAX ? `${name.slice(0, LEGEND_NAME_MAX - LEGEND_NAME_TAIL - 1)}\u2026${name.slice(-LEGEND_NAME_TAIL)}` : name;
99
- export function rmLegend(theme, count, position = "bottom") {
100
- const scroll = count > LEGEND_MAX_INLINE;
109
+ const LEGEND_ITEM_PAD = 32;
110
+ const LEGEND_CHAR_WIDTH = 6.2;
111
+ const LEGEND_WIDTH_BUDGET = 620;
112
+ const rmLegendWidth = (names) => names.reduce((total, name) => total + LEGEND_ITEM_PAD + rmLegendName(name).length * LEGEND_CHAR_WIDTH, 0);
113
+ export function rmLegend(theme, count, position = "bottom", names = []) {
114
+ const scroll = count > LEGEND_MAX_INLINE || rmLegendWidth(names) > LEGEND_WIDTH_BUDGET;
101
115
  return {
102
116
  ...position === "bottom" ? { bottom: 0, left: "center" } : { top: 0 },
103
117
  type: scroll ? "scroll" : "plain",
@@ -144,7 +158,7 @@ export function rmBarVerticalOption(categories, series, theme, palette, decal =
144
158
  return {
145
159
  ...rmBaseOption(theme, decal),
146
160
  tooltip: { ...rmBaseOption(theme, decal).tooltip, trigger: rmTooltipTrigger(series.length) },
147
- legend: series.length > 1 ? rmLegend(theme, series.length) : void 0,
161
+ legend: series.length > 1 ? rmLegend(theme, series.length, "bottom", series.map((s) => String(s.name ?? ""))) : void 0,
148
162
  // La legende passee sous le graphe prend sa place dans la gouttiere basse,
149
163
  // sinon elle recouvre les libelles de l'axe des abscisses.
150
164
  grid: { ...rmBaseOption(theme, decal).grid, bottom: series.length > 1 ? 56 : 36 },
@@ -229,7 +243,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
229
243
  },
230
244
  // Legende obligatoire en double axe : sans elle, rien ne dit quelle courbe
231
245
  // se lit sur quel axe.
232
- legend: series.length > 1 || dual ? rmLegend(theme, series.length, legendPosition) : void 0,
246
+ legend: series.length > 1 || dual ? rmLegend(theme, series.length, legendPosition, series.map((s) => String(s.name ?? ""))) : void 0,
233
247
  grid: {
234
248
  // Sans graduation a gauche, la gouttiere de 48 px ne sert plus qu'a
235
249
  // laisser depasser la premiere pastille de valeur.
@@ -258,6 +258,15 @@ export interface RmExportPayload {
258
258
  title?: string;
259
259
  el: Element;
260
260
  }[];
261
+ /**
262
+ * Cartes de l'ecran, a capturer telles quelles pour le rapport mis en page.
263
+ *
264
+ * Le rapport reconstruisait ses tableaux et ses fiches a partir de la
265
+ * donnee : il ne ressemblait plus a la page, colonnes et mises en forme
266
+ * comprises. Une page qui donne ses noeuds obtient une capture fidele ; les
267
+ * autres gardent le rendu reconstruit.
268
+ */
269
+ nodes?: RmExportNode[];
261
270
  /** URL a copier pour le format « Permalink ». */
262
271
  permalink?: string;
263
272
  /**
@@ -269,6 +278,15 @@ export interface RmExportPayload {
269
278
  /** Plafond de lignes par onglet, pour ne pas geler l'onglet du navigateur. */
270
279
  limit?: number;
271
280
  }
281
+ /**
282
+ * Une carte de l'ecran, pour le rapport fidele. `kind` dit a quelle case du
283
+ * panneau elle repond (chiffres cles, graphique, tableau, fiche).
284
+ */
285
+ export interface RmExportNode {
286
+ kind: 'kpi' | 'chart' | 'table' | 'card';
287
+ el: Element;
288
+ title?: string;
289
+ }
272
290
  /** Extension de fichier par format. */
273
291
  export declare const RM_EXPORT_EXTENSIONS: Record<string, string>;
274
292
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.178",
3
+ "version": "0.1.179",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,6 +33,7 @@
33
33
  "@nuxt/kit": "^3.16.0",
34
34
  "@phosphor-icons/vue": "^2.2.1",
35
35
  "echarts": "^5.6.0",
36
+ "html2canvas-pro": "^1.6.7",
36
37
  "jspdf": "^4.2.0",
37
38
  "jspdf-autotable": "^5.0.7",
38
39
  "write-excel-file": "^1.4.30"