';
- sec.querySelector(".rp-count").textContent = "";
- sec.querySelector(".rp-all").href = nativeSearchUrl(app.key, bases[app.key], "");
+ states[key] = {
+ status: "idle",
+ items: [],
+ message: "",
+ q: "",
+ base: key === "console" ? consoleBase : bases[key],
+ app: key,
+ };
+ renderSection(sec, states[key], activeTab === key);
});
+ consoleFetchedQ = null;
+ renderTabs();
return;
}
APPS.forEach(function (app) {
var sec = sections[app.key];
if (!sec) return;
- renderSection(sec, { status: "loading", app: app.key, base: bases[app.key], q: q });
+ states[app.key] = {
+ status: "loading",
+ items: [],
+ message: "",
+ q: q,
+ base: bases[app.key],
+ app: app.key,
+ };
+ renderSection(sec, states[app.key], activeTab === app.key);
var url;
if (app.key === hostApp) {
/* Même origine : pas de CORS, pas de CSP cross-origin. */
- url = "/" + searchPath(app.key, q);
+ url = "/" + searchPath(app.key, q, true);
} else {
- url = bases[app.key] + searchPath(app.key, q);
+ url = bases[app.key] + searchPath(app.key, q, true);
}
fetchSection(app.key, url, function (err, data) {
if (myRun !== runId) return;
@@ -393,25 +763,45 @@
msg = "Connexion requise sur " + app.label;
else if (err.status) msg = "Indisponible (HTTP " + err.status + ")";
else msg = "Injoignable depuis cet hôte (CORS ou réseau)";
- renderSection(sec, {
+ states[app.key] = {
status: "error",
+ items: [],
message: msg,
- app: app.key,
- base: bases[app.key],
q: q,
- });
- return;
+ base: bases[app.key],
+ app: app.key,
+ };
+ } else {
+ var items = normalize(app.key, data, bases[app.key], q, true);
+ states[app.key] = {
+ status: items.length ? "ok" : "empty",
+ items: items,
+ message: "",
+ q: q,
+ base: bases[app.key],
+ app: app.key,
+ };
}
- var items = normalize(app.key, data, bases[app.key], q);
- renderSection(sec, {
- status: items.length ? "ok" : "empty",
- items: items,
- app: app.key,
- base: bases[app.key],
- q: q,
- });
+ renderSection(sec, states[app.key], activeTab === app.key);
+ renderTabs();
});
});
+ /* Console : seulement si son onglet est affiché ; sinon son
+ * compteur reste vide (pas de faux 0) jusqu'à son activation. */
+ if (activeTab === "console") {
+ fetchConsole(q, myRun);
+ } else {
+ states.console = {
+ status: "idle",
+ items: [],
+ message: "",
+ q: q,
+ base: consoleBase,
+ app: "console",
+ };
+ consoleFetchedQ = null;
+ renderTabs();
+ }
}
function scheduleSearch(q, immediate) {
@@ -427,13 +817,33 @@
function syncUrl(q) {
try {
- var url = new URL(global.location.href);
- if (q) url.searchParams.set("q", q);
- else url.searchParams.delete("q");
- global.history.replaceState(null, "", url.toString());
+ global.history.replaceState(
+ null,
+ "",
+ global.location.pathname + buildSearchUrl(q, activeTab)
+ );
} catch (e) {}
}
+ function focusSearch() {
+ try {
+ input.focus({ preventScroll: true });
+ } catch (e) {
+ try {
+ input.focus();
+ } catch (e2) {}
+ }
+ }
+
+ function visibleLinks() {
+ return Array.prototype.slice
+ .call(document.querySelectorAll("[data-rp-link]"))
+ .filter(function (a) {
+ var s = a.closest ? a.closest("[data-rp-section]") : null;
+ return !s || !s.hidden;
+ });
+ }
+
input.addEventListener("input", function () {
var q = input.value.trim();
syncUrl(q);
@@ -441,7 +851,7 @@
});
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
- var first = document.querySelector("[data-rp-link]");
+ var first = visibleLinks()[0];
if (first) {
e.preventDefault();
first.click();
@@ -471,6 +881,9 @@
input.select();
return;
}
+ /* Sur un onglet, ↑/↓ ne déplacent pas les résultats : les
+ * flèches ←/→ du tablist s'en chargent (voir ci-dessous). */
+ if (target && target.closest && target.closest(".rp-tab")) return;
if (!typing && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
moveFocus(e.key === "ArrowDown" ? 1 : -1, e);
}
@@ -478,8 +891,62 @@
true
);
+ /* « Tout voir » reste sur la page : bascule vers l'onglet. */
+ root.addEventListener("click", function (e) {
+ var all = e.target && e.target.closest ? e.target.closest(".rp-all") : null;
+ if (!all) return;
+ var sec = all.closest ? all.closest("[data-rp-section]") : null;
+ if (!sec) return;
+ e.preventDefault();
+ setTab(sec.getAttribute("data-rp-section"), { push: true, focusTab: true });
+ });
+
+ /* Clic sur la barre « Rechercher partout » (bouton
+ * data-act="palette") pendant qu'on est déjà sur la page :
+ * replacer le curseur dans la boîte au lieu de recharger. */
+ document.addEventListener("click", function (e) {
+ var p = e.target && e.target.closest ? e.target.closest('[data-act="palette"]') : null;
+ if (!p) return;
+ e.preventDefault();
+ focusSearch();
+ try {
+ input.select();
+ } catch (err) {}
+ });
+
+ /* Onglets : clic + clavier (←/→, Début/Fin). */
+ Object.keys(tabButtons).forEach(function (key) {
+ tabButtons[key].addEventListener("click", function () {
+ setTab(key, { push: true });
+ });
+ });
+ if (tablist) {
+ tablist.addEventListener("keydown", function (e) {
+ var order = TABS;
+ var idx = order.indexOf(activeTab);
+ var next = null;
+ if (e.key === "ArrowRight") next = order[(idx + 1) % order.length];
+ else if (e.key === "ArrowLeft")
+ next = order[(idx - 1 + order.length) % order.length];
+ else if (e.key === "Home") next = order[0];
+ else if (e.key === "End") next = order[order.length - 1];
+ if (!next) return;
+ e.preventDefault();
+ setTab(next, { push: true, focusTab: true });
+ });
+ }
+
+ /* Retour arrière navigateur : restaurer q + onglet. */
+ global.addEventListener("popstate", function () {
+ var params = new URLSearchParams(global.location.search);
+ var q = (params.get("q") || "").trim();
+ input.value = q;
+ applyTab(params.get("tab"));
+ runSearch(q);
+ });
+
function moveFocus(delta, e) {
- var links = Array.prototype.slice.call(document.querySelectorAll("[data-rp-link]"));
+ var links = visibleLinks();
if (!links.length) return;
var active = document.activeElement;
var idx = links.indexOf(active);
@@ -492,12 +959,23 @@
var params = new URLSearchParams(global.location.search);
var initialQ = (params.get("q") || "").trim();
- if (initialQ) {
- input.value = initialQ;
- runSearch(initialQ);
- } else {
- runSearch("");
- }
+ var initialTab = parseTab(params.get("tab"));
+ if (initialQ) input.value = initialQ;
+ applyTab(initialTab);
+ runSearch(initialQ);
+
+ /* Focus explicite (voir en-tête) : l'attribut autofocus seul ne
+ * survit pas au chargement des surcouches. */
+ focusSearch();
+ try {
+ if (global.requestAnimationFrame) global.requestAnimationFrame(focusSearch);
+ } catch (e) {}
+ try {
+ setTimeout(focusSearch, 0);
+ } catch (e) {}
+ global.addEventListener("pageshow", function (e) {
+ if (e.persisted) focusSearch();
+ });
}
if (document.readyState === "loading") {
diff --git a/frontend/recherche.css b/frontend/recherche.css
index f8ab9d9..1168700 100644
--- a/frontend/recherche.css
+++ b/frontend/recherche.css
@@ -33,3 +33,58 @@
.rp-title { display: block; font-weight: 600; }
.rp-context { display: block; font-size: .8rem; opacity: .65; }
.rp-snippet { display: block; font-size: .88rem; opacity: .85; margin-top: 2px; }
+
+/* ---- Onglets (chantier 2+3) : mêmes onglets sur les 4 hôtes. ---- */
+.rp-tabs {
+ display: flex; gap: 4px; flex-wrap: wrap;
+ border-bottom: 1px solid #888; margin-bottom: 20px;
+}
+.rp-tab {
+ background: none; border: none; border-bottom: 2px solid transparent;
+ padding: 8px 10px; margin-bottom: -1px; font: inherit; cursor: pointer;
+ color: inherit; opacity: .7; white-space: nowrap;
+}
+.rp-tab:hover { opacity: 1; }
+.rp-tab.active { opacity: 1; border-bottom-color: #3b82f6; font-weight: 700; }
+.rp-tab:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
+.rp-tab-count {
+ font-size: .8rem; font-weight: 700; background: rgba(127, 127, 127, .25);
+ border-radius: 999px; padding: 1px 7px; margin-left: 2px;
+}
+.rp-tab-count:empty { display: none; }
+.rp-section[hidden] { display: none; }
+.rp-all[hidden] { display: none; }
+
+/* ---- TikTak : la page sort de la colonne sidebar (chantier 2+3).
+ * Cause : recherche.html étend base.html sans layout_class, donc
+ * .rp-page (enfant direct de .layout) tombait en auto-placement dans
+ * la 2e colonne de la grille (largeur sidebar). La règle ci-dessous,
+ * gardée par l'ancêtre .layout (TikTak seul), l'étend de la colonne 2
+ * à la fin de la grille — juste à droite du rail d'icônes — sur toute
+ * la largeur disponible, sans toucher aux autres pages (elles ne
+ * portent pas .rp-page). En mobile (≤760 px, point de bascule TikTak),
+ * la grille n'a qu'une colonne : la page la prend entièrement. */
+.layout > .rp-page {
+ grid-column: 2 / -1; max-width: none; min-width: 0; overflow-y: auto;
+}
+@media (max-width: 760px) {
+ .layout > .rp-page { grid-column: 1 / -1; }
+}
+
+/* ---- ObsiGate : navigation statique de la page autonome (chantier
+ * 2+3). La page /recherche est servie hors SPA (route avant le
+ * catch-all) : elle reprend les classes de l'app (style.css :
+ * .app-container, .header, .main-body, .sidebar, .content-wrapper,
+ * .content-area) et cette navigation, calée sur les variables de
+ * l'app, en guise de sidebar. Les vaults dynamiques restent dans
+ * la SPA (voir le commentaire dans recherche.html). */
+.rp-og-nav { display: flex; flex-direction: column; gap: 4px; padding: 12px; }
+.rp-og-nav a {
+ display: block; padding: 8px 10px; border-radius: 8px;
+ text-decoration: none; color: inherit;
+}
+.rp-og-nav a:hover { background: rgba(127, 127, 127, .15); }
+.rp-og-nav a[aria-current="page"] {
+ background: rgba(127, 127, 127, .18); font-weight: 700;
+}
+.rp-og-note { opacity: .65; font-size: .85rem; padding: 8px 10px; margin: 8px 0 0; }
diff --git a/frontend/recherche.html b/frontend/recherche.html
index 1a7a6fc..98c780e 100644
--- a/frontend/recherche.html
+++ b/frontend/recherche.html
@@ -1,55 +1,146 @@
-
+
Rechercher partout — ObsiGate
-
-
-
-
-
-
-
-
Rechercher partout
-
Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat.
-
-
+ le JS) — jamais d'URL inventée.
+ La barre ci-dessous est une copie statique du snippet figé de
+ index.html (commons v0.1.6) : les onglets sont des liens réels,
+ le bouton central replace le curseur dans la boîte (recherche.js,
+ clic data-act="palette"). Les menus, le panneau Spark, les
+ alertes et le domaine restent des comportements de la SPA et ne
+ sont pas rejoués ici — l'autre travailleur (onglet Console Spark
+ de la barre) devra reporter son ajout dans cette copie au
+ re-vendoring, comme pour tout hôte.
+ Sidebar statique (navigation de l'app) : les vaults, tags et
+ récents dynamiques vivent dans la SPA, pas sur cette page. -->
+
+
+
+
+
+
+
Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.