@tekkare/romulus 0.1.39 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/components/Button.vue +13 -4
- package/dist/runtime/components/ChartCard.vue +8 -0
- package/dist/runtime/components/ExportPanel.vue +1 -1
- package/dist/runtime/components/FilterDropdown.vue +56 -5
- package/dist/runtime/components/HybridTable.vue +111 -46
- package/dist/runtime/components/Kpi.vue +3 -0
- package/dist/runtime/components/LineChart.vue +1 -1
- package/dist/runtime/components/NumberInput.vue +93 -0
- package/dist/runtime/components/PageHeader.vue +13 -4
- package/dist/runtime/components/SimpleBar.vue +48 -57
- package/dist/runtime/components/Table.vue +1 -1
- package/dist/runtime/utils/chart.d.ts +43 -18
- package/dist/runtime/utils/chart.js +28 -12
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -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-body>.rm-empty{box-sizing:border-box;gap:6px;justify-content:center;max-width:none;min-height:240px;padding:36px 24px}.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}.tk-skeleton{height:14px;width:80%}.tk-empty{color:var(--text-muted);text-align:center}.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}.rm-skeleton{animation:rm-skeleton-shimmer 1.5s ease-in-out infinite;background:linear-gradient(90deg,var(--bg-hover) 0,var(--bg-surface-2) 50%,var(--bg-hover) 100%);background-size:200% 100%;border-radius:var(--radius-md)}@keyframes rm-skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (prefers-reduced-motion:reduce){.rm-skeleton{animation:none;background:var(--bg-hover)}}.rm-state-loading{display:flex;flex-direction:column;gap:10px;justify-content:center;min-height:240px;padding:8px 4px}.rm-spinner{animation:rm-spinner-turn .8s linear infinite;border:3px solid var(--border-default);border-radius:50%;border-top-color:var(--color-secondary);height:32px;width:32px}@keyframes rm-spinner-turn{to{transform:rotate(1turn)}}@media (prefers-reduced-motion:reduce){.rm-spinner{animation-duration:3s}}.rm-state{color:var(--text-secondary);flex-direction:column;font-family:var(--font-sans);gap:6px;min-height:240px;padding:36px 24px;text-align:center}.rm-state,.rm-state__icon{align-items:center;display:flex;justify-content:center}.rm-state__icon{background:var(--bg-surface-3);border-radius:50%;color:var(--text-muted);height:56px;margin-bottom:6px;width:56px}.rm-state__title{color:var(--text-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:0}.rm-state__text{color:var(--text-muted);font-size:var(--font-12);line-height:1.5;margin:0;max-width:280px}.rm-state__action{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-12);font-weight:var(--weight-semibold);gap:6px;margin-top:10px;padding:6px 14px}.rm-state__action:hover{background:var(--bg-surface-3);color:var(--text-primary)}.rm-state--error .rm-state__icon{background:var(--color-danger-bg);color:var(--color-danger)}.rm-state--error .rm-state__action{border-color:var(--color-danger);color:var(--color-danger)}.rm-state--error .rm-state__action:hover{background:var(--color-danger-bg)}
|
|
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-body>.rm-empty{box-sizing:border-box;gap:6px;justify-content:center;max-width:none;min-height:240px;padding:36px 24px}.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}.kpi-extra{margin-top:12px}.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}.tk-skeleton{height:14px;width:80%}.tk-empty{color:var(--text-muted);text-align:center}.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}.rm-skeleton{animation:rm-skeleton-shimmer 1.5s ease-in-out infinite;background:linear-gradient(90deg,var(--bg-hover) 0,var(--bg-surface-2) 50%,var(--bg-hover) 100%);background-size:200% 100%;border-radius:var(--radius-md)}@keyframes rm-skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (prefers-reduced-motion:reduce){.rm-skeleton{animation:none;background:var(--bg-hover)}}.rm-state-loading{display:flex;flex-direction:column;gap:10px;justify-content:center;min-height:240px;padding:8px 4px}.rm-spinner{animation:rm-spinner-turn .8s linear infinite;border:3px solid var(--border-default);border-radius:50%;border-top-color:var(--color-secondary);height:32px;width:32px}@keyframes rm-spinner-turn{to{transform:rotate(1turn)}}@media (prefers-reduced-motion:reduce){.rm-spinner{animation-duration:3s}}.rm-state{color:var(--text-secondary);flex-direction:column;font-family:var(--font-sans);gap:6px;min-height:240px;padding:36px 24px;text-align:center}.rm-state,.rm-state__icon{align-items:center;display:flex;justify-content:center}.rm-state__icon{background:var(--bg-surface-3);border-radius:50%;color:var(--text-muted);height:56px;margin-bottom:6px;width:56px}.rm-state__title{color:var(--text-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:0}.rm-state__text{color:var(--text-muted);font-size:var(--font-12);line-height:1.5;margin:0;max-width:280px}.rm-state__action{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-12);font-weight:var(--weight-semibold);gap:6px;margin-top:10px;padding:6px 14px}.rm-state__action:hover{background:var(--bg-surface-3);color:var(--text-primary)}.rm-state--error .rm-state__icon{background:var(--color-danger-bg);color:var(--color-danger)}.rm-state--error .rm-state__action{border-color:var(--color-danger);color:var(--color-danger)}.rm-state--error .rm-state__action:hover{background:var(--color-danger-bg)}
|
|
@@ -14,6 +14,12 @@ export interface RmButtonProps {
|
|
|
14
14
|
prefixIcon?: string
|
|
15
15
|
/** Phosphor icon name after the label */
|
|
16
16
|
suffixIcon?: string
|
|
17
|
+
/**
|
|
18
|
+
* Action en cours : le bouton tourne et n'accepte plus de clic. Sans cet
|
|
19
|
+
* etat, un export long laisse croire que le clic n'a pas pris et l'utilisateur
|
|
20
|
+
* relance la meme requete.
|
|
21
|
+
*/
|
|
22
|
+
loading?: boolean
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
withDefaults(defineProps<RmButtonProps>(), {
|
|
@@ -24,6 +30,7 @@ withDefaults(defineProps<RmButtonProps>(), {
|
|
|
24
30
|
type: 'button',
|
|
25
31
|
prefixIcon: undefined,
|
|
26
32
|
suffixIcon: undefined,
|
|
33
|
+
loading: false,
|
|
27
34
|
})
|
|
28
35
|
|
|
29
36
|
defineEmits<{
|
|
@@ -34,21 +41,23 @@ defineEmits<{
|
|
|
34
41
|
<template>
|
|
35
42
|
<button
|
|
36
43
|
:type="type"
|
|
37
|
-
:disabled="disabled"
|
|
44
|
+
:disabled="disabled || loading"
|
|
45
|
+
:aria-busy="loading ? 'true' : undefined"
|
|
38
46
|
:class="[
|
|
39
47
|
'rm-button',
|
|
40
48
|
`rm-button--${variant}`,
|
|
41
49
|
`rm-button--${size}`,
|
|
42
|
-
{ 'rm-button--block': block },
|
|
50
|
+
{ 'rm-button--block': block, 'rm-button--loading': loading },
|
|
43
51
|
]"
|
|
44
52
|
@click="$emit('click', $event)"
|
|
45
53
|
>
|
|
46
|
-
<RmIcon v-if="
|
|
54
|
+
<RmIcon v-if="loading" name="CircleNotch" :size="size === 'sm' ? 14 : 16" class="rm-button__spinner" />
|
|
55
|
+
<RmIcon v-else-if="prefixIcon" :name="prefixIcon" :size="size === 'sm' ? 14 : 16" />
|
|
47
56
|
<slot />
|
|
48
57
|
<RmIcon v-if="suffixIcon" :name="suffixIcon" :size="size === 'sm' ? 14 : 16" />
|
|
49
58
|
</button>
|
|
50
59
|
</template>
|
|
51
60
|
|
|
52
61
|
<style scoped>
|
|
53
|
-
.rm-button{align-items:center;border:1px solid transparent;border-radius:6px;cursor:pointer;display:inline-flex;font-family:var(--font-sans);font-weight:500;gap:8px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-button:focus-visible{outline:2px solid var(--color-healthcare);outline-offset:2px}.rm-button:disabled{cursor:not-allowed;opacity:.5}.rm-button--sm{font-size:13px;height:32px;padding:6px 16px}.rm-button--md{font-size:14px;height:40px;padding:10px 24px}.rm-button--lg{font-size:15px;height:48px;padding:12px 28px}.rm-button--primary{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-button--primary:active:not(:disabled),.rm-button--primary:hover:not(:disabled){background:var(--color-primary-hover);border-color:var(--color-primary-hover)}.rm-button--primary-fill{background:linear-gradient(180deg,rgba(var(--healthcare-rgb),.8) 0,rgba(var(--healthcare-rgb),.9) 100%);border-color:var(--color-healthcare);box-shadow:0 2px 3px 0 rgba(var(--healthcare-rgb),.5);color:var(--color-text-inverse)}.rm-button--primary-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-healthcare) 0,var(--color-healthcare) 100%);box-shadow:0 2px 4px 0 rgba(var(--healthcare-rgb),.6)}.rm-button--primary-fill:active:not(:disabled){background:var(--color-healthcare-active);box-shadow:0 1px 2px 0 rgba(var(--healthcare-rgb),.4)}.rm-button--primary-stroke{box-shadow:0 2px 3px 0 rgba(var(--healthcare-rgb),.5);color:var(--color-healthcare)}.rm-button--primary-stroke,.rm-button--primary-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-healthcare)}.rm-button--primary-stroke:hover:not(:disabled){box-shadow:none}.rm-button--primary-stroke:active:not(:disabled){background:var(--bg-hover);box-shadow:none}.rm-button--discovery-fill{background:linear-gradient(180deg,rgba(var(--discovery-rgb),.8) 0,rgba(var(--discovery-rgb),.9) 100%);border-color:var(--color-discovery);box-shadow:0 2px 3px 0 rgba(var(--discovery-rgb),.5);color:var(--color-text-inverse)}.rm-button--discovery-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-discovery) 0,var(--color-discovery) 100%);border-color:var(--color-discovery);box-shadow:none}.rm-button--discovery-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-discovery-active) 0,var(--color-discovery-active) 100%);box-shadow:none}.rm-button--discovery-stroke{box-shadow:0 2px 3px 0 rgba(var(--discovery-rgb),.5);color:var(--color-discovery)}.rm-button--discovery-stroke,.rm-button--discovery-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-discovery)}.rm-button--discovery-stroke:hover:not(:disabled){box-shadow:none}.rm-button--discovery-stroke:active:not(:disabled){background:rgba(var(--discovery-rgb),.05);box-shadow:none}.rm-button--databank-fill{background:linear-gradient(180deg,rgba(var(--databank-rgb),.8) 0,rgba(var(--databank-rgb),.9) 100%);border-color:var(--color-databank);box-shadow:0 2px 3px 0 rgba(var(--databank-rgb),.5);color:var(--color-text-inverse)}.rm-button--databank-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-databank) 0,var(--color-databank) 100%);border-color:var(--color-databank);box-shadow:none}.rm-button--databank-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-databank-active) 0,var(--color-databank-active) 100%);box-shadow:none}.rm-button--databank-stroke{box-shadow:0 2px 3px 0 rgba(var(--databank-rgb),.5);color:var(--color-databank)}.rm-button--databank-stroke,.rm-button--databank-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-databank)}.rm-button--databank-stroke:hover:not(:disabled){box-shadow:none}.rm-button--databank-stroke:active:not(:disabled){background:rgba(var(--databank-rgb),.05);box-shadow:none}.rm-button--pharma-fill{background:linear-gradient(180deg,var(--color-pharma-soft) 0,var(--color-pharma) 100%);border-color:var(--color-pharma);box-shadow:0 2px 3px 0 rgba(var(--pharma-rgb),.5);color:var(--color-text-inverse)}.rm-button--pharma-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-pharma) 0,var(--color-pharma) 100%);border-color:var(--color-pharma);box-shadow:none}.rm-button--pharma-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-pharma-active) 0,var(--color-pharma-active) 100%);box-shadow:none}.rm-button--pharma-stroke{box-shadow:0 2px 3px 0 rgba(var(--pharma-rgb),.5);color:var(--color-pharma)}.rm-button--pharma-stroke,.rm-button--pharma-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-pharma)}.rm-button--pharma-stroke:hover:not(:disabled){box-shadow:none}.rm-button--pharma-stroke:active:not(:disabled){background:rgba(var(--pharma-rgb),.05);box-shadow:none}.rm-button--secondary{background-color:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary)}.rm-button--secondary:active:not(:disabled),.rm-button--secondary:hover:not(:disabled){background-color:var(--bg-surface-3);border-color:var(--color-violet-pale);color:var(--text-primary)}.rm-button--outline{background-color:transparent;border-color:var(--border-default);color:var(--text-secondary)}.rm-button--outline:hover:not(:disabled){background-color:var(--bg-surface-2);border-color:var(--text-muted)}.rm-button--outline:active:not(:disabled){background-color:var(--bg-surface-3)}.rm-button--danger{background-color:var(--bg-surface);border-color:var(--color-danger);color:var(--color-danger)}.rm-button--danger:active:not(:disabled),.rm-button--danger:hover:not(:disabled){background-color:var(--color-danger);color:var(--color-text-inverse)}.rm-button--ghost{background-color:transparent;color:var(--text-secondary)}.rm-button--ghost:hover:not(:disabled){background-color:var(--bg-surface-3)}.rm-button--ghost:active:not(:disabled){background-color:var(--border-default)}.rm-button--block{width:100%}
|
|
62
|
+
.rm-button--loading{cursor:progress}.rm-button__spinner{animation:rm-button-spin .9s linear infinite}@keyframes rm-button-spin{to{transform:rotate(1turn)}}.rm-button{align-items:center;border:1px solid transparent;border-radius:6px;cursor:pointer;display:inline-flex;font-family:var(--font-sans);font-weight:500;gap:8px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-button:focus-visible{outline:2px solid var(--color-healthcare);outline-offset:2px}.rm-button:disabled{cursor:not-allowed;opacity:.5}.rm-button--sm{font-size:13px;height:32px;padding:6px 16px}.rm-button--md{font-size:14px;height:40px;padding:10px 24px}.rm-button--lg{font-size:15px;height:48px;padding:12px 28px}.rm-button--primary{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-button--primary:active:not(:disabled),.rm-button--primary:hover:not(:disabled){background:var(--color-primary-hover);border-color:var(--color-primary-hover)}.rm-button--primary-fill{background:linear-gradient(180deg,rgba(var(--healthcare-rgb),.8) 0,rgba(var(--healthcare-rgb),.9) 100%);border-color:var(--color-healthcare);box-shadow:0 2px 3px 0 rgba(var(--healthcare-rgb),.5);color:var(--color-text-inverse)}.rm-button--primary-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-healthcare) 0,var(--color-healthcare) 100%);box-shadow:0 2px 4px 0 rgba(var(--healthcare-rgb),.6)}.rm-button--primary-fill:active:not(:disabled){background:var(--color-healthcare-active);box-shadow:0 1px 2px 0 rgba(var(--healthcare-rgb),.4)}.rm-button--primary-stroke{box-shadow:0 2px 3px 0 rgba(var(--healthcare-rgb),.5);color:var(--color-healthcare)}.rm-button--primary-stroke,.rm-button--primary-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-healthcare)}.rm-button--primary-stroke:hover:not(:disabled){box-shadow:none}.rm-button--primary-stroke:active:not(:disabled){background:var(--bg-hover);box-shadow:none}.rm-button--discovery-fill{background:linear-gradient(180deg,rgba(var(--discovery-rgb),.8) 0,rgba(var(--discovery-rgb),.9) 100%);border-color:var(--color-discovery);box-shadow:0 2px 3px 0 rgba(var(--discovery-rgb),.5);color:var(--color-text-inverse)}.rm-button--discovery-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-discovery) 0,var(--color-discovery) 100%);border-color:var(--color-discovery);box-shadow:none}.rm-button--discovery-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-discovery-active) 0,var(--color-discovery-active) 100%);box-shadow:none}.rm-button--discovery-stroke{box-shadow:0 2px 3px 0 rgba(var(--discovery-rgb),.5);color:var(--color-discovery)}.rm-button--discovery-stroke,.rm-button--discovery-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-discovery)}.rm-button--discovery-stroke:hover:not(:disabled){box-shadow:none}.rm-button--discovery-stroke:active:not(:disabled){background:rgba(var(--discovery-rgb),.05);box-shadow:none}.rm-button--databank-fill{background:linear-gradient(180deg,rgba(var(--databank-rgb),.8) 0,rgba(var(--databank-rgb),.9) 100%);border-color:var(--color-databank);box-shadow:0 2px 3px 0 rgba(var(--databank-rgb),.5);color:var(--color-text-inverse)}.rm-button--databank-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-databank) 0,var(--color-databank) 100%);border-color:var(--color-databank);box-shadow:none}.rm-button--databank-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-databank-active) 0,var(--color-databank-active) 100%);box-shadow:none}.rm-button--databank-stroke{box-shadow:0 2px 3px 0 rgba(var(--databank-rgb),.5);color:var(--color-databank)}.rm-button--databank-stroke,.rm-button--databank-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-databank)}.rm-button--databank-stroke:hover:not(:disabled){box-shadow:none}.rm-button--databank-stroke:active:not(:disabled){background:rgba(var(--databank-rgb),.05);box-shadow:none}.rm-button--pharma-fill{background:linear-gradient(180deg,var(--color-pharma-soft) 0,var(--color-pharma) 100%);border-color:var(--color-pharma);box-shadow:0 2px 3px 0 rgba(var(--pharma-rgb),.5);color:var(--color-text-inverse)}.rm-button--pharma-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-pharma) 0,var(--color-pharma) 100%);border-color:var(--color-pharma);box-shadow:none}.rm-button--pharma-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-pharma-active) 0,var(--color-pharma-active) 100%);box-shadow:none}.rm-button--pharma-stroke{box-shadow:0 2px 3px 0 rgba(var(--pharma-rgb),.5);color:var(--color-pharma)}.rm-button--pharma-stroke,.rm-button--pharma-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-pharma)}.rm-button--pharma-stroke:hover:not(:disabled){box-shadow:none}.rm-button--pharma-stroke:active:not(:disabled){background:rgba(var(--pharma-rgb),.05);box-shadow:none}.rm-button--secondary{background-color:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary)}.rm-button--secondary:active:not(:disabled),.rm-button--secondary:hover:not(:disabled){background-color:var(--bg-surface-3);border-color:var(--color-violet-pale);color:var(--text-primary)}.rm-button--outline{background-color:transparent;border-color:var(--border-default);color:var(--text-secondary)}.rm-button--outline:hover:not(:disabled){background-color:var(--bg-surface-2);border-color:var(--text-muted)}.rm-button--outline:active:not(:disabled){background-color:var(--bg-surface-3)}.rm-button--danger{background-color:var(--bg-surface);border-color:var(--color-danger);color:var(--color-danger)}.rm-button--danger:active:not(:disabled),.rm-button--danger:hover:not(:disabled){background-color:var(--color-danger);color:var(--color-text-inverse)}.rm-button--ghost{background-color:transparent;color:var(--text-secondary)}.rm-button--ghost:hover:not(:disabled){background-color:var(--bg-surface-3)}.rm-button--ghost:active:not(:disabled){background-color:var(--border-default)}.rm-button--block{width:100%}
|
|
54
63
|
</style>
|
|
@@ -113,7 +113,15 @@ const t = computed(() => WORDS[props.lang])
|
|
|
113
113
|
|
|
114
114
|
// Une chaine deja formatee ("17 juin 2026") n'est pas parsable : on la garde
|
|
115
115
|
// telle quelle plutot que d'afficher "Invalid Date".
|
|
116
|
+
/**
|
|
117
|
+
* Une chaine deja formatee ("June 2026", "T2 2025") passe telle quelle : la
|
|
118
|
+
* reparser inventerait un jour que la source ne donne pas. Seules une date
|
|
119
|
+
* technique (Date, timestamp, ISO) sont mises au format long.
|
|
120
|
+
*/
|
|
121
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}/
|
|
122
|
+
|
|
116
123
|
const verifiedText = computed(() => {
|
|
124
|
+
if (typeof props.verified === 'string' && !ISO_DATE.test(props.verified)) return props.verified
|
|
117
125
|
const parsed = new Date(props.verified)
|
|
118
126
|
if (Number.isNaN(parsed.getTime())) return String(props.verified)
|
|
119
127
|
return props.lang === 'en'
|
|
@@ -287,7 +287,7 @@ const sizeKb = computed(() =>
|
|
|
287
287
|
)
|
|
288
288
|
|
|
289
289
|
const fullFileName = computed(() => {
|
|
290
|
-
if (isPermalink.value) return '
|
|
290
|
+
if (isPermalink.value) return '-'
|
|
291
291
|
const extension = RM_EXPORT_EXTENSIONS[config.format] ?? config.format
|
|
292
292
|
return `${config.fileName || 'export'}.${extension}`
|
|
293
293
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
3
|
-
import { PhCaretDown, PhCheck } from '@phosphor-icons/vue'
|
|
2
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
3
|
+
import { PhCaretDown, PhCheck, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
|
4
4
|
import type { Component } from 'vue'
|
|
5
5
|
|
|
6
6
|
export interface FilterDropdownOption {
|
|
@@ -14,17 +14,32 @@ const props = withDefaults(defineProps<{
|
|
|
14
14
|
options: FilterDropdownOption[]
|
|
15
15
|
multiple?: boolean
|
|
16
16
|
showSelectAll?: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Champ de recherche en tete de liste. `auto` l'ajoute au-dela de
|
|
19
|
+
* SEARCH_THRESHOLD options : une liste de 500 societes ne se parcourt pas a
|
|
20
|
+
* la molette, et une liste de six n'a rien a filtrer.
|
|
21
|
+
*/
|
|
22
|
+
searchable?: boolean | 'auto'
|
|
23
|
+
/** Texte du champ de recherche, quand il y en a un. */
|
|
24
|
+
searchPlaceholder?: string
|
|
17
25
|
}>(), {
|
|
18
26
|
icon: undefined,
|
|
19
27
|
multiple: true,
|
|
20
28
|
showSelectAll: false,
|
|
29
|
+
searchable: 'auto',
|
|
30
|
+
searchPlaceholder: 'Rechercher...',
|
|
21
31
|
})
|
|
22
32
|
|
|
23
33
|
const selected = defineModel<string[]>({ default: () => [] })
|
|
24
34
|
|
|
25
35
|
const isOpen = ref(false)
|
|
26
36
|
const chipRef = ref<HTMLElement>()
|
|
37
|
+
const searchRef = ref<HTMLInputElement>()
|
|
27
38
|
const hasUserChanged = ref(false)
|
|
39
|
+
const search = ref('')
|
|
40
|
+
|
|
41
|
+
/** Au-dela de ce nombre d'options, `searchable: 'auto'` pose le champ. */
|
|
42
|
+
const SEARCH_THRESHOLD = 12
|
|
28
43
|
|
|
29
44
|
// Initialise synchrone pour éviter hydration mismatch
|
|
30
45
|
if (!props.multiple && selected.value.length === 0 && props.options.length > 0) {
|
|
@@ -33,18 +48,41 @@ if (!props.multiple && selected.value.length === 0 && props.options.length > 0)
|
|
|
33
48
|
|
|
34
49
|
const isActive = computed(() => hasUserChanged.value && selected.value.length > 0)
|
|
35
50
|
|
|
51
|
+
// Un filtre a choix unique affiche toujours son choix, meme avant toute
|
|
52
|
+
// interaction : la valeur par defaut fait partie du filtre, la cacher revient
|
|
53
|
+
// a lire la page sans savoir sur quoi elle porte.
|
|
36
54
|
const displayLabel = computed(() => {
|
|
37
|
-
if (!props.multiple &&
|
|
55
|
+
if (!props.multiple && selected.value.length === 1) {
|
|
38
56
|
const option = props.options.find(o => o.value === selected.value[0])
|
|
39
57
|
return option ? `${props.label}: ${option.label}` : props.label
|
|
40
58
|
}
|
|
41
59
|
return props.label
|
|
42
60
|
})
|
|
43
61
|
|
|
62
|
+
const hasSearch = computed(() =>
|
|
63
|
+
props.searchable === 'auto' ? props.options.length > SEARCH_THRESHOLD : props.searchable,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const filteredOptions = computed(() => {
|
|
67
|
+
if (!hasSearch.value || !search.value.trim()) return props.options
|
|
68
|
+
const needle = search.value.trim().toLowerCase()
|
|
69
|
+
return props.options.filter(o => o.label.toLowerCase().includes(needle))
|
|
70
|
+
})
|
|
71
|
+
|
|
44
72
|
function toggle() {
|
|
45
73
|
isOpen.value = !isOpen.value
|
|
46
74
|
}
|
|
47
75
|
|
|
76
|
+
// La recherche repart vide a chaque ouverture, et prend le curseur : sinon le
|
|
77
|
+
// filtre garde la saisie precedente et la liste s'ouvre deja amputee.
|
|
78
|
+
watch(isOpen, async (open) => {
|
|
79
|
+
if (!open) return
|
|
80
|
+
search.value = ''
|
|
81
|
+
if (!hasSearch.value) return
|
|
82
|
+
await nextTick()
|
|
83
|
+
searchRef.value?.focus()
|
|
84
|
+
})
|
|
85
|
+
|
|
48
86
|
function toggleOption(value: string) {
|
|
49
87
|
hasUserChanged.value = true
|
|
50
88
|
if (props.multiple) {
|
|
@@ -106,6 +144,16 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
106
144
|
|
|
107
145
|
<Transition name="dropdown">
|
|
108
146
|
<div v-if="isOpen" class="filter-chip__dropdown">
|
|
147
|
+
<label v-if="hasSearch" class="filter-chip__search">
|
|
148
|
+
<PhMagnifyingGlass :size="14" />
|
|
149
|
+
<input
|
|
150
|
+
ref="searchRef"
|
|
151
|
+
v-model="search"
|
|
152
|
+
type="search"
|
|
153
|
+
:placeholder="searchPlaceholder"
|
|
154
|
+
@click.stop
|
|
155
|
+
/>
|
|
156
|
+
</label>
|
|
109
157
|
<button
|
|
110
158
|
v-if="showSelectAll && multiple && options.length > 0"
|
|
111
159
|
class="filter-chip__select-all"
|
|
@@ -117,8 +165,11 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
117
165
|
<span>{{ allSelected ? 'Deselect all' : 'Select all' }}</span>
|
|
118
166
|
</button>
|
|
119
167
|
<div :class="{ 'filter-chip__options-group': showSelectAll && multiple && options.length > 0 }">
|
|
168
|
+
<p v-if="hasSearch && filteredOptions.length === 0" class="filter-chip__no-match">
|
|
169
|
+
Aucun résultat
|
|
170
|
+
</p>
|
|
120
171
|
<button
|
|
121
|
-
v-for="option in
|
|
172
|
+
v-for="option in filteredOptions"
|
|
122
173
|
:key="option.value"
|
|
123
174
|
class="filter-chip__option"
|
|
124
175
|
:class="{ 'filter-chip__option--selected': isSelected(option.value) }"
|
|
@@ -141,5 +192,5 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
141
192
|
</template>
|
|
142
193
|
|
|
143
194
|
<style scoped>
|
|
144
|
-
.filter-chip-wrapper{position:relative}.filter-chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:8px;height:32px;padding:8px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--border-default)}.filter-chip__icon{color:var(--color-healthcare);flex-shrink:0}.filter-chip--active{border-color:var(--color-healthcare);box-shadow:0 0 8px 0 rgba(var(--healthcare-rgb),.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;flex-shrink:0;font-size:11px;font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:var(--color-text-inverse);border:1px solid var(--border-default);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:300px;max-width:420px;min-width:380px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);width:-moz-max-content;width:max-content;z-index:50}.filter-
|
|
195
|
+
.filter-chip-wrapper{position:relative}.filter-chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:8px;height:32px;padding:8px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--border-default)}.filter-chip__icon{color:var(--color-healthcare);flex-shrink:0}.filter-chip--active{border-color:var(--color-healthcare);box-shadow:0 0 8px 0 rgba(var(--healthcare-rgb),.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;flex-shrink:0;font-size:11px;font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:var(--color-text-inverse);border:1px solid var(--border-default);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:300px;max-width:420px;min-width:380px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);width:-moz-max-content;width:max-content;z-index:50}.filter-chip__search{align-items:center;background-color:var(--color-text-inverse);border-bottom:1px solid var(--border-soft);color:var(--text-muted);display:flex;gap:6px;margin:-4px -4px 4px;padding:8px;position:sticky;top:-4px;z-index:1}.filter-chip__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-14);min-width:0;outline:none}.filter-chip__search input::-webkit-search-cancel-button{cursor:pointer}.filter-chip__no-match{color:var(--text-muted);margin:0;padding:10px 8px}.filter-chip__no-match,.filter-chip__option{font-family:var(--font-sans);font-size:var(--font-14)}.filter-chip__option{align-items:center;background:none;border:none;border-radius:4px;color:var(--text-secondary);cursor:pointer;display:flex;gap:8px;padding:6px 8px;text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.filter-chip__option:hover{background-color:var(--bg-surface-2)}.filter-chip__option--selected{color:var(--text-primary);font-weight:var(--weight-medium)}.filter-chip__checkbox{align-items:center;border:1.5px solid var(--border-default);border-radius:3px;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__checkbox--checked{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.filter-chip__radio{align-items:center;border:1.5px solid var(--border-default);border-radius:100%;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__radio--checked{border-color:var(--color-healthcare)}.filter-chip__radio--checked:after{background-color:var(--color-healthcare);border-radius:100%;content:"";height:8px;width:8px}.filter-chip__select-all{align-items:center;background:none;border:none;border-radius:4px;color:var(--color-healthcare);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:500;gap:8px;padding:6px 8px;text-align:left;width:100%}.filter-chip__select-all:hover{background-color:rgba(var(--healthcare-rgb),.04)}.filter-chip__options-group{border-left:1px solid var(--border-default);margin-left:14px;padding-left:6px}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
|
|
145
196
|
</style>
|
|
@@ -1,71 +1,136 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Tableau a barres : un classement ou chaque ligne porte sa part sous forme de
|
|
3
|
+
barre, derriere son libelle, et sa valeur chiffree a droite.
|
|
4
|
+
|
|
5
|
+
Remplace `arg-hybrid-chart` d'Auriga, avec le meme contrat de lecture : la
|
|
6
|
+
barre la plus longue vaut la plus grande part, la valeur garde son pourcentage
|
|
7
|
+
entre parentheses, et un bouton deplie le reste du classement quand `limit`
|
|
8
|
+
coupe la liste.
|
|
9
|
+
|
|
10
|
+
La barre n'est pas un graphe : c'est un tableau, lisible au lecteur d'ecran
|
|
11
|
+
ligne a ligne, ou la barre n'est qu'un fond. C'est ce qui la distingue de
|
|
12
|
+
RmBarChart, qui peint un SVG.
|
|
13
|
+
-->
|
|
1
14
|
<script setup lang="ts">
|
|
2
15
|
import { computed, ref } from 'vue'
|
|
3
|
-
|
|
4
|
-
* HybridTable — a table with inline horizontal bars behind values.
|
|
5
|
-
* Each row shows a label, a value, and a bar proportional to the max value.
|
|
6
|
-
*/
|
|
16
|
+
import { rmNumber } from '../utils/format'
|
|
7
17
|
|
|
8
|
-
export interface
|
|
18
|
+
export interface RmHybridRow {
|
|
9
19
|
label: string
|
|
10
20
|
value: number
|
|
21
|
+
/** Part en pourcent, deja calculee (0-100). Affichee entre parentheses. */
|
|
22
|
+
percentage?: number | string
|
|
23
|
+
/** Colonnes de fin de ligne, alignees a droite apres la valeur. */
|
|
24
|
+
extra?: (string | number)[]
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
const props = withDefaults(defineProps<{
|
|
14
|
-
rows:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
rows: RmHybridRow[]
|
|
29
|
+
/**
|
|
30
|
+
* En-tetes de colonnes : le libelle, la valeur, puis un titre par colonne
|
|
31
|
+
* `extra`. Vide : le tableau n'a pas de tete.
|
|
32
|
+
*/
|
|
33
|
+
columns?: string[]
|
|
34
|
+
/**
|
|
35
|
+
* Couleur de la barre. Un chiffre de 1 a 6 prend la couleur categorielle du
|
|
36
|
+
* theme (`--chart-cat-N`), qui suit le mode nuit et les modes daltoniens ;
|
|
37
|
+
* une chaine passe telle quelle, pour une couleur imposee par la page.
|
|
38
|
+
*/
|
|
39
|
+
accent?: number | string
|
|
40
|
+
/** Nombre de lignes visibles au repos. 0 : tout le classement. */
|
|
41
|
+
limit?: number
|
|
42
|
+
/** Langue du bouton de depliage. Le reste du texte vient de la page. */
|
|
43
|
+
lang?: 'fr' | 'en'
|
|
44
|
+
/** Formatage de la valeur. Par defaut le format francais (rmNumber). */
|
|
45
|
+
valueFormatter?: (value: number) => string
|
|
19
46
|
}>(), {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
columns: () => [],
|
|
48
|
+
accent: 1,
|
|
49
|
+
limit: 0,
|
|
50
|
+
lang: 'fr',
|
|
51
|
+
valueFormatter: rmNumber,
|
|
24
52
|
})
|
|
25
53
|
|
|
54
|
+
const emit = defineEmits<{ showState: [value: boolean] }>()
|
|
55
|
+
|
|
26
56
|
const showAll = ref(false)
|
|
27
57
|
|
|
28
|
-
const
|
|
58
|
+
const barColor = computed(() =>
|
|
59
|
+
typeof props.accent === 'number' ? `var(--chart-cat-${props.accent})` : props.accent,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const visibleRows = computed(() =>
|
|
63
|
+
props.limit && !showAll.value ? props.rows.slice(0, props.limit) : props.rows,
|
|
64
|
+
)
|
|
29
65
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
const hasMore = computed(() => Boolean(props.limit) && props.rows.length > props.limit)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* La barre se lit en relatif : la plus grande part occupe toute la largeur,
|
|
70
|
+
* les autres se mesurent contre elle. Une barre proportionnelle a la part
|
|
71
|
+
* absolue serait illisible des que le premier du classement pese 3 %.
|
|
72
|
+
*/
|
|
73
|
+
const reference = computed(() => {
|
|
74
|
+
const shares = props.rows.map(share)
|
|
75
|
+
return Math.max(...shares, 0) || 1
|
|
33
76
|
})
|
|
34
77
|
|
|
35
|
-
|
|
78
|
+
function share(row: RmHybridRow) {
|
|
79
|
+
if (row.percentage !== undefined && row.percentage !== null && row.percentage !== '') {
|
|
80
|
+
return Number(row.percentage)
|
|
81
|
+
}
|
|
82
|
+
// Sans part fournie, la valeur se rapporte au total affiche.
|
|
83
|
+
const total = props.rows.reduce((sum, r) => sum + r.value, 0)
|
|
84
|
+
return total ? (row.value * 100) / total : 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function barWidth(row: RmHybridRow) {
|
|
88
|
+
return `${Math.min((share(row) * 100) / reference.value, 100)}%`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toggle() {
|
|
92
|
+
showAll.value = !showAll.value
|
|
93
|
+
emit('showState', showAll.value)
|
|
94
|
+
}
|
|
36
95
|
</script>
|
|
37
96
|
|
|
38
97
|
<template>
|
|
39
|
-
<div class="hybrid
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
>
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
class="
|
|
52
|
-
:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
<div class="rm-hybrid">
|
|
99
|
+
<table class="rm-hybrid__table">
|
|
100
|
+
<thead v-if="columns.length" class="rm-hybrid__head">
|
|
101
|
+
<tr>
|
|
102
|
+
<th v-for="(column, index) in columns" :key="index" class="rm-hybrid__th">
|
|
103
|
+
{{ column }}
|
|
104
|
+
</th>
|
|
105
|
+
</tr>
|
|
106
|
+
</thead>
|
|
107
|
+
<tbody>
|
|
108
|
+
<tr v-for="(row, index) in visibleRows" :key="index" class="rm-hybrid__row">
|
|
109
|
+
<th scope="row" class="rm-hybrid__label_cell">
|
|
110
|
+
<span class="rm-hybrid__bar" :style="{ background: barColor, width: barWidth(row) }" />
|
|
111
|
+
<span class="rm-hybrid__label" :title="row.label">{{ row.label }}</span>
|
|
112
|
+
</th>
|
|
113
|
+
<td class="rm-hybrid__value">
|
|
114
|
+
{{ valueFormatter(row.value) }}
|
|
115
|
+
<span v-if="row.percentage !== undefined && row.percentage !== null" class="rm-hybrid__share">
|
|
116
|
+
({{ row.percentage }} %)
|
|
117
|
+
</span>
|
|
118
|
+
</td>
|
|
119
|
+
<td v-for="(cell, cellIndex) in row.extra ?? []" :key="cellIndex" class="rm-hybrid__value">
|
|
120
|
+
{{ cell }}
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
58
125
|
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
>
|
|
64
|
-
{{ showAll ? 'Show less' : `Show all (${rows.length})` }}
|
|
65
|
-
</button>
|
|
126
|
+
<div v-if="hasMore" class="rm-hybrid__footer">
|
|
127
|
+
<RmButton variant="outline" size="sm" :prefix-icon="showAll ? 'CornersIn' : 'CornersOut'" @click="toggle">
|
|
128
|
+
{{ showAll ? (lang === 'fr' ? 'Voir moins' : 'Show less') : (lang === 'fr' ? 'Voir plus' : 'Show more') }}
|
|
129
|
+
</RmButton>
|
|
130
|
+
</div>
|
|
66
131
|
</div>
|
|
67
132
|
</template>
|
|
68
133
|
|
|
69
134
|
<style scoped>
|
|
70
|
-
.hybrid
|
|
135
|
+
.rm-hybrid{font-family:var(--font-sans);width:100%}.rm-hybrid__table{border-collapse:collapse;table-layout:auto;width:100%}.rm-hybrid__th{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-normal);line-height:14px;padding:4px 10px;text-align:right;white-space:nowrap}.rm-hybrid__th:first-child{padding-left:0;text-align:left}.rm-hybrid__th:last-child{padding-right:0}.rm-hybrid__row{height:34px}.rm-hybrid__label_cell{font-weight:var(--weight-normal);padding:0;position:relative;text-align:left;width:100%}.rm-hybrid__bar{border-radius:var(--radius-sm);bottom:2px;left:0;opacity:.14;position:absolute;top:2px}.rm-hybrid__label{display:block;overflow:hidden;padding:6px 10px 6px 8px;position:relative;text-overflow:ellipsis}.rm-hybrid__label,.rm-hybrid__value{color:var(--text-primary);font-size:var(--font-13);white-space:nowrap}.rm-hybrid__value{font-variant-numeric:tabular-nums;font-weight:var(--weight-medium);padding:6px 10px;text-align:right}.rm-hybrid__value:last-child{padding-right:0}.rm-hybrid__share{color:var(--text-muted);font-weight:var(--weight-normal);margin-left:2px}.rm-hybrid__footer{display:flex;justify-content:center;margin-top:16px}
|
|
71
136
|
</style>
|
|
@@ -59,5 +59,8 @@ const trendText = computed(() =>
|
|
|
59
59
|
<span v-if="trendLabel">{{ trendLabel }}</span>
|
|
60
60
|
</p>
|
|
61
61
|
<p v-if="note" class="kpi-foot">{{ note }}</p>
|
|
62
|
+
<!-- Complement de tuile : une repartition, une mini-courbe. Le contenu
|
|
63
|
+
projete reste sous le pied, pour ne pas repousser la valeur. -->
|
|
64
|
+
<div v-if="$slots.default" class="kpi-extra"><slot /></div>
|
|
62
65
|
</div>
|
|
63
66
|
</template>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Champ numerique borne, pour les filtres qui prennent un nombre plutot qu'une
|
|
3
|
+
liste : un "Top N", un seuil, un nombre de mois.
|
|
4
|
+
|
|
5
|
+
Remplace `arg-number-input` d'Auriga. Meme hauteur et meme rayon que la puce
|
|
6
|
+
de filtre (RmFilterDropdown) : les deux se rangent sur la meme rangee.
|
|
7
|
+
|
|
8
|
+
La valeur est toujours ramenee dans les bornes, y compris a la frappe : un
|
|
9
|
+
filtre qui laisse saisir 5000 quand la donnee s'arrete a 100 renvoie une page
|
|
10
|
+
vide sans rien dire.
|
|
11
|
+
-->
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed } from 'vue'
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<{
|
|
16
|
+
/** Libelle du champ, au-dessus. Vide : aucun libelle. */
|
|
17
|
+
label?: string
|
|
18
|
+
/** Borne basse. */
|
|
19
|
+
min?: number
|
|
20
|
+
/** Borne haute. */
|
|
21
|
+
max?: number
|
|
22
|
+
/** Pas des boutons moins et plus. */
|
|
23
|
+
step?: number
|
|
24
|
+
}>(), {
|
|
25
|
+
label: undefined,
|
|
26
|
+
min: 0,
|
|
27
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
28
|
+
step: 1,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const model = defineModel<number>({ default: 0 })
|
|
32
|
+
|
|
33
|
+
const atMin = computed(() => model.value <= props.min)
|
|
34
|
+
const atMax = computed(() => model.value >= props.max)
|
|
35
|
+
|
|
36
|
+
function clamp(value: number) {
|
|
37
|
+
if (Number.isNaN(value)) return props.min
|
|
38
|
+
return Math.min(Math.max(value, props.min), props.max)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function nudge(delta: number) {
|
|
42
|
+
model.value = clamp(model.value + delta)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onInput(event: Event) {
|
|
46
|
+
const input = event.target as HTMLInputElement
|
|
47
|
+
const next = clamp(Number(input.value))
|
|
48
|
+
model.value = next
|
|
49
|
+
// Le champ affiche ce qui est retenu : sans ca, une saisie hors bornes reste
|
|
50
|
+
// a l'ecran alors que la page filtre sur autre chose.
|
|
51
|
+
if (String(next) !== input.value) input.value = String(next)
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div class="rm-number">
|
|
57
|
+
<label v-if="label" class="rm-number__label">{{ label }}</label>
|
|
58
|
+
<div class="rm-number__box">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
class="rm-number__step"
|
|
62
|
+
:disabled="atMin"
|
|
63
|
+
:aria-label="`Diminuer${label ? ' ' + label : ''}`"
|
|
64
|
+
@click="nudge(-step)"
|
|
65
|
+
>
|
|
66
|
+
<RmIcon name="Minus" :size="14" />
|
|
67
|
+
</button>
|
|
68
|
+
<input
|
|
69
|
+
class="rm-number__input"
|
|
70
|
+
type="number"
|
|
71
|
+
:value="model"
|
|
72
|
+
:min="min"
|
|
73
|
+
:max="max"
|
|
74
|
+
:step="step"
|
|
75
|
+
:aria-label="label"
|
|
76
|
+
@input="onInput"
|
|
77
|
+
/>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="rm-number__step"
|
|
81
|
+
:disabled="atMax"
|
|
82
|
+
:aria-label="`Augmenter${label ? ' ' + label : ''}`"
|
|
83
|
+
@click="nudge(step)"
|
|
84
|
+
>
|
|
85
|
+
<RmIcon name="Plus" :size="14" />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
.rm-number{display:flex;flex-direction:column;font-family:var(--font-sans);gap:6px}.rm-number__label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-medium)}.rm-number__box{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;display:flex;height:32px;overflow:hidden;width:193px}.rm-number__step{align-items:center;background:none;border:none;color:var(--text-secondary);cursor:pointer;display:flex;flex:none;height:100%;justify-content:center;transition:background var(--duration-fast) var(--ease-in-out);width:32px}.rm-number__step:hover:not(:disabled){background:var(--bg-hover)}.rm-number__step:disabled{color:var(--text-muted);cursor:not-allowed;opacity:.5}.rm-number__input{-moz-appearance:textfield;-webkit-appearance:textfield;appearance:textfield;background:none;border:none;color:var(--text-primary);flex:1;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);height:100%;min-width:0;outline:none;text-align:center}.rm-number__input::-webkit-inner-spin-button,.rm-number__input::-webkit-outer-spin-button{-webkit-appearance:none;appearance:none;margin:0}
|
|
93
|
+
</style>
|
|
@@ -19,6 +19,12 @@ import type { RmHeaderTab } from './AppHeader.vue'
|
|
|
19
19
|
withDefaults(
|
|
20
20
|
defineProps<{
|
|
21
21
|
title?: string | null
|
|
22
|
+
/**
|
|
23
|
+
* Sur-titre, au-dessus du titre : le domaine dont la page traite
|
|
24
|
+
* ("Retail & Hospital Drugs"). Le titre seul ne dit pas toujours de quel
|
|
25
|
+
* jeu de donnees il parle.
|
|
26
|
+
*/
|
|
27
|
+
eyebrow?: string | null
|
|
22
28
|
/** Fleche de retour a gauche du titre. */
|
|
23
29
|
haveReturn?: boolean
|
|
24
30
|
/** Destination du retour ; sans valeur, l'historique. */
|
|
@@ -28,7 +34,7 @@ withDefaults(
|
|
|
28
34
|
/** Nom du groupe d'onglets pour les lecteurs d'ecran. */
|
|
29
35
|
tabsLabel?: string
|
|
30
36
|
}>(),
|
|
31
|
-
{ title: null, haveReturn: false, backTo: null, tabs: () => [], tabsLabel: undefined },
|
|
37
|
+
{ title: null, eyebrow: null, haveReturn: false, backTo: null, tabs: () => [], tabsLabel: undefined },
|
|
32
38
|
)
|
|
33
39
|
|
|
34
40
|
const emit = defineEmits<{ tab: [key: string] }>()
|
|
@@ -48,8 +54,11 @@ const router = useRouter()
|
|
|
48
54
|
</button>
|
|
49
55
|
</template>
|
|
50
56
|
|
|
51
|
-
<
|
|
52
|
-
|
|
57
|
+
<div class="rm-page-header__heading">
|
|
58
|
+
<p v-if="eyebrow" class="rm-page-header__eyebrow">{{ eyebrow }}</p>
|
|
59
|
+
<h1 v-if="title" class="rm-page-header__title">{{ title }}</h1>
|
|
60
|
+
<div v-else class="rm-page-header__title"><slot name="title" /></div>
|
|
61
|
+
</div>
|
|
53
62
|
|
|
54
63
|
<div v-if="$slots.actions" class="rm-page-header__actions"><slot name="actions" /></div>
|
|
55
64
|
</div>
|
|
@@ -71,5 +80,5 @@ const router = useRouter()
|
|
|
71
80
|
</template>
|
|
72
81
|
|
|
73
82
|
<style scoped>
|
|
74
|
-
.rm-page-header{backdrop-filter:blur(12px);background:rgba(245,245,249,.8);border-bottom:1px solid var(--border-default);box-sizing:border-box;color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);gap:12px;padding:16px 32px 12px;position:sticky;top:0;width:100%;z-index:95}.rm-page-header__row{align-items:center;align-self:stretch;display:flex;gap:16px}.rm-page-header__back{background:transparent;border:none;color:inherit;cursor:pointer;display:inline-flex;padding:0}.rm-page-header__title{flex-grow:2;font-size:23px;font-weight:900;line-height:normal;margin:0;min-width:0}.rm-page-header__title :deep(h1),.rm-page-header__title :deep(h2),.rm-page-header__title :deep(p){font-size:inherit;font-weight:inherit;line-height:inherit;margin:0}.rm-page-header__actions{align-items:center;display:flex;flex:0 0 auto;gap:16px}.rm-page-header__tabs{display:flex;gap:8px;margin-bottom:-13px;overflow-x:auto}.rm-page-header__tab{background:none;border:none;border-bottom:3px solid transparent;border-radius:8px 8px 0 0;color:var(--text-muted);cursor:pointer;font-family:inherit;font-size:14px;font-weight:500;line-height:1.6;padding:8px 16px 10px;transition:background-color .3s,color .15s;white-space:nowrap}.rm-page-header__tab:hover{color:var(--color-primary)}.rm-page-header__tab--active{background:color-mix(in srgb,var(--color-primary) 6%,transparent);border-bottom-color:var(--color-primary);color:var(--color-primary);font-weight:600}
|
|
83
|
+
.rm-page-header{backdrop-filter:blur(12px);background:rgba(245,245,249,.8);border-bottom:1px solid var(--border-default);box-sizing:border-box;color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);gap:12px;padding:16px 32px 12px;position:sticky;top:0;width:100%;z-index:95}.rm-page-header__row{align-items:center;align-self:stretch;display:flex;gap:16px}.rm-page-header__back{background:transparent;border:none;color:inherit;cursor:pointer;display:inline-flex;padding:0}.rm-page-header__heading{flex-grow:2;min-width:0}.rm-page-header__eyebrow{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-medium);margin:0 0 2px}.rm-page-header__title{flex-grow:2;font-size:23px;font-weight:900;line-height:normal;margin:0;min-width:0}.rm-page-header__title :deep(h1),.rm-page-header__title :deep(h2),.rm-page-header__title :deep(p){font-size:inherit;font-weight:inherit;line-height:inherit;margin:0}.rm-page-header__actions{align-items:center;display:flex;flex:0 0 auto;gap:16px}.rm-page-header__tabs{display:flex;gap:8px;margin-bottom:-13px;overflow-x:auto}.rm-page-header__tab{background:none;border:none;border-bottom:3px solid transparent;border-radius:8px 8px 0 0;color:var(--text-muted);cursor:pointer;font-family:inherit;font-size:14px;font-weight:500;line-height:1.6;padding:8px 16px 10px;transition:background-color .3s,color .15s;white-space:nowrap}.rm-page-header__tab:hover{color:var(--color-primary)}.rm-page-header__tab--active{background:color-mix(in srgb,var(--color-primary) 6%,transparent);border-bottom-color:var(--color-primary);color:var(--color-primary);font-weight:600}
|
|
75
84
|
</style>
|
|
@@ -1,86 +1,77 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Barre de repartition : une barre empilee, puis la legende ligne a ligne avec
|
|
3
|
+
la valeur et la part de chaque poste.
|
|
4
|
+
|
|
5
|
+
Remplace `arg-simple-chart` d'Auriga. Pas de moteur de graphe : quelques
|
|
6
|
+
divs, ce qui la rend utilisable a l'interieur d'une tuile KPI ou d'une carte
|
|
7
|
+
etroite, la ou un ECharts serait disproportionne.
|
|
8
|
+
-->
|
|
1
9
|
<script setup lang="ts">
|
|
2
10
|
import { computed } from 'vue'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Pure CSS, no dependencies. Great for small inline visualizations.
|
|
6
|
-
*/
|
|
11
|
+
import { useChartTheme } from '../composables/useChartTheme'
|
|
12
|
+
import { rmNumber } from '../utils/format'
|
|
7
13
|
|
|
8
|
-
export interface
|
|
14
|
+
export interface RmSimpleBarItem {
|
|
9
15
|
label: string
|
|
10
16
|
value: number
|
|
17
|
+
/** Couleur imposee. Sans elle, la couleur categorielle du rang. */
|
|
11
18
|
color?: string
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
const props = withDefaults(defineProps<{
|
|
15
|
-
items:
|
|
22
|
+
items: RmSimpleBarItem[]
|
|
16
23
|
title?: string
|
|
17
|
-
|
|
18
|
-
showPercent?: boolean
|
|
24
|
+
/** Hauteur de la barre empilee, en pixels. */
|
|
19
25
|
height?: number
|
|
26
|
+
/** Masque la legende : la barre seule, quand la place manque. */
|
|
27
|
+
hideLegend?: boolean
|
|
28
|
+
/** Formatage des valeurs de la legende. */
|
|
29
|
+
valueFormatter?: (value: number) => string
|
|
20
30
|
}>(), {
|
|
21
31
|
title: undefined,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
height: 8,
|
|
33
|
+
hideLegend: false,
|
|
34
|
+
valueFormatter: rmNumber,
|
|
25
35
|
})
|
|
26
36
|
|
|
27
|
-
const {
|
|
37
|
+
const { palette } = useChartTheme()
|
|
38
|
+
|
|
39
|
+
const total = computed(() => props.items.reduce((sum, item) => sum + item.value, 0))
|
|
40
|
+
|
|
41
|
+
function colorOf(item: RmSimpleBarItem, index: number) {
|
|
42
|
+
return item.color ?? palette.value.cat[index % palette.value.cat.length]
|
|
43
|
+
}
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
function shareOf(item: RmSimpleBarItem) {
|
|
46
|
+
return total.value ? (item.value * 100) / total.value : 0
|
|
47
|
+
}
|
|
31
48
|
</script>
|
|
32
49
|
|
|
33
50
|
<template>
|
|
34
|
-
<div class="simple-bar">
|
|
35
|
-
<
|
|
51
|
+
<div class="rm-simple-bar">
|
|
52
|
+
<p v-if="title" class="rm-simple-bar__title">{{ title }}</p>
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
<div v-if="showPercent" class="simple-bar__stacked" :style="{ height: `${height}px` }">
|
|
54
|
+
<div class="rm-simple-bar__track" :style="{ height: `${height}px` }">
|
|
39
55
|
<div
|
|
40
|
-
v-for="(item,
|
|
41
|
-
:key="
|
|
42
|
-
class="simple-bar__segment"
|
|
43
|
-
:style="{
|
|
44
|
-
|
|
45
|
-
backgroundColor: item.color ?? PALETTE[idx % PALETTE.length],
|
|
46
|
-
}"
|
|
56
|
+
v-for="(item, index) in items"
|
|
57
|
+
:key="index"
|
|
58
|
+
class="rm-simple-bar__segment"
|
|
59
|
+
:style="{ width: `${shareOf(item)}%`, background: colorOf(item, index) }"
|
|
60
|
+
:title="`${item.label} : ${valueFormatter(item.value)}`"
|
|
47
61
|
/>
|
|
48
62
|
</div>
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class="simple-
|
|
56
|
-
>
|
|
57
|
-
|
|
58
|
-
<span
|
|
59
|
-
class="simple-bar__dot"
|
|
60
|
-
:style="{ backgroundColor: item.color ?? PALETTE[idx % PALETTE.length] }"
|
|
61
|
-
/>
|
|
62
|
-
<span class="simple-bar__label">{{ item.label }}</span>
|
|
63
|
-
<span v-if="showValues" class="simple-bar__value">
|
|
64
|
-
{{ item.value.toLocaleString() }}
|
|
65
|
-
<span v-if="showPercent" class="simple-bar__pct">
|
|
66
|
-
({{ total > 0 ? ((item.value / total) * 100).toFixed(1) : 0 }}%)
|
|
67
|
-
</span>
|
|
68
|
-
</span>
|
|
69
|
-
</div>
|
|
70
|
-
<div v-if="!showPercent" class="simple-bar__track" :style="{ height: `${height}px` }">
|
|
71
|
-
<div
|
|
72
|
-
class="simple-bar__fill"
|
|
73
|
-
:style="{
|
|
74
|
-
width: `${(item.value / maxValue) * 100}%`,
|
|
75
|
-
backgroundColor: item.color ?? PALETTE[idx % PALETTE.length],
|
|
76
|
-
}"
|
|
77
|
-
/>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
64
|
+
<ul v-if="!hideLegend" class="rm-simple-bar__legend">
|
|
65
|
+
<li v-for="(item, index) in items" :key="index" class="rm-simple-bar__row">
|
|
66
|
+
<span class="rm-simple-bar__dot" :style="{ background: colorOf(item, index) }" />
|
|
67
|
+
<span class="rm-simple-bar__label">{{ item.label }}</span>
|
|
68
|
+
<span class="rm-simple-bar__value">{{ valueFormatter(item.value) }}</span>
|
|
69
|
+
<span class="rm-simple-bar__share">({{ shareOf(item).toFixed(1).replace('.', ',') }} %)</span>
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
81
72
|
</div>
|
|
82
73
|
</template>
|
|
83
74
|
|
|
84
75
|
<style scoped>
|
|
85
|
-
.simple-bar{font-family:var(--
|
|
76
|
+
.rm-simple-bar{font-family:var(--font-sans)}.rm-simple-bar__title{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-medium);margin:0 0 8px}.rm-simple-bar__track{background:var(--bg-surface-2);border-radius:999px;display:flex;overflow:hidden}.rm-simple-bar__segment{height:100%}.rm-simple-bar__legend{list-style:none;margin:10px 0 0;padding:0}.rm-simple-bar__row{align-items:center;display:flex;font-size:var(--font-12);gap:6px;padding:3px 0}.rm-simple-bar__dot{border-radius:2px;flex:none;height:8px;width:8px}.rm-simple-bar__label{color:var(--text-secondary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-simple-bar__value{color:var(--text-primary);font-variant-numeric:tabular-nums;font-weight:var(--weight-medium)}.rm-simple-bar__share{color:var(--text-muted)}
|
|
86
77
|
</style>
|
|
@@ -148,7 +148,7 @@ const sortedRows = computed(() => {
|
|
|
148
148
|
:class="col.align && `rm-table__td--${col.align}`"
|
|
149
149
|
>
|
|
150
150
|
<slot :name="col.key" :value="getCellValue(row, col.key)" :row="row" :index="idx">
|
|
151
|
-
{{ getCellValue(row, col.key) ?? '
|
|
151
|
+
{{ getCellValue(row, col.key) ?? '-' }}
|
|
152
152
|
</slot>
|
|
153
153
|
</td>
|
|
154
154
|
</tr>
|
|
@@ -94,6 +94,31 @@ export declare function rmBaseOption(theme: RmChartTheme, decal?: boolean): {
|
|
|
94
94
|
borderWidth: number;
|
|
95
95
|
};
|
|
96
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Axe de valeurs, avec son libelle d'unite au-dessus quand il est fourni.
|
|
99
|
+
*
|
|
100
|
+
* `hidden` masque la graduation mais GARDE les lignes de fond : c'est le seul
|
|
101
|
+
* axe qu'on a le droit de cacher, et uniquement quand chaque point porte deja
|
|
102
|
+
* sa valeur en etiquette. Retirer aussi les lignes de fond ferait perdre toute
|
|
103
|
+
* echelle de comparaison entre deux courbes.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* `formatter` s'applique AUSSI a la graduation, pas seulement a l'infobulle :
|
|
107
|
+
* un axe qui affiche 400 000 000 la ou l'infobulle dit 400 M donne deux
|
|
108
|
+
* lectures du meme nombre sur le meme graphe.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Couleur d'une serie a l'indice `index`.
|
|
112
|
+
*
|
|
113
|
+
* La palette categorielle compte six teintes ; au-dela, un simple modulo
|
|
114
|
+
* redonne EXACTEMENT la meme couleur a deux series et rend la legende
|
|
115
|
+
* inutilisable. Chaque tour de palette eclaircit donc la teinte d'un cran :
|
|
116
|
+
* la septieme serie reprend la premiere couleur, en plus clair.
|
|
117
|
+
*
|
|
118
|
+
* Ce n'est pas un remplacement du double encodage : au-dela d'une douzaine de
|
|
119
|
+
* series, un graphe de lignes ne se lit plus, quelle que soit la palette.
|
|
120
|
+
*/
|
|
121
|
+
export declare function rmSeriesColor(palette: string[], index: number): string;
|
|
97
122
|
export interface RmSeriesInput {
|
|
98
123
|
name?: string;
|
|
99
124
|
data: (number | null)[];
|
|
@@ -111,13 +136,21 @@ export interface RmSeriesInput {
|
|
|
111
136
|
/** Bar chart vertical, une ou plusieurs series, empilable. */
|
|
112
137
|
export declare function rmBarVerticalOption(categories: (string | number)[], series: RmSeriesInput[], theme: RmChartTheme, palette: string[], decal?: boolean): {
|
|
113
138
|
legend: {
|
|
114
|
-
|
|
139
|
+
bottom: number;
|
|
140
|
+
left: string;
|
|
115
141
|
textStyle: {
|
|
116
142
|
color: string;
|
|
117
143
|
fontSize: number;
|
|
118
144
|
fontFamily: string;
|
|
119
145
|
};
|
|
120
146
|
} | undefined;
|
|
147
|
+
grid: {
|
|
148
|
+
bottom: number;
|
|
149
|
+
left: number;
|
|
150
|
+
right: number;
|
|
151
|
+
top: number;
|
|
152
|
+
containLabel: boolean;
|
|
153
|
+
};
|
|
121
154
|
xAxis: {
|
|
122
155
|
splitLine: {
|
|
123
156
|
show: boolean;
|
|
@@ -174,13 +207,6 @@ export declare function rmBarVerticalOption(categories: (string | number)[], ser
|
|
|
174
207
|
textStyle: {
|
|
175
208
|
fontFamily: string;
|
|
176
209
|
};
|
|
177
|
-
grid: {
|
|
178
|
-
left: number;
|
|
179
|
-
right: number;
|
|
180
|
-
top: number;
|
|
181
|
-
bottom: number;
|
|
182
|
-
containLabel: boolean;
|
|
183
|
-
};
|
|
184
210
|
tooltip: {
|
|
185
211
|
trigger: string;
|
|
186
212
|
backgroundColor: string;
|
|
@@ -300,7 +326,12 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
300
326
|
* graphe ne porte plus aucun chiffre.
|
|
301
327
|
*/
|
|
302
328
|
hideValueAxis?: boolean;
|
|
303
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* Position de la legende. Par defaut sous le graphe : la carte porte deja
|
|
331
|
+
* son titre en haut, et une legende juste dessous se lit comme un
|
|
332
|
+
* sous-titre. En bas, elle se lit comme ce qu'elle est, la cle des
|
|
333
|
+
* couleurs, une fois la courbe regardee.
|
|
334
|
+
*/
|
|
304
335
|
legendPosition?: 'top' | 'bottom';
|
|
305
336
|
/**
|
|
306
337
|
* Ancre l'axe a zero quand toutes les valeurs sont positives. Sans ca
|
|
@@ -389,10 +420,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
389
420
|
};
|
|
390
421
|
axisLabel: {
|
|
391
422
|
show: boolean;
|
|
392
|
-
fontSize?: undefined;
|
|
393
|
-
color?: undefined;
|
|
394
|
-
fontFamily?: undefined;
|
|
395
423
|
} | {
|
|
424
|
+
formatter?: ((value: number) => string) | undefined;
|
|
396
425
|
fontSize: number;
|
|
397
426
|
color: string;
|
|
398
427
|
fontFamily: string;
|
|
@@ -432,10 +461,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
432
461
|
};
|
|
433
462
|
axisLabel: {
|
|
434
463
|
show: boolean;
|
|
435
|
-
fontSize?: undefined;
|
|
436
|
-
color?: undefined;
|
|
437
|
-
fontFamily?: undefined;
|
|
438
464
|
} | {
|
|
465
|
+
formatter?: ((value: number) => string) | undefined;
|
|
439
466
|
fontSize: number;
|
|
440
467
|
color: string;
|
|
441
468
|
fontFamily: string;
|
|
@@ -467,10 +494,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
467
494
|
};
|
|
468
495
|
axisLabel: {
|
|
469
496
|
show: boolean;
|
|
470
|
-
fontSize?: undefined;
|
|
471
|
-
color?: undefined;
|
|
472
|
-
fontFamily?: undefined;
|
|
473
497
|
} | {
|
|
498
|
+
formatter?: ((value: number) => string) | undefined;
|
|
474
499
|
fontSize: number;
|
|
475
500
|
color: string;
|
|
476
501
|
fontFamily: string;
|
|
@@ -90,13 +90,24 @@ export function rmBaseOption(theme, decal = false) {
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
export function rmSeriesColor(palette, index) {
|
|
94
|
+
const base = palette[index % palette.length];
|
|
95
|
+
const cycle = Math.floor(index / palette.length);
|
|
96
|
+
if (cycle === 0 || !base.startsWith("#")) return base;
|
|
97
|
+
const hex = base.slice(1);
|
|
98
|
+
const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
|
|
99
|
+
const channels = [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16));
|
|
100
|
+
const ratio = Math.min(cycle * 0.28, 0.7);
|
|
101
|
+
const mixed = channels.map((value) => Math.round(value + (255 - value) * ratio));
|
|
102
|
+
return "#" + mixed.map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
103
|
+
}
|
|
104
|
+
const valueAxis = (theme, label, hidden = false, formatter) => ({
|
|
94
105
|
type: "value",
|
|
95
106
|
name: hidden ? void 0 : label,
|
|
96
107
|
nameLocation: "end",
|
|
97
108
|
nameGap: 12,
|
|
98
109
|
nameTextStyle: { fontSize: 10, color: theme.dimmedColor, fontFamily: FONT, align: "left" },
|
|
99
|
-
axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT },
|
|
110
|
+
axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT, ...formatter ? { formatter } : {} },
|
|
100
111
|
axisLine: hidden ? { show: false } : { lineStyle: { color: theme.axisLine } },
|
|
101
112
|
axisTick: hidden ? { show: false } : void 0,
|
|
102
113
|
splitLine: { lineStyle: { color: theme.splitLine } }
|
|
@@ -109,7 +120,10 @@ const axisCommon = (theme) => ({
|
|
|
109
120
|
export function rmBarVerticalOption(categories, series, theme, palette, decal = false) {
|
|
110
121
|
return {
|
|
111
122
|
...rmBaseOption(theme, decal),
|
|
112
|
-
legend: series.length > 1 ? {
|
|
123
|
+
legend: series.length > 1 ? { bottom: 0, left: "center", textStyle: { color: theme.mutedColor, fontSize: 11, fontFamily: FONT } } : void 0,
|
|
124
|
+
// La legende passee sous le graphe prend sa place dans la gouttiere basse,
|
|
125
|
+
// sinon elle recouvre les libelles de l'axe des abscisses.
|
|
126
|
+
grid: { ...rmBaseOption(theme, decal).grid, bottom: series.length > 1 ? 56 : 36 },
|
|
113
127
|
xAxis: { type: "category", data: categories, ...axisCommon(theme), splitLine: { show: false } },
|
|
114
128
|
yAxis: { type: "value", ...axisCommon(theme) },
|
|
115
129
|
series: series.map((s, i) => ({
|
|
@@ -117,7 +131,7 @@ export function rmBarVerticalOption(categories, series, theme, palette, decal =
|
|
|
117
131
|
name: s.name,
|
|
118
132
|
stack: s.stack,
|
|
119
133
|
data: s.data,
|
|
120
|
-
itemStyle: { color: s.color ?? palette
|
|
134
|
+
itemStyle: { color: s.color ?? rmSeriesColor(palette, i), borderRadius: [3, 3, 0, 0] }
|
|
121
135
|
}))
|
|
122
136
|
};
|
|
123
137
|
}
|
|
@@ -131,7 +145,7 @@ export function rmBarHorizontalOption(labels, values, theme, palette, decal = fa
|
|
|
131
145
|
type: "bar",
|
|
132
146
|
data: values,
|
|
133
147
|
itemStyle: {
|
|
134
|
-
color: (params) => palette
|
|
148
|
+
color: (params) => rmSeriesColor(palette, params.dataIndex),
|
|
135
149
|
borderRadius: [0, 3, 3, 0]
|
|
136
150
|
},
|
|
137
151
|
label: {
|
|
@@ -154,7 +168,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
154
168
|
axisLabels,
|
|
155
169
|
valueLabels = false,
|
|
156
170
|
hideValueAxis = false,
|
|
157
|
-
legendPosition = "
|
|
171
|
+
legendPosition = "bottom",
|
|
158
172
|
startAtZero = false,
|
|
159
173
|
valueFormatter
|
|
160
174
|
} = options;
|
|
@@ -192,13 +206,13 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
192
206
|
},
|
|
193
207
|
xAxis: { type: "category", data: categories, ...axisCommon(theme), splitLine: { show: false }, boundaryGap: false },
|
|
194
208
|
yAxis: dual ? [
|
|
195
|
-
{ ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
|
|
209
|
+
{ ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
|
|
196
210
|
// Le second axe ne rejoue pas la grille : deux grilles superposees
|
|
197
211
|
// rendent le fond illisible.
|
|
198
|
-
{ ...valueAxis(theme, axisLabels?.[1], hideValueAxis), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
|
|
199
|
-
] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
|
|
212
|
+
{ ...valueAxis(theme, axisLabels?.[1], hideValueAxis, valueFormatter), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
|
|
213
|
+
] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
|
|
200
214
|
series: series.map((s, i) => {
|
|
201
|
-
const color = s.color ?? palette
|
|
215
|
+
const color = s.color ?? rmSeriesColor(palette, i);
|
|
202
216
|
return {
|
|
203
217
|
type: "line",
|
|
204
218
|
name: s.name,
|
|
@@ -211,7 +225,9 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
211
225
|
// second marqueur au meme endroit. On l'annule par sa taille et non
|
|
212
226
|
// par `symbol: 'none'` : ECharts accroche les etiquettes aux
|
|
213
227
|
// symboles, les supprimer supprime aussi les etiquettes.
|
|
214
|
-
|
|
228
|
+
// Le point est la cible de survol : trop petit, l'infobulle se derobe
|
|
229
|
+
// et les series courtes n'ont plus de reperes lisibles.
|
|
230
|
+
symbolSize: valueLabels ? 0 : 8,
|
|
215
231
|
label: valueLabels ? {
|
|
216
232
|
show: true,
|
|
217
233
|
position: "top",
|
|
@@ -256,7 +272,7 @@ export function rmDonutOption(data, theme, palette, decal = false) {
|
|
|
256
272
|
data: data.map((d, i) => ({
|
|
257
273
|
value: d.value,
|
|
258
274
|
name: d.name,
|
|
259
|
-
itemStyle: { color: palette
|
|
275
|
+
itemStyle: { color: rmSeriesColor(palette, i) }
|
|
260
276
|
})),
|
|
261
277
|
label: { fontSize: 11, color: theme.mutedColor, fontFamily: FONT }
|
|
262
278
|
}]
|