@tekkare/romulus 0.1.210 → 0.1.212

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.
@@ -229,6 +229,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
229
229
  legendPosition = "bottom",
230
230
  startAtZero = false,
231
231
  valueFormatter,
232
+ labelFormatter,
232
233
  tooltipFormatter
233
234
  } = options;
234
235
  const labelled = valueLabels ?? series.length <= RM_LINE_LABEL_MAX_SERIES;
@@ -310,7 +311,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
310
311
  backgroundColor: color,
311
312
  padding: [3, 5],
312
313
  borderRadius: 2,
313
- formatter: (p) => p.value === null || p.value === void 0 ? "" : format(p.value)
314
+ formatter: (p) => p.value === null || p.value === void 0 ? "" : labelFormatter ? labelFormatter(p.value, p.dataIndex, i) : format(p.value)
314
315
  } : void 0,
315
316
  lineStyle: { color, width: 2.5 },
316
317
  itemStyle: { color },
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Tableaux de bord a widgets (RmDashboard) : le modele enregistre, le
3
+ * contrat d'un widget et les regles pures que la coque, l'editeur et
4
+ * l'export partagent.
5
+ *
6
+ * Un widget n'est pas une requete libre : c'est une entree d'un catalogue
7
+ * (`RmWidgetKind`) que l'app declare, reglee par quelques parametres et
8
+ * affichee sous une des visualisations qu'elle permet. L'entree sait aller
9
+ * chercher sa donnee et la rendre sous une des quatre formes normalisees
10
+ * (series, parts, chiffres cles, lignes) ; le paquet ne connait que ces
11
+ * formes, jamais la source. C'est ce qui rend le tableau de bord deployable
12
+ * dans toute app : elle n'ecrit que son catalogue et deux appels d'API.
13
+ *
14
+ * Les marques d'un widget sont celles du contexte par defaut (le projet, la
15
+ * vue...) : un produit ajoute plus tard apparait sans retoucher le tableau.
16
+ * Une liste explicite dans `params.brands` fige le choix.
17
+ */
18
+ import type { RmExportSheet, RmExportSource } from './export.js';
19
+ export type RmDashboardLang = 'fr' | 'en';
20
+ export type RmDashboardViz = 'line' | 'bar' | 'table' | 'kpi' | 'donut' | 'cards';
21
+ export interface RmDashboardLayout {
22
+ x: number;
23
+ y: number;
24
+ w: number;
25
+ h: number;
26
+ }
27
+ /** Un widget tel qu'il s'enregistre : une entree du catalogue, ses reglages, sa place. */
28
+ export interface RmDashboardWidget {
29
+ id: string;
30
+ kind: string;
31
+ viz: RmDashboardViz;
32
+ /** Surcharge du titre de l'entree. */
33
+ title?: string;
34
+ params: Record<string, unknown>;
35
+ layout: RmDashboardLayout;
36
+ }
37
+ /**
38
+ * Le tableau enregistre, envoye en entier a chaque geste. L'API ne connait
39
+ * pas le catalogue : un `kind` disparu de l'app est ignore a l'affichage et
40
+ * garde sa place dans l'enregistrement.
41
+ */
42
+ export interface RmDashboardModel {
43
+ version: 1;
44
+ widgets: RmDashboardWidget[];
45
+ }
46
+ export interface RmWidgetSeriesData {
47
+ type: 'series';
48
+ categories: string[];
49
+ series: {
50
+ name: string;
51
+ data: (number | null)[];
52
+ }[];
53
+ }
54
+ export interface RmWidgetPartsData {
55
+ type: 'parts';
56
+ parts: {
57
+ name: string;
58
+ value: number;
59
+ }[];
60
+ }
61
+ export interface RmWidgetKpiData {
62
+ type: 'kpi';
63
+ items: {
64
+ label: string;
65
+ value: number;
66
+ previous: number | null;
67
+ trend?: number;
68
+ }[];
69
+ /** « juin 2026 vs juin 2025 » : la periode lue, que la tuile doit dire. */
70
+ period: string;
71
+ }
72
+ /**
73
+ * `optional` : colonne proposee mais masquee tant qu'on ne la coche pas.
74
+ * `long` : texte de longueur imprevisible (une indication), coupe a trois
75
+ * lignes avec « Lire plus », sur une colonne assez large pour que trois
76
+ * lignes disent quelque chose.
77
+ */
78
+ export interface RmWidgetColumn {
79
+ key: string;
80
+ label: string;
81
+ numeric?: boolean;
82
+ date?: boolean;
83
+ optional?: boolean;
84
+ long?: boolean;
85
+ }
86
+ /** Une liste de documents (avis, autorisations, prix). */
87
+ export interface RmWidgetRowsData {
88
+ type: 'rows';
89
+ columns: RmWidgetColumn[];
90
+ rows: Record<string, string | number | null>[];
91
+ /**
92
+ * Lecture en cartes : `kind` nomme le rendu que l'app fournit par le slot
93
+ * `#cards` de RmDashboard, `items` a la forme que ce rendu attend.
94
+ */
95
+ cards?: {
96
+ kind: string;
97
+ items: unknown[];
98
+ };
99
+ }
100
+ export type RmWidgetData = (RmWidgetSeriesData | RmWidgetPartsData | RmWidgetKpiData | RmWidgetRowsData) & {
101
+ /** Unite des valeurs : « € », ou rien pour un nombre de boites. */
102
+ unit?: string;
103
+ /** Avertissement a poser sous le contenu (troncature, marque sans donnee). */
104
+ note?: string;
105
+ };
106
+ export interface RmWidgetOption {
107
+ value: string;
108
+ label: Record<RmDashboardLang, string>;
109
+ }
110
+ export interface RmWidgetParam {
111
+ key: string;
112
+ label: Record<RmDashboardLang, string>;
113
+ options: RmWidgetOption[];
114
+ }
115
+ /** Ce que l'ecran donne a ses widgets. Une app l'etend avec ce qui lui est propre. */
116
+ export interface RmWidgetContext {
117
+ /** Marques retenues pour ce widget, deja filtrees par `rmWidgetBrands`. */
118
+ brands: string[];
119
+ lang: RmDashboardLang;
120
+ }
121
+ export interface RmWidgetKind<C extends RmWidgetContext = RmWidgetContext> {
122
+ kind: string;
123
+ /**
124
+ * Jeux cites en pied de carte, par identifiant de source (uuid Directus
125
+ * dans les apps OIP). Vide : la donnee est celle de l'ecran lui-meme.
126
+ */
127
+ sources: string[];
128
+ /** Groupe du catalogue, pour le tiroir d'ajout. */
129
+ group: Record<RmDashboardLang, string>;
130
+ title: Record<RmDashboardLang, string>;
131
+ description: Record<RmDashboardLang, string>;
132
+ /** Visualisations permises, la premiere par defaut. */
133
+ viz: RmDashboardViz[];
134
+ /** Le widget lit les marques : il propose le choix des marques. */
135
+ usesBrands: boolean;
136
+ params: RmWidgetParam[];
137
+ /** Taille de depart dans la grille de 12 colonnes. */
138
+ size: {
139
+ w: number;
140
+ h: number;
141
+ };
142
+ /** Taille en dessous de laquelle le widget ne se lit plus. */
143
+ min?: {
144
+ w: number;
145
+ h: number;
146
+ };
147
+ fetch: (params: Record<string, string>, context: C) => Promise<RmWidgetData>;
148
+ }
149
+ /** La source d'un widget, telle que la carte la cite et que l'export la reprend. */
150
+ export interface RmWidgetSource {
151
+ name: string;
152
+ verified: string | Date | number;
153
+ export?: RmExportSource[];
154
+ }
155
+ export declare const RM_VIZ_LABELS: Record<RmDashboardViz, Record<RmDashboardLang, string>>;
156
+ /** L'icone d'une visualisation, pour le catalogue. */
157
+ export declare const RM_VIZ_ICONS: Record<RmDashboardViz, string>;
158
+ /** Taille en dessous de laquelle aucune tuile ne se lit, a defaut de `min` sur l'entree. */
159
+ export declare const RM_WIDGET_MIN: {
160
+ w: number;
161
+ h: number;
162
+ };
163
+ export declare function rmNewWidgetId(): string;
164
+ export declare function rmWidgetKind<C extends RmWidgetContext>(catalog: RmWidgetKind<C>[], kind: string): RmWidgetKind<C> | undefined;
165
+ /** Les parametres d'un widget completes par les valeurs par defaut de son entree. */
166
+ export declare function rmWidgetParams(entry: RmWidgetKind<any>, params: Record<string, unknown>): Record<string, string>;
167
+ /** Les marques du widget : celles du contexte, sauf liste figee dans ses reglages. */
168
+ export declare function rmWidgetBrands(widget: Pick<RmDashboardWidget, 'params'>, contextBrands: string[]): string[];
169
+ /** La visualisation du widget, ramenee a celles que son entree permet. */
170
+ export declare function rmWidgetViz(widget: Pick<RmDashboardWidget, 'viz'>, entry: RmWidgetKind<any>): RmDashboardViz;
171
+ export declare function rmWidgetTitle(widget: Pick<RmDashboardWidget, 'title'>, entry: RmWidgetKind<any>, lang: RmDashboardLang): string;
172
+ /**
173
+ * Les colonnes affichees d'une liste : celles cochees dans les reglages, dans
174
+ * l'ordre choisi, sinon celles que l'entree montre par defaut (les `optional`
175
+ * restent masquees). Une colonne cochee qui n'existe plus (autre regroupement)
176
+ * est ignoree ; s'il n'en reste aucune, on revient au defaut plutot qu'a un
177
+ * tableau vide. Le widget, l'editeur et l'export lisent la meme regle.
178
+ */
179
+ export declare function rmShownColumns(columns: RmWidgetColumn[], chosen: unknown): RmWidgetColumn[];
180
+ /** Une serie par marque, alignee sur toutes les periodes rencontrees, dans l'ordre des marques. */
181
+ export declare function rmSeriesByBrand(rows: {
182
+ brand: string;
183
+ period: string;
184
+ value: number | null;
185
+ }[], brands: string[]): RmWidgetSeriesData;
186
+ /**
187
+ * Le reglage « Periode ». « Tout l'historique » en premier : c'est la valeur
188
+ * par defaut, celle des widgets poses avant que le reglage existe.
189
+ */
190
+ export declare function rmYearsParam(values: string[]): RmWidgetParam;
191
+ /**
192
+ * Les N dernieres annees d'une serie, comptees depuis sa derniere periode et
193
+ * pas depuis l'horloge : une source en retard garde une fenetre pleine. En
194
+ * mensuel, N x 12 mois ; en annuel, N millesimes. Une serie qui n'a plus
195
+ * rien dans la fenetre disparait.
196
+ */
197
+ export declare function rmLastYears(data: RmWidgetSeriesData, years: string | undefined, monthly: boolean): RmWidgetSeriesData;
198
+ /**
199
+ * Le premier jour retenu par une liste datee sur N ans, ou rien. Une liste de
200
+ * documents ne compare pas deux periodes : la fenetre se compte depuis
201
+ * aujourd'hui.
202
+ */
203
+ export declare function rmSince(years: string | undefined, today?: Date): string | null;
204
+ /** Les lignes datees depuis `from` ; sans fenetre, toutes, lignes sans date comprises. */
205
+ export declare function rmWithin<T extends Record<string, unknown>>(rows: T[], key: string, from: string | null): T[];
206
+ /** Le message de troncature, ou rien : jamais un graphe partiel sans le dire. */
207
+ export declare function rmTruncationNote(shown: number, total: number, lang: RmDashboardLang): string | undefined;
208
+ /** Les marques demandees qui ne rendent rien, dites plutot que tues. */
209
+ export declare function rmMissingNote(brands: string[], found: Set<string>, lang: RmDashboardLang): string | undefined;
210
+ export declare function rmJoinNotes(...notes: (string | undefined)[]): string | undefined;
211
+ export declare function rmWidgetEmpty(data: RmWidgetData | null): boolean;
212
+ export interface RmWidgetSheet {
213
+ columns: RmWidgetColumn[];
214
+ rows: Record<string, string | number | null>[];
215
+ }
216
+ /**
217
+ * Le tableau d'une donnee, quelle que soit sa forme, en nombres bruts : une
218
+ * serie temporelle fait une ligne par periode, des parts ajoutent leur
219
+ * pourcentage, des chiffres cles leur valeur un an plus tot. Le widget
220
+ * l'affiche mis au format, l'export l'ecrit tel quel : les deux lisent ici.
221
+ */
222
+ export declare function rmWidgetSheet(data: RmWidgetData, lang: RmDashboardLang, chosenColumns?: unknown): RmWidgetSheet;
223
+ /** L'onglet de classeur d'une feuille. Excel borne le nom a 31 caracteres. */
224
+ export declare function rmWidgetExportSheet(name: string, sheet: RmWidgetSheet): RmExportSheet;
225
+ /**
226
+ * Les onglets de l'export d'un tableau entier : un par widget charge, dans
227
+ * l'ordre de lecture de la grille (haut, puis gauche).
228
+ */
229
+ export declare function rmDashboardExportSheets(widgets: RmDashboardWidget[], loaded: Record<string, {
230
+ title: string;
231
+ data: RmWidgetData;
232
+ } | null | undefined>, lang: RmDashboardLang): RmExportSheet[];
@@ -0,0 +1,167 @@
1
+ export const RM_VIZ_LABELS = {
2
+ line: { fr: "Courbe", en: "Line" },
3
+ bar: { fr: "Barres", en: "Bars" },
4
+ table: { fr: "Tableau", en: "Table" },
5
+ kpi: { fr: "Chiffres cl\xE9s", en: "Key figures" },
6
+ donut: { fr: "Anneau", en: "Donut" },
7
+ cards: { fr: "Cartes", en: "Cards" }
8
+ };
9
+ export const RM_VIZ_ICONS = {
10
+ line: "ChartLine",
11
+ bar: "ChartBar",
12
+ table: "Table",
13
+ kpi: "Gauge",
14
+ donut: "ChartDonut",
15
+ cards: "SquaresFour"
16
+ };
17
+ export const RM_WIDGET_MIN = { w: 2, h: 3 };
18
+ export function rmNewWidgetId() {
19
+ return globalThis.crypto?.randomUUID?.() ?? `w-${Date.now()}-${Math.random().toString(36).slice(2)}`;
20
+ }
21
+ export function rmWidgetKind(catalog, kind) {
22
+ return catalog.find((entry) => entry.kind === kind);
23
+ }
24
+ export function rmWidgetParams(entry, params) {
25
+ const out = {};
26
+ for (const param of entry.params) {
27
+ const value = params[param.key];
28
+ out[param.key] = typeof value === "string" && param.options.some((o) => o.value === value) ? value : param.options[0].value;
29
+ }
30
+ return out;
31
+ }
32
+ export function rmWidgetBrands(widget, contextBrands) {
33
+ const chosen = widget.params.brands;
34
+ return Array.isArray(chosen) ? chosen : contextBrands;
35
+ }
36
+ export function rmWidgetViz(widget, entry) {
37
+ return entry.viz.includes(widget.viz) ? widget.viz : entry.viz[0];
38
+ }
39
+ export function rmWidgetTitle(widget, entry, lang) {
40
+ return widget.title?.trim() || entry.title[lang];
41
+ }
42
+ export function rmShownColumns(columns, chosen) {
43
+ const defaults = columns.filter((c) => !c.optional);
44
+ if (!Array.isArray(chosen)) return defaults;
45
+ const kept = chosen.flatMap((key) => columns.filter((c) => c.key === key));
46
+ return kept.length ? kept : defaults;
47
+ }
48
+ export function rmSeriesByBrand(rows, brands) {
49
+ const categories = [...new Set(rows.map((row) => row.period))].sort();
50
+ const series = brands.filter((brand) => rows.some((row) => row.brand === brand)).map((brand) => {
51
+ const byPeriod = new Map(rows.filter((row) => row.brand === brand).map((row) => [row.period, row.value]));
52
+ return { name: brand, data: categories.map((period) => byPeriod.get(period) ?? null) };
53
+ });
54
+ return { type: "series", categories, series };
55
+ }
56
+ export function rmYearsParam(values) {
57
+ return {
58
+ key: "years",
59
+ label: { fr: "P\xE9riode", en: "Period" },
60
+ options: [
61
+ { value: "all", label: { fr: "Tout l'historique", en: "Whole history" } },
62
+ ...values.map((value) => ({
63
+ value,
64
+ label: value === "1" ? { fr: "Derni\xE8re ann\xE9e", en: "Last year" } : { fr: `${value} derni\xE8res ann\xE9es`, en: `Last ${value} years` }
65
+ }))
66
+ ]
67
+ };
68
+ }
69
+ export function rmLastYears(data, years, monthly) {
70
+ const n = Number(years);
71
+ if (!n) return data;
72
+ const last = data.categories[data.categories.length - 1];
73
+ if (!last) return data;
74
+ const keep = data.categories.map((period, index) => ({ period, index })).filter(
75
+ ({ period, index }) => monthly ? index >= data.categories.length - n * 12 : Number(period) > Number(last.slice(0, 4)) - n
76
+ ).map(({ index }) => index);
77
+ return {
78
+ type: "series",
79
+ categories: keep.map((i) => data.categories[i]),
80
+ series: data.series.map((s) => ({ ...s, data: keep.map((i) => s.data[i] ?? null) })).filter((s) => s.data.some((v) => v !== null))
81
+ };
82
+ }
83
+ export function rmSince(years, today = /* @__PURE__ */ new Date()) {
84
+ const n = Number(years);
85
+ if (!n) return null;
86
+ const date = new Date(today);
87
+ date.setFullYear(date.getFullYear() - n);
88
+ return date.toISOString().slice(0, 10);
89
+ }
90
+ export function rmWithin(rows, key, from) {
91
+ if (!from) return rows;
92
+ return rows.filter((row) => typeof row[key] === "string" && row[key] >= from);
93
+ }
94
+ export function rmTruncationNote(shown, total, lang) {
95
+ if (total <= shown) return void 0;
96
+ return lang === "en" ? `Truncated results: ${shown} rows out of ${total}.` : `R\xE9sultats tronqu\xE9s : ${shown} lignes sur ${total}.`;
97
+ }
98
+ export function rmMissingNote(brands, found, lang) {
99
+ const absent = brands.filter((brand) => !found.has(brand));
100
+ if (!absent.length) return void 0;
101
+ return lang === "en" ? `No data in this source for ${absent.join(", ")}.` : `Aucune donn\xE9e dans cette source pour ${absent.join(", ")}.`;
102
+ }
103
+ export function rmJoinNotes(...notes) {
104
+ const kept = notes.filter(Boolean);
105
+ return kept.length ? kept.join(" ") : void 0;
106
+ }
107
+ export function rmWidgetEmpty(data) {
108
+ if (!data) return true;
109
+ if (data.type === "series") return !data.series.length;
110
+ if (data.type === "parts") return !data.parts.length;
111
+ if (data.type === "rows") return !data.rows.length;
112
+ return !data.items.length;
113
+ }
114
+ export function rmWidgetSheet(data, lang, chosenColumns) {
115
+ const en = lang === "en";
116
+ if (data.type === "rows") return { columns: rmShownColumns(data.columns, chosenColumns), rows: data.rows };
117
+ if (data.type === "series") {
118
+ return {
119
+ columns: [
120
+ { key: "period", label: en ? "Period" : "P\xE9riode" },
121
+ ...data.series.map((s, i) => ({ key: `s${i}`, label: s.name, numeric: true }))
122
+ ],
123
+ rows: data.categories.map((period, row) => ({
124
+ period,
125
+ ...Object.fromEntries(data.series.map((s, i) => [`s${i}`, s.data[row] ?? null]))
126
+ }))
127
+ };
128
+ }
129
+ if (data.type === "parts") {
130
+ const total = data.parts.reduce((sum, p) => sum + p.value, 0);
131
+ return {
132
+ columns: [
133
+ { key: "name", label: en ? "Item" : "\xC9l\xE9ment" },
134
+ { key: "value", label: en ? "Value" : "Valeur", numeric: true },
135
+ { key: "share", label: en ? "Share (%)" : "Part (%)", numeric: true }
136
+ ],
137
+ rows: data.parts.map((p) => ({ name: p.name, value: p.value, share: total ? Math.round(p.value / total * 1e3) / 10 : null }))
138
+ };
139
+ }
140
+ return {
141
+ columns: [
142
+ { key: "name", label: en ? "Brand" : "Marque" },
143
+ { key: "value", label: en ? "Value" : "Valeur", numeric: true },
144
+ { key: "previous", label: en ? "A year earlier" : "Un an plus t\xF4t", numeric: true },
145
+ { key: "trend", label: en ? "Change (%)" : "\xC9volution (%)", numeric: true }
146
+ ],
147
+ rows: data.items.map((i) => ({
148
+ name: i.label,
149
+ value: i.value,
150
+ previous: i.previous,
151
+ trend: i.trend === void 0 ? null : Math.round(i.trend * 10) / 10
152
+ }))
153
+ };
154
+ }
155
+ export function rmWidgetExportSheet(name, sheet) {
156
+ return {
157
+ name: name.slice(0, 31),
158
+ schema: sheet.columns.map((c) => ({ column: c.label, value: (row) => row[c.key] ?? null })),
159
+ data: sheet.rows
160
+ };
161
+ }
162
+ export function rmDashboardExportSheets(widgets, loaded, lang) {
163
+ return [...widgets].sort((a, b) => a.layout.y - b.layout.y || a.layout.x - b.layout.x).flatMap((widget) => {
164
+ const entry = loaded[widget.id];
165
+ return entry ? [rmWidgetExportSheet(entry.title, rmWidgetSheet(entry.data, lang, widget.params.columns))] : [];
166
+ });
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.210",
3
+ "version": "0.1.212",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,8 @@
27
27
  "scripts": {
28
28
  "build": "nuxt-module-build build",
29
29
  "dev": "nuxi dev playground",
30
- "typecheck": "nuxi typecheck"
30
+ "typecheck": "nuxi typecheck",
31
+ "test": "vitest run"
31
32
  },
32
33
  "dependencies": {
33
34
  "@nuxt/kit": "^3.16.0",
@@ -41,6 +42,7 @@
41
42
  "devDependencies": {
42
43
  "@nuxt/module-builder": "^0.8.4",
43
44
  "nuxt": "^3.16.0",
44
- "typescript": "^5.7.3"
45
+ "typescript": "^5.7.3",
46
+ "vitest": "^3.2.4"
45
47
  }
46
48
  }