PageSourceSearch

https://creaconnect.world/src/crea-mediatheque.js

js creaconnect.world collected 2026-09-24 10:08:37 UTC 56,827 bytes, 975 lines download raw bytes

1/* ═══════════════════════════════════════════════════════════════════════════
2 * LA MÉDIATHÈQUE — LE CATALOGUE DE CRÉA
3 * SEIZIÈME MORCEAU DÉTACHÉ DE src/app.js
4 * ═══════════════════════════════════════════════════════════════════════════
5 * CREA MEDIATHEQUE — le catalogue : ce qu'on a, ce qu'on a fait, ce qu'on a lu
6 *
7 * « J'ai l'impression qu'on l'enferme pour contenir des images et des fichiers.
8 *  Dis-moi tout ce qu'on peut en faire, pour la rendre riche, utile à tout
9 *  l'écosystème, vraiment puissante et ouverte en vrai. »
10 *
11 * ── LE RENVERSEMENT ────────────────────────────────────────────────────────
12 *
13 * Une bibliothèque municipale ne possède pas 3 % de ce à quoi elle donne
14 * accès. Ce qui la définit n'est pas son stock, c'est son ACCÈS : le
15 * catalogue, les bases auxquelles elle vous abonne, et le prêt entre
16 * bibliothèques quand elle n'a rien.
17 *
18 * La Médiathèque ne possède donc pas. Elle sait OÙ SONT les choses. Un fichier
19 * reste sur le disque, un livrable dans son projet, un document partagé chez
20 * son auteur. Ce qu'elle tient, c'est le lien — et c'est ce qui permet de
21 * chercher une seule fois dans seize matières au lieu de quatre fois dans
22 * quatre écrans.
23 *
24 * ── CE QUI ÉTAIT LÀ, ET POURQUOI C'ÉTAIT UN PLACARD ───────────────────────
25 *
26 * L'écran d'avant montrait DEUX choses : une grille plate de livrables, et un
27 * onglet de sessions. Relevé dans l'état de l'application : VINGT-SIX réserves
28 * de matière existent, sont sauvegardées, voyagent d'un appareil à l'autre —
29 * et vingt-quatre n'avaient aucun écran. Ce que CRÉA a lu pour vous
30 * (`filLus`, `filGardes`), ce qu'elle croit savoir (`memories`,
31 * `knowledgeGraph`), ce qu'elle a ouvert (`browserHistory`) : rien de tout ça
32 * n'était regardable, et rien n'était corrigible.
33 *
34 * ── DEUX SYSTÈMES, PAS UN ─────────────────────────────────────────────────
35 *
36 * « Même si tu les as liés, c'est deux systèmes différents. Ils se parlent,
37 *  ils travaillent ensemble, mais c'est deux systèmes différents. »
38 *
39 * La Médiathèque CATALOGUE. L'explorateur PARCOURT UN DISQUE. Ce sont DEUX
40 * BRANCHES VOISINES, pas l'une dans l'autre — deux entrées de la barre, deux
41 * fenêtres, deux réalités. L'explorateur a vécu une journée dans une SECTION
42 * de ce module, et c'était une faute de fond : enfermer un explorateur dans un
43 * bloc du catalogue lui retire précisément ce qui en fait un autre système.
44 *
45 * Ce module ne dessine donc aucun explorateur, et n'en ouvre plus aucun. Il
46 * lui PARLE : ce qui vient du disque arrive par `verser()`, et ce que le
47 * catalogue sait d'un fichier du disque repart par la colonne « Dans CRÉA ».
48 *
49 * ── CE QUE CE MODULE NE TIENT PAS, ET C'EST ÉCRIT À L'ÉCRAN ───────────────
50 *
51 * LE PARTAGE N'EXISTE PAS. Prêter une pièce à quelqu'un bute sur le même mur
52 * que le poste partagé : /api/sync/push range un instantané ENTIER par
53 * personne. À deux sur une même étagère, le dernier à écrire efface l'autre.
54 * Le verbe est nommé et ÉTEINT — il refuse même si on force le clic.
55 *
56 * LES SOURCES NE SONT PAS ENCORE GARDÉES. Mesuré : zéro. CRÉA cite et jette.
57 * La section existe et le dit, parce qu'un trou nommé se répare et qu'un trou
58 * caché s'oublie.
59 *
60 * LE MONDE EXTÉRIEUR EST EN PARTIE BRANCHÉ. Sept catalogues sont déjà atteints
61 * par le serveur (mesuré) ; les autres sont nommés et éteints.
62 *
63 * Emprunts : AUCUN nom lu du tronc autrement que par `window`.
64 *
65 * Expose window.CREA_MEDIATHEQUE.
66 * ═══════════════════════════════════════════════════════════════════════════ */
67;(function () {
68  'use strict'
69  if (window.CREA_MEDIATHEQUE) return
70
71  function S() { return window.STATE || (window.STATE = {}) }
72  function esc(s) {
73    if (typeof window.escHtml === 'function') return window.escHtml(s)
74    return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
75      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
76    })
77  }
78  function redessiner() { if (typeof window.render === 'function') window.render() }
79  function garder() { if (typeof window.save === 'function') window.save() }
80  function dire(t, g, s) { if (typeof window.logNotice === 'function') window.logNotice(t, g || 'info', s || 'ℹ️') }
81  function registre() { return window.fichiersRegistre || null }
82  function pieces() {
83    var r = registre()
84    if (!r || typeof r.lister !== 'function') return []
85    try { return r.lister() || [] } catch (e) {
86      /* Le registre se reconstruit au démarrage ; l'interroger trop tôt jette.
87       * Rendre une étagère vide est juste — elle se remplira au rendu suivant. */
88      return []
89    }
90  }
91  /* CE QUE LE CATALOGUE A REÇU DU DISQUE. C'est la trace de l'autre branche
92   * ICI, et elle se relit à chaque rendu : le catalogue ne tient pas la liste
93   * des dossiers accordés — il constate ce qui lui est arrivé. */
94  function versesDuDisque() {
95    return pieces().filter(function (f) {
96      return !f.supprime && f.origine && f.origine.studio === 'explorateur'
97    })
98  }
99  function jours(t) { return Math.floor((Date.now() - t) / 86400000) }
100  function ilYA(t) {
101    if (!t) return ''
102    var j = jours(t)
103    if (j <= 0) return "aujourd'hui"
104    if (j === 1) return 'hier'
105    if (j < 31) return 'il y a ' + j + ' j'
106    if (j < 365) return 'il y a ' + Math.round(j / 30) + ' mois'
107    return 'il y a ' + Math.round(j / 365) + ' an' + (j >= 730 ? 's' : '')
108  }
109  function poids(o) {
110    if (!o) return ''
111    if (o < 1024) return o + ' o'
112    if (o < 1048576) return Math.round(o / 1024) + ' Ko'
113    return (o / 1048576).toFixed(1).replace('.', ',') + ' Mo'
114  }
115
116  var IC = {
117    livre: '<path d="M4.5 4.8h4.8a2.5 2.5 0 0 1 2.7 2.5v12a2 2 0 0 0-2-2H4.5Z"/><path d="M19.5 4.8h-4.8a2.5 2.5 0 0 0-2.7 2.5v12a2 2 0 0 1 2-2h5.5Z"/>',
118    ordi: '<rect x="3" y="5" width="18" height="12" rx="2"/><path d="M8 20h8M12 17v3"/>',
119    image: '<rect x="3.5" y="5" width="17" height="14" rx="2"/><circle cx="9" cy="10" r="1.6"/><path d="m5 17 5-4.5 4 3.5 2.5-2 3 3"/>',
120    son: '<path d="M5 9.5h3l4-3.5v12l-4-3.5H5Z"/><path d="M16 9a4 4 0 0 1 0 6"/>',
121    video: '<rect x="3" y="6" width="13" height="12" rx="2"/><path d="m16 11 5-3v8l-5-3Z"/>',
122    doc: '<path d="M5 4.5h9l5 5V19.5H5Z"/><path d="M8.5 13h7M8.5 16h4.5"/>',
123    etincelle: '<path d="M12 3v3M12 18v3M3 12h3M18 12h3"/><rect x="7" y="7" width="10" height="10" rx="3"/>',
124    bulle: '<path d="M4 5.5h16v10H9l-4 3.5Z"/>',
125    horloge: '<circle cx="12" cy="12" r="8.2"/><path d="M12 7.4V12l3 1.8"/>',
126    gabarit: '<rect x="4" y="4" width="7" height="7" rx="1.6"/><rect x="13" y="13" width="7" height="7" rx="1.6"/><path d="M11 7.5h6.5V13"/>',
127    kiosque: '<path d="M4 6h11v13H4Z"/><path d="M15 9h5v10h-5"/><path d="M7 9.5h5M7 13h5"/>',
128    source: '<path d="M9 5.5h10v13H9Z"/><path d="M5 8v11h10"/>',
129    savoir: '<circle cx="12" cy="12" r="3"/><circle cx="12" cy="4.5" r="2"/><circle cx="19" cy="16" r="2"/><circle cx="5" cy="16" r="2"/><path d="M12 6.5v2.5M14.4 13.5l2.8 1.3M9.6 13.5l-2.8 1.3"/>',
130    lecture: '<path d="M5 4.5h9l5 5V19.5H5Z"/><path d="M8.5 13h7"/>',
131    monde: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.5 2.6 2.5 14.4 0 17M12 3.5c-2.5 2.6-2.5 14.4 0 17"/>',
132    relais: '<path d="M4 12h11"/><path d="m11 8 4 4-4 4"/><circle cx="19" cy="12" r="2.2"/>',
133    gens: '<circle cx="12" cy="6" r="2.4"/><circle cx="5.5" cy="17" r="2.4"/><circle cx="18.5" cy="17" r="2.4"/><path d="M10.4 7.8 7 14.6M13.6 7.8 17 14.6M8 17h8"/>',
134    corbeille: '<path d="M5 7h14M9.5 7V5h5v2M7 7v13h10V7"/>',
135    loupe: '<circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/>',
136    plus: '<path d="M12 5v14M5 12h14"/>',
137    chevron: '<path d="M6 9l6 6 6-6"/>',
138    dossier: '<path d="M3.5 6.5h6l1.6 2h9.4v9H3.5Z"/>',
139  }
140  function ic(n, t, w) {
141    return '<svg viewBox="0 0 24 24" width="' + (t || 14) + '" height="' + (t || 14) + '" fill="none" stroke="currentColor" ' +
142      'stroke-width="' + (w || 1.7) + '" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (IC[n] || IC.livre) + '</svg>'
143  }
144
145  /* ── LES SECTIONS, EN QUATRE FAMILLES ───────────────────────────────────
146   * Elles se comptent, elles ne s'annoncent pas : la barre d'état écrivait
147   * « 16 sections » quand le rail en dessinait dix-sept. Un nombre écrit à la
148   * main ment dès qu'on ajoute une ligne, et personne ne le relit.
149   * L'ordre n'est pas décoratif : il va de ce qui est le plus à vous vers ce
150   * qui l'est le moins. `porte: true` marque une section qui appartient à un
151   * AUTRE système et que la Médiathèque ne fait qu'ouvrir. */
152  var FAMILLES = [
153    { id: 'avoir', nom: 'Ce que vous avez' },
154    { id: 'crea', nom: 'Ce que CRÉA a fait' },
155    { id: 'lu', nom: "Ce qu'elle a lu" },
156    { id: 'dehors', nom: 'Dehors' },
157  ]
158  /* « Il ne faut pas mettre Fichiers, il faut mettre Explorateur pour faire la
159   * distinction, parce que dans Catalogue il y a déjà une forme de fic
159hiers. »
160   * Le catalogue range des PIÈCES — images, sons, documents. L'autre branche
161   * parcourt un DISQUE. Deux mots pour deux choses, sinon on cherche ses
162   * fichiers dans celui qui n'en a pas. */
163  var BRANCHES = ['catalogue', 'explorateur']
164  var SECTIONS = [
165    { id: 'images', nom: 'Images', ic: 'image', fam: 'avoir', types: ['image'] },
166    { id: 'sons', nom: 'Sons & voix', ic: 'son', fam: 'avoir', types: ['audio', 'son'] },
167    { id: 'videos', nom: 'Vidéos', ic: 'video', fam: 'avoir', types: ['video'] },
168    { id: 'documents', nom: 'Documents', ic: 'doc', fam: 'avoir', types: ['document', 'doc', 'pdf', 'texte'] },
169    { id: 'creations', nom: 'Créations', ic: 'etincelle', fam: 'crea' },
170    { id: 'conversations', nom: 'Conversations', ic: 'bulle', fam: 'crea' },
171    { id: 'fabrication', nom: 'En fabrication', ic: 'horloge', fam: 'crea' },
172    { id: 'modeles', nom: 'Modèles', ic: 'gabarit', fam: 'crea' },
173    { id: 'kiosque', nom: 'Kiosque', ic: 'kiosque', fam: 'lu' },
174    { id: 'sources', nom: 'Sources', ic: 'source', fam: 'lu' },
175    { id: 'savoir', nom: 'Savoir', ic: 'savoir', fam: 'lu' },
176    { id: 'lecture', nom: 'Salle de lecture', ic: 'lecture', fam: 'lu' },
177    { id: 'monde', nom: 'Le monde', ic: 'monde', fam: 'dehors' },
178    { id: 'emprunts', nom: 'Emprunts', ic: 'relais', fam: 'dehors' },
179    { id: 'partage', nom: 'Partagé', ic: 'gens', fam: 'dehors' },
180    { id: 'desherbage', nom: 'Désherbage', ic: 'corbeille', fam: 'dehors' },
181  ]
182
183  function etat() {
184    var st = S()
185    if (!st.mediatheque || typeof st.mediatheque !== 'object') st.mediatheque = {}
186    var m = st.mediatheque
187    /* DEUX BRANCHES, PUIS LES SECTIONS DE LA PREMIÈRE.
188     *
189     * « Je t'ai dit un catalogue, et à côté du catalogue une branche Fichiers
190     *  qui a sa propre vue. » Ce sont les deux entrées du fil, en haut :
191     * Catalogue et Fichiers. Les seize sections sont le rail DU CATALOGUE —
192     * Fichiers n'en est pas une, et n'en a pas.
193     *
194     * L'explorateur a vécu deux fausses vies avant celle-ci : dix-huitième
195     * section du rail, puis branche de la barre latérale, hors de la
196     * Médiathèque. Ni l'un ni l'autre. */
197    if (!m.section || (BRANCHES.indexOf(m.section) < 0 && !SECTIONS.some(function (x) { return x.id === m.section }))) m.section = 'catalogue'
198    if (typeof m.recherche !== 'string') m.recherche = ''
199    if (!m.tri) m.tri = 'recent'
200    return m
201  }
202  function section() { return etat().section }
203
204  /* UNE SEULE FAÇON D'OUVRIR UNE SECTION. Le rail, un fonds qui renvoie vers
205   * ses créations, un raccourci venu d'un autre espace : tous passent ici.
206   * Deux chemins, et l'un des deux oublierait de vider la recherche — on
207   * chercherait encore dans une section qu'on ne regarde plus. */
208  function ouvrirSection(id, recherche) {
209    var m = etat()
210    m.section = id
211    /* La recherche se vide — SAUF quand l'appelant mène à une trouvaille
212     * précise. Sans cette exception, la recherche de la page ouvrirait la
213     * bonne section et laisserait rechercher une SECONDE fois ce qu'on vient
214     * de trouver. C'est toujours un seul chemin : l'exception passe par ici. */
215    m.recherche = typeof recherche === 'string' ? recherche : ''
216    garder()
217    redessiner()
218  }
219
220  /* MENER À UN FICHIER PRÉCIS, depuis ailleurs.
221   *
222   * LE DÉGÂT SANS ELLE : la recherche de la page trouve désormais dans le
223   * CONTENU des fichiers du registre — c'est ce que CRÉA sait faire depuis
224   * `fichier_chercher` et que la personne ne pouvait pas. Trouver sans pouvoir
225   * ouvrir n'est pas trouver : il fallait une porte qui pose la vue sur LE
226   * fichier, pas sur la médiathèque en général.
227   *
228   * Elle rend `false` quand l'entrée n'est pas là — le registre est cloisonné
229   * par projet, et un identifiant d'un autre projet ne se force pas d'ici.
230   * L'appelant sait alors qu'il n'a rien ouvert, au lieu de croire que si. */
231  function montrer(fichierId) {
232    var id = String(fichierId || '')
233    if (!id) return false
234    var f = pieces().find(function (x) { return x && x.id === id && !x.supprime })
235    if (!f) return false
236    ouvrirSection(sectionDuType(f.type), String(f.nom || ''))
237    return true
238  }
239
240  /* ── LA MATIÈRE, RELUE ET JAMAIS RECOPIÉE ───────────────────────────────
241   * Chaque compteur du rail interroge sa source à l'instant du rendu. Rien
242   * n'est mis en cache : un chiffre gardé devient faux dès qu'on produit
243   * ailleurs, et un rail qui ment sur ses comptes est pire qu'un rail sans
244   * compte du tout. */
245  function parType(ids) {
246    return pieces().filter(function (f) {
247      return !f.supprime && ids.indexOf(String(f.type || '').toLowerCase()) >= 0
248    })
249  }
250  /* LES SESSIONS DES STUDIOS SONT DES SESSIONS AUSSI. Cette vue ne listait que
251   * les fils de chat : depuis qu'un fichier peut venir d'un studio, son
252   * origine n'apparaissait NULLE PART, et « N pièces » tombait à zéro pour
253   * tout ce qu'un studio avait fabriqué. Le tronc le dit déjà de son côté —
254   * « un seul système de sessions dans toute l'application, pas un par
255   * endroit » ; la Médiathèque le lit enfin comme tel. */
256  function conversations() {
257    var st = S()
258    var out = (st.conversations || [])
259      .filter(function (c) { return c && !c.archived && !c.ephemeral })
260      .map(function (c) {
261        return { id: c.id, title: c.title || 'Session', messages: c.messages || [],
262                 updatedAt: c.updatedAt, createdAt: c.createdAt, studio: '' }
263      })
264    var parStudio = st.studioSessions || {}
265    for (var mode in parStudio) {
266      if (!Object.prototype.hasOwnProperty.call(parStudio, mode)) continue
267      var liste = parStudio[mode]
268      if (!Array.isArray(liste)) continue
269      for (var i = 0; i < liste.length; i++) {
270        var s = liste[i]
271        if (!s || !s.id || s.archived) continue
272        out.push({ id: s.id, title: s.title || 'Session', messages: s.chat || [],
273                   updatedAt: s.updatedAt, createdAt: s.createdAt, studio: mode })
274      }
275    }
276    return out.sort(function (a, b) { return (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0) })
277  }
278  /* Le nom d'un studio se demande à SA table, jamais à une liste écrite ici :
279   * un studio ajouté demain doit se nommer sans qu'on touche à ce fichier. */
280  function nomStudio(id) {
281    try {
282      var reg = (typeof STUDIOS_CREA !== 'undefined' ? STUDIOS_CREA : [])
283      var s = reg.find(function (x) { return x.id === id })
284      if (s) return s.n
285    } catch (e) { /* table illisible : le nom brut du mode vaut mieux que rien */ }
286    return id ? id.charAt(0).toUpperCase() + id.slice(1) : ''
287  }
288  function gardes() { return S().filGardes || [] }
289  function lus() { return S().filLus || [] }
290  function memoires() {
291    var st = S()
292    var m = Array.isArray(st.memories) ? st.memories : []
293    var g = st.knowledgeGraph && Array.isArray(st.knowledgeGraph.nodes) ? st.knowledgeGraph.nodes : []
294    return { memoires: m, noeuds: g }
295  }
296  function enFabrication() {
297    return (S().bgTasks || []).filter(function (t) { return t.status === 'running' || t.status === 'queued' })
298  }
299  function modeles() { return (window.FOLDER_TEMPLATES || []).slice() }
300  function realisations() {
301    var r = registre()
302    if (!r || typeof r.realisations !== 'function') return []
303    try { return r.realisations() || [] } catch (e) {
304      /* Même raison que `pieces()` : trop tôt au démarrage. */
305      return []
306    }
307  }
308  function corbeille() {
309    var r = registre()
310    if (!r || typeof r.supprimes !== 'function') return []
311    try { return r.supprimes() || [] } catch (e) {
312      /* Même raison que `pieces()`. */
313      return []
314    }
315  }
316  /* LE DÉSHERBAGE SE MESURE, IL NE SE DEVINE PAS. Deux pièces sont un doublon
317   * quand elles portent le même nom ET le même poids : le nom seul accuse deux
318   * « rapport.pdf » qui n'ont rien à voir, et le poids seul rapproche deux
319   * images de 240 Ko sans rapport. */
320  function doublons() {
321    var vus = {}, sortie = []
322    pieces().forEach(function (f) {
323      if (f.supprime) return
324      var cle = String(f.nom || '').toLowerCase() + '|' + (f.octets || 0)
325      if (vus[cle]) { vus[cle].push(f); return }
326      vus[cle] = [f]
327    })
328    Object.keys(vus).forEach(function (k) { if (vus[k].length > 1) sortie.push(vus[k]) })
329    return sortie
330  }
331  function jamaisRouvertes() {
332    return pieces().filter(function (f) {
333      return !f.supprime && f.majAt && jours(f.majAt) > 180
334    })
335  }
336
337  function compte(id) {
338    var s = SECTIONS.find(function (x) { return x.id === id })
339    if (s && s.types) return parType(s.types).length
340    switch (id) {
341      case 'creations': return pieces().filter(function (f) { return !f.supprime }).length
342      case 'conversations': return conversations().length
343      case 'fabrication': return enFabrication().length
344      case 'modeles': return modeles().length
345      case 'kiosque': return gardes().length + lus().length
346      case 'sources': return 0
347      case 'savoir': return memoires().memoires.length + memoires().noeuds.length
348      case 'lecture': return 0
349      case 'monde': return CATALOGUES.filter(function (c) { return c.branche }).length
350      case 'emprunts': return 0
351      case 'partage': return 0
352      case 'desherbage': return doublons().length + jamaisRouvertes().length
353    }
354    return 0
355  }
356
357  /* ── LES CATALOGUES DU DEHORS ───────────────────────────────────────────
358   * `branche` a été RELEVÉ dans le serveur, pas décrété : le nombre est le
359   * nombre d'occurrences trouvées le jour du relevé. Un catalogue non branché
360   * est nommé et éteint — il n'est pas caché, parce qu'un manque nommé se
361   * répare et qu'un manque caché s'oublie. */
362  var CATALOGUES = [
363    { nom: 'Wikipédia', branche: true, dit: 'Encyclopédie libre — déjà atteinte par le serveur.' },
364    { nom: 'YouTube', branche: true, dit: 'Vidéos — déjà atteintes.' },
365    { nom: 'Flux RSS', branche: true, dit: "N'importe quel flux, n'importe quel podcast." },
366    { nom: 'Unsplash · Pexels', branche: true, dit: 'Images libres de droits.' },
367    { nom: 'NewsAPI · GDELT', branche: true, dit: 'La presse du jour, deux sources croisées.' },
368    { nom: 'OpenStreetMap', branche: true, dit: 'Lieux et adresses — la porte de Monde Réel.' },
369    { nom: 'Gallica · BnF', branche: false, dit: 'Des millions de documents du domaine public français.' },
370    { nom: 'Internet Archive', branche: false, dit: "Livres, films, et les pages telles qu'elles étaient." },
371    { nom: 'Légifrance', branche: false, dit: 'Le droit français, à jour et faisant foi.' },
372    { nom: 'data.gouv.fr', branche: false, dit: 'Les données publiques, dont les guides associatifs.' },
373    { nom: 'OpenLibrary · ISBN', branche: false, dit: "N'importe quel livre par son code-barres." },
374    { nom: 'Ma bibliothèque municipale', branche: false, dit: 'Chercher, voir si c’est disponible, réserver pour de vrai.' },
375    { nom: 'France Travail', branche: false, dit: 'Offres et alternance — pour ne pas enfermer les gens dans CRÉA.' },
376  ]
377
378  /* ═══ LA RECHERCHE QUI TRAVERSE ═════════════════════════════════════════
379   * C'est le verbe qui sépare un catalogue d'un placard : chercher UNE fois
380   * dans les seize matières. Avant, il fallait chercher quatre fois dans
381   * quatre écrans, et l'on ne trouvait que ce qu'on savait déjà où chercher. */
382  function chercher(q) {
383    var t = String(q || '').trim().toLowerCase()
384    if (!t) return []
385    var out = []
386    function pousse(o) { if (out.length < 60) out.push(o) }
387    pieces().forEach(function (f) {
388      if (f.supprime) return
389      var dans = String(f.nom || '').toLowerCase() + ' ' + String(f.note || '').toLowerCase() + ' ' + String(f.chemin || '').toLowerCase()
390      if (dans.indexOf(t) < 0) return
391      pousse({ nom: f.nom, ou: f.chemin || 'Créations', quand: f.majAt, section: sectionDuType(f.type), id: f.id })
392    })
393    conversations().forEach(function (c) {
394      if (String(c.title || '').toLowerCase().indexOf(t) < 0) return
395      pousse({ nom: c.title || 'Session', ou: 'Conversations', quand: c.updatedAt || c.createdAt, section: 'conversations', convId: c.id })
396    })
397    gardes().concat(lus()).forEach(function (a) {
398      var titre = String((a && (a.title || a.titre)) || '')
399      if (titre.toLowerCase().indexOf(t) < 0) return
400      pousse({ nom: titre, ou: 'Kiosque', quand: a.quand || a.at || 0, section: 'kiosque' })
401    })
402    memoires().memoires.forEach(function (m) {
403      var txt = String((m && (m.text || m.contenu || m)) || '')
404      if (txt.toLowerCase().indexOf(t) < 0) return
405      pousse({ nom: txt.slice(0, 70), ou: 'Savoir', quand: m.createdAt || 0, section: 'savoir' })
406    })
407    return out
408  }
409  function sectionDuType(type) {
410    var t = String(type || '').toLowerCase()
411    var s = SECTIONS.find(function (x) { return x.types && x.types.indexOf(t) >= 0 })
412    return s ? s.id : 'creations'
413  }
414
415  /* ═══ LE RENDU ══════════════════════════════════════════════════════════ */
416
417  function railSections() {
418    var cour = section()
419    return FAMILLES.map(function (fam) {
420      var siennes = SECTIONS.filter(function (s) { return s.fam === fam.id })
421      return `<p class="mq-fam">${esc(fam.nom)}</p>` + siennes.map(function (s) {
422        var n = compte(s.id)
423        return `<button class="mq-item${cour === s.id ? ' on f-' + fam.id : ''}" data-mq="section" data-mq-val="${esc(s.id)}">
424          <span class="i-${fam.id}">${ic(s.ic, 13)}</span>${esc(s.nom)}${s.porte ? '<span class="mq-porte" title="Un autre système">↗</span>' : ''}${n ? `<span class="n">${n}</span>` : ''}
425        </button>`
426      }).join('')
427    }).join('')
428  }
429
430  function enteteFin() {
431    var m = etat()
432    return `<div class="mq-entete-fin">
433      <span class="mq-compte">${pieces().filter(function (f) { return !f.supprime }).length} notices · ${SECTIONS.length} sections</span>
434      <button class="mq-primaire mq-mini" data-mq="acquerir">${ic('plus', 12)} Faire entrer</button>
435    </div>`
436  }
437
438  /* ── LE CATALOGUE — la page d'entrée ────────────────────────────────── */
439  function vueCatalogue() {
440    var m = etat()
441    var res = chercher(m.recherche)
442    var fonds = (S().folders || []).slice(0, 6)
443    return `<div class="mq-page">
444      <div class="mq-grande-recherche">
445        ${ic('loupe', 18, 1.9)}
446        <input id="mq-q" class="mq-champ-grand" placeholder="Chercher dans tout — et dehors" value="${esc(m.recherche)}" />
447        ${m.recherche ? `<button class="mq-verbe" data-mq="vider">Effacer</button>` : `<span class="mq-touche">⌘K</span>`}
448      </div>
449
450      ${m.recherche ? `<section class="mq-bloc">
451        <p class="mq-etiq">Trouvé <span class="mq-n">${res.length}</span></p>
452        ${res.length ? res.map(function (r) {
453          return `<button class="mq-ligne" data-mq="aller-section" data-mq-val="${esc(r.section)}">
454            <span class="mq-t">${esc(r.nom)}</span>
455            <span class="mq-m">${esc(r.ou)}${r.quand ? ' · ' + esc(ilYA(r.quand)) : ''}</span>
456          </button>`
457        }).join('') : vide('loupe', 'Rien sous ce mot', `Rien sous ce mot, ni dans les seize sections. Les catalogues du dehors ne sont pas encore interrogés par ce champ — c'est écrit plutôt que promis.`)}
458      </section>` : ''}
459
460      <section class="mq-bloc">
461        <p class="mq-etiq">Le fonds <span class="mq-apres">ce qui fait l'identité de la collection</span></p>
462        ${fonds.length ? `<div class="mq-bande">${fonds.map(function (f) {
463          var n = pieces().filter(function (x) { return !x.supprime && x.projectId === f.id }).length
464          return `<button class="mq-bande-c" data-mq="fonds" data-mq-val="${esc(f.id)}">
465            <span class="mq-vig" style="background:${esc(f.color || 'var(--accent)')}">${ic('dossier', 18)}</span>
466            <span class="mq-bande-n">${esc(f.name || 'Projet')}</span>
467            <span class="mq-bande-s">${n} pièce${n > 1 ? 's' : ''}</span>
468          </button>`
469        }).join('')}</div>`
470          : vide('livre', 'Aucun fonds encore', `Aucun projet encore. Un fonds, c'est ce qui donne une identité à une collection : le vôtre naîtra du premier projet que vous créerez.`)}
471      </section>
472
473      <section class="mq-bloc">
474        <p class="mq-etiq">Ce que CRÉA vous tend <span class="mq-apres">le conseil — c'est son meilleur rôle</span></p>
475        ${conseils().length ? conseils().map(function (c) {
476          return `<div class="mq-ligne-plate">
477            <span class="mq-t">${esc(c.quoi)}</span>
478            <span class="mq-source">${esc(c.ou)}</span>
479            <button class="mq-verbe" data-mq="aller-section" data-mq-val="${esc(c.section)}">${esc(c.verbe)}</button>
480          </div>`
481        }).join('') : vide('etincelle', 'Rien à vous tendre', `Rien à vous tendre pour l'instant. Le conseil se nourrit de ce que vous gardez, produisez et laissez de côté — il arrivera tout seul.`)}
482      </section>
483    </div>`
484  }
485
486  /* CE QUE CRÉA VOUS TEND — relu, jamais recopié. Chaque conseil naît d'un
487   * FAIT mesurable à l'instant du rendu, et disparaît quand le fait disparaît.
488   * Un conseil qu'on ne peut pas faire taire en agissant est une publicité. */
489  function conseils() {
490    var out = []
491    var d = doublons()
492    if (d.length) out.push({ quoi: d.length + ' pièce' + (d.length > 1 ? 's' : '') + ' en double', ou: 'désherbage', section: 'desherbage', verbe: 'Passer en revue' })
493    var vieux = jamaisRouvertes()
494    if (vieux.length) out.push({ quoi: vieux.length + ' pièce' + (vieux.length > 1 ? 's' : '') + ' jamais rouverte' + (vieux.length > 1 ? 's' : '') + ' depuis six mois', ou: 'désherbage', section: 'desherbage', verbe: 'Regarder' })
495    var g = gardes()
496    if (g.length) out.push({ quoi: g.length + ' article gardé' + (g.length > 1 ? 's' : '') + ', jamais rouvert', ou: 'kiosque', section: 'kiosque', verbe: 'Relire' })
497    var sons = parType(['audio', 'son'])
498    if (sons.length) out.push({ quoi: sons.length + ' enregistrement' + (sons.length > 1 ? 's' : '') + ' sans transcription', ou: 'sons', section: 'sons', verbe: 'Ouvrir' })
499    /* LE SEUL CONSEIL QUI SORT DU CATALOGUE, ET IL MÈNE À LA BRANCHE VOISINE.
500     * Il ne « joint » plus rien lui-même : joindre un dossier est un acte de
501     * DISQUE, et le catalogue n'en tient pas la liste. Il constate qu'il n'a
502     * rien reçu du disque, et montre la porte. */
503    if (!versesDuDisque().length) out.push({ quoi: "Rien de votre ordinateur n'est encore versé ici", ou: 'Explorateur', section: 'explorateur', verbe: "Ouvrir l'Explorateur" })
504    return out.slice(0, 5)
505  }
506
507  /* ── UN VIDE SE MONTRE, IL NE SE RACONTE PAS ────────────────────────────
508   *
509   * « Dans la maquette c'est imagé, c'est structuré. Là, tu ne m'as fait que
510   *  des écritures — tous les fichiers sont des écrans d'écriture bizarre. »
511   *
512   * La carte a été réparée ; restait ce qu'on voit quand il n'y a PAS de
513   * carte. Chaque section vide posait deux paragraphes l'un sous l'autre —
514   * la note qui annonce le rayon, puis le texte qui dit qu'il est vide — et
515   * les deux racontaient la même chose dans deux formulations différentes.
516   * Sur seize sections d'un catalogue neuf, c'est seize murs de prose là où
517   * il n'y a précisément rien à lire.
518   *
519   * Un vide de bibliothèque se reconnaît à l'œil : une marque, une phrase,
520   * et la porte à pousser s'il y en a une. Le RESTE — la raison, la promesse,
521   * ce qui viendra s'y ranger — est la note de la section, qui n'a plus
522   * besoin d'être dite deux fois. C'est la règle 3 portée à l'écran : la
523   * note se juge sur son terrain, et son terrain est le rayon vide.
524   */
525  function vide(icone, titre, dit, verbe) {
526    return `<div class="mq-vide">
527      <span class="mq-vide-marque">${ic(icone || 'livre', 26, 1.4)}</span>
528      <p class="mq-vide-titre">${titre}</p>
529      ${dit ? `<p class="mq-vide-dit">${dit}</p>` : ''}
530      ${verbe || ''}
531    </div>`
532  }
533
534  /* ── UNE ÉTAGÈRE — le gabarit commun aux sections de matière ───────────
535   * La note ne s'empile plus AU-DESSUS du rayon comme un bloc : elle passe
536   * sous le titre, alignée à gauche, à la largeur de lecture — c'est là
537   * qu'une explication se lit. La figure, elle, ne dit QUE ce qui manque. */
538  function etagere(titre, liste, dit, verbes, icone) {
539    return `<div class="mq-page">
540      <div class="mq-barre">
541        <div class="mq-titre-bloc">
542          <p class="mq-page-titre">${esc(titre)}</p>
543          ${dit ? `<p class="mq-dit">${dit}</p>` : ''}
544        </div>
545        ${verbes || ''}
546      </div>
547      ${liste.length ? `<div class="mq-grille">${liste.map(cartePiece).join('')}</div>`
548        : vide(icone, 'Cette étagère est encore vide', 'Elle existe quand même&nbsp;: une section qu’on ne voit pas est une section qui n’existe pas.')}
549    </div>`
550  }
551  /* ── UNE PIÈCE SE RECONNAÎT À SON IMAGE, PAS À SON NOM DE FICHIER ───────
552   *
553   * « Dans la maquette c'est imagé, c'est structuré. Là, tu ne m'as fait que
554   *  des écritures — tous les fichiers sont des écrans d'écriture bizarre. »
555   *
556   * Il a raison, et c'est la MÊME carte dans les seize sections : la réparer
557   * ici les répare toutes. Elle portait deux lignes de texte et rien d'autre.
558   *
559   * Une affiche se reconnaît à son visuel ; un son à sa forme d'onde ; un
560   * document à sa première page. Quand l'image existe VRAIMENT, on la montre.
561   * Quand elle n'existe pas, on ne fabrique pas un faux aperçu : on pose
562   * l'icône du type sur un fond teinté de sa famille — ce qui se reconnaît
563   * quand même d'un coup d'œil, sans mentir sur ce qu'on a. */
564  var TEINTES = {
565    image: 'img', photo: 'img', png: 'img', jpg: 'img', jpeg: 'img', svg: 'img', gif: 'img', webp: 'img',
566    audio: 'son', son: 'son', voix: 'son', mp3: 'son', wav: 'son',
567    video: 'vid', mp4: 'vid', mov: 'vid',
568    code: 'code', html: 'code', js: 'code', css: 'code', json: 'code',
569  }
570  function apercu(f) {
571    var t = String(f.type || '').toLowerCase()
572    var fam = TEINTES[t] || 'doc'
573    /* L'IMAGE N'EST MONTRÉE QUE SI ELLE EXISTE VRAIMENT. `url` peut être une
574     * adresse ou une donnée embarquée ; les deux s'affichent. Un `contenu`
575     * textuel n'est PAS une image, même pour un fichier dont le type dit
576     * « image » — c'est le cas d'un SVG écrit à la main, et le navigateur
577     * afficherait un carré cassé. */
578    var src = fam === 'img' && typeof f.url === 'string' && /^(https?:|data:|blob:)/.test(f.url) ? f.url : ''
579    if (src) {
580      return `<span class="mq-apercu mq-apercu-img"><img src="${esc(src)}" alt="" loading="lazy"></span>`
581    }
582    return `<span class="mq-apercu t-${fam}">${ic({ img: 'image', son: 'son', vid: 'video', code: 'source' }[fam] || 'doc', 22, 1.4)}</span>`
583  }
584  function cartePiece(f) {
585    var studio = f.origine && f.origine.studio
586    return `<article class="mq-carte" data-mq="piece" data-mq-val="${esc(f.id)}">
587      ${apercu(f)}
588      <span class="mq-carte-t" title="${esc(f.nom || 'Sans nom')}">${esc(f.nom || 'Sans nom')}</span>
589      <span class="mq-carte-lignes">
590        ${studio ? `<span class="mq-puce">${esc(studio)}</span>` : ''}
591        <span class="mq-carte-m">${esc([poids(f.octets), ilYA(f.majAt)].filter(Boolean).join(' · '))}</span>
592      </span>
593      <div class="mq-verbes">
594        <button class="mq-verbe" data-mq="verser" data-mq-val="${esc(f.id)}">Verser dans…</button>
595        ${f.convId ? `<button class="mq-verbe" data-mq="sa-session" data-mq-val="${esc(f.convId)}">Sa session</button>` : ''}
596      </div>
597    </article>`
598  }
599
600  /* ── LES SECTIONS ────────────────────────────────────────────────────── */
601  function vueSection(id) {
602    var s = SECTIONS.find(function (x) { return x.id === id })
603    if (!s) return vueCatalogue()
604
605    if (s.types) {
606      var dits = {
607        images: "Une photothèque, pas un dossier d'images : elle trie par ce que la photo montre, pas par son nom de fichier. La description est produite par CRÉA quand vous le lui demandez — elle n'est jamais inventée.",
608        sons: "Enregistrements, voix générées, ambiances. Une transcription rend deux heures d'assemblée cherchables en un mot ; elle n'est pas encore branchée, et c'est dit.",
609        videos: 'Les vôtres, celles produites ici, celles regardées.',
610        documents: "Ce qui compte n'est pas le nom du fichier mais ce qu'il y a dedans. L'indexation du contenu est le prochain lot.",
611      }
612      return etagere(s.nom, parType(s.types), dits[id] || '', '', s.ic)
613    }
614
615    if (id === 'creations') return vueCreations()
616    if (id === 'conversations') return vueConversations()
617    if (id === 'fabrication') return vueFabrication()
618    if (id === 'modeles') return vueModeles()
619    if (id === 'kiosque') return vueKiosque()
620    if (id === 'sources') return vueSources()
621    if (id === 'savoir') return vueSavoir()
622    if (id === 'lecture') return vueLecture()
623    if (id === 'monde') return vueMonde()
624    if (id === 'emprunts') return vueEmprunts()
625    if (id === 'partage') return vuePartage()
626    if (id === 'desherbage') return vueDesherbage()
627    return vueCatalogue()
628  }
629
630  /* FICHIERS — UNE PORTE, PAS UNE SECTION. C'est l'autre système. La
631   * Médiathèque n'en dessine pas l'écran : elle l'ouvre, et dit ce qui passe
632   * la frontière dans chaque sens. */
633  function vueCreations() {
634    var reals = realisations()
635    var seules = pieces().filter(function (f) { return !f.supprime && !f.realisation })
636    return `<div class="mq-page">
637      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Créations</p><p class="mq-dit">Ce que les studios ont produit. Une <b>réalisation</b>, c'est plusieurs fichiers sortis d'un seul geste — une affiche, son format carré, son tract et son texte pour les réseaux. Les compter séparément ferait perdre ce qui les relie.</p></div></div>
638      <section class="mq-bloc">
639        <p class="mq-etiq">Réalisations <span class="mq-n">${reals.length}</span></p>
640        ${reals.length ? reals.slice(0, 12).map(function (r) {
641          var fs = registre() && registre().fichiersDe ? (registre().fichiersDe(r.id) || []) : []
642          return `<div class="mq-ligne-plate">
643            <span class="mq-t">${esc(r.nom || 'Réalisation')}</span>
644            <span class="mq-m">${fs.length} fichier${fs.length > 1 ? 's' : ''} · ${esc(ilYA(r.majAt || r.creeAt))}</span>
645          </div>`
646        }).join('') : vide('etincelle', 'Aucune réalisation', `Aucune réalisation encore. Elles naissent toutes seules dès qu'un studio produit plusieurs pièces d'un coup.`)}
647      </section>
648      <section class="mq-bloc">
649        <p class="mq-etiq">Pièces seules <span class="mq-n">${seules.length}</span></p>
650        ${seules.length ? `<div class="mq-grille">${seules.slice(0, 24).map(cartePiece).join('')}</div>`
651          : vide('image', 'Aucune pièce seule', `Rien encore. Chaque image, document ou site produit dans un studio atterrit ici sans qu'on ait à le ranger.`)}
652      </section>
653    </div>`
654  }
655
656  function vueConversations() {
657    var cs = conversations()
658    return `<div class="mq-page">
659      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Conversations</p><p class="mq-dit">Les sessions ne sont pas un onglet à part : ce sont une <b>matière</b> du catalogue, au même titre qu'une image. On remonte d'un livrable à l'échange qui l'a fait naître, et l'inverse.</p></div></div>
660      ${cs.length ? `<div class="mq-grille">${cs.slice(0, 30).map(function (c) {
661        var produits = pieces().filter(function (f) { return !f.supprime && f.convId === c.id }).length
662        return `<article class="mq-carte" data-mq="ouvrir-session" data-mq-val="${esc(c.id)}">
663          <span class="mq-carte-t">${esc(c.title || 'Session')}</span>
664          <span class="mq-carte-m">${c.studio ? esc(nomStudio(c.studio)) + ' · ' : ''}${(c.messages || []).length} message${(c.messages || []).length > 1 ? 's' : ''}${produits ? ' · ' + produits + ' pièce' + (produits > 1 ? 's' : '') : ''} · ${esc(ilYA(c.updatedAt || c.createdAt))}</span>
665        </article>`
666      }).join('')}</div>` : vide('bulle', 'Aucune conversation', `Aucune session. La première conversation que vous ouvrirez apparaîtra ici avec ce qu'elle aura produit.`)}
667    </div>`
668  }
669
670  function vueFabrication() {
671    var t = enFabrication()
672    return `<div class="mq-page">
673      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">En fabrication</p><p class="mq-dit">Une médiathèque a une vie, pas seulement un inventaire. Ce qui se produit pendant que vous lisez cette page est ici.</p></div></div>
674      ${t.length ? t.map(function (x) {
675        return `<div class="mq-ligne-plate">
676          <span class="mq-t">${esc(String(x.label || x.prompt || 'Travail en cours').slice(0, 80))}</span>
677          <span class="mq-m">${(x.steps || []).length ? 'étape ' + (x.steps || []).length : x.status === 'queued' ? 'en file' : 'en cours'}</span>
678        </div>`
679      }).join('') : vide('horloge', 'Rien en fabrication', `Rien en cours de fabrication. Les travaux de fond lancés depuis les studios apparaîtront ici pendant qu'ils tournent.`)}
680    </div>`
681  }
682
683  function vueModeles() {
684    var ms = modeles()
685    return `<div class="mq-page">
686      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Modèles</p><p class="mq-dit">Ce qui sert à refaire : gabarits de projet, chartes, points de départ. C'est ici que viendront se brancher les modèles de document et de vidéo.</p></div></div>
687      ${ms.length ? `<div class="mq-grille">${ms.map(function (t) {
688        return `<article class="mq-carte" data-mq="modele" data-mq-val="${esc(t.key)}">
689          <span class="mq-carte-t">${esc(t.name)}</span>
690          <span class="mq-carte-m">${esc((t.studios || []).join(' · '))}</span>
691        </article>`
692      }).join('')}</div>` : vide('gabarit', 'Aucun modèle', `Aucun modèle.`)}
693    </div>`
694  }
695
696  function vueKiosque() {
697    var g = gardes(), l = lus()
698    return `<div class="mq-page">
699      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Kiosque</p><p class="mq-dit">Ce que CRÉA a lu pour vous. C'est la presse du jour — et surtout ce que vous en avez gardé, qui n'était visible nulle part jusqu'ici.</p></div></div>
700      <section class="mq-bloc">
701        <p class="mq-etiq">Gardé <span class="mq-n">${g.length}</span></p>
702        ${g.length ? g.slice(0, 20).map(ligneArticle).join('')
703          : vide('kiosque', 'Rien de gardé', `Rien de gardé. Ce que vous mettez de côté dans le fil d'actualités atterrit ici et n'en repart plus.`)}
704      </section>
705      <section class="mq-bloc">
706        <p class="mq-etiq">Lu <span class="mq-n">${l.length}</span></p>
707        ${l.length ? l.slice(0, 20).map(ligneArticle).join('')
708          : vide('kiosque', 'Rien encore lu', `Rien encore lu. Le fil d'actualités retient déjà ce que vous ouvrez — cette étagère se remplira toute seule.`)}
709      </section>
710    </div>`
711  }
712  function ligneArticle(a) {
713    var titre = String((a && (a.title || a.titre || a)) || 'Article')
714    return `<div class="mq-ligne-plate">
715      <span class="mq-t">${esc(titre.slice(0, 110))}</span>
716      ${a && (a.source || a.quand) ? `<span class="mq-m">${esc([a.source, a.quand ? ilYA(a.quand) : ''].filter(Boolean).join(' · '))}</span>` : ''}
717    </div>`
718  }
719
720  /* SOURCES — LE TROU LE PLUS GRAND, ET IL EST NOMMÉ.
721   * Mesuré dans le dépôt : zéro. CRÉA cite quatre pages dans une réponse et
722   * n'en garde aucune. Une source disparue du web rend son affirmation
723   * invérifiable pour toujours ; une source gardée à la date de lecture reste
724   * vérifiable. C'est ce que fait une bibliothèque, et c'est ce qu'aucun
725   * assistant ne fait. La section existe VIDE pour que le trou se voie. */
726  function vueSources() {
727    return `<div class="mq-page">
728      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Sources</p><p class="mq-dit">D'où vient ce que CRÉA a dit. Elle cite des pages dans ses réponses et n'en conserve aucune — relevé dans le code&nbsp;: <b>zéro</b>. Trois mois plus tard, la page citée a bougé ou disparu, et la réponse ne s'appuie plus sur rien.</p></div></div>
729      <section class="mq-bloc">
730        <p class="mq-etiq">Rien n'est encore gardé</p>
731        ${vide('source', 'Aucune source conservée', `Cette étagère se remplira le jour où une citation sera gardée avec sa date.`)}
732      </section>
733    </div>`
734  }
735
736  /* SAVOIR — CE QU'ELLE CROIT SAVOIR DE VOUS, ET QUE VOUS POUVEZ CORRIGER.
737   * Soixante-cinq usages dans le code, aucun écran : on ne pouvait ni voir ni
738   * réparer ce que CRÉA retenait. Montrer sans permettre de corriger serait
739   * pire que de cacher. */
740  function vueSavoir() {
741    var m = memoires()
742    return `<div class="mq-page">
743      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Savoir</p><p class="mq-dit">Ce que CRÉA a retenu de vous. Le montrer n'a de sens que si l'on peut le corriger&nbsp;: chaque ligne se retire.</p></div></div>
744      <section class="mq-bloc">
745        <p class="mq-etiq">Mémoires <span class="mq-n">${m.memoires.length}</span></p>
746        ${m.memoires.length ? m.memoires.slice(0, 25).map(function (x, i) {
747          var txt = String((x && (x.text || x.contenu)) || x || '')
748          return `<div class="mq-ligne-plate">
749            <span class="mq-t">${esc(txt.slice(0, 140))}</span>
750            <button class="mq-verbe" data-mq="memoire-retirer" data-mq-val="${i}">Retirer</button>
751          </div>`
752        }).join('') : vide('savoir', 'Rien retenu pour l’instant', `CRÉA n'a rien retenu de vous pour l'instant. Ce qu'elle retiendra apparaîtra ici, et pourra en être retiré.`)}
753      </section>
754      <section class="mq-bloc">
755        <p class="mq-etiq">Graphe de connaissances <span class="mq-n">${m.noeuds.length}</span></p>
756        ${m.noeuds.length ? `<div class="mq-jetons">${m.noeuds.slice(0, 40).map(function (n) {
757          return `<span class="mq-jeton">${esc(String((n && (n.label || n.id)) || n).slice(0, 40))}</span>`
758        }).join('')}</div>` : vide('savoir', 'Aucun nœud', `Aucun nœud. Le graphe se construit à mesure des échanges.`)}
759      </section>
760    </div>`
761  }
762
763  function vueLecture() {
764    return `<div class="mq-page">
765      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Salle de lecture</p><p class="mq-dit">Une médiathèque a des <b>lieux</b>, pas seulement des rayons. La salle de lecture est un endroit où l'on lit vraiment&nbsp;: une colonne, pas de barre d'outils, et la marge pour ce qu'on note.</p></div></div>
766      <section class="mq-bloc">
767        <p class="mq-etiq">Aucun ouvrage en salle</p>
768        ${vide('lecture', 'La salle n’est pas encore ouverte', `Le lecteur reste à construire&nbsp;: c'est un lieu, pas une liste, et il se fait à part.`)}
769      </section>
770    </div>`
771  }
772
773  function vueMonde() {
774    var branches = CATALOGUES.filter(function (c) { return c.branche })
775    var restants = CATALOGUES.filter(function (c) { return !c.branche })
776    return `<div class="mq-page">
777      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Le monde</p><p class="mq-dit">Ce qui définit une bibliothèque n'est pas son stock&nbsp;: c'est son accès. CRÉA atteint déjà six catalogues extérieurs tous les jours — et ne gardait rien de ce qu'elle en rapportait.</p></div></div>
778      <section class="mq-bloc">
779        <p class="mq-etiq">Branchés <span class="mq-n">${branches.length}</span></p>
780        ${branches.map(function (c) {
781          return `<div class="mq-ligne-plate"><span class="mq-source mq-source-vif">branché</span><span class="mq-t">${esc(c.nom)}</span><span class="mq-m">${esc(c.dit)}</span></div>`
782        }).join('')}
783      </section>
784      <section class="mq-bloc">
785        <p class="mq-etiq">À brancher <span class="mq-n">${restants.length}</span></p>
786        ${restants.map(function (c) {
787          return `<div class="mq-ligne-plate mq-eteint"><span class="mq-source">
787à venir</span><span class="mq-t">${esc(c.nom)}</span><span class="mq-m">${esc(c.dit)}</span></div>`
788        }).join('')}
789      </section>
790    </div>`
791  }
792
793  function vueEmprunts() {
794    return `<div class="mq-page">
795      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Emprunts</p><p class="mq-dit">Ce qu'on a <b>pour un temps</b>&nbsp;: une licence, un accès, un livre réservé. Et le verbe le plus fort d'une bibliothèque — le <b>prêt entre bibliothèques</b>&nbsp;: demander à quelqu'un d'autre ce qu'on n'a pas.</p></div></div>
796      <section class="mq-bloc">
797        <p class="mq-etiq">Rien en cours</p>
798        ${vide('relais', 'Aucun emprunt', `Le prêt bute sur le même mur que le poste partagé&nbsp;: la sauvegarde range un instantané entier par personne.`)}
799      </section>
800    </div>`
801  }
802
803  function vuePartage() {
804    return `<div class="mq-page">
805      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Partagé</p><p class="mq-dit">Ce qui vient des autres&nbsp;: les pièces jointes d'une mission, les ressources d'un cercle, ce qu'un membre d'un poste a déposé. L'auteur reste écrit dessus.</p></div></div>
806      <section class="mq-bloc">
807        <p class="mq-etiq">Rien de partagé</p>
808        ${vide('gens', 'Rien reçu', `Elle se remplira le jour où deux personnes pourront écrire dans le même espace sans s'effacer.`)}
809      </section>
810    </div>`
811  }
812
813  function vueDesherbage() {
814    var d = doublons(), v = jamaisRouvertes(), c = corbeille()
815    return `<div class="mq-page">
816      <div class="mq-barre"><div class="mq-titre-bloc"><p class="mq-page-titre">Désherbage</p><p class="mq-dit">Une bibliothèque qui ne désherbe pas meurt étouffée. Ici, ce qui existe en double et ce qui n'a pas été rouvert depuis six mois — plus la corbeille, qui rend toujours.</p></div></div>
817      <section class="mq-bloc">
818        <p class="mq-etiq">En double <span class="mq-n">${d.length}</span></p>
819        ${d.length ? d.slice(0, 15).map(function (grp) {
820          return `<div class="mq-ligne-plate">
821            <span class="mq-t">${esc(grp[0].nom)}</span>
822            <span class="mq-m">${grp.length} exemplaires · ${esc(poids(grp[0].octets))} chacun</span>
823          </div>`
824        }).join('') : vide('loupe', 'Aucun doublon', `Comptent comme doublons deux pièces de même nom <b>et</b> de même poids.`)}
825      </section>
826      <section class="mq-bloc">
827        <p class="mq-etiq">Jamais rouvert depuis six mois <span class="mq-n">${v.length}</span></p>
828        ${v.length ? v.slice(0, 15).map(function (f) {
829          return `<div class="mq-ligne-plate"><span class="mq-t">${esc(f.nom)}</span><span class="mq-m">${esc(ilYA(f.majAt))} · ${esc(poids(f.octets))}</span></div>`
830        }).join('') : vide('horloge', 'Rien d’oublié', `Rien d'oublié. Ce qui dort depuis six mois viendra ici — pour être regardé, pas pour être jeté d'office.`)}
831      </section>
832      <section class="mq-bloc">
833        <p class="mq-etiq">Corbeille <span class="mq-n">${c.length}</span></p>
834        ${c.length ? c.slice(0, 15).map(function (f) {
835          return `<div class="mq-ligne-plate">
836            <span class="mq-t">${esc(f.nom)}</span>
837            <button class="mq-verbe" data-mq="restaurer" data-mq-val="${esc(f.id)}">Rendre</button>
838          </div>`
839        }).join('') : vide('corbeille', 'La corbeille est vide', `La corbeille est vide. Ce qui y tombe en revient&nbsp;: rien de ce que la Médiathèque supprime ne part sans retour.`)}
840      </section>
841    </div>`
842  }
843
844  /* ═══ LA COQUILLE ═══════════════════════════════════════════════════════ */
845  function rendre() {
846    var cour = section()
847    var s = SECTIONS.find(function (x) { return x.id === cour })
848    var entete = typeof window._espaceMiniHeader === 'function'
849      ? window._espaceMiniHeader('Médiathèque', '', enteteFin())
850      : `<div class="mq-entete-repli">Médiathèque</div>`
851    return `<main class="mq-espace flex flex-1 flex-col overflow-hidden min-w-0" style="background:var(--bg-primary)">
852      ${entete}
853      <nav class="mq-fil">
854        <button class="mq-fil-seg${cour === 'catalogue' ? ' on' : ''}" data-mq="section" data-mq-val="catalogue">${ic('livre', 13)} Catalogue</button>
855        <button class="mq-fil-seg${cour === 'explorateur' ? ' on' : ''}" data-mq="section" data-mq-val="explorateur">${ic('ordi', 13)} Explorateur</button>
856        ${s ? `<span class="mq-fil-sep">›</span><span class="mq-fil-seg on">${esc(s.nom)}</span>` : ''}
857      </nav>
858      ${cour === 'explorateur' ? (window.CREA_DISQUE ? window.CREA_DISQUE.rendre() : `<div class="mq-defilement">${vide('ordi', 'L’Explorateur n’est pas chargé ici', 'Cette surface sert la Médiathèque sans le module de disque. Le catalogue, lui, fonctionne&nbsp;: c’est la branche voisine.')}</div>`) : `<div class="mq-defilement">
859        <div class="mq-colonnes">
860          <aside class="mq-rail">${railSections()}</aside>
861          <div class="mq-centre">${cour === 'catalogue' ? vueCatalogue() : vueSection(cour)}</div>
862        </div>
863      </div>`}
864      ${cour === 'explorateur' ? '' : `<div class="mq-etat">
865        <span>${pieces().filter(function (f) { return !f.supprime }).length} notices</span>
866        <span class="droite">${versesDuDisque().length} venue(s) du disque · ${CATALOGUES.filter(function (c) { return c.branche }).length} catalogues branchés</span>
867      </div>`}
868    </main>`
869  }
870
871  /* ═══ LES GESTES ════════════════════════════════════════════════════════ */
872  function cible(e) {
873    var n = e.target
874    while (n && n !== document) {
875      if (n.getAttribute && n.getAttribute('data-mq')) return n
876      n = n.parentNode
877    }
878    return null
879  }
880
881  async function agir(geste, val) {
882    var m = etat()
883    switch (geste) {
884      case 'section':
885      case 'aller-section':
886        ouvrirSection(val); return
887      case 'vider': m.recherche = ''; garder(); redessiner(); return
888
889
890      /* JOINDRE UN DOSSIER N'EST PLUS D'ICI, ET C'EST LE FOND DU SUJET.
891       * Un dossier accordé est un fait de DISQUE : il appartient à l'autre
892       * branche, qui en tient la seule liste. Le catalogue en gardait une
893       * copie — deux comptes qui divergeaient au premier retrait, et un bouton
894       * « Joindre » de chaque côté. */
895      case 'verser': {
896        var f = registre() && registre().lire ? registre().lire(val) : null
897        if (!f) return
898        dire('« ' + esc(f.nom) + ' » — le versement vers un projet ou une session arrive au prochain lot.', 'info', '📥')
899        return
900      }
901      case 'sa-session':
902      case 'ouvrir-session':
903        /* UN LIVRABLE SAIT D'OÙ IL VIENT, ET C'EST TOUT L'INTÉRÊT. Remonter au
904         * fil qui l'a produit est le lien que la Médiathèque tient et qu'aucun
905         * dossier ne tiendra jamais. */
906        /* DEUX SORTES DE SESSIONS, UNE SEULE PORTE. `openChatTab` supposait un
907         * fil de chat ; depuis qu'un fichier peut venir d'une session de
908         * STUDIO, cet identifiant désigne deux choses. Le résolveur
909         * (src/crea-chaines.js) sait laquelle et ouvre du bon côté. */
910        if (typeof window.ouvrirSessionParIdentifiant === 'function') {
911          if (window.ouvrirSessionParIdentifiant(val)) return
912          dire('Cette session n\'existe plus — le fichier, lui, est toujours là.', 'info', '🧭')
913          return
914        }
915        if (typeof window.openChatTab === 'function') { window.openChatTab(val); return }
916        if (typeof window.openModeTab === 'function') window.openModeTab('chat')
917        return
918      case 'fonds':
919        ouvrirSection('creations'); return
920      case 'modele':
921        if (typeof window.createFolderFromTemplate === 'function') { window.createFolderFromTemplate(val); return }
922        return
923      case 'memoire-retirer': {
924        var st = S()
925        if (!Array.isArray(st.memories)) return
926        st.memories.splice(Number(val), 1)
927        dire('Retiré de ce que CRÉA retient.', 'success', '🧠')
928        garder(); redessiner(); return
929      }
930      case 'restaurer':
931        if (registre() && typeof registre().restaurer === 'function') registre().restaurer(val)
932        garder(); redessiner(); return
933      case 'acquerir':
934        ouvrirSection('monde'); return
935    }
936  }
937
938  function brancher() {
939    document.addEventListener('click', function (e) {
940      var el = cible(e)
941      if (!el) return
942      if (S().workMode !== 'library') return
943      e.preventDefault()
944      agir(el.getAttribute('data-mq'), el.getAttribute('data-mq-val'))
945    })
946    /* LE CHAMP DE RECHERCHE NE PASSE PAS PAR LE RENDU GÉNÉRAL. Redessiner
947     * l'espace à chaque frappe ferait perdre le curseur une fois par lettre —
948     * c'est le défaut qu'on attribue au clavier. On écrit dans l'état, et on
949     * ne redessine que le résultat. */
950    document.addEventListener('input', function (e) {
951      if (!e.target || e.target.id !== 'mq-q') return
952      etat().recherche = e.target.value
953      redessiner()
954      var c = document.getElementById('mq-q')
955      if (c) { c.focus(); c.setSelectionRange(c.value.length, c.value.length) }
956    })
957  }
958  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', brancher)
959  else brancher()
960
961  window.CREA_MEDIATHEQUE = {
962    rendre: rendre,
963    sections: SECTIONS,
964    familles: FAMILLES,
965    /* LA PORTE QUE L'AUTRE SYSTÈME POUSSE. L'explorateur verse un fichier du
966     * disque dans le catalogue, puis mène ici — c'est son seul droit sur cet
967     * état, et il ne connaît pas la forme du reste. Elle a été retirée une
968     * journée, exportée pour un appelant qui n'était pas encore écrit : le
969     * cliquet des champs morts avait raison de la voir. */
970    ouvrirSection: ouvrirSection,
971    montrer: montrer,
972    chercher: chercher,
973    compte: compte,
974  }
975})()

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.