@tekkare/romulus 0.1.12 → 0.1.14
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/module.mjs +5 -0
- package/dist/runtime/assets/styles/chrome.css +1 -0
- package/dist/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/AiChat.vue +254 -0
- package/dist/runtime/components/AppShell.vue +68 -0
- package/dist/runtime/components/HybridTable.vue +70 -0
- package/dist/runtime/components/Modal.vue +79 -0
- package/dist/runtime/components/NavBar.vue +147 -0
- package/dist/runtime/components/SimpleBar.vue +85 -0
- package/dist/runtime/components/Table.vue +161 -0
- package/dist/runtime/components/ToolStrip.vue +73 -0
- package/dist/runtime/components/TopBar.vue +46 -0
- package/dist/runtime/composables/useTheme.js +2 -9
- package/dist/runtime/utils/transitionGuard.d.ts +24 -0
- package/dist/runtime/utils/transitionGuard.js +14 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
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;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{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}}.nav-item,.nav-section-header,.ptab,.sb-actions a,.strip-icon,.topbar-toggle{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;box-shadow:none;color:inherit;font:inherit;margin:0;text-align:inherit;text-decoration:none}.topbar-toggle{background:var(--bg-surface);border:1px solid var(--border-default)}.nav-section-header{align-items:center;cursor:pointer;display:flex;justify-content:space-between;width:100%}.strip-label{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -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, '&')
|
|
113
|
+
.replace(/</g, '<')
|
|
114
|
+
.replace(/>/g, '>')
|
|
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
|
-
|
|
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
|
+
}
|