@tekkare/romulus 0.1.12 → 0.1.13

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.11",
7
+ "version": "0.1.12",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -21,6 +21,7 @@ const module = defineNuxtModule({
21
21
  if (options.baseStyles)
22
22
  nuxt.options.css.push(resolve(runtimeDir, "assets/styles/base.css"));
23
23
  nuxt.options.css.push(resolve(runtimeDir, "assets/styles/components.css"));
24
+ nuxt.options.css.push(resolve(runtimeDir, "assets/styles/chrome.css"));
24
25
  if (options.utilities)
25
26
  nuxt.options.css.push(resolve(runtimeDir, "assets/styles/utilities.css"));
26
27
  await addComponentsDir({
@@ -38,6 +39,10 @@ const module = defineNuxtModule({
38
39
  { rel: "stylesheet", href: fontsHref }
39
40
  );
40
41
  }
42
+ const phosphorHref = "https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css";
43
+ if (!nuxt.options.app.head.link.some((l) => l.href === phosphorHref)) {
44
+ nuxt.options.app.head.link.push({ rel: "stylesheet", href: phosphorHref });
45
+ }
41
46
  }
42
47
  });
43
48
 
@@ -0,0 +1 @@
1
+ body.lcollapsed{padding-left:var(--sidebar-collapsed-w)}.nav-item .icn i,.rpanel-item .icn i{font-size:16px}.strip-icon i,.topbar-toggle i{font-size:18px}.chev i,.chev-r i{font-size:11px}.topbar-toggle{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;color:var(--text-secondary);cursor:pointer;display:flex;font-size:16px;height:32px;justify-content:center;width:32px}.topbar-toggle:hover{background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-secondary)}.logo-mark{align-items:center;background:linear-gradient(135deg,var(--color-primary),var(--color-secondary));border-radius:6px;box-shadow:0 2px 6px rgba(107,63,255,.25);color:var(--color-text-inverse);display:flex;flex-shrink:0;font-size:14px;height:28px;justify-content:center;width:28px}.logo-mark,.logo-text{font-family:var(--font-serif);font-weight:700}.logo-text{font-size:16px;line-height:1}.logo-sub{color:var(--text-muted);font-size:10px;letter-spacing:.05em;margin-top:2px;text-transform:uppercase}.sidebar{background:var(--bg-surface);border-right:1px solid var(--border-default);bottom:0;display:flex;flex-direction:column;left:0;overflow-x:hidden;overflow-y:auto;position:fixed;top:0;transition:width .24s cubic-bezier(.2,0,0,1);width:var(--sidebar-w);z-index:70}.sidebar-top{border-bottom:1px solid var(--border-default);flex-shrink:0;height:var(--topbar-h);justify-content:space-between;padding:0 14px}.sidebar-top,.sidebar-top .logo-block{align-items:center;display:flex;gap:8px}.sidebar-top .logo-block{min-width:0}body.lcollapsed .sidebar-top{justify-content:center;padding:0}body.lcollapsed .sidebar-top .logo-block{display:none}body.lcollapsed .sidebar-top .logo-mark{display:flex}.sidebar-search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:999px;display:flex;gap:10px;margin:10px 12px 4px;padding:7px 14px;transition:border-color .15s,background .15s}.sidebar-search:focus-within,.sidebar-search:hover{background:var(--bg-surface);border-color:var(--color-violet-pale)}.sidebar-search i{color:var(--text-muted);flex-shrink:0;font-size:14px}.sidebar-search input{background:transparent;border:none;color:var(--text-primary);flex:1;font-family:inherit;font-size:13px;min-width:0;outline:none}.sidebar-search input::-moz-placeholder{color:var(--text-muted)}.sidebar-search input::placeholder{color:var(--text-muted)}.sidebar-search kbd{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:4px;color:var(--text-secondary);flex-shrink:0;font-family:inherit;font-size:10px;font-weight:600;padding:1px 5px}body.lcollapsed .sidebar-search{display:none}body.lcollapsed .sidebar{width:var(--sidebar-collapsed-w)}.nav-group{padding:4px 0}.nav-section-header{align-items:center;cursor:pointer;display:flex;justify-content:space-between;padding:14px 14px 6px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.nav-section-header .label,.nav-section-header span{color:var(--text-muted);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.nav-section-header .chev{color:var(--text-muted);font-size:10px;transition:transform .2s}.nav-section.collapsed .chev{transform:rotate(-90deg)}.nav-section.collapsed .nav-items{display:none}.nav-item{align-items:center;color:var(--text-secondary);cursor:pointer;display:flex;font-size:13px;font-weight:500;gap:10px;padding:7px 14px 7px 22px;transition:background .15s,color .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.nav-item .icn{color:var(--text-muted);flex-shrink:0;text-align:center;width:16px}.nav-item .lbl{flex:1;overflow:hidden;text-overflow:ellipsis}.nav-item .chev-r{color:var(--text-muted);font-size:10px;transition:transform .2s}.nav-item.has-children.open .chev-r{transform:rotate(90deg)}.nav-item:hover{background:var(--bg-surface-3);color:var(--text-primary)}.nav-item:hover .icn{color:var(--text-secondary)}.nav-item.active{background:linear-gradient(90deg,var(--color-violet-pale) 0,var(--bg-hover) 100%);color:var(--color-primary);font-weight:700;position:relative}.nav-item.active:before{background:var(--color-secondary);border-radius:0 4px 4px 0;bottom:5px;content:"";left:0;position:absolute;top:5px;width:4px}.nav-item.active .icn{color:var(--color-secondary)}.nav-subitems{background:var(--bg-surface-2);border-bottom:1px solid var(--border-soft);border-top:1px solid var(--border-soft);display:none;padding:2px 0 4px}.nav-item.has-children.open+.nav-subitems{display:block}.nav-subitem{align-items:center;color:var(--text-secondary);cursor:pointer;display:flex;font-size:12px;gap:8px;padding:6px 14px 6px 44px}.nav-subitem:hover{background:var(--bg-surface-3);color:var(--text-primary)}.nav-subitem.active{background:linear-gradient(90deg,var(--color-violet-pale) 0,var(--bg-hover) 100%);color:var(--color-primary);font-weight:700;position:relative}.nav-subitem.active:before{background:var(--color-secondary);border-radius:0 4px 4px 0;bottom:4px;content:"";left:0;position:absolute;top:4px;width:4px}.sidebar-footer{border-top:1px solid var(--border-default);display:flex;flex-direction:column;gap:4px;margin-top:auto;padding:10px 8px}.sb-user{align-items:center;border-radius:8px;cursor:pointer;display:flex;gap:12px;padding:10px 12px;transition:background .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sb-user:hover{background:var(--bg-surface-3)}.sb-user-avatar{align-items:center;background:linear-gradient(135deg,var(--color-secondary),var(--color-accent-teal));border-radius:50%;box-shadow:0 2px 6px rgba(107,63,255,.22);color:var(--color-text-inverse);display:flex;flex-shrink:0;font-family:var(--font-serif);font-size:13px;font-weight:700;height:36px;justify-content:center;width:36px}.sb-user-info{flex:1;min-width:0}.sb-user-name{color:var(--text-primary);font-size:13px;font-weight:600;line-height:1.25}.sb-user-name,.sb-user-org{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sb-user-org{color:var(--text-muted);font-size:11px;line-height:1.2;margin-top:2px}.sb-user-chev{color:var(--text-muted);flex-shrink:0;font-size:13px}.sb-actions{display:flex;gap:4px}.sb-actions .sb-lang,.sb-actions a{align-items:center;border-radius:6px;color:var(--text-secondary);cursor:pointer;display:inline-flex;flex:1;font-family:inherit;font-size:12px;font-weight:600;gap:5px;justify-content:center;padding:8px 10px;text-decoration:none;transition:background .15s,color .15s}.sb-actions .sb-lang:hover,.sb-actions a:hover{background:var(--bg-surface-3);color:var(--text-primary)}.sb-actions a.primary{color:var(--color-primary)}.sb-actions a.primary:hover{background:var(--bg-hover);color:var(--color-secondary)}.sb-actions i{font-size:13px}body.lcollapsed .sidebar .chev-r,body.lcollapsed .sidebar .lbl,body.lcollapsed .sidebar .nav-section-header,body.lcollapsed .sidebar-footer{display:none}body.lcollapsed .sidebar .nav-item.has-children.open+.nav-subitems,body.lcollapsed .sidebar .nav-subitems{display:none!important}body.lcollapsed .sidebar .nav-item.has-children.open .chev-r{transform:none}body.lcollapsed .sidebar .nav-group{border-top:1px solid var(--border-soft);padding:4px 0}body.lcollapsed .sidebar .nav-group:first-of-type{border-top:none}.nav-subitems.flyout,body.lcollapsed .sidebar .nav-item.has-children.open+.nav-subitems.flyout,body.lcollapsed .sidebar .nav-subitems.flyout{animation:flyout-in .12s cubic-bezier(.2,0,0,1);background:var(--bg-surface);border:1px solid var(--border-default);border-left:none;border-radius:0 8px 8px 0;box-shadow:var(--shadow-lg);display:block!important;left:var(--sidebar-collapsed-w);min-width:220px;padding:8px 0;position:fixed;z-index:100}.nav-subitems.flyout:before{border-bottom:1px solid var(--border-soft);color:var(--text-muted);content:attr(data-parent-label);display:block;font-size:11px;font-weight:700;letter-spacing:.08em;margin-bottom:4px;padding:6px 16px 8px;text-transform:uppercase}.nav-subitems.flyout .nav-subitem{font-size:13px;padding:8px 16px}body.lcollapsed .sidebar .nav-item{position:relative}body.lcollapsed .sidebar .nav-item[data-tip]:hover:after{background:var(--text-primary);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.2);color:var(--color-text-inverse);content:attr(data-tip);font-size:12px;font-weight:500;left:var(--sidebar-collapsed-w);margin-left:6px;padding:5px 10px;pointer-events:none;position:fixed;top:var(--tip-top,0);white-space:nowrap;z-index:110}.rsidebar .strip-icon{position:relative}.rsidebar .strip-icon[data-tip]:hover:before{background:var(--text-primary);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.2);color:var(--color-text-inverse);content:attr(data-tip);font-size:12px;font-weight:500;margin-right:6px;padding:5px 10px;pointer-events:none;position:fixed;right:var(--rsidebar-collapsed-w);top:var(--tip-top,0);white-space:nowrap;z-index:110}body.ropen .rsidebar .strip-icon[data-tip]:hover:before{display:none}.rsidebar .strip-icon.has-flyout.show-flyout .rflyout{display:block}body.ropen .rsidebar .rflyout{display:none!important}body.lcollapsed .nav-item{justify-content:center;padding:10px 0}body.lcollapsed .nav-section-header{justify-content:center;padding:8px 0}.rsidebar{background:var(--bg-surface);border-left:1px solid var(--border-default);bottom:0;display:flex;flex-direction:column;overflow:hidden;position:fixed;right:0;top:0;width:var(--rsidebar-collapsed-w);z-index:70}.rstrip-top{align-items:center;border-bottom:1px solid var(--border-default);display:flex;flex-shrink:0;height:var(--topbar-h);justify-content:center}.rstrip-collapsed{align-items:stretch;gap:4px;padding:8px}.rstrip-collapsed,.strip-icon{display:flex;flex-direction:column}.strip-icon{align-items:center;border-radius:8px;color:var(--text-secondary);cursor:pointer;gap:5px;justify-content:center;padding:10px 4px;position:relative;transition:background .15s,color .15s}.strip-icon i{font-size:19px;transition:color .15s}.strip-icon .strip-label{color:inherit;font-size:10px;font-weight:600;letter-spacing:.01em;line-height:1;white-space:nowrap}.strip-icon:hover{background:var(--bg-surface-3);color:var(--text-primary)}.strip-icon.active,.strip-icon:hover i{color:var(--color-secondary)}.strip-icon.active{background:var(--bg-hover)}.strip-icon.active i{color:var(--color-secondary)}.strip-icon .dot{background:var(--color-notification);border-radius:50%;box-shadow:0 0 0 2px var(--color-text-inverse);height:8px;position:absolute;right:14px;top:6px;width:8px}.strip-icon.is-live .dot{background:var(--color-success);box-shadow:0 0 0 2px var(--bg-surface)}.strip-icon.is-live .dot:after{animation:stripLivePulse 1.8s ease-out infinite;background:var(--color-success);border-radius:50%;content:"";inset:-2px;position:absolute}.strip-icon.is-live .strip-label{background:var(--color-success);border-radius:var(--radius-pill);color:var(--color-text-inverse);display:inline-block;font-weight:var(--weight-bold);letter-spacing:.04em;line-height:1.2;margin-top:2px;padding:2px var(--space-6);text-transform:uppercase}@media (prefers-reduced-motion:reduce){.strip-icon.is-live .dot:after{animation:none}}.strip-badge{align-items:center;background:var(--color-primary);border-radius:999px;box-shadow:0 0 0 2px var(--color-text-inverse);color:var(--color-text-inverse);display:flex;font-size:9px;font-variant-numeric:tabular-nums;font-weight:700;height:16px;justify-content:center;line-height:1;min-width:18px;padding:0 5px;position:absolute;right:8px;top:4px}.strip-divider{background:var(--bg-surface-3);margin:6px auto;width:70%}body.ropen .rpanel-open{display:block}.ptabs.export-toptabs{align-items:stretch;border-bottom:1px solid var(--border-default);gap:0;height:auto;margin:0 0 var(--space-20);padding:0}.ptabs.export-toptabs .ptab{align-items:center;display:inline-flex;font-size:var(--font-14);font-weight:var(--weight-semibold);gap:var(--space-6);padding:var(--space-12) var(--space-16)}.ptabs.export-toptabs .ptab i{font-size:var(--font-16)}.ptabs.export-toptabs .lib-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}.ptabs.export-toptabs .ptab.active .lib-tab-count{background:var(--bg-hover);color:var(--color-secondary)}.ptabs.views-toptabs{align-items:stretch;border-bottom:1px solid var(--border-default);gap:0;height:auto;margin:0 0 var(--space-16);padding:0}.ptabs.views-toptabs .ptab{align-items:center;display:inline-flex;font-size:var(--font-14);font-weight:var(--weight-semibold);gap:var(--space-6);padding:var(--space-12) var(--space-16)}.ptabs.views-toptabs .ptab i{font-size:var(--font-16)}.ptabs.views-toptabs .lib-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}.ptabs.views-toptabs .ptab.active .lib-tab-count{background:var(--bg-hover);color:var(--color-secondary)}.ptabs.alerts-toptabs{gap:var(--space-4);margin:0 0 var(--space-16);padding:0}.ptabs.alerts-toptabs .ptab{font-size:var(--font-13);padding:var(--space-8) var(--space-12)}.ptabs.alerts-toptabs .ptab i{font-size:var(--font-16)}.topbar{background:var(--bg-surface);border-bottom:1px solid var(--border-default);padding-top:28px;position:sticky;top:var(--topbar-merged-h);transition:padding-top .2s ease;z-index:31}.topbar.compact{padding-top:10px}.topbar.compact .pane-title{font-size:19px;margin-bottom:8px}.ptabs{align-items:flex-end;display:flex;gap:4px;height:var(--tabs-h);padding:0 32px}.ptab{border-bottom:3px solid transparent;color:var(--text-secondary);cursor:pointer;font-size:14px;font-weight:500;padding:0 20px 12px;position:relative;transition:color .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.ptab:hover{color:var(--text-primary)}.ptab.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:700}.strip-icon--hub{display:none}.strip-divider{background:var(--border-default);height:1px;margin:var(--space-8) auto var(--space-4);width:60%}.topbar>*{margin-left:auto;margin-right:auto;max-width:var(--content-max-w);width:100%}body.lcollapsed .hint{left:calc(var(--sidebar-collapsed-w) + 16px)}body.ropen .hint{right:calc(var(--rsidebar-w) + 16px)}.mobile-hamburger{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:999px;box-shadow:0 2px 8px rgba(0,0,0,.08);cursor:pointer;display:none;height:40px;justify-content:center;left:10px;position:fixed;top:10px;width:40px;z-index:260}.mobile-hamburger i{color:var(--text-primary);font-size:18px}.mobile-backdrop{background:rgba(0,0,0,.4);display:none;inset:0;position:fixed;z-index:240}body.lopen .mobile-backdrop{display:block}@media (max-width:767px){.mobile-hamburger{display:inline-flex}body.lopen .mobile-hamburger{display:none}.sidebar{box-shadow:4px 0 16px rgba(0,0,0,.15);transform:translateX(-100%);transition:transform .24s cubic-bezier(.2,0,0,1);width:280px;z-index:250}body.lopen .sidebar{transform:translateX(0)}body.lcollapsed .sidebar{width:280px}#rDrawer,.rsidebar{display:none!important}.ptabs{flex-wrap:nowrap;overflow-x:auto;padding:0 14px;scrollbar-width:thin}.ptabs::-webkit-scrollbar{height:4px}.ptab{flex-shrink:0;white-space:nowrap}}body.rm-shell{background:var(--bg-app);color:var(--text-primary);font-family:var(--font-sans);margin:0;padding-left:var(--sidebar-w);padding-right:var(--rsidebar-collapsed-w);padding-top:var(--topbar-merged-h);transition:padding .24s cubic-bezier(.2,0,0,1)}body.rm-shell.lcollapsed{padding-left:var(--sidebar-collapsed-w)}body.rm-switching.rm-shell,body.theme-switching.rm-shell{transition:none!important}@media (max-width:767px){body.rm-shell,body.rm-shell.lcollapsed{padding-left:0;padding-right:0}}
@@ -1 +1 @@
1
- .grid-2{grid-template-columns:repeat(2,1fr)}.grid-2,.grid-3{display:grid;gap:20px}.grid-3{grid-template-columns:repeat(3,1fr)}.grid-4{display:grid;gap:16px;grid-template-columns:repeat(4,1fr)}.grid-2-1{display:grid;gap:20px;grid-template-columns:2fr 1fr}.pane-title{color:var(--text-primary);font-family:var(--font-serif);font-size:32px;font-weight:700;letter-spacing:-.02em;line-height:1.1;margin:0 0 16px}.pane-sub{color:var(--text-muted);font-size:13px;line-height:1.55;margin:0 0 24px;max-width:78ch}.chart-section{margin-inline:auto;max-width:var(--content-max-w,1440px);padding:0 0 40px;width:100%}.section-label{align-items:center;color:var(--text-secondary);display:flex;font-size:11px;font-weight:700;gap:8px;letter-spacing:.12em;margin:40px 0 18px;text-transform:uppercase}body.text-large .pane-title{font-size:38px}body.text-xl .pane-title{font-size:46px}body.text-large .section-label{font-size:13px}body.text-xl .section-label{font-size:14px}.chart-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:10px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;transition:box-shadow .18s,border-color .18s}.chart-card:hover{border-color:var(--color-violet-pale);box-shadow:0 6px 16px rgba(107,63,255,.1),0 1px 3px rgba(30,41,59,.04)}.chart-card-head{padding:22px 64px 14px 24px;position:relative}.chart-card-title{color:var(--text-primary);font-size:15px;font-weight:700;letter-spacing:-.01em;line-height:1.3;margin:0 0 5px}.chart-card-context{color:var(--text-muted);font-size:12px;line-height:1.55;margin:0}.chart-card-actions{position:absolute;right:12px;top:12px}.cc-export{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;color:var(--text-secondary);cursor:pointer;display:flex;font-size:14px;height:28px;justify-content:center;transition:background .15s,color .15s,border-color .15s;width:28px}.cc-export:hover{color:var(--color-secondary)}.cc-export:hover,.chart-card-actions.open .cc-export{background:var(--bg-hover);border-color:var(--color-violet-pale)}.chart-card-actions.open .cc-export{color:var(--color-primary)}.cc-menu{animation:cc-menu-in .12s cubic-bezier(.2,0,0,1);background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);display:none;min-width:200px;padding:6px 0;position:absolute;right:0;top:34px;z-index:var(--z-modal)}.chart-card-actions.open .cc-menu{display:block}.cc-menu-section{color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;padding:8px 14px 4px;text-transform:uppercase}.cc-menu-item{align-items:center;background:none;border:0;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:13px;gap:10px;padding:7px 14px;text-align:left;width:100%}.cc-menu-item i{color:var(--text-muted);font-size:14px}.cc-menu-item:hover{background:var(--bg-surface-3);color:var(--text-primary)}.cc-menu-item:hover i{color:var(--text-secondary)}.cc-menu-divider{background:var(--bg-surface-3);height:1px;margin:4px 0}@keyframes cc-menu-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.chart-card-body{flex:1;padding:4px 12px 8px}.chart-card-foot{align-items:center;background:var(--bg-surface-2);border-radius:0 0 10px 10px;border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:11px;gap:10px;padding:12px 24px}.chart-card-foot .src{align-items:center;display:flex;gap:4px}.chart-card-foot .src b{color:var(--text-secondary);font-weight:600}.chart-card-foot .dot{background:var(--border-default);border-radius:50%;height:3px;width:3px}.chart-card-foot .updated{color:var(--text-secondary);font-weight:500}.kpi-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:0 2px 8px rgba(30,41,59,.06);padding:16px 18px}.kpi-label{color:var(--text-muted);font-size:11px;font-weight:600;letter-spacing:.1em;margin:0 0 6px;text-transform:uppercase}.kpi-value{color:var(--color-primary);font-family:var(--font-serif);font-size:28px;font-weight:700;line-height:1;margin:0}.kpi-value.healthcare{color:var(--color-healthcare)}.kpi-value.discovery{color:var(--color-discovery)}.kpi-value.pharma{color:var(--color-pharma)}.kpi-value.databank{color:var(--color-databank)}.kpi-trend{align-items:center;display:inline-flex;font-size:12px;font-weight:600;gap:4px;margin-top:6px}.kpi-trend i{font-size:11px}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-danger)}.kpi-foot,.kpi-trend--flat{color:var(--text-muted)}.kpi-foot{font-size:11px;margin-top:4px}.badge{border-radius:4px;display:inline-block;font-size:10px;font-weight:600;padding:2px 8px}.badge-active{background:var(--color-success-bg);color:var(--color-success)}.badge-pending{background:var(--color-warning-bg);color:var(--color-warning)}.badge-error{background:var(--color-danger-bg);color:var(--color-danger)}.badge-info{background:var(--bg-hover);color:var(--color-primary)}.badge-grey{background:var(--bg-surface-3);color:var(--text-secondary)}table.tk{border-collapse:collapse;font-size:13px;width:100%}.sr-only,table.tk>caption.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}table.tk caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13);font-weight:var(--weight-bold);padding:0 0 var(--space-8);text-align:left}table.tk th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:11px;font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase}table.tk tbody th[scope=row]{background:transparent;font-size:var(--font-13);font-weight:var(--weight-bold);letter-spacing:0;text-transform:none}table.tk tbody th[scope=row],table.tk td{border-bottom:1px solid var(--border-soft);color:var(--text-primary)}table.tk td{padding:12px 14px}table.tk tbody tr:nth-child(2n) td,table.tk tbody tr:nth-child(2n) th[scope=row]{background:var(--bg-surface-2)}table.tk tbody tr:hover td,table.tk tbody tr:hover th[scope=row]{background:var(--bg-hover)}table.tk th[aria-sort]{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}table.tk th[aria-sort]:hover{color:var(--text-primary)}.tk-wrap{overflow-x:auto}.tk-num{font-variant-numeric:tabular-nums;text-align:right}.nav-group{display:flex;flex-direction:column}.nav-group-header{align-items:center;background:none;border:0;color:var(--text-muted);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:11px;font-weight:700;gap:8px;letter-spacing:.08em;padding:8px 12px;text-align:left;text-transform:uppercase;transition:color var(--duration-fast) var(--ease-out);width:100%}.nav-group-header:hover{color:var(--text-secondary)}.nav-group-header .caret{display:inline-flex;margin-left:auto;transition:transform var(--duration-fast) var(--ease-out)}.nav-group[data-open=true]>.nav-group-header{color:var(--color-primary)}.nav-group[data-open=true]>.nav-group-header .caret{transform:rotate(180deg)}.nav-group-items{display:flex;flex-direction:column;gap:2px;padding:2px 0 8px}.cc-export:focus-visible,.cc-menu-item:focus-visible,.chart-card :focus-visible,.nav-group-header:focus-visible,table.tk th[aria-sort]:focus-visible{box-shadow:var(--shadow-focus);outline:2px solid var(--color-secondary);outline-offset:2px}.cc-menu-item,.nav-group-header{min-height:40px}body.text-large .kpi-label{font-size:13px}body.text-large .kpi-value{font-size:34px}body.text-large .kpi-trend{font-size:14px}body.text-large .kpi-foot{font-size:13px}body.text-large .chart-card-title{font-size:17px}body.text-large .chart-card-context{font-size:14px}body.text-large .chart-card-foot{font-size:12px}body.text-large .badge{font-size:12px;padding:3px 9px}body.text-large table.tk{font-size:15px}body.text-large table.tk th{font-size:13px}body.text-xl .kpi-label{font-size:14px}body.text-xl .kpi-value{font-size:40px}body.text-xl .kpi-trend{font-size:16px}body.text-xl .kpi-foot{font-size:14px}body.text-xl .chart-card-title{font-size:20px}body.text-xl .chart-card-context{font-size:16px}body.text-xl .chart-card-foot{font-size:14px}body.text-xl .badge{font-size:14px;padding:3px 10px}body.text-xl table.tk{font-size:16px}body.text-xl table.tk th{font-size:14px}body.high-contrast .cc-export,body.high-contrast .cc-menu,body.high-contrast .chart-card,body.high-contrast .kpi-card{border-width:2px}body.high-contrast .chart-card:hover{border-color:var(--border-default)}body.theme-switching,body.theme-switching *,body.theme-switching :after,body.theme-switching :before{transition:none!important}body.high-contrast *{box-shadow:none!important}
1
+ .grid-2{grid-template-columns:repeat(2,1fr)}.grid-2,.grid-3{display:grid;gap:20px}.grid-3{grid-template-columns:repeat(3,1fr)}.grid-4{display:grid;gap:16px;grid-template-columns:repeat(4,1fr)}.grid-2-1{display:grid;gap:20px;grid-template-columns:2fr 1fr}.pane-title{color:var(--text-primary);font-family:var(--font-serif);font-size:32px;font-weight:700;letter-spacing:-.02em;line-height:1.1;margin:0 0 16px}.pane-sub{color:var(--text-muted);font-size:13px;line-height:1.55;margin:0 0 24px;max-width:78ch}.chart-section{margin-inline:auto;max-width:var(--content-max-w,1440px);padding:0 0 40px;width:100%}.section-label{align-items:center;color:var(--text-secondary);display:flex;font-size:11px;font-weight:700;gap:8px;letter-spacing:.12em;margin:40px 0 18px;text-transform:uppercase}body.text-large .pane-title{font-size:38px}body.text-xl .pane-title{font-size:46px}body.text-large .section-label{font-size:13px}body.text-xl .section-label{font-size:14px}.chart-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:10px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;transition:box-shadow .18s,border-color .18s}.chart-card:hover{border-color:var(--color-violet-pale);box-shadow:0 6px 16px rgba(107,63,255,.1),0 1px 3px rgba(30,41,59,.04)}.chart-card-head{padding:22px 64px 14px 24px;position:relative}.chart-card-title{color:var(--text-primary);font-size:15px;font-weight:700;letter-spacing:-.01em;line-height:1.3;margin:0 0 5px}.chart-card-context{color:var(--text-muted);font-size:12px;line-height:1.55;margin:0}.chart-card-actions{position:absolute;right:12px;top:12px}.cc-export{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;color:var(--text-secondary);cursor:pointer;display:flex;font-size:14px;height:28px;justify-content:center;transition:background .15s,color .15s,border-color .15s;width:28px}.cc-export:hover{color:var(--color-secondary)}.cc-export:hover,.chart-card-actions.open .cc-export{background:var(--bg-hover);border-color:var(--color-violet-pale)}.chart-card-actions.open .cc-export{color:var(--color-primary)}.cc-menu{animation:cc-menu-in .12s cubic-bezier(.2,0,0,1);background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);display:none;min-width:200px;padding:6px 0;position:absolute;right:0;top:34px;z-index:var(--z-modal)}.chart-card-actions.open .cc-menu{display:block}.cc-menu-section{color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;padding:8px 14px 4px;text-transform:uppercase}.cc-menu-item{align-items:center;background:none;border:0;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:13px;gap:10px;padding:7px 14px;text-align:left;width:100%}.cc-menu-item i{color:var(--text-muted);font-size:14px}.cc-menu-item:hover{background:var(--bg-surface-3);color:var(--text-primary)}.cc-menu-item:hover i{color:var(--text-secondary)}.cc-menu-divider{background:var(--bg-surface-3);height:1px;margin:4px 0}@keyframes cc-menu-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.chart-card-body{flex:1;padding:4px 12px 8px}.chart-card-foot{align-items:center;background:var(--bg-surface-2);border-radius:0 0 10px 10px;border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:11px;gap:10px;padding:12px 24px}.chart-card-foot .src{align-items:center;display:flex;gap:4px}.chart-card-foot .src b{color:var(--text-secondary);font-weight:600}.chart-card-foot .dot{background:var(--border-default);border-radius:50%;height:3px;width:3px}.chart-card-foot .updated{color:var(--text-secondary);font-weight:500}.kpi-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:0 2px 8px rgba(30,41,59,.06);padding:16px 18px}.kpi-label{color:var(--text-muted);font-size:11px;font-weight:600;letter-spacing:.1em;margin:0 0 6px;text-transform:uppercase}.kpi-value{color:var(--color-primary);font-family:var(--font-serif);font-size:28px;font-weight:700;line-height:1;margin:0}.kpi-value.healthcare{color:var(--color-healthcare)}.kpi-value.discovery{color:var(--color-discovery)}.kpi-value.pharma{color:var(--color-pharma)}.kpi-value.databank{color:var(--color-databank)}.kpi-trend{align-items:center;display:inline-flex;font-size:12px;font-weight:600;gap:4px;margin-top:6px}.kpi-trend i{font-size:11px}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-danger)}.kpi-foot,.kpi-trend--flat{color:var(--text-muted)}.kpi-foot{font-size:11px;margin-top:4px}.badge{border-radius:4px;display:inline-block;font-size:10px;font-weight:600;padding:2px 8px}.badge-active{background:var(--color-success-bg);color:var(--color-success)}.badge-pending{background:var(--color-warning-bg);color:var(--color-warning)}.badge-error{background:var(--color-danger-bg);color:var(--color-danger)}.badge-info{background:var(--bg-hover);color:var(--color-primary)}.badge-grey{background:var(--bg-surface-3);color:var(--text-secondary)}table.tk{border-collapse:collapse;font-size:13px;width:100%}.sr-only,table.tk>caption.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}table.tk caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13);font-weight:var(--weight-bold);padding:0 0 var(--space-8);text-align:left}table.tk th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:11px;font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase}table.tk tbody th[scope=row]{background:transparent;font-size:var(--font-13);font-weight:var(--weight-bold);letter-spacing:0;text-transform:none}table.tk tbody th[scope=row],table.tk td{border-bottom:1px solid var(--border-soft);color:var(--text-primary)}table.tk td{padding:12px 14px}table.tk tbody tr:nth-child(2n) td,table.tk tbody tr:nth-child(2n) th[scope=row]{background:var(--bg-surface-2)}table.tk tbody tr:hover td,table.tk tbody tr:hover th[scope=row]{background:var(--bg-hover)}table.tk th[aria-sort]{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}table.tk th[aria-sort]:hover{color:var(--text-primary)}.tk-wrap{overflow-x:auto}.tk-num{font-variant-numeric:tabular-nums;text-align:right}.nav-group{display:flex;flex-direction:column}.nav-group-header{align-items:center;background:none;border:0;color:var(--text-muted);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:11px;font-weight:700;gap:8px;letter-spacing:.08em;padding:8px 12px;text-align:left;text-transform:uppercase;transition:color var(--duration-fast) var(--ease-out);width:100%}.nav-group-header:hover{color:var(--text-secondary)}.nav-group-header .caret{display:inline-flex;margin-left:auto;transition:transform var(--duration-fast) var(--ease-out)}.nav-group[data-open=true]>.nav-group-header{color:var(--color-primary)}.nav-group[data-open=true]>.nav-group-header .caret{transform:rotate(180deg)}.nav-group-items{display:flex;flex-direction:column;gap:2px;padding:2px 0 8px}.cc-export:focus-visible,.cc-menu-item:focus-visible,.chart-card :focus-visible,.nav-group-header:focus-visible,table.tk th[aria-sort]:focus-visible{box-shadow:var(--shadow-focus);outline:2px solid var(--color-secondary);outline-offset:2px}.cc-menu-item,.nav-group-header{min-height:40px}body.text-large .kpi-label{font-size:13px}body.text-large .kpi-value{font-size:34px}body.text-large .kpi-trend{font-size:14px}body.text-large .kpi-foot{font-size:13px}body.text-large .chart-card-title{font-size:17px}body.text-large .chart-card-context{font-size:14px}body.text-large .chart-card-foot{font-size:12px}body.text-large .badge{font-size:12px;padding:3px 9px}body.text-large table.tk{font-size:15px}body.text-large table.tk th{font-size:13px}body.text-xl .kpi-label{font-size:14px}body.text-xl .kpi-value{font-size:40px}body.text-xl .kpi-trend{font-size:16px}body.text-xl .kpi-foot{font-size:14px}body.text-xl .chart-card-title{font-size:20px}body.text-xl .chart-card-context{font-size:16px}body.text-xl .chart-card-foot{font-size:14px}body.text-xl .badge{font-size:14px;padding:3px 10px}body.text-xl table.tk{font-size:16px}body.text-xl table.tk th{font-size:14px}body.high-contrast .cc-export,body.high-contrast .cc-menu,body.high-contrast .chart-card,body.high-contrast .kpi-card{border-width:2px}body.high-contrast .chart-card:hover{border-color:var(--border-default)}body.rm-switching,body.rm-switching *,body.rm-switching :after,body.rm-switching :before,body.theme-switching,body.theme-switching *,body.theme-switching :after,body.theme-switching :before{transition:none!important}body.high-contrast *{box-shadow:none!important}
@@ -1 +1 @@
1
- :root{--sidebar-w:220px;--sidebar-collapsed-w:56px;--rsidebar-w:280px;--rsidebar-collapsed-w:76px;--topbar-h:56px;--tabs-h:46px;--analysis-header-h:124px;--sourcebar-h:38px;--filterbar-h:56px;--color-primary:#2100ad;--color-primary-hover:#1a0088;--color-secondary:#6b3fff;--color-accent-soft:#b485ff;--color-violet-pale:#d6ccff;--color-healthcare:#4164ee;--color-discovery:#07816f;--color-pharma:#f7520e;--color-databank:#e04380;--color-healthcare-active:#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-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)}body.high-contrast:not(.night){--text-primary:#000;--text-secondary:#1a1a24;--text-muted:#2f2f40;--border-default:#000;--border-soft:#525670;--bg-app:#fff;--bg-surface:#fff;--bg-surface-2:#fff;--bg-surface-3:#f0f1f7;--bg-hover:#ffe56b;--color-primary:#1100a0;--color-primary-hover:#0a0070;--color-secondary:#5028d8;--color-success:#006b2c;--color-warning:#8c4500;--color-danger:#a60000;--color-info:#0040c8}body.night.high-contrast{--text-primary:#fff;--text-secondary:#f5f6fa;--text-muted:#cfd3e5;--border-default:#fff;--border-soft:#b8bcd0;--bg-app:#000;--bg-surface:#000;--bg-surface-2:#000;--bg-surface-3:#000;--bg-hover:#ffe56b;--color-primary:#d6ccff;--color-primary-hover:#fff;--color-secondary:#e5daff;--color-accent-soft:#fff;--color-success:#6fe8c5;--color-warning:#fbbf24;--color-danger:#fca5a5;--color-info:#7aa0ff;--brand-rgb:214,204,255}body.reduce-motion *,body.reduce-motion :after,body.reduce-motion :before{animation:none!important;scroll-behavior:auto!important;transition:none!important}:root{--r-color-primary-50:#f0f4ff;--r-color-primary-100:#dce5fd;--r-color-primary-200:#b9cbfb;--r-color-primary-300:#96b0f9;--r-color-primary-400:#7395f5;--r-color-primary-500:#5a7ef2;--r-color-primary-600:var(--color-healthcare);--r-color-primary-700:#3451d4;--r-color-primary-800:#283fb0;--r-color-primary-900:#1c2d8a;--r-color-gray-50:#f9fafb;--r-color-gray-100:#f3f4f6;--r-color-gray-200:#e5e7eb;--r-color-gray-300:#d1d5db;--r-color-gray-400:#9ca3af;--r-color-gray-500:#6b7280;--r-color-gray-600:#4b5563;--r-color-gray-700:#374151;--r-color-gray-800:#1f2937;--r-color-gray-900:#111827;--r-color-gray-950:#030712;--r-color-success-500:var(--color-success);--r-color-success-600:#16a34a;--r-color-warning-500:#f59e0b;--r-color-warning-600:var(--color-warning);--r-color-danger-500:#ef4444;--r-color-danger-600:var(--color-danger);--r-color-white:#fff;--r-color-black:#000;--r-font-family:var(--font-sans);--r-font-mono:var(--font-mono);--r-text-xs:0.75rem;--r-text-sm:0.875rem;--r-text-base:1rem;--r-text-lg:1.125rem;--r-text-xl:1.25rem;--r-text-2xl:1.5rem;--r-text-3xl:1.875rem;--r-text-4xl:2.25rem;--r-font-weight-normal:var(--weight-normal);--r-font-weight-medium:var(--weight-medium);--r-font-weight-semibold:var(--weight-semibold);--r-font-weight-bold:var(--weight-bold);--r-leading-tight:1.25;--r-leading-normal:1.5;--r-leading-relaxed:1.75;--r-space-0:0;--r-space-1:var(--space-4);--r-space-2:var(--space-8);--r-space-3:var(--space-12);--r-space-4:var(--space-16);--r-space-5:var(--space-20);--r-space-6:var(--space-24);--r-space-8:var(--space-32);--r-space-10:var(--space-40);--r-space-12:var(--space-48);--r-radius-none:0;--r-radius-sm:var(--radius-sm);--r-radius-md:var(--radius-md);--r-radius-lg:var(--radius-lg);--r-radius-xl:0.75rem;--r-radius-2xl:1rem;--r-radius-full:var(--radius-pill);--r-shadow-xs:0 1px 2px 0 rgba(0,0,0,.05);--r-shadow-sm:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--r-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--r-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--r-shadow-xl:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--r-transition-fast:var(--duration-fast) var(--ease-in-out);--r-transition-base:200ms var(--ease-in-out);--r-transition-slow:var(--duration-slow) var(--ease-in-out)}
1
+ :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-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)}body.high-contrast:not(.night){--text-primary:#000;--text-secondary:#1a1a24;--text-muted:#2f2f40;--border-default:#000;--border-soft:#525670;--bg-app:#fff;--bg-surface:#fff;--bg-surface-2:#fff;--bg-surface-3:#f0f1f7;--bg-hover:#ffe56b;--color-primary:#1100a0;--color-primary-hover:#0a0070;--color-secondary:#5028d8;--color-success:#006b2c;--color-warning:#8c4500;--color-danger:#a60000;--color-info:#0040c8}body.night.high-contrast{--text-primary:#fff;--text-secondary:#f5f6fa;--text-muted:#cfd3e5;--border-default:#fff;--border-soft:#b8bcd0;--bg-app:#000;--bg-surface:#000;--bg-surface-2:#000;--bg-surface-3:#000;--bg-hover:#ffe56b;--color-primary:#d6ccff;--color-primary-hover:#fff;--color-secondary:#e5daff;--color-accent-soft:#fff;--color-success:#6fe8c5;--color-warning:#fbbf24;--color-danger:#fca5a5;--color-info:#7aa0ff;--brand-rgb:214,204,255}body.reduce-motion *,body.reduce-motion :after,body.reduce-motion :before{animation:none!important;scroll-behavior:auto!important;transition:none!important}:root{--r-color-primary-50:#f0f4ff;--r-color-primary-100:#dce5fd;--r-color-primary-200:#b9cbfb;--r-color-primary-300:#96b0f9;--r-color-primary-400:#7395f5;--r-color-primary-500:#5a7ef2;--r-color-primary-600:var(--color-healthcare);--r-color-primary-700:#3451d4;--r-color-primary-800:#283fb0;--r-color-primary-900:#1c2d8a;--r-color-gray-50:#f9fafb;--r-color-gray-100:#f3f4f6;--r-color-gray-200:#e5e7eb;--r-color-gray-300:#d1d5db;--r-color-gray-400:#9ca3af;--r-color-gray-500:#6b7280;--r-color-gray-600:#4b5563;--r-color-gray-700:#374151;--r-color-gray-800:#1f2937;--r-color-gray-900:#111827;--r-color-gray-950:#030712;--r-color-success-500:var(--color-success);--r-color-success-600:#16a34a;--r-color-warning-500:#f59e0b;--r-color-warning-600:var(--color-warning);--r-color-danger-500:#ef4444;--r-color-danger-600:var(--color-danger);--r-color-white:#fff;--r-color-black:#000;--r-font-family:var(--font-sans);--r-font-mono:var(--font-mono);--r-text-xs:0.75rem;--r-text-sm:0.875rem;--r-text-base:1rem;--r-text-lg:1.125rem;--r-text-xl:1.25rem;--r-text-2xl:1.5rem;--r-text-3xl:1.875rem;--r-text-4xl:2.25rem;--r-font-weight-normal:var(--weight-normal);--r-font-weight-medium:var(--weight-medium);--r-font-weight-semibold:var(--weight-semibold);--r-font-weight-bold:var(--weight-bold);--r-leading-tight:1.25;--r-leading-normal:1.5;--r-leading-relaxed:1.75;--r-space-0:0;--r-space-1:var(--space-4);--r-space-2:var(--space-8);--r-space-3:var(--space-12);--r-space-4:var(--space-16);--r-space-5:var(--space-20);--r-space-6:var(--space-24);--r-space-8:var(--space-32);--r-space-10:var(--space-40);--r-space-12:var(--space-48);--r-radius-none:0;--r-radius-sm:var(--radius-sm);--r-radius-md:var(--radius-md);--r-radius-lg:var(--radius-lg);--r-radius-xl:0.75rem;--r-radius-2xl:1rem;--r-radius-full:var(--radius-pill);--r-shadow-xs:0 1px 2px 0 rgba(0,0,0,.05);--r-shadow-sm:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--r-shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--r-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--r-shadow-xl:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--r-transition-fast:var(--duration-fast) var(--ease-in-out);--r-transition-base:200ms var(--ease-in-out);--r-transition-slow:var(--duration-slow) var(--ease-in-out)}
@@ -0,0 +1,254 @@
1
+ <script setup lang="ts">
2
+ export interface AiChatMessage {
3
+ id: string
4
+ role: 'user' | 'assistant'
5
+ content: string
6
+ timestamp?: Date
7
+ }
8
+
9
+ export interface AiChatSuggestion {
10
+ icon?: string
11
+ label: string
12
+ message: string
13
+ }
14
+
15
+ export interface RmAiChatProps {
16
+ /** Whether the chat drawer is open */
17
+ modelValue: boolean
18
+ /** Chat title displayed in the header */
19
+ title?: string
20
+ /** Emoji or text icon for the header */
21
+ icon?: string
22
+ /** Input placeholder text */
23
+ placeholder?: string
24
+ /** Suggestion buttons displayed on empty state */
25
+ suggestions?: AiChatSuggestion[]
26
+ /** Chat messages */
27
+ messages?: AiChatMessage[]
28
+ /** Whether the assistant is currently responding */
29
+ loading?: boolean
30
+ /** Loading indicator text */
31
+ loadingText?: string
32
+ /** Welcome message displayed when no messages */
33
+ welcomeMessage?: string
34
+ /** Drawer size */
35
+ drawerSize?: 'sm' | 'md' | 'lg' | 'xl'
36
+ /** Show the floating trigger button */
37
+ showTrigger?: boolean
38
+ }
39
+
40
+ const props = withDefaults(defineProps<RmAiChatProps>(), {
41
+ title: 'AI Assistant',
42
+ icon: '',
43
+ placeholder: 'Type a message...',
44
+ suggestions: () => [],
45
+ messages: () => [],
46
+ loading: false,
47
+ loadingText: 'Thinking...',
48
+ welcomeMessage: 'Hello! How can I help you today?',
49
+ drawerSize: 'md',
50
+ showTrigger: true,
51
+ })
52
+
53
+ const emit = defineEmits<{
54
+ 'update:modelValue': [value: boolean]
55
+ 'send': [message: string]
56
+ 'clear': []
57
+ }>()
58
+
59
+ const inputValue = ref('')
60
+ const messagesContainer = ref<HTMLElement | null>(null)
61
+
62
+ function open() {
63
+ emit('update:modelValue', true)
64
+ }
65
+
66
+ function close() {
67
+ emit('update:modelValue', false)
68
+ }
69
+
70
+ function toggle() {
71
+ emit('update:modelValue', !props.modelValue)
72
+ }
73
+
74
+ function handleSubmit() {
75
+ if (!inputValue.value.trim() || props.loading) return
76
+ const message = inputValue.value
77
+ inputValue.value = ''
78
+ emit('send', message)
79
+ scrollToBottom()
80
+ }
81
+
82
+ function handleSuggestion(suggestion: AiChatSuggestion) {
83
+ emit('send', suggestion.message)
84
+ scrollToBottom()
85
+ }
86
+
87
+ function handleClear() {
88
+ emit('clear')
89
+ }
90
+
91
+ function scrollToBottom() {
92
+ nextTick(() => {
93
+ if (messagesContainer.value) {
94
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
95
+ }
96
+ })
97
+ }
98
+
99
+ // Scroll to bottom when drawer opens or new messages arrive
100
+ watch(() => props.modelValue, (open) => {
101
+ if (open) scrollToBottom()
102
+ })
103
+
104
+ watch(() => props.messages.length, () => {
105
+ scrollToBottom()
106
+ })
107
+
108
+ // Simple markdown renderer
109
+ function renderMarkdown(text: string): string {
110
+ if (!text) return ''
111
+ return text
112
+ .replace(/&/g, '&amp;')
113
+ .replace(/</g, '&lt;')
114
+ .replace(/>/g, '&gt;')
115
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
116
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
117
+ .replace(/^(\d+)\.\s+(.+)$/gm, '<li class="rm-ai-chat-numbered">$2</li>')
118
+ .replace(/^[-*]\s+(.+)$/gm, '<li class="rm-ai-chat-bullet">$1</li>')
119
+ .replace(/((?:<li class="rm-ai-chat-numbered">.*?<\/li>)+)/g, '<ol>$1</ol>')
120
+ .replace(/((?:<li class="rm-ai-chat-bullet">.*?<\/li>)+)/g, '<ul>$1</ul>')
121
+ .replace(/\n/g, '<br>')
122
+ }
123
+
124
+ // Close on Escape
125
+ onMounted(() => {
126
+ const handleEscape = (e: KeyboardEvent) => {
127
+ if (e.key === 'Escape' && props.modelValue) {
128
+ close()
129
+ }
130
+ }
131
+ document.addEventListener('keydown', handleEscape)
132
+ onUnmounted(() => {
133
+ document.removeEventListener('keydown', handleEscape)
134
+ })
135
+ })
136
+
137
+ defineExpose({ open, close, toggle, scrollToBottom })
138
+ </script>
139
+
140
+ <template>
141
+ <!-- Floating trigger button -->
142
+ <button
143
+ v-if="showTrigger"
144
+ class="rm-ai-chat-trigger"
145
+ :class="{ 'rm-ai-chat-trigger--open': modelValue }"
146
+ @click="toggle"
147
+ >
148
+ <slot name="trigger">
149
+ <RmIcon name="Robot" :size="24" weight="duotone" />
150
+ </slot>
151
+ </button>
152
+
153
+ <!-- Chat Drawer -->
154
+ <RmDrawer
155
+ :model-value="modelValue"
156
+ :size="drawerSize"
157
+ side="right"
158
+ @update:model-value="emit('update:modelValue', $event)"
159
+ >
160
+ <template #header>
161
+ <div class="rm-ai-chat-header">
162
+ <span v-if="icon" class="rm-ai-chat-header__icon">{{ icon }}</span>
163
+ <RmIcon v-else name="Robot" :size="22" weight="duotone" class="rm-ai-chat-header__robot" />
164
+ <h3 class="rm-ai-chat-header__title">{{ title }}</h3>
165
+ </div>
166
+ <button class="rm-ai-chat-header__clear" @click="handleClear" title="Clear chat">
167
+ <RmIcon name="Trash" :size="16" />
168
+ </button>
169
+ </template>
170
+
171
+ <div class="rm-ai-chat-body">
172
+ <div ref="messagesContainer" class="rm-ai-chat-messages">
173
+ <!-- Welcome state -->
174
+ <div v-if="messages.length === 0" class="rm-ai-chat-welcome">
175
+ <div class="rm-ai-chat-message rm-ai-chat-message--assistant rm-ai-chat-message--welcome">
176
+ <div class="rm-ai-chat-avatar rm-ai-chat-avatar--assistant">
177
+ <RmIcon name="Robot" :size="18" weight="duotone" />
178
+ </div>
179
+ <div class="rm-ai-chat-bubble rm-ai-chat-bubble--assistant rm-ai-chat-bubble--welcome">
180
+ <slot name="welcome">
181
+ {{ welcomeMessage }}
182
+ </slot>
183
+ </div>
184
+ </div>
185
+
186
+ <div v-if="suggestions.length" class="rm-ai-chat-suggestions">
187
+ <p class="rm-ai-chat-suggestions__hint">Try asking:</p>
188
+ <button
189
+ v-for="(suggestion, i) in suggestions"
190
+ :key="i"
191
+ class="rm-ai-chat-suggestion"
192
+ @click="handleSuggestion(suggestion)"
193
+ >
194
+ <span v-if="suggestion.icon" class="rm-ai-chat-suggestion__icon">{{ suggestion.icon }}</span>
195
+ {{ suggestion.label }}
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <!-- Messages -->
201
+ <div
202
+ v-for="message in messages"
203
+ :key="message.id"
204
+ class="rm-ai-chat-message"
205
+ :class="`rm-ai-chat-message--${message.role}`"
206
+ >
207
+ <div class="rm-ai-chat-avatar" :class="`rm-ai-chat-avatar--${message.role}`">
208
+ <RmIcon v-if="message.role === 'assistant'" name="Robot" :size="18" weight="duotone" />
209
+ <RmIcon v-else name="User" :size="18" />
210
+ </div>
211
+ <div class="rm-ai-chat-bubble" :class="`rm-ai-chat-bubble--${message.role}`">
212
+ <div v-html="renderMarkdown(message.content)" />
213
+ <slot name="message-footer" :message="message" />
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Loading indicator -->
218
+ <div v-if="loading" class="rm-ai-chat-message rm-ai-chat-message--assistant">
219
+ <div class="rm-ai-chat-avatar rm-ai-chat-avatar--assistant">
220
+ <RmIcon name="Robot" :size="18" weight="duotone" />
221
+ </div>
222
+ <div class="rm-ai-chat-bubble rm-ai-chat-bubble--assistant">
223
+ <div class="rm-ai-chat-loading">
224
+ <span class="rm-ai-chat-loading__pulse" />
225
+ <RmIcon name="CircleNotch" :size="16" class="rm-ai-chat-loading__icon" />
226
+ <span class="rm-ai-chat-loading__text">{{ loadingText }}</span>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <template #footer>
234
+ <form class="rm-ai-chat-input" @submit.prevent="handleSubmit">
235
+ <div class="rm-ai-chat-input__wrapper">
236
+ <input
237
+ v-model="inputValue"
238
+ type="text"
239
+ :placeholder="placeholder"
240
+ :disabled="loading"
241
+ @keydown.enter.prevent="handleSubmit"
242
+ />
243
+ <button type="submit" :disabled="loading || !inputValue.trim()" class="rm-ai-chat-input__send">
244
+ <RmIcon name="PaperPlaneRight" :size="18" weight="fill" />
245
+ </button>
246
+ </div>
247
+ </form>
248
+ </template>
249
+ </RmDrawer>
250
+ </template>
251
+
252
+ <style scoped>
253
+ .rm-ai-chat-trigger{align-items:center;background:linear-gradient(135deg,var(--r-color-primary-500) 0,var(--r-color-primary-600) 100%);border:none;border-radius:var(--r-radius-full);bottom:24px;box-shadow:var(--r-shadow-lg);color:var(--r-color-white);cursor:pointer;display:flex;height:52px;justify-content:center;position:fixed;right:24px;transition:all var(--r-transition-base);width:52px;z-index:89}.rm-ai-chat-trigger:hover{box-shadow:var(--r-shadow-xl);transform:scale(1.08)}.rm-ai-chat-trigger--open{opacity:.7;transform:scale(.9)}.rm-ai-chat-header{align-items:center;display:flex;gap:10px}.rm-ai-chat-header__icon{font-size:22px}.rm-ai-chat-header__robot{color:var(--r-color-primary-500)}.rm-ai-chat-header__title{color:var(--r-color-gray-900);font-size:var(--r-text-lg);font-weight:var(--r-font-weight-semibold);margin:0}.rm-ai-chat-header__clear{align-items:center;background:none;border:none;border-radius:var(--r-radius-md);color:var(--r-color-gray-400);cursor:pointer;display:flex;height:32px;justify-content:center;margin-right:8px;transition:all var(--r-transition-fast);width:32px}.rm-ai-chat-header__clear:hover{background-color:var(--r-color-gray-100);color:var(--r-color-gray-600)}.rm-ai-chat-body{display:flex;flex-direction:column;height:100%;margin:-24px}.rm-ai-chat-messages{background-color:var(--r-color-gray-50);display:flex;flex:1;flex-direction:column;gap:16px;overflow-y:auto;padding:20px}.rm-ai-chat-welcome{display:flex;flex-direction:column;gap:20px}.rm-ai-chat-bubble--welcome{background:linear-gradient(135deg,#f0f9ff,#e0f2fe)!important;border:1px solid #bae6fd}.rm-ai-chat-suggestions{align-items:center;display:flex;flex-direction:column;gap:8px}.rm-ai-chat-suggestions__hint{color:var(--r-color-gray-400);font-size:var(--r-text-xs);font-weight:var(--r-font-weight-medium);margin:0}.rm-ai-chat-suggestion{align-items:center;background:var(--r-color-white);border:1px solid var(--r-color-gray-200);border-radius:var(--r-radius-lg);color:var(--r-color-gray-600);cursor:pointer;display:flex;font-size:var(--r-text-sm);gap:8px;padding:10px 14px;text-align:left;transition:all var(--r-transition-fast);width:100%}.rm-ai-chat-suggestion:hover{background:var(--r-color-gray-50);border-color:var(--r-color-primary-400);color:var(--r-color-primary-600);transform:translateX(4px)}.rm-ai-chat-suggestion__icon{flex-shrink:0}.rm-ai-chat-message{animation:rm-ai-chat-fade-in .2s ease;display:flex;gap:10px}.rm-ai-chat-message--user{flex-direction:row-reverse}@keyframes rm-ai-chat-fade-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.rm-ai-chat-avatar{align-items:center;border-radius:var(--r-radius-full);display:flex;flex-shrink:0;height:32px;justify-content:center;width:32px}.rm-ai-chat-avatar--assistant{background:var(--r-color-primary-50);color:var(--r-color-primary-500)}.rm-ai-chat-avatar--user{background:linear-gradient(135deg,var(--r-color-primary-500) 0,var(--r-color-primary-600) 100%);color:var(--r-color-white)}.rm-ai-chat-bubble{border-radius:16px;font-size:var(--r-text-sm);line-height:var(--r-leading-relaxed);max-width:80%;padding:10px 14px}.rm-ai-chat-bubble--user{background:linear-gradient(135deg,var(--r-color-primary-500) 0,var(--r-color-primary-600) 100%);border-bottom-right-radius:4px;color:var(--r-color-white)}.rm-ai-chat-bubble--assistant{background:var(--r-color-white);border-bottom-left-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.06);color:var(--r-color-gray-700)}.rm-ai-chat-bubble :deep(ol),.rm-ai-chat-bubble :deep(ul){margin:6px 0;padding-left:20px}.rm-ai-chat-bubble :deep(li){margin:3px 0}.rm-ai-chat-bubble :deep(ol){list-style-type:decimal}.rm-ai-chat-bubble :deep(ul){list-style-type:disc}.rm-ai-chat-bubble :deep(strong){font-weight:var(--r-font-weight-semibold)}.rm-ai-chat-loading{align-items:center;display:flex;gap:8px;padding:2px 0}.rm-ai-chat-loading__pulse{animation:rm-ai-chat-pulse 2.5s ease-in-out infinite;background:rgba(99,102,241,.08);border-radius:var(--r-radius-full);height:28px;position:absolute;width:28px}.rm-ai-chat-loading__icon{animation:rm-ai-chat-spin 1.2s linear infinite;color:var(--r-color-primary-500);z-index:1}.rm-ai-chat-loading__text{color:var(--r-color-gray-500);font-size:var(--r-text-sm);font-weight:var(--r-font-weight-medium)}@keyframes rm-ai-chat-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes rm-ai-chat-pulse{0%,to{opacity:.4;transform:scale(.8)}50%{opacity:0;transform:scale(1.3)}}.rm-ai-chat-input{margin:0}.rm-ai-chat-input__wrapper{align-items:center;background:var(--r-color-gray-100);border-radius:var(--r-radius-lg);display:flex;gap:8px;padding:4px 4px 4px 16px}.rm-ai-chat-input__wrapper input{background:none;border:none;color:var(--r-color-gray-800);flex:1;font-family:inherit;font-size:var(--r-text-sm);outline:none;padding:10px 0}.rm-ai-chat-input__wrapper input::-moz-placeholder{color:var(--r-color-gray-400)}.rm-ai-chat-input__wrapper input::placeholder{color:var(--r-color-gray-400)}.rm-ai-chat-input__send{align-items:center;background:linear-gradient(135deg,var(--r-color-primary-500) 0,var(--r-color-primary-600) 100%);border:none;border-radius:var(--r-radius-md);color:var(--r-color-white);cursor:pointer;display:flex;flex-shrink:0;height:38px;justify-content:center;transition:all var(--r-transition-fast);width:38px}.rm-ai-chat-input__send:hover:not(:disabled){transform:scale(1.05)}.rm-ai-chat-input__send:disabled{cursor:not-allowed;opacity:.5}
254
+ </style>
@@ -0,0 +1,68 @@
1
+ <!--
2
+ RmAppShell - le chrome d'application du design system : barre laterale fixe
3
+ a gauche, strip d'actions fixe a droite, contenu decale entre les deux.
4
+
5
+ Structure et classes reprises de `app-shell.html`. La regle n°0 du skill est
6
+ de ne jamais reconstruire ce chrome de memoire : ce composant ne fait que
7
+ poser le markup attendu et gerer l'etat de repli.
8
+
9
+ Le decalage vit sur `body.rm-shell`, pose au montage et retire au demontage.
10
+ Sans ce retrait, une app multi-layout garderait le decalage sur ses autres
11
+ ecrans apres avoir quitte celui-ci.
12
+ -->
13
+ <script setup lang="ts">
14
+ import { onBeforeUnmount, onMounted, ref } from 'vue'
15
+ import { rmWithoutTransitions } from '../utils/transitionGuard'
16
+
17
+ const props = withDefaults(defineProps<{
18
+ /** Barre laterale repliee au chargement. */
19
+ navCollapsed?: boolean
20
+ }>(), { navCollapsed: false })
21
+
22
+ const collapsed = ref(props.navCollapsed)
23
+ const stripOpen = ref(false)
24
+
25
+ function sync() {
26
+ if (typeof document === 'undefined') return
27
+ const { body } = document
28
+ body.classList.add('rm-shell')
29
+ body.classList.toggle('lcollapsed', collapsed.value)
30
+ body.classList.toggle('ropen', stripOpen.value)
31
+ }
32
+
33
+ onMounted(sync)
34
+
35
+ onBeforeUnmount(() => {
36
+ if (typeof document === 'undefined') return
37
+ document.body.classList.remove('rm-shell', 'lcollapsed', 'ropen')
38
+ })
39
+
40
+ // Sans la garde, replier la barre laterale ne fait rien : sa largeur vient
41
+ // d'un `var()` et la propriete est sous transition (cf. utils/transitionGuard).
42
+ function toggleNav() {
43
+ rmWithoutTransitions(() => {
44
+ collapsed.value = !collapsed.value
45
+ sync()
46
+ })
47
+ }
48
+ function setStrip(open: boolean) {
49
+ rmWithoutTransitions(() => {
50
+ stripOpen.value = open
51
+ sync()
52
+ })
53
+ }
54
+
55
+ defineExpose({ collapsed, stripOpen, toggleNav, setStrip })
56
+ </script>
57
+
58
+ <template>
59
+ <div class="rm-app-shell">
60
+ <slot name="nav" :collapsed="collapsed" :toggle="toggleNav" />
61
+ <slot name="strip" :open="stripOpen" :set-open="setStrip" />
62
+ <slot />
63
+ </div>
64
+ </template>
65
+
66
+ <style scoped>
67
+ .rm-app-shell{display:contents}
68
+ </style>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * HybridTable — a table with inline horizontal bars behind values.
4
+ * Each row shows a label, a value, and a bar proportional to the max value.
5
+ */
6
+
7
+ export interface HybridTableRow {
8
+ label: string
9
+ value: number
10
+ }
11
+
12
+ const props = withDefaults(defineProps<{
13
+ rows: HybridTableRow[]
14
+ title?: string
15
+ color?: string
16
+ maxVisible?: number
17
+ valueFormatter?: (val: number) => string
18
+ }>(), {
19
+ title: undefined,
20
+ color: '#4164EE',
21
+ maxVisible: 10,
22
+ valueFormatter: (val: number) => val.toLocaleString(),
23
+ })
24
+
25
+ const showAll = ref(false)
26
+
27
+ const maxValue = computed(() => Math.max(...props.rows.map(r => r.value), 1))
28
+
29
+ const visibleRows = computed(() => {
30
+ if (showAll.value) return props.rows
31
+ return props.rows.slice(0, props.maxVisible)
32
+ })
33
+
34
+ const hasMore = computed(() => props.rows.length > props.maxVisible)
35
+ </script>
36
+
37
+ <template>
38
+ <div class="hybrid-table">
39
+ <h3 v-if="title" class="hybrid-table__title">{{ title }}</h3>
40
+
41
+ <div class="hybrid-table__list">
42
+ <div
43
+ v-for="(row, idx) in visibleRows"
44
+ :key="idx"
45
+ class="hybrid-table__row"
46
+ >
47
+ <span class="hybrid-table__label">{{ row.label }}</span>
48
+ <div class="hybrid-table__bar-wrapper">
49
+ <div
50
+ class="hybrid-table__bar"
51
+ :style="{ width: `${(row.value / maxValue) * 100}%`, backgroundColor: color }"
52
+ />
53
+ </div>
54
+ <span class="hybrid-table__value">{{ valueFormatter(row.value) }}</span>
55
+ </div>
56
+ </div>
57
+
58
+ <button
59
+ v-if="hasMore"
60
+ class="hybrid-table__toggle"
61
+ @click="showAll = !showAll"
62
+ >
63
+ {{ showAll ? 'Show less' : `Show all (${rows.length})` }}
64
+ </button>
65
+ </div>
66
+ </template>
67
+
68
+ <style scoped>
69
+ .hybrid-table{font-family:var(--r-font-family)}.hybrid-table__title{color:var(--r-color-gray-700);font-size:14px;font-weight:600;margin:0 0 12px}.hybrid-table__list{display:flex;flex-direction:column;gap:6px}.hybrid-table__row{align-items:center;display:flex;gap:12px;padding:6px 0}.hybrid-table__label{color:var(--r-color-gray-700);flex-shrink:0;font-size:13px;min-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hybrid-table__bar-wrapper{background:var(--r-color-gray-50);border-radius:4px;flex:1;height:20px;overflow:hidden}.hybrid-table__bar{border-radius:4px;height:100%;min-width:2px;transition:width .4s ease}.hybrid-table__value{color:var(--r-color-gray-900);flex-shrink:0;font-size:13px;font-weight:600;min-width:50px;text-align:right}.hybrid-table__toggle{background:none;border:none;color:#4164ee;cursor:pointer;font-family:var(--r-font-family);font-size:13px;margin-top:8px;padding:0;text-decoration:underline}.hybrid-table__toggle:hover{color:#3451d4}
70
+ </style>
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import { PhX } from '@phosphor-icons/vue'
3
+
4
+ export interface RmModalProps {
5
+ /** Whether the modal is open */
6
+ modelValue: boolean
7
+ /** Size: sm, md, lg, xl */
8
+ size?: 'sm' | 'md' | 'lg' | 'xl'
9
+ /** Mode: fixed (size is fixed) or free (adapts to content) */
10
+ mode?: 'fixed' | 'free'
11
+ }
12
+
13
+ const props = withDefaults(defineProps<RmModalProps>(), {
14
+ size: 'md',
15
+ mode: 'fixed',
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ 'update:modelValue': [value: boolean]
20
+ }>()
21
+
22
+ function close() {
23
+ emit('update:modelValue', false)
24
+ }
25
+
26
+ const sizeMap: Record<string, { width: string; height: string }> = {
27
+ sm: { width: '480px', height: '360px' },
28
+ md: { width: '640px', height: '500px' },
29
+ lg: { width: '800px', height: '640px' },
30
+ xl: { width: '960px', height: '780px' },
31
+ }
32
+
33
+ const modalStyle = computed(() => {
34
+ const s = sizeMap[props.size] ?? sizeMap.md
35
+ if (props.mode === 'fixed') {
36
+ return { width: s.width, height: s.height }
37
+ }
38
+ return { width: s.width, maxHeight: '85vh' }
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <Teleport to="body">
44
+ <Transition name="rm-modal">
45
+ <div
46
+ v-if="modelValue"
47
+ class="rm-modal-overlay"
48
+ @click.self="close"
49
+ >
50
+ <div class="rm-modal" :style="modalStyle">
51
+ <div class="rm-modal__header">
52
+ <div class="rm-modal__header-content">
53
+ <slot name="header" />
54
+ </div>
55
+ <button class="rm-modal__close" @click="close">
56
+ <PhX :size="20" />
57
+ </button>
58
+ </div>
59
+
60
+ <div v-if="$slots.tabs" class="rm-modal__tabs">
61
+ <slot name="tabs" />
62
+ </div>
63
+
64
+ <div class="rm-modal__body">
65
+ <slot />
66
+ </div>
67
+
68
+ <div v-if="$slots.footer" class="rm-modal__footer">
69
+ <slot name="footer" />
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </Transition>
74
+ </Teleport>
75
+ </template>
76
+
77
+ <style scoped>
78
+ .rm-modal-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(0,0,0,.4);display:flex;inset:0;justify-content:center;position:fixed;z-index:100}.rm-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.15);display:flex;flex-direction:column;max-height:90vh;max-width:95vw}.rm-modal__header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between;padding:20px 24px 0}.rm-modal__header-content{flex:1;min-width:0}.rm-modal__close{align-items:center;background:none;border:none;border-radius:var(--r-radius-md);color:var(--r-color-gray-400);cursor:pointer;display:flex;flex-shrink:0;height:32px;justify-content:center;transition:all var(--r-transition-fast);width:32px}.rm-modal__close:hover{background:var(--r-color-gray-50);color:var(--r-color-gray-700)}.rm-modal__tabs{padding:0}.rm-modal__body{flex:1;overflow-y:auto;padding:24px}.rm-modal__footer{border-top:1px solid var(--r-color-gray-200);padding:16px 24px}.rm-modal-enter-active,.rm-modal-leave-active{transition:opacity .2s ease}.rm-modal-enter-active .rm-modal,.rm-modal-leave-active .rm-modal{transition:transform .2s ease}.rm-modal-enter-from,.rm-modal-leave-to{opacity:0}.rm-modal-enter-from .rm-modal,.rm-modal-leave-to .rm-modal{transform:scale(.95)}
79
+ </style>
@@ -0,0 +1,147 @@
1
+ <!--
2
+ RmNavBar - la barre laterale du design system : identite, recherche, sections
3
+ de navigation, pied avec utilisateur et sortie.
4
+
5
+ Markup et classes reprises de `app-shell.html` (`.sidebar`, `.sidebar-top`,
6
+ `.sidebar-search`, `.nav-group`, `.nav-section`, `.nav-item`,
7
+ `.sidebar-footer`). Les icones utilisent la police Phosphor via
8
+ `<i class="ph ph-...">`, comme le design system : les regles de taille
9
+ d'oip.css ciblent cet element, un SVG ne les recevrait pas.
10
+
11
+ Les sections sont un accordeon exclusif : huit vues ne tiennent pas
12
+ depliees. L'etat vit ici, pas dans chaque section.
13
+ -->
14
+ <script setup lang="ts">
15
+ import { computed, ref, watch } from 'vue'
16
+
17
+ export interface RmNavItem {
18
+ label: string
19
+ /** Icone Phosphor, nom en kebab-case sans le prefixe `ph-`. */
20
+ icon: string
21
+ to: string
22
+ }
23
+ export interface RmNavSection {
24
+ key: string
25
+ label: string
26
+ items: RmNavItem[]
27
+ }
28
+
29
+ const props = withDefaults(defineProps<{
30
+ /** Nom court affiche a cote du logo. */
31
+ name: string
32
+ /** Sous-titre du logo. */
33
+ subtitle?: string
34
+ sections: RmNavSection[]
35
+ /** Chemin actif, pour marquer l'item courant. */
36
+ activePath?: string
37
+ collapsed?: boolean
38
+ searchPlaceholder?: string
39
+ /** Bloc utilisateur du pied. Masque si absent. */
40
+ user?: { initials: string, name: string, org?: string }
41
+ /** Lien de sortie du pied. */
42
+ exitLabel?: string
43
+ exitHref?: string
44
+ }>(), {
45
+ subtitle: undefined,
46
+ activePath: undefined,
47
+ collapsed: false,
48
+ searchPlaceholder: 'Rechercher...',
49
+ user: undefined,
50
+ exitLabel: undefined,
51
+ exitHref: undefined,
52
+ })
53
+
54
+ const emit = defineEmits<{
55
+ toggle: []
56
+ navigate: [to: string]
57
+ search: [term: string]
58
+ }>()
59
+
60
+ // Section ouverte : celle qui contient le chemin actif, sinon la premiere.
61
+ const sectionOf = (path?: string) =>
62
+ props.sections.find(s => s.items.some(i => i.to === path))?.key ?? props.sections[0]?.key ?? null
63
+
64
+ const openSection = ref<string | null>(sectionOf(props.activePath))
65
+ watch(() => props.activePath, (path) => {
66
+ const key = sectionOf(path)
67
+ if (key && key !== openSection.value) openSection.value = key
68
+ })
69
+
70
+ const term = ref('')
71
+ watch(term, v => emit('search', v))
72
+
73
+ const initials = computed(() => props.user?.initials ?? '')
74
+ </script>
75
+
76
+ <template>
77
+ <aside class="sidebar">
78
+ <div class="sidebar-top">
79
+ <div class="logo-block">
80
+ <div class="logo-mark">{{ name.charAt(0) }}</div>
81
+ <div>
82
+ <div class="logo-text">{{ name }}</div>
83
+ <div v-if="subtitle" class="logo-sub">{{ subtitle }}</div>
84
+ </div>
85
+ </div>
86
+ <button
87
+ class="topbar-toggle"
88
+ type="button"
89
+ :aria-label="collapsed ? 'Déplier la barre latérale' : 'Replier la barre latérale'"
90
+ :aria-expanded="!collapsed"
91
+ @click="emit('toggle')"
92
+ >
93
+ <i class="ph ph-sidebar-simple" />
94
+ </button>
95
+ </div>
96
+
97
+ <div class="sidebar-search">
98
+ <i class="ph ph-magnifying-glass" />
99
+ <input v-model="term" type="text" :placeholder="searchPlaceholder" aria-label="Rechercher dans la navigation">
100
+ <kbd>⌘K</kbd>
101
+ </div>
102
+
103
+ <div v-for="section in sections" :key="section.key" class="nav-group">
104
+ <div class="nav-section">
105
+ <button
106
+ class="nav-section-header"
107
+ type="button"
108
+ :aria-expanded="openSection === section.key"
109
+ @click="openSection = openSection === section.key ? null : section.key"
110
+ >
111
+ <span class="label">{{ section.label }}</span>
112
+ <span class="chev"><i class="ph ph-caret-down" /></span>
113
+ </button>
114
+ <div v-show="openSection === section.key" class="nav-items">
115
+ <a
116
+ v-for="item in section.items"
117
+ :key="item.to"
118
+ class="nav-item"
119
+ :class="{ active: item.to === activePath }"
120
+ :href="item.to"
121
+ :data-tip="collapsed ? item.label : undefined"
122
+ @click.prevent="emit('navigate', item.to)"
123
+ >
124
+ <span class="icn"><i :class="`ph ph-${item.icon}`" /></span>
125
+ <span class="lbl">{{ item.label }}</span>
126
+ </a>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="sidebar-footer">
132
+ <div v-if="user" class="sb-user">
133
+ <div class="sb-user-avatar">{{ initials }}</div>
134
+ <div class="sb-user-info">
135
+ <div class="sb-user-name">{{ user.name }}</div>
136
+ <div v-if="user.org" class="sb-user-org">{{ user.org }}</div>
137
+ </div>
138
+ </div>
139
+ <div class="sb-actions">
140
+ <slot name="footer-actions" />
141
+ <a v-if="exitHref" class="primary" :href="exitHref">
142
+ <i class="ph ph-arrow-up-right" /> {{ exitLabel ?? 'Sortir' }}
143
+ </a>
144
+ </div>
145
+ </div>
146
+ </aside>
147
+ </template>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SimpleBar — lightweight horizontal bar chart without ApexCharts.
4
+ * Pure CSS, no dependencies. Great for small inline visualizations.
5
+ */
6
+
7
+ export interface SimpleBarItem {
8
+ label: string
9
+ value: number
10
+ color?: string
11
+ }
12
+
13
+ const props = withDefaults(defineProps<{
14
+ items: SimpleBarItem[]
15
+ title?: string
16
+ showValues?: boolean
17
+ showPercent?: boolean
18
+ height?: number
19
+ }>(), {
20
+ title: undefined,
21
+ showValues: true,
22
+ showPercent: false,
23
+ height: 24,
24
+ })
25
+
26
+ const { PALETTE } = useChartTheme()
27
+
28
+ const total = computed(() => props.items.reduce((sum, i) => sum + i.value, 0))
29
+ const maxValue = computed(() => Math.max(...props.items.map(i => i.value), 1))
30
+ </script>
31
+
32
+ <template>
33
+ <div class="simple-bar">
34
+ <h3 v-if="title" class="simple-bar__title">{{ title }}</h3>
35
+
36
+ <!-- Stacked bar -->
37
+ <div v-if="showPercent" class="simple-bar__stacked" :style="{ height: `${height}px` }">
38
+ <div
39
+ v-for="(item, idx) in items"
40
+ :key="idx"
41
+ class="simple-bar__segment"
42
+ :style="{
43
+ width: `${(item.value / total) * 100}%`,
44
+ backgroundColor: item.color ?? PALETTE[idx % PALETTE.length],
45
+ }"
46
+ />
47
+ </div>
48
+
49
+ <!-- Individual bars -->
50
+ <div class="simple-bar__list">
51
+ <div
52
+ v-for="(item, idx) in items"
53
+ :key="idx"
54
+ class="simple-bar__item"
55
+ >
56
+ <div class="simple-bar__item-header">
57
+ <span
58
+ class="simple-bar__dot"
59
+ :style="{ backgroundColor: item.color ?? PALETTE[idx % PALETTE.length] }"
60
+ />
61
+ <span class="simple-bar__label">{{ item.label }}</span>
62
+ <span v-if="showValues" class="simple-bar__value">
63
+ {{ item.value.toLocaleString() }}
64
+ <span v-if="showPercent" class="simple-bar__pct">
65
+ ({{ total > 0 ? ((item.value / total) * 100).toFixed(1) : 0 }}%)
66
+ </span>
67
+ </span>
68
+ </div>
69
+ <div v-if="!showPercent" class="simple-bar__track" :style="{ height: `${height}px` }">
70
+ <div
71
+ class="simple-bar__fill"
72
+ :style="{
73
+ width: `${(item.value / maxValue) * 100}%`,
74
+ backgroundColor: item.color ?? PALETTE[idx % PALETTE.length],
75
+ }"
76
+ />
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </template>
82
+
83
+ <style scoped>
84
+ .simple-bar{font-family:var(--r-font-family)}.simple-bar__title{color:var(--r-color-gray-700);font-size:14px;font-weight:600;margin:0 0 12px}.simple-bar__stacked{border-radius:6px;display:flex;margin-bottom:12px;overflow:hidden}.simple-bar__segment{min-width:2px;transition:width .4s ease}.simple-bar__segment:first-child{border-radius:6px 0 0 6px}.simple-bar__segment:last-child{border-radius:0 6px 6px 0}.simple-bar__list{display:flex;flex-direction:column;gap:8px}.simple-bar__item-header{align-items:center;display:flex;gap:8px;margin-bottom:4px}.simple-bar__dot{border-radius:50%;flex-shrink:0;height:8px;width:8px}.simple-bar__label{color:var(--r-color-gray-700);flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.simple-bar__value{color:var(--r-color-gray-900);flex-shrink:0;font-size:13px;font-weight:600}.simple-bar__pct{color:var(--r-color-gray-400);font-weight:400}.simple-bar__track{background:var(--r-color-gray-50);border-radius:4px;overflow:hidden}.simple-bar__fill{border-radius:4px;height:100%;min-width:2px;transition:width .4s ease}
85
+ </style>
@@ -0,0 +1,161 @@
1
+ <script setup lang="ts">
2
+ import { PhCaretUp, PhCaretDown } from '@phosphor-icons/vue'
3
+
4
+ export interface RmTableColumn {
5
+ key: string
6
+ label: string
7
+ sortable?: boolean
8
+ align?: 'left' | 'center' | 'right'
9
+ width?: string
10
+ }
11
+
12
+ export interface RmTableProps {
13
+ columns: RmTableColumn[]
14
+ rows: Record<string, any>[]
15
+ sortField?: string
16
+ sortOrder?: 'asc' | 'desc'
17
+ striped?: boolean
18
+ hoverable?: boolean
19
+ stickyHeader?: boolean
20
+ maxHeight?: string
21
+ loading?: boolean
22
+ emptyText?: string
23
+ }
24
+
25
+ const props = withDefaults(defineProps<RmTableProps>(), {
26
+ sortField: '',
27
+ sortOrder: 'asc',
28
+ striped: true,
29
+ hoverable: true,
30
+ stickyHeader: true,
31
+ maxHeight: '500px',
32
+ loading: false,
33
+ emptyText: 'No data',
34
+ })
35
+
36
+ const emit = defineEmits<{
37
+ sort: [field: string, order: 'asc' | 'desc']
38
+ rowClick: [row: Record<string, any>, index: number]
39
+ }>()
40
+
41
+ const currentSort = ref(props.sortField)
42
+ const currentOrder = ref<'asc' | 'desc'>(props.sortOrder)
43
+
44
+ function toggleSort(col: RmTableColumn) {
45
+ if (!col.sortable) return
46
+
47
+ if (currentSort.value === col.key) {
48
+ currentOrder.value = currentOrder.value === 'asc' ? 'desc' : 'asc'
49
+ } else {
50
+ currentSort.value = col.key
51
+ currentOrder.value = 'asc'
52
+ }
53
+
54
+ emit('sort', currentSort.value, currentOrder.value)
55
+ }
56
+
57
+ function getCellValue(row: Record<string, any>, key: string): any {
58
+ return key.split('.').reduce((obj, k) => obj?.[k], row)
59
+ }
60
+
61
+ const sortedRows = computed(() => {
62
+ if (!currentSort.value) return props.rows
63
+
64
+ return [...props.rows].sort((a, b) => {
65
+ const aVal = getCellValue(a, currentSort.value) ?? ''
66
+ const bVal = getCellValue(b, currentSort.value) ?? ''
67
+
68
+ const aStr = Array.isArray(aVal) ? aVal.join(', ') : String(aVal)
69
+ const bStr = Array.isArray(bVal) ? bVal.join(', ') : String(bVal)
70
+
71
+ const aNum = Number(aStr)
72
+ const bNum = Number(bStr)
73
+ const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aStr !== '' && bStr !== ''
74
+
75
+ const cmp = isNumeric ? aNum - bNum : aStr.localeCompare(bStr)
76
+ return currentOrder.value === 'asc' ? cmp : -cmp
77
+ })
78
+ })
79
+ </script>
80
+
81
+ <template>
82
+ <div class="rm-table-wrapper" :style="maxHeight !== 'auto' && stickyHeader ? { maxHeight } : undefined">
83
+ <table class="rm-table">
84
+ <thead class="rm-table__head" :class="{ 'rm-table__head--sticky': stickyHeader }">
85
+ <tr>
86
+ <th
87
+ v-for="col in columns"
88
+ :key="col.key"
89
+ class="rm-table__th"
90
+ :class="[
91
+ col.align && `rm-table__th--${col.align}`,
92
+ col.sortable && 'rm-table__th--sortable',
93
+ ]"
94
+ :style="col.width ? { width: col.width } : undefined"
95
+ @click="toggleSort(col)"
96
+ >
97
+ <span class="rm-table__th-content">
98
+ {{ col.label }}
99
+ <span v-if="col.sortable" class="rm-table__sort">
100
+ <PhCaretUp
101
+ :size="10"
102
+ :weight="currentSort === col.key && currentOrder === 'asc' ? 'bold' : 'regular'"
103
+ :class="{ 'rm-table__sort--active': currentSort === col.key && currentOrder === 'asc' }"
104
+ />
105
+ <PhCaretDown
106
+ :size="10"
107
+ :weight="currentSort === col.key && currentOrder === 'desc' ? 'bold' : 'regular'"
108
+ :class="{ 'rm-table__sort--active': currentSort === col.key && currentOrder === 'desc' }"
109
+ />
110
+ </span>
111
+ </span>
112
+ </th>
113
+ </tr>
114
+ </thead>
115
+
116
+ <tbody v-if="loading">
117
+ <tr v-for="i in 5" :key="i">
118
+ <td v-for="col in columns" :key="col.key" class="rm-table__td">
119
+ <div class="rm-table__skeleton" />
120
+ </td>
121
+ </tr>
122
+ </tbody>
123
+
124
+ <tbody v-else-if="sortedRows.length === 0">
125
+ <tr>
126
+ <td :colspan="columns.length" class="rm-table__empty">
127
+ {{ emptyText }}
128
+ </td>
129
+ </tr>
130
+ </tbody>
131
+
132
+ <tbody v-else>
133
+ <tr
134
+ v-for="(row, idx) in sortedRows"
135
+ :key="idx"
136
+ class="rm-table__row"
137
+ :class="{
138
+ 'rm-table__row--striped': striped && idx % 2 === 1,
139
+ 'rm-table__row--hoverable': hoverable,
140
+ }"
141
+ @click="emit('rowClick', row, idx)"
142
+ >
143
+ <td
144
+ v-for="col in columns"
145
+ :key="col.key"
146
+ class="rm-table__td"
147
+ :class="col.align && `rm-table__td--${col.align}`"
148
+ >
149
+ <slot :name="col.key" :value="getCellValue(row, col.key)" :row="row" :index="idx">
150
+ {{ getCellValue(row, col.key) ?? '—' }}
151
+ </slot>
152
+ </td>
153
+ </tr>
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ </template>
158
+
159
+ <style scoped>
160
+ .rm-table-wrapper{background:#fff;border:1px solid #dbdef0;border-radius:8px;flex:1;overflow:auto}.rm-table{border-collapse:collapse;font-family:var(--r-font-family);font-size:var(--r-text-sm);width:100%}.rm-table__head{background:var(--r-color-gray-50)}.rm-table__head--sticky{position:sticky;top:0;z-index:2}.rm-table__th{border-bottom:1px solid #dbdef0;color:var(--r-color-gray-500);font-size:12px;font-weight:600;letter-spacing:.3px;padding:10px 16px;text-align:left;text-transform:uppercase;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-table__th--center{text-align:center}.rm-table__th--right{text-align:right}.rm-table__th--sortable{cursor:pointer}.rm-table__th--sortable:hover{color:var(--r-color-gray-700)}.rm-table__th-content{align-items:center;display:inline-flex;gap:4px}.rm-table__sort{color:var(--r-color-gray-300);display:inline-flex;flex-direction:column;gap:-2px}.rm-table__sort--active{color:#4164ee}.rm-table__td{border-bottom:1px solid var(--r-color-gray-100);color:var(--r-color-gray-700);padding:10px 16px;vertical-align:top}.rm-table__td--center{text-align:center}.rm-table__td--right{text-align:right}.rm-table__row:last-child .rm-table__td{border-bottom:none}.rm-table__row--striped{background-color:var(--r-color-gray-50)}.rm-table__row--hoverable:hover{background-color:rgba(65,100,238,.06);cursor:pointer}.rm-table__empty{color:var(--r-color-gray-400);font-style:italic;padding:32px 16px;text-align:center}.rm-table__skeleton{animation:rm-table-shimmer 1.5s infinite;background:linear-gradient(90deg,#eee 25%,#e0e0e0 50%,#eee 75%);background-size:200% 100%;border-radius:4px;height:14px;width:80%}@keyframes rm-table-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}
161
+ </style>
@@ -0,0 +1,73 @@
1
+ <!--
2
+ RmToolStrip - le strip d'actions fixe a droite. C'est une barre a part
3
+ entiere, pas un panneau replie dans le contenu : tuiles verticales toujours
4
+ visibles, panneau qui se superpose au clic.
5
+
6
+ Markup et classes de `app-shell.html` (`.rsidebar`, `.rstrip-top`,
7
+ `.rstrip-collapsed`, `.strip-icon`, `.strip-label`, `.strip-badge`).
8
+ Le skill recommande de conserver les tuiles Sources et Acces par defaut.
9
+ -->
10
+ <script setup lang="ts">
11
+ export interface RmStripTile {
12
+ key: string
13
+ label: string
14
+ /** Icone Phosphor, nom en kebab-case sans le prefixe `ph-`. */
15
+ icon: string
16
+ /** Compteur affiche en pastille. Un zero n'est pas affiche. */
17
+ badge?: number | string
18
+ /** Point d'attention, sans compteur. */
19
+ dot?: boolean
20
+ }
21
+
22
+ const props = withDefaults(defineProps<{
23
+ tiles: RmStripTile[]
24
+ /** Tuile dont le panneau est ouvert. */
25
+ active?: string | null
26
+ open?: boolean
27
+ }>(), { active: null, open: false })
28
+
29
+ const emit = defineEmits<{
30
+ select: [key: string]
31
+ toggle: []
32
+ }>()
33
+ </script>
34
+
35
+ <template>
36
+ <aside class="rsidebar">
37
+ <div class="rstrip-top">
38
+ <button
39
+ class="topbar-toggle"
40
+ type="button"
41
+ aria-label="Panneau d'actions"
42
+ :aria-expanded="open"
43
+ @click="emit('toggle')"
44
+ >
45
+ <i class="ph ph-sidebar-simple" />
46
+ </button>
47
+ </div>
48
+
49
+ <div class="rstrip-collapsed">
50
+ <button
51
+ v-for="tile in tiles"
52
+ :key="tile.key"
53
+ class="strip-icon"
54
+ :class="{ active: active === tile.key }"
55
+ type="button"
56
+ :data-open-section="tile.key"
57
+ :aria-pressed="active === tile.key"
58
+ @click="emit('select', tile.key)"
59
+ >
60
+ <i :class="`ph ph-${tile.icon}`" />
61
+ <span class="strip-label">{{ tile.label }}</span>
62
+ <span v-if="tile.badge" class="strip-badge">{{ tile.badge }}</span>
63
+ <span v-else-if="tile.dot" class="dot" />
64
+ </button>
65
+ </div>
66
+
67
+ <slot />
68
+ </aside>
69
+ </template>
70
+
71
+ <style scoped>
72
+ .strip-icon,.topbar-toggle{font:inherit;text-align:center}
73
+ </style>
@@ -0,0 +1,46 @@
1
+ <!--
2
+ RmTopBar - l'en-tete d'analyse : titre de page et onglets de sous-vues.
3
+
4
+ Classes de `app-shell.html` (`.topbar`, `.pane-title`, `.ptabs`, `.ptab`).
5
+ Regle du skill : la sidebar porte les grands chapitres, ces onglets portent
6
+ les sous-analyses du chapitre actif. Ne pas melanger les deux.
7
+ -->
8
+ <script setup lang="ts">
9
+ export interface RmPaneTab {
10
+ key: string
11
+ label: string
12
+ }
13
+
14
+ withDefaults(defineProps<{
15
+ title: string
16
+ tabs?: RmPaneTab[]
17
+ active?: string | null
18
+ }>(), { tabs: () => [], active: null })
19
+
20
+ const emit = defineEmits<{ select: [key: string] }>()
21
+ </script>
22
+
23
+ <template>
24
+ <div class="topbar">
25
+ <h1 class="pane-title">{{ title }}</h1>
26
+ <div v-if="tabs.length" class="ptabs" role="tablist">
27
+ <button
28
+ v-for="tab in tabs"
29
+ :key="tab.key"
30
+ class="ptab"
31
+ :class="{ active: active === tab.key }"
32
+ type="button"
33
+ role="tab"
34
+ :aria-selected="active === tab.key"
35
+ @click="emit('select', tab.key)"
36
+ >
37
+ {{ tab.label }}
38
+ </button>
39
+ </div>
40
+ <slot />
41
+ </div>
42
+ </template>
43
+
44
+ <style scoped>
45
+ .ptab{font:inherit}
46
+ </style>
@@ -1,4 +1,5 @@
1
1
  import { onMounted, readonly, ref } from "vue";
2
+ import { rmWithoutTransitions } from "../utils/transitionGuard.js";
2
3
  const CVD_CLASSES = {
3
4
  deutera: "cvd-deutera",
4
5
  protan: "cvd-protan",
@@ -16,15 +17,7 @@ export function useRomulusTheme() {
16
17
  const compact = ref(false);
17
18
  const cvd = ref("none");
18
19
  const textScale = ref("normal");
19
- function apply(mutate) {
20
- if (typeof document === "undefined") return;
21
- const { body } = document;
22
- body.classList.add("theme-switching");
23
- mutate();
24
- requestAnimationFrame(() => {
25
- requestAnimationFrame(() => body.classList.remove("theme-switching"));
26
- });
27
- }
20
+ const apply = rmWithoutTransitions;
28
21
  const setNight = (value) => apply(() => {
29
22
  night.value = value;
30
23
  document.body.classList.toggle("night", value);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Applique un changement d'etat sans transition.
3
+ *
4
+ * Chrome ne reevalue pas une propriete sous `transition` quand sa valeur vient
5
+ * d'un `var()` modifie par un changement de classe sur un ancetre. La
6
+ * propriete garde son ancienne valeur indefiniment, pas seulement le temps de
7
+ * l'animation. Deux cas verifies dans ce paquet :
8
+ *
9
+ * - `border-color: var(--border-default)` sur `.chart-card` : 2,5 s apres le
10
+ * passage en mode nuit, la bordure affichait encore la couleur du jour ;
11
+ * - `width: var(--sidebar-w)` sur `.sidebar` : le token passait bien a 333px
12
+ * et la largeur calculee restait a 220px. Replier la barre laterale ne
13
+ * faisait donc rien du tout.
14
+ *
15
+ * On neutralise les transitions, on applique le changement, on force un
16
+ * recalcul de style synchrone, puis on les reactive. Tout est synchrone
17
+ * volontairement : une version a base de `requestAnimationFrame` ne fonctionne
18
+ * pas dans un onglet en arriere-plan, ou aucune frame n'est produite. Teste
19
+ * dans un panneau masque : la bascule n'avait alors aucun effet.
20
+ *
21
+ * Consequence assumee : la bascule est instantanee, sans animation. Une
22
+ * transition qui n'aboutit pas vaut moins qu'un etat correct.
23
+ */
24
+ export declare function rmWithoutTransitions(mutate: () => void): void;
@@ -0,0 +1,14 @@
1
+ export function rmWithoutTransitions(mutate) {
2
+ if (typeof document === "undefined") {
3
+ mutate();
4
+ return;
5
+ }
6
+ const { body } = document;
7
+ body.classList.add("rm-switching");
8
+ try {
9
+ mutate();
10
+ void body.offsetWidth;
11
+ } finally {
12
+ body.classList.remove("rm-switching");
13
+ }
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",