@imenam/mcp-messenger 1.0.24 → 1.0.26
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 +79 -67
- 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"),
|
|
@@ -242,6 +242,19 @@ function avatar(agentId, extraClass) {
|
|
|
242
242
|
return avatarFor(agentId, label, title, extraClass, !who.registered);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Le nom de l'agent, et lui seul.
|
|
247
|
+
*
|
|
248
|
+
* `party().name` accole le nom de la boîte dès que plusieurs sessions d'un projet
|
|
249
|
+
* existent : indispensable là où deux sessions doivent se distinguer, encombrant dans le
|
|
250
|
+
* lecteur d'un fil, où l'on suit une conversation entre deux correspondants déjà connus.
|
|
251
|
+
* Ce que la boîte ajoute — son nom, son adresse — se lit dans les détails du message.
|
|
252
|
+
*/
|
|
253
|
+
function agentLabel(agentId) {
|
|
254
|
+
const who = party(agentId);
|
|
255
|
+
return who.agentName || who.box || shortAddress(agentId);
|
|
256
|
+
}
|
|
257
|
+
|
|
245
258
|
/**
|
|
246
259
|
* Pastille de l'agent, et non de la boîte.
|
|
247
260
|
*
|
|
@@ -462,9 +475,6 @@ function select(kind, key) {
|
|
|
462
475
|
if (isSelected(kind, key)) return;
|
|
463
476
|
state.selection = { kind, key };
|
|
464
477
|
state.threadId = null;
|
|
465
|
-
state.messageId = null;
|
|
466
|
-
el.sheet.hidden = true;
|
|
467
|
-
el.console.classList.remove("has-sheet");
|
|
468
478
|
refresh();
|
|
469
479
|
}
|
|
470
480
|
|
|
@@ -856,7 +866,7 @@ function renderThread(data) {
|
|
|
856
866
|
node(
|
|
857
867
|
"span",
|
|
858
868
|
"reader-participants",
|
|
859
|
-
`${participants.map(
|
|
869
|
+
`${participants.map(agentLabel).join(", ")} · ${plural(messages.length, "message")}`
|
|
860
870
|
)
|
|
861
871
|
);
|
|
862
872
|
const threadId = node("span", "mono muted", data.thread_id);
|
|
@@ -900,8 +910,7 @@ function fillMessage(item, message, isLast) {
|
|
|
900
910
|
const open = state.openMessages.has(message.message_id);
|
|
901
911
|
const classes = ["message"];
|
|
902
912
|
if (open) classes.push("is-expanded");
|
|
903
|
-
if (
|
|
904
|
-
else if (isLast) classes.push("is-last");
|
|
913
|
+
if (isLast) classes.push("is-last");
|
|
905
914
|
const row = node("div", classes.join(" "));
|
|
906
915
|
const column = node("div", "message-body-col");
|
|
907
916
|
|
|
@@ -916,16 +925,16 @@ function fillMessage(item, message, isLast) {
|
|
|
916
925
|
head.appendChild(agentAvatar(message.from));
|
|
917
926
|
|
|
918
927
|
const identity = node("span", "message-identity");
|
|
919
|
-
identity.appendChild(node("span", "message-from",
|
|
920
|
-
identity.appendChild(node("span", "message-to", `→ ${party(message.to).name}`));
|
|
928
|
+
identity.appendChild(node("span", "message-from", agentLabel(message.from)));
|
|
921
929
|
if (message.priority === "high") identity.appendChild(tag("tag tag--high", "priorité haute"));
|
|
922
930
|
if (message.priority === "low") identity.appendChild(tag("tag", "priorité basse"));
|
|
923
931
|
if (!message.read) {
|
|
924
932
|
const dot = node("span", "message-dot");
|
|
925
|
-
dot.title = `non lu par ${
|
|
933
|
+
dot.title = `non lu par ${agentLabel(message.to)}`;
|
|
926
934
|
identity.appendChild(dot);
|
|
927
935
|
}
|
|
928
936
|
identity.appendChild(node("span", "message-time", formatShort(message.created_at)));
|
|
937
|
+
identity.appendChild(detailsButton(message));
|
|
929
938
|
head.appendChild(identity);
|
|
930
939
|
|
|
931
940
|
// L'objet, toujours : c'est le titre du message, et c'est lui qu'on cherche du regard.
|
|
@@ -954,16 +963,8 @@ function fillMessage(item, message, isLast) {
|
|
|
954
963
|
const foot = node("div", "message-foot");
|
|
955
964
|
foot.appendChild(
|
|
956
965
|
message.read
|
|
957
|
-
? tag("tag tag--ok", `lu par ${
|
|
958
|
-
: tag("tag tag--warn", `non lu par ${
|
|
959
|
-
);
|
|
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
|
-
})
|
|
966
|
+
? tag("tag tag--ok", `lu par ${agentLabel(message.to)}`, "check")
|
|
967
|
+
: tag("tag tag--warn", `non lu par ${agentLabel(message.to)}`, "clock")
|
|
967
968
|
);
|
|
968
969
|
content.appendChild(foot);
|
|
969
970
|
|
|
@@ -983,44 +984,82 @@ function messageText(message) {
|
|
|
983
984
|
return container;
|
|
984
985
|
}
|
|
985
986
|
|
|
986
|
-
// ---
|
|
987
|
+
// --- Détails d'un message --------------------------------------------------
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Bouton d'information, au bout de la ligne.
|
|
991
|
+
*
|
|
992
|
+
* Un fil ne lie que deux boîtes : répéter à chaque message qui écrit à qui, sous quel nom
|
|
993
|
+
* de boîte, encombre la lecture pour une information déjà connue. Elle est donc rangée
|
|
994
|
+
* ici, à un clic, avec tout le reste — adresses, identifiants, distribution.
|
|
995
|
+
*/
|
|
996
|
+
function detailsButton(message) {
|
|
997
|
+
const open = node("button", "message-info");
|
|
998
|
+
open.type = "button";
|
|
999
|
+
open.title = "Détails du message";
|
|
1000
|
+
open.setAttribute("aria-label", "Détails du message");
|
|
1001
|
+
open.appendChild(icon("info"));
|
|
1002
|
+
// Le bandeau entier ouvre le message : ce clic-ci ne doit pas le replier au passage.
|
|
1003
|
+
open.addEventListener("click", (event) => {
|
|
1004
|
+
event.stopPropagation();
|
|
1005
|
+
openDetails(message.message_id);
|
|
1006
|
+
});
|
|
1007
|
+
return open;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
async function openDetails(messageId) {
|
|
1011
|
+
el.detailsBody.replaceChildren(node("p", "muted", "Chargement…"));
|
|
1012
|
+
el.details.showModal();
|
|
1013
|
+
try {
|
|
1014
|
+
renderDetails(await api(`/admin/messages/${encodeURIComponent(messageId)}`));
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
if (error.message === "unauthorized") el.details.close();
|
|
1017
|
+
else el.detailsBody.replaceChildren(node("p", "purge-error", `Erreur : ${error.message}`));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
987
1020
|
|
|
988
|
-
|
|
1021
|
+
/** Tout ce que la conversation ne montre pas, et que la supervision peut vouloir lire. */
|
|
1022
|
+
function renderDetails(message) {
|
|
989
1023
|
const container = document.createDocumentFragment();
|
|
990
1024
|
|
|
991
|
-
const head = node("div", "
|
|
1025
|
+
const head = node("div", "modal-head");
|
|
992
1026
|
const titles = node("div");
|
|
993
|
-
titles.appendChild(node("
|
|
994
|
-
titles.appendChild(node("p", "
|
|
1027
|
+
titles.appendChild(node("h2", null, "Détails du message"));
|
|
1028
|
+
titles.appendChild(node("p", "details-subject", message.subject));
|
|
995
1029
|
head.appendChild(titles);
|
|
996
1030
|
const close = node("button", "btn btn--icon btn--ghost");
|
|
997
1031
|
close.type = "button";
|
|
998
1032
|
close.title = "Fermer";
|
|
999
|
-
close.setAttribute("aria-label", "Fermer
|
|
1033
|
+
close.setAttribute("aria-label", "Fermer les détails");
|
|
1000
1034
|
close.appendChild(icon("close"));
|
|
1001
|
-
close.addEventListener("click",
|
|
1035
|
+
close.addEventListener("click", () => el.details.close());
|
|
1002
1036
|
head.appendChild(close);
|
|
1003
1037
|
container.appendChild(head);
|
|
1004
1038
|
|
|
1005
|
-
const ids = node("div", "
|
|
1039
|
+
const ids = node("div", "details-section");
|
|
1006
1040
|
ids.appendChild(node("span", "section-label", "Message"));
|
|
1007
1041
|
ids.appendChild(kvCopy("Identifiant", message.message_id));
|
|
1008
|
-
ids.appendChild(
|
|
1042
|
+
ids.appendChild(kv("Objet", node("span", "kv-value", message.subject)));
|
|
1009
1043
|
ids.appendChild(kv("Envoyé le", node("span", "kv-value", formatDate(message.created_at))));
|
|
1010
1044
|
ids.appendChild(kv("Priorité", priorityTag(message.priority)));
|
|
1011
1045
|
container.appendChild(ids);
|
|
1012
1046
|
|
|
1047
|
+
const thread = node("div", "details-section");
|
|
1048
|
+
thread.appendChild(node("span", "section-label", "Fil"));
|
|
1049
|
+
// Sans nom donné par un participant, le fil n'a que son identifiant et le sujet d'ouverture.
|
|
1050
|
+
thread.appendChild(kv("Nom", node("span", "kv-value", message.thread_title || "fil sans nom")));
|
|
1051
|
+
thread.appendChild(kvCopy("Identifiant", message.thread_id));
|
|
1052
|
+
container.appendChild(thread);
|
|
1053
|
+
|
|
1013
1054
|
container.appendChild(partySection("De", message.from));
|
|
1014
1055
|
container.appendChild(partySection("À", message.to));
|
|
1015
1056
|
|
|
1016
|
-
const deliveries = node("div", "
|
|
1057
|
+
const deliveries = node("div", "details-section");
|
|
1017
1058
|
deliveries.appendChild(node("span", "section-label", "Distribution"));
|
|
1018
1059
|
deliveries.appendChild(deliveriesTable(message.deliveries || []));
|
|
1019
1060
|
container.appendChild(deliveries);
|
|
1020
1061
|
|
|
1021
|
-
el.
|
|
1022
|
-
el.sheet.hidden = false;
|
|
1023
|
-
el.console.classList.add("has-sheet");
|
|
1062
|
+
el.detailsBody.replaceChildren(container);
|
|
1024
1063
|
}
|
|
1025
1064
|
|
|
1026
1065
|
function kv(key, valueNode) {
|
|
@@ -1050,7 +1089,7 @@ function priorityTag(priority) {
|
|
|
1050
1089
|
*/
|
|
1051
1090
|
function partySection(label, agentId) {
|
|
1052
1091
|
const who = party(agentId);
|
|
1053
|
-
const section = node("div", "
|
|
1092
|
+
const section = node("div", "details-section");
|
|
1054
1093
|
section.appendChild(node("span", "section-label", label));
|
|
1055
1094
|
|
|
1056
1095
|
const block = node("div", "party");
|
|
@@ -1092,24 +1131,13 @@ function deliveriesTable(deliveries) {
|
|
|
1092
1131
|
return table;
|
|
1093
1132
|
}
|
|
1094
1133
|
|
|
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
1134
|
// --- Navigation ------------------------------------------------------------
|
|
1104
1135
|
|
|
1105
1136
|
async function openThread(threadId) {
|
|
1106
1137
|
state.threadId = threadId;
|
|
1107
|
-
state.messageId = null;
|
|
1108
1138
|
// Un fil s'ouvre replié, même celui qu'on rouvre : ce qui était déplié l'était pour
|
|
1109
1139
|
// l'autre conversation.
|
|
1110
1140
|
state.openMessages.clear();
|
|
1111
|
-
el.sheet.hidden = true;
|
|
1112
|
-
el.console.classList.remove("has-sheet");
|
|
1113
1141
|
await refreshThread();
|
|
1114
1142
|
await refresh({ keepThread: true });
|
|
1115
1143
|
}
|
|
@@ -1126,16 +1154,6 @@ async function refreshThread() {
|
|
|
1126
1154
|
}
|
|
1127
1155
|
}
|
|
1128
1156
|
|
|
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
1157
|
// --- Boucle de rafraîchissement --------------------------------------------
|
|
1140
1158
|
|
|
1141
1159
|
async function refresh(options) {
|
|
@@ -1173,14 +1191,13 @@ async function refresh(options) {
|
|
|
1173
1191
|
/**
|
|
1174
1192
|
* Ce que chaque séparateur commande.
|
|
1175
1193
|
*
|
|
1176
|
-
* `edge` dit de quel côté se trouve le panneau réglé :
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1194
|
+
* `edge` dit de quel côté se trouve le panneau réglé : ici, chacun borde le panneau qui
|
|
1195
|
+
* le précède. La distinction reste écrite car un panneau ancré à droite se réglerait à
|
|
1196
|
+
* l'envers, en comptant depuis son bord droit.
|
|
1179
1197
|
*/
|
|
1180
1198
|
const PANELS = {
|
|
1181
1199
|
"resize-agents": { name: "--w-agents", panel: "agentsPane", edge: "before", min: 180, max: 520, initial: 240 },
|
|
1182
1200
|
"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
1201
|
};
|
|
1185
1202
|
|
|
1186
1203
|
const LAYOUT_KEY = "messenger.layout";
|
|
@@ -1457,9 +1474,6 @@ async function confirmPurge() {
|
|
|
1457
1474
|
state.selection = { kind: "all", key: null };
|
|
1458
1475
|
}
|
|
1459
1476
|
state.threadId = null;
|
|
1460
|
-
state.messageId = null;
|
|
1461
|
-
el.sheet.hidden = true;
|
|
1462
|
-
el.console.classList.remove("has-sheet");
|
|
1463
1477
|
await refresh();
|
|
1464
1478
|
} catch (error) {
|
|
1465
1479
|
purge.armed = false;
|
|
@@ -1547,11 +1561,9 @@ el.logout.addEventListener("click", async () => {
|
|
|
1547
1561
|
state.expanded = new Set();
|
|
1548
1562
|
state.showEmpty = new Set();
|
|
1549
1563
|
state.threadId = null;
|
|
1550
|
-
state.
|
|
1564
|
+
state.openMessages = new Set();
|
|
1551
1565
|
state.directory = new Map();
|
|
1552
1566
|
state.groups = [];
|
|
1553
|
-
el.sheet.hidden = true;
|
|
1554
|
-
el.console.classList.remove("has-sheet");
|
|
1555
1567
|
el.agents.replaceChildren();
|
|
1556
1568
|
el.threads.replaceChildren();
|
|
1557
1569
|
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.26";
|
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.26";
|