diff --git a/README.md b/README.md index 5a341ba..55cef7f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,16 @@ obligatoire, lien facultatif, période en heure de Paris, priorité, groupes AD - Aucun cache à purger : l'add-in interroge l'API à chaque nouveau message, la bannière apparaît et disparaît donc à l'heure. Les messages déjà envoyés la conservent tant que l'image reste dans la bibliothèque. +## Interface du back-office + +Aucune dépendance externe ni étape de compilation : tout est servi depuis `backend/public`. + +- `css/app.css` : feuille unique. Les couleurs, rayons et polices sont des variables en tête de fichier (`:root`). +- `js/app.js` : notifications, boîte de confirmation (`data-confirm` sur un formulaire), zone de dépôt (`data-dropzone`), + copie (`data-copy`), bouton occupé (`data-busy`). Sans JavaScript, les formulaires restent utilisables. +- `fonts/` : Atkinson Hyperlegible Next et Mono (licence OFL, fichier joint), hébergées localement. +- `resources/views/components/icon.blade.php` : icônes Lucide (licence ISC) intégrées, ``. + ## Syntaxe des templates | Syntaxe | Effet | diff --git a/backend/public/css/app.css b/backend/public/css/app.css new file mode 100644 index 0000000..7d3a89b --- /dev/null +++ b/backend/public/css/app.css @@ -0,0 +1,416 @@ +/* + * Back-office et portail « Signatures ». + * Feuille unique, sans préprocesseur ni CDN : modifiez les variables ci-dessous pour adapter les couleurs. + */ + +@font-face { + font-family: "Atkinson Hyperlegible Next"; + src: url("../fonts/atkinson-next.woff2") format("woff2"); + font-weight: 200 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Atkinson Hyperlegible Mono"; + src: url("../fonts/atkinson-mono.woff2") format("woff2"); + font-weight: 200 800; + font-style: normal; + font-display: swap; +} + +:root { + /* Texte et surfaces */ + --ink: #17242e; + --muted: #586a77; + --line: #dce3e8; + --line-strong: #bfcbd4; + --bg: #f2f5f7; + --surface: #ffffff; + --surface-2: #f7f9fa; + + /* Bleu nuit de la barre latérale, bleu canal des actions, jaune soleil en repère */ + --night: #0f3554; + --night-2: #1a4871; + --night-ink: #c9dbe9; + --accent: #16649a; + --accent-strong: #0f4f7d; + --accent-soft: #e4f0f8; + --sun: #f2b544; + + /* États */ + --ok: #1c7440; + --ok-soft: #e2f3e8; + --warn: #8a5300; + --warn-soft: #fff1d2; + --danger: #b3261e; + --danger-soft: #fdeceb; + + --r-s: 6px; + --r-m: 10px; + --r-l: 14px; + --sidebar: 248px; + --focus: 0 0 0 3px rgba(22, 100, 154, .28); + + --font: "Atkinson Hyperlegible Next", "Segoe UI", system-ui, sans-serif; + --mono: "Atkinson Hyperlegible Mono", Consolas, "Cascadia Mono", monospace; +} + +*, *::before, *::after { box-sizing: border-box; } + +html { -webkit-text-size-adjust: 100%; } + +body { + margin: 0; + font-family: var(--font); + font-size: 15px; + line-height: 1.45; + color: var(--ink); + background: var(--bg); +} + +h1 { font-size: 26px; line-height: 1.15; font-weight: 700; letter-spacing: -.01em; margin: 0; } +h2 { font-size: 16px; line-height: 1.3; font-weight: 700; margin: 0 0 12px; } +p { margin: 0 0 12px; } +a { color: var(--accent); text-underline-offset: 2px; } +a:hover { color: var(--accent-strong); } +code { font-family: var(--mono); font-size: .9em; background: var(--surface-2); border: 1px solid var(--line); border-radius: 4px; padding: 0 4px; } +strong { font-weight: 700; } + +:focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--r-s); } + +.icon { flex: none; vertical-align: -3px; } +.muted { color: var(--muted); font-size: 13px; } +.spacer { flex: 1; } +.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.nowrap { white-space: nowrap; } +.right { text-align: right; } +[hidden] { display: none !important; } + +.skip { position: absolute; left: -9999px; } +.skip:focus { left: 12px; top: 12px; z-index: 50; background: var(--surface); padding: 8px 12px; border-radius: var(--r-s); } + +/* ---------------------------------------------------------------- Structure */ + +.app { min-height: 100vh; } +.app--nav { display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); } + +.sidebar { + position: sticky; top: 0; height: 100vh; overflow-y: auto; + display: flex; flex-direction: column; + background: var(--night); color: var(--night-ink); + padding: 20px 14px 14px; +} + +.brand { display: block; color: #fff; text-decoration: none; padding: 0 8px 18px; } +.brand:hover { color: #fff; } +.brand-name { display: block; font-size: 21px; font-weight: 700; letter-spacing: -.01em; line-height: 1.1; } +.brand-stroke { display: block; width: 104px; height: 12px; margin: 3px 0 6px -2px; color: var(--sun); } +.brand-org { display: block; font-size: 12.5px; color: var(--night-ink); } + +.nav { display: flex; flex-direction: column; gap: 2px; } +.nav-group { font-size: 12px; color: #9db6ca; margin: 16px 0 4px; padding: 0 10px; } +.nav-link { + position: relative; + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; border-radius: var(--r-s); + color: var(--night-ink); text-decoration: none; font-weight: 500; +} +.nav-link:hover { background: var(--night-2); color: #fff; } +.nav-link.active { background: var(--night-2); color: #fff; font-weight: 700; } +.nav-link.active::before { content: ""; position: absolute; left: -14px; top: 7px; bottom: 7px; width: 4px; border-radius: 0 4px 4px 0; background: var(--sun); } +.nav-link:focus-visible { box-shadow: 0 0 0 3px rgba(242, 181, 68, .55); } + +.sidebar-foot { margin-top: auto; padding-top: 16px; display: flex; flex-direction: column; gap: 10px; } +.user { display: flex; align-items: center; gap: 10px; padding: 10px 8px 2px; border-top: 1px solid rgba(255, 255, 255, .14); } +.user-name { display: block; color: #fff; font-weight: 600; font-size: 14px; line-height: 1.2; overflow-wrap: anywhere; } +.user-role { display: block; font-size: 12.5px; color: #9db6ca; } +.user form { margin-left: auto; } +.icon-btn { + display: inline-flex; align-items: center; justify-content: center; + width: 34px; height: 34px; border-radius: var(--r-s); + background: transparent; border: 0; color: inherit; cursor: pointer; +} +.icon-btn:hover { background: rgba(127, 148, 165, .22); } +.sidebar .icon-btn { color: var(--night-ink); } +.sidebar .icon-btn:hover { background: var(--night-2); color: #fff; } + +.avatar { + flex: none; display: inline-flex; align-items: center; justify-content: center; + width: 34px; height: 34px; border-radius: 50%; + background: var(--accent-soft); color: var(--accent-strong); + font-size: 12.5px; font-weight: 700; +} +.sidebar .avatar { background: var(--night-2); color: #fff; } + +.content { min-width: 0; } +main { max-width: 1180px; margin: 0 auto; padding: 28px 28px 56px; } + +/* Portail agent : pas de barre latérale, simple bandeau */ +.topbar { + display: flex; align-items: center; gap: 12px; + background: var(--night); color: var(--night-ink); padding: 10px 20px; +} +.topbar .brand { padding: 0; } +.topbar .brand-stroke { display: none; } +.topbar .brand-name { font-size: 18px; } +.topbar .user { border: 0; padding: 0; margin-left: auto; } +.topbar .icon-btn { color: var(--night-ink); } +.topbar .icon-btn:hover { background: var(--night-2); color: #fff; } +.topbar .avatar { background: var(--night-2); color: #fff; } + +@media (max-width: 900px) { + .app--nav { display: block; } + .sidebar { position: static; height: auto; padding: 12px 12px 8px; } + .brand { padding: 0 6px 8px; } + .brand-stroke, .brand-org, .nav-group { display: none; } + .nav { flex-direction: row; overflow-x: auto; gap: 4px; padding-bottom: 6px; scrollbar-width: thin; } + .nav-link { white-space: nowrap; } + .nav-link.active::before { left: 10px; right: 10px; top: auto; bottom: 2px; width: auto; height: 3px; border-radius: 3px; } + .sidebar-foot { flex-direction: row; align-items: center; padding-top: 6px; } + .sidebar-foot .user { border: 0; padding: 0; margin-left: auto; } + .user-role { display: none; } + main { padding: 18px 14px 40px; } + h1 { font-size: 22px; } +} + +/* ---------------------------------------------------------------- En-tête de page */ + +.page-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; } +.page-head > div:first-child { flex: 1; min-width: 240px; } +.page-head .lead { color: var(--muted); max-width: 78ch; margin: 8px 0 0; } +.page-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.page-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.back { display: inline-flex; align-items: center; gap: 4px; font-size: 13.5px; text-decoration: none; margin-bottom: 6px; color: var(--muted); } +.back:hover { color: var(--accent-strong); } + +/* ---------------------------------------------------------------- Cartes */ + +.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-m); padding: 20px; margin-bottom: 16px; } +.card-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; } +.card-head h2 { margin: 0; } +.card--flush { padding: 0; overflow: hidden; } +.card--flush > .card-head { padding: 16px 20px 0; } +.card-foot { padding: 12px 20px; border-top: 1px solid var(--line); background: var(--surface-2); } +.card--flush > .card-foot:last-child { border-radius: 0 0 var(--r-m) var(--r-m); } + +.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; } +@media (max-width: 1000px) { .split { grid-template-columns: 1fr; } } + +/* Indicateurs du tableau de bord */ +.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 16px; } +.tile { + display: flex; align-items: center; gap: 12px; + background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-m); + padding: 14px 16px; color: inherit; text-decoration: none; +} +a.tile:hover { border-color: var(--accent); color: inherit; } +.tile-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: var(--r-s); background: var(--accent-soft); color: var(--accent-strong); } +.tile-value { display: block; font-size: 24px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; } +.tile-label { display: block; color: var(--muted); font-size: 13px; } + +/* ---------------------------------------------------------------- Tableaux */ + +.table-wrap { overflow-x: auto; } +table.list { width: 100%; border-collapse: collapse; } +table.list th, table.list td { text-align: left; padding: 11px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; } +table.list th { font-size: 12.5px; font-weight: 600; color: var(--muted); background: var(--surface-2); white-space: nowrap; } +table.list th:first-child, table.list td:first-child { padding-left: 20px; } +table.list th:last-child, table.list td:last-child { padding-right: 20px; } +table.list tbody tr:last-child td { border-bottom: 0; } +table.list tbody tr:hover td { background: #fafcfd; } +table.list .num { font-variant-numeric: tabular-nums; } +.cell-title { font-weight: 600; text-decoration: none; } +.cell-title:hover { text-decoration: underline; } +.cell-actions { text-align: right; white-space: nowrap; } +.cell-actions > * { margin: 2px 0 2px 4px; } +.btn.icon-only { padding: 6px 8px; } +.person { display: flex; align-items: center; gap: 10px; } + +/* Fiche descriptive (libellé / valeur) */ +dl.facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 0; margin: 0; } +dl.facts dt, dl.facts dd { padding: 9px 0; border-bottom: 1px solid var(--line); margin: 0; } +dl.facts dt { color: var(--muted); font-size: 13px; padding-right: 20px; } +dl.facts dt:last-of-type, dl.facts dd:last-of-type { border-bottom: 0; } + +.empty { text-align: center; padding: 36px 20px; color: var(--muted); } +.empty .icon { color: var(--line-strong); margin-bottom: 8px; } +.empty p { margin: 0 auto 12px; max-width: 46ch; } +.empty p:last-child { margin-bottom: 0; } +.empty strong { display: block; color: var(--ink); font-size: 16px; margin-bottom: 4px; } + +/* ---------------------------------------------------------------- Formulaires */ + +label { display: block; font-weight: 600; font-size: 13.5px; margin: 14px 0 5px; } +.card > label:first-child, .card > h2 + label { margin-top: 0; } +.hint { color: var(--muted); font-size: 13px; margin: 5px 0 0; } + +input[type=text], input[type=number], input[type=search], input[type=url], input[type=datetime-local], select, textarea { + width: 100%; padding: 8px 10px; + border: 1px solid var(--line-strong); border-radius: var(--r-s); + font: inherit; color: var(--ink); background: var(--surface); +} +input:hover, select:hover, textarea:hover { border-color: #93a5b2; } +input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--accent); box-shadow: var(--focus); } +input::placeholder { color: #8797a3; } +input.small { width: 96px; } +select { padding-right: 28px; } +textarea.code { font-family: var(--mono); font-size: 13px; line-height: 1.5; min-height: 280px; resize: vertical; tab-size: 2; background: var(--surface-2); } + +fieldset { border: 1px solid var(--line); border-radius: var(--r-s); margin: 14px 0 0; padding: 8px 14px 12px; min-width: 0; } +fieldset.bare { border: 0; margin: 0; padding: 0; } +legend { font-weight: 600; font-size: 13.5px; padding: 0 6px; } +.scroll { max-height: 230px; overflow: auto; } + +.field-row { display: flex; gap: 12px; flex-wrap: wrap; } +.field-row > * { flex: 1; min-width: 190px; } + +/* Champ de recherche avec icône */ +.search { position: relative; flex: 1; max-width: 380px; } +.search .icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--muted); } +.search input { padding-left: 34px; } + +/* Cases et boutons radio */ +label.check { display: flex; align-items: flex-start; gap: 8px; font-weight: 400; font-size: 14.5px; margin: 6px 0; cursor: pointer; } +label.check input { flex: none; width: 17px; height: 17px; margin: 2px 0 0; accent-color: var(--accent); } + +/* Interrupteur : une case à cocher habillée */ +.switch { display: inline-flex; align-items: center; gap: 10px; font-weight: 400; font-size: 14.5px; margin: 0; cursor: pointer; } +.switch input { position: absolute; opacity: 0; width: 1px; height: 1px; } +.switch-track { flex: none; position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--line-strong); transition: background .15s; } +.switch-track::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; } +.switch input:checked + .switch-track { background: var(--accent); } +.switch input:checked + .switch-track::after { transform: translateX(16px); } +.switch input:focus-visible + .switch-track { box-shadow: var(--focus); } +.switch input:disabled + .switch-track { opacity: .5; } + +/* Même interrupteur sous forme de bouton : il envoie son formulaire au clic (activation d'une campagne) */ +button.switch-btn { position: relative; width: 38px; height: 22px; padding: 0; border: 0; border-radius: 11px; background: var(--line-strong); cursor: pointer; vertical-align: middle; transition: background .15s; } +button.switch-btn::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; } +button.switch-btn[aria-checked=true] { background: var(--accent); } +button.switch-btn[aria-checked=true]::after { transform: translateX(16px); } + +/* Choix exclusifs présentés en liste de cartes (portail) */ +.option { display: flex; align-items: flex-start; gap: 10px; padding: 9px 12px; margin: 0; border: 1px solid var(--line); border-radius: var(--r-s); font-weight: 400; font-size: 14.5px; cursor: pointer; } +.option + .option { margin-top: 6px; } +fieldset > .option:first-of-type { margin-top: 4px; } +.option input[type=radio] { flex: none; width: 17px; height: 17px; margin: 2px 0 0; accent-color: var(--accent); } +.option > span { min-width: 0; } +.option:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); } + +/* Zone de dépôt de fichier */ +.dropzone { + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; + min-height: 112px; padding: 16px; margin: 0; + border: 1.5px dashed var(--line-strong); border-radius: var(--r-m); + background: var(--surface-2); color: var(--muted); + font-weight: 400; font-size: 14px; text-align: center; cursor: pointer; +} +.dropzone:hover, .dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); } +.dropzone:focus-within { border-color: var(--accent); box-shadow: var(--focus); } +.dropzone input[type=file] { position: absolute; opacity: 0; width: 1px; height: 1px; } +.dropzone-name { color: var(--ink); font-weight: 600; overflow-wrap: anywhere; } +.dropzone u { color: var(--accent); } +html:not(.js) .dropzone input[type=file] { position: static; opacity: 1; width: auto; height: auto; } + +/* ---------------------------------------------------------------- Boutons */ + +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 7px; + padding: 8px 14px; border-radius: var(--r-s); + border: 1px solid var(--accent); background: var(--accent); color: #fff; + font: inherit; font-size: 14.5px; font-weight: 600; line-height: 1.2; + text-decoration: none; cursor: pointer; white-space: nowrap; +} +.btn:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: #fff; } +.btn.secondary { background: var(--surface); color: var(--accent-strong); border-color: var(--line-strong); } +.btn.secondary:hover { background: var(--accent-soft); border-color: var(--accent); } +.btn.danger { background: var(--surface); color: var(--danger); border-color: #e3b5b1; } +.btn.danger:hover { background: var(--danger-soft); border-color: var(--danger); } +.btn.danger.solid { background: var(--danger); color: #fff; border-color: var(--danger); } +.btn.danger.solid:hover { background: #8f1d17; } +.btn.small { padding: 5px 10px; font-size: 13.5px; } +.btn:disabled, .btn[aria-disabled=true] { opacity: .5; cursor: default; pointer-events: none; } +.btn .icon { vertical-align: 0; } +.btn.is-busy .icon { animation: spin .8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +button.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; } +form.inline { display: inline; } + +/* ---------------------------------------------------------------- Étiquettes et messages */ + +.badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12.5px; font-weight: 600; background: var(--accent-soft); color: var(--accent-strong); white-space: nowrap; } +.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; } +.badge.ok { background: var(--ok-soft); color: var(--ok); } +.badge.warn { background: var(--warn-soft); color: var(--warn); } +.badge.off { background: #edf1f4; color: var(--muted); } +.badge.plain::before { display: none; } + +.alert { display: flex; gap: 10px; align-items: flex-start; padding: 11px 14px; border-radius: var(--r-s); margin-bottom: 16px; border: 1px solid; } +.alert .icon { margin-top: 1px; } +.alert > div { min-width: 0; } +.notice { background: var(--warn-soft); color: var(--warn); border-color: #ecd28f; } +.errors { background: var(--danger-soft); color: var(--danger); border-color: #f0c0bb; } +.flash { background: var(--ok-soft); color: var(--ok); border-color: #b7dfc5; } + +/* Notification de confirmation (message de session) */ +.toasts { position: fixed; bottom: 20px; right: 20px; z-index: 40; display: flex; flex-direction: column; gap: 8px; max-width: min(460px, calc(100vw - 32px)); } +.toast { + display: flex; gap: 10px; align-items: flex-start; + background: var(--ink); color: #fff; padding: 12px 12px 12px 14px; + border-radius: var(--r-m); box-shadow: 0 10px 30px rgba(15, 53, 84, .28); + overflow-wrap: anywhere; +} +.toast > .icon { color: #7fd9a0; margin-top: 1px; } +.toast .icon-btn { width: 26px; height: 26px; margin: -3px -3px 0 auto; color: #c9d5de; } +.toast.is-leaving { opacity: 0; transform: translateY(6px); transition: opacity .2s, transform .2s; } +html:not(.js) .toasts { position: static; max-width: none; margin-bottom: 16px; } +html:not(.js) .toast .icon-btn { display: none; } + +/* Boîte de confirmation */ +dialog.dialog { border: 0; border-radius: var(--r-l); padding: 0; width: min(440px, calc(100vw - 32px)); color: var(--ink); box-shadow: 0 24px 60px rgba(15, 53, 84, .35); } +dialog.dialog::backdrop { background: rgba(15, 35, 50, .45); } +.dialog-body { padding: 22px 22px 6px; } +.dialog-body h2 { font-size: 18px; margin-bottom: 8px; } +.dialog-body p { color: var(--muted); margin: 0; } +.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 18px 22px 20px; } + +/* ---------------------------------------------------------------- Écrans */ + +/* Variables cliquables de l'éditeur de template */ +.chips { display: flex; flex-wrap: wrap; gap: 6px; } +.chips button { background: var(--accent-soft); color: var(--accent-strong); border: 1px solid transparent; border-radius: var(--r-s); padding: 3px 9px; font: 13px var(--mono); cursor: pointer; } +.chips button:hover { border-color: var(--accent); } + +iframe.sig { display: block; width: 100%; min-height: 270px; border: 1px solid var(--line); border-radius: var(--r-s); background: #fff; resize: vertical; } +iframe.sig.short { min-height: 150px; } + +/* Bibliothèque d'images */ +.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; } +.media { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-m); overflow: hidden; } +.media-thumb { + display: flex; align-items: center; justify-content: center; height: 130px; padding: 12px; + /* damier : rend visibles les zones transparentes d'un logo */ + background: #fff repeating-conic-gradient(#eef2f5 0% 25%, #fff 0% 50%) 0 0 / 16px 16px; + border-bottom: 1px solid var(--line); +} +.media-thumb img { max-width: 100%; max-height: 100%; } +.media-body { padding: 12px 14px; flex: 1; } +.media-name { font-weight: 600; overflow-wrap: anywhere; } +.media-foot { display: flex; gap: 6px; padding: 10px 14px; border-top: 1px solid var(--line); background: var(--surface-2); } + +.thumb { display: block; max-width: 88px; max-height: 56px; border: 1px solid var(--line); border-radius: 4px; background: #fff; } +.preview-img { display: block; max-width: 100%; max-height: 160px; margin-top: 10px; border: 1px solid var(--line); border-radius: var(--r-s); background: #fff; } + +/* Pagination */ +.pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; } + +/* État de la synchronisation */ +.sync { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; } +.sync-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px var(--ok-soft); } +.sync-dot.warn { background: var(--warn); box-shadow: 0 0 0 4px var(--warn-soft); } +.sync p { margin: 0; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation: none !important; transition: none !important; } +} diff --git a/backend/public/fonts/OFL.txt b/backend/public/fonts/OFL.txt new file mode 100644 index 0000000..9459440 --- /dev/null +++ b/backend/public/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next) AtkinsonHyperlegibleNext-Italic[wght].ttf: Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/backend/public/fonts/atkinson-mono.woff2 b/backend/public/fonts/atkinson-mono.woff2 new file mode 100644 index 0000000..f5c1081 Binary files /dev/null and b/backend/public/fonts/atkinson-mono.woff2 differ diff --git a/backend/public/fonts/atkinson-next.woff2 b/backend/public/fonts/atkinson-next.woff2 new file mode 100644 index 0000000..a9ca4bd Binary files /dev/null and b/backend/public/fonts/atkinson-next.woff2 differ diff --git a/backend/public/js/app.js b/backend/public/js/app.js new file mode 100644 index 0000000..dfabd9b --- /dev/null +++ b/backend/public/js/app.js @@ -0,0 +1,111 @@ +/* + * Comportements communs du back-office (aucune dépendance). + * Chaque bloc s'active par un attribut data-* : sans JavaScript, les formulaires fonctionnent quand même. + */ +(() => { + 'use strict'; + + // --- Navigation en bandeau (petits écrans) : l'entrée active reste visible --------------------- + const current = document.querySelector('.nav .nav-link.active'); + if (current && current.parentElement.scrollWidth > current.parentElement.clientWidth) { + current.parentElement.scrollLeft = current.offsetLeft - 16; + } + + // --- Notifications : fermeture manuelle, et automatique après quelques secondes ---------------- + const dismiss = (toast) => { + toast.classList.add('is-leaving'); + setTimeout(() => toast.remove(), 220); + }; + document.querySelectorAll('[data-toast]').forEach((toast) => { + let timer = setTimeout(() => dismiss(toast), 9000); + toast.addEventListener('mouseenter', () => clearTimeout(timer)); + toast.addEventListener('mouseleave', () => { timer = setTimeout(() => dismiss(toast), 4000); }); + toast.querySelector('[data-dismiss]')?.addEventListener('click', () => { clearTimeout(timer); dismiss(toast); }); + }); + + // --- Confirmation :
+ const dialog = document.getElementById('confirm-dialog'); + if (dialog && typeof dialog.showModal === 'function') { + const message = dialog.querySelector('[data-message]'); + const ok = dialog.querySelector('[data-ok]'); + let pending = null; + + document.addEventListener('submit', (e) => { + const form = e.target; + if (!(form instanceof HTMLFormElement) || !form.dataset.confirm || form.dataset.confirmed) return; + e.preventDefault(); + e.stopImmediatePropagation(); + pending = form; + message.textContent = form.dataset.confirm; + ok.textContent = form.dataset.confirmLabel || 'Confirmer'; + ok.classList.toggle('danger', 'confirmDanger' in form.dataset); + ok.classList.toggle('solid', 'confirmDanger' in form.dataset); + dialog.returnValue = ''; + dialog.showModal(); + }, true); + + dialog.addEventListener('close', () => { + const form = pending; + pending = null; + if (!form || dialog.returnValue !== 'ok') return; + form.dataset.confirmed = '1'; + form.requestSubmit ? form.requestSubmit() : form.submit(); + }); + } + + // --- Bouton occupé : ---------------------------------- + document.addEventListener('submit', (e) => { + const form = e.target; + if (!(form instanceof HTMLFormElement) || !form.dataset.busy || e.defaultPrevented) return; + const button = form.querySelector('button[type=submit], button:not([type])'); + if (!button) return; + button.classList.add('is-busy'); + button.setAttribute('aria-disabled', 'true'); + const label = button.querySelector('[data-label]'); + if (label) label.textContent = form.dataset.busy; + }); + + // --- Copie dans le presse-papiers : - {{ $agents->total() }} agent(s) + + + {{ $agents->total() }} agent{{ $agents->total() > 1 ? 's' : '' }}
-
- - - - @forelse ($agents as $a) - - - - - - - @empty - - @endforelse - -
AgentServiceGroupes signature_*Template appliqué
{{ $a->display_name }}
{{ $a->mail }}
{{ $a->department }}{{ $a->groups->pluck('display_name')->join(', ') ?: '—' }}{{ $resolved[$a->id] ?? '— aucun —' }}
Aucun agent trouvé.
+ +
+
+ + + + @forelse ($agents as $a) + + + + + + + @empty + + @endforelse + +
AgentServiceGroupes signature_*Template appliqué
+
+ +
+ {{ $a->display_name }} +
{{ $a->mail }}
+
+
+
{{ $a->department }}{{ $a->groups->pluck('display_name')->join(', ') ?: '—' }} + @if ($resolved[$a->id] ?? null) + {{ $resolved[$a->id] }} + @else + aucun + @endif +
+
+ +

Aucun agent trouvé{{ $q !== '' ? 'Essayez un autre nom, une adresse ou un service.' : 'Lancez une synchronisation de l\'annuaire depuis le tableau de bord.' }}

+
+
+
@if ($agents->hasPages()) -