@imenam/mcp-messenger 1.0.24 → 1.0.25
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/README.md +10 -7
- package/dist/src/server/ui/app.js +61 -62
- package/dist/src/server/ui/index.html +215 -217
- package/dist/src/server/ui/styles.css +38 -83
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -424,7 +424,9 @@ correspondants et l'état de lecture :
|
|
|
424
424
|
pastille, qui écrit à qui, heure — et sur **l'objet du message**, qui est ce qui
|
|
425
425
|
l'identifie. Les deux forment le bandeau qui l'ouvre et le referme, cliquable d'un bout à
|
|
426
426
|
l'autre et grisé au survol pour se donner à voir comme une cible. Ouvrir un message ne
|
|
427
|
-
change rien à ce bandeau : le corps vient simplement se poser dessous.
|
|
427
|
+
change rien à ce bandeau : le corps vient simplement se poser dessous. Le destinataire
|
|
428
|
+
n'y figure pas — un fil ne lie que deux boîtes, le répéter à chaque ligne n'apprend
|
|
429
|
+
rien ;
|
|
428
430
|
Un fil se parcourt avant de se lire, et le rafraîchissement automatique ne referme pas
|
|
429
431
|
ce qui est en train d'être lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
|
|
430
432
|
citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
|
|
@@ -434,9 +436,10 @@ correspondants et l'état de lecture :
|
|
|
434
436
|
suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
|
|
435
437
|
Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
|
|
436
438
|
ligne, où un dièse ou une paire d'astérisques ne serait que du bruit ;
|
|
437
|
-
- **
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
- **les détails d'un message**, dans une modale ouverte par l'icône d'information au bout
|
|
440
|
+
de sa ligne : l'objet, le nom et l'identifiant du fil, les identifiants, la date, la
|
|
441
|
+
priorité, les deux noms et l'adresse de chaque boîte, et le tableau de distribution
|
|
442
|
+
(rôle, dossier, état). Tout ce qui se recopie a son bouton de copie ;
|
|
440
443
|
- une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
|
|
441
444
|
messages ;
|
|
442
445
|
- **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
|
|
@@ -447,8 +450,8 @@ correspondants et l'état de lecture :
|
|
|
447
450
|
|
|
448
451
|
**Les adresses ne sont pas de l'information de lecture.** La console résout partout
|
|
449
452
|
`box_9f2c4a01b3d7e5f8` en nom d'agent, et ne montre l'adresse qu'aux endroits où elle sert
|
|
450
|
-
vraiment :
|
|
451
|
-
un bouton de copie. Quand plusieurs sessions d'un même projet apparaissent, le nom de
|
|
453
|
+
vraiment : les détails d'un message, l'en-tête d'un fil et les boîtes sans nom, chaque fois
|
|
454
|
+
avec un bouton de copie. Quand plusieurs sessions d'un même projet apparaissent, le nom de
|
|
452
455
|
l'agent est suivi de celui de la boîte entre parenthèses — sans quoi un fil se lirait
|
|
453
456
|
« Documentor Dev → Documentor Dev » sans dire de quelles sessions il parle. Une boîte jamais connectée se distingue à son avatar en contour et à la mention
|
|
454
457
|
« jamais connectée » — elle reste listée, puisqu'elle a du courrier en attente.
|
|
@@ -638,7 +641,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
638
641
|
|
|
639
642
|
### Tests
|
|
640
643
|
|
|
641
|
-
|
|
644
|
+
287 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
|
|
642
645
|
désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
|
|
643
646
|
leur double confirmation et le refus nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
|
|
644
647
|
l'hôte, boîte orpheline, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
|
|
@@ -26,6 +26,7 @@ const ICONS = {
|
|
|
26
26
|
close: ["M18 6 6 18", "m6 6 12 12"],
|
|
27
27
|
link: ["M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1", "M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"],
|
|
28
28
|
chevron: ["m9 18 6-6-6-6"],
|
|
29
|
+
info: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 11v5", "M12 8h.01"],
|
|
29
30
|
trash: ["M3 6h18", "M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2", "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"],
|
|
30
31
|
};
|
|
31
32
|
|
|
@@ -52,8 +53,6 @@ const state = {
|
|
|
52
53
|
filter: "all",
|
|
53
54
|
search: "",
|
|
54
55
|
threadId: null,
|
|
55
|
-
/** Message dont le volet de détail est ouvert, ou null si le volet est fermé. */
|
|
56
|
-
messageId: null,
|
|
57
56
|
/**
|
|
58
57
|
* Messages dépliés dans le lecteur, par identifiant. Vide au départ : un fil s'ouvre
|
|
59
58
|
* replié, et se parcourt avant de se lire.
|
|
@@ -95,7 +94,8 @@ const el = {
|
|
|
95
94
|
chips: document.getElementById("chips"),
|
|
96
95
|
threads: document.getElementById("threads"),
|
|
97
96
|
reader: document.getElementById("reader"),
|
|
98
|
-
|
|
97
|
+
details: document.getElementById("details"),
|
|
98
|
+
detailsBody: document.getElementById("details-body"),
|
|
99
99
|
purge: document.getElementById("purge"),
|
|
100
100
|
purgeOpen: document.getElementById("purge-open"),
|
|
101
101
|
purgeClose: document.getElementById("purge-close"),
|
|
@@ -462,9 +462,6 @@ function select(kind, key) {
|
|
|
462
462
|
if (isSelected(kind, key)) return;
|
|
463
463
|
state.selection = { kind, key };
|
|
464
464
|
state.threadId = null;
|
|
465
|
-
state.messageId = null;
|
|
466
|
-
el.sheet.hidden = true;
|
|
467
|
-
el.console.classList.remove("has-sheet");
|
|
468
465
|
refresh();
|
|
469
466
|
}
|
|
470
467
|
|
|
@@ -900,8 +897,7 @@ function fillMessage(item, message, isLast) {
|
|
|
900
897
|
const open = state.openMessages.has(message.message_id);
|
|
901
898
|
const classes = ["message"];
|
|
902
899
|
if (open) classes.push("is-expanded");
|
|
903
|
-
if (
|
|
904
|
-
else if (isLast) classes.push("is-last");
|
|
900
|
+
if (isLast) classes.push("is-last");
|
|
905
901
|
const row = node("div", classes.join(" "));
|
|
906
902
|
const column = node("div", "message-body-col");
|
|
907
903
|
|
|
@@ -917,7 +913,6 @@ function fillMessage(item, message, isLast) {
|
|
|
917
913
|
|
|
918
914
|
const identity = node("span", "message-identity");
|
|
919
915
|
identity.appendChild(node("span", "message-from", party(message.from).name));
|
|
920
|
-
identity.appendChild(node("span", "message-to", `→ ${party(message.to).name}`));
|
|
921
916
|
if (message.priority === "high") identity.appendChild(tag("tag tag--high", "priorité haute"));
|
|
922
917
|
if (message.priority === "low") identity.appendChild(tag("tag", "priorité basse"));
|
|
923
918
|
if (!message.read) {
|
|
@@ -926,6 +921,7 @@ function fillMessage(item, message, isLast) {
|
|
|
926
921
|
identity.appendChild(dot);
|
|
927
922
|
}
|
|
928
923
|
identity.appendChild(node("span", "message-time", formatShort(message.created_at)));
|
|
924
|
+
identity.appendChild(detailsButton(message));
|
|
929
925
|
head.appendChild(identity);
|
|
930
926
|
|
|
931
927
|
// L'objet, toujours : c'est le titre du message, et c'est lui qu'on cherche du regard.
|
|
@@ -957,14 +953,6 @@ function fillMessage(item, message, isLast) {
|
|
|
957
953
|
? tag("tag tag--ok", `lu par ${party(message.to).name}`, "check")
|
|
958
954
|
: tag("tag tag--warn", `non lu par ${party(message.to).name}`, "clock")
|
|
959
955
|
);
|
|
960
|
-
// Le volet de détail, lui, s'ouvre à côté : c'est un autre état que le dépli du corps.
|
|
961
|
-
const sheetOpen = state.messageId === message.message_id;
|
|
962
|
-
foot.appendChild(
|
|
963
|
-
button("btn btn--link", sheetOpen ? "détails ouverts" : "détails", () => {
|
|
964
|
-
if (sheetOpen) closeSheet();
|
|
965
|
-
else openMessage(message.message_id);
|
|
966
|
-
})
|
|
967
|
-
);
|
|
968
956
|
content.appendChild(foot);
|
|
969
957
|
|
|
970
958
|
row.appendChild(column);
|
|
@@ -983,44 +971,82 @@ function messageText(message) {
|
|
|
983
971
|
return container;
|
|
984
972
|
}
|
|
985
973
|
|
|
986
|
-
// ---
|
|
974
|
+
// --- Détails d'un message --------------------------------------------------
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Bouton d'information, au bout de la ligne.
|
|
978
|
+
*
|
|
979
|
+
* Un fil ne lie que deux boîtes : répéter à chaque message qui écrit à qui, sous quel nom
|
|
980
|
+
* de boîte, encombre la lecture pour une information déjà connue. Elle est donc rangée
|
|
981
|
+
* ici, à un clic, avec tout le reste — adresses, identifiants, distribution.
|
|
982
|
+
*/
|
|
983
|
+
function detailsButton(message) {
|
|
984
|
+
const open = node("button", "message-info");
|
|
985
|
+
open.type = "button";
|
|
986
|
+
open.title = "Détails du message";
|
|
987
|
+
open.setAttribute("aria-label", "Détails du message");
|
|
988
|
+
open.appendChild(icon("info"));
|
|
989
|
+
// Le bandeau entier ouvre le message : ce clic-ci ne doit pas le replier au passage.
|
|
990
|
+
open.addEventListener("click", (event) => {
|
|
991
|
+
event.stopPropagation();
|
|
992
|
+
openDetails(message.message_id);
|
|
993
|
+
});
|
|
994
|
+
return open;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function openDetails(messageId) {
|
|
998
|
+
el.detailsBody.replaceChildren(node("p", "muted", "Chargement…"));
|
|
999
|
+
el.details.showModal();
|
|
1000
|
+
try {
|
|
1001
|
+
renderDetails(await api(`/admin/messages/${encodeURIComponent(messageId)}`));
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (error.message === "unauthorized") el.details.close();
|
|
1004
|
+
else el.detailsBody.replaceChildren(node("p", "purge-error", `Erreur : ${error.message}`));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
987
1007
|
|
|
988
|
-
|
|
1008
|
+
/** Tout ce que la conversation ne montre pas, et que la supervision peut vouloir lire. */
|
|
1009
|
+
function renderDetails(message) {
|
|
989
1010
|
const container = document.createDocumentFragment();
|
|
990
1011
|
|
|
991
|
-
const head = node("div", "
|
|
1012
|
+
const head = node("div", "modal-head");
|
|
992
1013
|
const titles = node("div");
|
|
993
|
-
titles.appendChild(node("
|
|
994
|
-
titles.appendChild(node("p", "
|
|
1014
|
+
titles.appendChild(node("h2", null, "Détails du message"));
|
|
1015
|
+
titles.appendChild(node("p", "details-subject", message.subject));
|
|
995
1016
|
head.appendChild(titles);
|
|
996
1017
|
const close = node("button", "btn btn--icon btn--ghost");
|
|
997
1018
|
close.type = "button";
|
|
998
1019
|
close.title = "Fermer";
|
|
999
|
-
close.setAttribute("aria-label", "Fermer
|
|
1020
|
+
close.setAttribute("aria-label", "Fermer les détails");
|
|
1000
1021
|
close.appendChild(icon("close"));
|
|
1001
|
-
close.addEventListener("click",
|
|
1022
|
+
close.addEventListener("click", () => el.details.close());
|
|
1002
1023
|
head.appendChild(close);
|
|
1003
1024
|
container.appendChild(head);
|
|
1004
1025
|
|
|
1005
|
-
const ids = node("div", "
|
|
1026
|
+
const ids = node("div", "details-section");
|
|
1006
1027
|
ids.appendChild(node("span", "section-label", "Message"));
|
|
1007
1028
|
ids.appendChild(kvCopy("Identifiant", message.message_id));
|
|
1008
|
-
ids.appendChild(
|
|
1029
|
+
ids.appendChild(kv("Objet", node("span", "kv-value", message.subject)));
|
|
1009
1030
|
ids.appendChild(kv("Envoyé le", node("span", "kv-value", formatDate(message.created_at))));
|
|
1010
1031
|
ids.appendChild(kv("Priorité", priorityTag(message.priority)));
|
|
1011
1032
|
container.appendChild(ids);
|
|
1012
1033
|
|
|
1034
|
+
const thread = node("div", "details-section");
|
|
1035
|
+
thread.appendChild(node("span", "section-label", "Fil"));
|
|
1036
|
+
// Sans nom donné par un participant, le fil n'a que son identifiant et le sujet d'ouverture.
|
|
1037
|
+
thread.appendChild(kv("Nom", node("span", "kv-value", message.thread_title || "fil sans nom")));
|
|
1038
|
+
thread.appendChild(kvCopy("Identifiant", message.thread_id));
|
|
1039
|
+
container.appendChild(thread);
|
|
1040
|
+
|
|
1013
1041
|
container.appendChild(partySection("De", message.from));
|
|
1014
1042
|
container.appendChild(partySection("À", message.to));
|
|
1015
1043
|
|
|
1016
|
-
const deliveries = node("div", "
|
|
1044
|
+
const deliveries = node("div", "details-section");
|
|
1017
1045
|
deliveries.appendChild(node("span", "section-label", "Distribution"));
|
|
1018
1046
|
deliveries.appendChild(deliveriesTable(message.deliveries || []));
|
|
1019
1047
|
container.appendChild(deliveries);
|
|
1020
1048
|
|
|
1021
|
-
el.
|
|
1022
|
-
el.sheet.hidden = false;
|
|
1023
|
-
el.console.classList.add("has-sheet");
|
|
1049
|
+
el.detailsBody.replaceChildren(container);
|
|
1024
1050
|
}
|
|
1025
1051
|
|
|
1026
1052
|
function kv(key, valueNode) {
|
|
@@ -1050,7 +1076,7 @@ function priorityTag(priority) {
|
|
|
1050
1076
|
*/
|
|
1051
1077
|
function partySection(label, agentId) {
|
|
1052
1078
|
const who = party(agentId);
|
|
1053
|
-
const section = node("div", "
|
|
1079
|
+
const section = node("div", "details-section");
|
|
1054
1080
|
section.appendChild(node("span", "section-label", label));
|
|
1055
1081
|
|
|
1056
1082
|
const block = node("div", "party");
|
|
@@ -1092,24 +1118,13 @@ function deliveriesTable(deliveries) {
|
|
|
1092
1118
|
return table;
|
|
1093
1119
|
}
|
|
1094
1120
|
|
|
1095
|
-
function closeSheet() {
|
|
1096
|
-
state.messageId = null;
|
|
1097
|
-
el.sheet.hidden = true;
|
|
1098
|
-
el.sheet.replaceChildren();
|
|
1099
|
-
el.console.classList.remove("has-sheet");
|
|
1100
|
-
if (state.threadId) refreshThread();
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
1121
|
// --- Navigation ------------------------------------------------------------
|
|
1104
1122
|
|
|
1105
1123
|
async function openThread(threadId) {
|
|
1106
1124
|
state.threadId = threadId;
|
|
1107
|
-
state.messageId = null;
|
|
1108
1125
|
// Un fil s'ouvre replié, même celui qu'on rouvre : ce qui était déplié l'était pour
|
|
1109
1126
|
// l'autre conversation.
|
|
1110
1127
|
state.openMessages.clear();
|
|
1111
|
-
el.sheet.hidden = true;
|
|
1112
|
-
el.console.classList.remove("has-sheet");
|
|
1113
1128
|
await refreshThread();
|
|
1114
1129
|
await refresh({ keepThread: true });
|
|
1115
1130
|
}
|
|
@@ -1126,16 +1141,6 @@ async function refreshThread() {
|
|
|
1126
1141
|
}
|
|
1127
1142
|
}
|
|
1128
1143
|
|
|
1129
|
-
async function openMessage(messageId) {
|
|
1130
|
-
state.messageId = messageId;
|
|
1131
|
-
try {
|
|
1132
|
-
renderSheet(await api(`/admin/messages/${encodeURIComponent(messageId)}`));
|
|
1133
|
-
await refreshThread();
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
if (error.message !== "unauthorized") el.sheet.replaceChildren(node("div", "banner", error.message));
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
1144
|
// --- Boucle de rafraîchissement --------------------------------------------
|
|
1140
1145
|
|
|
1141
1146
|
async function refresh(options) {
|
|
@@ -1173,14 +1178,13 @@ async function refresh(options) {
|
|
|
1173
1178
|
/**
|
|
1174
1179
|
* Ce que chaque séparateur commande.
|
|
1175
1180
|
*
|
|
1176
|
-
* `edge` dit de quel côté se trouve le panneau réglé :
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1181
|
+
* `edge` dit de quel côté se trouve le panneau réglé : ici, chacun borde le panneau qui
|
|
1182
|
+
* le précède. La distinction reste écrite car un panneau ancré à droite se réglerait à
|
|
1183
|
+
* l'envers, en comptant depuis son bord droit.
|
|
1179
1184
|
*/
|
|
1180
1185
|
const PANELS = {
|
|
1181
1186
|
"resize-agents": { name: "--w-agents", panel: "agentsPane", edge: "before", min: 180, max: 520, initial: 240 },
|
|
1182
1187
|
"resize-threads": { name: "--w-threads", panel: "threadsPane", edge: "before", min: 260, max: 720, initial: 400 },
|
|
1183
|
-
"resize-sheet": { name: "--w-sheet", panel: "sheet", edge: "after", min: 280, max: 720, initial: 360 },
|
|
1184
1188
|
};
|
|
1185
1189
|
|
|
1186
1190
|
const LAYOUT_KEY = "messenger.layout";
|
|
@@ -1457,9 +1461,6 @@ async function confirmPurge() {
|
|
|
1457
1461
|
state.selection = { kind: "all", key: null };
|
|
1458
1462
|
}
|
|
1459
1463
|
state.threadId = null;
|
|
1460
|
-
state.messageId = null;
|
|
1461
|
-
el.sheet.hidden = true;
|
|
1462
|
-
el.console.classList.remove("has-sheet");
|
|
1463
1464
|
await refresh();
|
|
1464
1465
|
} catch (error) {
|
|
1465
1466
|
purge.armed = false;
|
|
@@ -1547,11 +1548,9 @@ el.logout.addEventListener("click", async () => {
|
|
|
1547
1548
|
state.expanded = new Set();
|
|
1548
1549
|
state.showEmpty = new Set();
|
|
1549
1550
|
state.threadId = null;
|
|
1550
|
-
state.
|
|
1551
|
+
state.openMessages = new Set();
|
|
1551
1552
|
state.directory = new Map();
|
|
1552
1553
|
state.groups = [];
|
|
1553
|
-
el.sheet.hidden = true;
|
|
1554
|
-
el.console.classList.remove("has-sheet");
|
|
1555
1554
|
el.agents.replaceChildren();
|
|
1556
1555
|
el.threads.replaceChildren();
|
|
1557
1556
|
renderEmptyReader();
|
|
@@ -1,217 +1,215 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="fr">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>MCP Messenger — supervision</title>
|
|
7
|
-
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<header class="topbar">
|
|
11
|
-
<div class="brand">
|
|
12
|
-
<svg class="brand-mark" viewBox="0 0 24 24" aria-hidden="true">
|
|
13
|
-
<path d="M22 12h-6l-2 3h-4l-2-3H2" />
|
|
14
|
-
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
|
15
|
-
</svg>
|
|
16
|
-
<h1>MCP Messenger</h1>
|
|
17
|
-
<span class="pill pill--idle" id="status">
|
|
18
|
-
<span class="pill-dot"></span>
|
|
19
|
-
<span id="status-label">connexion…</span>
|
|
20
|
-
</span>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="topbar-search" id="search-box">
|
|
24
|
-
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
25
|
-
<circle cx="11" cy="11" r="8" />
|
|
26
|
-
<path d="m21 21-4.3-4.3" />
|
|
27
|
-
</svg>
|
|
28
|
-
<input type="search" id="search" placeholder="Rechercher un sujet, un fil, un agent…" />
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="topbar-controls" id="controls" hidden>
|
|
32
|
-
<label class="switch">
|
|
33
|
-
<input type="checkbox" id="auto-refresh" checked />
|
|
34
|
-
<span class="switch-track"><span class="switch-knob"></span></span>
|
|
35
|
-
<span class="switch-label">auto · 5 s</span>
|
|
36
|
-
</label>
|
|
37
|
-
<button type="button" id="refresh" class="btn btn--icon" title="Rafraîchir" aria-label="Rafraîchir">
|
|
38
|
-
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
39
|
-
<path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
|
|
40
|
-
<path d="M21 3v5h-5" />
|
|
41
|
-
<path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
|
|
42
|
-
<path d="M3 21v-5h5" />
|
|
43
|
-
</svg>
|
|
44
|
-
</button>
|
|
45
|
-
<button type="button" id="purge-open" class="btn btn--icon" title="Purger les boîtes inactives" aria-label="Purger les boîtes inactives">
|
|
46
|
-
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
47
|
-
<path d="M3 6h18" />
|
|
48
|
-
<path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
|
|
49
|
-
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
|
50
|
-
</svg>
|
|
51
|
-
</button>
|
|
52
|
-
<button type="button" id="logout" class="btn btn--link">Se déconnecter</button>
|
|
53
|
-
</div>
|
|
54
|
-
</header>
|
|
55
|
-
|
|
56
|
-
<!--
|
|
57
|
-
Purge des boîtes inactives : la seule action de la console qui écrit, et elle
|
|
58
|
-
détruit. Elle se fait donc en deux temps, l'aperçu puis la confirmation, et ne porte
|
|
59
|
-
jamais que sur les boîtes affichées.
|
|
60
|
-
-->
|
|
61
|
-
<dialog class="modal" id="purge">
|
|
62
|
-
<div class="modal-card">
|
|
63
|
-
<div class="modal-head">
|
|
64
|
-
<h2>Nettoyer les boîtes</h2>
|
|
65
|
-
<button type="button" class="btn btn--icon btn--ghost" id="purge-close" aria-label="Fermer">
|
|
66
|
-
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
67
|
-
<path d="M18 6 6 18" />
|
|
68
|
-
<path d="m6 6 12 12" />
|
|
69
|
-
</svg>
|
|
70
|
-
</button>
|
|
71
|
-
</div>
|
|
72
|
-
<p class="modal-sub">
|
|
73
|
-
Deux nettoyages, deux critères. La suppression est définitive dans les deux cas, et emporte les messages que
|
|
74
|
-
plus personne ne pourrait lire ; ce que le correspondant a reçu de son côté reste intact. Un agent dont toutes
|
|
75
|
-
les boîtes disparaissent quitte l'annuaire avec elles.
|
|
76
|
-
</p>
|
|
77
|
-
|
|
78
|
-
<!-- Premier critère : la boîte n'a jamais servi. Il ne dépend d'aucune date. -->
|
|
79
|
-
<div class="purge-controls">
|
|
80
|
-
<div class="purge-criterion">
|
|
81
|
-
<span class="field-label">Boîtes vides</span>
|
|
82
|
-
<span class="purge-hint">Rien n'y est jamais passé, ni envoi ni réception, quel que soit leur âge.</span>
|
|
83
|
-
</div>
|
|
84
|
-
<button type="button" class="btn" id="purge-preview-empty">Prévisualiser</button>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<!-- Second critère : la boîte a servi, mais plus rien ne l'a touchée depuis. -->
|
|
88
|
-
<div class="purge-controls">
|
|
89
|
-
<label class="field">
|
|
90
|
-
<span class="field-label">Boîtes sans activité depuis le</span>
|
|
91
|
-
<span class="field-input"><input type="date" id="purge-before" /></span>
|
|
92
|
-
<span class="purge-hint">Ni connexion, ni message. Une boîte créée après cette date est épargnée.</span>
|
|
93
|
-
</label>
|
|
94
|
-
<button type="button" class="btn" id="purge-preview">Prévisualiser</button>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div class="purge-result" id="purge-result"></div>
|
|
98
|
-
|
|
99
|
-
<div class="modal-foot">
|
|
100
|
-
<button type="button" class="btn btn--link" id="purge-cancel">Fermer</button>
|
|
101
|
-
<button type="button" class="btn btn--danger" id="purge-confirm" disabled>Supprimer</button>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</dialog>
|
|
105
|
-
|
|
106
|
-
<!--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</span>
|
|
129
|
-
<span class="field-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class="
|
|
162
|
-
id="
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
</body>
|
|
217
|
-
</html>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>MCP Messenger — supervision</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<div class="brand">
|
|
12
|
+
<svg class="brand-mark" viewBox="0 0 24 24" aria-hidden="true">
|
|
13
|
+
<path d="M22 12h-6l-2 3h-4l-2-3H2" />
|
|
14
|
+
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
|
15
|
+
</svg>
|
|
16
|
+
<h1>MCP Messenger</h1>
|
|
17
|
+
<span class="pill pill--idle" id="status">
|
|
18
|
+
<span class="pill-dot"></span>
|
|
19
|
+
<span id="status-label">connexion…</span>
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="topbar-search" id="search-box">
|
|
24
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
25
|
+
<circle cx="11" cy="11" r="8" />
|
|
26
|
+
<path d="m21 21-4.3-4.3" />
|
|
27
|
+
</svg>
|
|
28
|
+
<input type="search" id="search" placeholder="Rechercher un sujet, un fil, un agent…" />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="topbar-controls" id="controls" hidden>
|
|
32
|
+
<label class="switch">
|
|
33
|
+
<input type="checkbox" id="auto-refresh" checked />
|
|
34
|
+
<span class="switch-track"><span class="switch-knob"></span></span>
|
|
35
|
+
<span class="switch-label">auto · 5 s</span>
|
|
36
|
+
</label>
|
|
37
|
+
<button type="button" id="refresh" class="btn btn--icon" title="Rafraîchir" aria-label="Rafraîchir">
|
|
38
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
39
|
+
<path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
|
|
40
|
+
<path d="M21 3v5h-5" />
|
|
41
|
+
<path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
|
|
42
|
+
<path d="M3 21v-5h5" />
|
|
43
|
+
</svg>
|
|
44
|
+
</button>
|
|
45
|
+
<button type="button" id="purge-open" class="btn btn--icon" title="Purger les boîtes inactives" aria-label="Purger les boîtes inactives">
|
|
46
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
47
|
+
<path d="M3 6h18" />
|
|
48
|
+
<path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
|
|
49
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
<button type="button" id="logout" class="btn btn--link">Se déconnecter</button>
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
|
|
56
|
+
<!--
|
|
57
|
+
Purge des boîtes inactives : la seule action de la console qui écrit, et elle
|
|
58
|
+
détruit. Elle se fait donc en deux temps, l'aperçu puis la confirmation, et ne porte
|
|
59
|
+
jamais que sur les boîtes affichées.
|
|
60
|
+
-->
|
|
61
|
+
<dialog class="modal" id="purge">
|
|
62
|
+
<div class="modal-card">
|
|
63
|
+
<div class="modal-head">
|
|
64
|
+
<h2>Nettoyer les boîtes</h2>
|
|
65
|
+
<button type="button" class="btn btn--icon btn--ghost" id="purge-close" aria-label="Fermer">
|
|
66
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
67
|
+
<path d="M18 6 6 18" />
|
|
68
|
+
<path d="m6 6 12 12" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<p class="modal-sub">
|
|
73
|
+
Deux nettoyages, deux critères. La suppression est définitive dans les deux cas, et emporte les messages que
|
|
74
|
+
plus personne ne pourrait lire ; ce que le correspondant a reçu de son côté reste intact. Un agent dont toutes
|
|
75
|
+
les boîtes disparaissent quitte l'annuaire avec elles.
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
<!-- Premier critère : la boîte n'a jamais servi. Il ne dépend d'aucune date. -->
|
|
79
|
+
<div class="purge-controls">
|
|
80
|
+
<div class="purge-criterion">
|
|
81
|
+
<span class="field-label">Boîtes vides</span>
|
|
82
|
+
<span class="purge-hint">Rien n'y est jamais passé, ni envoi ni réception, quel que soit leur âge.</span>
|
|
83
|
+
</div>
|
|
84
|
+
<button type="button" class="btn" id="purge-preview-empty">Prévisualiser</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Second critère : la boîte a servi, mais plus rien ne l'a touchée depuis. -->
|
|
88
|
+
<div class="purge-controls">
|
|
89
|
+
<label class="field">
|
|
90
|
+
<span class="field-label">Boîtes sans activité depuis le</span>
|
|
91
|
+
<span class="field-input"><input type="date" id="purge-before" /></span>
|
|
92
|
+
<span class="purge-hint">Ni connexion, ni message. Une boîte créée après cette date est épargnée.</span>
|
|
93
|
+
</label>
|
|
94
|
+
<button type="button" class="btn" id="purge-preview">Prévisualiser</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="purge-result" id="purge-result"></div>
|
|
98
|
+
|
|
99
|
+
<div class="modal-foot">
|
|
100
|
+
<button type="button" class="btn btn--link" id="purge-cancel">Fermer</button>
|
|
101
|
+
<button type="button" class="btn btn--danger" id="purge-confirm" disabled>Supprimer</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</dialog>
|
|
105
|
+
|
|
106
|
+
<!--
|
|
107
|
+
Détails d'un message : tout ce que la conversation ne montre pas — adresses, noms de
|
|
108
|
+
boîtes, identifiants, distribution. Une modale, parce que c'est une consultation
|
|
109
|
+
ponctuelle : elle s'ouvre sur demande et rend la place en se fermant.
|
|
110
|
+
-->
|
|
111
|
+
<dialog class="modal" id="details">
|
|
112
|
+
<div class="modal-card" id="details-body"></div>
|
|
113
|
+
</dialog>
|
|
114
|
+
|
|
115
|
+
<!-- Écran de connexion : une carte seule, sans les panneaux vides derrière elle. -->
|
|
116
|
+
<main class="login" id="login" hidden>
|
|
117
|
+
<form class="card" id="login-form">
|
|
118
|
+
<div class="card-badge">
|
|
119
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
120
|
+
<rect x="3" y="11" width="18" height="11" rx="2" />
|
|
121
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
<h2>Console de supervision</h2>
|
|
125
|
+
<p class="card-sub" id="login-origin">MCP Messenger</p>
|
|
126
|
+
|
|
127
|
+
<label class="field">
|
|
128
|
+
<span class="field-label">Jeton administrateur</span>
|
|
129
|
+
<span class="field-input">
|
|
130
|
+
<input type="password" id="token" autocomplete="current-password" placeholder="MESSENGER_ADMIN_TOKEN" />
|
|
131
|
+
<button type="button" class="field-eye" id="token-reveal" aria-label="Afficher le jeton">
|
|
132
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
133
|
+
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" />
|
|
134
|
+
<circle cx="12" cy="12" r="3" />
|
|
135
|
+
</svg>
|
|
136
|
+
</button>
|
|
137
|
+
</span>
|
|
138
|
+
<span class="field-error" id="token-error"></span>
|
|
139
|
+
</label>
|
|
140
|
+
|
|
141
|
+
<button type="submit" class="btn btn--primary btn--block">Se connecter</button>
|
|
142
|
+
<p class="card-note">
|
|
143
|
+
Le jeton n'est jamais conservé dans le navigateur : la session tient dans un cookie HttpOnly, fermé à la
|
|
144
|
+
déconnexion.
|
|
145
|
+
</p>
|
|
146
|
+
</form>
|
|
147
|
+
</main>
|
|
148
|
+
|
|
149
|
+
<!-- Console : agents, fils, lecteur du fil. -->
|
|
150
|
+
<main class="console" id="console" hidden>
|
|
151
|
+
<aside class="agents" id="agents-pane">
|
|
152
|
+
<div class="agents-head">
|
|
153
|
+
<span class="section-label">Agents</span>
|
|
154
|
+
<button type="button" class="btn btn--icon btn--ghost" id="collapse" title="Replier" aria-label="Replier la liste des agents" aria-expanded="true">
|
|
155
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
156
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
157
|
+
<path d="M9 3v18" />
|
|
158
|
+
</svg>
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
<ul class="agent-list" id="agents"></ul>
|
|
162
|
+
<p class="agents-foot muted" id="agents-foot"></p>
|
|
163
|
+
</aside>
|
|
164
|
+
|
|
165
|
+
<!--
|
|
166
|
+
Séparateurs déplaçables. Chacun redimensionne le panneau qu'il borde, à la souris
|
|
167
|
+
comme aux flèches du clavier ; un double-clic rend sa largeur d'origine.
|
|
168
|
+
-->
|
|
169
|
+
<div
|
|
170
|
+
class="resizer"
|
|
171
|
+
id="resize-agents"
|
|
172
|
+
role="separator"
|
|
173
|
+
aria-orientation="vertical"
|
|
174
|
+
aria-label="Largeur de la colonne des agents"
|
|
175
|
+
tabindex="0"
|
|
176
|
+
></div>
|
|
177
|
+
|
|
178
|
+
<section class="threads">
|
|
179
|
+
<div class="threads-head">
|
|
180
|
+
<!--
|
|
181
|
+
L'en-tête dit sur quoi la liste est filtrée. Quand c'est un agent, il y tient la
|
|
182
|
+
place principale, pastille comprise : c'est le sujet de ce qu'on lit en dessous.
|
|
183
|
+
-->
|
|
184
|
+
<div class="threads-title">
|
|
185
|
+
<span class="threads-avatar" id="threads-avatar" hidden></span>
|
|
186
|
+
<div class="threads-heading">
|
|
187
|
+
<h2 id="threads-name">Fils</h2>
|
|
188
|
+
<p class="muted" id="threads-scope"></p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="chips" id="chips">
|
|
192
|
+
<button type="button" class="chip is-active" data-filter="all">Tous</button>
|
|
193
|
+
<button type="button" class="chip" data-filter="unread">Non lus</button>
|
|
194
|
+
<button type="button" class="chip" data-filter="high">Priorité haute</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<ul class="thread-list" id="threads"></ul>
|
|
198
|
+
</section>
|
|
199
|
+
|
|
200
|
+
<div
|
|
201
|
+
class="resizer"
|
|
202
|
+
id="resize-threads"
|
|
203
|
+
role="separator"
|
|
204
|
+
aria-orientation="vertical"
|
|
205
|
+
aria-label="Largeur de la liste des fils"
|
|
206
|
+
tabindex="0"
|
|
207
|
+
></div>
|
|
208
|
+
|
|
209
|
+
<section class="reader" id="reader"></section>
|
|
210
|
+
|
|
211
|
+
</main>
|
|
212
|
+
|
|
213
|
+
<script type="module" src="./app.js"></script>
|
|
214
|
+
</body>
|
|
215
|
+
</html>
|
|
@@ -554,29 +554,16 @@ body {
|
|
|
554
554
|
.console {
|
|
555
555
|
--w-agents: 240px;
|
|
556
556
|
--w-threads: 400px;
|
|
557
|
-
--w-sheet: 360px;
|
|
558
557
|
--w-gutter: 5px;
|
|
559
558
|
display: grid;
|
|
560
559
|
grid-template-columns: var(--w-agents) var(--w-gutter) var(--w-threads) var(--w-gutter) minmax(0, 1fr);
|
|
561
560
|
height: calc(100vh - var(--topbar-h));
|
|
562
561
|
}
|
|
563
562
|
|
|
564
|
-
.console.has-sheet {
|
|
565
|
-
grid-template-columns:
|
|
566
|
-
var(--w-agents) var(--w-gutter) var(--w-threads) var(--w-gutter)
|
|
567
|
-
minmax(0, 1fr) var(--w-gutter) var(--w-sheet);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
563
|
.console.is-collapsed {
|
|
571
564
|
grid-template-columns: 57px var(--w-gutter) var(--w-threads) var(--w-gutter) minmax(0, 1fr);
|
|
572
565
|
}
|
|
573
566
|
|
|
574
|
-
.console.is-collapsed.has-sheet {
|
|
575
|
-
grid-template-columns:
|
|
576
|
-
57px var(--w-gutter) var(--w-threads) var(--w-gutter)
|
|
577
|
-
minmax(0, 1fr) var(--w-gutter) var(--w-sheet);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
567
|
/* --- Séparateurs déplaçables ---------------------------------------------- */
|
|
581
568
|
|
|
582
569
|
.resizer {
|
|
@@ -598,11 +585,6 @@ body {
|
|
|
598
585
|
outline: none;
|
|
599
586
|
}
|
|
600
587
|
|
|
601
|
-
/* Le volet de détail n'est pas toujours là ; son séparateur non plus. */
|
|
602
|
-
.console:not(.has-sheet) .resizer--sheet {
|
|
603
|
-
display: none;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
588
|
/* Colonne repliée : il n'y a plus de largeur à régler, seulement un rail d'avatars. */
|
|
607
589
|
.console.is-collapsed #resize-agents {
|
|
608
590
|
cursor: default;
|
|
@@ -617,8 +599,7 @@ body {
|
|
|
617
599
|
|
|
618
600
|
.console.is-resizing .agents,
|
|
619
601
|
.console.is-resizing .threads,
|
|
620
|
-
.console.is-resizing .reader
|
|
621
|
-
.console.is-resizing .sheet {
|
|
602
|
+
.console.is-resizing .reader {
|
|
622
603
|
pointer-events: none;
|
|
623
604
|
}
|
|
624
605
|
|
|
@@ -1420,6 +1401,30 @@ body {
|
|
|
1420
1401
|
outline-offset: -2px;
|
|
1421
1402
|
}
|
|
1422
1403
|
|
|
1404
|
+
/*
|
|
1405
|
+
L'information s'ouvre depuis la ligne sans la replier. Visuellement c'est un bouton posé
|
|
1406
|
+
dans un autre ; dans le balisage, non — il est rangé au bout de la ligne d'identité, et
|
|
1407
|
+
son clic ne remonte pas au bandeau.
|
|
1408
|
+
*/
|
|
1409
|
+
.message-info {
|
|
1410
|
+
display: flex;
|
|
1411
|
+
padding: 2px;
|
|
1412
|
+
border: 0;
|
|
1413
|
+
border-radius: 50%;
|
|
1414
|
+
background: none;
|
|
1415
|
+
color: var(--muted);
|
|
1416
|
+
cursor: pointer;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
.message-info:hover {
|
|
1420
|
+
color: var(--accent);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
.message-info:focus-visible {
|
|
1424
|
+
outline: 2px solid var(--accent);
|
|
1425
|
+
outline-offset: 1px;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1423
1428
|
.message-head > .avatar {
|
|
1424
1429
|
grid-row: span 2;
|
|
1425
1430
|
}
|
|
@@ -1435,11 +1440,11 @@ body {
|
|
|
1435
1440
|
|
|
1436
1441
|
/*
|
|
1437
1442
|
L'objet du message, sur sa ligne, replié comme déplié : c'est ce qui identifie le
|
|
1438
|
-
message, et le bandeau ne doit pas changer de forme en s'ouvrant.
|
|
1439
|
-
|
|
1443
|
+
message, et le bandeau ne doit pas changer de forme en s'ouvrant. Il se lit sans graisse
|
|
1444
|
+
— c'est déjà la ligne la plus longue du bandeau, l'appuyer en ferait un titre de section.
|
|
1445
|
+
Ce qui dépasse est coupé : la ligne suivante appartient au message suivant.
|
|
1440
1446
|
*/
|
|
1441
1447
|
.message-subject {
|
|
1442
|
-
font-weight: 600;
|
|
1443
1448
|
min-width: 0;
|
|
1444
1449
|
white-space: nowrap;
|
|
1445
1450
|
overflow: hidden;
|
|
@@ -1464,11 +1469,6 @@ body {
|
|
|
1464
1469
|
font-weight: 600;
|
|
1465
1470
|
}
|
|
1466
1471
|
|
|
1467
|
-
.message-to {
|
|
1468
|
-
font-size: 13px;
|
|
1469
|
-
color: var(--muted);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
1472
|
.message-time {
|
|
1473
1473
|
margin-left: auto;
|
|
1474
1474
|
font-size: 12px;
|
|
@@ -1639,48 +1639,26 @@ body {
|
|
|
1639
1639
|
font-size: 12px;
|
|
1640
1640
|
}
|
|
1641
1641
|
|
|
1642
|
-
/* ---
|
|
1642
|
+
/* --- Détails d'un message ------------------------------------------------- */
|
|
1643
1643
|
|
|
1644
|
-
.
|
|
1645
|
-
|
|
1646
|
-
background: var(--panel);
|
|
1647
|
-
overflow-y: auto;
|
|
1648
|
-
min-width: 0;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
.sheet-head {
|
|
1652
|
-
display: flex;
|
|
1653
|
-
align-items: flex-start;
|
|
1654
|
-
gap: 8px;
|
|
1655
|
-
padding: 18px 20px 14px;
|
|
1656
|
-
border-bottom: 1px solid var(--border);
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
.sheet-head h3 {
|
|
1660
|
-
margin: 0;
|
|
1661
|
-
font-size: 15px;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
.sheet-subject {
|
|
1644
|
+
/* L'objet, sous le titre de la modale : il dit de quel message on lit les détails. */
|
|
1645
|
+
.details-subject {
|
|
1665
1646
|
margin: 4px 0 0;
|
|
1666
1647
|
font-size: 13px;
|
|
1667
1648
|
color: var(--muted);
|
|
1668
1649
|
}
|
|
1669
1650
|
|
|
1670
|
-
.
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
.sheet-section {
|
|
1675
|
-
padding: 16px 20px;
|
|
1676
|
-
border-bottom: 1px solid var(--border);
|
|
1651
|
+
.details-section {
|
|
1652
|
+
padding: 16px 0;
|
|
1653
|
+
border-bottom: 1px solid var(--border-soft);
|
|
1677
1654
|
}
|
|
1678
1655
|
|
|
1679
|
-
.
|
|
1656
|
+
.details-section:last-of-type {
|
|
1680
1657
|
border-bottom: 0;
|
|
1658
|
+
padding-bottom: 0;
|
|
1681
1659
|
}
|
|
1682
1660
|
|
|
1683
|
-
.
|
|
1661
|
+
.details-section .section-label {
|
|
1684
1662
|
display: block;
|
|
1685
1663
|
margin-bottom: 12px;
|
|
1686
1664
|
}
|
|
@@ -1817,13 +1795,6 @@ body {
|
|
|
1817
1795
|
|
|
1818
1796
|
/* --- Écrans étroits ------------------------------------------------------- */
|
|
1819
1797
|
|
|
1820
|
-
@media (max-width: 1200px) {
|
|
1821
|
-
.console.has-sheet,
|
|
1822
|
-
.console.is-collapsed.has-sheet {
|
|
1823
|
-
grid-template-columns: 57px 320px minmax(0, 1fr) 320px;
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
1798
|
@media (max-width: 1000px) {
|
|
1828
1799
|
.topbar-search {
|
|
1829
1800
|
max-width: 240px;
|
|
@@ -1831,22 +1802,6 @@ body {
|
|
|
1831
1802
|
|
|
1832
1803
|
.console,
|
|
1833
1804
|
.console.is-collapsed {
|
|
1834
|
-
grid-template-columns: 57px 300px minmax(0, 1fr);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
/* Trop étroit pour quatre colonnes : le volet recouvre le lecteur. */
|
|
1838
|
-
.console.has-sheet,
|
|
1839
|
-
.console.is-collapsed.has-sheet {
|
|
1840
|
-
grid-template-columns: 57px 300px minmax(0, 1fr);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
.console.has-sheet .sheet {
|
|
1844
|
-
position: fixed;
|
|
1845
|
-
top: var(--topbar-h);
|
|
1846
|
-
right: 0;
|
|
1847
|
-
bottom: 0;
|
|
1848
|
-
width: 340px;
|
|
1849
|
-
max-width: 100%;
|
|
1850
|
-
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
|
|
1805
|
+
grid-template-columns: 57px var(--w-gutter) 300px var(--w-gutter) minmax(0, 1fr);
|
|
1851
1806
|
}
|
|
1852
1807
|
}
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.25";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Généré par scripts/generate-version.js — ne pas modifier à la main.
|
|
2
|
-
export const APP_VERSION = "1.0.
|
|
2
|
+
export const APP_VERSION = "1.0.25";
|