@tekkare/romulus 0.1.134 → 0.1.135

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.133",
7
+ "version": "0.1.134",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -23,7 +23,7 @@
23
23
  -->
24
24
  <script setup lang="ts">
25
25
  import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
26
- import type { RmExportPayload } from '../utils/export'
26
+ import type { RmExportPayload, RmExportSource } from '../utils/export'
27
27
  import { rmCardCharts, rmCardOf } from '../utils/cardData'
28
28
  import { useRmCardActions } from '../composables/useRmCardActions'
29
29
 
@@ -34,6 +34,12 @@ const props = withDefaults(defineProps<{
34
34
  link?: string
35
35
  /** Nom du fichier et de l'onglet. A defaut, le titre lu dans la carte. */
36
36
  name?: string
37
+ /**
38
+ * Source citee par le pied de la carte. Dernier recours : la page
39
+ * (`payload.sources`) puis le cadre (`provideRmExportDefaults`) passent
40
+ * devant, parce qu'eux lisent le referentiel et sa date de mise a jour.
41
+ */
42
+ sources?: RmExportSource[]
37
43
  /**
38
44
  * Ce que la page a de mieux que le rendu de la carte : ses onglets complets
39
45
  * (`sheets`), mais aussi ses chiffres cles, ses sources, son sous-titre. Ce
@@ -54,6 +60,7 @@ const props = withDefaults(defineProps<{
54
60
  lang: 'fr',
55
61
  link: undefined,
56
62
  name: undefined,
63
+ sources: undefined,
57
64
  payload: undefined,
58
65
  visual: undefined,
59
66
  })
@@ -168,6 +175,7 @@ async function act(action: 'png' | 'svg' | 'csv' | 'xlsx' | 'link') {
168
175
  lang: props.lang,
169
176
  name: props.name,
170
177
  link: props.link,
178
+ sources: props.sources,
171
179
  payload: props.payload,
172
180
  })
173
181
  if (action === 'link') {
@@ -11,6 +11,7 @@
11
11
  -->
12
12
  <script setup lang="ts">
13
13
  import { computed } from 'vue'
14
+ import type { RmExportPayload } from '../utils/export'
14
15
  import { rmDate } from '../utils/format'
15
16
 
16
17
  const props = withDefaults(defineProps<{
@@ -37,6 +38,12 @@ const props = withDefaults(defineProps<{
37
38
  * qui est le bon lien pour une carte d'analyse : elle vit dans sa page.
38
39
  */
39
40
  link?: string
41
+ /**
42
+ * Ce que la page a de mieux que le rendu de la carte pour son export :
43
+ * onglets complets, sources, sous-titre (cf. RmCardActions). En fonction
44
+ * des qu'une liste monte la carte plusieurs fois.
45
+ */
46
+ payload?: Partial<RmExportPayload> | (() => Partial<RmExportPayload> | null | undefined | Promise<Partial<RmExportPayload> | null | undefined>)
40
47
  /**
41
48
  * Etat de la donnee. Les trois variantes se posent dans le CORPS ; la tete
42
49
  * et le pied restent stables, pour que le titre et la source restent sous
@@ -56,6 +63,7 @@ const props = withDefaults(defineProps<{
56
63
  extra: undefined,
57
64
  actions: true,
58
65
  link: undefined,
66
+ payload: undefined,
59
67
  state: 'ready',
60
68
  retryLabel: '',
61
69
  emptyActionLabel: '',
@@ -135,6 +143,8 @@ const verifiedText = computed(() => {
135
143
  :lang="lang"
136
144
  :link="link"
137
145
  :name="context ? `${title} (${context})` : title"
146
+ :sources="[{ name: source, date: verifiedText }]"
147
+ :payload="payload"
138
148
  @export-png="emit('exportPng')"
139
149
  @export-svg="emit('exportSvg')"
140
150
  @export-csv="emit('exportCsv')"
@@ -42,6 +42,7 @@
42
42
  <script setup lang="ts">
43
43
  import { computed, ref } from 'vue'
44
44
  import { useRmExport } from '../composables/useRmExport'
45
+ import { rmExportDefaults } from '../composables/useRmExportDefaults'
45
46
  import type { RmExportKpi, RmExportSheet, RmExportSource } from '../utils/export'
46
47
 
47
48
  const props = withDefaults(
@@ -151,6 +152,9 @@ async function write() {
151
152
  if (held.length) await Promise.all(held)
152
153
 
153
154
  const { run } = useRmExport()
155
+ // Sans sources declarees, celles du cadre : la page les cite deja dans
156
+ // son panneau « Sources », le classeur ne doit pas sortir sans.
157
+ const defaults = rmExportDefaults()
154
158
  const result = await run(
155
159
  {
156
160
  format: 'xlsx',
@@ -168,7 +172,7 @@ async function write() {
168
172
  title: props.title || buttonLabel.value,
169
173
  subtitle: props.subtitle,
170
174
  kpi: props.kpi,
171
- sources: props.sources,
175
+ sources: props.sources.length ? props.sources : defaults.sources,
172
176
  sheets: props.sheets,
173
177
  limit: props.limit,
174
178
  },
@@ -12,7 +12,7 @@
12
12
  * d'une declaration de la page : c'est ce qui rend le menu utilisable sur les
13
13
  * cartes deja ecrites, sans les rouvrir une par une.
14
14
  */
15
- import type { RmExportPayload } from '../utils/export.js';
15
+ import type { RmExportPayload, RmExportSource } from '../utils/export.js';
16
16
  /** Les actions qu'une carte propose. `link` copie, il ne telecharge pas. */
17
17
  export type RmCardAction = 'png' | 'svg' | 'csv' | 'xlsx' | 'link';
18
18
  export type RmCardPayload = Partial<RmExportPayload>;
@@ -24,6 +24,13 @@ export interface RmCardActionContext {
24
24
  name?: string;
25
25
  /** Lien copie par « Copier le lien direct ». A defaut, l'URL courante. */
26
26
  link?: string;
27
+ /**
28
+ * Ce que le pied de la carte dit de sa source. Ne sert que si ni la page
29
+ * (`payload`) ni le cadre (`provideRmExportDefaults`) n'ont mieux : le cadre
30
+ * lit le referentiel des sources, le pied d'une carte peut n'en porter
31
+ * qu'un libelle.
32
+ */
33
+ sources?: RmExportSource[];
27
34
  /**
28
35
  * Ce que la page a de mieux que le rendu a l'ecran : ses onglets complets,
29
36
  * ses chiffres cles, ses sources, son sous-titre. Ce qui est pose ici
@@ -1,5 +1,6 @@
1
1
  import { rmCardCharts, rmCardSheets, rmCardTitle } from "../utils/cardData.js";
2
2
  import { useRmExport } from "./useRmExport.js";
3
+ import { rmExportDefaults } from "./useRmExportDefaults.js";
3
4
  const FILE_NAME_MAX = 60;
4
5
  function fileNameOf(label) {
5
6
  const flat = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -42,15 +43,19 @@ export function useRmCardActions() {
42
43
  async function runCardAction(action, context) {
43
44
  const { card, lang = "fr" } = context;
44
45
  const provided = await (typeof context.payload === "function" ? context.payload() : context.payload) ?? {};
46
+ const defaults = rmExportDefaults();
45
47
  const title = context.name || provided.title || rmCardTitle(card) || "Export";
48
+ const sources = provided.sources?.length ? provided.sources : defaults.sources?.length ? defaults.sources : context.sources;
46
49
  const format = action === "link" ? "link" : action;
47
50
  const sheets = action === "png" || action === "svg" ? [] : provided.sheets?.length ? provided.sheets : await rmCardSheets(card, title, lang);
48
51
  return run(configOf(format, fileNameOf(title)), {
52
+ ...defaults,
49
53
  title,
50
54
  charts: rmCardCharts(card).map((el) => ({ title, el })),
51
55
  permalink: absolute(context.link),
52
56
  ...provided,
53
- sheets
57
+ sheets,
58
+ sources
54
59
  }, lang);
55
60
  }
56
61
  async function toggleCardFullscreen(card) {
@@ -11,6 +11,27 @@ function longDate(lang) {
11
11
  day: "numeric"
12
12
  });
13
13
  }
14
+ function sourceDate(source, lang) {
15
+ if (!source.date) return null;
16
+ const parsed = new Date(source.date);
17
+ if (Number.isNaN(parsed.getTime())) return String(source.date);
18
+ return parsed.toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
19
+ year: "numeric",
20
+ month: "long",
21
+ day: "numeric"
22
+ });
23
+ }
24
+ function sourceLabel(source, lang) {
25
+ const date = sourceDate(source, lang);
26
+ return date ? `${source.name} (${date})` : source.name;
27
+ }
28
+ function sourcesOf(config, payload) {
29
+ const wanted = config.sections.includes("sources") || config.options.includes("sources");
30
+ return wanted ? payload.sources ?? [] : [];
31
+ }
32
+ function escapeXml(text) {
33
+ return text.replace(/[<>&"']/g, (char) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&apos;" })[char] ?? char);
34
+ }
14
35
  function rowsOf(sheet, limit) {
15
36
  return limit && sheet.data.length > limit ? sheet.data.slice(0, limit) : sheet.data;
16
37
  }
@@ -187,15 +208,11 @@ export function useRmExport() {
187
208
  (filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
188
209
  );
189
210
  }
190
- const withSources = config.sections.includes("sources") || config.options.includes("sources");
191
- if (payload.sources?.length && withSources) {
211
+ const sources = sourcesOf(config, payload);
212
+ if (sources.length) {
192
213
  rows.push(filler());
193
- payload.sources.forEach((source) => {
194
- const date = source.date ? new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
195
- year: "numeric",
196
- month: "long",
197
- day: "numeric"
198
- }) : null;
214
+ sources.forEach((source) => {
215
+ const date = sourceDate(source, lang);
199
216
  const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
200
217
  line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
201
218
  });
@@ -237,6 +254,16 @@ export function useRmExport() {
237
254
  );
238
255
  blocks.push(lines.join("\n"));
239
256
  });
257
+ const sources = sourcesOf(config, payload);
258
+ if (sources.length) {
259
+ blocks.push(
260
+ sources.map((source) => {
261
+ const date = sourceDate(source, lang);
262
+ const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
263
+ return `# Source : ${source.name}${update}`;
264
+ }).join("\n")
265
+ );
266
+ }
240
267
  const fileName = `${config.fileName}.csv`;
241
268
  const blob = new Blob([`\uFEFF${blocks.join("\n\n")}
242
269
  `], { type: "text/csv;charset=utf-8" });
@@ -247,7 +274,8 @@ export function useRmExport() {
247
274
  if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
248
275
  const scale = 2;
249
276
  const pad = 24;
250
- const head = config.options.includes("logo") ? 72 : 0;
277
+ const sources = sourcesOf(config, payload);
278
+ const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
251
279
  const parts = charts.map((svg) => serialize(svg));
252
280
  const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
253
281
  const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
@@ -265,6 +293,9 @@ export function useRmExport() {
265
293
  context.fillStyle = "#6C6C8A";
266
294
  context.font = "12px Figtree, Helvetica, sans-serif";
267
295
  context.fillText(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`, pad, 52);
296
+ if (sources.length) {
297
+ context.fillText(`Source${sources.length > 1 ? "s" : ""} : ${sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`, pad, 68);
298
+ }
268
299
  }
269
300
  let y = head + pad;
270
301
  for (const part of parts) {
@@ -282,7 +313,8 @@ export function useRmExport() {
282
313
  const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
283
314
  if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
284
315
  const pad = 24;
285
- const head = config.options.includes("logo") ? 72 : 0;
316
+ const sources = sourcesOf(config, payload);
317
+ const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
286
318
  const parts = charts.map((svg) => serialize(svg));
287
319
  const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
288
320
  const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
@@ -292,7 +324,7 @@ export function useRmExport() {
292
324
  y += part.height + pad;
293
325
  return markup2;
294
326
  }).join("\n");
295
- 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">${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}</text>` : "";
327
+ 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>` : "") : "";
296
328
  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>`;
297
329
  const fileName = `${config.fileName}.svg`;
298
330
  const blob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
@@ -651,12 +683,12 @@ export function useRmExport() {
651
683
  y = doc.lastAutoTable?.finalY ?? margin;
652
684
  });
653
685
  }
654
- const withSources = config.sections.includes("sources") || config.options.includes("sources");
655
- if (payload.sources?.length && withSources) {
686
+ const sources = sourcesOf(config, payload);
687
+ if (sources.length) {
656
688
  doc.setTextColor(...grey);
657
689
  doc.setFont("helvetica", "normal");
658
690
  doc.setFontSize(8);
659
- const names = payload.sources.map((source) => `${source.name}${source.date ? ` (${new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", { year: "numeric", month: "long", day: "numeric" })})` : ""}`).join(" \xB7 ");
691
+ const names = sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ");
660
692
  const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
661
693
  doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
662
694
  }
@@ -0,0 +1,9 @@
1
+ import type { RmExportPayload } from '../utils/export.js';
2
+ export type RmExportDefaultsBuild = () => Partial<RmExportPayload> | null | undefined;
3
+ /**
4
+ * Pose les defauts de la page affichee. A appeler dans le `setup` du cadre
5
+ * (layout) : ils sont retires quand ce cadre est demonte.
6
+ */
7
+ export declare function provideRmExportDefaults(build: RmExportDefaultsBuild): void;
8
+ /** Les defauts courants, relus a chaque export : ils suivent la navigation. */
9
+ export declare function rmExportDefaults(): Partial<RmExportPayload>;
@@ -0,0 +1,13 @@
1
+ import { getCurrentScope, onScopeDispose, shallowRef } from "vue";
2
+ const registry = shallowRef(null);
3
+ export function provideRmExportDefaults(build) {
4
+ registry.value = build;
5
+ if (getCurrentScope()) {
6
+ onScopeDispose(() => {
7
+ if (registry.value === build) registry.value = null;
8
+ });
9
+ }
10
+ }
11
+ export function rmExportDefaults() {
12
+ return registry.value?.() ?? {};
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.134",
3
+ "version": "0.1.135",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",