@tekkare/romulus 0.1.38 → 0.1.39
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/shell.css +1 -1
- package/dist/runtime/components/ChartCard.vue +66 -16
- package/dist/runtime/components/ExportPanel.vue +4 -1
- package/dist/runtime/components/PageHeader.vue +28 -3
- package/dist/runtime/components/RailPanel.vue +14 -0
- package/dist/runtime/components/SidebarLeft.vue +38 -7
- package/dist/runtime/components/SidebarRight.vue +11 -2
- package/dist/runtime/composables/useRmExport.js +218 -74
- package/dist/runtime/utils/export.d.ts +48 -3
- package/dist/runtime/utils/export.js +9 -4
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--rm-shell-bg:#f9f9fa;--rm-shell-surface:#fff;--rm-shell-border:#dbdef0;--rm-shell-subtle:#f5f5f9;--rm-shell-text:#1e293b;--rm-shell-text-dark:#475569;--rm-shell-text-muted:#878ca6;--rm-shell-accent:#2017ad;--rm-shell-inset:8px;--rm-shell-radius:12px;--rm-bar-h:56px;--rm-nav-w:240px;--rm-nav-collapsed-w:64px;--rm-rail-w:56px;--rm-dots-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAcUlEQVR42u3TwQmAMAxAUdfQfUoSZ3OBTNMFHEqLVBEtCB6M8B8EevwktOsAAACAl0SkNzNXtaw6zut7SikNYeJq2Hksh4gs27rG7ZH+eWA7bttigMC78x4TIdBbceX8fJKnkSL1s5RY8zBxAAAA+LUF0ula10BxVDoAAAAASUVORK5CYII=");--rm-dots-size:12px;--rm-dots-opacity:0.1}body.night{--rm-shell-bg:var(--bg-app);--rm-shell-surface:var(--bg-surface,#1a1a24);--rm-shell-border:var(--border-default);--rm-shell-subtle:var(--bg-subtle,#22222e);--rm-shell-text:var(--text-primary);--rm-shell-text-dark:var(--text-secondary);--rm-shell-text-muted:var(--text-muted);--rm-shell-accent:var(--color-primary);--rm-dots-opacity:0.06}.rm-shell{background:var(--rm-shell-bg);color:var(--rm-shell-text);font-family:var(--font-sans,"Figtree",sans-serif);inset:0;overflow:hidden;position:fixed}.rm-shell__dots{background-image:var(--rm-dots-image);background-repeat:repeat;background-size:var(--rm-dots-size) var(--rm-dots-size);inset:0;opacity:var(--rm-dots-opacity);pointer-events:none;position:absolute}.rm-shell__frame{border:1px solid var(--rm-shell-border);border-radius:var(--rm-shell-radius);display:flex;inset:var(--rm-shell-inset);overflow:hidden;position:absolute}.rm-shell__body{display:flex;flex:1;flex-direction:column;min-width:0}.rm-shell__main{flex:1;min-width:0;overflow-y:auto}.rm-sb{background:var(--rm-shell-surface);display:flex;flex-direction:column;flex-shrink:0;height:100%}.rm-sb--left{border-right:1px solid var(--rm-shell-border);position:relative;transition:width .2s;width:var(--rm-nav-w);z-index:200}.rm-sb--left.rm-sb--collapsed{width:var(--rm-nav-collapsed-w)}.rm-sb--right{border-left:1px solid var(--rm-shell-border);width:var(--rm-rail-w)}.rm-sb__header{align-items:center;border-bottom:1px solid var(--rm-shell-border);display:flex;flex-shrink:0;gap:8px;height:var(--rm-bar-h);padding:0 16px}.rm-sb--collapsed .rm-sb__header{justify-content:center;padding:0}.rm-sb__logo{align-items:center;color:var(--rm-shell-text);display:flex;flex:1;font-size:20px;font-weight:700;gap:8px;line-height:1.4;min-width:0;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.rm-sb__toggle{-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;color:var(--rm-shell-text-muted);cursor:pointer;display:block;flex-shrink:0;height:20px;margin:0;padding:0;transition:color .15s;width:20px}.rm-sb__toggle:hover{color:var(--rm-shell-text)}.rm-sb__toggle svg{display:block;height:100%;width:100%}.rm-sb__nav{display:flex;flex:1;flex-direction:column;gap:16px;overflow-y:auto;padding:16px}.rm-sb--collapsed .rm-sb__nav{padding-left:8px;padding-right:8px}.rm-sb__group{display:flex;flex-direction:column;gap:4px}.rm-sb__label{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;color:var(--rm-shell-text-muted);display:flex;font-family:inherit;font-size:10px;font-weight:500;gap:4px;letter-spacing:.025em;line-height:1.6;margin:0;padding:0 2px;text-align:left;text-transform:uppercase;width:100%}button.rm-sb__label{cursor:pointer}button.rm-sb__label:hover{color:var(--rm-shell-text)}.rm-sb__label-text{flex:1;min-width:0}.rm-sb__chev{display:block;flex-shrink:0;height:12px;transition:transform .2s;width:12px}.rm-sb__label[aria-expanded=true] .rm-sb__chev{transform:rotate(90deg)}.rm-sb__rule{background:var(--rm-shell-border);height:1px;margin:4px}.rm-mi{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:4px;box-shadow:none;color:var(--rm-shell-text);cursor:pointer;display:flex;font-family:inherit;gap:8px;height:26px;margin:0;padding:2px;text-align:left;text-decoration:none;transition:background-color .15s;width:100%}.rm-mi:hover{background:color-mix(in srgb,var(--rm-shell-subtle) 50%,transparent)}.rm-mi.rm-mi--active{background:var(--rm-shell-subtle)}.rm-mi.rm-mi--current{background:color-mix(in srgb,var(--rm-shell-accent) 5%,transparent)}.rm-mi.rm-mi--current .rm-mi__icon{background:var(--rm-shell-accent);color:#fff}.rm-mi.rm-mi--current .rm-mi__icon svg{stroke:#fff}.rm-mi__icon{align-items:center;background:var(--rm-shell-subtle);border-radius:4px;color:var(--rm-shell-text-dark);display:flex;flex-shrink:0;height:22px;justify-content:center;line-height:1;width:22px}.rm-mi__icon svg{display:block;height:14px;width:14px}.rm-mi__icon i{font-size:14px;line-height:1}.rm-mi__label{color:var(--rm-shell-text);flex:1;font-size:14px;line-height:1.625;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-mi__badge{background:var(--rm-shell-subtle);border-radius:4px;color:var(--rm-shell-text-muted);flex-shrink:0;font-size:9px;font-weight:700;letter-spacing:.025em;padding:2px 4px;text-transform:uppercase}.rm-sb--collapsed .rm-mi{justify-content:center}.rm-sb--collapsed .rm-mi>:not(.rm-mi__icon),.rm-sb--collapsed .rm-sb__sub{display:none}.rm-sb__sub{display:flex;flex-direction:column;gap:2px;padding:2px 0 2px 28px}.rm-sb__sub-item{border-radius:4px;color:var(--rm-shell-text);display:block;font-size:14px;line-height:1.625;overflow:hidden;padding:2px 4px;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.rm-sb__sub-item:hover{background:color-mix(in srgb,var(--rm-shell-subtle) 50%,transparent)}.rm-rail__item{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;cursor:pointer;display:flex;flex-direction:column;font-family:inherit;gap:4px;margin:0;padding:12px 0 8px;text-decoration:none;width:100%}.rm-rail__avatar{align-items:center;border:2px solid var(--rm-shell-surface);border-radius:9999px;color:#fff;display:flex;font-size:14px;height:32px;justify-content:center;transition:opacity .15s;width:32px}.rm-rail__item:hover .rm-rail__avatar{opacity:.8}.rm-rail__tile{align-items:center;background:var(--rm-shell-subtle);border-radius:8px;color:var(--rm-shell-text-dark);display:flex;font-size:16px;height:32px;justify-content:center;transition:background-color .15s,color .15s;width:32px}.rm-rail__item:hover .rm-rail__tile{background:var(--rm-shell-border)}.rm-rail__item.rm-rail__item--active .rm-rail__tile{background:var(--rm-shell-accent);color:#fff}.rm-rail__caption{color:var(--rm-shell-text-muted);font-size:10px;line-height:1;max-width:100%;overflow:hidden;padding:0 2px;text-overflow:ellipsis;transition:color .15s;white-space:nowrap}.rm-rail__item--active .rm-rail__caption,.rm-rail__item:hover .rm-rail__caption{color:var(--rm-shell-text)}.rm-rail__bottom{display:flex;justify-content:center;margin-top:auto;padding-bottom:16px}.rm-lang{background:var(--rm-shell-subtle);border:1px solid var(--rm-shell-accent);border-radius:9999px;display:grid;gap:2px;grid-template-rows:1fr 1fr;padding:3px;position:relative}.rm-lang__thumb{background:var(--rm-shell-surface);border-radius:9999px;box-shadow:0 1px 4px rgba(30,41,59,.16);height:32px;left:3px;position:absolute;top:3px;transition:transform .26s cubic-bezier(.34,1.56,.64,1);width:32px}.rm-lang--second .rm-lang__thumb{transform:translateY(34px)}.rm-lang__option{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:9999px;box-shadow:none;cursor:pointer;display:flex;filter:grayscale(1);font-size:17px;height:32px;justify-content:center;line-height:1;margin:0;opacity:.5;padding:0;position:relative;transition:filter .18s ease,opacity .18s ease,transform .18s ease;width:32px}.rm-lang__option:hover{filter:grayscale(0);opacity:.85}.rm-lang__option.rm-lang__option--active{filter:grayscale(0);opacity:1}.rm-lang__option:focus-visible{outline:2px solid var(--rm-shell-accent);outline-offset:2px}@media (prefers-reduced-motion:reduce){.rm-lang__option,.rm-lang__thumb{transition:none}}.rm-hd{background:var(--rm-shell-surface);border-bottom:1px solid var(--rm-shell-border);display:flex;flex-direction:column;flex-shrink:0}.rm-hd__row{gap:16px;height:var(--rm-bar-h);padding:0 16px}.rm-hd__crumbs,.rm-hd__row{align-items:center;display:flex}.rm-hd__crumbs{flex:1;font-size:14px;gap:4px;min-width:0}.rm-hd__crumb{align-items:center;color:var(--rm-shell-text-muted);display:inline-flex;gap:6px;text-decoration:none;transition:color .15s;white-space:nowrap}a.rm-hd__crumb:hover{color:var(--rm-shell-text)}.rm-hd__sep{color:var(--rm-shell-text-muted)}.rm-hd__picker{display:inline-flex;min-width:0;position:relative}.rm-hd__crumb--picker{-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:4px;box-shadow:none;cursor:pointer;font:inherit;font-size:14px;margin:0;padding:2px 4px;transition:background-color .15s,color .15s}.rm-hd__crumb--picker:hover{background:var(--rm-shell-subtle)}.rm-hd__caret{display:block;flex-shrink:0;height:12px;opacity:.7;transition:transform .15s;width:12px}.rm-hd__caret--open{transform:rotate(180deg)}.rm-hd__menu{background:var(--rm-shell-surface);border:1px solid var(--rm-shell-border);border-radius:8px;box-shadow:0 8px 24px rgba(30,41,59,.12);left:0;max-height:60vh;max-width:320px;min-width:200px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);z-index:300}.rm-hd__menu-item{align-items:center;border-radius:4px;color:var(--rm-shell-text);display:flex;font-size:14px;gap:8px;line-height:1.5;overflow:hidden;padding:4px 6px;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.rm-hd__menu-item:hover{background:var(--rm-shell-subtle)}.rm-hd__menu-group{color:var(--rm-shell-text-muted);display:block;font-size:10px;font-weight:500;letter-spacing:.025em;line-height:1.6;padding:8px 6px 2px;text-transform:uppercase}.rm-hd__menu-group:first-child{padding-top:2px}.rm-hd__menu-item--active{background:color-mix(in srgb,var(--rm-shell-accent) 5%,transparent);font-weight:600}.rm-hd__menu-icon{align-items:center;background:var(--rm-shell-subtle);border-radius:4px;color:var(--rm-shell-text-dark);display:flex;flex-shrink:0;height:22px;justify-content:center;line-height:1;width:22px}.rm-hd__menu-icon i{font-size:14px;line-height:1}.rm-hd__menu-item--active .rm-hd__menu-icon{background:var(--rm-shell-accent);color:#fff}.rm-hd__crumb--last{color:var(--rm-shell-text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-hd__search{align-items:center;background:var(--rm-shell-surface);border:1px solid var(--rm-shell-border);border-radius:9999px;display:flex;flex-shrink:0;gap:4px;padding:4px 12px;width:300px}.rm-hd__search input{background:transparent;border:none;color:var(--rm-shell-text);flex:1;font-family:inherit;font-size:14px;line-height:20px;min-width:0;outline:none;padding:0}.rm-hd__search input::-moz-placeholder{color:var(--rm-shell-text-muted)}.rm-hd__search input::placeholder{color:var(--rm-shell-text-muted)}.rm-hd__search i,.rm-hd__search svg{color:var(--rm-shell-text-muted);display:block;flex-shrink:0;font-size:14px;height:14px;line-height:20px;width:14px}.rm-hd__tabs{display:flex;gap:16px;padding:0 16px}.rm-hd__tab{align-items:flex-start;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-bottom:2px solid transparent;border-radius:0;box-shadow:none;color:var(--rm-shell-text-dark);cursor:pointer;display:flex;flex-direction:column;font-family:inherit;font-size:14px;font-weight:700;line-height:1.6;margin:0;padding:4px 4px 8px;white-space:nowrap}.rm-hd__tab--active{border-bottom-color:var(--rm-shell-accent);color:var(--rm-shell-text)}@media (max-width:767px){.rm-sb--left{inset:0 auto 0 0;position:absolute;transform:translateX(-100%);transition:transform .2s;z-index:40}.rm-sb--left.rm-sb--open{transform:translateX(0)}.rm-sb--right{display:none}.rm-hd__search{flex:1;width:auto}}.rm-panel{background:var(--rm-shell-surface);border-left:1px solid var(--rm-shell-border);bottom:0;box-shadow:-8px 0 24px rgba(30,41,59,.08);display:flex;flex-direction:column;position:absolute;right:0;top:0;width:var(--rm-panel-w,360px);z-index:200}.rm-sb--right{position:relative;z-index:201}.rm-panel-enter-active,.rm-panel-leave-active,.rm-sb--right{transition:transform .22s cubic-bezier(.4,0,.2,1)}.rm-panel-enter-from,.rm-panel-leave-to{transform:translateX(100%)}@media (prefers-reduced-motion:reduce){.rm-panel-enter-active,.rm-panel-leave-active,.rm-sb--right{transition:none}}.rm-panel__header{align-items:center;border-bottom:1px solid var(--rm-shell-border);display:flex;flex-shrink:0;gap:8px;height:var(--rm-bar-h);padding:0 16px}.rm-panel__title{color:var(--rm-shell-text);flex:1;font-size:14px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-panel__close{-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;color:var(--rm-shell-text-muted);cursor:pointer;display:block;flex-shrink:0;height:20px;margin:0;padding:0;transition:color .15s;width:20px}.rm-panel__close:hover{color:var(--rm-shell-text)}.rm-panel__close svg{display:block;height:100%;width:100%}.rm-panel__body{flex:1;min-height:0;overflow-y:auto;padding:16px}@media (max-width:767px){.rm-panel{border-left:none;left:0;width:auto}}
|
|
1
|
+
:root{--rm-shell-bg:#f9f9fa;--rm-shell-surface:#fff;--rm-shell-border:#dbdef0;--rm-shell-subtle:#f5f5f9;--rm-shell-text:#1e293b;--rm-shell-text-dark:#475569;--rm-shell-text-muted:#878ca6;--rm-shell-accent:#2017ad;--rm-shell-inset:8px;--rm-shell-radius:12px;--rm-bar-h:56px;--rm-nav-w:240px;--rm-nav-collapsed-w:64px;--rm-rail-w:56px;--rm-dots-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAcUlEQVR42u3TwQmAMAxAUdfQfUoSZ3OBTNMFHEqLVBEtCB6M8B8EevwktOsAAACAl0SkNzNXtaw6zut7SikNYeJq2Hksh4gs27rG7ZH+eWA7bttigMC78x4TIdBbceX8fJKnkSL1s5RY8zBxAAAA+LUF0ula10BxVDoAAAAASUVORK5CYII=");--rm-dots-size:12px;--rm-dots-opacity:0.1}body.night{--rm-shell-bg:var(--bg-app);--rm-shell-surface:var(--bg-surface,#1a1a24);--rm-shell-border:var(--border-default);--rm-shell-subtle:var(--bg-subtle,#22222e);--rm-shell-text:var(--text-primary);--rm-shell-text-dark:var(--text-secondary);--rm-shell-text-muted:var(--text-muted);--rm-shell-accent:var(--color-primary);--rm-dots-opacity:0.06}.rm-shell{background:var(--rm-shell-bg);color:var(--rm-shell-text);font-family:var(--font-sans,"Figtree",sans-serif);inset:0;overflow:hidden;position:fixed}.rm-shell__dots{background-image:var(--rm-dots-image);background-repeat:repeat;background-size:var(--rm-dots-size) var(--rm-dots-size);inset:0;opacity:var(--rm-dots-opacity);pointer-events:none;position:absolute}.rm-shell__frame{border:1px solid var(--rm-shell-border);border-radius:var(--rm-shell-radius);display:flex;inset:var(--rm-shell-inset);overflow:hidden;position:absolute}.rm-shell__body{display:flex;flex:1;flex-direction:column;min-width:0}.rm-shell__main{flex:1;min-width:0;overflow-y:auto}.rm-sb{background:var(--rm-shell-surface);display:flex;flex-direction:column;flex-shrink:0;height:100%}.rm-sb--left{border-right:1px solid var(--rm-shell-border);position:relative;transition:width .2s;width:var(--rm-nav-w);z-index:200}.rm-sb--left.rm-sb--collapsed{width:var(--rm-nav-collapsed-w)}.rm-sb--right{border-left:1px solid var(--rm-shell-border);width:var(--rm-rail-w)}.rm-sb__header{align-items:center;border-bottom:1px solid var(--rm-shell-border);display:flex;flex-shrink:0;gap:8px;height:var(--rm-bar-h);padding:0 16px}.rm-sb--collapsed .rm-sb__header{justify-content:center;padding:0}.rm-sb__logo{align-items:center;color:var(--rm-shell-text);display:flex;flex:1;font-size:20px;font-weight:700;gap:8px;line-height:1.4;min-width:0;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.rm-sb__toggle{-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;color:var(--rm-shell-text-muted);cursor:pointer;display:block;flex-shrink:0;height:20px;margin:0;padding:0;transition:color .15s;width:20px}.rm-sb__toggle:hover{color:var(--rm-shell-text)}.rm-sb__toggle svg{display:block;height:100%;width:100%}.rm-sb__nav{display:flex;flex:1;flex-direction:column;gap:16px;overflow-y:auto;padding:16px}.rm-sb--collapsed .rm-sb__nav{padding-left:8px;padding-right:8px}.rm-sb__group{display:flex;flex-direction:column;gap:4px}.rm-sb__label{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;color:var(--rm-shell-text-muted);display:flex;font-family:inherit;font-size:10px;font-weight:500;gap:4px;letter-spacing:.025em;line-height:1.6;margin:0;padding:0 2px;text-align:left;text-transform:uppercase;width:100%}button.rm-sb__label{cursor:pointer}button.rm-sb__label:hover{color:var(--rm-shell-text)}.rm-sb__label-text{flex:1;min-width:0}.rm-sb__chev{display:block;flex-shrink:0;height:12px;transition:transform .2s;width:12px}.rm-sb__label[aria-expanded=true] .rm-sb__chev{transform:rotate(90deg)}.rm-sb__rule{background:var(--rm-shell-border);height:1px;margin:4px}.rm-mi{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:4px;box-shadow:none;color:var(--rm-shell-text);cursor:pointer;display:flex;font-family:inherit;gap:8px;height:26px;margin:0;padding:2px;text-align:left;text-decoration:none;transition:background-color .15s;width:100%}.rm-mi:hover{background:color-mix(in srgb,var(--rm-shell-subtle) 50%,transparent)}.rm-mi.rm-mi--active{background:var(--rm-shell-subtle)}.rm-mi.rm-mi--current{background:color-mix(in srgb,var(--rm-shell-accent) 5%,transparent)}.rm-mi.rm-mi--current .rm-mi__icon{background:var(--rm-shell-accent);color:#fff}.rm-mi.rm-mi--current .rm-mi__icon svg{stroke:#fff}.rm-mi__icon{align-items:center;background:var(--rm-shell-subtle);border-radius:4px;color:var(--rm-shell-text-dark);display:flex;flex-shrink:0;height:22px;justify-content:center;line-height:1;width:22px}.rm-mi__icon svg{display:block;height:14px;width:14px}.rm-mi__icon i{font-size:14px;line-height:1}.rm-mi__label{color:var(--rm-shell-text);flex:1;font-size:14px;line-height:1.625;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-mi__badge{background:var(--rm-shell-subtle);border-radius:4px;color:var(--rm-shell-text-muted);flex-shrink:0;font-size:9px;font-weight:700;letter-spacing:.025em;padding:2px 4px;text-transform:uppercase}.rm-sb--collapsed .rm-mi{justify-content:center}.rm-sb--collapsed .rm-mi>:not(.rm-mi__icon),.rm-sb--collapsed .rm-sb__sub{display:none}.rm-sb__sub{display:flex;flex-direction:column;gap:2px;padding:2px 0 2px 28px}.rm-sb__sub-item{border-radius:4px;color:var(--rm-shell-text);display:block;font-size:14px;line-height:1.625;overflow:hidden;padding:2px 4px;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.rm-sb__sub-item:hover{background:color-mix(in srgb,var(--rm-shell-subtle) 50%,transparent)}.rm-rail__item{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;cursor:pointer;display:flex;flex-direction:column;font-family:inherit;gap:4px;margin:0;padding:12px 0 8px;text-decoration:none;width:100%}.rm-rail__avatar{align-items:center;border:2px solid var(--rm-shell-surface);border-radius:9999px;color:#fff;display:flex;font-size:14px;height:32px;justify-content:center;transition:opacity .15s;width:32px}.rm-rail__item:hover .rm-rail__avatar{opacity:.8}.rm-rail__tile{align-items:center;background:var(--rm-shell-subtle);border-radius:8px;color:var(--rm-shell-text-dark);display:flex;font-size:16px;height:32px;justify-content:center;transition:background-color .15s,color .15s;width:32px}.rm-rail__item:hover .rm-rail__tile{background:var(--rm-shell-border)}.rm-rail__item.rm-rail__item--active .rm-rail__tile{background:var(--rm-shell-accent);color:#fff}.rm-rail__tile{position:relative}.rm-rail__badge{background:var(--rm-shell-accent);border-radius:9999px;color:#fff;font-size:10px;font-weight:700;height:16px;line-height:16px;min-width:16px;padding:0 4px;position:absolute;right:-4px;text-align:center;top:-4px}.rm-rail__item--active .rm-rail__badge{background:#fff;color:var(--rm-shell-accent)}.rm-rail__caption{color:var(--rm-shell-text-muted);font-size:10px;line-height:1;max-width:100%;overflow:hidden;padding:0 2px;text-overflow:ellipsis;transition:color .15s;white-space:nowrap}.rm-rail__item--active .rm-rail__caption,.rm-rail__item:hover .rm-rail__caption{color:var(--rm-shell-text)}.rm-rail__bottom{display:flex;justify-content:center;margin-top:auto;padding-bottom:16px}.rm-lang{background:var(--rm-shell-subtle);border:1px solid var(--rm-shell-accent);border-radius:9999px;display:grid;gap:2px;grid-template-rows:1fr 1fr;padding:3px;position:relative}.rm-lang__thumb{background:var(--rm-shell-surface);border-radius:9999px;box-shadow:0 1px 4px rgba(30,41,59,.16);height:32px;left:3px;position:absolute;top:3px;transition:transform .26s cubic-bezier(.34,1.56,.64,1);width:32px}.rm-lang--second .rm-lang__thumb{transform:translateY(34px)}.rm-lang__option{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:9999px;box-shadow:none;cursor:pointer;display:flex;filter:grayscale(1);font-size:17px;height:32px;justify-content:center;line-height:1;margin:0;opacity:.5;padding:0;position:relative;transition:filter .18s ease,opacity .18s ease,transform .18s ease;width:32px}.rm-lang__option:hover{filter:grayscale(0);opacity:.85}.rm-lang__option.rm-lang__option--active{filter:grayscale(0);opacity:1}.rm-lang__option:focus-visible{outline:2px solid var(--rm-shell-accent);outline-offset:2px}@media (prefers-reduced-motion:reduce){.rm-lang__option,.rm-lang__thumb{transition:none}}.rm-hd{background:var(--rm-shell-surface);border-bottom:1px solid var(--rm-shell-border);display:flex;flex-direction:column;flex-shrink:0}.rm-hd__row{gap:16px;height:var(--rm-bar-h);padding:0 16px}.rm-hd__crumbs,.rm-hd__row{align-items:center;display:flex}.rm-hd__crumbs{flex:1;font-size:14px;gap:4px;min-width:0}.rm-hd__crumb{align-items:center;color:var(--rm-shell-text-muted);display:inline-flex;gap:6px;text-decoration:none;transition:color .15s;white-space:nowrap}a.rm-hd__crumb:hover{color:var(--rm-shell-text)}.rm-hd__sep{color:var(--rm-shell-text-muted)}.rm-hd__picker{display:inline-flex;min-width:0;position:relative}.rm-hd__crumb--picker{-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:4px;box-shadow:none;cursor:pointer;font:inherit;font-size:14px;margin:0;padding:2px 4px;transition:background-color .15s,color .15s}.rm-hd__crumb--picker:hover{background:var(--rm-shell-subtle)}.rm-hd__caret{display:block;flex-shrink:0;height:12px;opacity:.7;transition:transform .15s;width:12px}.rm-hd__caret--open{transform:rotate(180deg)}.rm-hd__menu{background:var(--rm-shell-surface);border:1px solid var(--rm-shell-border);border-radius:8px;box-shadow:0 8px 24px rgba(30,41,59,.12);left:0;max-height:60vh;max-width:320px;min-width:200px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);z-index:300}.rm-hd__menu-item{align-items:center;border-radius:4px;color:var(--rm-shell-text);display:flex;font-size:14px;gap:8px;line-height:1.5;overflow:hidden;padding:4px 6px;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.rm-hd__menu-item:hover{background:var(--rm-shell-subtle)}.rm-hd__menu-group{color:var(--rm-shell-text-muted);display:block;font-size:10px;font-weight:500;letter-spacing:.025em;line-height:1.6;padding:8px 6px 2px;text-transform:uppercase}.rm-hd__menu-group:first-child{padding-top:2px}.rm-hd__menu-item--active{background:color-mix(in srgb,var(--rm-shell-accent) 5%,transparent);font-weight:600}.rm-hd__menu-icon{align-items:center;background:var(--rm-shell-subtle);border-radius:4px;color:var(--rm-shell-text-dark);display:flex;flex-shrink:0;height:22px;justify-content:center;line-height:1;width:22px}.rm-hd__menu-icon i{font-size:14px;line-height:1}.rm-hd__menu-item--active .rm-hd__menu-icon{background:var(--rm-shell-accent);color:#fff}.rm-hd__crumb--last{color:var(--rm-shell-text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-hd__search{align-items:center;background:var(--rm-shell-surface);border:1px solid var(--rm-shell-border);border-radius:9999px;display:flex;flex-shrink:0;gap:4px;padding:4px 12px;width:300px}.rm-hd__search input{background:transparent;border:none;color:var(--rm-shell-text);flex:1;font-family:inherit;font-size:14px;line-height:20px;min-width:0;outline:none;padding:0}.rm-hd__search input::-moz-placeholder{color:var(--rm-shell-text-muted)}.rm-hd__search input::placeholder{color:var(--rm-shell-text-muted)}.rm-hd__search i,.rm-hd__search svg{color:var(--rm-shell-text-muted);display:block;flex-shrink:0;font-size:14px;height:14px;line-height:20px;width:14px}.rm-hd__tabs{display:flex;gap:16px;padding:0 16px}.rm-hd__tab{align-items:flex-start;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-bottom:2px solid transparent;border-radius:0;box-shadow:none;color:var(--rm-shell-text-dark);cursor:pointer;display:flex;flex-direction:column;font-family:inherit;font-size:14px;font-weight:700;line-height:1.6;margin:0;padding:4px 4px 8px;white-space:nowrap}.rm-hd__tab--active{border-bottom-color:var(--rm-shell-accent);color:var(--rm-shell-text)}@media (max-width:767px){.rm-sb--left{inset:0 auto 0 0;position:absolute;transform:translateX(-100%);transition:transform .2s;z-index:40}.rm-sb--left.rm-sb--open{transform:translateX(0)}.rm-sb--right{display:none}.rm-hd__search{flex:1;width:auto}}.rm-panel{background:var(--rm-shell-surface);border-left:1px solid var(--rm-shell-border);bottom:0;box-shadow:-8px 0 24px rgba(30,41,59,.08);display:flex;flex-direction:column;position:absolute;right:0;top:0;width:var(--rm-panel-w,360px);z-index:401}.rm-panel__veil{backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);background:var(--rm-panel-veil,rgba(100,116,139,.12));cursor:default;inset:0;position:absolute;z-index:400}.rm-panel-veil-enter-active,.rm-panel-veil-leave-active{transition:opacity .22s ease}.rm-panel-veil-enter-from,.rm-panel-veil-leave-to{opacity:0}@media (prefers-reduced-motion:reduce){.rm-panel-veil-enter-active,.rm-panel-veil-leave-active{transition:none}}.rm-sb--right{position:relative;z-index:402}.rm-panel-enter-active,.rm-panel-leave-active,.rm-sb--right{transition:transform .22s cubic-bezier(.4,0,.2,1)}.rm-panel-enter-from,.rm-panel-leave-to{transform:translateX(100%)}@media (prefers-reduced-motion:reduce){.rm-panel-enter-active,.rm-panel-leave-active,.rm-sb--right{transition:none}}.rm-panel__header{align-items:center;border-bottom:1px solid var(--rm-shell-border);display:flex;flex-shrink:0;gap:8px;height:var(--rm-bar-h);padding:0 16px}.rm-panel__title{color:var(--rm-shell-text);flex:1;font-size:14px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-panel__close{-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:0;box-shadow:none;color:var(--rm-shell-text-muted);cursor:pointer;display:block;flex-shrink:0;height:20px;margin:0;padding:0;transition:color .15s;width:20px}.rm-panel__close:hover{color:var(--rm-shell-text)}.rm-panel__close svg{display:block;height:100%;width:100%}.rm-panel__body{flex:1;min-height:0;overflow-y:auto;padding:16px}@media (max-width:767px){.rm-panel{border-left:none;left:0;width:auto}}
|
|
@@ -20,8 +20,14 @@ const props = withDefaults(defineProps<{
|
|
|
20
20
|
context?: string
|
|
21
21
|
/** Organisme public : "HAS", "OCDE", "ANSM (Repertoire des specialites)". Jamais un outil interne. */
|
|
22
22
|
source: string
|
|
23
|
-
/** Date de notre verification. Un Date est formate en
|
|
23
|
+
/** Date de notre verification. Un Date est formate en long ; une chaine deja formatee passe telle quelle. */
|
|
24
24
|
verified: Date | string | number
|
|
25
|
+
/**
|
|
26
|
+
* Langue du chrome de la carte : pied, menu d'actions, corps vide ou en
|
|
27
|
+
* erreur. Le titre, le sous-titre et la source restent a la page, qui les
|
|
28
|
+
* ecrit deja dans sa langue.
|
|
29
|
+
*/
|
|
30
|
+
lang?: 'fr' | 'en'
|
|
25
31
|
/** Complement de fin de foot : frequence, millesime. */
|
|
26
32
|
extra?: string
|
|
27
33
|
/** Masque le menu d'actions quand la carte n'a rien a exporter. */
|
|
@@ -41,6 +47,7 @@ const props = withDefaults(defineProps<{
|
|
|
41
47
|
emptyActionLabel?: string
|
|
42
48
|
}>(), {
|
|
43
49
|
context: undefined,
|
|
50
|
+
lang: 'fr',
|
|
44
51
|
extra: undefined,
|
|
45
52
|
actions: true,
|
|
46
53
|
state: 'ready',
|
|
@@ -64,11 +71,54 @@ const emit = defineEmits<{
|
|
|
64
71
|
const menuOpen = ref(false)
|
|
65
72
|
const root = ref<HTMLElement | null>(null)
|
|
66
73
|
|
|
74
|
+
/** Le chrome de la carte, dans les deux langues du portail. */
|
|
75
|
+
const WORDS = {
|
|
76
|
+
fr: {
|
|
77
|
+
actions: 'Actions sur cette carte',
|
|
78
|
+
exportSection: 'Exporter',
|
|
79
|
+
png: 'Image PNG',
|
|
80
|
+
svg: 'Vectoriel SVG',
|
|
81
|
+
csv: 'Données CSV',
|
|
82
|
+
xlsx: 'Excel (XLSX)',
|
|
83
|
+
shareSection: 'Partager',
|
|
84
|
+
copyLink: 'Copier le lien direct',
|
|
85
|
+
fullscreen: 'Plein écran',
|
|
86
|
+
source: 'Source :',
|
|
87
|
+
verified: 'Vérifié le',
|
|
88
|
+
emptyTitle: 'Aucun résultat',
|
|
89
|
+
emptyText:
|
|
90
|
+
'Aucune ligne ne correspond aux filtres en cours. Élargissez votre période ou retirez une dimension.',
|
|
91
|
+
errorText: "La donnée de cette carte n'a pas pu être chargée.",
|
|
92
|
+
},
|
|
93
|
+
en: {
|
|
94
|
+
actions: 'Card actions',
|
|
95
|
+
exportSection: 'Export',
|
|
96
|
+
png: 'PNG image',
|
|
97
|
+
svg: 'SVG vector',
|
|
98
|
+
csv: 'CSV data',
|
|
99
|
+
xlsx: 'Excel (XLSX)',
|
|
100
|
+
shareSection: 'Share',
|
|
101
|
+
copyLink: 'Copy direct link',
|
|
102
|
+
fullscreen: 'Fullscreen',
|
|
103
|
+
source: 'Source:',
|
|
104
|
+
verified: 'Verified on',
|
|
105
|
+
emptyTitle: 'No results',
|
|
106
|
+
emptyText:
|
|
107
|
+
'No row matches the current filters. Widen your period or drop a dimension.',
|
|
108
|
+
errorText: 'This card could not load its data.',
|
|
109
|
+
},
|
|
110
|
+
} as const
|
|
111
|
+
|
|
112
|
+
const t = computed(() => WORDS[props.lang])
|
|
113
|
+
|
|
67
114
|
// Une chaine deja formatee ("17 juin 2026") n'est pas parsable : on la garde
|
|
68
115
|
// telle quelle plutot que d'afficher "Invalid Date".
|
|
69
116
|
const verifiedText = computed(() => {
|
|
70
117
|
const parsed = new Date(props.verified)
|
|
71
|
-
|
|
118
|
+
if (Number.isNaN(parsed.getTime())) return String(props.verified)
|
|
119
|
+
return props.lang === 'en'
|
|
120
|
+
? parsed.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' })
|
|
121
|
+
: rmDate(parsed)
|
|
72
122
|
})
|
|
73
123
|
|
|
74
124
|
function onDocumentClick(event: MouseEvent) {
|
|
@@ -104,33 +154,33 @@ function run(action: 'exportPng' | 'exportSvg' | 'exportCsv' | 'exportXlsx' | 'c
|
|
|
104
154
|
<button
|
|
105
155
|
class="cc-export"
|
|
106
156
|
type="button"
|
|
107
|
-
aria-label="
|
|
157
|
+
:aria-label="t.actions"
|
|
108
158
|
:aria-expanded="menuOpen"
|
|
109
159
|
@click.stop="menuOpen = !menuOpen"
|
|
110
160
|
>
|
|
111
161
|
<RmIcon name="Gear" :size="14" />
|
|
112
162
|
</button>
|
|
113
163
|
<div class="cc-menu" role="menu">
|
|
114
|
-
<div class="cc-menu-section">
|
|
164
|
+
<div class="cc-menu-section">{{ t.exportSection }}</div>
|
|
115
165
|
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportPng')">
|
|
116
|
-
<RmIcon name="Image" :size="14" />
|
|
166
|
+
<RmIcon name="Image" :size="14" /> {{ t.png }}
|
|
117
167
|
</button>
|
|
118
168
|
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportSvg')">
|
|
119
|
-
<RmIcon name="VectorThree" :size="14" />
|
|
169
|
+
<RmIcon name="VectorThree" :size="14" /> {{ t.svg }}
|
|
120
170
|
</button>
|
|
121
171
|
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportCsv')">
|
|
122
|
-
<RmIcon name="FileCsv" :size="14" />
|
|
172
|
+
<RmIcon name="FileCsv" :size="14" /> {{ t.csv }}
|
|
123
173
|
</button>
|
|
124
174
|
<button class="cc-menu-item" type="button" role="menuitem" @click="run('exportXlsx')">
|
|
125
|
-
<RmIcon name="FileXls" :size="14" />
|
|
175
|
+
<RmIcon name="FileXls" :size="14" /> {{ t.xlsx }}
|
|
126
176
|
</button>
|
|
127
177
|
<div class="cc-menu-divider" />
|
|
128
|
-
<div class="cc-menu-section">
|
|
178
|
+
<div class="cc-menu-section">{{ t.shareSection }}</div>
|
|
129
179
|
<button class="cc-menu-item" type="button" role="menuitem" @click="run('copyLink')">
|
|
130
|
-
<RmIcon name="Link" :size="14" />
|
|
180
|
+
<RmIcon name="Link" :size="14" /> {{ t.copyLink }}
|
|
131
181
|
</button>
|
|
132
182
|
<button class="cc-menu-item" type="button" role="menuitem" @click="run('fullscreen')">
|
|
133
|
-
<RmIcon name="ArrowsOut" :size="14" />
|
|
183
|
+
<RmIcon name="ArrowsOut" :size="14" /> {{ t.fullscreen }}
|
|
134
184
|
</button>
|
|
135
185
|
</div>
|
|
136
186
|
</div>
|
|
@@ -148,8 +198,8 @@ function run(action: 'exportPng' | 'exportSvg' | 'exportCsv' | 'exportXlsx' | 'c
|
|
|
148
198
|
<slot v-else-if="state === 'empty'" name="empty">
|
|
149
199
|
<RmEmptyState
|
|
150
200
|
icon="MagnifyingGlassMinus"
|
|
151
|
-
title="
|
|
152
|
-
text="
|
|
201
|
+
:title="t.emptyTitle"
|
|
202
|
+
:text="t.emptyText"
|
|
153
203
|
:action-label="emptyActionLabel"
|
|
154
204
|
action-icon="Broom"
|
|
155
205
|
@action="emit('emptyAction')"
|
|
@@ -157,7 +207,7 @@ function run(action: 'exportPng' | 'exportSvg' | 'exportCsv' | 'exportXlsx' | 'c
|
|
|
157
207
|
</slot>
|
|
158
208
|
<slot v-else name="error">
|
|
159
209
|
<RmErrorState
|
|
160
|
-
text="
|
|
210
|
+
:text="t.errorText"
|
|
161
211
|
:action-label="retryLabel"
|
|
162
212
|
@action="emit('retry')"
|
|
163
213
|
/>
|
|
@@ -165,9 +215,9 @@ function run(action: 'exportPng' | 'exportSvg' | 'exportCsv' | 'exportXlsx' | 'c
|
|
|
165
215
|
</div>
|
|
166
216
|
|
|
167
217
|
<div class="chart-card-foot">
|
|
168
|
-
<span class="src">
|
|
218
|
+
<span class="src">{{ t.source }} <b>{{ source }}</b></span>
|
|
169
219
|
<span class="dot" />
|
|
170
|
-
<span class="updated">
|
|
220
|
+
<span class="updated">{{ t.verified }} {{ verifiedText }}</span>
|
|
171
221
|
<template v-if="extra">
|
|
172
222
|
<span class="dot" />
|
|
173
223
|
<span>{{ extra }}</span>
|
|
@@ -250,7 +250,10 @@ function enabled(section: RmExportSection) {
|
|
|
250
250
|
if (!format.value) return false
|
|
251
251
|
if (section.kind === 'table') return !!format.value.tabular
|
|
252
252
|
if (section.kind === 'visual') return !!format.value.visual
|
|
253
|
-
|
|
253
|
+
// Une fiche suppose une mise en page ; un en-tete de document suppose une
|
|
254
|
+
// page de garde. Le classeur a la seconde sans la premiere.
|
|
255
|
+
if (section.kind === 'cards') return !!format.value.paged
|
|
256
|
+
return !!format.value.document
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
const showColumns = computed(() => !!format.value?.tabular && props.columns.length > 0)
|
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
se colle en haut de la colonne de contenu quand elle defile ; la barre de
|
|
6
6
|
filtres (RmFilterBar) se cale sous lui en suivant son id `rm_page_header`.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Il porte aussi les onglets de la page, sous le titre : c'etait la derniere
|
|
9
|
+
raison de garder `arg-header` dans les apps. Le dessin est celui des onglets
|
|
10
|
+
du bandeau de cadre (RmAppHeader) : libelle souligne quand il est actif.
|
|
11
|
+
|
|
12
|
+
Slots : #title (a la place de la prop), #actions (a droite du titre),
|
|
13
|
+
defaut (sous la rangee du titre, avant les onglets).
|
|
9
14
|
-->
|
|
10
15
|
<script setup lang="ts">
|
|
11
16
|
import { useRouter } from 'vue-router'
|
|
17
|
+
import type { RmHeaderTab } from './AppHeader.vue'
|
|
12
18
|
|
|
13
19
|
withDefaults(
|
|
14
20
|
defineProps<{
|
|
@@ -17,10 +23,16 @@ withDefaults(
|
|
|
17
23
|
haveReturn?: boolean
|
|
18
24
|
/** Destination du retour ; sans valeur, l'historique. */
|
|
19
25
|
backTo?: string | null
|
|
26
|
+
/** Onglets de la page. La page dit lequel est actif et ecoute `tab`. */
|
|
27
|
+
tabs?: RmHeaderTab[]
|
|
28
|
+
/** Nom du groupe d'onglets pour les lecteurs d'ecran. */
|
|
29
|
+
tabsLabel?: string
|
|
20
30
|
}>(),
|
|
21
|
-
{ title: null, haveReturn: false, backTo: null },
|
|
31
|
+
{ title: null, haveReturn: false, backTo: null, tabs: () => [], tabsLabel: undefined },
|
|
22
32
|
)
|
|
23
33
|
|
|
34
|
+
const emit = defineEmits<{ tab: [key: string] }>()
|
|
35
|
+
|
|
24
36
|
const router = useRouter()
|
|
25
37
|
</script>
|
|
26
38
|
|
|
@@ -42,9 +54,22 @@ const router = useRouter()
|
|
|
42
54
|
<div v-if="$slots.actions" class="rm-page-header__actions"><slot name="actions" /></div>
|
|
43
55
|
</div>
|
|
44
56
|
<slot />
|
|
57
|
+
|
|
58
|
+
<div v-if="tabs.length" class="rm-page-header__tabs" role="tablist" :aria-label="tabsLabel">
|
|
59
|
+
<button
|
|
60
|
+
v-for="tab in tabs"
|
|
61
|
+
:key="tab.key"
|
|
62
|
+
class="rm-page-header__tab"
|
|
63
|
+
:class="{ 'rm-page-header__tab--active': tab.active }"
|
|
64
|
+
type="button"
|
|
65
|
+
role="tab"
|
|
66
|
+
:aria-selected="tab.active ? 'true' : 'false'"
|
|
67
|
+
@click="emit('tab', tab.key)"
|
|
68
|
+
>{{ tab.label }}</button>
|
|
69
|
+
</div>
|
|
45
70
|
</header>
|
|
46
71
|
</template>
|
|
47
72
|
|
|
48
73
|
<style scoped>
|
|
49
|
-
.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}
|
|
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}
|
|
50
75
|
</style>
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
L'en-tete reprend la rangee de 56px du bandeau pour que le titre du panneau
|
|
13
13
|
s'aligne sur le fil d'Ariane, a la ligne pres.
|
|
14
|
+
|
|
15
|
+
Un voile flou et grise couvre le reste du cadre pendant l'ouverture, barre
|
|
16
|
+
laterale comprise : le panneau est ce qu'on lit, pas ce qu'on lisait. Un
|
|
17
|
+
clic sur le voile referme le panneau, comme un tiroir.
|
|
14
18
|
-->
|
|
15
19
|
<script setup lang="ts">
|
|
16
20
|
withDefaults(defineProps<{
|
|
@@ -30,6 +34,16 @@ const emit = defineEmits<{ close: [] }>()
|
|
|
30
34
|
</script>
|
|
31
35
|
|
|
32
36
|
<template>
|
|
37
|
+
<!-- Voile : le panneau ouvert prend la main, le reste du cadre passe en
|
|
38
|
+
flou grise. Un element a part plutot qu'un `filter` sur le contenu,
|
|
39
|
+
qui creerait un bloc conteneur et decrocherait les bandeaux collants
|
|
40
|
+
des pages. Il prend les clics et referme le panneau : c'est le geste
|
|
41
|
+
attendu de tout ce qui voile la page. Le clavier, lui, garde le bouton
|
|
42
|
+
de fermeture de l'en-tete, seul element expose. -->
|
|
43
|
+
<Transition name="rm-panel-veil">
|
|
44
|
+
<div v-if="open" class="rm-panel__veil" aria-hidden="true" @click="emit('close')" />
|
|
45
|
+
</Transition>
|
|
46
|
+
|
|
33
47
|
<Transition name="rm-panel">
|
|
34
48
|
<aside v-if="open" class="rm-panel" :style="{ '--rm-panel-w': width }">
|
|
35
49
|
<div class="rm-panel__header">
|
|
@@ -77,15 +77,46 @@ const collapsibles = computed(() =>
|
|
|
77
77
|
props.sections.filter(s => s.label && s.collapsible !== false),
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
/** Chemin comparable : sans query ni slash final. */
|
|
81
|
+
const trim = (path: string) => (path.split('?')[0] || '').replace(/\/+$/, '') || '/'
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Section ouverte : celle qui porte la page courante, sinon la premiere.
|
|
85
|
+
*
|
|
86
|
+
* L'entree marquee `active` decide d'abord. C'est l'app qui la calcule, et
|
|
87
|
+
* elle en sait plus que nous : son `to` passe par `localePath`, donc prefixe
|
|
88
|
+
* de la locale et parfois suivi d'un slash, quand `activePath` est brut. Une
|
|
89
|
+
* egalite stricte entre les deux ne tombait jamais juste et toutes les pages
|
|
90
|
+
* repliaient la premiere section.
|
|
91
|
+
*
|
|
92
|
+
* A defaut de drapeau, le chemin sert de recours : egalite d'abord, puis
|
|
93
|
+
* prefixe, pour qu'une sous-page (« /prices/jo ») ouvre la section de son
|
|
94
|
+
* entree.
|
|
95
|
+
*/
|
|
96
|
+
const sectionOf = (path?: string) => {
|
|
97
|
+
const byActive = collapsibles.value.find(s => s.items.some(i => i.active))
|
|
98
|
+
if (byActive) return byActive.key
|
|
99
|
+
|
|
100
|
+
const here = path ? trim(path) : null
|
|
101
|
+
if (here) {
|
|
102
|
+
const byPath = collapsibles.value.find(s =>
|
|
103
|
+
s.items.some((i) => {
|
|
104
|
+
if (!i.to) return false
|
|
105
|
+
const to = trim(i.to)
|
|
106
|
+
return here === to || here.startsWith(`${to}/`)
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
if (byPath) return byPath.key
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return collapsibles.value[0]?.key ?? null
|
|
113
|
+
}
|
|
85
114
|
|
|
86
115
|
const openSection = ref<string | null>(sectionOf(props.activePath))
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
// La cible se deduit des sections autant que du chemin : les deux arrivent
|
|
117
|
+
// ensemble a la navigation, et les droits d'acces peuvent recomposer la liste
|
|
118
|
+
// en cours de route.
|
|
119
|
+
watch(() => sectionOf(props.activePath), (key) => {
|
|
89
120
|
if (key && key !== openSection.value) openSection.value = key
|
|
90
121
|
})
|
|
91
122
|
|
|
@@ -20,6 +20,12 @@ export interface RmRailTile {
|
|
|
20
20
|
label: string
|
|
21
21
|
/** Icone Phosphor, nom en kebab-case sans le prefixe `ph-`. */
|
|
22
22
|
icon: string
|
|
23
|
+
/**
|
|
24
|
+
* Pastille de comptage posee sur la tuile (nombre de sources de la page,
|
|
25
|
+
* par exemple). Rien n'est dessine a zero ou sans valeur : une pastille
|
|
26
|
+
* vide dirait qu'il y a quelque chose a voir.
|
|
27
|
+
*/
|
|
28
|
+
count?: number
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export interface RmRailLocale {
|
|
@@ -87,11 +93,14 @@ const second = computed(() =>
|
|
|
87
93
|
class="rm-rail__item"
|
|
88
94
|
:class="{ 'rm-rail__item--active': active === tile.key }"
|
|
89
95
|
type="button"
|
|
90
|
-
:title="tile.label"
|
|
96
|
+
:title="tile.count ? `${tile.label} (${tile.count})` : tile.label"
|
|
91
97
|
:aria-pressed="active === tile.key"
|
|
92
98
|
@click="emit('select', tile.key)"
|
|
93
99
|
>
|
|
94
|
-
<span class="rm-rail__tile"
|
|
100
|
+
<span class="rm-rail__tile">
|
|
101
|
+
<i :class="`ph ph-${tile.icon}`" />
|
|
102
|
+
<span v-if="tile.count" class="rm-rail__badge">{{ tile.count > 99 ? '99+' : tile.count }}</span>
|
|
103
|
+
</span>
|
|
95
104
|
<span class="rm-rail__caption">{{ tile.label }}</span>
|
|
96
105
|
</button>
|
|
97
106
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RM_EXPORT_EXTENSIONS } from "../utils/export.js";
|
|
2
2
|
const BRAND_TITLE = "OIP Healthcare";
|
|
3
|
-
const BRAND_LINE = "
|
|
3
|
+
const BRAND_LINE = "By Tekkare \u{1F49C}";
|
|
4
|
+
const COVER_SHEET = "Tekkare";
|
|
4
5
|
function longDate(lang) {
|
|
5
6
|
return (/* @__PURE__ */ new Date()).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
6
7
|
year: "numeric",
|
|
@@ -16,6 +17,19 @@ function columnsOf(sheet, keep) {
|
|
|
16
17
|
const kept = sheet.schema.filter((column) => keep.includes(column.column));
|
|
17
18
|
return kept.length ? kept : sheet.schema;
|
|
18
19
|
}
|
|
20
|
+
const XLSX_CELL_MAX = 32767;
|
|
21
|
+
function xlsxSafeText(text) {
|
|
22
|
+
return text.length > XLSX_CELL_MAX ? `${text.slice(0, XLSX_CELL_MAX - 1)}\u2026` : text;
|
|
23
|
+
}
|
|
24
|
+
function xlsxColumnsOf(sheet, keep) {
|
|
25
|
+
return columnsOf(sheet, keep).map((column) => ({
|
|
26
|
+
...column,
|
|
27
|
+
value: (row) => {
|
|
28
|
+
const value = column.value(row);
|
|
29
|
+
return typeof value === "string" ? xlsxSafeText(value) : value;
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
19
33
|
function cellText(value, lang) {
|
|
20
34
|
if (value === null || value === void 0) return "";
|
|
21
35
|
if (value instanceof Date) {
|
|
@@ -76,63 +90,101 @@ async function raster(markup, width, height, scale = 2) {
|
|
|
76
90
|
function printable(text) {
|
|
77
91
|
return text.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
|
|
78
92
|
}
|
|
93
|
+
function cardsOf(payload, config, lang) {
|
|
94
|
+
if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ?? 100);
|
|
95
|
+
const sheet = payload.sheets[0];
|
|
96
|
+
if (!sheet) return [];
|
|
97
|
+
const pick = (name) => name ? sheet.schema.find((column) => column.column === name) : void 0;
|
|
98
|
+
const title = pick(sheet.titleColumn);
|
|
99
|
+
const subtitle = pick(sheet.subtitleColumn);
|
|
100
|
+
const columns = columnsOf(sheet, config.columns);
|
|
101
|
+
const head = title ?? columns[0];
|
|
102
|
+
if (!head) return [];
|
|
103
|
+
const fields = columns.filter((column) => column !== head && column !== subtitle);
|
|
104
|
+
return rowsOf(sheet, payload.cardLimit ?? 100).map((row) => ({
|
|
105
|
+
title: cellText(head.value(row), lang) || "\u2014",
|
|
106
|
+
subtitle: subtitle ? cellText(subtitle.value(row), lang) : void 0,
|
|
107
|
+
fields: fields.map((column) => ({ label: column.column, value: cellText(column.value(row), lang) })).filter((field) => field.value !== "")
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
79
110
|
export function useRmExport() {
|
|
80
111
|
function coverSheet(payload, config, lang) {
|
|
81
|
-
const
|
|
112
|
+
const COLUMNS = 40;
|
|
113
|
+
const TEXT_COLUMN = 7;
|
|
114
|
+
const KPI_COLUMNS = [6, 8];
|
|
115
|
+
const base = {
|
|
116
|
+
borderColor: "#FFFFFF",
|
|
117
|
+
align: "center",
|
|
118
|
+
alignVertical: "center",
|
|
119
|
+
fontFamily: "Figtree"
|
|
120
|
+
};
|
|
121
|
+
const empty = { value: "", borderColor: "#FFFFFF" };
|
|
122
|
+
const filler = () => Array.from({ length: COLUMNS }, () => ({ value: "", borderColor: "#FFFFFF", width: 20, height: 30 }));
|
|
123
|
+
const valueStyle = { ...base, fontSize: 18, fontWeight: "bold" };
|
|
124
|
+
const labelStyle = { ...base, fontSize: 18, color: "#b2b2b2", fontStyle: "italic" };
|
|
82
125
|
const rows = [];
|
|
83
|
-
const
|
|
126
|
+
const spread = (cells) => rows.push(Array.from({ length: COLUMNS }, (_, index) => cells[index] ?? empty));
|
|
127
|
+
const line = (value, style = {}) => spread({ [TEXT_COLUMN]: { ...base, value: xlsxSafeText(String(value ?? "")), ...style } });
|
|
128
|
+
rows.push(filler(), filler(), filler());
|
|
84
129
|
if (config.options.includes("logo")) {
|
|
85
|
-
line(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
line(payload.title, { fontSize: 20, fontWeight: "bold" });
|
|
90
|
-
if (payload.subtitle) line(payload.subtitle, { fontSize: 13, color: "#6C6C8A" });
|
|
91
|
-
line(
|
|
92
|
-
lang === "en" ? `Generated on ${longDate(lang)}` : `G\xE9n\xE9r\xE9 le ${longDate(lang)}`,
|
|
93
|
-
{ fontSize: 11, color: "#6C6C8A" }
|
|
94
|
-
);
|
|
95
|
-
rows.push([]);
|
|
96
|
-
if (payload.kpi?.length && config.sections.includes("kpi")) {
|
|
97
|
-
line(lang === "en" ? "Key figures" : "Chiffres cl\xE9s", { fontSize: 13, fontWeight: "bold" });
|
|
98
|
-
payload.kpi.forEach(
|
|
99
|
-
(kpi) => rows.push([
|
|
100
|
-
{ ...center, value: kpi.label, fontSize: 11, color: "#6C6C8A", width: 34 },
|
|
101
|
-
{ ...center, value: String(kpi.value), fontSize: 11, fontWeight: "bold", width: 24 }
|
|
102
|
-
])
|
|
130
|
+
line(
|
|
131
|
+
lang === "en" ? `This document was generated on ${longDate(lang)} by` : `Ce document a \xE9t\xE9 g\xE9n\xE9r\xE9 le ${longDate(lang)} par`,
|
|
132
|
+
{ fontSize: 16 }
|
|
103
133
|
);
|
|
104
|
-
|
|
134
|
+
line(BRAND_TITLE, { fontSize: 72, fontWeight: "bold", width: 60 });
|
|
135
|
+
line(BRAND_LINE, { fontSize: 24, width: 60 });
|
|
136
|
+
} else {
|
|
137
|
+
line(payload.title, { fontSize: 24, fontWeight: "bold" });
|
|
105
138
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
]
|
|
139
|
+
rows.push(filler());
|
|
140
|
+
const kpi = config.sections.includes("kpi") ? payload.kpi ?? [] : [];
|
|
141
|
+
if (kpi.length) {
|
|
142
|
+
line(lang === "en" ? "Your export currently contains :" : "Votre export contient :", { fontSize: 18 });
|
|
143
|
+
rows.push(filler());
|
|
144
|
+
for (let index = 0; index < kpi.length; index += 2) {
|
|
145
|
+
const pair = [kpi[index], kpi[index + 1]];
|
|
146
|
+
spread(
|
|
147
|
+
Object.fromEntries(
|
|
148
|
+
pair.map((item, side) => [
|
|
149
|
+
KPI_COLUMNS[side],
|
|
150
|
+
item ? { ...valueStyle, value: String(item.value) } : empty
|
|
151
|
+
])
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
spread(
|
|
155
|
+
Object.fromEntries(
|
|
156
|
+
pair.map((item, side) => [
|
|
157
|
+
KPI_COLUMNS[side],
|
|
158
|
+
item ? { ...labelStyle, value: String(item.label) } : empty
|
|
159
|
+
])
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
rows.push(filler());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (config.options.includes("logo")) line(payload.title, { fontSize: 20, fontWeight: "bold" });
|
|
166
|
+
if (payload.subtitle) line(payload.subtitle, { fontSize: 16 });
|
|
167
|
+
const filters = config.sections.includes("filters") ? payload.filters ?? [] : [];
|
|
168
|
+
if (filters.length) {
|
|
169
|
+
rows.push(filler());
|
|
170
|
+
filters.forEach(
|
|
171
|
+
(filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
|
|
113
172
|
);
|
|
114
|
-
rows.push([]);
|
|
115
173
|
}
|
|
116
174
|
const withSources = config.sections.includes("sources") || config.options.includes("sources");
|
|
117
175
|
if (payload.sources?.length && withSources) {
|
|
118
|
-
|
|
176
|
+
rows.push(filler());
|
|
119
177
|
payload.sources.forEach((source) => {
|
|
120
178
|
const date = source.date ? new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
121
179
|
year: "numeric",
|
|
122
180
|
month: "long",
|
|
123
181
|
day: "numeric"
|
|
124
182
|
}) : null;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{
|
|
128
|
-
...center,
|
|
129
|
-
value: date ? lang === "en" ? `Last update: ${date}` : `Derni\xE8re mise \xE0 jour : ${date}` : "",
|
|
130
|
-
fontSize: 11,
|
|
131
|
-
color: "#6C6C8A"
|
|
132
|
-
}
|
|
133
|
-
]);
|
|
183
|
+
const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
|
|
184
|
+
line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
|
|
134
185
|
});
|
|
135
186
|
}
|
|
187
|
+
for (let index = 0; index < 15; index += 1) rows.push(filler());
|
|
136
188
|
return rows;
|
|
137
189
|
}
|
|
138
190
|
async function toXlsx(config, payload, lang) {
|
|
@@ -141,13 +193,13 @@ export function useRmExport() {
|
|
|
141
193
|
const sheets = payload.sheets;
|
|
142
194
|
const data = [coverSheet(payload, config, lang)];
|
|
143
195
|
const schema = [null];
|
|
144
|
-
const names = [
|
|
196
|
+
const names = [COVER_SHEET];
|
|
145
197
|
let rowCount = 0;
|
|
146
198
|
sheets.forEach((sheet) => {
|
|
147
199
|
const rows = rowsOf(sheet, payload.limit);
|
|
148
200
|
rowCount += rows.length;
|
|
149
201
|
data.push(rows);
|
|
150
|
-
schema.push(
|
|
202
|
+
schema.push(xlsxColumnsOf(sheet, config.columns));
|
|
151
203
|
names.push(sheet.name.replace(/[[\]:*?/\\]/g, " ").slice(0, 31));
|
|
152
204
|
});
|
|
153
205
|
await writeXlsxFile(data, { schema, sheets: names, fileName, stickyRowsCount: 1 });
|
|
@@ -234,7 +286,12 @@ export function useRmExport() {
|
|
|
234
286
|
const { default: JsPdf } = await import("jspdf");
|
|
235
287
|
const { default: autoTable } = await import("jspdf-autotable");
|
|
236
288
|
const landscape = config.layout === "a4-landscape";
|
|
237
|
-
const doc = new JsPdf({
|
|
289
|
+
const doc = new JsPdf({
|
|
290
|
+
orientation: landscape ? "landscape" : "portrait",
|
|
291
|
+
unit: "mm",
|
|
292
|
+
format: "a4",
|
|
293
|
+
compress: true
|
|
294
|
+
});
|
|
238
295
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
239
296
|
const margin = 14;
|
|
240
297
|
const inner = pageWidth - margin * 2;
|
|
@@ -248,10 +305,10 @@ export function useRmExport() {
|
|
|
248
305
|
doc.setTextColor(255, 255, 255);
|
|
249
306
|
doc.setFont("helvetica", "bold");
|
|
250
307
|
doc.setFontSize(22);
|
|
251
|
-
doc.text(BRAND_TITLE, margin, 20);
|
|
308
|
+
doc.text(printable(BRAND_TITLE), margin, 20);
|
|
252
309
|
doc.setFont("helvetica", "normal");
|
|
253
310
|
doc.setFontSize(11);
|
|
254
|
-
doc.text(BRAND_LINE, margin, 28);
|
|
311
|
+
doc.text(printable(BRAND_LINE), margin, 28);
|
|
255
312
|
doc.setFontSize(9);
|
|
256
313
|
doc.text(longDate(lang), pageWidth - margin, 28, { align: "right" });
|
|
257
314
|
if (payload.logoDataUrl) {
|
|
@@ -332,47 +389,134 @@ export function useRmExport() {
|
|
|
332
389
|
doc.addImage(canvas.toDataURL("image/png"), "PNG", margin, y, drawWidth, drawHeight, void 0, "FAST");
|
|
333
390
|
y += drawHeight + 6;
|
|
334
391
|
}
|
|
392
|
+
let rowCount = 0;
|
|
393
|
+
if (config.sections.includes("cards")) {
|
|
394
|
+
const cards = cardsOf(payload, config, lang);
|
|
395
|
+
rowCount = cards.length;
|
|
396
|
+
const labelWidth = Math.min(46, inner * 0.34);
|
|
397
|
+
const valueWidth = inner - labelWidth - 8;
|
|
398
|
+
const pageHeight = doc.internal.pageSize.getHeight();
|
|
399
|
+
cards.forEach((card) => {
|
|
400
|
+
const titleLines = doc.splitTextToSize(printable(card.title), inner - 8);
|
|
401
|
+
const subtitleLines = card.subtitle ? doc.splitTextToSize(printable(card.subtitle), inner - 8) : [];
|
|
402
|
+
const fields = card.fields.map((field) => ({
|
|
403
|
+
label: doc.splitTextToSize(printable(field.label), labelWidth),
|
|
404
|
+
value: doc.splitTextToSize(printable(field.value), valueWidth)
|
|
405
|
+
}));
|
|
406
|
+
const height = 6 + titleLines.length * 5 + (subtitleLines.length ? subtitleLines.length * 4 + 1 : 0) + (card.badges?.length ? 6 : 0) + fields.reduce((total2, field) => total2 + Math.max(field.label.length, field.value.length) * 4.2, 0) + 5;
|
|
407
|
+
if (y + height > pageHeight - margin) {
|
|
408
|
+
doc.addPage();
|
|
409
|
+
y = margin;
|
|
410
|
+
}
|
|
411
|
+
doc.setDrawColor(232, 232, 240);
|
|
412
|
+
doc.setFillColor(252, 252, 254);
|
|
413
|
+
doc.roundedRect(margin, y, inner, height, 2, 2, "FD");
|
|
414
|
+
let cursor = y + 6;
|
|
415
|
+
doc.setTextColor(...ink);
|
|
416
|
+
doc.setFont("helvetica", "bold");
|
|
417
|
+
doc.setFontSize(11);
|
|
418
|
+
doc.text(titleLines, margin + 4, cursor);
|
|
419
|
+
cursor += titleLines.length * 5;
|
|
420
|
+
if (subtitleLines.length) {
|
|
421
|
+
doc.setTextColor(...grey);
|
|
422
|
+
doc.setFont("helvetica", "normal");
|
|
423
|
+
doc.setFontSize(9);
|
|
424
|
+
doc.text(subtitleLines, margin + 4, cursor + 1);
|
|
425
|
+
cursor += subtitleLines.length * 4 + 1;
|
|
426
|
+
}
|
|
427
|
+
if (card.badges?.length) {
|
|
428
|
+
let badgeX = margin + 4;
|
|
429
|
+
doc.setFontSize(7);
|
|
430
|
+
card.badges.forEach((badge) => {
|
|
431
|
+
const text = printable(badge);
|
|
432
|
+
const width = doc.getTextWidth(text) + 4;
|
|
433
|
+
doc.setFillColor(240, 238, 255);
|
|
434
|
+
doc.roundedRect(badgeX, cursor - 2, width, 5, 1.2, 1.2, "F");
|
|
435
|
+
doc.setTextColor(...violet);
|
|
436
|
+
doc.text(text, badgeX + 2, cursor + 1.6);
|
|
437
|
+
badgeX += width + 2;
|
|
438
|
+
});
|
|
439
|
+
cursor += 6;
|
|
440
|
+
}
|
|
441
|
+
fields.forEach((field) => {
|
|
442
|
+
const lines = Math.max(field.label.length, field.value.length);
|
|
443
|
+
doc.setTextColor(...grey);
|
|
444
|
+
doc.setFont("helvetica", "bold");
|
|
445
|
+
doc.setFontSize(7);
|
|
446
|
+
doc.text(field.label, margin + 4, cursor + 1);
|
|
447
|
+
doc.setTextColor(...ink);
|
|
448
|
+
doc.setFont("helvetica", "normal");
|
|
449
|
+
doc.setFontSize(9);
|
|
450
|
+
doc.text(field.value, margin + 4 + labelWidth, cursor + 1);
|
|
451
|
+
cursor += lines * 4.2;
|
|
452
|
+
});
|
|
453
|
+
y += height + 4;
|
|
454
|
+
});
|
|
455
|
+
const total = payload.sheets[0]?.data.length ?? 0;
|
|
456
|
+
if (total > cards.length) {
|
|
457
|
+
doc.setTextColor(...grey);
|
|
458
|
+
doc.setFont("helvetica", "italic");
|
|
459
|
+
doc.setFontSize(8);
|
|
460
|
+
doc.text(
|
|
461
|
+
printable(
|
|
462
|
+
lang === "en" ? `${cards.length} of ${total} records shown. Export to Excel for the full set.` : `${cards.length} fiches sur ${total}. Passez par l'export Excel pour la totalit\xE9.`
|
|
463
|
+
),
|
|
464
|
+
margin,
|
|
465
|
+
y + 2
|
|
466
|
+
);
|
|
467
|
+
y += 6;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
335
470
|
if (config.sections.includes("table")) {
|
|
336
|
-
|
|
471
|
+
const MAX_COLUMNS = 8;
|
|
337
472
|
payload.sheets.forEach((sheet) => {
|
|
338
|
-
const
|
|
473
|
+
const all = columnsOf(sheet, config.columns);
|
|
474
|
+
const columns = all.slice(0, MAX_COLUMNS);
|
|
339
475
|
const rows = rowsOf(sheet, payload.limit);
|
|
340
|
-
rowCount += rows.length;
|
|
476
|
+
if (!config.sections.includes("cards")) rowCount += rows.length;
|
|
477
|
+
doc.addPage("a4", "landscape");
|
|
478
|
+
const wide = doc.internal.pageSize.getWidth() - margin * 2;
|
|
479
|
+
doc.setTextColor(...ink);
|
|
480
|
+
doc.setFont("helvetica", "bold");
|
|
481
|
+
doc.setFontSize(11);
|
|
482
|
+
doc.text(printable(sheet.name), margin, margin + 2);
|
|
483
|
+
if (all.length > columns.length) {
|
|
484
|
+
doc.setFont("helvetica", "italic");
|
|
485
|
+
doc.setFontSize(8);
|
|
486
|
+
doc.setTextColor(...grey);
|
|
487
|
+
doc.text(
|
|
488
|
+
printable(
|
|
489
|
+
lang === "en" ? `${columns.length} of ${all.length} columns. The Excel export carries them all.` : `${columns.length} colonnes sur ${all.length}. L'export Excel les porte toutes.`
|
|
490
|
+
),
|
|
491
|
+
margin,
|
|
492
|
+
margin + 7
|
|
493
|
+
);
|
|
494
|
+
}
|
|
341
495
|
autoTable(doc, {
|
|
342
|
-
startY:
|
|
496
|
+
startY: margin + 10,
|
|
343
497
|
head: [columns.map((column) => printable(column.column))],
|
|
344
498
|
body: rows.map((row) => columns.map((column) => printable(cellText(column.value(row), lang)))),
|
|
345
|
-
styles: { fontSize:
|
|
346
|
-
headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold" },
|
|
499
|
+
styles: { fontSize: 6.5, cellPadding: 1.4, overflow: "linebreak", textColor: ink, minCellWidth: wide / columns.length / 2 },
|
|
500
|
+
headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold", valign: "middle" },
|
|
347
501
|
alternateRowStyles: { fillColor: [250, 250, 253] },
|
|
348
|
-
margin: { left: margin, right: margin },
|
|
349
|
-
|
|
350
|
-
if (payload.sheets.length > 1) {
|
|
351
|
-
doc.setTextColor(...grey);
|
|
352
|
-
doc.setFont("helvetica", "bold");
|
|
353
|
-
doc.setFontSize(8);
|
|
354
|
-
doc.text(printable(sheet.name), margin, margin - 4);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
502
|
+
margin: { left: margin, right: margin, top: margin, bottom: margin },
|
|
503
|
+
tableWidth: wide
|
|
357
504
|
});
|
|
358
|
-
y = doc.lastAutoTable?.finalY ??
|
|
505
|
+
y = doc.lastAutoTable?.finalY ?? margin;
|
|
359
506
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const fileName2 = `${config.fileName}.pdf`;
|
|
370
|
-
const blob2 = doc.output("blob");
|
|
371
|
-
return { fileName: fileName2, rows: rowCount, bytes: download(blob2, fileName2) };
|
|
507
|
+
}
|
|
508
|
+
const withSources = config.sections.includes("sources") || config.options.includes("sources");
|
|
509
|
+
if (payload.sources?.length && withSources) {
|
|
510
|
+
doc.setTextColor(...grey);
|
|
511
|
+
doc.setFont("helvetica", "normal");
|
|
512
|
+
doc.setFontSize(8);
|
|
513
|
+
const names = payload.sources.map((source) => `${source.name}${source.date ? ` (${new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", { year: "numeric", month: "long", day: "numeric" })})` : ""}`).join(" \xB7 ");
|
|
514
|
+
const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
|
|
515
|
+
doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
|
|
372
516
|
}
|
|
373
517
|
const fileName = `${config.fileName}.pdf`;
|
|
374
518
|
const blob = doc.output("blob");
|
|
375
|
-
return { fileName, rows:
|
|
519
|
+
return { fileName, rows: rowCount, bytes: download(blob, fileName) };
|
|
376
520
|
}
|
|
377
521
|
async function toPermalink(payload) {
|
|
378
522
|
const url = payload.permalink || (typeof window !== "undefined" ? window.location.href : "");
|
|
@@ -27,6 +27,15 @@ export interface RmExportSheet {
|
|
|
27
27
|
name: string;
|
|
28
28
|
schema: RmExportColumn[];
|
|
29
29
|
data: any[];
|
|
30
|
+
/**
|
|
31
|
+
* Colonne qui identifie une ligne, par son intitule. Elle sert de titre aux
|
|
32
|
+
* fiches du rapport PDF. Sans elle, le titre retombe sur la premiere colonne
|
|
33
|
+
* du schema, qui est souvent la date : une fiche titree « 12 mars 2025 » ne
|
|
34
|
+
* dit pas de quel produit elle parle.
|
|
35
|
+
*/
|
|
36
|
+
titleColumn?: string;
|
|
37
|
+
/** Colonne posee sous le titre de la fiche (le laboratoire, la date...). */
|
|
38
|
+
subtitleColumn?: string;
|
|
30
39
|
}
|
|
31
40
|
/** Chiffre cle repris sur la page de garde. */
|
|
32
41
|
export interface RmExportKpi {
|
|
@@ -44,6 +53,21 @@ export interface RmExportFilter {
|
|
|
44
53
|
label: string;
|
|
45
54
|
value: string;
|
|
46
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Une fiche : ce que la page montre sous forme de carte, rendu tel quel dans
|
|
58
|
+
* le rapport PDF. Une page qui n'en declare pas en obtient quand meme, les
|
|
59
|
+
* fiches etant alors derivees des lignes du tableau (cf. useRmExport).
|
|
60
|
+
*/
|
|
61
|
+
export interface RmExportCard {
|
|
62
|
+
title: string;
|
|
63
|
+
subtitle?: string;
|
|
64
|
+
/** Etiquettes courtes posees sous le titre (SMR, statut, pays...). */
|
|
65
|
+
badges?: string[];
|
|
66
|
+
fields: {
|
|
67
|
+
label: string;
|
|
68
|
+
value: string;
|
|
69
|
+
}[];
|
|
70
|
+
}
|
|
47
71
|
/**
|
|
48
72
|
* Un format d'export.
|
|
49
73
|
*
|
|
@@ -64,15 +88,25 @@ export interface RmExportFormat {
|
|
|
64
88
|
tabular?: boolean;
|
|
65
89
|
/** Le format rend les graphiques. */
|
|
66
90
|
visual?: boolean;
|
|
67
|
-
/** Le format est mis en page :
|
|
91
|
+
/** Le format est mis en page : choix A4 et fiches detaillees. */
|
|
68
92
|
paged?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Le format porte un en-tete de document : page de garde, chiffres cles,
|
|
95
|
+
* filtres actifs, sources. Vrai pour le PDF et le classeur, faux pour un
|
|
96
|
+
* CSV ou une image, qui ne savent porter que leur contenu.
|
|
97
|
+
*/
|
|
98
|
+
document?: boolean;
|
|
69
99
|
/** Format annonce mais pas encore livre : la carte reste visible, grisee. */
|
|
70
100
|
disabled?: boolean;
|
|
71
101
|
/** Raison affichee sur une carte grisee. */
|
|
72
102
|
disabledHint?: string;
|
|
73
103
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Nature d'une section : elle decide de quels formats la section releve.
|
|
106
|
+
* `cards` ne vaut que pour un format mis en page, `meta` que pour un format
|
|
107
|
+
* qui porte un en-tete de document.
|
|
108
|
+
*/
|
|
109
|
+
export type RmExportSectionKind = 'table' | 'visual' | 'meta' | 'cards';
|
|
76
110
|
/** Section proposee a l'etape « Donnees ». */
|
|
77
111
|
export interface RmExportSection {
|
|
78
112
|
key: string;
|
|
@@ -142,6 +176,17 @@ export interface RmExportPayload {
|
|
|
142
176
|
sources?: RmExportSource[];
|
|
143
177
|
filters?: RmExportFilter[];
|
|
144
178
|
sheets: RmExportSheet[];
|
|
179
|
+
/**
|
|
180
|
+
* Fiches du rapport. Absentes, elles sont derivees des lignes du premier
|
|
181
|
+
* onglet : le titre vient de la premiere colonne, les champs des suivantes.
|
|
182
|
+
*/
|
|
183
|
+
cards?: RmExportCard[];
|
|
184
|
+
/**
|
|
185
|
+
* Plafond de fiches dans un rapport. Une fiche fait une dizaine de lignes :
|
|
186
|
+
* mille resultats donneraient un PDF de plusieurs centaines de pages, que
|
|
187
|
+
* personne ne lit et qu'aucune messagerie n'accepte.
|
|
188
|
+
*/
|
|
189
|
+
cardLimit?: number;
|
|
145
190
|
/**
|
|
146
191
|
* Graphiques a rasteriser ou vectoriser. Les charts Romulus sont rendus par
|
|
147
192
|
* ECharts en SVG : on recupere le noeud, pas une capture d'ecran.
|
|
@@ -12,10 +12,11 @@ export const RM_EXPORT_FORMATS = [
|
|
|
12
12
|
label: "PDF",
|
|
13
13
|
icon: "FilePdf",
|
|
14
14
|
color: "var(--color-danger)",
|
|
15
|
-
desc: "Rapport mis en page, pr\xEAt \xE0 partager.
|
|
15
|
+
desc: "Rapport mis en page, pr\xEAt \xE0 partager. Chiffres cl\xE9s, fiches, graphiques, en-t\xEAte OIP Healthcare.",
|
|
16
16
|
tabular: true,
|
|
17
17
|
visual: true,
|
|
18
|
-
paged: true
|
|
18
|
+
paged: true,
|
|
19
|
+
document: true
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
key: "xlsx",
|
|
@@ -24,7 +25,7 @@ export const RM_EXPORT_FORMATS = [
|
|
|
24
25
|
color: "var(--color-success)",
|
|
25
26
|
desc: "Donn\xE9es brutes avec page de garde. Pour analyse en local ou consolidation.",
|
|
26
27
|
tabular: true,
|
|
27
|
-
|
|
28
|
+
document: true
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
31
|
key: "csv",
|
|
@@ -61,7 +62,11 @@ export const RM_EXPORT_FORMATS = [
|
|
|
61
62
|
export const RM_EXPORT_SECTIONS = [
|
|
62
63
|
{ key: "kpi", label: "Chiffres cl\xE9s", kind: "meta", on: true },
|
|
63
64
|
{ key: "charts", label: "Graphiques", kind: "visual", on: true },
|
|
64
|
-
{ key: "
|
|
65
|
+
{ key: "cards", label: "Fiches d\xE9taill\xE9es", kind: "cards", on: true },
|
|
66
|
+
// Decochee : dans un rapport, les fiches disent la meme chose en lisible.
|
|
67
|
+
// La case reste offerte a qui veut le tableau en annexe. Elle ne concerne
|
|
68
|
+
// que le PDF — les onglets du classeur ne dependent pas d'elle.
|
|
69
|
+
{ key: "table", label: "Tableau r\xE9capitulatif", kind: "table" },
|
|
65
70
|
{ key: "filters", label: "Filtres actifs", kind: "meta", on: true },
|
|
66
71
|
{ key: "sources", label: "Sources et mises \xE0 jour", kind: "meta", on: true }
|
|
67
72
|
];
|