@imenam/mcp-messenger 1.0.19 → 1.0.20
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 +19 -7
- package/dist/src/server/app.js +17 -0
- package/dist/src/server/ui/app.js +133 -22
- package/dist/src/server/ui/styles.css +82 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -401,7 +401,12 @@ correspondants et l'état de lecture :
|
|
|
401
401
|
identiques sont départagées par l'adresse pour que l'ordre ne bouge pas d'un
|
|
402
402
|
rafraîchissement à l'autre ;
|
|
403
403
|
- **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
|
|
404
|
-
nom, ses correspondants, son dernier message et sa priorité la plus haute.
|
|
404
|
+
nom, ses correspondants, son dernier message et sa priorité la plus haute. Les deux
|
|
405
|
+
correspondants y sont d'abord **deux pastilles côte à côte**, séparées et non superposées
|
|
406
|
+
— leurs initiales doivent rester lisibles —, de la couleur que leur adresse leur donne
|
|
407
|
+
partout ailleurs : on voit qui parle avant de lire. Au survol, la
|
|
408
|
+
pastille dit d'où la boîte écrit, ce dont la session s'occupe, son adresse, et si c'est
|
|
409
|
+
elle qui a écrit en dernier. Trois puces
|
|
405
410
|
la filtrent : tous, non lus, priorité haute ;
|
|
406
411
|
- **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
|
|
407
412
|
qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
|
|
@@ -459,7 +464,13 @@ chose :**
|
|
|
459
464
|
touchées depuis. **Une boîte créée après cette date est épargnée, même vide** — une
|
|
460
465
|
session qui vient de démarrer et n'a pas encore parlé n'est pas une boîte à l'abandon.
|
|
461
466
|
|
|
462
|
-
|
|
467
|
+
S'y ajoute la suppression d'**une boîte en particulier** : chaque ligne de la colonne de
|
|
468
|
+
gauche porte une corbeille, révélée au survol ou au clavier. Elle n'efface rien d'elle-même,
|
|
469
|
+
elle ouvre la même fenêtre sur cette seule boîte, avec ce qu'elle contient sous les yeux.
|
|
470
|
+
C'est la seule des trois qui ne demande aucun critère : l'adresse vient d'être montrée du
|
|
471
|
+
doigt, il n'y a rien à revérifier.
|
|
472
|
+
|
|
473
|
+
Toutes se font en deux temps, parce qu'elles ne se rattrapent pas :
|
|
463
474
|
|
|
464
475
|
1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
|
|
465
476
|
de messages et sa dernière activité. Chaque ligne se décoche.
|
|
@@ -545,6 +556,7 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
545
556
|
| `GET` | `/admin/mailboxes/stale` | Boîtes sans activité depuis `before`, avec ce que chacune contient |
|
|
546
557
|
| `GET` | `/admin/mailboxes/empty` | Boîtes par lesquelles rien n'est jamais passé, sans critère de date |
|
|
547
558
|
| `POST` | `/admin/mailboxes/purge` | Supprime les boîtes nommées (`mailboxes`, plus `before` **ou** `empty: true`) — refusé si l'une d'elles ne répond plus au critère |
|
|
559
|
+
| `DELETE` | `/admin/mailboxes/:id` | Supprime une boîte désignée, quel que soit son état — 404 si l'adresse est inconnue |
|
|
548
560
|
| `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
|
|
549
561
|
|
|
550
562
|
---
|
|
@@ -568,8 +580,8 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
568
580
|
rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
|
|
569
581
|
CSP interdit tout script tiers.
|
|
570
582
|
- La console ne peut ni écrire ni modifier un message. Ses seules commandes destructrices
|
|
571
|
-
|
|
572
|
-
|
|
583
|
+
suppriment des boîtes : réservées au jeton d'administration, confirmées deux fois, et
|
|
584
|
+
revérifiées par le serveur quand elles portent sur un lot.
|
|
573
585
|
- Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
|
|
574
586
|
agents via `MESSENGER_SERVER_URL`.
|
|
575
587
|
|
|
@@ -610,9 +622,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
610
622
|
|
|
611
623
|
### Tests
|
|
612
624
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
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
|
|
625
|
+
266 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
|
|
626
|
+
désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
|
|
627
|
+
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
|
|
616
628
|
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
|
|
617
629
|
l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
|
|
618
630
|
ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
|
package/dist/src/server/app.js
CHANGED
|
@@ -264,6 +264,23 @@ export function createApp({ store, config }) {
|
|
|
264
264
|
logger.warn(`admin purge: ${removed.mailboxes} mailbox(es) deleted, ${reason}`);
|
|
265
265
|
res.json({ before: input.before ?? null, empty: input.empty ?? false, ...removed });
|
|
266
266
|
});
|
|
267
|
+
/**
|
|
268
|
+
* Suppression d'une boîte désignée à la main, quel que soit son état.
|
|
269
|
+
*
|
|
270
|
+
* Pas de critère à revérifier ici, et c'est voulu : les deux nettoyages en lot se
|
|
271
|
+
* protègent d'une liste qui aurait vieilli entre l'aperçu et la confirmation, alors que
|
|
272
|
+
* celle-ci porte sur une boîte que l'on vient de montrer du doigt. La boîte doit
|
|
273
|
+
* exister — supprimer une adresse inconnue serait un geste sans effet présenté comme un
|
|
274
|
+
* succès.
|
|
275
|
+
*/
|
|
276
|
+
admin.delete("/mailboxes/:id", (req, res) => {
|
|
277
|
+
const id = normalizeAgentId(String(req.params.id));
|
|
278
|
+
if (!store.getAgent(id))
|
|
279
|
+
throw new MessengerError(`mailbox not found: ${id}`, 404);
|
|
280
|
+
const removed = store.deleteMailboxes([id]);
|
|
281
|
+
logger.warn(`admin delete: mailbox ${id} deleted (${removed.messages} message(s) erased)`);
|
|
282
|
+
res.json({ mailbox: id, ...removed });
|
|
283
|
+
});
|
|
267
284
|
app.use("/admin", admin);
|
|
268
285
|
// Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le
|
|
269
286
|
// contrôle par route (identité de l'agent) vient ensuite.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Console de supervision : les routes /admin du serveur, en lecture — à
|
|
2
|
-
// près, les
|
|
3
|
-
// détruisent. Elles
|
|
4
|
-
//
|
|
1
|
+
// Console de supervision : les routes /admin du serveur, en lecture — à trois exceptions
|
|
2
|
+
// près, les suppressions de boîtes, seules commandes de cette page qui écrivent, et qui
|
|
3
|
+
// détruisent. Elles passent toutes par la même fenêtre : un aperçu de ce qui va partir,
|
|
4
|
+
// puis une confirmation en deux clics.
|
|
5
5
|
//
|
|
6
6
|
// Tout le contenu affiché provient d'agents : il est inséré via textContent uniquement,
|
|
7
7
|
// jamais par innerHTML. Les seuls fragments de balisage construits ici sont les icônes,
|
|
@@ -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
|
+
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"],
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const state = {
|
|
@@ -284,13 +285,22 @@ async function api(path, params) {
|
|
|
284
285
|
return response.json();
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
/** Envoi d'une action à la console.
|
|
288
|
+
/** Envoi d'une action à la console. Les seules qui existent suppriment : elles passent par ici. */
|
|
288
289
|
async function post(path, body) {
|
|
290
|
+
return action("POST", path, body);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Suppression d'une ressource nommée par son adresse : pas de corps, l'URL suffit. */
|
|
294
|
+
async function remove(path) {
|
|
295
|
+
return action("DELETE", path, null);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function action(method, path, body) {
|
|
289
299
|
const response = await fetch(path, {
|
|
290
|
-
method
|
|
300
|
+
method,
|
|
291
301
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
292
302
|
credentials: "same-origin",
|
|
293
|
-
body: JSON.stringify(body),
|
|
303
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
294
304
|
});
|
|
295
305
|
if (response.status === 401) {
|
|
296
306
|
setLoggedIn(false);
|
|
@@ -603,10 +613,37 @@ function boxRow(box) {
|
|
|
603
613
|
if (box.unread > 0) row.appendChild(node("span", "count count--accent", String(box.unread)));
|
|
604
614
|
row.title = `${box.id}\n${plural(box.sent, "envoyé")} · ${plural(box.received, "reçu")}`;
|
|
605
615
|
row.addEventListener("click", () => select("box", box.id));
|
|
606
|
-
|
|
616
|
+
|
|
617
|
+
// La ligne et son bouton de suppression sont deux boutons voisins, jamais imbriqués :
|
|
618
|
+
// un bouton dans un bouton n'est pas du HTML, et le clic n'y serait pas dissociable.
|
|
619
|
+
const line = node("div", "box-line");
|
|
620
|
+
line.appendChild(row);
|
|
621
|
+
line.appendChild(deleteBoxButton(box));
|
|
622
|
+
item.appendChild(line);
|
|
607
623
|
return item;
|
|
608
624
|
}
|
|
609
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Suppression d'une boîte, à la main.
|
|
628
|
+
*
|
|
629
|
+
* Le bouton n'efface rien : il ouvre la fenêtre de nettoyage sur cette seule boîte, avec
|
|
630
|
+
* ce qu'elle contient sous les yeux et la même double confirmation que les nettoyages en
|
|
631
|
+
* lot. Un geste destructeur à un clic d'une liste ne peut pas être plus court que ça.
|
|
632
|
+
*/
|
|
633
|
+
function deleteBoxButton(box) {
|
|
634
|
+
const label = `Supprimer la boîte ${boxLabel(box)}`;
|
|
635
|
+
const remove = node("button", "box-delete");
|
|
636
|
+
remove.type = "button";
|
|
637
|
+
remove.title = label;
|
|
638
|
+
remove.setAttribute("aria-label", label);
|
|
639
|
+
remove.appendChild(icon("trash"));
|
|
640
|
+
remove.addEventListener("click", (event) => {
|
|
641
|
+
event.stopPropagation();
|
|
642
|
+
openPurgeFor(box);
|
|
643
|
+
});
|
|
644
|
+
return remove;
|
|
645
|
+
}
|
|
646
|
+
|
|
610
647
|
/** Nombre de fils affiché en pied de colonne, conservé entre deux rendus locaux. */
|
|
611
648
|
function currentThreadCount() {
|
|
612
649
|
return state.threadCount || 0;
|
|
@@ -672,7 +709,7 @@ function threadRow(thread) {
|
|
|
672
709
|
top.appendChild(node("span", "thread-time", formatShort(thread.last_message_at)));
|
|
673
710
|
row.appendChild(top);
|
|
674
711
|
|
|
675
|
-
row.appendChild(
|
|
712
|
+
row.appendChild(threadParticipants(thread));
|
|
676
713
|
|
|
677
714
|
const bottom = node("div", "thread-bottom");
|
|
678
715
|
bottom.appendChild(node("span", "thread-preview", thread.last_message_preview));
|
|
@@ -686,6 +723,38 @@ function threadRow(thread) {
|
|
|
686
723
|
return item;
|
|
687
724
|
}
|
|
688
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Qui parle dans ce fil : les pastilles d'abord, les noms ensuite.
|
|
728
|
+
*
|
|
729
|
+
* Un fil lie exactement deux boîtes, la pastille suffit donc à les distinguer d'un coup
|
|
730
|
+
* d'œil, sans compter ni déborder. Chaque pastille garde la couleur de son adresse d'un
|
|
731
|
+
* écran à l'autre, et dit au survol tout ce qu'on peut vouloir savoir de la boîte : le
|
|
732
|
+
* projet d'où elle écrit, ce dont cette session s'occupe, son adresse, et si c'est elle
|
|
733
|
+
* qui a écrit en dernier.
|
|
734
|
+
*/
|
|
735
|
+
function threadParticipants(thread) {
|
|
736
|
+
const who = node("div", "thread-who");
|
|
737
|
+
|
|
738
|
+
const stack = node("div", "avatar-stack");
|
|
739
|
+
for (const id of thread.participants) {
|
|
740
|
+
const person = party(id);
|
|
741
|
+
const badge = avatar(id, "avatar--sm avatar--stack");
|
|
742
|
+
badge.title = [
|
|
743
|
+
person.agentName || "agent sans nom",
|
|
744
|
+
person.box || (person.registered ? "boîte sans nom" : "boîte jamais connectée"),
|
|
745
|
+
id,
|
|
746
|
+
id === thread.last_message_from ? "a écrit le dernier message" : null,
|
|
747
|
+
]
|
|
748
|
+
.filter(Boolean)
|
|
749
|
+
.join("\n");
|
|
750
|
+
stack.appendChild(badge);
|
|
751
|
+
}
|
|
752
|
+
who.appendChild(stack);
|
|
753
|
+
|
|
754
|
+
who.appendChild(node("span", "thread-names", thread.participants.map((id) => party(id).name).join(" · ")));
|
|
755
|
+
return who;
|
|
756
|
+
}
|
|
757
|
+
|
|
689
758
|
// --- Lecteur du fil --------------------------------------------------------
|
|
690
759
|
|
|
691
760
|
function renderEmptyReader() {
|
|
@@ -1100,6 +1169,29 @@ function openPurge() {
|
|
|
1100
1169
|
el.purge.showModal();
|
|
1101
1170
|
}
|
|
1102
1171
|
|
|
1172
|
+
/**
|
|
1173
|
+
* Ouvre la fenêtre sur une seule boîte, celle qu'on vient de désigner dans la colonne.
|
|
1174
|
+
*
|
|
1175
|
+
* Rien à demander au serveur : l'annuaire vient d'être lu, et ce qu'il en dit est ce que
|
|
1176
|
+
* la ligne cliquée affiche. La liste est donc dressée sur place, avec la même mise en
|
|
1177
|
+
* forme et la même confirmation que les nettoyages en lot.
|
|
1178
|
+
*/
|
|
1179
|
+
function openPurgeFor(box) {
|
|
1180
|
+
openPurge();
|
|
1181
|
+
purge.mode = "one";
|
|
1182
|
+
purge.mailboxes = [
|
|
1183
|
+
{
|
|
1184
|
+
id: box.id,
|
|
1185
|
+
agent_name: box.agent_name,
|
|
1186
|
+
display_name: box.display_name,
|
|
1187
|
+
last_activity_at: box.last_activity_at,
|
|
1188
|
+
messages: box.sent + box.received,
|
|
1189
|
+
},
|
|
1190
|
+
];
|
|
1191
|
+
purge.selected = new Set([box.id]);
|
|
1192
|
+
renderPurgePreview();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1103
1195
|
/** Toute modification remet la confirmation à zéro : on ne supprime que ce qu'on vient de voir. */
|
|
1104
1196
|
function resetPurge() {
|
|
1105
1197
|
purge.mode = null;
|
|
@@ -1141,19 +1233,28 @@ async function previewPurge(mode, load, before = null) {
|
|
|
1141
1233
|
function renderPurgePreview() {
|
|
1142
1234
|
const container = document.createDocumentFragment();
|
|
1143
1235
|
const count = purge.mailboxes.length;
|
|
1144
|
-
// Le critère est rappelé sous les yeux de qui va confirmer : les
|
|
1145
|
-
// remplissent la même liste, et
|
|
1146
|
-
const since =
|
|
1147
|
-
const
|
|
1148
|
-
purge.mode === "empty" ? plural(count, "boîte vide", "boîtes vides") : `${plural(count, "boîte")}${since}`;
|
|
1236
|
+
// Le critère est rappelé sous les yeux de qui va confirmer : les trois suppressions
|
|
1237
|
+
// remplissent la même liste, et elles ne portent pas sur les mêmes boîtes.
|
|
1238
|
+
const since = ` sans activité depuis le ${formatDay(purge.before)}`;
|
|
1239
|
+
const criterion = purge.mode === "empty" ? "vide" : purge.mode === "one" ? "" : since;
|
|
1149
1240
|
|
|
1150
1241
|
if (count === 0) {
|
|
1151
|
-
container.appendChild(node("p", "purge-empty", `Aucune boîte ${
|
|
1242
|
+
container.appendChild(node("p", "purge-empty", `Aucune boîte ${criterion.trim()}.`));
|
|
1152
1243
|
el.purgeResult.replaceChildren(container);
|
|
1153
1244
|
return;
|
|
1154
1245
|
}
|
|
1155
1246
|
|
|
1156
|
-
container.appendChild(
|
|
1247
|
+
container.appendChild(
|
|
1248
|
+
node(
|
|
1249
|
+
"p",
|
|
1250
|
+
"purge-count",
|
|
1251
|
+
purge.mode === "one"
|
|
1252
|
+
? "Cette boîte, et tout ce que personne d'autre ne pourrait plus lire :"
|
|
1253
|
+
: purge.mode === "empty"
|
|
1254
|
+
? `${plural(count, "boîte vide", "boîtes vides")}.`
|
|
1255
|
+
: `${plural(count, "boîte")}${since}.`
|
|
1256
|
+
)
|
|
1257
|
+
);
|
|
1157
1258
|
|
|
1158
1259
|
const list = node("ul", "purge-list");
|
|
1159
1260
|
for (const mailbox of purge.mailboxes) {
|
|
@@ -1207,12 +1308,18 @@ async function confirmPurge() {
|
|
|
1207
1308
|
}
|
|
1208
1309
|
|
|
1209
1310
|
el.purgeConfirm.disabled = true;
|
|
1311
|
+
const deleted = [...purge.selected];
|
|
1210
1312
|
try {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1313
|
+
// Une boîte désignée à la main s'efface par son adresse : il n'y a pas de critère à
|
|
1314
|
+
// revérifier, l'utilisateur vient de la montrer du doigt.
|
|
1315
|
+
const removed =
|
|
1316
|
+
purge.mode === "one"
|
|
1317
|
+
? await remove(`/admin/mailboxes/${encodeURIComponent(deleted[0])}`)
|
|
1318
|
+
: await post("/admin/mailboxes/purge", {
|
|
1319
|
+
// Le critère repart avec les adresses : c'est lui que le serveur revérifie.
|
|
1320
|
+
...(purge.mode === "empty" ? { empty: true } : { before: purge.before }),
|
|
1321
|
+
mailboxes: deleted,
|
|
1322
|
+
});
|
|
1216
1323
|
resetPurge();
|
|
1217
1324
|
el.purgeResult.replaceChildren(
|
|
1218
1325
|
node(
|
|
@@ -1221,7 +1328,11 @@ async function confirmPurge() {
|
|
|
1221
1328
|
`${plural(removed.mailboxes, "boîte")} supprimée(s), ${plural(removed.messages, "message")} et ${plural(removed.threads, "fil")} avec.`
|
|
1222
1329
|
)
|
|
1223
1330
|
);
|
|
1224
|
-
// La console montre encore ce qui n'existe plus : elle se relit entièrement
|
|
1331
|
+
// La console montre encore ce qui n'existe plus : elle se relit entièrement, et son
|
|
1332
|
+
// filtre est relâché s'il portait sur une boîte qui vient de disparaître.
|
|
1333
|
+
if (state.selection.kind === "box" && deleted.includes(state.selection.key)) {
|
|
1334
|
+
state.selection = { kind: "all", key: null };
|
|
1335
|
+
}
|
|
1225
1336
|
state.threadId = null;
|
|
1226
1337
|
state.messageId = null;
|
|
1227
1338
|
el.sheet.hidden = true;
|
|
@@ -769,6 +769,54 @@ body {
|
|
|
769
769
|
padding: 7px 10px;
|
|
770
770
|
}
|
|
771
771
|
|
|
772
|
+
/* La ligne d'une boîte et son bouton de suppression, côte à côte. */
|
|
773
|
+
.box-line {
|
|
774
|
+
display: flex;
|
|
775
|
+
align-items: center;
|
|
776
|
+
gap: 2px;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.box-line .box {
|
|
780
|
+
flex: 1;
|
|
781
|
+
min-width: 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/*
|
|
785
|
+
Le bouton ne se montre qu'au survol ou au clavier : une corbeille par ligne, affichée en
|
|
786
|
+
permanence, transformerait la colonne en champ de mines. Il reste dans le flux — révélé
|
|
787
|
+
par l'opacité et non par `display` — pour que la ligne ne change pas de largeur.
|
|
788
|
+
*/
|
|
789
|
+
.box-delete {
|
|
790
|
+
display: flex;
|
|
791
|
+
padding: 5px;
|
|
792
|
+
border: 0;
|
|
793
|
+
border-radius: var(--radius);
|
|
794
|
+
background: none;
|
|
795
|
+
color: var(--muted);
|
|
796
|
+
cursor: pointer;
|
|
797
|
+
opacity: 0;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.box-line:hover .box-delete,
|
|
801
|
+
.box-delete:focus-visible {
|
|
802
|
+
opacity: 1;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.box-delete:hover {
|
|
806
|
+
background: var(--danger-soft);
|
|
807
|
+
color: var(--danger);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.box-delete:focus-visible {
|
|
811
|
+
outline: 2px solid var(--accent);
|
|
812
|
+
outline-offset: -2px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* Colonne repliée : il n'y a plus de boîtes affichées, donc rien à supprimer. */
|
|
816
|
+
.is-collapsed .box-delete {
|
|
817
|
+
display: none;
|
|
818
|
+
}
|
|
819
|
+
|
|
772
820
|
.box .agent-name {
|
|
773
821
|
font-weight: 500;
|
|
774
822
|
font-size: 13px;
|
|
@@ -1100,15 +1148,48 @@ body {
|
|
|
1100
1148
|
color: var(--accent);
|
|
1101
1149
|
}
|
|
1102
1150
|
|
|
1151
|
+
/* Les pastilles des deux correspondants, puis leurs noms : on voit qui parle avant de lire. */
|
|
1103
1152
|
.thread-who {
|
|
1153
|
+
display: flex;
|
|
1154
|
+
align-items: center;
|
|
1155
|
+
gap: 8px;
|
|
1156
|
+
margin-top: 4px;
|
|
1157
|
+
min-width: 0;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.thread-names {
|
|
1104
1161
|
font-size: 12px;
|
|
1105
1162
|
color: var(--muted);
|
|
1106
|
-
margin-top: 2px;
|
|
1107
1163
|
white-space: nowrap;
|
|
1108
1164
|
overflow: hidden;
|
|
1109
1165
|
text-overflow: ellipsis;
|
|
1110
1166
|
}
|
|
1111
1167
|
|
|
1168
|
+
/* Dans une liste, la pastille sert de repère, pas de portrait : elle est plus petite. */
|
|
1169
|
+
.thread-who .avatar--sm {
|
|
1170
|
+
width: 22px;
|
|
1171
|
+
height: 22px;
|
|
1172
|
+
font-size: 10px;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/*
|
|
1176
|
+
Ici les pastilles ne se chevauchent pas. Le chevauchement du lecteur dit « ces gens vont
|
|
1177
|
+
ensemble » quand la place manque ; dans une liste qui se survole, il mange les initiales,
|
|
1178
|
+
et ce sont justement elles qui identifient la boîte. L'anneau qui détachait les pastilles
|
|
1179
|
+
superposées n'a alors plus rien à détacher.
|
|
1180
|
+
*/
|
|
1181
|
+
.thread-who .avatar-stack {
|
|
1182
|
+
gap: 4px;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.thread-who .avatar-stack .avatar + .avatar {
|
|
1186
|
+
margin-left: 0;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.thread-who .avatar--stack {
|
|
1190
|
+
box-shadow: none;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1112
1193
|
.thread-bottom {
|
|
1113
1194
|
display: flex;
|
|
1114
1195
|
align-items: center;
|
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.20";
|
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.20";
|