@tekkare/romulus 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.4",
7
+ "version": "0.1.6",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -17,12 +17,23 @@ const module = defineNuxtModule({
17
17
  const runtimeDir = fileURLToPath(new URL("./runtime", import.meta.url));
18
18
  nuxt.options.css.push(resolve(runtimeDir, "assets/styles/tokens.css"));
19
19
  nuxt.options.css.push(resolve(runtimeDir, "assets/styles/base.css"));
20
+ nuxt.options.css.push(resolve(runtimeDir, "assets/styles/components.css"));
20
21
  nuxt.options.css.push(resolve(runtimeDir, "assets/styles/utilities.css"));
21
22
  await addComponentsDir({
22
23
  path: resolve(runtimeDir, "components"),
23
24
  prefix: options.prefix
24
25
  });
25
26
  addImportsDir(resolve(runtimeDir, "composables"));
27
+ addImportsDir(resolve(runtimeDir, "utils"));
28
+ nuxt.options.app.head.link = nuxt.options.app.head.link || [];
29
+ const fontsHref = "https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&family=Caudex:wght@400;700&display=swap";
30
+ if (!nuxt.options.app.head.link.some((l) => l.href === fontsHref)) {
31
+ nuxt.options.app.head.link.push(
32
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
33
+ { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" },
34
+ { rel: "stylesheet", href: fontsHref }
35
+ );
36
+ }
26
37
  }
27
38
  });
28
39
 
@@ -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:#4164ee;--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:#22c55e;--r-color-success-600:#16a34a;--r-color-warning-500:#f59e0b;--r-color-warning-600:#d97706;--r-color-danger-500:#ef4444;--r-color-danger-600:#dc2626;--r-color-white:#fff;--r-color-black:#000;--r-font-family:"Figtree",ui-sans-serif,system-ui,-apple-system,sans-serif;--r-font-mono:"JetBrains Mono",ui-monospace,"Cascadia Code",monospace;--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:400;--r-font-weight-medium:500;--r-font-weight-semibold:600;--r-font-weight-bold:700;--r-leading-tight:1.25;--r-leading-normal:1.5;--r-leading-relaxed:1.75;--r-space-0:0;--r-space-1:0.25rem;--r-space-2:0.5rem;--r-space-3:0.75rem;--r-space-4:1rem;--r-space-5:1.25rem;--r-space-6:1.5rem;--r-space-8:2rem;--r-space-10:2.5rem;--r-space-12:3rem;--r-space-16:4rem;--r-space-20:5rem;--r-radius-none:0;--r-radius-sm:0.25rem;--r-radius-md:0.375rem;--r-radius-lg:0.5rem;--r-radius-xl:0.75rem;--r-radius-2xl:1rem;--r-radius-full:9999px;--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:150ms cubic-bezier(0.4,0,0.2,1);--r-transition-base:200ms cubic-bezier(0.4,0,0.2,1);--r-transition-slow:300ms cubic-bezier(0.4,0,0.2,1)}
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)}
@@ -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">&#160;{{ 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(".", ",");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",