@tekkare/romulus 0.1.50 → 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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/CardActions.vue +229 -0
- package/dist/runtime/components/ChartCard.vue +27 -77
- package/dist/runtime/components/Drawer.vue +25 -2
- package/dist/runtime/components/ExportPanel.vue +95 -25
- package/dist/runtime/components/FilterComplex.vue +135 -28
- package/dist/runtime/components/FilterDropdown.vue +53 -17
- package/dist/runtime/components/Modal.vue +29 -4
- package/dist/runtime/components/ResultCard.vue +44 -3
- package/dist/runtime/components/SidebarLeft.vue +21 -3
- package/dist/runtime/components/TableCard.vue +28 -2
- package/dist/runtime/components/ViewPill.vue +2 -2
- package/dist/runtime/composables/useChartTheme.d.ts +1 -1
- package/dist/runtime/composables/useChartTheme.js +7 -6
- package/dist/runtime/composables/useRmCardActions.d.ts +32 -0
- package/dist/runtime/composables/useRmCardActions.js +53 -0
- package/dist/runtime/composables/useRmDialog.d.ts +27 -0
- package/dist/runtime/composables/useRmDialog.js +64 -0
- package/dist/runtime/composables/useRmExport.js +32 -11
- package/dist/runtime/composables/useTheme.d.ts +6 -1
- package/dist/runtime/composables/useTheme.js +23 -1
- package/dist/runtime/plugins/a11yFilters.client.d.ts +2 -0
- package/dist/runtime/plugins/a11yFilters.client.js +30 -0
- package/dist/runtime/utils/cardData.d.ts +41 -0
- package/dist/runtime/utils/cardData.js +138 -0
- package/dist/runtime/utils/export.d.ts +17 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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,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
|
+
}
|
|
@@ -155,14 +155,30 @@ export interface RmExportTemplate {
|
|
|
155
155
|
color?: string;
|
|
156
156
|
config: Partial<RmExportConfig>;
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Ligne de l'onglet « Mes exports ».
|
|
160
|
+
*
|
|
161
|
+
* Une entree sans `id` est un simple historique : elle se lit, rien de plus.
|
|
162
|
+
* Une entree avec `id` est conservee par l'app, donc rejouable — le panneau
|
|
163
|
+
* emet alors `rerun` / `forget`, et c'est l'app qui sait quoi en faire.
|
|
164
|
+
*/
|
|
159
165
|
export interface RmExportRecent {
|
|
166
|
+
/** Identifiant cote app. Absent : l'entree n'est qu'un historique. */
|
|
167
|
+
id?: string;
|
|
160
168
|
name: string;
|
|
161
169
|
format: string;
|
|
162
170
|
rows?: number;
|
|
163
171
|
size?: string;
|
|
164
172
|
/** Date de l'export, ISO ou Date. */
|
|
165
173
|
at: string | Date;
|
|
174
|
+
/**
|
|
175
|
+
* Ecran d'origine, nomme pour les entrees venues d'ailleurs. Le panneau ne
|
|
176
|
+
* connait qu'une page a la fois : un export tire d'un autre ecran ne peut
|
|
177
|
+
* pas se rejouer ici, il s'y ouvre.
|
|
178
|
+
*/
|
|
179
|
+
pageLabel?: string;
|
|
180
|
+
/** Faux : l'entree vient d'un autre ecran, elle s'ouvre au lieu de partir. */
|
|
181
|
+
runnable?: boolean;
|
|
166
182
|
}
|
|
167
183
|
/**
|
|
168
184
|
* Ce que les generateurs consomment. La page fournit le contenu, le wizard
|