@tekkare/romulus 0.1.218 → 0.1.220

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.217",
7
+ "version": "0.1.219",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -1 +1 @@
1
- :root{--sidebar-w:220px;--sidebar-collapsed-w:56px;--rsidebar-w:280px;--rsidebar-collapsed-w:76px;--topbar-h:56px;--topbar-merged-h:0px;--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:#3451d4;--color-discovery-active:#066b5c;--color-pharma-active:#d9470c;--color-databank-active:#c73a6f;--color-pharma-soft:#ff7942;--healthcare-rgb:65,100,238;--discovery-rgb:7,129,111;--pharma-rgb:247,82,14;--databank-rgb:224,67,128;--surface-rgb:255,255,255;--shadow-rgb:30,41,59;--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-10:10px;--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-healthcare:#7aa0ff;--color-discovery:#2ecc71;--color-pharma:#ff8c00;--color-databank:#f472b6;--color-healthcare-active:#4164ee;--color-discovery-active:#07816f;--color-pharma-active:#f7520e;--color-databank-active:#e04380;--healthcare-rgb:122,160,255;--discovery-rgb:46,204,113;--pharma-rgb:255,140,0;--databank-rgb:244,114,182;--surface-rgb:26,26,36;--shadow-rgb:0,0,0;--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);color-scheme:dark}body.text-small{--font-10:9px;--font-11:10px;--font-12:11px;--font-13:12px;--font-14:13px;--font-16:15px;--font-18:17px;--font-22:20px;--font-24:22px;--font-32:29px}body.text-large{--font-10:11px;--font-11:12px;--font-12:13px;--font-13:15px;--font-14:16px;--font-16:18px;--font-18:20px;--font-22:25px;--font-24:27px;--font-32:36px}body.text-xl{--font-10:12px;--font-11:13px;--font-12:15px;--font-13:16px;--font-14:18px;--font-16:20px;--font-18:22px;--font-22:28px;--font-24:30px;--font-32:40px}body.density-compact{--space-4:3px;--space-6:4px;--space-8:6px;--space-10:8px;--space-12:9px;--space-16:12px;--space-20:15px;--space-24:18px;--space-32:24px;--space-40:30px;--space-48:36px}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}body.cvd-deutera .pane,body.cvd-deutera .rm-cvd-target,body.cvd-deutera .rm-shell,body.cvd-deutera .rsidebar,body.cvd-deutera .sidebar,body.cvd-deutera>.rm-action-bar__overlay,body.cvd-deutera>.rm-drawer,body.cvd-deutera>.rm-drawer-overlay,body.cvd-deutera>.rm-modal-overlay{filter:url(#a11y-deutera)}body.cvd-protan .pane,body.cvd-protan .rm-cvd-target,body.cvd-protan .rm-shell,body.cvd-protan .rsidebar,body.cvd-protan .sidebar,body.cvd-protan>.rm-action-bar__overlay,body.cvd-protan>.rm-drawer,body.cvd-protan>.rm-drawer-overlay,body.cvd-protan>.rm-modal-overlay{filter:url(#a11y-protan)}body.cvd-tritan .pane,body.cvd-tritan .rm-cvd-target,body.cvd-tritan .rm-shell,body.cvd-tritan .rsidebar,body.cvd-tritan .sidebar,body.cvd-tritan>.rm-action-bar__overlay,body.cvd-tritan>.rm-drawer,body.cvd-tritan>.rm-drawer-overlay,body.cvd-tritan>.rm-modal-overlay{filter:url(#a11y-tritan)}body.cvd-mono .pane,body.cvd-mono .rm-cvd-target,body.cvd-mono .rm-shell,body.cvd-mono .rsidebar,body.cvd-mono .sidebar,body.cvd-mono>.rm-action-bar__overlay,body.cvd-mono>.rm-drawer,body.cvd-mono>.rm-drawer-overlay,body.cvd-mono>.rm-modal-overlay{filter:url(#a11y-mono)}: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
+ :root{--sidebar-w:220px;--sidebar-collapsed-w:56px;--rsidebar-w:280px;--rsidebar-collapsed-w:76px;--topbar-h:56px;--topbar-merged-h:0px;--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:#3451d4;--color-discovery-active:#066b5c;--color-pharma-active:#d9470c;--color-databank-active:#c73a6f;--color-pharma-soft:#ff7942;--healthcare-rgb:65,100,238;--discovery-rgb:7,129,111;--pharma-rgb:247,82,14;--databank-rgb:224,67,128;--surface-rgb:255,255,255;--shadow-rgb:30,41,59;--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;--rm-text-c-gray:#6b7280;--rm-text-c-red:#b91c1c;--rm-text-c-orange:#c2410c;--rm-text-c-amber:#a16207;--rm-text-c-green:#15803d;--rm-text-c-teal:#0f766e;--rm-text-c-blue:#1d4ed8;--rm-text-c-violet:#6d28d9;--rm-text-c-pink:#be185d;--rm-text-bg-gray:hsla(220,9%,46%,.16);--rm-text-bg-red:rgba(239,68,68,.18);--rm-text-bg-orange:rgba(249,115,22,.2);--rm-text-bg-amber:rgba(250,204,21,.32);--rm-text-bg-green:rgba(34,197,94,.2);--rm-text-bg-teal:rgba(20,184,166,.2);--rm-text-bg-blue:rgba(59,130,246,.18);--rm-text-bg-violet:rgba(139,92,246,.18);--rm-text-bg-pink:rgba(236,72,153,.18);--space-2:2px;--space-4:4px;--space-6:6px;--space-8:8px;--space-10:10px;--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-healthcare:#7aa0ff;--color-discovery:#2ecc71;--color-pharma:#ff8c00;--color-databank:#f472b6;--color-healthcare-active:#4164ee;--color-discovery-active:#07816f;--color-pharma-active:#f7520e;--color-databank-active:#e04380;--healthcare-rgb:122,160,255;--discovery-rgb:46,204,113;--pharma-rgb:255,140,0;--databank-rgb:244,114,182;--surface-rgb:26,26,36;--shadow-rgb:0,0,0;--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;--rm-text-c-gray:#9ca3af;--rm-text-c-red:#fca5a5;--rm-text-c-orange:#fdba74;--rm-text-c-amber:#fde047;--rm-text-c-green:#86efac;--rm-text-c-teal:#5eead4;--rm-text-c-blue:#93c5fd;--rm-text-c-violet:#c4b5fd;--rm-text-c-pink:#f9a8d4;--rm-text-bg-gray:rgba(156,163,175,.22);--rm-text-bg-red:hsla(0,91%,71%,.24);--rm-text-bg-orange:rgba(251,146,60,.24);--rm-text-bg-amber:rgba(250,204,21,.24);--rm-text-bg-green:rgba(74,222,128,.22);--rm-text-bg-teal:rgba(45,212,191,.22);--rm-text-bg-blue:rgba(96,165,250,.24);--rm-text-bg-violet:rgba(167,139,250,.26);--rm-text-bg-pink:rgba(244,114,182,.24);background:var(--bg-app);color:var(--text-primary);color-scheme:dark}body.text-small{--font-10:9px;--font-11:10px;--font-12:11px;--font-13:12px;--font-14:13px;--font-16:15px;--font-18:17px;--font-22:20px;--font-24:22px;--font-32:29px}body.text-large{--font-10:11px;--font-11:12px;--font-12:13px;--font-13:15px;--font-14:16px;--font-16:18px;--font-18:20px;--font-22:25px;--font-24:27px;--font-32:36px}body.text-xl{--font-10:12px;--font-11:13px;--font-12:15px;--font-13:16px;--font-14:18px;--font-16:20px;--font-18:22px;--font-22:28px;--font-24:30px;--font-32:40px}body.density-compact{--space-4:3px;--space-6:4px;--space-8:6px;--space-10:8px;--space-12:9px;--space-16:12px;--space-20:15px;--space-24:18px;--space-32:24px;--space-40:30px;--space-48:36px}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}body.cvd-deutera .pane,body.cvd-deutera .rm-cvd-target,body.cvd-deutera .rm-shell,body.cvd-deutera .rsidebar,body.cvd-deutera .sidebar,body.cvd-deutera>.rm-action-bar__overlay,body.cvd-deutera>.rm-drawer,body.cvd-deutera>.rm-drawer-overlay,body.cvd-deutera>.rm-modal-overlay{filter:url(#a11y-deutera)}body.cvd-protan .pane,body.cvd-protan .rm-cvd-target,body.cvd-protan .rm-shell,body.cvd-protan .rsidebar,body.cvd-protan .sidebar,body.cvd-protan>.rm-action-bar__overlay,body.cvd-protan>.rm-drawer,body.cvd-protan>.rm-drawer-overlay,body.cvd-protan>.rm-modal-overlay{filter:url(#a11y-protan)}body.cvd-tritan .pane,body.cvd-tritan .rm-cvd-target,body.cvd-tritan .rm-shell,body.cvd-tritan .rsidebar,body.cvd-tritan .sidebar,body.cvd-tritan>.rm-action-bar__overlay,body.cvd-tritan>.rm-drawer,body.cvd-tritan>.rm-drawer-overlay,body.cvd-tritan>.rm-modal-overlay{filter:url(#a11y-tritan)}body.cvd-mono .pane,body.cvd-mono .rm-cvd-target,body.cvd-mono .rm-shell,body.cvd-mono .rsidebar,body.cvd-mono .sidebar,body.cvd-mono>.rm-action-bar__overlay,body.cvd-mono>.rm-drawer,body.cvd-mono>.rm-drawer-overlay,body.cvd-mono>.rm-modal-overlay{filter:url(#a11y-mono)}: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,726 @@
1
+ <!--
2
+ RmRichTextEditor - editeur de texte enrichi facon Notion, sur Tiptap.
3
+
4
+ A ne pas confondre avec RmNoteEditor, qui redige les notes a blocs types du
5
+ panneau « Notes ». Celui-ci produit un document libre : le JSON de Tiptap
6
+ (`v-model`), que l'appelant enregistre tel quel.
7
+
8
+ Ce qu'il sait faire :
9
+ - gras, italique, souligne, barre, code, liens ;
10
+ - couleurs de texte et surlignage, dans une palette limitee qui se relit de
11
+ jour comme de nuit (variables CSS enregistrees dans le document) ;
12
+ - titres, listes, cases a cocher, citation, bloc de code, separateur,
13
+ tableaux (ajout et retrait de lignes et de colonnes) ;
14
+ - un menu `/` pour inserer un bloc ;
15
+ - un menu `@` qui cite un membre (`members`), une ressource
16
+ (`searchResources`) ou pose un lien vers une page (`pages`), et propose de
17
+ creer la page tapee (`createPage`) ;
18
+ - Alt+Haut / Alt+Bas deplacent le bloc courant (pas de poignee de glisser :
19
+ l'extension Tiptap exige la pile de collaboration Yjs).
20
+
21
+ L'editeur ne sait rien de ce qu'il cite : un clic sur une mention ou un lien
22
+ de page emet `open-resource`, `open-member` ou `open-page`, a l'appelant de
23
+ naviguer.
24
+
25
+ Noeuds ecrits dans le document, que le serveur peut relire :
26
+ - `mention` : `attrs.kind` ("user" | "resource"), `id`, `label`,
27
+ `resourceType` (ressource seulement) ;
28
+ - `noteLink` : `attrs.noteId`, `label`, `icon`.
29
+ -->
30
+ <script setup lang="ts">
31
+ import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
32
+ import { EditorContent, useEditor } from '@tiptap/vue-3'
33
+ import { Extension, Node, mergeAttributes, type Editor, type Range } from '@tiptap/core'
34
+ import { PluginKey } from '@tiptap/pm/state'
35
+ import StarterKit from '@tiptap/starter-kit'
36
+ import { Color, TextStyle } from '@tiptap/extension-text-style'
37
+ import Highlight from '@tiptap/extension-highlight'
38
+ import { TableKit } from '@tiptap/extension-table'
39
+ import { TaskItem, TaskList } from '@tiptap/extension-list'
40
+ import { Placeholder } from '@tiptap/extensions'
41
+ import Mention from '@tiptap/extension-mention'
42
+ import Suggestion, { type SuggestionProps } from '@tiptap/suggestion'
43
+ import {
44
+ RM_RICH_TEXT_COLORS,
45
+ mentionItems,
46
+ moveBlock,
47
+ richTextColor,
48
+ richTextHighlight,
49
+ slashItems,
50
+ type RmRichTextColor,
51
+ type RmRichTextMember,
52
+ type RmRichTextMentionItem,
53
+ type RmRichTextPage,
54
+ type RmRichTextResource,
55
+ type RmRichTextSlashItem,
56
+ } from '../utils/richText'
57
+
58
+ const props = withDefaults(
59
+ defineProps<{
60
+ modelValue?: Record<string, unknown> | null
61
+ editable?: boolean
62
+ placeholder?: string
63
+ /** Les membres que `@` peut citer. Vide : pas de mention de personne. */
64
+ members?: RmRichTextMember[]
65
+ /** Les pages vers lesquelles `@` pose un lien. */
66
+ pages?: RmRichTextPage[]
67
+ /** Cherche les ressources citables pour une saisie. Absent : pas de ressources. */
68
+ searchResources?: (query: string) => Promise<RmRichTextResource[]>
69
+ /** Cree une page du nom tape et la rend. Absent : pas de creation. */
70
+ createPage?: (title: string) => Promise<RmRichTextPage>
71
+ /** Affiche la barre d'outils. */
72
+ toolbar?: boolean
73
+ }>(),
74
+ {
75
+ modelValue: null,
76
+ editable: true,
77
+ placeholder: 'Écrivez, ou tapez « / » pour insérer un bloc, « @ » pour citer…',
78
+ members: () => [],
79
+ pages: () => [],
80
+ searchResources: undefined,
81
+ createPage: undefined,
82
+ toolbar: true,
83
+ },
84
+ )
85
+
86
+ const emit = defineEmits<{
87
+ 'update:modelValue': [value: Record<string, unknown>]
88
+ 'open-page': [id: string]
89
+ 'open-member': [id: string]
90
+ 'open-resource': [resource: { id: string; resourceType: string }]
91
+ }>()
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Menus flottants (`/` et `@`) : un seul etat, rendu par le template.
95
+ // ---------------------------------------------------------------------------
96
+
97
+ type MenuKind = 'slash' | 'mention'
98
+ type MenuItem = RmRichTextSlashItem | RmRichTextMentionItem
99
+ const menu = reactive({
100
+ open: false,
101
+ kind: 'slash' as MenuKind,
102
+ items: [] as MenuItem[],
103
+ index: 0,
104
+ loading: false,
105
+ style: {} as Record<string, string>,
106
+ })
107
+ let menuCommand: ((item: MenuItem) => void) | null = null
108
+
109
+ function placeMenu(rect: DOMRect | null | undefined) {
110
+ if (!rect) return
111
+ const vh = window.innerHeight
112
+ const below = vh - rect.bottom
113
+ const left = Math.max(8, Math.min(rect.left, window.innerWidth - 300))
114
+ menu.style =
115
+ below < 280 && rect.top > below
116
+ ? { position: 'fixed', left: `${left}px`, bottom: `${vh - rect.top + 6}px` }
117
+ : { position: 'fixed', left: `${left}px`, top: `${rect.bottom + 6}px` }
118
+ }
119
+
120
+ function menuRenderer(kind: MenuKind) {
121
+ return () => ({
122
+ onStart(p: SuggestionProps<MenuItem>) {
123
+ menu.kind = kind
124
+ menu.items = p.items
125
+ menu.index = 0
126
+ menu.open = true
127
+ menuCommand = p.command
128
+ placeMenu(p.clientRect?.())
129
+ },
130
+ onUpdate(p: SuggestionProps<MenuItem>) {
131
+ menu.items = p.items
132
+ menu.index = 0
133
+ menuCommand = p.command
134
+ placeMenu(p.clientRect?.())
135
+ },
136
+ onKeyDown({ event }: { event: KeyboardEvent }) {
137
+ if (!menu.open) return false
138
+ const count = Math.max(1, menu.items.length)
139
+ if (event.key === 'ArrowDown') {
140
+ menu.index = (menu.index + 1) % count
141
+ return true
142
+ }
143
+ if (event.key === 'ArrowUp') {
144
+ menu.index = (menu.index - 1 + count) % count
145
+ return true
146
+ }
147
+ if (event.key === 'Enter' || event.key === 'Tab') {
148
+ const item = menu.items[menu.index]
149
+ if (!item) return false
150
+ pick(item)
151
+ return true
152
+ }
153
+ if (event.key === 'Escape') {
154
+ menu.open = false
155
+ return true
156
+ }
157
+ return false
158
+ },
159
+ onExit() {
160
+ menu.open = false
161
+ menuCommand = null
162
+ },
163
+ })
164
+ }
165
+
166
+ function pick(item: MenuItem) {
167
+ menuCommand?.(item)
168
+ menu.open = false
169
+ }
170
+
171
+ // Les ressources se cherchent chez l'appelant : la derniere reponse gagne.
172
+ let resourceTicket = 0
173
+ async function findResources(query: string): Promise<RmRichTextResource[]> {
174
+ if (!props.searchResources || query.trim().length < 2) return []
175
+ const ticket = ++resourceTicket
176
+ menu.loading = true
177
+ try {
178
+ const found = await props.searchResources(query.trim())
179
+ return ticket === resourceTicket ? found : []
180
+ } catch {
181
+ return []
182
+ } finally {
183
+ if (ticket === resourceTicket) menu.loading = false
184
+ }
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Extensions
189
+ // ---------------------------------------------------------------------------
190
+
191
+ /** Lien vers une autre page. */
192
+ const NoteLink = Node.create({
193
+ name: 'noteLink',
194
+ group: 'inline',
195
+ inline: true,
196
+ atom: true,
197
+ selectable: true,
198
+ addAttributes() {
199
+ return {
200
+ noteId: { default: null, parseHTML: (el) => el.getAttribute('data-note-id') },
201
+ label: { default: '', parseHTML: (el) => el.getAttribute('data-label') },
202
+ icon: { default: null, parseHTML: (el) => el.getAttribute('data-icon') },
203
+ }
204
+ },
205
+ parseHTML() {
206
+ return [{ tag: 'span[data-note-link]' }]
207
+ },
208
+ renderHTML({ node, HTMLAttributes }) {
209
+ return [
210
+ 'span',
211
+ mergeAttributes(HTMLAttributes, {
212
+ 'data-note-link': '',
213
+ 'data-note-id': node.attrs.noteId,
214
+ 'data-label': node.attrs.label,
215
+ class: 'rm-rte-pagelink',
216
+ }),
217
+ `${node.attrs.icon ? `${node.attrs.icon} ` : ''}${node.attrs.label || 'Page sans titre'}`,
218
+ ]
219
+ },
220
+ renderText({ node }) {
221
+ return node.attrs.label
222
+ },
223
+ })
224
+
225
+ /** Mention d'un membre ou d'une ressource ; `@` ouvre aussi les pages. */
226
+ const RichMention = Mention.extend({
227
+ addAttributes() {
228
+ return {
229
+ ...this.parent?.(),
230
+ kind: { default: 'user', parseHTML: (el) => el.getAttribute('data-kind') },
231
+ resourceType: { default: null, parseHTML: (el) => el.getAttribute('data-resource-type') },
232
+ }
233
+ },
234
+ }).configure({
235
+ renderHTML({ node }) {
236
+ return [
237
+ 'span',
238
+ {
239
+ 'data-type': 'mention',
240
+ 'data-kind': node.attrs.kind,
241
+ 'data-id': node.attrs.id,
242
+ 'data-label': node.attrs.label,
243
+ ...(node.attrs.resourceType && { 'data-resource-type': node.attrs.resourceType }),
244
+ class: `rm-rte-mention rm-rte-mention--${node.attrs.kind}`,
245
+ },
246
+ `@${node.attrs.label ?? node.attrs.id}`,
247
+ ]
248
+ },
249
+ renderText({ node }) {
250
+ return `@${node.attrs.label ?? node.attrs.id}`
251
+ },
252
+ suggestion: {
253
+ char: '@',
254
+ pluginKey: new PluginKey('rmRichTextMention'),
255
+ allowSpaces: true,
256
+ items: async ({ query }: { query: string }) =>
257
+ mentionItems({
258
+ query,
259
+ members: props.members,
260
+ pages: props.pages,
261
+ resources: await findResources(query),
262
+ canCreatePage: !!props.createPage,
263
+ }),
264
+ command: ({ editor, range, props: item }) => {
265
+ void insertMention(editor, range, item as unknown as RmRichTextMentionItem)
266
+ },
267
+ render: menuRenderer('mention'),
268
+ },
269
+ })
270
+
271
+ async function insertMention(editor: Editor, range: Range, item: RmRichTextMentionItem) {
272
+ if (item.kind === 'create-page') {
273
+ if (!props.createPage) return
274
+ editor.chain().focus().deleteRange(range).run()
275
+ const page = await props.createPage(item.label)
276
+ editor
277
+ .chain()
278
+ .focus()
279
+ .insertContent([
280
+ { type: 'noteLink', attrs: { noteId: page.id, label: page.label, icon: page.icon ?? null } },
281
+ { type: 'text', text: ' ' },
282
+ ])
283
+ .run()
284
+ return
285
+ }
286
+ const node =
287
+ item.kind === 'page'
288
+ ? { type: 'noteLink', attrs: { noteId: item.id, label: item.label, icon: item.icon ?? null } }
289
+ : {
290
+ type: 'mention',
291
+ attrs: {
292
+ kind: item.kind,
293
+ id: item.id,
294
+ label: item.label,
295
+ resourceType: item.kind === 'resource' ? item.resourceType : null,
296
+ },
297
+ }
298
+ editor.chain().focus().deleteRange(range).insertContent([node, { type: 'text', text: ' ' }]).run()
299
+ }
300
+
301
+ function runSlash(editor: Editor, range: Range, key: string) {
302
+ const chain = editor.chain().focus().deleteRange(range)
303
+ switch (key) {
304
+ case 'paragraph': chain.setParagraph().run(); break
305
+ case 'h1': chain.setHeading({ level: 1 }).run(); break
306
+ case 'h2': chain.setHeading({ level: 2 }).run(); break
307
+ case 'h3': chain.setHeading({ level: 3 }).run(); break
308
+ case 'bullet': chain.toggleBulletList().run(); break
309
+ case 'ordered': chain.toggleOrderedList().run(); break
310
+ case 'task': chain.toggleTaskList().run(); break
311
+ case 'quote': chain.toggleBlockquote().run(); break
312
+ case 'code': chain.toggleCodeBlock().run(); break
313
+ case 'table': chain.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); break
314
+ case 'divider': chain.setHorizontalRule().run(); break
315
+ case 'mention': chain.insertContent('@').run(); break
316
+ }
317
+ }
318
+
319
+ /** Le menu `/` et le deplacement de bloc au clavier. */
320
+ const RichCommands = Extension.create({
321
+ name: 'rmRichTextCommands',
322
+ addKeyboardShortcuts() {
323
+ return {
324
+ 'Alt-ArrowUp': () => this.editor.commands.command(moveBlock(-1)),
325
+ 'Alt-ArrowDown': () => this.editor.commands.command(moveBlock(1)),
326
+ }
327
+ },
328
+ addProseMirrorPlugins() {
329
+ return [
330
+ Suggestion<RmRichTextSlashItem>({
331
+ editor: this.editor,
332
+ char: '/',
333
+ pluginKey: new PluginKey('rmRichTextSlash'),
334
+ items: ({ query }) => slashItems(query),
335
+ command: ({ editor, range, props: item }) => runSlash(editor, range, item.key),
336
+ render: menuRenderer('slash') as any,
337
+ }),
338
+ ]
339
+ },
340
+ })
341
+
342
+ let lastEmitted = ''
343
+
344
+ const editor = useEditor({
345
+ content: props.modelValue ?? '',
346
+ editable: props.editable,
347
+ extensions: [
348
+ StarterKit.configure({
349
+ heading: { levels: [1, 2, 3] },
350
+ link: { openOnClick: false, autolink: true, HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' } },
351
+ }),
352
+ TextStyle,
353
+ Color,
354
+ Highlight.configure({ multicolor: true }),
355
+ TableKit.configure({ table: { resizable: false } }),
356
+ TaskList,
357
+ TaskItem.configure({ nested: true }),
358
+ Placeholder.configure({ placeholder: () => props.placeholder }),
359
+ NoteLink,
360
+ RichMention,
361
+ RichCommands,
362
+ ],
363
+ onUpdate: ({ editor: e }) => {
364
+ const json = e.getJSON()
365
+ lastEmitted = JSON.stringify(json)
366
+ emit('update:modelValue', json)
367
+ },
368
+ })
369
+
370
+ // Un document qui change du dehors (autre page chargee) remplace le contenu ;
371
+ // l'echo de notre propre `update:modelValue` ne le touche pas.
372
+ watch(
373
+ () => props.modelValue,
374
+ (value) => {
375
+ const e = editor.value
376
+ if (!e) return
377
+ const next = JSON.stringify(value ?? null)
378
+ if (next === lastEmitted || next === JSON.stringify(e.getJSON())) return
379
+ lastEmitted = next
380
+ e.commands.setContent(value ?? '', { emitUpdate: false })
381
+ },
382
+ )
383
+ watch(() => props.editable, (value) => editor.value?.setEditable(value))
384
+
385
+ onBeforeUnmount(() => editor.value?.destroy())
386
+
387
+ // Un clic sur une mention ou un lien de page : a l'appelant de naviguer.
388
+ function onClick(event: MouseEvent) {
389
+ const target = (event.target as HTMLElement).closest<HTMLElement>('[data-note-link], .rm-rte-mention')
390
+ if (!target) return
391
+ if (target.hasAttribute('data-note-link')) {
392
+ const id = target.getAttribute('data-note-id')
393
+ if (id) emit('open-page', id)
394
+ return
395
+ }
396
+ const id = target.getAttribute('data-id')
397
+ if (!id) return
398
+ if (target.getAttribute('data-kind') === 'resource') {
399
+ emit('open-resource', { id, resourceType: target.getAttribute('data-resource-type') ?? '' })
400
+ } else {
401
+ emit('open-member', id)
402
+ }
403
+ }
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // Barre d'outils
407
+ // ---------------------------------------------------------------------------
408
+
409
+ const inTable = computed(() => !!editor.value?.isActive('table'))
410
+ const blockStyle = computed(() => {
411
+ const e = editor.value
412
+ if (!e) return 'paragraph'
413
+ for (const level of [1, 2, 3] as const) if (e.isActive('heading', { level })) return `h${level}`
414
+ return 'paragraph'
415
+ })
416
+
417
+ function setBlockStyle(value: string) {
418
+ const chain = editor.value?.chain().focus()
419
+ if (!chain) return
420
+ if (value === 'paragraph') chain.setParagraph().run()
421
+ else chain.setHeading({ level: Number(value.slice(1)) as 1 | 2 | 3 }).run()
422
+ }
423
+
424
+ const colorsOpen = ref(false)
425
+ const colorsButton = ref<HTMLElement>()
426
+ const colorsPanel = ref<HTMLElement>()
427
+ const colorsStyle = ref<Record<string, string>>({})
428
+
429
+ function toggleColors() {
430
+ colorsOpen.value = !colorsOpen.value
431
+ const rect = colorsButton.value?.getBoundingClientRect()
432
+ if (rect) colorsStyle.value = { position: 'fixed', left: `${rect.left}px`, top: `${rect.bottom + 6}px` }
433
+ }
434
+
435
+ function setColor(key: RmRichTextColor | null) {
436
+ const chain = editor.value?.chain().focus()
437
+ if (!chain) return
438
+ if (key) chain.setColor(richTextColor(key)).run()
439
+ else chain.unsetColor().run()
440
+ colorsOpen.value = false
441
+ }
442
+
443
+ function setHighlight(key: RmRichTextColor | null) {
444
+ const chain = editor.value?.chain().focus()
445
+ if (!chain) return
446
+ if (key) chain.setHighlight({ color: richTextHighlight(key) }).run()
447
+ else chain.unsetHighlight().run()
448
+ colorsOpen.value = false
449
+ }
450
+
451
+ const linkOpen = ref(false)
452
+ const linkUrl = ref('')
453
+ const linkInput = ref<HTMLInputElement>()
454
+
455
+ function openLink() {
456
+ linkUrl.value = (editor.value?.getAttributes('link').href as string | undefined) ?? ''
457
+ linkOpen.value = true
458
+ setTimeout(() => linkInput.value?.focus())
459
+ }
460
+
461
+ function applyLink() {
462
+ const chain = editor.value?.chain().focus().extendMarkRange('link')
463
+ if (!chain) return
464
+ const url = linkUrl.value.trim()
465
+ if (!url) chain.unsetLink().run()
466
+ else chain.setLink({ href: /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `https://${url}` }).run()
467
+ linkOpen.value = false
468
+ }
469
+
470
+ function onDocumentPointer(event: PointerEvent) {
471
+ const target = event.target as globalThis.Node
472
+ if (colorsOpen.value && !colorsPanel.value?.contains(target) && !colorsButton.value?.contains(target)) {
473
+ colorsOpen.value = false
474
+ }
475
+ }
476
+ if (typeof document !== 'undefined') document.addEventListener('pointerdown', onDocumentPointer)
477
+ onBeforeUnmount(() => document.removeEventListener('pointerdown', onDocumentPointer))
478
+
479
+ const marks = [
480
+ { name: 'bold', icon: 'TextB', label: 'Gras (Ctrl+B)', run: (e: Editor) => e.chain().focus().toggleBold().run() },
481
+ { name: 'italic', icon: 'TextItalic', label: 'Italique (Ctrl+I)', run: (e: Editor) => e.chain().focus().toggleItalic().run() },
482
+ { name: 'underline', icon: 'TextUnderline', label: 'Souligné (Ctrl+U)', run: (e: Editor) => e.chain().focus().toggleUnderline().run() },
483
+ { name: 'strike', icon: 'TextStrikethrough', label: 'Barré', run: (e: Editor) => e.chain().focus().toggleStrike().run() },
484
+ { name: 'code', icon: 'Code', label: 'Code', run: (e: Editor) => e.chain().focus().toggleCode().run() },
485
+ ]
486
+
487
+ const blocks = [
488
+ { name: 'bulletList', icon: 'ListBullets', label: 'Liste à puces', run: (e: Editor) => e.chain().focus().toggleBulletList().run() },
489
+ { name: 'orderedList', icon: 'ListNumbers', label: 'Liste numérotée', run: (e: Editor) => e.chain().focus().toggleOrderedList().run() },
490
+ { name: 'taskList', icon: 'ListChecks', label: 'Liste de tâches', run: (e: Editor) => e.chain().focus().toggleTaskList().run() },
491
+ { name: 'blockquote', icon: 'Quotes', label: 'Citation', run: (e: Editor) => e.chain().focus().toggleBlockquote().run() },
492
+ ]
493
+
494
+ const tableActions = [
495
+ { icon: 'RowsPlusBottom', label: 'Ajouter une ligne', run: (e: Editor) => e.chain().focus().addRowAfter().run() },
496
+ { icon: 'ColumnsPlusRight', label: 'Ajouter une colonne', run: (e: Editor) => e.chain().focus().addColumnAfter().run() },
497
+ { icon: 'Rows', label: 'Supprimer la ligne', run: (e: Editor) => e.chain().focus().deleteRow().run() },
498
+ { icon: 'Columns', label: 'Supprimer la colonne', run: (e: Editor) => e.chain().focus().deleteColumn().run() },
499
+ { icon: 'Trash', label: 'Supprimer le tableau', run: (e: Editor) => e.chain().focus().deleteTable().run() },
500
+ ]
501
+
502
+ defineExpose({ editor })
503
+
504
+ const GROUP_LABEL: Record<RmRichTextMentionItem['kind'], string> = {
505
+ user: 'Personnes',
506
+ resource: 'Ressources',
507
+ page: 'Pages',
508
+ 'create-page': 'Nouvelle page',
509
+ }
510
+ const MENTION_ICON: Record<RmRichTextMentionItem['kind'], string> = {
511
+ user: 'User',
512
+ resource: 'Pill',
513
+ page: 'FileText',
514
+ 'create-page': 'Plus',
515
+ }
516
+ const asSlash = (item: MenuItem) => item as RmRichTextSlashItem
517
+ const asMention = (item: MenuItem) => item as RmRichTextMentionItem
518
+ const itemKey = (item: MenuItem) => ('kind' in item ? `${item.kind}-${item.id}` : `slash-${item.key}`)
519
+ const showGroup = (i: number) =>
520
+ menu.kind === 'mention' && (i === 0 || asMention(menu.items[i]!).kind !== asMention(menu.items[i - 1]!).kind)
521
+ </script>
522
+
523
+ <template>
524
+ <div class="rm-rte" :class="{ 'rm-rte--readonly': !editable }">
525
+ <div v-if="toolbar && editable && editor" class="rm-rte__toolbar" role="toolbar" aria-label="Mise en forme">
526
+ <select
527
+ class="rm-rte__style"
528
+ :value="blockStyle"
529
+ aria-label="Style du paragraphe"
530
+ @change="setBlockStyle(($event.target as HTMLSelectElement).value)"
531
+ >
532
+ <option value="paragraph">Texte</option>
533
+ <option value="h1">Titre 1</option>
534
+ <option value="h2">Titre 2</option>
535
+ <option value="h3">Titre 3</option>
536
+ </select>
537
+ <span class="rm-rte__sep" />
538
+ <button
539
+ v-for="mark in marks"
540
+ :key="mark.name"
541
+ type="button"
542
+ class="rm-rte__btn"
543
+ :class="{ 'is-active': editor.isActive(mark.name) }"
544
+ :aria-pressed="editor.isActive(mark.name)"
545
+ :title="mark.label"
546
+ :aria-label="mark.label"
547
+ @click="mark.run(editor)"
548
+ >
549
+ <RmIcon :name="mark.icon" :size="16" />
550
+ </button>
551
+ <button
552
+ ref="colorsButton"
553
+ type="button"
554
+ class="rm-rte__btn"
555
+ :class="{ 'is-active': colorsOpen }"
556
+ title="Couleur du texte et surlignage"
557
+ aria-label="Couleur du texte et surlignage"
558
+ :aria-expanded="colorsOpen"
559
+ @click="toggleColors"
560
+ >
561
+ <RmIcon name="Palette" :size="16" />
562
+ </button>
563
+ <button
564
+ type="button"
565
+ class="rm-rte__btn"
566
+ :class="{ 'is-active': editor.isActive('link') }"
567
+ title="Lien"
568
+ aria-label="Lien"
569
+ @click="openLink"
570
+ >
571
+ <RmIcon name="Link" :size="16" />
572
+ </button>
573
+ <span class="rm-rte__sep" />
574
+ <button
575
+ v-for="block in blocks"
576
+ :key="block.name"
577
+ type="button"
578
+ class="rm-rte__btn"
579
+ :class="{ 'is-active': editor.isActive(block.name) }"
580
+ :aria-pressed="editor.isActive(block.name)"
581
+ :title="block.label"
582
+ :aria-label="block.label"
583
+ @click="block.run(editor)"
584
+ >
585
+ <RmIcon :name="block.icon" :size="16" />
586
+ </button>
587
+ <button
588
+ type="button"
589
+ class="rm-rte__btn"
590
+ title="Insérer un tableau"
591
+ aria-label="Insérer un tableau"
592
+ :disabled="inTable"
593
+ @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"
594
+ >
595
+ <RmIcon name="Table" :size="16" />
596
+ </button>
597
+ <template v-if="inTable">
598
+ <span class="rm-rte__sep" />
599
+ <button
600
+ v-for="action in tableActions"
601
+ :key="action.label"
602
+ type="button"
603
+ class="rm-rte__btn"
604
+ :title="action.label"
605
+ :aria-label="action.label"
606
+ @click="action.run(editor)"
607
+ >
608
+ <RmIcon :name="action.icon" :size="16" />
609
+ </button>
610
+ </template>
611
+ <form v-if="linkOpen" class="rm-rte__link" @submit.prevent="applyLink">
612
+ <input
613
+ ref="linkInput"
614
+ v-model="linkUrl"
615
+ type="text"
616
+ placeholder="https://… (vide pour retirer le lien)"
617
+ aria-label="Adresse du lien"
618
+ @keydown.escape.prevent="linkOpen = false"
619
+ >
620
+ <button type="submit" class="rm-rte__btn" aria-label="Appliquer le lien" title="Appliquer">
621
+ <RmIcon name="Check" :size="16" />
622
+ </button>
623
+ </form>
624
+ </div>
625
+
626
+ <EditorContent :editor="editor" class="rm-rte__content" @click="onClick" />
627
+
628
+ <Teleport to="body" :disabled="!colorsOpen">
629
+ <div
630
+ v-if="colorsOpen && editor"
631
+ ref="colorsPanel"
632
+ class="rm-rte__layer"
633
+ :style="colorsStyle"
634
+ >
635
+ <div class="rm-rte__layer-title">Couleur du texte</div>
636
+ <div class="rm-rte__swatches">
637
+ <button type="button" class="rm-rte-swatch" title="Par défaut" aria-label="Couleur par défaut" @click="setColor(null)">A</button>
638
+ <button
639
+ v-for="color in RM_RICH_TEXT_COLORS"
640
+ :key="color.key"
641
+ type="button"
642
+ class="rm-rte-swatch"
643
+ :style="{ color: richTextColor(color.key) }"
644
+ :title="color.label"
645
+ :aria-label="`Texte ${color.label.toLowerCase()}`"
646
+ @click="setColor(color.key)"
647
+ >
648
+ A
649
+ </button>
650
+ </div>
651
+ <div class="rm-rte__layer-title">Surlignage</div>
652
+ <div class="rm-rte__swatches">
653
+ <button type="button" class="rm-rte-swatch" title="Aucun" aria-label="Sans surlignage" @click="setHighlight(null)">
654
+ <RmIcon name="Prohibit" :size="14" />
655
+ </button>
656
+ <button
657
+ v-for="color in RM_RICH_TEXT_COLORS"
658
+ :key="color.key"
659
+ type="button"
660
+ class="rm-rte-swatch"
661
+ :style="{ background: richTextHighlight(color.key) }"
662
+ :title="color.label"
663
+ :aria-label="`Surlignage ${color.label.toLowerCase()}`"
664
+ @click="setHighlight(color.key)"
665
+ />
666
+ </div>
667
+ </div>
668
+ </Teleport>
669
+
670
+ <Teleport to="body" :disabled="!menu.open">
671
+ <div
672
+ v-if="menu.open"
673
+ class="rm-rte__layer rm-rte__menu"
674
+ :style="menu.style"
675
+ role="listbox"
676
+ :aria-label="menu.kind === 'slash' ? 'Insérer un bloc' : 'Citer'"
677
+ @mousedown.prevent
678
+ >
679
+ <div v-if="!menu.items.length" class="rm-rte__empty">
680
+ {{ menu.loading ? 'Recherche…' : 'Aucun résultat' }}
681
+ </div>
682
+ <template v-for="(item, i) in menu.items" :key="itemKey(item)">
683
+ <div v-if="showGroup(i)" class="rm-rte__group">
684
+ {{ GROUP_LABEL[asMention(item).kind] }}
685
+ </div>
686
+ <button
687
+ type="button"
688
+ role="option"
689
+ class="rm-rte__item"
690
+ :class="{ 'is-active': i === menu.index }"
691
+ :aria-selected="i === menu.index"
692
+ @mouseenter="menu.index = i"
693
+ @click="pick(item)"
694
+ >
695
+ <template v-if="menu.kind === 'slash'">
696
+ <span class="rm-rte__item-icon"><RmIcon :name="asSlash(item).icon" :size="16" /></span>
697
+ <span class="rm-rte__item-text">
698
+ <span>{{ asSlash(item).label }}</span>
699
+ <small>{{ asSlash(item).hint }}</small>
700
+ </span>
701
+ </template>
702
+ <template v-else>
703
+ <span class="rm-rte__item-icon">
704
+ <template v-if="asMention(item).kind === 'page' && (asMention(item) as RmRichTextPage).icon">
705
+ {{ (asMention(item) as RmRichTextPage).icon }}
706
+ </template>
707
+ <RmIcon v-else :name="MENTION_ICON[asMention(item).kind]" :size="16" />
708
+ </span>
709
+ <span class="rm-rte__item-text">
710
+ <span>
711
+ {{ asMention(item).kind === 'create-page' ? `Créer la page « ${asMention(item).label} »` : asMention(item).label }}
712
+ </span>
713
+ <small v-if="'hint' in item && item.hint">{{ item.hint }}</small>
714
+ </span>
715
+ </template>
716
+ </button>
717
+ </template>
718
+ <div v-if="menu.loading && menu.items.length" class="rm-rte__empty">Recherche des ressources…</div>
719
+ </div>
720
+ </Teleport>
721
+ </div>
722
+ </template>
723
+
724
+ <style scoped>
725
+ .rm-rte{color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);min-height:0}.rm-rte__toolbar{align-items:center;background:var(--bg-surface);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-wrap:wrap;gap:var(--space-2);padding:var(--space-4);position:sticky;top:0;z-index:var(--z-sticky,20)}.rm-rte__sep{background:var(--border-soft);height:20px;margin:0 var(--space-4);width:1px}.rm-rte__style{background:transparent;border:1px solid transparent;border-radius:var(--radius-sm);color:var(--text-primary);cursor:pointer;font:inherit;font-size:var(--font-13);height:28px;padding:0 var(--space-6)}.rm-rte__style:hover{background:var(--bg-hover)}.rm-rte__btn{align-items:center;background:transparent;border:0;border-radius:var(--radius-sm);color:var(--text-secondary);cursor:pointer;display:inline-flex;height:28px;justify-content:center;width:28px}.rm-rte__btn:hover:not(:disabled){background:var(--bg-hover);color:var(--text-primary)}.rm-rte__btn.is-active{background:var(--color-accent-soft);color:var(--color-primary)}.rm-rte__btn:disabled{cursor:default;opacity:.4}.rm-rte__btn:focus-visible,.rm-rte__style:focus-visible{box-shadow:var(--shadow-focus);outline:none}.rm-rte__link{align-items:center;display:flex;flex:1 1 220px;gap:var(--space-4)}.rm-rte__link input{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-sm);color:var(--text-primary);flex:1;font:inherit;font-size:var(--font-13);height:28px;padding:0 var(--space-8)}.rm-rte__content{flex:1;min-height:0}.rm-rte__content :deep(.ProseMirror){font-size:var(--font-14);line-height:1.65;min-height:240px;outline:none;overflow-wrap:anywhere;padding:var(--space-16) var(--space-4)}.rm-rte--readonly .rm-rte__content :deep(.ProseMirror){min-height:0}.rm-rte__content :deep(.ProseMirror>*+*){margin-top:.6em}.rm-rte__content :deep(h1){font-family:var(--font-serif);font-size:var(--font-24);font-weight:var(--weight-bold);line-height:1.25;margin:1.2em 0 0}.rm-rte__content :deep(h2){font-size:var(--font-18);font-weight:var(--weight-semibold);line-height:1.3;margin:1.1em 0 0}.rm-rte__content :deep(h3){font-size:var(--font-16);font-weight:var(--weight-semibold);margin:1em 0 0}.rm-rte__content :deep(.ProseMirror>:first-child){margin-top:0}.rm-rte__content :deep(p){margin:0}.rm-rte__content :deep(ol),.rm-rte__content :deep(ul){margin:0;padding-left:1.4em}.rm-rte__content :deep(a){color:var(--color-primary);text-decoration:underline;text-underline-offset:2px}.rm-rte__content :deep(code){background:var(--bg-surface-3);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:.9em;padding:.1em .35em}.rm-rte__content :deep(pre){background:var(--bg-surface-3);border-radius:var(--radius-md);overflow-x:auto;padding:var(--space-12)}.rm-rte__content :deep(pre code){background:none;padding:0}.rm-rte__content :deep(blockquote){border-left:3px solid var(--border-default);color:var(--text-secondary);margin:0;padding-left:var(--space-12)}.rm-rte__content :deep(hr){border:0;border-top:1px solid var(--border-default);margin:1.2em 0}.rm-rte__content :deep(mark){border-radius:2px;color:inherit;padding:0 .1em}.rm-rte__content :deep(ul[data-type=taskList]){list-style:none;padding-left:.2em}.rm-rte__content :deep(ul[data-type=taskList] li){align-items:flex-start;display:flex;gap:var(--space-8)}.rm-rte__content :deep(ul[data-type=taskList] li>label){margin-top:.2em}.rm-rte__content :deep(ul[data-type=taskList] li>div){flex:1}.rm-rte__content :deep(ul[data-type=taskList] li[data-checked=true]>div){color:var(--text-muted);text-decoration:line-through}.rm-rte__content :deep(input[type=checkbox]){accent-color:var(--color-primary)}.rm-rte__content :deep(.tableWrapper){overflow-x:auto}.rm-rte__content :deep(table){border-collapse:collapse;table-layout:fixed;width:100%}.rm-rte__content :deep(td),.rm-rte__content :deep(th){border:1px solid var(--border-default);min-width:80px;padding:var(--space-6) var(--space-8);position:relative;vertical-align:top}.rm-rte__content :deep(th){background:var(--bg-surface-2);font-weight:var(--weight-semibold);text-align:left}.rm-rte__content :deep(.selectedCell):after{background:var(--color-accent-soft);content:"";inset:0;opacity:.5;pointer-events:none;position:absolute}.rm-rte__content :deep(p.is-editor-empty:first-child):before{color:var(--text-muted);content:attr(data-placeholder);float:left;height:0;pointer-events:none}.rm-rte__content :deep(.rm-rte-mention),.rm-rte__content :deep(.rm-rte-pagelink){border-radius:var(--radius-sm);cursor:pointer;display:inline-block;font-weight:var(--weight-medium);padding:0 .35em;white-space:nowrap}.rm-rte__content :deep(.rm-rte-mention--user){background:var(--color-accent-soft);color:var(--color-primary)}.rm-rte__content :deep(.rm-rte-mention--resource){background:var(--rm-text-bg-teal);color:var(--rm-text-c-teal)}.rm-rte__content :deep(.rm-rte-pagelink){color:var(--text-primary);text-decoration:underline;text-decoration-color:var(--border-default);text-underline-offset:3px}.rm-rte__content :deep(.rm-rte-pagelink:hover){background:var(--bg-hover)}.rm-rte__content :deep(.ProseMirror-selectednode){box-shadow:var(--shadow-focus)}.rm-rte__layer{background:var(--bg-surface);border:1px solid var(--border-soft);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);color:var(--text-primary);font-family:var(--font-sans);font-size:var(--font-13);padding:var(--space-6);z-index:var(--z-popover,1000)}.rm-rte__menu{max-height:320px;overflow-y:auto;width:290px}.rm-rte__group,.rm-rte__layer-title{color:var(--text-muted);font-size:var(--font-11);font-weight:var(--weight-semibold);letter-spacing:.04em;padding:var(--space-6) var(--space-8) var(--space-2);text-transform:uppercase}.rm-rte__item{align-items:center;background:transparent;border:0;border-radius:var(--radius-sm);color:inherit;cursor:pointer;display:flex;font:inherit;gap:var(--space-8);padding:var(--space-6) var(--space-8);text-align:left;width:100%}.rm-rte__item.is-active{background:var(--bg-hover)}.rm-rte__item-icon{align-items:center;background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-sm);color:var(--text-secondary);display:inline-flex;flex:none;height:28px;justify-content:center;width:28px}.rm-rte__item-text{display:flex;flex-direction:column;min-width:0}.rm-rte__item-text>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-rte__item-text small{color:var(--text-muted);font-size:var(--font-11)}.rm-rte__empty{color:var(--text-muted);padding:var(--space-8)}.rm-rte__swatches{display:grid;gap:var(--space-4);grid-template-columns:repeat(5,28px);padding:var(--space-4) var(--space-6) var(--space-6)}.rm-rte-swatch{align-items:center;background:var(--bg-surface);border:1px solid var(--border-soft);border-radius:var(--radius-sm);color:var(--text-primary);cursor:pointer;display:inline-flex;font-weight:var(--weight-semibold);height:28px;justify-content:center;width:28px}.rm-rte-swatch:hover{border-color:var(--border-default);box-shadow:var(--shadow-xs)}
726
+ </style>
@@ -37,6 +37,11 @@ const props = withDefaults(
37
37
  saveLabel?: string
38
38
  /** Rien a sauvegarder (aucun filtre pose, par exemple). */
39
39
  saveDisabled?: boolean
40
+ /**
41
+ * Libelle du bouton « Nouvelle vue », a cote du Sauvegarder : la page
42
+ * repart de zero (plus de vue, filtres au defaut). Vide : pas de bouton.
43
+ */
44
+ resetLabel?: string
40
45
  /** Onglets de portee. Masques s'il y en a moins de deux. */
41
46
  tabs?: RmSavedViewsTab[]
42
47
  /** Onglet courant (v-model). */
@@ -68,6 +73,7 @@ const props = withDefaults(
68
73
  description: '',
69
74
  saveLabel: '',
70
75
  saveDisabled: false,
76
+ resetLabel: '',
71
77
  tabs: () => [],
72
78
  tab: '',
73
79
  menu: () => [],
@@ -87,6 +93,8 @@ const props = withDefaults(
87
93
  const emit = defineEmits<{
88
94
  'update:tab': [key: string]
89
95
  'save-current': []
96
+ /** « Nouvelle vue » : remettre la page a l'etat vierge. */
97
+ reset: []
90
98
  activate: [id: string]
91
99
  star: [id: string]
92
100
  rename: [id: string, title: string]
@@ -169,19 +177,31 @@ const visibleTabs = computed(() => (props.tabs.length > 1 ? props.tabs : []))
169
177
 
170
178
  <template>
171
179
  <div class="rm-views">
172
- <div v-if="title || saveLabel" class="rm-views__head">
180
+ <div v-if="title || saveLabel || resetLabel" class="rm-views__head">
173
181
  <h2 v-if="title" class="rm-views__title">{{ title }}</h2>
174
- <RmButton
175
- v-if="saveLabel"
176
- variant="primary"
177
- size="sm"
178
- prefix-icon="BookmarkSimple"
179
- :disabled="saveDisabled"
180
- :block="!title"
181
- @click="emit('save-current')"
182
- >
183
- {{ saveLabel }}
184
- </RmButton>
182
+ <div v-if="saveLabel || resetLabel" class="rm-views__actions" :class="{ 'rm-views__actions--block': !title }">
183
+ <RmButton
184
+ v-if="saveLabel"
185
+ variant="primary"
186
+ size="sm"
187
+ prefix-icon="BookmarkSimple"
188
+ :disabled="saveDisabled"
189
+ :block="!title"
190
+ @click="emit('save-current')"
191
+ >
192
+ {{ saveLabel }}
193
+ </RmButton>
194
+ <RmButton
195
+ v-if="resetLabel"
196
+ variant="secondary"
197
+ size="sm"
198
+ prefix-icon="Plus"
199
+ :block="!title"
200
+ @click="emit('reset')"
201
+ >
202
+ {{ resetLabel }}
203
+ </RmButton>
204
+ </div>
185
205
  </div>
186
206
 
187
207
  <p v-if="description" class="rm-views__desc">{{ description }}</p>
@@ -291,5 +311,5 @@ const visibleTabs = computed(() => (props.tabs.length > 1 ? props.tabs : []))
291
311
  </template>
292
312
 
293
313
  <style scoped>
294
- .rm-views{flex-direction:column;font-family:var(--font-sans)}.rm-views,.rm-views__head{display:flex;gap:var(--space-12)}.rm-views__head{align-items:center;justify-content:space-between}.rm-views__title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-22);font-weight:var(--weight-bold);margin:0}.rm-views__desc{color:var(--text-secondary);font-size:var(--font-13);line-height:1.5;margin:0}.rm-views__tabs{align-items:stretch;border-bottom:1px solid var(--border-default);display:flex}.rm-views__tab{align-items:center;background:none;border:none;border-bottom:3px solid transparent;color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-semibold);gap:var(--space-6);padding:var(--space-12) var(--space-16);transition:color var(--duration-fast) var(--ease-out)}.rm-views__tab:hover{color:var(--text-primary)}.rm-views__tab--active{background:linear-gradient(180deg,transparent 30%,rgba(var(--brand-rgb),.06) 100%);border-bottom-color:var(--color-primary);border-radius:6px 6px 0 0;color:var(--color-primary);font-weight:var(--weight-bold)}.rm-views__tab-count{background:var(--bg-surface-3);border-radius:var(--radius-pill);color:var(--text-muted);font-size:var(--font-11);font-variant-numeric:tabular-nums;font-weight:var(--weight-bold);min-width:18px;padding:0 var(--space-6);text-align:center}.rm-views__tab--active .rm-views__tab-count{background:var(--bg-hover);color:var(--color-secondary)}.rm-views__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-muted);display:flex;gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-views__search:focus-within{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.1)}.rm-views__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:inherit;font-size:var(--font-13);min-width:0;outline:none}.rm-views__search input::-moz-placeholder{color:var(--text-muted)}.rm-views__search input::placeholder{color:var(--text-muted)}.rm-views__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-views__chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-12);gap:var(--space-4);padding:5px var(--space-10)}.rm-views__chip:hover{background:var(--bg-surface-3)}.rm-views__chip--on{background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-views__chip-count{font-variant-numeric:tabular-nums;opacity:.8}.rm-views__list{display:flex;flex-direction:column;gap:var(--space-12)}
314
+ .rm-views{flex-direction:column;font-family:var(--font-sans)}.rm-views,.rm-views__head{display:flex;gap:var(--space-12)}.rm-views__head{align-items:center;justify-content:space-between}.rm-views__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-views__actions--block,.rm-views__actions--block>*{flex:1}.rm-views__title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-22);font-weight:var(--weight-bold);margin:0}.rm-views__desc{color:var(--text-secondary);font-size:var(--font-13);line-height:1.5;margin:0}.rm-views__tabs{align-items:stretch;border-bottom:1px solid var(--border-default);display:flex}.rm-views__tab{align-items:center;background:none;border:none;border-bottom:3px solid transparent;color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-semibold);gap:var(--space-6);padding:var(--space-12) var(--space-16);transition:color var(--duration-fast) var(--ease-out)}.rm-views__tab:hover{color:var(--text-primary)}.rm-views__tab--active{background:linear-gradient(180deg,transparent 30%,rgba(var(--brand-rgb),.06) 100%);border-bottom-color:var(--color-primary);border-radius:6px 6px 0 0;color:var(--color-primary);font-weight:var(--weight-bold)}.rm-views__tab-count{background:var(--bg-surface-3);border-radius:var(--radius-pill);color:var(--text-muted);font-size:var(--font-11);font-variant-numeric:tabular-nums;font-weight:var(--weight-bold);min-width:18px;padding:0 var(--space-6);text-align:center}.rm-views__tab--active .rm-views__tab-count{background:var(--bg-hover);color:var(--color-secondary)}.rm-views__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-muted);display:flex;gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-views__search:focus-within{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.1)}.rm-views__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:inherit;font-size:var(--font-13);min-width:0;outline:none}.rm-views__search input::-moz-placeholder{color:var(--text-muted)}.rm-views__search input::placeholder{color:var(--text-muted)}.rm-views__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-views__chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-12);gap:var(--space-4);padding:5px var(--space-10)}.rm-views__chip:hover{background:var(--bg-surface-3)}.rm-views__chip--on{background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-views__chip-count{font-variant-numeric:tabular-nums;opacity:.8}.rm-views__list{display:flex;flex-direction:column;gap:var(--space-12)}
295
315
  </style>
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Ce que RmRichTextEditor sait sans dependre de Vue : la palette, les entrees du
3
+ * menu `/`, le filtre des suggestions `@` et le deplacement d'un bloc.
4
+ *
5
+ * Les couleurs sont enregistrees dans le document sous forme de variable CSS
6
+ * (`var(--rm-text-c-blue)`), jamais en hexadecimal : la meme note se relit de
7
+ * jour comme de nuit, la variable change de valeur avec le theme.
8
+ */
9
+ import type { EditorState, Transaction } from '@tiptap/pm/state';
10
+ export declare const RM_RICH_TEXT_COLORS: readonly [{
11
+ readonly key: "gray";
12
+ readonly label: "Gris";
13
+ }, {
14
+ readonly key: "red";
15
+ readonly label: "Rouge";
16
+ }, {
17
+ readonly key: "orange";
18
+ readonly label: "Orange";
19
+ }, {
20
+ readonly key: "amber";
21
+ readonly label: "Jaune";
22
+ }, {
23
+ readonly key: "green";
24
+ readonly label: "Vert";
25
+ }, {
26
+ readonly key: "teal";
27
+ readonly label: "Turquoise";
28
+ }, {
29
+ readonly key: "blue";
30
+ readonly label: "Bleu";
31
+ }, {
32
+ readonly key: "violet";
33
+ readonly label: "Violet";
34
+ }, {
35
+ readonly key: "pink";
36
+ readonly label: "Rose";
37
+ }];
38
+ export type RmRichTextColor = (typeof RM_RICH_TEXT_COLORS)[number]['key'];
39
+ /** La valeur ecrite dans le document pour une couleur de texte. */
40
+ export declare const richTextColor: (key: RmRichTextColor) => string;
41
+ /** La valeur ecrite dans le document pour un surlignage. */
42
+ export declare const richTextHighlight: (key: RmRichTextColor) => string;
43
+ /** Un membre que `@` peut citer. */
44
+ export interface RmRichTextMember {
45
+ id: string;
46
+ label: string;
47
+ hint?: string;
48
+ }
49
+ /** Une ressource que `@` peut citer (une fiche, une marque). */
50
+ export interface RmRichTextResource {
51
+ id: string;
52
+ resourceType: string;
53
+ label: string;
54
+ hint?: string;
55
+ }
56
+ /** Une page vers laquelle `@` peut poser un lien. */
57
+ export interface RmRichTextPage {
58
+ id: string;
59
+ label: string;
60
+ icon?: string | null;
61
+ }
62
+ /** Une entree du menu `@`, toutes familles confondues. */
63
+ export type RmRichTextMentionItem = {
64
+ kind: 'user';
65
+ id: string;
66
+ label: string;
67
+ hint?: string;
68
+ } | {
69
+ kind: 'resource';
70
+ id: string;
71
+ resourceType: string;
72
+ label: string;
73
+ hint?: string;
74
+ } | {
75
+ kind: 'page';
76
+ id: string;
77
+ label: string;
78
+ icon?: string | null;
79
+ } | {
80
+ kind: 'create-page';
81
+ id: string;
82
+ label: string;
83
+ };
84
+ /** Vrai quand `query` est vide ou figure dans `label` (sans accents ni casse). */
85
+ export declare function matchesQuery(label: string, query: string): boolean;
86
+ /**
87
+ * Le menu `@` : les membres, puis les pages, filtres par la saisie, au plus
88
+ * `limit` de chaque ; les ressources arrivent deja filtrees par l'appelant.
89
+ * La derniere entree propose de creer la page tapee quand aucune ne porte
90
+ * exactement ce nom.
91
+ */
92
+ export declare function mentionItems(input: {
93
+ query: string;
94
+ members: RmRichTextMember[];
95
+ pages: RmRichTextPage[];
96
+ resources: RmRichTextResource[];
97
+ canCreatePage: boolean;
98
+ limit?: number;
99
+ }): RmRichTextMentionItem[];
100
+ /** Une entree du menu `/`. `run` recoit la chaine de commandes Tiptap. */
101
+ export interface RmRichTextSlashItem {
102
+ key: string;
103
+ label: string;
104
+ hint: string;
105
+ icon: string;
106
+ keywords: string[];
107
+ }
108
+ export declare const RM_RICH_TEXT_SLASH_ITEMS: RmRichTextSlashItem[];
109
+ /** Les entrees du menu `/` dont le libelle ou un mot-cle contient la saisie. */
110
+ export declare function slashItems(query: string): RmRichTextSlashItem[];
111
+ /**
112
+ * Commande ProseMirror : echange le bloc de premier niveau qui porte le
113
+ * curseur avec son voisin (`-1` au-dessus, `1` au-dessous). Le curseur suit le
114
+ * bloc. Remplace la poignee de glisser-deposer, dont l'extension Tiptap exige
115
+ * la pile de collaboration (Yjs).
116
+ */
117
+ export declare function moveBlock(direction: -1 | 1): ({ state, tr, dispatch }: {
118
+ state: EditorState;
119
+ tr: Transaction;
120
+ dispatch?: (tr: Transaction) => void;
121
+ }) => boolean;
@@ -0,0 +1,68 @@
1
+ import { TextSelection } from "@tiptap/pm/state";
2
+ export const RM_RICH_TEXT_COLORS = [
3
+ { key: "gray", label: "Gris" },
4
+ { key: "red", label: "Rouge" },
5
+ { key: "orange", label: "Orange" },
6
+ { key: "amber", label: "Jaune" },
7
+ { key: "green", label: "Vert" },
8
+ { key: "teal", label: "Turquoise" },
9
+ { key: "blue", label: "Bleu" },
10
+ { key: "violet", label: "Violet" },
11
+ { key: "pink", label: "Rose" }
12
+ ];
13
+ export const richTextColor = (key) => `var(--rm-text-c-${key})`;
14
+ export const richTextHighlight = (key) => `var(--rm-text-bg-${key})`;
15
+ const fold = (text) => text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
16
+ export function matchesQuery(label, query) {
17
+ return !query || fold(label).includes(fold(query.trim()));
18
+ }
19
+ export function mentionItems(input) {
20
+ const { query, limit = 5 } = input;
21
+ const users = input.members.filter((m) => matchesQuery(m.label, query)).slice(0, limit).map((m) => ({ kind: "user", ...m }));
22
+ const pages = input.pages.filter((p) => matchesQuery(p.label, query)).slice(0, limit).map((p) => ({ kind: "page", ...p }));
23
+ const resources = input.resources.slice(0, limit).map((r) => ({ kind: "resource", ...r }));
24
+ const title = query.trim();
25
+ const exists = input.pages.some((p) => fold(p.label) === fold(title));
26
+ const create = input.canCreatePage && title && !exists ? [{ kind: "create-page", id: "__create__", label: title }] : [];
27
+ return [...users, ...resources, ...pages, ...create];
28
+ }
29
+ export const RM_RICH_TEXT_SLASH_ITEMS = [
30
+ { key: "paragraph", label: "Texte", hint: "Un paragraphe simple", icon: "TextT", keywords: ["texte", "paragraphe"] },
31
+ { key: "h1", label: "Titre 1", hint: "Grand titre de section", icon: "TextHOne", keywords: ["titre", "h1"] },
32
+ { key: "h2", label: "Titre 2", hint: "Titre moyen", icon: "TextHTwo", keywords: ["titre", "h2"] },
33
+ { key: "h3", label: "Titre 3", hint: "Petit titre", icon: "TextHThree", keywords: ["titre", "h3"] },
34
+ { key: "bullet", label: "Liste \xE0 puces", hint: "Une liste simple", icon: "ListBullets", keywords: ["liste", "puces"] },
35
+ { key: "ordered", label: "Liste num\xE9rot\xE9e", hint: "Une liste ordonn\xE9e", icon: "ListNumbers", keywords: ["liste", "numero"] },
36
+ { key: "task", label: "Liste de t\xE2ches", hint: "Des cases \xE0 cocher", icon: "ListChecks", keywords: ["tache", "todo", "case"] },
37
+ { key: "quote", label: "Citation", hint: "Un passage cit\xE9", icon: "Quotes", keywords: ["citation", "quote"] },
38
+ { key: "code", label: "Code", hint: "Un bloc de code", icon: "Code", keywords: ["code"] },
39
+ { key: "table", label: "Tableau", hint: "Trois colonnes, trois lignes", icon: "Table", keywords: ["tableau", "table"] },
40
+ { key: "divider", label: "S\xE9parateur", hint: "Une ligne horizontale", icon: "Minus", keywords: ["separateur", "ligne"] },
41
+ { key: "mention", label: "Mention ou lien", hint: "Une personne, une ressource, une page", icon: "At", keywords: ["mention", "lien", "page", "personne"] }
42
+ ];
43
+ export function slashItems(query) {
44
+ return RM_RICH_TEXT_SLASH_ITEMS.filter(
45
+ (item) => matchesQuery(item.label, query) || item.keywords.some((k) => matchesQuery(k, query))
46
+ );
47
+ }
48
+ export function moveBlock(direction) {
49
+ return ({ state, tr, dispatch }) => {
50
+ const { doc, selection } = state;
51
+ const index = selection.$from.index(0);
52
+ const target = index + direction;
53
+ if (target < 0 || target >= doc.childCount) return false;
54
+ let start = 0;
55
+ for (let i = 0; i < index; i++) start += doc.child(i).nodeSize;
56
+ const node = doc.child(index);
57
+ const offset = selection.from - start;
58
+ const neighbor = doc.child(target);
59
+ const at = direction < 0 ? start - neighbor.nodeSize : start + neighbor.nodeSize;
60
+ if (dispatch) {
61
+ tr.delete(start, start + node.nodeSize);
62
+ tr.insert(at, node);
63
+ tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(at + offset, tr.doc.content.size))));
64
+ dispatch(tr.scrollIntoView());
65
+ }
66
+ return true;
67
+ };
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.218",
3
+ "version": "0.1.220",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,8 +31,19 @@
31
31
  "test": "vitest run"
32
32
  },
33
33
  "dependencies": {
34
+ "@floating-ui/dom": "^1.8.0",
34
35
  "@nuxt/kit": "^3.16.0",
35
36
  "@phosphor-icons/vue": "^2.2.1",
37
+ "@tiptap/extension-highlight": "^3.31.4",
38
+ "@tiptap/extension-list": "^3.31.4",
39
+ "@tiptap/extension-mention": "^3.31.4",
40
+ "@tiptap/extension-table": "^3.31.4",
41
+ "@tiptap/extension-text-style": "^3.31.4",
42
+ "@tiptap/extensions": "^3.31.4",
43
+ "@tiptap/pm": "^3.31.4",
44
+ "@tiptap/starter-kit": "^3.31.4",
45
+ "@tiptap/suggestion": "^3.31.4",
46
+ "@tiptap/vue-3": "^3.31.4",
36
47
  "echarts": "^5.6.0",
37
48
  "html2canvas-pro": "^1.6.7",
38
49
  "jspdf": "^4.2.0",