@tekkare/romulus 0.1.134 → 0.1.136

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.135",
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,13 +152,18 @@ 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',
157
161
  range: 'all',
158
162
  sections: [],
159
163
  columns: [],
160
- options: [],
164
+ // Sans ce drapeau la page de garde taisait les sources, meme
165
+ // fournies : le generateur ne les imprime que si on les demande.
166
+ options: ['sources'],
161
167
  fileName: stampedName.value,
162
168
  layout: '',
163
169
  schedule: 'once',
@@ -168,7 +174,7 @@ async function write() {
168
174
  title: props.title || buttonLabel.value,
169
175
  subtitle: props.subtitle,
170
176
  kpi: props.kpi,
171
- sources: props.sources,
177
+ sources: props.sources.length ? props.sources : defaults.sources,
172
178
  sheets: props.sheets,
173
179
  limit: props.limit,
174
180
  },
@@ -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,29 @@ function longDate(lang) {
11
11
  day: "numeric"
12
12
  });
13
13
  }
14
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}/;
15
+ function sourceDate(source, lang) {
16
+ if (!source.date) return null;
17
+ if (typeof source.date === "string" && !ISO_DATE.test(source.date)) return source.date;
18
+ const parsed = new Date(source.date);
19
+ if (Number.isNaN(parsed.getTime())) return String(source.date);
20
+ return parsed.toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
21
+ year: "numeric",
22
+ month: "long",
23
+ day: "numeric"
24
+ });
25
+ }
26
+ function sourceLabel(source, lang) {
27
+ const date = sourceDate(source, lang);
28
+ return date ? `${source.name} (${date})` : source.name;
29
+ }
30
+ function sourcesOf(config, payload) {
31
+ const wanted = config.sections.includes("sources") || config.options.includes("sources");
32
+ return wanted ? payload.sources ?? [] : [];
33
+ }
34
+ function escapeXml(text) {
35
+ return text.replace(/[<>&"']/g, (char) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&apos;" })[char] ?? char);
36
+ }
14
37
  function rowsOf(sheet, limit) {
15
38
  return limit && sheet.data.length > limit ? sheet.data.slice(0, limit) : sheet.data;
16
39
  }
@@ -187,15 +210,11 @@ export function useRmExport() {
187
210
  (filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
188
211
  );
189
212
  }
190
- const withSources = config.sections.includes("sources") || config.options.includes("sources");
191
- if (payload.sources?.length && withSources) {
213
+ const sources = sourcesOf(config, payload);
214
+ if (sources.length) {
192
215
  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;
216
+ sources.forEach((source) => {
217
+ const date = sourceDate(source, lang);
199
218
  const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
200
219
  line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
201
220
  });
@@ -237,6 +256,16 @@ export function useRmExport() {
237
256
  );
238
257
  blocks.push(lines.join("\n"));
239
258
  });
259
+ const sources = sourcesOf(config, payload);
260
+ if (sources.length) {
261
+ blocks.push(
262
+ sources.map((source) => {
263
+ const date = sourceDate(source, lang);
264
+ const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
265
+ return `# Source : ${source.name}${update}`;
266
+ }).join("\n")
267
+ );
268
+ }
240
269
  const fileName = `${config.fileName}.csv`;
241
270
  const blob = new Blob([`\uFEFF${blocks.join("\n\n")}
242
271
  `], { type: "text/csv;charset=utf-8" });
@@ -247,7 +276,8 @@ export function useRmExport() {
247
276
  if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
248
277
  const scale = 2;
249
278
  const pad = 24;
250
- const head = config.options.includes("logo") ? 72 : 0;
279
+ const sources = sourcesOf(config, payload);
280
+ const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
251
281
  const parts = charts.map((svg) => serialize(svg));
252
282
  const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
253
283
  const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
@@ -265,6 +295,9 @@ export function useRmExport() {
265
295
  context.fillStyle = "#6C6C8A";
266
296
  context.font = "12px Figtree, Helvetica, sans-serif";
267
297
  context.fillText(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`, pad, 52);
298
+ if (sources.length) {
299
+ context.fillText(`Source${sources.length > 1 ? "s" : ""} : ${sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`, pad, 68);
300
+ }
268
301
  }
269
302
  let y = head + pad;
270
303
  for (const part of parts) {
@@ -282,7 +315,8 @@ export function useRmExport() {
282
315
  const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
283
316
  if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
284
317
  const pad = 24;
285
- const head = config.options.includes("logo") ? 72 : 0;
318
+ const sources = sourcesOf(config, payload);
319
+ const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
286
320
  const parts = charts.map((svg) => serialize(svg));
287
321
  const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
288
322
  const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
@@ -292,7 +326,7 @@ export function useRmExport() {
292
326
  y += part.height + pad;
293
327
  return markup2;
294
328
  }).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>` : "";
329
+ 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
330
  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
331
  const fileName = `${config.fileName}.svg`;
298
332
  const blob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
@@ -651,12 +685,12 @@ export function useRmExport() {
651
685
  y = doc.lastAutoTable?.finalY ?? margin;
652
686
  });
653
687
  }
654
- const withSources = config.sections.includes("sources") || config.options.includes("sources");
655
- if (payload.sources?.length && withSources) {
688
+ const sources = sourcesOf(config, payload);
689
+ if (sources.length) {
656
690
  doc.setTextColor(...grey);
657
691
  doc.setFont("helvetica", "normal");
658
692
  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 ");
693
+ const names = sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ");
660
694
  const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
661
695
  doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
662
696
  }
@@ -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.136",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",