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,14 @@
import { toast } from './toast.ts';
export async function copyToClipboard(text: string, message?: string): Promise<void> {
try {
await navigator.clipboard.writeText(text);
toast(message || 'Copiado');
} catch {
toast('Error al copiar');
}
}
export function copyMrf(mrf: string): void {
copyToClipboard(mrf, `MRF copiado: ${mrf.slice(0, 8)}...`);
}

View File

@@ -0,0 +1,44 @@
export const $ = <T extends HTMLElement>(
selector: string,
parent: ParentNode = document
): T | null => parent.querySelector<T>(selector);
export const $$ = <T extends HTMLElement>(
selector: string,
parent: ParentNode = document
): T[] => Array.from(parent.querySelectorAll<T>(selector));
export function createElement<K extends keyof HTMLElementTagNameMap>(
tag: K,
attrs?: Record<string, string>,
children?: (HTMLElement | string)[]
): HTMLElementTagNameMap[K] {
const el = document.createElement(tag);
if (attrs) {
Object.entries(attrs).forEach(([key, value]) => {
if (key === 'className') el.className = value;
else if (key.startsWith('data-')) el.setAttribute(key, value);
else el.setAttribute(key, value);
});
}
if (children) {
children.forEach(child => {
el.append(typeof child === 'string' ? child : child);
});
}
return el;
}
export function delegateEvent<T extends Event>(
container: HTMLElement,
selector: string,
eventType: string,
handler: (event: T, target: HTMLElement) => void
): void {
container.addEventListener(eventType, (event) => {
const target = (event.target as HTMLElement).closest<HTMLElement>(selector);
if (target && container.contains(target)) {
handler(event as T, target);
}
});
}

View File

@@ -0,0 +1,39 @@
import type { Tag, LangType } from '@/types/index.ts';
import { getName } from './i18n.ts';
export interface FilterOptions {
search: string;
group: string;
library: string;
libraryMembers: Set<string>;
lang: LangType;
}
export function filterTags(tags: Tag[], options: FilterOptions): Tag[] {
const { search, group, library, libraryMembers, lang } = options;
const q = search.toLowerCase();
return tags.filter(tag => {
// Library filter
if (library !== 'all' && !libraryMembers.has(tag.mrf)) {
return false;
}
// Group filter
if (group !== 'all' && tag.set_hst !== group) {
return false;
}
// Search filter
if (q) {
const name = getName(tag, lang).toLowerCase();
const ref = (tag.ref || '').toLowerCase();
const alias = (tag.alias || '').toLowerCase();
if (!name.includes(q) && !ref.includes(q) && !alias.includes(q)) {
return false;
}
}
return true;
});
}

View File

@@ -0,0 +1,42 @@
import type { Tag, LangType } from '@/types/index.ts';
export function getName(tag: Tag, lang: LangType): string {
if (lang === 'es' && tag.name_es) return tag.name_es;
if (lang === 'en' && tag.name_en) return tag.name_en;
if (lang === 'ch' && tag.name_ch) return tag.name_ch;
return tag.name_es || tag.name_en || tag.alias || tag.ref || tag.mrf.slice(0, 8);
}
// Create a map of mrf -> display name for resolving groups
export function createNameMap(tags: Tag[], lang: LangType): Map<string, string> {
const map = new Map<string, string>();
tags.forEach(tag => {
map.set(tag.mrf, getName(tag, lang));
});
return map;
}
// Resolve a group mrf to its display name
export function resolveGroupName(mrf: string | undefined, nameMap: Map<string, string>): string {
if (!mrf) return 'Sin grupo';
return nameMap.get(mrf) || mrf.slice(0, 8);
}
const ATC_BASE = 'https://atc.tzzrdeck.me';
function resolveImgUrl(url: string | undefined): string {
if (!url) return '';
// Relative paths (e.g., "thumbs/xxx.png") need ATC base
if (url && !url.startsWith('http')) {
return `${ATC_BASE}/${url}`;
}
return url;
}
export function getImg(tag: Tag): string {
return resolveImgUrl(tag.img_thumb_url);
}
export function getFullImg(tag: Tag): string {
return resolveImgUrl(tag.img_url) || resolveImgUrl(tag.img_thumb_url);
}

View File

@@ -0,0 +1,5 @@
export { $, $$, createElement, delegateEvent } from './dom.ts';
export { getName, getImg, getFullImg, createNameMap, resolveGroupName } from './i18n.ts';
export { filterTags, type FilterOptions } from './filters.ts';
export { copyToClipboard, copyMrf } from './clipboard.ts';
export { toast } from './toast.ts';

View File

@@ -0,0 +1,21 @@
let toastEl: HTMLElement | null = null;
let toastTimeout: number | null = null;
export function toast(message: string, duration = 2000): void {
if (!toastEl) {
toastEl = document.createElement('div');
toastEl.className = 'toast';
document.body.appendChild(toastEl);
}
if (toastTimeout) {
clearTimeout(toastTimeout);
}
toastEl.textContent = message;
toastEl.classList.add('show');
toastTimeout = window.setTimeout(() => {
toastEl?.classList.remove('show');
}, duration);
}