Add pending apps and frontend components
- apps/captain-mobile: Mobile API service - apps/flow-ui: Flow UI application - apps/mindlink: Mindlink application - apps/storage: Storage API and workers - apps/tzzr-cli: TZZR CLI tool - deck-frontend/backups: Historical TypeScript versions - hst-frontend: Standalone HST frontend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
148
hst-frontend/js/app.js
Normal file
148
hst-frontend/js/app.js
Normal file
@@ -0,0 +1,148 @@
|
||||
// === APPLICATION INIT ===
|
||||
|
||||
function parseHash() {
|
||||
const h = window.location.hash.replace(/^#\/?/, "").replace(/\/?$/, "").split("/").filter(Boolean);
|
||||
if (h[0] && ["hst","flg","itm","loc","ply"].includes(h[0].toLowerCase())) {
|
||||
state.base = h[0].toLowerCase();
|
||||
}
|
||||
if (h[1] && ["grid","tree","graph"].includes(h[1].toLowerCase())) {
|
||||
state.view = h[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function updateHash() {
|
||||
const p = [state.base];
|
||||
if (state.view !== "grid") p.push(state.view);
|
||||
window.location.hash = "/" + p.join("/") + "/";
|
||||
}
|
||||
|
||||
async function init() {
|
||||
parseHash();
|
||||
|
||||
// Update UI to match state
|
||||
document.querySelectorAll(".base-btn").forEach(b =>
|
||||
b.classList.toggle("active", b.dataset.base === state.base)
|
||||
);
|
||||
document.querySelectorAll(".view-tab").forEach(t =>
|
||||
t.classList.toggle("active", t.dataset.view === state.view)
|
||||
);
|
||||
|
||||
// Show loading
|
||||
document.getElementById("grid-view").innerHTML = '<div class="loading">Cargando</div>';
|
||||
|
||||
// Fetch initial data
|
||||
await Promise.all([fetchTags(), fetchGroups(), fetchLibraries()]);
|
||||
|
||||
// Render UI
|
||||
renderGroups();
|
||||
renderLibraries();
|
||||
renderView();
|
||||
}
|
||||
|
||||
// === EVENT BINDINGS ===
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Base selector
|
||||
document.querySelectorAll(".base-btn").forEach(b => b.onclick = async () => {
|
||||
document.querySelectorAll(".base-btn").forEach(x => x.classList.remove("active"));
|
||||
b.classList.add("active");
|
||||
state.base = b.dataset.base;
|
||||
|
||||
// Reset state
|
||||
state.group = "all";
|
||||
state.library = "all";
|
||||
state.libraryMembers.clear();
|
||||
state.search = "";
|
||||
state.graphEdges = [];
|
||||
state.treeEdges = [];
|
||||
clearSelection();
|
||||
closeDetail();
|
||||
document.getElementById("search").value = "";
|
||||
updateHash();
|
||||
|
||||
// Reload
|
||||
document.getElementById("grid-view").innerHTML = '<div class="loading">Cargando</div>';
|
||||
await fetchTags();
|
||||
await fetchGroups();
|
||||
renderGroups();
|
||||
renderView();
|
||||
});
|
||||
|
||||
// View tabs
|
||||
document.querySelectorAll(".view-tab").forEach(t => t.onclick = () => {
|
||||
document.querySelectorAll(".view-tab").forEach(x => x.classList.remove("active"));
|
||||
t.classList.add("active");
|
||||
state.view = t.dataset.view;
|
||||
updateHash();
|
||||
closeDetail();
|
||||
renderView();
|
||||
});
|
||||
|
||||
// Search
|
||||
let st;
|
||||
document.getElementById("search").oninput = e => {
|
||||
clearTimeout(st);
|
||||
st = setTimeout(() => {
|
||||
state.search = e.target.value;
|
||||
renderView();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
// Language selector
|
||||
document.getElementById("lang-select").addEventListener("change", function(e) {
|
||||
state.lang = this.value;
|
||||
renderView();
|
||||
if (state.selectedTag) showDetail(state.selectedTag.mrf);
|
||||
});
|
||||
|
||||
// Selection mode
|
||||
document.getElementById("btn-sel").onclick = () => {
|
||||
state.selectionMode = !state.selectionMode;
|
||||
document.getElementById("btn-sel").classList.toggle("active", state.selectionMode);
|
||||
if (!state.selectionMode) {
|
||||
state.selected.clear();
|
||||
updateSelCount();
|
||||
}
|
||||
renderView();
|
||||
};
|
||||
|
||||
// Get selected
|
||||
document.getElementById("btn-get").onclick = () => {
|
||||
if (!state.selected.size) return toast("No hay seleccionados");
|
||||
navigator.clipboard.writeText([...state.selected].join("\n"))
|
||||
.then(() => toast(`Copiados ${state.selected.size} mrfs`));
|
||||
};
|
||||
|
||||
// API modal
|
||||
document.getElementById("btn-api").onclick = () =>
|
||||
document.getElementById("api-modal").classList.add("open");
|
||||
document.getElementById("api-modal-close").onclick = () =>
|
||||
document.getElementById("api-modal").classList.remove("open");
|
||||
document.getElementById("api-modal").onclick = e => {
|
||||
if (e.target.id === "api-modal") e.target.classList.remove("open");
|
||||
};
|
||||
|
||||
// Hash change
|
||||
window.onhashchange = () => {
|
||||
parseHash();
|
||||
init();
|
||||
};
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.onkeydown = e => {
|
||||
if (e.key === "Escape") {
|
||||
closeDetail();
|
||||
document.getElementById("api-modal").classList.remove("open");
|
||||
if (state.selectionMode) {
|
||||
clearSelection();
|
||||
renderView();
|
||||
}
|
||||
}
|
||||
if (e.key === "/" && document.activeElement.tagName !== "INPUT") {
|
||||
e.preventDefault();
|
||||
document.getElementById("search").focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Start app
|
||||
init();
|
||||
});
|
||||
Reference in New Issue
Block a user