@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 +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/RichTextEditor.vue +726 -0
- package/dist/runtime/components/SavedViews.vue +33 -13
- package/dist/runtime/utils/richText.d.ts +121 -0
- package/dist/runtime/utils/richText.js +68 -0
- package/package.json +12 -1
package/dist/module.json
CHANGED
|
@@ -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
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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",
|