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:
ARCHITECT
2026-01-16 18:26:59 +00:00
parent 17506aaee2
commit 9b244138b5
177 changed files with 15063 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
import { View } from '../View.ts';
import { getName, getFullImg, copyMrf, delegateEvent } from '@/utils/index.ts';
import { fetchChildren, fetchRelated } from '@/api/index.ts';
import type { Store } from '@/state/store.ts';
import type { AppState, Tag } from '@/types/index.ts';
export class DetailPanel extends View {
private panelEl: HTMLElement;
constructor(
container: HTMLElement,
store: Store<AppState>
) {
super(container, store);
this.panelEl = container;
}
async showDetail(mrf: string): Promise<void> {
const state = this.getState();
const tag = state.tags.find(t => t.mrf === mrf);
if (!tag) return;
this.setState({ selectedTag: tag });
this.panelEl.classList.add('open');
await this.renderDetail(tag);
}
close(): void {
this.panelEl.classList.remove('open');
this.setState({ selectedTag: null });
}
render(): void {
const state = this.getState();
if (state.selectedTag) {
this.renderDetail(state.selectedTag);
}
}
private async renderDetail(tag: Tag): Promise<void> {
const state = this.getState();
const img = getFullImg(tag);
const name = getName(tag, state.lang);
this.panelEl.innerHTML = `
<div class="detail-header">
${img
? `<img class="detail-img" src="${img}" alt="${tag.ref}">`
: `<div class="detail-placeholder">${tag.ref?.slice(0, 2) || 'T'}</div>`
}
<button class="detail-close">&times;</button>
</div>
<div class="detail-body">
<div class="detail-ref">${tag.ref || ''}</div>
<div class="detail-mrf" data-mrf="${tag.mrf}">${tag.mrf}</div>
<div class="detail-name">${name}</div>
<div class="detail-desc">${tag.txt || tag.alias || ''}</div>
<div id="children-section" class="detail-section" style="display:none">
<h4>Hijos</h4>
<div id="children-list" class="chip-list"></div>
</div>
<div id="related-section" class="detail-section" style="display:none">
<h4>Relacionados</h4>
<div id="related-list" class="chip-list"></div>
</div>
</div>
`;
this.bindDetailEvents();
await this.loadRelations(tag.mrf);
}
private bindDetailEvents(): void {
const closeBtn = this.panelEl.querySelector('.detail-close');
closeBtn?.addEventListener('click', () => this.close());
const mrfEl = this.panelEl.querySelector('.detail-mrf');
mrfEl?.addEventListener('click', () => {
const mrf = (mrfEl as HTMLElement).dataset.mrf;
if (mrf) copyMrf(mrf);
});
delegateEvent<MouseEvent>(this.panelEl, '.tag-chip', 'click', (_, target) => {
const mrf = target.dataset.mrf;
if (mrf) this.showDetail(mrf);
});
}
private async loadRelations(mrf: string): Promise<void> {
const [children, related] = await Promise.all([
fetchChildren(mrf),
fetchRelated(mrf)
]);
const childrenSection = this.panelEl.querySelector('#children-section') as HTMLElement;
const childrenList = this.panelEl.querySelector('#children-list') as HTMLElement;
if (children.length > 0) {
childrenSection.style.display = 'block';
childrenList.innerHTML = children.map(c => {
const label = c.name_es || c.alias || c.ref || c.mrf.slice(0, 8);
return `<span class="tag-chip" data-mrf="${c.mrf}">${label}</span>`;
}).join('');
}
const relatedSection = this.panelEl.querySelector('#related-section') as HTMLElement;
const relatedList = this.panelEl.querySelector('#related-list') as HTMLElement;
if (related.length > 0) {
relatedSection.style.display = 'block';
relatedList.innerHTML = related.map(r => {
const label = r.name_es || r.alias || r.ref || r.mrf.slice(0, 8);
return `<span class="tag-chip" data-mrf="${r.mrf}" title="${r.edge_type}">${label}</span>`;
}).join('');
}
}
}