@tekkare/romulus 0.1.39 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.38",
7
+ "version": "0.1.39",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -1 +1 @@
1
- .grid-2{grid-template-columns:repeat(2,1fr)}.grid-2,.grid-3{display:grid;gap:20px}.grid-3{grid-template-columns:repeat(3,1fr)}.grid-4{display:grid;gap:16px;grid-template-columns:repeat(4,1fr)}.grid-2-1{display:grid;gap:20px;grid-template-columns:2fr 1fr}.pane-title{color:var(--text-primary);font-family:var(--font-serif);font-size:32px;font-weight:700;letter-spacing:-.02em;line-height:1.1;margin:0 0 16px}.pane-sub{color:var(--text-muted);font-size:13px;line-height:1.55;margin:0 0 24px;max-width:78ch}.chart-section{margin-inline:auto;max-width:var(--content-max-w,1440px);padding:0 0 40px;width:100%}.section-label{align-items:center;color:var(--text-secondary);display:flex;font-size:11px;font-weight:700;gap:8px;letter-spacing:.12em;margin:40px 0 18px;text-transform:uppercase}body.text-large .pane-title{font-size:38px}body.text-xl .pane-title{font-size:46px}body.text-large .section-label{font-size:13px}body.text-xl .section-label{font-size:14px}.chart-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:10px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;transition:box-shadow .18s,border-color .18s}.chart-card:hover{border-color:var(--color-violet-pale);box-shadow:0 6px 16px rgba(107,63,255,.1),0 1px 3px rgba(30,41,59,.04)}.chart-card-head{padding:22px 64px 14px 24px;position:relative}.chart-card-title{color:var(--text-primary);font-size:15px;font-weight:700;letter-spacing:-.01em;line-height:1.3;margin:0 0 5px}.chart-card-context{color:var(--text-muted);font-size:12px;line-height:1.55;margin:0}.chart-card-actions{position:absolute;right:12px;top:12px}.cc-export{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;color:var(--text-secondary);cursor:pointer;display:flex;font-size:14px;height:28px;justify-content:center;transition:background .15s,color .15s,border-color .15s;width:28px}.cc-export:hover{color:var(--color-secondary)}.cc-export:hover,.chart-card-actions.open .cc-export{background:var(--bg-hover);border-color:var(--color-violet-pale)}.chart-card-actions.open .cc-export{color:var(--color-primary)}.cc-menu{animation:cc-menu-in .12s cubic-bezier(.2,0,0,1);background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);display:none;min-width:200px;padding:6px 0;position:absolute;right:0;top:34px;z-index:var(--z-modal)}.chart-card-actions.open .cc-menu{display:block}.cc-menu-section{color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;padding:8px 14px 4px;text-transform:uppercase}.cc-menu-item{align-items:center;background:none;border:0;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:13px;gap:10px;padding:7px 14px;text-align:left;width:100%}.cc-menu-item i{color:var(--text-muted);font-size:14px}.cc-menu-item:hover{background:var(--bg-surface-3);color:var(--text-primary)}.cc-menu-item:hover i{color:var(--text-secondary)}.cc-menu-divider{background:var(--bg-surface-3);height:1px;margin:4px 0}@keyframes cc-menu-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.chart-card-body{flex:1;padding:4px 12px 8px}.chart-card-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="prefixIcon" :name="prefixIcon" :size="size === 'sm' ? 14 : 16" />
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 && isActive.value && selected.value.length === 1) {
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 options"
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-chip__option{align-items:center;background:none;border:none;border-radius:4px;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);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)}
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 HybridTableRow {
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: HybridTableRow[]
15
- title?: string
16
- color?: string
17
- maxVisible?: number
18
- valueFormatter?: (val: number) => string
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
- title: undefined,
21
- color: '#4164EE',
22
- maxVisible: 10,
23
- valueFormatter: (val: number) => val.toLocaleString(),
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 maxValue = computed(() => Math.max(...props.rows.map(r => r.value), 1))
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 visibleRows = computed(() => {
31
- if (showAll.value) return props.rows
32
- return props.rows.slice(0, props.maxVisible)
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
- const hasMore = computed(() => props.rows.length > props.maxVisible)
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-table">
40
- <h3 v-if="title" class="hybrid-table__title">{{ title }}</h3>
41
-
42
- <div class="hybrid-table__list">
43
- <div
44
- v-for="(row, idx) in visibleRows"
45
- :key="idx"
46
- class="hybrid-table__row"
47
- >
48
- <span class="hybrid-table__label">{{ row.label }}</span>
49
- <div class="hybrid-table__bar-wrapper">
50
- <div
51
- class="hybrid-table__bar"
52
- :style="{ width: `${(row.value / maxValue) * 100}%`, backgroundColor: color }"
53
- />
54
- </div>
55
- <span class="hybrid-table__value">{{ valueFormatter(row.value) }}</span>
56
- </div>
57
- </div>
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
- <button
60
- v-if="hasMore"
61
- class="hybrid-table__toggle"
62
- @click="showAll = !showAll"
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-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}
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
- <h1 v-if="title" class="rm-page-header__title">{{ title }}</h1>
52
- <div v-else class="rm-page-header__title"><slot name="title" /></div>
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
- * SimpleBar — lightweight horizontal bar chart without ApexCharts.
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 SimpleBarItem {
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: SimpleBarItem[]
22
+ items: RmSimpleBarItem[]
16
23
  title?: string
17
- showValues?: boolean
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
- showValues: true,
23
- showPercent: false,
24
- height: 24,
32
+ height: 8,
33
+ hideLegend: false,
34
+ valueFormatter: rmNumber,
25
35
  })
26
36
 
27
- const { PALETTE } = useChartTheme()
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
- const total = computed(() => props.items.reduce((sum, i) => sum + i.value, 0))
30
- const maxValue = computed(() => Math.max(...props.items.map(i => i.value), 1))
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
- <h3 v-if="title" class="simple-bar__title">{{ title }}</h3>
51
+ <div class="rm-simple-bar">
52
+ <p v-if="title" class="rm-simple-bar__title">{{ title }}</p>
36
53
 
37
- <!-- Stacked bar -->
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, idx) in items"
41
- :key="idx"
42
- class="simple-bar__segment"
43
- :style="{
44
- width: `${(item.value / total) * 100}%`,
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
- <!-- Individual bars -->
51
- <div class="simple-bar__list">
52
- <div
53
- v-for="(item, idx) in items"
54
- :key="idx"
55
- class="simple-bar__item"
56
- >
57
- <div class="simple-bar__item-header">
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(--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}
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)[];
@@ -389,10 +414,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
389
414
  };
390
415
  axisLabel: {
391
416
  show: boolean;
392
- fontSize?: undefined;
393
- color?: undefined;
394
- fontFamily?: undefined;
395
417
  } | {
418
+ formatter?: ((value: number) => string) | undefined;
396
419
  fontSize: number;
397
420
  color: string;
398
421
  fontFamily: string;
@@ -432,10 +455,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
432
455
  };
433
456
  axisLabel: {
434
457
  show: boolean;
435
- fontSize?: undefined;
436
- color?: undefined;
437
- fontFamily?: undefined;
438
458
  } | {
459
+ formatter?: ((value: number) => string) | undefined;
439
460
  fontSize: number;
440
461
  color: string;
441
462
  fontFamily: string;
@@ -467,10 +488,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
467
488
  };
468
489
  axisLabel: {
469
490
  show: boolean;
470
- fontSize?: undefined;
471
- color?: undefined;
472
- fontFamily?: undefined;
473
491
  } | {
492
+ formatter?: ((value: number) => string) | undefined;
474
493
  fontSize: number;
475
494
  color: string;
476
495
  fontFamily: string;
@@ -90,13 +90,24 @@ export function rmBaseOption(theme, decal = false) {
90
90
  }
91
91
  };
92
92
  }
93
- const valueAxis = (theme, label, hidden = false) => ({
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 } }
@@ -117,7 +128,7 @@ export function rmBarVerticalOption(categories, series, theme, palette, decal =
117
128
  name: s.name,
118
129
  stack: s.stack,
119
130
  data: s.data,
120
- itemStyle: { color: s.color ?? palette[i % palette.length], borderRadius: [3, 3, 0, 0] }
131
+ itemStyle: { color: s.color ?? rmSeriesColor(palette, i), borderRadius: [3, 3, 0, 0] }
121
132
  }))
122
133
  };
123
134
  }
@@ -131,7 +142,7 @@ export function rmBarHorizontalOption(labels, values, theme, palette, decal = fa
131
142
  type: "bar",
132
143
  data: values,
133
144
  itemStyle: {
134
- color: (params) => palette[params.dataIndex % palette.length],
145
+ color: (params) => rmSeriesColor(palette, params.dataIndex),
135
146
  borderRadius: [0, 3, 3, 0]
136
147
  },
137
148
  label: {
@@ -192,13 +203,13 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
192
203
  },
193
204
  xAxis: { type: "category", data: categories, ...axisCommon(theme), splitLine: { show: false }, boundaryGap: false },
194
205
  yAxis: dual ? [
195
- { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
206
+ { ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
196
207
  // Le second axe ne rejoue pas la grille : deux grilles superposees
197
208
  // 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 },
209
+ { ...valueAxis(theme, axisLabels?.[1], hideValueAxis, valueFormatter), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
210
+ ] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
200
211
  series: series.map((s, i) => {
201
- const color = s.color ?? palette[i % palette.length];
212
+ const color = s.color ?? rmSeriesColor(palette, i);
202
213
  return {
203
214
  type: "line",
204
215
  name: s.name,
@@ -256,7 +267,7 @@ export function rmDonutOption(data, theme, palette, decal = false) {
256
267
  data: data.map((d, i) => ({
257
268
  value: d.value,
258
269
  name: d.name,
259
- itemStyle: { color: palette[i % palette.length] }
270
+ itemStyle: { color: rmSeriesColor(palette, i) }
260
271
  })),
261
272
  label: { fontSize: 11, color: theme.mutedColor, fontFamily: FONT }
262
273
  }]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",