@tekkare/romulus 0.1.219 → 0.1.221

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.218",
7
+ "version": "0.1.220",
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,810 @@
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
+ // L'etat de l'editeur n'est pas reactif pour Vue : un `computed` qui lit
410
+ // `isActive` garderait sa premiere valeur. Chaque transaction avance ce
411
+ // compteur, et les etats derives le lisent pour se recalculer.
412
+ const tick = ref(0)
413
+ watch(editor, (e) => e?.on('transaction', () => tick.value++), { immediate: true })
414
+
415
+ const inTable = computed(() => tick.value >= 0 && !!editor.value?.isActive('table'))
416
+ const blockStyle = computed(() => {
417
+ const e = editor.value
418
+ if (!e || tick.value < 0) return 'paragraph'
419
+ for (const level of [1, 2, 3] as const) if (e.isActive('heading', { level })) return `h${level}`
420
+ return 'paragraph'
421
+ })
422
+
423
+ function setBlockStyle(value: string) {
424
+ const chain = editor.value?.chain().focus()
425
+ if (!chain) return
426
+ if (value === 'paragraph') chain.setParagraph().run()
427
+ else chain.setHeading({ level: Number(value.slice(1)) as 1 | 2 | 3 }).run()
428
+ }
429
+
430
+ const colorsOpen = ref(false)
431
+ const colorsButton = ref<HTMLElement>()
432
+ const colorsPanel = ref<HTMLElement>()
433
+ const colorsStyle = ref<Record<string, string>>({})
434
+
435
+ function toggleColors() {
436
+ colorsOpen.value = !colorsOpen.value
437
+ const rect = colorsButton.value?.getBoundingClientRect()
438
+ if (rect) colorsStyle.value = { position: 'fixed', left: `${rect.left}px`, top: `${rect.bottom + 6}px` }
439
+ }
440
+
441
+ function setColor(key: RmRichTextColor | null) {
442
+ const chain = editor.value?.chain().focus()
443
+ if (!chain) return
444
+ if (key) chain.setColor(richTextColor(key)).run()
445
+ else chain.unsetColor().run()
446
+ colorsOpen.value = false
447
+ }
448
+
449
+ function setHighlight(key: RmRichTextColor | null) {
450
+ const chain = editor.value?.chain().focus()
451
+ if (!chain) return
452
+ if (key) chain.setHighlight({ color: richTextHighlight(key) }).run()
453
+ else chain.unsetHighlight().run()
454
+ colorsOpen.value = false
455
+ }
456
+
457
+ const linkOpen = ref(false)
458
+ const linkUrl = ref('')
459
+ const linkInput = ref<HTMLInputElement>()
460
+
461
+ function openLink() {
462
+ linkUrl.value = (editor.value?.getAttributes('link').href as string | undefined) ?? ''
463
+ linkOpen.value = true
464
+ setTimeout(() => linkInput.value?.focus())
465
+ }
466
+
467
+ function applyLink() {
468
+ const chain = editor.value?.chain().focus().extendMarkRange('link')
469
+ if (!chain) return
470
+ const url = linkUrl.value.trim()
471
+ if (!url) chain.unsetLink().run()
472
+ else chain.setLink({ href: /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `https://${url}` }).run()
473
+ linkOpen.value = false
474
+ }
475
+
476
+ function onDocumentPointer(event: PointerEvent) {
477
+ const target = event.target as globalThis.Node
478
+ if (colorsOpen.value && !colorsPanel.value?.contains(target) && !colorsButton.value?.contains(target)) {
479
+ colorsOpen.value = false
480
+ }
481
+ }
482
+ if (typeof document !== 'undefined') document.addEventListener('pointerdown', onDocumentPointer)
483
+ onBeforeUnmount(() => document.removeEventListener('pointerdown', onDocumentPointer))
484
+
485
+ const marks = [
486
+ { name: 'bold', icon: 'TextB', label: 'Gras (Ctrl+B)', run: (e: Editor) => e.chain().focus().toggleBold().run() },
487
+ { name: 'italic', icon: 'TextItalic', label: 'Italique (Ctrl+I)', run: (e: Editor) => e.chain().focus().toggleItalic().run() },
488
+ { name: 'underline', icon: 'TextUnderline', label: 'Souligné (Ctrl+U)', run: (e: Editor) => e.chain().focus().toggleUnderline().run() },
489
+ { name: 'strike', icon: 'TextStrikethrough', label: 'Barré', run: (e: Editor) => e.chain().focus().toggleStrike().run() },
490
+ { name: 'code', icon: 'Code', label: 'Code', run: (e: Editor) => e.chain().focus().toggleCode().run() },
491
+ ]
492
+
493
+ const blocks = [
494
+ { name: 'bulletList', icon: 'ListBullets', label: 'Liste à puces', run: (e: Editor) => e.chain().focus().toggleBulletList().run() },
495
+ { name: 'orderedList', icon: 'ListNumbers', label: 'Liste numérotée', run: (e: Editor) => e.chain().focus().toggleOrderedList().run() },
496
+ { name: 'taskList', icon: 'ListChecks', label: 'Liste de tâches', run: (e: Editor) => e.chain().focus().toggleTaskList().run() },
497
+ { name: 'blockquote', icon: 'Quotes', label: 'Citation', run: (e: Editor) => e.chain().focus().toggleBlockquote().run() },
498
+ ]
499
+
500
+ /**
501
+ * Les gestes sur un tableau, par groupes. Ils vivent dans un menu pose
502
+ * au-dessus du tableau, pas dans la barre d'outils : en haut de l'editeur,
503
+ * elle sort de l'ecran des que la note est longue, et l'on cherchait les
504
+ * boutons la ou l'on ecrit.
505
+ */
506
+ const tableGroups: Array<Array<{ key: string; icon: string; label: string; run: (e: Editor) => boolean; can?: (e: Editor) => boolean; active?: (e: Editor) => boolean }>> = [
507
+ [
508
+ { key: 'row-before', icon: 'RowsPlusTop', label: 'Insérer une ligne au-dessus', run: (e) => e.chain().focus().addRowBefore().run() },
509
+ { key: 'row-after', icon: 'RowsPlusBottom', label: 'Insérer une ligne en dessous', run: (e) => e.chain().focus().addRowAfter().run() },
510
+ { key: 'col-before', icon: 'ColumnsPlusLeft', label: 'Insérer une colonne à gauche', run: (e) => e.chain().focus().addColumnBefore().run() },
511
+ { key: 'col-after', icon: 'ColumnsPlusRight', label: 'Insérer une colonne à droite', run: (e) => e.chain().focus().addColumnAfter().run() },
512
+ ],
513
+ [
514
+ { key: 'header-row', icon: 'TextHOne', label: "Ligne d'en-tête", run: (e) => e.chain().focus().toggleHeaderRow().run(), active: (e) => isHeaderRow(e) },
515
+ { key: 'merge', icon: 'ArrowsInLineHorizontal', label: 'Fusionner les cellules sélectionnées', run: (e) => e.chain().focus().mergeCells().run(), can: (e) => e.can().mergeCells() },
516
+ { key: 'split', icon: 'ArrowsOutLineHorizontal', label: 'Scinder la cellule', run: (e) => e.chain().focus().splitCell().run(), can: (e) => e.can().splitCell() },
517
+ ],
518
+ [
519
+ { key: 'del-row', icon: 'Rows', label: 'Supprimer la ligne', run: (e) => e.chain().focus().deleteRow().run() },
520
+ { key: 'del-col', icon: 'Columns', label: 'Supprimer la colonne', run: (e) => e.chain().focus().deleteColumn().run() },
521
+ { key: 'del-table', icon: 'Trash', label: 'Supprimer le tableau', run: (e) => e.chain().focus().deleteTable().run() },
522
+ ],
523
+ ]
524
+
525
+ /** La premiere ligne du tableau courant est-elle une ligne d'en-tete ? */
526
+ function isHeaderRow(e: Editor) {
527
+ const { $from } = e.state.selection
528
+ for (let depth = $from.depth; depth > 0; depth--) {
529
+ const node = $from.node(depth)
530
+ if (node.type.name === 'table') return node.firstChild?.firstChild?.type.name === 'tableHeader'
531
+ }
532
+ return false
533
+ }
534
+
535
+ // Le menu du tableau suit le tableau : place a chaque transaction, et tant
536
+ // qu'il est ouvert, a chaque defilement (la page defile dans un conteneur).
537
+ const tableMenuStyle = ref<Record<string, string>>({})
538
+ function tableElement(): HTMLElement | null {
539
+ const e = editor.value
540
+ if (!e || !e.isActive('table')) return null
541
+ const { node } = e.view.domAtPos(e.state.selection.from)
542
+ const el = node.nodeType === 1 ? (node as HTMLElement) : node.parentElement
543
+ return el?.closest('table') ?? null
544
+ }
545
+ function placeTableMenu() {
546
+ const rect = tableElement()?.getBoundingClientRect()
547
+ if (!rect) return
548
+ // Au-dessus du tableau ; dessous quand son haut est sorti de l'ecran.
549
+ const above = rect.top - 46
550
+ const top = above >= 8 ? above : Math.min(rect.bottom + 6, window.innerHeight - 52)
551
+ tableMenuStyle.value = { position: 'fixed', left: `${Math.max(8, rect.left)}px`, top: `${top}px` }
552
+ }
553
+ const showTableMenu = computed(() => props.editable && inTable.value)
554
+ watch(editor, (e) => e?.on('transaction', placeTableMenu), { immediate: true })
555
+ watch(showTableMenu, (open) => {
556
+ if (typeof window === 'undefined') return
557
+ if (open) {
558
+ placeTableMenu()
559
+ window.addEventListener('scroll', placeTableMenu, true)
560
+ window.addEventListener('resize', placeTableMenu)
561
+ } else {
562
+ window.removeEventListener('scroll', placeTableMenu, true)
563
+ window.removeEventListener('resize', placeTableMenu)
564
+ }
565
+ })
566
+ onBeforeUnmount(() => {
567
+ window.removeEventListener('scroll', placeTableMenu, true)
568
+ window.removeEventListener('resize', placeTableMenu)
569
+ })
570
+
571
+ defineExpose({ editor })
572
+
573
+ const GROUP_LABEL: Record<RmRichTextMentionItem['kind'], string> = {
574
+ user: 'Personnes',
575
+ resource: 'Ressources',
576
+ page: 'Pages',
577
+ 'create-page': 'Nouvelle page',
578
+ }
579
+ const MENTION_ICON: Record<RmRichTextMentionItem['kind'], string> = {
580
+ user: 'User',
581
+ resource: 'Pill',
582
+ page: 'FileText',
583
+ 'create-page': 'Plus',
584
+ }
585
+ const asSlash = (item: MenuItem) => item as RmRichTextSlashItem
586
+ const asMention = (item: MenuItem) => item as RmRichTextMentionItem
587
+ const itemKey = (item: MenuItem) => ('kind' in item ? `${item.kind}-${item.id}` : `slash-${item.key}`)
588
+ const showGroup = (i: number) =>
589
+ menu.kind === 'mention' && (i === 0 || asMention(menu.items[i]!).kind !== asMention(menu.items[i - 1]!).kind)
590
+ </script>
591
+
592
+ <template>
593
+ <div class="rm-rte" :class="{ 'rm-rte--readonly': !editable }">
594
+ <div v-if="toolbar && editable && editor" class="rm-rte__toolbar" role="toolbar" aria-label="Mise en forme">
595
+ <select
596
+ class="rm-rte__style"
597
+ :value="blockStyle"
598
+ aria-label="Style du paragraphe"
599
+ @change="setBlockStyle(($event.target as HTMLSelectElement).value)"
600
+ >
601
+ <option value="paragraph">Texte</option>
602
+ <option value="h1">Titre 1</option>
603
+ <option value="h2">Titre 2</option>
604
+ <option value="h3">Titre 3</option>
605
+ </select>
606
+ <span class="rm-rte__sep" />
607
+ <button
608
+ v-for="mark in marks"
609
+ :key="mark.name"
610
+ type="button"
611
+ class="rm-rte__btn"
612
+ :class="{ 'is-active': editor.isActive(mark.name) }"
613
+ :aria-pressed="editor.isActive(mark.name)"
614
+ :title="mark.label"
615
+ :aria-label="mark.label"
616
+ @click="mark.run(editor)"
617
+ >
618
+ <RmIcon :name="mark.icon" :size="16" />
619
+ </button>
620
+ <button
621
+ ref="colorsButton"
622
+ type="button"
623
+ class="rm-rte__btn"
624
+ :class="{ 'is-active': colorsOpen }"
625
+ title="Couleur du texte et surlignage"
626
+ aria-label="Couleur du texte et surlignage"
627
+ :aria-expanded="colorsOpen"
628
+ @click="toggleColors"
629
+ >
630
+ <RmIcon name="Palette" :size="16" />
631
+ </button>
632
+ <button
633
+ type="button"
634
+ class="rm-rte__btn"
635
+ :class="{ 'is-active': editor.isActive('link') }"
636
+ title="Lien"
637
+ aria-label="Lien"
638
+ @click="openLink"
639
+ >
640
+ <RmIcon name="Link" :size="16" />
641
+ </button>
642
+ <span class="rm-rte__sep" />
643
+ <button
644
+ v-for="block in blocks"
645
+ :key="block.name"
646
+ type="button"
647
+ class="rm-rte__btn"
648
+ :class="{ 'is-active': editor.isActive(block.name) }"
649
+ :aria-pressed="editor.isActive(block.name)"
650
+ :title="block.label"
651
+ :aria-label="block.label"
652
+ @click="block.run(editor)"
653
+ >
654
+ <RmIcon :name="block.icon" :size="16" />
655
+ </button>
656
+ <button
657
+ type="button"
658
+ class="rm-rte__btn"
659
+ title="Insérer un tableau"
660
+ aria-label="Insérer un tableau"
661
+ :disabled="inTable"
662
+ @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"
663
+ >
664
+ <RmIcon name="Table" :size="16" />
665
+ </button>
666
+ <form v-if="linkOpen" class="rm-rte__link" @submit.prevent="applyLink">
667
+ <input
668
+ ref="linkInput"
669
+ v-model="linkUrl"
670
+ type="text"
671
+ placeholder="https://… (vide pour retirer le lien)"
672
+ aria-label="Adresse du lien"
673
+ @keydown.escape.prevent="linkOpen = false"
674
+ >
675
+ <button type="submit" class="rm-rte__btn" aria-label="Appliquer le lien" title="Appliquer">
676
+ <RmIcon name="Check" :size="16" />
677
+ </button>
678
+ </form>
679
+ </div>
680
+
681
+ <EditorContent :editor="editor" class="rm-rte__content" @click="onClick" />
682
+
683
+ <Teleport to="body" :disabled="!showTableMenu">
684
+ <div
685
+ v-if="showTableMenu && editor"
686
+ class="rm-rte__layer rm-rte__tablemenu"
687
+ :style="tableMenuStyle"
688
+ role="toolbar"
689
+ aria-label="Tableau"
690
+ @mousedown.prevent
691
+ >
692
+ <template v-for="(group, g) in tableGroups" :key="g">
693
+ <span v-if="g" class="rm-rte__sep" />
694
+ <button
695
+ v-for="action in group"
696
+ :key="action.key"
697
+ type="button"
698
+ class="rm-rte__btn"
699
+ :class="{ 'is-active': action.active?.(editor) }"
700
+ :aria-pressed="action.active ? action.active(editor) : undefined"
701
+ :disabled="action.can ? !action.can(editor) : false"
702
+ :title="action.label"
703
+ :aria-label="action.label"
704
+ @click="action.run(editor)"
705
+ >
706
+ <RmIcon :name="action.icon" :size="16" />
707
+ </button>
708
+ </template>
709
+ </div>
710
+ </Teleport>
711
+
712
+ <Teleport to="body" :disabled="!colorsOpen">
713
+ <div
714
+ v-if="colorsOpen && editor"
715
+ ref="colorsPanel"
716
+ class="rm-rte__layer"
717
+ :style="colorsStyle"
718
+ >
719
+ <div class="rm-rte__layer-title">Couleur du texte</div>
720
+ <div class="rm-rte__swatches">
721
+ <button type="button" class="rm-rte-swatch" title="Par défaut" aria-label="Couleur par défaut" @click="setColor(null)">A</button>
722
+ <button
723
+ v-for="color in RM_RICH_TEXT_COLORS"
724
+ :key="color.key"
725
+ type="button"
726
+ class="rm-rte-swatch"
727
+ :style="{ color: richTextColor(color.key) }"
728
+ :title="color.label"
729
+ :aria-label="`Texte ${color.label.toLowerCase()}`"
730
+ @click="setColor(color.key)"
731
+ >
732
+ A
733
+ </button>
734
+ </div>
735
+ <div class="rm-rte__layer-title">Surlignage</div>
736
+ <div class="rm-rte__swatches">
737
+ <button type="button" class="rm-rte-swatch" title="Aucun" aria-label="Sans surlignage" @click="setHighlight(null)">
738
+ <RmIcon name="Prohibit" :size="14" />
739
+ </button>
740
+ <button
741
+ v-for="color in RM_RICH_TEXT_COLORS"
742
+ :key="color.key"
743
+ type="button"
744
+ class="rm-rte-swatch"
745
+ :style="{ background: richTextHighlight(color.key) }"
746
+ :title="color.label"
747
+ :aria-label="`Surlignage ${color.label.toLowerCase()}`"
748
+ @click="setHighlight(color.key)"
749
+ />
750
+ </div>
751
+ </div>
752
+ </Teleport>
753
+
754
+ <Teleport to="body" :disabled="!menu.open">
755
+ <div
756
+ v-if="menu.open"
757
+ class="rm-rte__layer rm-rte__menu"
758
+ :style="menu.style"
759
+ role="listbox"
760
+ :aria-label="menu.kind === 'slash' ? 'Insérer un bloc' : 'Citer'"
761
+ @mousedown.prevent
762
+ >
763
+ <div v-if="!menu.items.length" class="rm-rte__empty">
764
+ {{ menu.loading ? 'Recherche…' : 'Aucun résultat' }}
765
+ </div>
766
+ <template v-for="(item, i) in menu.items" :key="itemKey(item)">
767
+ <div v-if="showGroup(i)" class="rm-rte__group">
768
+ {{ GROUP_LABEL[asMention(item).kind] }}
769
+ </div>
770
+ <button
771
+ type="button"
772
+ role="option"
773
+ class="rm-rte__item"
774
+ :class="{ 'is-active': i === menu.index }"
775
+ :aria-selected="i === menu.index"
776
+ @mouseenter="menu.index = i"
777
+ @click="pick(item)"
778
+ >
779
+ <template v-if="menu.kind === 'slash'">
780
+ <span class="rm-rte__item-icon"><RmIcon :name="asSlash(item).icon" :size="16" /></span>
781
+ <span class="rm-rte__item-text">
782
+ <span>{{ asSlash(item).label }}</span>
783
+ <small>{{ asSlash(item).hint }}</small>
784
+ </span>
785
+ </template>
786
+ <template v-else>
787
+ <span class="rm-rte__item-icon">
788
+ <template v-if="asMention(item).kind === 'page' && (asMention(item) as RmRichTextPage).icon">
789
+ {{ (asMention(item) as RmRichTextPage).icon }}
790
+ </template>
791
+ <RmIcon v-else :name="MENTION_ICON[asMention(item).kind]" :size="16" />
792
+ </span>
793
+ <span class="rm-rte__item-text">
794
+ <span>
795
+ {{ asMention(item).kind === 'create-page' ? `Créer la page « ${asMention(item).label} »` : asMention(item).label }}
796
+ </span>
797
+ <small v-if="'hint' in item && item.hint">{{ item.hint }}</small>
798
+ </span>
799
+ </template>
800
+ </button>
801
+ </template>
802
+ <div v-if="menu.loading && menu.items.length" class="rm-rte__empty">Recherche des ressources…</div>
803
+ </div>
804
+ </Teleport>
805
+ </div>
806
+ </template>
807
+
808
+ <style scoped>
809
+ .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__tablemenu{align-items:center;display:flex;gap:var(--space-2);padding:var(--space-4)}.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)}
810
+ </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.219",
3
+ "version": "0.1.221",
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",