@tekkare/romulus 0.1.51 → 0.1.52

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.
@@ -139,7 +139,7 @@ onBeforeUnmount(() => {
139
139
  :aria-expanded="open"
140
140
  @click="open = !open"
141
141
  >
142
- <RmIcon name="BookmarkSimple" weight="fill" :size="16" class="rm-vpill__icon" />
142
+ <RmIcon name="BookmarkSimple" :size="16" class="rm-vpill__icon" />
143
143
  <span class="rm-vpill__key">{{ t.key }}</span>
144
144
  <span class="rm-vpill__value">{{ label }}</span>
145
145
  <span v-if="dirty" class="rm-vpill__dot" :title="t.unsaved" aria-hidden="true" />
@@ -194,5 +194,5 @@ onBeforeUnmount(() => {
194
194
  </template>
195
195
 
196
196
  <style scoped>
197
- .rm-vpill{position:relative}.rm-vpill,.rm-vpill__pill{align-items:center;display:flex}.rm-vpill__pill{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h,32px);padding:0 14px;white-space:nowrap}.rm-vpill__pill:hover{background:var(--bg-surface-3)}.rm-vpill__pill.is-open{border-color:var(--color-primary);color:var(--color-primary)}.rm-vpill__icon{color:var(--color-secondary);flex-shrink:0}.rm-vpill__key{color:var(--text-muted);font-weight:var(--weight-normal)}.rm-vpill__value{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-vpill__pill.has-unsaved{border-color:var(--color-warning)}.rm-vpill__dot{animation:rm-vpill-pulse 2s ease-out infinite;background:var(--color-warning);border-radius:50%;flex-shrink:0;height:8px;width:8px}@keyframes rm-vpill-pulse{0%{box-shadow:0 0 0 0 rgba(217,119,6,.6)}70%{box-shadow:0 0 0 6px rgba(217,119,6,0)}to{box-shadow:0 0 0 0 rgba(217,119,6,0)}}@media (prefers-reduced-motion:reduce){.rm-vpill__dot,.rm-vpill__unsaved-dot:before{animation:none}}.rm-vpill__caret{color:var(--text-muted);flex-shrink:0}.rm-vpill__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);box-sizing:border-box;font-family:var(--font-sans);left:0;max-width:460px;min-width:320px;padding:6px;position:absolute;top:calc(100% + 6px);z-index:60}.rm-vpill__unsaved{align-items:center;background:var(--color-warning-bg);border-radius:var(--radius-md);color:var(--color-warning);display:flex;font-size:var(--font-12);gap:8px;padding:8px 12px}.rm-vpill__unsaved-dot{align-items:center;display:inline-flex;flex-shrink:0;height:14px;justify-content:center;width:14px}.rm-vpill__unsaved-dot:before{animation:rm-vpill-pulse 2s ease-out infinite;background:var(--color-warning);border-radius:50%;content:"";height:7px;width:7px}.rm-vpill__unsaved-label{flex:1;font-weight:var(--weight-semibold)}.rm-vpill__unsaved-cta{background:transparent;border:none;color:var(--color-warning);cursor:pointer;font-family:inherit;font-size:inherit;font-weight:var(--weight-bold);padding:0;text-decoration:underline;text-underline-offset:2px}.rm-vpill__unsaved-cta:hover{color:var(--text-primary)}.rm-vpill__divider{background:var(--border-soft);height:1px;margin:6px -6px}.rm-vpill__section{color:var(--text-muted);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.08em;margin:0;padding:8px 12px 4px 34px;text-align:left;text-transform:uppercase}.rm-vpill__item{align-items:center;background:none;border:none;border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:8px;padding:8px 12px;text-align:left;transition:background .1s,color .1s;width:100%}.rm-vpill__item:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-vpill__item.is-active{background:var(--bg-hover);color:var(--color-secondary);font-weight:var(--weight-semibold)}.rm-vpill__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-vpill__item--view .rm-vpill__check{color:var(--color-secondary);flex-shrink:0;opacity:0;transition:opacity .1s}.rm-vpill__item--view.is-active .rm-vpill__check{opacity:1}
197
+ .rm-vpill{position:relative}.rm-vpill,.rm-vpill__pill{align-items:center;display:flex}.rm-vpill__pill{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h,32px);padding:0 14px;white-space:nowrap}.rm-vpill__pill:hover{background:var(--bg-surface-3)}.rm-vpill__pill.is-open{border-color:var(--color-primary);color:var(--color-primary)}.rm-vpill__icon{color:currentColor;flex-shrink:0}.rm-vpill__key{color:var(--text-muted);font-weight:var(--weight-normal)}.rm-vpill__value{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-vpill__pill.has-unsaved{border-color:var(--color-warning)}.rm-vpill__dot{animation:rm-vpill-pulse 2s ease-out infinite;background:var(--color-warning);border-radius:50%;flex-shrink:0;height:8px;width:8px}@keyframes rm-vpill-pulse{0%{box-shadow:0 0 0 0 rgba(217,119,6,.6)}70%{box-shadow:0 0 0 6px rgba(217,119,6,0)}to{box-shadow:0 0 0 0 rgba(217,119,6,0)}}@media (prefers-reduced-motion:reduce){.rm-vpill__dot,.rm-vpill__unsaved-dot:before{animation:none}}.rm-vpill__caret{color:var(--text-muted);flex-shrink:0}.rm-vpill__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);box-sizing:border-box;font-family:var(--font-sans);left:0;max-width:460px;min-width:320px;padding:6px;position:absolute;top:calc(100% + 6px);z-index:60}.rm-vpill__unsaved{align-items:center;background:var(--color-warning-bg);border-radius:var(--radius-md);color:var(--color-warning);display:flex;font-size:var(--font-12);gap:8px;padding:8px 12px}.rm-vpill__unsaved-dot{align-items:center;display:inline-flex;flex-shrink:0;height:14px;justify-content:center;width:14px}.rm-vpill__unsaved-dot:before{animation:rm-vpill-pulse 2s ease-out infinite;background:var(--color-warning);border-radius:50%;content:"";height:7px;width:7px}.rm-vpill__unsaved-label{flex:1;font-weight:var(--weight-semibold)}.rm-vpill__unsaved-cta{background:transparent;border:none;color:var(--color-warning);cursor:pointer;font-family:inherit;font-size:inherit;font-weight:var(--weight-bold);padding:0;text-decoration:underline;text-underline-offset:2px}.rm-vpill__unsaved-cta:hover{color:var(--text-primary)}.rm-vpill__divider{background:var(--border-soft);height:1px;margin:6px -6px}.rm-vpill__section{color:var(--text-muted);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.08em;margin:0;padding:8px 12px 4px 34px;text-align:left;text-transform:uppercase}.rm-vpill__item{align-items:center;background:none;border:none;border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:8px;padding:8px 12px;text-align:left;transition:background .1s,color .1s;width:100%}.rm-vpill__item:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-vpill__item.is-active{background:var(--bg-hover);color:var(--color-secondary);font-weight:var(--weight-semibold)}.rm-vpill__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-vpill__item--view .rm-vpill__check{color:var(--color-secondary);flex-shrink:0;opacity:0;transition:opacity .1s}.rm-vpill__item--view.is-active .rm-vpill__check{opacity:1}
198
198
  </style>
@@ -1,6 +1,6 @@
1
1
  export declare function useChartTheme(): {
2
2
  night: import("vue").Ref<boolean, boolean>;
3
- cvd: import("vue").Ref<boolean, boolean>;
3
+ cvdSafe: import("vue").Ref<boolean, boolean>;
4
4
  theme: import("vue").ComputedRef<import("../utils/chart").RmChartTheme>;
5
5
  palette: import("vue").ComputedRef<import("../utils/chart").RmPalette>;
6
6
  decal: import("vue").ComputedRef<boolean>;
@@ -1,15 +1,16 @@
1
1
  import { computed, onBeforeUnmount, onMounted, ref } from "vue";
2
2
  import { rmChartTheme, rmPalette } from "../utils/chart.js";
3
- const CVD_CLASSES = ["cvd-deutera", "cvd-protan", "cvd-trita", "cvd-mono"];
4
3
  export function useChartTheme() {
5
4
  const night = ref(false);
6
- const cvd = ref(false);
5
+ const cvdSafe = ref(false);
6
+ const decalOn = ref(false);
7
7
  let observer = null;
8
8
  function read() {
9
9
  if (typeof document === "undefined") return;
10
10
  const list = document.body.classList;
11
11
  night.value = list.contains("night");
12
- cvd.value = CVD_CLASSES.some((c) => list.contains(c));
12
+ cvdSafe.value = list.contains("pal-cvd-safe");
13
+ decalOn.value = list.contains("chart-decal");
13
14
  }
14
15
  onMounted(() => {
15
16
  read();
@@ -21,7 +22,7 @@ export function useChartTheme() {
21
22
  observer = null;
22
23
  });
23
24
  const theme = computed(() => rmChartTheme(night.value));
24
- const palette = computed(() => rmPalette(cvd.value ? "cvdSafe" : "default", night.value));
25
- const decal = computed(() => cvd.value);
26
- return { night, cvd, theme, palette, decal };
25
+ const palette = computed(() => rmPalette(cvdSafe.value ? "cvdSafe" : "default", night.value));
26
+ const decal = computed(() => decalOn.value || cvdSafe.value);
27
+ return { night, cvdSafe, theme, palette, decal };
27
28
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * useRmCardActions - ce que fait le menu en haut a droite d'une carte.
3
+ *
4
+ * Le menu est le meme sur toutes les cartes du portail, donc son travail vit
5
+ * dans le paquet : deux apps ne peuvent pas sortir le PNG d'un graphique avec
6
+ * deux en-tetes differents, ni nommer leurs fichiers autrement. Les fichiers
7
+ * sont produits par les generateurs du panneau d'export (`useRmExport`), pas
8
+ * par une seconde chaine parallele : la carte n'est qu'un perimetre plus
9
+ * etroit que la page.
10
+ *
11
+ * La donnee, elle, est relue dans le DOM de la carte (`rmCardSheets`) faute
12
+ * d'une declaration de la page : c'est ce qui rend le menu utilisable sur les
13
+ * cartes deja ecrites, sans les rouvrir une par une.
14
+ */
15
+ import type { RmExportSheet } from '../utils/export.js';
16
+ /** Les actions qu'une carte propose. `link` copie, il ne telecharge pas. */
17
+ export type RmCardAction = 'png' | 'svg' | 'csv' | 'xlsx' | 'link';
18
+ export interface RmCardActionContext {
19
+ /** La carte visee. */
20
+ card: Element;
21
+ lang?: 'fr' | 'en';
22
+ /** Nom du fichier et de l'onglet. A defaut, le titre lu dans la carte. */
23
+ name?: string;
24
+ /** Lien copie par « Copier le lien direct ». A defaut, l'URL courante. */
25
+ link?: string;
26
+ /** Onglets fournis par la page, quand elle a mieux que le rendu a l'ecran. */
27
+ sheets?: RmExportSheet[];
28
+ }
29
+ export declare function useRmCardActions(): {
30
+ runCardAction: (action: RmCardAction, context: RmCardActionContext) => Promise<import("./useRmExport").RmExportResult>;
31
+ toggleCardFullscreen: (card: Element) => Promise<void>;
32
+ };
@@ -0,0 +1,53 @@
1
+ import { rmCardCharts, rmCardSheets, rmCardTitle } from "../utils/cardData.js";
2
+ import { useRmExport } from "./useRmExport.js";
3
+ const FILE_NAME_MAX = 60;
4
+ function fileNameOf(label) {
5
+ const flat = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6
+ if (!flat) return "export";
7
+ if (flat.length <= FILE_NAME_MAX) return flat;
8
+ const cut = flat.slice(0, FILE_NAME_MAX);
9
+ return cut.slice(0, cut.lastIndexOf("-") + 1 || FILE_NAME_MAX).replace(/-+$/, "");
10
+ }
11
+ function configOf(format, fileName) {
12
+ return {
13
+ format,
14
+ range: "all",
15
+ sections: ["table", "charts", "kpi", "sources"],
16
+ columns: [],
17
+ options: ["logo", "sources"],
18
+ fileName,
19
+ layout: "table",
20
+ schedule: "once",
21
+ frequency: "",
22
+ email: false
23
+ };
24
+ }
25
+ function absolute(link) {
26
+ if (!link) return void 0;
27
+ if (typeof window === "undefined") return link;
28
+ return new URL(link, window.location.href).href;
29
+ }
30
+ export function useRmCardActions() {
31
+ const { run } = useRmExport();
32
+ async function runCardAction(action, context) {
33
+ const { card, lang = "fr" } = context;
34
+ const title = context.name || rmCardTitle(card) || "Export";
35
+ const format = action === "link" ? "link" : action;
36
+ const sheets = action === "png" || action === "svg" ? [] : context.sheets?.length ? context.sheets : await rmCardSheets(card, title, lang);
37
+ return run(configOf(format, fileNameOf(title)), {
38
+ title,
39
+ sheets,
40
+ charts: rmCardCharts(card).map((el) => ({ title, el })),
41
+ permalink: absolute(context.link)
42
+ }, lang);
43
+ }
44
+ async function toggleCardFullscreen(card) {
45
+ if (typeof document === "undefined") return;
46
+ if (document.fullscreenElement) {
47
+ await document.exitFullscreen();
48
+ return;
49
+ }
50
+ await card.requestFullscreen?.();
51
+ }
52
+ return { runCardAction, toggleCardFullscreen };
53
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Comportement clavier commun aux surfaces bloquantes (modale, tiroir).
3
+ *
4
+ * La checklist d'accessibilite du design system demande trois choses d'une
5
+ * surface qui recouvre la page, et aucune ne s'obtient en CSS :
6
+ *
7
+ * 1. `Escape` la ferme. Sans ca, un utilisateur au clavier qui ouvre une
8
+ * modale par erreur doit retrouver la croix a la souris.
9
+ * 2. Le focus entre dedans a l'ouverture et n'en sort plus au `Tab`. Sinon le
10
+ * focus continue de parcourir la page derriere, invisible sous le voile :
11
+ * l'utilisateur tabule dans le vide.
12
+ * 3. Le focus revient a l'element qui a ouvert la surface a la fermeture,
13
+ * pour ne pas renvoyer au debut du document.
14
+ *
15
+ * `aria-modal="true"` dit aux lecteurs d'ecran d'ignorer le reste de la page ;
16
+ * il ne deplace pas le focus clavier, d'ou le piege ci-dessous.
17
+ */
18
+ import { type Ref } from 'vue';
19
+ export interface RmDialogOptions {
20
+ /** Surface ouverte. */
21
+ open: Ref<boolean>;
22
+ /** Racine de la surface, portee par un `ref` de template. */
23
+ surface: Ref<HTMLElement | null>;
24
+ /** Appele par `Escape`. */
25
+ close: () => void;
26
+ }
27
+ export declare function useRmDialog({ open, surface, close }: RmDialogOptions): void;
@@ -0,0 +1,64 @@
1
+ import { nextTick, onBeforeUnmount, watch } from "vue";
2
+ const FOCUSABLE = [
3
+ "a[href]",
4
+ "button:not([disabled])",
5
+ "input:not([disabled])",
6
+ "select:not([disabled])",
7
+ "textarea:not([disabled])",
8
+ '[tabindex]:not([tabindex="-1"])'
9
+ ].join(",");
10
+ function focusables(root) {
11
+ return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement);
12
+ }
13
+ export function useRmDialog({ open, surface, close }) {
14
+ let opener = null;
15
+ function onKeydown(event) {
16
+ if (event.key === "Escape") {
17
+ event.stopPropagation();
18
+ close();
19
+ return;
20
+ }
21
+ if (event.key !== "Tab") return;
22
+ const root = surface.value;
23
+ if (!root) return;
24
+ const items = focusables(root);
25
+ if (!items.length) {
26
+ event.preventDefault();
27
+ root.focus();
28
+ return;
29
+ }
30
+ const first = items[0];
31
+ const last = items[items.length - 1];
32
+ const active = document.activeElement;
33
+ if (event.shiftKey && (active === first || active === root)) {
34
+ event.preventDefault();
35
+ last.focus();
36
+ } else if (!event.shiftKey && active === last) {
37
+ event.preventDefault();
38
+ first.focus();
39
+ }
40
+ }
41
+ function bind() {
42
+ document.addEventListener("keydown", onKeydown, true);
43
+ }
44
+ function unbind() {
45
+ document.removeEventListener("keydown", onKeydown, true);
46
+ }
47
+ watch(open, async (isOpen) => {
48
+ if (typeof document === "undefined") return;
49
+ if (isOpen) {
50
+ opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
51
+ bind();
52
+ await nextTick();
53
+ const root = surface.value;
54
+ if (!root) return;
55
+ const items = focusables(root);
56
+ (items[0] ?? root).focus();
57
+ return;
58
+ }
59
+ unbind();
60
+ if (opener?.isConnected) opener.focus();
61
+ opener = null;
62
+ });
63
+ onBeforeUnmount(unbind);
64
+ }
@@ -87,8 +87,22 @@ async function raster(markup, width, height, scale = 2) {
87
87
  context.drawImage(image, 0, 0, canvas.width, canvas.height);
88
88
  return canvas;
89
89
  }
90
+ const PRINTABLE_SUBSTITUTES = [
91
+ [/€/g, "EUR"],
92
+ [/[’‘]/g, "'"],
93
+ [/[“”]/g, '"'],
94
+ [/[–—]/g, "-"],
95
+ [/…/g, "..."],
96
+ [/≥/g, ">="],
97
+ [/≤/g, "<="],
98
+ [/[ ]/g, " "]
99
+ ];
90
100
  function printable(text) {
91
- return text.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
101
+ const substituted = PRINTABLE_SUBSTITUTES.reduce(
102
+ (out, [pattern, replacement]) => out.replace(pattern, replacement),
103
+ text
104
+ );
105
+ return substituted.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
92
106
  }
93
107
  function cardsOf(payload, config, lang) {
94
108
  if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ?? 100);
@@ -355,17 +369,16 @@ export function useRmExport() {
355
369
  doc.setTextColor(...ink);
356
370
  doc.setFont("helvetica", "bold");
357
371
  doc.setFontSize(10);
358
- doc.text(lang === "en" ? "Active filters" : "Filtres actifs", margin, y);
372
+ doc.text(lang === "en" ? "Reading this view" : "Lecture de la vue", margin, y);
359
373
  y += 5;
360
374
  doc.setFont("helvetica", "normal");
361
375
  doc.setFontSize(9);
362
376
  doc.setTextColor(...grey);
363
- payload.filters.forEach((filter) => {
364
- const lines = doc.splitTextToSize(printable(`${filter.label} : ${filter.value}`), inner);
365
- doc.text(lines, margin, y);
366
- y += lines.length * 4.5;
367
- });
368
- y += 4;
377
+ const applied = payload.filters.map((filter) => `${filter.label} = ${filter.value}`).join(" ; ");
378
+ const legend = lang === "en" ? `This report reproduces the page as it was displayed, with the following filters: ${applied}.` : `Ce rapport reprend la page telle qu'elle \xE9tait affich\xE9e, avec les filtres suivants : ${applied}.`;
379
+ const lines = doc.splitTextToSize(printable(legend), inner);
380
+ doc.text(lines, margin, y);
381
+ y += lines.length * 4.5 + 4;
369
382
  }
370
383
  const charts = config.sections.includes("charts") ? payload.charts ?? [] : [];
371
384
  for (const chart of charts) {
@@ -468,13 +481,13 @@ export function useRmExport() {
468
481
  }
469
482
  }
470
483
  if (config.sections.includes("table")) {
471
- const MAX_COLUMNS = 8;
484
+ const MAX_COLUMNS = landscape ? 16 : 10;
472
485
  payload.sheets.forEach((sheet) => {
473
486
  const all = columnsOf(sheet, config.columns);
474
487
  const columns = all.slice(0, MAX_COLUMNS);
475
488
  const rows = rowsOf(sheet, payload.limit);
476
489
  if (!config.sections.includes("cards")) rowCount += rows.length;
477
- doc.addPage("a4", "landscape");
490
+ doc.addPage();
478
491
  const wide = doc.internal.pageSize.getWidth() - margin * 2;
479
492
  doc.setTextColor(...ink);
480
493
  doc.setFont("helvetica", "bold");
@@ -496,7 +509,15 @@ export function useRmExport() {
496
509
  startY: margin + 10,
497
510
  head: [columns.map((column) => printable(column.column))],
498
511
  body: rows.map((row) => columns.map((column) => printable(cellText(column.value(row), lang)))),
499
- styles: { fontSize: 6.5, cellPadding: 1.4, overflow: "linebreak", textColor: ink, minCellWidth: wide / columns.length / 2 },
512
+ // Passe huit colonnes, la police et les marges se resserrent : c'est
513
+ // ce qui fait tenir un suivi mensuel complet sans casser les mots.
514
+ styles: {
515
+ fontSize: columns.length > 8 ? 5.5 : 6.5,
516
+ cellPadding: columns.length > 8 ? 1 : 1.4,
517
+ overflow: "linebreak",
518
+ textColor: ink,
519
+ minCellWidth: wide / columns.length / 2
520
+ },
500
521
  headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold", valign: "middle" },
501
522
  alternateRowStyles: { fillColor: [250, 250, 253] },
502
523
  margin: { left: margin, right: margin, top: margin, bottom: margin },
@@ -1,4 +1,5 @@
1
- export type RmCvdMode = 'none' | 'deutera' | 'protan' | 'trita' | 'mono';
1
+ export type RmCvdMode = 'none' | 'deutera' | 'protan' | 'tritan' | 'mono';
2
+ export type RmChartPalette = 'default' | 'cvdSafe';
2
3
  export type RmTextScale = 'normal' | 'large' | 'xl';
3
4
  export declare function useRomulusTheme(): {
4
5
  night: Readonly<import("vue").Ref<boolean, boolean>>;
@@ -7,11 +8,15 @@ export declare function useRomulusTheme(): {
7
8
  compact: Readonly<import("vue").Ref<boolean, boolean>>;
8
9
  cvd: Readonly<import("vue").Ref<RmCvdMode, RmCvdMode>>;
9
10
  textScale: Readonly<import("vue").Ref<RmTextScale, RmTextScale>>;
11
+ chartPalette: Readonly<import("vue").Ref<RmChartPalette, RmChartPalette>>;
12
+ chartDecal: Readonly<import("vue").Ref<boolean, boolean>>;
10
13
  setNight: (value: boolean) => void;
11
14
  setHighContrast: (value: boolean) => void;
12
15
  setReduceMotion: (value: boolean) => void;
13
16
  setCompact: (value: boolean) => void;
14
17
  setCvd: (mode: RmCvdMode) => void;
15
18
  setTextScale: (scale: RmTextScale) => void;
19
+ setChartPalette: (palette: RmChartPalette) => void;
20
+ setChartDecal: (value: boolean) => void;
16
21
  reset: () => void;
17
22
  };
@@ -3,13 +3,15 @@ import { rmWithoutTransitions } from "../utils/transitionGuard.js";
3
3
  const CVD_CLASSES = {
4
4
  deutera: "cvd-deutera",
5
5
  protan: "cvd-protan",
6
- trita: "cvd-trita",
6
+ tritan: "cvd-tritan",
7
7
  mono: "cvd-mono"
8
8
  };
9
9
  const TEXT_CLASSES = {
10
10
  large: "text-large",
11
11
  xl: "text-xl"
12
12
  };
13
+ const PALETTE_CLASS = "pal-cvd-safe";
14
+ const DECAL_CLASS = "chart-decal";
13
15
  export function useRomulusTheme() {
14
16
  const night = ref(false);
15
17
  const highContrast = ref(false);
@@ -17,6 +19,8 @@ export function useRomulusTheme() {
17
19
  const compact = ref(false);
18
20
  const cvd = ref("none");
19
21
  const textScale = ref("normal");
22
+ const chartPalette = ref("default");
23
+ const chartDecal = ref(false);
20
24
  const apply = rmWithoutTransitions;
21
25
  const setNight = (value) => apply(() => {
22
26
  night.value = value;
@@ -46,6 +50,14 @@ export function useRomulusTheme() {
46
50
  Object.values(TEXT_CLASSES).forEach((c) => body.classList.remove(c));
47
51
  if (scale !== "normal") body.classList.add(TEXT_CLASSES[scale]);
48
52
  });
53
+ const setChartPalette = (palette) => apply(() => {
54
+ chartPalette.value = palette;
55
+ document.body.classList.toggle(PALETTE_CLASS, palette === "cvdSafe");
56
+ });
57
+ const setChartDecal = (value) => apply(() => {
58
+ chartDecal.value = value;
59
+ document.body.classList.toggle(DECAL_CLASS, value);
60
+ });
49
61
  const reset = () => apply(() => {
50
62
  const { body } = document;
51
63
  night.value = false;
@@ -54,11 +66,15 @@ export function useRomulusTheme() {
54
66
  compact.value = false;
55
67
  cvd.value = "none";
56
68
  textScale.value = "normal";
69
+ chartPalette.value = "default";
70
+ chartDecal.value = false;
57
71
  body.classList.remove(
58
72
  "night",
59
73
  "high-contrast",
60
74
  "reduce-motion",
61
75
  "density-compact",
76
+ PALETTE_CLASS,
77
+ DECAL_CLASS,
62
78
  ...Object.values(CVD_CLASSES),
63
79
  ...Object.values(TEXT_CLASSES)
64
80
  );
@@ -72,6 +88,8 @@ export function useRomulusTheme() {
72
88
  const found = Object.entries(CVD_CLASSES).find(([, c]) => list.contains(c));
73
89
  cvd.value = found ? found[0] : "none";
74
90
  textScale.value = list.contains("text-xl") ? "xl" : list.contains("text-large") ? "large" : "normal";
91
+ chartPalette.value = list.contains(PALETTE_CLASS) ? "cvdSafe" : "default";
92
+ chartDecal.value = list.contains(DECAL_CLASS);
75
93
  });
76
94
  return {
77
95
  night: readonly(night),
@@ -80,12 +98,16 @@ export function useRomulusTheme() {
80
98
  compact: readonly(compact),
81
99
  cvd: readonly(cvd),
82
100
  textScale: readonly(textScale),
101
+ chartPalette: readonly(chartPalette),
102
+ chartDecal: readonly(chartDecal),
83
103
  setNight,
84
104
  setHighContrast,
85
105
  setReduceMotion,
86
106
  setCompact,
87
107
  setCvd,
88
108
  setTextScale,
109
+ setChartPalette,
110
+ setChartDecal,
89
111
  reset
90
112
  };
91
113
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,30 @@
1
+ import { defineNuxtPlugin } from "#app";
2
+ const ROOT_ID = "rm-a11y-filters";
3
+ const FILTERS = [
4
+ ["a11y-deutera", "0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"],
5
+ ["a11y-protan", "0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"],
6
+ ["a11y-tritan", "0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"],
7
+ ["a11y-mono", "0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0"]
8
+ ];
9
+ const SVG_NS = "http://www.w3.org/2000/svg";
10
+ export default defineNuxtPlugin(() => {
11
+ if (document.getElementById(ROOT_ID)) return;
12
+ const svg = document.createElementNS(SVG_NS, "svg");
13
+ svg.setAttribute("id", ROOT_ID);
14
+ svg.setAttribute("width", "0");
15
+ svg.setAttribute("height", "0");
16
+ svg.setAttribute("aria-hidden", "true");
17
+ svg.style.position = "absolute";
18
+ const defs = document.createElementNS(SVG_NS, "defs");
19
+ for (const [id, values] of FILTERS) {
20
+ const filter = document.createElementNS(SVG_NS, "filter");
21
+ filter.setAttribute("id", id);
22
+ const matrix = document.createElementNS(SVG_NS, "feColorMatrix");
23
+ matrix.setAttribute("type", "matrix");
24
+ matrix.setAttribute("values", values);
25
+ filter.appendChild(matrix);
26
+ defs.appendChild(filter);
27
+ }
28
+ svg.appendChild(defs);
29
+ document.body.appendChild(svg);
30
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Ce qu'une carte porte, relu dans son DOM au moment ou l'utilisateur demande
3
+ * un export depuis son menu d'actions.
4
+ *
5
+ * Le menu vit sur toutes les cartes du portail (graphique, tableau, groupe de
6
+ * KPI, carte de resultat) et il n'y en a pas deux qui declarent leur donnee de
7
+ * la meme facon. Plutot que d'imposer a chaque page de redeclarer ce qu'elle
8
+ * affiche deja (et de laisser les deux diverger a la premiere retouche), on
9
+ * relit le rendu : une carte devient exportable sans une ligne dans la page.
10
+ *
11
+ * Une page qui a mieux a offrir (la donnee complete, les nombres bruts) passe
12
+ * ses propres onglets a `RmCardActions` via `sheets` ; ce module n'est que le
13
+ * defaut.
14
+ *
15
+ * Deux regles communes :
16
+ *
17
+ * - les valeurs d'un tableau sont prises telles qu'elles sont ecrites a
18
+ * l'ecran, mise en forme comprise. Celles d'un graphique viennent de son
19
+ * option ECharts, donc en nombres : c'est la seule source qui les porte
20
+ * encore brutes ;
21
+ * - un graphe dessine dans une cellule (courbe de tendance d'une ligne) est
22
+ * une decoration du tableau : il ne fait pas un export a lui seul.
23
+ */
24
+ import type { RmExportSheet } from './export.js';
25
+ /** La carte qui contient un noeud, quel que soit le composant qui la rend. */
26
+ export declare function rmCardOf(el: Element | null): HTMLElement | null;
27
+ /**
28
+ * Titre d'une carte. Le contexte (l'annee, le secteur) est repris avec lui :
29
+ * trois cartes de classement ne different que par lui, et deux fichiers nommes
30
+ * pareil ne se distinguent plus dans un dossier de telechargement.
31
+ */
32
+ export declare function rmCardTitle(card: Element): string;
33
+ /** Les graphes exportables d'une carte, vides tant que la donnee n'est pas rendue. */
34
+ export declare function rmCardCharts(card: Element): HTMLElement[];
35
+ /**
36
+ * Ce qu'une carte peut mettre dans un classeur ou un CSV, par ordre de
37
+ * fidelite : son tableau s'il en a un, sinon ses graphes, sinon ses chiffres
38
+ * cles, sinon ses champs. Un seul de ces registres est retenu : un tableau et
39
+ * le graphe qui le resume portent la meme donnee deux fois.
40
+ */
41
+ export declare function rmCardSheets(card: Element, name: string, lang?: 'fr' | 'en'): Promise<RmExportSheet[]>;
@@ -0,0 +1,138 @@
1
+ const CARD_SELECTOR = ".chart-card, .rm-table-card, .rm-rcard";
2
+ const CHART_SELECTOR = ".rm-chart .echart";
3
+ const WORDS = {
4
+ fr: { label: "Libell\xE9", value: "Valeur", series: "S\xE9rie", table: "Tableau", field: "Champ", chart: "Graphique", title: "Intitul\xE9", subtitle: "Compl\xE9ment", source: "Provenance" },
5
+ en: { label: "Label", value: "Value", series: "Series", table: "Table", field: "Field", chart: "Chart", title: "Name", subtitle: "Details", source: "Provenance" }
6
+ };
7
+ function cellText(el) {
8
+ return (el?.textContent ?? "").replace(/\s+/g, " ").trim();
9
+ }
10
+ export function rmCardOf(el) {
11
+ return el?.closest(CARD_SELECTOR) ?? null;
12
+ }
13
+ export function rmCardTitle(card) {
14
+ const title = cellText(card.querySelector(".chart-card-title")) || cellText(card.querySelector(".rm-table-card__title")) || cellText(card.querySelector(".rm-rcard__title"));
15
+ if (!title) return "";
16
+ const context = cellText(card.querySelector(".chart-card-context"));
17
+ return context ? `${title} (${context})` : title;
18
+ }
19
+ export function rmCardCharts(card) {
20
+ return [...card.querySelectorAll(CHART_SELECTOR)].filter(
21
+ (holder) => holder.querySelector("svg") && !holder.closest("table")
22
+ );
23
+ }
24
+ function sheetOf(name, columns, rows) {
25
+ return {
26
+ name,
27
+ schema: columns.map((column, index) => ({
28
+ column,
29
+ value: (row) => row?.[index] ?? null
30
+ })),
31
+ data: rows
32
+ };
33
+ }
34
+ function tableSheets(card, name, lang) {
35
+ const t = WORDS[lang];
36
+ return [...card.querySelectorAll("table")].flatMap((table, index) => {
37
+ const head = [...table.querySelectorAll("thead th")].map((th) => cellText(th));
38
+ const rows = [...table.querySelectorAll("tbody tr")].map((tr) => [...tr.querySelectorAll("th, td")].map((cell) => cellText(cell))).filter((cells) => cells.some((cell) => cell !== ""));
39
+ if (!rows.length) return [];
40
+ const columns = head.length ? head : rows[0].map((_, position) => `${t.value} ${position + 1}`);
41
+ const sheetName = index === 0 ? name : `${name} (${t.table} ${index + 1})`;
42
+ return [sheetOf(sheetName, columns, rows)];
43
+ });
44
+ }
45
+ function pointValue(point) {
46
+ if (point === null || point === void 0) return null;
47
+ if (Array.isArray(point)) return point[point.length - 1] ?? null;
48
+ if (typeof point === "object") return point.value ?? null;
49
+ return point;
50
+ }
51
+ function pointName(point) {
52
+ if (point && typeof point === "object" && !Array.isArray(point)) {
53
+ return String(point.name ?? "");
54
+ }
55
+ if (Array.isArray(point)) return String(point[0] ?? "");
56
+ return "";
57
+ }
58
+ function axisCategories(option) {
59
+ const axes = [...option.xAxis ?? [], ...option.yAxis ?? []];
60
+ const category = axes.find((axis) => axis?.type === "category" && Array.isArray(axis.data) && axis.data.length);
61
+ return category ? category.data : null;
62
+ }
63
+ async function chartSheets(card, name, lang) {
64
+ const holders = rmCardCharts(card);
65
+ if (!holders.length) return [];
66
+ const t = WORDS[lang];
67
+ const echarts = await import("echarts");
68
+ return holders.flatMap((holder, index) => {
69
+ const instance = echarts.getInstanceByDom(holder);
70
+ if (!instance) return [];
71
+ const option = instance.getOption();
72
+ const series = (option.series ?? []).filter((entry) => Array.isArray(entry?.data));
73
+ if (!series.length) return [];
74
+ const sheetName = index === 0 ? name : `${name} (${t.chart} ${index + 1})`;
75
+ const categories = axisCategories(option);
76
+ if (categories) {
77
+ const columns = [t.label, ...series.map((entry, position) => String(entry.name || `${t.series} ${position + 1}`))];
78
+ const rows2 = categories.map((category, line) => [
79
+ category,
80
+ ...series.map((entry) => pointValue(entry.data[line]))
81
+ ]);
82
+ return [sheetOf(sheetName, columns, rows2)];
83
+ }
84
+ const flat = series[0];
85
+ const rows = flat.data.map((point) => [pointName(point), pointValue(point)]).filter((row) => row[0] !== "");
86
+ if (!rows.length) return [];
87
+ return [sheetOf(sheetName, [t.label, String(flat.name || t.value)], rows)];
88
+ });
89
+ }
90
+ function kpiSheets(card, name, lang) {
91
+ const t = WORDS[lang];
92
+ const rows = [...card.querySelectorAll(".rm-kpibar__item, .kpi-card")].map((item) => [
93
+ [
94
+ cellText(item.querySelector(".rm-kpibar__legend, .kpi-label")),
95
+ cellText(item.querySelector(".rm-kpibar__subtitle, .kpi-foot"))
96
+ ].filter(Boolean).join(", "),
97
+ cellText(item.querySelector(".rm-kpibar__value, .kpi-value"))
98
+ ]).filter((row) => row[1] !== "");
99
+ return rows.length ? [sheetOf(name, [t.label, t.value], rows)] : [];
100
+ }
101
+ function resultSheets(card, name, lang) {
102
+ const t = WORDS[lang];
103
+ const rows = [];
104
+ const push = (label, value) => {
105
+ if (value) rows.push([label, value]);
106
+ };
107
+ push(t.title, rmCardTitle(card));
108
+ push(t.subtitle, cellText(card.querySelector(".rm-rcard__subtitle")));
109
+ const count = cellText(card.querySelector(".rm-rcard__count-value"));
110
+ if (count) {
111
+ push(
112
+ cellText(card.querySelector(".rm-rcard__count-label")) || t.value,
113
+ [count, cellText(card.querySelector(".rm-rcard__count-extra"))].filter(Boolean).join(" \xB7 ")
114
+ );
115
+ }
116
+ card.querySelectorAll(".rm-rcard__meta > *").forEach((item) => {
117
+ const text = cellText(item);
118
+ const [head, ...rest] = text.split("\xB7");
119
+ if (rest.length) push(head.trim(), rest.join("\xB7").trim());
120
+ else push(t.field, text);
121
+ });
122
+ card.querySelectorAll(".rm-rcard__main p, .rm-rcard__main li").forEach((item) => {
123
+ if (item.closest(".rm-rcard__meta") || item.classList.contains("rm-rcard__more")) return;
124
+ push(t.field, cellText(item));
125
+ });
126
+ push(t.source, cellText(card.querySelector(".rm-rcard__footer")));
127
+ return rows.length ? [sheetOf(name, [t.field, t.value], rows)] : [];
128
+ }
129
+ export async function rmCardSheets(card, name, lang = "fr") {
130
+ const label = name || rmCardTitle(card) || WORDS[lang].table;
131
+ const tables = tableSheets(card, label, lang);
132
+ if (tables.length) return tables;
133
+ const charts = await chartSheets(card, label, lang);
134
+ if (charts.length) return charts;
135
+ const kpis = kpiSheets(card, label, lang);
136
+ if (kpis.length) return kpis;
137
+ return resultSheets(card, label, lang);
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",