@tekkare/romulus 0.1.5 → 0.1.8
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.d.mts +19 -1
- package/dist/module.d.ts +19 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +18 -3
- package/dist/runtime/assets/styles/components.css +1 -0
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/assets/styles/utilities.css +1 -1
- package/dist/runtime/components/Badge.vue +15 -0
- package/dist/runtime/components/ChartCard.vue +129 -0
- package/dist/runtime/components/DataTable.vue +92 -0
- package/dist/runtime/components/Kpi.vue +63 -0
- package/dist/runtime/components/NavGroup.vue +37 -0
- package/dist/runtime/utils/format.d.ts +27 -0
- package/dist/runtime/utils/format.js +7 -0
- package/package.json +1 -1
package/dist/module.d.mts
CHANGED
|
@@ -3,9 +3,27 @@ import * as _nuxt_schema from '@nuxt/schema';
|
|
|
3
3
|
interface RomulusModuleOptions {
|
|
4
4
|
/**
|
|
5
5
|
* Prefix for all Romulus components.
|
|
6
|
-
* @default '
|
|
6
|
+
* @default 'Rm'
|
|
7
7
|
*/
|
|
8
8
|
prefix?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Injecte `base.css`, un reset global (`* { margin: 0 }`, `button { border: none }`,
|
|
11
|
+
* `a { text-decoration: none }`, couleur et fond sur `html`/`body`).
|
|
12
|
+
*
|
|
13
|
+
* A passer a `false` dans une app qui cohabite avec un autre socle CSS
|
|
14
|
+
* (Auriga, une feuille maison) : le reset y repeindrait toutes les pages
|
|
15
|
+
* existantes, pas seulement celles qui utilisent Romulus.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
baseStyles?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Injecte `utilities.css`. Les classes ne sont pas prefixees (`.row`,
|
|
21
|
+
* `.column`, `.block`, `.border`, `.absolute`...) et entrent donc en
|
|
22
|
+
* collision avec le CSS d'une app existante. A passer a `false` en
|
|
23
|
+
* cohabitation ; les tokens et les composants restent disponibles.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
utilities?: boolean;
|
|
9
27
|
}
|
|
10
28
|
declare const _default: _nuxt_schema.NuxtModule<RomulusModuleOptions, RomulusModuleOptions, false>;
|
|
11
29
|
|
package/dist/module.d.ts
CHANGED
|
@@ -3,9 +3,27 @@ import * as _nuxt_schema from '@nuxt/schema';
|
|
|
3
3
|
interface RomulusModuleOptions {
|
|
4
4
|
/**
|
|
5
5
|
* Prefix for all Romulus components.
|
|
6
|
-
* @default '
|
|
6
|
+
* @default 'Rm'
|
|
7
7
|
*/
|
|
8
8
|
prefix?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Injecte `base.css`, un reset global (`* { margin: 0 }`, `button { border: none }`,
|
|
11
|
+
* `a { text-decoration: none }`, couleur et fond sur `html`/`body`).
|
|
12
|
+
*
|
|
13
|
+
* A passer a `false` dans une app qui cohabite avec un autre socle CSS
|
|
14
|
+
* (Auriga, une feuille maison) : le reset y repeindrait toutes les pages
|
|
15
|
+
* existantes, pas seulement celles qui utilisent Romulus.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
baseStyles?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Injecte `utilities.css`. Les classes ne sont pas prefixees (`.row`,
|
|
21
|
+
* `.column`, `.block`, `.border`, `.absolute`...) et entrent donc en
|
|
22
|
+
* collision avec le CSS d'une app existante. A passer a `false` en
|
|
23
|
+
* cohabitation ; les tokens et les composants restent disponibles.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
utilities?: boolean;
|
|
9
27
|
}
|
|
10
28
|
declare const _default: _nuxt_schema.NuxtModule<RomulusModuleOptions, RomulusModuleOptions, false>;
|
|
11
29
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -10,19 +10,34 @@ const module = defineNuxtModule({
|
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
defaults: {
|
|
13
|
-
prefix: "Rm"
|
|
13
|
+
prefix: "Rm",
|
|
14
|
+
baseStyles: true,
|
|
15
|
+
utilities: true
|
|
14
16
|
},
|
|
15
17
|
async setup(options, nuxt) {
|
|
16
18
|
const { resolve } = createResolver(import.meta.url);
|
|
17
19
|
const runtimeDir = fileURLToPath(new URL("./runtime", import.meta.url));
|
|
18
20
|
nuxt.options.css.push(resolve(runtimeDir, "assets/styles/tokens.css"));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
if (options.baseStyles)
|
|
22
|
+
nuxt.options.css.push(resolve(runtimeDir, "assets/styles/base.css"));
|
|
23
|
+
nuxt.options.css.push(resolve(runtimeDir, "assets/styles/components.css"));
|
|
24
|
+
if (options.utilities)
|
|
25
|
+
nuxt.options.css.push(resolve(runtimeDir, "assets/styles/utilities.css"));
|
|
21
26
|
await addComponentsDir({
|
|
22
27
|
path: resolve(runtimeDir, "components"),
|
|
23
28
|
prefix: options.prefix
|
|
24
29
|
});
|
|
25
30
|
addImportsDir(resolve(runtimeDir, "composables"));
|
|
31
|
+
addImportsDir(resolve(runtimeDir, "utils"));
|
|
32
|
+
nuxt.options.app.head.link = nuxt.options.app.head.link || [];
|
|
33
|
+
const fontsHref = "https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&family=Caudex:wght@400;700&display=swap";
|
|
34
|
+
if (!nuxt.options.app.head.link.some((l) => l.href === fontsHref)) {
|
|
35
|
+
nuxt.options.app.head.link.push(
|
|
36
|
+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
37
|
+
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" },
|
|
38
|
+
{ rel: "stylesheet", href: fontsHref }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
42
|
});
|
|
28
43
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.grid-2{grid-template-columns:repeat(2,1fr)}.grid-2,.grid-3{display:grid;gap:20px}.grid-3{grid-template-columns:repeat(3,1fr)}.grid-4{display:grid;gap:16px;grid-template-columns:repeat(4,1fr)}.grid-2-1{display:grid;gap:20px;grid-template-columns:2fr 1fr}.chart-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:10px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;transition:box-shadow .18s,border-color .18s}.chart-card:hover{border-color:var(--color-violet-pale);box-shadow:0 6px 16px rgba(107,63,255,.1),0 1px 3px rgba(30,41,59,.04)}.chart-card-head{padding:22px 64px 14px 24px;position:relative}.chart-card-title{color:var(--text-primary);font-size:15px;font-weight:700;letter-spacing:-.01em;line-height:1.3;margin:0 0 5px}.chart-card-context{color:var(--text-muted);font-size:12px;line-height:1.55;margin:0}.chart-card-actions{position:absolute;right:12px;top:12px}.cc-export{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;color:var(--text-secondary);cursor:pointer;display:flex;font-size:14px;height:28px;justify-content:center;transition:background .15s,color .15s,border-color .15s;width:28px}.cc-export:hover{color:var(--color-secondary)}.cc-export:hover,.chart-card-actions.open .cc-export{background:var(--bg-hover);border-color:var(--color-violet-pale)}.chart-card-actions.open .cc-export{color:var(--color-primary)}.cc-menu{animation:cc-menu-in .12s cubic-bezier(.2,0,0,1);background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);display:none;min-width:200px;padding:6px 0;position:absolute;right:0;top:34px;z-index:var(--z-modal)}.chart-card-actions.open .cc-menu{display:block}.cc-menu-section{color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;padding:8px 14px 4px;text-transform:uppercase}.cc-menu-item{align-items:center;background:none;border:0;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:13px;gap:10px;padding:7px 14px;text-align:left;width:100%}.cc-menu-item i{color:var(--text-muted);font-size:14px}.cc-menu-item:hover{background:var(--bg-surface-3);color:var(--text-primary)}.cc-menu-item:hover i{color:var(--text-secondary)}.cc-menu-divider{background:var(--bg-surface-3);height:1px;margin:4px 0}@keyframes cc-menu-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.chart-card-body{flex:1;padding:4px 12px 8px}.chart-card-foot{align-items:center;background:var(--bg-surface-2);border-radius:0 0 10px 10px;border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:11px;gap:10px;padding:12px 24px}.chart-card-foot .src{align-items:center;display:flex;gap:4px}.chart-card-foot .src b{color:var(--text-secondary);font-weight:600}.chart-card-foot .dot{background:var(--border-default);border-radius:50%;height:3px;width:3px}.chart-card-foot .updated{color:var(--text-secondary);font-weight:500}.kpi-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:0 2px 8px rgba(30,41,59,.06);padding:16px 18px}.kpi-label{color:var(--text-muted);font-size:11px;font-weight:600;letter-spacing:.1em;margin:0 0 6px;text-transform:uppercase}.kpi-value{color:var(--color-primary);font-family:var(--font-serif);font-size:28px;font-weight:700;line-height:1;margin:0}.kpi-value.healthcare{color:var(--color-healthcare)}.kpi-value.discovery{color:var(--color-discovery)}.kpi-value.pharma{color:var(--color-pharma)}.kpi-value.databank{color:var(--color-databank)}.kpi-trend{align-items:center;display:inline-flex;font-size:12px;font-weight:600;gap:4px;margin-top:6px}.kpi-trend i{font-size:11px}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-danger)}.kpi-foot,.kpi-trend--flat{color:var(--text-muted)}.kpi-foot{font-size:11px;margin-top:4px}.badge{border-radius:4px;display:inline-block;font-size:10px;font-weight:600;padding:2px 8px}.badge-active{background:var(--color-success-bg);color:var(--color-success)}.badge-pending{background:var(--color-warning-bg);color:var(--color-warning)}.badge-error{background:var(--color-danger-bg);color:var(--color-danger)}.badge-info{background:var(--bg-hover);color:var(--color-primary)}.badge-grey{background:var(--bg-surface-3);color:var(--text-secondary)}table.tk{border-collapse:collapse;font-size:13px;width:100%}.sr-only,table.tk>caption.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}table.tk caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13);font-weight:var(--weight-bold);padding:0 0 var(--space-8);text-align:left}table.tk th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:11px;font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase}table.tk tbody th[scope=row]{background:transparent;font-size:var(--font-13);font-weight:var(--weight-bold);letter-spacing:0;text-transform:none}table.tk tbody th[scope=row],table.tk td{border-bottom:1px solid var(--border-soft);color:var(--text-primary)}table.tk td{padding:12px 14px}table.tk tbody tr:nth-child(2n) td,table.tk tbody tr:nth-child(2n) th[scope=row]{background:var(--bg-surface-2)}table.tk tbody tr:hover td,table.tk tbody tr:hover th[scope=row]{background:var(--bg-hover)}table.tk th[aria-sort]{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}table.tk th[aria-sort]:hover{color:var(--text-primary)}.tk-wrap{overflow-x:auto}.tk-num{font-variant-numeric:tabular-nums;text-align:right}.nav-group{display:flex;flex-direction:column}.nav-group-header{align-items:center;background:none;border:0;color:var(--text-muted);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:11px;font-weight:700;gap:8px;letter-spacing:.08em;padding:8px 12px;text-align:left;text-transform:uppercase;transition:color var(--duration-fast) var(--ease-out);width:100%}.nav-group-header:hover{color:var(--text-secondary)}.nav-group-header .caret{display:inline-flex;margin-left:auto;transition:transform var(--duration-fast) var(--ease-out)}.nav-group[data-open=true]>.nav-group-header{color:var(--color-primary)}.nav-group[data-open=true]>.nav-group-header .caret{transform:rotate(180deg)}.nav-group-items{display:flex;flex-direction:column;gap:2px;padding:2px 0 8px}.cc-export:focus-visible,.cc-menu-item:focus-visible,.chart-card :focus-visible,.nav-group-header:focus-visible,table.tk th[aria-sort]:focus-visible{box-shadow:var(--shadow-focus);outline:2px solid var(--color-secondary);outline-offset:2px}.cc-menu-item,.nav-group-header{min-height:40px}body.text-large .kpi-label{font-size:13px}body.text-large .kpi-value{font-size:34px}body.text-large .kpi-trend{font-size:14px}body.text-large .kpi-foot{font-size:13px}body.text-large .chart-card-title{font-size:17px}body.text-large .chart-card-context{font-size:14px}body.text-large .chart-card-foot{font-size:12px}body.text-large .badge{font-size:12px;padding:3px 9px}body.text-large table.tk{font-size:15px}body.text-large table.tk th{font-size:13px}body.text-xl .kpi-label{font-size:14px}body.text-xl .kpi-value{font-size:40px}body.text-xl .kpi-trend{font-size:16px}body.text-xl .kpi-foot{font-size:14px}body.text-xl .chart-card-title{font-size:20px}body.text-xl .chart-card-context{font-size:16px}body.text-xl .chart-card-foot{font-size:14px}body.text-xl .badge{font-size:14px;padding:3px 10px}body.text-xl table.tk{font-size:16px}body.text-xl table.tk th{font-size:14px}body.high-contrast .cc-export,body.high-contrast .cc-menu,body.high-contrast .chart-card,body.high-contrast .kpi-card{border-width:2px}body.high-contrast .chart-card:hover{border-color:var(--border-default)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--r-color-primary-50:#f0f4ff;--r-color-primary-100:#dce5fd;--r-color-primary-200:#b9cbfb;--r-color-primary-300:#96b0f9;--r-color-primary-400:#7395f5;--r-color-primary-500:#5a7ef2;--r-color-primary-600
|
|
1
|
+
:root{--sidebar-w:220px;--sidebar-collapsed-w:56px;--rsidebar-w:280px;--rsidebar-collapsed-w:76px;--topbar-h:56px;--tabs-h:46px;--analysis-header-h:124px;--sourcebar-h:38px;--filterbar-h:56px;--color-primary:#2100ad;--color-primary-hover:#1a0088;--color-secondary:#6b3fff;--color-accent-soft:#b485ff;--color-violet-pale:#d6ccff;--color-healthcare:#4164ee;--color-discovery:#07816f;--color-pharma:#f7520e;--color-databank:#e04380;--color-healthcare-active:#1a65e0;--color-discovery-active:#066b5c;--color-pharma-active:#d9470c;--color-databank-active:#c73a6f;--healthcare-rgb:65,100,238;--discovery-rgb:7,129,111;--pharma-rgb:247,82,14;--databank-rgb:224,67,128;--color-accent-teal:#00d4a8;--color-notification:#e59;--color-text-inverse:#fff;--color-success:#16a34a;--color-success-bg:#dcfce7;--color-warning:#d97706;--color-warning-bg:#fef3c7;--color-danger:#dc2626;--color-danger-bg:#fee2e2;--color-info:#4164ee;--color-info-bg:#dbeafe;--bg-app:#eef1f5;--bg-surface:#fff;--bg-surface-2:#fbfbfd;--bg-surface-3:#f5f5f9;--bg-hover:#eef1fe;--border-default:#dbdef0;--border-soft:#f0f1f7;--text-primary:#1e293b;--text-secondary:#525670;--text-muted:#878ca6;--brand-rgb:33,0,173;--chart-cat-1:#2100ad;--chart-cat-2:#00a38e;--chart-cat-3:#e67e22;--chart-cat-4:#d63384;--chart-cat-5:#4164ee;--chart-cat-6:#16a34a;--space-2:2px;--space-4:4px;--space-6:6px;--space-8:8px;--space-12:12px;--space-16:16px;--space-20:20px;--space-24:24px;--space-32:32px;--space-40:40px;--space-48:48px;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-xl:10px;--radius-2xl:12px;--radius-pill:999px;--font-10:10px;--font-11:11px;--font-12:12px;--font-13:13px;--font-14:14px;--font-16:16px;--font-18:18px;--font-22:22px;--font-24:24px;--font-32:32px;--weight-normal:400;--weight-medium:500;--weight-semibold:600;--weight-bold:700;--font-serif:"Caudex",Georgia,serif;--font-sans:"Figtree",system-ui,sans-serif;--font-mono:"SF Mono",Menlo,monospace;--shadow-xs:0 1px 2px rgba(30,41,59,.04);--shadow-sm:0 2px 6px rgba(30,41,59,.06),0 1px 2px rgba(30,41,59,.03);--shadow-md:0 4px 12px rgba(30,41,59,.06),0 1px 3px rgba(30,41,59,.04);--shadow-lg:0 8px 24px rgba(30,41,59,.12);--shadow-focus:0 0 0 3px rgba(107,63,255,.14);--duration-instant:100ms;--duration-fast:150ms;--duration-base:240ms;--duration-slow:400ms;--duration-slowest:600ms;--ease-out:cubic-bezier(.2,0,0,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--ease-spring:cubic-bezier(.5,1.6,.4,1);--z-base:1;--z-sticky:20;--z-header:30;--z-drawer:40;--z-modal:50;--z-toast:60;--z-help:70}body.night{--color-primary:#9b7cff;--color-secondary:#b485ff;--color-accent-soft:#d6ccff;--color-databank:#f472b6;--bg-app:#0e0e14;--bg-surface:#1a1a24;--bg-surface-2:#232330;--bg-surface-3:#2a2a38;--bg-hover:#2d2a42;--border-default:#45455c;--border-soft:#32323f;--text-primary:#f5f6fa;--text-secondary:#cfd3e5;--text-muted:#8a90a8;--brand-rgb:155,124,255;--color-success:#6fe8c5;--color-success-bg:rgba(0,212,168,.18);--color-warning:#fbbf24;--color-warning-bg:rgba(245,158,11,.18);--color-danger:#fca5a5;--color-danger-bg:rgba(239,68,68,.18);--color-info:#7aa0ff;--color-info-bg:rgba(65,100,238,.18);--chart-cat-1:#9b7cff;--chart-cat-2:#6fe8c5;--chart-cat-3:#fbbf24;--chart-cat-4:#f472b6;--chart-cat-5:#7b9ef5;--chart-cat-6:#bef264;background:var(--bg-app);color:var(--text-primary)}body.high-contrast:not(.night){--text-primary:#000;--text-secondary:#1a1a24;--text-muted:#2f2f40;--border-default:#000;--border-soft:#525670;--bg-app:#fff;--bg-surface:#fff;--bg-surface-2:#fff;--bg-surface-3:#f0f1f7;--bg-hover:#ffe56b;--color-primary:#1100a0;--color-primary-hover:#0a0070;--color-secondary:#5028d8;--color-success:#006b2c;--color-warning:#8c4500;--color-danger:#a60000;--color-info:#0040c8}body.night.high-contrast{--text-primary:#fff;--text-secondary:#f5f6fa;--text-muted:#cfd3e5;--border-default:#fff;--border-soft:#b8bcd0;--bg-app:#000;--bg-surface:#000;--bg-surface-2:#000;--bg-surface-3:#000;--bg-hover:#ffe56b;--color-primary:#d6ccff;--color-primary-hover:#fff;--color-secondary:#e5daff;--color-accent-soft:#fff;--color-success:#6fe8c5;--color-warning:#fbbf24;--color-danger:#fca5a5;--color-info:#7aa0ff;--brand-rgb:214,204,255}body.reduce-motion *,body.reduce-motion :after,body.reduce-motion :before{animation:none!important;scroll-behavior:auto!important;transition:none!important}:root{--r-color-primary-50:#f0f4ff;--r-color-primary-100:#dce5fd;--r-color-primary-200:#b9cbfb;--r-color-primary-300:#96b0f9;--r-color-primary-400:#7395f5;--r-color-primary-500:#5a7ef2;--r-color-primary-600:var(--color-healthcare);--r-color-primary-700:#3451d4;--r-color-primary-800:#283fb0;--r-color-primary-900:#1c2d8a;--r-color-gray-50:#f9fafb;--r-color-gray-100:#f3f4f6;--r-color-gray-200:#e5e7eb;--r-color-gray-300:#d1d5db;--r-color-gray-400:#9ca3af;--r-color-gray-500:#6b7280;--r-color-gray-600:#4b5563;--r-color-gray-700:#374151;--r-color-gray-800:#1f2937;--r-color-gray-900:#111827;--r-color-gray-950:#030712;--r-color-success-500:var(--color-success);--r-color-success-600:#16a34a;--r-color-warning-500:#f59e0b;--r-color-warning-600:var(--color-warning);--r-color-danger-500:#ef4444;--r-color-danger-600:var(--color-danger);--r-color-white:#fff;--r-color-black:#000;--r-font-family:var(--font-sans);--r-font-mono:var(--font-mono);--r-text-xs:0.75rem;--r-text-sm:0.875rem;--r-text-base:1rem;--r-text-lg:1.125rem;--r-text-xl:1.25rem;--r-text-2xl:1.5rem;--r-text-3xl:1.875rem;--r-text-4xl:2.25rem;--r-font-weight-normal:var(--weight-normal);--r-font-weight-medium:var(--weight-medium);--r-font-weight-semibold:var(--weight-semibold);--r-font-weight-bold:var(--weight-bold);--r-leading-tight:1.25;--r-leading-normal:1.5;--r-leading-relaxed:1.75;--r-space-0:0;--r-space-1:var(--space-4);--r-space-2:var(--space-8);--r-space-3:var(--space-12);--r-space-4:var(--space-16);--r-space-5:var(--space-20);--r-space-6:var(--space-24);--r-space-8:var(--space-32);--r-space-10:var(--space-40);--r-space-12:var(--space-48);--r-radius-none:0;--r-radius-sm:var(--radius-sm);--r-radius-md:var(--radius-md);--r-radius-lg:var(--radius-lg);--r-radius-xl:0.75rem;--r-radius-2xl:1rem;--r-radius-full:var(--radius-pill);--r-shadow-xs:0 1px 2px 0 rgba(0,0,0,.05);--r-shadow-sm:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--r-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--r-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--r-shadow-xl:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--r-transition-fast:var(--duration-fast) var(--ease-in-out);--r-transition-base:200ms var(--ease-in-out);--r-transition-slow:var(--duration-slow) var(--ease-in-out)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.row{display:flex;flex-direction:row}.column{display:flex;flex-direction:column}.row-reverse{display:flex;flex-direction:row-reverse}.column-reverse{display:flex;flex-direction:column-reverse}.x-start{justify-content:flex-start}.x-center{justify-content:center}.x-end{justify-content:flex-end}.x-between{justify-content:space-between}.x-around{justify-content:space-around}.x-evenly{justify-content:space-evenly}.y-start{align-items:flex-start}.y-center{align-items:center}.y-end{align-items:flex-end}.y-between{align-content:space-between}.y-stretch{align-items:stretch}.y-baseline{align-items:baseline}.wrap{flex-wrap:wrap}.nowrap{flex-wrap:nowrap}.grow{flex-grow:1}.grow-0{flex-grow:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-1{flex:1}.flex-auto{flex:auto}.flex-none{flex:none}.self-start{align-self:flex-start}.self-center{align-self:center}.self-end{align-self:flex-end}.self-stretch{align-self:stretch}.w-full{width:100%}.w-auto{width:auto}.w-fit{width:-moz-fit-content;width:fit-content}.min-w-0{min-width:0}.h-full{height:100%}.h-auto{height:auto}.h-screen{height:100vh}.min-h-0{min-height:0}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.sticky{position:sticky}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.block{display:block}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.gap-
|
|
1
|
+
.row{display:flex;flex-direction:row}.column{display:flex;flex-direction:column}.row-reverse{display:flex;flex-direction:row-reverse}.column-reverse{display:flex;flex-direction:column-reverse}.x-start{justify-content:flex-start}.x-center{justify-content:center}.x-end{justify-content:flex-end}.x-between{justify-content:space-between}.x-around{justify-content:space-around}.x-evenly{justify-content:space-evenly}.y-start{align-items:flex-start}.y-center{align-items:center}.y-end{align-items:flex-end}.y-between{align-content:space-between}.y-stretch{align-items:stretch}.y-baseline{align-items:baseline}.wrap{flex-wrap:wrap}.nowrap{flex-wrap:nowrap}.grow{flex-grow:1}.grow-0{flex-grow:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-1{flex:1}.flex-auto{flex:auto}.flex-none{flex:none}.self-start{align-self:flex-start}.self-center{align-self:center}.self-end{align-self:flex-end}.self-stretch{align-self:stretch}.w-full{width:100%}.w-auto{width:auto}.w-fit{width:-moz-fit-content;width:fit-content}.min-w-0{min-width:0}.h-full{height:100%}.h-auto{height:auto}.h-screen{height:100vh}.min-h-0{min-height:0}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.sticky{position:sticky}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.block{display:block}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.gap-2{gap:var(--space-2)}.gap-4{gap:var(--space-4)}.gap-6{gap:var(--space-6)}.gap-8{gap:var(--space-8)}.gap-12{gap:var(--space-12)}.gap-16{gap:var(--space-16)}.gap-20{gap:var(--space-20)}.gap-24{gap:var(--space-24)}.gap-32{gap:var(--space-32)}.gap-40{gap:var(--space-40)}.gap-48{gap:var(--space-48)}.p-0{padding:0}.p-2{padding:var(--space-2)}.p-4{padding:var(--space-4)}.p-6{padding:var(--space-6)}.p-8{padding:var(--space-8)}.p-12{padding:var(--space-12)}.p-16{padding:var(--space-16)}.p-20{padding:var(--space-20)}.p-24{padding:var(--space-24)}.p-32{padding:var(--space-32)}.p-40{padding:var(--space-40)}.p-48{padding:var(--space-48)}.px-2{padding-left:var(--space-2);padding-right:var(--space-2)}.px-4{padding-left:var(--space-4);padding-right:var(--space-4)}.px-6{padding-left:var(--space-6);padding-right:var(--space-6)}.px-8{padding-left:var(--space-8);padding-right:var(--space-8)}.px-12{padding-left:var(--space-12);padding-right:var(--space-12)}.px-16{padding-left:var(--space-16);padding-right:var(--space-16)}.px-20{padding-left:var(--space-20);padding-right:var(--space-20)}.px-24{padding-left:var(--space-24);padding-right:var(--space-24)}.px-32{padding-left:var(--space-32);padding-right:var(--space-32)}.px-40{padding-left:var(--space-40);padding-right:var(--space-40)}.px-48{padding-left:var(--space-48);padding-right:var(--space-48)}.py-2{padding-bottom:var(--space-2);padding-top:var(--space-2)}.py-4{padding-bottom:var(--space-4);padding-top:var(--space-4)}.py-6{padding-bottom:var(--space-6);padding-top:var(--space-6)}.py-8{padding-bottom:var(--space-8);padding-top:var(--space-8)}.py-12{padding-bottom:var(--space-12);padding-top:var(--space-12)}.py-16{padding-bottom:var(--space-16);padding-top:var(--space-16)}.py-20{padding-bottom:var(--space-20);padding-top:var(--space-20)}.py-24{padding-bottom:var(--space-24);padding-top:var(--space-24)}.py-32{padding-bottom:var(--space-32);padding-top:var(--space-32)}.py-40{padding-bottom:var(--space-40);padding-top:var(--space-40)}.py-48{padding-bottom:var(--space-48);padding-top:var(--space-48)}.m-0{margin:0}.m-auto{margin:auto}.mx-auto{margin-right:auto}.ml-auto,.mx-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-auto{margin-top:auto}.mb-auto{margin-bottom:auto}.border{border:1px solid var(--border-default)}.border-t{border-top:1px solid var(--border-default)}.border-b{border-bottom:1px solid var(--border-default)}.border-l{border-left:1px solid var(--border-default)}.border-r{border-right:1px solid var(--border-default)}.border-none{border:none}.rounded-sm{border-radius:var(--radius-sm)}.rounded-md{border-radius:var(--radius-md)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-full{border-radius:var(--radius-pill)}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmBadge - label de statut STATIQUE (non cliquable).
|
|
3
|
+
Style : components.css (.badge). Jamais la couleur seule : le badge porte
|
|
4
|
+
toujours son texte. Pour un filtre cliquable, ce n'est pas un badge.
|
|
5
|
+
-->
|
|
6
|
+
<script setup lang="ts">
|
|
7
|
+
withDefaults(defineProps<{
|
|
8
|
+
/** active = favorable/inscrit/valide · pending = en cours/restreint · error = radie/refus · info · grey = neutre */
|
|
9
|
+
variant?: 'active' | 'pending' | 'error' | 'info' | 'grey'
|
|
10
|
+
}>(), { variant: 'grey' })
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span class="badge" :class="`badge-${variant}`"><slot /></span>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmChartCard - LE seul container de donnees autorise (chart, table, groupe
|
|
3
|
+
de KPI). Style : components.css (.chart-card*).
|
|
4
|
+
|
|
5
|
+
Le foot n'est pas une option decorative : c'est la premiere regle Critical
|
|
6
|
+
du design system. Toute donnee affichee porte l'organisme source ET la date
|
|
7
|
+
de notre verification. D'ou `source` et `verified` en props REQUISES : on ne
|
|
8
|
+
peut pas produire une carte sans dire d'ou vient la donnee ni quand on l'a
|
|
9
|
+
controlee. La date de la DONNEE (millesime, periode) vit dans le graphe ou
|
|
10
|
+
le tableau, elle ne se confond jamais avec la date de verification.
|
|
11
|
+
-->
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
|
14
|
+
import { rmDate } from '../utils/format'
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
/** Titre de la carte. */
|
|
18
|
+
title: string
|
|
19
|
+
/** Sous-titre : perimetre, methode, aide a la lecture. */
|
|
20
|
+
context?: string
|
|
21
|
+
/** Organisme public : "HAS", "OCDE", "ANSM (Repertoire des specialites)". Jamais un outil interne. */
|
|
22
|
+
source: string
|
|
23
|
+
/** Date de notre verification. Un Date est formate en francais long ; une chaine deja formatee passe telle quelle. */
|
|
24
|
+
verified: Date | string | number
|
|
25
|
+
/** Complement de fin de foot : frequence, millesime. */
|
|
26
|
+
extra?: string
|
|
27
|
+
/** Masque le menu d'actions quand la carte n'a rien a exporter. */
|
|
28
|
+
actions?: boolean
|
|
29
|
+
}>(), {
|
|
30
|
+
context: undefined,
|
|
31
|
+
extra: undefined,
|
|
32
|
+
actions: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
exportPng: []
|
|
37
|
+
exportCsv: []
|
|
38
|
+
exportXlsx: []
|
|
39
|
+
copyLink: []
|
|
40
|
+
fullscreen: []
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
const menuOpen = ref(false)
|
|
44
|
+
const root = ref<HTMLElement | null>(null)
|
|
45
|
+
|
|
46
|
+
// Une chaine deja formatee ("17 juin 2026") n'est pas parsable : on la garde
|
|
47
|
+
// telle quelle plutot que d'afficher "Invalid Date".
|
|
48
|
+
const verifiedText = computed(() => {
|
|
49
|
+
const parsed = new Date(props.verified)
|
|
50
|
+
return Number.isNaN(parsed.getTime()) ? String(props.verified) : rmDate(parsed)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function onDocumentClick(event: MouseEvent) {
|
|
54
|
+
if (!menuOpen.value) return
|
|
55
|
+
if (root.value && !root.value.contains(event.target as Node)) menuOpen.value = false
|
|
56
|
+
}
|
|
57
|
+
function onEscape(event: KeyboardEvent) {
|
|
58
|
+
if (event.key === 'Escape') menuOpen.value = false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onMounted(() => {
|
|
62
|
+
document.addEventListener('click', onDocumentClick)
|
|
63
|
+
document.addEventListener('keydown', onEscape)
|
|
64
|
+
})
|
|
65
|
+
onBeforeUnmount(() => {
|
|
66
|
+
document.removeEventListener('click', onDocumentClick)
|
|
67
|
+
document.removeEventListener('keydown', onEscape)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
function run(action: 'exportPng' | 'exportCsv' | 'exportXlsx' | 'copyLink' | 'fullscreen') {
|
|
71
|
+
menuOpen.value = false
|
|
72
|
+
emit(action)
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<div class="chart-card">
|
|
78
|
+
<div class="chart-card-head">
|
|
79
|
+
<p class="chart-card-title">{{ title }}</p>
|
|
80
|
+
<p v-if="context" class="chart-card-context">{{ context }}</p>
|
|
81
|
+
|
|
82
|
+
<div v-if="actions" ref="root" class="chart-card-actions" :class="{ open: menuOpen }">
|
|
83
|
+
<button
|
|
84
|
+
class="cc-export"
|
|
85
|
+
type="button"
|
|
86
|
+
aria-label="Actions sur cette carte"
|
|
87
|
+
:aria-expanded="menuOpen"
|
|
88
|
+
@click.stop="menuOpen = !menuOpen"
|
|
89
|
+
>
|
|
90
|
+
<RmIcon name="Gear" :size="14" />
|
|
91
|
+
</button>
|
|
92
|
+
<div class="cc-menu" role="menu">
|
|
93
|
+
<div class="cc-menu-section">Exporter</div>
|
|
94
|
+
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportPng')">
|
|
95
|
+
<RmIcon name="Image" :size="14" /> Image PNG
|
|
96
|
+
</button>
|
|
97
|
+
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportCsv')">
|
|
98
|
+
<RmIcon name="FileCsv" :size="14" /> Données CSV
|
|
99
|
+
</button>
|
|
100
|
+
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportXlsx')">
|
|
101
|
+
<RmIcon name="FileXls" :size="14" /> Excel (XLSX)
|
|
102
|
+
</button>
|
|
103
|
+
<div class="cc-menu-divider" />
|
|
104
|
+
<div class="cc-menu-section">Partager</div>
|
|
105
|
+
<button class="cc-menu-item" type="button" role="menuitem" @click="run('copyLink')">
|
|
106
|
+
<RmIcon name="Link" :size="14" /> Copier le lien direct
|
|
107
|
+
</button>
|
|
108
|
+
<button class="cc-menu-item" type="button" role="menuitem" @click="run('fullscreen')">
|
|
109
|
+
<RmIcon name="ArrowsOut" :size="14" /> Plein écran
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="chart-card-body">
|
|
116
|
+
<slot />
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="chart-card-foot">
|
|
120
|
+
<span class="src">Source : <b>{{ source }}</b></span>
|
|
121
|
+
<span class="dot" />
|
|
122
|
+
<span class="updated">Vérifié le {{ verifiedText }}</span>
|
|
123
|
+
<template v-if="extra">
|
|
124
|
+
<span class="dot" />
|
|
125
|
+
<span>{{ extra }}</span>
|
|
126
|
+
</template>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmDataTable - table `table.tk` du design system. Style : components.css.
|
|
3
|
+
|
|
4
|
+
Accessibilite portee par le composant, pas par l'appelant : `caption`
|
|
5
|
+
obligatoire (masquee visuellement, elle donne son nom accessible a la
|
|
6
|
+
table), colonnes d'identite en `<th scope="row">`, tri annonce via
|
|
7
|
+
`aria-sort`.
|
|
8
|
+
|
|
9
|
+
Le tri est pilote par le parent (props `sortKey` / `sortDir` + evenement
|
|
10
|
+
`sort`). Pas d'etat cache dans le composant : c'est exactement le defaut
|
|
11
|
+
qui empeche de fermer un sous-menu Auriga depuis l'exterieur.
|
|
12
|
+
-->
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
export interface RmColumn {
|
|
15
|
+
/** Cle de la propriete dans la ligne, et nom du slot `#cell-<key>`. */
|
|
16
|
+
key: string
|
|
17
|
+
/** En-tete de colonne. */
|
|
18
|
+
label: string
|
|
19
|
+
/** Colonne d'identite : rendue en `<th scope="row">`. Une seule par table. */
|
|
20
|
+
rowHeader?: boolean
|
|
21
|
+
/** Valeur numerique : alignee a droite, chiffres tabulaires. */
|
|
22
|
+
numeric?: boolean
|
|
23
|
+
/** Colonne triable : rend l'en-tete cliquable et annonce `aria-sort`. */
|
|
24
|
+
sortable?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<{
|
|
28
|
+
/** Nom accessible de la table. Masque visuellement par defaut. */
|
|
29
|
+
caption: string
|
|
30
|
+
/** Affiche la caption au-dessus de la table (hors chart-card, qui porte deja le titre). */
|
|
31
|
+
captionVisible?: boolean
|
|
32
|
+
columns: RmColumn[]
|
|
33
|
+
rows: Record<string, unknown>[]
|
|
34
|
+
sortKey?: string
|
|
35
|
+
sortDir?: 'asc' | 'desc'
|
|
36
|
+
/** Message affiche quand il n'y a aucune ligne. Le hors-perimetre se dit, il ne s'estime pas. */
|
|
37
|
+
emptyText?: string
|
|
38
|
+
}>(), {
|
|
39
|
+
captionVisible: false,
|
|
40
|
+
sortKey: undefined,
|
|
41
|
+
sortDir: undefined,
|
|
42
|
+
emptyText: 'Aucune donnée disponible pour cette sélection.',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{ sort: [key: string] }>()
|
|
46
|
+
|
|
47
|
+
function ariaSort(column: RmColumn) {
|
|
48
|
+
if (!column.sortable) return undefined
|
|
49
|
+
if (props.sortKey !== column.key) return 'none'
|
|
50
|
+
return props.sortDir === 'desc' ? 'descending' : 'ascending'
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div class="tk-wrap">
|
|
56
|
+
<table class="tk">
|
|
57
|
+
<caption :class="captionVisible ? undefined : 'sr-only'">{{ caption }}</caption>
|
|
58
|
+
<thead>
|
|
59
|
+
<tr>
|
|
60
|
+
<th
|
|
61
|
+
v-for="column in columns"
|
|
62
|
+
:key="column.key"
|
|
63
|
+
scope="col"
|
|
64
|
+
:class="column.numeric ? 'tk-num' : undefined"
|
|
65
|
+
:aria-sort="ariaSort(column)"
|
|
66
|
+
:tabindex="column.sortable ? 0 : undefined"
|
|
67
|
+
@click="column.sortable && emit('sort', column.key)"
|
|
68
|
+
@keydown.enter.prevent="column.sortable && emit('sort', column.key)"
|
|
69
|
+
@keydown.space.prevent="column.sortable && emit('sort', column.key)"
|
|
70
|
+
>
|
|
71
|
+
{{ column.label }}
|
|
72
|
+
</th>
|
|
73
|
+
</tr>
|
|
74
|
+
</thead>
|
|
75
|
+
<tbody>
|
|
76
|
+
<tr v-if="!rows.length">
|
|
77
|
+
<td :colspan="columns.length">{{ emptyText }}</td>
|
|
78
|
+
</tr>
|
|
79
|
+
<tr v-for="(row, index) in rows" :key="index">
|
|
80
|
+
<template v-for="column in columns" :key="column.key">
|
|
81
|
+
<th v-if="column.rowHeader" scope="row">
|
|
82
|
+
<slot :name="`cell-${column.key}`" :value="row[column.key]" :row="row">{{ row[column.key] }}</slot>
|
|
83
|
+
</th>
|
|
84
|
+
<td v-else :class="column.numeric ? 'tk-num' : undefined">
|
|
85
|
+
<slot :name="`cell-${column.key}`" :value="row[column.key]" :row="row">{{ row[column.key] }}</slot>
|
|
86
|
+
</td>
|
|
87
|
+
</template>
|
|
88
|
+
</tr>
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmKpi - tuile KPI (chiffre + label + tendance). Style : components.css.
|
|
3
|
+
La valeur est toujours en serif (Caudex) : une valeur KPI en Figtree est
|
|
4
|
+
un bug. Pas de bloc de texte ici, seulement un chiffre et ses libelles.
|
|
5
|
+
La tendance porte un signe explicite ET une fleche, jamais la couleur seule.
|
|
6
|
+
-->
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
/** Libelle court, rendu en majuscules par le design system. */
|
|
12
|
+
label: string
|
|
13
|
+
/** Valeur deja formatee (voir utils/format : rmNumber, rmBig, rmPrice). */
|
|
14
|
+
value: string | number
|
|
15
|
+
/** Unite affichee apres la valeur : €, %, patients... */
|
|
16
|
+
unit?: string
|
|
17
|
+
/** Variation en points de pourcentage, deja signee. Affiche fleche + couleur semantique. */
|
|
18
|
+
trend?: number
|
|
19
|
+
/** Libelle de la periode comparee : "vs 2023". Obligatoire des qu'il y a une tendance. */
|
|
20
|
+
trendLabel?: string
|
|
21
|
+
/** Note de bas de tuile : precision de perimetre ou de methode. */
|
|
22
|
+
note?: string
|
|
23
|
+
/** Couleur produit. `brand` = violet de marque, le defaut en contexte generique. */
|
|
24
|
+
accent?: 'brand' | 'healthcare' | 'discovery' | 'pharma' | 'databank'
|
|
25
|
+
}>(), {
|
|
26
|
+
unit: undefined,
|
|
27
|
+
trend: undefined,
|
|
28
|
+
trendLabel: undefined,
|
|
29
|
+
note: undefined,
|
|
30
|
+
accent: 'brand',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const direction = computed(() => {
|
|
34
|
+
if (props.trend === undefined) return null
|
|
35
|
+
if (props.trend > 0) return 'up'
|
|
36
|
+
if (props.trend < 0) return 'down'
|
|
37
|
+
return 'flat'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const arrow = computed(() => ({ up: 'ArrowUp', down: 'ArrowDown', flat: 'ArrowRight' }[direction.value!]))
|
|
41
|
+
|
|
42
|
+
// Signe explicite + virgule decimale + espace avant le %, cf. utils/format.
|
|
43
|
+
const trendText = computed(() =>
|
|
44
|
+
props.trend === undefined
|
|
45
|
+
? ''
|
|
46
|
+
: (props.trend > 0 ? '+' : '') + props.trend.toFixed(1).replace('.', ',') + ' %',
|
|
47
|
+
)
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div class="kpi-card">
|
|
52
|
+
<p class="kpi-label">{{ label }}</p>
|
|
53
|
+
<p class="kpi-value" :class="accent === 'brand' ? undefined : accent">
|
|
54
|
+
{{ value }}<span v-if="unit" class="unit"> {{ unit }}</span>
|
|
55
|
+
</p>
|
|
56
|
+
<p v-if="direction" class="kpi-trend" :class="`kpi-trend--${direction}`">
|
|
57
|
+
<RmIcon :name="arrow" :size="11" weight="bold" />
|
|
58
|
+
<span>{{ trendText }}</span>
|
|
59
|
+
<span v-if="trendLabel">{{ trendLabel }}</span>
|
|
60
|
+
</p>
|
|
61
|
+
<p v-if="note" class="kpi-foot">{{ note }}</p>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmNavGroup - section de navigation repliable. Style : components.css.
|
|
3
|
+
|
|
4
|
+
L'etat d'ouverture est pilote par le PARENT (`v-model:open`). C'est le
|
|
5
|
+
point qui manquait a `argSubMenu` d'Auriga : son `openMenu` vivait a
|
|
6
|
+
l'interieur du composant, donc impossible de refermer une section depuis
|
|
7
|
+
l'exterieur, donc pas d'accordeon exclusif. Ici le parent garde une seule
|
|
8
|
+
cle ouverte et la sidebar reste courte meme avec beaucoup de sections.
|
|
9
|
+
-->
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
label: string
|
|
13
|
+
/** Icone Phosphor sans le prefixe "Ph". */
|
|
14
|
+
icon?: string
|
|
15
|
+
open?: boolean
|
|
16
|
+
}>(), { icon: undefined, open: false })
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{ 'update:open': [value: boolean] }>()
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="nav-group" :data-open="String(props.open)">
|
|
23
|
+
<button
|
|
24
|
+
class="nav-group-header"
|
|
25
|
+
type="button"
|
|
26
|
+
:aria-expanded="props.open"
|
|
27
|
+
@click="emit('update:open', !props.open)"
|
|
28
|
+
>
|
|
29
|
+
<RmIcon v-if="icon" :name="icon" :size="14" />
|
|
30
|
+
<span>{{ label }}</span>
|
|
31
|
+
<span class="caret"><RmIcon name="CaretDown" :size="12" weight="bold" /></span>
|
|
32
|
+
</button>
|
|
33
|
+
<div v-show="props.open" class="nav-group-items">
|
|
34
|
+
<slot />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatage OIP - locale francaise. Regles reprises du skill
|
|
3
|
+
* `tekkare-dashboard-design` (`utils/oip-format.ts`), qui fait foi.
|
|
4
|
+
*
|
|
5
|
+
* Regle dates : le mois est TOUJOURS en toutes lettres. Ce n'est pas une
|
|
6
|
+
* regle esthetique mais une regle de securite de lecture : `05/09/2025` se
|
|
7
|
+
* lit « 5 septembre » en France et « 9 mai » au format US. Interdits
|
|
8
|
+
* partout, y compris dans une cellule de tableau : tout en chiffres, ISO,
|
|
9
|
+
* mois abrege anglais.
|
|
10
|
+
*
|
|
11
|
+
* Regle nombres : virgule decimale, espace avant le %, separateur de
|
|
12
|
+
* milliers insecable via `toLocaleString('fr-FR')`.
|
|
13
|
+
*/
|
|
14
|
+
/** "17 juin 2026" - prose, foot « Verifie le », tableaux, partout. */
|
|
15
|
+
export declare const rmDate: (d: Date | string | number) => string;
|
|
16
|
+
/** "juin 2026" - quand le jour est inconnu. */
|
|
17
|
+
export declare const rmMonth: (d: Date | string | number) => string;
|
|
18
|
+
/** "1 234" - separateur de milliers francais. */
|
|
19
|
+
export declare const rmNumber: (n: number) => string;
|
|
20
|
+
/** "2 043 €" */
|
|
21
|
+
export declare const rmPrice: (n: number) => string;
|
|
22
|
+
/** "9,2 Md€" · "43 M€" · "12 k" - grands nombres abreges. */
|
|
23
|
+
export declare const rmBig: (n: number) => string;
|
|
24
|
+
/** "+18,2 %" / "-3,1 %" - signe explicite, virgule, espace avant le %. */
|
|
25
|
+
export declare const rmPct: (n: number, digits?: number) => string;
|
|
26
|
+
/** "8,7" - decimale francaise sans unite (ratios : HR, OR, taux). */
|
|
27
|
+
export declare const rmDecimal: (n: number, digits?: number) => string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const rmDate = (d) => new Date(d).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" });
|
|
2
|
+
export const rmMonth = (d) => new Date(d).toLocaleDateString("fr-FR", { month: "long", year: "numeric" });
|
|
3
|
+
export const rmNumber = (n) => n.toLocaleString("fr-FR");
|
|
4
|
+
export const rmPrice = (n) => n.toLocaleString("fr-FR") + " \u20AC";
|
|
5
|
+
export const rmBig = (n) => n >= 1e9 ? (n / 1e9).toFixed(1).replace(".", ",") + " Md\u20AC" : n >= 1e6 ? (n / 1e6).toFixed(0) + " M\u20AC" : n >= 1e3 ? (n / 1e3).toFixed(0) + " k" : String(n);
|
|
6
|
+
export const rmPct = (n, digits = 1) => (n > 0 ? "+" : "") + n.toFixed(digits).replace(".", ",") + " %";
|
|
7
|
+
export const rmDecimal = (n, digits = 1) => n.toFixed(digits).replace(".", ",");
|